mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Merge commit 'cb8a2e617dd5edbe1dc3dafbfc82f1a48bf3d6f6' into integrate/pr558plus-fixes-20261008
# Conflicts: # docs/TCP_COMMANDS.md # generated/OpenBIOS.emitter.sha # runtime/src/main.cpp
This commit is contained in:
@@ -55,6 +55,7 @@ Columns: **N** = native, **D** = DuckStation oracle.
|
||||
| `screenshot_wide_hires` | ✓ | | `path`, `base_x` | The displayed band of the native-wide surface at internal resolution (`wide_w×S` by `height×S`). `present_shot` is capped at the window; this checks a widescreen + internal-resolution combination at full size |
|
||||
| `present_shot` | ✓ | | `path` | PNG of the **composed present surface** — the frame after the backend fits the display buffer to the window, so it carries the presented aspect. ⚠ every other capture resolves the display buffer *before* that fit: on a 508×256 display in a 4:3 window they answer 508×256 while the player sees 640×480. Use this one for anything aspect-shaped (widescreen, letterbox), where a pre-fit buffer would hide the very stage the change touches. Staged and fulfilled on the next present, so the ack means *queued* — poll `present_shot_seq`. Unavailable headless and on the Vulkan backend (its swapchain has no readback hook) |
|
||||
| `present_shot_seq` | ✓ | | — | Completion counter for `present_shot`, plus `wrote` (1 = that completion produced a PNG). Sample before staging, poll until `seq` moves. Advances on success *and* failure, so the poll always terminates |
|
||||
| `post_aa` | ✓ | | `mode` (optional: 0 off, 1 fxaa, 2 fxaa_hq) | Read or set post-process anti-aliasing live (`[video] antialiasing_mode`). Replies `mode`, `passes` (filtered presents so far) and `gpu_us`: the mean pass time since the last query when the run has `PSX_POST_AA_TIME=N` (N passes per present between `glFinish` fences, a measurement mode), else 0 |
|
||||
| `hd_textures` | ✓ | | optional `replacements`, `dump` (0 or 1), `reload` (1) | HD pack root, switches, backend support, replacement count, matched/ready/applied draw counts, and queued dump count. Optional controls update the configured host pack on the emulation thread; no configured root is an error. Reload retains unchanged upload identities. OpenGL displays replacements; software/Vulkan retain original artwork. Pair with `present_shot` for visual evidence and native VRAM probes for architectural data |
|
||||
| `gl_interp` | ✓ | | — | OpenGL frame-rate presenter ([FRAME_RATE.md](FRAME_RATE.md)): enabled/suspended, host and target Hz, swaps, `source` (`vblank`/`flip`), `flip_period`, `captures` (new source frames) and `duplicates` (VBlanks that re-presented the same frame) |
|
||||
| `render_pass_stats` | ✓ | | — | Render passes ([RENDER_PASSES.md](RENDER_PASSES.md)): plans, phases wanted/planned (shedding), passes, rollbacks (`nesting_repairs`: watchdog aborts whose skipped frame exits the restore undid), dropped device stores by class, `verify_mismatch` under `PSX_RENDER_PASS_VERIFY=1`, host-time split per pass, smoothed pass cost (`cost_us`; `cost_rewarms`: estimates no pass had run on for a while, measured again), presents made from pass images (`late_presents`: held past the frame's planned end because the next flip was late; `expired`: frames whose images stopped showing after several frame lengths without a flip), pass image textures allocated (`image_textures`, `image_bytes`), `status` (`psx_mod_render_pass_status`: 0 ready, 1 no presenter, 2 backend, 3 disabled, 4 session, 5 fast-forward, 6 busy), `backups_reused` (passes that reused the previous pass's VRAM backup), `spans` / `span_failures` and the last failure `span_fail` (`psx_mod_run_guest_span`: reason, exit PC, `$ra`, PC after a call that did not return) |
|
||||
@@ -479,9 +480,9 @@ between Play and TCP availability.
|
||||
|
||||
## Complete command index (generated)
|
||||
|
||||
**361 commands registered** — 348 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
**362 commands registered** — 349 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
|
||||
73 of 361 have prose above; **288 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
74 of 362 have prose above; **288 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
|
||||
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
|
||||
|
||||
@@ -712,6 +713,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `phase_hot` | ✓ | | |
|
||||
| `phase_profile` | ✓ | | ✓ |
|
||||
| `ping` | ✓ | ✓ | ✓ |
|
||||
| `post_aa` | ✓ | | ✓ |
|
||||
| `present_image_ring_get` | ✓ | | |
|
||||
| `present_image_ring_stats` | ✓ | | |
|
||||
| `present_ring` | ✓ | | |
|
||||
|
||||
@@ -604,6 +604,58 @@ the pixels to show it. Native keeps the window exactly as before.
|
||||
downsamples to the window. It accepts 1 to 32 in both `game.toml` and the
|
||||
player's `settings.toml`; 1 (the default) is native and unchanged.
|
||||
|
||||
### Anti-aliasing (`antialiasing_mode`, `supersample`)
|
||||
|
||||
Both are opt-in and change nothing unless set.
|
||||
|
||||
```toml
|
||||
[video]
|
||||
supersample = 1.5 # 1.0..4.0, default 1.0
|
||||
antialiasing_mode = "fxaa" # off (default) | fxaa | fxaa_hq
|
||||
```
|
||||
|
||||
`supersample` multiplies the target of a line preset or Match display before
|
||||
it becomes a scale: Match display on a 1080 px monitor with 2.0 asks for 2160
|
||||
lines (S = 9) and the present's area resolve brings it back to 1080, which is
|
||||
ordered-grid SSAA of everything, 2D included. Native and the legacy
|
||||
`supersampling` factor are left alone. Dynamic resolution takes the result as
|
||||
its ceiling and steps under it as before. `PSX_SUPERSAMPLE=<factor>` overrides
|
||||
it for one run.
|
||||
|
||||
To make supersampling the elastic part only, set the dynamic resolution floor
|
||||
to the output: `dynamic_resolution_min = "display"` (also `PSX_DYNRES_MIN=display`
|
||||
and settings.toml) means the selected internal resolution *without* the
|
||||
supersample factor, i.e. the monitor's own lines under Match display or the
|
||||
preset's lines otherwise. Dynamic resolution then steps between the
|
||||
supersampled ceiling and the output and never renders below it:
|
||||
|
||||
```toml
|
||||
[video]
|
||||
internal_resolution = "display"
|
||||
supersample = 2.0
|
||||
dynamic_resolution = true
|
||||
dynamic_resolution_min = "display"
|
||||
```
|
||||
|
||||
`antialiasing_mode` filters the composed game image on OpenGL: after the game
|
||||
quad (4:3, native-wide, Smooth motion and generated frames alike) and before
|
||||
hold-last capture, the OSD, screenshots and the swap. It reads only output
|
||||
pixels, so guest VRAM, readbacks, render passes and the Smooth motion sources
|
||||
are untouched; the bezel and OSD stay sharp; 24-bit FMV frames are not
|
||||
filtered. The PS1 HUD is part of the same image and is filtered too. `fxaa` is one copy and one full-screen pass (on an M4, at most 0.4 /
|
||||
0.7 / 1.5 ms at 1080p / 1440p / 4K, fence-timed with the copy); `fxaa_hq` lowers the contrast threshold and searches further along
|
||||
long edges. `PSX_AA_MODE=off|fxaa|fxaa_hq` overrides it for one run and the
|
||||
`post_aa` TCP command switches it live. The older boolean `antialiasing` key is
|
||||
unrelated: it is the linear present filter the launcher already persists.
|
||||
|
||||
Not offered: MSAA. The hi-res surface *is* guest VRAM (texture pages, CLUTs,
|
||||
VRAM copies, readbacks and the mask stencil all sample or copy it between
|
||||
draws), so a multisampled target would need a resolve before every one of
|
||||
those and the mask/semi-transparency rules would have to run per sample; that
|
||||
is a renderer redesign, not an option. SMAA needs its area/search lookup
|
||||
textures and three passes; on an image that is already area-resolved from a
|
||||
higher internal resolution it buys little over FXAA, so it is left for later.
|
||||
|
||||
The runtime clamps N per backend:
|
||||
|
||||
- Software and Vulkan stop at 4.
|
||||
|
||||
@@ -1 +1 @@
|
||||
7d8356d54277a80240c604717bd3a801c321a19bd789bbb74adbd62e635379d1
|
||||
b352f488a134fd511614abed04c6076152b9a80ab24c796a6b671126adfd4bdd
|
||||
|
||||
@@ -715,8 +715,7 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
int value = 0;
|
||||
bool ok = false;
|
||||
if (ir.is_string()) {
|
||||
ok = psx_ir_parse(ir.as_string().str.c_str(), &value) != 0 &&
|
||||
value != PSX_IR_DISPLAY;
|
||||
ok = psx_ir_parse(ir.as_string().str.c_str(), &value) != 0;
|
||||
} else if (ir.is_integer()) {
|
||||
const auto n = ir.as_integer();
|
||||
ok = n >= PSX_IR_MIN_LINES && n <= PSX_IR_MAX_LINES;
|
||||
@@ -725,7 +724,7 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
if (!ok) {
|
||||
throw std::runtime_error(
|
||||
"[video] dynamic_resolution_min must be native, 720p, 1080p, "
|
||||
"1440p, 4k, 5k, 8k, or a number of lines");
|
||||
"1440p, 4k, 5k, 8k, display, or a number of lines");
|
||||
}
|
||||
rt.video_dynamic_resolution_min = value;
|
||||
}
|
||||
@@ -748,6 +747,24 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
if (video.contains("antialiasing")) {
|
||||
rt.video_antialiasing = toml::find<bool>(video, "antialiasing");
|
||||
}
|
||||
if (video.contains("antialiasing_mode")) {
|
||||
const auto mode = toml::find<std::string>(video, "antialiasing_mode");
|
||||
if (mode == "off") rt.video_antialiasing_mode = 0;
|
||||
else if (mode == "fxaa") rt.video_antialiasing_mode = 1;
|
||||
else if (mode == "fxaa_hq") rt.video_antialiasing_mode = 2;
|
||||
else throw std::runtime_error(fmt::format(
|
||||
"[video] antialiasing_mode must be \"off\", \"fxaa\" or \"fxaa_hq\": {}", mode));
|
||||
}
|
||||
if (video.contains("supersample")) {
|
||||
const auto& v = toml::find(video, "supersample");
|
||||
const double f = v.is_integer() ? static_cast<double>(v.as_integer())
|
||||
: toml::get<double>(v);
|
||||
if (!(f >= 1.0 && f <= 4.0)) {
|
||||
throw std::runtime_error(fmt::format(
|
||||
"[video] supersample out of range (1.0..4.0): {}", f));
|
||||
}
|
||||
rt.video_supersample_milli = static_cast<int>(f * 1000.0 + 0.5);
|
||||
}
|
||||
if (video.contains("texture_filtering")) {
|
||||
const auto mode = toml::find<std::string>(video, "texture_filtering");
|
||||
if (mode == "nearest") rt.video_texture_filter = 0;
|
||||
@@ -2807,8 +2824,7 @@ UserSettings load_user_settings(const fs::path& path) {
|
||||
const toml::value& ir = toml::find(v, "dynamic_resolution_min");
|
||||
int value = 0;
|
||||
if (ir.is_string()) {
|
||||
if (psx_ir_parse(ir.as_string().str.c_str(), &value) &&
|
||||
value != PSX_IR_DISPLAY) {
|
||||
if (psx_ir_parse(ir.as_string().str.c_str(), &value)) {
|
||||
s.dynamic_resolution_min = value; s.has_dynamic_resolution_min = true;
|
||||
}
|
||||
} else if (ir.is_integer()) {
|
||||
@@ -3218,8 +3234,7 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
|
||||
if (s.has_dynamic_resolution)
|
||||
f << "dynamic_resolution = " << (s.dynamic_resolution ? "true" : "false") << "\n";
|
||||
if (s.has_dynamic_resolution_min &&
|
||||
psx_ir_value_valid(s.dynamic_resolution_min) &&
|
||||
s.dynamic_resolution_min != PSX_IR_DISPLAY) {
|
||||
psx_ir_value_valid(s.dynamic_resolution_min)) {
|
||||
const char* id = psx_ir_id_for(s.dynamic_resolution_min);
|
||||
if (id)
|
||||
f << "dynamic_resolution_min = \"" << id << "\"\n";
|
||||
|
||||
@@ -450,6 +450,15 @@ struct RuntimeConfig {
|
||||
// Defaults to true.
|
||||
bool video_antialiasing = true;
|
||||
|
||||
// antialiasing_mode: post-process anti-aliasing of the composed game image
|
||||
// (OpenGL; GL_POST_AA_* in gpu_gl_renderer.h): "off" (default) | "fxaa" |
|
||||
// "fxaa_hq". Separate from the legacy antialiasing bool above, which the
|
||||
// launcher and settings.toml already persist as the linear present filter.
|
||||
int video_antialiasing_mode = 0;
|
||||
// supersample: factor on the internal-resolution target (Match display or
|
||||
// a line preset), 1.0..4.0, kept in thousandths (internal_resolution.h).
|
||||
int video_supersample_milli = 1000;
|
||||
|
||||
// texture_filtering: "nearest" (default, native PSX look) | "bilinear"
|
||||
// (smooths textures and 2D backgrounds). Stored as 0/1.
|
||||
int video_texture_filter = 0;
|
||||
|
||||
@@ -264,6 +264,16 @@ if(BUILD_TESTING)
|
||||
--sdl-library "$<TARGET_FILE:SDL3::SDL3-static>"
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-filter-results")
|
||||
set_tests_properties(gl_texture_filter_test PROPERTIES LABELS "gpu;opengl;hardware" TIMEOUT 180)
|
||||
add_test(NAME gl_post_aa_test
|
||||
COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_texture_filter.py"
|
||||
--fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_post_aa.c"
|
||||
--cc "${CMAKE_C_COMPILER}"
|
||||
--sdl-include "$<TARGET_PROPERTY:SDL3::Headers,INTERFACE_INCLUDE_DIRECTORIES>"
|
||||
--sdl-library "$<TARGET_FILE:SDL3::SDL3-static>"
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-post-aa-results")
|
||||
set_tests_properties(gl_post_aa_test PROPERTIES
|
||||
LABELS "gpu;opengl;hardware" TIMEOUT 180 SKIP_RETURN_CODE 77)
|
||||
add_test(NAME gl_hd_texture_pack_test
|
||||
COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_texture_filter.py"
|
||||
|
||||
@@ -176,6 +176,15 @@ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linea
|
||||
* Returns 0 only if a texture could not be created. */
|
||||
int gl_renderer_set_bezel(const void *rgba, int w, int h);
|
||||
int gl_renderer_has_bezel(void);
|
||||
/* Post-process anti-aliasing of the composed game image ([video]
|
||||
* antialiasing_mode, PSX_AA_MODE). Off by default; OSD and bezel are not
|
||||
* filtered and guest VRAM is never touched. Takes effect at the next present;
|
||||
* safe to call from any thread. */
|
||||
enum { GL_POST_AA_OFF = 0, GL_POST_AA_FXAA = 1, GL_POST_AA_FXAA_HQ = 2 };
|
||||
int gl_renderer_set_post_aa(int mode);
|
||||
int gl_renderer_post_aa(void);
|
||||
uint64_t gl_renderer_post_aa_passes(void);
|
||||
double gl_renderer_post_aa_gpu_us(void); /* debug builds, PSX_POST_AA_TIME=N: mean pass time (us), resets */
|
||||
|
||||
/* Clear to black + swap (display-disabled frame). */
|
||||
void gl_renderer_present_blank(void);
|
||||
|
||||
@@ -112,6 +112,54 @@ static inline int psx_resolve_internal_scale(int value, int ref_lines,
|
||||
return s < 1 ? 1 : s;
|
||||
}
|
||||
|
||||
/* ---- Supersample ([video] supersample, PSX_SUPERSAMPLE) ----------------------
|
||||
* A factor on the TARGET of a line-count preset or Match display, in
|
||||
* thousandths (1000 = 1.0, the historical behaviour; range 1000..4000). The
|
||||
* renderer already area-resolves an internal image taller than the output, so
|
||||
* asking for 2x the display's lines is ordered-grid SSAA of the whole frame:
|
||||
* Match display at 1080 px with 2.0 renders 2160 lines (S = 9) and resolves
|
||||
* to 1080. Native and the legacy integer factor are left alone. Dynamic
|
||||
* resolution takes the result as its ceiling and steps under it as usual. */
|
||||
#define PSX_SS_MILLI_MIN 1000
|
||||
#define PSX_SS_MILLI_MAX 4000
|
||||
|
||||
static inline int psx_ss_milli_clamp(int m) {
|
||||
return m < PSX_SS_MILLI_MIN ? PSX_SS_MILLI_MIN
|
||||
: m > PSX_SS_MILLI_MAX ? PSX_SS_MILLI_MAX : m;
|
||||
}
|
||||
|
||||
/* "1.5", "2", "2x" -> 1500, 2000, 2000. 0 for anything else. */
|
||||
static inline int psx_ss_parse_milli(const char *s) {
|
||||
if (!s || !*s) return 0;
|
||||
long whole = 0, frac = 0, div = 1;
|
||||
const char *p = s;
|
||||
while (*p >= '0' && *p <= '9') { whole = whole * 10 + (*p - '0'); p++; if (whole > 100) return 0; }
|
||||
if (p == s) return 0;
|
||||
if (*p == '.') { p++; while (*p >= '0' && *p <= '9') { if (div < 1000) { frac = frac * 10 + (*p - '0'); div *= 10; } p++; } }
|
||||
if (*p == 'x' || *p == 'X') p++;
|
||||
if (*p) return 0;
|
||||
long m = whole * 1000 + frac * (1000 / div);
|
||||
if (m < PSX_SS_MILLI_MIN || m > PSX_SS_MILLI_MAX) return 0;
|
||||
return (int)m;
|
||||
}
|
||||
|
||||
/* psx_resolve_internal_scale with the supersample factor on the target. */
|
||||
static inline int psx_resolve_internal_scale_ss(int value, int ref_lines,
|
||||
int display_px_h, int s_max,
|
||||
int ss_milli) {
|
||||
ss_milli = psx_ss_milli_clamp(ss_milli);
|
||||
if (ss_milli != PSX_SS_MILLI_MIN) {
|
||||
if (value == PSX_IR_DISPLAY) {
|
||||
if (display_px_h > 0)
|
||||
display_px_h = (int)(((long long)display_px_h * ss_milli + 999) / 1000);
|
||||
} else if (value >= PSX_IR_MIN_LINES) {
|
||||
long long t = ((long long)value * ss_milli + 999) / 1000;
|
||||
value = t > 4LL * PSX_IR_MAX_LINES ? 4 * PSX_IR_MAX_LINES : (int)t;
|
||||
}
|
||||
}
|
||||
return psx_resolve_internal_scale(value, ref_lines, display_px_h, s_max);
|
||||
}
|
||||
|
||||
/* A legacy [video] supersampling factor as a value the launcher can show:
|
||||
* the preset that resolves to the same scale (1 -> Native, 3 -> 720p,
|
||||
* 5 -> 1080p, 6 -> 1440p, 9 -> 4K, 12 -> 5K, 18 -> 8K at 240 lines),
|
||||
@@ -193,3 +241,17 @@ static inline PsxIrAdopted psx_ir_adopt_launcher(int has_row, int preset,
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
/* ---- Dynamic resolution floor ([video] dynamic_resolution_min) -------------
|
||||
* A preset or line count resolves as usual. "display" (PSX_IR_DISPLAY) is the
|
||||
* output's own lines: the selected internal resolution WITHOUT the supersample
|
||||
* factor (Match display = the monitor's pixel height). With supersample > 1
|
||||
* the controller then trades only supersampling and never drops below the
|
||||
* output. Clamped to [1, ceiling]. */
|
||||
static inline int psx_dynres_floor_scale(int min_value, int selected_ir, int ref_lines,
|
||||
int display_px_h, int ceiling) {
|
||||
const int v = (min_value == PSX_IR_DISPLAY && selected_ir != PSX_IR_UNSET)
|
||||
? selected_ir : min_value;
|
||||
int s = psx_resolve_internal_scale(v, ref_lines, display_px_h, ceiling);
|
||||
return s > ceiling ? ceiling : s;
|
||||
}
|
||||
|
||||
@@ -8557,6 +8557,17 @@ static void handle_synth_recurse(int id, const char *json)
|
||||
#endif
|
||||
}
|
||||
|
||||
/* post_aa: post-process anti-aliasing, live. {"cmd":"post_aa"} reads it,
|
||||
* {"cmd":"post_aa","mode":0|1|2} sets it (off, fxaa, fxaa_hq). */
|
||||
static void handle_post_aa(int id, const char *json)
|
||||
{
|
||||
int mode = json_get_int(json, "mode", -1);
|
||||
if (mode >= 0) (void)gl_renderer_set_post_aa(mode);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"mode\":%d,\"passes\":%llu,\"gpu_us\":%.1f}", id,
|
||||
gl_renderer_post_aa(), (unsigned long long)gl_renderer_post_aa_passes(),
|
||||
gl_renderer_post_aa_gpu_us());
|
||||
}
|
||||
|
||||
static void handle_frame_perf(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
@@ -15648,6 +15659,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "gpu_timeline", handle_gpu_timeline },
|
||||
{ "nclip_stats", handle_nclip_stats },
|
||||
{ "frame_perf", handle_frame_perf },
|
||||
{ "post_aa", handle_post_aa },
|
||||
{ "gl_ws_ablate", handle_gl_ws_ablate },
|
||||
{ "gl_interp", handle_gl_interp },
|
||||
{ "render_pass_stats", handle_render_pass_stats },
|
||||
|
||||
@@ -601,6 +601,7 @@ static int s_osd_tw = 0, s_osd_th = 0;
|
||||
static GLuint s_present_prog = 0, s_present_vao = 0;
|
||||
static GLuint s_xr_color_prog = 0, s_xr_native_tex = 0;
|
||||
static void gl_swap_with_osd(void);
|
||||
static int s_post_aa = 0; /* GL_POST_AA_*; post-process AA (post_aa_apply) */
|
||||
static int s_native_surface_enabled, s_native_surface_pending;
|
||||
static int s_native_surface_rect[4]; /* Fresh native backbuffer content, GL coordinates. */
|
||||
static double s_native_surface_distance, s_native_surface_width, s_native_surface_units;
|
||||
@@ -1338,8 +1339,12 @@ int gl_renderer_coh_get(uint64_t seq, GlCohEvent *out) {
|
||||
static GlPresEvent s_pres_ring[GL_PRES_RING_CAP];
|
||||
static uint64_t s_pres_seq = 0;
|
||||
|
||||
static void post_aa_apply(int lx, int ly, int lw, int lh);
|
||||
static void pres_record(int path, int dx, int dy, int w, int h,
|
||||
int lx, int ly, int lw, int lh) {
|
||||
/* The composed game image, before hold-last capture and the OSD. */
|
||||
if (s_post_aa && (path == GL_PRES_VRAM || path == GL_PRES_WIDE || path == GL_PRES_INTERP))
|
||||
post_aa_apply(lx, ly, lw, lh);
|
||||
s_native_surface_pending=s_native_surface_enabled &&
|
||||
(path==GL_PRES_VRAM || path==GL_PRES_WIDE || path==GL_PRES_CPU || path==GL_PRES_BLANK);
|
||||
s_native_surface_rect[0]=lx;s_native_surface_rect[1]=ly;
|
||||
@@ -1407,6 +1412,185 @@ static const char *PRESENT_VS =
|
||||
" v_uv = vec2(mix(u_uv_rect.x,u_uv_rect.z,p.x),\n"
|
||||
" mix(u_uv_rect.y,u_uv_rect.w,1.0-p.y));\n"
|
||||
" gl_Position = vec4(p*2.0-1.0,0.0,1.0); }\n";
|
||||
/* ---- Post-process anti-aliasing ([video] antialiasing_mode) ----------------
|
||||
* Opt-in, off by default: with the mode off nothing below runs and the present
|
||||
* is byte-identical to a build without it. When on, the composed game image
|
||||
* (the letterbox rect pres_record is told about, after the game quad and
|
||||
* before hold-last capture, OSD, screenshots and the swap) is copied out of
|
||||
* the drawable and redrawn through an edge filter. It works on the output
|
||||
* pixels only: guest VRAM, the hr/wide surfaces, readbacks, render passes and
|
||||
* the Smooth motion / frame generation sources are never touched, so it
|
||||
* composes with every present path (4:3, native-wide, interpolated and
|
||||
* generated frames) and with internal resolution / dynamic resolution, whose
|
||||
* area resolve has already happened by then. OSD and bezel stay sharp.
|
||||
*
|
||||
* FXAA here is an independent implementation of the published technique
|
||||
* (luma contrast test, edge orientation, bounded end-of-edge search,
|
||||
* sub-pixel blend); it is one copy plus one full-screen pass. */
|
||||
static GLuint build_program(const char *vs, const char *fs);
|
||||
static GLuint s_paa_prog = 0, s_paa_tex = 0;
|
||||
static int s_paa_tw = 0, s_paa_th = 0, s_paa_failed = 0;
|
||||
static GLint s_paa_uTex = -1, s_paa_uRcp = -1, s_paa_uUvRect = -1, s_paa_uQuality = -1;
|
||||
static uint64_t s_paa_passes = 0;
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
/* Cost of the pass (debug server post_aa), only with PSX_POST_AA_TIME=N: the
|
||||
* pass runs N times (1..64; N > 1 re-filters the frame and amortizes the
|
||||
* fence) bracketed by glFinish and timed on the host clock. That serializes
|
||||
* the GPU, so it is a measurement mode, never on by default. (Timestamp
|
||||
* queries read 0 on Apple's GL.) */
|
||||
static int s_paa_time = -1;
|
||||
static double s_paa_gpu_us_sum = 0.0;
|
||||
static uint64_t s_paa_gpu_n = 0, s_paa_t0 = 0;
|
||||
#endif
|
||||
|
||||
static const char *POST_AA_FS =
|
||||
"#version 330\n"
|
||||
"in vec2 v_uv; out vec4 o;\n"
|
||||
"uniform sampler2D u_tex; uniform vec2 u_rcp; uniform int u_quality;\n"
|
||||
"float L(vec3 c){ return dot(c, vec3(0.299,0.587,0.114)); }\n"
|
||||
"float LS(vec2 p){ return L(textureLod(u_tex,p,0.0).rgb); }\n"
|
||||
"void main(){\n"
|
||||
" vec3 cM = textureLod(u_tex, v_uv, 0.0).rgb; float lM = L(cM);\n"
|
||||
" float lN = LS(v_uv+vec2(0.0, u_rcp.y)), lS = LS(v_uv-vec2(0.0, u_rcp.y));\n"
|
||||
" float lE = LS(v_uv+vec2(u_rcp.x,0.0)), lW = LS(v_uv-vec2(u_rcp.x,0.0));\n"
|
||||
" float mx = max(lM,max(max(lN,lS),max(lE,lW)));\n"
|
||||
" float mn = min(lM,min(min(lN,lS),min(lE,lW)));\n"
|
||||
" float range = mx - mn;\n"
|
||||
/* thresholds: absolute 1/16 (1/24 at high quality), relative 1/8 */
|
||||
" float thr = u_quality > 0 ? 0.0417 : 0.0625;\n"
|
||||
" if (range < max(thr, mx*0.125)) { o = vec4(cM,1.0); return; }\n"
|
||||
" float lNE = LS(v_uv+u_rcp), lSW = LS(v_uv-u_rcp);\n"
|
||||
" float lNW = LS(v_uv+vec2(-u_rcp.x,u_rcp.y)), lSE = LS(v_uv+vec2(u_rcp.x,-u_rcp.y));\n"
|
||||
" float eH = abs(lNW+lNE-2.0*lN) + 2.0*abs(lW+lE-2.0*lM) + abs(lSW+lSE-2.0*lS);\n"
|
||||
" float eV = abs(lNW+lSW-2.0*lW) + 2.0*abs(lN+lS-2.0*lM) + abs(lNE+lSE-2.0*lE);\n"
|
||||
" bool horz = eH >= eV;\n"
|
||||
" float l1 = horz ? lS : lW, l2 = horz ? lN : lE;\n"
|
||||
" float g1 = abs(l1-lM), g2 = abs(l2-lM);\n"
|
||||
" bool neg = g1 >= g2;\n"
|
||||
" float grad = 0.25*max(g1,g2);\n"
|
||||
" float step = horz ? u_rcp.y : u_rcp.x;\n"
|
||||
" float lLocal; if (neg) { step = -step; lLocal = 0.5*(l1+lM); } else lLocal = 0.5*(l2+lM);\n"
|
||||
" vec2 p = v_uv; if (horz) p.y += 0.5*step; else p.x += 0.5*step;\n"
|
||||
" vec2 off = horz ? vec2(u_rcp.x,0.0) : vec2(0.0,u_rcp.y);\n"
|
||||
" vec2 p1 = p - off, p2 = p + off;\n"
|
||||
" float e1 = LS(p1)-lLocal, e2 = LS(p2)-lLocal;\n"
|
||||
" bool d1 = abs(e1) >= grad, d2 = abs(e2) >= grad;\n"
|
||||
" int steps = u_quality > 0 ? 12 : 8;\n"
|
||||
" for (int i = 1; i < steps && !(d1 && d2); i++) {\n"
|
||||
" float k = i < 2 ? 1.0 : (i < 5 ? 1.5 : (i < 8 ? 2.0 : 4.0));\n"
|
||||
" if (!d1) { p1 -= off*k; e1 = LS(p1)-lLocal; d1 = abs(e1) >= grad; }\n"
|
||||
" if (!d2) { p2 += off*k; e2 = LS(p2)-lLocal; d2 = abs(e2) >= grad; }\n"
|
||||
" }\n"
|
||||
" float dist1 = horz ? v_uv.x-p1.x : v_uv.y-p1.y;\n"
|
||||
" float dist2 = horz ? p2.x-v_uv.x : p2.y-v_uv.y;\n"
|
||||
" bool near1 = dist1 < dist2; float dmin = min(dist1,dist2);\n"
|
||||
" float len = dist1 + dist2;\n"
|
||||
" bool mLess = (lM - lLocal) < 0.0;\n"
|
||||
" bool good = ((near1 ? e1 : e2) < 0.0) != mLess;\n"
|
||||
" float eo = good ? (0.5 - dmin/len) : 0.0;\n"
|
||||
" float avg = (2.0*(lN+lS+lE+lW) + lNE+lNW+lSE+lSW) / 12.0;\n"
|
||||
" float sp = clamp(abs(avg-lM)/range, 0.0, 1.0);\n"
|
||||
" sp = (-2.0*sp + 3.0)*sp*sp; sp = sp*sp*0.75;\n"
|
||||
" float f = max(eo, sp);\n"
|
||||
" vec2 q = v_uv; if (horz) q.y += f*step; else q.x += f*step;\n"
|
||||
" o = vec4(textureLod(u_tex, q, 0.0).rgb, 1.0);\n"
|
||||
"}\n";
|
||||
|
||||
int gl_renderer_set_post_aa(int mode) {
|
||||
if (mode < GL_POST_AA_OFF || mode > GL_POST_AA_FXAA_HQ) mode = GL_POST_AA_OFF;
|
||||
s_post_aa = mode; /* read by the GL thread at the next present */
|
||||
return 1;
|
||||
}
|
||||
int gl_renderer_post_aa(void) { return s_post_aa; }
|
||||
uint64_t gl_renderer_post_aa_passes(void) { return s_paa_passes; }
|
||||
|
||||
static void post_aa_release(void) {
|
||||
s_paa_prog = 0; s_paa_tex = 0; s_paa_tw = s_paa_th = 0; s_paa_failed = 0;
|
||||
}
|
||||
|
||||
/* The composed image in [lx,ly,lw,lh] of the bound window target, in place. */
|
||||
static void post_aa_apply(int lx, int ly, int lw, int lh) {
|
||||
if (!s_post_aa || !s_ctx || s_paa_failed) return;
|
||||
int ww = 0, wh = 0;
|
||||
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||||
if (lx < 0) { lw += lx; lx = 0; }
|
||||
if (ly < 0) { lh += ly; ly = 0; }
|
||||
if (lx + lw > ww) lw = ww - lx;
|
||||
if (ly + lh > wh) lh = wh - ly;
|
||||
if (lw < 3 || lh < 3) return;
|
||||
if (!s_paa_prog) {
|
||||
s_paa_prog = build_program(PRESENT_VS, POST_AA_FS);
|
||||
if (!s_paa_prog) { s_paa_failed = 1; return; }
|
||||
s_paa_uTex = p_glGetUniformLocation(s_paa_prog, "u_tex");
|
||||
s_paa_uRcp = p_glGetUniformLocation(s_paa_prog, "u_rcp");
|
||||
s_paa_uUvRect = p_glGetUniformLocation(s_paa_prog, "u_uv_rect");
|
||||
s_paa_uQuality = p_glGetUniformLocation(s_paa_prog, "u_quality");
|
||||
}
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
if (!s_paa_tex || s_paa_tw != lw || s_paa_th != lh) {
|
||||
if (!s_paa_tex) glGenTextures(1, &s_paa_tex);
|
||||
glBindTexture(GL_TEXTURE_2D, s_paa_tex);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, lw, lh, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
s_paa_tw = lw; s_paa_th = lh;
|
||||
} else {
|
||||
glBindTexture(GL_TEXTURE_2D, s_paa_tex);
|
||||
}
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
if (s_paa_time < 0) {
|
||||
const char *e = getenv("PSX_POST_AA_TIME");
|
||||
s_paa_time = e ? atoi(e) : 0;
|
||||
if (s_paa_time < 0) s_paa_time = 0;
|
||||
if (s_paa_time > 64) s_paa_time = 64;
|
||||
}
|
||||
if (s_paa_time) { glFinish(); s_paa_t0 = SDL_GetPerformanceCounter(); }
|
||||
const int reps = s_paa_time > 1 ? s_paa_time : 1;
|
||||
#else
|
||||
const int reps = 1;
|
||||
#endif
|
||||
for (int r = 0; r < reps; r++) {
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, lx, ly, lw, lh);
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||||
glViewport(lx, ly, lw, lh);
|
||||
p_glUseProgram(s_paa_prog);
|
||||
p_glUniform1i(s_paa_uTex, 0);
|
||||
p_glUniform2f(s_paa_uRcp, 1.0f / (float)lw, 1.0f / (float)lh);
|
||||
p_glUniform1i(s_paa_uQuality, s_post_aa == GL_POST_AA_FXAA_HQ ? 1 : 0);
|
||||
/* PRESENT_VS flips v; (0,1,1,0) cancels it for a bottom-up copy. */
|
||||
p_glUniform4f(s_paa_uUvRect, 0.0f, 1.0f, 1.0f, 0.0f);
|
||||
p_glBindVertexArray(s_present_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
p_glBindVertexArray(0);
|
||||
p_glUseProgram(0);
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
if (s_paa_time) {
|
||||
glFinish();
|
||||
s_paa_gpu_us_sum += (double)(SDL_GetPerformanceCounter() - s_paa_t0) * 1e6 /
|
||||
(double)SDL_GetPerformanceFrequency() / (double)reps;
|
||||
s_paa_gpu_n++;
|
||||
}
|
||||
#endif
|
||||
s_paa_passes++;
|
||||
}
|
||||
|
||||
/* Mean microseconds per pass since the last call (debug builds with
|
||||
* PSX_POST_AA_TIME=N; 0 otherwise). Resets the mean. */
|
||||
double gl_renderer_post_aa_gpu_us(void) {
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
double m = s_paa_gpu_n ? s_paa_gpu_us_sum / (double)s_paa_gpu_n : 0.0;
|
||||
s_paa_gpu_us_sum = 0.0; s_paa_gpu_n = 0;
|
||||
return m;
|
||||
#else
|
||||
return 0.0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Present sampling. u_sharp==0 is the historical behaviour: sample straight at
|
||||
* v_uv, so the texture's own filter (NEAREST or LINEAR) decides everything.
|
||||
*
|
||||
@@ -5961,6 +6145,7 @@ void gl_renderer_shutdown(void) {
|
||||
s_hold_tw = 0;
|
||||
s_hold_th = 0;
|
||||
s_interp_fbo = 0; /* died with the context */
|
||||
post_aa_release();
|
||||
}
|
||||
|
||||
/* CPU-readout present (24-bit FMV frames and the PSX_GL_FORCE_CPU_PRESENT
|
||||
|
||||
+39
-9
@@ -1220,6 +1220,8 @@ extern "C" EMSCRIPTEN_KEEPALIVE void psx_web_set_smooth_60fps(int enabled) {
|
||||
/* [video] options, resolved from the game config (defaults: native + AA). */
|
||||
static int g_video_scale = 1; /* internal-resolution SSAA factor */
|
||||
static bool g_video_aa = true; /* linear present filtering */
|
||||
static int g_video_post_aa = 0; /* [video] antialiasing_mode / PSX_AA_MODE (GL_POST_AA_*) */
|
||||
static int g_video_ss_milli = 1000; /* [video] supersample / PSX_SUPERSAMPLE */
|
||||
/* FMV present reconstruction (VIDEO_FMV_FILTER_*), pushed to the GL renderer
|
||||
* once the config is resolved. Only consulted while g_video_aa is on. */
|
||||
static int g_video_fmv_filter = PSXRecompV4::VIDEO_FMV_FILTER_DEFAULT;
|
||||
@@ -1370,8 +1372,9 @@ static int effective_internal_resolution(void) {
|
||||
static void apply_internal_resolution(int display_px_h) {
|
||||
const int preset = effective_internal_resolution();
|
||||
if (preset == PSX_IR_UNSET) return;
|
||||
g_video_scale = psx_resolve_internal_scale(preset, g_video_ref_lines,
|
||||
display_px_h, video_scale_ceiling());
|
||||
g_video_scale = psx_resolve_internal_scale_ss(preset, g_video_ref_lines,
|
||||
display_px_h, video_scale_ceiling(),
|
||||
g_video_ss_milli);
|
||||
}
|
||||
|
||||
/* The value the launcher row starts on: the preset, or the legacy factor shown
|
||||
@@ -9364,9 +9367,15 @@ static void dynres_setup(void) {
|
||||
"dual raster)\n");
|
||||
return;
|
||||
}
|
||||
int floor_s = psx_resolve_internal_scale(dynres_min_value(), g_video_ref_lines,
|
||||
psx_sdl_display_pixel_height(nullptr),
|
||||
ceiling);
|
||||
/* Floor "display": the output's own lines, i.e. the selected internal
|
||||
* resolution without the supersample factor (Match display = the
|
||||
* monitor's pixel height). Dynamic resolution then only gives back
|
||||
* supersampling and never renders below the output. */
|
||||
int floor_s = psx_dynres_floor_scale(dynres_min_value(),
|
||||
effective_internal_resolution(),
|
||||
g_video_ref_lines,
|
||||
psx_sdl_display_pixel_height(nullptr),
|
||||
ceiling);
|
||||
if (floor_s > ceiling) floor_s = ceiling;
|
||||
DynresParams params;
|
||||
dynres_default_params(¶ms);
|
||||
@@ -15965,6 +15974,8 @@ int main(int argc, char** argv) {
|
||||
g_video_win_w = gc.runtime.video_window_width;
|
||||
}
|
||||
g_video_aa = gc.runtime.video_antialiasing;
|
||||
g_video_post_aa = gc.runtime.video_antialiasing_mode;
|
||||
g_video_ss_milli = gc.runtime.video_supersample_milli;
|
||||
g_video_texfilter = gc.runtime.video_texture_filter;
|
||||
g_video_fmv_filter = gc.runtime.video_fmv_filter;
|
||||
g_video_geometry_correction =
|
||||
@@ -18259,15 +18270,31 @@ session_reboot:
|
||||
else std::fprintf(stdout, "psxrecomp: PSX_INTERNAL_RESOLUTION=%s not understood "
|
||||
"(native, 720p, 1080p, 1440p, 4k, 5k, 8k, display, or lines)\n", e);
|
||||
}
|
||||
/* PSX_SUPERSAMPLE (1.0..4.0) and PSX_AA_MODE (off, fxaa, fxaa_hq): one-run
|
||||
* overrides of [video] supersample / antialiasing_mode. */
|
||||
if (const char* e = std::getenv("PSX_SUPERSAMPLE")) {
|
||||
const int m = psx_ss_parse_milli(e);
|
||||
if (m) g_video_ss_milli = m;
|
||||
else std::fprintf(stdout, "psxrecomp: PSX_SUPERSAMPLE=%s not understood (1.0..4.0)\n", e);
|
||||
}
|
||||
if (const char* e = std::getenv("PSX_AA_MODE")) {
|
||||
if (!std::strcmp(e, "off")) g_video_post_aa = GL_POST_AA_OFF;
|
||||
else if (!std::strcmp(e, "fxaa")) g_video_post_aa = GL_POST_AA_FXAA;
|
||||
else if (!std::strcmp(e, "fxaa_hq")) g_video_post_aa = GL_POST_AA_FXAA_HQ;
|
||||
else std::fprintf(stdout, "psxrecomp: PSX_AA_MODE=%s not understood (off, fxaa, fxaa_hq)\n", e);
|
||||
}
|
||||
if (g_video_ss_milli != 1000)
|
||||
std::fprintf(stdout, "psxrecomp: supersample %.3gx on the internal-resolution target\n",
|
||||
g_video_ss_milli / 1000.0);
|
||||
apply_internal_resolution(psx_sdl_display_pixel_height(nullptr));
|
||||
if (g_video_scale < 1) g_video_scale = 1;
|
||||
if (const char* e = std::getenv("PSX_DYNRES"))
|
||||
g_video_dynres_env = (*e && *e != '0') ? 1 : 0;
|
||||
if (const char* e = std::getenv("PSX_DYNRES_MIN")) {
|
||||
int v = 0;
|
||||
if (psx_ir_parse(e, &v) && v != PSX_IR_DISPLAY) g_video_dynres_min_env = v;
|
||||
if (psx_ir_parse(e, &v)) g_video_dynres_min_env = v;
|
||||
else std::fprintf(stdout, "psxrecomp: PSX_DYNRES_MIN=%s not understood "
|
||||
"(native, 720p, 1080p, 1440p, 4k, 5k, 8k, or lines)\n", e);
|
||||
"(native, 720p, 1080p, 1440p, 4k, 5k, 8k, display, or lines)\n", e);
|
||||
}
|
||||
{
|
||||
/* Per-backend ceiling. OpenGL allocates its hr surface at context init
|
||||
@@ -18673,6 +18700,7 @@ session_reboot:
|
||||
return 1;
|
||||
}
|
||||
gl_renderer_set_swap_interval(0);
|
||||
(void)gl_renderer_set_post_aa(g_video_post_aa);
|
||||
g_gl_active = gl_renderer_init_context(s_headless_gl_window) != 0;
|
||||
if (!g_gl_active || gr_backend() != GR_BACKEND_OPENGL ||
|
||||
!(SDL_GetWindowFlags(s_headless_gl_window) & SDL_WINDOW_HIDDEN)) {
|
||||
@@ -18854,8 +18882,9 @@ session_reboot:
|
||||
* surface is allocated at context init below. */
|
||||
if (effective_internal_resolution() == PSX_IR_DISPLAY && g_video_scale_applies) {
|
||||
const int dh = psx_sdl_display_pixel_height(sdl_window);
|
||||
const int s = psx_resolve_internal_scale(PSX_IR_DISPLAY, g_video_ref_lines,
|
||||
dh, GL_MAX_INTERNAL_SCALE);
|
||||
const int s = psx_resolve_internal_scale_ss(PSX_IR_DISPLAY, g_video_ref_lines,
|
||||
dh, GL_MAX_INTERNAL_SCALE,
|
||||
g_video_ss_milli);
|
||||
gr_set_scale(s);
|
||||
g_video_requested_scale = s;
|
||||
std::fprintf(stdout, "psxrecomp: internal resolution Match display: "
|
||||
@@ -18882,6 +18911,7 @@ session_reboot:
|
||||
#endif
|
||||
gl_renderer_set_dynamic_resolution(
|
||||
((dynres_requested() || dbg_live_scale) && g_video_scale_applies) ? 1 : 0);
|
||||
(void)gl_renderer_set_post_aa(g_video_post_aa);
|
||||
g_gl_active = (gl_renderer_init_context(sdl_window) != 0);
|
||||
dynres_setup();
|
||||
if (dbg_live_scale && g_gl_active) {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/* Post-process anti-aliasing ([video] antialiasing_mode) on a real, hidden
|
||||
* OpenGL context.
|
||||
*
|
||||
* - off: the composed frame is untouched (byte-identical before and after a
|
||||
* round trip through the AA modes; the pass never runs);
|
||||
* - fxaa / fxaa_hq: a hard polygon edge gains intermediate shades (less
|
||||
* stair-stepping) while flat regions away from the edge stay identical;
|
||||
* - guest VRAM / readbacks are the same whatever the mode.
|
||||
* With --dump PATH the off frame is written raw (RGB, bottom-up) so a run
|
||||
* against another tree can be compared byte for byte. */
|
||||
#define GL_PRESENT_TEST_HOOK(gen) aa_capture_hook(gen)
|
||||
static void aa_capture_hook(int gen);
|
||||
#define main scale_fixture_main
|
||||
#include "test_gl_scale_invariance.c"
|
||||
#undef main
|
||||
void present_shot_done(int ok) { (void)ok; }
|
||||
int host_osd_needs_present(void) { return 0; }
|
||||
int psx_present_vsync_owns_cadence(void) { return 0; }
|
||||
void latency_ring_mark(LatencyStage stage) { (void)stage; }
|
||||
void psx_host_sleep_ms(unsigned ms) { SDL_Delay(ms); }
|
||||
int present_shot_take(char* out,int n) { (void)out; (void)n; return 0; }
|
||||
int host_osd_image(const uint32_t** p,int* w,int* h) { (void)p; (void)w; (void)h; return 0; }
|
||||
int host_osd_volume_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
|
||||
void host_osd_present_done(void) {}
|
||||
int psx_rewind_overlay_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
|
||||
float psx_rewind_slide(void) { return 0; }
|
||||
int psx_savestate_menu_overlay_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
|
||||
|
||||
static uint8_t *s_cap; static int s_cap_w, s_cap_h, s_cap_n;
|
||||
static void aa_capture_hook(int gen) {
|
||||
(void)gen;
|
||||
SDL_GL_GetDrawableSize(s_win, &s_cap_w, &s_cap_h);
|
||||
free(s_cap);
|
||||
s_cap = (uint8_t *)malloc((size_t)s_cap_w * s_cap_h * 3);
|
||||
glReadBuffer(GL_BACK);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, s_cap_w, s_cap_h, GL_RGB, GL_UNSIGNED_BYTE, s_cap);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
s_cap_n++;
|
||||
}
|
||||
|
||||
/* A white polygon with a shallow top edge (long stair steps) on black. */
|
||||
static uint8_t *frame(int mode, uint64_t *vram_hash) {
|
||||
#ifndef AA_FIXTURE_NO_POST_AA
|
||||
gl_renderer_set_post_aa(mode);
|
||||
#else
|
||||
(void)mode;
|
||||
#endif
|
||||
glb_set_draw_area(0, 0, FRAME_W - 1, FRAME_H - 1); glb_set_draw_offset(0, 0);
|
||||
glb_set_mask_bits(0, 0); glb_set_semi_transparency(0, 0);
|
||||
glb_fill_rect(0, 0, FRAME_W, FRAME_H, 0x0000);
|
||||
glb_draw_flat_triangle(20, 150, 300, 110, 300, 200, 0x7fff);
|
||||
glb_draw_flat_triangle(20, 150, 300, 200, 20, 200, 0x7fff);
|
||||
gl_renderer_sync_cpu();
|
||||
check(gl_renderer_fbo_peek(0, 0, FRAME_W, FRAME_H, peek), "vram readback");
|
||||
*vram_hash = fnv(peek, FRAME_W * FRAME_H * 2, 0xcbf29ce484222325ull);
|
||||
const int n0 = s_cap_n;
|
||||
gl_renderer_present_vram(0, 0, FRAME_W, FRAME_H, 0, 0);
|
||||
check(s_cap_n == n0 + 1 && s_cap, "present captured");
|
||||
uint8_t *c = (uint8_t *)malloc((size_t)s_cap_w * s_cap_h * 3);
|
||||
memcpy(c, s_cap, (size_t)s_cap_w * s_cap_h * 3);
|
||||
return c;
|
||||
}
|
||||
|
||||
static int mid_tones(const uint8_t *c) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < s_cap_w * s_cap_h; i++) { int g = c[i * 3 + 1]; n += g > 24 && g < 231; }
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Pixels whose 5x5 neighbourhood in ref is flat must be identical in c. */
|
||||
static int flat_changed(const uint8_t *ref, const uint8_t *c) {
|
||||
int bad = 0;
|
||||
for (int y = 2; y < s_cap_h - 2; y++)
|
||||
for (int x = 2; x < s_cap_w - 2; x++) {
|
||||
const uint8_t *r = ref + ((size_t)y * s_cap_w + x) * 3;
|
||||
int flat = 1;
|
||||
for (int dy = -2; dy <= 2 && flat; dy++)
|
||||
for (int dx = -2; dx <= 2; dx++)
|
||||
if (memcmp(ref + ((size_t)(y + dy) * s_cap_w + x + dx) * 3, r, 3)) { flat = 0; break; }
|
||||
if (flat && memcmp(c + ((size_t)y * s_cap_w + x) * 3, r, 3)) bad++;
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *dump = argc > 2 && !strcmp(argv[1], "--dump") ? argv[2] : NULL;
|
||||
if (SDL_Init(SDL_INIT_VIDEO) != 0) { fprintf(stderr, "SKIP no video\n"); return 77; }
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
#if defined(__APPLE__)
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
|
||||
#endif
|
||||
SDL_Window *win = SDL_CreateWindow("Post AA hidden test", 0, 0, 640, 480,
|
||||
SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN);
|
||||
if (!win) { fprintf(stderr, "SKIP no window\n"); return 77; }
|
||||
glb_init(vram); glb_set_scale(1); gl_renderer_set_swap_interval(0);
|
||||
if (!gl_renderer_init_context(win)) { fprintf(stderr, "SKIP no GL context\n"); return 77; }
|
||||
uint64_t h_off = 0, h_fx = 0, h_hq = 0, h_off2 = 0;
|
||||
uint8_t *off = frame(GL_POST_AA_OFF, &h_off);
|
||||
if (dump) {
|
||||
FILE *f = fopen(dump, "wb");
|
||||
check(f && fwrite(off, 1, (size_t)s_cap_w * s_cap_h * 3, f) == (size_t)s_cap_w * s_cap_h * 3, "dump");
|
||||
if (f) fclose(f);
|
||||
}
|
||||
#ifndef AA_FIXTURE_NO_POST_AA
|
||||
check(gl_renderer_post_aa_passes() == 0, "off never runs the AA pass");
|
||||
uint8_t *fx = frame(GL_POST_AA_FXAA, &h_fx);
|
||||
uint8_t *hq = frame(GL_POST_AA_FXAA_HQ, &h_hq);
|
||||
uint8_t *off2 = frame(GL_POST_AA_OFF, &h_off2);
|
||||
const size_t bytes = (size_t)s_cap_w * s_cap_h * 3;
|
||||
const int m_off = mid_tones(off), m_fx = mid_tones(fx), m_hq = mid_tones(hq);
|
||||
printf("drawable=%dx%d mid_tones off=%d fxaa=%d fxaa_hq=%d flat_changed fxaa=%d hq=%d passes=%llu\n",
|
||||
s_cap_w, s_cap_h, m_off, m_fx, m_hq, flat_changed(off, fx), flat_changed(off, hq),
|
||||
(unsigned long long)gl_renderer_post_aa_passes());
|
||||
check(gl_renderer_post_aa_passes() == 2, "one pass per AA present");
|
||||
check(m_fx > m_off + 40, "fxaa softens the polygon edge");
|
||||
check(m_hq > m_off + 40, "fxaa_hq softens the polygon edge");
|
||||
check(memcmp(off, fx, bytes) != 0, "fxaa changes the frame");
|
||||
check(flat_changed(off, fx) == 0 && flat_changed(off, hq) == 0, "flat regions untouched");
|
||||
check(memcmp(off, off2, bytes) == 0, "off after AA is byte-identical");
|
||||
check(h_off == h_fx && h_off == h_hq && h_off == h_off2, "guest VRAM identical in every mode");
|
||||
free(fx); free(hq); free(off2);
|
||||
#endif
|
||||
free(off);
|
||||
check(glGetError() == GL_NO_ERROR, "GL errors");
|
||||
printf("checks=%d failures=%d\n", checks, failures);
|
||||
gl_renderer_shutdown(); SDL_DestroyWindow(win); SDL_Quit();
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -200,6 +200,28 @@ int main() {
|
||||
expect("legacy round trip",
|
||||
psx_resolve_internal_scale(psx_ir_from_supersampling(n, 240), 240, 0, 32), n);
|
||||
|
||||
// Supersample: a factor on the target, not on native or the legacy factor.
|
||||
expect("ss parse 1.5", psx_ss_parse_milli("1.5"), 1500);
|
||||
expect("ss parse 2x", psx_ss_parse_milli("2x"), 2000);
|
||||
expect("ss parse 4", psx_ss_parse_milli("4"), 4000);
|
||||
expect("ss reject 0.5", psx_ss_parse_milli("0.5"), 0);
|
||||
expect("ss reject 5", psx_ss_parse_milli("5"), 0);
|
||||
// Dynamic resolution floor "display": the output's lines, no supersample.
|
||||
expect("floor display 1080 MD", psx_dynres_floor_scale(PSX_IR_DISPLAY, PSX_IR_DISPLAY, 240, 1080, 9), 5);
|
||||
expect("floor display 1440 MD", psx_dynres_floor_scale(PSX_IR_DISPLAY, PSX_IR_DISPLAY, 240, 1440, 12), 6);
|
||||
expect("floor display preset", psx_dynres_floor_scale(PSX_IR_DISPLAY, 1080, 240, 2160, 9), 5);
|
||||
expect("floor display unset", psx_dynres_floor_scale(PSX_IR_DISPLAY, PSX_IR_UNSET, 240, 1080, 9), 5);
|
||||
expect("floor display clamp", psx_dynres_floor_scale(PSX_IR_DISPLAY, PSX_IR_DISPLAY, 240, 2160, 4), 4);
|
||||
expect("floor 720p", psx_dynres_floor_scale(720, PSX_IR_DISPLAY, 240, 1080, 9), 3);
|
||||
expect("ss reject junk", psx_ss_parse_milli("2y"), 0);
|
||||
expect("ss 1.0 is the plain resolve",
|
||||
psx_resolve_internal_scale_ss(PSX_IR_DISPLAY, 240, 1080, 32, 1000), 5);
|
||||
expect("ss 2.0 match display 1080", psx_resolve_internal_scale_ss(PSX_IR_DISPLAY, 240, 1080, 32, 2000), 9);
|
||||
expect("ss 1.5 at 1440p", psx_resolve_internal_scale_ss(1440, 240, 0, 32, 1500), 9);
|
||||
expect("ss leaves native", psx_resolve_internal_scale_ss(PSX_IR_NATIVE, 240, 1080, 32, 4000), 1);
|
||||
expect("ss clamps to the ceiling", psx_resolve_internal_scale_ss(4320, 240, 0, 32, 4000), 32);
|
||||
expect("ss unknown display stays 1", psx_resolve_internal_scale_ss(PSX_IR_DISPLAY, 240, 0, 32, 2000), 1);
|
||||
|
||||
launcher_round_trips();
|
||||
|
||||
if (g_failures) return 1;
|
||||
|
||||
Reference in New Issue
Block a user