diff --git a/docs/TCP_COMMANDS.md b/docs/TCP_COMMANDS.md index 1681aff1..7c724721 100644 --- a/docs/TCP_COMMANDS.md +++ b/docs/TCP_COMMANDS.md @@ -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":""}` 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":""}` 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` | ✓ | | | diff --git a/docs/config_schema.md b/docs/config_schema.md index 16102b0d..187dd648 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -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=` 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. diff --git a/generated/OpenBIOS.emitter.sha b/generated/OpenBIOS.emitter.sha index 25a4a441..a560ef86 100644 --- a/generated/OpenBIOS.emitter.sha +++ b/generated/OpenBIOS.emitter.sha @@ -1 +1 @@ -7d8356d54277a80240c604717bd3a801c321a19bd789bbb74adbd62e635379d1 +b352f488a134fd511614abed04c6076152b9a80ab24c796a6b671126adfd4bdd diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index e4bdeb5e..294e3a11 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -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(video, "antialiasing"); } + if (video.contains("antialiasing_mode")) { + const auto mode = toml::find(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(v.as_integer()) + : toml::get(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(f * 1000.0 + 0.5); + } if (video.contains("texture_filtering")) { const auto mode = toml::find(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"; diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index b4802653..86d9810b 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -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; diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 68d7ba4f..e954602b 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -264,6 +264,16 @@ if(BUILD_TESTING) --sdl-library "$" --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 "$" + --sdl-library "$" + --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" diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index a12b9e86..570d2bf4 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -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); diff --git a/runtime/include/internal_resolution.h b/runtime/include/internal_resolution.h index 9362d209..50e61016 100644 --- a/runtime/include/internal_resolution.h +++ b/runtime/include/internal_resolution.h @@ -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; +} diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 2ec0c08d..649ac92e 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -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 }, diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 6937b38b..3cf4c6f1 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -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 diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index b20d4c72..814acff6 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -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) { diff --git a/runtime/tests/test_gl_post_aa.c b/runtime/tests/test_gl_post_aa.c new file mode 100644 index 00000000..430bae86 --- /dev/null +++ b/runtime/tests/test_gl_post_aa.c @@ -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; +} diff --git a/runtime/tests/test_internal_resolution.cpp b/runtime/tests/test_internal_resolution.cpp index e8219104..25b04cdd 100644 --- a/runtime/tests/test_internal_resolution.cpp +++ b/runtime/tests/test_internal_resolution.cpp @@ -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;