From 3e1949acc1f8a503e2145e07d547136ef014a863 Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Tue, 6 Oct 2026 02:44:54 -0700 Subject: [PATCH] Dynamic resolution on the render thread: replay recorded steps in every build; GL equivalence test steps the level The step record's replay sat inside the debug-tools block, so release builds dropped every recorded step. gl_render_thread_test gains a dynres mode: the same level steps between frames with the thread off (applied at once) and on (recorded), identical guest readbacks, VRAM, frame and wide surface, and the render thread's frame cost measured. Also drop the duplicate gpu_ws_background_requires_full_composite stub the #508 merge left in the scale-invariance fixture. Co-Authored-By: Claude Opus 5.5 --- runtime/src/gpu_gl_renderer.c | 2 +- runtime/tests/run_gl_render_thread.py | 18 ++++++++++ runtime/tests/test_gl_render_thread.c | 48 ++++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index c7f4c34f..bc6d6559 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -9795,13 +9795,13 @@ static void gl_rth_exec(void *user, const RtCmd *c, const void *payload) { break; #ifndef PSX_NO_DEBUG_TOOLS case RTH_RING_CAPTURE: rth_ring_capture_now(v); break; +#endif case RTH_DYN_STEP: s_dyn_rth_disp_valid = 1; memcpy(s_dyn_rth_disp, v + 1, sizeof s_dyn_rth_disp); (void)dyn_apply(v[0]); s_dyn_rth_disp_valid = 0; break; -#endif case RTH_PEEK: { uint64_t ptr = (uint64_t)(uint32_t)v[4] | ((uint64_t)(uint32_t)v[5] << 32); (void)gl_renderer_fbo_peek(v[0], v[1], v[2], v[3], (uint16_t *)(uintptr_t)ptr); diff --git a/runtime/tests/run_gl_render_thread.py b/runtime/tests/run_gl_render_thread.py index e78b39fd..e338ba7f 100644 --- a/runtime/tests/run_gl_render_thread.py +++ b/runtime/tests/run_gl_render_thread.py @@ -140,6 +140,24 @@ def main(): print(f"FAIL scale {s}: {k} differs with the render thread on " f"({runs[0][k]} off, {runs[1][k]} on)") ok = False + # Dynamic resolution: the same level steps between frames, recorded with + # the thread on (scales above 1 only; 1x has no levels below it). + for s in [int(v) for v in args.scales.split(",") if v and int(v) > 1]: + runs = {} + for threaded in (0, 1): + r = run([probe, s, threaded, args.frames, "dynres"]) + p = parse(r.stdout) + tail = r.stdout.strip().splitlines()[-7:] + print(f"scale {s} dynres render_thread={threaded}: exit={r.returncode}", tail, + r.stderr.strip()[-800:]) + if r.returncode or p["failures"] != 0 or any(p[k] is None for k in KEYS): + ok = False + runs[threaded] = p + for k in KEYS: + if runs[0][k] != runs[1][k]: + print(f"FAIL scale {s} dynres: {k} differs with the render thread on " + f"({runs[0][k]} off, {runs[1][k]} on)") + ok = False print("PASS" if ok else "FAIL") return 0 if ok else 1 diff --git a/runtime/tests/test_gl_render_thread.c b/runtime/tests/test_gl_render_thread.c index 11e79573..d7cd46f1 100644 --- a/runtime/tests/test_gl_render_thread.c +++ b/runtime/tests/test_gl_render_thread.c @@ -18,6 +18,13 @@ * stream latch that flips mid-frame (sent as state); * - presents and frame boundaries every frame, with readbacks (GPUREAD * style gr_vram_read, VRAM->CPU transfer_out) in some frames only. + * With argv[4] == "dynres" ([video] dynamic_resolution) the surfaces are + * allocated at the scale and the level steps between frames + * (gl_renderer_step_internal_scale_now after a frame boundary, as the + * controller does): recorded in stream order with the thread on, run at once + * with it off, or at once when the frame is held at a sync point; the + * render thread's frame cost is measured too. Back at the ceiling before the + * final readbacks. * Printed: rb (digest of every value the "guest" read back), digest (native * VRAM at the end), hires (frame at internal resolution), wide (native-wide * surface), and with the thread on its record/acquire counters. @@ -220,6 +227,7 @@ int main(int argc, char **argv) { int scale = argc > 1 ? atoi(argv[1]) : 1; int threaded = argc > 2 && argv[2][0] == '1'; int frames = argc > 3 ? atoi(argv[3]) : 80; + int dynres = argc > 4 && !strcmp(argv[4], "dynres"); /* No video device, window or GL context (headless, Windows over SSH): * exit 77, which the harness reports as a CTest skip. */ if (SDL_Init(SDL_INIT_VIDEO) != 0) { @@ -250,6 +258,7 @@ int main(int argc, char **argv) { gr_set_backend(GR_BACKEND_OPENGL); gr_init(vram); gr_set_scale(scale); + if (dynres) gl_renderer_set_dynamic_resolution(1); gl_renderer_set_swap_interval(0); if (!gl_renderer_init_context(win)) { fprintf(stderr, "FAIL context\n"); return 2; } check(gr_backend() == GR_BACKEND_OPENGL, "OpenGL backend"); @@ -259,7 +268,44 @@ int main(int argc, char **argv) { check(gl_renderer_render_thread_start(2) == 1, "render thread started"); check(gl_renderer_render_thread_active(), "render thread active"); } - for (int f = 0; f < frames; f++) frame(f, (f & 1) ? 512 : 0); + if (dynres) { + check(gl_renderer_dynamic_resolution_ceiling() == scale, "dynres ceiling"); + if (threaded) gl_renderer_render_thread_measure(1); + } + static const int levels[] = { 2, 1, 3, 1, 4, 2 }; + int nsteps = 0, level = scale; + for (int f = 0; f < frames; f++) { + frame(f, (f & 1) ? 512 : 0); + if (dynres && f % 7 == 6) { /* includes frames held at a sync point */ + int l = levels[(f / 7) % 6]; + if (l > scale) l = scale; + if (l != level) nsteps++; + level = l; + check(gl_renderer_step_internal_scale_now(l) == 1, "dynres step"); + GlDynresStats ds; + gl_renderer_dynres_stats(&ds); + check(ds.level == l, "dynres level as asked"); + check(gr_scale() == l, "gr_scale follows the level"); + } + } + if (dynres) { + if (level != scale) nsteps++; + check(gl_renderer_step_internal_scale_now(scale) == 1, "dynres back to the ceiling"); + gl_renderer_render_thread_sync("test"); + GlDynresStats ds; + gl_renderer_dynres_stats(&ds); + printf("dyn_steps=%llu asked=%d\n", (unsigned long long)ds.steps, nsteps); + check(ds.steps == (uint64_t)nsteps && ds.level == scale, "every step applied"); + if (threaded) { + GlRthCosts co; + gl_renderer_render_thread_costs(&co); + printf("rth_cost_frames=%llu gpu_frames=%llu dropped=%llu mean_ms=%.3f\n", + (unsigned long long)co.frames, (unsigned long long)co.gpu_frames, + (unsigned long long)co.dropped, + co.frames ? (double)co.cost_ns / (double)co.frames * 1e-6 : 0.0); + check(co.frames > 0, "render-thread frame costs measured"); + } + } /* Guest-visible VRAM, the frame at internal resolution, the wide surface. */ static uint16_t native[1024 * 512];