diff --git a/runtime/include/render_thread.h b/runtime/include/render_thread.h index c40c4641..681c5ca8 100644 --- a/runtime/include/render_thread.h +++ b/runtime/include/render_thread.h @@ -12,8 +12,9 @@ * ring end; payloads are copied, so the producer may reuse its buffers. * - rt_frame_end() closes a guest frame. At most max_frames closed frames may * wait in the ring; past that the emulation thread blocks (backpressure). - * While replaying, rt_newer_frame_queued() tells the backend a later frame - * is already recorded, so it may skip presenting a stale one. + * While replaying, rt_frames_ahead() counts the complete frames already + * recorded after the one being replayed, so the backend can skip + * presenting a frame it is far behind on. * - rt_acquire() is the one sync point: it waits until every record has been * executed, has the render thread release the context, and makes it * current on the calling (emulation) thread. The emulation thread then @@ -22,7 +23,7 @@ * thread at all. * - No lock is held while exec() runs. Locks only guard sleeping/waking. * - * Every function except the rt_on_render_thread() / rt_newer_frame_queued() + * Every function except the rt_on_render_thread() / rt_frames_ahead() * queries is emulation-thread only. */ #ifndef PSX_RENDER_THREAD_H #define PSX_RENDER_THREAD_H @@ -81,9 +82,9 @@ void rt_cmd_commit(void); * max_frames closed frames are unconsumed. */ void rt_frame_end(void); -/* Render-thread query while executing: a frame marker newer than the record - * being executed is already queued (the frame being replayed is stale). */ -int rt_newer_frame_queued(void); +/* Render-thread query while executing: how many complete frames are queued + * after the frame whose record is being executed (0 = it is the newest). */ +int rt_frames_ahead(void); /* Sync point. Drain, then move the context to the calling thread (held). * reason is a static string kept in the acquire ring. No-op when already held diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 877efc01..802f0fd8 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -9281,9 +9281,12 @@ static int rth_record_present(uint16_t op, int n, const int32_t *v) { static void rth_replay_present(const RtCmd *c, const uint8_t *p) { RthOvHdr hd; memcpy(&hd, p, sizeof hd); - if (rt_newer_frame_queued()) { - /* Behind: a later frame is already recorded. Drawing this one only - * delays it; the guest never waits for a present. */ + if (rt_frames_ahead() >= 2) { + /* Two later frames are already recorded, so the emulation thread is + * at (or near) the in-flight bound waiting on us. Showing this frame + * would only delay those; skip its present (its drawing has been + * replayed and stays). One frame behind is the normal pipelined + * state and still presents every frame. */ s_rth_presents_stale++; return; } diff --git a/runtime/src/render_thread.c b/runtime/src/render_thread.c index 33aa71e5..bdb86e5a 100644 --- a/runtime/src/render_thread.c +++ b/runtime/src/render_thread.c @@ -323,8 +323,10 @@ void rt_frame_end(void) { } } -int rt_newer_frame_queued(void) { - return atomic_load(&R.frames_produced) >= atomic_load(&R.frames_consumed) + 2; +int rt_frames_ahead(void) { + int64_t d = (int64_t)(atomic_load(&R.frames_produced) - + atomic_load(&R.frames_consumed)) - 1; + return d < 0 ? 0 : (int)d; } void rt_drain(void) { diff --git a/runtime/tests/test_render_thread.c b/runtime/tests/test_render_thread.c index a906e482..2353be2b 100644 --- a/runtime/tests/test_render_thread.c +++ b/runtime/tests/test_render_thread.c @@ -41,7 +41,9 @@ static _Atomic int exec_errors; static _Atomic int exec_off_thread; static _Atomic int exec_without_ctx; static _Atomic int stale_seen; +static _Atomic int far_behind_seen; /* two complete frames queued after this one */ static int exec_delay_us; +static int g_max_frames = 1; static void sleep_us(int us) { struct timespec ts = { us / 1000000, (long)(us % 1000000) * 1000 }; @@ -56,7 +58,9 @@ static void fake_exec(void *user, const RtCmd *c, const void *payload) { if (!rt_on_render_thread()) atomic_fetch_add(&exec_off_thread, 1); if (atomic_load(&ctx_owner) != 2) atomic_fetch_add(&exec_without_ctx, 1); if (c->op == OP_PRESENT) { - if (rt_newer_frame_queued()) atomic_fetch_add(&stale_seen, 1); + if (rt_frames_ahead() >= 1) atomic_fetch_add(&stale_seen, 1); + if (rt_frames_ahead() >= 2) atomic_fetch_add(&far_behind_seen, 1); + if (rt_frames_ahead() > g_max_frames) atomic_fetch_add(&exec_errors, 1); /* the bound */ } else if (c->op == OP_DATA) { const uint64_t *p = (const uint64_t *)payload; if (c->payload < 8 || p[0] != exec_next) atomic_fetch_add(&exec_errors, 1); @@ -89,12 +93,14 @@ static void reset_state(void) { atomic_store(&exec_off_thread, 0); atomic_store(&exec_without_ctx, 0); atomic_store(&stale_seen, 0); + atomic_store(&far_behind_seen, 0); atomic_store(&ctx_violations, 0); atomic_store(&ctx_owner, 1); /* the "context" starts current here */ exec_delay_us = 0; } static int start(size_t ring, int frames) { + g_max_frames = frames; RtConfig cfg = { ring, frames, fake_exec, fake_ctx, NULL }; return rt_start(&cfg); } @@ -157,7 +163,8 @@ static void test_backpressure(void) { RtStats st; rt_get_stats(&st); CHECK(worst <= 2, "in-flight bound held (worst %llu)", (unsigned long long)worst); CHECK(st.backpressure_waits > 0, "producer was held back"); - CHECK(atomic_load(&stale_seen) > 0, "stale presents detected while behind"); + CHECK(atomic_load(&stale_seen) > 0, "presents one frame behind seen"); + CHECK(atomic_load(&far_behind_seen) > 0, "presents two frames behind seen (skip case)"); CHECK(atomic_load(&exec_errors) == 0, "order kept under backpressure"); rt_stop();