diff --git a/docs/internal/FAITHFUL_TIMING_PLAN.md b/docs/internal/FAITHFUL_TIMING_PLAN.md index 0eaae45e..e61454f8 100644 --- a/docs/internal/FAITHFUL_TIMING_PLAN.md +++ b/docs/internal/FAITHFUL_TIMING_PLAN.md @@ -213,6 +213,16 @@ on a fixed region -> next. ## 5. Status / Log (update every session) +- 2026-10-21 BRIEF-4 Stage 3 continuation: temporal GPU transactions now stay + on the render context owner. Recorded redraws carry exact PGXP model-word + provenance and homogeneous geometry, avoiding a second guest draw and the + prior camera-fitting generator. Optional presents use a nonblocking queue + and readiness polling; missed extras are dropped. CPU-transaction identity + matches 4,501 off/on entries. GPU dataflow acceptance remains open: quiet + has three >25ms gaps, p99 70.1%, and deficient physical cadence. Correcting + source PTS scheduling and limiting input-origin bookkeeping to GTE words; + the faithful guest timing path and R4's 40-section choice are unchanged. + - 2026-10-21 BRIEF-4 Stage 3 WIP: restore asynchronous temporal eligibility, queue presentation deadlines on the presenter, release rejected plans, and credit frontend deadline sleep without adding overlapping renderer idle. diff --git a/overlay_toolchain/include/mod_plugins.h b/overlay_toolchain/include/mod_plugins.h index 3c3422f4..a43b6e9a 100644 --- a/overlay_toolchain/include/mod_plugins.h +++ b/overlay_toolchain/include/mod_plugins.h @@ -490,6 +490,7 @@ int psx_mod_set_render_pass_flip(uint32_t mode); */ int psx_mod_activate_render_pass_rate(const char* package, const char* feature, const char* option, uint32_t flip_mode); +int psx_mod_set_recorded_render_passes(int enabled); typedef struct PSXModRenderPassFrame { uint32_t struct_size; /* sizeof(PSXModRenderPassFrame) */ uint32_t period_vblanks; /* VBlanks this game frame stays on screen */ diff --git a/runtime/include/frame_gen.h b/runtime/include/frame_gen.h index 587ed518..8a6956f2 100644 --- a/runtime/include/frame_gen.h +++ b/runtime/include/frame_gen.h @@ -115,6 +115,11 @@ typedef struct FgVert { float a[3]; /* FG_PLACE_OBJECT: the older camera-space position */ } FgVert; enum { FG_PLACE_UNCHANGED = 0, FG_PLACE_CAMERA, FG_PLACE_OBJECT, FG_PLACE_NEIGHBOUR }; +/* Recorded-pass path: exact, value-validated model identities and homogeneous + * projections. No position-cache guessing or fitted camera. */ +uint32_t fg_dataflow_match(const FgPrimList *older, const FgPrimList *newer, FgVert *vertices); +void fg_dataflow_place(const FgPrimList *newer, const FgVert *vertices, + double phase, float *x, float *y); /* Fits the camera motion older -> newer per view and decides each newer * vertex's placement; returns fit->ok (the verdict). */ diff --git a/runtime/include/frame_interpolation.h b/runtime/include/frame_interpolation.h index 57b92049..4e80efc5 100644 --- a/runtime/include/frame_interpolation.h +++ b/runtime/include/frame_interpolation.h @@ -17,9 +17,6 @@ typedef struct FrameInterpolationSchedule { * plans the whole 0..1 window, which is the historical behaviour. */ double phase_lo; double phase_hi; - /* Bounded asynchronous producer: preparing several frames ahead is normal, - * so an early arrival must not reset the presentation clock. */ - int queued; } FrameInterpolationSchedule; /* Plan one stock guest-frame interval. Presentation deadlines remain anchored diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index 6ac0f527..e73bd74e 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -16,6 +16,10 @@ struct SDL_Window; #ifdef __cplusplus extern "C" { #endif +void gl_renderer_set_recorded_passes(int enabled); +int gl_renderer_recorded_passes_enabled(void); +uint64_t gl_renderer_recorded_pass_count(void); +void gl_renderer_recorded_sources(const float projection[9], const uint32_t identity[3]); /* Create the GL context on a window made with SDL_WINDOW_OPENGL. * Returns 1 on success, 0 to fall back to the SDL_Renderer present path. */ diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index 8c6743a3..933c3ffb 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -853,6 +853,9 @@ int psx_mod_set_render_pass_netplay(int enabled); */ int psx_mod_activate_render_pass_rate(const char* package, const char* feature, const char* option, uint32_t flip_mode); +/* Render-thread redraw from value-validated PGXP geometry. Requires the + * render thread; returns zero so a plugin can retain its CPU replay fallback. */ +int psx_mod_set_recorded_render_passes(int enabled); typedef struct PSXModRenderPassFrame { uint32_t struct_size; /* sizeof(PSXModRenderPassFrame) */ uint32_t period_vblanks; /* VBlanks this game frame stays on screen */ diff --git a/runtime/include/pgxp.h b/runtime/include/pgxp.h index 62a4ad8e..8c77b762 100644 --- a/runtime/include/pgxp.h +++ b/runtime/include/pgxp.h @@ -182,6 +182,11 @@ int pgxp_projection_tracking(void); void pgxp_gte_set_projection(const PGXPProjection *projection); int pgxp_load_projection(uint32_t addr, uint32_t packed, PGXPProjection *out); int pgxp_get_gte_projection(uint32_t index, uint32_t packed, PGXPProjection *out); +/* Exact full-word dataflow provenance for recorded temporal redraws. */ +uint32_t pgxp_gte_input_identity(uint32_t vertex); +void pgxp_gte_projection_identity(uint32_t identity); +int pgxp_load_temporal_projection(uint32_t addr, uint32_t packed, + PGXPProjection *out, uint32_t *identity); /* Current SXY FIFO shadow (index 0..3 selects GTE data regs 12..15). * Returns nonzero when the shadow is live and carries X/Y precision. */ diff --git a/runtime/include/present_thread.h b/runtime/include/present_thread.h index 9a47d8f0..35846998 100644 --- a/runtime/include/present_thread.h +++ b/runtime/include/present_thread.h @@ -65,6 +65,9 @@ int pt_current(void); * 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); +/* Optional frame: publish only if another slot is already free. Returns -1 + * without changing the current slot or taking ownership of ready otherwise. */ +int pt_try_submit(void *ready, void **next_done); /* Producer: wait until nothing is queued or presenting. */ void pt_drain(void); diff --git a/runtime/src/frame_gen.c b/runtime/src/frame_gen.c index 5c4b1580..e2b99859 100644 --- a/runtime/src/frame_gen.c +++ b/runtime/src/frame_gen.c @@ -923,3 +923,59 @@ int fg_view_affine(const FgCamFit *fit, int vi, double t, float A[9], float b[3] b[r] = (float)(t * v->t[r] - (Am[3 * r] * v->t[0] + Am[3 * r + 1] * v->t[1] + Am[3 * r + 2] * v->t[2])); return 1; } + +/* Identity buckets preserve separate instances of a shared mesh. Repeated + * corners within an instance are harmless; choose its closest previous + * projection in the same view, bounded against cuts and teleports. */ +uint32_t fg_dataflow_match(const FgPrimList *a, const FgPrimList *b, FgVert *out) { + uint32_t cap = 16, count = a->n * 3u, matched = 0; + while (cap < count * 2u && cap < (1u << 29)) cap <<= 1; + uint32_t *head = (uint32_t *)calloc(cap, sizeof *head); + uint32_t *next = (uint32_t *)calloc(count ? count : 1u, sizeof *next); + memset(out, 0, (size_t)b->n * 3u * sizeof *out); + if (!head || !next) { free(head); free(next); return 0; } + for (uint32_t i = 0; i < count; i++) { + const FgPrim *p = &a->v[i/3u]; uint32_t k = i%3u; + if (!p->vid[k] || !(p->p[k][2] > 0.f)) continue; + uint32_t h = (p->vid[k] * 2654435761u ^ p->view) & (cap-1u); + next[i] = head[h]; head[h] = i+1u; + } + for (uint32_t i = 0; i < b->n * 3u; i++) { + const FgPrim *p = &b->v[i/3u]; uint32_t k = i%3u; + if (!p->vid[k] || !(p->p[k][2] > 0.f)) continue; + uint32_t h = (p->vid[k] * 2654435761u ^ p->view) & (cap-1u); + float best = 96.f * 96.f; uint32_t at = UINT32_MAX; + for (uint32_t n = head[h]; n; n = next[n-1u]) { + uint32_t j = n-1u; const FgPrim *q = &a->v[j/3u]; uint32_t v = j%3u; + if (q->vid[v] != p->vid[k] || q->view != p->view) continue; + float dx = q->x[v]-p->x[k], dy = q->y[v]-p->y[k]; + float d = dx*dx+dy*dy; + /* Depth disambiguates overlapping instances, while allowing + * ordinary camera travel through the near scene. */ + float ratio = q->p[v][2] / p->p[k][2]; + if (ratio < .25f || ratio > 4.f) continue; + if (d < best) { best = d; at = j; } + } + if (at != UINT32_MAX) { + out[i].paired = 1; + memcpy(out[i].a, a->v[at/3u].p[at%3u], sizeof out[i].a); + matched++; + } + } + free(head); free(next); return matched; +} +void fg_dataflow_place(const FgPrimList *b, const FgVert *v, double phase, + float *x, float *y) { + float t = (float)phase; + for (uint32_t i=0;in*3u;i++) { + const FgPrim *p=&b->v[i/3u]; uint32_t k=i%3u; + x[i]=p->x[k]; y[i]=p->y[k]; + if (!v[i].paired || !(p->p[k][2]>0.f)) continue; + float z=v[i].a[2]+(p->p[k][2]-v[i].a[2])*t; + if (!(z>0.f)) continue; + float hx=v[i].a[0]+(p->p[k][0]-v[i].a[0])*t; + float hy=v[i].a[1]+(p->p[k][1]-v[i].a[1])*t; + x[i]+=hx/z-p->p[k][0]/p->p[k][2]; + y[i]+=hy/z-p->p[k][1]/p->p[k][2]; + } +} diff --git a/runtime/src/frame_interpolation.c b/runtime/src/frame_interpolation.c index de760e07..0b4576ed 100644 --- a/runtime/src/frame_interpolation.c +++ b/runtime/src/frame_interpolation.c @@ -45,7 +45,7 @@ int frame_interpolation_schedule_begin_phase(FrameInterpolationSchedule *schedul * Normal guest work lands after the prior deadline but before the next one; * retaining the old anchor makes that work consume the pacing budget. */ if (schedule->source_deadline <= 0.0 || - (!schedule->queued && now_d + source_period < schedule->source_deadline) || + now_d + source_period < schedule->source_deadline || now_d > schedule->source_deadline + source_period * 4.0) { schedule->frame_start = now_d; schedule->source_deadline = now_d + source_period; diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 9349b2ec..490f3bc1 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -4598,7 +4598,7 @@ void psx_mod_set_native_wide_projection_correction(int enabled) { s_native_wide_projection_vertices = 0; /* Signed camera-space provenance is required for correct native-wide * faces, including corners behind the camera. Arm it with the mode. */ - pgxp_set_projection_tracking(s_native_wide_projection_correction); + pgxp_set_projection_tracking(s_native_wide_projection_correction || gl_renderer_recorded_passes_enabled()); gpu_pgxp_rederive_enable(); } @@ -4702,6 +4702,10 @@ uint32_t gpu_texture_correction_hits(void) { * caller. */ static void prepare_precise_triangle(int i0, int i1, int i2, const int32_t vx[3], const int32_t vy[3]) { + const int recorded = gl_renderer_recorded_passes_enabled(); + float temporal_projection[9] = {0}; + uint32_t temporal_id[3] = {0}; + if (recorded) gl_renderer_recorded_sources(NULL, NULL); if (gr_backend() == GR_BACKEND_OPENGL) { const int indices[3] = {i0,i1,i2}; int32_t raw_x[3], raw_y[3]; @@ -4767,6 +4771,14 @@ static void prepare_precise_triangle(int i0, int i1, int i2, uint32_t addr = (gp0_cmd_source_addr == 0xFFFFFFFFu) ? 0xFFFFFFFFu : gp0_cmd_source_addr + (uint32_t)idx[i] * 4u; + if (recorded) { + PGXPProjection p; + if (pgxp_load_temporal_projection(addr, word, &p, &temporal_id[i])) { + temporal_projection[3*i] = p.x; + temporal_projection[3*i+1] = p.y; + temporal_projection[3*i+2] = p.z; + } + } int32_t px, py; uint16_t sz; int src = PGXP_SRC_NATIVE; @@ -4800,6 +4812,7 @@ static void prepare_precise_triangle(int i0, int i1, int i2, rec.y16[i] = fy[i]; rec.z[i] = sz; /* receipt retains the hardware SZ, not the visual depth */ } + if (recorded) gl_renderer_recorded_sources(temporal_projection, temporal_id); if (geometry) { pgxp_note_triangle(n_precise); /* G1.1 crack exposure: mixed share */ rec.frame = (uint32_t)s_frame_count; diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 27d5c248..79913d4d 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -76,6 +76,7 @@ #include "host_overlay.h" #include "gpu.h" #include "gpu_render.h" +#include "pgxp.h" #include "gl_scale_limits.h" #include "gpu_sw_renderer.h" #include "gpu_gl_renderer.h" @@ -322,6 +323,8 @@ 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; +typedef void (APIENTRY *PFN_glGetSynciv)(void *, GLenum, GLsizei, GLsizei *, GLint *); +static PFN_glGetSynciv p_glGetSynciv; static PFN_glDeleteSync p_glDeleteSync; static PFN_glBindFramebuffer p_glBindFramebuffer_raw; static int s_pt_on = 0; /* redirect active (present thread running) */ @@ -384,6 +387,7 @@ static int load_modern_gl(void) { /* 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_glGetSynciv = (void *)SDL_GL_GetProcAddress("glGetSynciv"); p_glDeleteSync = (void *)SDL_GL_GetProcAddress("glDeleteSync"); if (p_glBindFramebuffer && p_glBindFramebuffer != gl_bind_fb_redirect) { p_glBindFramebuffer_raw = p_glBindFramebuffer; @@ -487,6 +491,16 @@ static void fg_flush(void); static void fg_invalidate(void); static uint64_t fg_tick(void *user, uint64_t now); static void fg_note_guest_frame(void); +static int s_recorded_passes; +typedef struct RecordedProjection { float p[9]; uint32_t id[3]; } RecordedProjection; +static RecordedProjection s_recorded_source; +static void recorded_note_present(uint16_t op, const uint8_t *payload); +static void recorded_prepare_generation(void); +static int s_recorded_replaying; +static int s_recorded_preparing; +static _Atomic uint64_t s_recorded_completed; +static _Atomic uint32_t s_recorded_paired, s_recorded_sources; +#define RTHF_PROJECTION 16u static void rthf_gpu_pause(void); 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? */ @@ -523,8 +537,12 @@ enum { 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, RTH_RING_CAPTURE, RTH_DYN_STEP, - RTH_FRAME, RTH_FG_SRC, RTH_DEPTH, RTH_HD_NOTE, RTH_DITHER, RTH_WIDE_FEEDBACK, RTH_PASS_SERVICE + RTH_FRAME, RTH_FG_SRC, RTH_DEPTH, RTH_HD_NOTE, RTH_DITHER, RTH_WIDE_FEEDBACK, RTH_PASS_SERVICE, + RTH_PASS_BEGIN, RTH_PASS_END, RTH_PASS_COST, RTH_PASS_SHOWN }; +static int temporal_recording(void) { + return s_rth_on && !rt_on_render_thread() && !rt_held(); +} /* GP0(A0) whose payload gpu.c is still streaming into its array (the * facade's vram_upload_open, emulation thread), tracked whatever the HD * state: under HD authority the emulation thread holds the context from the @@ -539,6 +557,7 @@ static int s_rth_a0_open = 0; static int s_hd_authority_pending = 0; static int rth_record_mode(void); +static void rth_pass_boundary(void); /* Render-thread frame cost (dynamic resolution; defined with GL_RT_BACKEND). */ static _Atomic uint64_t s_rth_present_wait_ns; static uint64_t s_rthf_swap_ns = 0; /* render thread: time in the swap */ @@ -716,6 +735,11 @@ static double s_interp_target_hz = 0.0; static double s_interp_source_hz = 0.0; static FrameInterpolationSchedule s_interp_schedule; static uint64_t s_interp_pending_deadline; +static uint64_t s_interp_input_ticks; +static uint64_t s_interp_pending_identity; +static int s_interp_pending_extra; +static _Atomic uint64_t s_temporal_completed, s_temporal_refused, s_temporal_verify_failed; +static _Atomic uint64_t s_temporal_presented, s_temporal_dropped; static _Atomic uint64_t s_pass_frontend_idle; /* Blend source (psx_mod_set_frame_interpolation_source). VBLANK (0, the * default) treats every guest VBlank present as a new frame, exactly as @@ -735,6 +759,7 @@ static void pass_gens_invalidate(void); static void pass_note_new_frame(int origin_x, int origin_y, int source_path, int pw, int ph); static void pass_apply_promotion(void); +static void pass_publish_schedule(void); static int pass_gen_present(uint64_t deadline); static int stereo_present(int w, int h); static int xr_mirror_mode(void); @@ -2874,7 +2899,7 @@ static void hiw_clear_rect(int x, int y, int w, int h, float r, float g, float b glClearColor(r, g, b, a); glClearStencil(stencil); glStencilMask(0xFF); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); } } @@ -5222,7 +5247,7 @@ static void fill_segment(int x, int y, int w, int h, float r, float g, float b) glClearColor(r, g, b, 0.0f); glClearStencil(0); glStencilMask(0xFF); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); rect_add(&s_pack_dirty, x, y, x + w - 1, y + h - 1); if (!cpu_vram_authoritative()) rect_add(&s_cpu_dirty, x, y, x + w - 1, y + h - 1); } @@ -5779,7 +5804,7 @@ static void depth24_clear_skipped_fb(void) { glClearColor(0.f, 0.f, 0.f, 0.f); glClearStencil(0); glStencilMask(0xFF); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); hiw_clear_rect(x0, y0, rw, rh, 0.f, 0.f, 0.f, 0.f, 0); if (s_raw_fbo) { p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_raw_fbo); @@ -6401,7 +6426,7 @@ static int init_gpu_raster(void) { glClearColor(0, 0, 0, 0); glClearStencil(0); glStencilMask(0xFF); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); rect_clear(&s_pack_dirty); rect_clear(&s_stencil_stale); @@ -7179,7 +7204,7 @@ static int wide_alloc_surface(int S, GLuint *tex, GLuint *rb, GLuint *fbo) { glClearColor(0, 0, 0, 0); glClearStencil(0); glStencilMask(0xFF); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); return 1; } @@ -7297,7 +7322,7 @@ static void glb_wide_clear(int base_x, int y, int h, uint16_t color) { glClearColor(r, g, b, a); glClearStencil((color >> 15) & 1); /* stencil mirrors bit15, like the color alpha */ glStencilMask(0xFF); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); glDisable(GL_SCISSOR_TEST); p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); gl_perf_mirror_end(); @@ -7337,11 +7362,11 @@ static void glb_wide_clear_margins(int base_x, int y, int h, uint16_t color, int glStencilMask(0xFF); if (sides & 1) { glScissor(0, y0, margin, y1 - y0); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); } if (sides & 2) { glScissor(W - margin, y0, margin, y1 - y0); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); } glDisable(GL_SCISSOR_TEST); p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); @@ -7999,6 +8024,7 @@ static int interp_capture(GLuint fbo, int x, int y, int w, int h, return 1; } +static float s_interp_uv_x = 1.f, s_interp_uv_y = 1.f; static void interp_draw_textures(GLuint prev_tex, GLuint curr_tex, float alpha, int blend_mode, int lx, int ly, int lw, int lh) { p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); @@ -8017,7 +8043,7 @@ static void interp_draw_textures(GLuint prev_tex, GLuint curr_tex, float alpha, p_glUniform1f(s_interp_uAlpha, alpha); p_glUniform1i(s_interp_uBlendMode, blend_mode); present_set_gamma(s_interp_uGamma, 1); - p_glUniform4f(s_interp_uUvRect, 0.f, 0.f, 1.f, 1.f); + p_glUniform4f(s_interp_uUvRect, 0.f, 0.f, s_interp_uv_x, s_interp_uv_y); /* Interp textures hold exactly the display rect (uv_rect is 0..1), so pitch * == display height == s_interp_src_h rows at the output scale (the * texture itself can be smaller, see hiw_capture_size). */ @@ -8106,18 +8132,19 @@ void gl_renderer_pass_note_frontend_idle(uint64_t ticks) { static void interp_present_source_interval(void) { static uint64_t diag_start, diag_swaps, diag_captures; const int asynchronous = s_interp_hold && s_pt_on && s_rth_on && rt_on_render_thread(); - s_interp_schedule.queued = asynchronous; uint64_t frequency = SDL_GetPerformanceFrequency(); uint64_t now = SDL_GetPerformanceCounter(); uint64_t deadline; float alpha; if (!frame_interpolation_schedule_begin_phase( - &s_interp_schedule, now, frequency, + &s_interp_schedule, asynchronous && s_interp_input_ticks ? s_interp_input_ticks : now, frequency, s_interp_source_hz, s_interp_target_hz, s_interp_phase_lo, s_interp_phase_hi)) return; pass_apply_promotion(); + if (s_recorded_passes) recorded_prepare_generation(); + pass_publish_schedule(); while (frame_interpolation_schedule_next( &s_interp_schedule, SDL_GetPerformanceCounter(), @@ -8178,6 +8205,8 @@ typedef struct PassGen { int valid; int promoted; uint64_t order,created_cycle; + uint64_t base_identity; + GLuint external_base; /* RT-only generation borrows the completed history image */ int x, y, w, h; /* captured VRAM rect (what is presented) */ int rx, ry, rw, rh; /* the plugin's pass rect (backed up) */ int tex_w, tex_h; /* slot size (hr pixels; wide band if wide) */ @@ -8263,6 +8292,29 @@ static uint32_t s_intervals_since_plan = 0; static int s_pass_budget_pct = -1; static GLRenderPassPlanDiag s_pass_plan_diag; +/* Only this small mailbox is shared. No GL operation, queue drain or guest + * callback runs under its lock. The producer plans against a completed + * render-thread schedule; all GPU transaction state remains on that thread. */ +typedef struct PassScheduleMailbox { + RenderPassPlanInput plan; + double source_period; + uint32_t status, cap; + uint64_t published; +} PassScheduleMailbox; +static PassScheduleMailbox s_pass_mail; +static atomic_flag s_pass_mail_lock = ATOMIC_FLAG_INIT; +static void pass_mail_lock(void) { + while (atomic_flag_test_and_set_explicit(&s_pass_mail_lock, memory_order_acquire)) {} +} +static void pass_mail_unlock(void) { + atomic_flag_clear_explicit(&s_pass_mail_lock, memory_order_release); +} +static PassScheduleMailbox pass_mail_read(void) { + PassScheduleMailbox m; + pass_mail_lock(); m = s_pass_mail; pass_mail_unlock(); + return m; +} + static int s_pass_verify = -1; static uint8_t *s_pv_hr = NULL, *s_pv_raw = NULL; static size_t s_pv_hr_cap = 0, s_pv_raw_cap = 0; @@ -8327,6 +8379,12 @@ static void pass_refusal_init(void) { } uint32_t gl_renderer_pass_unavailable(void) { + if (temporal_recording()) { + PassScheduleMailbox m = pass_mail_read(); + if (!m.published || SDL_GetPerformanceCounter() - m.published > + SDL_GetPerformanceFrequency() / 4u) return PSX_MOD_RENDER_PASS_BUSY; + return m.status; + } GL_RT_SYNC("pass_unavailable"); pass_refusal_init(); if (!s_ctx || !s_raster_ok || !s_interp_enabled || s_interp_suspended || @@ -8355,7 +8413,6 @@ 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; } @@ -8373,15 +8430,53 @@ static uint32_t pass_slot_cap(int tex_w, int tex_h) { if (cap > PASS_SLOTS) cap = PASS_SLOTS; return cap; } +static GLuint pass_gen_texture(int gi, uint32_t slot) { + if (s_pgen[gi].external_base) + return slot ? s_pgen_tex[gi][slot-1u] : s_pgen[gi].external_base; + return s_pgen_tex[gi][slot]; +} + +static void pass_publish_schedule(void) { + PassScheduleMailbox m = {0}; + m.status = gl_renderer_pass_unavailable(); + m.cap = pass_slot_cap(s_interp_w, s_interp_h); + m.source_period = s_interp_source_hz > 0.0 ? + (double)SDL_GetPerformanceFrequency() / s_interp_source_hz : 0.0; + m.plan.next_deadline = s_interp_schedule.next_present_deadline; + m.plan.target_period = s_interp_schedule.target_period; + m.plan.frame_start = s_interp_schedule.source_deadline; + m.plan.pass_cost = render_pass_cost_estimate(&s_pass_cost); + m.published = SDL_GetPerformanceCounter(); + pass_mail_lock(); s_pass_mail = m; pass_mail_unlock(); +} void gl_renderer_pass_cancel_plan(void) { - if (s_rth_on && !rt_on_render_thread()) gl_rth_release(); + /* Recorded plans own no context or resources. */ + if (!temporal_recording() && s_rth_on && !rt_on_render_thread()) gl_rth_release(); } 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) { + if (temporal_recording()) { + PassScheduleMailbox m = pass_mail_read(); + if (wanted) *wanted = 0; + if (gl_renderer_pass_unavailable() != PSX_MOD_RENDER_PASS_READY || + !alpha_q16 || !max || m.cap < 2) return 0; + RenderPassPlanInput in = m.plan; + in.frame_start += shown_after_vblanks * m.source_period; + in.frame_length = period_vblanks * m.source_period; + in.max = max < m.cap - 1u ? max : m.cap - 1u; + /* The CPU sandbox cost is measured on the producer, independently + * of asynchronous GPU execution. Admission uses its pacing credit. */ + double credit = (double)atomic_exchange_explicit(&s_pass_frontend_idle, 0, + memory_order_relaxed); + /* A worker/presenter queue wait is also spare CPU time. It must not + * starve admission merely because the main pacer didn't sleep. */ + in.budget = fmax(credit * 0.8, in.frame_length * 0.4); + return render_pass_plan_phases(&in, alpha_q16, wanted); + } GL_RT_SYNC("pass_plan"); RenderPassPlanInput in; double freq, sp, spare; @@ -8470,6 +8565,10 @@ void gl_renderer_pass_plan_diag(GLRenderPassPlanDiag *out) { } void gl_renderer_pass_note_cost(uint64_t ticks) { + if (temporal_recording()) { + RTH_REC(RTH_PASS_COST, 0, (uint32_t)ticks, (uint32_t)(ticks >> 32)); + return; + } 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); @@ -8485,8 +8584,13 @@ void gl_renderer_pass_note_cost(uint64_t ticks) { } uint32_t gl_renderer_pass_leaks(void) { + if (temporal_recording()) return 0; /* GPU journal is owned by the worker. */ GL_RT_SYNC("pass_leaks"); return s_pass_leaks; } int gl_renderer_pass_verify_vram(void) { + /* Completed GPU transactions are checked by their owner; pending work + * cannot be inspected by taking its context back. CPU state is checked + * independently by render_pass.c before the producer continues. */ + if (temporal_recording()) return atomic_load(&s_temporal_verify_failed) == 0; GL_RT_SYNC("pass_verify_vram"); return s_pv_ok; } @@ -8500,7 +8604,9 @@ static GLuint pass_wide_fbo_for(int base_x) { static int pass_make_color_fbo(GLuint *tex, GLuint *rb, GLuint *fbo, int *cur_w, int *cur_h, int w, int h, GLenum internal, GLenum fmt, GLenum type) { - if (*fbo && *cur_w == w && *cur_h == h) return 1; + int backup = tex == &s_pb_hr_tex || tex == &s_pb_raw_tex || tex == &s_pb_wide_tex; + if (*fbo && ((*cur_w == w && *cur_h == h) || + (backup && *cur_w >= w && *cur_h >= h))) return 1; s_pass_begin_diag.gl_error_before = pass_gl_errors(); s_pass_begin_diag.gl_error = 0; s_pass_begin_diag.fbo_status = 0; @@ -8542,11 +8648,12 @@ static int pass_gen_reserve(int gi, uint32_t need, int w, int h) { s_pass_begin_diag.resource = "generation_slot_cap"; return 0; } - if (s_pgen_alloc_w[gi] != w || s_pgen_alloc_h[gi] != h) { + if (s_pgen_alloc_w[gi] < w || s_pgen_alloc_h[gi] < h) { pass_gen_release(gi); s_pgen_alloc_w[gi] = w; s_pgen_alloc_h[gi] = h; } + w = s_pgen_alloc_w[gi]; h = s_pgen_alloc_h[gi]; while (s_pgen_alloc_n[gi] < need) { GLuint *t = &s_pgen_tex[gi][s_pgen_alloc_n[gi]]; s_pass_begin_diag.gl_error_before = pass_gl_errors(); @@ -8705,7 +8812,7 @@ static int pass_journal_protect(int x, int y, int w, int h) { if (render_pass_journal_add(&s_pj_cpu, s_vram, VRAM_W, x, y, w, h) != i) return 0; pass_blit(s_hr_fbo, j->hr_fbo, x * S, y * S, 0, 0, w * S, h * S, - GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); pass_blit(s_raw_fbo, j->raw_fbo, x, y, 0, 0, w, h, GL_COLOR_BUFFER_BIT); s_pj_total++; return 1; @@ -8718,7 +8825,7 @@ static void pass_journal_rollback(void) { int x = s_pj_cpu.x[i], y = s_pj_cpu.y[i]; int w = s_pj_cpu.w[i], h = s_pj_cpu.h[i]; pass_blit(j->hr_fbo, s_hr_fbo, 0, 0, x * S, y * S, w * S, h * S, - GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); pass_blit(j->raw_fbo, s_raw_fbo, 0, 0, x, y, w, h, GL_COLOR_BUFFER_BIT); } render_pass_journal_rollback(&s_pj_cpu, s_vram, VRAM_W); @@ -8741,7 +8848,11 @@ static int transaction_begin(int x, int y, int w, int h, int open_gen, s_pass_begin_diag.source_path = s_interp_source_path; s_pass_begin_diag.capture_w = s_interp_w; s_pass_begin_diag.capture_h = s_interp_h; - if(open_gen && !stereo) { + if(open_gen && !stereo && s_recorded_preparing) { + /* Composed presents own independent slot textures. Once their GL + * commands are queued, this context may reuse its one extra image. */ + s_pgen_write=s_pgen_cur; + } else if(open_gen && !stereo) { uint64_t order[PASS_GENERATIONS];uint32_t valid=0; for(int i=0;iorder=++s_pgen_order;g->created_cycle=psx_cycle_count; @@ -8802,7 +8913,8 @@ static int transaction_begin(int x, int y, int w, int h, int open_gen, g->source_path = wide ? GL_PRES_WIDE : GL_PRES_VRAM; g->period = period_vblanks ? period_vblanks : 1u; g->shown = s_pass_flip_shown; - pass_capture_into(s_pgen_tex[gi][0], g); /* the game's own image */ + if (s_recorded_preparing) g->external_base=s_interp_tex[s_interp_prev]; + else pass_capture_into(s_pgen_tex[gi][0], g); /* the game's own image */ g->phase[0] = 0; g->n = 1; g->valid = 1; @@ -8832,7 +8944,7 @@ static int transaction_begin(int x, int y, int w, int h, int open_gen, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT)) return pass_begin_refuse("backup_raw"); pass_blit(s_hr_fbo, s_pb_hr_fbo, x * S, y * S, 0, 0, w * S, h * S, - GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); pass_blit(s_raw_fbo, s_pb_raw_fbo, x, y, 0, 0, w, h, GL_COLOR_BUFFER_BIT); s_pb_wide_src = g_wide_w > 0 ? pass_wide_fbo_for(x) : 0; if (s_pb_wide_src) { @@ -8842,7 +8954,7 @@ static int transaction_begin(int x, int y, int w, int h, int open_gen, h * S, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE)) return pass_begin_refuse("backup_wide"); pass_blit(s_pb_wide_src, s_pb_wide_fbo, 0, y * S, 0, 0, g_wide_w * S, - h * S, GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + h * S, GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); } if (s_pb_cpu_cap < (size_t)w * (size_t)h) { free(s_pb_cpu); @@ -8886,8 +8998,8 @@ backed_up: * colour (hr surface, native-wide band); everything else is put back. */ static void transaction_restore_ex(int keep_color) { int S = s_hr_scale, gi = s_pgen_write; - const GLbitfield hr_bits = keep_color ? GL_STENCIL_BUFFER_BIT - : GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT; + const GLbitfield hr_bits = keep_color ? GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT + : GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT; (void)gi; if (!s_pass_active) return; /* Roll the journal, then the rect back. */ @@ -8968,6 +9080,9 @@ 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) { + if (temporal_recording()) + return rth_rec_ints(RTH_PASS_BEGIN, 0, 7, + (int32_t[]){x, y, w, h, open_gen, (int32_t)period, reuse}); GL_RT_SYNC("pass_begin"); return transaction_begin(x, y, w, h, open_gen, period, reuse, 0); } @@ -8976,11 +9091,25 @@ int gl_renderer_stereo_begin(int x, int y, int w, int h, int reuse) { return transaction_begin(x, y, w, h, 0, 0, reuse, 1); } void gl_renderer_pass_end(uint32_t alpha_q16, int keep) { + if (temporal_recording()) { + RTH_REC(RTH_PASS_END, 0, alpha_q16, keep); + rth_pass_boundary(); + return; + } GL_RT_SYNC("pass_end"); int gi = s_pgen_write; PassGen *g = &s_pgen[gi]; if (!s_pass_active) return; flush_flat_batch(); flush_tex_batch(); flush_cpu_upload(); + if (g->external_base) { + if (keep && alpha_q16 && g->valid && g->nn,g->tex_w,g->tex_h)) { + pass_capture_into(s_pgen_tex[gi][g->n-1u],g); + g->phase[g->n++]=alpha_q16; + } + transaction_restore(); + return; + } if (keep && alpha_q16 && g->valid && !g->promoted && g->n < PASS_SLOTS && g->n < pass_slot_cap(g->tex_w, g->tex_h) && pass_gen_reserve(gi, g->n + 1u, g->tex_w, g->tex_h)) { @@ -9487,6 +9616,7 @@ static int pass_gen_present(uint64_t deadline) { float t = 0.0f; double p; if (!g->valid || !g->promoted || !(g->t_len > 0.0)) return 0; + if (g->external_base && !s_interp_hold) return 0; if (g->source_path != s_interp_source_path || g->tex_w != s_interp_w || g->tex_h != s_interp_h) return 0; @@ -9499,8 +9629,16 @@ static int pass_gen_present(uint64_t deadline) { s_pgen_expired++; return 0; } - if (!interp_present_pair(s_pgen_tex[s_pgen_cur][lo], - s_pgen_tex[s_pgen_cur][hi], t, s_interp_blend_mode)) + s_interp_uv_x = g->external_base && !lo ? 1.f : (float)g->tex_w / s_pgen_alloc_w[s_pgen_cur]; + s_interp_uv_y = g->external_base && !lo ? 1.f : (float)g->tex_h / s_pgen_alloc_h[s_pgen_cur]; + s_interp_pending_extra = lo != 0 || hi != 0; + s_interp_pending_identity = (g->base_identity << 16) | g->phase[lo]; + int presented = interp_present_pair(pass_gen_texture(s_pgen_cur,lo), + pass_gen_texture(s_pgen_cur,hi), t, s_interp_blend_mode); + s_interp_uv_x = s_interp_uv_y = 1.f; + s_interp_pending_extra = 0; + s_interp_pending_identity = 0; + if (!presented) return 0; s_pgen_presents++; if (lo != hi) s_pgen_blends++; @@ -9580,6 +9718,7 @@ static uint64_t s_present_ticks_accum_fwd(uint64_t add) { } void gl_renderer_pass_set_flip_shown(int shown) { + if (temporal_recording()) { RTH_REC(RTH_PASS_SHOWN, 0, shown); return; } 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 @@ -9619,19 +9758,26 @@ static void pass_dump_generation(int gi) { size_t n = (size_t)g->tex_w * (size_t)g->tex_h * 3u; uint8_t *rgb = (uint8_t *)malloc(n); if (!rgb) return; + GLint read_fbo=0; GLuint fbo=0; + glGetIntegerv(0x8CAA /* READ_FRAMEBUFFER_BINDING */,&read_fbo); + p_glGenFramebuffers(1,&fbo); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,fbo); + glReadBuffer(PSXGL_COLOR_ATTACHMENT0); glPixelStorei(GL_PACK_ALIGNMENT, 1); for (uint32_t i = 0; i < g->n; i++) { char path[512]; FILE *f; - glBindTexture(GL_TEXTURE_2D, s_pgen_tex[gi][i]); - glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, GL_UNSIGNED_BYTE, rgb); + p_glFramebufferTexture2D(PSXGL_READ_FRAMEBUFFER,PSXGL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D,pass_gen_texture(gi,i),0); + glReadPixels(0,0,g->tex_w,g->tex_h,GL_RGB,GL_UNSIGNED_BYTE,rgb); snprintf(path, sizeof path, "%s/g%03llu_%02u_a%05u.png", s_pdump_dir, (unsigned long long)s_pdump_gen, (unsigned)i, (unsigned)g->phase[i]); f = fopen(path, "wb"); if (f) { (void)png_write_rgb(f, rgb, (uint32_t)g->tex_w, (uint32_t)g->tex_h); fclose(f); } } - glBindTexture(GL_TEXTURE_2D, 0); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,(GLuint)read_fbo); + p_glDeleteFramebuffers(1,&fbo); free(rgb); s_pdump_gen++; s_pdump_left--; @@ -9648,7 +9794,10 @@ static void pass_apply_promotion(void) { PassGen *g = &s_pgen[s_pgen_cur]; double sp = s_interp_schedule.frame_end - s_interp_schedule.frame_start; g->promoted = 1; - g->t_start = s_interp_schedule.frame_start; + /* The first output deadline displays phase zero. Starting at the + * interval boundary instead selects the half-phase immediately at + * 60 Hz, then repeats it on the second VBlank of a 30 Hz source. */ + g->t_start = s_interp_schedule.next_present_deadline; g->t_len = (double)g->period * sp; /* Phase 0 is the image the game actually flipped to. The capture at * the pass point is the same for a game that draws nothing more into @@ -9870,6 +10019,20 @@ static GLenum s_pt_format = GL_RGBA8; /* the window's: no alpha -> GL_R 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 uint64_t s_pts_deadline[PT_MAX_SLOTS]; +static uint64_t s_pts_identity[PT_MAX_SLOTS]; +typedef struct PresentSample { uint64_t ticks, identity, deadline; } PresentSample; +static _Atomic uint64_t s_pt_clock_ticks, s_pt_window_ticks, s_pt_late_drops, s_pt_unready_drops; +static PresentSample s_present_samples[128]; +static uint64_t s_present_sample_count; +static atomic_flag s_present_sample_lock = ATOMIC_FLAG_INIT; +static void present_sample(uint64_t identity, uint64_t deadline) { + while (atomic_flag_test_and_set_explicit(&s_present_sample_lock,memory_order_acquire)) {} + PresentSample *s=&s_present_samples[s_present_sample_count++ % 128u]; + s->ticks=SDL_GetPerformanceCounter(); s->identity=identity; + s->deadline=deadline; + atomic_flag_clear_explicit(&s_present_sample_lock,memory_order_release); +} +static int s_pts_extra[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) */ @@ -9931,7 +10094,7 @@ static int pt_cb_ctx(void *user, int current) { s_ptp_fbo[i] = 0; s_ptp_gen[i] = 0; } - glFinish(); + glFlush(); SDL_GL_MakeCurrent(s_win, NULL); return 1; } @@ -9954,7 +10117,24 @@ static void *pt_cb_present(void *user, int slot, void *ready) { 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); } + /* Never make a late temporal image stall a present. The fence is only + * a readiness test (zero timeout), after the CPU deadline wait. */ + uint64_t wait_start=SDL_GetPerformanceCounter(); + interp_wait_until_ex(s_pts_deadline[slot], SDL_GetPerformanceFrequency(), 0); + atomic_fetch_add(&s_pt_clock_ticks,SDL_GetPerformanceCounter()-wait_start); + int late = s_pts_extra[slot] && s_pts_deadline[slot] && + SDL_GetPerformanceCounter() > s_pts_deadline[slot] + SDL_GetPerformanceFrequency() / 1000u; + if (ready) { + GLint status = 0; + p_glGetSynciv(ready, 0x9114 /* SYNC_STATUS */, 1, NULL, &status); + p_glDeleteSync(ready); + if (status != 0x9119 /* SIGNALED */ || late) { + if (status != 0x9119) atomic_fetch_add(&s_pt_unready_drops,1); + if (late) atomic_fetch_add(&s_pt_late_drops,1); + if (s_pts_extra[slot]) atomic_fetch_add(&s_temporal_dropped, 1); + return NULL; + } + } 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]); @@ -9981,8 +10161,12 @@ static void *pt_cb_present(void *user, int slot, void *ready) { GL_PRESENT_THREAD_TEST_HOOK(slot); #endif void *done = p_glFenceSync(PT_SYNC_GPU_COMMANDS_COMPLETE, 0); - interp_wait_until_ex(s_pts_deadline[slot], SDL_GetPerformanceFrequency(), 0); + uint64_t swap_start=SDL_GetPerformanceCounter(); gl_window_swap(); + atomic_fetch_add(&s_pt_window_ticks,SDL_GetPerformanceCounter()-swap_start); + if (s_recorded_passes) present_sample(s_pts_identity[slot],s_pts_deadline[slot]); + if (s_pts_extra[slot]) atomic_fetch_add(&s_temporal_presented, 1); + glFlush(); return done; } @@ -9994,7 +10178,7 @@ static void pt_cb_dispose(void *user, void *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) { + if (!p_glFenceSync || !p_glWaitSync || !p_glGetSynciv || !p_glDeleteSync) { fprintf(stdout, "psxrecomp: present thread: no sync objects; swaps stay on the render thread\n"); return 0; } @@ -10064,10 +10248,18 @@ static void gl_present_swap(void) { if (!s_pt_on) { gl_window_swap(); return; } (void)pt_target_fbo(); /* a frame that never bound 0 still has its slot */ s_pts_deadline[pt_current()] = s_interp_pending_deadline; + s_pts_identity[pt_current()] = s_interp_pending_identity ? s_interp_pending_identity : s_interp_captures << 16; + s_pts_extra[pt_current()] = s_interp_pending_extra; void *ready = p_glFenceSync(PT_SYNC_GPU_COMMANDS_COMPLETE, 0); glFlush(); void *done = NULL; - (void)pt_submit(ready, &done, NULL); + if (s_interp_enabled && s_rth_on && rt_on_render_thread()) { + if (pt_try_submit(ready, &done)<0) { + p_glDeleteSync(ready); + if (s_interp_pending_extra) atomic_fetch_add(&s_temporal_dropped,1); + return; + } + } else (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; @@ -10085,11 +10277,15 @@ int gl_renderer_present_thread_json(char *out, size_t cap) { 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}", + "\"queued_high\":%d,\"clock_wait_ms\":%.3f,\"window_swap_ms\":%.3f,\"late_drops\":%llu,\"unready_drops\":%llu}", 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); + st.present_max_ns / 1e6, st.queued_high, + (double)atomic_load(&s_pt_clock_ticks)*1000.0/SDL_GetPerformanceFrequency(), + (double)atomic_load(&s_pt_window_ticks)*1000.0/SDL_GetPerformanceFrequency(), + (unsigned long long)atomic_load(&s_pt_late_drops), + (unsigned long long)atomic_load(&s_pt_unready_drops)); } static void gl_swap_now(int generated); static uint64_t s_capture_source_sequence; @@ -11425,6 +11621,9 @@ 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 void rth_pass_boundary(void) { + s_rthe_flat_bd = s_rthe_vp_w = s_rthe_bg_full = -1; +} static uint64_t s_rth_presents = 0, s_rth_presents_stale = 0; #define RTHF_PRIM 0x0001u @@ -11490,9 +11689,16 @@ static int rth_rec_ints(uint16_t op, uint16_t flags, int n, const int32_t *v) { !(op >= RTH_FLAT_TRI && op <= RTH_SHADED_LINE)) { s_rth_material_valid = 0; } - int32_t *p = (int32_t *)rt_cmd_begin(op, flags, (uint32_t)n * 4u); + const int projected = s_recorded_passes && + (op == RTH_FLAT_TRI || op == RTH_GOURAUD_TRI || op == RTH_TEX_TRI || op == RTH_SHADED_TEX_TRI); + uint32_t extra = projected ? sizeof s_recorded_source : 0u; + int32_t *p = (int32_t *)rt_cmd_begin(op, flags | (projected ? RTHF_PROJECTION : 0), (uint32_t)n * 4u + extra); if (!p) return 0; memcpy(p, v, (size_t)n * 4u); + if (projected) { + memcpy((uint8_t *)p + (size_t)n * 4u, &s_recorded_source, sizeof s_recorded_source); + memset(&s_recorded_source, 0, sizeof s_recorded_source); + } rt_cmd_commit(); if (material >= 0) { memcpy(s_rth_material[material], v, (size_t)n * 4u); @@ -11531,7 +11737,7 @@ 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 && !(s_hd_native_authority && s_rth_a0_open) && - !gpu_display_is_depth24() && !s_pass_active && + !gpu_display_is_depth24() && (temporal_recording() || !s_pass_active) && !psx_netplay_active() && !psx_openxr_session_active() && gr_backend() == GR_BACKEND_OPENGL; } @@ -11549,6 +11755,7 @@ static void rth_hd_publish_private(void) { static void gl_rth_acquire(const char *reason) { if (!s_rth_on || rt_on_render_thread() || rt_held()) return; rt_acquire(reason); + if (s_recorded_passes) s_fg_broken = 1; rth_hd_publish_private(); s_vram = s_rth_vram_pub; rth_rebind_vram(s_rth_vram_pub); @@ -11577,6 +11784,7 @@ static int rth_record_mode(void) { /* Present records carry the host overlays the emulation thread rasterized. */ typedef struct RthOvHdr { + uint64_t source_ticks; /* producer wall-clock PTS, independent of replay bursts */ int32_t needs_present, has, w[4], h[4]; float slide; int32_t args[8], nargs; @@ -11588,6 +11796,7 @@ static int rth_record_present(uint16_t op, int n, const int32_t *v) { int w[4] = { 0 }, h[4] = { 0 }; RthOvHdr hd; memset(&hd, 0, sizeof hd); + hd.source_ticks = SDL_GetPerformanceCounter(); 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) { @@ -11620,6 +11829,7 @@ static int rth_record_present(uint16_t op, int n, const int32_t *v) { static void rth_present_payload(uint16_t op, const uint8_t *p); static void rth_replay_present(const RtCmd *c, const uint8_t *p) { s_capture_source_sequence++; + if (s_recorded_passes) recorded_note_present(c->op, p); static int skipped_last = 0; /* Readback deliberately slows diagnostic captures. Do not let that * workload discard the intervening frames the burst was asked to show. */ @@ -11646,6 +11856,7 @@ static void rth_replay_present(const RtCmd *c, const uint8_t *p) { static void rth_present_payload(uint16_t op, const uint8_t *p) { RthOvHdr hd; memcpy(&hd, p, sizeof hd); + s_interp_input_ticks = hd.source_ticks; const uint8_t *q = p + sizeof hd; memset(&s_rth_ov, 0, sizeof s_rth_ov); s_rth_ov.valid = 1; @@ -12306,7 +12517,15 @@ static void fg_capture(const RtCmd *c, const void *payload) { /* The identities belong to this triangle only if it is drawn where * they were looked up (a culled triangle leaves its record behind). */ for (int k = 0; k < 3; k++) raw.vid[k] = 0; - if (s_fg_src_valid) { + if (c->flags & RTHF_PROJECTION) { + RecordedProjection meta; + memcpy(&meta, (const uint8_t *)payload + c->payload - sizeof meta, sizeof meta); + for (int k = 0; k < 3; k++) { + raw.vid[k] = meta.id[k]; + memcpy(raw.p[k], meta.p + 3*k, sizeof raw.p[k]); + raw.h[k] = 0; + } + } else if (s_fg_src_valid) { int at = 1; for (int k = 0; k < 3; k++) if (fabsf(raw.x[k] - (float)s_fg_src[3 + 2 * k]) > 1.5f || @@ -12561,7 +12780,9 @@ static void fg_replay(const FgList *a, const FgList *b, double t, GLuint gen_wid if (nx && ny) s_fg_pos_cap = b->prims.n; } if (s_fg_pos_cap >= b->prims.n) { - fg_cam_place(&b->prims, &s_fg_fit, s_fg_verts, t, s_fg_px, s_fg_py, s_fg_margin); + if (s_recorded_replaying) + fg_dataflow_place(&b->prims, s_fg_verts, t, s_fg_px, s_fg_py); + else fg_cam_place(&b->prims, &s_fg_fit, s_fg_verts, t, s_fg_px, s_fg_py, s_fg_margin); if (a) fg_hud_lerp(&b->prims, &a->prims, 1.0 - t, s_fg_px, s_fg_py, 24.0f); placed = 1; } @@ -12622,7 +12843,7 @@ static void fg_replay(const FgList *a, const FgList *b, double t, GLuint gen_wid * camera uncovers (the road below the receding near edge) shows * that frame's pixels instead of the clear colour. Fills after * drawing (panels, letterbox bands) are part of the picture. */ - if (drawn && !s_fg_replay_objects_only && fg_rect_meets(v[0], v[1], v[2], v[3], disp)) + if ((drawn || s_recorded_replaying) && !s_fg_replay_objects_only && fg_rect_meets(v[0], v[1], v[2], v[3], disp)) glb_fill_rect(v[0], v[1], v[2], v[3], (uint16_t)v[4]); break; case RTH_FLAT_RECT: @@ -12715,6 +12936,15 @@ static void fg_replay(const FgList *a, const FgList *b, double t, GLuint gen_wid } for (int k = 0; k < 3; k++) { x[k] += ddx; y[k] += ddy; } drawn = 1; + if (s_recorded_replaying && pb->p[0][2] > 0 && pb->p[1][2] > 0 && pb->p[2][2] > 0) { + float z[3]; + for (int k=0;k<3;k++) { + const FgVert *fv = &s_fg_verts[3*pi+k]; + z[k] = fv->paired ? fv->a[2]+(pb->p[k][2]-fv->a[2])*(float)t : pb->p[k][2]; + } + if (s_pz_valid) glb_set_depth_triangle(1, z[0], z[1], z[2]); + if (s_pq_valid) glb_set_perspective_triangle(1, 1.f/z[0], 1.f/z[1], 1.f/z[2]); + } fg_draw_tri_at(h.op, v, pl, x, y); precise_consumed(); pc_pending = 0; @@ -12731,6 +12961,123 @@ static void fg_replay(const FgList *a, const FgList *b, double t, GLuint gen_wid hiw_flush_queue(); } + +/* Recorded temporal passes share the transaction's small display-rect + * backups, not a second full VRAM renderer. Lists and their projections are + * immutable once closed; all replay/capture/restore runs on this context. */ +static uint64_t s_recorded_capture; +void gl_renderer_set_recorded_passes(int on) { + GL_RT_SYNC("recorded_passes"); + s_recorded_passes = on != 0; + s_recorded_capture = 0; + atomic_store(&s_recorded_completed, 0); + memset(&s_recorded_source, 0, sizeof s_recorded_source); + fg_invalidate(); + if (on) pgxp_set_projection_tracking(1); + else { + pgxp_set_projection_tracking(gpu_ws_native_wide_projection_correction(NULL)); + } +} +int gl_renderer_recorded_passes_enabled(void) { return s_recorded_passes; } +uint64_t gl_renderer_recorded_pass_count(void) { return atomic_load(&s_recorded_completed); } +void gl_renderer_recorded_sources(const float p[9], const uint32_t id[3]) { + if (!p || !id) { memset(&s_recorded_source, 0, sizeof s_recorded_source); return; } + memcpy(s_recorded_source.p, p, sizeof s_recorded_source.p); + memcpy(s_recorded_source.id, id, sizeof s_recorded_source.id); +} +static void recorded_note_present(uint16_t op, const uint8_t *payload) { + RthOvHdr hdr; memcpy(&hdr, payload, sizeof hdr); + const int32_t *a = hdr.args; + int wide = op == RTH_PRESENT_WIDE; + int disp[4] = {a[0], a[1], wide ? g_wide_w-2*g_wide_off : a[2], wide ? a[2] : a[3]}; + if (s_fg_broken) { + fg_invalidate(); pass_gens_invalidate(); s_fg_broken=0; s_recorded_capture=0; + } + int k; + for (k=0;k=0 && s_fg_buf[s_fg_cur_buf][0]==disp[0] && s_fg_buf[s_fg_cur_buf][1]==disp[1]) { + fg_rotate(); s_fg_cur_buf=-1; + } + FgList *b=&s_fg_l[s_fg_newer]; + if (b->valid && b->disp[0]==disp[0] && b->disp[1]==disp[1]) { + b->wide=wide; b->linear=wide ? a[3] : a[4]; b->force43=wide ? 0 : a[5]; + } +} +static void recorded_prepare_generation(void) { + if (!rt_on_render_thread() || s_interp_valid<2 || s_hiw || + s_recorded_capture==s_interp_captures) return; + s_recorded_capture=s_interp_captures; + FgList *a=&s_fg_l[s_fg_older], *b=&s_fg_l[s_fg_newer]; + if (!a->valid || !b->valid || !b->prims.n || + b->disp[0]!=s_interp_origin_x || b->disp[1]!=s_interp_origin_y || + a->disp[2]!=b->disp[2] || a->disp[3]!=b->disp[3] || + a->wide_w!=b->wide_w || b->wide_w!=g_wide_w) return; + uint32_t period=s_interp_flip.period; + if (period<2 || period>8) return; + if (s_fg_verts_capprims.n) { + FgVert *v=(FgVert *)realloc(s_fg_verts,(size_t)b->prims.n*3*sizeof *v); + if (!v) return; + s_fg_verts=v; s_fg_verts_cap=b->prims.n; + } + uint32_t paired=fg_dataflow_match(&a->prims,&b->prims,s_fg_verts), sources=0; + for (uint32_t i=0;iprims.n;i++) for (int k=0;k<3;k++) + if (b->prims.v[i].vid[k] && b->prims.v[i].p[k][2]>0.f) sources++; + atomic_store(&s_recorded_paired,paired); atomic_store(&s_recorded_sources,sources); + if (!paired) return; + FgState state; fg_state_capture(&state); + GLuint wide_cur=g_wide_cur; + int pc=s_pc_valid,pq=s_pq_valid,pz=s_pz_valid,dither=s_dither_bit; + float attrib[12]; + memcpy(attrib,s_pc_x,12); memcpy(attrib+3,s_pc_y,12); + memcpy(attrib+6,s_pq,12); memcpy(attrib+9,s_pz,12); + int need=s_depth_need_clear, used=s_depth_used, reason=s_depth_need_reason; + float avg=s_depth_last_avg; + double frame=(double)period*SDL_GetPerformanceFrequency()/s_interp_source_hz; + uint32_t n=(uint32_t)floor(frame/s_interp_schedule.target_period+.01); + if (n>PASS_SLOTS) n=PASS_SLOTS; + uint32_t cap=pass_slot_cap(s_interp_w,s_interp_h); + if (n>cap) n=cap; + if (n<2) return; + int saved_shown=s_pass_flip_shown; + int written=-1; + s_pass_flip_shown=1; + s_recorded_preparing=1; + for (uint32_t phase=1;phase=deadline) break; + if (!gl_renderer_pass_begin(b->disp[0],b->disp[1],b->disp[2],b->disp[3],phase==1,period,phase!=1)) break; + int gi=s_pgen_write; + written=gi; + if (phase==1) s_pgen[gi].base_identity=s_interp_captures-1u; + s_depth_need_clear=1; + uint32_t leaks=s_pass_leaks; + s_recorded_replaying=1; + fg_replay(a,b,(double)phase/n,pass_wide_fbo_for(b->disp[0])); + s_recorded_replaying=0; + int keep=s_pass_leaks==leaks; + gl_renderer_pass_end((uint32_t)((uint64_t)phase*65536u/n),keep); + if (keep && s_pgen[gi].n>phase) atomic_fetch_add(&s_recorded_completed,1); + } + s_recorded_preparing=0; + if (written>=0 && s_pgen[written].n>1) { + s_pgen[s_pgen_cur].promoted=0; + s_pgen_cur=written; s_pgen_promote=-1; + s_pgen[written].promoted=1; + s_pgen[written].t_start=s_interp_schedule.next_present_deadline; + s_pgen[written].t_len=frame; + } + s_pass_flip_shown=saved_shown; + fg_state_apply(&state,wide_cur); + s_pc_valid=pc; s_pq_valid=pq; s_pz_valid=pz; s_dither_bit=dither; + memcpy(s_pc_x,attrib,12); memcpy(s_pc_y,attrib+3,12); + memcpy(s_pq,attrib+6,12); memcpy(s_pz,attrib+9,12); + s_depth_need_clear=need; s_depth_used=used; s_depth_need_reason=reason; s_depth_last_avg=avg; +} + /* The generated image composed like a real present (no hold-last, no * interpolation capture, no present bookkeeping), then swapped. */ static void fg_compose(const FgList *b, GLuint wide_fbo, GLuint wide_tex, int linear, @@ -14318,8 +14665,34 @@ static const GpuRenderBackend GL_RT_BACKEND = { /* ---- replay (render thread) ---------------------------------------------- */ static void gl_rth_exec(void *user, const RtCmd *c, const void *payload) { + static int pass_rejected; + static uint32_t pass_leaks_before; (void)user; const int32_t *v = (const int32_t *)payload; + if (c->op == RTH_PASS_BEGIN) { + pass_rejected = !gl_renderer_pass_begin(v[0], v[1], v[2], v[3], + v[4], (uint32_t)v[5], v[6]); + if (pass_rejected) atomic_fetch_add(&s_temporal_refused, 1); + pass_leaks_before = s_pass_leaks; + return; + } + if (c->op == RTH_PASS_END) { + if (!pass_rejected) { + int keep = v[1] && s_pass_leaks == pass_leaks_before; + gl_renderer_pass_end((uint32_t)v[0], keep); + if (keep) atomic_fetch_add(&s_temporal_completed, 1); + else atomic_fetch_add(&s_temporal_refused, 1); + if (!s_pv_ok) atomic_fetch_add(&s_temporal_verify_failed, 1); + } + pass_rejected = 0; + return; + } + if (pass_rejected) return; /* Do not execute draws from a refused transaction. */ + if (c->op == RTH_PASS_COST) { + gl_renderer_pass_note_cost((uint32_t)v[0] | ((uint64_t)(uint32_t)v[1] << 32)); + return; + } + if (c->op == RTH_PASS_SHOWN) { s_pass_flip_shown = v[0] != 0; return; } 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; @@ -14328,7 +14701,7 @@ static void gl_rth_exec(void *user, const RtCmd *c, const void *payload) { if (!s_rthf_open && (atomic_load_explicit(&s_rthf_want, memory_order_relaxed) || s_fg_on)) rthf_begin(); rthf_gpu_resume(); - if (s_fg_on) fg_capture(c, payload); + if (s_fg_on || s_recorded_passes) fg_capture(c, payload); switch (c->op) { case RTH_SEMI: glb_set_semi_transparency(v[0], v[1]); break; case RTH_DITHER: glb_set_dither(v[0]); break; @@ -14555,7 +14928,33 @@ 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, + "],\"temporal_completed\":%llu,\"temporal_refused\":%llu," + "\"temporal_verify_failed\":%llu,\"temporal_presented\":%llu,\"temporal_dropped\":%llu,", + (unsigned long long)atomic_load(&s_temporal_completed), + (unsigned long long)atomic_load(&s_temporal_refused), + (unsigned long long)atomic_load(&s_temporal_verify_failed), + (unsigned long long)atomic_load(&s_temporal_presented), + (unsigned long long)atomic_load(&s_temporal_dropped)); + if (k>0 && (size_t)k0 && (size_t)k128u ? sample_count-128u : 0; + for (uint64_t i=first;i0 && (size_t)kticks*1e6/SDL_GetPerformanceFrequency(), + (unsigned long long)(s->identity>>16),(unsigned)(s->identity&65535u), + (double)s->deadline*1e6/SDL_GetPerformanceFrequency()); + } + if (k>0 && (size_t)k 0 && (size_t)k < cap) k += gl_renderer_present_thread_json(out + k, cap - (size_t)k); return k; } diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index b8b19f9e..f74adf56 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -1257,16 +1257,24 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr) if (!proj_override && pgxp_projection_tracking() && shift == 12 && !lm) { const double z = (double)(mac3 >> 12); const double h_proj = (double)gte_h_scaled(gte); - double hx = (double)gte->MAC1 * h_proj; + double hx = (double)(mac1 >> 12) * h_proj; if (do_squash) hx = hx * s_ws_xnum / s_ws_xden; else if (s_ws_dome_on && s_ws_dome_num != s_ws_dome_den && !gpu_ws_present_native_43() && gte->SZ[3] >= s_ws_far_threshold) hx = hx * s_ws_dome_num / s_ws_dome_den; PGXPProjection projection = { (float)(gte->OFX / 65536.0 * z + hx), - (float)(gte->OFY / 65536.0 * z + (double)gte->MAC2 * h_proj), + (float)(gte->OFY / 65536.0 * z + (double)(mac2 >> 12) * h_proj), (float)z, (float)h_proj / 2.0f}; pgxp_gte_set_projection(&projection); + unsigned vi = V == gte->V1 ? 1u : V == gte->V2 ? 2u : 0u; + uint32_t id = pgxp_gte_input_identity(vi); + /* The address disambiguates identical local coordinates in + * different course meshes. Values also validate reused storage. */ + id = (id ^ (uint16_t)V[0]) * 16777619u; + id = (id ^ (uint16_t)V[1]) * 16777619u; + id = (id ^ (uint16_t)V[2]) * 16777619u; + pgxp_gte_projection_identity(id ? id : 1u); } } geom_note((uint32_t)gte->SXY[2], sx16, sy16); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 9abb3916..6ffda197 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1985,6 +1985,7 @@ extern "C" int psx_mod_set_adaptive_display_aspect( static PSXModSessionBaseline g_mod_owned_baseline; static void reset_mod_owned_presentation(void) { + gl_renderer_set_recorded_passes(0); psx_mod_internal_resolution_reset(); memcard_set_extension_codec(nullptr, nullptr); psx_local_mouse_clear(); @@ -4067,6 +4068,14 @@ static int present_should_wall_pace(void) { return g_frame_period_ms > 0.0; } +extern "C" int psx_mod_set_recorded_render_passes(int enabled) { + /* Activation precedes window/context creation and application of the + * render-thread preference. Record the request just like interpolation. */ + if (enabled && psx_netplay_active()) return 0; + gl_renderer_set_recorded_passes(enabled); + return 1; +} + static void apply_present_cadence(void) { #ifndef PSX_SDL_NO_RENDER const int interval = present_effective_swap_interval(); diff --git a/runtime/src/pgxp.cpp b/runtime/src/pgxp.cpp index 323cbfbc..40cc3e44 100644 --- a/runtime/src/pgxp.cpp +++ b/runtime/src/pgxp.cpp @@ -73,6 +73,7 @@ struct PGXPValue { uint16_t flags; uint32_t value; /* the guest word this shadow describes */ uint32_t gen; /* valid iff == s_gen (O(1) invalidate-all) */ + uint32_t identity; /* projected vertex identity (valid with PROJECTION) */ PGXPProjection projection; }; @@ -95,6 +96,7 @@ static PGXPValue *s_ram = nullptr; /* lazily allocated: RAM + mod reg static PGXPValue s_scratch[PGXP_SCRATCH_WORDS]; static PGXPValue s_gpr[34]; /* 32 GPRs + HI + LO */ static PGXPValue s_gte[32]; /* GTE data registers */ +static uint32_t s_model_origin[6]; /* only the six model input words */ static PGXPValue s_gtc[32]; /* GTE control registers (CTC2) */ /* Scalar tier (cpu-mode). Engines also use the GTE as a multiplier for @@ -144,6 +146,7 @@ static void ck_wrapped(void); extern "C" void pgxp_invalidate_all(void) { if (s_suppress != 0) { s_deferred_invalidate = 1; return; } + std::memset(s_model_origin, 0, sizeof s_model_origin); if (++s_gen == 0) { ck_wrapped(); /* generation wrapped: physically clear so stale slots can't revive */ @@ -382,6 +385,7 @@ static PGXPJournalEntry *s_ck_log = nullptr; static size_t s_ck_n = 0, s_ck_cap = 0; static int s_ck_lossy = 0; static PGXPValue s_ck_gpr[34], s_ck_gte[32], s_ck_gtc[32]; +static uint32_t s_ck_model_origin[6]; static PGXPScalar s_ck_gpr_s[34], s_ck_gte_s[32], s_ck_gtc_s[32]; static uint32_t s_ck_gen = 0, s_ck_suppress = 0; static int s_ck_deferred = 0; @@ -519,6 +523,7 @@ extern "C" void pgxp_checkpoint_begin(void) { s_ck_n = 0; std::memcpy(s_ck_gpr, s_gpr, sizeof s_gpr); std::memcpy(s_ck_gte, s_gte, sizeof s_gte); + std::memcpy(s_ck_model_origin, s_model_origin, sizeof s_model_origin); std::memcpy(s_ck_gtc, s_gtc, sizeof s_gtc); std::memcpy(s_ck_gpr_s, s_gpr_s, sizeof s_gpr_s); std::memcpy(s_ck_gte_s, s_gte_s, sizeof s_gte_s); @@ -540,6 +545,7 @@ extern "C" void pgxp_checkpoint_rollback(void) { s_ck_n = 0; std::memcpy(s_gpr, s_ck_gpr, sizeof s_gpr); std::memcpy(s_gte, s_ck_gte, sizeof s_gte); + std::memcpy(s_model_origin, s_ck_model_origin, sizeof s_model_origin); std::memcpy(s_gtc, s_ck_gtc, sizeof s_gtc); std::memcpy(s_gpr_s, s_ck_gpr_s, sizeof s_gpr_s); std::memcpy(s_gte_s, s_ck_gte_s, sizeof s_gte_s); @@ -841,6 +847,7 @@ extern "C" void psx_pgxp_cop2(struct CPUState *cpu, uint32_t instr, } else { pv_reset(dst, value); } + if (s_projection_tracking && f_rt(instr) < 6u) s_model_origin[f_rt(instr)] = addr & 0x1FFFFFFFu; sc_kill(&s_gte_s[f_rt(instr)]); /* memory carries no scalars */ gte_sxy2_mirror(f_rt(instr)); return; @@ -881,6 +888,7 @@ extern "C" void psx_pgxp_cop2(struct CPUState *cpu, uint32_t instr, case 0x04: { /* MTC2: gte[rd] <- gpr[rt] */ uint32_t rt = f_rt(instr); uint32_t rd = f_rd(instr); + if (rd < 6u) s_model_origin[rd] = 0; PGXPValue *dst = &s_gte[rd]; if (rt != 0 && s_gpr[rt].gen == s_gen) { pv_validate(&s_gpr[rt], value); @@ -1612,6 +1620,7 @@ extern "C" int pgxp_get_gte_sxy(uint32_t index, int32_t *x16, int32_t *y16) { extern "C" void pgxp_gte_set_projection(const PGXPProjection *projection) { if (!g_pgxp_active || !s_projection_tracking || !projection) return; s_gte[14].projection = *projection; + s_gte[14].identity = 0; /* a projection has no model identity until its producer supplies one */ s_gte[14].flags |= PGXP_F_PROJECTION; s_gte[15] = s_gte[14]; } @@ -1625,6 +1634,20 @@ static int projection_read(const PGXPValue *pv, uint32_t packed, PGXPProjection extern "C" int pgxp_load_projection(uint32_t addr, uint32_t packed, PGXPProjection *out) { return projection_read(pgxp_ptr(addr), packed, out); } +extern "C" uint32_t pgxp_gte_input_identity(uint32_t vertex) { + return vertex < 3u ? s_model_origin[vertex * 2u] : 0; +} +extern "C" void pgxp_gte_projection_identity(uint32_t identity) { + s_gte[14].identity = identity; + s_gte[15].identity = identity; +} +extern "C" int pgxp_load_temporal_projection(uint32_t addr, uint32_t packed, + PGXPProjection *out, uint32_t *identity) { + const PGXPValue *v = pgxp_ptr(addr); + if (!projection_read(v, packed, out)) return 0; + *identity = v->identity; + return *identity != 0; +} extern "C" int pgxp_get_gte_projection(uint32_t index, uint32_t packed, PGXPProjection *out) { return index < 4 ? projection_read(&s_gte[12+index], packed, out) : 0; } @@ -1678,6 +1701,7 @@ extern "C" void pgxp_gte_reg_written(int reg, uint32_t value) { * machine state back, so its writes must not stick to the shadows). */ if (s_suppress != 0) return; if (reg < 0 || reg > 31) return; + if (reg < 6) s_model_origin[reg] = 0; if (reg == 15) { /* SXYP is a FIFO push: SXY1 moves to SXY0, SXY2 to SXY1, the value * lands in SXY2 (psx-spx GTE "SXYP"). Engines draw quads from a diff --git a/runtime/src/present_thread.c b/runtime/src/present_thread.c index 3a6a6552..8e173542 100644 --- a/runtime/src/present_thread.c +++ b/runtime/src/present_thread.c @@ -216,6 +216,30 @@ int pt_submit(void *ready, void **next_done, uint64_t *wait_ns) { return next; } +int pt_try_submit(void *ready, void **next_done) { + if (next_done) *next_done = NULL; + if (!P.running) return -1; + mtx_lock(&P.mtx); + const int cur = P.current; + int next = -1; + for (int k=1;kP.st.queued_high) P.st.queued_high=P.q_len; + 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; + cnd_bcast(&P.cv); + mtx_unlock(&P.mtx); + return next; +} + void pt_drain(void) { if (!P.running) return; mtx_lock(&P.mtx); diff --git a/runtime/src/render_pass.c b/runtime/src/render_pass.c index fc47f7b3..a1b65001 100644 --- a/runtime/src/render_pass.c +++ b/runtime/src/render_pass.c @@ -39,6 +39,7 @@ extern uint8_t *memory_get_ram_ptr(void); extern uint8_t *memory_get_scratchpad_ptr(void); +extern uint16_t *gpu_get_vram_ptr(void); /* Live main-RAM geometry (psx_ram_geometry.c): 2 MiB retail, or 8 MiB with * the opt-in 8 MB RAM mod. Latched by memory_init() once per boot, so it * cannot change inside a pass, but it can between sessions. */ @@ -260,6 +261,9 @@ static uint8_t *s_ram_copy; /* s_ram_copy_cap bytes */ static uint32_t s_ram_copy_cap; static uint8_t *s_dma_copy; static RenderPassCheckpoint s_ck; +/* The renderer worker has its own native VRAM. CPU-side GP0 uploads during + * a recorded pass must also roll back gpu.c's public copy. */ +static uint16_t s_pass_cpu_vram[1024u * 512u]; static PsxCycleFreeze s_freeze; static jmp_buf s_abort_jmp; static volatile int s_abort_armed; @@ -320,6 +324,7 @@ static int verify_on(void) { void render_pass_get_stats(RenderPassStats *out) { if (!out) return; *out = s_stats; + out->passes += gl_renderer_recorded_pass_count(); for (int i = 0; i < RENDER_PASS_DROP_CLASSES; i++) out->dropped[i] = g_render_pass_dropped_writes[i]; } @@ -555,6 +560,7 @@ static int checkpoint_save(const CPUState *cpu) { return 0; } s_ck.cpu = *cpu; + memcpy(s_pass_cpu_vram, gpu_get_vram_ptr(), sizeof s_pass_cpu_vram); s_ck.ram_bytes = ram_bytes; memcpy(s_ram_copy, memory_get_ram_ptr(), ram_bytes); memcpy(s_ck.spad, memory_get_scratchpad_ptr(), RP_SPAD_SIZE); @@ -579,6 +585,7 @@ static int checkpoint_save(const CPUState *cpu) { } static void checkpoint_restore(CPUState *cpu) { + memcpy(gpu_get_vram_ptr(), s_pass_cpu_vram, sizeof s_pass_cpu_vram); gpu_pass_checkpoint_restore(); (void)dma_snapshot_read(s_dma_copy, s_ck.dma_len); timers_set_snapshot(s_ck.t_counter, s_ck.t_mode, s_ck.t_target, diff --git a/runtime/src/render_pass_plan.c b/runtime/src/render_pass_plan.c index 4795203c..a7b9aad0 100644 --- a/runtime/src/render_pass_plan.c +++ b/runtime/src/render_pass_plan.c @@ -143,8 +143,17 @@ int render_pass_gen_select(const uint32_t *phases, uint32_t n, double p, int render_pass_gen_select_mode(const uint32_t *phases, uint32_t n, double p, int hold, uint32_t *lo, uint32_t *hi, float *t) { + /* Planned phases are Q16. Quantize the sampled clock to that same grid: + * a rounded host tick just below 1/2 must not repeat phase zero. */ + if (hold) { + if (!phases || !n || !isfinite(p) || p > RENDER_PASS_GEN_HOLD_MAX) return 0; + double q = floor(p * Q16 + 0.5); + uint32_t i = 0; + while (i + 1u < n && (double)phases[i + 1u] <= q) i++; + *lo = *hi = i; *t = 0.f; + return 1; + } if (!render_pass_gen_select(phases, n, p, lo, hi, t)) return 0; - if (hold) { *hi = *lo; *t = 0.0f; } return 1; } diff --git a/runtime/tests/test_frame_gen.c b/runtime/tests/test_frame_gen.c index 3d15d1d7..6ebe7cd9 100644 --- a/runtime/tests/test_frame_gen.c +++ b/runtime/tests/test_frame_gen.c @@ -492,7 +492,32 @@ static void test_small_view_policy(void) { check(fit.freeze_small_views, "fit propagates explicit reprojection policy"); } + +static void test_dataflow_projection(void) { + FgPrim a[2]={0},b[2]={0}; FgVert v[6]; float x[6],y[6]; + for(int j=0;j<2;j++) for(int k=0;k<3;k++) { + a[j].view=b[j].view=j+1; + a[j].vid[k]=b[j].vid[k]=k+1; + a[j].x[k]=100.f+j*200+k; b[j].x[k]=110.f+j*200+k; + a[j].y[k]=b[j].y[k]=30.f+k; + a[j].p[k][2]=150000.f; b[j].p[k][2]=160000.f; + a[j].p[k][0]=a[j].x[k]*a[j].p[k][2]; b[j].p[k][0]=b[j].x[k]*b[j].p[k][2]; + a[j].p[k][1]=a[j].y[k]*a[j].p[k][2]; b[j].p[k][1]=b[j].y[k]*b[j].p[k][2]; + } + FgPrimList old={a,2,2},cur={b,2,2}; + check(fg_dataflow_match(&old,&cur,v)==6,"exact source identities pair separate views at full depth"); + fg_dataflow_place(&cur,v,.5,x,y); + check(fabsf(x[0]-(100.f*150000.f+110.f*160000.f)/310000.f)<.001f, + "middle phase interpolates homogeneous geometry beyond hardware SZ range"); + check(fabsf(x[3]-x[0]-200.f)<.001f,"identical model ids do not cross views"); + fg_dataflow_place(&cur,v,1.,x,y); + check(x[0]==b[0].x[0] && y[0]==b[0].y[0],"new endpoint stays exact"); + b[0].vid[0]=0; + fg_dataflow_match(&old,&cur,v); fg_dataflow_place(&cur,v,.5,x,y); + check(x[0]==b[0].x[0] && !v[0].paired,"unproven vertices are never guessed"); +} int main(void) { + test_dataflow_projection(); test_small_view_policy(); test_view_affine(); test_hud_lerp(); diff --git a/runtime/tests/test_frame_interpolation_schedule.c b/runtime/tests/test_frame_interpolation_schedule.c index afb80d79..a5e768df 100644 --- a/runtime/tests/test_frame_interpolation_schedule.c +++ b/runtime/tests/test_frame_interpolation_schedule.c @@ -285,23 +285,23 @@ int main(void) { "output below source cadence should be rejected"); frame_interpolation_schedule_reset(&schedule); - schedule.queued = 1; - for (int i = 1; i <= 6; ++i) { - uint64_t deadline; + for (int i=1;i<=6;i++) { + uint64_t published=(uint64_t)(1000000.0+(i-1)*1000000.0/60.0+.5), deadline; float alpha; - CHECK(frame_interpolation_schedule_begin(&schedule, 1000000u, - 1000000u, 60.0, 60.0), "queued interval accepted"); - CHECK(frame_interpolation_schedule_next(&schedule, 1000000u, - &deadline, &alpha), "queued interval has one output"); - CHECK(fabs((double)deadline - (1000000.0 + i * 1000000.0 / 60.0)) < 1.0, - "early producer retains every presentation deadline"); - CHECK(!frame_interpolation_schedule_next(&schedule, 1000000u, - &deadline, &alpha), "queued interval has no duplicate output"); + CHECK(frame_interpolation_schedule_begin(&schedule,published,1000000u,60.,60.), + "producer PTS plans an asynchronous interval"); + CHECK(frame_interpolation_schedule_next(&schedule,published+2000u,&deadline,&alpha), + "replay arrival does not replace source PTS"); + CHECK(fabs((double)deadline-(1000000.0+i*1000000.0/60.0))<1.0, + "published source clock keeps the display grid stable"); + } + frame_interpolation_schedule_reset(&schedule); + for (int i=0;i<12;i++) { + CHECK(frame_interpolation_schedule_begin(&schedule,1000000u,1000000u,60.,60.), + "unpaced startup source accepted"); + CHECK(frame_interpolation_schedule_end(&schedule)<=1033334u, + "startup replay cannot accumulate arbitrarily future presents"); } - CHECK(frame_interpolation_schedule_begin(&schedule, 2000000u, - 1000000u, 60.0, 60.0), "queued clock recovers after sustained stall"); - CHECK(frame_interpolation_schedule_end(&schedule) == 2016667u, - "late queued producer reanchors after sustained stall"); printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures); return failures ? 1 : 0; diff --git a/runtime/tests/test_pgxp.cpp b/runtime/tests/test_pgxp.cpp index 9821b076..ca9a7364 100644 --- a/runtime/tests/test_pgxp.cpp +++ b/runtime/tests/test_pgxp.cpp @@ -882,13 +882,24 @@ int main(void) { { PGXPProjection p={12345.5f,-7311.0f,-62.0f,150.0f}, out; pgxp_set_projection_tracking(1); + psx_pgxp_cop2(nullptr,enc_i(0x32,0,0,0),0x01020304u,0x8020A000u); + CHECK(pgxp_gte_input_identity(0)==0x0020A000u); + pgxp_checkpoint_begin(); + psx_pgxp_cop2(nullptr,enc_i(0x32,0,0,0),0x01020304u,0x8020B000u); + CHECK(pgxp_gte_input_identity(0)==0x0020B000u); + pgxp_checkpoint_rollback(); + CHECK(pgxp_gte_input_identity(0)==0x0020A000u); pgxp_gte_push_sxy(X16,Y16,0,PACKED); pgxp_gte_set_projection(&p); + pgxp_gte_projection_identity(0x12345678u); CHECK(pgxp_get_gte_projection(2,PACKED,&out) && out.z==-62.0f); CHECK(!pgxp_get_gte_projection(2,PACKED^1,&out)); psx_pgxp_cop2(nullptr,MFC2(9,14),PACKED,0); psx_pgxp_store(nullptr,SW(1,9),ADDR_B,PACKED); CHECK(pgxp_load_projection(ADDR_B,PACKED,&out) && out.x==p.x && out.z==p.z); + uint32_t identity=0; + CHECK(pgxp_load_temporal_projection(ADDR_B,PACKED,&out,&identity) && identity==0x12345678u); + CHECK(!pgxp_load_temporal_projection(ADDR_B,PACKED^1,&out,&identity)); CHECK(!pgxp_load_projection(ADDR_B,PACKED^1,&out)); pgxp_checkpoint_begin(); psx_pgxp_store(nullptr,SB(1,9),ADDR_B,PACKED&255); diff --git a/runtime/tests/test_present_thread.c b/runtime/tests/test_present_thread.c index c1a8aa19..9dfa3631 100644 --- a/runtime/tests/test_present_thread.c +++ b/runtime/tests/test_present_thread.c @@ -32,11 +32,13 @@ static long present_us = 0; static _Atomic int disposed; static uintptr_t last_done[PT_MAX_SLOTS]; static _Atomic int order_bad; +static _Atomic int hold_present; 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); + while (atomic_load(&hold_present)) sleep_us(100); 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); @@ -108,7 +110,30 @@ static void run(int slots, long us, int expect_waits) { check(!pt_running() && pt_submit(NULL, NULL, NULL) == -1, "stopped"); } +static void test_optional_submit(void) { + reset(); present_us=0; + atomic_store(&hold_present,1); + PtConfig c=cfg(2); + check(pt_start(&c),"optional start"); + atomic_store(&slot_busy[pt_current()],1); + void *done=NULL; + check(pt_try_submit((void *)1,&done)==1,"first optional frame reserves free slot"); + int current=pt_current(); + check(pt_try_submit((void *)2,&done)==-1 && pt_current()==current && !done, + "full optional queue returns immediately without publishing or changing ownership"); + PtStats st; pt_get_stats(&st); + check(st.submits==1 && st.waits==0,"optional submission never waits"); + atomic_store(&hold_present,0); pt_drain(); + atomic_store(&slot_busy[pt_current()],1); + check(pt_try_submit((void *)2,&done)==0 && (uintptr_t)done==0x180u, + "optional retry reuses only completed slot and returns its token"); + pt_stop(); + check(atomic_load(&n_presented)==2 && presented[0]==1 && presented[1]==2, + "rejected optional frame did not enter presentation order"); +} + int main(void) { + test_optional_submit(); srand(7); run(3, 0, 0); run(3, 1500, 1); diff --git a/runtime/tests/test_render_pass_abort.c b/runtime/tests/test_render_pass_abort.c index 889edbda..d8ef3b54 100644 --- a/runtime/tests/test_render_pass_abort.c +++ b/runtime/tests/test_render_pass_abort.c @@ -165,6 +165,9 @@ uint64_t render_pass_mod_arenas_hash(void) { return 0; } uint64_t gpu_pass_state_hash(void) { return 42; } int gpu_pass_checkpoint_save(void) { return 1; } void gpu_pass_checkpoint_restore(void) {} +static uint16_t s_public_vram[1024u * 512u]; +uint16_t *gpu_get_vram_ptr(void) { return s_public_vram; } +uint64_t gl_renderer_recorded_pass_count(void) { return 0; } static uint64_t s_ticks; uint64_t gl_renderer_perf_ticks(void) { return s_ticks += 1000; } uint64_t gl_renderer_perf_frequency(void) { return 1000000000u; } diff --git a/runtime/tests/test_render_pass_guards.py b/runtime/tests/test_render_pass_guards.py index f4941417..5c02b07d 100644 --- a/runtime/tests/test_render_pass_guards.py +++ b/runtime/tests/test_render_pass_guards.py @@ -216,4 +216,20 @@ for header in re.findall(r"runtime/include/([\w.]+)", hash_list): assert "psx_cycle_freeze.h" not in hash_list, ( "psx_cycle_freeze.h is runtime-only; keep it out of the codegen hash") +# Temporal GPU work must stay on the context-owning worker. These guards +# catch accidental synchronous fallbacks in the producer/presenter hot path. +for sig, opcode in ( + ("int gl_renderer_pass_begin(", "RTH_PASS_BEGIN"), + ("void gl_renderer_pass_end(", "RTH_PASS_END"), + ("void gl_renderer_pass_note_cost(", "RTH_PASS_COST"), + ("void gl_renderer_pass_set_flip_shown(", "RTH_PASS_SHOWN")): + b = body(gl, sig) + assert b.index("temporal_recording()") < b.index("GL_RT_SYNC(") + assert opcode in b +b = body(gl, "static void *pt_cb_present(") +assert "p_glGetSynciv(" in b and "return NULL;" in b +assert "glClientWaitSync" not in b and "p_glWaitSync" not in b and "glFinish" not in b +b = body(gl, "static void gl_rth_exec(") +assert "if (pass_rejected) return;" in b and "RTH_PASS_END" in b +assert "memcpy(gpu_get_vram_ptr(), s_pass_cpu_vram" in rp print("render-pass sandbox guards passed") diff --git a/runtime/tests/test_render_pass_plan.c b/runtime/tests/test_render_pass_plan.c index 12398f3c..20346616 100644 --- a/runtime/tests/test_render_pass_plan.c +++ b/runtime/tests/test_render_pass_plan.c @@ -434,6 +434,10 @@ int main(void) { lo == 1 && hi == 1 && blend == 0.0f, "HOLD keeps a native sample between phases"); CHECK(render_pass_gen_select_mode(phases, 4, 0.5, 1, &lo, &hi, &blend) && lo == 2 && hi == 2 && blend == 0.0f, "HOLD advances at the next native phase"); + CHECK(render_pass_gen_select_mode(phases,4,0.5-1e-10,1,&lo,&hi,&blend) && lo==2 && hi==2, + "rounded host tick selects the planned Q16 halfway image"); + CHECK(render_pass_gen_select_mode(phases,4,0.5-0.75/65536.,1,&lo,&hi,&blend) && lo==1, + "a genuinely earlier tick still holds the prior phase"); CHECK(render_pass_gen_select_mode(phases, 4, 0.375, 0, &lo, &hi, &blend) && lo == 1 && hi == 2 && blend == 0.5f, "explicit blending remains supported"); CHECK(render_pass_gen_select_mode(phases, 4, 2.0, 1, &lo, &hi, &blend) && diff --git a/runtime/tests/test_render_pass_sandbox.c b/runtime/tests/test_render_pass_sandbox.c index 63eef85e..17af5707 100644 --- a/runtime/tests/test_render_pass_sandbox.c +++ b/runtime/tests/test_render_pass_sandbox.c @@ -219,6 +219,9 @@ uint64_t gpu_pass_state_hash(void) { return 42; } static int s_checkpoint_ok = 1; int gpu_pass_checkpoint_save(void) { return s_checkpoint_ok; } void gpu_pass_checkpoint_restore(void) {} +static uint16_t s_public_vram[1024u * 512u]; +uint16_t *gpu_get_vram_ptr(void) { return s_public_vram; } +uint64_t gl_renderer_recorded_pass_count(void) { return 0; } static uint64_t s_ticks; uint64_t gl_renderer_perf_ticks(void) { return s_ticks += 1000; } uint64_t gl_renderer_perf_frequency(void) { return 1000000000u; } @@ -373,6 +376,8 @@ static void test_store_policy(void) { /* ---- 2. a real pass ------------------------------------------------------ */ static int pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) { (void)user; (void)alpha_q16; + s_public_vram[0] = 0x1234; + s_public_vram[1024u * 512u - 1u] = 0x5678; cpu->gpr[8] = 0xDEADu; guest_store(0x80010000u, 0x12345678u, 4); /* RAM */ guest_store(0x1F800020u, 0x99u, 1); /* scratchpad */ @@ -444,7 +449,11 @@ static void test_pass(void) { irqs0 = s_irqs_raised; s_mmio_calls = 0; + s_public_vram[0] = 0xA55A; + s_public_vram[1024u * 512u - 1u] = 0x5AA5; CHECK(psx_mod_render_pass(&cpu, &pass, pass_fn, NULL) == 1, "the pass ran and was kept"); + CHECK(s_public_vram[0] == 0xA55A && s_public_vram[1024u * 512u - 1u] == 0x5AA5, + "queued GPU uploads restore the entire public VRAM copy"); render_pass_get_stats(&st); CHECK(st.passes == 1 && s_kept == 1 && s_open_passes == 0, "one image, pass closed"); CHECK(st.dropped[RENDER_PASS_DROP_SPU] == 2, "2 SPU stores dropped and counted");