From d73dc5f36e266ba84bd9db46c0dda79756ba2d39 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 19 Jul 2026 13:40:03 -0700 Subject: [PATCH] runtime: add transactional overlay differential replay --- runtime/include/lockstep.h | 14 ++ runtime/include/psx_cycles.h | 14 ++ runtime/src/debug_server.c | 4 + runtime/src/dirty_ram_interp.c | 71 +++++++- runtime/src/gte.cpp | 68 +++++-- runtime/src/overlay_loader.c | 311 +++++++++++++++++++++++++++++++-- runtime/src/psx_cycles.c | 35 +++- runtime/src/psx_icache.c | 37 +++- 8 files changed, 513 insertions(+), 41 deletions(-) diff --git a/runtime/include/lockstep.h b/runtime/include/lockstep.h index 1229830f..775634b0 100644 --- a/runtime/include/lockstep.h +++ b/runtime/include/lockstep.h @@ -25,6 +25,20 @@ extern int g_ls_mode; * perturbs real global timing state. */ extern int g_ls_replay_active; +/* Side-effect-free whole-call trace used by the overlay differential gate. + * RECORD executes the interpreter authoritatively and records every memory + * operation. REPLAY feeds the recorded reads to native code and verifies + * writes without touching RAM, scratchpad, or MMIO a second time. These calls + * are deliberately single-owner/non-nestable; a false return fails the + * comparison closed. */ +int ls_shadow_record_begin(void); +int ls_shadow_record_end(uint32_t *ops, int *saw_exception); +int ls_shadow_replay_begin(void); +int ls_shadow_replay_end(uint32_t *ops, int *mismatch_kind, + uint32_t *pc, uint32_t *addr, + uint32_t *expected, uint32_t *actual); +void ls_shadow_abort(void); + /* Memory chokepoint hooks, called from the psx_read_ and psx_write_ funcs. */ uint32_t ls_read_hook(uint32_t addr, int size, uint32_t real_val); void ls_write_hook(uint32_t addr, int size, uint32_t val); diff --git a/runtime/include/psx_cycles.h b/runtime/include/psx_cycles.h index a13bab2b..be6506f0 100644 --- a/runtime/include/psx_cycles.h +++ b/runtime/include/psx_cycles.h @@ -20,6 +20,20 @@ extern uint64_t psx_cycle_count; * psx_cycles.c "Event-deadline device servicing"). */ void psx_advance_cycles(uint32_t cycles); +/* Diagnostic replay clock: advance the CPU-visible guest clock without + * servicing devices, then restore the authoritative live clock afterward. + * Used only while g_ls_replay_active is set. */ +int psx_cycle_replay_begin(uint64_t start_cycle); +uint64_t psx_cycle_replay_end(void); + +/* Transactional I-cache view for the overlay differential harness. The + * interpreter keeps the authoritative post-call cache; native replay starts + * from the saved entry tags, mutates a temporary view, then restores live. */ +int psx_icache_shadow_record_begin(void); +int psx_icache_shadow_replay_begin(void); +void psx_icache_shadow_replay_end(void); +void psx_icache_shadow_abort(void); + /* Event-deadline device model (psx_cycles.c): catch every device up to the * charged guest-cycle position and force a deadline recompute. memory.c calls * this at the top of every device-MMIO read/write so handlers always see diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 437d17de..40f56722 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -2008,6 +2008,10 @@ static inline void cyc_watch_observe(uint32_t block_leader_phys); /* defined be volatile uint32_t g_psx_last_fn_entry = 0; void debug_server_log_call_entry(uint32_t func_addr) { + /* Whole-call native replay is diagnostic and must not double-consume or + * overwrite live trace/stack-watch state. Architectural CPU state is + * compared by the caller; these observers are intentionally inert. */ + { extern int g_ls_replay_active; if (g_ls_replay_active) return; } g_psx_last_fn_entry = func_addr; #ifdef PSX_STACK_GUARD g_psx_recent_fn[g_psx_recent_fn_i++ & (PSX_RECENT_FN_CAP - 1u)] = func_addr; diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 6559f371..8cf93d7b 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -2580,6 +2580,8 @@ static int s_ls_trace_n = 0, s_ls_trace_idx = 0; static int s_ls_overflow = 0, s_ls_mismatch = 0; static int s_ls_replay_done = 0; /* trace exhausted: stop replay (benign count mismatch) */ static uint32_t s_ls_cur_pc = 0; /* pc of the instruction being replayed */ +static int s_ls_shadow_owner = 0; /* 0 none, 1 record, 2 replay */ +static int s_ls_shadow_saw_exception = 0; static CPUState s_ls_R0; /* register snapshot at block entry */ static uint32_t s_ls_block = 0; static int s_ls_prev_valid = 0; @@ -2634,6 +2636,71 @@ void ls_func_set_record_only(int on) { s_lsf_record_only = on; } void ls_note_exception_entry(void) { if (s_lsf_active) s_lsf_saw_irq = 1; + if (s_ls_shadow_owner == 1) s_ls_shadow_saw_exception = 1; +} + +int ls_shadow_record_begin(void) { + if (s_ls_shadow_owner || g_ls_mode != 0 || g_ls_replay_active || + s_lsf_active || s_ls_prev_valid) + return 0; + s_ls_trace_n = 0; + s_ls_trace_idx = 0; + s_ls_overflow = 0; + s_ls_mismatch = 0; + s_ls_m_kind = 0; + s_ls_replay_done = 0; + s_ls_shadow_saw_exception = 0; + s_ls_shadow_owner = 1; + g_ls_mode = 1; + return 1; +} + +int ls_shadow_record_end(uint32_t *ops, int *saw_exception) { + if (s_ls_shadow_owner != 1) return 0; + g_ls_mode = 0; + s_ls_shadow_owner = 0; + if (ops) *ops = (uint32_t)s_ls_trace_n; + if (saw_exception) *saw_exception = s_ls_shadow_saw_exception; + return !s_ls_overflow; +} + +int ls_shadow_replay_begin(void) { + if (s_ls_shadow_owner || g_ls_mode != 0 || g_ls_replay_active || + s_ls_overflow) + return 0; + s_ls_trace_idx = 0; + s_ls_mismatch = 0; + s_ls_m_kind = 0; + s_ls_replay_done = 0; + s_ls_shadow_owner = 2; + g_ls_replay_active = 1; + g_ls_mode = 2; + return 1; +} + +int ls_shadow_replay_end(uint32_t *ops, int *mismatch_kind, + uint32_t *pc, uint32_t *addr, + uint32_t *expected, uint32_t *actual) { + if (s_ls_shadow_owner != 2) return 0; + int complete = !s_ls_mismatch && !s_ls_replay_done && + s_ls_trace_idx == s_ls_trace_n; + g_ls_mode = 0; + g_ls_replay_active = 0; + s_ls_shadow_owner = 0; + if (ops) *ops = (uint32_t)s_ls_trace_idx; + if (mismatch_kind) *mismatch_kind = s_ls_m_kind; + if (pc) *pc = s_ls_m_pc; + if (addr) *addr = s_ls_m_addr; + if (expected) *expected = s_ls_m_exp; + if (actual) *actual = s_ls_m_act; + return complete; +} + +void ls_shadow_abort(void) { + g_ls_mode = 0; + g_ls_replay_active = 0; + s_ls_shadow_owner = 0; + s_ls_shadow_saw_exception = 0; } void ls_suppress_begin(void) { @@ -2648,7 +2715,7 @@ uint32_t ls_read_hook(uint32_t addr, int size, uint32_t real_val) { if (g_ls_mode == 2) { /* replay: serve from trace */ if (s_ls_trace_idx >= s_ls_trace_n) { s_ls_replay_done = 1; return 0; } /* count mismatch: benign block-boundary artifact */ ls_op_t *o = &s_ls_trace[s_ls_trace_idx]; - if (o->is_write || o->addr != addr) { + if (o->is_write || o->size != (uint8_t)size || o->addr != addr) { if (!s_ls_mismatch) { s_ls_mismatch = 1; s_ls_m_kind = 1; s_ls_m_pc = s_ls_cur_pc; s_ls_m_addr = addr; s_ls_m_exp = addr; s_ls_m_act = o->addr; } return 0; @@ -2669,7 +2736,7 @@ void ls_write_hook(uint32_t addr, int size, uint32_t val) { if (g_ls_mode == 2) { /* replay: verify against trace */ if (s_ls_trace_idx >= s_ls_trace_n) { s_ls_replay_done = 1; return; } /* count mismatch: benign block-boundary artifact */ ls_op_t *o = &s_ls_trace[s_ls_trace_idx]; - if (!o->is_write || o->addr != addr) { + if (!o->is_write || o->size != (uint8_t)size || o->addr != addr) { if (!s_ls_mismatch) { s_ls_mismatch = 1; s_ls_m_kind = 2; s_ls_m_pc = s_ls_cur_pc; s_ls_m_addr = addr; s_ls_m_exp = addr; s_ls_m_act = o->addr; } return; diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index b6471ac6..e3b5492a 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -185,6 +185,7 @@ static void depth_cue_from_ir(GTEState* gte, uint32_t instr) { static int32_t s_ws_xnum = 1, s_ws_xden = 1; extern "C" int gpu_ws_present_native_43(void); /* gpu.c — suppress on 4:3 frames */ extern "C" void psx_ws_note_gte_project(int nverts); /* gpu.c — gte_game_mode stamp */ +static int s_gte_replay_sandbox = 0; /* Optional visual-only geometry precision cache. The PS1-visible SXY FIFO * remains integer and fully faithful; this side cache retains the discarded @@ -243,6 +244,7 @@ extern "C" void gte_precision_tracking_set(int enabled) { } extern "C" void gte_precision_store_word(uint32_t addr, uint8_t reg) { + if (s_gte_replay_sandbox) return; if (!s_precision_tracking || reg < 12 || reg > 15) return; int index = reg == 15 ? 2 : (int)reg - 12; const PreciseProjection &projection = s_precise_sxy[index]; @@ -256,6 +258,7 @@ extern "C" void gte_precision_store_word(uint32_t addr, uint8_t reg) { } extern "C" void gte_precision_invalidate_word(uint32_t addr) { + if (s_gte_replay_sandbox) return; if (!s_precision_tracking) return; uint32_t physical; if (!precision_ram_address(addr, &physical)) return; @@ -312,6 +315,7 @@ extern "C" int gte_geometry_correction_lookup(uint32_t packed, } static inline void geom_note(uint32_t packed, int64_t x16, int64_t y16) { + if (s_gte_replay_sandbox) return; if (!s_geom_enabled) return; /* Saturated off-screen projections are unsuitable for subpixel recovery. */ int32_t x = (int16_t)(packed & 0xFFFFu); @@ -408,6 +412,7 @@ extern "C" void gte_dome_probe(int on, int thr) { if (on) for (int i = 0; i < DOME_PROBE_SLOTS; i++) { s_dome_probe[i].func = 0; s_dome_probe[i].count = 0; s_dome_probe[i].max_sz = 0; } } static inline void dome_probe_note(int32_t sz) { + if (s_gte_replay_sandbox) return; if (!s_dome_probe_on || sz < s_dome_probe_thr) return; /* Tally the guest RA (the GAME fn that jal'd to libgte for this projection), * NOT g_debug_current_func_addr (which is the libgte leaf 0x80000F40). */ @@ -475,6 +480,7 @@ static inline int gte_sxx(int32_t p){ int v=p&0xFFFF; return v>=0x8000? v-0x1000 static inline int gte_syy(int32_t p){ int v=(p>>16)&0xFFFF; return v>=0x8000? v-0x10000:v; } static void gte_rtp_record(const GTEState* g, uint32_t cmd) { + if (s_gte_replay_sandbox) return; if (!s_gte_rtp_ring) { s_gte_rtp_ring = (GteRtpRec*)calloc(GTE_RTP_RING_CAP, sizeof(GteRtpRec)); if (!s_gte_rtp_ring) return; @@ -553,6 +559,7 @@ static uint64_t s_gte_intpl_seq = 0; static void gte_intpl_record(const GTEState* g, const int16_t pre_ir[4], const int32_t pre_fc[3]) { + if (s_gte_replay_sandbox) return; if (!s_gte_intpl_ring) { s_gte_intpl_ring = (GteIntplRec*)calloc(GTE_INTPL_RING_CAP, sizeof(GteIntplRec)); if (!s_gte_intpl_ring) return; @@ -769,10 +776,15 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0) { /* Depth-gated: un-squash only FAR geometry (the backdrop); keep near * props squashed so they stay aligned. Record SZ stats for tuning. */ int32_t sz = gte->SZ[3]; - if (sz < s_ws_sz_min) s_ws_sz_min = sz; - if (sz > s_ws_sz_max) s_ws_sz_max = sz; - s_ws_sz_n++; - if (sz >= s_ws_far_threshold) { do_squash = false; s_ws_sz_far++; } + if (!s_gte_replay_sandbox) { + if (sz < s_ws_sz_min) s_ws_sz_min = sz; + if (sz > s_ws_sz_max) s_ws_sz_max = sz; + s_ws_sz_n++; + } + if (sz >= s_ws_far_threshold) { + do_squash = false; + if (!s_gte_replay_sandbox) s_ws_sz_far++; + } } if (do_squash) xterm = xterm * s_ws_xnum / s_ws_xden; @@ -780,12 +792,14 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0) { else if (s_ws_dome_on && s_ws_dome_num != s_ws_dome_den && !gpu_ws_present_native_43()) { int32_t sz = gte->SZ[3]; - if (sz < s_ws_sz_min) s_ws_sz_min = sz; - if (sz > s_ws_sz_max) s_ws_sz_max = sz; - s_ws_sz_n++; + if (!s_gte_replay_sandbox) { + if (sz < s_ws_sz_min) s_ws_sz_min = sz; + if (sz > s_ws_sz_max) s_ws_sz_max = sz; + s_ws_sz_n++; + } if (sz >= s_ws_far_threshold) { xterm = xterm * s_ws_dome_num / s_ws_dome_den; - s_ws_sz_far++; + if (!s_gte_replay_sandbox) s_ws_sz_far++; } } dome_probe_note(gte->SZ[3]); /* locate the dome draw fn (far-vertex tally) */ @@ -794,14 +808,16 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0) { int64_t sx = sx16 >> 16; int64_t sy = sy16 >> 16; gte->push_sxy(sx, sy); - s_precise_sxy[0] = s_precise_sxy[1]; - s_precise_sxy[1] = s_precise_sxy[2]; - s_precise_sxy[2].packed = (uint32_t)gte->SXY[2]; - s_precise_sxy[2].x16 = (int32_t)sx16; - s_precise_sxy[2].y16 = (int32_t)sy16; - s_precise_sxy[2].z = gte->SZ[3]; - s_precise_sxy[2].valid = gte->SZ[3] != 0; - s_precise_sxy[3] = s_precise_sxy[2]; + if (!s_gte_replay_sandbox) { + s_precise_sxy[0] = s_precise_sxy[1]; + s_precise_sxy[1] = s_precise_sxy[2]; + s_precise_sxy[2].packed = (uint32_t)gte->SXY[2]; + s_precise_sxy[2].x16 = (int32_t)sx16; + s_precise_sxy[2].y16 = (int32_t)sy16; + s_precise_sxy[2].z = gte->SZ[3]; + s_precise_sxy[2].valid = gte->SZ[3] != 0; + s_precise_sxy[3] = s_precise_sxy[2]; + } geom_note((uint32_t)gte->SXY[2], sx16, sy16); // Step 5: Depth cueing (MAC0/IR0) — only for last vertex of RTPT or RTPS @@ -1426,10 +1442,24 @@ uint32_t gte_cfc2(GTEState* gte, uint8_t reg) { // --------------------------------------------------------------------------- static uint64_t s_gte_exec_count = 0; extern "C" uint64_t gte_get_exec_count(void) { return s_gte_exec_count; } +static uint32_t s_gte_replay_saved_caller_ra = 0; +extern "C" int gte_replay_side_effects_begin(void) { + using namespace PSXRecomp::GTE; + if (s_gte_replay_sandbox) return 0; + s_gte_replay_saved_caller_ra = s_gte_caller_ra; + s_gte_replay_sandbox = 1; + return 1; +} +extern "C" void gte_replay_side_effects_end(void) { + using namespace PSXRecomp::GTE; + if (!s_gte_replay_sandbox) return; + s_gte_caller_ra = s_gte_replay_saved_caller_ra; + s_gte_replay_sandbox = 0; +} extern "C" void gte_execute(CPUState* cpu, uint32_t cmd) { using namespace PSXRecomp::GTE; - s_gte_exec_count++; + if (!s_gte_replay_sandbox) s_gte_exec_count++; s_gte_caller_ra = cpu->gpr[31]; /* dome-locate probe: game fn that issued this projection */ GTEState gte; @@ -1445,7 +1475,7 @@ extern "C" void gte_execute(CPUState* cpu, uint32_t cmd) { /* GTE-activity gameplay detector ([widescreen] gte_game_mode): note every * perspective projection so gpu.c can stamp real 3D frames as gameplay * (no-op unless the game opts in — early-out on the config flag). */ - if (func == 0x01 || func == 0x30) + if (!s_gte_replay_sandbox && (func == 0x01 || func == 0x30)) psx_ws_note_gte_project(func == 0x30 ? 3 : 1); /* INTPL ring: snapshot inputs before the op mutates IR (outputs recorded * after the switch). */ @@ -1533,7 +1563,7 @@ extern "C" void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val) { GTEState gte; gte_load_data(gte, cpu); gte_mtc2(>e, reg, val); - if (reg >= 12 && reg <= 15) + if (!s_gte_replay_sandbox && reg >= 12 && reg <= 15) for (int i = 0; i < 4; i++) s_precise_sxy[i].valid = 0; for (int i = 0; i < 32; i++) cpu->gte_data[i] = gte_mfc2(>e, i); for (int i = 0; i < 32; i++) cpu->gte_ctrl[i] = gte_cfc2(>e, i); diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index 3655758e..408ff099 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -7,6 +7,7 @@ #include "interrupts.h" #include "debug_server.h" #include "psx_cycles.h" +#include "lockstep.h" #include "overlay_posix.h" #include #include @@ -332,6 +333,11 @@ static const void *s_shadow_cand = NULL; * a larger epoch is an exception armed INSIDE the shadow (contained). */ static int s_shadow_saved_native_exec = 0; static int s_shadow_saved_supp = 0; +static int s_shadow_saved_exec_phase = 0; +static int s_shadow_saved_call_bail = 0; +static uint32_t s_shadow_saved_debug_func = 0; +static uint32_t s_shadow_saved_irq_calls = 0; +static uint64_t s_shadow_saved_irq_supp = 0; static uint64_t s_shadow_epoch = 0; static uint32_t s_shadow_escapes = 0; static uint32_t s_shadow_escapes_native = 0; @@ -339,6 +345,39 @@ static uint32_t s_shadow_escapes_native = 0; * live-execution mode are gone; overlay gaps fall to the interpreter.) */ static void run_shadow_diff(CPUState *cpu, Candidate *c, uint32_t addr); +static int cand_range_contains(const Candidate *c, uint32_t phys) { + for (int r = 0; r < c->nranges; r++) + if (phys >= c->range_lo[r] && phys < c->range_lo[r] + c->range_len[r]) + return 1; + return 0; +} + +static int cand_ranges_overlap(const Candidate *a, const Candidate *b) { + for (int ar = 0; ar < a->nranges; ar++) { + uint32_t alo = a->range_lo[ar], ahi = alo + a->range_len[ar]; + for (int br = 0; br < b->nranges; br++) { + uint32_t blo = b->range_lo[br], bhi = blo + b->range_len[br]; + if (alo < bhi && blo < ahi) return 1; + } + } + return 0; +} + +/* An explicit entry filter owns its whole code envelope, including separately + * seeded continuation candidates. This prevents an IRQ resume at a mid-function + * PC from bypassing the diagnostic quarantine and running native live. The scan + * is intentionally diagnostic-only (s_diff_addr != 0). */ +static int cand_selected_for_diff(const Candidate *c) { + if (!s_diff_addr) return 1; + uint32_t target = s_diff_addr & 0x1FFFFFFFu; + if (c->addr == target || cand_range_contains(c, target)) return 1; + for (int i = 0; i < s_cand_n; i++) { + const Candidate *owner = &s_cand[i]; + if (owner->addr == target && cand_ranges_overlap(c, owner)) return 1; + } + return 0; +} + /* ---- Counters (surfaced via overlay_loader_status) --------------------- */ static int s_ndlls = 0; /* DLLs LoadLibrary'd */ static uint64_t s_load_total_us = 0; @@ -1372,6 +1411,9 @@ void overlay_loader_get_irq_suppress(int *mode, uint32_t *rl, uint64_t *supp) { } static int overlay_irq_suppressed_now(void) { + /* Differential replay (and its authoritative interpreter pass) is atomic. + * Never let a previously armed rate-limit punch a real IRQ into a shadow. */ + if (s_in_shadow) { s_irq_suppressed++; return 1; } if (s_suppress_irq) { if (s_irq_ratelimit == 0) { s_irq_suppressed++; return 1; } if ((++s_irq_callcount % s_irq_ratelimit) != 0) { s_irq_suppressed++; return 1; } @@ -2394,10 +2436,9 @@ retry_candidates: * too — the blocklist blocks LIVE native, not the sandbox). */ int in_own_shadow = (s_in_shadow && (const void *)c == s_shadow_cand); if (!in_own_shadow) { - int want_diff = s_diff_mode && - (!s_diff_addr || phys == (s_diff_addr & 0x1FFFFFFFu)); + int want_diff = s_diff_mode && cand_selected_for_diff(c); if (want_diff && addr < 0x10000u) want_diff = 0; - if (want_diff && c->diff_passes < OVERLAY_DIFF_BUDGET) { + if (want_diff && (s_diff_addr || c->diff_passes < OVERLAY_DIFF_BUDGET)) { if (_probe) s_cps_probe_outcome = 5; s_diffgate_interp++; s_disp_interp++; @@ -2501,8 +2542,7 @@ retry_candidates: * gcc/DLL candidates are the trusted tier (validated at dev time) and * run native directly; they are diffed only in explicit dev diff mode * (PSX_OVERLAY_DIFF / overlay_diff cmd). */ - int want_diff = s_diff_mode && - (!s_diff_addr || phys == (s_diff_addr & 0x1FFFFFFFu)); + int want_diff = s_diff_mode && cand_selected_for_diff(c); /* Kernel-window candidates (call gates 0xA0/0xB0/0xC0 and the RAM * kernel) are NOT shadow-diffable: the gates tail-jump via RAM * tables into kernel SERVICES whose behavior depends on scheduler/ @@ -2512,7 +2552,8 @@ retry_candidates: * ring). The diff harness validates OVERLAY shard codegen against * the interp oracle; kernel gates stay on their normal route. */ if (want_diff && addr < 0x10000u) want_diff = 0; - if (want_diff && !s_in_shadow && c->diff_passes < OVERLAY_DIFF_BUDGET) { + if (want_diff && !s_in_shadow && + (s_diff_addr || c->diff_passes < OVERLAY_DIFF_BUDGET)) { /* NEVER start a shadow inside an exception dispatch: the guest's * ReturnFromException longjmps to the setjmp frame BELOW us * (psx_check_interrupts), unwinding run_shadow_diff WITHOUT its @@ -2762,16 +2803,19 @@ int overlay_fp_enabled(void) { * computation is correct and the fault is timing/interrupt-ordering. */ #define SHADOW_RAM_SIZE (2u * 1024u * 1024u) #define SHADOW_SPAD_SIZE 1024u -static uint8_t s_ram0[SHADOW_RAM_SIZE]; /* pre-call main-RAM snapshot */ -static uint8_t s_ramN[SHADOW_RAM_SIZE]; /* post-native main-RAM */ -static uint8_t s_ramI[SHADOW_RAM_SIZE]; /* post-interp main-RAM (kept) */ -static uint8_t s_spad0[SHADOW_SPAD_SIZE]; /* pre-call scratchpad snapshot*/ -static uint8_t s_spadI[SHADOW_SPAD_SIZE]; /* post-interp scratchpad (kept)*/ -static uint64_t s_shadow_skipped_dev = 0; /* candidates skipped: touch MMIO */ +static uint8_t s_ram0[SHADOW_RAM_SIZE], s_ramN[SHADOW_RAM_SIZE], s_ramI[SHADOW_RAM_SIZE]; +static uint8_t s_spad0[SHADOW_SPAD_SIZE], s_spadI[SHADOW_SPAD_SIZE]; +static uint64_t s_shadow_skipped_dev = 0; /* unsafe/incomplete traces skipped */ /* s_diff_mode / s_in_shadow declared above (before dispatch). */ typedef struct { uint64_t seq; uint32_t addr; + int kind; /* architectural/trace difference class */ + int index; /* register/array index, or -1 */ + uint64_t cycle_native, cycle_interp; + uint32_t trace_ops, replay_ops; + int trace_kind; + uint32_t trace_pc, trace_addr, trace_expected, trace_actual; int reg; /* first differing gpr index, -1 none */ uint32_t reg_native, reg_interp; int hi_diff, lo_diff; @@ -2793,7 +2837,7 @@ static uint32_t s_detail_nat_hi, s_detail_nat_lo, s_detail_int_hi, s_detail_int_ void overlay_loader_set_diff_mode(int on) { s_diff_mode = on ? 1 : 0; } -static void run_shadow_diff(CPUState *cpu, Candidate *c, uint32_t addr) { +static void run_shadow_diff_legacy(CPUState *cpu, Candidate *c, uint32_t addr) { extern uint8_t *memory_get_ram_ptr(void); extern uint8_t *memory_get_scratchpad_ptr(void); extern int dirty_ram_dispatch(CPUState *cpu, uint32_t addr, uint32_t stop_addr); @@ -2960,6 +3004,215 @@ static void run_shadow_diff(CPUState *cpu, Candidate *c, uint32_t addr) { s_in_shadow = 0; } +enum { + SHDIFF_NONE = 0, SHDIFF_PC, SHDIFF_GPR, SHDIFF_HI, SHDIFF_LO, + SHDIFF_COP0, SHDIFF_GTE_DATA, SHDIFF_GTE_CTRL, + SHDIFF_MULDIV_TS, SHDIFF_GTE_TS, SHDIFF_READ_ABSORB, + SHDIFF_READ_WHICH, SHDIFF_READ_FUDGE, SHDIFF_LD_WHICH, + SHDIFF_LD_ABSORB, SHDIFF_CYCLE, SHDIFF_TRACE, SHDIFF_SETUP +}; + +static int shadow_cpu_diff(const CPUState *native, const CPUState *interp, + int *index, uint32_t *nv, uint32_t *iv) { +#define SHCMP_SCALAR(kind_, field_) do { \ + if (native->field_ != interp->field_) { \ + *index = -1; *nv = (uint32_t)native->field_; *iv = (uint32_t)interp->field_; \ + return (kind_); \ + } \ +} while (0) +#define SHCMP_ARRAY(kind_, field_, count_) do { \ + for (int _i = 0; _i < (count_); _i++) { \ + if (native->field_[_i] != interp->field_[_i]) { \ + *index = _i; *nv = (uint32_t)native->field_[_i]; \ + *iv = (uint32_t)interp->field_[_i]; return (kind_); \ + } \ + } \ +} while (0) + if (native->pc != interp->pc) { + *index = -1; *nv = native->pc; *iv = interp->pc; return SHDIFF_PC; + } + SHCMP_ARRAY(SHDIFF_GPR, gpr, 32); + SHCMP_SCALAR(SHDIFF_HI, hi); + SHCMP_SCALAR(SHDIFF_LO, lo); + SHCMP_ARRAY(SHDIFF_COP0, cop0, 32); + SHCMP_ARRAY(SHDIFF_GTE_DATA, gte_data, 32); + SHCMP_ARRAY(SHDIFF_GTE_CTRL, gte_ctrl, 32); + SHCMP_SCALAR(SHDIFF_MULDIV_TS, muldiv_ts_done); + SHCMP_SCALAR(SHDIFF_GTE_TS, gte_ts_done); + SHCMP_ARRAY(SHDIFF_READ_ABSORB, read_absorb, 33); + SHCMP_SCALAR(SHDIFF_READ_WHICH, read_absorb_which); + SHCMP_SCALAR(SHDIFF_READ_FUDGE, read_fudge); + SHCMP_SCALAR(SHDIFF_LD_WHICH, ld_which_t); + SHCMP_SCALAR(SHDIFF_LD_ABSORB, ld_absorb); +#undef SHCMP_ARRAY +#undef SHCMP_SCALAR + *index = -1; *nv = 0; *iv = 0; + return SHDIFF_NONE; +} + +/* Authoritative interpreter RECORD + side-effect-free native REPLAY. + * Memory reads are replayed and writes verified/no-op, the replay clock advances + * without servicing devices, and GTE visual/diagnostic sidecars are sandboxed. + * Thus RAM/MMIO/devices mutate exactly once while the comparison covers the full + * CPU architectural and timing state plus the ordered memory-operation trace. */ +static void run_shadow_diff(CPUState *cpu, Candidate *c, uint32_t addr) { + extern int dirty_ram_dispatch(CPUState *cpu, uint32_t addr, uint32_t stop_addr); + extern void psx_dispatch_call(CPUState *cpu, uint32_t addr, uint32_t return_addr); + extern int gte_replay_side_effects_begin(void); + extern void gte_replay_side_effects_end(void); + extern uint32_t g_debug_current_func_addr; + extern uint64_t psx_exception_setjmp_epoch(void); + + s_in_shadow = 1; + s_shadow_epoch = psx_exception_setjmp_epoch(); + int saved_supp = s_suppress_irq; + int saved_native = s_native_exec; + s_suppress_irq = 1; + s_native_exec = 0; + s_shadow_saved_supp = saved_supp; + s_shadow_saved_native_exec = saved_native; + s_shadow_saved_exec_phase = g_exec_phase; + s_shadow_saved_call_bail = g_psx_call_bail; + s_shadow_saved_debug_func = g_debug_current_func_addr; + s_shadow_saved_irq_calls = s_irq_callcount; + s_shadow_saved_irq_supp = s_irq_suppressed; + + CPUState cpu0 = *cpu; + uint64_t cycle0 = psx_get_cycle_count(); + uint32_t stop_ra = cpu0.gpr[31]; + uint32_t record_ops = 0, replay_ops = 0; + int saw_exception = 0; + int icache_record_ok = psx_icache_shadow_record_begin(); + int record_owned = 0; + int record_ok = icache_record_ok && ls_shadow_record_begin(); + if (record_ok) { + record_owned = 1; + dirty_ram_dispatch(cpu, addr, stop_ra); + record_ok = ls_shadow_record_end(&record_ops, &saw_exception); + record_owned = 0; + } else { + dirty_ram_dispatch(cpu, addr, stop_ra); + } + s_shadow_calls++; + + CPUState cpuI = *cpu; + uint64_t cycleI = psx_get_cycle_count(); + if (!record_ok || saw_exception) { + s_shadow_skipped_dev++; + if (record_owned) ls_shadow_abort(); + psx_icache_shadow_abort(); + s_native_exec = saved_native; + s_suppress_irq = saved_supp; + s_in_shadow = 0; + return; + } + + CPUState cpuN = cpu0; + int trace_kind = 0; + uint32_t trace_pc = 0, trace_addr = 0, trace_expected = 0, trace_actual = 0; + int replay_owned = 0; + int replay_ok = ls_shadow_replay_begin(); + if (replay_ok) replay_owned = 1; + int icache_ok = replay_ok && psx_icache_shadow_replay_begin(); + int clock_ok = icache_ok && psx_cycle_replay_begin(cycle0); + int gte_ok = clock_ok && gte_replay_side_effects_begin(); + uint32_t saved_debug_func = g_debug_current_func_addr; + uint32_t saved_irq_callcount = s_irq_callcount; + uint64_t saved_irq_suppressed = s_irq_suppressed; + int saved_bail = g_psx_call_bail; + + if (gte_ok) { + s_shadow_cand = c; + { + int prev_phase = g_exec_phase; + OverlayFlushFn prev_flush = overlay_flush_enter(c); + g_exec_phase = 2; + c->fn(&cpuN); + overlay_flush_leave(prev_flush); + g_exec_phase = prev_phase; + } + { + int guard = 0; + while (cpuN.pc != 0 && !g_psx_call_bail && guard++ < 8192) { + uint32_t tv = cpuN.pc; + if ((tv & 0x1FFFFFFFu) == (stop_ra & 0x1FFFFFFFu)) break; + cpuN.pc = 0; + int prev_phase = g_exec_phase; + g_exec_phase = 3; + psx_dispatch_call(&cpuN, tv, stop_ra); + g_exec_phase = prev_phase; + } + } + s_shadow_cand = NULL; + } + + if (gte_ok) gte_replay_side_effects_end(); + uint64_t cycleN = clock_ok ? psx_cycle_replay_end() : cycle0; + psx_icache_shadow_replay_end(); + if (replay_ok) { + replay_ok = ls_shadow_replay_end(&replay_ops, &trace_kind, &trace_pc, + &trace_addr, &trace_expected, &trace_actual); + replay_owned = 0; + } else if (replay_owned) { + ls_shadow_abort(); + replay_owned = 0; + } + psx_icache_shadow_abort(); + g_debug_current_func_addr = saved_debug_func; + s_irq_callcount = saved_irq_callcount; + s_irq_suppressed = saved_irq_suppressed; + g_psx_call_bail = saved_bail; + + int index = -1; + uint32_t native_value = 0, interp_value = 0; + int kind = (!gte_ok || !replay_ok) ? SHDIFF_TRACE : + shadow_cpu_diff(&cpuN, &cpuI, &index, &native_value, &interp_value); + if (kind == SHDIFF_NONE && cycleN - cycle0 != cycleI - cycle0) { + kind = SHDIFF_CYCLE; + native_value = (uint32_t)(cycleN - cycle0); + interp_value = (uint32_t)(cycleI - cycle0); + } + + if (kind == SHDIFF_NONE) { + if (c->diff_passes < OVERLAY_DIFF_BUDGET) c->diff_passes++; + } else { + c->diff_passes = 0; + c->device_touch = 1; /* fail closed: all later calls stay interpreted */ + s_shadow_divs++; + if (!s_detail_captured) { + s_detail_captured = 1; + s_detail_addr = c->addr; + memcpy(s_detail_nat_gpr, cpuN.gpr, sizeof s_detail_nat_gpr); + memcpy(s_detail_int_gpr, cpuI.gpr, sizeof s_detail_int_gpr); + s_detail_nat_hi = cpuN.hi; s_detail_nat_lo = cpuN.lo; + s_detail_int_hi = cpuI.hi; s_detail_int_lo = cpuI.lo; + } + int addr_recs = 0; + for (int i = 0; i < s_sdiv_n; i++) + if (s_sdiv[i].addr == c->addr && ++addr_recs >= 16) break; + if (s_sdiv_n < SDIV_CAP && addr_recs < 16) { + ShadowDiv *d = &s_sdiv[s_sdiv_n++]; + memset(d, 0, sizeof *d); + d->seq = s_shadow_calls; d->addr = c->addr; + d->kind = kind; d->index = index; + d->cycle_native = cycleN - cycle0; + d->cycle_interp = cycleI - cycle0; + d->trace_ops = record_ops; d->replay_ops = replay_ops; + d->trace_kind = trace_kind; d->trace_pc = trace_pc; + d->trace_addr = trace_addr; d->trace_expected = trace_expected; + d->trace_actual = trace_actual; + d->reg = kind == SHDIFF_GPR ? index : -1; + d->reg_native = native_value; d->reg_interp = interp_value; + d->hi_diff = kind == SHDIFF_HI; d->lo_diff = kind == SHDIFF_LO; + d->ram_off = -1; + } + } + + s_native_exec = saved_native; + s_suppress_irq = saved_supp; + s_in_shadow = 0; +} + /* Called by deferred_exception_longjmp() (interrupts.c) before it unwinds. * If the longjmp target frame predates a live shadow run (target_epoch <= * s_shadow_epoch), the unwind blows through run_shadow_diff and its epilogue @@ -2981,12 +3234,27 @@ void overlay_loader_shadow_escape_fixup(uint64_t target_epoch) { g_overlay_flush_pending_cycles = NULL; if (!s_in_shadow) return; if (target_epoch > s_shadow_epoch) return; /* contained: leave armed */ + if (s_shadow_cand != NULL) { + Candidate *escaped = (Candidate *)s_shadow_cand; + escaped->diff_passes = 0; + escaped->device_touch = 1; /* native escape is unverified: fail closed */ + } + { extern void gte_replay_side_effects_end(void); gte_replay_side_effects_end(); } + (void)psx_cycle_replay_end(); + psx_icache_shadow_abort(); + ls_shadow_abort(); s_shadow_escapes++; if (s_shadow_cand != NULL) s_shadow_escapes_native++; s_in_shadow = 0; s_shadow_cand = NULL; s_native_exec = s_shadow_saved_native_exec; s_suppress_irq = s_shadow_saved_supp; + g_exec_phase = s_shadow_saved_exec_phase; + g_psx_call_bail = s_shadow_saved_call_bail; + { extern uint32_t g_debug_current_func_addr; + g_debug_current_func_addr = s_shadow_saved_debug_func; } + s_irq_callcount = s_shadow_saved_irq_calls; + s_irq_suppressed = s_shadow_saved_irq_supp; } int overlay_loader_dump_shadow_detail(char *out, int cap) { @@ -3026,13 +3294,22 @@ int overlay_loader_dump_shadow(char *out, int cap) { for (int i = 0; i < s_sdiv_n && n < cap - 200; i++) { ShadowDiv *d = &s_sdiv[i]; n += snprintf(out + n, cap - n, - "%s{\"seq\":%llu,\"addr\":\"0x%08X\",\"reg\":%d," + "%s{\"seq\":%llu,\"addr\":\"0x%08X\",\"kind\":%d,\"index\":%d,\"reg\":%d," "\"reg_native\":\"0x%08X\",\"reg_interp\":\"0x%08X\"," "\"hi\":%d,\"lo\":%d,\"ram_off\":%lld," - "\"ram_native\":\"0x%08X\",\"ram_interp\":\"0x%08X\"}", - i ? "," : "", (unsigned long long)d->seq, d->addr, d->reg, + "\"ram_native\":\"0x%08X\",\"ram_interp\":\"0x%08X\"," + "\"cycle_native\":%llu,\"cycle_interp\":%llu," + "\"trace_ops\":%u,\"replay_ops\":%u,\"trace_kind\":%d," + "\"trace_pc\":\"0x%08X\",\"trace_addr\":\"0x%08X\"," + "\"trace_expected\":\"0x%08X\",\"trace_actual\":\"0x%08X\"}", + i ? "," : "", (unsigned long long)d->seq, d->addr, + d->kind, d->index, d->reg, d->reg_native, d->reg_interp, d->hi_diff, d->lo_diff, - (long long)d->ram_off, d->ram_native, d->ram_interp); + (long long)d->ram_off, d->ram_native, d->ram_interp, + (unsigned long long)d->cycle_native, + (unsigned long long)d->cycle_interp, + d->trace_ops, d->replay_ops, d->trace_kind, + d->trace_pc, d->trace_addr, d->trace_expected, d->trace_actual); } n += snprintf(out + n, cap - n, "]}"); return n; diff --git a/runtime/src/psx_cycles.c b/runtime/src/psx_cycles.c index 3d7f8dca..8e051272 100644 --- a/runtime/src/psx_cycles.c +++ b/runtime/src/psx_cycles.c @@ -17,6 +17,26 @@ #endif uint64_t psx_cycle_count = 0; +static int s_cycle_replay_active = 0; +static uint64_t s_cycle_replay_live = 0; + +int psx_cycle_replay_begin(uint64_t start_cycle) { + extern int g_ls_replay_active; + if (!g_ls_replay_active || s_cycle_replay_active) return 0; + s_cycle_replay_live = psx_cycle_count; + psx_cycle_count = start_cycle; + s_cycle_replay_active = 1; + return 1; +} + +uint64_t psx_cycle_replay_end(void) { + uint64_t replay_cycle = psx_cycle_count; + if (s_cycle_replay_active) { + psx_cycle_count = s_cycle_replay_live; + s_cycle_replay_active = 0; + } + return replay_cycle; +} /* Throttle watchdog check to once per ~64K cycles to keep hot-path cost * negligible (most blocks emit 5-30 cycles, so the check fires every @@ -188,7 +208,15 @@ static void psx_advance_cycles_exact(uint32_t cycles) { } void psx_advance_cycles(uint32_t cycles) { - { extern int g_ls_replay_active; if (g_ls_replay_active) return; } /* lockstep replay: no global cycle/device mutation */ + { extern int g_ls_replay_active; + if (g_ls_replay_active) { + /* Replay owns a private-in-time view of the global clock. Advance it + * so GTE/muldiv deadlines and memory wait states see the same time as + * the authoritative pass, but never service devices or observers. */ + if (s_cycle_replay_active) psx_cycle_count += (uint64_t)cycles; + return; + } + } if (cycles == 0) return; uint32_t charged_cycles = cycles; #ifdef PSX_COSIM @@ -321,9 +349,12 @@ void psx_idle_note_check(CPUState *cpu, uint32_t check_pc) { extern int psx_get_in_exception(void); extern uint64_t g_guest_store_count, g_mmio_access_count; + /* A speculative replay must not clear or train the authoritative live idle + * detector. Its clock/device view is transactional and cannot be skipped. */ + if (g_ls_replay_active) return; if (!idle_skip_on() || g_idle_note_suppress) return; if (check_pc == 0 || psx_get_in_exception() || g_psx_call_bail || - g_precise_mode || g_ls_mode != 0 || g_ls_replay_active) { + g_precise_mode || g_ls_mode != 0) { s_idle_pc = 0; s_idle_streak = 0; return; diff --git a/runtime/src/psx_icache.c b/runtime/src/psx_icache.c index a38dea5f..658dd64f 100644 --- a/runtime/src/psx_icache.c +++ b/runtime/src/psx_icache.c @@ -30,6 +30,7 @@ #include "cpu_state.h" #include #include +#include extern void psx_advance_cycles(uint32_t cycles); @@ -52,6 +53,36 @@ int psx_icache_enabled(void) { * never equal an aligned fetch address (aligned addrs have bits 0-1 = 0), so every * line starts cold (miss). */ static uint32_t s_icache_tv[1024]; +static uint32_t s_icache_shadow_entry[1024]; +static uint32_t s_icache_shadow_live[1024]; +static int s_icache_shadow_state = 0; /* 0 none, 1 recorded, 2 replay */ + +int psx_icache_shadow_record_begin(void) { + if (s_icache_shadow_state != 0) return 0; + memcpy(s_icache_shadow_entry, s_icache_tv, sizeof s_icache_tv); + s_icache_shadow_state = 1; + return 1; +} + +int psx_icache_shadow_replay_begin(void) { + if (s_icache_shadow_state != 1) return 0; + memcpy(s_icache_shadow_live, s_icache_tv, sizeof s_icache_tv); + memcpy(s_icache_tv, s_icache_shadow_entry, sizeof s_icache_tv); + s_icache_shadow_state = 2; + return 1; +} + +void psx_icache_shadow_replay_end(void) { + if (s_icache_shadow_state != 2) return; + memcpy(s_icache_tv, s_icache_shadow_live, sizeof s_icache_tv); + s_icache_shadow_state = 0; +} + +void psx_icache_shadow_abort(void) { + if (s_icache_shadow_state == 2) + memcpy(s_icache_tv, s_icache_shadow_live, sizeof s_icache_tv); + s_icache_shadow_state = 0; +} void psx_icache_reset(void) { for (int i = 0; i < 1024; i++) s_icache_tv[i] = 0x1u; /* bit0 set => never matches */ @@ -60,7 +91,11 @@ void psx_icache_reset(void) { /* Charge the fetch cost for the instruction at `addr` and update the cache tags. * `addr` is the runtime guest virtual PC (aligned). Gated on PSX_ENABLE_BLOCK_CYCLES. */ void psx_icache_fetch(CPUState* cpu, uint32_t addr) { - { extern int g_ls_replay_active; if (g_ls_replay_active) return; } /* lockstep replay: no global icache mutation */ + { extern int g_ls_replay_active; + /* Ordinary lockstep replay must not perturb the shared cache. The whole- + * call shadow owns a private transactional cache view and may evolve it. */ + if (g_ls_replay_active && s_icache_shadow_state != 2) return; + } #ifdef PSX_ENABLE_BLOCK_CYCLES if (!psx_icache_enabled()) return; uint32_t idx = (addr & 0xFFCu) >> 2;