mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=[]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user