[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 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-20 15:26:45 -07:00
parent e3ea739d7c
commit 28d82a9441
5 changed files with 53 additions and 10 deletions
+8
View File
@@ -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,
+8
View File
@@ -772,6 +772,14 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
}
rt.video_resolution_reference_lines = static_cast<int>(n);
}
if (video.contains("match_display_max_lines")) {
const auto n = toml::find<int64_t>(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<int>(n);
}
if (video.contains("window_width")) {
const auto n = toml::find<int64_t>(video, "window_width");
if (n < 640 || n > 7680) {
+3
View File
@@ -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.
@@ -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;
+23 -10
View File
@@ -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<std::mutex> 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. */