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 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-06 02:44:54 -07:00
parent a58661f999
commit 3e1949acc1
3 changed files with 66 additions and 2 deletions
+1 -1
View File
@@ -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);
+18
View File
@@ -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
+47 -1
View File
@@ -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];