/* Render-pass watchdog abort (render_pass.c psx_mod_render_pass). * * A pass that runs away is cut off by a longjmp from the frozen clock's * watchdog, deep inside nested guest frames. That longjmp skips every exit * those frames would have run: generated functions' bb_defer cleanup, * overlay_loader_call_native's unit depth, the DMA kick's exec depth, the * interpreter's active/phase/precise flags and pending load, the native-shard * stack and its cycle flush hook. The live game must resume exactly as if * the pass never happened, so this test aborts a pass from such frames and * checks each of those values, the clock and RAM afterwards. The GPU, VRAM, * device snapshots and presenter are mocks; render_pass.c and psx_cycles.c * are the real ones. Build/run: ctest -R render_pass_abort_test */ #include "render_pass.h" #include #include #include #include #include "cpu_state.h" #include "dirty_ram_interp.h" #include "mod_plugins.h" #include "mod_runtime.h" #include "psx_cyc.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 two 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; /* ---- mock devices and memory ------------------------------------------ */ static uint8_t s_ram[2u * 1024u * 1024u]; static uint8_t s_spad[1024]; static uint8_t s_dma_regs[64]; static uint32_t s_csv = 1234; static uint16_t s_tc[3] = {1, 2, 3}, s_tt[3] = {4, 5, 6}; static uint32_t s_tm[3] = {7, 8, 9}, s_tf[3]; static int32_t s_ti[3]; uint8_t *memory_get_ram_ptr(void) { return s_ram; } uint32_t memory_get_ram_bytes(void) { return (uint32_t)sizeof s_ram; } 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) {} void timers_get_snapshot(uint16_t c[3], uint32_t m[3], uint16_t t[3], int32_t irq[3], uint32_t f[3]) { memcpy(c, s_tc, sizeof s_tc); memcpy(m, s_tm, sizeof s_tm); memcpy(t, s_tt, sizeof s_tt); memcpy(irq, s_ti, sizeof s_ti); memcpy(f, s_tf, sizeof s_tf); } void timers_set_snapshot(const uint16_t c[3], const uint32_t m[3], const uint16_t t[3], const int32_t irq[3], const uint32_t f[3]) { memcpy(s_tc, c, sizeof s_tc); memcpy(s_tm, m, sizeof s_tm); memcpy(s_tt, t, sizeof s_tt); memcpy(s_ti, irq, sizeof s_ti); memcpy(s_tf, f, sizeof s_tf); } 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; } int psx_netplay_active(void) { return 0; } int psx_netplay_is_resimulating(void) { return 0; } void psx_netplay_local_view_clear(void) {} int psx_netplay_local_view_shed(void) { return 0; } void gpu_ws_set_local_view_scope(int on) { (void)on; } int psx_selfcheck_resim_active(void) { return 0; } int psx_selfcheck_enabled(void) { return 0; } int psx_rewind_is_open(void) { return 0; } /* psx_cycles.c device side: nothing may advance inside a pass. */ 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 timers_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 timers_cycles_to_irq(uint32_t m) { (void)m; return UINT32_MAX; } 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) {} /* The interpreter's deferred load and the native-shard stack live in those * modules' statics; model them here the same way. */ 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 int s_ov_active_depth; static uint32_t s_ov_inprogress; void overlay_loader_native_nesting(int *d, uint32_t *ip) { *d = s_ov_active_depth; *ip = s_ov_inprogress; } void overlay_loader_set_native_nesting(int d, uint32_t ip) { s_ov_active_depth = d; s_ov_inprogress = ip; } /* Precision-tracking checkpoint (gte.cpp): every pass opens one and every * restore - including the watchdog landing - closes it. */ static int s_prec_open, s_prec_begins; void gte_precision_checkpoint_begin(void) { s_prec_open++; s_prec_begins++; } void gte_precision_checkpoint_rollback(void) { s_prec_open--; } /* 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; } /* GPU and presenter. */ uint64_t gpu_pass_state_hash(void) { return 42; } int gpu_pass_checkpoint_save(void) { return 1; } 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; } uint32_t gl_renderer_pass_unavailable(void) { return 0; } int psx_presentation_fast_forward(void) { return 0; } 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; static uint32_t s_stereo_mask; static uint64_t s_stereo_published; void gl_renderer_pass_begin_diag(GLRenderPassBeginDiag *out) { memset(out, 0, sizeof *out); } 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; 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) {} uint32_t g_psx_vblank_cycles = 564480u; uint32_t gl_renderer_local_view_unavailable(void) { return 0; } 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, keep); return 1; } uint32_t gl_renderer_stereo_unavailable(void) { return 0; } 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) s_stereo_mask |= 1u << eye; return 1; } void gl_renderer_stereo_stage_reset(void) { s_stereo_mask = 0; } int gl_renderer_stereo_stage_readback(uint32_t eye,uint32_t w,uint32_t h,uint8_t *rgba) { (void)eye;(void)w;(void)h;(void)rgba;return 0; } 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; } /* ---- an overlay shard's cycle shim (overlay_dispatch_preamble.c.inc) --- */ static uint32_t s_shard_pending; static void shard_flush(void) { uint32_t c = s_shard_pending; s_shard_pending = 0; if (c) psx_advance_cycles(c); } static void live_flush(void) {} /* ---- guest frames ------------------------------------------------------ */ typedef struct Frames { int depth; /* nested frames to open */ int run_away; /* innermost frame spins until the watchdog fires */ } Frames; /* One generated function that calls through a native overlay unit, a DMA * kick and the interpreter, each with the runtime's own entry/exit protocol, * then recurses. */ static void guest_frame(CPUState *cpu, const Frames *f, int level) { __attribute__((cleanup(psx_cyc_bb_defer_cleanup))) int guard = 1; int prev_unit = g_call_unit_depth, prev_active = g_dirty_interp_active; int prev_phase = g_exec_phase, prev_precise = g_precise_mode; int prev_ov_depth = s_ov_active_depth; uint32_t prev_ov_ip = s_ov_inprogress; void (*prev_flush)(void) = g_overlay_flush_pending_cycles; ModFunctionEntryContext prev_mod = s_mod_entry; const unsigned prev_host_call = s_host_call_depth; s_host_call_depth++; s_mod_entry.depth++; s_mod_entry.plugin = f; s_mod_entry.cpu = cpu; s_mod_entry.finished = !s_mod_entry.finished; (void)guard; psx_cyc_bb_defer_begin(); g_call_unit_depth = prev_unit + 1; /* overlay_loader_call_native */ s_ov_active_depth = prev_ov_depth + 1; /* overlay_loader_dispatch */ s_ov_inprogress = 0x80100000u + (uint32_t)level; g_overlay_flush_pending_cycles = shard_flush; g_exec_phase = 2; g_dma_exec_depth++; /* dma.c kick */ g_host_store_depth++; /* psx_host_write_* */ g_dma_cur_ch = 2; g_dirty_interp_active = 1; /* dirty_ram_dispatch */ g_precise_mode = 1; /* psx_run_precise */ s_ld.armed = 1; s_ld.rt = 8; s_ld.val = 0xDEAD0000u + (uint32_t)level; s_shard_pending = 777; /* shard-side charges */ s_ram[0x1000 + level] = 0xAA; /* the pass draws */ cpu->gpr[8] = 0x1234u + (uint32_t)level; if (level + 1 < f->depth) { guest_frame(cpu, f, level + 1); } else if (f->run_away) { /* A wait loop that never ends in frozen time: charges per * instruction, publishes at every branch edge. */ for (uint32_t i = 0; i < 100000000u; i++) { psx_cyc_charge(4); psx_cyc_bb_defer_flush(); } } s_mod_entry = prev_mod; s_host_call_depth = prev_host_call; s_ld.armed = 0; /* retired on interpreter exit */ g_precise_mode = prev_precise; g_dirty_interp_active = prev_active; g_dma_exec_depth--; g_host_store_depth--; g_dma_cur_ch = -1; g_exec_phase = prev_phase; shard_flush(); g_overlay_flush_pending_cycles = prev_flush; s_ov_inprogress = prev_ov_ip; s_ov_active_depth = prev_ov_depth; g_call_unit_depth = prev_unit; } /* The interpreter's guest-span state (dirty_ram_run_span): a span replays * guest frames like any other code, so a runaway inside one leaves it open * when the watchdog longjmps out. */ static uint32_t s_span_lo, s_span_hi; static const Frames *s_span_frames; 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)max; if (s_span_hi) return 0; s_span_lo = start & 0x1FFFFFFFu; s_span_hi = stop & 0x1FFFFFFFu; guest_frame(cpu, s_span_frames, 0); s_span_lo = s_span_hi = 0; return 1; } static int span_pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) { (void)alpha_q16; s_span_frames = (const Frames *)user; return psx_mod_run_guest_span(cpu, 0x800143FCu, 0x8001473Cu); } static int s_device_accesses; static int device_read_pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) { const uint32_t *access = (const uint32_t *)user; (void)alpha_q16; cpu->gpr[8] = 0xBADu; s_ram[0x1000] ^= 0xff; psx_advance_cycles(1000); CHECK(render_pass_mmio_read_allowed(0x1F801070u, 4), "checkpointed IRQ read allowed"); CHECK(render_pass_mmio_read_allowed(0x1F801110u, 2), "checkpointed timer read allowed"); CHECK(render_pass_mmio_read_allowed(0x1F801814u, 4), "checkpointed GPU read allowed"); if (render_pass_mmio_read_allowed(access[0], access[1])) s_device_accesses++; CHECK(0, "unsupported read must unwind before returning a fabricated value"); return 1; } static int pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) { (void)alpha_q16; guest_frame(cpu, (const Frames *)user, 0); return 1; } static int leaky_pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) { (void)cpu; (void)user; (void)alpha_q16; g_call_unit_depth++; /* forgets its exit */ return 1; } static int mod_leaky_pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) { (void)cpu; (void)user; (void)alpha_q16; s_mod_entry.depth++; s_mod_entry.plugin = user; return 1; } static int stereo_draw(struct CPUState *cpu, void *user, uint32_t eye) { Frames frames = {5, user && eye == PSX_MOD_EYE_RIGHT}; CHECK(psx_mod_render_view_offset(eye ? -24 : 24, 0, 0), "view setter inside eye"); 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"); guest_frame(cpu, &frames, 0); return 1; } typedef struct Live { ModFunctionEntryContext mod_entry; unsigned host_call_depth; int bb_defer, unit, dma, host, dma_ch, active, phase, precise, dispatch; int ov_depth; uint32_t ov_ip, batch, resume, span_lo, span_hi; void (*flush)(void); DirtyRamLoadDelay ld; uint64_t cycles, next_service, dev_cycles; } Live; static void snap(Live *l) { memset(l, 0, sizeof *l); l->mod_entry = s_mod_entry; l->host_call_depth = s_host_call_depth; l->bb_defer = g_psx_cyc_bb_defer; l->unit = g_call_unit_depth; l->dma = g_dma_exec_depth; l->host = g_host_store_depth; l->dma_ch = g_dma_cur_ch; l->active = g_dirty_interp_active; l->phase = g_exec_phase; l->precise = g_precise_mode; l->dispatch = g_psx_dispatch_depth; l->ov_depth = s_ov_active_depth; l->ov_ip = s_ov_inprogress; l->batch = g_psx_cyc_batch; l->resume = g_dirty_safe_resume_pc; l->flush = g_overlay_flush_pending_cycles; l->span_lo = s_span_lo; l->span_hi = s_span_hi; l->ld = s_ld; l->cycles = psx_cycle_count; l->next_service = psx_next_service_cycle; l->dev_cycles = s_dev_cycles; } static void check_live(const Live *a, const char *when) { Live b; char m[160]; snap(&b); #define SAME(field, what) do { \ snprintf(m, sizeof m, "%s: %s restored", when, what); \ CHECK(a->field == b.field, m); } while (0) SAME(mod_entry.depth, "mod callback depth"); SAME(mod_entry.plugin, "mod callback owner"); SAME(mod_entry.cpu, "mod callback CPU"); SAME(mod_entry.finished, "mod callback completion"); SAME(host_call_depth, "host adapter depth"); SAME(bb_defer, "g_psx_cyc_bb_defer"); SAME(unit, "g_call_unit_depth"); SAME(dma, "g_dma_exec_depth"); SAME(host, "g_host_store_depth"); SAME(dma_ch, "g_dma_cur_ch"); SAME(active, "g_dirty_interp_active"); SAME(phase, "g_exec_phase"); SAME(precise, "g_precise_mode"); SAME(dispatch, "g_psx_dispatch_depth"); SAME(ov_depth, "native-shard stack depth"); SAME(ov_ip, "native in-progress entry"); SAME(batch, "pending cycle batch"); SAME(resume, "interpreter resume latch"); SAME(flush, "shard cycle-flush hook"); SAME(span_lo, "guest span start"); SAME(span_hi, "guest span end"); SAME(ld.armed, "pending load (armed)"); SAME(ld.val, "pending load (value)"); SAME(cycles, "guest clock"); SAME(next_service, "service deadline"); SAME(dev_cycles, "device time (nothing advanced)"); #undef SAME } int main(void) { CPUState cpu; PSXModRenderPass pass; RenderPassStats st; Frames runaway = {5, 1}, clean = {5, 0}; Live live; uint8_t ram_before[64]; #ifdef _WIN32 _putenv_s("PSX_RENDER_PASS_VERIFY", "1"); #else setenv("PSX_RENDER_PASS_VERIFY", "1", 1); #endif memset(&cpu, 0, sizeof cpu); cpu.gpr[8] = 0x55u; psx_cycles_resync_after_restore(NULL); psx_advance_cycles(50000); /* The live context the pass interrupts: inside two generated functions * (the game's VSync entry hook), a native shard's flush hook installed, * the interpreter's resume latch and a pending load of its own. */ s_mod_entry.depth = 1; s_mod_entry.plugin = &cpu; s_mod_entry.cpu = &cpu; s_mod_entry.finished = 1; s_host_call_depth = 2; g_psx_cyc_bb_defer = 2; g_psx_cyc_batch = 9; g_psx_dispatch_depth = 3; g_exec_phase = 3; g_dirty_safe_resume_pc = 0x80012344u; g_overlay_flush_pending_cycles = live_flush; s_ld.armed = 1; s_ld.rt = 3; s_ld.val = 0x11112222u; s_ld.age = 1; s_ov_active_depth = 1; s_ov_inprogress = 0x80110000u; memcpy(ram_before, s_ram + 0x1000, sizeof ram_before); memset(&pass, 0, sizeof pass); pass.struct_size = sizeof pass; pass.w = 320; pass.h = 240; pass.alpha_q16 = 32768u; /* 1. A pass that runs away inside five nested frames. */ snap(&live); CHECK(psx_mod_render_pass(&cpu, &pass, pass_fn, &runaway) == 0, "a runaway pass is rolled back, not presented"); render_pass_get_stats(&st); CHECK(st.watchdog == 1 && st.aborted == 1 && st.passes == 0, "the watchdog cut it off once"); CHECK(st.nesting_repairs == 1, "the landing found and undid skipped exits"); CHECK(strstr(st.last_abort_detail, "mod entries +5") && strstr(st.last_abort_detail, "mod owner"), "TCP abort detail includes skipped mod exits"); CHECK(!st.disabled, "one fault does not disable passes"); CHECK(s_open_passes == 0 && s_kept == 0, "the presenter closed the pass, no image"); CHECK(!g_psx_render_pass_active, "time is live again"); check_live(&live, "after a watchdog abort"); CHECK(s_shard_pending == 0, "the shard's unpublished pass cycles went into the frozen clock"); CHECK(memcmp(ram_before, s_ram + 0x1000, sizeof ram_before) == 0, "RAM the pass wrote is restored"); CHECK(cpu.gpr[8] == 0x55u, "CPU registers are restored"); CHECK(st.verify_mismatch == 0, "PSX_RENDER_PASS_VERIFY: machine state identical after the abort"); /* 2. The same frames, returning normally, present an image. */ snap(&live); CHECK(psx_mod_render_pass(&cpu, &pass, pass_fn, &clean) == 1, "a pass that completes is presented"); render_pass_get_stats(&st); CHECK(st.passes == 1 && s_kept == 1, "one image kept"); CHECK(st.nesting_repairs == 1, "a normal return needs no repair"); check_live(&live, "after a normal pass"); CHECK(st.verify_mismatch == 0, "a balanced pass verifies clean"); /* One complete pair, then a right eye that aborts five guest frames deep. * The left staging eye never replaces the previous published pair. */ { PSXModStereoFrame frame = {sizeof frame, 2, 0, 0, 320, 240}; RenderStereoStats stereo; uint64_t kept; snap(&live); CHECK(psx_mod_render_stereo(&cpu, &frame, stereo_draw, NULL) == 1, "complete stereo pair publishes"); kept = s_stereo_published; CHECK(psx_mod_render_stereo(&cpu, &frame, stereo_draw, &runaway) == 0, "right-eye watchdog discards the entire pair"); render_stereo_get_stats(&stereo); render_pass_get_stats(&st); CHECK(s_stereo_published == kept && s_stereo_mask == 0, "right-eye abort retains previous pair and clears staging"); CHECK(stereo.last_failed_eye == 1 && stereo.retained_pair_id == kept, "failure producer records retained pair and right eye"); CHECK(s_view[0] == 0 && s_view[1] == 0 && s_view[2] == 0, "watchdog restores render-view ambient"); CHECK(!s_pose.struct_size && !s_pose.projection, "watchdog restores rotation/projection"); CHECK(st.verify_mismatch == 0 && !g_psx_render_pass_active, "aborted eye restores machine state and time"); check_live(&live, "after right-eye watchdog"); CHECK(psx_mod_render_stereo(&cpu, &frame, stereo_draw, NULL) == 1, "a later pair recovers after the watchdog"); } /* 3. Verify mode flags a pass that leaves the nesting unbalanced (the * restore would otherwise hide it), and still restores it. */ snap(&live); (void)psx_mod_render_pass(&cpu, &pass, leaky_pass_fn, NULL); render_pass_get_stats(&st); CHECK(st.verify_mismatch == 1, "an unbalanced pass is reported in verify mode"); check_live(&live, "after an unbalanced pass"); snap(&live); (void)psx_mod_render_pass(&cpu, &pass, mod_leaky_pass_fn, NULL); render_pass_get_stats(&st); CHECK(st.verify_mismatch == 2, "mod-only imbalance is reported"); check_live(&live, "after a mod-only unbalanced pass"); /* 4. A guest span runs only inside a pass. */ CHECK(psx_mod_run_guest_span(&cpu, 0x800143FCu, 0x8001473Cu) == 0, "a guest span outside a pass is refused"); render_pass_get_stats(&st); CHECK(st.spans == 0 && st.span_failures == 0, "a refused span counts nothing"); /* 5. A span that completes inside a pass is counted and closed. */ snap(&live); CHECK(psx_mod_render_pass(&cpu, &pass, span_pass_fn, &clean) == 1, "a pass that replays a span is presented"); render_pass_get_stats(&st); CHECK(st.spans == 1, "the span reached its stop"); check_live(&live, "after a pass with a span"); /* 6. A span that runs away: the watchdog's landing closes it, so later * dispatches of those PCs take their compiled bodies again. */ snap(&live); CHECK(psx_mod_render_pass(&cpu, &pass, span_pass_fn, &runaway) == 0, "a runaway span is rolled back"); render_pass_get_stats(&st); /* Cumulative: the plain abort, the stereo right-eye abort, and this span. */ CHECK(st.watchdog == 3, "the watchdog cut the span off"); CHECK(s_span_hi == 0 && s_span_lo == 0, "the abort closed the open span"); check_live(&live, "after a span watchdog abort"); CHECK(s_prec_begins > 0 && s_prec_open == 0, "precision shadows were checkpointed and rolled back on every pass, aborts included"); /* Reads can consume a FIFO or ACK while time is frozen. Refuse before * the device access and restore the same live state as a watchdog abort. */ { const uint32_t reads[][2] = {{0x1F801040u,1}, {0x1F801044u,2}, {0x1F801800u,1}, {0x1F801820u,4}, {0x1F801DA8u,2}}; render_pass_reset_session(); for (unsigned i=0;i