mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
510560e599
Co-authored-by: Yves <calibratedbeats@gmail.com>
1136 lines
53 KiB
C
1136 lines
53 KiB
C
/* Render-pass sandbox: what a pass may write and what it gets back.
|
|
*
|
|
* 1. Store policy (render_pass_plan.c render_pass_store_to, the function
|
|
* memory.c routes every store made inside a pass through): RAM and
|
|
* scratchpad are written directly; SPU, CD, timer, other-DMA, GP1 and
|
|
* everything else outside the allow-list is dropped under its class and
|
|
* never reaches the device; GP0, GP1 DMA mode, I_STAT/I_MASK, GPU/OTC DMA
|
|
* and DPCR/DICR reach it.
|
|
* 2. A real pass (render_pass.c, psx_cycles.c and the real timers.c) that
|
|
* runs 10^6 guest cycles, makes those stores, acknowledges and masks
|
|
* interrupts: the drops are counted per class, no device advances, no
|
|
* timer IRQ is raised, and RAM, scratchpad, I_STAT/I_MASK, timers and the
|
|
* clock are exactly as before. Status gates refuse the pass without
|
|
* running it. The same with 8 MiB RAM live (the opt-in 8 MB RAM map):
|
|
* stores reach unique RAM up to 0x7FFFFF and all 8 MiB are restored; a
|
|
* retail pass after it folds mirrors again.
|
|
* 3. VRAM journal (render_pass_vram_policy / _journal_add / _rollback, the
|
|
* CPU side of gpu_gl_renderer.c's out-of-rect journal): writes inside the
|
|
* pass rect are allowed, writes outside are backed up first and rolled
|
|
* back exactly, covered writes are not journaled twice, the last slot spills
|
|
* to a whole-VRAM backup, and a mode that cannot journal refuses.
|
|
*
|
|
* The GPU, presenter and remaining devices are mocks. Build/run:
|
|
* ctest -R render_pass_sandbox_test */
|
|
|
|
#include "frame_trace.h"
|
|
#include "render_pass.h"
|
|
#include "render_pass_plan.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
#include "cpu_state.h"
|
|
#include "dirty_ram_interp.h"
|
|
#include "mod_plugins.h"
|
|
#include "mod_runtime.h"
|
|
#include "psx_cycles.h"
|
|
#include "timers.h"
|
|
|
|
static ModFunctionEntryContext s_mod_entry;
|
|
static unsigned s_host_call_depth;
|
|
unsigned psx_snapshot_host_call_depth(void) { return s_host_call_depth; }
|
|
void psx_snapshot_host_call_restore(unsigned depth) { s_host_call_depth = depth; }
|
|
void mod_runtime_function_entry_context_save(ModFunctionEntryContext *out) {
|
|
*out = s_mod_entry;
|
|
}
|
|
void mod_runtime_function_entry_context_restore(const ModFunctionEntryContext *in) {
|
|
s_mod_entry = *in;
|
|
}
|
|
static int failures;
|
|
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr, "FAIL: %s\n", m); failures++; } } while (0)
|
|
|
|
/* ---- runtime globals the units share with the rest of the runtime ----- */
|
|
int g_ls_replay_active = 0;
|
|
int g_boot_state_cpu_timing_restored = 0;
|
|
int psx_mod_host_output_scope(void) { return 0; }
|
|
int g_ls_mode = 0;
|
|
int g_precise_mode = 0;
|
|
int g_psx_call_bail = 0;
|
|
int g_psx_dispatch_depth = 0;
|
|
uint32_t i_stat = 0, i_mask = 0;
|
|
uint64_t g_guest_store_count = 0;
|
|
uint64_t g_mmio_access_count = 0;
|
|
uint32_t g_debug_last_store_pc = 0;
|
|
uint32_t g_debug_current_func_addr = 0;
|
|
uint32_t g_psx_icache_tv[1024];
|
|
uint64_t g_render_pass_dropped_writes[RENDER_PASS_DROP_CLASSES];
|
|
void (*g_overlay_flush_pending_cycles)(void) = NULL;
|
|
int g_call_unit_depth = 0;
|
|
int g_dma_exec_depth = 0;
|
|
int g_host_store_depth = 0;
|
|
int g_dma_cur_ch = -1;
|
|
uint32_t g_dma_cur_madr = 0, g_dma_cur_bcr = 0, g_dma_initiator_pc = 0;
|
|
int g_dirty_interp_active = 0;
|
|
int g_exec_phase = 0;
|
|
uint32_t g_dirty_safe_resume_pc = 0;
|
|
|
|
/* ---- memory and the MMIO devices a pass may reach ----------------------- */
|
|
/* Backing store sized for the largest geometry, as memory.c's; the live size
|
|
* (what memory_get_ram_bytes() answers and memory.c passes the store policy)
|
|
* is 2 MiB retail or 8 MiB with the 8 MB RAM mod. */
|
|
#define RAM_2MB (2u * 1024u * 1024u)
|
|
#define RAM_8MB (8u * 1024u * 1024u)
|
|
static uint8_t s_ram[RAM_8MB];
|
|
static uint32_t s_ram_live = RAM_2MB;
|
|
static uint8_t s_spad[1024];
|
|
static uint8_t s_dma_regs[64];
|
|
static uint32_t s_csv = 1234;
|
|
static int s_mmio_calls, s_gp0_words, s_gp1_calls, s_irq_refresh;
|
|
static uint32_t s_last_mmio;
|
|
|
|
static void mock_mmio_write(uint32_t phys, uint32_t val, uint32_t width) {
|
|
(void)width;
|
|
s_mmio_calls++;
|
|
s_last_mmio = phys;
|
|
if (phys == 0x1F801070u) i_stat &= val; /* acknowledge */
|
|
else if (phys == 0x1F801074u) i_mask = val;
|
|
else if (phys == 0x1F801810u) s_gp0_words++;
|
|
else if (phys == 0x1F801814u) s_gp1_calls++;
|
|
else if (phys >= 0x1F801100u && phys <= 0x1F80112Fu)
|
|
timers_write(phys, val); /* would move a timer */
|
|
}
|
|
|
|
static RenderPassStoreTarget target(void) {
|
|
RenderPassStoreTarget t;
|
|
memset(&t, 0, sizeof t);
|
|
t.ram = s_ram;
|
|
t.ram_size = s_ram_live;
|
|
t.scratchpad = s_spad;
|
|
t.scratchpad_size = sizeof s_spad;
|
|
t.mmio_write = mock_mmio_write;
|
|
return t;
|
|
}
|
|
|
|
/* What memory.c psx_write_word/half/byte do while a pass is active. */
|
|
static void guest_store(uint32_t addr, uint32_t val, uint32_t width) {
|
|
RenderPassStoreTarget t = target();
|
|
int cls;
|
|
if (!g_psx_render_pass_active) {
|
|
fprintf(stderr, "FAIL: guest_store outside a pass\n");
|
|
failures++;
|
|
return;
|
|
}
|
|
cls = render_pass_store_to(&t, addr, val, width);
|
|
if (cls >= 0) g_render_pass_dropped_writes[cls]++;
|
|
}
|
|
|
|
uint8_t *memory_get_ram_ptr(void) { return s_ram; }
|
|
uint32_t memory_get_ram_bytes(void) { return s_ram_live; }
|
|
uint8_t *memory_get_scratchpad_ptr(void) { return s_spad; }
|
|
uint32_t dma_snapshot_bytes(void) { return sizeof s_dma_regs; }
|
|
void dma_snapshot_write(uint8_t *p) { memcpy(p, s_dma_regs, sizeof s_dma_regs); }
|
|
int dma_snapshot_read(const uint8_t *p, uint32_t len) {
|
|
if (len != sizeof s_dma_regs) return 0;
|
|
memcpy(s_dma_regs, p, len);
|
|
return 1;
|
|
}
|
|
int dma_gpu_linked_list_active(void) { return 0; }
|
|
uint32_t interrupts_get_cycles_since_vblank(void) { return s_csv; }
|
|
void interrupts_set_cycles_since_vblank(uint32_t v) { s_csv = v; }
|
|
void psx_irq_refresh_cause_ip2(void) { s_irq_refresh++; }
|
|
static int s_irqs_raised;
|
|
void psx_irq_raise(uint32_t bit, uint32_t detail) {
|
|
(void)detail;
|
|
s_irqs_raised++;
|
|
i_stat |= 1u << bit;
|
|
}
|
|
void event_ring_record_aux(int ev, uint8_t src, uint32_t aux) {
|
|
(void)ev; (void)src; (void)aux;
|
|
}
|
|
uint32_t cdrom_snapshot_bytes(void) { return 0; }
|
|
void cdrom_snapshot_write(uint8_t *p) { (void)p; }
|
|
uint32_t spu_snapshot_bytes(void) { return 0; }
|
|
void spu_snapshot_write(uint8_t *p) { (void)p; }
|
|
uint32_t sio_snapshot_bytes(void) { return 0; }
|
|
void sio_snapshot_write(uint8_t *p) { (void)p; }
|
|
uint32_t mdec_snapshot_bytes(void) { return 0; }
|
|
void mdec_snapshot_write(uint8_t *p) { (void)p; }
|
|
|
|
int psx_get_in_exception(void) { return 0; }
|
|
static int s_netplay_active, s_netplay_resim;
|
|
int psx_netplay_active(void) { return s_netplay_active; }
|
|
int psx_netplay_is_resimulating(void) { return s_netplay_resim; }
|
|
static int s_crop_clears;
|
|
#define s_netplay s_netplay_active
|
|
#define s_resim s_netplay_resim
|
|
void psx_netplay_local_view_clear(void) { s_crop_clears++; }
|
|
static int s_view_shed;
|
|
int psx_netplay_local_view_shed(void) { return s_view_shed; }
|
|
static int s_ws_scope;
|
|
void gpu_ws_set_local_view_scope(int on) { s_ws_scope = on; }
|
|
int psx_selfcheck_resim_active(void) { return 0; }
|
|
int psx_selfcheck_enabled(void) { return 0; }
|
|
int psx_rewind_is_open(void) { return 0; }
|
|
static int s_fast_forward;
|
|
int psx_presentation_fast_forward(void) { return s_fast_forward; }
|
|
|
|
/* psx_cycles.c device side (timers are the real ones). */
|
|
static uint64_t s_dev_cycles;
|
|
void sio_advance(uint32_t c) { s_dev_cycles += c; }
|
|
void cdrom_advance(uint32_t c) { s_dev_cycles += c; }
|
|
void dma_advance(uint32_t c) { s_dev_cycles += c; }
|
|
void interrupts_advance_cycles(uint32_t c) { s_dev_cycles += c; }
|
|
void interrupts_service_scheduled_events(void) {}
|
|
uint32_t interrupts_cycles_to_vblank(void) { return 564480u; }
|
|
uint32_t cdrom_cycles_to_irq(uint32_t m) { (void)m; return UINT32_MAX; }
|
|
uint32_t dma_cycles_to_internal_event(void) { return UINT32_MAX; }
|
|
uint32_t dma_cycles_to_deliverable_irq(uint32_t m) { (void)m; return UINT32_MAX; }
|
|
uint32_t sio_cycles_to_irq(uint32_t m) { (void)m; return UINT32_MAX; }
|
|
uint32_t psx_spu_sample_event_cycles_to_next(void) { return UINT32_MAX; }
|
|
void psx_spu_sample_event_service(void) {}
|
|
void starvation_watchdog_check(void) {}
|
|
void starvation_ring_pc_sample(void) {}
|
|
void dirty_ram_irq_ambient_resync_after_restore(void) {}
|
|
|
|
static DirtyRamLoadDelay s_ld;
|
|
void dirty_ram_ld_delay_discard(void) { memset(&s_ld, 0, sizeof s_ld); }
|
|
void dirty_ram_ld_delay_save(DirtyRamLoadDelay *o) { *o = s_ld; }
|
|
void dirty_ram_ld_delay_restore(const DirtyRamLoadDelay *i) { s_ld = *i; }
|
|
static uint32_t s_span_lo, s_span_hi;
|
|
void dirty_ram_span_get(uint32_t *lo, uint32_t *hi) { *lo = s_span_lo; *hi = s_span_hi; }
|
|
void dirty_ram_span_set(uint32_t lo, uint32_t hi) { s_span_lo = lo; s_span_hi = hi; }
|
|
int dirty_ram_run_span(CPUState *cpu, uint32_t start, uint32_t stop, uint64_t max) {
|
|
(void)cpu; (void)start; (void)stop; (void)max;
|
|
return 0;
|
|
}
|
|
void overlay_loader_native_nesting(int *d, uint32_t *ip) { *d = 0; *ip = 0; }
|
|
void overlay_loader_set_native_nesting(int d, uint32_t ip) { (void)d; (void)ip; }
|
|
/* memory.c mod arenas (render_pass_mod_store): nothing journaled here. */
|
|
void render_pass_mod_arenas_rollback(void) {}
|
|
uint64_t render_pass_mod_arenas_hash(void) { return 0; }
|
|
static int s_prec_open;
|
|
void gte_precision_checkpoint_begin(void) { s_prec_open++; }
|
|
void gte_precision_checkpoint_rollback(void) { s_prec_open--; }
|
|
|
|
/* GPU and presenter. */
|
|
uint64_t gpu_pass_state_hash(void) { return 42; }
|
|
static int s_checkpoint_ok = 1;
|
|
int gpu_pass_checkpoint_save(void) { return s_checkpoint_ok; }
|
|
void gpu_pass_checkpoint_restore(void) {}
|
|
static uint64_t s_ticks;
|
|
uint64_t gl_renderer_perf_ticks(void) { return s_ticks += 1000; }
|
|
uint64_t gl_renderer_perf_frequency(void) { return 1000000000u; }
|
|
/* 1000 ticks per sample and a 1 GHz tick make one runtime sample exactly 1 us,
|
|
* so a bracketed guest span is the caller's own bump plus a known microsecond. */
|
|
static void tick_bump(uint64_t ticks) { s_ticks += ticks; }
|
|
static uint32_t s_gl_status = PSX_MOD_RENDER_PASS_READY;
|
|
uint32_t gl_renderer_pass_unavailable(void) { return s_gl_status; }
|
|
uint32_t gl_renderer_pass_plan(uint32_t p, uint32_t s, uint32_t *a,
|
|
uint32_t max, uint32_t *wanted) {
|
|
(void)p; (void)s; (void)a; (void)max;
|
|
if (wanted) *wanted = 0;
|
|
return 0;
|
|
}
|
|
static int s_open_passes, s_kept, s_begin_ok = 1, s_diag_null;
|
|
void gl_renderer_pass_begin_diag(GLRenderPassBeginDiag *out) {
|
|
memset(out, 0, sizeof *out);
|
|
if (s_diag_null) return;
|
|
out->reason = "capture_size";
|
|
out->requested_w = 320; out->requested_h = 240;
|
|
out->capture_w = 512; out->capture_h = 240;
|
|
}
|
|
void gl_renderer_pass_set_flip_shown(int shown) { (void)shown; }
|
|
int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
|
|
uint32_t period, int reuse_backup) {
|
|
(void)x; (void)y; (void)w; (void)h; (void)open_gen; (void)period;
|
|
(void)reuse_backup;
|
|
if (!s_begin_ok) return 0;
|
|
s_open_passes++;
|
|
return 1;
|
|
}
|
|
void gl_renderer_pass_end(uint32_t alpha_q16, int keep) {
|
|
(void)alpha_q16;
|
|
s_open_passes--;
|
|
if (keep) s_kept++;
|
|
}
|
|
uint32_t gl_renderer_pass_leaks(void) { return 0; }
|
|
int gl_renderer_pass_verify_vram(void) { return 1; }
|
|
void gl_renderer_pass_note_cost(uint64_t t) { (void)t; }
|
|
void gl_renderer_pass_service_presents(void) {}
|
|
static uint32_t s_local_status = PSX_MOD_RENDER_PASS_READY;
|
|
static int s_local_kept;
|
|
uint32_t gl_renderer_local_view_unavailable(void) { return s_local_status; }
|
|
int gl_renderer_local_view_begin(int x, int y, int w, int h) {
|
|
return gl_renderer_pass_begin(x, y, w, h, 0, 0, 0);
|
|
}
|
|
int gl_renderer_local_view_end(int keep) {
|
|
gl_renderer_pass_end(0, 0);
|
|
if (keep) s_local_kept++;
|
|
return 1;
|
|
}
|
|
static uint32_t s_stereo_mask;
|
|
static uint64_t s_stereo_published;
|
|
static int s_capture_fail_eye = -1;
|
|
uint32_t gl_renderer_stereo_unavailable(void) { return s_gl_status; }
|
|
int gl_renderer_stereo_begin(int x, int y, int w, int h, int reuse) {
|
|
return gl_renderer_pass_begin(x, y, w, h, 0, 0, reuse);
|
|
}
|
|
int gl_renderer_stereo_end(uint32_t eye, int keep) {
|
|
gl_renderer_pass_end(0, keep);
|
|
if (keep && (int)eye == s_capture_fail_eye) return 0;
|
|
if (keep) s_stereo_mask |= 1u << eye;
|
|
return 1;
|
|
}
|
|
void gl_renderer_stereo_stage_reset(void) { s_stereo_mask = 0; }
|
|
int gl_renderer_stereo_stage_readback_pair(uint32_t w,uint32_t h,uint8_t *rgba) {
|
|
if((s_stereo_mask & 3u) != 3u) return 0;
|
|
memset(rgba,0x5A,(size_t)w*h*8);return 1;
|
|
}
|
|
void gl_renderer_stereo_reset(void) { s_stereo_mask = 0; s_stereo_published = 0; }
|
|
int gl_renderer_stereo_set_presentation(uint32_t mode) { return mode <= 1; }
|
|
static int32_t s_view[3];
|
|
void gte_render_view_get(int32_t xyz[3]) { memcpy(xyz, s_view, sizeof s_view); }
|
|
void gte_render_view_set(const int32_t xyz[3]) { memcpy(s_view, xyz, sizeof s_view); }
|
|
static PSXModRenderView s_pose;
|
|
void gte_render_pose_get(PSXModRenderView *v) { *v = s_pose; memcpy(v->translation, s_view, sizeof s_view); }
|
|
void gte_render_pose_set(const PSXModRenderView *v) { s_pose = *v; memcpy(s_view, v->translation, sizeof s_view); }
|
|
int gl_renderer_stereo_publish(uint64_t id, uint64_t cycle, const int32_t view[2][3]) {
|
|
(void)cycle;
|
|
(void)view;
|
|
if (s_stereo_mask != 3u) return 0;
|
|
s_stereo_published = id;
|
|
return 1;
|
|
}
|
|
|
|
/* Frame-trace draw/diag inputs and the host clock the stereo shedding and
|
|
* tracing use. Not GL state this sandbox models, but render_pass.c references
|
|
* them, so they must exist to link. */
|
|
void gl_renderer_runtime_diag(uint64_t out[6]) { memset(out, 0, 6 * sizeof *out); }
|
|
static uint64_t s_prims_stub;
|
|
void gl_renderer_scene_prim_counts(uint64_t *prims, uint64_t *textured) {
|
|
if (prims) *prims = s_prims_stub;
|
|
if (textured) *textured = s_prims_stub / 2u;
|
|
}
|
|
static uint64_t s_host_ms;
|
|
uint64_t psx_host_mono_ms(void) { return s_host_ms; }
|
|
|
|
/* ---- 1. store policy --------------------------------------------------- */
|
|
static void test_store_policy(void) {
|
|
RenderPassStoreTarget t = target();
|
|
int calls;
|
|
memset(s_ram, 0, sizeof s_ram);
|
|
memset(s_spad, 0, sizeof s_spad);
|
|
s_mmio_calls = 0;
|
|
|
|
CHECK(render_pass_store_to(&t, 0x80001000u, 0x11223344u, 4) == -1 &&
|
|
s_ram[0x1000] == 0x44 && s_ram[0x1003] == 0x11, "KSEG0 RAM word written");
|
|
CHECK(render_pass_store_to(&t, 0xA0001004u, 0xBEEFu, 2) == -1 &&
|
|
s_ram[0x1004] == 0xEF && s_ram[0x1005] == 0xBE, "KSEG1 RAM half written");
|
|
CHECK(render_pass_store_to(&t, 0x00201008u, 0x5Au, 1) == -1 &&
|
|
s_ram[0x1008] == 0x5A, "RAM mirror folds onto main RAM");
|
|
CHECK(render_pass_store_to(&t, 0x1F800010u, 0xCAFEF00Du, 4) == -1 &&
|
|
s_spad[0x10] == 0x0D && s_spad[0x13] == 0xCA, "scratchpad written");
|
|
CHECK(s_mmio_calls == 0, "memory stores never touch MMIO");
|
|
|
|
/* Dropped, per class, and never delivered. */
|
|
CHECK(render_pass_store_to(&t, 0x1F801D88u, 0xFFFFu, 2) == RENDER_PASS_DROP_SPU,
|
|
"SPU key-on dropped (SPU)");
|
|
CHECK(render_pass_store_to(&t, 0x1F801C00u, 0x3FFFu, 2) == RENDER_PASS_DROP_SPU,
|
|
"SPU voice volume dropped (SPU)");
|
|
CHECK(render_pass_store_to(&t, 0x1F801801u, 0x1Bu, 1) == RENDER_PASS_DROP_CD,
|
|
"CD command dropped (CD)");
|
|
CHECK(render_pass_store_to(&t, 0x1F801104u, 0x58u, 4) == RENDER_PASS_DROP_TIMER,
|
|
"timer mode dropped (TIMER)");
|
|
CHECK(render_pass_store_to(&t, 0x1F801128u, 100u, 2) == RENDER_PASS_DROP_TIMER,
|
|
"timer target dropped (TIMER)");
|
|
CHECK(render_pass_store_to(&t, 0x1F8010C0u, 0x80100000u, 4) == RENDER_PASS_DROP_DMA,
|
|
"SPU DMA MADR dropped (DMA)");
|
|
CHECK(render_pass_store_to(&t, 0x1F8010B8u, 0x11000000u, 4) == RENDER_PASS_DROP_DMA,
|
|
"CD DMA CHCR dropped (DMA)");
|
|
CHECK(render_pass_store_to(&t, 0x1F801814u, 0x00000000u, 4) == RENDER_PASS_DROP_GPU,
|
|
"GP1 reset dropped (GPU)");
|
|
CHECK(render_pass_store_to(&t, 0x1F801810u, 0xE1000000u, 2) == RENDER_PASS_DROP_GPU,
|
|
"a half-word GP0 store is dropped (GPU)");
|
|
CHECK(render_pass_store_to(&t, 0x1F801040u, 0x11u, 1) == RENDER_PASS_DROP_OTHER,
|
|
"SIO dropped (OTHER)");
|
|
CHECK(render_pass_store_to(&t, 0x1F801000u, 0x1F000000u, 4) == RENDER_PASS_DROP_OTHER,
|
|
"memory control dropped (OTHER)");
|
|
CHECK(render_pass_store_to(&t, 0x1F801820u, 0u, 4) == RENDER_PASS_DROP_OTHER,
|
|
"MDEC dropped (OTHER)");
|
|
CHECK(render_pass_store_to(&t, 0x1F000000u, 1u, 1) == RENDER_PASS_DROP_OTHER,
|
|
"expansion 1 dropped (OTHER)");
|
|
CHECK(render_pass_store_to(&t, 0xFFFE0130u, 0x804u, 4) == RENDER_PASS_DROP_OTHER,
|
|
"KSEG2 cache control dropped (OTHER)");
|
|
CHECK(s_mmio_calls == 0, "no dropped store reached a device");
|
|
|
|
/* Allowed: all restored after the pass. */
|
|
calls = s_mmio_calls;
|
|
CHECK(render_pass_store_to(&t, 0x1F801810u, 0x28FFFFFFu, 4) == -1, "GP0 word allowed");
|
|
CHECK(render_pass_store_to(&t, 0x1F801814u, 0x04000002u, 4) == -1, "GP1 DMA mode allowed");
|
|
CHECK(render_pass_store_to(&t, 0x1F801814u, 0x10000007u, 4) == -1, "GP1 info allowed");
|
|
CHECK(render_pass_store_to(&t, 0x1F801070u, 0xFFFFFFFEu, 4) == -1, "I_STAT ack allowed");
|
|
CHECK(render_pass_store_to(&t, 0x1F801074u, 0x0000000Du, 4) == -1, "I_MASK allowed");
|
|
CHECK(render_pass_store_to(&t, 0x1F8010A8u, 0x01000401u, 4) == -1, "GPU DMA allowed");
|
|
CHECK(render_pass_store_to(&t, 0x1F8010E8u, 0x11000002u, 4) == -1, "OTC DMA allowed");
|
|
CHECK(render_pass_store_to(&t, 0x1F8010F0u, 0x0F6F4321u, 4) == -1, "DPCR allowed");
|
|
CHECK(s_mmio_calls == calls + 8, "allowed stores reached their device");
|
|
|
|
/* Isolated cache: nothing but the I-cache would change. */
|
|
t.isolate_cache = 1;
|
|
s_ram[0x2000] = 0x77;
|
|
CHECK(render_pass_store_to(&t, 0x80002000u, 0u, 4) == -1 && s_ram[0x2000] == 0x77,
|
|
"IsC store absorbed, RAM untouched");
|
|
}
|
|
|
|
/* ---- 2. a real pass ------------------------------------------------------ */
|
|
static int pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
|
|
(void)user; (void)alpha_q16;
|
|
cpu->gpr[8] = 0xDEADu;
|
|
guest_store(0x80010000u, 0x12345678u, 4); /* RAM */
|
|
guest_store(0x1F800020u, 0x99u, 1); /* scratchpad */
|
|
guest_store(0x1F801D88u, 0x00FFu, 2); /* SPU key-on */
|
|
guest_store(0x1F801DAAu, 0xC000u, 2); /* SPU control */
|
|
guest_store(0x1F801801u, 0x0Au, 1); /* CD: Init */
|
|
guest_store(0x1F801124u, 0x0000u, 4); /* timer 2 mode */
|
|
guest_store(0x1F8010C8u, 0x01000201u, 4); /* SPU DMA CHCR */
|
|
guest_store(0x1F801814u, 0x03000001u, 4); /* GP1 display off */
|
|
guest_store(0x1F801044u, 0x0001u, 2); /* SIO */
|
|
guest_store(0x1F801810u, 0x02000000u, 4); /* GP0 fill: allowed */
|
|
guest_store(0x1F801070u, 0u, 4); /* ack every IRQ */
|
|
guest_store(0x1F801074u, 0u, 4); /* mask every IRQ */
|
|
for (int i = 0; i < 1000; i++) psx_advance_cycles(1000u); /* 10^6 */
|
|
/* A device-side change the store policy cannot see (as if some runtime
|
|
* path wrote a timer directly): the checkpoint must still undo it. */
|
|
timers_write(0x1F801100u, 0x1234u);
|
|
return 1;
|
|
}
|
|
|
|
static int never_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
|
|
(void)cpu; (void)user; (void)alpha_q16;
|
|
failures++;
|
|
fprintf(stderr, "FAIL: a refused pass ran its callback\n");
|
|
return 1;
|
|
}
|
|
|
|
static void test_pass(void) {
|
|
CPUState cpu;
|
|
PSXModRenderPass pass;
|
|
RenderPassStats st;
|
|
uint16_t tc0[3], tt0[3], tc1[3], tt1[3];
|
|
uint32_t tm0[3], tf0[3], tm1[3], tf1[3];
|
|
int32_t ti0[3], ti1[3];
|
|
uint8_t ram0[64], spad0[64];
|
|
uint64_t cyc0, dev0, next0;
|
|
uint32_t stat0, mask0;
|
|
int irqs0;
|
|
|
|
memset(&cpu, 0, sizeof cpu);
|
|
cpu.gpr[8] = 0x55u;
|
|
memset(s_ram, 0xA5, sizeof s_ram);
|
|
memset(s_spad, 0x3C, sizeof s_spad);
|
|
psx_cycles_resync_after_restore(NULL);
|
|
|
|
/* Live machine: timer 2 counts the system clock and interrupts on its
|
|
* target every 1000 cycles; timer 0 runs free. */
|
|
timers_init();
|
|
timers_write(0x1F801128u, 1000u);
|
|
timers_write(0x1F801124u, 0x0058u);
|
|
i_mask = 0x0040u; /* timer 2 unmasked */
|
|
i_stat = 0x0001u; /* a pending VBlank */
|
|
psx_advance_cycles(2500u);
|
|
CHECK(s_irqs_raised > 0, "sanity: timer 2 interrupts while time is live");
|
|
|
|
memset(&pass, 0, sizeof pass);
|
|
pass.struct_size = sizeof pass;
|
|
pass.w = 320; pass.h = 240;
|
|
pass.alpha_q16 = 32768u;
|
|
|
|
timers_get_snapshot(tc0, tm0, tt0, ti0, tf0);
|
|
memcpy(ram0, s_ram + 0x10000, sizeof ram0);
|
|
memcpy(spad0, s_spad, sizeof spad0);
|
|
cyc0 = psx_cycle_count;
|
|
next0 = psx_next_service_cycle;
|
|
dev0 = s_dev_cycles;
|
|
stat0 = i_stat;
|
|
mask0 = i_mask;
|
|
irqs0 = s_irqs_raised;
|
|
s_mmio_calls = 0;
|
|
|
|
CHECK(psx_mod_render_pass(&cpu, &pass, pass_fn, NULL) == 1, "the pass ran and was kept");
|
|
render_pass_get_stats(&st);
|
|
CHECK(st.passes == 1 && s_kept == 1 && s_open_passes == 0, "one image, pass closed");
|
|
CHECK(st.dropped[RENDER_PASS_DROP_SPU] == 2, "2 SPU stores dropped and counted");
|
|
CHECK(st.dropped[RENDER_PASS_DROP_CD] == 1, "1 CD store dropped and counted");
|
|
CHECK(st.dropped[RENDER_PASS_DROP_TIMER] == 1, "1 timer store dropped and counted");
|
|
CHECK(st.dropped[RENDER_PASS_DROP_DMA] == 1, "1 other-DMA store dropped and counted");
|
|
CHECK(st.dropped[RENDER_PASS_DROP_GPU] == 1, "1 GP1 store dropped and counted");
|
|
CHECK(st.dropped[RENDER_PASS_DROP_OTHER] == 1, "1 SIO store dropped and counted");
|
|
CHECK(s_mmio_calls == 3, "only GP0, I_STAT and I_MASK reached a device");
|
|
CHECK(s_irqs_raised == irqs0, "no interrupt raised in 10^6 frozen cycles");
|
|
CHECK(s_dev_cycles == dev0, "no device advanced");
|
|
CHECK(psx_cycle_count == cyc0 && psx_next_service_cycle == next0,
|
|
"guest clock and service deadline restored");
|
|
CHECK(i_stat == stat0 && i_mask == mask0, "I_STAT / I_MASK restored");
|
|
CHECK(s_irq_refresh > 0, "CAUSE IP2 recomputed after the restore");
|
|
timers_get_snapshot(tc1, tm1, tt1, ti1, tf1);
|
|
CHECK(memcmp(tc0, tc1, sizeof tc0) == 0 && memcmp(tm0, tm1, sizeof tm0) == 0 &&
|
|
memcmp(tt0, tt1, sizeof tt0) == 0 && memcmp(ti0, ti1, sizeof ti0) == 0 &&
|
|
memcmp(tf0, tf1, sizeof tf0) == 0, "timers exactly as before");
|
|
CHECK(memcmp(ram0, s_ram + 0x10000, sizeof ram0) == 0, "RAM restored");
|
|
CHECK(memcmp(spad0, s_spad, sizeof spad0) == 0, "scratchpad restored");
|
|
CHECK(cpu.gpr[8] == 0x55u, "CPU registers restored");
|
|
CHECK(!g_psx_render_pass_active, "time is live again");
|
|
|
|
/* Time is live again: the same cycles now move the timer and interrupt. */
|
|
psx_advance_cycles(2500u);
|
|
CHECK(s_irqs_raised > irqs0, "timer 2 interrupts again after the pass");
|
|
|
|
/* Status gates refuse without running the callback. */
|
|
s_fast_forward = 1;
|
|
CHECK(psx_mod_render_pass_status() == PSX_MOD_RENDER_PASS_FAST_FORWARD,
|
|
"fast-forward status");
|
|
CHECK(psx_mod_render_pass(&cpu, &pass, never_fn, NULL) == 0,
|
|
"no pass during fast-forward");
|
|
s_fast_forward = 0;
|
|
s_gl_status = PSX_MOD_RENDER_PASS_BACKEND;
|
|
CHECK(psx_mod_render_pass_status() == PSX_MOD_RENDER_PASS_BACKEND,
|
|
"backend status");
|
|
CHECK(psx_mod_render_pass(&cpu, &pass, never_fn, NULL) == 0,
|
|
"no pass while the backend declines");
|
|
s_gl_status = PSX_MOD_RENDER_PASS_READY;
|
|
CHECK(psx_mod_render_pass_status() == PSX_MOD_RENDER_PASS_READY, "ready again");
|
|
|
|
render_pass_get_stats(&st);
|
|
CHECK(st.status_refused == 2 && st.refused == 0 &&
|
|
st.last_failure.status == PSX_MOD_RENDER_PASS_BACKEND,
|
|
"pass refusal counters are separate from plan refusals");
|
|
s_begin_ok = 0;
|
|
CHECK(psx_mod_render_pass(&cpu, &pass, never_fn, NULL) == 0, "begin refusal");
|
|
render_pass_get_stats(&st);
|
|
CHECK(st.begin_refused == 1 && st.checkpoint_refused == 0 &&
|
|
strcmp(st.last_failure.reason, "capture_size") == 0 &&
|
|
st.last_failure.gl.requested_w == 320 && st.last_failure.gl.capture_w == 512,
|
|
"begin failure retains producer dimensions");
|
|
uint64_t failed_attempt = st.last_failure.attempt;
|
|
s_begin_ok = 1;
|
|
s_checkpoint_ok = 0;
|
|
CHECK(psx_mod_render_pass(&cpu, &pass, never_fn, NULL) == 0, "checkpoint refusal");
|
|
render_pass_get_stats(&st);
|
|
CHECK(st.checkpoint_refused == 1 && s_open_passes == 0 &&
|
|
strcmp(st.last_failure.reason, "checkpoint_gpu") == 0 &&
|
|
st.last_failure.attempt > failed_attempt,
|
|
"checkpoint failure closes transaction without calling guest code");
|
|
failed_attempt = st.last_failure.attempt;
|
|
s_checkpoint_ok = 1;
|
|
/* A refusal whose producer named no reason still reads as a failure. */
|
|
s_begin_ok = 0;
|
|
s_diag_null = 1;
|
|
CHECK(psx_mod_render_pass(&cpu, &pass, never_fn, NULL) == 0, "anonymous begin refusal");
|
|
render_pass_get_stats(&st);
|
|
CHECK(st.last_failure.reason && st.last_failure.reason[0] &&
|
|
st.last_failure.attempt > failed_attempt,
|
|
"refusal without a producer reason records a fallback reason");
|
|
failed_attempt = st.last_failure.attempt;
|
|
s_diag_null = 0;
|
|
s_begin_ok = 1;
|
|
CHECK(psx_mod_render_pass(&cpu, &pass, pass_fn, NULL) == 1, "success after refusals");
|
|
render_pass_get_stats(&st);
|
|
CHECK(st.last_failure.attempt == failed_attempt && st.pass_attempts == 7,
|
|
"success does not erase last failure");
|
|
render_pass_reset_session();
|
|
render_pass_get_stats(&st);
|
|
CHECK(!st.last_failure.reason && st.pass_attempts == 0,
|
|
"session reset clears failure diagnostics");
|
|
s_netplay_active = 1;
|
|
CHECK(psx_mod_render_pass_status() == PSX_MOD_RENDER_PASS_SESSION, "netplay requires explicit title opt-in");
|
|
psx_mod_set_render_pass_netplay(1);
|
|
CHECK(psx_mod_render_pass_status() == PSX_MOD_RENDER_PASS_READY, "forward netplay can render in the sandbox");
|
|
s_netplay_resim = 1;
|
|
CHECK(psx_mod_render_pass_status() == PSX_MOD_RENDER_PASS_SESSION, "rollback replay refuses extra rendering");
|
|
s_netplay_resim = 0;
|
|
g_ls_replay_active = 1;
|
|
CHECK(psx_mod_render_pass_status() == PSX_MOD_RENDER_PASS_SESSION, "lockstep replay refuses extra rendering");
|
|
g_ls_replay_active = 0;
|
|
render_pass_reset_session();
|
|
CHECK(!render_pass_netplay_enabled(), "netplay opt-in does not carry into the next session");
|
|
s_netplay_active = 0;
|
|
}
|
|
|
|
/* ---- 2b. 8 MiB main RAM live (psx.enhancement.8mb-ram) ------------------ */
|
|
static int s_retail_fold_seen;
|
|
static int pass8_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
|
|
(void)user; (void)alpha_q16;
|
|
cpu->gpr[9] = 0xBEEFu;
|
|
guest_store(0x80600000u, 0xCAFEBABEu, 4); /* unique above 2 MiB */
|
|
guest_store(0xA07FFFFCu, 0x01020304u, 4); /* last word of the map */
|
|
guest_store(0x80200010u, 0x55u, 1); /* no retail fold */
|
|
guest_store(0x80000100u, 0x66u, 1); /* low RAM as well */
|
|
return 1;
|
|
}
|
|
|
|
static int pass_retail_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
|
|
const uint8_t *before = (const uint8_t *)user;
|
|
(void)cpu; (void)alpha_q16;
|
|
guest_store(0x80600010u, 0x77u, 1); /* retail: folds onto 0x10 */
|
|
s_retail_fold_seen = s_ram[0x10] == 0x77 && s_ram[0x600010] == before[0x600010];
|
|
return 1;
|
|
}
|
|
|
|
static void test_ram_8mb(void) {
|
|
RenderPassStoreTarget t;
|
|
CPUState cpu;
|
|
PSXModRenderPass pass;
|
|
RenderPassStats st0, st1;
|
|
uint8_t *ram0 = (uint8_t *)malloc(RAM_8MB);
|
|
if (!ram0) { CHECK(0, "8 MiB test buffer"); return; }
|
|
|
|
/* Store policy with the expanded map: the window no longer mirrors. */
|
|
s_ram_live = RAM_8MB;
|
|
t = target();
|
|
memset(s_ram, 0, sizeof s_ram);
|
|
CHECK(render_pass_store_to(&t, 0x80600000u, 0x11223344u, 4) == -1 &&
|
|
s_ram[0x600000] == 0x44 && s_ram[0x600003] == 0x11 && s_ram[0] == 0,
|
|
"8 MiB: 0x80600000 is its own RAM, not a mirror of 0");
|
|
CHECK(render_pass_store_to(&t, 0x00201008u, 0x5Au, 1) == -1 &&
|
|
s_ram[0x201008] == 0x5A && s_ram[0x1008] == 0,
|
|
"8 MiB: 0x00201008 is not folded onto 0x1008");
|
|
CHECK(render_pass_store_to(&t, 0xA07FFFFEu, 0xBEEFu, 2) == -1 &&
|
|
s_ram[0x7FFFFE] == 0xEF && s_ram[0x7FFFFF] == 0xBE,
|
|
"8 MiB: the top of the window is RAM");
|
|
CHECK(render_pass_store_to(&t, 0x80800000u, 1u, 1) == RENDER_PASS_DROP_OTHER,
|
|
"8 MiB: past the decode window is not RAM");
|
|
|
|
/* A real pass: all 8 MiB come back, the upper 6 MiB included. */
|
|
for (uint32_t i = 0; i < RAM_8MB; i++)
|
|
s_ram[i] = (uint8_t)((i * 2654435761u) >> 24);
|
|
memcpy(ram0, s_ram, RAM_8MB);
|
|
memset(&cpu, 0, sizeof cpu);
|
|
memset(&pass, 0, sizeof pass);
|
|
pass.struct_size = sizeof pass;
|
|
pass.w = 320; pass.h = 240;
|
|
pass.alpha_q16 = 16384u;
|
|
render_pass_get_stats(&st0);
|
|
CHECK(psx_mod_render_pass(&cpu, &pass, pass8_fn, NULL) == 1,
|
|
"8 MiB: the pass ran and was kept");
|
|
render_pass_get_stats(&st1);
|
|
CHECK(st1.passes == st0.passes + 1, "8 MiB: one more pass");
|
|
CHECK(memcmp(ram0, s_ram, RAM_8MB) == 0,
|
|
"8 MiB: all of RAM restored, above 2 MiB too");
|
|
CHECK(cpu.gpr[9] == 0u, "8 MiB: CPU registers restored");
|
|
|
|
/* The next session without the mod (retail again): stores fold into the
|
|
* low 2 MiB and the restore covers them; the backing bytes above the
|
|
* live size are neither written nor needed. */
|
|
s_ram_live = RAM_2MB;
|
|
s_retail_fold_seen = 0;
|
|
CHECK(psx_mod_render_pass(&cpu, &pass, pass_retail_fn, ram0) == 1,
|
|
"retail after 8 MiB: the pass ran");
|
|
CHECK(s_retail_fold_seen, "retail after 8 MiB: 0x80600010 folded onto 0x10");
|
|
CHECK(memcmp(ram0, s_ram, RAM_8MB) == 0,
|
|
"retail after 8 MiB: RAM restored");
|
|
free(ram0);
|
|
}
|
|
|
|
/* ---- 3. VRAM journal ----------------------------------------------------- */
|
|
#define VW 1024
|
|
#define VH 512
|
|
static uint16_t s_vram[VW * VH], s_vram0[VW * VH];
|
|
|
|
static void paint(int x, int y, int w, int h, uint16_t v) {
|
|
for (int r = y; r < y + h; r++)
|
|
for (int c = x; c < x + w; c++) s_vram[r * VW + c] = v;
|
|
}
|
|
|
|
static void test_journal(void) {
|
|
RenderPassJournal j;
|
|
int x, y, w, h, n;
|
|
memset(&j, 0, sizeof j);
|
|
for (int i = 0; i < VW * VH; i++) s_vram[i] = (uint16_t)(i * 2654435761u >> 16);
|
|
memcpy(s_vram0, s_vram, sizeof s_vram);
|
|
|
|
/* The pass draws buffer 1 at (0, 240) 320x240. */
|
|
#define POLICY(X, Y, W, H, CAN) \
|
|
(x = (X), y = (Y), w = (W), h = (H), \
|
|
render_pass_vram_policy(&j, 0, 240, 320, 240, VW, VH, (CAN), &x, &y, &w, &h))
|
|
CHECK(POLICY(10, 250, 100, 100, 1) == RENDER_PASS_VRAM_ALLOW, "inside the rect: allowed");
|
|
paint(10, 250, 100, 100, 0x1111);
|
|
CHECK(POLICY(0, 0, 0, 5, 1) == RENDER_PASS_VRAM_ALLOW, "empty write: allowed");
|
|
|
|
CHECK(POLICY(640, 0, 64, 32, 1) == RENDER_PASS_VRAM_JOURNAL, "outside: journal");
|
|
CHECK(render_pass_journal_add(&j, s_vram, VW, x, y, w, h) == 0, "journaled as entry 0");
|
|
paint(640, 0, 64, 32, 0x2222);
|
|
CHECK(POLICY(650, 4, 16, 16, 1) == RENDER_PASS_VRAM_ALLOW, "covered: not journaled twice");
|
|
paint(650, 4, 16, 16, 0x3333);
|
|
|
|
CHECK(POLICY(300, 200, 40, 60, 1) == RENDER_PASS_VRAM_JOURNAL, "straddling the rect: journal");
|
|
CHECK(render_pass_journal_add(&j, s_vram, VW, x, y, w, h) == 1, "entry 1");
|
|
paint(300, 200, 40, 60, 0x4444);
|
|
|
|
/* Overlapping entry 0 partly: rolled back newest first. */
|
|
CHECK(POLICY(600, 16, 64, 32, 1) == RENDER_PASS_VRAM_JOURNAL, "overlapping: journal");
|
|
CHECK(render_pass_journal_add(&j, s_vram, VW, x, y, w, h) == 2, "entry 2");
|
|
paint(600, 16, 64, 32, 0x5555);
|
|
|
|
/* Clipped to VRAM. */
|
|
CHECK(POLICY(-8, -8, 24, 24, 1) == RENDER_PASS_VRAM_JOURNAL && x == 0 && y == 0 &&
|
|
w == 16 && h == 16, "negative origin clipped");
|
|
CHECK(render_pass_journal_add(&j, s_vram, VW, x, y, w, h) == 3, "entry 3");
|
|
paint(x, y, w, h, 0x6666);
|
|
CHECK(POLICY(1016, 508, 32, 32, 1) == RENDER_PASS_VRAM_JOURNAL && w == 8 && h == 4,
|
|
"far edge clipped");
|
|
CHECK(render_pass_journal_add(&j, s_vram, VW, x, y, w, h) == 4, "entry 4");
|
|
paint(x, y, w, h, 0x7777);
|
|
|
|
CHECK(POLICY(800, 300, 8, 8, 0) == RENDER_PASS_VRAM_REFUSE,
|
|
"a mode that cannot journal refuses");
|
|
|
|
n = j.n;
|
|
while (j.n < RENDER_PASS_JOURNAL_MAX) {
|
|
int k = j.n;
|
|
CHECK(POLICY(400 + 8 * k, 100, 4, 4, 1) == RENDER_PASS_VRAM_JOURNAL, "fill journal");
|
|
render_pass_journal_add(&j, s_vram, VW, x, y, w, h);
|
|
/* Policy can enlarge the BACKUP, never the actual GPU write. */
|
|
paint(400 + 8 * k, 100, 4, 4, (uint16_t)(0x8000 + k));
|
|
}
|
|
CHECK(j.n == RENDER_PASS_JOURNAL_MAX && n == 5, "journal full");
|
|
CHECK(j.x[j.n-1] == 0 && j.y[j.n-1] == 0 &&
|
|
j.w[j.n-1] == VW && j.h[j.n-1] == VH, "last slot protects all VRAM");
|
|
CHECK(POLICY(900, 400, 4, 4, 1) == RENDER_PASS_VRAM_ALLOW, "spill covers later writes");
|
|
paint(900, 400, 4, 4, 0x9999);
|
|
CHECK(POLICY(650, 4, 16, 16, 1) == RENDER_PASS_VRAM_ALLOW, "spill permits overlapping rewrites");
|
|
paint(650, 4, 16, 16, 0xAAAA);
|
|
CHECK(render_pass_journal_add(&j, s_vram, VW, 900, 400, 4, 4) == -1,
|
|
"add refuses past the limit");
|
|
|
|
/* End of pass: roll back, then the pass rect itself is restored by the
|
|
* renderer's own backup; outside it VRAM must equal the original. */
|
|
render_pass_journal_rollback(&j, s_vram, VW);
|
|
CHECK(j.n == 0, "journal emptied");
|
|
paint(0, 240, 320, 240, 0);
|
|
{
|
|
int bad = 0;
|
|
uint16_t *a = s_vram, *b = s_vram0;
|
|
for (int r = 0; r < VH; r++)
|
|
for (int c = 0; c < VW; c++) {
|
|
if (r >= 240 && r < 480 && c < 320) continue;
|
|
if (a[r * VW + c] != b[r * VW + c]) bad++;
|
|
}
|
|
CHECK(bad == 0, "every out-of-rect write rolled back exactly");
|
|
}
|
|
render_pass_journal_free(&j);
|
|
#undef POLICY
|
|
}
|
|
|
|
static int s_projective_scope, s_projective_refuse;
|
|
int gpu_render_projective_begin(void) {
|
|
if (s_projective_refuse) return 0;
|
|
++s_projective_scope;
|
|
return 1;
|
|
}
|
|
void gpu_render_projective_end(void) { s_projective_scope=0; }
|
|
void psx_mod_set_render_projective(int enabled) { (void)enabled; }
|
|
static int s_eye_calls, s_eye_decline = -1;
|
|
static int pair_draw(CPUState *cpu, void *user, uint32_t eye) {
|
|
(void)user;
|
|
CHECK(cpu->gpr[8] == 123 && s_ram[0x456] == 0x77,
|
|
"both eyes start from original CPU and RAM");
|
|
CHECK(psx_mod_render_stereo_status() == PSX_MOD_RENDER_PASS_BUSY,
|
|
"nested pair capture is busy");
|
|
s_eye_calls++;
|
|
CHECK(s_view[0] == 0, "view offset restored before each eye");
|
|
CHECK(psx_mod_render_view_offset(eye ? 24 : -24, 0, 0), "scoped view accepted");
|
|
PSXModRenderView v = {0}; v.struct_size = sizeof v;
|
|
v.rotation_q12[2] = 4096; v.rotation_q12[4] = 4096; v.rotation_q12[6] = -4096;
|
|
v.projection = 1; v.fx_q16 = 200 << 16; v.fy_q16 = 100 << 16;
|
|
v.translation[0] = eye ? 24 : -24;
|
|
CHECK(psx_mod_render_view(&v), "full scoped pose accepted");
|
|
v.precise=1; v.rotation[2]=1;v.rotation[4]=1;v.rotation[6]=-1;
|
|
v.translation_precise[0]=v.translation[0]+.125;
|
|
CHECK(psx_mod_render_view(&v), "fractional scoped pose accepted");
|
|
v.translation_precise[1]=NAN;
|
|
CHECK(!psx_mod_render_view(&v), "nonfinite precise pose rejected");
|
|
v.translation_precise[1]=0;
|
|
v.rotation[0]=2;
|
|
CHECK(!psx_mod_render_view(&v), "unbounded precise rotation rejected");
|
|
|
|
|
|
cpu->gpr[8] = eye + 1; s_ram[0x456] = (uint8_t)eye;
|
|
psx_advance_cycles(5000u + eye);
|
|
return (int)eye != s_eye_decline;
|
|
}
|
|
static void test_stereo(void) {
|
|
CPUState cpu = {0};
|
|
PSXModStereoFrame f = {sizeof f, 2, 0, 240, 320, 240};
|
|
RenderStereoStats stats;
|
|
uint64_t published, cycle;
|
|
render_pass_reset_session(); s_gl_status = 0; s_begin_ok = 1;
|
|
cpu.gpr[8] = 123; s_ram[0x456] = 0x77;
|
|
cycle = psx_cycle_count;
|
|
CHECK(psx_mod_render_stereo(&cpu, &f, pair_draw, NULL) == 1,
|
|
"complete pair publishes without a temporal plan");
|
|
render_stereo_get_stats(&stats);
|
|
CHECK(stats.pairs == 1 && s_eye_calls == 2 && s_open_passes == 0,
|
|
"two restored eye transactions form one pair");
|
|
CHECK(stats.eye_cycle[0] == cycle && stats.eye_cycle[1] == cycle &&
|
|
stats.eye_hash[0] == stats.eye_hash[1], "same checkpoint identifiers");
|
|
/* PR3: the pair captures one machine baseline and the second eye reuses it,
|
|
* yet both eyes still start from that same checkpoint (checked above). */
|
|
CHECK(stats.checkpoint_saves == 1 && stats.checkpoint_reuses == 1,
|
|
"one baseline per pair, reused by the second eye");
|
|
CHECK(stats.checkpoint_ram_bytes > 0,
|
|
"the captured baseline reports the RAM it copied");
|
|
CHECK(s_prec_open == 0 && s_projective_scope == 0, "precision tracking is armed and rolled back per eye");
|
|
CHECK(cpu.gpr[8] == 123 && s_ram[0x456] == 0x77 && psx_cycle_count == cycle,
|
|
"live state restored after the pair");
|
|
CHECK(s_view[0] == 0 && stats.eye_view[0][0] == -24 && stats.eye_view[1][0] == 24,
|
|
"producer offsets are recorded and do not escape");
|
|
CHECK(s_pose.struct_size == 0 && !s_pose.projection, "pose restored after pair");
|
|
CHECK(!psx_mod_render_view_offset(1, 0, 0), "view rejected outside render callback");
|
|
published = s_stereo_published;
|
|
s_projective_refuse = 1;
|
|
CHECK(!psx_mod_render_stereo(&cpu, &f, pair_draw, NULL) &&
|
|
s_stereo_published == published && s_stereo_mask == 0,
|
|
"projective PGXP refusal rolls the checkpoint back without publication");
|
|
CHECK(s_prec_open == 0 && s_projective_scope == 0 && cpu.gpr[8] == 123 &&
|
|
s_ram[0x456] == 0x77 && psx_cycle_count == cycle,
|
|
"projective refusal restores the complete live state");
|
|
s_projective_refuse = 0;
|
|
s_eye_decline = 1;
|
|
CHECK(!psx_mod_render_stereo(&cpu, &f, pair_draw, NULL) &&
|
|
s_stereo_published == published && s_stereo_mask == 0,
|
|
"right decline preserves previous complete pair");
|
|
s_eye_decline = -1; s_capture_fail_eye = 1;
|
|
CHECK(!psx_mod_render_stereo(&cpu, &f, pair_draw, NULL) &&
|
|
s_stereo_published == published && s_stereo_mask == 0,
|
|
"right texture capture failure cannot publish half a pair");
|
|
s_capture_fail_eye = -1;
|
|
CHECK(cpu.gpr[8] == 123 && s_ram[0x456] == 0x77 && psx_cycle_count == cycle,
|
|
"capture failure still restores live state");
|
|
f.period_vblanks = 0;
|
|
CHECK(!psx_mod_render_stereo(&cpu, &f, pair_draw, NULL), "invalid cadence refused");
|
|
|
|
/* The headset deadline the OpenXR layer pushes outranks the guest-VBlank
|
|
* fallback, and dropping it restores the fallback. */
|
|
f.period_vblanks = 2;
|
|
psx_mod_stereo_reset_budget();
|
|
psx_mod_stereo_set_budget_ms(11.1);
|
|
CHECK(psx_mod_render_stereo(&cpu, &f, pair_draw, NULL) == 1,
|
|
"pair renders under a headset deadline");
|
|
render_stereo_get_stats(&stats);
|
|
CHECK(stats.budget_source == 1 && stats.budget_ms > 11.0 && stats.budget_ms < 11.2,
|
|
"headset deadline drives the pair budget");
|
|
psx_mod_stereo_reset_budget();
|
|
CHECK(psx_mod_render_stereo(&cpu, &f, pair_draw, NULL) == 1,
|
|
"pair renders after the deadline is dropped");
|
|
render_stereo_get_stats(&stats);
|
|
CHECK(stats.budget_source == 0 && stats.budget_ms > 0.0,
|
|
"guest-VBlank fallback restored when no headset deadline is set");
|
|
|
|
/* Tracing is observation only: the same pair must publish the same id and
|
|
* reach the same guest-state hashes with capture on as with it off. */
|
|
{
|
|
uint64_t off_pub, off_hash[2], off_pair;
|
|
f.period_vblanks = 2;
|
|
psx_frame_trace_set_enabled(0);
|
|
render_pass_reset_session(); psx_frame_trace_set_enabled(0);
|
|
s_gl_status = 0; s_begin_ok = 1; s_eye_calls = 0;
|
|
cpu.gpr[8] = 123; s_ram[0x456] = 0x77;
|
|
CHECK(psx_mod_render_stereo(&cpu, &f, pair_draw, NULL) == 1, "pair with tracing off");
|
|
render_stereo_get_stats(&stats);
|
|
off_pub = s_stereo_published;
|
|
off_hash[0] = stats.eye_hash[0]; off_hash[1] = stats.eye_hash[1];
|
|
off_pair = stats.last_pair_id;
|
|
|
|
render_pass_reset_session();
|
|
psx_frame_trace_set_enabled(1); psx_frame_trace_reset();
|
|
s_gl_status = 0; s_begin_ok = 1; s_eye_calls = 0;
|
|
cpu.gpr[8] = 123; s_ram[0x456] = 0x77;
|
|
CHECK(psx_mod_render_stereo(&cpu, &f, pair_draw, NULL) == 1, "pair with tracing on");
|
|
render_stereo_get_stats(&stats);
|
|
CHECK(s_stereo_published == off_pub && stats.last_pair_id == off_pair &&
|
|
stats.eye_hash[0] == off_hash[0] && stats.eye_hash[1] == off_hash[1],
|
|
"tracing changes neither the published pair nor the guest state");
|
|
CHECK(psx_frame_trace_committed() > 0, "the traced pair was recorded");
|
|
psx_frame_trace_set_enabled(0); psx_frame_trace_reset();
|
|
}
|
|
|
|
/* PSX_STEREO_BASELINE=0 restores the pre-PR3 capture per eye, and a pair
|
|
* rendered that way must still start both eyes from one checkpoint. */
|
|
{
|
|
#ifdef _WIN32
|
|
_putenv("PSX_STEREO_BASELINE=0");
|
|
#else
|
|
setenv("PSX_STEREO_BASELINE", "0", 1);
|
|
#endif
|
|
f.period_vblanks = 2;
|
|
render_pass_reset_session(); s_gl_status = 0; s_begin_ok = 1; s_eye_calls = 0;
|
|
cpu.gpr[8] = 123; s_ram[0x456] = 0x77;
|
|
CHECK(psx_mod_render_stereo(&cpu, &f, pair_draw, NULL) == 1,
|
|
"pair renders with baseline reuse disabled");
|
|
render_stereo_get_stats(&stats);
|
|
CHECK(stats.checkpoint_saves == 2 && stats.checkpoint_reuses == 0,
|
|
"reuse off captures a baseline per eye");
|
|
CHECK(stats.eye_hash[0] == stats.eye_hash[1] && s_eye_calls == 2,
|
|
"capture-per-eye still starts both eyes from one checkpoint");
|
|
#ifdef _WIN32
|
|
_putenv("PSX_STEREO_BASELINE=1");
|
|
#else
|
|
setenv("PSX_STEREO_BASELINE", "1", 1);
|
|
#endif
|
|
render_pass_reset_session();
|
|
}
|
|
}
|
|
|
|
/* ---- 4. per-eye trace counters and guest sub-stage spans ---------------- */
|
|
/* The shape of the game hook's per-eye replay: bracket the guest calls, and let
|
|
* the runtime time them. See vr/psx_vr_stereo.c vr_draw_scene. */
|
|
static int s_traced_calls;
|
|
static int traced_draw(CPUState *cpu, void *user, uint32_t eye) {
|
|
(void)user;
|
|
s_traced_calls++;
|
|
s_prims_stub += 100u + 100u * eye; /* 100 prims, then 200 */
|
|
psx_mod_frame_trace_stage_begin(PSX_TRACE_GAME_TRAVERSE);
|
|
tick_bump(3000); /* 3 us, plus 1 per sample */
|
|
/* The visibility override runs inside the traversal it belongs to, so its
|
|
* span nests in that one: it must be timed on its own without truncating
|
|
* the span it is inside. */
|
|
psx_mod_frame_trace_stage_begin(PSX_TRACE_GAME_SELECT);
|
|
tick_bump(2000);
|
|
psx_mod_frame_trace_stage_end(PSX_TRACE_GAME_SELECT);
|
|
tick_bump(1000);
|
|
psx_mod_frame_trace_stage_end(PSX_TRACE_GAME_TRAVERSE);
|
|
/* An end for a stage that was never opened, and a begin that is never
|
|
* ended: neither may reach the record. */
|
|
psx_mod_frame_trace_stage_begin(PSX_TRACE_GAME_ENV);
|
|
tick_bump(1000);
|
|
psx_mod_frame_trace_stage_end(PSX_TRACE_GAME_SUBMIT);
|
|
psx_mod_frame_trace_stage_begin(PSX_TRACE_GAME_ICON);
|
|
tick_bump(1000);
|
|
psx_advance_cycles(4000u + 1000u * eye);
|
|
return 1;
|
|
}
|
|
|
|
static void test_trace_substages(void) {
|
|
CPUState cpu = {0};
|
|
PSXModStereoFrame f = {sizeof f, 2, 0, 240, 320, 240};
|
|
PSXFrameTraceRecord rec;
|
|
PSXModRenderPass rect = {sizeof rect, 0, 0, 0, 2, 2};
|
|
int first = 1, second = 2;
|
|
uint8_t pixels[32] = {0};
|
|
|
|
/* A session reset re-reads PSX_FRAME_TRACE from the env, so enabling the
|
|
* trace has to come after it (as it does in a real session). */
|
|
render_pass_reset_session();
|
|
s_gl_status = 0; s_begin_ok = 1; s_eye_decline = -1; s_eye_calls = 0;
|
|
s_prims_stub = 0; s_traced_calls = 0;
|
|
psx_frame_trace_set_enabled(1);
|
|
psx_frame_trace_reset();
|
|
|
|
CHECK(psx_mod_render_stereo(&cpu, &f, traced_draw, NULL) == 1,
|
|
"the traced pair rendered");
|
|
CHECK(s_traced_calls == 2, "both eyes replayed the guest draw");
|
|
CHECK(psx_frame_trace_read(&rec, 1) == 1, "the pair left one record");
|
|
CHECK(rec.prims_l == 100 && rec.prims_r == 200,
|
|
"each eye's own primitive delta is recorded");
|
|
CHECK(rec.prims == 200 && rec.textured_prims == 100,
|
|
"the pair-level count is still the last eye's");
|
|
CHECK(rec.eye_cycles_l >= 4000u && rec.eye_cycles_r >= 5000u &&
|
|
rec.eye_cycles_r > rec.eye_cycles_l,
|
|
"each eye's own guest cycles are recorded");
|
|
CHECK(rec.stages_us[PSX_TRACE_GAME_TRAVERSE] == 18,
|
|
"both eyes' guest spans are recorded (9 us per eye, nesting included)");
|
|
CHECK(rec.stages_us[PSX_TRACE_GAME_SELECT] == 6,
|
|
"a nested span is timed on its own (3 us per eye) and stays a subset");
|
|
CHECK(rec.stages_us[PSX_TRACE_GAME_ENV] == 0 &&
|
|
rec.stages_us[PSX_TRACE_GAME_SUBMIT] == 0,
|
|
"an end that does not match an open span records nothing");
|
|
CHECK(rec.stages_us[PSX_TRACE_GAME_ICON] == 0,
|
|
"an unpaired begin records nothing rather than leak into the next span");
|
|
|
|
/* A HUD capture's guest draws run outside a traced eye: the hook's spans
|
|
* must not land in the frame they belong to. */
|
|
psx_frame_trace_reset();
|
|
s_prims_stub = 0;
|
|
s_traced_calls = 0;
|
|
render_pass_reset_session(); s_gl_status = 0; s_begin_ok = 1;
|
|
psx_frame_trace_set_enabled(1);
|
|
CHECK(psx_frame_trace_open(), "a frame record is open across the capture");
|
|
CHECK(psx_mod_render_capture_rgba_pair(&cpu, &rect, traced_draw, &first,
|
|
&second, pixels, sizeof pixels),
|
|
"the capture rendered both mattes");
|
|
CHECK(s_traced_calls == 2 && psx_frame_trace_read(&rec, 1) == 0,
|
|
"the capture replayed the draw and left no record of its own");
|
|
psx_frame_trace_commit();
|
|
CHECK(psx_frame_trace_read(&rec, 1) == 1 &&
|
|
rec.stages_us[PSX_TRACE_GAME_TRAVERSE] == 0 &&
|
|
rec.prims_l == 0 && rec.prims_r == 0,
|
|
"a capture's guest draws write no per-eye or sub-stage slots");
|
|
|
|
/* Same with the trace off: the hook may bracket freely. */
|
|
psx_frame_trace_set_enabled(0);
|
|
psx_frame_trace_reset();
|
|
s_prims_stub = 0;
|
|
s_traced_calls = 0;
|
|
render_pass_reset_session(); s_gl_status = 0; s_begin_ok = 1;
|
|
CHECK(psx_mod_render_stereo(&cpu, &f, traced_draw, NULL) == 1,
|
|
"the pair renders with tracing off");
|
|
CHECK(s_traced_calls == 2 && psx_frame_trace_committed() == 0,
|
|
"nothing is recorded while tracing is off");
|
|
render_pass_reset_session();
|
|
}
|
|
|
|
static int local_draw(CPUState *cpu, void *user, uint32_t alpha) {
|
|
(void)user;
|
|
CHECK(alpha == 0, "a local view has no phase");
|
|
CHECK(s_ws_scope == 1 && psx_mod_local_view_scope() == 1,
|
|
"own-view presentation scope open while the local view draws");
|
|
cpu->gpr[8] = 0xBEEFu;
|
|
s_ram[0x456] = 0x11; /* the game's draw code writes RAM */
|
|
psx_advance_cycles(5000u);
|
|
return 1;
|
|
}
|
|
|
|
/* Netplay local view: forward netplay frames only; guest state restored
|
|
* exactly; a committed image cancels the crop request. */
|
|
static void test_local_view(void) {
|
|
CPUState cpu = {0};
|
|
PSXModRenderPass r;
|
|
RenderPassStats st;
|
|
uint64_t cycle;
|
|
render_pass_reset_session(); s_gl_status = 0; s_begin_ok = 1;
|
|
memset(&r, 0, sizeof r);
|
|
r.struct_size = sizeof r;
|
|
r.y = 240; r.w = 320; r.h = 240;
|
|
cpu.gpr[8] = 7; s_ram[0x456] = 0x77;
|
|
cycle = psx_cycle_count;
|
|
s_netplay = 0;
|
|
CHECK(psx_mod_render_local_view_status() == PSX_MOD_RENDER_PASS_SESSION,
|
|
"local view refused offline");
|
|
CHECK(!psx_mod_render_local_view(&cpu, &r, never_fn, NULL), "no offline call");
|
|
s_netplay = 1;
|
|
CHECK(psx_mod_render_pass_status() == PSX_MOD_RENDER_PASS_SESSION,
|
|
"temporal passes stay off in netplay");
|
|
CHECK(psx_mod_render_local_view_status() == PSX_MOD_RENDER_PASS_READY,
|
|
"local view ready in netplay");
|
|
s_resim = 1;
|
|
CHECK(psx_mod_render_local_view_status() == PSX_MOD_RENDER_PASS_SESSION &&
|
|
!psx_mod_render_local_view(&cpu, &r, never_fn, NULL),
|
|
"no local view while resimulating");
|
|
s_resim = 0;
|
|
s_view_shed = 1;
|
|
CHECK(psx_mod_render_local_view_status() == PSX_MOD_RENDER_PASS_FAST_FORWARD &&
|
|
!psx_mod_render_local_view(&cpu, &r, never_fn, NULL),
|
|
"a peer behind the match sheds its own view first");
|
|
s_view_shed = 0;
|
|
s_local_status = PSX_MOD_RENDER_PASS_BACKEND;
|
|
CHECK(psx_mod_render_local_view_status() == PSX_MOD_RENDER_PASS_BACKEND &&
|
|
!psx_mod_render_local_view(&cpu, &r, never_fn, NULL),
|
|
"no local view without a separate presenter surface");
|
|
s_local_status = PSX_MOD_RENDER_PASS_READY;
|
|
s_crop_clears = 0;
|
|
CHECK(psx_mod_render_local_view(&cpu, &r, local_draw, NULL) == 1,
|
|
"local view committed");
|
|
render_pass_get_stats(&st);
|
|
CHECK(st.local_views == 1 && s_local_kept == 1 && s_open_passes == 0 &&
|
|
st.passes == 0, "one kept local image, transaction closed");
|
|
CHECK(s_crop_clears == 1, "the committed image cancels the crop request");
|
|
CHECK(s_ws_scope == 0 && psx_mod_local_view_scope() == 0,
|
|
"own-view scope closed after the local view");
|
|
CHECK(cpu.gpr[8] == 7 && s_ram[0x456] == 0x77 && psx_cycle_count == cycle,
|
|
"guest state restored after the local view");
|
|
r.x = 900;
|
|
CHECK(!psx_mod_render_local_view(&cpu, &r, never_fn, NULL),
|
|
"rect outside VRAM refused");
|
|
s_netplay = 0;
|
|
}
|
|
|
|
static void test_texture_stream_journal(void) {
|
|
RenderPassJournal j = {0};
|
|
memcpy(s_vram, s_vram0, sizeof s_vram);
|
|
/* The Jersey draw OT alternates animated texture strips and CLUT rows.
|
|
* All 32 destinations must be backed up before the replay writes them. */
|
|
for (int i = 0; i < 32; ++i) {
|
|
int x = (i / 2) * 16, y = (i & 1) ? 492 : 64;
|
|
int w = (i & 1) ? 16 : 4, h = (i & 1) ? 1 : 64;
|
|
CHECK(render_pass_vram_policy(&j, 512, 0, 512, 240, VW, VH,
|
|
1, &x, &y, &w, &h) == RENDER_PASS_VRAM_JOURNAL,
|
|
"texture stream fits the bounded journal");
|
|
CHECK(render_pass_journal_add(&j, s_vram, VW, x, y, w, h) == i,
|
|
"texture destination backed up");
|
|
paint(x, y, w, h, (uint16_t)(0x9000 + i));
|
|
}
|
|
render_pass_journal_rollback(&j, s_vram, VW);
|
|
CHECK(!memcmp(s_vram, s_vram0, sizeof s_vram),
|
|
"texture stream and palettes restored exactly");
|
|
render_pass_journal_free(&j);
|
|
}
|
|
|
|
static void *s_dual_users[2];
|
|
static int s_dual_draws;
|
|
static int isolated_draw(CPUState *cpu,void *user,uint32_t alpha) {
|
|
CHECK(alpha==0,"isolated capture has no temporal phase");
|
|
if(s_dual_draws<2)s_dual_users[s_dual_draws]=user;
|
|
s_dual_draws++;
|
|
cpu->gpr[8]=0x1234;s_ram[0x456]=0x42;psx_advance_cycles(5000);
|
|
return 1;
|
|
}
|
|
static void test_isolated_capture(void) {
|
|
CPUState cpu={0};PSXModRenderPass rect={sizeof rect,0,0,0,2,2};
|
|
int first=1,second=2;
|
|
uint8_t pixels[32]={0};
|
|
render_pass_reset_session();s_gl_status=0;s_begin_ok=1;s_netplay=0;
|
|
cpu.gpr[8]=123;s_ram[0x456]=0x77;
|
|
uint64_t cycle=psx_cycle_count,published=s_stereo_published;
|
|
s_dual_draws=0;
|
|
CHECK(psx_mod_render_capture_rgba_pair(&cpu,&rect,isolated_draw,&first,&second,
|
|
pixels,sizeof pixels),
|
|
"isolated dual capture succeeds without publishing a stereo pair");
|
|
CHECK(s_dual_draws==2 && s_dual_users[0]==&first && s_dual_users[1]==&second,
|
|
"one transaction per matte, each with its own user");
|
|
CHECK(cpu.gpr[8]==123 && s_ram[0x456]==0x77 && psx_cycle_count==cycle,
|
|
"isolated capture restores CPU, RAM and the guest clock");
|
|
CHECK(s_stereo_published==published && s_stereo_mask==0 &&
|
|
pixels[0]==0x5A && pixels[31]==0x5A,
|
|
"both isolated planes copied and temporary staging discarded");
|
|
/* The two mattes draw from one machine state, so the second one reuses the
|
|
* checkpoint the first one captured, the way a pair's right eye does. */
|
|
RenderStereoStats stats;
|
|
render_stereo_get_stats(&stats);
|
|
CHECK(stats.checkpoint_saves==1 && stats.checkpoint_reuses==1,
|
|
"the two mattes of one capture share one checkpoint");
|
|
s_dual_draws=0;
|
|
CHECK(!psx_mod_render_capture_rgba_pair(&cpu,&rect,isolated_draw,&first,&second,
|
|
pixels,sizeof pixels-1),
|
|
"undersized pixel buffer refused");
|
|
CHECK(s_dual_draws==0 && !psx_mod_render_capture_rgba_pair(&cpu,&rect,isolated_draw,
|
|
&first,NULL,pixels,sizeof pixels),
|
|
"a missing second matte is refused before any draw");
|
|
/* The capture's shared checkpoint is scoped to the capture: the pair that
|
|
* renders later in the same frame must still capture its first eye. */
|
|
PSXModStereoFrame f={sizeof f,2,0,240,320,240};
|
|
render_pass_reset_session();s_gl_status=0;s_begin_ok=1;s_eye_decline=-1;s_eye_calls=0;
|
|
CHECK(psx_mod_render_stereo(&cpu,&f,pair_draw,NULL)==1,
|
|
"a pair still renders after a capture");
|
|
render_stereo_get_stats(&stats);
|
|
CHECK(stats.checkpoint_saves==1 && stats.checkpoint_reuses==1,
|
|
"the pair after a capture captures its own baseline");
|
|
}
|
|
int main(void) {
|
|
test_store_policy();
|
|
test_pass();
|
|
test_ram_8mb();
|
|
test_journal();
|
|
test_stereo();
|
|
test_isolated_capture();
|
|
test_trace_substages();
|
|
test_local_view();
|
|
test_texture_stream_journal();
|
|
CHECK(s_prec_open == 0, "precision checkpoints balanced");
|
|
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
|
|
return failures ? 1 : 0;
|
|
}
|