From b570da1a42ff8a4d3f8ed7c3088d2727d6e62329 Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:29:47 -0700 Subject: [PATCH] Reuse desktop GL storage across dynamic-resolution steps --- docs/internal/FAITHFUL_TIMING_PLAN.md | 2 + runtime/src/gpu_gl_renderer.c | 60 +++++++++++++++++++++++++-- runtime/tests/test_gl_render_thread.c | 5 +++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/docs/internal/FAITHFUL_TIMING_PLAN.md b/docs/internal/FAITHFUL_TIMING_PLAN.md index 6bb33c3c..36c9665c 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 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. - **2026-10-21 (BRIEF-4 GPU timing):** Split TIME_ELAPSED brackets around command starvation and presentation waits. Real-context 30ms producer-idle control reports 35.141ms with the old bracket / 5.620ms active GPU work; Windows and Mac GL fixtures and render-thread unit checks pass. Completion stalls remain; dynamic-resolution reallocations are the next target. No guest or completion-clock changes. diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index abe6c7cc..1886a98f 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -10809,6 +10809,49 @@ static void dyn_stats_lock(void) { while (atomic_flag_test_and_set_explicit(&s_d static void dyn_stats_unlock(void) { atomic_flag_clear_explicit(&s_dyn_stats_lock, memory_order_release); } static int s_rthm_scale = 1; /* emulation-side level (recording) */ +/* Desktop immediate-mode GL can reuse the ceiling allocation. On Apple's + * GL-on-Metal, attachment load/store bandwidth scales with allocation size, + * so keep allocating at the active level there. The control retains the old + * allocation path for exact image comparisons on either backend. */ +static int dyn_reuse_storage(void) { + static int enabled = -1; + if (enabled < 0) { + const char *e = getenv("PSX_GL_DYNRES_REUSE_STORAGE"); +#ifdef __APPLE__ + enabled = e && e[0] == '1'; +#else + enabled = !(e && e[0] == '0'); +#endif + } + return enabled; +} +static void dyn_clear_wide_rest(GLuint fbo, const int (*cols)[2], int ncols, + int y0, int y1, int scale) { + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, fbo); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glClearColor(0, 0, 0, 0); + glEnable(GL_SCISSOR_TEST); + if (y0 > 0) { + glScissor(0, 0, g_wide_w * scale, y0 * scale); glClear(GL_COLOR_BUFFER_BIT); + } + if (y1 < VRAM_H) { + glScissor(0, y1 * scale, g_wide_w * scale, (VRAM_H - y1) * scale); + glClear(GL_COLOR_BUFFER_BIT); + } + int x = 0; + for (int c = 0; c <= ncols; ++c) { + int end = c < ncols ? cols[c][0] : g_wide_w; + if (end > x) { + glScissor(x * scale, y0 * scale, (end - x) * scale, (y1 - y0) * scale); + glClear(GL_COLOR_BUFFER_BIT); + } + if (c < ncols) x = cols[c][1]; + } + glDisable(GL_SCISSOR_TEST); + glStencilMask(0xFF); glClearStencil(0); glClear(GL_STENCIL_BUFFER_BIT); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); +} + static int dyn_apply(int snew) { /* Never in the windowed high-resolution mode (s_hiw): dyn_eligible keeps * s_alloc_scale 0 there; refuse here too rather than step its tiles. */ @@ -10904,7 +10947,14 @@ static int dyn_apply(int snew) { * step down reseeds the old surface's corner in place; a step up past * the old allocation is refused. */ GLuint ntex = 0, nrb = 0, nfbo = 0; - if (!hr_alloc_surface(snew, &ntex, &nrb, &nfbo)) { + if (dyn_reuse_storage() && snew <= s_hr_alloc) { + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); + glDisable(GL_SCISSOR_TEST); + glDepthMask(GL_TRUE); glClearDepth(1.0); + glStencilMask(0xFF); glClearStencil(0); + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + } else if (!hr_alloc_surface(snew, &ntex, &nrb, &nfbo)) { if (snew > s_hr_alloc) { s_dyn_stats.refused++; fprintf(stdout, "psxrecomp: dynamic resolution %dx -> %dx refused (the " @@ -10960,7 +11010,11 @@ static int dyn_apply(int snew) { * before and the surface keeps its larger allocation; a step up * past it drops the surface (recreated at the next target). */ GLuint ntex = 0, nrb = 0, nfbo = 0; - if (wide_alloc_surface(snew, &ntex, &nrb, &nfbo)) { + if (dyn_reuse_storage() && snew <= s_wide_as[i]) { + dyn_rescale_in_place(s_wide_fbo[i], g_wide_w, (const int (*)[2])cols, nc, + y0, y1, sold, snew); + dyn_clear_wide_rest(s_wide_fbo[i], (const int (*)[2])cols, nc, y0, y1, snew); + } else if (wide_alloc_surface(snew, &ntex, &nrb, &nfbo)) { for (int c = 0; c < nc; c++) if (cols[c][1] > cols[c][0]) dyn_rescale_rect(s_wide_tex[i], cols[c][0] * sold, y0 * sold, sold, @@ -10993,7 +11047,7 @@ static int dyn_apply(int snew) { int hw = VRAM_W * snew, hh = VRAM_H * snew; int sw = snew <= 2 ? hw : (hw < GL_SCRATCH_TILE ? hw : GL_SCRATCH_TILE); int sh = snew <= 2 ? hh : (hh < GL_SCRATCH_TILE ? hh : GL_SCRATCH_TILE); - if (s_scratch_w > sw || s_scratch_h > sh) { + if (!dyn_reuse_storage() && (s_scratch_w > sw || s_scratch_h > sh)) { p_glActiveTexture(PSXGL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, s_scratch_tex); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, sw, sh, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); diff --git a/runtime/tests/test_gl_render_thread.c b/runtime/tests/test_gl_render_thread.c index 5b3ba939..7cba4dd3 100644 --- a/runtime/tests/test_gl_render_thread.c +++ b/runtime/tests/test_gl_render_thread.c @@ -341,6 +341,11 @@ int main(int argc, char **argv) { 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"); + printf("dyn_allocs hr=%llu wide=%llu\n", (unsigned long long)ds.hr_reallocs, + (unsigned long long)ds.wide_reallocs); + if (dyn_reuse_storage()) + check(ds.hr_reallocs == 0 && ds.wide_reallocs == 0, + "ceiling storage reused at every level"); if (threaded) { GlRthCosts co; gl_renderer_render_thread_costs(&co);