mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
685f38dacb
Packed games can rewrite code over their own loaded EXE text after static recompilation. A dirty-page bit alone is too coarse for dispatch decisions: it prevents stale native execution, but it also cannot distinguish data writes in a text page from instruction bytes that actually diverged from the compiled image.\n\nRegister the boot EXE image as a reference and let memory writes mark text pages whose bytes differ from that image. Static game dispatch now asks whether the target remains native-safe; diverged pages fall through to execute live RAM through the dirty interpreter, while matching code bytes can still use the compiled path.\n\nConstraint: Runtime-installed or rewritten RAM code must execute the program's own instructions, not HLE or stale static code\nRejected: Keep the existing dirty-page-only gate | too coarse for packed/self-modifying main text and data writes sharing code pages\nConfidence: medium\nScope-risk: moderate\nDirective: Do not bypass dirty_ram_text_native_ok() at game compiled dispatch sites without proving RAM still matches the compiled image\nTested: Local Tekken 3 branch using this mechanism reaches playable fights; clean upstream worktree compiles changed main/memory/dirty files before hitting an unrelated existing macOS overlay_loader LONG/InterlockedExchange build error\nNot-tested: Full upstream macOS build completion; non-macOS build matrix
2908 lines
134 KiB
C
2908 lines
134 KiB
C
/* dirty_ram_interp.c — small MIPS interpreter for install-at-runtime RAM.
|
|
*
|
|
* See CLAUDE.md Rule 18, docs/dynamic_handler_install.md, and the inline
|
|
* note in memory.c (search for "Option B") for the architectural rationale.
|
|
*
|
|
* Scope: only fires when psx_dispatch lands on a PC whose page has been
|
|
* written-to since boot. Runs one basic block (terminator: jr/jalr/j/jal
|
|
* or branch) and returns; dispatch trampoline re-enters for the next block.
|
|
*
|
|
* Strict policy: any opcode not implemented here aborts fatally. This
|
|
* surfaces unknown install patterns immediately so we expand the support
|
|
* set deliberately, never silently.
|
|
*
|
|
* Future option (Option B, see docs/dynamic_handler_install.md): JIT-compile
|
|
* dirty pages via the existing StrictTranslator instead of interpreting.
|
|
* Pros: single source of MIPS semantics shared with the build-time path,
|
|
* native-speed install stubs, generalizes to game JIT cases. Cons: gcc-at-
|
|
* runtime build dep, ~200 ms compile latency stall on first dispatch, file
|
|
* I/O on hot path, cache-invalidation complexity, Windows MinGW + dlopen
|
|
* friction. Today install stubs are cold-path glue (~4k instructions per
|
|
* directory-load); interpretation is sub-microsecond and the right fit.
|
|
* Revisit if measurement shows install-stub instructions becoming a
|
|
* meaningful fraction of total runtime work.
|
|
*/
|
|
|
|
#include "dirty_ram_interp.h"
|
|
#include "cpu_state.h"
|
|
#include "debug_server.h"
|
|
#include "interrupts.h"
|
|
#include "psx_cycles.h"
|
|
#include "psx_instr_cost.h" /* psx_instr_base_cycles — single-source cycle cost */
|
|
#include "gpu.h" /* psx_ws_is_backdrop_site / psx_ws_backdrop_x (interp hook) */
|
|
#include "ws_backdrop_detect.h" /* shared backdrop-window detector (auto_backdrop) */
|
|
#include "lockstep.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
uint64_t g_dirty_ram_blocks_run = 0;
|
|
uint64_t g_dirty_ram_insns_run = 0;
|
|
uint64_t g_dirty_window_dispatches = 0; /* capture-window interp dispatches */
|
|
uint64_t g_dirty_ram_aborts = 0;
|
|
uint64_t g_dirty_ram_guard_yields = 0;
|
|
/* Scheduling-contract pump telemetry (see dirty_ram_dispatch). Always-on:
|
|
* g_dirty_pump_max_gap_insns is the largest interpreted-insn gap ever seen
|
|
* between two interrupt pumps — must stay bounded (~4096 + one block) once the
|
|
* region-independent pump is in place; a runaway value means a dirty path is
|
|
* advancing cycles without surfacing to psx_check_interrupts (the softlock). */
|
|
uint64_t g_dirty_pump_max_gap_insns = 0;
|
|
uint64_t g_dirty_pump_count = 0;
|
|
static uint64_t s_last_dirty_irq_pump_insns = 0;
|
|
|
|
/* EPC de-overload signal (Tomba 2 frame-1997 fix). Set to the committed guest PC
|
|
* immediately around a dirty-pump psx_check_interrupts() call, 0 otherwise. When
|
|
* non-zero at exception entry, the interrupt is delivered at a dirty-interp safe point
|
|
* with a precise guest resume PC, so psx_check_interrupts sets COP0_EPC to the REAL PC
|
|
* (architectural RFE resume) instead of the host sentinel 0x80000048. The TCB then
|
|
* saves the real PC, so a game-installed handler that drives ReturnFromException
|
|
* itself (sync OR async) resumes correctly. Compiled-code block-boundary pumps leave
|
|
* this 0 -> the sentinel + host-GPR-restore + longjmp path (unchanged: T1/MMX6/Ape). */
|
|
uint32_t g_dirty_safe_resume_pc = 0;
|
|
|
|
/* Cycle-budgeted precise event slicing (PRECISE_IRQ_SLICE.md). Set while the
|
|
* block-leader slice guard is running a block through the per-instruction
|
|
* interpreter so the interrupt is taken at the EXACT architectural instruction
|
|
* (not the coarse block edge). While set, exec_one's jal/jalr short-circuit to a
|
|
* plain transfer (cpu->pc = target; return 1) so precise-mode steps INTO callees
|
|
* per-instruction instead of running them compiled — the "ignore native
|
|
* availability" invariant (ChatGPT-validated option b). Default 0 => every other
|
|
* path is byte-for-byte unchanged. */
|
|
int g_precise_mode = 0;
|
|
/* #2 lockstep: set ONLY around the dirty-interp loop's per-instruction
|
|
* cyc_observe so the lockstep comparator can tell a real COMPILED block leader
|
|
* (g_ls_dirty_observe==0) from an interpreted per-instruction sample (==1). */
|
|
int g_ls_dirty_observe = 0;
|
|
extern int g_ls_replay_active; /* defined in the lockstep section; used by exec_one's jal/jalr guard */
|
|
|
|
#ifdef PSX_COSIM
|
|
static int g_cosim_exec_one_hooked = 0;
|
|
static void cosim_exec_one_begin(void) { g_cosim_exec_one_hooked = 0; }
|
|
static int cosim_exec_one_did_hook(void) { return g_cosim_exec_one_hooked; }
|
|
static void cosim_exec_one_transfer_hook(uint32_t pc) {
|
|
extern void cosim_instr(uint32_t);
|
|
cosim_instr(pc);
|
|
g_cosim_exec_one_hooked = 1;
|
|
}
|
|
#else
|
|
static void cosim_exec_one_begin(void) {}
|
|
static int cosim_exec_one_did_hook(void) { return 0; }
|
|
static void cosim_exec_one_transfer_hook(uint32_t pc) { (void)pc; }
|
|
#endif
|
|
uint64_t g_slice_fired = 0; /* diagnostic: slices actually run */
|
|
uint64_t g_slice_irq_taken = 0; /* diagnostic: IRQs taken inside precise-mode */
|
|
/* First-divergence trace for the precise slice (PRECISE_IRQ_SLICE.md Task #4). */
|
|
uint32_t g_slice_last_block = 0; /* block_addr the guard fired on */
|
|
uint32_t g_slice_last_first_pc = 0; /* pc of the first interp instruction */
|
|
uint32_t g_slice_last_first_insn= 0; /* raw word of that first instruction */
|
|
uint32_t g_slice_last_committed = 0; /* committed PC handed back as resume/EPC */
|
|
uint32_t g_slice_last_istat = 0; /* i_stat at the take */
|
|
uint32_t g_slice_last_imask = 0; /* i_mask at the take */
|
|
uint32_t g_slice_last_sr = 0; /* COP0 SR at the take */
|
|
uint32_t g_slice_entry_deliverable = 0; /* was IRQ deliverable at slice entry? */
|
|
uint32_t g_slice_exit_pc = 0; /* PC published by the last precise slice */
|
|
uint32_t g_slice_exit_reason = 0; /* 1=safe, 2=unsupported, 3=bail, 4=guard */
|
|
uint32_t g_slice_exit_iter = 0; /* loop iteration at exit */
|
|
uint32_t g_slice_exit_dispatchable = 0; /* precise_pc_dispatchable(exit_pc) */
|
|
uint32_t g_slice_exit_dirty = 0; /* dirty_ram_is_dirty(exit_pc phys) */
|
|
uint32_t g_slice_exit_in_text = 0; /* psx_game_address_in_text(exit_pc) */
|
|
uint32_t g_slice_exit_want = 0; /* want_exit value at exit */
|
|
|
|
/* Persistent async-RFE resume PC (Tomba 2 frame-1997 fix). psx_check_interrupts latches
|
|
* this from g_dirty_safe_resume_pc at each dirty-safe-point exception entry, so it holds
|
|
* the real guest PC of the most recent dirty-interp interruption. When a game-installed
|
|
* handler drives ReturnFromException ASYNCHRONOUSLY (sentinel RFE with in_exception==0),
|
|
* the sentinel gates resume the guest here instead of resolving to pc=0 (abnormal exit).
|
|
* Unlike g_dirty_safe_resume_pc (transient, scoped to the pump call) this persists across
|
|
* the gap between the interrupt and the game's later ReturnFromException. */
|
|
uint32_t g_async_rfe_resume_pc = 0;
|
|
/* Diagnostics for the async-RFE fix: times the resume PC was latched at a dirty-safe
|
|
* exception entry, and times the sentinel gates actually redirected to it. */
|
|
uint64_t g_async_rfe_set_count = 0;
|
|
uint64_t g_async_rfe_fire_count = 0;
|
|
/* Reach diagnostics: which gate the sentinel dispatch lands in, and what
|
|
* g_async_rfe_resume_pc held there. */
|
|
uint64_t g_sentinel_reach_dirty = 0;
|
|
uint64_t g_sentinel_reach_traps = 0;
|
|
uint32_t g_sentinel_reach_async = 0;
|
|
|
|
/* One-shot pc=0 producer tripwire (MMX6 boot-wedge investigation, Task #4 family).
|
|
* Latches the FIRST time dirty_ram_dispatch returns "handled" (r==1) but leaves
|
|
* cpu->pc == 0 — i.e. the dirty path produced the abnormal-exit null PC rather
|
|
* than psx_unknown_dispatch (which has its own fail-fast). Captures the dispatch
|
|
* addr + interrupt-return context so the exact producing path is identifiable
|
|
* without printf. Surfaced in freeze_check (pczero_*). */
|
|
int g_cosim_dirty_pump_site = 0; /* PSX_COSIM diagnostic: current dirty IRQ pump site */
|
|
int g_pczero_latched = 0;
|
|
uint32_t g_pczero_addr = 0;
|
|
uint32_t g_pczero_ra = 0;
|
|
uint32_t g_pczero_in_exc = 0;
|
|
uint32_t g_pczero_async_rfe = 0;
|
|
uint32_t g_pczero_dirty_safe = 0;
|
|
uint64_t g_pczero_count = 0;
|
|
|
|
/* Mid-block unsupported-opcode counters. Bumped instead of fprintf-spamming
|
|
* stderr (CLAUDE.md §3). Read via dirty_ram_get_unsupported(). The "last_*"
|
|
* fields capture the most recent occurrence so a TCP query can see what
|
|
* opcode is missing without needing log scraping. */
|
|
uint64_t g_dirty_ram_unsupported_midblock = 0;
|
|
uint32_t g_dirty_ram_last_unsupported_entry = 0;
|
|
uint32_t g_dirty_ram_last_unsupported_entry_ra = 0;
|
|
uint32_t g_dirty_ram_last_unsupported_entry_sp = 0;
|
|
uint32_t g_dirty_ram_last_unsupported_insns = 0;
|
|
uint32_t g_dirty_ram_last_unsupported_pc = 0;
|
|
uint32_t g_dirty_ram_last_unsupported_insn = 0;
|
|
const char *g_dirty_ram_last_unsupported_reason = NULL;
|
|
|
|
DirtyRamPcEntry g_dirty_ram_pc_table[DIRTY_RAM_PC_TABLE_SIZE] = {0};
|
|
DirtyRamPcEntry g_dirty_ram_exec_pc_table[DIRTY_RAM_PC_TABLE_SIZE] = {0};
|
|
|
|
DirtyRamBlockLogEntry g_dirty_ram_block_log[DIRTY_RAM_BLOCK_LOG_CAP] = {0};
|
|
uint64_t g_dirty_ram_block_log_seq = 0;
|
|
DirtyRamFlowLogEntry g_dirty_ram_flow_log[DIRTY_RAM_FLOW_LOG_CAP] = {0};
|
|
uint64_t g_dirty_ram_flow_log_seq = 0;
|
|
DirtyRamInsnLogEntry g_dirty_ram_insn_log[DIRTY_RAM_INSN_LOG_CAP] = {0};
|
|
uint64_t g_dirty_ram_insn_log_seq = 0;
|
|
|
|
/* Current frame counter, defined in debug_server.c. */
|
|
extern uint64_t s_frame_count;
|
|
|
|
/* Linear-probed insert/lookup keyed on entry PC. Probe length is HARD
|
|
* BOUNDED: a saturated table must degrade to "stop tracking" (NULL) at O(1)
|
|
* cost, never to an O(table) scan per lookup. Tomba2's Trolley attract demo
|
|
* overflowed the old unbounded probe (working set > table size) and burned
|
|
* ~99% of the host emu thread rescanning 64K slots per executed instruction
|
|
* (0.3 fps). 128 probes keeps misses vanishingly rare below ~85% fill with
|
|
* this hash; past that the tracking data was already degrading anyway. */
|
|
static DirtyRamPcEntry *pc_table_get_or_insert_in(DirtyRamPcEntry *table, uint32_t pc) {
|
|
uint32_t h = (pc * 2654435761u) & (DIRTY_RAM_PC_TABLE_SIZE - 1);
|
|
for (uint32_t i = 0; i < 128u; i++) {
|
|
uint32_t idx = (h + i) & (DIRTY_RAM_PC_TABLE_SIZE - 1);
|
|
DirtyRamPcEntry *e = &table[idx];
|
|
if (e->pc == pc) return e;
|
|
if (e->pc == 0) { e->pc = pc; return e; }
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static DirtyRamPcEntry *pc_table_get_or_insert(uint32_t pc) {
|
|
return pc_table_get_or_insert_in(g_dirty_ram_pc_table, pc);
|
|
}
|
|
|
|
/* Record every PC the interpreter executes (not just block entries) so
|
|
* overlay_capture can report execution-verified seeds for the region. */
|
|
static void exec_pc_table_record(uint32_t pc) {
|
|
DirtyRamPcEntry *e = pc_table_get_or_insert_in(g_dirty_ram_exec_pc_table,
|
|
pc & 0x1FFFFFFFu);
|
|
if (e) e->hits++;
|
|
}
|
|
|
|
/* From debug_server.c — keep our outer-frame attribution coherent. */
|
|
extern uint32_t g_debug_current_func_addr;
|
|
extern uint32_t g_debug_last_store_pc;
|
|
|
|
/* Execution-mode tag for the event-timeline ring: 1 while a dirty_ram_dispatch
|
|
* call is on the stack. Cleared defensively at the psx_check_interrupts longjmp
|
|
* landing (interrupts.c) so the EPC-sentinel longjmp at dirty_ram_dispatch
|
|
* can't leave it stuck on. NATIVE_OVERLAY (inprogress!=0) takes precedence in
|
|
* the ring's mode resolution, so this only distinguishes INTERP from STATIC. */
|
|
int g_dirty_interp_active = 0;
|
|
int dirty_ram_interp_is_active(void) { return g_dirty_interp_active; }
|
|
|
|
/* TCP-armed instruction-window capture (native↔interp divergence drill).
|
|
* g_insn_gate_*: an extra runtime-settable PC range that the per-insn log
|
|
* records (on top of the hardwired kernel ranges). Freeze latch: on the Nth
|
|
* dispatch of candidate g_insn_freeze_addr the insn ring stops recording, so
|
|
* its tail preserves the window immediately BEFORE that dispatch — query the
|
|
* ring afterward at leisure (ring-first; no arm-then-hope). */
|
|
uint32_t g_insn_gate_lo = 0; /* extra always-log phys range [lo,hi) */
|
|
uint32_t g_insn_gate_hi = 0; /* 0 = extra range disabled */
|
|
uint32_t g_insn_freeze_addr = 0; /* candidate phys entry to watch */
|
|
uint32_t g_insn_freeze_nth = 0; /* freeze before its Nth dispatch */
|
|
uint32_t g_insn_freeze_count = 0;
|
|
int g_insn_log_frozen = 0;
|
|
/* Tomba2 wild-jump capture (Confirm-(b)): freeze the insn ring the instant an
|
|
* interpreted instruction's transfer TARGET (or next_pc) equals this watched
|
|
* value, so the offending jr/jalr is preserved as the ring's last entry with its
|
|
* full register snapshot. Set via the `insn_freeze_target` debug command (0 =
|
|
* disabled). Matches on full 32-bit value (wild PCs are not phys-normalizable). */
|
|
uint32_t g_insn_freeze_on_target = 0;
|
|
/* Register snapshot captured at the instruction that hits g_insn_freeze_on_target
|
|
* (the offending jr/jalr). g_freeze_snap_valid latches 1; pc/insn identify the
|
|
* branch, gpr[] is the full guest register file at that moment (the source
|
|
* register still holds the wild target; scan for which reg == the target). */
|
|
int g_freeze_snap_valid = 0;
|
|
uint32_t g_freeze_snap_pc = 0;
|
|
uint32_t g_freeze_snap_insn = 0;
|
|
uint32_t g_freeze_snap_tcb = 0;
|
|
uint32_t g_freeze_snap_gpr[32] = {0};
|
|
/* ra-load watch (Confirm-(b)): capture the instruction that sets $ra to this
|
|
* value. 0 = disabled. */
|
|
uint32_t g_ra_load_watch = 0;
|
|
int g_ra_load_snap_valid = 0;
|
|
uint32_t g_ra_load_snap_pc = 0;
|
|
uint32_t g_ra_load_snap_insn = 0;
|
|
uint32_t g_ra_load_snap_before_ra = 0;
|
|
uint32_t g_ra_load_snap_srcaddr = 0;
|
|
uint32_t g_ra_load_snap_gpr[32] = {0};
|
|
|
|
/* Overlay-region floor (phys) = the loaded game's main-EXE text end. Defaults to
|
|
* the BIOS-only value; main.cpp pins it to (load_address + text_size) at game
|
|
* load. See dirty_ram_interp.h for the per-game rationale. */
|
|
uint32_t g_overlay_region_floor = OVERLAY_REGION_FLOOR_DEFAULT;
|
|
|
|
#ifdef PSX_HAS_GAME_DISPATCH
|
|
extern int psx_dispatch_game_compiled(CPUState* cpu, uint32_t addr);
|
|
extern int psx_game_address_in_text(uint32_t addr);
|
|
extern int psx_game_is_function_entry(uint32_t addr); /* non-destructive entry test */
|
|
#endif
|
|
extern void psx_dispatch_call(CPUState* cpu, uint32_t addr, uint32_t return_addr);
|
|
|
|
/* Forward decls from memory.c — used to read instruction bytes. */
|
|
extern uint8_t *memory_get_ram_ptr(void);
|
|
|
|
/* MIPS instruction field decoders. */
|
|
static inline uint32_t op_field (uint32_t i) { return (i >> 26) & 0x3Fu; }
|
|
static inline uint32_t rs_field (uint32_t i) { return (i >> 21) & 0x1Fu; }
|
|
static inline uint32_t rt_field (uint32_t i) { return (i >> 16) & 0x1Fu; }
|
|
static inline uint32_t rd_field (uint32_t i) { return (i >> 11) & 0x1Fu; }
|
|
static inline uint32_t shamt_field (uint32_t i) { return (i >> 6) & 0x1Fu; }
|
|
static inline uint32_t funct_field (uint32_t i) { return i & 0x3Fu; }
|
|
static inline uint32_t imm16_field (uint32_t i) { return i & 0xFFFFu; }
|
|
static inline int32_t simm16_field(uint32_t i) { return (int32_t)(int16_t)imm16_field(i); }
|
|
static inline uint32_t target26 (uint32_t i) { return i & 0x03FFFFFFu; }
|
|
|
|
/* Read a 32-bit instruction word from kernel RAM at the given physical addr.
|
|
* Caller has already verified the address is in dirty kernel RAM. */
|
|
static inline uint32_t fetch_word(uint32_t phys) {
|
|
const uint8_t *ram = memory_get_ram_ptr();
|
|
return (uint32_t)ram[phys]
|
|
| ((uint32_t)ram[phys + 1] << 8)
|
|
| ((uint32_t)ram[phys + 2] << 16)
|
|
| ((uint32_t)ram[phys + 3] << 24);
|
|
}
|
|
|
|
/* Widescreen render-funnel cull detection for the interpreter ([widescreen.cull]
|
|
* auto_screen_x). A `sltiu …,0x140/0x141` is widened ONLY when its enclosing
|
|
* render function also carries a `sltiu …,0xE0/0xF1` (the GTE per-vertex trivial-
|
|
* reject signature) — a lone 0x140 elsewhere stays vanilla. The interp has no
|
|
* function boundaries, so we scan a +/-512-byte window around the site (clamped
|
|
* to main RAM) via the shared psx_ws_func_has_screen_cull, cached per-PC so a
|
|
* hot render loop pays the scan once. Single-threaded interp => static buffers ok. */
|
|
/* Per-PC site caches: FULL-PC tagged, sized for a whole overlay's working set
|
|
* (the old 256-slot direct map thrashed on any two hot PCs 1 KB apart — the
|
|
* ±512-byte rescan then ran per EXECUTED INSTRUCTION and collapsed native-wide
|
|
* 16:9 to ~12fps, entirely CPU-side). Entries carry the dirty-RAM code
|
|
* generation (memory.c g_dirty_ram_code_gen) so an overlay reload re-derives
|
|
* every classification instead of trusting a stale kind. */
|
|
extern uint32_t g_dirty_ram_code_gen;
|
|
#define WS_SITE_CACHE_SLOTS 8192u /* 32 KB of code coverage per cache */
|
|
|
|
static int ws_cull_site(uint32_t pc) {
|
|
enum { WIN = 128 }; /* +/- 128 words = +/- 512 bytes */
|
|
static struct { uint32_t pc; uint32_t gen; uint32_t word; int8_t flag; } cache[WS_SITE_CACHE_SLOTS];
|
|
uint32_t slot = (pc >> 2) & (WS_SITE_CACHE_SLOTS - 1u);
|
|
uint32_t phys = pc & 0x1FFFFFFFu;
|
|
/* Entry valid iff pc + generation + the SITE'S OWN INSTRUCTION WORD all
|
|
* match. The word tag catches code replaced by plain CPU stores (loader
|
|
* memcpy), which never routes through the page-marking hooks the
|
|
* generation counter sees — a stale verdict here REWRITES a live GPR and
|
|
* corrupts the guest (wild-jump fatal). */
|
|
if (cache[slot].pc == pc && cache[slot].gen == g_dirty_ram_code_gen &&
|
|
cache[slot].word == fetch_word(phys))
|
|
return cache[slot].flag;
|
|
uint32_t lo = (phys > (uint32_t)(WIN * 4)) ? phys - (uint32_t)(WIN * 4) : 0u;
|
|
uint32_t hi = phys + (uint32_t)(WIN * 4);
|
|
if (hi > 0x200000u) hi = 0x200000u; /* 2 MB main RAM */
|
|
static uint32_t words[2 * WIN + 1];
|
|
int n = 0;
|
|
for (uint32_t a = lo; a + 4u <= hi && n < (int)(2 * WIN + 1); a += 4u)
|
|
words[n++] = fetch_word(a);
|
|
int flag = psx_ws_func_has_screen_cull(words, n);
|
|
cache[slot].pc = pc; cache[slot].gen = g_dirty_ram_code_gen;
|
|
cache[slot].word = fetch_word(phys); cache[slot].flag = (int8_t)flag;
|
|
return flag;
|
|
}
|
|
|
|
/* Widescreen far-backdrop column-PRELOAD site classification for the interpreter
|
|
* ([widescreen.cull] auto_backdrop). The scrolling-backdrop column-window
|
|
* generators run interpreted in the dev build, so the recompiler emit can't
|
|
* reach them; the interp re-derives the same START/END rewrite sites via the
|
|
* shared detector over a +/-512-byte window around the PC (physical space, so
|
|
* the detector's absolute-PC math matches the masked PC), cached per-PC so a hot
|
|
* generator pays the scan once. Returns WS_BD_NONE / WS_BD_START_ZERO /
|
|
* WS_BD_END_WIDEN. Single-threaded interp => static buffers are safe. */
|
|
static int ws_backdrop_site_kind(uint32_t pc, int *out_cols) {
|
|
enum { WIN = 128 }; /* +/- 128 words = +/- 512 bytes */
|
|
static struct { uint32_t pc; uint32_t gen; uint32_t word; int8_t kind; int16_t cols; } cache[WS_SITE_CACHE_SLOTS];
|
|
uint32_t slot = (pc >> 2) & (WS_SITE_CACHE_SLOTS - 1u);
|
|
uint32_t phys = pc & 0x1FFFFFFFu;
|
|
/* pc + generation + site instruction word — see ws_cull_site: the word tag
|
|
* is what protects against CPU-store code reloads the generation counter
|
|
* cannot see. A stale KIND here is worse than a stale cull flag: it
|
|
* substitutes a GPR value and skips the real instruction. */
|
|
if (cache[slot].pc == pc && cache[slot].gen == g_dirty_ram_code_gen &&
|
|
cache[slot].word == fetch_word(phys)) {
|
|
if (out_cols) *out_cols = cache[slot].cols;
|
|
return cache[slot].kind;
|
|
}
|
|
uint32_t lo = (phys > (uint32_t)(WIN * 4)) ? phys - (uint32_t)(WIN * 4) : 0u;
|
|
uint32_t hi = phys + (uint32_t)(WIN * 4 + 4);
|
|
if (hi > 0x200000u) hi = 0x200000u; /* 2 MB main RAM */
|
|
static uint32_t words[2 * WIN + 2];
|
|
int n = 0;
|
|
for (uint32_t a = lo; a + 4u <= hi && n < (int)(2 * WIN + 2); a += 4u)
|
|
words[n++] = fetch_word(a);
|
|
int cols = 0;
|
|
int kind = psx_ws_backdrop_kind_at(words, n, lo, phys, &cols); /* all physical-space */
|
|
cache[slot].pc = pc; cache[slot].gen = g_dirty_ram_code_gen;
|
|
cache[slot].word = fetch_word(phys);
|
|
cache[slot].kind = (int8_t)kind; cache[slot].cols = (int16_t)cols;
|
|
if (out_cols) *out_cols = cols;
|
|
return kind;
|
|
}
|
|
|
|
static void dirty_ram_log_instruction(CPUState *cpu, uint32_t pc, uint32_t insn,
|
|
uint32_t before_s0, uint32_t next_pc,
|
|
uint32_t target, int transferred) {
|
|
if (g_insn_log_frozen) return;
|
|
/* Freeze-on-target (Confirm-(b)): checked BEFORE the gate filter so it fires
|
|
* for ANY interpreted instruction transferring to the watched wild PC, even
|
|
* if that jr/jalr sits outside the recorded gate range. We do NOT record this
|
|
* out-of-gate entry (the gate still governs what's stored); we just stop the
|
|
* ring so the in-gate window leading up to the wild jump is preserved. */
|
|
if (g_insn_freeze_on_target != 0u &&
|
|
(target == g_insn_freeze_on_target || next_pc == g_insn_freeze_on_target)) {
|
|
if (!g_freeze_snap_valid) {
|
|
g_freeze_snap_pc = pc;
|
|
g_freeze_snap_insn = insn;
|
|
uint32_t tcb_ptr = cpu->read_word(0x00000108u);
|
|
g_freeze_snap_tcb = tcb_ptr ? cpu->read_word(tcb_ptr) : 0;
|
|
for (int r = 0; r < 32; r++) g_freeze_snap_gpr[r] = cpu->gpr[r];
|
|
g_freeze_snap_valid = 1;
|
|
}
|
|
g_insn_log_frozen = 1;
|
|
return;
|
|
}
|
|
uint32_t phys = pc & 0x1FFFFFFFu;
|
|
if (!((g_insn_gate_hi != 0u && phys >= g_insn_gate_lo && phys < g_insn_gate_hi) ||
|
|
(phys >= 0x000E0000u && phys < 0x000F0000u) ||
|
|
(phys >= 0x000000A0u && phys < 0x000000C0u) ||
|
|
(phys >= 0x000005C0u && phys < 0x00000620u) ||
|
|
(phys >= 0x00001E00u && phys < 0x00002000u))) {
|
|
return;
|
|
}
|
|
|
|
uint32_t tcb_ptr_addr = cpu->read_word(0x00000108u);
|
|
uint32_t current_tcb = tcb_ptr_addr ? cpu->read_word(tcb_ptr_addr) : 0;
|
|
uint32_t task_ptr = cpu->read_word(0x1F8001D4u);
|
|
|
|
uint64_t s = g_dirty_ram_insn_log_seq++;
|
|
DirtyRamInsnLogEntry *e =
|
|
&g_dirty_ram_insn_log[s & (DIRTY_RAM_INSN_LOG_CAP - 1u)];
|
|
e->seq = s;
|
|
e->pc = pc;
|
|
e->insn = insn;
|
|
e->next_pc = next_pc;
|
|
e->target = target;
|
|
e->before_s0 = before_s0;
|
|
e->after_s0 = cpu->gpr[16];
|
|
e->sp = cpu->gpr[29];
|
|
e->ra = cpu->gpr[31];
|
|
e->v0 = cpu->gpr[2];
|
|
e->v1 = cpu->gpr[3];
|
|
e->a0 = cpu->gpr[4];
|
|
e->a1 = cpu->gpr[5];
|
|
e->a2 = cpu->gpr[6];
|
|
e->a3 = cpu->gpr[7];
|
|
e->t0 = cpu->gpr[8];
|
|
e->t1 = cpu->gpr[9];
|
|
e->t2 = cpu->gpr[10];
|
|
e->current_tcb = current_tcb;
|
|
e->task_ptr = task_ptr;
|
|
e->task_mode = task_ptr ? cpu->read_half(task_ptr + 72u) : 0;
|
|
e->task_submode = task_ptr ? cpu->read_half(task_ptr + 74u) : 0;
|
|
e->frame = (uint32_t)s_frame_count;
|
|
e->transferred = (uint8_t)(transferred ? 1u : 0u);
|
|
}
|
|
|
|
/* Marker entries delimit NATIVE candidate executions in the insn ring (native
|
|
* code emits no per-insn entries; markers keep the two runs' timelines
|
|
* alignable). transferred: 200 = native entry, 201 = native exit. */
|
|
void dirty_ram_log_marker(uint32_t addr, uint32_t tag, int kind) {
|
|
if (g_insn_log_frozen) return;
|
|
uint64_t s = g_dirty_ram_insn_log_seq++;
|
|
DirtyRamInsnLogEntry *e =
|
|
&g_dirty_ram_insn_log[s & (DIRTY_RAM_INSN_LOG_CAP - 1u)];
|
|
memset(e, 0, sizeof *e);
|
|
e->seq = s;
|
|
e->pc = addr;
|
|
e->target = tag;
|
|
e->frame = (uint32_t)s_frame_count;
|
|
e->transferred = (uint8_t)(200 + kind);
|
|
}
|
|
|
|
/* LWL/LWR are merge-only here; the timed aligned-word read is done by the caller via
|
|
* psx_cyc_load_word (Beetle reads the aligned word; GPR_DEP rs only, arms LDWhich=rt). */
|
|
static inline uint32_t lwl_merge(uint32_t addr, uint32_t word, uint32_t rt_value) {
|
|
switch (addr & 3u) {
|
|
case 0: return (rt_value & 0x00FFFFFFu) | (word << 24);
|
|
case 1: return (rt_value & 0x0000FFFFu) | (word << 16);
|
|
case 2: return (rt_value & 0x000000FFu) | (word << 8);
|
|
default: return word;
|
|
}
|
|
}
|
|
|
|
static inline uint32_t lwr_merge(uint32_t addr, uint32_t word, uint32_t rt_value) {
|
|
switch (addr & 3u) {
|
|
case 0: return word;
|
|
case 1: return (rt_value & 0xFF000000u) | (word >> 8);
|
|
case 2: return (rt_value & 0xFFFF0000u) | (word >> 16);
|
|
default: return (rt_value & 0xFFFFFF00u) | (word >> 24);
|
|
}
|
|
}
|
|
|
|
static inline void interp_swl(CPUState *cpu, uint32_t addr, uint32_t value) {
|
|
uint32_t aligned = addr & ~3u;
|
|
uint32_t word = cpu->read_word(aligned);
|
|
switch (addr & 3u) {
|
|
case 0: word = (word & 0xFFFFFF00u) | (value >> 24); break;
|
|
case 1: word = (word & 0xFFFF0000u) | (value >> 16); break;
|
|
case 2: word = (word & 0xFF000000u) | (value >> 8); break;
|
|
default: word = value; break;
|
|
}
|
|
cpu->write_word(aligned, word);
|
|
}
|
|
|
|
static inline void interp_swr(CPUState *cpu, uint32_t addr, uint32_t value) {
|
|
uint32_t aligned = addr & ~3u;
|
|
uint32_t word = cpu->read_word(aligned);
|
|
switch (addr & 3u) {
|
|
case 0: word = value; break;
|
|
case 1: word = (word & 0x000000FFu) | (value << 8); break;
|
|
case 2: word = (word & 0x0000FFFFu) | (value << 16); break;
|
|
default: word = (word & 0x00FFFFFFu) | (value << 24); break;
|
|
}
|
|
cpu->write_word(aligned, word);
|
|
}
|
|
|
|
/* Soft-fail thread-local flag. When the interpreter encounters an opcode
|
|
* it doesn't implement, it sets this flag and returns instead of aborting,
|
|
* letting the caller (psx_dispatch via dirty_ram_dispatch) fall back to
|
|
* psx_unknown_dispatch — which has its own ad-hoc resolver for known
|
|
* trampoline patterns (jr-based vector dispatch, etc.).
|
|
*
|
|
* This is a deliberate retreat from "always pick the most complete option"
|
|
* for ONE narrow case: dispatch into pages that have been written-to but
|
|
* don't actually contain valid stub code at the dispatched PC (e.g.
|
|
* stale data, return-target addresses that point to non-code areas). The
|
|
* pre-existing psx_unknown_dispatch already handles those — we just need
|
|
* to let it. If a true install stub uses an opcode we don't have, this
|
|
* will silently route it to psx_unknown_dispatch, which will likely return
|
|
* a no-op cpu->pc=0. When that happens, we'll see "card protocol stalls"
|
|
* in measurement and add the missing opcode here. */
|
|
static int g_unsupported_seen = 0;
|
|
static uint32_t g_unsupported_pc = 0;
|
|
static uint32_t g_unsupported_insn = 0;
|
|
static const char *g_unsupported_reason = NULL;
|
|
|
|
static int abort_unsupported(uint32_t pc, uint32_t insn, const char *reason) {
|
|
g_dirty_ram_aborts++;
|
|
g_unsupported_seen = 1;
|
|
g_unsupported_pc = pc;
|
|
g_unsupported_insn = insn;
|
|
g_unsupported_reason = reason;
|
|
return 1; /* signal "control transferred" so the caller stops */
|
|
}
|
|
|
|
/* A dirty page that should run as a locally-chained overlay: anything ABOVE the
|
|
* kernel window. The kernel window [0, DIRTY_RAM_KERNEL_WINDOW_END) intentionally
|
|
* stays per-block (it runs in exception context, where interrupt-check cadence and
|
|
* EPC handling are delicate — see dirty_ram_interp.h window note; kernel native
|
|
* coverage comes from overlay_loader candidates, not interp chaining).
|
|
*
|
|
* NOTE this is the kernel-window end, NOT OVERLAY_REGION_FLOOR. The floor is the
|
|
* end of the boot-EXE text, and the original design assumed [0x10000, FLOOR) is
|
|
* immutable statically-recompiled text. That assumption is FALSE for games that
|
|
* discard their boot/init code and load gameplay overlays OVER the boot-text region
|
|
* (Tomba 2: a START.BIN-loader overlay at 0x8001Dxxx, well below its 0x38800 floor).
|
|
* Such a page is dirty (RAM != compiled image), so dispatching to it must interpret
|
|
* the live overlay and chain locally — exactly like the [FLOOR, RAM_SIZE) overlay
|
|
* region — NOT run the stale compiled image or route through the bail-prone
|
|
* non-local-call contract (the Whoopee-Camp splash wild-jr). dirty_ram_is_dirty()
|
|
* keeps clean boot text on the fast compiled path; only overwritten pages divert. */
|
|
static inline int phys_is_overlay_flow_region(uint32_t phys) {
|
|
return phys >= DIRTY_RAM_KERNEL_WINDOW_END;
|
|
}
|
|
|
|
static int is_local_dirty_target(uint32_t target) {
|
|
uint32_t phys = target & 0x1FFFFFFFu;
|
|
return phys_is_overlay_flow_region(phys) && dirty_ram_is_dirty(phys);
|
|
}
|
|
|
|
/* Target the last interp run handed back to the dispatch loop (chained
|
|
* continuation). Consumed by the next dirty dispatch to tell apart
|
|
* external entries (from native code) from the interpreter's own
|
|
* block-to-block chaining. */
|
|
static uint32_t g_dirty_interp_chain_target = 0;
|
|
|
|
/* A dirty-RAM target only deserves interpretation if its first word decodes
|
|
* as a plausible MIPS instruction. A scatter-loaded overlay leaves data
|
|
* bytes in dirty pages; jumping into those and interpreting them as code is
|
|
* how the original cache build produced black/blue screens. When the target
|
|
* word doesn't look like code, fall back to the normal dispatch path. */
|
|
static int dirty_ram_word_looks_decodable(uint32_t insn) {
|
|
if (insn == 0xFFFFFFFFu || insn == 0xFFFFFFFDu) return 0;
|
|
|
|
uint32_t op = op_field(insn);
|
|
uint32_t fn = funct_field(insn);
|
|
uint32_t rt = rt_field(insn);
|
|
|
|
if (op == 0x00u) {
|
|
switch (fn) {
|
|
case 0x00u: case 0x02u: case 0x03u: case 0x04u:
|
|
case 0x06u: case 0x07u: case 0x08u: case 0x09u:
|
|
case 0x0Cu: case 0x0Du:
|
|
case 0x10u: case 0x11u: case 0x12u: case 0x13u:
|
|
case 0x18u: case 0x19u: case 0x1Au: case 0x1Bu:
|
|
case 0x20u: case 0x21u: case 0x22u: case 0x23u:
|
|
case 0x24u: case 0x25u: case 0x26u: case 0x27u:
|
|
case 0x2Au: case 0x2Bu:
|
|
return 1;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
if (op == 0x01u) {
|
|
return rt == 0x00u || rt == 0x01u || rt == 0x10u || rt == 0x11u;
|
|
}
|
|
|
|
switch (op) {
|
|
case 0x02u: case 0x03u: case 0x04u: case 0x05u:
|
|
case 0x06u: case 0x07u:
|
|
case 0x08u: case 0x09u: case 0x0Au: case 0x0Bu:
|
|
case 0x0Cu: case 0x0Du: case 0x0Eu: case 0x0Fu:
|
|
case 0x10u: case 0x12u:
|
|
case 0x20u: case 0x21u: case 0x22u: case 0x23u:
|
|
case 0x24u: case 0x25u: case 0x26u:
|
|
case 0x28u: case 0x29u: case 0x2Au: case 0x2Bu:
|
|
case 0x2Eu:
|
|
case 0x30u: case 0x32u: case 0x38u: case 0x3Au:
|
|
return 1;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static int dirty_ram_same_pc(uint32_t a, uint32_t b) {
|
|
return (((a ^ b) & 0x1FFFFFFFu) == 0);
|
|
}
|
|
|
|
static int dirty_ram_pump_boundary(CPUState *cpu, uint32_t committed_pc, int site) {
|
|
uint32_t prev_safe = g_dirty_safe_resume_pc;
|
|
int prev_site = g_cosim_dirty_pump_site;
|
|
|
|
s_last_dirty_irq_pump_insns = g_dirty_ram_insns_run;
|
|
if (!psx_interrupts_checked_at_current_cycle(committed_pc)) {
|
|
g_dirty_safe_resume_pc = committed_pc;
|
|
g_cosim_dirty_pump_site = site;
|
|
psx_check_interrupts(cpu);
|
|
g_dirty_safe_resume_pc = prev_safe;
|
|
g_cosim_dirty_pump_site = prev_site;
|
|
}
|
|
|
|
if (g_psx_call_bail) return 1;
|
|
if (cpu->pc == 0u && committed_pc != 0u) {
|
|
cpu->pc = committed_pc;
|
|
g_async_rfe_fire_count++;
|
|
}
|
|
if (cpu->pc != 0u && !dirty_ram_same_pc(cpu->pc, committed_pc)) {
|
|
return 1;
|
|
}
|
|
|
|
cpu->pc = committed_pc;
|
|
return 0;
|
|
}
|
|
|
|
static int dirty_ram_finish_call_return(CPUState *cpu, uint32_t return_pc,
|
|
uint32_t *next_pc_out) {
|
|
uint32_t prev_pc = cpu->pc;
|
|
cpu->pc = return_pc;
|
|
if (dirty_ram_pump_boundary(cpu, return_pc, 5)) return 1;
|
|
cpu->pc = prev_pc;
|
|
*next_pc_out = return_pc;
|
|
return 0;
|
|
}
|
|
|
|
static int dispatch_nonlocal_call(CPUState *cpu, uint32_t target,
|
|
uint32_t return_pc,
|
|
uint32_t *next_pc_out) {
|
|
cpu->pc = 0;
|
|
psx_dispatch_call(cpu, target, return_pc);
|
|
/* psx_dispatch_call validated the (return_pc, sp) contract; a bail
|
|
* unwind in progress surfaces with cpu->pc = the guest's true target. */
|
|
if (g_psx_call_bail) return 1;
|
|
if (cpu->pc != 0) return 1;
|
|
return dirty_ram_finish_call_return(cpu, return_pc, next_pc_out);
|
|
}
|
|
|
|
/* ── Mixed interp<->compiled dispatch owner (host-stack recursion fix) ──────
|
|
* The dirty interpreter nests psx_dispatch_game_compiled when an interpreted
|
|
* block transfers into compiled code. A guest tail-dispatch loop that crosses the
|
|
* boundary (an interpreted overlay <-> the main-EXE per-frame loop func_8001A954)
|
|
* grows the HOST stack unboundedly while the GUEST stack stays flat — the long-run
|
|
* freeze (docs/RECURSION_BUG.md). The compiled side keeps tail-call loops flat via
|
|
* the psx_dispatch_impl trampoline + g_psx_call_bail unwind; the interpreter
|
|
* bypassed it by nesting directly.
|
|
*
|
|
* Fix: when an interp->compiled crossing would push the HOST stack past a safe
|
|
* watermark, publish the target as a wild-flow bail instead of nesting. The
|
|
* existing contract (compiled frames honor `if (g_psx_call_bail) return;`) unwinds
|
|
* it to the OUTERMOST psx_dispatch_impl, whose flatten path clears the bail and
|
|
* re-dispatches cpu->pc in its loop — so the mutual tail-recursion iterates with
|
|
* bounded native depth. Runtime-only (reuses the existing bail + flatten; no regen).
|
|
*
|
|
* The trigger is the ACTUAL host-stack usage (TEB StackBase - rsp), NOT a nesting
|
|
* counter. A counter LEAKS across the fiber exception path (psx_exception_longjmp
|
|
* never returns, so interp_enter_compiled's post-call decrement is skipped) and
|
|
* creeps up until it falsely trips on a benign call — observed as the FIRST dialogue
|
|
* freezing. Stack usage can't leak and is the real resource we protect: legitimate
|
|
* code (boot, dialogue, deep-but-bounded recursion) stays far below the watermark;
|
|
* only the runaway approaches it. Toggle off with PSX_MIXED_OWNER=0; watermark via
|
|
* PSX_MIXED_STACK_KB (default 700; fiber ~1MB, native-stack guard ~768KB used). */
|
|
static int psx_mixed_owner_enabled(void) {
|
|
static int v = -1;
|
|
if (v < 0) { const char *e = getenv("PSX_MIXED_OWNER"); v = (e && e[0] == '0') ? 0 : 1; }
|
|
return v;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
#include <intrin.h> /* __readgsqword — fiber TEB StackBase */
|
|
static size_t interp_host_stack_used(void) {
|
|
char probe;
|
|
uintptr_t base = (uintptr_t)__readgsqword(0x08); /* TEB StackBase (high end) */
|
|
uintptr_t sp = (uintptr_t)&probe;
|
|
return (base > sp) ? (size_t)(base - sp) : 0;
|
|
}
|
|
#else
|
|
static size_t interp_host_stack_used(void) { return 0; }
|
|
#endif
|
|
|
|
static size_t psx_mixed_stack_watermark(void) {
|
|
static long v = -1;
|
|
if (v < 0) { const char *e = getenv("PSX_MIXED_STACK_KB");
|
|
v = (e && *e) ? atol(e) : 700; if (v < 64) v = 64; }
|
|
return (size_t)v * 1024u;
|
|
}
|
|
|
|
/* ── Flight recorder: per-frame count of the re-entry edge interp->0x8001A954 ──
|
|
* (RECURSION_BUG.md §15). The long-run freeze is a per-frame update that, at
|
|
* ~frame 50k, re-enters func_8001A954 (the per-frame loop head) unboundedly via
|
|
* an interpreted overlay. Record, per emulated frame, how many times that edge
|
|
* fires + the cycle counter, in a ring the crash report dumps. Answers: is the
|
|
* re-entry ORDINARY bounded per-frame behavior that stops TERMINATING at ~50k
|
|
* (count goes unbounded), or a brand-new edge — and whether cycles advance.
|
|
* Only the INTERP->compiled re-entry passes through here; the normal compiled
|
|
* per-frame entry to func_8001A954 does not, so this isolates the suspect edge. */
|
|
extern uint64_t psx_cycle_count;
|
|
#define SITE_CAP 32u /* 3 sites x 256 overflowed crash_trace's 8KB sub-buffer; 32 frames of lead-up is plenty */
|
|
typedef struct { uint32_t frame; uint32_t count; uint64_t cycle; } SiteEntry;
|
|
typedef struct { SiteEntry ring[SITE_CAP]; uint64_t seq; uint32_t cur;
|
|
uint32_t last_frame; uint32_t maxf; } SiteRec;
|
|
/* Per-frame dispatch-count recorders for the suspect loop sites (RECURSION_BUG.md
|
|
* §15). g_site_interp (interp->0x8001A954) was found ORDINARY (1/frame); the
|
|
* recursion re-dispatches the compiled loop via dirty_ram_dispatch's entry because
|
|
* the CD-DMA dirty bitmap is saturated. So record dirty_ram_dispatch(0x8001A954)
|
|
* = loop head and dirty_ram_dispatch(0x80046264) = loop tail. Bounded normally,
|
|
* spike at the freeze => "the per-frame loop stopped terminating." */
|
|
static SiteRec g_site_interp = { .last_frame = 0xFFFFFFFFu }; /* interp -> 0x8001A954 */
|
|
static SiteRec g_site_dd954 = { .last_frame = 0xFFFFFFFFu }; /* dirty_ram_dispatch(0x8001A954) */
|
|
static SiteRec g_site_dd264 = { .last_frame = 0xFFFFFFFFu }; /* dirty_ram_dispatch(0x80046264) */
|
|
|
|
static void site_note(SiteRec *s) {
|
|
uint32_t f = (uint32_t)s_frame_count;
|
|
if (f != s->last_frame) {
|
|
if (s->last_frame != 0xFFFFFFFFu) {
|
|
SiteEntry *e = &s->ring[s->seq++ & (SITE_CAP - 1u)];
|
|
e->frame = s->last_frame; e->count = s->cur; e->cycle = psx_cycle_count;
|
|
if (s->cur > s->maxf) s->maxf = s->cur;
|
|
}
|
|
s->cur = 0; s->last_frame = f;
|
|
}
|
|
s->cur++;
|
|
}
|
|
|
|
static int site_json(char *out, int cap, const char *name, const SiteRec *s) {
|
|
int n = snprintf(out, cap, " \"%s\": {\"max_per_frame\":%u,\"cur_frame\":%u,\"cur_count\":%u,\"history\":[",
|
|
name, s->maxf, s->last_frame, s->cur);
|
|
uint64_t total = s->seq;
|
|
int avail = (total < SITE_CAP) ? (int)total : (int)SITE_CAP;
|
|
uint64_t start = total - (uint64_t)avail;
|
|
for (int i = 0; i < avail && n < cap - 96; i++) {
|
|
const SiteEntry *e = &s->ring[(start + (uint64_t)i) & (SITE_CAP - 1u)];
|
|
n += snprintf(out + n, cap - n, "%s{\"f\":%u,\"n\":%u,\"cyc\":%llu}",
|
|
i ? "," : "", e->frame, e->count, (unsigned long long)e->cycle);
|
|
}
|
|
n += snprintf(out + n, cap - n, "]},\n");
|
|
return n;
|
|
}
|
|
|
|
/* Emit all three loop-site recorders as JSON (called by crash_trace). */
|
|
int dirty_ram_re954_json(char *out, int cap) {
|
|
int n = 0;
|
|
n += site_json(out + n, cap - n, "reentry_interp_8001A954", &g_site_interp);
|
|
n += site_json(out + n, cap - n, "dispatch_8001A954", &g_site_dd954);
|
|
n += site_json(out + n, cap - n, "dispatch_80046264", &g_site_dd264);
|
|
return n;
|
|
}
|
|
|
|
/* ── Boundary control-flow flight recorder (RECURSION_BUG.md §18) ──────────────
|
|
* The long-run freeze is a runaway interp↔compiled re-entry. To choose among the
|
|
* four shapes (gradual boundary leak / sudden same-frame spin / interpreter
|
|
* fabrication / real guest wait-loop) WITHOUT assuming any of them, record every
|
|
* interp→compiled crossing at BOTH nesting sites (interp_enter_compiled for guest
|
|
* JAL/JALR, and dirty_ram_dispatch_inner's psx_dispatch_game_compiled) plus the
|
|
* watched-set tail-transfers (J/JR to the loop addresses). Two always-on rings:
|
|
* - per-FRAME summary -> the time-series (does depth/crossings grow across
|
|
* frames, or explode in one?). Answers Q1/Q2/Q3.
|
|
* - per-CROSSING detail -> src_pc, decoded op, target, delay-slot insn, sp/ra,
|
|
* mixed depth, host-stack KB, I_STAT/I_MASK. Answers Q4.
|
|
* Trips EARLY (per-frame crossings or host-stack KB over a low threshold) so the
|
|
* detail ring still holds the ONSET (the transition frame's first crossings), not
|
|
* the 23k steady state that the terminal native-stack guard would capture. The
|
|
* dump routes through the normal crash report (xprobe_json), reusing the proven
|
|
* flush+halt+serve path. Watched set is flagged, but ALL crossings are recorded
|
|
* so the real driver address is not pre-assumed. NB: there is no due-cycle event
|
|
* scheduler in this build (psx_cycles.c), so "next scheduled event cycle" is N/A;
|
|
* cycles-advanced-per-frame + I_STAT/I_MASK cover event progress instead. */
|
|
extern uint32_t i_stat; /* owned by memory.c */
|
|
extern uint32_t i_mask;
|
|
|
|
enum { XOP_JAL = 0, XOP_JALR = 1, XOP_JR = 2, XOP_J = 3, XOP_DD = 4, XOP_BR = 5,
|
|
XOP_RES = 6 /* watched-target call RESOLUTION: site = path code (XRES_*),
|
|
ds_insn = v0 after the path ran */ };
|
|
enum { XSITE_INTERP = 0, XSITE_DD = 1 };
|
|
/* XOP_RES path codes (in the `site` field). */
|
|
enum { XRES_EC_BAIL = 2, XRES_EC_PC = 3, XRES_EC_CONTRACT = 4, XRES_EC_RET = 5,
|
|
XRES_OV_BAIL = 6, XRES_OV_PC = 7, XRES_OV_CONTRACT = 8, XRES_OV_RET = 9,
|
|
XRES_NONLOCAL = 10, XRES_PCCHAIN = 11, XRES_UNDECODABLE = 12 };
|
|
|
|
int g_mixed_depth = 0; /* best-effort interp→compiled nesting depth; reset per frame */
|
|
|
|
typedef struct {
|
|
uint32_t frame; uint64_t cycle; uint32_t src_pc; uint32_t target;
|
|
uint32_t ds_insn; uint32_t sp; uint32_t ra; uint32_t stk_kb;
|
|
uint32_t istat; uint32_t imask; uint16_t depth; uint8_t op; uint8_t site;
|
|
} XDetail;
|
|
typedef struct {
|
|
uint32_t frame; uint32_t crossings; uint32_t a954; int32_t depth_max;
|
|
uint32_t stk_max_kb; uint64_t cyc_advanced;
|
|
} XSum;
|
|
|
|
#define XDET_CAP 16384u
|
|
#define XSUM_CAP 512u
|
|
static XDetail g_xdet[XDET_CAP];
|
|
static XSum g_xsum[XSUM_CAP];
|
|
static uint64_t g_xdet_seq = 0, g_xsum_seq = 0;
|
|
|
|
/* per-frame accumulators */
|
|
static uint32_t s_xf_frame = 0xFFFFFFFFu;
|
|
static uint32_t s_xf_crossings = 0, s_xf_a954 = 0;
|
|
static int32_t s_xf_depth_max = 0;
|
|
static uint32_t s_xf_stk_max_kb = 0;
|
|
static uint64_t s_xf_cyc_start = 0;
|
|
|
|
/* first crossing where mixed depth became > 0 this run (Q1) */
|
|
static uint32_t s_xf_first_depth_frame = 0xFFFFFFFFu;
|
|
static uint64_t s_xf_first_depth_cycle = 0;
|
|
static int s_xprobe_tripped = 0;
|
|
|
|
/* Trip thresholds — DISABLED by default (0). Deep but LEGITIMATE boot stacks
|
|
* reach ~300KB host at crossing points (mixed-depth small) and would spuriously
|
|
* trip an absolute threshold. So arm at RUNTIME via the `xprobe_arm` TCP command
|
|
* once the game is idling and the normal per-frame baseline is known (idle stack
|
|
* is a flat ~5KB, so a threshold ~3x the idle crossing baseline / a stack KB well
|
|
* above 5KB then fires only on the runaway, with the onset still in the ring).
|
|
* warmup suppresses any trip before that frame regardless. Env overrides allow
|
|
* arming at launch (PSX_XPROBE_FRAME_TRIP / _STK_KB / _WARMUP). */
|
|
int g_xprobe_frame_trip = 0; /* per-frame crossings; 0 = disabled */
|
|
int g_xprobe_stk_kb = 0; /* host-stack KB; 0 = disabled */
|
|
int g_xprobe_warmup = 0; /* no trip before this frame */
|
|
static int g_xprobe_env_done = 0;
|
|
static void xprobe_env_init(void) {
|
|
if (g_xprobe_env_done) return;
|
|
g_xprobe_env_done = 1;
|
|
const char *a = getenv("PSX_XPROBE_FRAME_TRIP"); if (a && *a) g_xprobe_frame_trip = atoi(a);
|
|
const char *b = getenv("PSX_XPROBE_STK_KB"); if (b && *b) g_xprobe_stk_kb = atoi(b);
|
|
const char *c = getenv("PSX_XPROBE_WARMUP"); if (c && *c) g_xprobe_warmup = atoi(c);
|
|
}
|
|
void dirty_ram_xprobe_arm(int frame_trip, int stk_kb, int warmup) {
|
|
g_xprobe_frame_trip = frame_trip; g_xprobe_stk_kb = stk_kb; g_xprobe_warmup = warmup;
|
|
s_xprobe_tripped = 0; /* (re-)arm */
|
|
}
|
|
|
|
static void xprobe_flush_frame(void) {
|
|
if (s_xf_frame == 0xFFFFFFFFu) return;
|
|
XSum *s = &g_xsum[g_xsum_seq++ & (XSUM_CAP - 1u)];
|
|
s->frame = s_xf_frame; s->crossings = s_xf_crossings; s->a954 = s_xf_a954;
|
|
s->depth_max = s_xf_depth_max; s->stk_max_kb = s_xf_stk_max_kb;
|
|
s->cyc_advanced = psx_cycle_count - s_xf_cyc_start;
|
|
}
|
|
|
|
/* Record one boundary crossing. want_detail=1 for interp-site guest transfers
|
|
* (rich context), 0 for the dd-site (count + depth only). Runs the per-frame
|
|
* flush on frame change (leak-proof reset of g_mixed_depth) and the early trip. */
|
|
static int g_xprobe_watch(uint32_t t) {
|
|
return t == 0x8001A954u || t == 0x80046264u || t == 0x8004630Cu || t == 0x8004DFA0u
|
|
/* MMX6 card-load firstfile flow (mmx6_card_load_regression_state):
|
|
* the mount 0x8001C1AC (works) vs firstfile flow 0x8001C4C0 (its body
|
|
* never runs) — record every call event + resolution for both. */
|
|
|| t == 0x8001C1ACu || t == 0x8001C4C0u;
|
|
}
|
|
static void xprobe_event(uint32_t src_pc, uint8_t op, uint8_t site, uint32_t target,
|
|
uint32_t ds_insn, uint32_t sp, uint32_t ra, int want_detail);
|
|
/* Watched-target call note for NON-interp call sites (overlay shard call-outs
|
|
* via dispatch_call / psx_sljit_call). phase 0 = before the call (ds = a0),
|
|
* phase 1 = after it returned (ds = v0). Recorded as XOP_RES with
|
|
* site = 20+phase so the `watched` dump shows the shard-side call + result. */
|
|
void dirty_ram_xprobe_call_note(CPUState *cpu, uint32_t target, uint32_t ra, uint8_t phase) {
|
|
if (!g_xprobe_watch(target)) return;
|
|
xprobe_event(ra, XOP_RES, (uint8_t)(20u + phase), target,
|
|
phase ? cpu->gpr[2] : cpu->gpr[4], cpu->gpr[29], cpu->gpr[31], 1);
|
|
}
|
|
static void xprobe_event(uint32_t src_pc, uint8_t op, uint8_t site, uint32_t target,
|
|
uint32_t ds_insn, uint32_t sp, uint32_t ra, int want_detail) {
|
|
uint32_t f = (uint32_t)s_frame_count;
|
|
if (f != s_xf_frame) {
|
|
xprobe_flush_frame();
|
|
s_xf_frame = f; s_xf_crossings = 0; s_xf_a954 = 0; s_xf_depth_max = 0;
|
|
s_xf_stk_max_kb = 0; s_xf_cyc_start = psx_cycle_count;
|
|
g_mixed_depth = 0; /* shallow at frame top (proven ~5KB) — leak-proof reset */
|
|
}
|
|
s_xf_crossings++;
|
|
if (target == 0x8001A954u) s_xf_a954++;
|
|
if (g_mixed_depth > s_xf_depth_max) s_xf_depth_max = g_mixed_depth;
|
|
if (g_mixed_depth > 0 && s_xf_first_depth_frame == 0xFFFFFFFFu) {
|
|
s_xf_first_depth_frame = f; s_xf_first_depth_cycle = psx_cycle_count;
|
|
}
|
|
uint32_t stk = (uint32_t)(interp_host_stack_used() >> 10);
|
|
if (stk > s_xf_stk_max_kb) s_xf_stk_max_kb = stk;
|
|
|
|
if (want_detail || g_xprobe_watch(target)) {
|
|
XDetail *e = &g_xdet[g_xdet_seq++ & (XDET_CAP - 1u)];
|
|
e->frame = f; e->cycle = psx_cycle_count; e->src_pc = src_pc; e->target = target;
|
|
e->ds_insn = ds_insn; e->sp = sp; e->ra = ra; e->stk_kb = stk;
|
|
e->istat = i_stat; e->imask = i_mask;
|
|
e->depth = (uint16_t)(g_mixed_depth > 0xFFFF ? 0xFFFF : g_mixed_depth);
|
|
e->op = op; e->site = site;
|
|
}
|
|
|
|
xprobe_env_init();
|
|
if (!s_xprobe_tripped && f >= (uint32_t)g_xprobe_warmup &&
|
|
((g_xprobe_frame_trip > 0 && (int)s_xf_crossings > g_xprobe_frame_trip) ||
|
|
(g_xprobe_stk_kb > 0 && (int)stk > g_xprobe_stk_kb))) {
|
|
s_xprobe_tripped = 1;
|
|
extern void psx_fatal_halt(const char *reason);
|
|
psx_fatal_halt("xprobe: interp<->compiled boundary onset trip "
|
|
"(per-frame crossings/host-stack over armed threshold — see xprobe rings)");
|
|
}
|
|
}
|
|
|
|
/* Emit the flight-recorder rings as JSON (called by crash_trace). The detail dump
|
|
* is OLDEST-first so the transition (last normal frame -> first runaway crossings)
|
|
* is visible; the trip fires early enough that the onset is still in the ring. */
|
|
int dirty_ram_xprobe_json(char *out, int cap) {
|
|
xprobe_flush_frame(); /* fold the in-progress frame into the summary */
|
|
int n = snprintf(out, cap,
|
|
"{\n"
|
|
" \"tripped\": %d, \"trip_frame\": %u, \"mixed_depth_now\": %d,\n"
|
|
" \"first_depth_frame\": %u, \"first_depth_cycle\": %llu,\n"
|
|
" \"frame_trip\": %d, \"stk_kb_trip\": %d, \"warmup\": %d,\n",
|
|
s_xprobe_tripped, s_xf_frame, g_mixed_depth,
|
|
s_xf_first_depth_frame, (unsigned long long)s_xf_first_depth_cycle,
|
|
g_xprobe_frame_trip, g_xprobe_stk_kb, g_xprobe_warmup);
|
|
|
|
/* watched-target events FIRST (sparse, the highest-value content): the WHOLE
|
|
* ring filtered to g_xprobe_watch targets + every XOP_RES resolution record.
|
|
* Emitted before summary/detail so the 56KB response cap can never truncate
|
|
* it (it truncated to [] when this section trailed the detail dump). */
|
|
n += snprintf(out + n, cap - n, " \"watched\": [");
|
|
{
|
|
static const char *opn2[] = {"JAL","JALR","JR","J","DD","BR","RES"};
|
|
uint64_t total = g_xdet_seq;
|
|
uint32_t avail = total < XDET_CAP ? (uint32_t)total : XDET_CAP;
|
|
uint64_t base = total - avail;
|
|
int firstw = 1;
|
|
for (uint32_t i = 0; i < avail && n < cap - 240; i++) {
|
|
const XDetail *e = &g_xdet[(base + i) & (XDET_CAP - 1u)];
|
|
if (e->op != XOP_RES && !g_xprobe_watch(e->target)) continue;
|
|
n += snprintf(out + n, cap - n,
|
|
"%s{\"f\":%u,\"cyc\":%llu,\"op\":\"%s\",\"site\":%u,\"src\":\"0x%08X\","
|
|
"\"tgt\":\"0x%08X\",\"ds\":\"0x%08X\",\"sp\":\"0x%08X\",\"ra\":\"0x%08X\","
|
|
"\"d\":%u,\"stk\":%u}",
|
|
firstw ? "" : ",", e->frame, (unsigned long long)e->cycle,
|
|
e->op < 7 ? opn2[e->op] : "?", e->site, e->src_pc, e->target, e->ds_insn,
|
|
e->sp, e->ra, e->depth, e->stk_kb);
|
|
firstw = 0;
|
|
}
|
|
}
|
|
n += snprintf(out + n, cap - n, "],\n");
|
|
|
|
/* per-frame summary (whole window in the ring) */
|
|
n += snprintf(out + n, cap - n, " \"summary\": [");
|
|
{
|
|
uint64_t total = g_xsum_seq;
|
|
uint32_t avail = total < XSUM_CAP ? (uint32_t)total : XSUM_CAP;
|
|
uint64_t start = total - avail;
|
|
for (uint32_t i = 0; i < avail && n < cap - 160; i++) {
|
|
const XSum *s = &g_xsum[(start + i) & (XSUM_CAP - 1u)];
|
|
n += snprintf(out + n, cap - n,
|
|
"%s{\"f\":%u,\"cr\":%u,\"a954\":%u,\"dmax\":%d,\"stk\":%u,\"cyc\":%llu}",
|
|
i ? "," : "", s->frame, s->crossings, s->a954, s->depth_max,
|
|
s->stk_max_kb, (unsigned long long)s->cyc_advanced);
|
|
}
|
|
}
|
|
n += snprintf(out + n, cap - n, "],\n");
|
|
|
|
/* per-crossing detail, CENTERED on the focus frame (trip frame, or the current
|
|
* frame when live): dump from ~24 crossings before the focus frame's first
|
|
* crossing forward, so the transition (last normal frame -> runaway onset) is
|
|
* always captured regardless of ring fill. */
|
|
n += snprintf(out + n, cap - n, " \"detail\": [");
|
|
{
|
|
static const char *opn[] = {"JAL","JALR","JR","J","DD","BR"};
|
|
uint64_t total = g_xdet_seq;
|
|
uint32_t avail = total < XDET_CAP ? (uint32_t)total : XDET_CAP;
|
|
uint64_t base = total - avail; /* oldest live entry */
|
|
uint32_t focus = s_xf_frame;
|
|
uint32_t first = avail; /* first index of the focus frame */
|
|
for (uint32_t i = 0; i < avail; i++) {
|
|
if (g_xdet[(base + i) & (XDET_CAP - 1u)].frame == focus) { first = i; break; }
|
|
}
|
|
uint32_t starto = (first == avail) ? (avail > 24u ? avail - 24u : 0u)
|
|
: (first > 24u ? first - 24u : 0u);
|
|
int firstdump = 1;
|
|
for (uint32_t i = starto; i < avail && n < cap - 240; i++) {
|
|
const XDetail *e = &g_xdet[(base + i) & (XDET_CAP - 1u)];
|
|
n += snprintf(out + n, cap - n,
|
|
"%s{\"f\":%u,\"cyc\":%llu,\"op\":\"%s\",\"site\":%u,\"src\":\"0x%08X\","
|
|
"\"tgt\":\"0x%08X\",\"ds\":\"0x%08X\",\"sp\":\"0x%08X\",\"ra\":\"0x%08X\","
|
|
"\"d\":%u,\"stk\":%u,\"ist\":\"0x%X\",\"imk\":\"0x%X\"}",
|
|
firstdump ? "" : ",", e->frame, (unsigned long long)e->cycle,
|
|
e->op < 6 ? opn[e->op] : "?", e->site, e->src_pc, e->target, e->ds_insn,
|
|
e->sp, e->ra, e->depth, e->stk_kb, e->istat, e->imask);
|
|
firstdump = 0;
|
|
}
|
|
}
|
|
n += snprintf(out + n, cap - n, "]\n }");
|
|
return n;
|
|
}
|
|
|
|
/* Enter compiled code from the interpreter for a guest control transfer. Returns
|
|
* like psx_dispatch_game_compiled (1 = handled, OR a stack-watermark bail was
|
|
* surfaced; 0 = not a compiled target — caller falls through to overlay/dirty/
|
|
* nonlocal paths). On surface: cpu->pc = target and g_psx_call_bail set.
|
|
*
|
|
* Only present when a game dispatch table is linked (PSX_HAS_GAME_DISPATCH).
|
|
* In the BIOS-only build psx_dispatch_game_compiled does not exist, and the
|
|
* two callers below are themselves gated behind the same macro. */
|
|
#ifdef PSX_HAS_GAME_DISPATCH
|
|
static int interp_enter_compiled(CPUState *cpu, uint32_t target) {
|
|
if (target == 0x8001A954u) site_note(&g_site_interp);
|
|
/* Decline when the target page no longer matches the static game image.
|
|
* Returning 0 lets the JAL/JALR handler fall through to local-flow interp
|
|
* of the live RAM bytes instead of running stale compiled code. */
|
|
if (!dirty_ram_text_native_ok(target & 0x1FFFFFFFu)) return 0;
|
|
if (psx_mixed_owner_enabled()
|
|
&& interp_host_stack_used() > psx_mixed_stack_watermark()) {
|
|
cpu->pc = target;
|
|
g_psx_call_bail = 1;
|
|
return 1;
|
|
}
|
|
g_mixed_depth++;
|
|
ls_func_enter(target, cpu);
|
|
int r = psx_dispatch_game_compiled(cpu, target);
|
|
ls_func_exit(target, cpu, r);
|
|
g_mixed_depth--;
|
|
return r;
|
|
}
|
|
#endif /* PSX_HAS_GAME_DISPATCH */
|
|
|
|
/* Execute ONE instruction at *pc on the given CPU state. Returns:
|
|
* 0 = continue (advance pc by 4)
|
|
* 1 = control transferred OR unsupported opcode (caller checks
|
|
* g_unsupported_seen to distinguish).
|
|
* Branches encode their delay slot themselves before returning 1. */
|
|
static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out);
|
|
|
|
/* Forward: helper for delay-slot execution on jumps/branches. */
|
|
static void exec_delay_slot(CPUState *cpu, uint32_t pc) {
|
|
/* Delay-slot instruction at pc must NOT be a control transfer.
|
|
* Recursively interpret as a single non-branching instruction. */
|
|
uint32_t ds_phys = pc & 0x1FFFFFFFu;
|
|
uint32_t insn = fetch_word(ds_phys);
|
|
uint32_t opc = op_field(insn);
|
|
uint32_t fnt = funct_field(insn);
|
|
/* Reject branches/jumps in delay slots — undefined on R3000A and our
|
|
* static recompiler explicitly handles this case differently (the
|
|
* fall-through fix from 2026-04-21). In install stubs, delay slots
|
|
* are always nop or simple arithmetic. */
|
|
if (opc == 0x02 /*j*/ || opc == 0x03 /*jal*/ ||
|
|
opc == 0x04 /*beq*/ || opc == 0x05 /*bne*/ ||
|
|
opc == 0x06 /*blez*/ || opc == 0x07 /*bgtz*/ ||
|
|
opc == 0x01 /*regimm*/ ||
|
|
(opc == 0x00 && (fnt == 0x08 /*jr*/ || fnt == 0x09 /*jalr*/))) {
|
|
(void)abort_unsupported(pc, insn, "control-transfer in delay slot");
|
|
return;
|
|
}
|
|
uint32_t dummy_next = 0;
|
|
(void)exec_one(cpu, pc, &dummy_next);
|
|
g_dirty_ram_insns_run++;
|
|
/* CYCLE MODEL: the delay-slot instruction is a real retired R3000A instruction
|
|
* and is charged its own per-instruction interlock INSIDE exec_one (top-of-fn
|
|
* §1+deps+DO_LDS, or psx_cyc_load_* for a load delay slot) — so a branch+slot
|
|
* pair costs both, matching hardware. No separate charge here. */
|
|
}
|
|
|
|
static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
|
exec_pc_table_record(pc);
|
|
uint32_t phys = pc & 0x1FFFFFFFu;
|
|
uint32_t insn = fetch_word(phys);
|
|
uint32_t opc = op_field(insn);
|
|
uint32_t rs = rs_field(insn);
|
|
uint32_t rt = rt_field(insn);
|
|
uint32_t rd = rd_field(insn);
|
|
uint32_t sh = shamt_field(insn);
|
|
uint32_t fnt = funct_field(insn);
|
|
int32_t simm = simm16_field(insn);
|
|
uint32_t imm = imm16_field(insn);
|
|
|
|
*next_pc_out = pc + 4;
|
|
|
|
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
|
/* Instruction FETCH cost (I-cache) — charged FIRST, before the §1 base, exactly
|
|
* like Beetle ReadInstruction precedes the per-instruction base (cpu.cpp). HIT=+0,
|
|
* KSEG1=+4, cached miss=+3+refill; a miss also clears the load give-back. */
|
|
psx_icache_fetch(cpu, pc);
|
|
|
|
/* Per-instruction R3000A load-delay interlock (single-source: psx_cyc.h, shared
|
|
* with both static emitters). §1 base + GPR_DEPRES + DO_LDS run HERE, before the
|
|
* instruction body, so §1 precedes any muldiv/GTE deadline stall the body applies
|
|
* (Beetle order). CPU loads (op 0x20-0x26) are skipped here — psx_cyc_load_* runs
|
|
* their full interlock inside the body (and arms LDWhich=rt). This replaces the
|
|
* old flat per-instruction psx_advance_cycles(psx_instr_base_cycles). */
|
|
if (!(opc >= 0x20u && opc <= 0x26u))
|
|
psx_cyc_step(cpu, psx_cyc_dep_res_mask(insn));
|
|
#endif
|
|
|
|
/* Update last-store PC tracker so SIO PC tracer attribution stays
|
|
* coherent through interpreted stubs. */
|
|
g_debug_last_store_pc = pc;
|
|
|
|
/* Widescreen far-backdrop column PRELOAD (auto_backdrop). At a detected
|
|
* window START/END finalize, force the loop bound (START->0 / END->sentinel)
|
|
* so the generator's own clamps preload the WHOLE finite tile row into the
|
|
* revealed 16:9 margin. The site is a move/addiu that writes exactly one GPR
|
|
* and has no other effect, so substituting the value and advancing pc+4 is
|
|
* complete. Gated on the runtime predicate first => 4:3 pays nothing. Each
|
|
* rewrite is recorded to the always-on backdrop ring with the live extent
|
|
* (s7), camera-X (scratchpad 0x176) and DL count for `ws_backdrop_ring`. */
|
|
/* Opcode pre-filter FIRST: only addu/or/addi/addiu can be a rewrite site
|
|
* (the detector below rewrites exactly one GPR-writing move/addiu). The old
|
|
* order ran the ±512-byte site scan for EVERY interpreted instruction; with
|
|
* the small direct-mapped cache thrashing, that was the mechanism of the
|
|
* native-wide 16:9 CPU collapse (Tomba2 attract 12fps, stack-sampled). */
|
|
if (((opc == 0x00u && (fnt == 0x21u || fnt == 0x25u)) ||
|
|
opc == 0x09u || opc == 0x08u) &&
|
|
psx_ws_backdrop_preload()) {
|
|
int wcols = 0;
|
|
int bk = ws_backdrop_site_kind(pc, &wcols);
|
|
if (bk != WS_BD_NONE) {
|
|
uint32_t dest = (opc == 0x00u && (fnt == 0x21u || fnt == 0x25u)) ? rd
|
|
: (opc == 0x09u || opc == 0x08u) ? rt
|
|
: 0xFFFFFFFFu;
|
|
if (dest != 0xFFFFFFFFu) {
|
|
/* orig = the instruction's normal result, so the widen shifts the
|
|
* real camera-tracked bound (move: gpr[src]; addiu: gpr[rs]+simm). */
|
|
uint32_t orig = (opc == 0x00u)
|
|
? ((rt == 0u) ? cpu->gpr[rs] : cpu->gpr[rt])
|
|
: (cpu->gpr[rs] + (uint32_t)simm);
|
|
/* Tell psx_ws_backdrop_value the interp will record the rich ring
|
|
* entry (it skips its own note when this flag is set). */
|
|
g_ws_bd_from_interp = 1;
|
|
uint32_t finalv = psx_ws_backdrop_value(orig, bk == WS_BD_END, wcols);
|
|
cpu->gpr[dest] = finalv;
|
|
cpu->gpr[0] = 0;
|
|
/* Rich snapshot for the ring: s7 (gpr[23]) = byte tile-row extent;
|
|
* s0 (gpr[16]) = DL object, count byte at +3; camera-X = scratchpad
|
|
* halfword 0x176. */
|
|
{
|
|
int extent = (int)(int16_t)cpu->gpr[23];
|
|
int camx = (int)(int16_t)(cpu->read_word(0x1F800174u) >> 16);
|
|
uint32_t s0a = cpu->gpr[16] + 3u;
|
|
int count = (int)((cpu->read_word(s0a & ~3u) >> ((s0a & 3u) * 8u)) & 0xFFu);
|
|
/* base = s4 (gpr[20]) = backdrop data ptr (extent@+0, table@+4);
|
|
* dl = s0 (gpr[16]) = the ordering-table object. */
|
|
uint32_t a1 = cpu->gpr[20];
|
|
psx_ws_backdrop_ring_note(pc, bk, wcols, orig, finalv, extent, camx,
|
|
count, a1, cpu->gpr[16]);
|
|
/* Publish the backdrop structure's address range so the GL
|
|
* 2D-stretch gate can match tile prims by gp0_cmd_source_addr.
|
|
* Tiles live at a1 + table[col]; bound by the LAST table entry
|
|
* (table is ascending) + packet slack. */
|
|
if (extent > 0 && extent <= 256) {
|
|
extern uint32_t g_ws_backdrop_lo, g_ws_backdrop_hi;
|
|
uint32_t tbl_last = cpu->read_word(a1 + 4u + (uint32_t)(extent - 1) * 4u);
|
|
g_ws_backdrop_lo = a1;
|
|
g_ws_backdrop_hi = a1 + tbl_last + 0x400u;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (opc) {
|
|
case 0x00: /* SPECIAL */
|
|
switch (fnt) {
|
|
case 0x00: /* SLL rd, rt, sh (also nop when all fields are 0) */
|
|
cpu->gpr[rd] = cpu->gpr[rt] << sh;
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x02: /* SRL */
|
|
cpu->gpr[rd] = cpu->gpr[rt] >> sh;
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x03: /* SRA */
|
|
cpu->gpr[rd] = (uint32_t)((int32_t)cpu->gpr[rt] >> sh);
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x04: /* SLLV */
|
|
cpu->gpr[rd] = cpu->gpr[rt] << (cpu->gpr[rs] & 31);
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x06: /* SRLV */
|
|
cpu->gpr[rd] = cpu->gpr[rt] >> (cpu->gpr[rs] & 31);
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x07: /* SRAV */
|
|
cpu->gpr[rd] = (uint32_t)((int32_t)cpu->gpr[rt] >> (cpu->gpr[rs] & 31));
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x08: { /* JR rs */
|
|
uint32_t target = cpu->gpr[rs];
|
|
exec_delay_slot(cpu, pc + 4);
|
|
cosim_exec_one_transfer_hook(pc + 4);
|
|
/* crossing (if target is compiled) is counted at the block-loop
|
|
* tail-transfer site (interp_enter_compiled, §18) — not here, to
|
|
* avoid double-counting J/JR-to-compiled. */
|
|
cpu->pc = target;
|
|
return 1;
|
|
}
|
|
case 0x09: { /* JALR rd, rs */
|
|
uint32_t target = cpu->gpr[rs];
|
|
uint32_t return_pc = pc + 8;
|
|
cpu->gpr[rd ? rd : 31] = return_pc;
|
|
cpu->gpr[0] = 0;
|
|
exec_delay_slot(cpu, pc + 4);
|
|
cosim_exec_one_transfer_hook(pc + 4);
|
|
uint32_t site_sp = cpu->gpr[29]; /* call contract: sp at the call */
|
|
xprobe_event(pc, XOP_JALR, XSITE_INTERP, target,
|
|
fetch_word((pc + 4) & 0x1FFFFFFFu), site_sp, cpu->gpr[31], 1);
|
|
if (g_precise_mode || g_ls_replay_active) { cpu->pc = target; return 1; } /* slice / lockstep-replay: plain transfer, never execute the callee */
|
|
#ifdef PSX_HAS_GAME_DISPATCH
|
|
cpu->pc = 0;
|
|
if (interp_enter_compiled(cpu, target)) {
|
|
if (g_psx_call_bail) return 1; /* wild unwind: cpu->pc = true target */
|
|
if (cpu->pc != 0) return 1;
|
|
if (rd == 0 || rd == 31) {
|
|
if (psx_call_contract(cpu, return_pc, site_sp)) return 1;
|
|
}
|
|
return dirty_ram_finish_call_return(cpu, return_pc, next_pc_out);
|
|
}
|
|
#endif
|
|
/* Native overlay candidates get the SAME call contract as
|
|
* statically-compiled callees: run as a unit, resume at
|
|
* return_pc. A bare pc-chain here loses the return obligation
|
|
* when the callee runs natively (C return, pc==0) — the dispatch
|
|
* loop unwinds past the suspended caller, leaking its frame. */
|
|
{
|
|
extern int overlay_loader_call_native(CPUState *cpu, uint32_t addr);
|
|
cpu->pc = 0;
|
|
if (overlay_loader_call_native(cpu, target)) {
|
|
if (g_psx_call_bail) return 1;
|
|
if (cpu->pc != 0) return 1;
|
|
if (rd == 0 || rd == 31) {
|
|
if (psx_call_contract(cpu, return_pc, site_sp)) return 1;
|
|
}
|
|
return dirty_ram_finish_call_return(cpu, return_pc, next_pc_out);
|
|
}
|
|
}
|
|
if (!is_local_dirty_target(target)) {
|
|
return dispatch_nonlocal_call(cpu, target, return_pc, next_pc_out);
|
|
}
|
|
cpu->pc = target;
|
|
return 1;
|
|
}
|
|
case 0x0C: /* SYSCALL */
|
|
cpu->pc = pc;
|
|
psx_syscall(cpu, (insn >> 6) & 0xFFFFFu);
|
|
return (cpu->pc != 0);
|
|
case 0x0D: /* BREAK */
|
|
psx_break(cpu, (insn >> 6) & 0xFFFFFu, pc);
|
|
return 1;
|
|
case 0x0F: /* SYNC */
|
|
return 0;
|
|
case 0x10: /* MFHI */
|
|
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
|
psx_muldiv_stall(cpu); /* stall to mult/div completion (faithful) */
|
|
#endif
|
|
cpu->gpr[rd] = cpu->hi;
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x11: /* MTHI */
|
|
cpu->hi = cpu->gpr[rs];
|
|
return 0;
|
|
case 0x12: /* MFLO */
|
|
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
|
psx_muldiv_stall(cpu); /* stall to mult/div completion (faithful) */
|
|
#endif
|
|
cpu->gpr[rd] = cpu->lo;
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x13: /* MTLO */
|
|
cpu->lo = cpu->gpr[rs];
|
|
return 0;
|
|
case 0x18: { /* MULT */
|
|
int64_t r = (int64_t)(int32_t)cpu->gpr[rs] * (int64_t)(int32_t)cpu->gpr[rt];
|
|
cpu->lo = (uint32_t)r;
|
|
cpu->hi = (uint32_t)((uint64_t)r >> 32);
|
|
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
|
psx_muldiv_set(cpu, psx_mult_latency_s(cpu->gpr[rs])); /* completion deadline */
|
|
#endif
|
|
return 0;
|
|
}
|
|
case 0x19: { /* MULTU */
|
|
uint64_t r = (uint64_t)cpu->gpr[rs] * (uint64_t)cpu->gpr[rt];
|
|
cpu->lo = (uint32_t)r;
|
|
cpu->hi = (uint32_t)(r >> 32);
|
|
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
|
psx_muldiv_set(cpu, psx_mult_latency_u(cpu->gpr[rs])); /* completion deadline */
|
|
#endif
|
|
return 0;
|
|
}
|
|
case 0x1A: { /* DIV */
|
|
int32_t a = (int32_t)cpu->gpr[rs];
|
|
int32_t b = (int32_t)cpu->gpr[rt];
|
|
if (b == 0) {
|
|
cpu->lo = (a < 0) ? 1u : 0xFFFFFFFFu;
|
|
cpu->hi = (uint32_t)a;
|
|
} else if ((uint32_t)a == 0x80000000u && b == -1) {
|
|
cpu->lo = 0x80000000u;
|
|
cpu->hi = 0;
|
|
} else {
|
|
cpu->lo = (uint32_t)(a / b);
|
|
cpu->hi = (uint32_t)(a % b);
|
|
}
|
|
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
|
psx_muldiv_set(cpu, 37u); /* DIV completion deadline (fixed) */
|
|
#endif
|
|
return 0;
|
|
}
|
|
case 0x1B: /* DIVU */
|
|
if (cpu->gpr[rt] == 0) {
|
|
cpu->lo = 0xFFFFFFFFu;
|
|
cpu->hi = cpu->gpr[rs];
|
|
} else {
|
|
cpu->lo = cpu->gpr[rs] / cpu->gpr[rt];
|
|
cpu->hi = cpu->gpr[rs] % cpu->gpr[rt];
|
|
}
|
|
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
|
psx_muldiv_set(cpu, 37u); /* DIVU completion deadline (fixed) */
|
|
#endif
|
|
return 0;
|
|
case 0x20: /* ADD - overflow traps are delegated if they occur. */
|
|
case 0x21: /* ADDU rd, rs, rt */
|
|
cpu->gpr[rd] = cpu->gpr[rs] + cpu->gpr[rt];
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x22: /* SUB - overflow traps are delegated if they occur. */
|
|
case 0x23: /* SUBU */
|
|
cpu->gpr[rd] = cpu->gpr[rs] - cpu->gpr[rt];
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x24: /* AND */
|
|
cpu->gpr[rd] = cpu->gpr[rs] & cpu->gpr[rt];
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x25: /* OR */
|
|
cpu->gpr[rd] = cpu->gpr[rs] | cpu->gpr[rt];
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x26: /* XOR */
|
|
cpu->gpr[rd] = cpu->gpr[rs] ^ cpu->gpr[rt];
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x27: /* NOR */
|
|
cpu->gpr[rd] = ~(cpu->gpr[rs] | cpu->gpr[rt]);
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x2A: /* SLT */
|
|
cpu->gpr[rd] = ((int32_t)cpu->gpr[rs] < (int32_t)cpu->gpr[rt]) ? 1u : 0u;
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x2B: /* SLTU */
|
|
cpu->gpr[rd] = (cpu->gpr[rs] < cpu->gpr[rt]) ? 1u : 0u;
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
default:
|
|
return abort_unsupported(pc, insn, "SPECIAL funct");
|
|
}
|
|
break;
|
|
|
|
case 0x02: { /* J target */
|
|
uint32_t target = ((pc + 4) & 0xF0000000u) | (target26(insn) << 2);
|
|
exec_delay_slot(cpu, pc + 4);
|
|
cosim_exec_one_transfer_hook(pc + 4);
|
|
/* crossing counted at the block-loop tail-transfer site (§18). */
|
|
cpu->pc = target;
|
|
return 1;
|
|
}
|
|
case 0x03: { /* JAL target */
|
|
uint32_t target = ((pc + 4) & 0xF0000000u) | (target26(insn) << 2);
|
|
uint32_t return_pc = pc + 8;
|
|
cpu->gpr[31] = return_pc;
|
|
exec_delay_slot(cpu, pc + 4);
|
|
cosim_exec_one_transfer_hook(pc + 4);
|
|
uint32_t site_sp = cpu->gpr[29]; /* call contract: sp at the call */
|
|
int xw = g_xprobe_watch(target); /* record RESOLUTION path for watched targets */
|
|
xprobe_event(pc, XOP_JAL, XSITE_INTERP, target,
|
|
fetch_word((pc + 4) & 0x1FFFFFFFu), site_sp, cpu->gpr[31], 1);
|
|
#define XRES(code) do { if (xw) xprobe_event(pc, XOP_RES, (uint8_t)(code), target, \
|
|
cpu->gpr[2], cpu->gpr[29], cpu->gpr[31], 1); } while (0)
|
|
if (g_precise_mode || g_ls_replay_active) { cpu->pc = target; return 1; } /* slice / lockstep-replay: plain transfer, never execute the callee */
|
|
#ifdef PSX_HAS_GAME_DISPATCH
|
|
cpu->pc = 0;
|
|
if (interp_enter_compiled(cpu, target)) {
|
|
if (g_psx_call_bail) { XRES(XRES_EC_BAIL); return 1; } /* wild unwind: cpu->pc = true target */
|
|
if (cpu->pc != 0) { XRES(XRES_EC_PC); return 1; }
|
|
if (psx_call_contract(cpu, return_pc, site_sp)) { XRES(XRES_EC_CONTRACT); return 1; }
|
|
XRES(XRES_EC_RET);
|
|
return dirty_ram_finish_call_return(cpu, return_pc, next_pc_out);
|
|
}
|
|
#endif
|
|
/* Native overlay candidates get the SAME call contract as statically-
|
|
* compiled callees: run as a unit, resume at return_pc. A bare
|
|
* pc-chain here loses the return obligation when the callee runs
|
|
* natively (C return, pc==0) — the dispatch loop unwinds past the
|
|
* suspended caller, leaking its frame (dwarf->overworld root cause:
|
|
* F=0x800338A8's epilogue skipped, sp leaked 0x18 per entity). */
|
|
{
|
|
extern int overlay_loader_call_native(CPUState *cpu, uint32_t addr);
|
|
cpu->pc = 0;
|
|
if (overlay_loader_call_native(cpu, target)) {
|
|
if (g_psx_call_bail) { XRES(XRES_OV_BAIL); return 1; }
|
|
if (cpu->pc != 0) { XRES(XRES_OV_PC); return 1; }
|
|
if (psx_call_contract(cpu, return_pc, site_sp)) { XRES(XRES_OV_CONTRACT); return 1; }
|
|
XRES(XRES_OV_RET);
|
|
return dirty_ram_finish_call_return(cpu, return_pc, next_pc_out);
|
|
}
|
|
}
|
|
if (!is_local_dirty_target(target)) {
|
|
XRES(XRES_NONLOCAL);
|
|
return dispatch_nonlocal_call(cpu, target, return_pc, next_pc_out);
|
|
}
|
|
if (!dirty_ram_word_looks_decodable(fetch_word(target & 0x1FFFFFFFu))) {
|
|
XRES(XRES_UNDECODABLE);
|
|
return dispatch_nonlocal_call(cpu, target, return_pc, next_pc_out);
|
|
}
|
|
XRES(XRES_PCCHAIN);
|
|
cpu->pc = target;
|
|
return 1;
|
|
#undef XRES
|
|
}
|
|
case 0x04: { /* BEQ rs, rt, simm */
|
|
int taken = (cpu->gpr[rs] == cpu->gpr[rt]);
|
|
exec_delay_slot(cpu, pc + 4);
|
|
cosim_exec_one_transfer_hook(pc + 4);
|
|
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
|
|
return 1;
|
|
}
|
|
case 0x05: { /* BNE */
|
|
int taken = (cpu->gpr[rs] != cpu->gpr[rt]);
|
|
exec_delay_slot(cpu, pc + 4);
|
|
cosim_exec_one_transfer_hook(pc + 4);
|
|
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
|
|
return 1;
|
|
}
|
|
case 0x06: { /* BLEZ */
|
|
int taken = ((int32_t)cpu->gpr[rs] <= 0);
|
|
exec_delay_slot(cpu, pc + 4);
|
|
cosim_exec_one_transfer_hook(pc + 4);
|
|
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
|
|
return 1;
|
|
}
|
|
case 0x07: { /* BGTZ */
|
|
int taken = ((int32_t)cpu->gpr[rs] > 0);
|
|
exec_delay_slot(cpu, pc + 4);
|
|
cosim_exec_one_transfer_hook(pc + 4);
|
|
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
|
|
return 1;
|
|
}
|
|
case 0x01: { /* REGIMM: BLTZ/BGEZ/BLTZAL/BGEZAL by rt field */
|
|
int taken;
|
|
switch (rt) {
|
|
case 0x00: /* BLTZ */ taken = ((int32_t)cpu->gpr[rs] < 0); break;
|
|
case 0x01: /* BGEZ */ taken = ((int32_t)cpu->gpr[rs] >= 0); break;
|
|
case 0x10: /* BLTZAL */ taken = ((int32_t)cpu->gpr[rs] < 0);
|
|
cpu->gpr[31] = pc + 8; break;
|
|
case 0x11: /* BGEZAL */ taken = ((int32_t)cpu->gpr[rs] >= 0);
|
|
cpu->gpr[31] = pc + 8; break;
|
|
default: return abort_unsupported(pc, insn, "REGIMM rt");
|
|
}
|
|
exec_delay_slot(cpu, pc + 4);
|
|
cosim_exec_one_transfer_hook(pc + 4);
|
|
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
|
|
return 1;
|
|
}
|
|
case 0x08: /* ADDI rt, rs, simm — same as ADDIU, sans overflow trap (we don't model traps here) */
|
|
cpu->gpr[rt] = cpu->gpr[rs] + (uint32_t)simm;
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x09: /* ADDIU rt, rs, simm */
|
|
cpu->gpr[rt] = cpu->gpr[rs] + (uint32_t)simm;
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x0A: /* SLTI */
|
|
cpu->gpr[rt] = ((int32_t)cpu->gpr[rs] < simm) ? 1u : 0u;
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x0B: /* SLTIU */
|
|
/* Widescreen render-funnel cull widening (auto_screen_x): always apply
|
|
* the shared helper for a flagged render-cull site — it is byte-identical
|
|
* to the vanilla compare at 4:3 (margin 0) and widens at 16:9, so the one
|
|
* code path serves both aspects (no widescreen-specific caching). */
|
|
if ((imm == 0x140 || imm == 0x141) && ws_cull_site(pc))
|
|
cpu->gpr[rt] = (uint32_t)psx_ws_cull_sltiu(cpu->gpr[rs], imm);
|
|
else
|
|
cpu->gpr[rt] = (cpu->gpr[rs] < (uint32_t)simm) ? 1u : 0u;
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x0C: /* ANDI */
|
|
cpu->gpr[rt] = cpu->gpr[rs] & imm;
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x0D: /* ORI */
|
|
cpu->gpr[rt] = cpu->gpr[rs] | imm;
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x0E: /* XORI */
|
|
cpu->gpr[rt] = cpu->gpr[rs] ^ imm;
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x0F: /* LUI rt, imm */
|
|
cpu->gpr[rt] = imm << 16;
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
case 0x10: { /* COP0 */
|
|
uint32_t cop_op = rs;
|
|
if (cop_op == 0x00) { /* MFC0 — delayed load (Beetle: LDAbsorb=0, LDWhich=rt) */
|
|
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
|
cpu->ld_absorb = 0u;
|
|
cpu->ld_which_t = (uint8_t)rt;
|
|
#endif
|
|
cpu->gpr[rt] = cpu->cop0[rd];
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
}
|
|
if (cop_op == 0x04) { /* MTC0 */
|
|
cpu->cop0[rd] = cpu->gpr[rt];
|
|
return 0;
|
|
}
|
|
if (cop_op == 0x10 && fnt == 0x10) { /* RFE */
|
|
uint32_t sr = cpu->cop0[12];
|
|
cpu->cop0[12] = (sr & 0xFFFFFFF0u) | ((sr >> 2) & 0x0Fu);
|
|
return 0;
|
|
}
|
|
return abort_unsupported(pc, insn, "COP0 op");
|
|
}
|
|
case 0x12: { /* COP2 / GTE */
|
|
uint32_t cop_op = rs;
|
|
/* Faithful GTE: any COP2 register access stalls to the pending command
|
|
* completion deadline (gte_execute armed it via psx_gte_set). */
|
|
if (cop_op == 0x00) { /* MFC2 — read: stall + give-back (Beetle) */
|
|
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
|
psx_gte_read(cpu, rt);
|
|
#endif
|
|
cpu->gpr[rt] = gte_read_data(cpu, (uint8_t)rd);
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
}
|
|
if (cop_op == 0x02) { /* CFC2 — read: stall + give-back (Beetle) */
|
|
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
|
psx_gte_read(cpu, rt);
|
|
#endif
|
|
cpu->gpr[rt] = gte_read_ctrl(cpu, (uint8_t)rd);
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
}
|
|
if (cop_op == 0x04) { /* MTC2 */
|
|
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
|
psx_gte_stall(cpu);
|
|
#endif
|
|
gte_write_data(cpu, (uint8_t)rd, cpu->gpr[rt]);
|
|
return 0;
|
|
}
|
|
if (cop_op == 0x06) { /* CTC2 */
|
|
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
|
psx_gte_stall(cpu);
|
|
#endif
|
|
gte_write_ctrl(cpu, (uint8_t)rd, cpu->gpr[rt]);
|
|
return 0;
|
|
}
|
|
if (cop_op & 0x10) {
|
|
gte_execute(cpu, insn & 0x1FFFFFFu);
|
|
return 0;
|
|
}
|
|
return abort_unsupported(pc, insn, "COP2 op");
|
|
}
|
|
/* CPU loads: the full per-instruction R3000A interlock (§1 + GPR_DEPRES(rs) +
|
|
* DO_LDS + ReadMemory timing + arm LDWhich=rt) lives in psx_cyc_load_*; exec_one's
|
|
* top-of-function step is skipped for op 0x20-0x26 so it is not double-charged.
|
|
* #ifndef PSX_ENABLE_BLOCK_CYCLES these still read the value via the uncharged
|
|
* accessor (psx_cyc_load_* falls back to a plain read when cycles are off). */
|
|
case 0x20: { /* LB rt, simm(rs) */
|
|
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
|
cpu->gpr[rt] = (uint32_t)(int32_t)(int8_t)psx_cyc_load_byte(cpu, addr, rt, 1u << rs);
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
}
|
|
case 0x21: { /* LH */
|
|
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
|
cpu->gpr[rt] = (uint32_t)(int32_t)(int16_t)psx_cyc_load_half(cpu, addr, rt, 1u << rs);
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
}
|
|
case 0x22: { /* LWL */
|
|
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
|
uint32_t word = psx_cyc_load_word(cpu, addr & ~3u, rt, 1u << rs);
|
|
cpu->gpr[rt] = lwl_merge(addr, word, cpu->gpr[rt]);
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
}
|
|
case 0x23: { /* LW */
|
|
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
|
cpu->gpr[rt] = psx_cyc_load_word(cpu, addr, rt, 1u << rs);
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
}
|
|
case 0x24: { /* LBU */
|
|
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
|
cpu->gpr[rt] = (uint32_t)psx_cyc_load_byte(cpu, addr, rt, 1u << rs);
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
}
|
|
case 0x25: { /* LHU */
|
|
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
|
cpu->gpr[rt] = (uint32_t)psx_cyc_load_half(cpu, addr, rt, 1u << rs);
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
}
|
|
case 0x26: { /* LWR */
|
|
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
|
uint32_t word = psx_cyc_load_word(cpu, addr & ~3u, rt, 1u << rs);
|
|
cpu->gpr[rt] = lwr_merge(addr, word, cpu->gpr[rt]);
|
|
cpu->gpr[0] = 0;
|
|
return 0;
|
|
}
|
|
case 0x28: { /* SB */
|
|
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
|
cpu->write_byte(addr, (uint8_t)cpu->gpr[rt]);
|
|
return 0;
|
|
}
|
|
case 0x29: { /* SH */
|
|
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
|
uint16_t val = (uint16_t)cpu->gpr[rt];
|
|
/* Widescreen backdrop screenX squash on the interpreter path: mirrors
|
|
* the recompiler emit at [widescreen.backdrop] x_sites. Overlay code
|
|
* (the parallax backdrop handlers) runs interpreted when no cache DLL
|
|
* is loaded, so without this the squash never happens. Identity at 4:3
|
|
* (psx_ws_backdrop_x gates on ws_active). */
|
|
if (psx_ws_is_backdrop_site(pc))
|
|
val = (uint16_t)psx_ws_backdrop_x((int16_t)val);
|
|
cpu->write_half(addr, val);
|
|
return 0;
|
|
}
|
|
case 0x2A: { /* SWL */
|
|
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
|
interp_swl(cpu, addr, cpu->gpr[rt]);
|
|
return 0;
|
|
}
|
|
case 0x2B: { /* SW */
|
|
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
|
cpu->write_word(addr, cpu->gpr[rt]);
|
|
return 0;
|
|
}
|
|
case 0x2E: { /* SWR */
|
|
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
|
interp_swr(cpu, addr, cpu->gpr[rt]);
|
|
return 0;
|
|
}
|
|
case 0x32: { /* LWC2 — §1+DO_LDS charged by exec_one's top step (mask 0) */
|
|
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
|
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
|
psx_gte_stall(cpu); /* COP2 reg write stalls to GTE completion */
|
|
#endif
|
|
gte_write_data(cpu, (uint8_t)rt, psx_cyc_lwc2_read(cpu, addr));
|
|
return 0;
|
|
}
|
|
case 0x3A: { /* SWC2 */
|
|
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
|
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
|
psx_gte_stall(cpu); /* COP2 reg read stalls to GTE completion */
|
|
#endif
|
|
cpu->write_word(addr, gte_read_data(cpu, (uint8_t)rt));
|
|
return 0;
|
|
}
|
|
default:
|
|
return abort_unsupported(pc, insn, "primary opcode");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_addr);
|
|
|
|
/* Public entry point. Caller (psx_dispatch) has translated `addr` to a
|
|
* KSEG-stripped form already in some cases, so accept any address and
|
|
* mask. Returns 1 if interpretation handled the basic block; 0 if the
|
|
* caller should fall back (e.g. unsupported opcode at the entry, page
|
|
* not dirty).
|
|
*
|
|
* Thin wrapper that brackets the body with g_dirty_interp_active so the
|
|
* event-timeline ring can tag events as INTERP vs STATIC. The EPC-sentinel
|
|
* branch inside can longjmp out (not restoring here); psx_check_interrupts
|
|
* clears the flag at its longjmp landing to cover that case. */
|
|
/* Always-on runaway-recursion guard. The host C stack mirrors the guest call
|
|
* graph (each guest call = a nested psx_dispatch_call -> dirty_ram_dispatch C
|
|
* frame), so an unbounded self-recursion — e.g. a bogus jump-table handler that
|
|
* re-enters its own entity loop (issue #1 seesaw crash: v0=0x8001DFF8) — silently
|
|
* overflows the host stack with no diagnostic (raw STATUS_STACK_OVERFLOW). Trip
|
|
* well below overflow: dump full state (crash report carries dispatch_depth + the
|
|
* dirty_block cycle that NAMES the recursing PC) and halt-and-serve, so the next
|
|
* reproduction (whoever hits it) is captured cleanly + live-inspectable instead of
|
|
* a bare SEH. Normal guest nesting is shallow (<~64); the default 256 sits far
|
|
* above that and below overflow (~500+ frames on a 1 MB stack). Override via
|
|
* PSX_RECURSION_LIMIT. One-shot. NOT a fix — the upstream bogus-pointer divergence
|
|
* still needs the oracle; this converts a hard crash into a catchable, diagnosed
|
|
* bail (Rule 15: make the failure observable). */
|
|
int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {
|
|
extern int g_psx_dispatch_depth;
|
|
extern void psx_fatal_halt(const char *reason);
|
|
static int s_rec_guard = 0;
|
|
static int s_rec_limit = 0;
|
|
if (s_rec_limit == 0) {
|
|
const char *e = getenv("PSX_RECURSION_LIMIT");
|
|
s_rec_limit = (e && *e) ? atoi(e) : 256;
|
|
if (s_rec_limit < 32) s_rec_limit = 32;
|
|
}
|
|
if (!s_rec_guard && g_psx_dispatch_depth > s_rec_limit) {
|
|
s_rec_guard = 1; /* one-shot: first trip wins the report */
|
|
psx_fatal_halt("dispatch recursion guard tripped — runaway self-call "
|
|
"(see dispatch_depth + dirty_block cycle for the recursing PC)");
|
|
}
|
|
if (addr == 0x8001A954u) site_note(&g_site_dd954); /* loop head re-dispatch */
|
|
else if (addr == 0x80046264u) site_note(&g_site_dd264); /* loop tail re-dispatch */
|
|
int prev = g_dirty_interp_active;
|
|
g_dirty_interp_active = 1;
|
|
int r = dirty_ram_dispatch_inner(cpu, addr, stop_addr);
|
|
|
|
/* pc=0 producer tripwire: the dirty path reported "handled" yet published a
|
|
* null PC — the abnormal-exit source we're hunting (MMX6 boot wedge). Latch
|
|
* full context the first time; count every occurrence. */
|
|
if (r == 1 && cpu->pc == 0u) {
|
|
extern uint32_t g_async_rfe_resume_pc, g_dirty_safe_resume_pc;
|
|
g_pczero_count++;
|
|
if (!g_pczero_latched) {
|
|
g_pczero_latched = 1;
|
|
g_pczero_addr = addr;
|
|
g_pczero_ra = cpu->gpr[31];
|
|
g_pczero_in_exc = (uint32_t)psx_get_in_exception();
|
|
g_pczero_async_rfe = g_async_rfe_resume_pc;
|
|
g_pczero_dirty_safe = g_dirty_safe_resume_pc;
|
|
}
|
|
}
|
|
|
|
/* Dirty-RAM scheduling contract (Tomba 2 Whoopee-Camp softlock fix):
|
|
* ANY path that advanced guest cycles must periodically expose the CPU to
|
|
* the interrupt/event pump, regardless of RAM region. The local-dirty-flow
|
|
* fast path (overlay region, phys >= OVERLAY_REGION_FLOOR) already pumps
|
|
* psx_check_interrupts() every 4096 insns internally — but a LOW-RAM kernel
|
|
* dirty loop (e.g. 0x2CA8 / 0xE10 / 0xB0 polling for VBlank) takes the
|
|
* per-block-return path instead and re-dispatches through the CPS chain
|
|
* WITHOUT ever surfacing to the top-level interrupt check. Result: VBlank is
|
|
* never raised, i_stat stays 0, the wait-loop deadlocks (frame frozen,
|
|
* total_checks frozen, dirty_ram_insns -> billions). OVERLAY_REGION_FLOOR may
|
|
* decide HOW dirty code is dispatched; it must never decide whether time
|
|
* exists. So pump here on a region-independent insn budget. This is a clean
|
|
* poll boundary: the inner committed cpu->pc, the executed instruction and
|
|
* its delay slot are fully retired, and psx_check_interrupts() clears
|
|
* g_dirty_interp_active at its longjmp landing (same contract as the inner
|
|
* local-flow pump), so a deliver-via-longjmp here is safe. */
|
|
if (r == 1) {
|
|
uint64_t now = g_dirty_ram_insns_run;
|
|
uint64_t gap = now - s_last_dirty_irq_pump_insns;
|
|
if (gap > g_dirty_pump_max_gap_insns) g_dirty_pump_max_gap_insns = gap;
|
|
if (gap >= 4096u) {
|
|
s_last_dirty_irq_pump_insns = now;
|
|
g_dirty_pump_count++;
|
|
uint32_t committed = cpu->pc; /* block already retired; this is next PC */
|
|
int prev_site = g_cosim_dirty_pump_site;
|
|
g_cosim_dirty_pump_site = 6;
|
|
g_dirty_safe_resume_pc = committed;
|
|
psx_check_interrupts(cpu); /* may exception-enter / longjmp */
|
|
/* Frame-1997 fix: a game-driven ReturnFromException longjmp'd through the
|
|
* handler and left cpu->pc=0. Here (outer pump, the block already returned)
|
|
* that null PC would propagate up the trampoline to the OUTERMOST dispatch
|
|
* and be read as a clean "execution completed" exit. Restore the committed
|
|
* guest PC so the trampoline re-dispatches and the game resumes. (The
|
|
* local-flow internal pump resumes via its own loop, so this only matters at
|
|
* block-return pumps.) */
|
|
if (cpu->pc == 0u && committed != 0u) {
|
|
cpu->pc = committed;
|
|
g_async_rfe_fire_count++;
|
|
}
|
|
g_dirty_safe_resume_pc = 0;
|
|
g_cosim_dirty_pump_site = prev_site;
|
|
}
|
|
}
|
|
|
|
g_dirty_interp_active = prev;
|
|
return r;
|
|
}
|
|
|
|
/* ===== Cycle-budgeted precise event slicing (PRECISE_IRQ_SLICE.md) ===== */
|
|
|
|
/* Is a hardware interrupt deliverable to the guest at this exact instruction
|
|
* boundary? Mirrors the gate in psx_check_interrupts: a pending+unmasked I_STAT
|
|
* bit, COP0 IEc + IM2 set, and not already inside the exception handler. */
|
|
static int precise_irq_deliverable(CPUState *cpu) {
|
|
extern uint32_t i_stat;
|
|
if ((i_stat & i_mask) == 0) return 0;
|
|
if (psx_get_in_exception()) return 0;
|
|
uint32_t sr = cpu->cop0[12];
|
|
if (!(sr & 0x1u)) return 0; /* IEc: interrupts globally enabled */
|
|
if (!(sr & (1u << 10))) return 0; /* IM2: hardware interrupt mask bit */
|
|
return 1;
|
|
}
|
|
|
|
/* Is `pc` a point the TOP-LEVEL dispatcher can re-enter? Clean (compiled) game
|
|
* text is only re-enterable at a FUNCTION ENTRY: psx_dispatch_game_compiled is a
|
|
* switch over entries and returns 0 for any mid-function PC; the dirty path then
|
|
* sees a non-dirty page and also returns 0, which the top dispatch reads as PC=0
|
|
* ("execution completed" — the deterministic precise-slice boot exit, root-caused
|
|
* to a mid-function clean-text resume PC, PRECISE_IRQ_SLICE.md Task #4). Every
|
|
* other PC class — BIOS ROM, dirty RAM (overlays / install-at-runtime stubs),
|
|
* overlay candidates, the A0/B0/C0 kernel call vectors, scratchpad — is handled by
|
|
* psx_dispatch / the dirty interp / the overlay loader, so it is dispatchable.
|
|
* Therefore the precise slicer must never hand control back at a mid-function
|
|
* clean-text PC: it keeps interpreting (exec_one handles arbitrary mid-function
|
|
* flow) until cpu->pc satisfies this predicate. */
|
|
static int precise_pc_dispatchable(uint32_t pc) {
|
|
#ifdef PSX_HAS_GAME_DISPATCH
|
|
uint32_t phys = pc & 0x1FFFFFFFu;
|
|
if (psx_game_address_in_text(pc) && !dirty_ram_is_dirty(phys))
|
|
return psx_game_is_function_entry(pc);
|
|
#endif
|
|
(void)pc;
|
|
return 1;
|
|
}
|
|
|
|
/* Run the guest per-instruction from cpu->pc through the interpreter, taking any
|
|
* deliverable interrupt at the EXACT instruction boundary, until a safe resume
|
|
* point. g_precise_mode makes exec_one step INTO jal/jalr callees per-instruction
|
|
* (ignore native availability), so the slice is owned by the event deadline, not
|
|
* the function boundary. ALL exits land on a control-transfer target — every
|
|
* branch/jump target is dispatchable (compiled blocks end with cpu->pc=target;
|
|
* return), whereas an arbitrary mid-block PC is not — so after taking a mid-block
|
|
* IRQ we keep interpreting to the next transfer before handing back to compiled.
|
|
* `bcyc` = originating block's cycle budget; `deadline_entry` = entered because an
|
|
* event is due within bcyc (vs a side-effect-only block, which runs one block). */
|
|
static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) {
|
|
int prev_precise = g_precise_mode;
|
|
int prev_active = g_dirty_interp_active;
|
|
g_precise_mode = 1;
|
|
g_dirty_interp_active = 1;
|
|
|
|
uint32_t pc = cpu->pc;
|
|
g_slice_last_block = pc;
|
|
g_slice_last_first_pc = pc;
|
|
g_slice_last_first_insn = fetch_word(pc & 0x1FFFFFFFu);
|
|
g_slice_exit_pc = pc;
|
|
g_slice_exit_reason = 0;
|
|
g_slice_exit_iter = 0;
|
|
g_slice_exit_dispatchable = 0;
|
|
g_slice_exit_dirty = dirty_ram_is_dirty(pc & 0x1FFFFFFFu) ? 1u : 0u;
|
|
#ifdef PSX_HAS_GAME_DISPATCH
|
|
g_slice_exit_in_text = psx_game_address_in_text(pc) ? 1u : 0u;
|
|
#else
|
|
g_slice_exit_in_text = 0;
|
|
#endif
|
|
g_slice_exit_want = 0;
|
|
int irq_taken = 0; /* one take per slice (avoid re-taking an unacked IRQ) */
|
|
enum { MAX_PRECISE_INSNS = 200000 };
|
|
for (int i = 0; i < MAX_PRECISE_INSNS; i++) {
|
|
if (!irq_taken && precise_irq_deliverable(cpu)) {
|
|
uint32_t committed = pc;
|
|
extern uint32_t i_stat;
|
|
g_slice_last_committed = committed;
|
|
g_slice_last_istat = i_stat;
|
|
g_slice_last_imask = i_mask;
|
|
g_slice_last_sr = cpu->cop0[12];
|
|
cpu->pc = committed;
|
|
int prev_site = g_cosim_dirty_pump_site;
|
|
g_cosim_dirty_pump_site = 7;
|
|
g_dirty_safe_resume_pc = committed;
|
|
s_last_dirty_irq_pump_insns = g_dirty_ram_insns_run;
|
|
psx_check_interrupts(cpu);
|
|
if (cpu->pc == 0u && committed != 0u) {
|
|
cpu->pc = committed;
|
|
g_async_rfe_fire_count++;
|
|
}
|
|
g_dirty_safe_resume_pc = 0;
|
|
g_cosim_dirty_pump_site = prev_site;
|
|
g_slice_irq_taken++;
|
|
irq_taken = 1;
|
|
pc = cpu->pc ? cpu->pc : committed;
|
|
cpu->pc = pc;
|
|
if (precise_pc_dispatchable(cpu->pc)) {
|
|
g_slice_exit_reason = 1;
|
|
g_slice_exit_iter = (uint32_t)i;
|
|
g_slice_exit_want = 1;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
uint32_t next_pc = 0;
|
|
g_unsupported_seen = 0;
|
|
cosim_exec_one_begin();
|
|
int transferred = exec_one(cpu, pc, &next_pc); /* charges its own interlock */
|
|
g_dirty_ram_insns_run++;
|
|
#ifdef PSX_COSIM
|
|
if (!cosim_exec_one_did_hook()) { extern void cosim_instr(uint32_t); cosim_instr(pc); }
|
|
#endif
|
|
if (g_unsupported_seen) {
|
|
/* Valid compiled code always decodes; if not, hand the committed PC to
|
|
* the dispatcher rather than abort the slice. */
|
|
g_unsupported_seen = 0;
|
|
pc = transferred ? cpu->pc : next_pc;
|
|
g_slice_exit_reason = 2;
|
|
g_slice_exit_iter = (uint32_t)i;
|
|
g_slice_exit_want = 0;
|
|
break;
|
|
}
|
|
uint32_t committed = transferred ? cpu->pc : next_pc;
|
|
|
|
/* `want_exit`: the slice has done its job and would like to hand back. But
|
|
* it may ONLY return control at a dispatchable PC (precise_pc_dispatchable)
|
|
* — a mid-function clean-text resume PC is unhandleable by the top dispatch
|
|
* (root cause of the deterministic boot PC=0). When we want to exit but the
|
|
* committed PC is mid-function clean text, keep interpreting (exec_one runs
|
|
* arbitrary mid-function flow) until control lands on a real entry / leaves
|
|
* clean game text. */
|
|
int want_exit = 0;
|
|
|
|
/* Exact take-point: an interrupt deliverable on THIS instruction's cycle is
|
|
* taken before the next instruction retires — once per slice. Re-checking
|
|
* after a take re-fires an IRQ the handler has not acked yet (an
|
|
* 8-takes-in-11-insns storm), so gate on !irq_taken. */
|
|
if (!irq_taken && precise_irq_deliverable(cpu)) {
|
|
extern uint32_t i_stat;
|
|
g_slice_last_committed = committed;
|
|
g_slice_last_istat = i_stat;
|
|
g_slice_last_imask = i_mask;
|
|
g_slice_last_sr = cpu->cop0[12];
|
|
cpu->pc = committed;
|
|
int prev_site = g_cosim_dirty_pump_site;
|
|
g_cosim_dirty_pump_site = 7;
|
|
g_dirty_safe_resume_pc = committed; /* real EPC for exception entry */
|
|
s_last_dirty_irq_pump_insns = g_dirty_ram_insns_run;
|
|
psx_check_interrupts(cpu); /* takes it; runs handler; restores GPRs */
|
|
g_dirty_safe_resume_pc = 0;
|
|
g_cosim_dirty_pump_site = prev_site;
|
|
g_slice_irq_taken++;
|
|
irq_taken = 1;
|
|
committed = cpu->pc ? cpu->pc : committed;
|
|
want_exit = 1;
|
|
}
|
|
|
|
pc = committed;
|
|
cpu->pc = committed;
|
|
|
|
if (g_psx_call_bail) {
|
|
g_slice_exit_reason = 3;
|
|
g_slice_exit_iter = (uint32_t)i;
|
|
g_slice_exit_want = want_exit ? 1u : 0u;
|
|
break; /* wild unwind: hand cpu->pc to the dispatcher */
|
|
}
|
|
|
|
if (irq_taken) {
|
|
want_exit = 1; /* after the take, leave as soon as it is safe */
|
|
} else if (transferred) {
|
|
want_exit = 1;
|
|
/* else still imminent — keep slicing across this boundary */
|
|
}
|
|
|
|
/* Hand back ONLY at a dispatchable PC. Otherwise (mid-function clean text)
|
|
* keep interpreting until one is reached. */
|
|
if (want_exit && precise_pc_dispatchable(cpu->pc)) {
|
|
g_slice_exit_reason = 1;
|
|
g_slice_exit_iter = (uint32_t)i;
|
|
g_slice_exit_want = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (g_slice_exit_reason == 0) {
|
|
g_slice_exit_reason = 4;
|
|
g_slice_exit_iter = MAX_PRECISE_INSNS;
|
|
}
|
|
|
|
cpu->pc = pc;
|
|
g_slice_exit_pc = cpu->pc;
|
|
g_slice_exit_dispatchable = precise_pc_dispatchable(cpu->pc) ? 1u : 0u;
|
|
g_slice_exit_dirty = dirty_ram_is_dirty(cpu->pc & 0x1FFFFFFFu) ? 1u : 0u;
|
|
#ifdef PSX_HAS_GAME_DISPATCH
|
|
g_slice_exit_in_text = psx_game_address_in_text(cpu->pc) ? 1u : 0u;
|
|
#else
|
|
g_slice_exit_in_text = 0;
|
|
#endif
|
|
g_precise_mode = prev_precise;
|
|
g_dirty_interp_active = prev_active;
|
|
}
|
|
|
|
/* Block-leader slice guard, called by the emitted prologue of every compiled
|
|
* block BEFORE it charges cycles / runs its body:
|
|
* if (psx_slice_block(cpu, <block_addr>, <bcyc>, <side_effects>)) return;
|
|
* Returns 1 if it sliced (ran the block — and possibly more — through the precise
|
|
* interpreter and left cpu->pc at a dispatchable resume point; the caller MUST
|
|
* `return` so its compiled body does not re-execute the same instructions).
|
|
* Returns 0 if the whole block is provably safe to run as fast compiled C. */
|
|
int psx_slice_block(CPUState *cpu, uint32_t block_addr, uint32_t bcyc, int side_effects) {
|
|
/* Runtime A/B toggle (diagnostic, stays in the build): PSX_PRECISE_SLICE=0
|
|
* disables precise slicing entirely so the SAME binary can be run slice-on vs
|
|
* slice-off to isolate the first divergence the slice introduces. Read once. */
|
|
/* PARKED (PRECISE_IRQ_SLICE.md): precise take-point slicing is a later
|
|
* correctness upgrade, NOT the current FMV blocker (that is the -8 cycle
|
|
* drift / faithful per-instruction cycle model — see CLAUDE.md Rule -1).
|
|
* Default OFF so the runtime boots on the baseline; opt in with
|
|
* PSX_PRECISE_SLICE=1 to continue the block-leader-continuation work. */
|
|
static int s_slice_enabled = -1;
|
|
if (s_slice_enabled < 0) {
|
|
const char *e = getenv("PSX_PRECISE_SLICE");
|
|
s_slice_enabled = (e && e[0] == '1') ? 1 : 0;
|
|
}
|
|
if (!s_slice_enabled) return 0;
|
|
|
|
/* No nested slicing: a handler dispatched from inside precise-mode, and any
|
|
* block executed while in_exception, run compiled (interrupts are gated during
|
|
* exception handling anyway). Keeps re-entrancy structurally impossible. */
|
|
if (g_precise_mode || psx_get_in_exception()) return 0;
|
|
|
|
static int s_slice_always = -1;
|
|
static int s_slice_margin = -1;
|
|
if (s_slice_always < 0) {
|
|
const char *e = getenv("PSX_PRECISE_ALWAYS");
|
|
s_slice_always = (e && e[0] == '1') ? 1 : 0;
|
|
}
|
|
if (s_slice_margin < 0) {
|
|
const char *e = getenv("PSX_PRECISE_MARGIN");
|
|
s_slice_margin = (e && *e) ? atoi(e) : 0;
|
|
if (s_slice_margin < 0) s_slice_margin = 0;
|
|
}
|
|
|
|
uint32_t deadline = cycles_to_next_event();
|
|
uint32_t budget = bcyc + (uint32_t)s_slice_margin;
|
|
if (budget < bcyc) budget = 0xFFFFFFFFu;
|
|
int entry_deliverable = precise_irq_deliverable(cpu);
|
|
int has_deadline = s_slice_always || entry_deliverable || (deadline <= budget);
|
|
if (!has_deadline && !side_effects) return 0; /* fast path: no event in this block */
|
|
|
|
g_slice_entry_deliverable = (uint32_t)entry_deliverable;
|
|
g_slice_fired++;
|
|
cpu->pc = block_addr;
|
|
psx_run_precise(cpu, bcyc, has_deadline);
|
|
return 1;
|
|
}
|
|
|
|
static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {
|
|
uint32_t phys = addr & 0x1FFFFFFFu;
|
|
int clean_game_text_miss = 0;
|
|
|
|
if (addr == 0x80000048u) {
|
|
g_sentinel_reach_dirty++;
|
|
if (!psx_get_in_exception()) g_sentinel_reach_traps++; /* reuse: in_exc==0 dirty reaches */
|
|
g_sentinel_reach_async = g_async_rfe_resume_pc;
|
|
cpu->pc = 0;
|
|
if (psx_get_in_exception()) {
|
|
psx_exception_longjmp(); /* does not return */
|
|
}
|
|
/* Async ReturnFromException (Tomba 2 frame-1997 fix): game-installed handler
|
|
* RFE'd outside our exception window. Resume at the latched real guest PC
|
|
* instead of returning pc=0 (which the top dispatch reads as a clean exit). */
|
|
if (g_async_rfe_resume_pc != 0u) {
|
|
g_async_rfe_fire_count++;
|
|
cpu->pc = g_async_rfe_resume_pc;
|
|
return 1;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
#ifdef PSX_HAS_GAME_DISPATCH
|
|
xprobe_event(cpu->gpr[31], XOP_DD, XSITE_DD, addr, 0u, cpu->gpr[29], cpu->gpr[31], 0);
|
|
/* Run the statically-compiled game function only while the target is still
|
|
* native-safe. Dirty overlay pages and pages whose text bytes diverged from
|
|
* the original EXE image fall through to interpret the live RAM bytes. */
|
|
if (dirty_ram_text_native_ok(phys)) {
|
|
g_mixed_depth++;
|
|
{
|
|
ls_func_enter(addr, cpu);
|
|
int _gc = psx_dispatch_game_compiled(cpu, addr);
|
|
ls_func_exit(addr, cpu, _gc);
|
|
g_mixed_depth--;
|
|
if (_gc) return 1;
|
|
}
|
|
clean_game_text_miss = psx_game_address_in_text(addr) ? 1 : 0;
|
|
}
|
|
#endif
|
|
|
|
/* B-2: statically-compiled overlay functions (generated/overlays_static.c).
|
|
* Inert unless a game provides an overlays_static.c at build time. */
|
|
#ifdef PSX_HAS_OVERLAY_DISPATCH
|
|
{
|
|
extern int psx_overlay_dispatch(CPUState *cpu, uint32_t addr);
|
|
if (psx_overlay_dispatch(cpu, addr)) return 1;
|
|
}
|
|
#endif
|
|
|
|
/* A-1: dynamically-loaded overlay DLL functions, checked before the
|
|
* interpreter. No-op (returns 0) until overlay_loader_init() runs, which
|
|
* only happens when the overlay cache is enabled in config. */
|
|
/* §5-E native↔interp fingerprint: capture entry register state for a
|
|
* candidate overlay function, so native and interpreted runs can be diffed
|
|
* by sequence. Additive only — no control-flow change. */
|
|
extern int overlay_loader_is_candidate(uint32_t phys);
|
|
extern void overlay_regs_snap(uint32_t out[34], const CPUState *cpu);
|
|
extern void overlay_fp_log(uint32_t addr, const uint32_t *in_regs,
|
|
const CPUState *cpu, int native);
|
|
int _ovfp = overlay_cache_window_contains(phys) &&
|
|
overlay_loader_is_candidate(phys);
|
|
uint32_t _in_regs[34];
|
|
if (_ovfp) {
|
|
overlay_regs_snap(_in_regs, cpu);
|
|
/* One-shot insn-ring freeze BEFORE the Nth dispatch of the watched
|
|
* candidate — the ring tail then holds the pre-divergence window. */
|
|
if (g_insn_freeze_addr && phys == g_insn_freeze_addr &&
|
|
++g_insn_freeze_count == g_insn_freeze_nth)
|
|
g_insn_log_frozen = 1;
|
|
}
|
|
|
|
{
|
|
extern int overlay_loader_dispatch(CPUState *cpu, uint32_t addr);
|
|
if (overlay_loader_dispatch(cpu, addr)) {
|
|
if (_ovfp) overlay_fp_log(addr, _in_regs, cpu, 1);
|
|
return 1;
|
|
}
|
|
}
|
|
#define OV_FPLOG_RET1() do { if (_ovfp) overlay_fp_log(addr, _in_regs, cpu, 0); return 1; } while (0)
|
|
|
|
if (!dirty_ram_is_dirty(phys) && !clean_game_text_miss) return 0;
|
|
|
|
/* Interp-pressure signal for variant-capture automation (step 2.8):
|
|
* counts dispatches the interpreter actually handles inside a capture
|
|
* window. The autocapture tick reads-and-resets this to decide whether
|
|
* an unseen region variant is being interp-executed right now. */
|
|
if (overlay_cache_window_contains(phys)) g_dirty_window_dispatches++;
|
|
|
|
/* Reset soft-fail state at block entry. */
|
|
g_unsupported_seen = 0;
|
|
/* Overlay flow above the kernel window — kernel window stays per-block (see
|
|
* is_local_dirty_target / phys_is_overlay_flow_region). Includes boot-text
|
|
* pages overwritten by a runtime overlay (Tomba 2), not just [FLOOR, RAM). */
|
|
int allow_local_dirty_flow = phys_is_overlay_flow_region(phys);
|
|
|
|
/* Per-PC entry counter (visible via dirty_ram_stats). */
|
|
DirtyRamPcEntry *pc_entry = pc_table_get_or_insert(phys);
|
|
if (pc_entry) pc_entry->hits++;
|
|
|
|
/* External-entry attribution: when the previous interp run exited by
|
|
* handing a target back to the dispatch loop, the very next dirty
|
|
* dispatch at that target is the SAME logical execution continuing —
|
|
* not a new entry. Anything else arrived from native code (a call or
|
|
* a fresh dispatch) and is real interior-entry evidence for alias
|
|
* seeding. */
|
|
if (pc_entry && addr != g_dirty_interp_chain_target) pc_entry->entry_hits++;
|
|
g_dirty_interp_chain_target = 0;
|
|
|
|
/* Block-entry ring buffer — answers "who tried to JALR into this RAM
|
|
* stub" by capturing cpu->gpr[31] (the caller's RA) at dispatch time.
|
|
* Always-on; eviction keeps memory bounded. Honors the capture freeze. */
|
|
if (!g_insn_log_frozen) {
|
|
uint64_t s = g_dirty_ram_block_log_seq++;
|
|
DirtyRamBlockLogEntry *e =
|
|
&g_dirty_ram_block_log[s & (DIRTY_RAM_BLOCK_LOG_CAP - 1u)];
|
|
e->seq = s;
|
|
e->target = addr;
|
|
e->ra = cpu->gpr[31];
|
|
e->a0 = cpu->gpr[4];
|
|
e->a1 = cpu->gpr[5];
|
|
e->a2 = cpu->gpr[6];
|
|
e->a3 = cpu->gpr[7];
|
|
e->t0 = cpu->gpr[8];
|
|
e->t1 = cpu->gpr[9];
|
|
e->t2 = cpu->gpr[10];
|
|
e->sp = cpu->gpr[29];
|
|
e->frame = (uint32_t)s_frame_count;
|
|
}
|
|
|
|
if (debug_server_dirty_break_maybe_pause(addr, cpu)) {
|
|
debug_server_wait_if_paused();
|
|
}
|
|
|
|
/* Run dirty code locally until it returns to compiled/non-dirty code.
|
|
* Runtime-loaded overlays are larger than BIOS install stubs, so stopping
|
|
* at every local branch burns the dispatch loop. */
|
|
enum { MAX_INSNS_PER_DISPATCH = 1000000 };
|
|
uint32_t pc = addr;
|
|
int insns_executed = 0;
|
|
#ifndef PSX_NO_DEBUG_TOOLS
|
|
extern void debug_server_cyc_observe(uint32_t block_leader_phys);
|
|
#endif
|
|
for (int i = 0; i < MAX_INSNS_PER_DISPATCH; i++) {
|
|
uint32_t next_pc = 0;
|
|
#ifndef PSX_NO_DEBUG_TOOLS
|
|
/* Interp-path cycle ruler: make every interpreted PC anchorable by
|
|
* cyc_watch (parity with the compiled emitter's block-leader observe).
|
|
* Early-returns when cyc_watch is disarmed → ~free in normal runs. */
|
|
g_ls_dirty_observe = 1;
|
|
debug_server_cyc_observe(pc & 0x1FFFFFFFu);
|
|
g_ls_dirty_observe = 0;
|
|
#endif
|
|
#ifdef PSX_COSIM
|
|
{ extern void cosim_block(uint32_t); cosim_block(pc); }
|
|
#endif
|
|
uint32_t insn = fetch_word(pc & 0x1FFFFFFFu);
|
|
uint32_t before_s0 = cpu->gpr[16];
|
|
uint32_t before_ra = cpu->gpr[31];
|
|
cosim_exec_one_begin();
|
|
int transferred = exec_one(cpu, pc, &next_pc);
|
|
/* $ra->1 corruption tripwire (confirm-first probe): did THIS overlay
|
|
* instruction clobber $ra to 1? Latches once, cheap after. */
|
|
if (cpu->gpr[31] == 1u && before_ra != 1u) {
|
|
extern void psx_ra_tripwire(CPUState *, uint32_t, uint32_t, uint32_t);
|
|
psx_ra_tripwire(cpu, before_ra, pc, 0u /*INTERP*/);
|
|
}
|
|
/* Tomba2 ra-corruption pin (Confirm-(b)): capture the EXACT instruction
|
|
* that loads/sets $ra to the watched wild value (0x49422E54). For a
|
|
* `lw ra, off(sp)` this records the loading pc/insn + sp + the source
|
|
* stack address so we can see whether sp is wrong or the filename string
|
|
* was written onto the saved-ra slot. Latches g_ra_load_snap_valid. */
|
|
if (g_ra_load_watch != 0u && cpu->gpr[31] == g_ra_load_watch &&
|
|
before_ra != g_ra_load_watch && !g_ra_load_snap_valid) {
|
|
g_ra_load_snap_valid = 1;
|
|
g_ra_load_snap_pc = pc;
|
|
g_ra_load_snap_insn = insn;
|
|
g_ra_load_snap_before_ra = before_ra;
|
|
for (int r = 0; r < 32; r++) g_ra_load_snap_gpr[r] = cpu->gpr[r];
|
|
/* If it's a load (lw/lbu/...), decode base+imm to record the source addr. */
|
|
uint32_t op = insn >> 26;
|
|
if (op >= 0x20u && op <= 0x25u) { /* lb/lh/lwl/lw/lbu/lhu */
|
|
uint32_t base = cpu->gpr[(insn >> 21) & 0x1Fu];
|
|
int16_t imm = (int16_t)(insn & 0xFFFFu);
|
|
g_ra_load_snap_srcaddr = base + (int32_t)imm;
|
|
} else {
|
|
g_ra_load_snap_srcaddr = 0;
|
|
}
|
|
}
|
|
/* Per-instruction cycle cost (R3000A load-delay interlock) is charged
|
|
* INSIDE exec_one (top-of-fn §1+deps+DO_LDS, or psx_cyc_load_* for loads). */
|
|
dirty_ram_log_instruction(cpu, pc, insn, before_s0, next_pc,
|
|
transferred ? cpu->pc : next_pc,
|
|
transferred);
|
|
#ifdef PSX_COSIM
|
|
if (!cosim_exec_one_did_hook()) { extern void cosim_instr(uint32_t); cosim_instr(pc); }
|
|
#endif
|
|
if (g_unsupported_seen) {
|
|
if (insns_executed == 0) {
|
|
/* Couldn't decode the first instruction. Most likely
|
|
* dispatch landed in a dirty page that's not actually
|
|
* code (stale data, return-target into save area, etc.).
|
|
* Hand off to psx_unknown_dispatch which has its own
|
|
* pattern-matching trampoline resolver. */
|
|
return 0;
|
|
}
|
|
/* Made some progress, then hit unknown. Treat as a no-op
|
|
* return like psx_unknown_dispatch does for unrecognized
|
|
* targets — set cpu->pc=0 so the trampoline exits cleanly.
|
|
* If this turns out to be load-bearing, measurement will
|
|
* surface it as a card-protocol stall and we can add the
|
|
* missing opcode.
|
|
*
|
|
* No fprintf — read the last-* globals via TCP if needed
|
|
* (CLAUDE.md §3). Synchronous stderr at the rate this fires
|
|
* starves the dispatch loop and the debug-server poll. */
|
|
g_dirty_ram_unsupported_midblock++;
|
|
g_dirty_ram_last_unsupported_entry = addr;
|
|
g_dirty_ram_last_unsupported_entry_ra = cpu->gpr[31];
|
|
g_dirty_ram_last_unsupported_entry_sp = cpu->gpr[29];
|
|
g_dirty_ram_last_unsupported_insns = (uint32_t)insns_executed;
|
|
g_dirty_ram_last_unsupported_pc = g_unsupported_pc;
|
|
g_dirty_ram_last_unsupported_insn = g_unsupported_insn;
|
|
g_dirty_ram_last_unsupported_reason = g_unsupported_reason;
|
|
cpu->pc = 0;
|
|
OV_FPLOG_RET1();
|
|
}
|
|
g_dirty_ram_insns_run++;
|
|
insns_executed++;
|
|
if (transferred) {
|
|
if (g_psx_call_bail) {
|
|
/* A bail unwind began inside a surfaced call: stop the interp
|
|
* run and hand the wild target (cpu->pc) up to the dispatch
|
|
* loop's bail handling. */
|
|
g_dirty_ram_blocks_run++;
|
|
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
|
OV_FPLOG_RET1();
|
|
}
|
|
uint32_t target = cpu->pc;
|
|
if (target != 0u && dirty_ram_pump_boundary(cpu, target, 1)) {
|
|
g_dirty_ram_blocks_run++;
|
|
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
|
g_dirty_interp_chain_target = cpu->pc;
|
|
OV_FPLOG_RET1();
|
|
}
|
|
target = cpu->pc;
|
|
#ifdef PSX_HAS_GAME_DISPATCH
|
|
if (target != 0) {
|
|
/* §20 FIX — the long-run idle freeze. A guest TAIL-transfer (j/jr/
|
|
* branch) from an interpreted overlay into compiled code used to NEST
|
|
* a fresh psx_dispatch_game_compiled trampoline here. That nest never
|
|
* unwound for the per-frame render chain, leaking ~one host call chain
|
|
* (~1.17 KB) PER FRAME -> the 64 MB guest stack overflowed after ~40k
|
|
* idle frames -> the native-stack guard tripped (RECURSION_BUG.md
|
|
* §19/§20; confirmed by the ce_profile linear climb).
|
|
*
|
|
* A tail-transfer carries NO return obligation, so the correct action
|
|
* is to SURFACE the target (leave it in cpu->pc) and return; the OUTER
|
|
* psx_dispatch_impl trampoline re-dispatches cpu->pc FLAT
|
|
* (full_function_emitter.cpp:1230), keeping the host stack bounded
|
|
* across frames. Dirty-overlay targets still take the local-dirty-flow
|
|
* just below (kept flat in-interpreter); compiled / static / unknown
|
|
* targets fall through to the surface return (case 3). Real guest
|
|
* CALLS (jal/jalr) are unaffected — they nest inline in exec_one and
|
|
* return normally. (The §14 watermark surfaced via g_psx_call_bail,
|
|
* which is wild-return semantics and wedged; a plain tail surface does
|
|
* not touch the bail.) */
|
|
xprobe_event(g_debug_last_store_pc, XOP_BR, XSITE_INTERP, target,
|
|
fetch_word(g_debug_last_store_pc & 0x1FFFFFFFu),
|
|
cpu->gpr[29], cpu->gpr[31], 1);
|
|
cpu->pc = target; /* surfaced; trampoline re-dispatches flat */
|
|
}
|
|
#endif
|
|
uint32_t target_phys = target & 0x1FFFFFFFu;
|
|
if (allow_local_dirty_flow && target != 0 &&
|
|
target != stop_addr &&
|
|
phys_is_overlay_flow_region(target_phys) &&
|
|
dirty_ram_is_dirty(target_phys)) {
|
|
/* Capture freeze gates ONLY the ring write — never flow. */
|
|
if (!g_insn_log_frozen) {
|
|
uint64_t s = g_dirty_ram_flow_log_seq++;
|
|
DirtyRamFlowLogEntry *e =
|
|
&g_dirty_ram_flow_log[s & (DIRTY_RAM_FLOW_LOG_CAP - 1u)];
|
|
e->seq = s;
|
|
e->pc = pc;
|
|
e->target = target;
|
|
e->ra = cpu->gpr[31];
|
|
e->a0 = cpu->gpr[4];
|
|
e->a1 = cpu->gpr[5];
|
|
e->a2 = cpu->gpr[6];
|
|
e->a3 = cpu->gpr[7];
|
|
e->sp = cpu->gpr[29];
|
|
e->frame = (uint32_t)s_frame_count;
|
|
}
|
|
pc = target;
|
|
if ((insns_executed & 0xFFF) == 0) {
|
|
debug_server_poll();
|
|
debug_server_wait_if_paused();
|
|
}
|
|
continue;
|
|
}
|
|
g_dirty_ram_blocks_run++;
|
|
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
|
g_dirty_interp_chain_target = cpu->pc;
|
|
OV_FPLOG_RET1();
|
|
}
|
|
pc = next_pc;
|
|
/* Straight-line flow reaching the dispatch return contract — exit
|
|
* so the loop returns into the suspended native caller (same
|
|
* hazard as a transfer to stop_addr). */
|
|
if (stop_addr != 0 && pc == stop_addr) {
|
|
cpu->pc = pc;
|
|
if (dirty_ram_pump_boundary(cpu, pc, 2)) {
|
|
g_dirty_ram_blocks_run++;
|
|
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
|
g_dirty_interp_chain_target = cpu->pc;
|
|
OV_FPLOG_RET1();
|
|
}
|
|
g_dirty_ram_blocks_run++;
|
|
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
|
g_dirty_interp_chain_target = pc;
|
|
OV_FPLOG_RET1();
|
|
}
|
|
/* Straight-line code that left the dirty page — hand back to
|
|
* static dispatch by setting cpu->pc and returning. */
|
|
uint32_t next_phys = pc & 0x1FFFFFFFu;
|
|
if (!dirty_ram_is_dirty(next_phys)) {
|
|
cpu->pc = pc;
|
|
if (dirty_ram_pump_boundary(cpu, pc, 3)) {
|
|
g_dirty_ram_blocks_run++;
|
|
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
|
g_dirty_interp_chain_target = cpu->pc;
|
|
OV_FPLOG_RET1();
|
|
}
|
|
g_dirty_ram_blocks_run++;
|
|
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
|
g_dirty_interp_chain_target = pc;
|
|
OV_FPLOG_RET1();
|
|
}
|
|
}
|
|
g_dirty_ram_last_unsupported_pc = pc;
|
|
g_dirty_ram_last_unsupported_insn = fetch_word(pc & 0x1FFFFFFFu);
|
|
g_dirty_ram_last_unsupported_reason = "instruction guard";
|
|
g_dirty_ram_guard_yields++;
|
|
g_dirty_ram_blocks_run++;
|
|
cpu->pc = pc;
|
|
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
|
g_dirty_interp_chain_target = pc;
|
|
{
|
|
uint64_t gap = g_dirty_ram_insns_run - s_last_dirty_irq_pump_insns;
|
|
if (gap >= 4096u) {
|
|
uint32_t committed = cpu->pc;
|
|
int prev_site = g_cosim_dirty_pump_site;
|
|
g_cosim_dirty_pump_site = 4;
|
|
g_dirty_safe_resume_pc = committed;
|
|
s_last_dirty_irq_pump_insns = g_dirty_ram_insns_run;
|
|
psx_check_interrupts(cpu);
|
|
/* Frame-1997 fix (see outer pump): restore the committed PC if a game RFE
|
|
* longjmp left cpu->pc=0, so the trampoline re-dispatches instead of exiting. */
|
|
if (cpu->pc == 0u && committed != 0u) {
|
|
cpu->pc = committed;
|
|
g_async_rfe_fire_count++;
|
|
}
|
|
g_dirty_safe_resume_pc = 0;
|
|
g_cosim_dirty_pump_site = prev_site;
|
|
}
|
|
}
|
|
OV_FPLOG_RET1();
|
|
}
|
|
#undef OV_FPLOG_RET1
|
|
|
|
/* ============================================================================
|
|
* #2 — Lockstep "unit-test interp" comparator. See lockstep.h.
|
|
* Compiled-first + read-trace replay, per basic block, window-gated.
|
|
* ==========================================================================*/
|
|
#include "lockstep.h"
|
|
|
|
int g_ls_mode = 0;
|
|
int g_ls_replay_active = 0;
|
|
int g_ls_suppress_record = 0;
|
|
|
|
static uint32_t s_ls_frame_lo = 0, s_ls_frame_hi = 0; /* hi==0 => disabled */
|
|
|
|
typedef struct { uint8_t is_write, size; uint32_t addr, val; } ls_op_t;
|
|
enum { LS_TRACE_CAP = 65536 };
|
|
static ls_op_t s_ls_trace[LS_TRACE_CAP];
|
|
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 CPUState s_ls_R0; /* register snapshot at block entry */
|
|
static uint32_t s_ls_block = 0;
|
|
static int s_ls_prev_valid = 0;
|
|
|
|
/* mismatch detail captured by the hooks (read by ls_replay_and_compare) */
|
|
static int s_ls_m_kind = 0; /* 1 read-addr 2 write-addr 3 write-val 4 trace-exhausted */
|
|
static uint32_t s_ls_m_pc = 0, s_ls_m_addr = 0, s_ls_m_exp = 0, s_ls_m_act = 0;
|
|
|
|
static struct {
|
|
int found;
|
|
int kind; /* see ls_kind below */
|
|
uint32_t frame, block, pc, addr;
|
|
uint32_t exp, act; /* exp = interp(correct), act = compiled(actual) */
|
|
int detail; /* reg index for REG kind, else 0 */
|
|
uint64_t blocks_checked;
|
|
} s_ls_div = {0};
|
|
/* kinds: 1 REG 2 HI 3 LO 4 WRITE-VAL 5 READ-ADDR 6 WRITE-ADDR 7 TRACE-EXHAUSTED
|
|
* 8 COMPILED-EXTRA-OPS 9 PATH-CAP */
|
|
/* Post-mortem: the recorded op-trace of the diverging block + the replay's
|
|
* op index where it diverged, so the desync can be read off directly. */
|
|
static ls_op_t s_ls_div_trace[48];
|
|
static int s_ls_div_trace_n = 0, s_ls_div_idx = 0;
|
|
|
|
static int s_ls_record_only = 0; /* diagnostic: record but skip inline replay (perturbation test) */
|
|
|
|
static uint32_t s_lsf_frame_lo = 0, s_lsf_frame_hi = 0; /* hi==0 => disabled */
|
|
static int s_lsf_record_only = 0;
|
|
static int s_lsf_active = 0;
|
|
static int s_lsf_recording = 0;
|
|
static int s_lsf_saw_irq = 0;
|
|
static CPUState s_lsf_R0;
|
|
static uint32_t s_lsf_entry = 0, s_lsf_dispatch_entry = 0, s_lsf_frame = 0;
|
|
|
|
static struct {
|
|
int found;
|
|
int kind; /* see lsf_kind names in ls_get_func_json */
|
|
uint32_t frame, entry, pc, addr;
|
|
uint32_t exp, act;
|
|
int detail;
|
|
uint64_t segments_checked, skipped_irq, skipped_overflow;
|
|
uint64_t skipped_unhandled, skipped_conflict, skipped_disabled;
|
|
uint32_t trace_ops, replay_ops, steps;
|
|
} s_lsf_div = {0};
|
|
|
|
static ls_op_t s_lsf_div_trace[48];
|
|
static int s_lsf_div_trace_n = 0, s_lsf_div_idx = 0;
|
|
|
|
void ls_set_window(uint32_t lo, uint32_t hi) { s_ls_frame_lo = lo; s_ls_frame_hi = hi; }
|
|
void ls_set_record_only(int on) { s_ls_record_only = on; }
|
|
void ls_func_set_window(uint32_t lo, uint32_t hi) { s_lsf_frame_lo = lo; s_lsf_frame_hi = hi; }
|
|
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;
|
|
}
|
|
|
|
void ls_suppress_begin(void) {
|
|
g_ls_suppress_record++;
|
|
}
|
|
|
|
void ls_suppress_end(void) {
|
|
if (g_ls_suppress_record > 0) g_ls_suppress_record--;
|
|
}
|
|
|
|
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 (!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;
|
|
}
|
|
s_ls_trace_idx++;
|
|
return o->val;
|
|
}
|
|
if (g_ls_mode == 1) { /* record */
|
|
if (s_ls_trace_n < LS_TRACE_CAP) {
|
|
ls_op_t *o = &s_ls_trace[s_ls_trace_n++];
|
|
o->is_write = 0; o->size = (uint8_t)size; o->addr = addr; o->val = real_val;
|
|
} else s_ls_overflow = 1;
|
|
}
|
|
return real_val;
|
|
}
|
|
|
|
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 (!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;
|
|
}
|
|
if (o->val != val) { /* compiled wrote a different value than interp */
|
|
if (!s_ls_mismatch) { s_ls_mismatch = 1; s_ls_m_kind = 3; s_ls_m_pc = s_ls_cur_pc;
|
|
s_ls_m_addr = addr; s_ls_m_exp = val; s_ls_m_act = o->val; }
|
|
}
|
|
s_ls_trace_idx++;
|
|
return;
|
|
}
|
|
if (g_ls_mode == 1) { /* record */
|
|
if (s_ls_trace_n < LS_TRACE_CAP) {
|
|
ls_op_t *o = &s_ls_trace[s_ls_trace_n++];
|
|
o->is_write = 1; o->size = (uint8_t)size; o->addr = addr; o->val = val;
|
|
} else s_ls_overflow = 1;
|
|
}
|
|
}
|
|
|
|
static void ls_latch(int kind, uint32_t frame, uint32_t pc, uint32_t addr,
|
|
uint32_t exp, uint32_t act, int detail) {
|
|
if (s_ls_div.found) return;
|
|
s_ls_div.found = 1; s_ls_div.kind = kind; s_ls_div.frame = frame;
|
|
s_ls_div.block = s_ls_block; s_ls_div.pc = pc; s_ls_div.addr = addr;
|
|
s_ls_div.exp = exp; s_ls_div.act = act; s_ls_div.detail = detail;
|
|
}
|
|
|
|
/* Re-interpret the just-recorded block from the entry snapshot, comparing the
|
|
* interp result to native (cpu == post-block native state). */
|
|
static void ls_replay_and_compare(CPUState *cpu, uint32_t frame, uint32_t target_phys) {
|
|
CPUState rep = s_ls_R0; /* registers from block entry (pc is stale in compiled) */
|
|
rep.pc = s_ls_block | 0x80000000u; /* start at the recorded block's leader (KSEG0) */
|
|
s_ls_trace_idx = 0; s_ls_mismatch = 0; s_ls_m_kind = 0; s_ls_replay_done = 0;
|
|
/* The replay's exec_one writes process-global decode state on any
|
|
* instruction it can't handle; snapshot+restore so the replay can never
|
|
* divert the REAL dispatch (which reacts to g_unsupported_seen). */
|
|
int sav_unsup_seen = g_unsupported_seen;
|
|
uint32_t sav_unsup_pc = g_unsupported_pc;
|
|
uint32_t sav_unsup_insn = g_unsupported_insn;
|
|
const char *sav_unsup_reason = g_unsupported_reason;
|
|
g_ls_replay_active = 1;
|
|
g_ls_mode = 2;
|
|
uint32_t pc = rep.pc;
|
|
int cap = 512, steps = 0;
|
|
for (;;) {
|
|
/* Stop at the next block leader (fall-through block, or a branch's
|
|
* target). steps>0 so a self-looping block runs one iteration. */
|
|
if (steps > 0 && (pc & 0x1FFFFFFFu) == target_phys) break;
|
|
if (cap-- <= 0 || s_ls_mismatch || s_ls_replay_done) break;
|
|
uint32_t next_pc = 0;
|
|
s_ls_cur_pc = pc;
|
|
int transferred = exec_one(&rep, pc, &next_pc);
|
|
pc = transferred ? rep.pc : next_pc;
|
|
steps++;
|
|
/* Also stop at the block TERMINATOR (branch/jump/call) — do NOT follow
|
|
* it. A call would run the callee (kernel/syscall/another fn) which the
|
|
* recording dispatched separately or whose ops trail in the trace;
|
|
* following it desyncs. Compare only the block's own in-line memory ops,
|
|
* checked op-by-op against the trace as we go. */
|
|
if (transferred) break;
|
|
}
|
|
g_ls_mode = 0;
|
|
g_ls_replay_active = 0;
|
|
g_unsupported_seen = sav_unsup_seen; /* undo any replay-side decode-state leak */
|
|
g_unsupported_pc = sav_unsup_pc;
|
|
g_unsupported_insn = sav_unsup_insn;
|
|
g_unsupported_reason = sav_unsup_reason;
|
|
s_ls_div.blocks_checked++;
|
|
|
|
/* Divergence = an in-block memory-op mismatch (the interp replay read a
|
|
* different address, or the compiled block wrote a different value, at the
|
|
* same op index given identical entry state). Leftover trace ops past the
|
|
* terminator are the callee's (recording spanned a call) and are ignored;
|
|
* registers are not compared (boundary-sensitive across calls). A wrong
|
|
* register value still surfaces here later, as a wrong store value / load
|
|
* address, when the bad value is used. */
|
|
if (s_ls_mismatch) {
|
|
int n = s_ls_trace_n; if (n > 48) n = 48;
|
|
for (int i = 0; i < n; i++) s_ls_div_trace[i] = s_ls_trace[i];
|
|
s_ls_div_trace_n = n;
|
|
s_ls_div_idx = s_ls_trace_idx;
|
|
int k = (s_ls_m_kind == 1) ? 5 : (s_ls_m_kind == 2) ? 6 : (s_ls_m_kind == 3) ? 4 : 7;
|
|
ls_latch(k, frame, s_ls_m_pc, s_ls_m_addr, s_ls_m_exp, s_ls_m_act, 0);
|
|
return;
|
|
}
|
|
}
|
|
|
|
static int ls_same_phys(uint32_t a, uint32_t b) {
|
|
return (((a ^ b) & 0x1FFFFFFFu) == 0);
|
|
}
|
|
|
|
static void lsf_copy_div_trace(void) {
|
|
int n = s_ls_trace_n;
|
|
if (n > 48) n = 48;
|
|
for (int i = 0; i < n; i++) s_lsf_div_trace[i] = s_ls_trace[i];
|
|
s_lsf_div_trace_n = n;
|
|
s_lsf_div_idx = s_ls_trace_idx;
|
|
}
|
|
|
|
static void lsf_latch(int kind, uint32_t frame, uint32_t entry, uint32_t pc,
|
|
uint32_t addr, uint32_t exp, uint32_t act, int detail,
|
|
uint32_t steps) {
|
|
if (s_lsf_div.found) return;
|
|
s_lsf_div.found = 1;
|
|
s_lsf_div.kind = kind;
|
|
s_lsf_div.frame = frame;
|
|
s_lsf_div.entry = entry;
|
|
s_lsf_div.pc = pc;
|
|
s_lsf_div.addr = addr;
|
|
s_lsf_div.exp = exp;
|
|
s_lsf_div.act = act;
|
|
s_lsf_div.detail = detail;
|
|
s_lsf_div.trace_ops = (uint32_t)s_ls_trace_n;
|
|
s_lsf_div.replay_ops = (uint32_t)s_ls_trace_idx;
|
|
s_lsf_div.steps = steps;
|
|
lsf_copy_div_trace();
|
|
}
|
|
|
|
static void lsf_compare_regs(CPUState *rep, CPUState *nat, uint32_t frame,
|
|
uint32_t entry, uint32_t steps) {
|
|
if (!ls_same_phys(rep->pc, nat->pc)) {
|
|
lsf_latch(4, frame, entry, rep->pc, 0, rep->pc, nat->pc, 0, steps);
|
|
return;
|
|
}
|
|
for (int i = 0; i < 32; i++) {
|
|
if (rep->gpr[i] != nat->gpr[i]) {
|
|
lsf_latch(1, frame, entry, rep->pc, 0, rep->gpr[i], nat->gpr[i], i, steps);
|
|
return;
|
|
}
|
|
}
|
|
if (rep->hi != nat->hi) {
|
|
lsf_latch(2, frame, entry, rep->pc, 0, rep->hi, nat->hi, 0, steps);
|
|
return;
|
|
}
|
|
if (rep->lo != nat->lo) {
|
|
lsf_latch(3, frame, entry, rep->pc, 0, rep->lo, nat->lo, 0, steps);
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void lsf_replay_and_compare(CPUState *native_post, uint32_t frame,
|
|
uint32_t entry_pc) {
|
|
CPUState rep = s_lsf_R0;
|
|
uint32_t target = native_post->pc;
|
|
rep.pc = entry_pc;
|
|
|
|
s_ls_trace_idx = 0;
|
|
s_ls_mismatch = 0;
|
|
s_ls_m_kind = 0;
|
|
s_ls_replay_done = 0;
|
|
|
|
int sav_unsup_seen = g_unsupported_seen;
|
|
uint32_t sav_unsup_pc = g_unsupported_pc;
|
|
uint32_t sav_unsup_insn = g_unsupported_insn;
|
|
const char *sav_unsup_reason = g_unsupported_reason;
|
|
uint32_t sav_store_pc = g_debug_last_store_pc;
|
|
uint32_t sav_func_addr = g_debug_current_func_addr;
|
|
|
|
g_ls_replay_active = 1;
|
|
g_ls_mode = 2;
|
|
|
|
uint32_t pc = entry_pc;
|
|
uint32_t steps = 0;
|
|
int done = 0;
|
|
enum { MAX_FUNC_REPLAY_INSNS = 250000 };
|
|
for (;;) {
|
|
if (steps > 0 && target != 0 && ls_same_phys(pc, target) &&
|
|
s_ls_trace_idx == s_ls_trace_n) {
|
|
done = 1;
|
|
break;
|
|
}
|
|
if (steps >= MAX_FUNC_REPLAY_INSNS) {
|
|
lsf_latch(10, frame, entry_pc, pc, 0, target, pc, 0, steps);
|
|
break;
|
|
}
|
|
if (s_ls_mismatch || s_ls_replay_done) break;
|
|
|
|
uint32_t next_pc = 0;
|
|
s_ls_cur_pc = pc;
|
|
g_unsupported_seen = 0;
|
|
int transferred = exec_one(&rep, pc, &next_pc);
|
|
if (g_unsupported_seen) {
|
|
lsf_latch(11, frame, entry_pc, pc, 0, 0, g_unsupported_insn, 0, steps);
|
|
break;
|
|
}
|
|
pc = transferred ? rep.pc : next_pc;
|
|
rep.pc = pc;
|
|
steps++;
|
|
}
|
|
|
|
g_ls_mode = 0;
|
|
g_ls_replay_active = 0;
|
|
g_unsupported_seen = sav_unsup_seen;
|
|
g_unsupported_pc = sav_unsup_pc;
|
|
g_unsupported_insn = sav_unsup_insn;
|
|
g_unsupported_reason = sav_unsup_reason;
|
|
g_debug_last_store_pc = sav_store_pc;
|
|
g_debug_current_func_addr = sav_func_addr;
|
|
|
|
s_lsf_div.segments_checked++;
|
|
|
|
if (s_ls_mismatch) {
|
|
int k = (s_ls_m_kind == 1) ? 6 : (s_ls_m_kind == 2) ? 7 :
|
|
(s_ls_m_kind == 3) ? 5 : 8;
|
|
lsf_latch(k, frame, entry_pc, s_ls_m_pc, s_ls_m_addr,
|
|
s_ls_m_exp, s_ls_m_act, 0, steps);
|
|
return;
|
|
}
|
|
if (s_ls_replay_done) {
|
|
lsf_latch(8, frame, entry_pc, s_ls_cur_pc, 0, (uint32_t)s_ls_trace_n,
|
|
(uint32_t)s_ls_trace_idx, 0, steps);
|
|
return;
|
|
}
|
|
if (!done) {
|
|
lsf_latch(10, frame, entry_pc, pc, 0, target, pc, 0, steps);
|
|
return;
|
|
}
|
|
if (s_ls_trace_idx != s_ls_trace_n) {
|
|
lsf_latch(9, frame, entry_pc, pc, 0, (uint32_t)s_ls_trace_n,
|
|
(uint32_t)s_ls_trace_idx, 0, steps);
|
|
return;
|
|
}
|
|
|
|
lsf_compare_regs(&rep, native_post, frame, entry_pc, steps);
|
|
}
|
|
|
|
void ls_func_enter(uint32_t entry_pc, CPUState *cpu) {
|
|
if (s_lsf_frame_hi == 0 || s_lsf_div.found || !cpu) return;
|
|
if (g_ls_replay_active || g_ls_mode != 0 || s_lsf_active) {
|
|
s_lsf_div.skipped_conflict++;
|
|
return;
|
|
}
|
|
if (psx_get_in_exception()) {
|
|
s_lsf_div.skipped_irq++;
|
|
return;
|
|
}
|
|
uint32_t frame = (uint32_t)s_frame_count;
|
|
if (frame < s_lsf_frame_lo || frame > s_lsf_frame_hi) {
|
|
s_lsf_div.skipped_disabled++;
|
|
return;
|
|
}
|
|
uint32_t phys = entry_pc & 0x1FFFFFFFu;
|
|
if (phys < 0x00010000u || phys >= 0x00200000u) return;
|
|
|
|
s_lsf_dispatch_entry = entry_pc;
|
|
s_lsf_entry = entry_pc;
|
|
s_lsf_frame = frame;
|
|
s_lsf_saw_irq = 0;
|
|
s_lsf_active = 1;
|
|
s_lsf_recording = 0;
|
|
}
|
|
|
|
void ls_func_exit(uint32_t entry_pc, CPUState *cpu, int handled) {
|
|
if (!s_lsf_active) return;
|
|
if (s_lsf_dispatch_entry != entry_pc) {
|
|
g_ls_mode = 0;
|
|
s_lsf_active = 0;
|
|
s_lsf_recording = 0;
|
|
s_lsf_div.skipped_conflict++;
|
|
return;
|
|
}
|
|
g_ls_mode = 0;
|
|
s_lsf_active = 0;
|
|
if (!s_lsf_recording) {
|
|
s_lsf_div.skipped_unhandled++;
|
|
return;
|
|
}
|
|
s_lsf_recording = 0;
|
|
|
|
if (!handled || !cpu) {
|
|
s_lsf_div.skipped_unhandled++;
|
|
return;
|
|
}
|
|
if (s_lsf_saw_irq) {
|
|
s_lsf_div.skipped_irq++;
|
|
return;
|
|
}
|
|
if (s_ls_overflow) {
|
|
s_lsf_div.skipped_overflow++;
|
|
return;
|
|
}
|
|
if (s_lsf_record_only) {
|
|
s_lsf_div.segments_checked++;
|
|
return;
|
|
}
|
|
lsf_replay_and_compare(cpu, s_lsf_frame, s_lsf_entry);
|
|
}
|
|
|
|
void ls_at_leader(uint32_t leader_phys, CPUState *cpu) {
|
|
if (s_lsf_active) {
|
|
if (!s_lsf_recording && cpu && !s_lsf_div.found && g_ls_mode == 0) {
|
|
s_lsf_R0 = *cpu;
|
|
s_lsf_entry = leader_phys | 0x80000000u;
|
|
s_ls_trace_n = 0;
|
|
s_ls_trace_idx = 0;
|
|
s_ls_overflow = 0;
|
|
s_ls_mismatch = 0;
|
|
s_ls_m_kind = 0;
|
|
s_lsf_recording = 1;
|
|
g_ls_mode = 1;
|
|
}
|
|
return; /* function-scope recording owns g_ls_mode */
|
|
}
|
|
if (s_ls_frame_hi == 0 || s_ls_div.found || !cpu) return;
|
|
if (g_ls_mode == 2) return; /* re-entrancy guard (shouldn't happen) */
|
|
uint32_t frame = (uint32_t)s_frame_count;
|
|
int in_win = (frame >= s_ls_frame_lo && frame <= s_ls_frame_hi);
|
|
if (!in_win) { if (g_ls_mode == 1) { g_ls_mode = 0; s_ls_prev_valid = 0; } return; }
|
|
|
|
if (s_ls_prev_valid && g_ls_mode == 1) { /* finalize previous game block */
|
|
g_ls_mode = 0;
|
|
if (!s_ls_record_only) { /* record_only: skip inline replay (perturbation test) */
|
|
ls_replay_and_compare(cpu, frame, leader_phys); /* this leader = where prev block went */
|
|
if (s_ls_div.found) { s_ls_prev_valid = 0; return; }
|
|
} else {
|
|
s_ls_div.blocks_checked++; /* count recorded blocks */
|
|
}
|
|
s_ls_prev_valid = 0;
|
|
}
|
|
/* Only START recording for a genuinely COMPILED game-text block:
|
|
* - !g_ls_dirty_observe: the dirty-RAM interp loop also calls cyc_observe
|
|
* (per-instruction, for the cycle ruler); skip those — they're already
|
|
* interpreted (clean game text dispatched FROM the dirty path still runs
|
|
* compiled, so we can't use g_dirty_interp_active here).
|
|
* - [0x10000, 0x200000): game EXE text in main RAM (above the low-RAM
|
|
* kernel/relocated-BIOS area, which isn't the regression locus). */
|
|
if (!g_ls_dirty_observe && leader_phys >= 0x00010000u && leader_phys < 0x00200000u) {
|
|
s_ls_R0 = *cpu;
|
|
s_ls_block = leader_phys;
|
|
s_ls_trace_n = 0; s_ls_trace_idx = 0; s_ls_overflow = 0; s_ls_mismatch = 0;
|
|
s_ls_prev_valid = 1;
|
|
g_ls_mode = 1;
|
|
}
|
|
}
|
|
|
|
int ls_get_diverge_json(char *buf, int buflen) {
|
|
static const char *kn[] = {"none","reg","hi","lo","write-val","read-addr",
|
|
"write-addr","trace-exhausted","compiled-extra-ops","path-cap"};
|
|
int k = (s_ls_div.kind >= 0 && s_ls_div.kind <= 9) ? s_ls_div.kind : 0;
|
|
int p = snprintf(buf, buflen,
|
|
"{\"found\":%d,\"kind\":\"%s\",\"frame\":%u,\"block\":\"0x%08X\",\"pc\":\"0x%08X\","
|
|
"\"addr\":\"0x%08X\",\"interp_expected\":\"0x%08X\",\"compiled_actual\":\"0x%08X\","
|
|
"\"reg\":%d,\"blocks_checked\":%llu,\"window\":[%u,%u],\"div_idx\":%d,\"trace\":[",
|
|
s_ls_div.found, kn[k], s_ls_div.frame, s_ls_div.block, s_ls_div.pc,
|
|
s_ls_div.addr, s_ls_div.exp, s_ls_div.act, s_ls_div.detail,
|
|
(unsigned long long)s_ls_div.blocks_checked, s_ls_frame_lo, s_ls_frame_hi,
|
|
s_ls_div_idx);
|
|
for (int i = 0; i < s_ls_div_trace_n && p < buflen - 48; i++) {
|
|
p += snprintf(buf + p, buflen - p, "%s\"%c%d:%08X=%08X\"",
|
|
i ? "," : "", s_ls_div_trace[i].is_write ? 'W' : 'R',
|
|
s_ls_div_trace[i].size, s_ls_div_trace[i].addr, s_ls_div_trace[i].val);
|
|
}
|
|
p += snprintf(buf + p, buflen - p, "]}");
|
|
return p;
|
|
}
|
|
|
|
int ls_get_func_json(char *buf, int buflen) {
|
|
static const char *kn[] = {
|
|
"none","gpr","hi","lo","pc","write-val","read-addr","write-addr",
|
|
"trace-exhausted","trace-leftover","path-cap","unsupported"
|
|
};
|
|
int k = (s_lsf_div.kind >= 0 && s_lsf_div.kind <= 11) ? s_lsf_div.kind : 0;
|
|
int p = snprintf(buf, buflen,
|
|
"{\"found\":%d,\"kind\":\"%s\",\"frame\":%u,\"entry\":\"0x%08X\","
|
|
"\"pc\":\"0x%08X\",\"addr\":\"0x%08X\","
|
|
"\"interp_expected\":\"0x%08X\",\"compiled_actual\":\"0x%08X\","
|
|
"\"reg\":%d,\"segments_checked\":%llu,\"skipped_irq\":%llu,"
|
|
"\"skipped_overflow\":%llu,\"skipped_unhandled\":%llu,"
|
|
"\"skipped_conflict\":%llu,\"skipped_disabled\":%llu,"
|
|
"\"window\":[%u,%u],\"trace_ops\":%u,\"replay_ops\":%u,"
|
|
"\"steps\":%u,\"div_idx\":%d,\"trace\":[",
|
|
s_lsf_div.found, kn[k], s_lsf_div.frame, s_lsf_div.entry,
|
|
s_lsf_div.pc, s_lsf_div.addr, s_lsf_div.exp, s_lsf_div.act,
|
|
s_lsf_div.detail, (unsigned long long)s_lsf_div.segments_checked,
|
|
(unsigned long long)s_lsf_div.skipped_irq,
|
|
(unsigned long long)s_lsf_div.skipped_overflow,
|
|
(unsigned long long)s_lsf_div.skipped_unhandled,
|
|
(unsigned long long)s_lsf_div.skipped_conflict,
|
|
(unsigned long long)s_lsf_div.skipped_disabled,
|
|
s_lsf_frame_lo, s_lsf_frame_hi, s_lsf_div.trace_ops,
|
|
s_lsf_div.replay_ops, s_lsf_div.steps, s_lsf_div_idx);
|
|
for (int i = 0; i < s_lsf_div_trace_n && p < buflen - 48; i++) {
|
|
p += snprintf(buf + p, buflen - p, "%s\"%c%d:%08X=%08X\"",
|
|
i ? "," : "", s_lsf_div_trace[i].is_write ? 'W' : 'R',
|
|
s_lsf_div_trace[i].size, s_lsf_div_trace[i].addr,
|
|
s_lsf_div_trace[i].val);
|
|
}
|
|
p += snprintf(buf + p, buflen - p, "]}");
|
|
return p;
|
|
}
|