diff --git a/docs/config_schema.md b/docs/config_schema.md index cdce0fcc..b2989de5 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -702,16 +702,23 @@ player's `settings.toml`, and a game may ship a default the same way: [video] internal_resolution = "4k" # native | 720p | 1080p | 1440p | 4k | 5k | 8k | display, or a number of lines resolution_reference_lines = 240 # game.toml only: the title's usual display height (120..1024) +supersample_disable_at_lines = 0 # game.toml only: supersample 1.0 at/above this display height (0 = off, 240..8640) match_display_max_lines = 0 # game.toml only: cap on the display height Match display uses (0 = none, 240..8640) ``` `match_display_max_lines` caps the monitor height that Match display, its dynamic-resolution floor (`display`) and graphics-preset detection resolve -against. With no cap, an 8K panel asks 18x (27x with `supersample = 1.5`), which is +against. It also caps the final Match display target after supersampling, +so a larger saved factor cannot exceed it. Fixed presets keep their own target. +With no cap, an 8K panel asks 18x (27x with `supersample = 1.5`), which is past the full-VRAM surface. The high-resolution window then engages, and dynamic resolution cannot step there. A title whose GPU budget is set for 4K lines sets 2160. +`supersample_disable_at_lines` uses the physical monitor height before the cap. +At or above its threshold the effective factor is 1.0; the saved factor is +retained for smaller monitors. Zero preserves the default behaviour. + A preset is a target height. The runtime renders at the integer scale `S = ceil(target / resolution_reference_lines)`: at the default 240 lines, 720p is 3x, 1080p is 5x (1200 lines, area-resolved to 1080), 1440p 6x, 4K 9x, diff --git a/generated/OpenBIOS.emitter.sha b/generated/OpenBIOS.emitter.sha index e8134784..70cb65a7 100644 --- a/generated/OpenBIOS.emitter.sha +++ b/generated/OpenBIOS.emitter.sha @@ -1 +1 @@ -75a8d9834efb1c994169a939fbe043073b1b4b133bfe7b0235b80338af7f2f08 +61bf4f6d29680623d215fbe407958408cab0a1e08dbda3c62baa25e607d63b7b diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 46161cf4..768350b7 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -780,6 +780,13 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& } rt.video_match_display_max_lines = static_cast(n); } + if (video.contains("supersample_disable_at_lines")) { + const auto n = toml::find(video, "supersample_disable_at_lines"); + if (n != 0 && (n < 240 || n > 8640)) + throw std::runtime_error(fmt::format( + "[video] supersample_disable_at_lines must be 0 or 240..8640: {}", n)); + rt.video_supersample_disable_at_lines = static_cast(n); + } if (video.contains("window_width")) { const auto n = toml::find(video, "window_width"); if (n < 640 || n > 7680) { diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 7fb4375b..1cca1185 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -472,6 +472,8 @@ struct RuntimeConfig { /* [video] match_display_max_lines: cap on the display height Match * display (and its dynamic-resolution floor) resolves against; 0 = none. */ int video_match_display_max_lines = 0; + /* Disable target supersampling on displays at least this tall (0 = off). */ + int video_supersample_disable_at_lines = 0; // Optional initial window width declared by the title profile. Zero keeps // the historical fit-to-display behavior; player settings may override it. diff --git a/recompiler/tests/video_enhancement_settings_test.cpp b/recompiler/tests/video_enhancement_settings_test.cpp index 002bbd8f..cdc5b83a 100644 --- a/recompiler/tests/video_enhancement_settings_test.cpp +++ b/recompiler/tests/video_enhancement_settings_test.cpp @@ -226,14 +226,17 @@ static void test_internal_resolution_game_toml() { "resolution_reference_lines defaults to 240"); check(gc.runtime.video_match_display_max_lines == 0, "match_display_max_lines defaults to 0 (no cap)"); + check(gc.runtime.video_supersample_disable_at_lines == 0, "display SS policy defaults off"); fs::remove(p); p = write_game_toml("psxrecomp_ir_match_cap.toml", "[video]\n" "internal_resolution = \"display\"\n" - "match_display_max_lines = 2160\n"); + "match_display_max_lines = 2160\n" + "supersample_disable_at_lines = 2160\n"); gc = PSXRecompV4::load_game_config(p); check(gc.runtime.video_match_display_max_lines == 2160, "match_display_max_lines parses"); + check(gc.runtime.video_supersample_disable_at_lines == 2160, "display SS policy parses"); fs::remove(p); p = write_game_toml("psxrecomp_ir_4k.toml", @@ -257,6 +260,7 @@ static void test_internal_resolution_game_toml() { "internal_resolution = 1\n", "resolution_reference_lines = 50\n", "match_display_max_lines = 100\n", + "supersample_disable_at_lines = 100\n", "supersampling = 33\n" }) { p = write_game_toml("psxrecomp_ir_bad.toml", std::string("[video]\n") + bad); bool rejected = false; diff --git a/runtime/include/internal_resolution.h b/runtime/include/internal_resolution.h index 50e61016..c37dc32a 100644 --- a/runtime/include/internal_resolution.h +++ b/runtime/include/internal_resolution.h @@ -143,6 +143,13 @@ static inline int psx_ss_parse_milli(const char *s) { return (int)m; } +/* Opt-in display policy; keep the saved factor intact for smaller displays. */ +static inline int psx_ir_display_supersample(int ss_milli, int display_px_h, + int disable_at_lines) { + return disable_at_lines > 0 && display_px_h >= disable_at_lines + ? PSX_SS_MILLI_MIN : ss_milli; +} + /* 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, @@ -160,6 +167,18 @@ static inline int psx_resolve_internal_scale_ss(int value, int ref_lines, return psx_resolve_internal_scale(value, ref_lines, display_px_h, s_max); } +/* A Match display cap bounds the final target, including supersampling. + * Fixed presets and the legacy integer scale retain their own ceilings. */ +static inline int psx_resolve_internal_scale_ss_cap(int value, int ref_lines, + int display_px_h, int s_max, int ss_milli, int max_display_lines) { + int s = psx_resolve_internal_scale_ss(value, ref_lines, display_px_h, s_max, ss_milli); + if (value == PSX_IR_DISPLAY && max_display_lines > 0) { + int cap = psx_resolve_internal_scale(max_display_lines, ref_lines, 0, s_max); + if (s > cap) s = cap; + } + return s; +} + /* 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), diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 484a98f8..6bf5eea9 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1272,6 +1272,11 @@ static int g_video_ss_milli = 1000; /* [video] supersample / PSX_SUPER * full-VRAM surface, where the high-resolution window engages and dynamic * resolution cannot step; a title can cap it at, say, 4K lines. */ static int g_video_match_max_lines = 0; +static int g_video_ss_disable_at_lines = 0; +static int display_supersample(SDL_Window *window) { + return psx_ir_display_supersample(g_video_ss_milli, + psx_sdl_display_pixel_height(window), g_video_ss_disable_at_lines); +} static int match_display_px_h(SDL_Window *window) { const int h = psx_sdl_display_pixel_height(window); return (g_video_match_max_lines > 0 && h > g_video_match_max_lines) @@ -1454,9 +1459,8 @@ 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_ss(preset, g_video_ref_lines, - display_px_h, video_scale_ceiling(), - g_video_ss_milli); + g_video_scale = psx_resolve_internal_scale_ss_cap(preset, g_video_ref_lines, + display_px_h, video_scale_ceiling(), display_supersample(nullptr), g_video_match_max_lines); } /* The value the launcher row starts on: the preset, or the legacy factor shown @@ -17860,6 +17864,7 @@ int main(int argc, char** argv) { g_video_internal_res = gc.runtime.video_internal_resolution; g_video_ref_lines = gc.runtime.video_resolution_reference_lines; g_video_match_max_lines = gc.runtime.video_match_display_max_lines; + g_video_ss_disable_at_lines = gc.runtime.video_supersample_disable_at_lines; g_video_dynres = gc.runtime.video_dynamic_resolution ? 1 : 0; g_video_dynres_min = gc.runtime.video_dynamic_resolution_min; if (gc.runtime.video_window_width > 0) { @@ -20353,9 +20358,10 @@ session_reboot: 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) + const int effective_ss = display_supersample(nullptr); + if (!g_video_ss_disable_at_lines && effective_ss != 1000) std::fprintf(stdout, "psxrecomp: supersample %.3gx on the internal-resolution target\n", - g_video_ss_milli / 1000.0); + effective_ss / 1000.0); apply_internal_resolution(match_display_px_h(nullptr)); if (g_video_scale < 1) g_video_scale = 1; if (const char* e = std::getenv("PSX_DYNRES")) @@ -20996,17 +21002,22 @@ session_reboot: /* Match display: the monitor's pixel height, now that the window says * which monitor. glb_set_scale only records the request; the hr * surface is allocated at context init below. */ - if (effective_internal_resolution() == PSX_IR_DISPLAY && g_video_scale_applies) { + const int ir = effective_internal_resolution(); + if (g_video_scale_applies && ir != PSX_IR_UNSET && + (ir == PSX_IR_DISPLAY || g_video_ss_disable_at_lines > 0)) { const int dh = match_display_px_h(sdl_window); - const int s = psx_resolve_internal_scale_ss(PSX_IR_DISPLAY, g_video_ref_lines, - dh, GL_MAX_INTERNAL_SCALE, - g_video_ss_milli); + const int ss = display_supersample(sdl_window); + const int s = psx_resolve_internal_scale_ss_cap(ir, g_video_ref_lines, + dh, GL_MAX_INTERNAL_SCALE, ss, g_video_match_max_lines); gr_set_scale(s); g_video_requested_scale = s; - std::fprintf(stdout, "psxrecomp: internal resolution Match display: " - "%d px -> %dx requested\n", dh, s); + const char *label = psx_ir_label_for(ir); + std::fprintf(stdout, "psxrecomp: internal resolution %s: " + "%d px -> %dx requested (supersample %.3gx)\n", + label ? label : "custom", dh, s, ss / 1000.0); } gl_renderer_set_swap_interval(present_effective_swap_interval()); /* applied at context init */ + /* Dynamic resolution: the surfaces are allocated at the scale above * (the ceiling) and the level steps under it (dynres_setup). */ #ifndef PSX_NO_DEBUG_TOOLS diff --git a/runtime/tests/test_internal_resolution.cpp b/runtime/tests/test_internal_resolution.cpp index 25b04cdd..597c271c 100644 --- a/runtime/tests/test_internal_resolution.cpp +++ b/runtime/tests/test_internal_resolution.cpp @@ -222,6 +222,21 @@ int main() { 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); + expect("ss policy opt-out", psx_ir_display_supersample(1500, 4320, 0), 1500); + expect("ss below threshold", psx_ir_display_supersample(1500, 2159, 2160), 1500); + expect("ss at threshold", psx_ir_display_supersample(1500, 2160, 2160), 1000); + expect("ss on 8K", psx_ir_display_supersample(1500, 4320, 2160), 1000); + expect("ss unknown display", psx_ir_display_supersample(1500, 0, 2160), 1500); + expect("cap includes SS below threshold", psx_resolve_internal_scale_ss_cap( + PSX_IR_DISPLAY, 240, 1440, 32, 4000, 4320), 18); + expect("cap below 8K", psx_resolve_internal_scale_ss_cap( + PSX_IR_DISPLAY, 240, 1080, 32, 1500, 4320), 7); + expect("8K capped after SS", psx_resolve_internal_scale_ss_cap( + PSX_IR_DISPLAY, 240, 8640, 32, 4000, 4320), 18); + expect("fixed preset keeps own ceiling", psx_resolve_internal_scale_ss_cap( + 4320, 240, 8640, 32, 4000, 4320), 32); + expect("cap opt-out", psx_resolve_internal_scale_ss_cap( + PSX_IR_DISPLAY, 240, 4320, 32, 1500, 0), 27); launcher_round_trips(); if (g_failures) return 1;