Render thread: skip a present only when two newer frames wait

One frame behind is the normal pipelined state when the render thread is
the slower side; skipping on it dropped nearly every present at 4K
widescreen. rt_frames_ahead() counts the complete frames queued after
the one being replayed; the backend skips only at two, where the
emulation thread is at the in-flight bound waiting on the render thread.
The unit test checks the count never exceeds the bound and that both
cases occur under a slow consumer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-06 00:47:32 -07:00
parent 831f5fa03e
commit 9abcff08a4
4 changed files with 26 additions and 13 deletions
+7 -6
View File
@@ -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
+6 -3
View File
@@ -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;
}
+4 -2
View File
@@ -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) {
+9 -2
View File
@@ -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();