WIP: checkpoint render-thread temporal presentation work

This commit is contained in:
Shokunin
2026-10-21 16:16:30 -07:00
parent af942084ea
commit f4b6809138
26 changed files with 738 additions and 68 deletions
+10
View File
@@ -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.
+1
View File
@@ -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 */
+5
View File
@@ -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). */
-3
View File
@@ -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
+4
View File
@@ -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. */
+3
View File
@@ -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 */
+5
View File
@@ -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. */
+3
View File
@@ -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);
+56
View File
@@ -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;i<b->n*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];
}
}
+1 -1
View File
@@ -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;
+14 -1
View File
@@ -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;
+444 -45
View File
@@ -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;i<PASS_GENERATIONS;++i){order[i]=s_pgen[i].order;if(s_pgen[i].valid)valid|=1u<<i;}
s_pgen_write=render_pass_generation_write(s_pgen_cur,valid,order,PASS_GENERATIONS);
@@ -8792,7 +8903,7 @@ static int transaction_begin(int x, int y, int w, int h, int open_gen,
} else if (open_gen) {
if (tw != s_interp_w || th != s_interp_h)
return pass_begin_refuse("capture_size"); /* not what is presented */
if (!pass_gen_reserve(gi, 1u, tw, th))
if (!s_recorded_preparing && !pass_gen_reserve(gi, 1u, tw, th))
return pass_begin_refuse("generation_reserve");
memset(g, 0, sizeof *g);
g->order=++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->n<PASS_SLOTS &&
pass_gen_reserve(gi,g->n,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<s_fg_nbuf;k++) if (s_fg_buf[k][0]==disp[0] && s_fg_buf[k][1]==disp[1]) break;
if (k==4) { s_fg_nbuf=0; k=0; fg_invalidate(); }
if (k==s_fg_nbuf) s_fg_nbuf++;
memcpy(s_fg_buf[k],disp,sizeof disp);
if (s_fg_cur_buf>=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_cap<b->prims.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;i<b->prims.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<n;phase++) {
/* Already late: drop this optional redraw, never wait for it. */
double deadline=s_interp_schedule.next_present_deadline + phase*s_interp_schedule.target_period;
if ((double)SDL_GetPerformanceCounter()>=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)k<cap) k+=snprintf(out+k,cap-(size_t)k,
"\"recorded_enabled\":%d,\"recorded_completed\":%llu,\"recorded_paired\":%u,\"recorded_sources\":%u,",
s_recorded_passes,(unsigned long long)atomic_load(&s_recorded_completed),
atomic_load(&s_recorded_paired),atomic_load(&s_recorded_sources));
PresentSample samples[128]; uint64_t sample_count;
while (atomic_flag_test_and_set_explicit(&s_present_sample_lock,memory_order_acquire)) {}
sample_count=s_present_sample_count;
memcpy(samples,s_present_samples,sizeof samples);
atomic_flag_clear_explicit(&s_present_sample_lock,memory_order_release);
if (k>0 && (size_t)k<cap) k+=snprintf(out+k,cap-(size_t)k,"\"present_samples\":[");
uint64_t first=sample_count>128u ? sample_count-128u : 0;
for (uint64_t i=first;i<sample_count && k>0 && (size_t)k<cap;i++) {
PresentSample *s=&samples[i%128u];
k+=snprintf(out+k,cap-(size_t)k,"%s{\"us\":%.0f,\"source\":%llu,\"phase\":%u,\"deadline_us\":%.0f}",
i==first ? "" : ",",(double)s->ticks*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<cap) k+=snprintf(out+k,cap-(size_t)k,"],");
if (k > 0 && (size_t)k < cap) k += gl_renderer_present_thread_json(out + k, cap - (size_t)k);
return k;
}
+10 -2
View File
@@ -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);
+9
View File
@@ -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();
+24
View File
@@ -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
+24
View File
@@ -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;k<P.cfg.slots;k++) {
const int s=(cur+k)%P.cfg.slots;
if (P.state[s]==S_FREE) { next=s; break; }
}
if (next<0) { mtx_unlock(&P.mtx); return -1; }
P.ready[cur]=ready; P.state[cur]=S_QUEUED;
P.queue[(P.q_head+P.q_len)%PT_MAX_SLOTS]=cur;
P.q_len++; P.st.submits++;
if (P.q_len>P.st.queued_high) P.st.queued_high=P.q_len;
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);
+7
View File
@@ -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,
+10 -1
View File
@@ -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;
}
+25
View File
@@ -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();
@@ -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;
+11
View File
@@ -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);
+25
View File
@@ -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);
+3
View File
@@ -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; }
+16
View File
@@ -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")
+4
View File
@@ -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) &&
+9
View File
@@ -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");