mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 18:52:11 -04:00
c496dc1db1
Pig-into-pig crash (TombaRecomp issue #1, v0.1.5): when a cached-DLL native overlay function called uncached overlay code, the dirty interp's overlay local-flow chained the callee's jr-ra straight past the nested dispatch's return point (the target page is always dirty for overlays), executing the native caller's tail in the interpreter. The suspended native host frames then resumed and double-executed those tails against a moved guest stack, restoring garbage callee-saved registers — main loop's s1 mode-switch constant destroyed -> zombie loop (blue screen, music on IRQs) or wild pointer -> fatal MMIO READ16. Thread stop_addr (psx_dispatch_call's return_addr) through dirty_ram_dispatch; local flow exits when any transfer or straight-line fall-through reaches it, handing control back to the dispatch loop's existing pc==stop_addr return. Signature change is deliberate: stale generated dispatch fails to compile, forcing regen (BIOS + Tomba regenerated; other titles regen on their own branches). Sibling of the Bug A call-contract fix, on the return side. Root-caused live via always-on rings on a wedged instance; user-verified fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
202 lines
9.1 KiB
C
202 lines
9.1 KiB
C
/* dirty_ram_interp.h — interpret-on-dispatch for install-at-runtime RAM.
|
|
*
|
|
* See CLAUDE.md Rule 18 and docs/dynamic_handler_install.md for the full
|
|
* rationale. The PS1 BIOS dynamically writes 4-instruction dispatch stubs
|
|
* into kernel RAM (notably RAM 0xCF0 for the SIO data-byte handler). A
|
|
* static recompiler can't see those bytes at compile time, so a small MIPS
|
|
* interpreter here runs them at dispatch time on the same CPUState.
|
|
*
|
|
* Scope: this is NOT a fallback for code the recompiler failed to translate.
|
|
* It runs only against PCs in pages that have been written-to since boot.
|
|
* Static-recompiled code continues to handle ROM-resident code and game
|
|
* RAM. See docs/dynamic_handler_install.md for the inline note about a
|
|
* potential future migration to runtime JIT (Option B).
|
|
*/
|
|
#ifndef PSXRECOMP_DIRTY_RAM_INTERP_H
|
|
#define PSXRECOMP_DIRTY_RAM_INTERP_H
|
|
|
|
#include <stdint.h>
|
|
#include "cpu_state.h"
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
/* Returns 1 if `addr` lies in a dirty kernel-RAM page and the interpreter
|
|
* ran a basic block at that PC. Returns 0 if `addr` is clean (caller must
|
|
* fall back to the static dispatch table). On return with 1, cpu->pc is
|
|
* either 0 (block ended on jr $ra style return) or the target of a tail
|
|
* jump that the dispatch trampoline should re-enter.
|
|
*
|
|
* `stop_addr` is the dispatch loop's return contract (psx_dispatch_call's
|
|
* return_addr; 0 for plain tail dispatches). The interpreter's overlay
|
|
* local-flow chaining MUST NOT run past it: when a transfer (or fall-
|
|
* through) reaches stop_addr, the interpreter exits with cpu->pc ==
|
|
* stop_addr so the dispatch loop returns into the suspended native
|
|
* caller. Skipping this check lets the interp execute the native
|
|
* caller's tail itself; the suspended native host frame then resumes and
|
|
* double-executes that tail against a moved guest stack, restoring
|
|
* garbage callee-saved registers (the pig-throw blue-screen/MMIO-fatal
|
|
* corruption, 2026-06-10). */
|
|
int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr);
|
|
|
|
/* Overlay-cache windows — the address ranges eligible for capture, offline
|
|
* recompilation, and per-entry-validated native execution (Rule 18 code that
|
|
* does not exist in any compile-time image):
|
|
*
|
|
* Kernel RAM [0x00000, 0x10000): the BIOS part-2 image relocated to RAM
|
|
* plus install-at-runtime stubs (e.g. the SIO data-byte stub at 0xCF0).
|
|
* Dirty-tracked per CPU store (dirty_ram_mark_kernel_write).
|
|
* Overlay region [OVERLAY_REGION_FLOOR, RAM_SIZE): game overlays loaded by
|
|
* CD DMA (dirty_ram_mark_executable_range).
|
|
*
|
|
* Main-EXE text [0x10000, OVERLAY_REGION_FLOOR) is statically recompiled and
|
|
* is NEVER a capture/candidate window. The floor equals the main EXE text end
|
|
* (Tomba: load 0x10000 + text 0x88000).
|
|
*
|
|
* The interpreter's LOCAL-FLOW gates (is_local_dirty_target /
|
|
* allow_local_dirty_flow) intentionally use OVERLAY_REGION_FLOOR alone, NOT
|
|
* this window predicate: kernel-RAM code runs in exception context where the
|
|
* verified per-block dispatch cadence is preserved. Native coverage for the
|
|
* kernel window comes from the overlay loader, not from interp chaining. */
|
|
#define DIRTY_RAM_KERNEL_WINDOW_END 0x00010000u
|
|
#define OVERLAY_REGION_FLOOR 0x00098000u
|
|
static inline int overlay_cache_window_contains(uint32_t phys) {
|
|
return phys < DIRTY_RAM_KERNEL_WINDOW_END || phys >= OVERLAY_REGION_FLOOR;
|
|
}
|
|
|
|
/* Test whether a given physical kernel-RAM address is in a page that was
|
|
* written-to since boot. Defined in memory.c. */
|
|
int dirty_ram_is_dirty(uint32_t phys);
|
|
uint32_t dirty_ram_get_bitmap(void);
|
|
uint32_t dirty_ram_get_bitmap_word(uint32_t word_index);
|
|
uint32_t dirty_ram_get_bitmap_word_count(void);
|
|
void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len);
|
|
|
|
/* Counters for visibility / TCP debug. Increment in interpreter; expose
|
|
* via debug_server.c if helpful. */
|
|
extern uint64_t g_dirty_ram_blocks_run; /* basic blocks interpreted */
|
|
extern uint64_t g_dirty_ram_insns_run; /* instructions interpreted */
|
|
extern uint64_t g_dirty_window_dispatches; /* interp dispatches inside a
|
|
* capture window (autocapture
|
|
* pressure signal, step 2.8) */
|
|
extern uint64_t g_dirty_ram_aborts; /* unsupported-opcode aborts */
|
|
extern uint64_t g_dirty_ram_guard_yields; /* long dirty loops yielded */
|
|
extern uint64_t g_dirty_ram_unsupported_midblock;
|
|
extern uint32_t g_dirty_ram_last_unsupported_entry;
|
|
extern uint32_t g_dirty_ram_last_unsupported_entry_ra;
|
|
extern uint32_t g_dirty_ram_last_unsupported_entry_sp;
|
|
extern uint32_t g_dirty_ram_last_unsupported_insns;
|
|
extern uint32_t g_dirty_ram_last_unsupported_pc;
|
|
extern uint32_t g_dirty_ram_last_unsupported_insn;
|
|
extern const char *g_dirty_ram_last_unsupported_reason;
|
|
|
|
/* Per-entry-PC counters. Aggregate counters above hide which install
|
|
* stubs actually fire — a single noisy spurious-dispatch site can mask
|
|
* a legitimate handler that never runs. This open-addressed table keys
|
|
* on the entry PC of each interpreted block. */
|
|
#define DIRTY_RAM_PC_TABLE_SIZE 65536
|
|
typedef struct {
|
|
uint32_t pc; /* entry PC, 0 = empty slot */
|
|
uint64_t hits; /* number of times dispatched here */
|
|
uint64_t insns; /* total instructions executed across hits */
|
|
uint64_t entry_hits; /* dispatches that arrived from NATIVE code (call /
|
|
* dispatch loop), not from the interpreter's own
|
|
* block-to-block chaining. `hits` conflates the two:
|
|
* below the local-flow floor every taken branch
|
|
* re-dispatches, so chain blocks dominate. entry_hits
|
|
* is the evidence stream for interior-alias seeds. */
|
|
} DirtyRamPcEntry;
|
|
extern DirtyRamPcEntry g_dirty_ram_pc_table[DIRTY_RAM_PC_TABLE_SIZE];
|
|
/* Companion table: every PC the interpreter actually executes (not just block
|
|
* entries). overlay_capture uses both to report execution-verified seeds. */
|
|
extern DirtyRamPcEntry g_dirty_ram_exec_pc_table[DIRTY_RAM_PC_TABLE_SIZE];
|
|
|
|
/* Block-entry ring buffer. Records every dispatch into dirty RAM with the
|
|
* caller's RA at entry, plus argument context — answers
|
|
* "who tried to JALR into this RAM stub, with what args".
|
|
* Always-on, eviction keeps memory bounded; callers query the window of
|
|
* interest (CLAUDE.md global rule on ring buffers).
|
|
*
|
|
* Limitation: this captures dispatches into RAM-resident code only. ROM
|
|
* recompiled-C → recompiled-C calls (direct C function calls in
|
|
* generated dispatch table) are NOT captured here. For BIOS shell
|
|
* investigation that's fine — shell code lives in RAM (0x800XXXXX).
|
|
*
|
|
* `ra` is cpu->gpr[31] at dispatch time. For normal JAL-style calls,
|
|
* (ra - 8) gives the caller's PC. For J/JR/tail-calls it's only a
|
|
* heuristic — treat ra as "ra_callsite_guess", not authoritative. */
|
|
#define DIRTY_RAM_BLOCK_LOG_CAP (1u << 22) /* 4M entries (~128 MB).
|
|
* At ~580K dispatches/s during boot, this retains ~7s of history; at
|
|
* ~10K/s during modal idle, ~400s. Sized for retroactive press-window
|
|
* analysis without the prior 16K ring's 28-ms eviction problem. */
|
|
typedef struct {
|
|
uint64_t seq; /* monotonic, unique per entry */
|
|
uint32_t target; /* entry PC (RAM address) */
|
|
uint32_t ra; /* cpu->gpr[31] at dispatch — caller's return target */
|
|
uint32_t a0; /* cpu->gpr[4] at dispatch */
|
|
uint32_t a1; /* cpu->gpr[5] at dispatch */
|
|
uint32_t a2; /* cpu->gpr[6] at dispatch */
|
|
uint32_t a3; /* cpu->gpr[7] at dispatch */
|
|
uint32_t t0; /* cpu->gpr[8] at dispatch */
|
|
uint32_t t1; /* cpu->gpr[9] at dispatch */
|
|
uint32_t t2; /* cpu->gpr[10] at dispatch */
|
|
uint32_t sp; /* cpu->gpr[29] at dispatch */
|
|
uint32_t frame; /* s_frame_count at the time of dispatch */
|
|
} DirtyRamBlockLogEntry;
|
|
extern DirtyRamBlockLogEntry g_dirty_ram_block_log[DIRTY_RAM_BLOCK_LOG_CAP];
|
|
extern uint64_t g_dirty_ram_block_log_seq;
|
|
|
|
#define DIRTY_RAM_FLOW_LOG_CAP (1u << 16)
|
|
typedef struct {
|
|
uint64_t seq;
|
|
uint32_t pc;
|
|
uint32_t target;
|
|
uint32_t ra;
|
|
uint32_t a0;
|
|
uint32_t a1;
|
|
uint32_t a2;
|
|
uint32_t a3;
|
|
uint32_t sp;
|
|
uint32_t frame;
|
|
} DirtyRamFlowLogEntry;
|
|
extern DirtyRamFlowLogEntry g_dirty_ram_flow_log[DIRTY_RAM_FLOW_LOG_CAP];
|
|
extern uint64_t g_dirty_ram_flow_log_seq;
|
|
|
|
#define DIRTY_RAM_INSN_LOG_CAP (1u << 16)
|
|
typedef struct {
|
|
uint64_t seq;
|
|
uint32_t pc;
|
|
uint32_t insn;
|
|
uint32_t next_pc;
|
|
uint32_t target;
|
|
uint32_t before_s0;
|
|
uint32_t after_s0;
|
|
uint32_t sp;
|
|
uint32_t ra;
|
|
uint32_t v0;
|
|
uint32_t v1;
|
|
uint32_t a0;
|
|
uint32_t a1;
|
|
uint32_t a2;
|
|
uint32_t a3;
|
|
uint32_t t0;
|
|
uint32_t t1;
|
|
uint32_t t2;
|
|
uint32_t current_tcb;
|
|
uint32_t task_ptr;
|
|
uint32_t task_mode;
|
|
uint32_t task_submode;
|
|
uint32_t frame;
|
|
uint8_t transferred;
|
|
uint8_t pad[3];
|
|
} DirtyRamInsnLogEntry;
|
|
extern DirtyRamInsnLogEntry g_dirty_ram_insn_log[DIRTY_RAM_INSN_LOG_CAP];
|
|
extern uint64_t g_dirty_ram_insn_log_seq;
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* PSXRECOMP_DIRTY_RAM_INTERP_H */
|