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>
This commit is contained in:
Shokunin
2026-10-06 04:59:00 -07:00
parent 90aca02153
commit 85a3d8d6d0
17 changed files with 891 additions and 39 deletions
+3
View File
@@ -802,6 +802,9 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
if (video.contains("render_thread")) {
rt.video_render_thread = toml::find<bool>(video, "render_thread");
}
if (video.contains("present_thread")) {
rt.video_present_thread = toml::find<bool>(video, "present_thread");
}
if (video.contains("frame_generation")) {
rt.video_frame_generation = toml::find<bool>(video, "frame_generation");
}
+8
View File
@@ -583,6 +583,14 @@ struct RuntimeConfig {
// PSX_FRAME_GEN=0/1 overrides.
bool video_frame_generation = false;
// present_thread: with render_thread, composed frames go to offscreen
// slots and a present thread (second, shared GL context on the window)
// does the copy and the swap, so the window compositor's wait does not
// block rendering (docs/RENDER_THREAD.md "Present thread"). Same pixels.
// Off by default; PSX_PRESENT_THREAD=0/1 overrides,
// PSX_PRESENT_THREAD_SLOTS=2..4 (default 3).
bool video_present_thread = false;
// low_latency_input: re-sample the pad after the wall-clock pacer (just
// before present) so the next CPU frame reads near-fresh input instead of
// input ~one frame stale. Default on. vsync: present/swap mode —
@@ -335,6 +335,21 @@ static void test_render_thread() {
fs::remove(q);
}
/* [video] present_thread (docs/RENDER_THREAD.md): opt-in, off by default. */
static void test_present_thread() {
fs::path p = write_game_toml("psxrecomp_pt_default.toml", "");
auto gc = PSXRecompV4::load_game_config(p);
check(!gc.runtime.video_present_thread, "present_thread defaults OFF");
fs::remove(p);
fs::path q = write_game_toml("psxrecomp_pt_on.toml",
"[video]\n"
"render_thread = true\n"
"present_thread = true\n");
auto gq = PSXRecompV4::load_game_config(q);
check(gq.runtime.video_present_thread, "[video] present_thread = true is honoured");
fs::remove(q);
}
/* [video] frame_generation (docs/FRAME_GENERATION.md): opt-in, off by default. */
static void test_frame_generation() {
fs::path p = write_game_toml("psxrecomp_fg_default.toml", "");
@@ -416,6 +431,7 @@ int main() {
test_texture_window_batching();
test_render_thread();
test_frame_generation();
test_present_thread();
test_pgxp_title_keys();
if (failures) {
+7
View File
@@ -1152,6 +1152,13 @@ if(BUILD_TESTING)
find_package(Threads REQUIRED)
target_link_libraries(render_thread_test PRIVATE Threads::Threads)
add_test(NAME render_thread_test COMMAND render_thread_test)
# Present thread core ([video] present_thread): submission order is
# presentation order, slots are never composed while queued or on
# screen, done tokens reach the next composer, stop drains.
add_executable(present_thread_test tests/test_present_thread.c src/present_thread.c)
target_include_directories(present_thread_test PRIVATE include)
target_link_libraries(present_thread_test PRIVATE Threads::Threads)
add_test(NAME present_thread_test COMMAND present_thread_test)
endif()
# Same family of bug as frame_pacing: the starvation watchdog's staleness
+8
View File
@@ -503,6 +503,14 @@ int gl_renderer_render_thread_active(void);
void gl_renderer_render_thread_frame_boundary(void);
void gl_renderer_render_thread_sync(const char *reason);
int gl_renderer_render_thread_json(char *out, size_t cap);
/* [video] present_thread (docs/RENDER_THREAD.md "Present thread"): set before
* gl_renderer_render_thread_start; with the render thread, composed frames go
* to `slots` (2..4) offscreen slots and a present thread with a shared context
* does the copy + swap. Falls back to direct swaps when it cannot start.
* json: the "present_thread" member for the render_thread debug command. */
void gl_renderer_set_present_thread(int on, int slots);
int gl_renderer_present_thread_active(void);
int gl_renderer_present_thread_json(char *out, size_t cap);
int gl_renderer_fbo_peek_deferred(int x, int y, int w, int h, uint16_t *out);
void gl_renderer_ring_capture(uint32_t frame, int wide, int base_x, int disp_y, int disp_h,
int cx, int cy, int cw, int ch);
+80
View File
@@ -0,0 +1,80 @@
/* present_thread.h — opt-in present thread under the render thread
* ([video] present_thread, docs/RENDER_THREAD.md "Present thread").
*
* The thread that composes frames (the render thread, or the emulation thread
* while it holds the context at a sync point) draws each presented frame into
* an offscreen slot instead of the window, then hands the slot over; the
* present thread owns a second graphics context on the same window and does
* the final copy and the swap. The swap's wait on the window compositor then
* lands on the present thread, not on rendering.
*
* This core is backend-neutral: a ring of `slots` numbered slots, each FREE,
* COMPOSING (the producer's current slot, exactly one), QUEUED or PRESENTING.
* Queued slots are presented in submission order. The producer blocks in
* pt_submit() only while every other slot is queued or presenting (the
* display is the bottleneck; that wait is the swap's wait, moved).
*
* Fences are opaque: `ready` travels with a submitted slot to present();
* present() returns a `done` token (e.g. a fence after its copy) that the
* producer receives when it next takes that slot, before drawing into it.
*
* Producer calls (pt_current / pt_submit) may come from different threads
* over time, but never concurrently (the context hand-off orders them). */
#ifndef PSX_PRESENT_THREAD_H
#define PSX_PRESENT_THREAD_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define PT_MAX_SLOTS 4
/* Present thread: make the present context current (current != 0, return 0 on
* failure: pt_start then fails) or release it (current == 0). */
typedef int (*PtCtxFn)(void *user, int current);
/* Present thread: show `slot` (wait for `ready`, copy, swap). Returns the
* slot's done token (may be NULL). */
typedef void *(*PtPresentFn)(void *user, int slot, void *ready);
/* Present thread, after the last present at stop or for a token nobody will
* collect: dispose of a done token. Optional. */
typedef void (*PtDisposeFn)(void *user, void *token);
typedef struct PtConfig {
int slots; /* 2..PT_MAX_SLOTS */
PtCtxFn ctx;
PtPresentFn present;
PtDisposeFn dispose; /* optional */
void *user;
} PtConfig;
/* Start the thread; returns once its ctx(1) has run. 1 on success (the
* producer's current slot is 0), 0 on failure (nothing running). */
int pt_start(const PtConfig *cfg);
/* Present everything queued, release the context on the present thread,
* join. Done tokens still held are passed to dispose on the present thread
* before it releases its context. Safe when not running. */
void pt_stop(void);
int pt_running(void);
/* Producer: the slot being composed. */
int pt_current(void);
/* Producer: queue the current slot with `ready`, then take the next free slot
* (blocking while none is). *next_done receives that slot's done token (NULL
* if none); *wait_ns the time spent blocked. Returns the new current slot, or
* -1 when not running. */
int pt_submit(void *ready, void **next_done, uint64_t *wait_ns);
/* Producer: wait until nothing is queued or presenting. */
void pt_drain(void);
typedef struct PtStats {
uint64_t submits, presents, waits, wait_ns, present_ns, present_max_ns;
int slots, queued_high;
} PtStats;
void pt_get_stats(PtStats *out);
#ifdef __cplusplus
}
#endif
#endif /* PSX_PRESENT_THREAD_H */
+1
View File
@@ -370,6 +370,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/gpu_vram_dirty.c
${PSXRECOMP_ROOT}/runtime/src/gpu_render.c
${PSXRECOMP_ROOT}/runtime/src/render_thread.c
${PSXRECOMP_ROOT}/runtime/src/present_thread.c
${PSXRECOMP_ROOT}/runtime/src/frame_gen.c
${PSXRECOMP_ROOT}/runtime/src/gpu_gl_renderer.c
${PSXRECOMP_ROOT}/runtime/src/gpu_hd_textures.cpp
+259 -4
View File
@@ -95,6 +95,7 @@
#include "psx_rewind.h"
#include "psx_openxr.h"
#include "openxr_color.h"
#include "present_thread.h"
#include "psx_sdl.h"
#if defined(PSX_SDL3)
@@ -293,6 +294,25 @@ static PFN_glBindRenderbuffer p_glBindRenderbuffer;
static PFN_glRenderbufferStorage p_glRenderbufferStorage;
static PFN_glFramebufferRenderbuffer p_glFramebufferRenderbuffer;
/* Present thread (docs/RENDER_THREAD.md "Present thread"): while it runs,
* the default framebuffer of the composing context is an offscreen slot.
* Every bind of framebuffer 0 on that context lands on the current slot; the
* present thread's own context binds the real one (p_glBindFramebuffer_raw). */
typedef void *(APIENTRY *PFN_glFenceSync)(GLenum, GLbitfield);
typedef void (APIENTRY *PFN_glWaitSync)(void *, GLbitfield, uint64_t);
typedef void (APIENTRY *PFN_glDeleteSync)(void *);
static PFN_glFenceSync p_glFenceSync;
static PFN_glWaitSync p_glWaitSync;
static PFN_glDeleteSync p_glDeleteSync;
static PFN_glBindFramebuffer p_glBindFramebuffer_raw;
static int s_pt_on = 0; /* redirect active (present thread running) */
static volatile int s_pt_interval_gen = 0; /* bumped per swap-interval change */
static GLuint pt_target_fbo(void);
static void APIENTRY gl_bind_fb_redirect(GLenum target, GLuint fb) {
if (fb == 0 && s_pt_on) fb = pt_target_fbo();
p_glBindFramebuffer_raw(target, fb);
}
static int load_modern_gl(void) {
int ok = 1;
#define LOAD(p, n) do { p = (void *)SDL_GL_GetProcAddress(n); if (!p) ok = 0; } while (0)
@@ -340,6 +360,14 @@ static int load_modern_gl(void) {
/* Optional: only the debug presented-image ring maps pack buffers. */
p_glMapBuffer = (void *)SDL_GL_GetProcAddress("glMapBuffer");
p_glUnmapBuffer = (void *)SDL_GL_GetProcAddress("glUnmapBuffer");
/* Optional: the present thread needs sync objects (GL 3.2). */
p_glFenceSync = (void *)SDL_GL_GetProcAddress("glFenceSync");
p_glWaitSync = (void *)SDL_GL_GetProcAddress("glWaitSync");
p_glDeleteSync = (void *)SDL_GL_GetProcAddress("glDeleteSync");
if (p_glBindFramebuffer && p_glBindFramebuffer != gl_bind_fb_redirect) {
p_glBindFramebuffer_raw = p_glBindFramebuffer;
p_glBindFramebuffer = gl_bind_fb_redirect;
}
#undef LOAD
return ok;
}
@@ -1296,7 +1324,7 @@ static void pres_record(int path, int dx, int dy, int w, int h,
* we pass in are already bottom-origin GL window coords). */
uint8_t px[3] = { 0, 0, 0 };
if (probe_pixels && lw > 0 && lh > 0) {
glReadBuffer(GL_BACK);
glReadBuffer(s_pt_on ? PSXGL_COLOR_ATTACHMENT0 : GL_BACK);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(lx + lw / 2, ly + lh / 2, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, px);
glPixelStorei(GL_PACK_ALIGNMENT, 4);
@@ -5432,6 +5460,7 @@ int gl_renderer_init_context(SDL_Window *win) {
void gl_renderer_set_swap_interval(int interval) {
GL_RT_SYNC("set_swap_interval");
s_swap_interval = interval;
s_pt_interval_gen++; /* the present thread's context applies it */
if (s_ctx) {
if (SDL_GL_SetSwapInterval(interval) != 0 && interval < 0) {
SDL_GL_SetSwapInterval(1);
@@ -5442,6 +5471,7 @@ void gl_renderer_set_swap_interval(int interval) {
int gl_renderer_get_swap_interval(void) {
GL_RT_SYNC("get_swap_interval");
if(!s_ctx)return -2;
if(s_pt_on)return s_swap_interval;
#if defined(PSX_SDL3)
int interval=0;
return SDL_GL_GetSwapInterval(&interval)?interval:-2;
@@ -7756,7 +7786,7 @@ static int openxr_copy_native(uint32_t eye,uint32_t texture,int w,int h) {
int ok=p_glCheckFramebufferStatus(PSXGL_DRAW_FRAMEBUFFER)==PSXGL_FRAMEBUFFER_COMPLETE;
if(ok) {
int *r=s_native_surface_rect;
glDisable(GL_SCISSOR_TEST);p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,0);glReadBuffer(GL_BACK);
glDisable(GL_SCISSOR_TEST);p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,0);glReadBuffer(s_pt_on?PSXGL_COLOR_ATTACHMENT0:GL_BACK);
glDisable(0x8DB9);
/* Default framebuffer is already upright: unlike native VRAM/eye
* textures, its bottom GL row is the displayed bottom row. */
@@ -8190,6 +8220,227 @@ static int ov_needs_present(void) {
}
static uint64_t s_swaps_total = 0; /* every swap, real or generated (diagnostic) */
/* ---- Present thread ([video] present_thread) -------------------------------
* Opt-in under the render thread. Each composed frame (everything that used
* to land in the window's back buffer: the display quad, OSD, captures) goes
* to an offscreen slot of the window's size; the swap becomes "fence, queue
* the slot, take the next free one". The present thread owns a second context
* on the same window, sharing objects with s_ctx, and does the 1:1 copy and
* the real swap, so the compositor's wait no longer blocks rendering. The
* composing side waits only while every slot is queued or on screen. */
#define PT_SYNC_GPU_COMMANDS_COMPLETE 0x9117
#define PT_TIMEOUT_IGNORED 0xFFFFFFFFFFFFFFFFull
static int s_pt_want = 0, s_pt_slots = 3;
static GLenum s_pt_format = GL_RGBA8; /* the window's: no alpha -> GL_RGB8 */
static SDL_GLContext s_pt_ctx = NULL;
static GLuint s_pts_tex[PT_MAX_SLOTS], s_pts_rb[PT_MAX_SLOTS], s_pts_fbo[PT_MAX_SLOTS];
static int s_pts_w[PT_MAX_SLOTS], s_pts_h[PT_MAX_SLOTS];
static unsigned s_pts_gen[PT_MAX_SLOTS];
/* present thread's own objects (FBOs are per-context) */
static GLuint s_ptp_fbo[PT_MAX_SLOTS];
static unsigned s_ptp_gen[PT_MAX_SLOTS];
static int s_ptp_interval_gen = -1;
void gl_renderer_set_present_thread(int on, int slots) {
s_pt_want = on ? 1 : 0;
s_pt_slots = slots < 2 ? 2 : slots > PT_MAX_SLOTS ? PT_MAX_SLOTS : slots;
}
int gl_renderer_present_thread_active(void) { return s_pt_on; }
/* Composing context: the current slot, (re)allocated at the window's size. */
static GLuint pt_target_fbo(void) {
const int i = pt_current();
if (i < 0) return 0;
int ww = 0, wh = 0;
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
if (ww < 1) ww = 1;
if (wh < 1) wh = 1;
if (s_pts_fbo[i] && s_pts_w[i] == ww && s_pts_h[i] == wh) return s_pts_fbo[i];
GLint tex_prev = 0, rb_prev = 0;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &tex_prev);
glGetIntegerv(0x8CA7 /* GL_RENDERBUFFER_BINDING */, &rb_prev);
if (s_pts_fbo[i]) {
p_glDeleteFramebuffers(1, &s_pts_fbo[i]);
p_glDeleteRenderbuffers(1, &s_pts_rb[i]);
glDeleteTextures(1, &s_pts_tex[i]);
s_pts_fbo[i] = s_pts_rb[i] = s_pts_tex[i] = 0;
}
s_pts_tex[i] = make_tex(s_pt_format, ww, wh, GL_RGBA, GL_UNSIGNED_BYTE);
p_glGenRenderbuffers(1, &s_pts_rb[i]);
p_glBindRenderbuffer(PSXGL_RENDERBUFFER, s_pts_rb[i]);
p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, ww, wh);
p_glGenFramebuffers(1, &s_pts_fbo[i]);
p_glBindFramebuffer_raw(PSXGL_FRAMEBUFFER, s_pts_fbo[i]);
p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, s_pts_tex[i], 0);
p_glFramebufferRenderbuffer(PSXGL_FRAMEBUFFER, PSXGL_DEPTH_STENCIL_ATTACHMENT,
PSXGL_RENDERBUFFER, s_pts_rb[i]);
if (p_glCheckFramebufferStatus(PSXGL_FRAMEBUFFER) != PSXGL_FRAMEBUFFER_COMPLETE)
fprintf(stdout, "psxrecomp: present thread: slot FBO incomplete\n");
glBindTexture(GL_TEXTURE_2D, (GLuint)tex_prev);
p_glBindRenderbuffer(PSXGL_RENDERBUFFER, (GLuint)rb_prev);
s_pts_w[i] = ww; s_pts_h[i] = wh;
s_pts_gen[i]++;
return s_pts_fbo[i];
}
static int pt_cb_ctx(void *user, int current) {
(void)user;
if (current) {
if (SDL_GL_MakeCurrent(s_win, s_pt_ctx) != 0) return 0;
s_ptp_interval_gen = -1;
return 1;
}
for (int i = 0; i < PT_MAX_SLOTS; i++) {
if (s_ptp_fbo[i]) p_glDeleteFramebuffers(1, &s_ptp_fbo[i]);
s_ptp_fbo[i] = 0;
s_ptp_gen[i] = 0;
}
glFinish();
SDL_GL_MakeCurrent(s_win, NULL);
return 1;
}
static void *pt_cb_present(void *user, int slot, void *ready) {
(void)user;
if (s_ptp_interval_gen != s_pt_interval_gen) {
s_ptp_interval_gen = s_pt_interval_gen;
if (SDL_GL_SetSwapInterval(s_swap_interval) != 0 && s_swap_interval < 0)
SDL_GL_SetSwapInterval(1);
}
if (ready) { p_glWaitSync(ready, 0, PT_TIMEOUT_IGNORED); p_glDeleteSync(ready); }
if (!s_ptp_fbo[slot] || s_ptp_gen[slot] != s_pts_gen[slot]) {
if (!s_ptp_fbo[slot]) p_glGenFramebuffers(1, &s_ptp_fbo[slot]);
p_glBindFramebuffer_raw(PSXGL_READ_FRAMEBUFFER, s_ptp_fbo[slot]);
p_glFramebufferTexture2D(PSXGL_READ_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
s_pts_tex[slot], 0);
s_ptp_gen[slot] = s_pts_gen[slot];
}
const int sw = s_pts_w[slot], sh = s_pts_h[slot];
int ww = 0, wh = 0;
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
p_glBindFramebuffer_raw(PSXGL_READ_FRAMEBUFFER, s_ptp_fbo[slot]);
p_glBindFramebuffer_raw(PSXGL_DRAW_FRAMEBUFFER, 0);
glDisable(GL_SCISSOR_TEST);
if (ww == sw && wh == sh) {
p_glBlitFramebuffer(0, 0, sw, sh, 0, 0, sw, sh, GL_COLOR_BUFFER_BIT, GL_NEAREST);
} else {
/* The window changed after this frame was composed: fit it. */
glViewport(0, 0, ww, wh);
glClearColor(0.f, 0.f, 0.f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
p_glBlitFramebuffer(0, 0, sw, sh, 0, 0, ww, wh, GL_COLOR_BUFFER_BIT, GL_LINEAR);
}
#ifdef GL_PRESENT_THREAD_TEST_HOOK
GL_PRESENT_THREAD_TEST_HOOK(slot);
#endif
void *done = p_glFenceSync(PT_SYNC_GPU_COMMANDS_COMPLETE, 0);
SDL_GL_SwapWindow(s_win);
return done;
}
static void pt_cb_dispose(void *user, void *token) {
(void)user;
if (token) p_glDeleteSync(token);
}
/* Composing context current; 1 when the present thread runs. */
static int pt_begin(void) {
if (!s_pt_want || s_pt_on || !s_win || !s_ctx) return 0;
if (!p_glFenceSync || !p_glWaitSync || !p_glDeleteSync) {
fprintf(stdout, "psxrecomp: present thread: no sync objects; swaps stay on the render thread\n");
return 0;
}
/* The slots stand in for the window's back buffer: same channels, so
* destination alpha and readbacks behave as they did there. */
{
GLint a = 8;
typedef void (APIENTRY *PFN_gfap)(GLenum, GLenum, GLenum, GLint *);
PFN_gfap gfap = (PFN_gfap)SDL_GL_GetProcAddress("glGetFramebufferAttachmentParameteriv");
p_glBindFramebuffer_raw(PSXGL_FRAMEBUFFER, 0);
while (glGetError() != GL_NO_ERROR) {}
if (gfap) gfap(PSXGL_FRAMEBUFFER, GL_BACK_LEFT, 0x8215 /* ALPHA_SIZE */, &a);
if (glGetError() != GL_NO_ERROR) a = 8;
s_pt_format = a > 0 ? GL_RGBA8 : GL_RGB8;
}
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);
s_pt_ctx = SDL_GL_CreateContext(s_win);
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0);
if (!s_pt_ctx) {
fprintf(stdout, "psxrecomp: present thread: shared context failed (%s); swaps stay on "
"the render thread\n", SDL_GetError());
SDL_GL_MakeCurrent(s_win, s_ctx);
return 0;
}
SDL_GL_MakeCurrent(s_win, NULL); /* creation made it current here */
SDL_GL_MakeCurrent(s_win, s_ctx);
memset(s_pts_fbo, 0, sizeof s_pts_fbo);
memset(s_ptp_fbo, 0, sizeof s_ptp_fbo);
PtConfig c = { s_pt_slots, pt_cb_ctx, pt_cb_present, pt_cb_dispose, NULL };
if (!pt_start(&c)) {
fprintf(stdout, "psxrecomp: present thread: start failed; swaps stay on the render thread\n");
SDL_GL_DeleteContext(s_pt_ctx);
s_pt_ctx = NULL;
SDL_GL_MakeCurrent(s_win, s_ctx);
return 0;
}
s_pt_on = 1;
fprintf(stdout, "psxrecomp: present thread on (%d slots)\n", s_pt_slots);
return 1;
}
/* Composing context current, render thread stopped. */
static void pt_end(void) {
if (!s_pt_on) return;
glFlush();
pt_stop();
s_pt_on = 0;
for (int i = 0; i < PT_MAX_SLOTS; i++) {
if (s_pts_fbo[i]) {
p_glDeleteFramebuffers(1, &s_pts_fbo[i]);
p_glDeleteRenderbuffers(1, &s_pts_rb[i]);
glDeleteTextures(1, &s_pts_tex[i]);
}
s_pts_fbo[i] = s_pts_rb[i] = s_pts_tex[i] = 0;
s_pts_w[i] = s_pts_h[i] = 0;
}
SDL_GL_DeleteContext(s_pt_ctx);
s_pt_ctx = NULL;
SDL_GL_MakeCurrent(s_win, s_ctx);
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
}
/* The swap: direct, or queued to the present thread. */
static void gl_present_swap(void) {
if (!s_pt_on) { SDL_GL_SwapWindow(s_win); return; }
(void)pt_target_fbo(); /* a frame that never bound 0 still has its slot */
void *ready = p_glFenceSync(PT_SYNC_GPU_COMMANDS_COMPLETE, 0);
glFlush();
void *done = NULL;
(void)pt_submit(ready, &done, NULL);
if (done) { p_glWaitSync(done, 0, PT_TIMEOUT_IGNORED); p_glDeleteSync(done); }
/* Whatever is bound as the window now means the new slot. */
GLint d = 0, r = 0;
glGetIntegerv(0x8CA6 /* DRAW_FRAMEBUFFER_BINDING */, &d);
glGetIntegerv(0x8CAA /* READ_FRAMEBUFFER_BINDING */, &r);
for (int i = 0; i < PT_MAX_SLOTS; i++) {
if (s_pts_fbo[i] && (GLuint)d == s_pts_fbo[i]) p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
if (s_pts_fbo[i] && (GLuint)r == s_pts_fbo[i]) p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
}
}
int gl_renderer_present_thread_json(char *out, size_t cap) {
PtStats st;
pt_get_stats(&st);
return snprintf(out, cap,
"\"present_thread\":{\"active\":%d,\"slots\":%d,\"submits\":%llu,\"presents\":%llu,"
"\"waits\":%llu,\"wait_ms\":%.3f,\"swap_ms_avg\":%.3f,\"swap_ms_max\":%.3f,"
"\"queued_high\":%d}",
s_pt_on, st.slots, (unsigned long long)st.submits, (unsigned long long)st.presents,
(unsigned long long)st.waits, st.wait_ns / 1e6,
st.presents ? (double)st.present_ns / (double)st.presents / 1e6 : 0.0,
st.present_max_ns / 1e6, st.queued_high);
}
static void gl_swap_now(int generated);
static int fg_keep_real(void); /* frame generation */
static void fg_note_swap(uint64_t ns);
@@ -8288,7 +8539,7 @@ static void gl_swap_now(int generated) {
s_swaps_total++;
uint64_t t0 = s_dyn_on ? SDL_GetPerformanceCounter() : 0;
const uint64_t rthf_t0 = rth_replaying() ? host_now_ns_rthf() : 0;
SDL_GL_SwapWindow(s_win);
gl_present_swap();
if (rthf_t0) {
const uint64_t sw = host_now_ns_rthf() - rthf_t0;
s_rthf_swap_ns += sw;
@@ -11091,7 +11342,9 @@ int gl_renderer_render_thread_start(int max_frames) {
cfg.user = NULL;
cfg.tick = fg_tick;
fg_invalidate();
(void)pt_begin(); /* [video] present_thread; falls back to direct swaps */
if (!rt_start(&cfg)) {
pt_end();
s_rth_on = 0;
s_vram = s_rth_vram_pub;
sw_renderer_rebind_vram(s_rth_vram_pub);
@@ -11106,6 +11359,7 @@ int gl_renderer_render_thread_start(int max_frames) {
void gl_renderer_render_thread_stop(void) {
if (!s_rth_on) return;
rt_stop(); /* drains; the context is current here again */
pt_end();
s_rth_on = 0;
s_vram = s_rth_vram_pub;
sw_renderer_rebind_vram(s_rth_vram_pub);
@@ -11162,7 +11416,8 @@ int gl_renderer_render_thread_json(char *out, size_t cap) {
k += snprintf(out + k, cap - (size_t)k, "%s{\"frame\":%llu,\"wait_us\":%.1f,\"reason\":\"%s\"}",
i ? "," : "", (unsigned long long)ev[i].frame, ev[i].wait_ns / 1e3,
ev[i].reason);
if (k > 0 && (size_t)k < cap) k += snprintf(out + k, cap - (size_t)k, "]");
if (k > 0 && (size_t)k < cap) k += snprintf(out + k, cap - (size_t)k, "],");
if (k > 0 && (size_t)k < cap) k += gl_renderer_present_thread_json(out + k, cap - (size_t)k);
return k;
}
+42 -32
View File
@@ -1459,6 +1459,9 @@ static int g_render_thread = 0;
static int g_render_thread_frames = 2;
/* [video] frame_generation (docs/FRAME_GENERATION.md), with the render thread. */
static int g_frame_generation = 0;
/* [video] present_thread (docs/RENDER_THREAD.md), with the render thread. */
static int g_present_thread = 0;
static int g_present_thread_slots = 3;
static int s_render_thread_tried = 0;
static int g_video_vsync = 1;
static int g_frame_interpolation = 0;
@@ -9212,6 +9215,40 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
return ep;
}
/* Render thread: start once, then close every frame (after its present,
* before pacing, so the render thread draws while the guest waits). */
static void render_thread_vblank(void) {
#ifndef PSX_SDL_NO_RENDER
if (!g_render_thread) return;
if (!s_render_thread_tried) {
s_render_thread_tried = 1;
gl_renderer_set_present_thread(g_present_thread, g_present_thread_slots);
if (g_gl_active && gr_backend() == GR_BACKEND_OPENGL &&
gl_renderer_render_thread_start(g_render_thread_frames)) {
std::fprintf(stdout, "psxrecomp: render thread on (OpenGL, %d frame(s) in flight)\n",
g_render_thread_frames);
if (g_frame_generation) {
gl_renderer_set_frame_generation(1);
std::fprintf(stdout, "psxrecomp: frame generation on (render thread, "
"from surplus only)\n");
}
} else {
std::fprintf(stdout, "psxrecomp: render thread requested but not started "
"(needs the OpenGL backend without netplay, frame "
"interpolation or a 24-bit display)\n");
if (g_frame_generation)
std::fprintf(stdout, "psxrecomp: frame generation needs the render thread; off\n");
}
std::fflush(stdout);
}
if (g_frame_generation)
gl_renderer_frame_gen_configure(g_host_refresh_hz,
g_guest_frame_period_ms > 0.0
? 1000.0 / g_guest_frame_period_ms : 0.0);
gl_renderer_render_thread_frame_boundary();
#endif
}
/* ---- Dynamic internal resolution: the host side ---------------------------
* [video] dynamic_resolution (docs/ENHANCEMENTS.md, IR3). The controller
* (dynamic_resolution.c) decides; this feeds it one sample per guest VBlank,
@@ -9577,38 +9614,6 @@ extern "C" int psx_dynres_status_json(char *out, int cap) {
c.down_block_until > now_s ? c.down_block_until - now_s : 0.0, blocked);
}
/* Render thread: start once, then close every frame (after its present,
* before pacing, so the render thread draws while the guest waits). */
static void render_thread_vblank(void) {
#ifndef PSX_SDL_NO_RENDER
if (!g_render_thread) return;
if (!s_render_thread_tried) {
s_render_thread_tried = 1;
if (g_gl_active && gr_backend() == GR_BACKEND_OPENGL &&
gl_renderer_render_thread_start(g_render_thread_frames)) {
std::fprintf(stdout, "psxrecomp: render thread on (OpenGL, %d frame(s) in flight)\n",
g_render_thread_frames);
if (g_frame_generation) {
gl_renderer_set_frame_generation(1);
std::fprintf(stdout, "psxrecomp: frame generation on (render thread, "
"from surplus only)\n");
}
} else {
std::fprintf(stdout, "psxrecomp: render thread requested but not started "
"(needs the OpenGL backend without netplay, frame "
"interpolation or a 24-bit display)\n");
if (g_frame_generation)
std::fprintf(stdout, "psxrecomp: frame generation needs the render thread; off\n");
}
std::fflush(stdout);
}
if (g_frame_generation)
gl_renderer_frame_gen_configure(g_host_refresh_hz,
g_guest_frame_period_ms > 0.0
? 1000.0 / g_guest_frame_period_ms : 0.0);
gl_renderer_render_thread_frame_boundary();
#endif
}
static void sdl_vblank_present(void) {
sync_guest_cadence_to_video_standard();
@@ -15717,6 +15722,7 @@ int main(int argc, char** argv) {
gc.runtime.video_texture_window_batching ? 1 : 0);
g_render_thread = gc.runtime.video_render_thread ? 1 : 0;
g_frame_generation = gc.runtime.video_frame_generation ? 1 : 0;
g_present_thread = gc.runtime.video_present_thread ? 1 : 0;
g_video_vsync = gc.runtime.video_vsync;
g_frame_interpolation = gc.runtime.video_frame_interpolation ? 1 : 0;
g_frame_interpolation_fps = gc.runtime.video_frame_interpolation_fps;
@@ -18023,6 +18029,10 @@ session_reboot:
* flight (default 2). */
if (const char* e = std::getenv("PSX_RENDER_THREAD"))
g_render_thread = (*e && *e != '0') ? 1 : 0;
if (const char* e = std::getenv("PSX_PRESENT_THREAD"))
g_present_thread = (*e && *e != '0') ? 1 : 0;
if (const char* e = std::getenv("PSX_PRESENT_THREAD_SLOTS"))
g_present_thread_slots = std::atoi(e) > 0 ? std::atoi(e) : 3;
if (const char* e = std::getenv("PSX_FRAME_GEN"))
g_frame_generation = (*e && *e != '0') ? 1 : 0;
if (const char* e = std::getenv("PSX_RENDER_THREAD_FRAMES"))
+238
View File
@@ -0,0 +1,238 @@
/* 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);
}
+49 -1
View File
@@ -77,7 +77,7 @@ def main():
# facade (gpu_render.c) and render thread (render_thread.c).
sources = [("probe", fixture), ("sw", src / "gpu_sw_renderer.c"),
("fi", src / "frame_interpolation.c"), ("rp", src / "render_pass_plan.c"),
("xr", src / "psx_openxr.c"), ("rth", src / "render_thread.c"),
("xr", src / "psx_openxr.c"), ("rth", src / "render_thread.c"), ("pth", src / "present_thread.c"),
("facade", src / "gpu_render.c"), ("fg", src / "frame_gen.c")]
sections = [] if platform.system() == "Darwin" else ["-ffunction-sections", "-fdata-sections"]
# MinGW's PE linker reports undefined references from sections it later
@@ -139,6 +139,54 @@ def main():
print(f"FAIL scale {s} {path} {timing}: generated frames off={runs[0]['generated']} "
f"on={runs[1]['generated']}")
ok = False
# Present thread ([video] present_thread): the fixture checks the
# window shows every composed image bit for bit, in order. The real
# images match the direct-swap run: bit for bit, except that
# rasterizing into a texture instead of the window's drawable may
# round a filtered present pixel differently (seen on Apple's
# GL-on-Metal: 1 LSB, a pixel or two per frame); allowed up to 1 LSB
# on at most 0.05 % of the pixels, and reported.
for fg in (0, 1):
dirs = {}
for mode in ("direct", "pt"):
sub = dest / f"pt-{s}-{path}-{timing}-{fg}-{mode}"
sub.mkdir(exist_ok=True)
cmd = [str(c) for c in (probe, s, fg, path, args.frames, timing)]
if mode == "pt":
cmd.append("pt")
env = dict(os.environ, FG_DUMP="1")
r = subprocess.run(cmd, cwd=sub, capture_output=True, text=True,
encoding="utf-8", errors="replace", env=env)
p = parse(r.stdout)
if r.returncode or p["failures"] != 0:
print(f"FAIL scale {s} {path} {timing} fg={fg} {mode}: exit={r.returncode}",
r.stdout.strip().splitlines()[-4:], r.stderr.strip()[-800:])
ok = False
dirs[mode] = sub
files = sorted(f.name for f in dirs["direct"].glob("real*.rgba"))
other = sorted(f.name for f in dirs["pt"].glob("real*.rgba"))
if files != other or not files:
print(f"FAIL scale {s} {path} {timing} fg={fg}: present thread real frames "
f"{len(other)} vs {len(files)}")
ok = False
continue
exact, worst, worst_px = 0, 0, 0.0
for name in files:
a = (dirs["direct"] / name).read_bytes()
b = (dirs["pt"] / name).read_bytes()
if a == b:
exact += 1
continue
d = [abs(x - y) for x, y in zip(a, b) if x != y]
px = len({i // 4 for i, (x, y) in enumerate(zip(a, b)) if x != y})
worst = max(worst, max(d) if d else 0)
worst_px = max(worst_px, px / (len(a) / 4))
print(f"scale {s} {path} {timing} frame_generation={fg} present_thread=1: "
f"{exact}/{len(files)} real frames bit-identical, max diff {worst} LSB "
f"on {worst_px * 100:.3f} % of pixels")
if worst > 1 or worst_px > 0.0005:
print(f"FAIL scale {s} {path} {timing} fg={fg}: present thread changed real frames")
ok = False
print("PASS" if ok else "FAIL")
return 0 if ok else 1
+2
View File
@@ -73,6 +73,8 @@ def main():
("xr", framework / "runtime/src/psx_openxr.c")]
if (framework / "runtime/src/render_thread.c").exists():
sources.append(("rth", framework / "runtime/src/render_thread.c"))
if (framework / "runtime/src/present_thread.c").exists():
sources.append(("pth", framework / "runtime/src/present_thread.c"))
if (framework / "runtime/src/frame_gen.c").exists():
sources.append(("fg", framework / "runtime/src/frame_gen.c"))
for name, source in sources:
+1 -1
View File
@@ -84,7 +84,7 @@ def main():
# facade (gpu_render.c) and render thread (render_thread.c).
sources = [("probe", fixture), ("sw", src / "gpu_sw_renderer.c"),
("fi", src / "frame_interpolation.c"), ("rp", src / "render_pass_plan.c"),
("xr", src / "psx_openxr.c"), ("rth", src / "render_thread.c"),
("xr", src / "psx_openxr.c"), ("rth", src / "render_thread.c"), ("pth", src / "present_thread.c"),
("facade", src / "gpu_render.c"), ("fg", src / "frame_gen.c")]
sections = [] if platform.system() == "Darwin" else ["-ffunction-sections", "-fdata-sections"]
# MinGW's PE linker reports undefined references from sections it later
+2
View File
@@ -136,6 +136,8 @@ def main():
sources.append(("xr", framework / "runtime/src/psx_openxr.c"))
if (framework / "runtime/src/render_thread.c").exists():
sources.append(("rth", framework / "runtime/src/render_thread.c"))
if (framework / "runtime/src/present_thread.c").exists():
sources.append(("pth", framework / "runtime/src/present_thread.c"))
if (framework / "runtime/src/frame_gen.c").exists():
sources.append(("fg", framework / "runtime/src/frame_gen.c"))
# Unused renderer functions reference the rest of the runtime; the linker
+2
View File
@@ -41,6 +41,8 @@ def main():
sources.append(('xr',fw/'runtime/src/psx_openxr.c'))
if (fw/'runtime/src/render_thread.c').exists():
sources.append(('rth',fw/'runtime/src/render_thread.c'))
if (fw/'runtime/src/present_thread.c').exists():
sources.append(('pth',fw/'runtime/src/present_thread.c'))
if (fw/'runtime/src/frame_gen.c').exists():
sources.append(('fg',fw/'runtime/src/frame_gen.c'))
objects=[]
+45 -1
View File
@@ -18,6 +18,11 @@
* Original source-owned scene; no retail payload. */
static void fixture_present(int generated);
#define GL_PRESENT_TEST_HOOK(gen) fixture_present(gen)
/* argv[6] == "pt": the present thread is on; every image it puts in the
* window (read back after its copy, before its swap) must be the image the
* composing side presented, in the same order. */
static void fixture_window(int slot);
#define GL_PRESENT_THREAD_TEST_HOOK(slot) fixture_window(slot)
#include "gpu_gl_renderer.c"
#include "gpu_hd_texture_stubs.inc"
#include "mod_texture_banks.c"
@@ -101,7 +106,37 @@ static uint64_t read_back(uint32_t *dst) {
glReadPixels(0, 0, ww, wh, GL_RGBA, GL_UNSIGNED_BYTE, dst);
return fnv(dst, (size_t)ww * wh * 4, 0xcbf29ce484222325ull);
}
#define MAX_SHOWN 4096
static int pt_mode;
static uint64_t composed_d[MAX_SHOWN], window_d[MAX_SHOWN];
static int n_composed, n_window;
static void fixture_window(int slot) {
(void)slot;
int ww = 0, wh = 0;
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
uint32_t *px = (uint32_t *)malloc((size_t)ww * wh * 4);
p_glBindFramebuffer_raw(PSXGL_READ_FRAMEBUFFER, 0);
glReadBuffer(GL_BACK);
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glReadPixels(0, 0, ww, wh, GL_RGBA, GL_UNSIGNED_BYTE, px);
if (n_window < MAX_SHOWN)
window_d[n_window] = fnv(px, (size_t)ww * wh * 4, 0xcbf29ce484222325ull);
n_window++;
free(px);
}
static void fixture_present(int generated) {
if (pt_mode) {
int ww = 0, wh = 0;
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
uint32_t *px = (uint32_t *)malloc((size_t)ww * wh * 4);
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glReadPixels(0, 0, ww, wh, GL_RGBA, GL_UNSIGNED_BYTE, px);
if (n_composed < MAX_SHOWN)
composed_d[n_composed] = fnv(px, (size_t)ww * wh * 4, 0xcbf29ce484222325ull);
n_composed++;
free(px);
}
if (generated) { n_gen++; return; }
int ww = 0, wh = 0;
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
@@ -183,6 +218,7 @@ int main(int argc, char **argv) {
/* "late": the game flips to a frame one VBlank after it starts drawing
* the next one (as R4 does), so a flip is not a list boundary. */
int late = argc > 5 && !strcmp(argv[5], "late");
pt_mode = argc > 6 && !strcmp(argv[6], "pt");
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
fprintf(stderr, "SKIP no video (%s)\n", SDL_GetError());
return 77;
@@ -214,7 +250,9 @@ int main(int argc, char **argv) {
if (!gl_renderer_init_context(win)) { fprintf(stderr, "FAIL context\n"); return 2; }
textures();
if (wide_mode) gr_wide_configure(426, 53);
if (pt_mode) gl_renderer_set_present_thread(1, 3);
check(gl_renderer_render_thread_start(2) == 1, "render thread started");
if (pt_mode) check(gl_renderer_present_thread_active(), "present thread started");
if (fg) {
setenv("PSX_FRAME_GEN_FORCE", "1", 1);
gl_renderer_set_frame_generation(1);
@@ -280,8 +318,14 @@ int main(int argc, char **argv) {
check(glGetError() == GL_NO_ERROR, "GL error");
printf("real=%016llx\n", (unsigned long long)real_seq);
printf("presents=%llu generated=%llu\n", (unsigned long long)n_real, (unsigned long long)n_gen);
gl_renderer_shutdown(); /* stops the render and present threads: all shown */
if (pt_mode) {
int same = n_composed == n_window && n_composed <= MAX_SHOWN && n_composed > 0;
for (int i = 0; same && i < n_composed; i++) same = composed_d[i] == window_d[i];
printf("pt composed=%d shown=%d\n", n_composed, n_window);
check(same, "the window shows every composed image, in order");
}
printf("checks=%d failures=%d\n", checks, failures);
gl_renderer_shutdown();
SDL_DestroyWindow(win);
SDL_Quit();
return failures ? 1 : 0;
+128
View File
@@ -0,0 +1,128 @@
/* present_thread_test — the present thread core (present_thread.c) against a
* fake presenter: submission order is presentation order, a slot is never
* composed while queued or presenting, each done token reaches the producer
* that next takes its slot, a slow display blocks the producer (and a fast one
* still keeps every invariant), stop presents everything queued and disposes leftover tokens, a
* failed context start reports failure. */
#include "present_thread.h"
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
static int checks, failures;
static void check(int ok, const char *label) {
checks++;
if (!ok) { fprintf(stderr, "FAIL %s\n", label); failures++; }
}
static void sleep_us(long us) {
struct timespec ts = { us / 1000000, (us % 1000000) * 1000 };
nanosleep(&ts, NULL);
}
#define N_FRAMES 600
static _Atomic int slot_busy[PT_MAX_SLOTS]; /* 1 while queued or presenting */
static uintptr_t presented[N_FRAMES + 8];
static _Atomic int n_presented;
static int ctx_ok = 1, ctx_on, ctx_off;
static long present_us = 0;
static _Atomic int disposed;
static uintptr_t last_done[PT_MAX_SLOTS];
static _Atomic int order_bad;
static int fake_ctx(void *u, int cur) { (void)u; if (cur) { ctx_on++; return ctx_ok; } ctx_off++; return 1; }
static void *fake_present(void *u, int slot, void *ready) {
(void)u;
if (!atomic_load(&slot_busy[slot])) atomic_store(&order_bad, 1);
int i = atomic_fetch_add(&n_presented, 1);
if (i < N_FRAMES + 8) presented[i] = (uintptr_t)ready;
if (present_us) sleep_us(present_us + (rand() % 3) * present_us / 2);
atomic_store(&slot_busy[slot], 0);
/* done token: slot in the low byte, frame above */
return (void *)(((uintptr_t)ready << 8) | (uintptr_t)slot | 0x80u);
}
static void fake_dispose(void *u, void *t) { (void)u; (void)t; atomic_fetch_add(&disposed, 1); }
static PtConfig cfg(int slots) {
PtConfig c = { slots, fake_ctx, fake_present, fake_dispose, NULL };
return c;
}
static void reset(void) {
for (int i = 0; i < PT_MAX_SLOTS; i++) { atomic_store(&slot_busy[i], 0); last_done[i] = 0; }
atomic_store(&n_presented, 0);
atomic_store(&disposed, 0);
atomic_store(&order_bad, 0);
ctx_on = ctx_off = 0;
}
static void run(int slots, long us, int expect_waits) {
reset();
present_us = us;
PtConfig c = cfg(slots);
check(pt_start(&c), "start");
check(pt_current() == 0, "first slot is 0");
int bad_reuse = 0, bad_token = 0;
for (int f = 1; f <= N_FRAMES; f++) {
const int cur = pt_current();
if (atomic_load(&slot_busy[cur])) bad_reuse++;
/* producer "composes" for a bit */
if (f % 7 == 0) sleep_us(200);
atomic_store(&slot_busy[cur], 1);
void *done = NULL;
uint64_t w = 0;
const int next = pt_submit((void *)(uintptr_t)f, &done, &w);
if (next < 0 || next >= slots || next == cur || atomic_load(&slot_busy[next])) bad_reuse++;
if ((uintptr_t)done != last_done[next]) bad_token++;
/* this slot's next token is unknown until presented again */
last_done[next] = 0;
/* remember what present will hand back for cur */
last_done[cur] = (((uintptr_t)f) << 8) | (uintptr_t)cur | 0x80u;
}
pt_drain();
check(atomic_load(&n_presented) == N_FRAMES, "every submitted slot presented");
int ordered = 1;
for (int i = 0; i < N_FRAMES; i++) if (presented[i] != (uintptr_t)(i + 1)) ordered = 0;
check(ordered, "presented in submission order");
check(!bad_reuse, "a slot is never composed while queued or presenting");
check(!bad_token, "done token reaches the producer taking the slot");
check(!atomic_load(&order_bad), "present sees only queued slots");
PtStats st;
pt_get_stats(&st);
check(st.submits == N_FRAMES && st.presents == N_FRAMES, "stats count");
check(st.queued_high <= slots, "bounded queue");
if (expect_waits > 0) check(st.waits > 0 && st.wait_ns > 0, "slow display blocks the producer");
/* stop with frames queued: they are presented, leftover tokens disposed */
present_us = 2000;
for (int f = 0; f < slots - 1; f++) {
void *d = NULL;
atomic_store(&slot_busy[pt_current()], 1);
(void)pt_submit((void *)(uintptr_t)(N_FRAMES + 1 + f), &d, NULL);
}
pt_stop();
check(atomic_load(&n_presented) == N_FRAMES + slots - 1, "stop presents the queue");
check(atomic_load(&disposed) > 0, "stop disposes uncollected tokens");
check(ctx_on == 1 && ctx_off == 1, "context taken and released once");
check(!pt_running() && pt_submit(NULL, NULL, NULL) == -1, "stopped");
}
int main(void) {
srand(7);
run(3, 0, 0);
run(3, 1500, 1);
run(2, 800, 1);
run(4, 300, -1); /* may or may not wait: invariants only */
/* context failure */
reset();
ctx_ok = 0;
PtConfig c = cfg(3);
check(!pt_start(&c), "start fails when the context cannot be made current");
check(!pt_running(), "not running after failed start");
ctx_ok = 1;
PtConfig bad = cfg(1);
check(!pt_start(&bad), "one slot refused");
printf("present_thread_test: %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}