diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 18545855..2cc5aafa 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -1074,6 +1074,19 @@ if(BUILD_TESTING) SKIP_REGULAR_EXPRESSION "^SKIP:") endif() + # #636 / PR #660: restore-vs-live IRQ delivery at the restore edge, over + # the real scheduler (traps.c) and IRQ delivery (interrupts.c). Compiled + # game, BIOS ROM, CPS continuation and interpreted resumes; no pending + # IRQ; I_MASK/SR gating; same-edge dedup; null-PC recovery. + add_executable(restore_edge_irq_replay_test + tests/test_restore_edge_irq_replay.c + tests/restore_edge_irq_stubs.c + src/traps.c + src/interrupts.c) + target_include_directories(restore_edge_irq_replay_test PRIVATE include src) + add_test(NAME restore_edge_irq_replay_test COMMAND restore_edge_irq_replay_test) + set_tests_properties(restore_edge_irq_replay_test PROPERTIES TIMEOUT 60) + add_executable(dma_gpu_linked_list_timing_test tests/test_dma_gpu_linked_list_timing.c src/dma_gpu_ll.c) diff --git a/runtime/tests/restore_edge_irq_stubs.c b/runtime/tests/restore_edge_irq_stubs.c new file mode 100644 index 00000000..1f1007c8 --- /dev/null +++ b/runtime/tests/restore_edge_irq_stubs.c @@ -0,0 +1,128 @@ +/* Link fixture for restore_edge_irq_replay_test: the device, debug, mod and + * host collaborators of the REAL traps.c (scheduler) and interrupts.c (IRQ + * delivery). Everything the scheduler/IRQ decision reads that matters to the + * test (cycle clock, I_STAT/I_MASK, guest RAM, the null-PC recovery hook) is + * owned by the harness through the hooks declared at the bottom; the rest is + * inert diagnostics or devices with no pending work. */ +#include +#include +#include +#include + +struct CPUState; + +/* ---- Guest state owned by the fixture (memory.c / psx_cycles.c in the runtime) ---- */ +uint8_t g_test_ram[0x200000]; +uint64_t psx_cycle_count = 0; +uint64_t psx_next_service_cycle = 0; +uint32_t i_stat = 0; +uint32_t i_mask = 0; +uint32_t g_psx_ram_size = 0x200000u; +uint32_t g_psx_vblank_cycles = 564480u; + +uint64_t psx_get_cycle_count(void) { return psx_cycle_count; } +void psx_advance_cycles_slow(uint32_t cycles) { psx_cycle_count += cycles; } + +uint32_t psx_read_word(uint32_t addr) +{ + uint32_t v; + memcpy(&v, &g_test_ram[addr & 0x1FFFFCu], 4); + return v; +} +void psx_host_write_word(uint32_t addr, uint32_t value) +{ + memcpy(&g_test_ram[addr & 0x1FFFFCu], &value, 4); +} +uint8_t *memory_get_ram_ptr(void) { return g_test_ram; } + +/* Null-PC recovery (psx_netplay_rb.c): the harness decides. */ +int (*g_test_recover_null_pc)(struct CPUState *, uint32_t *) = NULL; +int psx_netplay_rb_recover_null_pc(struct CPUState *c, uint32_t *out_pc) +{ + return g_test_recover_null_pc ? g_test_recover_null_pc(c, out_pc) : 0; +} + +/* ---- Runtime globals the two units reference ---- */ +uint64_t g_async_rfe_fire_count = 0; +uint32_t g_async_rfe_resume_pc = 0; +int g_call_unit_depth = 0; +int g_cosim_dirty_pump_site = 0; +int g_dirty_interp_active = 0; +uint32_t g_dirty_safe_resume_pc = 0; +int g_dma_exec_depth = 0; +int g_event_step_conservative = 0; +int g_exec_phase = 0; +int g_idle_skip_enabled = 0; +int g_ls_replay_active = 0; +int g_ls_suppress_record = 0; +uint32_t g_psx_cyc_batch = 0; +uint32_t g_psx_cyc_batch_limit = 0; +int g_psx_cyc_bb_defer = 0; +uint32_t *g_psx_cyc_local_acc = NULL; +int g_psx_dispatch_depth = 0; +int g_psx_guest_time_frozen = 0; +uint32_t g_sentinel_reach_async = 0; +uint64_t g_sentinel_reach_traps = 0; +volatile int g_sio_timing_active = 0; +int psx_in_device_service = 0; +uint64_t s_frame_count = 0; +struct CPUState *debug_cpu_ptr = NULL; + +/* ---- Devices: nothing scheduled, nothing pending ---- */ +uint32_t cdrom_cycles_to_irq(uint32_t m) { (void)m; return 0x7FFFFFFFu; } +uint32_t dma_cycles_to_irq(uint32_t m) { (void)m; return 0x7FFFFFFFu; } +uint32_t sio_cycles_to_irq(uint32_t m) { (void)m; return 0x7FFFFFFFu; } +uint32_t timers_cycles_to_irq(uint32_t m){ (void)m; return 0x7FFFFFFFu; } +void cdrom_tick(void) {} +void timers_tick(int c) { (void)c; } +void sio_tick_quantum(void) {} +uint32_t sio_get_seq(void) { return 0; } +int sio_card_protocol_active(void) { return 0; } +void gpu_vblank_tick(void) {} +void gpu_vblank_flush_present(void) {} +void gpu_vblank_clear_deferred_present(void) {} +void psx_guest_cycle_scale_vblank(void) {} +void psx_devices_service_to_now(void) {} +int dma_cdrom_transfer_active(void) { return 0; } +uint16_t spu_ctrl_read(void) { return 0; } +int psx_idle_skip_is_enabled(void) { return 0; } +void psx_idle_note_check(struct CPUState *c, uint32_t pc) { (void)c; (void)pc; } + +/* ---- Host features that are off in this fixture ---- */ +int psx_netplay_active(void) { return 0; } +int psx_selfcheck_enabled(void) { return 0; } +int psx_mod_function_entry_active(void) { return 0; } +void savestate_poll(struct CPUState *c, uint32_t pc) { (void)c; (void)pc; } +void psx_netplay_poll_snap(struct CPUState *c, uint32_t pc) { (void)c; (void)pc; } +void psx_selfcheck_poll(struct CPUState *c, uint32_t pc) { (void)c; (void)pc; } +void psx_rewind_poll(struct CPUState *c, uint32_t pc) { (void)c; (void)pc; } +void mod_runtime_function_entry_context_save(void *ctx) { (void)ctx; } +void mod_runtime_function_entry_context_restore(const void *ctx) { (void)ctx; } +int overlay_loader_shadow_native_thread_switch_bail(void) { return 0; } +void overlay_loader_shadow_escape_fixup(uint64_t epoch) { (void)epoch; } +void overlay_loader_shadow_scheduler_escape_fixup(void) {} +void ls_note_exception_entry(void) {} + +/* Host fibers: only the LLE scheduler uses them; the HLE scheduler is default. */ +void *psx_fiber_current(void) { return NULL; } +void *psx_fiber_convert_thread(void) { return NULL; } +void *psx_fiber_create(void *fn, void *arg) { (void)fn; (void)arg; return NULL; } +void psx_fiber_destroy(void *f) { (void)f; } +void psx_fiber_switch(void *f) { (void)f; } + +/* ---- Diagnostics ---- */ +void debug_server_poll(void) {} +void debug_server_log_restore_event(uint32_t k, uint32_t a, uint32_t b) { (void)k; (void)a; (void)b; } +void debug_server_log_thread_event(int k, struct CPUState *c, uint32_t a, uint32_t b, uint32_t d) +{ (void)k; (void)c; (void)a; (void)b; (void)d; } +void ds_note_thread_switch(void) {} +void device_trace_note(uint32_t s, uint32_t d) { (void)s; (void)d; } +void event_ring_record(uint16_t k, uint8_t d) { (void)k; (void)d; } +void event_ring_record_aux(uint16_t k, uint8_t d, uint32_t a) { (void)k; (void)d; (void)a; } +int parity_trace_is_armed(void) { return 0; } +void parity_trace_record(void) {} +uint32_t crash_trace_dispatch_ring_get(int i) { (void)i; return 0; } +uint64_t crash_trace_dispatch_seq_get(void) { return 0; } +void psx_unknown_dispatch_record(uint32_t a, uint32_t p) { (void)a; (void)p; } +void psx_crash_trace_dump(const char *r, void *i) { (void)i; fprintf(stderr, "crash: %s\n", r); } +void psx_fatal_halt(const char *r) { fprintf(stderr, "FATAL: %s\n", r); exit(2); } diff --git a/runtime/tests/test_restore_edge_irq_replay.c b/runtime/tests/test_restore_edge_irq_replay.c new file mode 100644 index 00000000..411504b8 --- /dev/null +++ b/runtime/tests/test_restore_edge_irq_replay.c @@ -0,0 +1,516 @@ +/* restore_edge_irq_replay_test (#636, PR #660 review). + * + * Executable restore-vs-live regression over the REAL scheduler (traps.c: + * psx_scheduler_run / psx_scheduler_resume_at) and the REAL IRQ delivery + * (interrupts.c: psx_check_interrupts*, interrupts_resync_after_restore). + * Only psx_dispatch is the harness: a small deterministic guest whose blocks + * follow the generated-code contract + * + * compiled game entry psx_check_interrupts_dispatch_entry(pc), body, + * edge check psx_check_interrupts_at(next) + * BIOS ROM / CPS cont. body, edge check psx_check_interrupts_at(next) + * (no dispatch-entry check) + * interpreted (dirty) body, g_dirty_safe_resume_pc = next, + * psx_check_interrupts + * + * and an exception handler behind the RAM vector (LUI/ADDIU k0) that records + * delivery cycle, EPC, CAUSE, SR and the interrupted registers, saves EPC and + * registers into the current TCB (PCB at 0x108), acks I_STAT and RFEs. + * + * Each case runs LIVE from cold, capturing state at one block edge (just + * before that edge's IRQ check, with the edge's resume PC — what a poll + * inside psx_check_interrupts captures), then REPLAYS by restoring that + * snapshot from inside guest execution the way savestate/rewind/selfcheck/ + * rollback do (interrupts_resync_after_restore + psx_scheduler_resume_at). + * Every delivery (cycle, EPC, CAUSE, SR, I_STAT, interrupted GPRs, TCB) and + * every block (pc, cycle, GPR hash), plus the final CPU/RAM/INTC state, must + * match the live run from the snapshot on. */ +#include "cpu_state.h" +#include "interrupts.h" +#include "psx_scheduler.h" + +#include +#include +#include +#include + +extern uint8_t g_test_ram[0x200000]; +extern uint64_t psx_cycle_count; +extern uint32_t i_stat, i_mask; +extern uint32_t g_dirty_safe_resume_pc; +extern int (*g_test_recover_null_pc)(CPUState *, uint32_t *); +extern uint32_t psx_read_word(uint32_t addr); +extern void psx_host_write_word(uint32_t addr, uint32_t value); + +#define SR_IEC 0x00000001u +#define SR_IM2 0x00000400u +#define C0_SR 12 +#define C0_CAUSE 13 +#define C0_EPC 14 + +#define HANDLER_PC 0x80001000u +#define LOADER_PC 0x80003000u +#define PCB_ADDR 0x80000200u +#define TCB_ADDR 0x80000300u +#define STEP_ADDR 0x80060000u /* guest step counter (RAM, snapshotted) */ +#define VBCNT_ADDR 0x80060004u /* handler-side VBlank counter */ +#define SCRATCH 0x80050000u + +enum kind { K_GAME, K_ROM, K_CONT, K_INTERP }; + +typedef struct { + enum kind kind; + uint32_t base; /* first block PC */ + uint32_t nblocks; /* blocks per loop (PC = base + 0x20*i) */ + uint32_t cost; /* cycles per block body */ + uint32_t steps; /* blocks to run before the guest exits */ + uint32_t snap_step; /* capture at the edge after this many blocks */ + uint64_t raise_cycle; /* device raises I_STAT.VBlank at this cycle */ + uint32_t imask0, sr0; + /* guest unmask/enable at this step (0 = never): sets I_MASK/SR */ + uint32_t enable_step, enable_imask, enable_sr; + int handler_zero_cost; /* handler takes no guest cycles (dedup case) */ +} Program; + +typedef struct { + CPUState cpu; + uint8_t ram[0x200000]; + uint32_t istat, imask; + uint64_t cycle; + int raised; + uint32_t resume_pc; +} Snap; + +/* Event log ----------------------------------------------------------- */ +typedef struct { + int is_irq; + uint32_t pc; /* block pc, or EPC for an IRQ */ + uint64_t cycle; + uint32_t cause, sr, istat; + uint32_t gpr_hash; + uint32_t tcb_epc, tcb_r8, tcb_r9, tcb_ra; +} Ev; + +#define LOG_CAP 4096 +typedef struct { Ev ev[LOG_CAP]; int n; } Log; + +static Log g_live, g_replay, *g_log; +static int g_logging; +static const Program *g_prog; +static int g_raised; +static Snap *g_snap; +static int g_snap_taken; +static int g_mode_replay; +static int g_entry_prechecked; /* checked_at_current_cycle seen at first replay entry */ +static int g_first_replay_entry; +static uint32_t g_recover_pc; +static int g_recover_used; +static int g_recover_log_idx; +static int g_irq_count; + +static uint32_t rd(uint32_t a) { return psx_read_word(a); } +static void wr(uint32_t a, uint32_t v) { psx_host_write_word(a, v); } + +static uint32_t hash_gprs(const CPUState *c) +{ + uint32_t h = 2166136261u; + for (int i = 0; i < 32; i++) { h ^= c->gpr[i]; h *= 16777619u; } + h ^= c->hi; h *= 16777619u; h ^= c->lo; h *= 16777619u; + return h; +} + +static void log_ev(const Ev *e) +{ + if (!g_logging) return; + if (g_log->n >= LOG_CAP) { fprintf(stderr, "log overflow\n"); exit(2); } + g_log->ev[g_log->n++] = *e; +} + +static uint32_t block_pc(uint32_t i) { return g_prog->base + 0x20u * (i % g_prog->nblocks); } + +static void take_snapshot(CPUState *cpu, uint32_t resume_pc) +{ + memcpy(&g_snap->cpu, cpu, sizeof(*cpu)); + memcpy(g_snap->ram, g_test_ram, sizeof(g_test_ram)); + g_snap->istat = i_stat; + g_snap->imask = i_mask; + g_snap->cycle = psx_cycle_count; + g_snap->raised = g_raised; + g_snap->resume_pc = resume_pc; + g_snap_taken = 1; + g_logging = 1; /* compare everything after the edge */ +} + +/* The block edge: generated code's psx_check_interrupts_at(next) or the + * dirty interpreter's pump. The capture sits where a poll inside the check + * captures: same edge, same resume PC, before delivery is evaluated. */ +static void edge_check(CPUState *cpu, uint32_t next, uint32_t step) +{ + if (!g_mode_replay && !g_snap_taken && step == g_prog->snap_step) + take_snapshot(cpu, next); + if (g_prog->kind == K_INTERP) { + g_dirty_safe_resume_pc = next; + psx_check_interrupts(cpu); + g_dirty_safe_resume_pc = 0; + } else { + psx_check_interrupts_at(cpu, next); + } +} + +static void run_handler(CPUState *cpu) +{ + Ev e = {0}; + e.is_irq = 1; + e.pc = cpu->cop0[C0_EPC]; + e.cycle = psx_cycle_count; + e.cause = cpu->cop0[C0_CAUSE]; + e.sr = cpu->cop0[C0_SR]; + e.istat = i_stat & i_mask; + e.gpr_hash = hash_gprs(cpu); + /* BIOS-style save of the interrupted context into the current TCB. */ + uint32_t tcb = rd(rd(0x108u) & 0x1FFFFFFFu); + wr(tcb + 0x88u, cpu->cop0[C0_EPC]); + wr(tcb + 0x20u, cpu->gpr[8]); + wr(tcb + 0x24u, cpu->gpr[9]); + wr(tcb + 0x7Cu, cpu->gpr[31]); + e.tcb_epc = rd(tcb + 0x88u); + e.tcb_r8 = rd(tcb + 0x20u); + e.tcb_r9 = rd(tcb + 0x24u); + e.tcb_ra = rd(tcb + 0x7Cu); + log_ev(&e); + g_irq_count++; + wr(VBCNT_ADDR, rd(VBCNT_ADDR) + 1u); + i_stat = 0; /* ack */ + cpu->gpr[8] = 0xDEAD0008u; /* handler clobbers */ + cpu->gpr[27] = 0xDEAD001Bu; + if (!g_prog->handler_zero_cost) psx_cycle_count += 37; + /* RFE: pop the SR stack, return to EPC. */ + uint32_t sr = cpu->cop0[C0_SR]; + cpu->cop0[C0_SR] = (sr & ~0x0Fu) | ((sr >> 2) & 0x0Fu); + cpu->pc = cpu->cop0[C0_EPC]; +} + +static void restore_snapshot_and_resume(CPUState *cpu) +{ + memcpy(cpu, &g_snap->cpu, sizeof(*cpu)); + memcpy(g_test_ram, g_snap->ram, sizeof(g_test_ram)); + i_stat = g_snap->istat; + i_mask = g_snap->imask; + psx_cycle_count = g_snap->cycle; + g_raised = g_snap->raised; + g_logging = 1; + g_first_replay_entry = 1; + interrupts_resync_after_restore(); + cpu->pc = g_snap->resume_pc; + psx_scheduler_resume_at(g_snap->resume_pc); /* never returns */ +} + +static int block_index(uint32_t pc, uint32_t *idx) +{ + if (pc < g_prog->base) return 0; + uint32_t off = pc - g_prog->base; + if ((off & 0x1Fu) || off / 0x20u >= g_prog->nblocks) return 0; + *idx = off / 0x20u; + return 1; +} + +void psx_dispatch(CPUState *cpu, uint32_t pc) +{ + for (;;) { + cpu->pc = pc; + if (pc == 0u) return; + if (pc == HANDLER_PC) { run_handler(cpu); return; } + if (pc == LOADER_PC) { restore_snapshot_and_resume(cpu); return; } + uint32_t idx; + if (!block_index(pc, &idx)) { + fprintf(stderr, "harness: dispatch to unknown pc %08X\n", pc); + exit(2); + } + if (g_prog->kind == K_GAME) { + if (g_first_replay_entry) + g_entry_prechecked = psx_interrupts_checked_at_current_cycle(pc); + psx_check_interrupts_dispatch_entry(cpu, pc); + } + g_first_replay_entry = 0; + /* Block body: deterministic function of the guest state. */ + uint32_t step = rd(STEP_ADDR) + 1u; + wr(STEP_ADDR, step); + cpu->gpr[8] = cpu->gpr[8] * 31u + pc + (uint32_t)psx_cycle_count; + cpu->gpr[9] ^= cpu->gpr[8] + step; + cpu->gpr[31] = pc + 8u; + wr(SCRATCH + (step & 15u) * 4u, cpu->gpr[8]); + if (g_prog->enable_step && step == g_prog->enable_step) { + i_mask |= g_prog->enable_imask; + cpu->cop0[C0_SR] |= g_prog->enable_sr; + } + psx_cycle_count += g_prog->cost; + if (!g_raised && psx_cycle_count >= g_prog->raise_cycle) { + i_stat |= 1u; /* VBlank */ + g_raised = 1; + } + Ev e = {0}; + e.pc = pc; e.cycle = psx_cycle_count; e.gpr_hash = hash_gprs(cpu); + e.istat = i_stat; e.sr = cpu->cop0[C0_SR]; + log_ev(&e); + if (step == g_prog->steps || step == g_prog->steps + 10u) { /* guest exit */ + cpu->pc = 0; + return; + } + uint32_t next = block_pc(idx + 1u); + edge_check(cpu, next, step); + pc = next; + } +} + +static void reset_machine(CPUState *cpu, const Program *p) +{ + memset(cpu, 0, sizeof(*cpu)); + memset(g_test_ram, 0, sizeof(g_test_ram)); + cpu->read_word = psx_read_word; + cpu->write_word = psx_host_write_word; + /* RAM exception vector: lui k0,0x8000 ; addiu k0,k0,0x1000 */ + wr(0x80000080u, 0x3C1A8000u); + wr(0x80000084u, 0x275A1000u); + wr(0x108u, PCB_ADDR); + wr(PCB_ADDR, TCB_ADDR); + cpu->cop0[C0_SR] = p->sr0; + for (int i = 1; i < 32; i++) cpu->gpr[i] = 0x01010101u * (uint32_t)i; + psx_cycle_count = 1000; + i_stat = 0; + i_mask = p->imask0; + g_raised = 0; + g_dirty_safe_resume_pc = 0; + g_first_replay_entry = 0; + interrupts_resync_after_restore(); + psx_scheduler_top_level_resume_clear(); +} + +typedef struct { + CPUState cpu; + uint32_t istat, imask; + uint64_t cycle; +} Final; + +static void capture_final(Final *f, const CPUState *c) +{ + memcpy(&f->cpu, c, sizeof(*c)); + f->istat = i_stat; f->imask = i_mask; f->cycle = psx_cycle_count; +} + +static Snap g_snap_storage; +static uint8_t g_live_ram[0x200000]; + +static int compare_runs(const char *name, const Final *lf, const Final *rf) +{ + int n = g_live.n < g_replay.n ? g_live.n : g_replay.n; + for (int i = 0; i < n; i++) { + const Ev *a = &g_live.ev[i], *b = &g_replay.ev[i]; + if (memcmp(a, b, sizeof(*a)) != 0) { + fprintf(stderr, + "%s: event %d differs: live %s pc=%08X cyc=%llu sr=%08X tcb_epc=%08X | " + "replay %s pc=%08X cyc=%llu sr=%08X tcb_epc=%08X\n", name, i, + a->is_irq ? "IRQ" : "blk", a->pc, (unsigned long long)a->cycle, a->sr, a->tcb_epc, + b->is_irq ? "IRQ" : "blk", b->pc, (unsigned long long)b->cycle, b->sr, b->tcb_epc); + return 1; + } + } + if (g_live.n != g_replay.n) { + fprintf(stderr, "%s: event count live=%d replay=%d\n", name, g_live.n, g_replay.n); + return 1; + } + if (memcmp(lf->cpu.gpr, rf->cpu.gpr, sizeof(lf->cpu.gpr)) || + memcmp(lf->cpu.cop0, rf->cpu.cop0, sizeof(lf->cpu.cop0)) || + lf->cpu.hi != rf->cpu.hi || lf->cpu.lo != rf->cpu.lo || + lf->istat != rf->istat || lf->imask != rf->imask || lf->cycle != rf->cycle) { + fprintf(stderr, "%s: final CPU/INTC state differs (cycle live=%llu replay=%llu)\n", + name, (unsigned long long)lf->cycle, (unsigned long long)rf->cycle); + return 1; + } + if (memcmp(g_live_ram, g_test_ram, sizeof(g_test_ram)) != 0) { + fprintf(stderr, "%s: final RAM differs\n", name); + return 1; + } + return 0; +} + +static int count_irqs(const Log *l) +{ + int n = 0; + for (int i = 0; i < l->n; i++) n += l->ev[i].is_irq; + return n; +} + +/* Live run from cold, then replay from the captured edge. Returns 0 on match. */ +static int live_vs_replay(const char *name, const Program *p, int expect_irq_at_edge) +{ + static CPUState cpu; + Final lf, rf; + g_prog = p; + g_snap = &g_snap_storage; + + /* LIVE */ + reset_machine(&cpu, p); + g_mode_replay = 0; g_snap_taken = 0; g_logging = 0; + g_live.n = 0; g_log = &g_live; + cpu.pc = p->base; + psx_scheduler_run(&cpu); + if (!g_snap_taken) { fprintf(stderr, "%s: snapshot edge never reached\n", name); return 1; } + capture_final(&lf, &cpu); + memcpy(g_live_ram, g_test_ram, sizeof(g_test_ram)); + + /* The live edge must (or must not) deliver right at the snapshot edge. */ + int live_irq_at_edge = g_live.n > 0 && g_live.ev[0].is_irq && + g_live.ev[0].pc == g_snap->resume_pc && + g_live.ev[0].cycle == g_snap->cycle; + if (live_irq_at_edge != expect_irq_at_edge) { + fprintf(stderr, "%s: fixture: live delivery at snapshot edge = %d, expected %d\n", + name, live_irq_at_edge, expect_irq_at_edge); + return 1; + } + + /* REPLAY: the load is polled from inside guest execution and resumes + * through the scheduler, like every restore consumer. */ + reset_machine(&cpu, p); + g_mode_replay = 1; g_logging = 0; + g_replay.n = 0; g_log = &g_replay; + cpu.pc = LOADER_PC; + psx_scheduler_run(&cpu); + capture_final(&rf, &cpu); + if (compare_runs(name, &lf, &rf)) return 1; + printf(" %-34s OK (%d events, %d IRQs, edge pc=%08X cyc=%llu)\n", name, + g_live.n, count_irqs(&g_live), g_snap->resume_pc, + (unsigned long long)g_snap->cycle); + return 0; +} + +static int recover_hook(CPUState *c, uint32_t *out_pc) +{ + (void)c; + if (g_recover_used) return 0; + g_recover_used = 1; + g_recover_log_idx = g_log->n; + *out_pc = g_recover_pc; + return 1; +} + +int main(void) +{ + int fail = 0; + const uint32_t SR_ON = SR_IEC | SR_IM2; + + /* Snapshot edge after block 40 at 1000 + 40*50 = 3000; VBlank raised by + * the block that ends there, so it is pending at the edge. */ + Program game = { K_GAME, 0x80010000u, 7, 50, 120, 40, 3000, 1u, SR_ON, 0, 0, 0, 0 }; + Program rom = game; rom.kind = K_ROM; rom.base = 0xBFC07600u; + Program cont = game; cont.kind = K_CONT; cont.base = 0x80030000u; + Program intp = game; intp.kind = K_INTERP; intp.base = 0x80040000u; + + puts("restore-edge replay (pending IRQ at the restore edge):"); + fail |= live_vs_replay("compiled game resume", &game, 1); + if (count_irqs(&g_replay) != 1) { + fprintf(stderr, "compiled game resume: %d deliveries, expected 1\n", count_irqs(&g_replay)); + fail = 1; + } + /* Same-edge dedup: with a handler that takes no guest cycles, the edge + * delivery leaves cycle+PC stamped, so the compiled game dispatch entry + * that follows at the same cycle+PC is a no-op - live (after the edge + * check) and on replay (after the scheduler's restore-edge check). One + * delivery, not two, and the entry saw the edge as already checked. */ + { + Program p = game; + p.handler_zero_cost = 1; + g_entry_prechecked = 0; + fail |= live_vs_replay("compiled game, same-edge dedup", &p, 1); + int at_edge = 0; + for (int i = 0; i < g_replay.n; i++) + at_edge += g_replay.ev[i].is_irq && g_replay.ev[i].pc == g_snap->resume_pc && + g_replay.ev[i].cycle == g_snap->cycle; + if (!g_entry_prechecked || at_edge != 1) { + fprintf(stderr, "same-edge dedup: entry prechecked=%d, deliveries at edge=%d " + "(expected 1, 1)\n", g_entry_prechecked, at_edge); + fail = 1; + } + } + fail |= live_vs_replay("BIOS ROM resume", &rom, 1); + fail |= live_vs_replay("CPS continuation resume", &cont, 1); + fail |= live_vs_replay("interpreted (dirty) resume", &intp, 1); + + puts("no pending IRQ at the restore edge:"); + { + Program p; + p = game; p.raise_cycle = 4000; + fail |= live_vs_replay("game, VBlank later", &p, 0); + p = rom; p.raise_cycle = 4000; fail |= live_vs_replay("ROM, VBlank later", &p, 0); + p = intp; p.raise_cycle = 4000; fail |= live_vs_replay("interp, VBlank later", &p, 0); + p = rom; p.raise_cycle = ~0ull; fail |= live_vs_replay("ROM, no IRQ at all", &p, 0); + } + + puts("interrupt masking at the restore edge (guest unmasks at step 45):"); + { + Program p; + p = rom; p.imask0 = 0; p.enable_step = 45; p.enable_imask = 1u; + fail |= live_vs_replay("ROM, I_MASK clear", &p, 0); + p = rom; p.sr0 = SR_IM2; p.enable_step = 45; p.enable_sr = SR_IEC; + fail |= live_vs_replay("ROM, SR.IEc clear", &p, 0); + p = rom; p.sr0 = SR_IEC; p.enable_step = 45; p.enable_sr = SR_IM2; + fail |= live_vs_replay("ROM, SR.IM2 clear", &p, 0); + p = game; p.imask0 = 0; p.enable_step = 45; p.enable_imask = 1u; + fail |= live_vs_replay("game, I_MASK clear", &p, 0); + p = intp; p.sr0 = SR_IM2; p.enable_step = 45; p.enable_sr = SR_IEC; + fail |= live_vs_replay("interp, SR.IEc clear", &p, 0); + } + + /* Recovery path: after a restore has consumed its one-shot check, the + * guest's top-level dispatch returns PC 0 and the null-PC recovery + * resumes a ROM block via RESUME_CURRENT with a VBlank pending. It is not + * a restore edge: the block body runs first and the IRQ is taken at the + * block's own edge (pre-#660 behaviour), not at the recovery PC. */ + puts("recovery path (scheduler escape without resume_at):"); + { + static CPUState cpu; + Program p = rom; + p.steps = 60; p.raise_cycle = ~0ull; + g_prog = &p; g_snap = &g_snap_storage; + reset_machine(&cpu, &p); + g_mode_replay = 0; g_snap_taken = 0; g_logging = 0; g_log = &g_live; g_live.n = 0; + cpu.pc = p.base; + psx_scheduler_run(&cpu); /* live run to take a snapshot */ + reset_machine(&cpu, &p); + g_mode_replay = 1; g_log = &g_replay; g_replay.n = 0; + g_recover_pc = block_pc(3); + g_recover_used = 0; + g_test_recover_null_pc = recover_hook; + /* VBlank is raised by the block the guest exits after (step 60), so + * it is pending, undelivered, when the recovery resumes. */ + p.raise_cycle = 1000 + 60u * p.cost; + g_irq_count = 0; + cpu.pc = LOADER_PC; + psx_scheduler_run(&cpu); /* restore -> run to exit -> recover -> exit */ + g_test_recover_null_pc = NULL; + const Ev *irq = NULL; + int recov_idx = g_recover_used ? g_recover_log_idx : -1; + for (int i = recov_idx < 0 ? 0 : recov_idx; i < g_replay.n; i++) + if (g_replay.ev[i].is_irq) { irq = &g_replay.ev[i]; break; } + /* Expected after recovery: [block g_recover_pc] [IRQ, EPC = next block]. */ + if (!g_recover_used || !irq || recov_idx >= g_replay.n) { + fprintf(stderr, "recovery: fixture did not recover with a pending IRQ " + "(used=%d irq=%d)\n", g_recover_used, irq != NULL); + fail = 1; + } else if (g_replay.ev[recov_idx].is_irq || g_replay.ev[recov_idx].pc != g_recover_pc || + irq->pc != block_pc(4) || (irq - g_replay.ev) != recov_idx + 1) { + fprintf(stderr, "recovery: after resuming at %08X the first event is %s %08X and " + "the IRQ EPC is %08X; expected the block body first and the IRQ " + "at its edge %08X (the recovery inherited the restore-edge check)\n", + g_recover_pc, g_replay.ev[recov_idx].is_irq ? "IRQ" : "block", + g_replay.ev[recov_idx].pc, irq->pc, block_pc(4)); + fail = 1; + } else { + printf(" %-34s OK (recovered at %08X, IRQ at its edge %08X)\n", + "null-PC recovery after restore", g_recover_pc, irq->pc); + } + } + + if (fail) { puts("restore_edge_irq_replay_test: FAIL"); return 1; } + puts("restore_edge_irq_replay_test: PASS"); + return 0; +}