mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
474 lines
25 KiB
C
474 lines
25 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 <string.h>
|
|
#include "psx_memory.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);
|
|
|
|
/* Replay [start_pc, stop_pc) of already-loaded guest code from the CPU state
|
|
* given: every PC in the range is interpreted from its live RAM bytes, even
|
|
* where a compiled body or a compiled resume point (the return address of a
|
|
* call) begins, so a span can start in the middle of a compiled function.
|
|
* Calls the span makes run on their normal backend and return into it.
|
|
* Returns 1 when control reaches stop_pc, 0 when it leaves the range any other
|
|
* way (a return, a jump out, a nested span) or runs past max_insns
|
|
* interpreted instructions. The range is half-open, CODE identity, one span
|
|
* at a time. A span is a replay, not a function entry: it fires no
|
|
* function-entry hooks at its own PCs and stays out of capture, alias seeding
|
|
* and per-PC entry statistics. Instruction hooks fire as in compiled code.
|
|
* Mods reach it through psx_mod_run_guest_span (render passes only). */
|
|
int dirty_ram_run_span(CPUState* cpu, uint32_t start_pc, uint32_t stop_pc,
|
|
uint64_t max_insns);
|
|
/* Why the last span that failed stopped: 1 returned (pc 0), 2 left the range,
|
|
* 3 call-bail unwind, 4 instruction budget, 5 the interpreter declined the PC,
|
|
* 6 bad arguments or a span already open, 7 a call did not return to the
|
|
* span (`after`). `pc` is where control was. */
|
|
typedef struct DirtyRamSpanFailure {
|
|
uint32_t reason, pc, start_pc, stop_pc;
|
|
uint32_t ra, after; /* $ra at the exit; pc after a call that did not return */
|
|
uint64_t insns;
|
|
} DirtyRamSpanFailure;
|
|
void dirty_ram_span_last_failure(DirtyRamSpanFailure *out);
|
|
/* The open span (0,0 = none), for landings that longjmp past its exit. */
|
|
void dirty_ram_span_get(uint32_t *lo, uint32_t *hi);
|
|
void dirty_ram_span_set(uint32_t lo, uint32_t hi);
|
|
|
|
/* Retire a deferred R3000A load-delay writeback (see dirty_ram_interp.c). The
|
|
* interpreter defers a load's destination-register write past the delay-slot
|
|
* instruction, as hardware does; call this anywhere control leaves the
|
|
* interpreter or an exception is delivered, so no register write is dropped. */
|
|
void dirty_ram_ld_delay_flush(CPUState* cpu);
|
|
|
|
/* Drop a deferred load-delay writeback WITHOUT applying it. Required on
|
|
* savestate/RB restore: the snap already has architectural GPRs, and a
|
|
* host-static pending load from the pre-load timeline must not stomp them
|
|
* on the next interp step (selfcheck MotK v0=countdown vs v0=1 forks). */
|
|
void dirty_ram_ld_delay_discard(void);
|
|
|
|
/* Save / put back the deferred load-delay writeback exactly. For a landing
|
|
* that longjmps out of interpreted code and then resumes the interrupted
|
|
* code (the render-pass watchdog abort, render_pass.c): the pass's pending
|
|
* load must not reach the resumed timeline, and the interrupted one's must
|
|
* survive. */
|
|
typedef struct DirtyRamLoadDelay {
|
|
uint32_t rt, val, age;
|
|
int armed;
|
|
} DirtyRamLoadDelay;
|
|
void dirty_ram_ld_delay_save(DirtyRamLoadDelay *out);
|
|
void dirty_ram_ld_delay_restore(const DirtyRamLoadDelay *in);
|
|
|
|
/* Re-anchor host-only dirty IRQ pump ambient (entry-poll stride + 4096-insn
|
|
* gap) after snap restore. Not in boot_state — peers that drifted through FMV
|
|
* otherwise entered the post-FMV dirty wait on opposite poll phases. */
|
|
void dirty_ram_irq_ambient_resync_after_restore(void);
|
|
|
|
/* 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, live RAM end): game overlays loaded by
|
|
* CD DMA (dirty_ram_mark_executable_range).
|
|
*
|
|
* Main-EXE text [0x10000, OVERLAY_REGION_FLOOR): CLEAN pages are statically
|
|
* recompiled and never captured. But games that discard their boot/init code
|
|
* load gameplay overlays OVER this region (Tomba 2: hot gameplay code at
|
|
* 0x1DA88/0x24548/0x3116C, well below its 0x38800 floor). Post-baseline
|
|
* (dirty_ram_clear_image_baseline) a DIRTY page here is by definition live
|
|
* overlay code — the compiled image is stale for it — so dirty boot-text
|
|
* pages ARE a capture/candidate window (2026-07-06; before this they could
|
|
* only interpret, which dominated wall time at ~300ns/insn).
|
|
*
|
|
* The interpreter's LOCAL-FLOW gates (is_local_dirty_target /
|
|
* phys_is_overlay_flow_region) use DIRTY_RAM_KERNEL_WINDOW_END alone: only
|
|
* the kernel window stays per-block (it runs in exception context where the
|
|
* verified dispatch cadence is delicate). Native coverage for the kernel
|
|
* window comes from the overlay loader, not from interp chaining. */
|
|
#define DIRTY_RAM_KERNEL_WINDOW_END 0x00010000u
|
|
|
|
/* The overlay-region floor is the END of THIS game's statically-recompiled
|
|
* main-EXE text (phys). Everything at/above it is runtime-loaded overlay code,
|
|
* eligible for in-interpreter local-flow chaining and overlay-cache capture.
|
|
*
|
|
* It is a RUNTIME value (g_overlay_region_floor), NOT a compile-time constant,
|
|
* because it differs per game: Tomba 1's text ends at 0x98000 (0x10000+0x88000)
|
|
* but Tomba 2's boot EXE is only 0x28800 (text ends at 0x38800). A hardcoded
|
|
* 0x98000 misclassified Tomba 2's overlays (loaded at 0x85000+) as main-EXE
|
|
* text, forcing them onto the slow block-by-block dispatch path AND through the
|
|
* bail-prone non-local-call contract — the Whoopee-Camp splash freeze. main.cpp
|
|
* sets g_overlay_region_floor = (load_address + text_size) & 0x1FFFFFFF at game
|
|
* load; OVERLAY_REGION_FLOOR_DEFAULT applies for BIOS-only runs. */
|
|
#define OVERLAY_REGION_FLOOR_DEFAULT 0x00098000u
|
|
extern uint32_t g_overlay_region_floor;
|
|
#define OVERLAY_REGION_FLOOR (g_overlay_region_floor)
|
|
|
|
/* The text-image BASE (phys) = this game's main-EXE load address. The floor
|
|
* above only bounds the text from ABOVE; on its own it encodes the assumption
|
|
* that the boot EXE sits at the bottom of usable RAM, so that [KERNEL_END,
|
|
* FLOOR) is text and [FLOOR, RAM) is overlay. That holds for a load_address of
|
|
* 0x80010000 (text base == KERNEL_END, so the region below text is empty) but
|
|
* is FALSE for a boot EXE that loads HIGH and streams its gameplay code into
|
|
* the RAM BELOW itself: Klonoa loads at 0x80180000 (text 0x180000-0x18B000,
|
|
* overlays at 0x10000-0x130000), Street Fighter Alpha 3 at 0x80113B00,
|
|
* Bomberman Fantasy Race at 0x8003004C. For those the sub-text RAM was
|
|
* misclassified as main-EXE text — clear_image_baseline() wiped its dirty bits
|
|
* and the dispatch admit-heuristic refused it, so a JALR into an overlay page
|
|
* the CD DMA'd there fell through to psx_unknown_dispatch and fail-fast
|
|
* exit(1) (Klonoa, frame 687, target 0x80123D00).
|
|
*
|
|
* The overlay region is therefore BOTH sides of the text image, not just above
|
|
* it. main.cpp pins g_text_image_lo = load_address & 0x1FFFFFFF at game load;
|
|
* the default equals DIRTY_RAM_KERNEL_WINDOW_END so the below-text clause is
|
|
* empty for BIOS-only runs and for every bottom-loading game. */
|
|
extern uint32_t g_text_image_lo;
|
|
|
|
/* 1 iff phys is runtime-loaded overlay RAM rather than main-EXE text — either
|
|
* side of the text image. Identical to `phys >= FLOOR` whenever the game loads
|
|
* at the bottom of RAM (g_text_image_lo == KERNEL_WINDOW_END). */
|
|
static inline int phys_is_overlay_region(uint32_t phys) {
|
|
return phys >= g_overlay_region_floor ||
|
|
(phys >= DIRTY_RAM_KERNEL_WINDOW_END && phys < g_text_image_lo);
|
|
}
|
|
|
|
/* 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);
|
|
|
|
/* Kernel-image bless (memory.c). A dispatch key in the relocated kernel
|
|
* window (RAM [0x500,0x8500), the BIOS's boot-time copy of ROM 0x1FC10000+)
|
|
* may run its statically-recompiled native function IFF the live RAM bytes
|
|
* of the code reachable from it (emitter-supplied body extent) byte-match
|
|
* the ROM source. Lazily verified, cached per entry, invalidated precisely
|
|
* on writes into the body. Runtime-patched bodies (pad/SIO install stubs)
|
|
* never verify and keep interpreting — faithful either way. */
|
|
int psx_kernel_bless_dispatchable(uint32_t phys);
|
|
/* True when a declared kernel patch range ENDS at this RAM address. The
|
|
* emitter registered that PC as a continuation key, so the interpreter hands
|
|
* straight-line flow back to static dispatch there and only the guest's
|
|
* patched words interpret (memory.c psx_bios_kernel_patch_ranges). */
|
|
int psx_kernel_patch_range_ends_at(uint32_t phys);
|
|
void psx_kernel_bless_note_range(uint32_t phys, uint32_t len);
|
|
void psx_kernel_bless_stats(uint64_t out[8]);
|
|
void psx_kernel_bless_resync_after_restore(void);
|
|
/* Soft-return rematch / BIOS switch: drop latched SCPH↔OpenBIOS window +
|
|
* CLEAN/MISMATCH so the next kbless_on() re-reads psx_bios_image. */
|
|
void psx_kernel_bless_reset_for_boot(void);
|
|
|
|
/* Capture/candidate window membership. Kernel window and overlay region are
|
|
* unconditional; main-EXE text [KERNEL_WINDOW_END, FLOOR) is included only
|
|
* when the page is dirty — i.e. a gameplay overlay overwrote the boot text
|
|
* after the game-start baseline, so the bytes there are runtime overlay code
|
|
* (see the window model note above). Clean boot text never enters the window
|
|
* (it runs compiled; capturing it would violate the static-first design). */
|
|
static inline int overlay_cache_window_contains(uint32_t phys) {
|
|
return phys < DIRTY_RAM_KERNEL_WINDOW_END
|
|
|| phys >= OVERLAY_REGION_FLOOR
|
|
|| dirty_ram_is_dirty(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_set_bitmap_words(const uint32_t* words, uint32_t count);
|
|
/* Rematch / session_reboot: wipe host dirty tracking so dig0 matches a cold
|
|
* process (memory_init clears RAM but used to leave these bitmaps sticky). */
|
|
void dirty_ram_reset_for_boot(void);
|
|
/* After bulk RAM restore (savestate): bump overlay page gens + lazy-miss epoch
|
|
* so native overlays re-hash against restored bytes; also drop sticky
|
|
* text_diverged/modified bitmaps (host-only — restored RAM may match ref). */
|
|
void overlay_watch_invalidate_after_ram_restore(void);
|
|
void dirty_ram_text_guard_resync_after_restore(void);
|
|
void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len);
|
|
void dirty_ram_register_text_image(uint32_t phys_lo, const uint8_t *bytes,
|
|
uint32_t len);
|
|
int dirty_ram_text_native_ok(uint32_t phys);
|
|
/* Exact CFG ranges; exec_pc clips ranges that end before the resume PC. */
|
|
int dirty_ram_text_native_ok_ranges_from(const uint32_t *lo_len_pairs,
|
|
uint32_t count,
|
|
uint32_t exec_pc);
|
|
int dirty_ram_text_native_ok_ranges(const uint32_t *lo_len_pairs,
|
|
uint32_t count);
|
|
int dirty_ram_text_image_registered(void);
|
|
/* Bless an intentional runtime data patch (e.g. text_xlate string/glyph tables)
|
|
* into the text reference image so it is not mistaken for self-modifying code. */
|
|
void dirty_ram_text_bless(uint32_t phys, const uint8_t *bytes, uint32_t len);
|
|
uint64_t dirty_ram_text_native_blocked(void);
|
|
uint32_t dirty_ram_text_diverged_pages(void);
|
|
|
|
/* 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. */
|
|
/* 65536 -> 262144 (2026-07-03): Tomba2's Trolley attract demo interprets a
|
|
* working set past 64K distinct PCs; a full table degraded every lookup to a
|
|
* full-table scan (0.3 fps host burn in pc_table_get_or_insert_in) and
|
|
* silently truncated overlay-capture seed lists (MAX_CAPTURE_PCS aliases
|
|
* this). Probe length is bounded in pc_table_get_or_insert_in either way. */
|
|
#define DIRTY_RAM_PC_TABLE_SIZE 262144
|
|
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. */
|
|
/* Enrichment (2026-09-05, BoF3): make a bare interpreted PC explainable
|
|
* from a session-long per_pc snapshot alone, with no offline join and no
|
|
* live-ring window. Stamped only on EXTERNAL entries (arrived from native
|
|
* dispatch, addr != chain target), which is where the value is. */
|
|
uint32_t occ_crc; /* tier 1: psx_overlay_resident_crc_at(pc) at the last
|
|
* external entry -- the manifest CRC of the static
|
|
* variant whose code ranges span this PC (DLL path:
|
|
* last hash taken). Disambiguates a mixed band
|
|
* (0x801D0C00 carries BATTLE/ETC/SCENARIO/WORLD
|
|
* occupants) to the one actually loaded, which the
|
|
* offline enrich_pcs join cannot. 0 = nothing compiled
|
|
* spans this PC (BIOS / kernel / boot EXE). */
|
|
uint8_t occ_ok; /* 1 = that variant validated at the time (interior
|
|
* gap inside live native code: an Axis B seed);
|
|
* 0 = it is resident but CRC-missing (data inside the
|
|
* code range rewritten, or a different section than
|
|
* the one compiled) -- the whole band runs
|
|
* interpreted and no seed will fix it. */
|
|
uint32_t last_ext_ra;/* tier 2: gpr[31] (caller RA) at the most recent
|
|
* external entry — names the call site that reaches a
|
|
* function-pointer-table interior. This is the §9 (LOGO
|
|
* effect-handler tables) diagnosis made durable and
|
|
* session-long instead of ring-bounded; the full
|
|
* call/jalr/jr transfer split stays in the fp-log ring. */
|
|
} DirtyRamPcEntry;
|
|
extern DirtyRamPcEntry g_dirty_ram_pc_table[DIRTY_RAM_PC_TABLE_SIZE];
|
|
/* Every aligned main-RAM word is a possible instruction PC. Execution
|
|
* coverage only needs presence, not a hit count, so record it in a direct
|
|
* bitmap instead of probing a large hash table for every retired instruction.
|
|
* This covers every word of the host RAM backing (the old 262K-entry hash
|
|
* could saturate) -- retail 2 MiB and the opt-in 8 MiB map alike -- and is
|
|
* also the execution-verified seed source used by overlay_capture. */
|
|
#define DIRTY_RAM_EXEC_WORD_COUNT (PSX_MAIN_RAM_BACKING_BYTES / 4u)
|
|
#define DIRTY_RAM_EXEC_BITMAP_WORDS ((DIRTY_RAM_EXEC_WORD_COUNT + 31u) / 32u)
|
|
extern uint32_t g_dirty_ram_exec_pc_bitmap[DIRTY_RAM_EXEC_BITMAP_WORDS];
|
|
/* One bit per 4 KiB page. RAM writes use this as a one-test stale-evidence
|
|
* guard; they clear that page's capture bits rather than serializing from the
|
|
* universal store hot path. */
|
|
#define DIRTY_RAM_EXEC_PAGE_BITMAP_WORDS \
|
|
(((PSX_MAIN_RAM_BACKING_BYTES / 4096u) + 31u) / 32u)
|
|
extern uint32_t g_dirty_ram_exec_page_bitmap[DIRTY_RAM_EXEC_PAGE_BITMAP_WORDS];
|
|
/* Presence-only companion for interpreted block/dispatch entries. The richer
|
|
* counter table remains for telemetry, while capture can snapshot/reset this
|
|
* compact evidence independently at overlay-generation boundaries. */
|
|
extern uint32_t g_dirty_ram_dispatch_pc_bitmap[DIRTY_RAM_EXEC_BITMAP_WORDS];
|
|
|
|
/* The segment each interpreted dispatch entered through
|
|
* (docs/SEGMENT_AWARE_CODE.md §5.7). The bytes are segment-free, but a PC is
|
|
* not: a KUSEG, KSEG0 or KSEG1 entry of the same word links, traps and fetches
|
|
* in its own segment, so each needs its own compiled shard. The bitmap above
|
|
* keeps its meaning (dispatched in any segment); these siblings record which
|
|
* segments, one bit per word per segment, set at the same interpreted
|
|
* dispatch. The execution bitmap needs no segment: direct edges keep the
|
|
* entry's segment. Only the three segments that map physical memory have a
|
|
* sibling; a PC elsewhere (0x20000000-0x7FFFFFFF, KSEG2) addresses no RAM in
|
|
* Beetle and is never recorded. */
|
|
#define PSX_CODE_SEGMENT_COUNT 3
|
|
static inline int psx_code_segment_index(uint32_t pc) {
|
|
switch (pc & 0xE0000000u) {
|
|
case 0x00000000u: return 0; /* KUSEG */
|
|
case 0x80000000u: return 1; /* KSEG0 */
|
|
case 0xA0000000u: return 2; /* KSEG1 */
|
|
default: return -1;
|
|
}
|
|
}
|
|
static inline uint32_t psx_code_segment_base(int index) {
|
|
return index == 0 ? 0x00000000u : index == 1 ? 0x80000000u : 0xA0000000u;
|
|
}
|
|
/* The names the capture JSON and the per-segment cache directories use. */
|
|
static inline const char *psx_code_segment_name(int index) {
|
|
return index == 0 ? "kuseg" : index == 1 ? "kseg0" : "kseg1";
|
|
}
|
|
extern uint32_t g_dirty_ram_dispatch_seg_bitmap[PSX_CODE_SEGMENT_COUNT]
|
|
[DIRTY_RAM_EXEC_BITMAP_WORDS];
|
|
|
|
/* Clear dispatch evidence (the any-segment bitmap and its segment siblings)
|
|
* for `nwords` bitmap words from `first_word`. Every site that drops dispatch
|
|
* evidence goes through this, so a stale segment bit can never outlive its
|
|
* any-segment bit. */
|
|
static inline void dirty_ram_dispatch_evidence_clear(uint32_t first_word,
|
|
uint32_t nwords) {
|
|
if (first_word >= DIRTY_RAM_EXEC_BITMAP_WORDS) return;
|
|
if (nwords > DIRTY_RAM_EXEC_BITMAP_WORDS - first_word)
|
|
nwords = DIRTY_RAM_EXEC_BITMAP_WORDS - first_word;
|
|
memset(&g_dirty_ram_dispatch_pc_bitmap[first_word], 0,
|
|
(size_t)nwords * sizeof(uint32_t));
|
|
for (int s = 0; s < PSX_CODE_SEGMENT_COUNT; s++)
|
|
memset(&g_dirty_ram_dispatch_seg_bitmap[s][first_word], 0,
|
|
(size_t)nwords * sizeof(uint32_t));
|
|
}
|
|
|
|
/* 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. */
|
|
#ifdef PSX_NO_DEBUG_TOOLS
|
|
/* Keep ABI-visible one-element sentinels so crash/debug stubs still link, while
|
|
* Release does not reserve the diagnostic rings' ~227 MiB of address space. */
|
|
#define DIRTY_RAM_BLOCK_LOG_CAP 1u
|
|
#else
|
|
#define DIRTY_RAM_BLOCK_LOG_CAP (1u << 22) /* 4M entries (~224 MiB at 56 B each).
|
|
* 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. */
|
|
#endif
|
|
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;
|
|
|
|
#ifdef PSX_NO_DEBUG_TOOLS
|
|
#define DIRTY_RAM_FLOW_LOG_CAP 1u
|
|
#else
|
|
#define DIRTY_RAM_FLOW_LOG_CAP (1u << 16)
|
|
#endif
|
|
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;
|
|
|
|
#ifdef PSX_NO_DEBUG_TOOLS
|
|
#define DIRTY_RAM_INSN_LOG_CAP 1u
|
|
#else
|
|
#define DIRTY_RAM_INSN_LOG_CAP (1u << 16)
|
|
#endif
|
|
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;
|
|
/* Kernel scratch registers. $at is the assembler-temp that hand-written
|
|
* kernel asm uses to stash a base across a load-delay slot, and $k0/$k1
|
|
* are the exception handlers' only free registers — i.e. exactly the state
|
|
* that matters when debugging BIOS exception/handler code, which is what
|
|
* this interpreter mostly runs. */
|
|
uint32_t at;
|
|
uint32_t k0;
|
|
uint32_t k1;
|
|
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 */
|