Files
psxrecomp/runtime/src/render_pass.c
T
2026-10-24 09:42:28 -07:00

1355 lines
59 KiB
C

/* render_pass.c - host-timed render passes (psx_mod_render_pass_plan /
* psx_mod_render_pass, mod_plugins.h). See docs/RENDER_PASSES.md.
*
* A pass is a sandboxed nested guest call. The guest-visible machine is
* checkpointed, time is frozen (psx_cycle_freeze.h), the plugin draws an image of
* the scene through the game's own code, the OpenGL backend captures the
* declared display rect, and then everything is put back: CPU (with the
* GTE), RAM, scratchpad, I-cache tags, I_STAT/I_MASK, timers, DMA and GPU
* registers, and the VRAM rect. Stores that could escape that restore (SPU,
* CD, timers, other DMA channels, ...) never reach their device in the first
* place (memory.c render_pass_store).
*
* Nothing here runs unless a trusted plugin calls the API, and the plan
* refuses in netplay without explicit plugin opt-in, rollback resimulation,
* rewind, fast-forward, self-check
* resimulation, or without the OpenGL presenter's flip-aware interpolation
* (psx_mod_render_pass_status says which). */
#include "render_pass.h"
#include <setjmp.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cpu_state.h"
#include "dirty_ram_interp.h"
#include "dma.h"
#include "frame_trace.h"
#include "gpu.h"
#include "gpu_gl_renderer.h"
#include "host_time.h"
#include "mod_plugins.h"
#include "mod_runtime.h"
#include "interrupts.h"
#include "overlay_loader.h"
#include "psx_icache.h"
#include "timers.h"
#include "psx_video_timing.h"
#include "gte_view.h"
#include "stereo_shed_policy.h"
extern int gpu_render_projective_begin(void);
extern void gpu_render_projective_end(void);
extern uint8_t *memory_get_ram_ptr(void);
extern uint8_t *memory_get_scratchpad_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. */
extern uint32_t memory_get_ram_bytes(void);
extern uint32_t i_stat;
extern uint32_t i_mask;
/* gte.cpp: precision-tracking shadows (PGXP + the position cache) roll back
* with the machine, or they describe the words a pass wrote. */
extern void gte_precision_checkpoint_begin(void);
extern void gte_precision_checkpoint_rollback(void);
/* memory.c: mod arena pages a pass wrote, and their hash for verify mode. */
extern void render_pass_mod_arenas_rollback(void);
extern uint64_t render_pass_mod_arenas_hash(void);
extern uint32_t dma_snapshot_bytes(void);
extern void dma_snapshot_write(uint8_t *p);
extern int dma_snapshot_read(const uint8_t *p, uint32_t len);
extern uint32_t interrupts_get_cycles_since_vblank(void);
extern void interrupts_set_cycles_since_vblank(uint32_t v);
extern void psx_irq_refresh_cause_ip2(void);
extern int psx_get_in_exception(void);
extern int psx_netplay_active(void);
extern int psx_netplay_is_resimulating(void);
extern void psx_netplay_local_view_clear(void);
extern int psx_netplay_local_view_shed(void);
extern void gpu_ws_set_local_view_scope(int on);
extern int psx_selfcheck_resim_active(void);
extern int psx_rewind_is_open(void);
extern int psx_presentation_fast_forward(void);
extern uint64_t g_guest_store_count;
extern uint64_t g_mmio_access_count;
extern uint32_t g_debug_last_store_pc;
extern uint32_t g_debug_current_func_addr;
extern int g_psx_dispatch_depth;
extern int g_psx_call_bail;
extern int g_ls_mode;
extern int g_ls_replay_active;
static int s_netplay_allowed;
extern void (*g_overlay_flush_pending_cycles)(void);
/* Host nesting that runtime frames set on entry and put back on exit. */
extern int g_call_unit_depth; /* overlay_loader_call_native */
extern int g_dma_exec_depth; /* dma.c kick / async writes */
extern int g_host_store_depth; /* memory.c psx_host_write_* */
extern int g_dma_cur_ch;
extern uint32_t g_dma_cur_madr;
extern uint32_t g_dma_cur_bcr;
extern uint32_t g_dma_initiator_pc;
extern int g_dirty_interp_active; /* dirty-RAM interpreter */
extern int g_exec_phase;
extern int g_precise_mode;
extern uint32_t g_dirty_safe_resume_pc;
extern uint32_t cdrom_snapshot_bytes(void);
extern void cdrom_snapshot_write(uint8_t *p);
extern uint32_t spu_snapshot_bytes(void);
extern void spu_snapshot_write(uint8_t *p);
extern uint32_t sio_snapshot_bytes(void);
extern void sio_snapshot_write(uint8_t *p);
extern uint32_t mdec_snapshot_bytes(void);
extern void mdec_snapshot_write(uint8_t *p);
#define RP_SPAD_SIZE 1024u
/* ~8 M guest cycles is about four PS1 frames of CPU time: far beyond any
* frame's draw code, short enough that a pass stuck on a wait loop costs the
* host well under a frame before it is rolled back. */
#define RP_WATCHDOG_CYCLES 8000000u
/* Faults (watchdog, VRAM leaks) before passes stay off for the session. */
#define RP_FAULT_LIMIT 8u
/* Host-side nesting state. Generated functions, native overlay calls, DMA
* kicks, the dirty-RAM interpreter and GP0 processing each set some of this
* on entry and put it back on exit. The watchdog abort longjmps past those
* exits, so the landing puts the interrupted code's values back from the
* checkpoint, as the scheduler and exception landings reset theirs
* (traps.c psx_scheduler_run, interrupts.c psx_check_interrupts). The cycle
* deferral depth (g_psx_cyc_bb_defer) is part of PsxCycleFreeze. After a
* pass that returns normally all of it is already balanced. */
typedef struct RenderPassNesting {
int call_unit_depth;
int dma_exec_depth, dma_cur_ch, host_store_depth;
uint32_t dma_cur_madr, dma_cur_bcr, dma_initiator_pc;
int dirty_interp_active, exec_phase, precise_mode;
uint32_t dirty_safe_resume_pc;
DirtyRamLoadDelay ld_delay;
int ov_active_depth;
uint32_t ov_inprogress;
void (*ov_flush)(void);
ModFunctionEntryContext mod_entry;
unsigned snapshot_host_call_depth;
PSXModRenderView render_pose;
uint32_t span_lo, span_hi; /* open guest span (dirty_ram_run_span) */
} RenderPassNesting;
static void nesting_save(RenderPassNesting *n) {
n->call_unit_depth = g_call_unit_depth;
n->dma_exec_depth = g_dma_exec_depth;
n->host_store_depth = g_host_store_depth;
n->dma_cur_ch = g_dma_cur_ch;
n->dma_cur_madr = g_dma_cur_madr;
n->dma_cur_bcr = g_dma_cur_bcr;
n->dma_initiator_pc = g_dma_initiator_pc;
n->dirty_interp_active = g_dirty_interp_active;
n->exec_phase = g_exec_phase;
n->precise_mode = g_precise_mode;
n->dirty_safe_resume_pc = g_dirty_safe_resume_pc;
dirty_ram_ld_delay_save(&n->ld_delay);
overlay_loader_native_nesting(&n->ov_active_depth, &n->ov_inprogress);
n->ov_flush = g_overlay_flush_pending_cycles;
mod_runtime_function_entry_context_save(&n->mod_entry);
n->snapshot_host_call_depth = psx_snapshot_host_call_depth();
gte_render_pose_get(&n->render_pose);
dirty_ram_span_get(&n->span_lo, &n->span_hi);
}
static void nesting_restore(const RenderPassNesting *n) {
g_call_unit_depth = n->call_unit_depth;
g_dma_exec_depth = n->dma_exec_depth;
g_host_store_depth = n->host_store_depth;
g_dma_cur_ch = n->dma_cur_ch;
g_dma_cur_madr = n->dma_cur_madr;
g_dma_cur_bcr = n->dma_cur_bcr;
g_dma_initiator_pc = n->dma_initiator_pc;
g_dirty_interp_active = n->dirty_interp_active;
g_exec_phase = n->exec_phase;
g_precise_mode = n->precise_mode;
g_dirty_safe_resume_pc = n->dirty_safe_resume_pc;
dirty_ram_ld_delay_restore(&n->ld_delay);
overlay_loader_set_native_nesting(n->ov_active_depth, n->ov_inprogress);
g_overlay_flush_pending_cycles = n->ov_flush;
mod_runtime_function_entry_context_restore(&n->mod_entry);
psx_snapshot_host_call_restore(n->snapshot_host_call_depth);
gte_render_pose_set(&n->render_pose);
dirty_ram_span_set(n->span_lo, n->span_hi);
}
/* PSX_RENDER_PASS_VERIFY: a pass that returned normally must leave the
* nesting balanced; the restore would otherwise hide the imbalance. (The
* deferred load is not nesting: the interpreter retires it on exit.) */
static int nesting_balanced(const RenderPassNesting *n) {
RenderPassNesting now;
memset(&now, 0, sizeof now);
nesting_save(&now);
return now.call_unit_depth == n->call_unit_depth &&
now.dma_exec_depth == n->dma_exec_depth &&
now.host_store_depth == n->host_store_depth &&
now.dirty_interp_active == n->dirty_interp_active &&
now.exec_phase == n->exec_phase &&
now.precise_mode == n->precise_mode &&
now.ov_active_depth == n->ov_active_depth &&
now.ov_flush == n->ov_flush &&
now.span_lo == n->span_lo && now.span_hi == n->span_hi &&
now.mod_entry.depth == n->mod_entry.depth &&
now.mod_entry.plugin == n->mod_entry.plugin &&
now.mod_entry.cpu == n->mod_entry.cpu &&
now.mod_entry.finished == n->mod_entry.finished &&
now.snapshot_host_call_depth == n->snapshot_host_call_depth;
}
/* After a watchdog abort, before the restore: which exits the longjmp
* skipped (for the fault log). Returns how many values it found changed. */
static int nesting_describe(const RenderPassNesting *n, int bb_defer_ck,
char *out, size_t cap) {
RenderPassNesting now;
int count = 0;
size_t len = 0;
memset(&now, 0, sizeof now);
nesting_save(&now);
out[0] = '\0';
#define RP_NOTE(cond, ...) do { if (cond) { \
int w_ = snprintf(out + len, cap - len, "%s", count ? ", " : ""); \
if (w_ > 0 && (size_t)w_ < cap - len) len += (size_t)w_; \
w_ = snprintf(out + len, cap - len, __VA_ARGS__); \
if (w_ > 0 && (size_t)w_ < cap - len) len += (size_t)w_; \
count++; } } while (0)
RP_NOTE(g_psx_cyc_bb_defer != bb_defer_ck, "cycle deferral %+d",
g_psx_cyc_bb_defer - bb_defer_ck);
RP_NOTE(now.call_unit_depth != n->call_unit_depth, "call unit %+d",
now.call_unit_depth - n->call_unit_depth);
RP_NOTE(now.ov_active_depth != n->ov_active_depth, "shard stack %+d",
now.ov_active_depth - n->ov_active_depth);
RP_NOTE(now.ov_flush != n->ov_flush, "shard cycle hook");
RP_NOTE(now.dma_exec_depth != n->dma_exec_depth, "DMA depth %+d",
now.dma_exec_depth - n->dma_exec_depth);
RP_NOTE(now.host_store_depth != n->host_store_depth, "host store depth %+d",
now.host_store_depth - n->host_store_depth);
RP_NOTE(now.dirty_interp_active != n->dirty_interp_active, "interpreter flag");
RP_NOTE(now.precise_mode != n->precise_mode, "precise flag");
RP_NOTE(now.exec_phase != n->exec_phase, "exec phase %d->%d",
now.exec_phase, n->exec_phase);
RP_NOTE(now.ld_delay.armed != n->ld_delay.armed, "pending load");
RP_NOTE(now.mod_entry.depth != n->mod_entry.depth, "mod entries %+d",
(int)now.mod_entry.depth - (int)n->mod_entry.depth);
RP_NOTE(now.mod_entry.plugin != n->mod_entry.plugin, "mod owner");
RP_NOTE(now.mod_entry.cpu != n->mod_entry.cpu ||
now.mod_entry.finished != n->mod_entry.finished, "mod completion context");
RP_NOTE(now.snapshot_host_call_depth != n->snapshot_host_call_depth,
"host adapter depth %u->%u", n->snapshot_host_call_depth,
now.snapshot_host_call_depth);
RP_NOTE(now.span_hi != n->span_hi, "guest span");
#undef RP_NOTE
return count;
}
typedef struct RenderPassCheckpoint {
CPUState cpu;
uint8_t spad[RP_SPAD_SIZE];
uint32_t icache[1024];
uint32_t i_stat, i_mask, csv;
uint16_t t_counter[3], t_target[3];
uint32_t t_mode[3], t_frac[3];
int32_t t_irq[3];
uint64_t store_count, mmio_count;
uint32_t last_store_pc, current_func;
int dispatch_depth, call_bail;
uint32_t dma_len;
uint32_t ram_bytes; /* live RAM geometry at the checkpoint */
RenderPassNesting nest;
} RenderPassCheckpoint;
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;
static PsxCycleFreeze s_freeze;
static jmp_buf s_abort_jmp;
static volatile int s_abort_armed;
static const CPUState* s_pass_cpu;
static int s_nesting;
static int s_device_read_fault;
static RenderPassStats s_stats;
static int s_verify = -1;
static int s_open_generation; /* next pass captures frame N's image */
static uint32_t s_plan_period = 2;
/* The last pass's restore left the machine as it found it; while no guest
* code runs (clock and store count unchanged) and the plan is the same, the
* next pass may reuse that pass's VRAM backup. */
static uint64_t s_plan_serial, s_restored_plan;
static uint64_t s_restored_cycle, s_restored_stores;
static int s_restored_valid;
static RenderPassFailure s_attempt;
static const char *s_checkpoint_failure;
static RenderStereoStats s_stereo;
static int s_pair_busy;
/* Whole-pair shedding/recovery policy (bounded in time and count; see
* stereo_shed_policy.c). Replaces the old attempt counter, which could suppress
* many pairs after a single over-budget frame. */
static StereoShedPolicy s_shed;
/* Headset deadline pushed by the OpenXR layer (target display period, ms).
* 0 = no headset: fall back to the guest-VBlank redraw budget. */
static double s_shed_xr_budget_ms;
static uint32_t s_shed_period;
/* Pair-scoped baseline (PR3). Both eyes of a stereo pair render from the same
* guest checkpoint: eye L captures it and the restore that ends eye L puts the
* live machine back to exactly that captured state, so eye R can reuse the
* capture instead of copying the whole live RAM a second time. Guards:
* - only stereo eyes inside one pair reuse it (s_baseline_ready is cleared by
* any non-stereo transaction and at the start of every pair);
* - a verify mismatch while reusing it retires the optimization for the
* session, falling back to a capture per eye (PSX_STEREO_BASELINE=0 does
* the same up front). */
static int s_baseline_ready; /* live machine == captured baseline */
static int s_baseline_enabled = -1; /* PSX_STEREO_BASELINE; -1 = not read yet */
static int32_t s_shed_w, s_shed_h;
static int s_shed_geom_valid;
/* A HUD-only capture transaction reuses eye 0's slot but is not an eye draw;
* frame tracing must not attribute its cost to the left eye. */
static int s_capture_only;
/* The two transactions of one HUD capture call are a pair of their own: the
* second matte draws from exactly the state the restore that ended the first
* one put back, so it may reuse the first one's checkpoint the way a pair's
* right eye reuses the left's. Scoped to the capture call, so the reuse never
* reaches the pair that renders later in the same frame. */
static int s_capture_pair;
static int pass_refuse(const char *reason, uint32_t status) {
/* A refusal always carries a reason: last_failure.reason == NULL means
* "no failure" to the TCP reader. */
s_attempt.reason = reason ? reason : "unspecified";
s_attempt.status = status;
s_stats.last_failure = s_attempt;
return 0;
}
/* PSX_RENDER_PASS_WATCHDOG=<guest cycles> lowers the cut-off (debug), so a
* title's rollback path can be exercised on real passes. */
static uint64_t watchdog_cycles(void) {
static uint64_t v;
if (!v) {
const char *e = getenv("PSX_RENDER_PASS_WATCHDOG");
long long n = e ? atoll(e) : 0;
v = (n >= 1000 && n < (long long)RP_WATCHDOG_CYCLES)
? (uint64_t)n : (uint64_t)RP_WATCHDOG_CYCLES;
if (v != RP_WATCHDOG_CYCLES)
fprintf(stderr, "psxrecomp: render pass watchdog lowered to %llu "
"guest cycles (PSX_RENDER_PASS_WATCHDOG)\n",
(unsigned long long)v);
}
return v;
}
static int verify_on(void) {
if (s_verify < 0) {
const char *e = getenv("PSX_RENDER_PASS_VERIFY");
s_verify = (e && e[0] && e[0] != '0') ? 1 : 0;
}
return s_verify;
}
/* PSX_STEREO_SHED_BUDGET_MS, when set, replaces the guest-VBlank-derived budget
* with a real headset deadline (e.g. 11.1 at 90 Hz, 8.3 at 120 Hz). The VBlank
* cadence is only a fallback: it is a guest redraw budget, not an OpenXR
* display deadline, and conflating the two is wrong on every non-60 Hz headset.
* Returns 0 when unset so the caller derives the fallback. */
static double shed_budget_override_ms(void) {
static double v = -1.0;
if (v < 0.0) {
const char *e = getenv("PSX_STEREO_SHED_BUDGET_MS");
double d = e ? atof(e) : 0.0;
v = (d > 0.0 && d < 1000.0) ? d : 0.0;
}
return v;
}
/* The pair budget actually used, in priority order: the explicit env override,
* the headset display deadline the OpenXR layer pushed, then the guest-VBlank
* redraw budget as a last resort. */
static double shed_budget_ms(uint32_t period_vblanks, uint32_t *source) {
double ov = shed_budget_override_ms();
if (ov > 0.0) { if (source) *source = 2; return ov; }
if (s_shed_xr_budget_ms > 0.0) { if (source) *source = 1; return s_shed_xr_budget_ms; }
if (source) *source = 0;
return period_vblanks * (double)g_psx_vblank_cycles / 33868.8 * 0.8;
}
/* Pair-scoped checkpoint reuse is on by default; PSX_STEREO_BASELINE=0 forces a
* machine capture per eye (the pre-PR3 behavior) so the two can be compared, and
* a restore-verify mismatch turns it off for the rest of the session. */
static int baseline_reuse_enabled(void) {
if (s_baseline_enabled < 0) {
const char *e = getenv("PSX_STEREO_BASELINE");
s_baseline_enabled = (e && e[0] == '0') ? 0 : 1;
}
return s_baseline_enabled;
}
void render_pass_get_stats(RenderPassStats *out) {
if (!out) return;
*out = s_stats;
for (int i = 0; i < RENDER_PASS_DROP_CLASSES; i++)
out->dropped[i] = g_render_pass_dropped_writes[i];
}
int render_pass_verification_enabled(void) { return verify_on(); }
void render_pass_reset_session(void) {
psx_mod_set_render_projective(0);
s_netplay_allowed = 0;
memset(&s_stats, 0, sizeof s_stats);
memset(g_render_pass_dropped_writes, 0, sizeof g_render_pass_dropped_writes);
s_open_generation = 0;
s_device_read_fault = 0;
s_restored_valid = 0;
memset(&s_attempt, 0, sizeof s_attempt);
memset(&s_stereo, 0, sizeof s_stereo);
s_stereo.last_eye = -1;
s_pair_busy = 0;
/* Shedding policy: defaults, then env overrides (PSX_STEREO_SHED=0 gives the
* baseline diagnostic with shedding fully disabled). */
stereo_shed_policy_init(&s_shed);
stereo_shed_policy_configure_from_env(&s_shed);
stereo_shed_policy_reset(&s_shed);
s_shed_geom_valid = 0;
/* Pair-scoped baseline: re-read the env and start uncaptured. */
s_baseline_ready = 0;
s_baseline_enabled = -1;
/* Frame tracing is opt-in per session: re-read the env, install the host
* clock, and clear any previous session's records. */
psx_frame_trace_configure_from_env();
psx_frame_trace_set_clock(psx_host_mono_ms);
psx_frame_trace_reset();
gl_renderer_stereo_reset();
gl_renderer_pass_set_flip_shown(0);
}
int psx_mod_set_render_pass_netplay(int enabled) {
s_netplay_allowed = enabled ? 1 : 0;
return 1;
}
int render_pass_netplay_enabled(void) { return s_netplay_allowed; }
int psx_mod_set_render_pass_flip(uint32_t mode) {
if (mode != PSX_MOD_RENDER_PASS_FLIP_PENDING &&
mode != PSX_MOD_RENDER_PASS_FLIP_SHOWN)
return 0;
gl_renderer_pass_set_flip_shown(mode == PSX_MOD_RENDER_PASS_FLIP_SHOWN);
return 1;
}
/* Transaction kinds: what `eye` holds in render_transaction. */
#define RP_EYE_PASS (-1) /* temporal pass */
#define RP_EYE_LOCAL (-2) /* netplay local view */
/* Everything that must hold before guest code may run frozen, as a
* PSX_MOD_RENDER_PASS_* reason (READY = all of it holds). kind: 0 temporal
* pass, 1 stereo, 2 netplay local view (forward netplay frames only). */
static uint32_t transaction_status(int kind) {
uint32_t gl;
if (s_stats.disabled) return PSX_MOD_RENDER_PASS_DISABLED;
if ((kind == 2 ? !psx_netplay_active() :
(psx_netplay_active() && !s_netplay_allowed)) || psx_netplay_is_resimulating() ||
psx_selfcheck_resim_active() || psx_rewind_is_open() ||
g_ls_mode || g_ls_replay_active)
return PSX_MOD_RENDER_PASS_SESSION;
if (psx_presentation_fast_forward()) return PSX_MOD_RENDER_PASS_FAST_FORWARD;
/* A peer behind the match catches up first: its own view is the first
* thing to go (presentation only), like any other fast-forward. */
if (kind == 2 && psx_netplay_local_view_shed())
return PSX_MOD_RENDER_PASS_FAST_FORWARD;
gl = kind == 2 ? gl_renderer_local_view_unavailable()
: kind == 1 ? gl_renderer_stereo_unavailable() : gl_renderer_pass_unavailable();
if (gl != PSX_MOD_RENDER_PASS_READY) return gl;
if (s_nesting || g_psx_guest_time_frozen || psx_get_in_exception() ||
dma_gpu_linked_list_active())
return PSX_MOD_RENDER_PASS_BUSY;
return PSX_MOD_RENDER_PASS_READY;
}
static uint32_t pass_status(void) { return transaction_status(0); }
uint32_t psx_mod_render_stereo_status(void) {
return s_pair_busy ? PSX_MOD_RENDER_PASS_BUSY : transaction_status(1);
}
void render_stereo_get_stats(RenderStereoStats *out) { if (out) *out = s_stereo; }
int psx_mod_set_stereo_presentation(uint32_t mode) {
return mode <= 1u ? gl_renderer_stereo_set_presentation(mode) : 0;
}
/* Game-side annotation for the frame trace (mod_plugins.h): a title hook marks
* a scene/state transition on the record the runtime opened for this frame. A
* no-op when tracing is off or no record is open. */
void psx_mod_frame_trace_note(const char *tag) {
psx_frame_trace_note(tag);
psx_frame_trace_flag(PSX_FRAME_TRACE_SCENE_TRANSITION);
}
int psx_mod_render_view_offset(int32_t x, int32_t y, int32_t z) {
int32_t xyz[3] = {x, y, z};
if (!g_psx_render_pass_active) return 0;
gte_render_view_set(xyz);
return 1;
}
int psx_mod_render_view(const PSXModRenderView *view) {
if (!g_psx_render_pass_active || !view || view->struct_size < sizeof *view ||
view->projection > 1u || view->precise > 1u || view->projection_h_ref > 65535u) return 0;
/* Bound products well inside int64. Rigid matrices are supplied by caller. */
for (int i = 0; i < 9; ++i)
if (view->rotation_q12[i] < -4096 || view->rotation_q12[i] > 4096) return 0;
for (int i = 0; i < 3; ++i)
if (view->translation[i] < -65536 || view->translation[i] > 65536) return 0;
if (view->projection && (view->fx_q16 <= 0 || view->fy_q16 <= 0)) return 0;
if (view->precise) {
for (int i=0;i<9;++i)
if (!isfinite(view->rotation[i]) || fabs(view->rotation[i])>1.001) return 0;
for (int i=0;i<3;++i)
if (!isfinite(view->translation_precise[i]) || fabs(view->translation_precise[i])>65536) return 0;
}
gte_render_pose_set(view);
return 1;
}
static int passes_allowed(void) {
return pass_status() == PSX_MOD_RENDER_PASS_READY;
}
uint32_t psx_mod_render_pass_status(void) {
return pass_status();
}
int psx_mod_run_guest_span(CPUState *cpu, uint32_t start_pc, uint32_t stop_pc) {
if (!g_psx_render_pass_active || !cpu) return 0;
if (dirty_ram_run_span(cpu, start_pc, stop_pc, 1000000u)) {
s_stats.spans++;
return 1;
}
s_stats.span_failures++;
return 0;
}
uint32_t psx_mod_render_pass_plan(uint32_t period_vblanks,
uint32_t shown_after_vblanks,
uint32_t *alpha_q16, uint32_t max) {
uint32_t wanted = 0, n;
s_open_generation = 0;
if (!alpha_q16 || max == 0 || period_vblanks == 0 || period_vblanks > 8 ||
shown_after_vblanks > 8)
return 0;
if (!passes_allowed()) return 0;
n = gl_renderer_pass_plan(period_vblanks, shown_after_vblanks,
alpha_q16, max, &wanted);
s_stats.wanted += wanted;
if (!n) {
if (wanted) s_stats.refused++;
return 0;
}
s_stats.plans++;
s_stats.planned += n;
s_plan_serial++;
s_open_generation = 1;
s_plan_period = period_vblanks;
return n;
}
/* FNV-1a step over 64-bit words, then the tail bytes. Each step is a
* bijection of h, so a change confined to one word always changes the hash.
* Word steps take an eighth of the time of byte steps, which keeps
* PSX_RENDER_PASS_VERIFY affordable with 8 MiB of RAM live. */
static uint64_t fnv(uint64_t h, const void *p, size_t n) {
const uint8_t *b = (const uint8_t *)p;
size_t i = 0;
for (; i + 8u <= n; i += 8u) {
uint64_t w;
memcpy(&w, b + i, sizeof w);
h = (h ^ w) * 1099511628211ULL;
}
for (; i < n; i++) h = (h ^ b[i]) * 1099511628211ULL;
return h;
}
/* Guest-visible state hash for PSX_RENDER_PASS_VERIFY. */
static uint64_t state_hash(const CPUState *cpu) {
uint64_t h = 1469598103934665603ULL;
uint16_t tc[3], tt[3];
uint32_t tm[3], tf[3];
int32_t ti[3];
uint32_t csv = interrupts_get_cycles_since_vblank();
uint64_t cyc = psx_cycle_count;
uint32_t guest_cycle_scale[3];
psx_guest_cycle_scale_snapshot(guest_cycle_scale);
h = fnv(h, cpu->gpr, sizeof cpu->gpr);
h = fnv(h, &cpu->hi, sizeof cpu->hi);
h = fnv(h, &cpu->lo, sizeof cpu->lo);
h = fnv(h, cpu->cop0, sizeof cpu->cop0);
h = fnv(h, cpu->gte_data, sizeof cpu->gte_data);
h = fnv(h, cpu->gte_ctrl, sizeof cpu->gte_ctrl);
h = fnv(h, memory_get_ram_ptr(), memory_get_ram_bytes());
h = fnv(h, memory_get_scratchpad_ptr(), RP_SPAD_SIZE);
{
const uint64_t mh = render_pass_mod_arenas_hash();
h = fnv(h, &mh, sizeof mh);
}
h = fnv(h, g_psx_icache_tv, sizeof g_psx_icache_tv);
h = fnv(h, &i_stat, sizeof i_stat);
h = fnv(h, &i_mask, sizeof i_mask);
h = fnv(h, &csv, sizeof csv);
h = fnv(h, &cyc, sizeof cyc);
h = fnv(h, guest_cycle_scale, sizeof guest_cycle_scale);
timers_get_snapshot(tc, tm, tt, ti, tf);
h = fnv(h, tc, sizeof tc); h = fnv(h, tm, sizeof tm);
h = fnv(h, tt, sizeof tt); h = fnv(h, ti, sizeof ti); h = fnv(h, tf, sizeof tf);
if (s_dma_copy) {
uint32_t n = dma_snapshot_bytes();
uint8_t *tmp = (uint8_t *)malloc(n);
if (tmp) { dma_snapshot_write(tmp); h = fnv(h, tmp, n); free(tmp); }
}
{
uint64_t g = gpu_pass_state_hash();
h = fnv(h, &g, sizeof g);
}
/* Devices a pass must never touch (their stores are dropped, their
* clocks frozen): prove it. */
{
uint32_t (*bytes[4])(void) = { cdrom_snapshot_bytes, spu_snapshot_bytes,
sio_snapshot_bytes, mdec_snapshot_bytes };
void (*write[4])(uint8_t *) = { cdrom_snapshot_write, spu_snapshot_write,
sio_snapshot_write, mdec_snapshot_write };
for (int i = 0; i < 4; i++) {
uint32_t n = bytes[i]();
uint8_t *tmp = n ? (uint8_t *)malloc(n) : NULL;
if (!tmp) continue;
write[i](tmp);
h = fnv(h, tmp, n);
free(tmp);
}
}
return h;
}
/* `reuse` skips the bulk capture (RAM, scratchpad, icache, DMA, CPU, timers)
* when a previous eye of the same pair already captured this exact machine
* state. The per-eye arms are never skipped: the GPU transaction snapshot and
* gte_precision_checkpoint_begin() bound the pass and end with the restore. */
static int checkpoint_save(const CPUState *cpu, int reuse) {
uint32_t dma_len = dma_snapshot_bytes();
/* The whole live RAM: a pass's stores fold through the same geometry
* (render_pass_store_to), so every byte one can reach is in here. */
uint32_t ram_bytes = memory_get_ram_bytes();
s_checkpoint_failure = NULL;
/* A reused baseline must own its buffers and describe this machine (RAM
* and DMA geometry cannot change inside a session). */
int can_reuse = reuse && s_ram_copy && s_dma_copy &&
s_ck.ram_bytes == ram_bytes && s_ck.dma_len == dma_len;
if (!can_reuse) {
if (s_ram_copy_cap < ram_bytes) {
uint8_t *p = (uint8_t *)realloc(s_ram_copy, ram_bytes);
if (!p) { s_checkpoint_failure = "checkpoint_ram"; return 0; }
s_ram_copy = p;
s_ram_copy_cap = ram_bytes;
}
if (!s_dma_copy) {
s_dma_copy = (uint8_t *)malloc(dma_len);
if (!s_dma_copy) { s_checkpoint_failure = "checkpoint_dma"; return 0; }
}
}
if (!gpu_pass_checkpoint_save()) {
s_checkpoint_failure = "checkpoint_gpu";
return 0;
}
if (!can_reuse) {
s_ck.cpu = *cpu;
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);
memcpy(s_ck.icache, g_psx_icache_tv, sizeof s_ck.icache);
s_ck.i_stat = i_stat;
s_ck.i_mask = i_mask;
s_ck.csv = interrupts_get_cycles_since_vblank();
timers_get_snapshot(s_ck.t_counter, s_ck.t_mode, s_ck.t_target,
s_ck.t_irq, s_ck.t_frac);
s_ck.dma_len = dma_len;
dma_snapshot_write(s_dma_copy);
s_ck.store_count = g_guest_store_count;
s_ck.mmio_count = g_mmio_access_count;
s_ck.last_store_pc = g_debug_last_store_pc;
s_ck.current_func = g_debug_current_func_addr;
s_ck.dispatch_depth = g_psx_dispatch_depth;
s_ck.call_bail = g_psx_call_bail;
memset(&s_ck.nest, 0, sizeof s_ck.nest);
nesting_save(&s_ck.nest);
}
gte_precision_checkpoint_begin();
return 1;
}
static void checkpoint_restore(CPUState *cpu) {
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,
s_ck.t_irq, s_ck.t_frac);
interrupts_set_cycles_since_vblank(s_ck.csv);
i_stat = s_ck.i_stat;
i_mask = s_ck.i_mask;
memcpy(memory_get_ram_ptr(), s_ram_copy, s_ck.ram_bytes);
memcpy(memory_get_scratchpad_ptr(), s_ck.spad, RP_SPAD_SIZE);
memcpy(g_psx_icache_tv, s_ck.icache, sizeof s_ck.icache);
*cpu = s_ck.cpu;
psx_irq_refresh_cause_ip2();
g_guest_store_count = s_ck.store_count;
g_mmio_access_count = s_ck.mmio_count;
g_debug_last_store_pc = s_ck.last_store_pc;
g_debug_current_func_addr = s_ck.current_func;
g_psx_dispatch_depth = s_ck.dispatch_depth;
g_psx_call_bail = s_ck.call_bail;
nesting_restore(&s_ck.nest);
render_pass_mod_arenas_rollback();
gte_precision_checkpoint_rollback();
}
static double s_ms_per_tick = 0.0;
static double ema_ms(double cur, uint64_t ticks) {
double ms = (double)ticks * s_ms_per_tick;
return cur > 0.0 ? cur * 0.9 + ms * 0.1 : ms;
}
/* Ticks -> microseconds for frame_trace. CPU submission time only: it is not
* GPU execution time. */
static double s_us_per_tick = 0.0;
static uint64_t trace_ticks_to_us(uint64_t ticks) {
if (s_us_per_tick == 0.0) {
double f = (double)gl_renderer_perf_frequency();
s_us_per_tick = f > 0.0 ? 1000000.0 / f : 1.0;
}
return (uint64_t)((double)ticks * s_us_per_tick);
}
/* ---- guest sub-stage spans (mod_plugins.h) ------------------------------
* The game hook brackets the guest calls it replays once per eye, and the
* runtime times them with its own tick clock, exactly as the eye stages are
* timed, so the parts sum against eye_guest. Accepted only inside a traced
* stereo eye (s_sub_eye): a HUD capture's guest draws and the flat path report
* their own stages and must not write these slots. */
static int s_sub_eye = -1; /* traced eye accepting spans, -1 = none */
/* One start tick per stage rather than one open span at a time, so a stage can
* be bracketed inside another: the hook's visibility override runs inside the
* guest traversal it is part of. The outer span keeps its own wall time, so a
* nested stage is a subset of it rather than a sibling. */
static int s_sub_open[PSX_TRACE_STAGE_COUNT];
static uint64_t s_sub_ticks[PSX_TRACE_STAGE_COUNT];
/* No span survives an eye boundary, so an eye aborted mid-entry cannot carry a
* start tick into the next one. */
static void sub_spans_forget(void) {
memset(s_sub_open, 0, sizeof s_sub_open);
}
void psx_mod_frame_trace_stage_begin(uint32_t stage) {
if (s_sub_eye < 0 || stage >= PSX_TRACE_STAGE_COUNT) return;
s_sub_open[stage] = 1; /* an unpaired begin restarts that span */
s_sub_ticks[stage] = gl_renderer_perf_ticks();
}
void psx_mod_frame_trace_stage_end(uint32_t stage) {
if (s_sub_eye < 0 || stage >= PSX_TRACE_STAGE_COUNT) return;
if (!s_sub_open[stage]) return; /* an end with no begin records nothing */
uint64_t now = gl_renderer_perf_ticks();
psx_frame_trace_add(stage, trace_ticks_to_us(now - s_sub_ticks[stage]));
s_sub_open[stage] = 0;
}
static void watchdog_overrun(void) {
s_stats.watchdog++;
s_stats.watchdog_flag = 1;
if (s_stats.watchdog <= 4 && s_pass_cpu)
fprintf(stderr, "psxrecomp: render pass watchdog pc=0x%08X "
"function=0x%08X ra=0x%08X last_store=0x%08X\n",
s_pass_cpu->pc, g_debug_current_func_addr,
s_pass_cpu->gpr[31], g_debug_last_store_pc);
if (s_abort_armed) {
s_abort_armed = 0;
longjmp(s_abort_jmp, 1);
}
}
int render_pass_mmio_read_allowed(uint32_t phys, uint32_t width) {
if (!g_psx_render_pass_active) return 1;
if (!((phys >= 0x1F801040u && phys <= 0x1F80105Fu) || /* SIO */
(phys >= 0x1F801800u && phys <= 0x1F801803u) || /* CD */
(phys >= 0x1F801820u && phys <= 0x1F801827u) || /* MDEC */
(phys >= 0x1F801C00u && phys <= 0x1F801FFFu))) /* SPU */
return 1;
s_device_read_fault = 1;
s_stats.device_reads++;
s_stats.last_device_read = phys;
s_stats.last_device_read_width = width;
if (s_stats.device_reads <= 4 && s_pass_cpu)
fprintf(stderr, "psxrecomp: render pass device read=0x%08X "
"pc=0x%08X function=0x%08X ra=0x%08X last_store=0x%08X\n",
phys, s_pass_cpu->pc, g_debug_current_func_addr,
s_pass_cpu->gpr[31], g_debug_last_store_pc);
s_attempt.reason = "device_read";
s_stats.last_failure = s_attempt;
if (s_abort_armed) {
s_abort_armed = 0;
longjmp(s_abort_jmp, 1);
}
return 0;
}
static char s_abort_detail[192];
static void note_fault(const char *what) {
s_stats.aborted++;
if (s_stats.aborted <= 4)
fprintf(stderr, "psxrecomp: render pass rolled back (%s%s%s)\n", what,
s_abort_detail[0] ? "; restored skipped exits: " : "",
s_abort_detail);
s_abort_detail[0] = '\0';
if (s_stats.watchdog + s_stats.vram_leaks + s_stats.device_reads >= RP_FAULT_LIMIT &&
!s_stats.disabled) {
s_stats.disabled = 1;
fprintf(stderr, "psxrecomp: render passes disabled for this session "
"after %u faults\n", (unsigned)RP_FAULT_LIMIT);
}
}
static int s_local_scope = 0;
static void local_scope_set(int on) {
s_local_scope = on;
gpu_ws_set_local_view_scope(on);
}
int psx_mod_local_view_scope(void) { return s_local_scope; }
static int transaction_kind(int eye) {
return eye >= 0 ? 1 : eye == RP_EYE_LOCAL ? 2 : 0;
}
static int transaction_end(int eye, uint32_t alpha, int keep) {
if (eye >= 0) return gl_renderer_stereo_end((uint32_t)eye, keep);
if (eye == RP_EYE_LOCAL) return gl_renderer_local_view_end(keep);
gl_renderer_pass_end(alpha, keep);
return 1;
}
static int render_transaction(struct CPUState *cpu, const PSXModRenderPass *pass,
PSXModRenderPassFn fn, void *user, int eye) {
uint64_t t0, t1, tv, tc, tb, tg, te, tr, hash_before = 0, hash_after = 0;
uint64_t cycles_before;
uint32_t leaks;
int ok = 0, open, reuse;
static uint32_t s_leaks_before;
uint32_t status;
memset(&s_attempt, 0, sizeof s_attempt);
s_attempt.attempt = ++s_stats.pass_attempts;
s_attempt.plan = s_plan_serial;
s_attempt.guest_cycle = psx_cycle_count;
/* No guest sub-stage span may survive into this transaction: the eye that
* accepts them is decided below, once the trace decision is known. */
s_sub_eye = -1;
sub_spans_forget();
if (pass) {
s_attempt.struct_size = pass->struct_size;
if (pass->struct_size >= sizeof *pass) {
s_attempt.alpha_q16 = pass->alpha_q16;
s_attempt.x = pass->x; s_attempt.y = pass->y;
s_attempt.w = pass->w; s_attempt.h = pass->h;
}
}
if (!cpu || !pass || !fn || pass->struct_size < sizeof *pass ||
pass->w == 0 || pass->h == 0 || (eye == RP_EYE_PASS &&
(pass->alpha_q16 == 0 || pass->alpha_q16 >= 65536u))) {
s_stats.argument_refused++;
return pass_refuse("arguments", transaction_status(transaction_kind(eye)));
}
status = transaction_status(transaction_kind(eye));
if (status != PSX_MOD_RENDER_PASS_READY) {
s_stats.status_refused++;
return pass_refuse("status", status);
}
/* An overlay DLL may still hold cycles it has not published. They belong
* to the live timeline: publish them now, or the pass's first store would
* publish them into the frozen clock and the restore would drop them. */
if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles();
s_attempt.guest_cycle = psx_cycle_count;
t0 = gl_renderer_perf_ticks();
open = eye == RP_EYE_PASS ? s_open_generation : 0;
{
static double inv = 0.0;
if (inv == 0.0) inv = 1000.0 / (double)gl_renderer_perf_frequency();
s_ms_per_tick = inv;
}
/* Per-eye trace sampling (opt-in): primitive counts and upload pixels are
* cumulative, so the eye's cost is the delta across its transaction. */
int trace_on = eye >= 0 && !s_capture_only && psx_frame_trace_enabled();
uint64_t trace_prims0 = 0, trace_tex0 = 0, trace_diag0[6] = {0,0,0,0,0,0};
if (trace_on) {
gl_renderer_scene_prim_counts(&trace_prims0, &trace_tex0);
gl_renderer_runtime_diag(trace_diag0);
}
/* This eye, and only this eye, accepts the hook's guest sub-stage spans. */
s_sub_eye = trace_on ? (int)eye : -1;
reuse = !open && eye != RP_EYE_LOCAL && s_restored_valid &&
s_restored_plan == s_plan_serial &&
s_restored_cycle == psx_cycle_count &&
s_restored_stores == g_guest_store_count;
/* Frame N's own image is captured by the first pass after a plan. */
if (!(eye >= 0 ? gl_renderer_stereo_begin(pass->x, pass->y, pass->w,
pass->h, reuse)
: eye == RP_EYE_LOCAL ? gl_renderer_local_view_begin(pass->x, pass->y,
pass->w, pass->h)
: gl_renderer_pass_begin(pass->x, pass->y, pass->w,
pass->h, open, s_plan_period, reuse))) {
gl_renderer_pass_begin_diag(&s_attempt.gl);
s_stats.begin_refused++;
return pass_refuse(s_attempt.gl.reason ? s_attempt.gl.reason : "gl_begin",
s_attempt.gl.status);
}
s_open_generation = 0;
s_leaks_before = gl_renderer_pass_leaks();
tv = gl_renderer_perf_ticks();
/* A stereo eye, or the second matte of a capture, whose machine state still
* equals the baseline captured for it skips re-capturing that state. */
int reuse_baseline = eye >= 0 && s_baseline_ready && baseline_reuse_enabled() &&
(!s_capture_only || s_capture_pair);
if (!checkpoint_save(cpu, reuse_baseline)) {
gl_renderer_pass_begin_diag(&s_attempt.gl);
s_stats.checkpoint_refused++;
/* Capture the rejection status before rolling the GL transaction back. */
status = transaction_status(transaction_kind(eye));
transaction_end(eye, 0, 0);
return pass_refuse(s_checkpoint_failure ? s_checkpoint_failure : "checkpoint", status);
}
if (eye >= 0 && !s_capture_only && !gpu_render_projective_begin()) {
gl_renderer_pass_begin_diag(&s_attempt.gl);
status = transaction_status(transaction_kind(eye));
transaction_end(eye, 0, 0);
checkpoint_restore(cpu);
return pass_refuse("projective_pgxp", status);
}
if (reuse_baseline) s_stereo.checkpoint_reuses++;
else { s_stereo.checkpoint_saves++; s_stereo.checkpoint_ram_bytes += s_ck.ram_bytes; }
tc = gl_renderer_perf_ticks();
if (verify_on()) hash_before = state_hash(cpu);
tb = gl_renderer_perf_ticks();
s_nesting = 1;
s_device_read_fault = 0;
cycles_before = psx_cycle_count;
if (eye >= 0) {
s_stereo.eye_cycle[eye] = cycles_before;
s_stereo.eye_hash[eye] = verify_on() ? hash_before : 0;
}
(void)psx_cycle_freeze_begin(&s_freeze, watchdog_cycles(),
watchdog_overrun);
/* Explicitly opted-in temporal passes of an own-view image need the
* same presentation-only hooks as its base local draw. Both execute in
* this restored transaction; rollback resimulation is refused above. */
if (eye == RP_EYE_LOCAL ||
(eye == RP_EYE_PASS && s_netplay_allowed && psx_netplay_active()))
local_scope_set(1);
if (setjmp(s_abort_jmp) == 0) {
s_pass_cpu = cpu;
s_abort_armed = 1;
ok = fn(cpu, user, pass->alpha_q16) ? 1 : 0;
s_abort_armed = 0;
if (!ok) s_stats.discarded++;
if (verify_on() && !nesting_balanced(&s_ck.nest)) {
s_stats.verify_mismatch++;
if (eye >= 0) ok = 0;
fprintf(stderr, "psxrecomp: RENDER PASS VERIFY mismatch "
"(host nesting not balanced after the pass)\n");
}
} else {
/* Sandbox abort from inside guest code. An overlay shard may still
* hold cycles it has not published: publish them now, into the
* frozen clock the restore discards, never into the live one. The
* nesting the longjmp skipped is put back by checkpoint_restore and
* psx_cycle_freeze_end below. */
if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles();
if (nesting_describe(&s_ck.nest, s_freeze.bb_defer, s_abort_detail,
sizeof s_abort_detail))
s_stats.nesting_repairs++;
snprintf(s_stats.last_abort_detail, sizeof s_stats.last_abort_detail,
"%s", s_abort_detail);
ok = 0;
}
/* Any span still open here was never ended by the hook, or the eye aborted
* mid-entry through the longjmp above. Drop them instead of closing them:
* the elapsed time is then not charged to an entry that never finished, and
* it shows up as the difference between eye_guest and the guest sub-stages. */
sub_spans_forget();
uint64_t guest_cycles = psx_cycle_count - cycles_before;
s_stats.guest_cycles_last = guest_cycles;
if (eye >= 0) gte_render_view_get(s_stereo.eye_view[eye]);
tg = gl_renderer_perf_ticks();
s_pass_cpu = NULL;
leaks = gl_renderer_pass_leaks() - s_leaks_before;
if (s_device_read_fault) ok = 0;
if (leaks) {
s_stats.vram_leaks += leaks;
ok = 0;
}
/* Capture (when kept), then roll the VRAM rect and renderer back. */
if (!transaction_end(eye, ok ? pass->alpha_q16 : 0, ok)) {
ok = 0;
if (eye >= 0) s_stereo.last_failure = "eye_capture";
}
te = gl_renderer_perf_ticks();
local_scope_set(0);
gpu_render_projective_end();
checkpoint_restore(cpu);
psx_cycle_freeze_end(&s_freeze);
s_nesting = 0;
/* A committed local view changed the presented rect: no later pass may
* reuse this backup. */
s_restored_valid = !(eye == RP_EYE_LOCAL && ok);
s_restored_plan = s_plan_serial;
s_restored_cycle = psx_cycle_count;
s_restored_stores = g_guest_store_count;
tr = gl_renderer_perf_ticks();
s_stats.avg_begin_ms = ema_ms(s_stats.avg_begin_ms, tb - t0);
s_stats.avg_guest_ms = ema_ms(s_stats.avg_guest_ms, tg - tb);
s_stats.avg_end_ms = ema_ms(s_stats.avg_end_ms, te - tg);
s_stats.avg_restore_ms = ema_ms(s_stats.avg_restore_ms, tr - te);
if (trace_on) {
uint32_t base = eye == PSX_MOD_EYE_LEFT ? PSX_TRACE_EYE_BEGIN_L
: PSX_TRACE_EYE_BEGIN_R;
psx_frame_trace_add(base + 0, trace_ticks_to_us(tb - t0));
psx_frame_trace_add(base + 1, trace_ticks_to_us(tg - tb));
psx_frame_trace_add(base + 2, trace_ticks_to_us(te - tg));
psx_frame_trace_add(base + 3, trace_ticks_to_us(tr - te));
uint64_t p1 = 0, t1x = 0, diag1[6] = {0,0,0,0,0,0};
gl_renderer_scene_prim_counts(&p1, &t1x);
gl_renderer_runtime_diag(diag1);
psx_frame_trace_draw(eye, (uint32_t)(p1 - trace_prims0),
(uint32_t)(t1x - trace_tex0));
psx_frame_trace_eye_cycles(eye, (uint32_t)guest_cycles);
/* Upload pixels -> bytes assumes the 16-bit VRAM format uploads use. */
psx_frame_trace_gpu((diag1[2] - trace_diag0[2]) * 2u,
(uint32_t)leaks);
s_sub_eye = -1;
sub_spans_forget();
if (reuse_baseline) psx_frame_trace_flag(PSX_FRAME_TRACE_BASELINE_REUSED);
}
int verify_ok = 1;
if (verify_on()) {
hash_after = state_hash(cpu);
s_stats.verify_checks++;
if (hash_after != hash_before || !gl_renderer_pass_verify_vram()) {
verify_ok = 0;
s_stats.verify_mismatch++;
if (eye >= 0) { ok = 0; s_stereo.last_failure = "restore_verify"; }
/* A mismatch while the eye rendered from the pair's shared baseline
* is exactly what this guard is for: retire the reuse for the
* session and capture per eye again from here on. */
if (reuse_baseline && baseline_reuse_enabled()) {
s_baseline_enabled = 0;
fprintf(stderr, "psxrecomp: pair baseline reuse disabled after "
"a restore-verify mismatch\n");
}
if (s_stats.verify_mismatch <= 8)
fprintf(stderr, "psxrecomp: RENDER PASS VERIFY mismatch "
"(state %016llx -> %016llx)\n",
(unsigned long long)hash_before,
(unsigned long long)hash_after);
}
}
/* The restore just put the live machine back to the captured baseline, so
* the other eye of this pair, or the other matte of this capture, may reuse
* it. Anything else - a non-stereo transaction, a capture that is not part
* of a matte pair, a failed eye or an unverified restore - leaves nothing
* safe to reuse. */
s_baseline_ready = (eye >= 0 && ok && verify_ok &&
(!s_capture_only || s_capture_pair)) ? 1 : 0;
t1 = gl_renderer_perf_ticks();
{
double ms = (double)(t1 - t0) * 1000.0 /
(double)gl_renderer_perf_frequency();
s_stats.last_pass_ms = ms;
s_stats.timed_passes++;
s_stats.total_gl_begin_ms += (tv - t0) * s_ms_per_tick;
s_stats.total_checkpoint_ms += (tc - tv) * s_ms_per_tick;
s_stats.total_verify_before_ms += (tb - tc) * s_ms_per_tick;
s_stats.total_guest_ms += (tg - tb) * s_ms_per_tick;
s_stats.total_gl_end_ms += (te - tg) * s_ms_per_tick;
s_stats.total_restore_ms += (tr - te) * s_ms_per_tick;
s_stats.total_verify_after_ms += (t1 - tr) * s_ms_per_tick;
s_stats.total_pass_ms += ms;
if (ms > s_stats.max_pass_ms) s_stats.max_pass_ms = ms;
s_stats.avg_pass_ms = s_stats.avg_pass_ms > 0.0
? s_stats.avg_pass_ms * 0.9 + ms * 0.1 : ms;
}
if (eye == RP_EYE_PASS) gl_renderer_pass_note_cost(t1 - t0);
if (ok && eye == RP_EYE_LOCAL) {
s_stats.local_views++;
/* The peer's own image supersedes a crop of the canonical frame. */
psx_netplay_local_view_clear();
} else if (ok) s_stats.passes++;
else if (leaks) note_fault("VRAM write outside the pass rect");
else if (s_stats.watchdog_flag) note_fault("guest-cycle watchdog");
else if (s_device_read_fault) note_fault("read from an uncheckpointed device");
s_stats.watchdog_flag = 0;
/* Present anything that fell due while the pass ran. */
if (eye == RP_EYE_PASS) gl_renderer_pass_service_presents();
return ok;
}
int psx_mod_render_local_view(struct CPUState *cpu, const PSXModRenderPass *rect,
PSXModRenderPassFn fn, void *user) {
PSXModRenderPass r;
s_stats.local_attempts++;
if (!rect || rect->struct_size < sizeof r ||
(uint32_t)rect->x + rect->w > 1024u || (uint32_t)rect->y + rect->h > 512u) {
s_stats.argument_refused++;
return pass_refuse("arguments", transaction_status(2));
}
r = *rect;
r.alpha_q16 = 0;
return render_transaction(cpu, &r, fn, user, RP_EYE_LOCAL);
}
uint32_t psx_mod_render_local_view_status(void) {
return transaction_status(2);
}
int psx_mod_render_pass(struct CPUState *cpu, const PSXModRenderPass *pass,
PSXModRenderPassFn fn, void *user) {
return render_transaction(cpu, pass, fn, user, -1);
}
typedef struct StereoCallback {
PSXModStereoFn fn;
void *user;
uint32_t eye;
} StereoCallback;
static int stereo_eye_draw(CPUState *cpu, void *user, uint32_t unused) {
StereoCallback *c = (StereoCallback *)user;
(void)unused;
return c->fn(cpu, c->user, c->eye);
}
int psx_mod_render_stereo(CPUState *cpu, const PSXModStereoFrame *frame,
PSXModStereoFn fn, void *user) {
PSXModRenderPass rect;
StereoCallback call;
uint64_t t0, cycle, stores, mismatches, aborted;
double budget_ms, elapsed;
uint32_t status;
int ok = 1;
/* Refuse re-entry without changing the outer pair's counters/selector. */
if (s_pair_busy) return 0;
s_stereo.attempts++;
s_stereo.last_eye = -1;
if (!cpu || !frame || !fn || frame->struct_size < sizeof *frame ||
!frame->w || !frame->h || !frame->period_vblanks ||
frame->period_vblanks > 8u || (uint32_t)frame->x + frame->w > 1024u ||
(uint32_t)frame->y + frame->h > 512u) {
s_stereo.refused++; s_stereo.last_failure = "arguments"; return 0;
}
status = transaction_status(1);
if (status != PSX_MOD_RENDER_PASS_READY) {
s_stereo.refused++; s_stereo.last_failure = "status"; return 0;
}
/* Frame trace (opt-in). The runtime already opened this frame's record in
* psx_openxr_begin() when a headset is active; opening here covers the
* desktop stereo path, and the guest-sim gap is the host time since the
* previous traced frame. */
int trace_on = psx_frame_trace_enabled();
int trace_opened = trace_on ? psx_frame_trace_open() : 0;
uint64_t now_ms = psx_host_mono_ms();
if (trace_on) {
static uint64_t prev_ms;
static int prev_valid;
if (prev_valid)
psx_frame_trace_add(PSX_TRACE_GUEST_SIM, (now_ms - prev_ms) * 1000u);
prev_ms = now_ms; prev_valid = 1;
psx_frame_trace_attempt(s_stereo.attempts, 0, 0);
}
budget_ms = shed_budget_ms(frame->period_vblanks, &s_stereo.budget_source);
s_stereo.budget_ms = budget_ms;
/* A scene/mode transition changes cadence and workload, so the previous
* average and budget do not carry over. */
if (!s_shed_geom_valid || frame->period_vblanks != s_shed_period ||
(int32_t)frame->w != s_shed_w || (int32_t)frame->h != s_shed_h) {
if (s_shed_geom_valid) s_stereo.budget_resets++;
s_shed_period = frame->period_vblanks;
s_shed_w = (int32_t)frame->w; s_shed_h = (int32_t)frame->h;
s_shed_geom_valid = 1;
stereo_shed_policy_reset(&s_shed);
}
stereo_shed_policy_set_budget(&s_shed, budget_ms);
{
int was_shedding = s_shed.shedding;
if (stereo_shed_policy_should_shed(&s_shed, now_ms)) {
s_stereo.shed++;
s_stereo.stale_frames++;
s_stereo.shed_streak = stereo_shed_policy_streak_sheds(&s_shed);
if (s_stereo.shed_streak > s_stereo.shed_streak_max)
s_stereo.shed_streak_max = s_stereo.shed_streak;
s_stereo.shed_streak_ms = stereo_shed_policy_streak_ms(&s_shed, now_ms);
s_stereo.shedding = 1;
if (trace_on) {
psx_frame_trace_attempt(s_stereo.attempts, s_stereo.shed_streak,
s_stereo.shed_streak_ms);
psx_frame_trace_flag(PSX_FRAME_TRACE_STALE_PAIR | PSX_FRAME_TRACE_SHED);
if (trace_opened) psx_frame_trace_commit();
}
return 0;
}
/* Log once per streak (not per frame), with both the skipped count and
* the elapsed wall clock the old attempt counter never tracked. */
if (was_shedding)
fprintf(stderr, "psxrecomp: stereo shed streak ended after %llu "
"skipped, %u ms\n",
(unsigned long long)s_stereo.shed_streak,
s_stereo.shed_streak_ms);
}
s_stereo.shedding = s_shed.shedding;
if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles();
cycle = psx_cycle_count; stores = g_guest_store_count;
mismatches = s_stats.verify_mismatch; aborted = s_stats.aborted;
s_stereo.last_failure = NULL;
s_stereo.eye_cycle[0] = s_stereo.eye_cycle[1] = 0;
s_stereo.eye_hash[0] = s_stereo.eye_hash[1] = 0;
gl_renderer_stereo_stage_reset();
memset(&rect, 0, sizeof rect);
rect.struct_size = sizeof rect;
rect.x = frame->x; rect.y = frame->y; rect.w = frame->w; rect.h = frame->h;
call.fn = fn; call.user = user;
s_pair_busy = 1;
s_plan_serial++; /* separate the backup lease from any temporal plan */
/* The guest has run since the previous pair, so the retained snapshot no
* longer describes the machine: only the second eye of this pair may reuse
* what the first one captures. */
s_baseline_ready = 0;
t0 = gl_renderer_perf_ticks();
for (call.eye = 0; call.eye < 2u; call.eye++) {
s_stereo.last_eye = (int)call.eye;
if (psx_cycle_count != cycle || g_guest_store_count != stores) {
ok = 0; s_stereo.last_failure = "checkpoint_changed"; break;
}
if (!render_transaction(cpu, &rect, stereo_eye_draw, &call, (int)call.eye)) {
ok = 0;
if (!s_stereo.last_failure) s_stereo.last_failure =
s_stats.aborted != aborted ? "eye_abort" : "eye_refused_or_discarded";
break;
}
}
if (s_stats.verify_mismatch != mismatches ||
s_stereo.eye_cycle[0] != s_stereo.eye_cycle[1] ||
(verify_on() && s_stereo.eye_hash[0] != s_stereo.eye_hash[1])) {
ok = 0;
if (!s_stereo.last_failure) s_stereo.last_failure = "eye_checkpoint_mismatch";
}
elapsed = (double)(gl_renderer_perf_ticks() - t0) * 1000.0 /
(double)gl_renderer_perf_frequency();
s_stereo.last_pair_ms = elapsed;
if (ok && !gl_renderer_stereo_publish(s_stereo.attempts, cycle, s_stereo.eye_view)) {
ok = 0; s_stereo.last_failure = "pair_publish";
}
if (ok) {
s_stereo.pairs++;
s_stereo.last_pair_id = s_stereo.attempts;
s_stereo.last_guest_cycle = cycle;
} else {
s_stereo.failed++;
s_stereo.last_failed_attempt = s_stereo.attempts;
s_stereo.last_failed_eye = s_stereo.last_eye;
s_stereo.last_failed_reason = s_stereo.last_failure;
s_stereo.retained_pair_id = s_stereo.last_pair_id;
gl_renderer_stereo_stage_reset();
}
/* Fold the attempt into the policy's estimate (it owns the EMA now). A
* failed or aborted attempt still counts as a measurement, so a stale high
* average cannot outlive the condition that produced it; only a published
* pair refreshes the retained-pair age. Pair age is tracked separately from
* the compute-time estimate so "how old is the picture" never hides behind
* "how fast was the last attempt". */
stereo_shed_policy_note_pair(&s_shed, elapsed, now_ms, ok);
s_stereo.avg_pair_ms = stereo_shed_policy_avg_ms(&s_shed);
s_stereo.last_pair_age_ms = stereo_shed_policy_pair_age_ms(&s_shed, now_ms);
s_stereo.shedding = s_shed.shedding;
if (!ok) s_stereo.stale_frames++;
if (trace_on) {
psx_frame_trace_add(PSX_TRACE_PAIR_TOTAL, (uint64_t)(elapsed * 1000.0));
psx_frame_trace_result(ok, ok ? s_stereo.last_pair_id : 0,
ok ? -1 : s_stereo.last_eye,
ok ? NULL : s_stereo.last_failure);
if (trace_opened) psx_frame_trace_commit();
}
s_pair_busy = 0;
return ok;
}
/* Game hook: a scene or mode transition invalidates the pair-time estimate and
* the derived budget, so the new scene does not inherit the old one's. Cheap
* and idempotent; safe to call on every transition. */
void psx_mod_stereo_reset_budget(void) {
stereo_shed_policy_reset(&s_shed);
s_shed_geom_valid = 0;
/* The deadline came from a session that is now gone or whose cadence just
* changed; the OpenXR layer re-pushes it on the next frame it runs. */
s_shed_xr_budget_ms = 0.0;
s_stereo.budget_resets++;
}
void psx_mod_stereo_set_budget_ms(double ms) {
/* Push the headset deadline. Ignore nonsense rather than letting a bad
* value widen or collapse the budget. */
s_shed_xr_budget_ms = (ms > 0.0 && ms < 1000.0) ? ms : 0.0;
}
/* Two mattes of one slice in a single capture: the first transaction stages
* eye 0 and the second eye 1, so one read returns both. The two transactions
* are captures, not eye draws: neither publishes a pair nor samples the
* per-eye trace, and the checkpoint they share is their own. */
int psx_mod_render_capture_rgba_pair(CPUState *cpu,const PSXModRenderPass *rect,
PSXModRenderPassFn fn,void *user_first,
void *user_second,uint8_t *rgba,uint32_t bytes) {
if(s_pair_busy || !cpu || !rect || !fn || !rgba || !user_first || !user_second ||
rect->struct_size<sizeof *rect ||
!rect->w || !rect->h || rect->x+rect->w>1024 || rect->y+rect->h>512 ||
(uint64_t)rect->w*rect->h*8>bytes || transaction_status(1)!=PSX_MOD_RENDER_PASS_READY)return 0;
if(g_overlay_flush_pending_cycles)g_overlay_flush_pending_cycles();
uint64_t trace_t0=gl_renderer_perf_ticks();
s_pair_busy=1;s_plan_serial++;
gl_renderer_stereo_stage_reset();
PSXModRenderPass capture=*rect;capture.alpha_q16=0;
/* Start uncaptured: the ready flag carries the pair's baseline from the
* previous frame, which describes a machine the guest has since changed. */
s_capture_only=1;s_capture_pair=1;s_baseline_ready=0;
uint64_t cycle=psx_cycle_count,stores=g_guest_store_count;
int ok=render_transaction(cpu,&capture,fn,user_first,0);
/* The second matte must draw from the state the first one's restore put
* back. If the clock or the store count moved, that baseline no longer
* describes it, so capture the machine again instead of reusing it. */
if(psx_cycle_count!=cycle || g_guest_store_count!=stores)s_baseline_ready=0;
if(ok)ok=render_transaction(cpu,&capture,fn,user_second,1);
s_capture_only=0;s_capture_pair=0;s_baseline_ready=0;
uint64_t trace_t1=gl_renderer_perf_ticks();
psx_frame_trace_add(PSX_TRACE_HUD_DRAW,
trace_ticks_to_us(trace_t1-trace_t0));
if(ok)ok=gl_renderer_stereo_stage_readback_pair(rect->w,rect->h,rgba);
psx_frame_trace_add(PSX_TRACE_HUD_READBACK,
trace_ticks_to_us(gl_renderer_perf_ticks()-trace_t1));
gl_renderer_stereo_stage_reset();s_pair_busy=0;
psx_frame_trace_add(PSX_TRACE_HUD_CAPTURE,
trace_ticks_to_us(gl_renderer_perf_ticks()-trace_t0));
return ok;
}