From 4a2f5a67d2df4774729906539165807bc76dd4d5 Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Tue, 6 Oct 2026 00:24:09 -0700 Subject: [PATCH] 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 --- runtime/include/gpu_gl_renderer.h | 19 + runtime/include/gpu_render.h | 1 + runtime/include/gpu_sw_renderer.h | 2 + runtime/src/gpu_gl_renderer.c | 883 ++++++++++++++++++++++- runtime/src/gpu_render.c | 10 + runtime/src/gpu_sw_renderer.c | 6 + runtime/tests/run_gl_readback_region.py | 2 + runtime/tests/run_gl_scale_invariance.py | 2 + runtime/tests/run_gl_texture_filter.py | 2 + runtime/tests/test_gl_readback_region.c | 4 + runtime/tests/test_gl_scale_invariance.c | 4 + 11 files changed, 900 insertions(+), 35 deletions(-) diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index b97199cd..41e29077 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -8,6 +8,7 @@ * same struct tag) so this header needs no SDL include. */ #include +#include #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 diff --git a/runtime/include/gpu_render.h b/runtime/include/gpu_render.h index 4dbad651..5f70d079 100644 --- a/runtime/include/gpu_render.h +++ b/runtime/include/gpu_render.h @@ -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); diff --git a/runtime/include/gpu_sw_renderer.h b/runtime/include/gpu_sw_renderer.h index c549a585..6525c2ab 100644 --- a/runtime/include/gpu_sw_renderer.h +++ b/runtime/include/gpu_sw_renderer.h @@ -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. diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 84c85148..f2d99b6f 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -349,6 +349,76 @@ static SDL_GLContext s_ctx = NULL; static uint16_t *s_vram = NULL; /* CPU VRAM array (gpu.c's storage) */ static int s_swap_interval = 1; /* SDL_GL swap interval (vsync mode) */ +/* ---- render thread ([video] render_thread, docs/RENDER_THREAD.md) -------- + * While s_rth_on, the render thread owns the GL context and replays the + * backend calls the emulation thread recorded (GL_RT_BACKEND, end of file). + * A GL entry point reached on the emulation thread is a sync point + * (GL_RT_SYNC): it drains the queue and moves the context to the emulation + * thread, which then runs every call directly, exactly as without the render + * thread, until the next frame boundary hands the context back. + * + * s_vram is the guest-visible VRAM (gpu.c's array) whenever the emulation + * thread runs GL, and a private copy while the render thread does: replayed + * uploads stage from the private copy, which receives each CPU->VRAM payload + * in command order, so the emulation thread can keep writing gpu.c's array. + * + * Guest state the backend reads while drawing (per-primitive widescreen tags, + * the native-wide fast-path latch, depth24) is captured when a call is + * recorded and read back through the ctx_* accessors below during replay. */ +#include "render_thread.h" +static int s_rth_on = 0; +static uint16_t *s_rth_vram_pub = NULL; /* gpu.c's VRAM (guest-visible) */ +static uint16_t *s_rth_vram_priv = NULL; /* render thread's upload source */ +/* Replay-side capture of guest state (valid only on the render thread). */ +static int s_rthx_prim = 0; /* current record carries prim tags */ +static int s_rthx_tagged = 0, s_rthx_backdrop = 0; +static int s_rths_flat_bd = 0, s_rths_vp_w = 0, s_rths_bg_full = 0; +static inline int rth_replaying(void) { return s_rth_on && rt_on_render_thread(); } +extern int psx_ws_prim_is_tagged(void); /* gpu.c: is the current GP0 prim sprite-tagged? */ +extern int psx_ws_prim_in_backdrop(void); /* gpu.c: is its source addr in the flower-field struct? */ +extern int gpu_ws_nw_flat_backdrop_enabled(void); /* gpu.c: per-title flat backdrop opt-in */ +static int ctx_prim_tagged(void) { + return rth_replaying() ? (s_rthx_prim ? s_rthx_tagged : 0) : psx_ws_prim_is_tagged(); +} +static int ctx_prim_in_backdrop(void) { + return rth_replaying() ? (s_rthx_prim ? s_rthx_backdrop : 0) : psx_ws_prim_in_backdrop(); +} +static int ctx_flat_backdrop(void) { + return rth_replaying() ? s_rths_flat_bd : gpu_ws_nw_flat_backdrop_enabled(); +} +static int ctx_vp_width(void) { + return rth_replaying() ? s_rths_vp_w : gpu_ws_netplay_local_viewport_width(); +} +static int ctx_bg_full(void) { + return rth_replaying() ? s_rths_bg_full : gpu_ws_background_requires_full_composite(); +} +/* The render thread only ever replays 15-bit frames: a depth24 display is a + * sync point (the emulation thread draws those frames itself). */ +static int ctx_depth24(void) { return rth_replaying() ? 0 : gpu_display_is_depth24(); } +static void gl_rth_acquire(const char *reason); +#define GL_RT_SYNC(reason) \ + do { if (s_rth_on && !rt_on_render_thread()) gl_rth_acquire(reason); } while (0) +/* Recording (defined with GL_RT_BACKEND at the end of the file). */ +enum { + RTH_SEMI = 1, RTH_MASK, RTH_TWIN, RTH_MOD, RTH_PRECISE, RTH_PERSP, + RTH_FILL, RTH_COPY, RTH_FLAT_TRI, RTH_GOURAUD_TRI, RTH_TEX_TRI, + RTH_SHADED_TEX_TRI, RTH_FLAT_RECT, RTH_TEX_RECT, RTH_TEX_RECT_SCALED, + RTH_LINE, RTH_SHADED_LINE, RTH_VRAM_WRITE, RTH_XFER_IN, RTH_AREA, + RTH_OFFSET, RTH_WIDE_CONFIGURE, RTH_WIDE_VIEW, RTH_WIDE_TARGET, + RTH_WIDE_DISABLE, RTH_WIDE_CLEAR, RTH_WIDE_CLEAR_MARGINS, RTH_PROJ_TRI, + RTH_WIDE_RECOVERY, RTH_INTERP_SUSPENDED, RTH_PRESENT_VRAM, + RTH_PRESENT_WIDE, RTH_STATE, RTH_PEEK +}; +static int rth_record_mode(void); +static int rth_rec_ints(uint16_t op, uint16_t flags, int n, const int32_t *v); +static uint16_t rth_prim_flags(void); +static int rth_record_present(uint16_t op, int n, const int32_t *v); +static int rth_mirror_wide_present_ok(int base_x); +/* Emulation-side mirror: the recorded wide target is a live surface. */ +static int s_rthm_cur = 0; +#define RTH_REC(op, fl, ...) do { const int32_t rth_v_[] = { __VA_ARGS__ }; \ + rth_rec_ints((op), (fl), (int)(sizeof rth_v_ / sizeof rth_v_[0]), rth_v_); } while (0) + /* ---- Two scales ------------------------------------------------------------ * s_hr_scale the scale of s_hr_fbo, the authoritative VRAM surface, and of * everything that reads, mirrors, backs up or restores it: hr @@ -763,9 +833,11 @@ static int s_wide_suppress = 0; * canonical and wide passes so a face cannot split at the center-copy edge. */ static uint64_t s_wide_triangle_recovery_count; void gl_renderer_note_wide_triangle_recovery(int recovered) { + if (rth_record_mode()) { RTH_REC(RTH_WIDE_RECOVERY, 0, recovered); return; } if (recovered && s_raster_ok && g_wide_cur) ++s_wide_triangle_recovery_count; } uint64_t gl_renderer_wide_triangle_recovery_count(void) { + GL_RT_SYNC("wide_triangle_recovery_count"); return s_wide_triangle_recovery_count; } @@ -1151,8 +1223,10 @@ static void coh_record(int kind, int x0, int y0, int x1, int y1) { present_dirty_rect(x0, y0, x1, y1, 1); } -uint64_t gl_renderer_coh_total(void) { return s_coh_seq; } +uint64_t gl_renderer_coh_total(void) { + GL_RT_SYNC("coh_total"); return s_coh_seq; } int gl_renderer_coh_get(uint64_t seq, GlCohEvent *out) { + GL_RT_SYNC("coh_get"); if (seq >= s_coh_seq) return 0; if (s_coh_seq - seq > GL_COH_RING_CAP) return 0; /* evicted */ *out = s_coh_ring[seq % GL_COH_RING_CAP]; @@ -1218,8 +1292,10 @@ static void pres_record(int path, int dx, int dy, int w, int h, s_pres_seq++; } -uint64_t gl_renderer_pres_total(void) { return s_pres_seq; } +uint64_t gl_renderer_pres_total(void) { + GL_RT_SYNC("pres_total"); return s_pres_seq; } int gl_renderer_pres_get(uint64_t seq, GlPresEvent *out) { + GL_RT_SYNC("pres_get"); if (seq >= s_pres_seq) return 0; if (s_pres_seq - seq > GL_PRES_RING_CAP) return 0; /* evicted */ *out = s_pres_ring[seq % GL_PRES_RING_CAP]; @@ -2653,10 +2729,6 @@ static void wide_target_end(GLint uXoff, GLint uXhalf) { p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); } -extern int psx_ws_prim_is_tagged(void); /* gpu.c: is the current GP0 prim sprite-tagged? */ -extern int psx_ws_prim_in_backdrop(void); /* gpu.c: is its source addr in the flower-field struct? */ -extern int gpu_ws_nw_flat_backdrop_enabled(void); /* gpu.c: per-title flat backdrop opt-in */ - /* Per-prim gate: stretch this prim iff native-wide + feature on AND the prim's * source address is inside the flower-field backdrop data structure (precise — * excludes the 3D rock/foreground, which is untagged AND has narrow prims so the @@ -2668,12 +2740,12 @@ static int bd_prim_gate(const int *xs, int n, int textured) { * Stretch those only in the native-wide mirror: the canonical framebuffer * remains byte-for-byte 4:3, while the flat backdrop reaches the reveal * margins. Opt-in because flat foreground geometry is title-dependent. */ - if (!textured && gpu_ws_nw_flat_backdrop_enabled()) return 1; - if (g_ws_bd_phase_mode != 0) return psx_ws_prim_in_backdrop(); /* default: precise address gate */ + if (!textured && ctx_flat_backdrop()) return 1; + if (g_ws_bd_phase_mode != 0) return ctx_prim_in_backdrop(); /* default: precise address gate */ /* mode 0: legacy tag+narrow heuristic (kept for comparison) */ int native_w = g_wide_w - 2 * g_wide_off; if (native_w <= 0) return 0; - if (psx_ws_prim_is_tagged()) return 0; + if (ctx_prim_tagged()) return 0; int base = g_wide_cur_base, lo = xs[0], hi = xs[0]; for (int i = 1; i < n; i++) { if (xs[i] < lo) lo = xs[i]; if (xs[i] > hi) hi = xs[i]; } if (lo < base - g_ws_bd_phase_thresh) return 0; /* into left margin -> GTE-wide */ @@ -2695,7 +2767,8 @@ static int bd_prim_gate(const int *xs, int n, int textured) { * conservative strict-inside test. 4:3 never runs any of this (g_wide_cur == 0). * Toggle via gl_wide_fast for A/B; default ON. */ static int s_wide_fast = 1; -void gl_renderer_set_wide_fast(int on) { s_wide_fast = on ? 1 : 0; } +void gl_renderer_set_wide_fast(int on) { + GL_RT_SYNC("set_wide_fast"); s_wide_fast = on ? 1 : 0; } int gl_renderer_get_wide_fast(void) { return s_wide_fast; } static int wide_fast_center_valid(void) { /* An explicitly stretched sky differs inside the canonical viewport too. @@ -2703,8 +2776,7 @@ static int wide_fast_center_valid(void) { * draws. Tags are installed before DMA, so no earlier center draws skip. */ // The native split seam occupies canonical edge columns that the expanded // camera renders as world. Copying those columns reinstates the divider. - return s_wide_fast && gpu_ws_netplay_local_viewport_width() <= 0 && - !gpu_ws_background_requires_full_composite(); + return s_wide_fast && ctx_vp_width() <= 0 && !ctx_bg_full(); } /* The one predicate for "the wide surface's centre columns [g_wide_off, * g_wide_off+native_w) are the canonical framebuffer, unshifted". A camera view @@ -2887,10 +2959,12 @@ static uint64_t s_batch_total = 0, s_batch_reason[7]; * window therefore stays a batch key while the check is on, and this mode * never makes a batch longer than the default mode would there. */ static int s_twin_batching = 0; -void gl_renderer_set_texture_window_batching(int on) { s_twin_batching = on ? 1 : 0; } +void gl_renderer_set_texture_window_batching(int on) { + GL_RT_SYNC("set_texture_window_batching"); s_twin_batching = on ? 1 : 0; } int gl_renderer_get_texture_window_batching(void) { return s_twin_batching; } void gl_renderer_batch_diag(uint64_t out[8]) { + GL_RT_SYNC("batch_diag"); out[0] = s_batch_total; for (int i = 0; i < 7; i++) out[i + 1] = s_batch_reason[i]; } @@ -4169,10 +4243,23 @@ static void native_draw_end(int previous) { sw_set_faithful_authority(previous); static inline void precise_consumed(void) { s_pc_valid = 0; s_pq_valid = 0; s_projected_uv_valid = 0; } int gl_renderer_projective_supported(void) { + if (s_rth_on && !rt_on_render_thread() && !rt_held()) + return s_raster_ok && s_rthm_cur && !cpu_vram_authoritative(); return s_raster_ok && g_wide_cur && !cpu_vram_authoritative(); } void gl_renderer_draw_projected_triangle(const PSXProjectedVertex vertices[3], uint16_t texpage, uint16_t cx, uint16_t cy, int raw, int semi, int perspective) { + if (rth_record_mode()) { + uint8_t *p = (uint8_t *)rt_cmd_begin(RTH_PROJ_TRI, rth_prim_flags(), + 3u * sizeof(PSXProjectedVertex) + 24u); + if (p) { + const int32_t a[6] = { texpage, cx, cy, raw, semi, perspective }; + memcpy(p, vertices, 3u * sizeof(PSXProjectedVertex)); + memcpy(p + 3u * sizeof(PSXProjectedVertex), a, sizeof a); + rt_cmd_commit(); + return; + } + } PSXProjectedVertex polygon[12]; const double left=fmin(s_area_x1, -wide_dx())-1.0; const double right=fmax(s_area_x2+1, g_wide_w-wide_dx())+1.0; @@ -4343,6 +4430,7 @@ static int glb_render_display_hires(uint32_t *o,int p,int dx,int dy,int dw,int * the GL surface cannot serve, keeps the mirror resolve. The backend's * render_display_hires (present fallback, screenshot_hires) is unchanged. */ int gl_renderer_capture_display_hires(uint32_t *o, int p, int dx, int dy, int dw, int dh) { + GL_RT_SYNC("capture_display_hires"); /* A pitch shorter than one scaled row would make either path below write * rows over each other and past a pitch*height buffer. */ if (!o || dw <= 0 || dh <= 0 || (int64_t)p < (int64_t)dw * s_out_scale * 4) @@ -4363,7 +4451,7 @@ int gl_renderer_capture_display_hires(uint32_t *o, int p, int dx, int dy, int dw * On leave: clear the skipped FB union in the FBO — do NOT restage CPU RGB888 * as 1555 (that painted MotK title rainbow/static). */ static int depth24_is_fb_transfer(int x, int y, int w, int h) { - if (!gpu_display_is_depth24() || w <= 0 || h <= 0) return 0; + if (!ctx_depth24() || w <= 0 || h <= 0) return 0; GpuDisplayInfo di; gpu_get_display_info(&di); int fb_w = (int)((di.width * 3u + 1u) / 2u); /* RGB W → halfwords */ @@ -4395,7 +4483,7 @@ static int depth24_is_fb_transfer(int x, int y, int w, int h) { static void depth24_mark_scanout_band(void) { GpuDisplayInfo di; int fb_w, fb_h, x0, y0, x1, y1; - if (!gpu_display_is_depth24()) return; + if (!ctx_depth24()) return; gpu_get_display_info(&di); fb_w = (int)((di.width * 3u + 1u) / 2u); fb_h = (int)di.height; @@ -4449,7 +4537,7 @@ static void depth24_clear_skipped_fb(void) { static void depth24_upload_policy(void) { if (!s_raster_ok) return; - int d24 = gpu_display_is_depth24(); + int d24 = ctx_depth24(); if (d24 && !s_depth24_skip_up) { /* Entering 24-bit: from here the frame is presented from the CPU * mirror, and MDEC only writes the movie's own rows. A letterboxed @@ -4537,6 +4625,7 @@ static void upload_present_tex(const uint32_t *pixels, int w, int h, int linear) static int s_fmv_filter_cfg = 0; /* VIDEO_FMV_FILTER_NEAREST */ void gl_renderer_set_fmv_filter(int cfg_value) { + GL_RT_SYNC("set_fmv_filter"); if (cfg_value >= 0 && cfg_value <= 3) s_fmv_filter_cfg = cfg_value; } @@ -4613,6 +4702,7 @@ static void present_set_scanline(GLint uOn, GLint uStr, GLint uLines, static int s_aspect_num = 4, s_aspect_den = 3; void gl_renderer_set_display_aspect(int num, int den) { + GL_RT_SYNC("set_display_aspect"); if (num <= 0 || den <= 0) { num = 4; den = 3; } s_aspect_num = num; s_aspect_den = den; } @@ -4622,6 +4712,7 @@ void gl_renderer_set_display_aspect(int num, int den) { * actual darkening is applied per-draw in the present/interpolation shaders and * fades in with output scale — see PSX_SCANLINE_FUNC. */ void gl_renderer_set_scanlines(int on, float strength) { + GL_RT_SYNC("set_scanlines"); s_scanline_on = on ? 1 : 0; if (strength < 0.f) strength = 0.f; if (strength > 1.f) strength = 1.f; @@ -4634,6 +4725,7 @@ int gl_renderer_get_scanlines(float *strength) { } void gl_renderer_set_post_gamma(float gamma) { + GL_RT_SYNC("set_post_gamma"); if (!isfinite(gamma)) gamma = 1.0f; if (gamma < 0.5f) gamma = 0.5f; @@ -5025,6 +5117,7 @@ static int init_gpu_raster(void) { int gl_renderer_texture_banks_supported(void) { return s_raster_ok && !s_cpu_auth_dual && !s_hd_native_authority; } int gl_renderer_fit_wide_aspect(int disp_w, int *num, int *den) { + GL_RT_SYNC("fit_wide_aspect"); if (!s_raster_ok || s_gl_max_dim <= 0) return 0; /* Against the allocation (the dynamic-resolution ceiling): the aspect * must not change when the level steps. */ @@ -5033,6 +5126,7 @@ int gl_renderer_fit_wide_aspect(int disp_w, int *num, int *den) { } int gl_renderer_scale_info(GlScaleInfo *out) { + GL_RT_SYNC("scale_info"); if (!out) return 0; memset(out, 0, sizeof(*out)); out->requested = s_req_scale; @@ -5131,11 +5225,13 @@ static int gl_read_display_argb(int x, int y, int w, int h, uint32_t *out, int gl_renderer_read_display_hires(int x, int y, int w, int h, uint32_t *out, int cap_px, int *ow, int *oh) { + GL_RT_SYNC("read_display_hires"); return gl_read_display_argb(x, y, w, h, out, w * s_out_scale * 4, cap_px, ow, oh); } int gl_renderer_select_texture_bank(uint16_t id) { + GL_RT_SYNC("select_texture_bank"); uint32_t width, height; const uint16_t* pixels; GLint alignment, row_length; @@ -5170,6 +5266,7 @@ int gl_renderer_select_texture_bank(uint16_t id) { } int gl_renderer_select_texture_bank_live_clut(uint16_t id) { + GL_RT_SYNC("select_texture_bank_live_clut"); if (!gl_renderer_select_texture_bank(id)) return 0; s_selected_bank_live_clut = id != 0; return 1; @@ -5265,6 +5362,7 @@ int gl_renderer_init_context(SDL_Window *win) { * Safe to call before or after context creation; applies live when a context * exists. Adaptive falls back to vsync if unsupported. */ void gl_renderer_set_swap_interval(int interval) { + GL_RT_SYNC("set_swap_interval"); s_swap_interval = interval; if (s_ctx) { if (SDL_GL_SetSwapInterval(interval) != 0 && interval < 0) { @@ -5274,6 +5372,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 defined(PSX_SDL3) int interval=0; @@ -5286,6 +5385,7 @@ int gl_renderer_get_swap_interval(void) { static void pass_resources_release(void); void gl_renderer_shutdown(void) { + gl_renderer_render_thread_stop(); /* the context comes back to this thread */ s_native_surface_enabled=s_native_surface_pending=0; gl_renderer_clear_hd_texture_cache(); pass_resources_release(); @@ -5332,6 +5432,7 @@ void gl_renderer_shutdown(void) { * widescreen presents them pillarboxed instead of distorted. */ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear, int force_4_3, int content_w) { + GL_RT_SYNC("present"); if (!s_ctx) return; interp_reset_history(); int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); @@ -5424,6 +5525,7 @@ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linea } void gl_renderer_present_blank(void) { + GL_RT_SYNC("present_blank"); if (!s_ctx) return; interp_reset_history(); int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); @@ -5442,10 +5544,12 @@ void gl_renderer_present_blank(void) { /* Sync the authoritative FBO down into CPU VRAM (no-op when current). * Screenshots / debug server. Not for 24-bit FMV scanout (see flush). */ void gl_renderer_sync_cpu(void) { + GL_RT_SYNC("sync_cpu"); ensure_cpu(); } void gl_renderer_invalidate_present(void) { + GL_RT_SYNC("invalidate_present"); stereo_invalidate(); for (int i = 0; i < PRES_ROWS; i++) s_present_dirty[i] = ~0ull; s_last_present_path = -1; @@ -5455,6 +5559,7 @@ void gl_renderer_invalidate_present(void) { } void gl_renderer_restage_vram_after_savestate(void) { + GL_RT_SYNC("restage_vram_after_savestate"); gpu_hd_textures_reset_tracking(); if (!s_raster_ok || !s_vram) return; /* Belt-and-suspenders after boot_state VRAM apply: force CPU mirror → FBO @@ -5465,7 +5570,7 @@ void gl_renderer_restage_vram_after_savestate(void) { s_depth24_skip_up = 0; up_add_transfer(0, 0, VRAM_W, VRAM_H); flush_cpu_upload(); - if (gpu_display_is_depth24()) { + if (ctx_depth24()) { s_depth24_skip_up = 1; depth24_mark_scanout_band(); } @@ -5477,6 +5582,7 @@ void gl_renderer_restage_vram_after_savestate(void) { } void gl_renderer_set_cpu_auth_dual(int on) { + GL_RT_SYNC("set_cpu_auth_dual"); s_cpu_auth_dual = on ? 1 : 0; if (s_cpu_auth_dual) { s_gpu_dirty = 0; @@ -5506,6 +5612,7 @@ int gl_renderer_cpu_auth_dual(void) { } void gl_renderer_present_probe_reset(void) { + GL_RT_SYNC("present_probe_reset"); s_probe_skip = 0; s_probe_swap = 0; s_probe_dirty_marks = 0; @@ -5514,6 +5621,7 @@ void gl_renderer_present_probe_reset(void) { void gl_renderer_present_probe_take(uint64_t *skip_delta, uint64_t *swap_delta, uint64_t *dirty_mark_delta, int *force_remaining) { + GL_RT_SYNC("present_probe_take"); if (skip_delta) { *skip_delta = s_probe_skip; s_probe_skip = 0; } if (swap_delta) { *swap_delta = s_probe_swap; s_probe_swap = 0; } if (dirty_mark_delta) { @@ -5524,11 +5632,13 @@ void gl_renderer_present_probe_take(uint64_t *skip_delta, uint64_t *swap_delta, } int gl_renderer_present_rect_dirty(int disp_x, int disp_y, int w, int h) { + GL_RT_SYNC("present_rect_dirty"); if (!s_raster_ok || w <= 0 || h <= 0) return 0; return present_dirty_test(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1); } void gl_renderer_flush_cpu_uploads(void) { + GL_RT_SYNC("flush_cpu_uploads"); if (!s_raster_ok) return; flush_flat_batch(); flush_tex_batch(); @@ -5540,6 +5650,7 @@ void gl_renderer_flush_cpu_uploads(void) { * VRAM — lets a probe diff FBO truth against CPU truth. Returns 0 when the * GL pipeline is inactive (software backend). */ int gl_renderer_fbo_peek(int x, int y, int w, int h, uint16_t *out) { + GL_RT_SYNC("fbo_peek"); if (!s_raster_ok || !s_ctx) return 0; if (x < 0 || y < 0 || w < 1 || h < 1 || x + w > VRAM_W || y + h > VRAM_H) return 0; @@ -5555,6 +5666,21 @@ int gl_renderer_fbo_peek(int x, int y, int w, int h, uint16_t *out) { return 1; } +/* fbo_peek at this point of the command stream without a sync point: with + * the render thread recording, the readback is queued and lands in out when + * the render thread reaches it (a reader of out must sync first). Otherwise + * it is gl_renderer_fbo_peek. Used by the per-frame display ring. */ +int gl_renderer_fbo_peek_deferred(int x, int y, int w, int h, uint16_t *out) { + if (rth_record_mode()) { + if (!s_raster_ok || !s_ctx || x < 0 || y < 0 || w < 1 || h < 1 || + x + w > VRAM_W || y + h > VRAM_H) return 0; + uint64_t ptr = (uint64_t)(uintptr_t)out; + RTH_REC(RTH_PEEK, 0, x, y, w, h, (int32_t)(ptr & 0xFFFFFFFFu), (int32_t)(ptr >> 32)); + return 1; + } + return gl_renderer_fbo_peek(x, y, w, h, out); +} + /* Diagnostic (debug server "gl_vram_diff"): full-VRAM comparison of the * GPU-side truth (FBO via pack) against the CPU array, WITHOUT writing * either. Reports mismatch count + bounding box + a few sample coords. @@ -5562,6 +5688,7 @@ int gl_renderer_fbo_peek(int x, int y, int w, int h, uint16_t *out) { * at upload-only scenes the two must match exactly. */ int gl_renderer_vram_diff(uint32_t *count, int bbox[4], int samples[8][2], uint16_t samples_px[8][2]) { + GL_RT_SYNC("vram_diff"); if (!s_raster_ok || !s_ctx) return 0; uint16_t *tmp = (uint16_t *)malloc((size_t)VRAM_W * VRAM_H * 2); if (!tmp) return 0; @@ -5599,6 +5726,7 @@ int gl_renderer_vram_diff(uint32_t *count, int bbox[4], /* Diagnostic state for the debug server: coherency flags + dirty rects. */ void gl_renderer_diag(int *gpu_dirty, int pending[5], int pack[5]) { + GL_RT_SYNC("diag"); if (gpu_dirty) *gpu_dirty = s_gpu_dirty; if (pending) { /* [0] = pending rect count; [1..4] = union bbox (diagnostic only — @@ -6056,7 +6184,8 @@ static void gl_perf_present_enter(void) { g_bdg_applied = s_bdg_applied; g_bdg_prims = s_bdg_prims; g_bdg_clearx = s_bdg_clearx; g_bdg_cur = (g_wide_cur != 0); g_bdg_base = g_wide_cur_base; g_bdg_w = g_wide_w; g_bdg_off = g_wide_off; s_bdg_applied = 0; s_bdg_prims = 0; s_bdg_clearx = -999999; - { extern void psx_ws_dbg_gate_frame_snapshot(void); psx_ws_dbg_gate_frame_snapshot(); } + /* Replay: taken on the emulation thread when the present was recorded. */ + { extern void psx_ws_dbg_gate_frame_snapshot(void); if (!rth_replaying()) psx_ws_dbg_gate_frame_snapshot(); } if (!s_pf_on) return; uint64_t now = SDL_GetPerformanceCounter(); s_pf_enter = now; @@ -6130,11 +6259,13 @@ static void gl_perf_present_exit(int wide) { /* Cumulative textured fraction of scene prims (decides flat vs textured batching * priority). out_tex_frac = textured/total since boot; returns total prim count. */ uint64_t gl_renderer_perf_prim_split(double *out_tex_frac) { + GL_RT_SYNC("perf_prim_split"); if (out_tex_frac) *out_tex_frac = s_scene_prims ? (double)s_scene_prims_tex / (double)s_scene_prims : 0.0; return s_scene_prims; } int gl_renderer_perf_aggregate(int wide_filter, double out[18]) { + GL_RT_SYNC("perf_aggregate"); for (int i = 0; i < 18; i++) out[i] = 0.0; if (!s_pf_on) return 0; int navail = (int)(s_pf_ring_seq < (uint64_t)GLPERF_RING ? s_pf_ring_seq : GLPERF_RING); @@ -6168,7 +6299,8 @@ int gl_renderer_perf_aggregate(int wide_filter, double out[18]) { } /* Native-wide mirror ablation (perf attribution): see s_ws_ablate. */ -void gl_renderer_set_ws_ablate(int mode) { s_ws_ablate = (mode >= 0 && mode <= 3) ? mode : 0; } +void gl_renderer_set_ws_ablate(int mode) { + GL_RT_SYNC("set_ws_ablate"); s_ws_ablate = (mode >= 0 && mode <= 3) ? mode : 0; } int gl_renderer_get_ws_ablate(void) { return s_ws_ablate; } static void interp_reset_history_unlocked(void) { @@ -6190,6 +6322,7 @@ static void interp_reset_history(void) { void gl_renderer_set_interpolation(int enabled, double host_hz, double target_hz, double source_hz, int blend_mode) { + GL_RT_SYNC("set_interpolation"); double effective_hz = target_hz > 0.0 ? target_hz : host_hz; if (effective_hz < source_hz) effective_hz = source_hz; int active = (enabled && source_hz >= 1.0 && source_hz <= 1000.0 && @@ -6219,17 +6352,20 @@ void gl_renderer_set_interpolation(int enabled, double host_hz, double target_hz } void gl_renderer_set_interpolation_blend(int blend_mode) { + GL_RT_SYNC("set_interpolation_blend"); s_interp_blend_mode = blend_mode == 1 ? 1 : 0; s_interp_hold = blend_mode == 2 ? 1 : 0; } void gl_renderer_set_interpolation_suspended(int suspended) { + if (rth_record_mode()) { RTH_REC(RTH_INTERP_SUSPENDED, 0, suspended ? 1 : 0); return; } suspended = suspended ? 1 : 0; if (suspended != s_interp_suspended) interp_reset_history_unlocked(); s_interp_suspended = suspended; } void gl_renderer_set_interpolation_source(int source) { + GL_RT_SYNC("set_interpolation_source"); source = source == 1 ? 1 : 0; if (source != s_interp_source) interp_reset_history_unlocked(); s_interp_source = source; @@ -6241,6 +6377,7 @@ void gl_renderer_set_interpolation_source(int source) { void gl_renderer_interpolation_source_diag(int *source, uint32_t *flip_period, uint64_t *captures, uint64_t *duplicates) { + GL_RT_SYNC("interpolation_source_diag"); if (source) *source = s_interp_source; if (flip_period) *flip_period = s_interp_flip.period; if (captures) *captures = s_interp_captures; @@ -6266,6 +6403,7 @@ void gl_renderer_interpolation_diag(int *enabled, int *suspended, int *history_frames, double *host_hz, double *target_hz, uint64_t *swaps) { + GL_RT_SYNC("interpolation_diag"); if (enabled) *enabled = s_interp_enabled; if (suspended) *suspended = s_interp_suspended; if (history_frames) *history_frames = s_interp_valid; @@ -6607,6 +6745,7 @@ static int s_pass_force_refuse = -1; /* -1: read PSX_RENDER_PASS_REFUSE */ static GLRenderPassBeginDiag s_pass_begin_diag; void gl_renderer_pass_begin_diag(GLRenderPassBeginDiag *out) { + GL_RT_SYNC("pass_begin_diag"); if (out) *out = s_pass_begin_diag; } @@ -6626,6 +6765,7 @@ static uint32_t pass_gl_errors(void) { } void gl_renderer_pass_force_refuse(int on) { + GL_RT_SYNC("pass_force_refuse"); s_pass_force_refuse = on ? 1 : 0; } @@ -6640,6 +6780,7 @@ static void pass_refusal_init(void) { } uint32_t gl_renderer_pass_unavailable(void) { + GL_RT_SYNC("pass_unavailable"); pass_refusal_init(); if (!s_ctx || !s_raster_ok || !s_interp_enabled || s_interp_suspended || s_interp_source != 1 || !(s_interp_source_hz > 0.0)) @@ -6660,6 +6801,7 @@ uint32_t gl_renderer_pass_unavailable(void) { } int gl_renderer_pass_ready(void) { + GL_RT_SYNC("pass_ready"); return gl_renderer_pass_unavailable() == PSX_MOD_RENDER_PASS_READY; } @@ -6676,6 +6818,7 @@ uint32_t gl_renderer_pass_plan(uint32_t period_vblanks, uint32_t shown_after_vblanks, uint32_t *alpha_q16, uint32_t max, uint32_t *wanted) { + GL_RT_SYNC("pass_plan"); RenderPassPlanInput in; double freq, sp, spare; uint32_t cap; @@ -6760,6 +6903,7 @@ void gl_renderer_pass_plan_diag(GLRenderPassPlanDiag *out) { } void gl_renderer_pass_note_cost(uint64_t ticks) { + GL_RT_SYNC("pass_note_cost"); if (s_pass_cost_w != s_interp_w || s_pass_cost_h != s_interp_h) { memset(&s_pass_cost, 0, sizeof s_pass_cost); s_pass_cost_w = s_interp_w; @@ -6773,8 +6917,10 @@ void gl_renderer_pass_note_cost(uint64_t ticks) { if (s_dyn_on) s_dyn_ledger.pass_ticks += ticks; } -uint32_t gl_renderer_pass_leaks(void) { return s_pass_leaks; } -int gl_renderer_pass_verify_vram(void) { return s_pv_ok; } +uint32_t gl_renderer_pass_leaks(void) { + GL_RT_SYNC("pass_leaks"); return s_pass_leaks; } +int gl_renderer_pass_verify_vram(void) { + GL_RT_SYNC("pass_verify_vram"); return s_pv_ok; } static GLuint pass_wide_fbo_for(int base_x) { @@ -7212,9 +7358,11 @@ uint32_t gl_renderer_local_view_unavailable(void) { return PSX_MOD_RENDER_PASS_READY; } int gl_renderer_local_view_begin(int x, int y, int w, int h) { + GL_RT_SYNC("local_view_begin"); return transaction_begin(x, y, w, h, 0, 0, 0, 2); } int gl_renderer_local_view_end(int keep) { + GL_RT_SYNC("local_view_end"); if (!s_pass_active || !s_pass_local) return 0; flush_flat_batch(); flush_tex_batch(); flush_cpu_upload(); transaction_restore_ex(keep ? 1 : 0); @@ -7224,12 +7372,15 @@ int gl_renderer_local_view_end(int keep) { int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen, uint32_t period, int reuse) { + GL_RT_SYNC("pass_begin"); return transaction_begin(x, y, w, h, open_gen, period, reuse, 0); } int gl_renderer_stereo_begin(int x, int y, int w, int h, int reuse) { + GL_RT_SYNC("stereo_begin"); return transaction_begin(x, y, w, h, 0, 0, reuse, 1); } void gl_renderer_pass_end(uint32_t alpha_q16, int keep) { + GL_RT_SYNC("pass_end"); int gi = 1 - s_pgen_cur; PassGen *g = &s_pgen[gi]; if (!s_pass_active) return; @@ -7252,14 +7403,16 @@ void gl_renderer_pass_end(uint32_t alpha_q16, int keep) { } uint32_t gl_renderer_stereo_unavailable(void) { + GL_RT_SYNC("stereo_unavailable"); pass_refusal_init(); - if (!s_ctx || !s_raster_ok || !s_hr_fbo || gpu_display_is_depth24()) + if (!s_ctx || !s_raster_ok || !s_hr_fbo || ctx_depth24()) return PSX_MOD_RENDER_PASS_NO_PRESENTER; if (s_cpu_auth_dual || s_hd_native_authority || s_hiw || g_wide_w > 0 || s_pass_force_refuse > 0) return PSX_MOD_RENDER_PASS_BACKEND; return PSX_MOD_RENDER_PASS_READY; } void gl_renderer_stereo_stage_reset(void) { + GL_RT_SYNC("stereo_stage_reset"); s_stereo_pair[1 - s_stereo_current].mask = 0; } static void stereo_invalidate(void) { @@ -7267,11 +7420,13 @@ static void stereo_invalidate(void) { s_stereo_pair[0].mask = s_stereo_pair[1].mask = 0; } void gl_renderer_stereo_reset(void) { + GL_RT_SYNC("stereo_reset"); stereo_invalidate(); s_stereo_mode = 0; s_stereo_dump_left = 0; s_stereo_capture_count = 0; s_stereo_presents = 0; } int gl_renderer_stereo_set_presentation(uint32_t mode) { + GL_RT_SYNC("stereo_set_presentation"); if (mode > 1u) return 0; s_stereo_mode = (int)mode; return 1; @@ -7285,6 +7440,7 @@ static void stereo_resources_release(void) { stereo_invalidate(); } int gl_renderer_stereo_end(uint32_t eye, int keep) { + GL_RT_SYNC("stereo_end"); StereoPair *p = &s_stereo_pair[1 - s_stereo_current]; PassGen g; int ok = 0, tw = s_pass_w * s_hr_scale, th = s_pass_h * s_hr_scale; @@ -7306,6 +7462,7 @@ int gl_renderer_stereo_end(uint32_t eye, int keep) { return keep ? ok : 1; } void gl_renderer_stereo_diag(GLRenderStereoDiag *out) { + GL_RT_SYNC("stereo_diag"); StereoPair *p = &s_stereo_pair[s_stereo_current]; if (!out) return; memset(out, 0, sizeof *out); @@ -7316,12 +7473,14 @@ void gl_renderer_stereo_diag(GLRenderStereoDiag *out) { out->presents = s_stereo_presents; } void gl_renderer_stereo_dump_arm(const char *dir, int pairs) { + GL_RT_SYNC("stereo_dump_arm"); if (!dir || !*dir || pairs <= 0) { s_stereo_dump_left = 0; return; } snprintf(s_stereo_dump_dir, sizeof s_stereo_dump_dir, "%s", dir); s_stereo_dump_left = pairs > STEREO_CAPTURE_MAX ? STEREO_CAPTURE_MAX : pairs; s_stereo_capture_count = 0; } uint32_t gl_renderer_stereo_capture_records(GLRenderStereoCapture *out, uint32_t capacity) { + GL_RT_SYNC("stereo_capture_records"); uint32_t count = s_stereo_capture_count; if (!out) return 0; if (count > capacity) count = capacity; @@ -7361,6 +7520,7 @@ static void stereo_dump(const StereoPair *p) { free(rgb); free(sbs); s_stereo_dump_left--; } int gl_renderer_stereo_publish(uint64_t id, uint64_t cycle, const int32_t view[2][3]) { + GL_RT_SYNC("stereo_publish"); StereoPair *p = &s_stereo_pair[1 - s_stereo_current]; if (p->mask != 3u || p->tw[0] != p->tw[1] || p->th[0] != p->th[1]) return 0; p->id = id; p->cycle = cycle; @@ -7372,16 +7532,20 @@ int gl_renderer_stereo_publish(uint64_t id, uint64_t cycle, const int32_t view[2 static uint64_t s_xr_begin_pair; int psx_mod_openxr_enable(int enabled) { + GL_RT_SYNC("openxr_enable"); PSXOpenXRStats s; psx_openxr_stats(&s); if (s_pass_active || s.frame_open) return 0; return psx_openxr_enable(enabled); } -void psx_mod_openxr_recenter(void) { psx_openxr_recenter(); } +void psx_mod_openxr_recenter(void) { + GL_RT_SYNC("openxr_recenter"); psx_openxr_recenter(); } int psx_mod_openxr_quad(double distance,double width,double height) { + GL_RT_SYNC("openxr_quad"); if(s_pass_active)return 0; return psx_openxr_quad(distance,width,height); } int psx_mod_openxr_native_surface(double distance,double width,double units) { + GL_RT_SYNC("openxr_native_surface"); PSXOpenXRStats stats;psx_openxr_stats(&stats); if(s_pass_active || stats.frame_open || !stats.compiled || !isfinite(distance) || !isfinite(width) || !isfinite(units))return 0; @@ -7391,19 +7555,23 @@ int psx_mod_openxr_native_surface(double distance,double width,double units) { return 1; } int psx_mod_openxr_begin(uint32_t width, uint32_t height, double units) { + GL_RT_SYNC("openxr_begin"); if (s_native_surface_enabled || s_pass_active || gl_renderer_stereo_unavailable() != PSX_MOD_RENDER_PASS_READY || !width || !height || width > VRAM_W || height > VRAM_H) return 0; s_xr_begin_pair = s_stereo_valid ? s_stereo_pair[s_stereo_current].id : 0; return psx_openxr_begin((int)width, (int)height, units); } int psx_mod_openxr_view(uint32_t eye, PSXModRenderView *view) { + GL_RT_SYNC("openxr_view"); return psx_openxr_view(eye, view); } int psx_mod_openxr_input(PSXModOpenXRInput *input) { + GL_RT_SYNC("openxr_input"); if (s_pass_active) return 0; return psx_openxr_input(input); } int psx_mod_openxr_hands(PSXModOpenXRHands *hands) { + GL_RT_SYNC("openxr_hands"); return psx_openxr_hands(hands); /* Snapshot only, also safe during replay. */ } /* The target stores display-encoded RGB for an sRGB swapchain, or explicitly @@ -7476,6 +7644,7 @@ static int openxr_copy_eye(uint32_t eye, uint32_t texture, int w, int h) { p_glDeleteFramebuffers(1, &target);return ok; } int psx_mod_openxr_end(int rendered) { + GL_RT_SYNC("openxr_end"); int fresh = rendered && s_stereo_valid && !s_pass_active && s_stereo_pair[s_stereo_current].id != s_xr_begin_pair; if (fresh) psx_openxr_pair_metadata(s_stereo_pair[s_stereo_current].id, @@ -7614,6 +7783,7 @@ static int pass_gen_present(uint64_t deadline) { } void gl_renderer_pass_service_presents(void) { + GL_RT_SYNC("pass_service_presents"); uint64_t deadline; double sp; if (!gl_renderer_pass_ready() || s_pass_active) return; @@ -7630,6 +7800,7 @@ void gl_renderer_pass_service_presents(void) { } void gl_renderer_pass_diag(uint64_t out[10]) { + GL_RT_SYNC("pass_diag"); out[0] = s_pgen_promotions; out[1] = s_pgen_presents; out[2] = s_pgen_blends; @@ -7643,10 +7814,13 @@ void gl_renderer_pass_diag(uint64_t out[10]) { out[9] = s_pass_cost_rewarms; } -uint64_t gl_renderer_pass_journaled(void) { return s_pj_total; } -uint64_t gl_renderer_pass_backups_reused(void) { return s_pb_reused; } +uint64_t gl_renderer_pass_journaled(void) { + GL_RT_SYNC("pass_journaled"); return s_pj_total; } +uint64_t gl_renderer_pass_backups_reused(void) { + GL_RT_SYNC("pass_backups_reused"); return s_pb_reused; } uint32_t gl_renderer_pass_image_textures(uint64_t *bytes) { + GL_RT_SYNC("pass_image_textures"); uint32_t n = s_pgen_alloc_n[0] + s_pgen_alloc_n[1]; if (bytes) { *bytes = 0; @@ -7668,7 +7842,8 @@ static uint64_t s_present_ticks_accum_fwd(uint64_t add) { return s_present_ticks_accum; } -void gl_renderer_pass_set_flip_shown(int shown) { s_pass_flip_shown = shown ? 1 : 0; } +void gl_renderer_pass_set_flip_shown(int shown) { + GL_RT_SYNC("pass_set_flip_shown"); s_pass_flip_shown = shown ? 1 : 0; } /* FLIP source saw a new frame: it is the flip a pending generation was built * for (render_pass_gen_flip_matches) or a frame without passes. */ @@ -7694,6 +7869,7 @@ static char s_pdump_dir[400]; static int s_pdump_left = 0; static uint64_t s_pdump_gen = 0; void gl_renderer_pass_dump_arm(const char *dir, int generations) { + GL_RT_SYNC("pass_dump_arm"); if (!dir || !*dir || generations <= 0) { s_pdump_left = 0; return; } snprintf(s_pdump_dir, sizeof s_pdump_dir, "%s", dir); s_pdump_left = generations; @@ -7893,6 +8069,35 @@ static void present_image_ring_capture_gl(void) { } #endif +/* Host overlays (OSD text, volume bar, rewind strip, savestate menu) are + * rasterized by the emulation thread. A recorded present carries a copy taken + * at record time; replay draws that copy (ov_* below), never the live ones. */ +static struct { + int valid, needs_present; + const uint32_t *px[4]; + int w[4], h[4]; + float slide; +} s_rth_ov; +static int ov_image(int i, const uint32_t **px, int *w, int *h) { + if (rth_replaying() && s_rth_ov.valid) { + *px = s_rth_ov.px[i]; *w = s_rth_ov.w[i]; *h = s_rth_ov.h[i]; + return s_rth_ov.px[i] != NULL; + } + switch (i) { + case 0: return host_osd_image(px, w, h); + case 1: return host_osd_volume_image(px, w, h); + case 2: return psx_rewind_overlay_image(px, w, h); + default: return psx_savestate_menu_overlay_image(px, w, h); + } +} +static float ov_rewind_slide(void) { + return (rth_replaying() && s_rth_ov.valid) ? s_rth_ov.slide : psx_rewind_slide(); +} +static int ov_needs_present(void) { + return (rth_replaying() && s_rth_ov.valid) ? s_rth_ov.needs_present + : host_osd_needs_present(); +} + static void gl_swap_with_osd(void) { openxr_present_native(); /* Copy guest content before host-only overlays. */ s_native_surface_pending=0; /* Hold-last/resim cannot reuse a native source. */ @@ -7911,17 +8116,17 @@ static void gl_swap_with_osd(void) { if (ui < 1) ui = 1; if (ui > 8) ui = 8; margin = 8 * ui; - if (host_osd_image(&px, &ow, &oh) && px) + if (ov_image(0, &px, &ow, &oh) && px) gl_draw_osd_image(px, ow, oh, ow * ui, oh * ui, margin, margin, ww, wh); - if (host_osd_volume_image(&px, &ow, &oh) && px) { + if (ov_image(1, &px, &ow, &oh) && px) { const int dw = ow * ui, dh = oh * ui; int vx = (ww > dw + margin) ? (ww - dw - margin) : margin; int vy = (wh > dh) ? ((wh - dh) / 2) : margin; gl_draw_osd_image(px, ow, oh, dw, dh, vx, vy, ww, wh); } - if (psx_rewind_overlay_image(&px, &ow, &oh) && px) { - float slide = psx_rewind_slide(); + if (ov_image(2, &px, &ow, &oh) && px) { + float slide = ov_rewind_slide(); int dw = ww; int dh = (wh * oh) / 480; int vy; @@ -7929,11 +8134,11 @@ static void gl_swap_with_osd(void) { vy = wh - (int)((float)dh * slide + 0.5f); gl_draw_osd_image(px, ow, oh, dw, dh, 0, vy, ww, wh); } - if (psx_savestate_menu_overlay_image(&px, &ow, &oh) && px) + if (ov_image(3, &px, &ow, &oh) && px) gl_draw_osd_image(px, ow, oh, ww, wh, 0, 0, ww, wh); } } - host_osd_present_done(); + if (!rth_replaying()) host_osd_present_done(); /* replay: done at record */ /* present_shot (GL backend): the default framebuffer now holds the composed * frame — display quad fitted to the window, plus OSD — so this is the only * capture that carries the presented aspect. Buffer-level captures resolve @@ -8073,6 +8278,7 @@ static void present_target_quad(GLuint tex, float tex_w, float tex_h, static GLuint s_bezel_tex = 0; int gl_renderer_set_bezel(const void *rgba, int w, int h) { + GL_RT_SYNC("set_bezel"); if (s_bezel_tex) { glDeleteTextures(1, &s_bezel_tex); s_bezel_tex = 0; } if (!rgba || w <= 0 || h <= 0) return 1; if (!s_ctx || !s_raster_ok) return 0; @@ -8115,6 +8321,7 @@ static void present_bezel(int ww, int wh, int lx, int ly, int lw, int lh) { } int gl_renderer_present_hold_last(void) { + GL_RT_SYNC("present_hold_last"); int ww = 0, wh = 0; int lx, ly, lw, lh; if (!s_ctx || !s_win || s_hold_kind == HOLD_NONE || !s_hold_tex) @@ -8184,6 +8391,11 @@ static float present_alloc_extent(int units, int scale) { static void present_vram_impl(int disp_x, int disp_y, int w, int h, int linear, int force_4_3) { + if (rth_record_mode()) { + const int32_t a[6] = { disp_x, disp_y, w, h, linear, force_4_3 }; + if (rth_record_present(RTH_PRESENT_VRAM, 6, a)) return; + gl_rth_acquire("present_oversize"); + } if (!s_ctx || !s_raster_ok) return; flush_flat_batch(); flush_tex_batch(); @@ -8194,7 +8406,7 @@ static void present_vram_impl(int disp_x, int disp_y, int w, int h, int linear, s_last_dx == disp_x && s_last_dy == disp_y && s_last_dw == w && s_last_dh == h && !present_dirty_test(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1) && - !host_osd_needs_present() && + !ov_needs_present() && !psx_present_vsync_owns_cadence() && !gl_renderer_interpolation_owns_cadence()) { s_probe_skip++; @@ -8356,6 +8568,13 @@ static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h * bottom-origin → window top). Returns 0 if there's no wide surface for base_x * (caller falls back). disp_x is the displayed buffer base (the wide-surface key). */ static int present_wide_fbo_impl(int disp_x, int disp_y, int disp_h, int linear) { + if (rth_record_mode()) { + /* The caller falls back to the CPU path on 0: answer from the mirror. */ + if (!s_ctx || !s_raster_ok || !rth_mirror_wide_present_ok(disp_x)) return 0; + const int32_t a[4] = { disp_x, disp_y, disp_h, linear }; + if (rth_record_present(RTH_PRESENT_WIDE, 4, a)) return 1; + gl_rth_acquire("present_oversize"); + } if (!s_ctx || !s_raster_ok || g_wide_w <= 0) return 0; GLuint fbo = 0, tex = 0; for (int i = 0; i < WIDE_MAX_SURF; i++) @@ -8383,7 +8602,7 @@ static int present_wide_fbo_impl(int disp_x, int disp_y, int disp_h, int linear) s_last_dx == disp_x && s_last_dy == disp_y && s_last_dw == g_wide_w && s_last_dh == disp_h && !present_dirty_test(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1) && - !host_osd_needs_present() && + !ov_needs_present() && !psx_present_vsync_owns_cadence() && !gl_renderer_interpolation_owns_cadence()) { s_probe_skip++; @@ -8903,4 +9122,598 @@ static const GpuRenderBackend GL_BACKEND = { .wide_dump_full = glb_wide_dump_full, }; -const GpuRenderBackend *gl_backend_get(void) { return &GL_BACKEND; } +/* ==== Render thread: recording, replay, hand-off =========================== + * See the state block near the top of the file and docs/RENDER_THREAD.md. */ + +/* Emulation-side mirrors of backend state the emulation thread must answer + * without a sync point. They follow the recorded calls exactly as the backend + * will apply them, and are re-read from the backend whenever the emulation + * thread holds the context (rth_mirror_resync). */ +static int s_rthm_wide_w = 0, s_rthm_wide_off = 0; +static int s_rthm_base[WIDE_MAX_SURF]; +static int s_rthm_area[4] = { 0, 0, VRAM_W - 1, VRAM_H - 1 }; +/* Last stream state sent (RTH_STATE); -1 forces the first send. */ +static int s_rthe_flat_bd = -1, s_rthe_vp_w = -1, s_rthe_bg_full = -1; +static uint64_t s_rth_presents = 0, s_rth_presents_stale = 0; + +#define RTHF_PRIM 0x0001u +#define RTHF_TAGGED 0x0002u +#define RTHF_BD_SHIFT 2 /* bits 2-3: psx_ws_prim_in_backdrop() 0..3 */ + +static void rth_mirror_resync(void) { + s_rthm_wide_w = g_wide_w; + s_rthm_wide_off = g_wide_off; + s_rthm_cur = g_wide_cur != 0; + for (int i = 0; i < WIDE_MAX_SURF; i++) + s_rthm_base[i] = s_wide_fbo[i] ? s_wide_base[i] : -1; + s_rthm_area[0] = s_area_x1; s_rthm_area[1] = s_area_y1; + s_rthm_area[2] = s_area_x2; s_rthm_area[3] = s_area_y2; +} + +/* wide_fbo_for() as it will run on the render thread: the same lookup, limit + * check and slot allocation. Only a driver allocation failure can differ. */ +static int rth_mirror_wide_for(int base_x) { + if (s_rthm_wide_w <= 0) return 0; + for (int i = 0; i < WIDE_MAX_SURF; i++) + if (s_rthm_base[i] == base_x) return 1; + if (s_gl_max_dim > 0 && + ((int64_t)s_rthm_wide_w * s_out_scale > s_gl_max_dim || + (int64_t)VRAM_H * s_out_scale > s_gl_max_dim)) + return 0; + for (int i = 0; i < WIDE_MAX_SURF; i++) + if (s_rthm_base[i] < 0) { s_rthm_base[i] = base_x; return 1; } + return 0; +} +static int rth_mirror_wide_present_ok(int base_x) { + if (s_rthm_wide_w <= 0) return 0; + for (int i = 0; i < WIDE_MAX_SURF; i++) + if (s_rthm_base[i] == base_x) return 1; + return 0; +} + +static int rth_rec_ints(uint16_t op, uint16_t flags, int n, const int32_t *v) { + int32_t *p = (int32_t *)rt_cmd_begin(op, flags, (uint32_t)n * 4u); + if (!p) return 0; + memcpy(p, v, (size_t)n * 4u); + rt_cmd_commit(); + return 1; +} + +/* Native-wide stream state read by the fast-path gate (wide_fast_center_valid) + * and the backdrop gate; sent whenever it changes, before the call that would + * first observe the change. */ +static void rth_send_state(void) { + if (s_rthm_wide_w <= 0) return; + int flat = gpu_ws_nw_flat_backdrop_enabled() ? 1 : 0; + int vp = gpu_ws_netplay_local_viewport_width(); + int bg = gpu_ws_background_requires_full_composite() ? 1 : 0; + if (flat == s_rthe_flat_bd && vp == s_rthe_vp_w && bg == s_rthe_bg_full) return; + s_rthe_flat_bd = flat; s_rthe_vp_w = vp; s_rthe_bg_full = bg; + RTH_REC(RTH_STATE, 0, flat, vp, bg); +} + +/* Per-primitive widescreen tags, read by bd_prim_gate during replay. Taken + * only when the backend would read them (native-wide on, stretch gate on). */ +static uint16_t rth_prim_flags(void) { + if (s_rthm_wide_w <= 0 || !g_ws_bd_stretch_on) return 0; + uint16_t f = RTHF_PRIM; + if (g_ws_bd_phase_mode != 0) + f |= (uint16_t)((psx_ws_prim_in_backdrop() & 3) << RTHF_BD_SHIFT); + else if (psx_ws_prim_is_tagged()) + f |= RTHF_TAGGED; + return f; +} + +static int gl_rth_eligible(void) { + extern int psx_netplay_active(void); + return s_raster_ok && s_ctx && !s_cpu_auth_dual && !s_depth24_skip_up && + !gpu_display_is_depth24() && !s_interp_enabled && !s_pass_active && + !psx_netplay_active() && !psx_openxr_session_active() && + gr_backend() == GR_BACKEND_OPENGL; +} + +static void gl_rth_acquire(const char *reason) { + if (!s_rth_on || rt_on_render_thread() || rt_held()) return; + rt_acquire(reason); + s_vram = s_rth_vram_pub; + sw_renderer_rebind_vram(s_rth_vram_pub); +} + +static void gl_rth_release(void) { + if (!s_rth_on || !rt_held()) return; + /* Everything the emulation thread did is in gpu.c's array; the render + * thread continues from a copy of it. */ + memcpy(s_rth_vram_priv, s_rth_vram_pub, (size_t)VRAM_W * VRAM_H * sizeof(uint16_t)); + s_vram = s_rth_vram_priv; + sw_renderer_rebind_vram(s_rth_vram_priv); + rth_mirror_resync(); + rt_release(); +} + +/* Record this call (1) or run it directly (0). Direct: no render thread, on + * the render thread itself (replay), or while the emulation thread holds the + * context. A depth24 display turns the call into a sync point first. */ +static int rth_record_mode(void) { + if (!s_rth_on || rt_on_render_thread() || rt_held()) return 0; + if (gpu_display_is_depth24()) { gl_rth_acquire("depth24"); return 0; } + rth_send_state(); + return 1; +} + +/* Present records carry the host overlays the emulation thread rasterized. */ +typedef struct RthOvHdr { + int32_t needs_present, has, w[4], h[4]; + float slide; + int32_t args[8], nargs; +} RthOvHdr; + +static int rth_record_present(uint16_t op, int n, const int32_t *v) { + const uint32_t *px[4] = { NULL, NULL, NULL, NULL }; + int w[4] = { 0 }, h[4] = { 0 }; + RthOvHdr hd; + memset(&hd, 0, sizeof hd); + hd.needs_present = host_osd_needs_present(); + for (int i = 0; i < 4; i++) { + if (ov_image(i, &px[i], &w[i], &h[i]) && px[i] && w[i] > 0 && h[i] > 0) { + hd.has |= 1 << i; hd.w[i] = w[i]; hd.h[i] = h[i]; + } + } + hd.slide = psx_rewind_slide(); + hd.nargs = n; + for (int i = 0; i < n && i < 8; i++) hd.args[i] = v[i]; + size_t bytes = sizeof hd; + for (int i = 0; i < 4; i++) + if (hd.has & (1 << i)) bytes += (size_t)hd.w[i] * hd.h[i] * 4u; + uint8_t *p = (uint8_t *)rt_cmd_begin(op, 0, (uint32_t)bytes); + if (!p) return 0; + memcpy(p, &hd, sizeof hd); + uint8_t *q = p + sizeof hd; + for (int i = 0; i < 4; i++) + if (hd.has & (1 << i)) { + size_t n4 = (size_t)hd.w[i] * hd.h[i] * 4u; + memcpy(q, px[i], n4); + q += n4; + } + rt_cmd_commit(); + host_osd_present_done(); + { extern void psx_ws_dbg_gate_frame_snapshot(void); psx_ws_dbg_gate_frame_snapshot(); } + s_rth_presents++; + return 1; +} + +static void rth_replay_present(const RtCmd *c, const uint8_t *p) { + RthOvHdr hd; + memcpy(&hd, p, sizeof hd); + if (rt_newer_frame_queued()) { + /* Behind: a later frame is already recorded. Drawing this one only + * delays it; the guest never waits for a present. */ + s_rth_presents_stale++; + return; + } + const uint8_t *q = p + sizeof hd; + memset(&s_rth_ov, 0, sizeof s_rth_ov); + s_rth_ov.valid = 1; + s_rth_ov.needs_present = hd.needs_present; + s_rth_ov.slide = hd.slide; + for (int i = 0; i < 4; i++) + if (hd.has & (1 << i)) { + s_rth_ov.px[i] = (const uint32_t *)q; + s_rth_ov.w[i] = hd.w[i]; + s_rth_ov.h[i] = hd.h[i]; + q += (size_t)hd.w[i] * hd.h[i] * 4u; + } + const int32_t *a = hd.args; + if (c->op == RTH_PRESENT_VRAM) + gl_renderer_present_vram(a[0], a[1], a[2], a[3], a[4], a[5]); + else + (void)gl_renderer_present_wide_fbo(a[0], a[1], a[2], a[3]); + s_rth_ov.valid = 0; +} + +/* ---- the recording vtable ------------------------------------------------ */ +#define RTH_DIRECT_OR(rec) do { if (rth_record_mode()) { rec; return; } } while (0) +static void rtb_init(uint16_t *vram) { + GL_RT_SYNC("init"); + if (s_rth_on) s_rth_vram_pub = vram; + glb_init(vram); + if (s_rth_on && !rt_held()) sw_renderer_rebind_vram(s_rth_vram_priv); +} +static void rtb_set_scale(int sc) { GL_RT_SYNC("set_scale"); glb_set_scale(sc); } +static int rtb_scale(void) { return s_out_scale; } /* changes only under a sync point */ +static void rtb_set_texture_filter(int b) { GL_RT_SYNC("texture_filter"); glb_set_texture_filter(b); } +static int rtb_texture_filter(void) { return s_tex_filter; } +static void rtb_set_semi_transparency(int e, int m) { + RTH_DIRECT_OR(RTH_REC(RTH_SEMI, 0, e, m)); glb_set_semi_transparency(e, m); } +static void rtb_set_mask_bits(int sb, int cb) { + RTH_DIRECT_OR(RTH_REC(RTH_MASK, 0, sb, cb)); glb_set_mask_bits(sb, cb); } +static void rtb_set_texture_window(uint32_t r) { + RTH_DIRECT_OR(RTH_REC(RTH_TWIN, 0, (int32_t)r)); glb_set_texture_window(r); } +static void rtb_set_color_modulation(int r, int g, int b, int raw) { + RTH_DIRECT_OR(RTH_REC(RTH_MOD, 0, r, g, b, raw)); glb_set_color_modulation(r, g, b, raw); } +static void rtb_set_precise_triangle(int en, int32_t x0, int32_t y0, int32_t x1, int32_t y1, + int32_t x2, int32_t y2) { + RTH_DIRECT_OR(RTH_REC(RTH_PRECISE, 0, en, x0, y0, x1, y1, x2, y2)); + glb_set_precise_triangle(en, x0, y0, x1, y1, x2, y2); +} +static void rtb_set_perspective_triangle(int en, float q0, float q1, float q2) { + if (rth_record_mode()) { + int32_t v[4] = { en, 0, 0, 0 }; + memcpy(&v[1], &q0, 4); memcpy(&v[2], &q1, 4); memcpy(&v[3], &q2, 4); + rth_rec_ints(RTH_PERSP, 0, 4, v); + return; + } + glb_set_perspective_triangle(en, q0, q1, q2); +} +static void rtb_fill_rect(int x, int y, int w, int h, uint16_t c) { + RTH_DIRECT_OR(RTH_REC(RTH_FILL, 0, x, y, w, h, c)); glb_fill_rect(x, y, w, h, c); } +static void rtb_copy_rect(int sx, int sy, int dx, int dy, int w, int h) { + RTH_DIRECT_OR(RTH_REC(RTH_COPY, 0, sx, sy, dx, dy, w, h)); glb_copy_rect(sx, sy, dx, dy, w, h); } +static void rtb_draw_flat_triangle(int x0, int y0, int x1, int y1, int x2, int y2, uint16_t c) { + RTH_DIRECT_OR(RTH_REC(RTH_FLAT_TRI, rth_prim_flags(), x0, y0, x1, y1, x2, y2, c)); + glb_draw_flat_triangle(x0, y0, x1, y1, x2, y2, c); +} +static void rtb_draw_gouraud_triangle(int x0, int y0, uint16_t c0, int x1, int y1, uint16_t c1, + int x2, int y2, uint16_t c2) { + RTH_DIRECT_OR(RTH_REC(RTH_GOURAUD_TRI, rth_prim_flags(), x0, y0, c0, x1, y1, c1, x2, y2, c2)); + glb_draw_gouraud_triangle(x0, y0, c0, x1, y1, c1, x2, y2, c2); +} +static void rtb_draw_textured_triangle(int x0, int y0, int u0, int v0, int x1, int y1, int u1, int v1, + int x2, int y2, int u2, int v2, + uint16_t cx, uint16_t cy, uint16_t tp) { + RTH_DIRECT_OR(RTH_REC(RTH_TEX_TRI, rth_prim_flags(), x0, y0, u0, v0, x1, y1, u1, v1, + x2, y2, u2, v2, cx, cy, tp)); + glb_draw_textured_triangle(x0, y0, u0, v0, x1, y1, u1, v1, x2, y2, u2, v2, cx, cy, tp); +} +static void rtb_draw_shaded_textured_triangle(int x0, int y0, int u0, int v0, uint32_t c0, + int x1, int y1, int u1, int v1, uint32_t c1, + int x2, int y2, int u2, int v2, uint32_t c2, + uint16_t cx, uint16_t cy, uint16_t tp, int raw) { + RTH_DIRECT_OR(RTH_REC(RTH_SHADED_TEX_TRI, rth_prim_flags(), + x0, y0, u0, v0, (int32_t)c0, x1, y1, u1, v1, (int32_t)c1, + x2, y2, u2, v2, (int32_t)c2, cx, cy, tp, raw)); + glb_draw_shaded_textured_triangle(x0, y0, u0, v0, c0, x1, y1, u1, v1, c1, + x2, y2, u2, v2, c2, cx, cy, tp, raw); +} +static void rtb_draw_flat_rect(int x, int y, int w, int h, uint16_t c) { + RTH_DIRECT_OR(RTH_REC(RTH_FLAT_RECT, rth_prim_flags(), x, y, w, h, c)); + glb_draw_flat_rect(x, y, w, h, c); +} +static void rtb_draw_textured_rect(int x, int y, int w, int h, int u, int v, + uint16_t cx, uint16_t cy, uint16_t tp) { + RTH_DIRECT_OR(RTH_REC(RTH_TEX_RECT, rth_prim_flags(), x, y, w, h, u, v, cx, cy, tp)); + glb_draw_textured_rect(x, y, w, h, u, v, cx, cy, tp); +} +static void rtb_draw_textured_rect_scaled(int x, int y, int w, int h, int u0, int v0, int u1, int v1, + uint16_t cx, uint16_t cy, uint16_t tp) { + RTH_DIRECT_OR(RTH_REC(RTH_TEX_RECT_SCALED, rth_prim_flags(), x, y, w, h, u0, v0, u1, v1, + cx, cy, tp)); + glb_draw_textured_rect_scaled(x, y, w, h, u0, v0, u1, v1, cx, cy, tp); +} +static void rtb_draw_line(int x0, int y0, int x1, int y1, uint16_t c) { + RTH_DIRECT_OR(RTH_REC(RTH_LINE, rth_prim_flags(), x0, y0, x1, y1, c)); + glb_draw_line(x0, y0, x1, y1, c); +} +static void rtb_draw_shaded_line(int x0, int y0, uint16_t c0, int x1, int y1, uint16_t c1) { + RTH_DIRECT_OR(RTH_REC(RTH_SHADED_LINE, rth_prim_flags(), x0, y0, c0, x1, y1, c1)); + glb_draw_shaded_line(x0, y0, c0, x1, y1, c1); +} +static int rtb_render_display(uint32_t *o, int p, int dx, int dy, int dw, int dh) { + GL_RT_SYNC("render_display"); return glb_render_display(o, p, dx, dy, dw, dh); } +static int rtb_render_display_hires(uint32_t *o, int p, int dx, int dy, int dw, int dh) { + GL_RT_SYNC("render_display_hires"); return glb_render_display_hires(o, p, dx, dy, dw, dh); } +static void rtb_vram_write(int x, int y, uint16_t px) { + RTH_DIRECT_OR(RTH_REC(RTH_VRAM_WRITE, 0, x, y, px)); glb_vram_write(x, y, px); } +static uint16_t rtb_vram_read(int x, int y) { GL_RT_SYNC("vram_read"); return glb_vram_read(x, y); } +static void rtb_vram_transfer_in(int x, int y, int w, int h, const uint16_t *d) { + if (rth_record_mode() && w > 0 && h > 0) { + size_t n = (size_t)w * (size_t)h; + int32_t *p = (int32_t *)rt_cmd_begin(RTH_XFER_IN, 0, (uint32_t)(16u + n * 2u)); + if (p) { + p[0] = x; p[1] = y; p[2] = w; p[3] = h; + memcpy(p + 4, d, n * 2u); + rt_cmd_commit(); + return; + } + gl_rth_acquire("oversize_upload"); + } + glb_vram_transfer_in(x, y, w, h, d); +} +static void rtb_vram_transfer_out(int x, int y, int w, int h, uint16_t *d) { + GL_RT_SYNC("vram_transfer_out"); glb_vram_transfer_out(x, y, w, h, d); } +static void rtb_set_draw_area(int x1, int y1, int x2, int y2) { + if (rth_record_mode()) { + s_rthm_area[0] = x1; s_rthm_area[1] = y1; s_rthm_area[2] = x2; s_rthm_area[3] = y2; + RTH_REC(RTH_AREA, 0, x1, y1, x2, y2); + return; + } + glb_set_draw_area(x1, y1, x2, y2); +} +static void rtb_get_draw_area(int *x1, int *y1, int *x2, int *y2) { + if (s_rth_on && !rt_on_render_thread() && !rt_held()) { + *x1 = s_rthm_area[0]; *y1 = s_rthm_area[1]; *x2 = s_rthm_area[2]; *y2 = s_rthm_area[3]; + return; + } + glb_get_draw_area(x1, y1, x2, y2); +} +static void rtb_set_draw_offset(int x, int y) { + RTH_DIRECT_OR(RTH_REC(RTH_OFFSET, 0, x, y)); glb_set_draw_offset(x, y); } +static void rtb_wide_configure(int wide_w, int offset) { + if (rth_record_mode()) { + if (s_raster_ok) { + if (wide_w <= 0 || wide_w != s_rthm_wide_w) { + for (int i = 0; i < WIDE_MAX_SURF; i++) s_rthm_base[i] = -1; + s_rthm_cur = 0; + } + s_rthm_wide_w = wide_w > 0 ? wide_w : 0; + s_rthm_wide_off = wide_w > 0 ? offset : 0; + } + RTH_REC(RTH_WIDE_CONFIGURE, 0, wide_w, offset); + return; + } + glb_wide_configure(wide_w, offset); +} +static void rtb_wide_set_view(int en, int shift, int pl, int pr) { + RTH_DIRECT_OR(RTH_REC(RTH_WIDE_VIEW, 0, en, shift, pl, pr)); glb_wide_set_view(en, shift, pl, pr); } +static void rtb_wide_set_target(int base_x) { + if (rth_record_mode()) { + s_rthm_cur = s_raster_ok ? rth_mirror_wide_for(base_x) : 0; + RTH_REC(RTH_WIDE_TARGET, 0, base_x); + return; + } + glb_wide_set_target(base_x); +} +static void rtb_wide_disable_target(void) { + if (rth_record_mode()) { s_rthm_cur = 0; RTH_REC(RTH_WIDE_DISABLE, 0, 0); return; } + glb_wide_disable_target(); +} +static void rtb_wide_clear(int base_x, int y, int h, uint16_t color) { + if (rth_record_mode()) { + if (s_raster_ok && s_ws_ablate != 1) (void)rth_mirror_wide_for(base_x); + RTH_REC(RTH_WIDE_CLEAR, 0, base_x, y, h, color); + return; + } + glb_wide_clear(base_x, y, h, color); +} +static void rtb_wide_clear_margins(int base_x, int y, int h, uint16_t color, int sides) { + if (rth_record_mode()) { + if (s_raster_ok && s_ws_ablate != 1 && s_rthm_wide_off > 0) + (void)rth_mirror_wide_for(base_x); + RTH_REC(RTH_WIDE_CLEAR_MARGINS, 0, base_x, y, h, color, sides); + return; + } + glb_wide_clear_margins(base_x, y, h, color, sides); +} +static int rtb_render_wide_display(uint32_t *out, int pitch, int base_x, int disp_y, int disp_h) { + GL_RT_SYNC("render_wide_display"); + return glb_render_wide_display(out, pitch, base_x, disp_y, disp_h); +} +static int rtb_wide_dump_full(uint32_t *out, int cap, int *ow, int *oh, int base_x) { + GL_RT_SYNC("wide_dump_full"); + return glb_wide_dump_full(out, cap, ow, oh, base_x); +} + +static const GpuRenderBackend GL_RT_BACKEND = { + .name = "opengl", + .init = rtb_init, .set_scale = rtb_set_scale, .scale = rtb_scale, + .set_texture_filter = rtb_set_texture_filter, .texture_filter = rtb_texture_filter, + .set_semi_transparency = rtb_set_semi_transparency, .set_mask_bits = rtb_set_mask_bits, + .set_texture_window = rtb_set_texture_window, .set_color_modulation = rtb_set_color_modulation, + .set_precise_triangle = rtb_set_precise_triangle, + .set_perspective_triangle = rtb_set_perspective_triangle, + .fill_rect = rtb_fill_rect, .copy_rect = rtb_copy_rect, + .draw_flat_triangle = rtb_draw_flat_triangle, .draw_gouraud_triangle = rtb_draw_gouraud_triangle, + .draw_textured_triangle = rtb_draw_textured_triangle, + .draw_shaded_textured_triangle = rtb_draw_shaded_textured_triangle, + .draw_flat_rect = rtb_draw_flat_rect, .draw_textured_rect = rtb_draw_textured_rect, + .draw_textured_rect_scaled = rtb_draw_textured_rect_scaled, + .draw_line = rtb_draw_line, .draw_shaded_line = rtb_draw_shaded_line, + .render_display = rtb_render_display, .render_display_hires = rtb_render_display_hires, + .vram_write = rtb_vram_write, .vram_read = rtb_vram_read, + .vram_transfer_in = rtb_vram_transfer_in, .vram_transfer_out = rtb_vram_transfer_out, + .set_draw_area = rtb_set_draw_area, .get_draw_area = rtb_get_draw_area, + .set_draw_offset = rtb_set_draw_offset, + .wide_configure = rtb_wide_configure, + .wide_set_view = rtb_wide_set_view, + .wide_set_target = rtb_wide_set_target, + .wide_disable_target = rtb_wide_disable_target, + .wide_clear = rtb_wide_clear, + .wide_clear_margins = rtb_wide_clear_margins, + .render_wide_display = rtb_render_wide_display, + .wide_dump_full = rtb_wide_dump_full, +}; + +/* ---- replay (render thread) ---------------------------------------------- */ +static void gl_rth_exec(void *user, const RtCmd *c, const void *payload) { + (void)user; + const int32_t *v = (const int32_t *)payload; + s_rthx_prim = (c->flags & RTHF_PRIM) != 0; + s_rthx_tagged = (c->flags & RTHF_TAGGED) != 0; + s_rthx_backdrop = (c->flags >> RTHF_BD_SHIFT) & 3; + switch (c->op) { + case RTH_SEMI: glb_set_semi_transparency(v[0], v[1]); break; + case RTH_MASK: glb_set_mask_bits(v[0], v[1]); break; + case RTH_TWIN: glb_set_texture_window((uint32_t)v[0]); break; + case RTH_MOD: glb_set_color_modulation(v[0], v[1], v[2], v[3]); break; + case RTH_PRECISE: glb_set_precise_triangle(v[0], v[1], v[2], v[3], v[4], v[5], v[6]); break; + case RTH_PERSP: { + float q[3]; + memcpy(q, &v[1], sizeof q); + glb_set_perspective_triangle(v[0], q[0], q[1], q[2]); + break; + } + case RTH_FILL: glb_fill_rect(v[0], v[1], v[2], v[3], (uint16_t)v[4]); break; + case RTH_COPY: glb_copy_rect(v[0], v[1], v[2], v[3], v[4], v[5]); break; + case RTH_FLAT_TRI: glb_draw_flat_triangle(v[0], v[1], v[2], v[3], v[4], v[5], (uint16_t)v[6]); break; + case RTH_GOURAUD_TRI: + glb_draw_gouraud_triangle(v[0], v[1], (uint16_t)v[2], v[3], v[4], (uint16_t)v[5], + v[6], v[7], (uint16_t)v[8]); + break; + case RTH_TEX_TRI: + glb_draw_textured_triangle(v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], + v[8], v[9], v[10], v[11], + (uint16_t)v[12], (uint16_t)v[13], (uint16_t)v[14]); + break; + case RTH_SHADED_TEX_TRI: + glb_draw_shaded_textured_triangle(v[0], v[1], v[2], v[3], (uint32_t)v[4], + v[5], v[6], v[7], v[8], (uint32_t)v[9], + v[10], v[11], v[12], v[13], (uint32_t)v[14], + (uint16_t)v[15], (uint16_t)v[16], (uint16_t)v[17], v[18]); + break; + case RTH_FLAT_RECT: glb_draw_flat_rect(v[0], v[1], v[2], v[3], (uint16_t)v[4]); break; + case RTH_TEX_RECT: + glb_draw_textured_rect(v[0], v[1], v[2], v[3], v[4], v[5], + (uint16_t)v[6], (uint16_t)v[7], (uint16_t)v[8]); + break; + case RTH_TEX_RECT_SCALED: + glb_draw_textured_rect_scaled(v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], + (uint16_t)v[8], (uint16_t)v[9], (uint16_t)v[10]); + break; + case RTH_LINE: glb_draw_line(v[0], v[1], v[2], v[3], (uint16_t)v[4]); break; + case RTH_SHADED_LINE: + glb_draw_shaded_line(v[0], v[1], (uint16_t)v[2], v[3], v[4], (uint16_t)v[5]); + break; + case RTH_VRAM_WRITE: glb_vram_write(v[0], v[1], (uint16_t)v[2]); break; + case RTH_XFER_IN: glb_vram_transfer_in(v[0], v[1], v[2], v[3], (const uint16_t *)(v + 4)); break; + case RTH_AREA: glb_set_draw_area(v[0], v[1], v[2], v[3]); break; + case RTH_OFFSET: glb_set_draw_offset(v[0], v[1]); break; + case RTH_WIDE_CONFIGURE: glb_wide_configure(v[0], v[1]); break; + case RTH_WIDE_VIEW: glb_wide_set_view(v[0], v[1], v[2], v[3]); break; + case RTH_WIDE_TARGET: glb_wide_set_target(v[0]); break; + case RTH_WIDE_DISABLE: glb_wide_disable_target(); break; + case RTH_WIDE_CLEAR: glb_wide_clear(v[0], v[1], v[2], (uint16_t)v[3]); break; + case RTH_WIDE_CLEAR_MARGINS: glb_wide_clear_margins(v[0], v[1], v[2], (uint16_t)v[3], v[4]); break; + case RTH_PROJ_TRI: { + PSXProjectedVertex vx[3]; + memcpy(vx, payload, sizeof vx); + const int32_t *a = (const int32_t *)((const uint8_t *)payload + sizeof vx); + gl_renderer_draw_projected_triangle(vx, (uint16_t)a[0], (uint16_t)a[1], (uint16_t)a[2], + a[3], a[4], a[5]); + break; + } + case RTH_WIDE_RECOVERY: gl_renderer_note_wide_triangle_recovery(v[0]); break; + case RTH_INTERP_SUSPENDED: gl_renderer_set_interpolation_suspended(v[0]); break; + case RTH_PRESENT_VRAM: + case RTH_PRESENT_WIDE: rth_replay_present(c, (const uint8_t *)payload); break; + case RTH_STATE: + s_rths_flat_bd = v[0]; s_rths_vp_w = v[1]; s_rths_bg_full = v[2]; + break; + case RTH_PEEK: { + uint64_t ptr = (uint64_t)(uint32_t)v[4] | ((uint64_t)(uint32_t)v[5] << 32); + (void)gl_renderer_fbo_peek(v[0], v[1], v[2], v[3], (uint16_t *)(uintptr_t)ptr); + break; + } + default: break; + } + s_rthx_prim = 0; +} + +static void gl_rth_ctx(void *user, int current) { + (void)user; + if (current) { + SDL_GL_MakeCurrent(s_win, s_ctx); + } else { + glFlush(); + SDL_GL_MakeCurrent(s_win, NULL); + } +} + +/* ---- lifecycle / public ---------------------------------------------------- */ +extern void gr_refresh_backend(void); /* gpu_render.c */ + +int gl_renderer_render_thread_start(int max_frames) { + if (s_rth_on) return 1; + if (!s_ctx || !s_raster_ok || !s_vram || !gl_rth_eligible()) return 0; + uint16_t *priv = (uint16_t *)malloc((size_t)VRAM_W * VRAM_H * sizeof(uint16_t)); + if (!priv) return 0; + /* Cocoa: a background thread's swap must not wait on the main thread, + * which may itself be waiting on the render thread. */ + SDL_SetHint("SDL_MAC_OPENGL_ASYNC_DISPATCH", "1"); + flush_flat_batch(); + flush_tex_batch(); + flush_cpu_upload(); + memcpy(priv, s_vram, (size_t)VRAM_W * VRAM_H * sizeof(uint16_t)); + s_rth_vram_pub = s_vram; + s_rth_vram_priv = priv; + s_rthe_flat_bd = s_rthe_vp_w = s_rthe_bg_full = -1; + rth_mirror_resync(); + s_vram = priv; + sw_renderer_rebind_vram(priv); + s_rth_on = 1; + RtConfig cfg; + cfg.ring_bytes = (size_t)32u << 20; + cfg.max_frames = max_frames < 1 ? 1 : max_frames > 8 ? 8 : max_frames; + cfg.exec = gl_rth_exec; + cfg.ctx = gl_rth_ctx; + cfg.user = NULL; + if (!rt_start(&cfg)) { + s_rth_on = 0; + s_vram = s_rth_vram_pub; + sw_renderer_rebind_vram(s_rth_vram_pub); + free(priv); + s_rth_vram_priv = NULL; + return 0; + } + gr_refresh_backend(); + return 1; +} + +void gl_renderer_render_thread_stop(void) { + if (!s_rth_on) return; + rt_stop(); /* drains; the context is current here again */ + s_rth_on = 0; + s_vram = s_rth_vram_pub; + sw_renderer_rebind_vram(s_rth_vram_pub); + free(s_rth_vram_priv); + s_rth_vram_priv = NULL; + gr_refresh_backend(); +} + +int gl_renderer_render_thread_active(void) { return s_rth_on; } + +/* Frame boundary (once per vblank, after the present): hand the context back + * to the render thread when the frame may run asynchronously, take it when it + * may not, then close the frame (the in-flight bound applies here). */ +void gl_renderer_render_thread_frame_boundary(void) { + if (!s_rth_on) return; + if (rt_held()) { + if (gl_rth_eligible()) gl_rth_release(); + } else if (!gl_rth_eligible()) { + gl_rth_acquire("ineligible"); + } + rt_frame_end(); +} + +/* Explicit sync point for callers that read renderer-written host state + * without calling a GL entry point (debug rings). */ +void gl_renderer_render_thread_sync(const char *reason) { GL_RT_SYNC(reason); } + +int gl_renderer_render_thread_json(char *out, size_t cap) { + RtStats st; + rt_get_stats(&st); + RtAcquireEvent ev[16]; + int n = rt_acquire_events(ev, 16); + int k = snprintf(out, cap, + "\"active\":%d,\"held\":%d,\"max_frames\":%d,\"records\":%llu,\"bytes\":%llu," + "\"frames_produced\":%llu,\"frames_consumed\":%llu,\"presents\":%llu," + "\"presents_stale\":%llu,\"acquires\":%llu,\"releases\":%llu,\"oversize\":%llu," + "\"backpressure_waits\":%llu,\"backpressure_ms\":%.3f,\"ring_full_waits\":%llu," + "\"ring_full_ms\":%.3f,\"acquire_ms\":%.3f,\"render_busy_ms\":%.3f," + "\"render_idle_ms\":%.3f,\"ring_high_water\":%llu,\"recent_acquires\":[", + s_rth_on, st.held, st.max_frames, (unsigned long long)st.records, + (unsigned long long)st.bytes, (unsigned long long)st.frames_produced, + (unsigned long long)st.frames_consumed, (unsigned long long)s_rth_presents, + (unsigned long long)s_rth_presents_stale, (unsigned long long)st.acquires, + (unsigned long long)st.releases, (unsigned long long)st.oversize, + (unsigned long long)st.backpressure_waits, st.backpressure_ns / 1e6, + (unsigned long long)st.ring_full_waits, st.ring_full_ns / 1e6, + st.acquire_ns / 1e6, st.render_busy_ns / 1e6, st.render_idle_ns / 1e6, + (unsigned long long)st.ring_high_water); + for (int i = 0; i < n && k > 0 && (size_t)k < cap; i++) + 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, "]"); + return k; +} + +const GpuRenderBackend *gl_backend_get(void) { return s_rth_on ? &GL_RT_BACKEND : &GL_BACKEND; } diff --git a/runtime/src/gpu_render.c b/runtime/src/gpu_render.c index 1cbf16fd..c205fb80 100644 --- a/runtime/src/gpu_render.c +++ b/runtime/src/gpu_render.c @@ -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); } diff --git a/runtime/src/gpu_sw_renderer.c b/runtime/src/gpu_sw_renderer.c index a00a5e51..e472a06e 100644 --- a/runtime/src/gpu_sw_renderer.c +++ b/runtime/src/gpu_sw_renderer.c @@ -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 */ /* ------------------------------------------------------------------ */ diff --git a/runtime/tests/run_gl_readback_region.py b/runtime/tests/run_gl_readback_region.py index b7643b90..14810c25 100644 --- a/runtime/tests/run_gl_readback_region.py +++ b/runtime/tests/run_gl_readback_region.py @@ -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, diff --git a/runtime/tests/run_gl_scale_invariance.py b/runtime/tests/run_gl_scale_invariance.py index b078f8b5..33c55215 100644 --- a/runtime/tests/run_gl_scale_invariance.py +++ b/runtime/tests/run_gl_scale_invariance.py @@ -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 diff --git a/runtime/tests/run_gl_texture_filter.py b/runtime/tests/run_gl_texture_filter.py index fd407225..d4899edd 100644 --- a/runtime/tests/run_gl_texture_filter.py +++ b/runtime/tests/run_gl_texture_filter.py @@ -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) diff --git a/runtime/tests/test_gl_readback_region.c b/runtime/tests/test_gl_readback_region.c index b51e33a6..e4da089b 100644 --- a/runtime/tests/test_gl_readback_region.c +++ b/runtime/tests/test_gl_readback_region.c @@ -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;} diff --git a/runtime/tests/test_gl_scale_invariance.c b/runtime/tests/test_gl_scale_invariance.c index 61ed9b0f..e5611f81 100644 --- a/runtime/tests/test_gl_scale_invariance.c +++ b/runtime/tests/test_gl_scale_invariance.c @@ -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;}