Files
psxrecomp/runtime/src/present_thread.c
T
Shokunin f10db8a0f0 Present thread: swap off the render thread ([video] present_thread)
Opt-in under the render thread. Composed frames go to a ring of offscreen
slots at the window's size (binds of framebuffer 0 on the composing context
land on the current slot); a present thread with a second, shared context on
the same window waits on the slot's fence, copies it 1:1 and swaps. The
compositor's wait moves off rendering; the composing side waits only while
every slot is queued or on screen. Falls back to direct swaps when the shared
context or sync objects are unavailable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:56:48 -07:00

239 lines
7.2 KiB
C

/* present_thread.c — present thread core. See present_thread.h and
* docs/RENDER_THREAD.md ("Present thread").
*
* Presents run at display rate (tens to a few hundred per second), so one
* mutex guards all slot state; nothing is held while present() runs. */
#include "present_thread.h"
#include <string.h>
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
typedef SRWLOCK pt_mutex;
typedef CONDITION_VARIABLE pt_cond;
static void mtx_init(pt_mutex *m) { InitializeSRWLock(m); }
static void mtx_lock(pt_mutex *m) { AcquireSRWLockExclusive(m); }
static void mtx_unlock(pt_mutex *m) { ReleaseSRWLockExclusive(m); }
static void cnd_init(pt_cond *c) { InitializeConditionVariable(c); }
static void cnd_wait(pt_cond *c, pt_mutex *m) { SleepConditionVariableSRW(c, m, INFINITE, 0); }
static void cnd_bcast(pt_cond *c) { WakeAllConditionVariable(c); }
static uint64_t now_ns(void) {
LARGE_INTEGER f, t;
QueryPerformanceFrequency(&f);
QueryPerformanceCounter(&t);
return (uint64_t)((double)t.QuadPart * 1e9 / (double)f.QuadPart);
}
typedef HANDLE pt_thread;
#else
# include <pthread.h>
# if defined(__APPLE__)
# include <pthread/qos.h>
# endif
# include <time.h>
typedef pthread_mutex_t pt_mutex;
typedef pthread_cond_t pt_cond;
static void mtx_init(pt_mutex *m) { pthread_mutex_init(m, NULL); }
static void mtx_lock(pt_mutex *m) { pthread_mutex_lock(m); }
static void mtx_unlock(pt_mutex *m) { pthread_mutex_unlock(m); }
static void cnd_init(pt_cond *c) { pthread_cond_init(c, NULL); }
static void cnd_wait(pt_cond *c, pt_mutex *m) { pthread_cond_wait(c, m); }
static void cnd_bcast(pt_cond *c) { pthread_cond_broadcast(c); }
static uint64_t now_ns(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
}
typedef pthread_t pt_thread;
#endif
enum { S_FREE = 0, S_COMPOSING, S_QUEUED, S_PRESENTING };
static struct {
PtConfig cfg;
pt_mutex mtx;
pt_cond cv; /* any state change; both sides wait on it */
pt_thread thread;
int running, stop, init_done, init_ok;
int state[PT_MAX_SLOTS];
void *ready[PT_MAX_SLOTS];
void *done[PT_MAX_SLOTS];
int queue[PT_MAX_SLOTS];
int q_head, q_len;
int current;
PtStats st;
} P;
static int s_pt_inited = 0;
static void *present_loop(void) {
int ok = P.cfg.ctx(P.cfg.user, 1);
mtx_lock(&P.mtx);
P.init_done = 1;
P.init_ok = ok;
cnd_bcast(&P.cv);
if (!ok) { mtx_unlock(&P.mtx); return NULL; }
for (;;) {
while (!P.q_len && !P.stop) cnd_wait(&P.cv, &P.mtx);
if (!P.q_len) break; /* stop with nothing queued */
const int slot = P.queue[P.q_head];
P.q_head = (P.q_head + 1) % PT_MAX_SLOTS;
P.q_len--;
P.state[slot] = S_PRESENTING;
void *ready = P.ready[slot];
P.ready[slot] = NULL;
mtx_unlock(&P.mtx);
const uint64_t t0 = now_ns();
void *done = P.cfg.present(P.cfg.user, slot, ready);
const uint64_t dt = now_ns() - t0;
mtx_lock(&P.mtx);
P.st.presents++;
P.st.present_ns += dt;
if (dt > P.st.present_max_ns) P.st.present_max_ns = dt;
P.done[slot] = done;
P.state[slot] = S_FREE;
cnd_bcast(&P.cv);
}
/* Tokens nobody will collect any more. */
for (int i = 0; i < P.cfg.slots; i++) {
if (P.done[i] && P.cfg.dispose) P.cfg.dispose(P.cfg.user, P.done[i]);
P.done[i] = NULL;
}
mtx_unlock(&P.mtx);
P.cfg.ctx(P.cfg.user, 0);
return NULL;
}
#ifdef _WIN32
static DWORD WINAPI present_main(LPVOID a) { (void)a; present_loop(); return 0; }
#else
static void *present_main(void *a) { (void)a; return present_loop(); }
#endif
int pt_start(const PtConfig *cfg) {
if (P.running || !cfg || !cfg->ctx || !cfg->present) return 0;
if (cfg->slots < 2 || cfg->slots > PT_MAX_SLOTS) return 0;
if (!s_pt_inited) { mtx_init(&P.mtx); cnd_init(&P.cv); s_pt_inited = 1; }
mtx_lock(&P.mtx);
P.cfg = *cfg;
P.stop = P.init_done = P.init_ok = 0;
P.q_head = P.q_len = 0;
for (int i = 0; i < PT_MAX_SLOTS; i++) {
P.state[i] = S_FREE;
P.ready[i] = P.done[i] = NULL;
}
P.current = 0;
P.state[0] = S_COMPOSING;
memset(&P.st, 0, sizeof P.st);
P.st.slots = cfg->slots;
mtx_unlock(&P.mtx);
int ok;
#ifdef _WIN32
P.thread = CreateThread(NULL, 0, present_main, NULL, 0, NULL);
ok = P.thread != NULL;
if (ok) SetThreadPriority(P.thread, THREAD_PRIORITY_ABOVE_NORMAL);
#else
pthread_attr_t attr;
pthread_attr_init(&attr);
# if defined(__APPLE__)
pthread_attr_set_qos_class_np(&attr, QOS_CLASS_USER_INTERACTIVE, 0);
# endif
ok = pthread_create(&P.thread, &attr, present_main, NULL) == 0;
pthread_attr_destroy(&attr);
#endif
if (!ok) return 0;
mtx_lock(&P.mtx);
while (!P.init_done) cnd_wait(&P.cv, &P.mtx);
ok = P.init_ok;
mtx_unlock(&P.mtx);
if (!ok) {
#ifdef _WIN32
WaitForSingleObject(P.thread, INFINITE);
CloseHandle(P.thread);
#else
pthread_join(P.thread, NULL);
#endif
return 0;
}
P.running = 1;
return 1;
}
void pt_stop(void) {
if (!P.running) return;
mtx_lock(&P.mtx);
P.stop = 1;
cnd_bcast(&P.cv);
mtx_unlock(&P.mtx);
#ifdef _WIN32
WaitForSingleObject(P.thread, INFINITE);
CloseHandle(P.thread);
#else
pthread_join(P.thread, NULL);
#endif
P.running = 0;
}
int pt_running(void) { return P.running; }
int pt_current(void) { return P.running ? P.current : -1; }
int pt_submit(void *ready, void **next_done, uint64_t *wait_ns) {
if (next_done) *next_done = NULL;
if (wait_ns) *wait_ns = 0;
if (!P.running) return -1;
mtx_lock(&P.mtx);
const int cur = P.current;
P.ready[cur] = ready;
P.state[cur] = S_QUEUED;
P.queue[(P.q_head + P.q_len) % PT_MAX_SLOTS] = cur;
P.q_len++;
P.st.submits++;
if (P.q_len > P.st.queued_high) P.st.queued_high = P.q_len;
cnd_bcast(&P.cv);
int next = -1;
uint64_t t0 = 0;
for (;;) {
/* Prefer the slot after the current one: the oldest free. */
for (int k = 1; k <= P.cfg.slots; k++) {
const int s = (cur + k) % P.cfg.slots;
if (P.state[s] == S_FREE) { next = s; break; }
}
if (next >= 0) break;
if (!t0) { t0 = now_ns(); P.st.waits++; }
cnd_wait(&P.cv, &P.mtx);
}
if (t0) {
const uint64_t w = now_ns() - t0;
P.st.wait_ns += w;
if (wait_ns) *wait_ns = w;
}
P.state[next] = S_COMPOSING;
P.current = next;
if (next_done) *next_done = P.done[next];
else if (P.done[next] && P.cfg.dispose) P.cfg.dispose(P.cfg.user, P.done[next]);
P.done[next] = NULL;
mtx_unlock(&P.mtx);
return next;
}
void pt_drain(void) {
if (!P.running) return;
mtx_lock(&P.mtx);
for (;;) {
int busy = P.q_len > 0;
for (int i = 0; i < P.cfg.slots && !busy; i++)
busy = P.state[i] == S_PRESENTING;
if (!busy) break;
cnd_wait(&P.cv, &P.mtx);
}
mtx_unlock(&P.mtx);
}
void pt_get_stats(PtStats *out) {
if (!out) return;
if (!s_pt_inited) { memset(out, 0, sizeof *out); return; }
mtx_lock(&P.mtx);
*out = P.st;
mtx_unlock(&P.mtx);
}