diff --git a/docs/config_schema.md b/docs/config_schema.md index a157b522..b2989de5 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -708,7 +708,9 @@ match_display_max_lines = 0 # game.toml only: cap on the display height `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. diff --git a/docs/internal/FAITHFUL_TIMING_PLAN.md b/docs/internal/FAITHFUL_TIMING_PLAN.md index 36c9665c..bb22382d 100644 --- a/docs/internal/FAITHFUL_TIMING_PLAN.md +++ b/docs/internal/FAITHFUL_TIMING_PLAN.md @@ -213,6 +213,8 @@ on a fixed region -> next. ## 5. Status / Log (update every session) +- **2026-10-21 (BRIEF-4 cap edges):** Apply the display ceiling after supersampling and resolve the physical-monitor supersample policy after creating the window, including fixed presets. Saved large factors cannot exceed the Match display ceiling; legacy unset scales stay intact. Resolution tests pass on Windows/Mac; all 21 R4 production-build CTests pass. Final regeneration and receipt verification pending. + - **2026-10-21 (BRIEF-4 storage reuse):** Desktop GL retains ceiling storage and rescales in place, preserving cleared depth/stencil and unwritten wide regions. Apple keeps level-sized allocations by default. Windows old/new output controls and forced-reuse Mac fixtures pass. With corrected presenter accounting, R4 60s fullscreen quiet/loaded runs have zero completion gaps >25ms, p99 busy 44.6/48.8%, 17x..18x scales. R4 final pin/default checks remain in progress. - **2026-10-21 (BRIEF-4 presenter accounting):** Publish composer swap/slot wait separately and exclude that pacing from dynamic-resolution work backpressure. Windows and Mac real-context checks pass. Quiet fullscreen route: 60.020 Hz, four completion gaps >25ms, p99 busy 48.0%; allocation stalls remain. Raw completion and queue-wait metrics are unchanged. diff --git a/runtime/include/internal_resolution.h b/runtime/include/internal_resolution.h index ebc435e9..c37dc32a 100644 --- a/runtime/include/internal_resolution.h +++ b/runtime/include/internal_resolution.h @@ -167,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 c3ef819c..86a9b9d4 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1466,9 +1466,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(), - display_supersample(nullptr)); + 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 @@ -20482,7 +20481,7 @@ session_reboot: else std::fprintf(stdout, "psxrecomp: PSX_AA_MODE=%s not understood (off, fxaa, fxaa_hq)\n", e); } const int effective_ss = display_supersample(nullptr); - if (effective_ss != 1000 || effective_ss != g_video_ss_milli) + if (!g_video_ss_disable_at_lines && effective_ss != 1000) std::fprintf(stdout, "psxrecomp: supersample %.3gx on the internal-resolution target\n", effective_ss / 1000.0); apply_internal_resolution(match_display_px_h(nullptr)); @@ -21128,17 +21127,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, - display_supersample(sdl_window)); + 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 2fbbace7..597c271c 100644 --- a/runtime/tests/test_internal_resolution.cpp +++ b/runtime/tests/test_internal_resolution.cpp @@ -227,6 +227,16 @@ int main() { 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;