mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
1013 lines
42 KiB
C
1013 lines
42 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 <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "cpu_state.h"
|
|
#include "dirty_ram_interp.h"
|
|
#include "dma.h"
|
|
#include "gpu.h"
|
|
#include "gpu_gl_renderer.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"
|
|
|
|
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. */
|
|
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;
|
|
/* 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;
|
|
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;
|
|
static uint32_t s_pair_retry;
|
|
|
|
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;
|
|
}
|
|
|
|
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];
|
|
}
|
|
|
|
int render_pass_verification_enabled(void) { return verify_on(); }
|
|
|
|
void render_pass_reset_session(void) {
|
|
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;
|
|
s_pair_retry = 0;
|
|
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;
|
|
}
|
|
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->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;
|
|
gte_render_pose_set(view);
|
|
return 1;
|
|
}
|
|
|
|
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;
|
|
{
|
|
uint32_t status = pass_status();
|
|
if (status != PSX_MOD_RENDER_PASS_READY) {
|
|
s_stats.status_refused++;
|
|
gl_renderer_pass_cancel_plan();
|
|
return pass_refuse("plan_status", status);
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
static int checkpoint_save(const CPUState *cpu) {
|
|
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;
|
|
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;
|
|
}
|
|
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);
|
|
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) {
|
|
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,
|
|
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;
|
|
}
|
|
|
|
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, 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;
|
|
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;
|
|
}
|
|
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();
|
|
|
|
if (!checkpoint_save(cpu)) {
|
|
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 (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;
|
|
}
|
|
s_stats.guest_cycles_last = psx_cycle_count - cycles_before;
|
|
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);
|
|
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 (verify_on()) {
|
|
hash_after = state_hash(cpu);
|
|
s_stats.verify_checks++;
|
|
if (hash_after != hash_before || !gl_renderer_pass_verify_vram()) {
|
|
s_stats.verify_mismatch++;
|
|
if (eye >= 0) { ok = 0; s_stereo.last_failure = "restore_verify"; }
|
|
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);
|
|
}
|
|
}
|
|
|
|
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.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;
|
|
}
|
|
budget_ms = frame->period_vblanks * (double)g_psx_vblank_cycles /
|
|
33868.8 * 0.8;
|
|
if (s_stereo.avg_pair_ms > budget_ms && ++s_pair_retry < 30u) {
|
|
s_stereo.shed++; return 0;
|
|
}
|
|
s_pair_retry = 0;
|
|
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 */
|
|
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;
|
|
s_stereo.avg_pair_ms = s_stereo.avg_pair_ms > 0.0
|
|
? s_stereo.avg_pair_ms * 0.9 + elapsed * 0.1 : 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();
|
|
}
|
|
s_pair_busy = 0;
|
|
return ok;
|
|
}
|
|
|
|
int psx_mod_render_capture_rgba(CPUState *cpu,const PSXModRenderPass *rect,
|
|
PSXModRenderPassFn fn,void *user,
|
|
uint8_t *rgba,uint32_t bytes) {
|
|
if(s_pair_busy || !cpu || !rect || !fn || !rgba || 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*4>bytes || transaction_status(1)!=PSX_MOD_RENDER_PASS_READY)return 0;
|
|
if(g_overlay_flush_pending_cycles)g_overlay_flush_pending_cycles();
|
|
s_pair_busy=1;s_plan_serial++;
|
|
gl_renderer_stereo_stage_reset();
|
|
PSXModRenderPass capture=*rect;capture.alpha_q16=0;
|
|
int ok=render_transaction(cpu,&capture,fn,user,0);
|
|
if(ok)ok=gl_renderer_stereo_stage_readback(0,rect->w,rect->h,rgba);
|
|
gl_renderer_stereo_stage_reset();s_pair_busy=0;
|
|
return ok;
|
|
}
|