From 84f2ea2f3b87ebb50ce2c18753961513a60e7ca6 Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:26:45 -0700 Subject: [PATCH] [video] match_display_max_lines: opt-in cap on Match display's height On an 8K panel Match display asks 18x (27x with supersample 1.5), past the full-VRAM surface: the high-resolution window engages, dynamic resolution is ineligible there, and the render thread saturates (R4 0.8 on an RTX 4080 at 7680x4320: fixed 27x, render_busy 100 %, Low 51.9 guest Hz). A game.toml key caps the display height that Match display, its dynamic-resolution floor and preset detection resolve against. Default 0 keeps today's behaviour for every title. Co-Authored-By: Claude Opus 5.5 --- docs/config_schema.md | 8 +++++ recompiler/src/config_loader.cpp | 8 +++++ recompiler/src/config_loader.h | 3 ++ .../tests/video_enhancement_settings_test.cpp | 11 +++++++ runtime/src/main.cpp | 33 +++++++++++++------ 5 files changed, 53 insertions(+), 10 deletions(-) diff --git a/docs/config_schema.md b/docs/config_schema.md index f6ffd56e..cdce0fcc 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -702,8 +702,16 @@ 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) +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 +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. + 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/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index a8cb2764..46161cf4 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -772,6 +772,14 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& } rt.video_resolution_reference_lines = static_cast(n); } + if (video.contains("match_display_max_lines")) { + const auto n = toml::find(video, "match_display_max_lines"); + if (n != 0 && (n < 240 || n > 8640)) { + throw std::runtime_error(fmt::format( + "[video] match_display_max_lines must be 0 (no cap) or 240..8640: {}", n)); + } + rt.video_match_display_max_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 3766dffd..7fb4375b 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -469,6 +469,9 @@ struct RuntimeConfig { // preset divides into (S = ceil(target / reference)). 240 for NTSC // 320x240 games; 120..1024. int video_resolution_reference_lines = 240; + /* [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; // 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 33dda58c..002bbd8f 100644 --- a/recompiler/tests/video_enhancement_settings_test.cpp +++ b/recompiler/tests/video_enhancement_settings_test.cpp @@ -224,6 +224,16 @@ static void test_internal_resolution_game_toml() { "internal_resolution defaults unset (supersampling stands)"); check(gc.runtime.video_resolution_reference_lines == 240, "resolution_reference_lines defaults to 240"); + check(gc.runtime.video_match_display_max_lines == 0, + "match_display_max_lines defaults to 0 (no cap)"); + fs::remove(p); + + p = write_game_toml("psxrecomp_ir_match_cap.toml", + "[video]\n" + "internal_resolution = \"display\"\n" + "match_display_max_lines = 2160\n"); + gc = PSXRecompV4::load_game_config(p); + check(gc.runtime.video_match_display_max_lines == 2160, "match_display_max_lines parses"); fs::remove(p); p = write_game_toml("psxrecomp_ir_4k.toml", @@ -246,6 +256,7 @@ static void test_internal_resolution_game_toml() { for (const char* bad : { "internal_resolution = \"9k\"\n", "internal_resolution = 1\n", "resolution_reference_lines = 50\n", + "match_display_max_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/src/main.cpp b/runtime/src/main.cpp index b46655f8..6c809e2e 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1262,6 +1262,18 @@ static int g_video_post_aa = 0; /* [video] antialiasing_mode / PSX_A static float g_video_bloom = 0.0f; /* [video] bloom (PSX_BLOOM overrides) */ static std::string g_video_post_shader; /* [video] post_shader (PSX_POST_SHADER overrides) */ static int g_video_ss_milli = 1000; /* [video] supersample / PSX_SUPERSAMPLE */ + +/* [video] match_display_max_lines (0 = none): the display height Match + * display, its dynamic-resolution floor and the quality detection resolve + * against, capped. On an 8K panel Match display x1.5 asks 27x, past the + * 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 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) + ? g_video_match_max_lines : h; +} /* 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; @@ -10184,7 +10196,7 @@ static void dynres_setup(void) { int floor_s = psx_dynres_floor_scale(dynres_min_value(), effective_internal_resolution(), g_video_ref_lines, - psx_sdl_display_pixel_height(nullptr), + match_display_px_h(nullptr), ceiling); if (floor_s > ceiling) floor_s = ceiling; DynresParams params; @@ -16460,7 +16472,7 @@ namespace { void quality_fill_game_info(RecompLauncherCGameInfo* gi) { if (!g_quality_offered) return; quality_publish_strings(); - g_quality_display_h = psx_sdl_display_pixel_height(nullptr); + g_quality_display_h = match_display_px_h(nullptr); gi->quality_offered_mask = (int)g_quality_offered; gi->quality_detected = (g_quality.detected >= 0 && g_quality.detected < PSX_QUALITY_COUNT) ? g_quality.detected + 1 : 0; @@ -17089,7 +17101,7 @@ static void recomp_overlay_setup(const char* argv0, const std::string& title) { g_ovl_q_reason = g_quality.reason; g_ovl_q_detected = (g_quality.detected >= 0 && g_quality.detected < PSX_QUALITY_COUNT) ? g_quality.detected + 1 : 0; - g_quality_display_h = psx_sdl_display_pixel_height(nullptr); + g_quality_display_h = match_display_px_h(nullptr); { std::lock_guard lk(g_ovl_mu); g_ovl_q_sel.reset(); @@ -17834,6 +17846,7 @@ int main(int argc, char** argv) { gc.runtime.video_depth24_trailing_margin; 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_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) { @@ -18996,7 +19009,7 @@ int main(int argc, char** argv) { const int ir_preset_seeded = g_video_internal_res; const int ir_ss_seeded = psx_ir_launcher_seed_supersampling( kLauncherHasInternalResolution, g_video_internal_res, g_video_scale, - g_video_ref_lines, psx_sdl_display_pixel_height(nullptr)); + g_video_ref_lines, match_display_px_h(nullptr)); seed.supersampling = ir_ss_seeded; seed.has_supersampling = true; int ir_row_result = PSX_IR_UNSET; /* the row's pick, when it has one */ #if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION) @@ -19872,7 +19885,7 @@ int main(int argc, char** argv) { const PsxIrAdopted ir = psx_ir_adopt_launcher( kLauncherHasInternalResolution, ir_preset_seeded, ir_ss_seeded, seed.supersampling, ir_row_result, g_video_ref_lines, - psx_sdl_display_pixel_height(nullptr), video_scale_ceiling()); + match_display_px_h(nullptr), video_scale_ceiling()); g_video_internal_res = ir.preset; g_video_scale = ir.scale; seed.supersampling = ir.save_ss; @@ -20327,7 +20340,7 @@ session_reboot: 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)); + 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")) g_video_dynres_env = (*e && *e != '0') ? 1 : 0; @@ -20965,7 +20978,7 @@ session_reboot: * 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 dh = psx_sdl_display_pixel_height(sdl_window); + 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); @@ -20986,7 +20999,7 @@ session_reboot: if (dbg_live_scale) { const int ds = psx_resolve_internal_scale( PSX_IR_DISPLAY, g_video_ref_lines, - psx_sdl_display_pixel_height(sdl_window), GL_MAX_INTERNAL_SCALE); + match_display_px_h(sdl_window), GL_MAX_INTERNAL_SCALE); if (ds > dbg_start_scale) gr_set_scale(ds); } #else @@ -21831,7 +21844,7 @@ soft_return_lobby: const int ir_preset_seeded = g_video_internal_res; const int ir_ss_seeded = psx_ir_launcher_seed_supersampling( kLauncherHasInternalResolution, g_video_internal_res, g_video_scale, - g_video_ref_lines, psx_sdl_display_pixel_height(nullptr)); + g_video_ref_lines, match_display_px_h(nullptr)); ls.supersampling = ir_ss_seeded; #if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION) ls.internal_resolution = internal_resolution_for_launcher(); @@ -22161,7 +22174,7 @@ soft_return_lobby: const PsxIrAdopted ir = psx_ir_adopt_launcher( kLauncherHasInternalResolution, ir_preset_seeded, ir_ss_seeded, ls.supersampling, ir_row_result, g_video_ref_lines, - psx_sdl_display_pixel_height(nullptr), + match_display_px_h(nullptr), ls.renderer == 1 ? GL_MAX_INTERNAL_SCALE : SW_MAX_INTERNAL_SCALE); /* Persist controller (and rematch video) choices without wiping * the rest of settings.toml — merge into the on-disk file. */