mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
479cbb789c
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>
246 lines
9.6 KiB
C
246 lines
9.6 KiB
C
/* Render thread core (render_thread.c): ordering, payload integrity across
|
|
* ring wraps, the frame bound (backpressure), stale-frame detection, the
|
|
* sync point (drain + context hand-off) and shutdown. The "context" here is a
|
|
* token that may be current on one thread at a time; ctx() checks that. */
|
|
#include "render_thread.h"
|
|
|
|
#include <pthread.h>
|
|
#include <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
static int checks, failures;
|
|
#define CHECK(c, ...) do { checks++; if (!(c)) { failures++; \
|
|
fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
|
|
fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } } while (0)
|
|
|
|
/* ---- context token ------------------------------------------------------- */
|
|
static _Atomic int ctx_owner; /* 0 none, 1 emu, 2 render */
|
|
static _Atomic int ctx_violations;
|
|
static __thread int t_is_render; /* set by exec on first call */
|
|
static void fake_ctx(void *user, int current) {
|
|
(void)user;
|
|
int me = rt_on_render_thread() ? 2 : 1;
|
|
if (current) {
|
|
int expect = 0;
|
|
if (!atomic_compare_exchange_strong(&ctx_owner, &expect, me))
|
|
atomic_fetch_add(&ctx_violations, 1);
|
|
} else {
|
|
int expect = me;
|
|
if (!atomic_compare_exchange_strong(&ctx_owner, &expect, 0))
|
|
atomic_fetch_add(&ctx_violations, 1);
|
|
}
|
|
}
|
|
|
|
/* ---- executor ------------------------------------------------------------ */
|
|
static uint64_t exec_next; /* next expected sequence id */
|
|
static _Atomic uint64_t exec_count;
|
|
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 };
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
|
|
enum { OP_DATA = 1, OP_PRESENT = 2 };
|
|
|
|
static void fake_exec(void *user, const RtCmd *c, const void *payload) {
|
|
(void)user;
|
|
t_is_render = 1;
|
|
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_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);
|
|
const uint8_t *b = (const uint8_t *)payload;
|
|
for (uint32_t i = 8; i < c->payload; i++)
|
|
if (b[i] != (uint8_t)(p[0] * 31u + i)) { atomic_fetch_add(&exec_errors, 1); break; }
|
|
exec_next++;
|
|
}
|
|
if (exec_delay_us) sleep_us(exec_delay_us);
|
|
atomic_fetch_add(&exec_count, 1);
|
|
}
|
|
|
|
static uint64_t emit_seq;
|
|
static int emit(uint32_t bytes) {
|
|
if (bytes < 8) bytes = 8;
|
|
uint8_t *p = (uint8_t *)rt_cmd_begin(OP_DATA, 0, bytes);
|
|
if (!p) return 0;
|
|
if (((uintptr_t)p & (RT_ALIGN - 1)) != 0) { failures++; fprintf(stderr, "FAIL unaligned payload\n"); }
|
|
memcpy(p, &emit_seq, 8);
|
|
for (uint32_t i = 8; i < bytes; i++) p[i] = (uint8_t)(emit_seq * 31u + i);
|
|
rt_cmd_commit();
|
|
emit_seq++;
|
|
return 1;
|
|
}
|
|
|
|
static void reset_state(void) {
|
|
exec_next = 0; emit_seq = 0;
|
|
atomic_store(&exec_count, 0);
|
|
atomic_store(&exec_errors, 0);
|
|
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);
|
|
}
|
|
|
|
static uint32_t rng = 12345u;
|
|
static uint32_t rnd(void) { rng = rng * 1664525u + 1013904223u; return rng >> 8; }
|
|
|
|
/* 1. Ordering and payload integrity across many wraps of a small ring. */
|
|
static void test_order_and_wrap(void) {
|
|
reset_state();
|
|
CHECK(start(1u << 20, 3), "start");
|
|
CHECK(atomic_load(&ctx_owner) != 1, "context left the starting thread");
|
|
for (int f = 0; f < 400; f++) {
|
|
int n = 1 + (int)(rnd() % 40);
|
|
for (int i = 0; i < n; i++) CHECK(emit(8 + rnd() % 20000), "emit");
|
|
rt_frame_end();
|
|
}
|
|
rt_drain();
|
|
CHECK(atomic_load(&exec_count) == emit_seq, "all records executed (%llu/%llu)",
|
|
(unsigned long long)atomic_load(&exec_count), (unsigned long long)emit_seq);
|
|
CHECK(atomic_load(&exec_errors) == 0, "order/payload errors %d", atomic_load(&exec_errors));
|
|
CHECK(atomic_load(&exec_off_thread) == 0, "exec on render thread only");
|
|
CHECK(atomic_load(&exec_without_ctx) == 0, "exec with context current");
|
|
RtStats st; rt_get_stats(&st);
|
|
CHECK(st.frames_produced == 400 && st.frames_consumed == 400, "frame counters");
|
|
CHECK(st.ring_high_water <= (1u << 20), "high water within ring");
|
|
rt_stop();
|
|
CHECK(atomic_load(&ctx_owner) == 1, "stop returns the context to the caller");
|
|
CHECK(atomic_load(&ctx_violations) == 0, "context never current on two threads");
|
|
}
|
|
|
|
/* 2. Oversize records are refused (caller runs them directly). */
|
|
static void test_oversize(void) {
|
|
reset_state();
|
|
CHECK(start(1u << 20, 2), "start");
|
|
CHECK(rt_cmd_begin(OP_DATA, 0, (1u << 19)) == NULL, "half-ring record refused");
|
|
CHECK(emit((1u << 19) - 64), "just under half fits");
|
|
rt_drain();
|
|
RtStats st; rt_get_stats(&st);
|
|
CHECK(st.oversize == 1, "oversize counted");
|
|
rt_stop();
|
|
}
|
|
|
|
/* 3. Backpressure: with a slow render thread the producer never runs more
|
|
* than max_frames closed frames ahead, and stale frames are flagged. */
|
|
static void test_backpressure(void) {
|
|
reset_state();
|
|
exec_delay_us = 2000; /* 2 ms per record, 2 records per frame */
|
|
CHECK(start(1u << 22, 2), "start");
|
|
uint64_t worst = 0;
|
|
for (int f = 0; f < 40; f++) {
|
|
emit(64);
|
|
uint8_t *p = (uint8_t *)rt_cmd_begin(OP_PRESENT, 0, 0); (void)p; rt_cmd_commit();
|
|
rt_frame_end();
|
|
RtStats st; rt_get_stats(&st);
|
|
uint64_t d = st.frames_produced - st.frames_consumed;
|
|
if (d > worst) worst = d;
|
|
}
|
|
rt_drain();
|
|
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, "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();
|
|
|
|
/* A fast render thread never sees a stale frame for a 1-deep queue. */
|
|
reset_state();
|
|
CHECK(start(1u << 20, 1), "start");
|
|
for (int f = 0; f < 50; f++) {
|
|
emit(64);
|
|
rt_cmd_begin(OP_PRESENT, 0, 0); rt_cmd_commit();
|
|
rt_frame_end();
|
|
rt_drain();
|
|
}
|
|
CHECK(atomic_load(&stale_seen) == 0, "no stale frames when drained each frame");
|
|
rt_stop();
|
|
}
|
|
|
|
/* 4. Sync point: acquire drains everything first and moves the context;
|
|
* release hands it back; interleaving with records and frames stays ordered
|
|
* and never puts the context on two threads. */
|
|
static void test_acquire_release(void) {
|
|
reset_state();
|
|
exec_delay_us = 50;
|
|
CHECK(start(1u << 20, 3), "start");
|
|
uint64_t expected_acquires = 0;
|
|
for (int round = 0; round < 300; round++) {
|
|
if (rt_held()) {
|
|
/* The backend never records while it holds the context. */
|
|
if (rnd() % 2) rt_release(); else { rt_frame_end(); continue; }
|
|
}
|
|
int n = (int)(rnd() % 8);
|
|
for (int i = 0; i < n; i++) emit(8 + rnd() % 4000);
|
|
if (rnd() % 3 == 0) rt_frame_end();
|
|
uint64_t before = emit_seq;
|
|
rt_acquire("test");
|
|
expected_acquires++;
|
|
CHECK(atomic_load(&exec_count) >= before, "acquire drained");
|
|
CHECK(atomic_load(&ctx_owner) == 1, "context current on the caller while held");
|
|
CHECK(rt_held(), "held");
|
|
rt_frame_end(); /* held: counted, not queued */
|
|
}
|
|
rt_release();
|
|
rt_drain();
|
|
RtStats st; rt_get_stats(&st);
|
|
CHECK(st.acquires == expected_acquires, "acquire count %llu", (unsigned long long)st.acquires);
|
|
CHECK(atomic_load(&ctx_violations) == 0, "context exclusive (%d)", atomic_load(&ctx_violations));
|
|
CHECK(atomic_load(&exec_without_ctx) == 0, "exec always with context");
|
|
RtAcquireEvent ev[4];
|
|
CHECK(rt_acquire_events(ev, 4) == 4 && !strcmp(ev[0].reason, "test"), "acquire ring");
|
|
rt_stop();
|
|
CHECK(atomic_load(&ctx_owner) == 1, "context back on caller after stop");
|
|
}
|
|
|
|
/* 5. Stop while held: the caller keeps the context; nothing is lost. */
|
|
static void test_stop_while_held(void) {
|
|
reset_state();
|
|
CHECK(start(1u << 20, 3), "start");
|
|
for (int i = 0; i < 100; i++) emit(100);
|
|
rt_acquire("stop");
|
|
rt_stop();
|
|
CHECK(atomic_load(&exec_count) == 100, "drained before stop");
|
|
CHECK(atomic_load(&ctx_owner) == 1, "caller owns the context");
|
|
CHECK(atomic_load(&ctx_violations) == 0, "no violations");
|
|
CHECK(!rt_running(), "stopped");
|
|
/* Restart works. */
|
|
CHECK(start(1u << 20, 3), "restart");
|
|
emit(16);
|
|
rt_stop();
|
|
}
|
|
|
|
int main(void) {
|
|
test_order_and_wrap();
|
|
test_oversize();
|
|
test_backpressure();
|
|
test_acquire_release();
|
|
test_stop_while_held();
|
|
printf("checks=%d failures=%d\n", checks, failures);
|
|
return failures ? 1 : 0;
|
|
}
|