diff --git a/docs/internal/FAITHFUL_TIMING_PLAN.md b/docs/internal/FAITHFUL_TIMING_PLAN.md index ecc681ca..6bb33c3c 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 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. - **2026-10-21 (BRIEF-4 depth state):** Removed per-batch GL state round trips, including the mask-check exception absent in the old experiment. Real-context state restoration and image/readback controls pass on Windows and Mac. Fullscreen quiet route remains unaccepted (8 completion gaps, 53.2% p99 busy). Investigating GPU query spans that include host pacing and provoke expensive dynamic-resolution reallocations. diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index 4a27bdb2..4b930a23 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -457,6 +457,7 @@ void gl_renderer_host_ledger(GlHostLedger *out); * gl_renderer_render_thread_measure(1) (dynamic resolution on). */ typedef struct GlRthCosts { uint64_t frames, cost_ns, cpu_ns, gpu_ns, gpu_frames, dropped; + uint64_t present_wait_ns; /* composer blocked on swap / a free present slot */ } GlRthCosts; void gl_renderer_render_thread_measure(int on); void gl_renderer_render_thread_costs(GlRthCosts *out); diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 35738590..abe6c7cc 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -485,6 +485,7 @@ static int s_hd_authority_pending = 0; static int rth_record_mode(void); /* Render-thread frame cost (dynamic resolution; defined with GL_RT_BACKEND). */ +static _Atomic uint64_t s_rth_present_wait_ns; static uint64_t s_rthf_swap_ns = 0; /* render thread: time in the swap */ static uint64_t host_now_ns_rthf(void); /* The SW rasterizer and the HD texture residency read the same native VRAM @@ -10059,6 +10060,7 @@ static void gl_swap_now(int generated) { if (rthf_t0) { const uint64_t sw = host_now_ns_rthf() - rthf_t0; s_rthf_swap_ns += sw; + atomic_fetch_add_explicit(&s_rth_present_wait_ns, sw, memory_order_relaxed); fg_note_swap(sw); } if (s_dyn_on) { @@ -11552,6 +11554,7 @@ void gl_renderer_render_thread_costs(GlRthCosts *out) { out->gpu_ns = atomic_load(&s_rthf_gpu_ns); out->gpu_frames = atomic_load(&s_rthf_gpu_frames); out->dropped = atomic_load(&s_rthf_dropped); + out->present_wait_ns = atomic_load_explicit(&s_rth_present_wait_ns, memory_order_relaxed); } /* ---- frame generation ([video] frame_generation) --------------------------- diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index dcc7153b..c3ef819c 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -10627,7 +10627,20 @@ static void dynres_tick_rt(double now_s, double wall, double period, int held, const int frames = (int)(co.frames - c0.frames); const double cost = (double)(co.cost_ns - c0.cost_ns) * 1e-9; const uint64_t bp_ns = rs.backpressure_ns + rs.ring_full_ns; - const double bp = (double)(bp_ns - g_dynres.last_bp_ns) * 1e-9; + uint64_t work_bp_ns = bp_ns - g_dynres.last_bp_ns; + /* A bounded presenter intentionally holds the renderer at display rate. + * That wait propagates to the guest queue even with ample render headroom; + * it is not evidence that lowering resolution would help. GPU/CPU work + * remains measured separately and still drives overload decisions. */ + static const bool account_present = [] { + const char *e = std::getenv("PSX_DYNRES_PRESENT_WAIT_ACCOUNTING"); + return !(e && e[0] == '0'); + }(); + if (account_present) { + const uint64_t present_ns = co.present_wait_ns - c0.present_wait_ns; + work_bp_ns = work_bp_ns > present_ns ? work_bp_ns - present_ns : 0; + } + const double bp = (double)work_bp_ns * 1e-9; g_dynres.rt_acc_cpu += (double)(co.cpu_ns - c0.cpu_ns) * 1e-6; g_dynres.rt_acc_gpu += (double)(co.gpu_ns - c0.gpu_ns) * 1e-6; g_dynres.rt_acc_frames += co.frames - c0.frames; diff --git a/runtime/tests/test_gl_render_thread.c b/runtime/tests/test_gl_render_thread.c index 134a6ae3..5b3ba939 100644 --- a/runtime/tests/test_gl_render_thread.c +++ b/runtime/tests/test_gl_render_thread.c @@ -359,6 +359,7 @@ int main(int argc, char **argv) { double gpu_ms = co.gpu_frames ? (double)co.gpu_ns / co.gpu_frames * 1e-6 : 0.0; printf("idle_gpu_ms=%.3f gpu_frames=%llu\n", gpu_ms, (unsigned long long)co.gpu_frames); check(co.gpu_frames > 0, "idle timing has completed GPU samples"); + check(co.present_wait_ns > 0, "composer wait published independently of render cost"); if (rthf_active_segments()) check(gpu_ms < 15.0, "GPU work excludes the 30ms producer wait"); }