OpenGL backend: record on the emulation thread, replay on the render thread

With the render thread running, the backend's facade table records every
state change, primitive, fill, copy, CPU->VRAM upload (payload copied)
and native-wide call, and the present, into the command ring; the render
thread replays them through the unchanged glb_* functions. With it off
gl_backend_get returns the existing table and nothing new runs.

Sync points: every other GL entry point reached on the emulation thread
(readbacks, savestate restage, render passes, OpenXR, settings,
diagnostics) drains the ring and takes the context (GL_RT_SYNC); the
emulation thread then runs the rest of that frame directly, exactly as
without the thread, and hands the context back at the frame boundary when
the frame may run asynchronously again (no depth24 display, dual-raster,
frame interpolation, render pass, netplay or OpenXR).

Guest-visible VRAM stays gpu.c's array: while the render thread owns the
context, uploads stage from a private copy that receives each payload in
command order. Guest state the backend reads while drawing (per-primitive
widescreen tags, the native-wide fast-path latch) is captured at record
time; host overlays travel with the present; a present that a newer
recorded frame already supersedes is skipped. The emulation thread answers
projective/wide-present/draw-area queries from mirrors of the recorded
calls. The per-frame display-ring readback is queued (fbo_peek_deferred)
instead of being a sync point.

The GL fixtures link render_thread.c and stub the two facade hooks; the
scale fixture also gains the gpu_ws_background_requires_full_composite
stub it was already missing (it failed to link on master).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-06 00:24:09 -07:00
parent 2abd2200d0
commit 4a2f5a67d2
11 changed files with 900 additions and 35 deletions
+19
View File
@@ -8,6 +8,7 @@
* same struct tag) so this header needs no SDL include. */
#include <stdint.h>
#include <stddef.h>
#include "gpu_projective_clip.h"
struct SDL_Window;
@@ -464,6 +465,24 @@ void gl_renderer_batch_diag(uint64_t out[8]);
void gl_renderer_set_texture_window_batching(int on);
int gl_renderer_get_texture_window_batching(void);
/* Render thread ([video] render_thread, docs/RENDER_THREAD.md). Opt-in, off
* by default; with it off nothing below runs and the backend is unchanged.
* start: after gl_renderer_init_context, on the thread holding the context;
* moves the context to a new render thread and records backend calls from
* here on (1 = running). max_frames bounds closed frames in flight. stop:
* drain and take the context back (gl_renderer_shutdown also stops it).
* frame_boundary: once per vblank after the present; hands the context to or
* from the render thread and applies the in-flight bound. sync: an explicit
* sync point for readers of renderer-written host state outside the GL entry
* points. json: stats object body for the render_thread debug command. */
int gl_renderer_render_thread_start(int max_frames);
void gl_renderer_render_thread_stop(void);
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);
int gl_renderer_fbo_peek_deferred(int x, int y, int w, int h, uint16_t *out);
#ifdef __cplusplus
}
#endif
+1
View File
@@ -25,6 +25,7 @@ typedef enum {
void gr_set_backend(GrBackend backend); /* call before gr_init() */
GrBackend gr_backend(void); /* effective backend after init */
void gr_refresh_backend(void); /* re-fetch the GL table (render thread) */
/* Lifecycle / global state */
void gr_init(uint16_t *vram);
+2
View File
@@ -12,6 +12,8 @@ extern "C" {
/* Initialize software renderer */
void sw_renderer_init(uint16_t* vram);
/* Rebind the VRAM array only; draw state is kept. */
void sw_renderer_rebind_vram(uint16_t* vram);
/* Internal-resolution supersampling (SSAA).
* scale == 1 : disabled, renderer behaves exactly as native VRAM only.
File diff suppressed because it is too large Load Diff
+10
View File
@@ -137,6 +137,16 @@ void gr_set_backend(GrBackend backend) {
GrBackend gr_backend(void) { return g_effective; }
/* Re-fetch the OpenGL table after the render thread starts or stops
* (gl_backend_get then returns the recording table, or the direct one). The
* effective backend does not change. */
void gr_refresh_backend(void) {
if (g_effective == GR_BACKEND_OPENGL) {
const GpuRenderBackend *gl = gl_backend_get();
if (gl) g_b = gl;
}
}
/* ---- Dispatch wrappers (one line each; forward to the active backend) ---- */
void gr_init(uint16_t *vram) { g_b->init(vram); gpu_hd_textures_set_vram(vram); g_hd_offset_x = g_hd_offset_y = 0; g_hd_texture_window = 0; g_hd_semi = g_hd_perspective = 0; }
void gr_set_scale(int scale) { g_b->set_scale(scale); }
+6
View File
@@ -481,6 +481,12 @@ void sw_renderer_init(uint16_t *vram) {
g_texture_filter_req = 0;
}
/* Point the rasterizer at another VRAM array without touching any draw
* state (the OpenGL render thread switches its upload source this way). */
void sw_renderer_rebind_vram(uint16_t *vram) {
g_vram = vram;
}
/* ------------------------------------------------------------------ */
/* Supersampling control */
/* ------------------------------------------------------------------ */
+2
View File
@@ -71,6 +71,8 @@ def main():
sources = [("probe", fixture), ("sw", framework / "runtime/src/gpu_sw_renderer.c"),
("rp", framework / "runtime/src/render_pass_plan.c"),
("xr", framework / "runtime/src/psx_openxr.c")]
if (framework / "runtime/src/render_thread.c").exists():
sources.append(("rth", framework / "runtime/src/render_thread.c"))
for name, source in sources:
if run([compiler / "gcc.exe", "-std=c11", "-O2", "-flto", "-DPSX_SDL3=1",
"-DPSX_NO_DEBUG_TOOLS=1", *includes, "-c", source,
+2
View File
@@ -134,6 +134,8 @@ def main():
sources.append(("rp", framework / "runtime/src/render_pass_plan.c"))
if (framework / "runtime/src/psx_openxr.c").exists():
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"))
# Unused renderer functions reference the rest of the runtime; the linker
# drops them (-dead_strip, or per-function sections with --gc-sections).
# MinGW's PE linker reports undefined references from sections it later
+2
View File
@@ -41,6 +41,8 @@ def main():
('rp',fw/'runtime/src/render_pass_plan.c')]
if (fw/'runtime/src/psx_openxr.c').exists():
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'))
objects=[]
for name,source in sources:
obj=out/(name+'.o'); run([args.cc,*flags,'-c',source,'-o',obj]); objects.append(obj)
+4
View File
@@ -16,6 +16,10 @@ void gpu_vram_dirty_mark_row_impl(uint32_t y){}
void gpu_vram_dirty_mark_rect(int x,int y,int w,int h){}
void gpu_vram_dirty_mark_all(void){}
int psx_netplay_active(void){return 0;}
/* The renderer's facade hooks (gpu_render.c) for the render thread, which
* these fixtures never start. */
GrBackend gr_backend(void){return GR_BACKEND_OPENGL;}
void gr_refresh_backend(void){}
static int test_depth24;
int gpu_display_is_depth24(void){return test_depth24;}
void gpu_get_display_info(GpuDisplayInfo *out){memset(out,0,sizeof(*out));out->display_x=32;out->display_y=32;out->width=320;out->height=16;}
+4
View File
@@ -88,6 +88,10 @@ void gpu_vram_dirty_mark_row_impl(uint32_t y){(void)y;}
void gpu_vram_dirty_mark_rect(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;}
void gpu_vram_dirty_mark_all(void){}
int psx_netplay_active(void){return 0;}
/* The renderer's facade hooks (gpu_render.c) for the render thread, which
* these fixtures never start. */
GrBackend gr_backend(void){return GR_BACKEND_OPENGL;}
void gr_refresh_backend(void){}
int gpu_display_is_depth24(void){return 0;}
void gpu_get_display_info(GpuDisplayInfo *out){memset(out,0,sizeof(*out));out->width=320;out->height=240;}
int psx_ws_prim_in_backdrop(void){return 0;}