Files
psxrecomp/runtime/src/overlay_loader.c
T
Matthew Stanley fcb7cc9022 sljit: enforce gcc>sljit load priority — obsolete superseded shards
Root-cause class: load priority is static > gcc shard > sljit shard > interp,
but it was only emergent from chain registration order (LIFO prepend), not
enforced, and a superseded sljit shard was never retired. A shipped/optimized
gcc DLL must outrank — and obsolete — a user machine's locally-JIT'd sljit shard
for the same content.

Fix (overlay_loader.c): when a gcc/DLL candidate registers (cand_register),
retire any same-PC sljit shard JIT'd from IDENTICAL bytes (same crc_code) —
blacklist it so it can never run even if the gcc candidate is later self-mod
blacklisted. Precise by content: a sljit shard for a DIFFERENT overlay variant
at the same address is distinct coverage and is kept. New counter
s_sljit_obsoleted, surfaced via overlay_loader_sljit_obsoleted().

debug_server.c: sljit_status now reports live + obsoleted (observability for
the validation playtest — measure, don't eyeball).

Runtime-only (no regen). Refs SLJIT.md §11 Phase B.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:12:16 -07:00

1519 lines
68 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "overlay_loader.h"
#include "overlay_api.h"
#include "code_provider.h"
#include "overlay_sljit.h"
#include "crc32.h"
#include "dirty_ram_interp.h"
#include "interrupts.h"
#include "debug_server.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
#else
# include <dlfcn.h>
#endif
/* ============================================================================
* Inc3 — Per-entry validity + multi-candidate dispatch (design doc §8)
*
* Validity is tracked per *compiled entry*, not per dirty region. Each entry
* carries its tight code byte-ranges (from the recompiler's per-function walk,
* emitted as a {base}_{crc}.ranges manifest beside the DLL), a content hash of
* those ranges, and a page-generation snapshot. A write to a watched page bumps
* that page's generation (memory.c); dispatch lazily re-hashes an entry only
* when the generation over its ranges changed. This:
* - kills the false-invalidation churn of the old [fn_lo,fn_hi) region flag
* (data interleaved between functions no longer nukes the whole region), and
* - makes reload-on-return gradual and automatic — each entry flips back to
* native the moment its own code bytes reappear (hash matches), with no
* region-wide atomic threshold.
*
* PC -> candidate list: different overlays can reuse the same RAM address
* (Tomba loads village and overworld both at 0x800E7xxx). A single-entry table
* would let the later DLL clobber the earlier candidate, making reload-on-return
* impossible. So each PC maps to a chain of candidates; dispatch picks the one
* whose code hash matches live RAM.
*
* §8.3 RESOLVED: jump tables compile to `switch (live register)` with a
* call_by_address default, so table contents are an optimization, not a
* correctness dependency. The dependency set is the entry's code bytes only.
* ==========================================================================*/
typedef void (*OverlayFn)(CPUState *);
#define MAX_CODE_RANGES 16 /* code ranges per function (coalesced; usually 1) */
enum { ENTRY_VALID = 0, ENTRY_INVALID = 1, ENTRY_BLACKLIST = 2 };
typedef struct {
uint32_t addr; /* phys entry address */
OverlayFn fn;
uint32_t range_lo[MAX_CODE_RANGES]; /* phys code-range starts */
uint32_t range_len[MAX_CODE_RANGES];
int nranges;
uint32_t crc_code; /* hash of code ranges at registration*/
uint32_t val_gen; /* pagegen sum when last (in)validated*/
int state; /* ENTRY_VALID/INVALID/BLACKLIST */
int dll; /* source DLL index */
int next; /* next candidate at same addr, -1 end*/
uint32_t diff_passes; /* clean same-state diffs (verify budget)*/
int device_touch; /* 1 = touches MMIO; never run its shard,
* always interp (shadow diff can't safely
* double-execute device I/O). */
} Candidate;
/* Same-state differential verify budget: diff a candidate this many times with
* 0 divergence, then trust it (stop diffing — run it normally). Bounds the
* differential's cost to ~(distinct functions × budget) instead of every
* dispatch forever, making a validation playtest playable; a DIVERGING
* candidate never reaches the budget, so it stays diff-gated (interp result
* kept) and never executes native live. */
#define OVERLAY_DIFF_BUDGET 32u
#define CAND_CAP 16384
static Candidate s_cand[CAND_CAP];
static int s_cand_n = 0;
/* Open-addressed index: phys entry addr -> head candidate index (-1 sentinel
* stored as chain terminator on each Candidate). addr 0 = empty slot. */
#define IDX_CAP 32768u
#define IDX_MASK (IDX_CAP - 1u)
typedef struct { uint32_t addr; int head; } IdxSlot;
static IdxSlot s_idx[IDX_CAP];
static int idx_head(uint32_t phys) {
uint32_t h = (phys * 2654435761u) & IDX_MASK;
for (uint32_t i = 0; i < IDX_CAP; i++) {
uint32_t k = (h + i) & IDX_MASK;
if (s_idx[k].addr == 0) return -1;
if (s_idx[k].addr == phys) return s_idx[k].head;
}
return -1;
}
static void idx_set_head(uint32_t phys, int head) {
uint32_t h = (phys * 2654435761u) & IDX_MASK;
for (uint32_t i = 0; i < IDX_CAP; i++) {
uint32_t k = (h + i) & IDX_MASK;
if (s_idx[k].addr == 0 || s_idx[k].addr == phys) {
s_idx[k].addr = phys;
s_idx[k].head = head;
return;
}
}
}
/* Active native-entry stack (self-modification detection, §8.5). */
static int s_active_stack[64];
static int s_active_depth = 0;
/* ---- Observability (recomp-debug: measure, don't eyeball) -------------- */
/* Always-on ring of native overlay calls, so the FIRST divergence / last
* function executed before a corruption can be read back from the window of
* interest — never "arm a trace then hope". s_native_inprogress holds the entry
* currently executing (nonzero at dump => a native fn was entered and never
* returned: a freeze INSIDE native code, the strongest single suspect). */
#define NRING_CAP 1024
typedef struct { uint32_t addr; uint32_t crc; uint64_t seq; int returned; } NRingEnt;
static NRingEnt s_nring[NRING_CAP];
static uint32_t s_nring_pos = 0;
static uint64_t s_nring_seq = 0;
static uint32_t s_native_inprogress = 0; /* addr of fn currently in native, 0 = none */
static uint64_t s_native_calls_total = 0;
/* Runtime A/B: when 0, candidates are still hashed/validated and recorded, but
* NEVER executed native — execution falls to the interpreter. Lets us prove
* whether native EXECUTION is the cause without a rebuild or losing candidate
* visibility. Default on. */
static int s_native_exec = 1;
static uint64_t s_would_run_native = 0; /* matched but skipped (exec off) */
void overlay_loader_set_native_exec(int on) { s_native_exec = on ? 1 : 0; }
int overlay_loader_get_native_exec(void) { return s_native_exec; }
/* Address of the native overlay function currently executing (0 if none).
* Used by the event-timeline ring to tag an event's execution mode. */
uint32_t overlay_loader_get_inprogress(void) { return s_native_inprogress; }
/* Same-state differential — defined fully below; declared here for dispatch. */
static int s_diff_mode = 0;
static int s_in_shadow = 0;
/* Live sljit mode (PSX_OVERLAY_SLJIT_LIVE / sljit_live debug cmd): JIT overlay
* functions on-miss and run them LIVE without the per-shard differential — the
* production model (sljit is the sync on-miss producer; safety is the emitter's
* decline-on-unsupported contract, validated broadly). Off by default; the diff
* gate above stays the dev path. Lets a developer feel the real toolchain-less
* player experience (pure sljit + interp floor, no gcc, no diff overhead). */
static int s_sljit_live = 0;
void overlay_loader_set_sljit_live(int on) { s_sljit_live = on ? 1 : 0; }
int overlay_loader_get_sljit_live(void) { return s_sljit_live; }
static void run_shadow_diff(CPUState *cpu, Candidate *c, uint32_t addr);
/* ---- Counters (surfaced via overlay_loader_status) --------------------- */
static int s_ndlls = 0; /* DLLs LoadLibrary'd */
static int s_valid_count = 0; /* candidates currently VALID */
static uint64_t s_disp_native = 0;
static uint64_t s_disp_interp = 0;
static uint64_t s_stale_blocked = 0; /* dispatches skipped (candidate !valid)*/
static uint32_t s_invalidations = 0; /* VALID -> INVALID transitions */
static uint32_t s_revalidations = 0; /* INVALID -> VALID (reload-on-return) */
static uint32_t s_rehashes = 0; /* code-range hashes computed */
static uint32_t s_rehash_miss = 0; /* hashes that didn't match crc_code */
static uint32_t s_last_crc = 0;
static uint32_t s_no_manifest = 0; /* exports skipped (no manifest range)*/
static uint32_t s_selfmod = 0; /* entries blacklisted (self-mod) */
static uint32_t s_sljit_registered = 0; /* sljit Tier-2 shards registered */
static uint32_t s_sljit_obsoleted = 0; /* sljit shards superseded by a gcc DLL*/
static uint32_t s_last_write_pc = 0;
static uint32_t s_last_write_addr = 0;
static uint32_t s_last_write_size = 0;
extern uint8_t *memory_get_ram_ptr(void);
extern uint32_t overlay_watch_pagegen_sum(uint32_t phys, uint32_t len);
/* ---- Per-candidate hash / generation over its code ranges -------------- */
static uint32_t cand_crc(const Candidate *c) {
const uint8_t *ram = memory_get_ram_ptr();
uint32_t crc = 0xFFFFFFFFu;
for (int i = 0; i < c->nranges; i++)
crc = crc32_update(crc, ram + c->range_lo[i], c->range_len[i]);
return crc ^ 0xFFFFFFFFu;
}
static uint32_t cand_gensum(const Candidate *c) {
uint32_t s = 0;
for (int i = 0; i < c->nranges; i++)
s += overlay_watch_pagegen_sum(c->range_lo[i], c->range_len[i]);
return s;
}
/* ---- Per-DLL code-range manifest --------------------------------------- */
/* Minimal line format emitted by tools/compile_overlays.py beside each DLL:
* F <entry_hex> start a function (entry = virtual export addr)
* R <lo_hex> <len_hex> a code byte-range (virtual addr, byte length)
* The recompiler's per-function instruction walk yields exactly the executed
* code bytes — interleaved jump tables / constant pools are excluded, which is
* what makes the hash stable across reloads. */
typedef struct {
uint32_t entry;
uint32_t crc; /* expected hash of the compiled-from code bytes */
int has_crc; /* 1 if the manifest supplied the authoritative hash */
uint32_t lo[MAX_CODE_RANGES];
uint32_t len[MAX_CODE_RANGES];
int n;
} ManFn;
static ManFn *parse_manifest(const char *path, int *out_n) {
*out_n = 0;
FILE *f = fopen(path, "r");
if (!f) return NULL;
int cap = 1024, n = 0;
ManFn *arr = (ManFn *)malloc(sizeof(ManFn) * cap);
if (!arr) { fclose(f); return NULL; }
char line[160];
ManFn *cur = NULL;
while (fgets(line, sizeof(line), f)) {
if (line[0] == 'F') {
uint32_t e = 0, crc = 0;
int got = sscanf(line + 1, "%x %x", &e, &crc);
if (got >= 1) {
if (n >= cap) {
cap *= 2;
ManFn *na = (ManFn *)realloc(arr, sizeof(ManFn) * cap);
if (!na) break;
arr = na;
}
cur = &arr[n++];
cur->entry = e;
cur->crc = crc;
cur->has_crc = (got >= 2) ? 1 : 0;
cur->n = 0;
}
} else if (line[0] == 'R' && cur) {
uint32_t lo, len;
if (sscanf(line + 1, "%x %x", &lo, &len) == 2 &&
cur->n < MAX_CODE_RANGES && len > 0) {
cur->lo[cur->n] = lo;
cur->len[cur->n] = len;
cur->n++;
}
}
}
fclose(f);
*out_n = n;
return arr;
}
static ManFn *man_find(ManFn *arr, int n, uint32_t entry) {
for (int i = 0; i < n; i++)
if (arr[i].entry == entry) return &arr[i];
return NULL;
}
/* ---- Candidate registration -------------------------------------------- */
static void loader_log(const char *fmt, ...); /* defined below */
static void cand_register(uint32_t phys, OverlayFn fn, const ManFn *m, int dll) {
if (s_cand_n >= CAND_CAP) return;
int idx = s_cand_n++;
Candidate *c = &s_cand[idx];
c->addr = phys;
c->fn = fn;
c->dll = dll;
c->state = ENTRY_VALID;
c->nranges = 0;
for (int i = 0; i < m->n && c->nranges < MAX_CODE_RANGES; i++) {
c->range_lo[c->nranges] = m->lo[i] & 0x1FFFFFFFu;
c->range_len[c->nranges] = m->len[i];
c->nranges++;
}
/* Watch the code pages so future writes bump their generation. */
extern void overlay_watch_set_range(uint32_t phys, uint32_t len);
for (int i = 0; i < c->nranges; i++)
overlay_watch_set_range(c->range_lo[i], c->range_len[i]);
/* crc_code is the AUTHORITATIVE hash of the bytes the recompiler compiled
* from (supplied by the manifest) — NOT a sample of live RAM at this instant
* (registration is a single transient moment; the overlay may not be fully
* present yet, and other overlays sharing the merged DLL are not present at
* all). A candidate is callable iff live RAM matches this compiled-from hash,
* which makes validity timing-independent and reload-on-return work. */
if (m->has_crc) {
c->crc_code = m->crc;
} else {
c->crc_code = cand_crc(c); /* legacy manifest without hashes */
}
c->val_gen = cand_gensum(c);
c->state = (cand_crc(c) == c->crc_code) ? ENTRY_VALID : ENTRY_INVALID;
c->next = idx_head(phys);
idx_set_head(phys, idx);
if (c->state == ENTRY_VALID) s_valid_count++;
/* Obsolescence (load priority: static > gcc shard > sljit shard > interp). A
* gcc DLL is the optimized, dev-validated artifact and OUTRANKS an sljit shard
* a user machine JIT'd for the SAME content. Once this gcc candidate is in the
* chain it already wins dispatch (it's at the head, ahead of the older sljit
* shard, and content-keyed dispatch picks the first match), but we also
* explicitly retire the superseded sljit shard so it can never run — even if
* this gcc candidate is later self-mod-blacklisted. Precise by content: only a
* shard JIT'd from the IDENTICAL bytes (same crc_code) is superseded; an sljit
* shard for a DIFFERENT overlay variant at this address is distinct coverage
* and is kept. */
for (int j = c->next; j >= 0; j = s_cand[j].next) {
Candidate *o = &s_cand[j];
if (o->dll == -1 && o->state != ENTRY_BLACKLIST &&
o->crc_code == c->crc_code) {
if (o->state == ENTRY_VALID && s_valid_count > 0) s_valid_count--;
o->state = ENTRY_BLACKLIST;
s_sljit_obsoleted++;
loader_log("sljit shard 0x%08X obsoleted by gcc DLL (crc 0x%08X)",
o->addr, o->crc_code);
}
}
}
/* ---- sljit Tier-2 shard registration (SLJIT.md §7 step 5) -------------- */
/* Register an in-process JIT shard as a native candidate, parallel to
* cand_register but without a .ranges manifest: the shard was JIT'd from the
* live RAM bytes over [lo, lo+len), so crc_code is hashed from those same bytes
* — a later dispatch runs the shard iff the code is still byte-identical
* (reload-on-return / self-mod safety, the same live-byte contract gcc
* candidates obey). */
static void register_sljit_candidate(uint32_t phys, OverlayFn fn,
uint32_t lo, uint32_t len) {
if (s_cand_n >= CAND_CAP || len == 0) return;
int idx = s_cand_n++;
Candidate *c = &s_cand[idx];
c->addr = phys & 0x1FFFFFFFu;
c->fn = fn;
c->dll = -1; /* sentinel: sljit shard, not a DLL */
c->nranges = 1;
c->range_lo[0] = lo & 0x1FFFFFFFu;
c->range_len[0] = len;
extern void overlay_watch_set_range(uint32_t phys, uint32_t len);
overlay_watch_set_range(c->range_lo[0], c->range_len[0]);
c->crc_code = cand_crc(c); /* live bytes == the bytes we JIT'd from */
c->val_gen = cand_gensum(c);
c->state = ENTRY_VALID;
c->diff_passes = 0;
c->device_touch = 0;
c->next = idx_head(c->addr);
idx_set_head(c->addr, idx);
s_valid_count++;
s_sljit_registered++;
loader_log("sljit shard registered 0x%08X [%u bytes]", c->addr, len);
}
/* JIT-on-miss memo: phys entries already attempted (compiled or declined), so a
* declined fragment isn't re-attempted every dispatch. */
#define MAX_SLJIT_TRIED 512
static uint32_t s_sljit_tried[MAX_SLJIT_TRIED];
static int s_sljit_tried_n = 0;
static int sljit_already_tried(uint32_t phys) {
for (int i = 0; i < s_sljit_tried_n; i++)
if (s_sljit_tried[i] == phys) return 1;
return 0;
}
static void sljit_mark_tried(uint32_t phys) {
if (s_sljit_tried_n < MAX_SLJIT_TRIED) s_sljit_tried[s_sljit_tried_n++] = phys;
}
/* Attempt an in-process JIT of the leaf function at `phys` from live RAM, and
* register the shard as a candidate on success. Gated by the caller to the
* validation configuration only (backend==sljit + diff_mode); see the dispatch
* hook. Decodes from live RAM exactly as the interpreter does. */
static void try_sljit_region(uint32_t addr) {
uint32_t phys = addr & 0x1FFFFFFFu;
const CodeProvider *cp = code_provider_active();
if (!cp->compile_fragment) return;
if (sljit_already_tried(phys)) return;
extern int dirty_ram_is_dirty(uint32_t phys);
if (!dirty_ram_is_dirty(phys)) return; /* only JIT real runtime code */
sljit_mark_tried(phys);
uint8_t *ram = memory_get_ram_ptr();
if (!ram) return;
CompiledFragment frag = {0};
/* Decode from live RAM: bytes = RAM base, image_base_vram = 0 ⇒ byte offset =
* (entry & 0x1FFFFFFF) = phys. Pass the VIRTUAL entry so return_pc and
* jal/J targets carry the KSEG bits the guest uses — the interpreter computes
* pc from the virtual address, and saved $ra values must match byte-exact. */
cp->compile_fragment(addr, ram, 2u * 1024u * 1024u, 0u, &frag);
if (frag.fn)
register_sljit_candidate(phys, (OverlayFn)frag.fn, frag.code_lo, frag.code_len);
}
/* ---- Global state ------------------------------------------------------ */
static char s_cache_dir[512];
static char s_game_id[64];
static int s_active = 0;
/* Rule 3: no stderr logging. Most recent loader event recorded here and
* surfaced through overlay_loader_status. */
static char s_last_msg[256] = {0};
static void loader_log(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vsnprintf(s_last_msg, sizeof(s_last_msg), fmt, ap);
va_end(ap);
}
const char *overlay_loader_last_msg(void) { return s_last_msg; }
/* ---- Cache index: region_start -> dll path ----------------------------- */
#define CACHE_IDX_CAP 256
typedef struct { uint32_t region_start; char path[768]; } CacheEntry;
static CacheEntry s_cache_idx[CACHE_IDX_CAP];
static int s_cache_idx_count = 0;
static int cache_idx_has_path(const char *path) {
for (int i = 0; i < s_cache_idx_count; i++)
if (strcmp(s_cache_idx[i].path, path) == 0) return 1;
return 0;
}
/* Canonical cache arch-abi tag (see SLJIT.md §4 — caches are namespaced per
* backend AND per target so a Windows-x64 gcc DLL and, later, a same-OS arm64
* or an sljit blob for the same fragment never comingle). compile_overlays.py
* computes the IDENTICAL string from platform.system()/machine(); keep the two
* mappings in lockstep ("<os>-<arch>": win|linux|macos + x64|arm64|x86). */
#if defined(_WIN32)
# define PSX_OL_OS "win"
#elif defined(__APPLE__)
# define PSX_OL_OS "macos"
#else
# define PSX_OL_OS "linux"
#endif
#if defined(__aarch64__) || defined(_M_ARM64)
# define PSX_OL_ARCH "arm64"
#elif defined(__x86_64__) || defined(_M_X64)
# define PSX_OL_ARCH "x64"
#elif defined(__i386__) || defined(_M_IX86)
# define PSX_OL_ARCH "x86"
#else
# define PSX_OL_ARCH "unknown"
#endif
#define PSX_OVERLAY_ARCH_ABI PSX_OL_OS "-" PSX_OL_ARCH
const char *overlay_loader_arch_abi(void) { return PSX_OVERLAY_ARCH_ABI; }
/* Scan one directory for <addr8>_<crc8>.dll cache entries into the index.
* `dir` is a full directory path. Idempotent (skips already-indexed paths). */
static void scan_one_cache_dir(const char *dir) {
#ifdef _WIN32
char pattern[768];
snprintf(pattern, sizeof(pattern), "%s/*_*.dll", dir);
WIN32_FIND_DATAA fd;
HANDLE h = FindFirstFileA(pattern, &fd);
if (h == INVALID_HANDLE_VALUE) return;
do {
if (strlen(fd.cFileName) != 21) continue; /* 8+1+8+4 = 21 */
/* Validate the <addr8>_<crc8>.dll shape explicitly: region_start 0 is
* LEGAL (the kernel-RAM window starts at phys 0), so a zero parse
* result can't be used as the invalid sentinel. */
int valid = (fd.cFileName[8] == '_');
for (int ci = 0; valid && ci < 8; ci++) {
char c = fd.cFileName[ci];
valid = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') ||
(c >= 'a' && c <= 'f');
}
if (!valid) continue;
uint32_t addr = (uint32_t)strtoul(fd.cFileName, NULL, 16);
if (s_cache_idx_count >= CACHE_IDX_CAP) break;
char full[768];
snprintf(full, sizeof(full), "%s/%s", dir, fd.cFileName);
if (cache_idx_has_path(full)) continue; /* rescan idempotence */
CacheEntry *e = &s_cache_idx[s_cache_idx_count++];
e->region_start = addr;
snprintf(e->path, sizeof(e->path), "%s", full);
} while (FindNextFileA(h, &fd));
FindClose(h);
#else
(void)dir;
#endif
}
/* Scan the namespaced gcc cache (gcc/<arch-abi>/) AND the legacy flat layout
* (<game_id>/*.dll) for back-compat — existing caches keep loading with no
* migration. The sljit/<arch-abi>/ namespace is reserved (no on-disk blobs in
* v1: sljit re-JITs from the coverage manifest; see SLJIT.md §5.3). */
static void scan_cache_dir(void) {
char dir[768];
snprintf(dir, sizeof(dir), "%s/%s/gcc/%s",
s_cache_dir, s_game_id, PSX_OVERLAY_ARCH_ABI);
scan_one_cache_dir(dir);
/* Legacy flat layout (pre-namespacing). */
snprintf(dir, sizeof(dir), "%s/%s", s_cache_dir, s_game_id);
scan_one_cache_dir(dir);
}
/* True if the cache holds a DLL for this region compiled from an image with
* this CRC (filename <addr8>_<crc8>.dll). Autocapture's "unseen" test. */
int overlay_loader_has_cached_crc(uint32_t region_start, uint32_t crc) {
for (int i = 0; i < s_cache_idx_count; i++) {
if (s_cache_idx[i].region_start != region_start) continue;
const char *fn = strrchr(s_cache_idx[i].path, '/');
fn = fn ? fn + 1 : s_cache_idx[i].path;
if (strlen(fn) == 21 && (uint32_t)strtoul(fn + 9, NULL, 16) == crc)
return 1;
}
return 0;
}
/* ---- Runtime callbacks wired into overlay DLLs via overlay_init() ------ */
extern void psx_dispatch_call(CPUState *cpu, uint32_t addr, uint32_t ra);
extern void psx_check_interrupts(CPUState *cpu);
extern void gte_execute(CPUState *cpu, uint32_t cmd);
extern void psx_syscall(CPUState *cpu, uint32_t code);
extern void psx_unknown_dispatch(CPUState *cpu, uint32_t addr, uint32_t phys);
extern void debug_server_log_call_entry(uint32_t func_addr);
static OverlayCallbacks s_callbacks;
/* Timing-hypothesis probe: native overlay code calls psx_check_interrupts at
* EVERY block (up to ~100x/function), whereas the dirty-RAM interpreter checks
* only ~every 4096 instructions + at function exit. That cadence gap can deliver
* an interrupt at a different point in native vs interp -> divergence with no
* mistranslation. When s_suppress_irq is set we drop native's per-block checks
* (cadence ~ interp). If the blue screen vanishes, the cause is interrupt timing.
* Two modes: full suppress, or rate-limit (call the real check every Nth time).
*
* NOTE: this probe rests on the cross-game finding that overlay code never
* installs its own IRQ/DMA/callback handlers (PsyQ convention — all timing-
* critical handlers live in resident static code; holds for every sampled
* title). That makes interrupt-check *cadence* the only native-vs-interp
* difference. If a future title violates the convention (an overlay installs a
* handler), this probe is no longer sufficient and a discriminator is needed. */
static int s_suppress_irq = 0;
static uint32_t s_irq_ratelimit = 0; /* 0 = full suppress; N = every Nth call */
static uint32_t s_irq_callcount = 0;
static uint64_t s_irq_suppressed = 0;
void overlay_loader_set_irq_suppress(int mode, uint32_t ratelimit) {
s_suppress_irq = mode ? 1 : 0;
s_irq_ratelimit = ratelimit;
/* Reset counters on every (re)arm so an A/B run reports a clean, isolated
* suppressed-count for that arming rather than a cumulative total. */
s_irq_callcount = 0;
s_irq_suppressed = 0;
}
void overlay_loader_get_irq_suppress(int *mode, uint32_t *rl, uint64_t *supp) {
if (mode) *mode = s_suppress_irq;
if (rl) *rl = s_irq_ratelimit;
if (supp) *supp = s_irq_suppressed;
}
static void overlay_ci_wrapper(CPUState *cpu) {
if (s_suppress_irq) {
if (s_irq_ratelimit == 0) { s_irq_suppressed++; return; }
if ((++s_irq_callcount % s_irq_ratelimit) != 0) { s_irq_suppressed++; return; }
}
psx_check_interrupts(cpu);
}
static void init_callbacks(void) {
extern void psx_restore_state_escape(void);
s_callbacks.dispatch_call = psx_dispatch_call;
s_callbacks.check_interrupts = overlay_ci_wrapper;
s_callbacks.gte_execute = gte_execute;
s_callbacks.psx_syscall = psx_syscall;
s_callbacks.psx_unknown_dispatch = psx_unknown_dispatch;
s_callbacks.log_call_entry = debug_server_log_call_entry;
s_callbacks.psx_restore_state_escape = psx_restore_state_escape;
/* Call-contract state (ABI v2): DLL code shares the runtime's bail
* flag and counters through these pointers. */
s_callbacks.call_bail_flag = &g_psx_call_bail;
s_callbacks.bail_first = &g_psx_bail_first;
s_callbacks.bail_resolved = &g_psx_bail_resolved;
}
/* ---- DLL loading and export enumeration -------------------------------- */
#ifdef _WIN32
static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll) {
HMODULE h = LoadLibraryA(dll_path);
if (!h) {
loader_log("LoadLibrary(%s) failed: %lu", dll_path, GetLastError());
return 0;
}
/* ABI gate: reject any DLL whose contract ABI doesn't match this
* runtime (pre-versioning DLLs lack the export entirely). Delete the
* stale file so the autocompile path regenerates it with the current
* emitter. */
typedef int (*AbiFn)(void);
AbiFn abi_fn = (AbiFn)GetProcAddress(h, "overlay_abi");
int abi = abi_fn ? abi_fn() : 0;
/* Tag = ABI version (low 16) | codegen flavor (high 16). Mismatch on either
* (wrong ABI, or a different-flavor cache e.g. widescreen vs base) is
* rejected + deleted so autocompile regenerates it for THIS build. */
if (abi != PSX_OVERLAY_ABI_TAG) {
loader_log("ABI/flavor mismatch in %s: dll=0x%X runtime=0x%X — rejecting "
"and deleting stale cache entry", dll_path, abi,
PSX_OVERLAY_ABI_TAG);
FreeLibrary(h);
DeleteFileA(dll_path);
return 0;
}
typedef void (*InitFn)(const OverlayCallbacks *);
InitFn init_fn = (InitFn)GetProcAddress(h, "overlay_init");
if (!init_fn) {
loader_log("no overlay_init in %s", dll_path);
FreeLibrary(h);
return 0;
}
init_fn(&s_callbacks);
BYTE *base = (BYTE *)h;
IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)base;
IMAGE_NT_HEADERS *nt = (IMAGE_NT_HEADERS *)(base + dos->e_lfanew);
IMAGE_DATA_DIRECTORY *exp_dd =
&nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (!exp_dd->VirtualAddress) {
loader_log("no export dir in %s", dll_path);
FreeLibrary(h);
return 0;
}
IMAGE_EXPORT_DIRECTORY *exp =
(IMAGE_EXPORT_DIRECTORY *)(base + exp_dd->VirtualAddress);
DWORD *names = (DWORD *)(base + exp->AddressOfNames);
WORD *ordinals = (WORD *)(base + exp->AddressOfNameOrdinals);
DWORD *funcs = (DWORD *)(base + exp->AddressOfFunctions);
int registered = 0;
for (DWORD i = 0; i < exp->NumberOfNames; i++) {
const char *name = (const char *)(base + names[i]);
if (strncmp(name, "func_", 5) != 0) continue;
if (strlen(name) != 13) continue;
uint32_t addr = (uint32_t)strtoul(name + 5, NULL, 16);
if (addr == 0) continue;
WORD ord = ordinals[i];
OverlayFn fn = (OverlayFn)(base + funcs[ord]);
/* Precision-first (§1): only register a candidate when the recompiler
* gave us its code ranges. A function with no manifest entry is left to
* the interpreter rather than registered with a guessed extent. */
ManFn *m = man_find(man, man_n, addr);
if (!m || m->n == 0) { s_no_manifest++; continue; }
cand_register(addr & 0x1FFFFFFFu, fn, m, dll);
registered++;
}
loader_log("loaded %s -> %d candidates (%u no-manifest)",
dll_path, registered, s_no_manifest);
return registered;
}
#else
static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll) {
(void)man; (void)man_n; (void)dll;
void *h = dlopen(dll_path, RTLD_NOW | RTLD_LOCAL);
if (!h) { loader_log("dlopen(%s) failed: %s", dll_path, dlerror()); return 0; }
/* ABI gate (see the _WIN32 branch). */
typedef int (*AbiFn)(void);
AbiFn abi_fn = (AbiFn)dlsym(h, "overlay_abi");
int abi = abi_fn ? abi_fn() : 0;
if (abi != PSX_OVERLAY_ABI_TAG) {
loader_log("ABI/flavor mismatch in %s: dll=0x%X runtime=0x%X — rejecting "
"and deleting stale cache entry", dll_path, abi,
PSX_OVERLAY_ABI_TAG);
dlclose(h);
remove(dll_path);
return 0;
}
typedef void (*InitFn)(const OverlayCallbacks *);
InitFn init_fn = (InitFn)dlsym(h, "overlay_init");
if (!init_fn) { loader_log("no overlay_init in %s", dll_path); dlclose(h); return 0; }
init_fn(&s_callbacks);
loader_log("%s loaded (posix export scan TODO)", dll_path);
return 0;
}
#endif
/* ---- Public API -------------------------------------------------------- */
void overlay_loader_init(const char *cache_dir, const char *game_id) {
strncpy(s_cache_dir, cache_dir, sizeof(s_cache_dir) - 1);
strncpy(s_game_id, game_id, sizeof(s_game_id) - 1);
init_callbacks();
scan_cache_dir();
/* PSX_OVERLAY_SLJIT_LIVE=1 -> run sljit shards live on-miss (no diff gate):
* the toolchain-less production model. Only meaningful with backend=sljit. */
{
const char *e = getenv("PSX_OVERLAY_SLJIT_LIVE");
if (e && *e && *e != '0') {
s_sljit_live = 1;
loader_log("sljit LIVE mode on (JIT-on-miss, no diff gate)");
}
}
s_active = 1;
}
void overlay_loader_check_cache(uint32_t load_addr, uint32_t size,
const uint8_t *bytes) {
/* DLL loading is deferred to the first dispatch miss (try_load_region). */
(void)load_addr; (void)size; (void)bytes;
}
/* ---- Lazy region cache check (first dispatch miss for a region) -------- */
#define MAX_CHECKED 64
static uint32_t s_checked[MAX_CHECKED];
static int s_nchecked = 0;
static int s_last_file_found = 0;
static int already_checked(uint32_t region_start) {
for (int i = 0; i < s_nchecked; i++)
if (s_checked[i] == region_start) return 1;
return 0;
}
static void mark_checked(uint32_t region_start) {
if (s_nchecked < MAX_CHECKED) s_checked[s_nchecked++] = region_start;
}
/* Re-scan the cache dir for DLLs compiled after init (step 2.8 autocompile)
* and clear the checked-regions memo so the next dispatch into a window
* region reconsiders the cache. Already-loaded DLLs stay loaded;
* dll_already_loaded() makes the re-walk idempotent. */
void overlay_loader_rescan(void) {
if (!s_active) return;
scan_cache_dir();
s_nchecked = 0;
}
/* Loaded-DLL set — the cache is ADDITIVE: a memory slot reused by several
* overlays (Tomba's village and overworld both at 0x800E7xxx) has one cached
* DLL per distinct overlay (keyed by content crc in the filename). We load
* ALL of them; each contributes its functions as separate candidates, and
* per-entry validity (the live-RAM hash) decides which candidate is callable
* at any moment. Nothing is ever clobbered — discoveries accumulate. */
#define MAX_LOADED_DLLS 128
static char s_loaded_paths[MAX_LOADED_DLLS][768];
static int s_nloaded_paths = 0;
static int dll_already_loaded(const char *path) {
for (int i = 0; i < s_nloaded_paths; i++)
if (strcmp(s_loaded_paths[i], path) == 0) return 1;
return 0;
}
static int load_one_dll(const char *dll_path) {
/* Sibling code-range manifest: {base}_{crc}.ranges next to the DLL. */
char ranges_path[800];
snprintf(ranges_path, sizeof(ranges_path), "%s", dll_path);
size_t plen = strlen(ranges_path);
if (plen >= 4 && strcmp(ranges_path + plen - 4, ".dll") == 0)
snprintf(ranges_path + plen - 4, sizeof(ranges_path) - (plen - 4), ".ranges");
int man_n = 0;
ManFn *man = parse_manifest(ranges_path, &man_n);
if (!man || man_n == 0) {
loader_log("no/empty manifest %s — DLL left to interpreter", ranges_path);
free(man);
return 0;
}
int registered = load_overlay_dll(dll_path, man, man_n, s_ndlls);
free(man);
if (registered <= 0) return 0;
if (s_nloaded_paths < MAX_LOADED_DLLS) {
strncpy(s_loaded_paths[s_nloaded_paths], dll_path, 767);
s_loaded_paths[s_nloaded_paths][767] = '\0';
s_nloaded_paths++;
}
s_ndlls++;
return registered;
}
static void try_load_region(uint32_t phys) {
extern uint32_t dirty_ram_get_bitmap_word(uint32_t word_index);
uint32_t page_sz = 4096u;
/* Walk back over the contiguous dirty run to recover region_start — cache
* DLLs are keyed by this start address in their filename. */
uint32_t pg = phys / page_sz;
while (pg > 0) {
uint32_t pp = pg - 1;
if (!((dirty_ram_get_bitmap_word(pp >> 5) >> (pp & 31u)) & 1u)) break;
pg = pp;
}
uint32_t region_start = pg * page_sz;
if (already_checked(region_start)) return;
mark_checked(region_start);
/* Load EVERY cached DLL for this region_start (additive / multi-candidate),
* not just the first. */
for (int ci = 0; ci < s_cache_idx_count; ci++) {
if (s_cache_idx[ci].region_start != region_start) continue;
if (dll_already_loaded(s_cache_idx[ci].path)) continue;
s_last_file_found = 1;
load_one_dll(s_cache_idx[ci].path);
}
}
/* ---- Dispatch ---------------------------------------------------------- */
int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) {
uint32_t phys = addr & 0x1FFFFFFFu;
int head = idx_head(phys);
if (head < 0 && s_active && overlay_cache_window_contains(phys)) {
try_load_region(phys);
head = idx_head(phys);
/* sljit JIT-on-miss (SLJIT.md §7 step 5). Fires when the active backend is
* sljit and EITHER the same-state differential is armed (dev validation
* path — shards created then diffed vs interp) OR live mode is on (the
* production model — shards created and run live, no per-shard diff).
* Never inside a shadow run. */
if (head < 0 && (s_diff_mode || s_sljit_live) && !s_in_shadow &&
overlay_backend_active() == OVERLAY_BACKEND_SLJIT) {
try_sljit_region(addr); /* virtual entry (carries KSEG bits) */
head = idx_head(phys);
}
}
for (int i = head; i >= 0; i = s_cand[i].next) {
Candidate *c = &s_cand[i];
if (c->state == ENTRY_BLACKLIST) continue;
/* CORRECTNESS-FIRST — parity (native == interpreter) is not yet proven,
* so we verify the live code bytes match what we compiled on EVERY
* dispatch, immediately before running native. This is the safe stance
* AND the decisive test: if a stale-native blue screen still occurs when
* we PROVABLY only run native on byte-exact matches, the fault is the
* native translation itself (parity), not the validity tracking. The
* generation counter is retained only as a diagnostic (cand_gensum). */
uint32_t live = cand_crc(c);
s_rehashes++;
s_last_crc = live;
c->val_gen = cand_gensum(c);
if (live == c->crc_code) {
if (c->state != ENTRY_VALID) {
c->state = ENTRY_VALID;
s_revalidations++; /* reload-on-return */
s_valid_count++;
}
/* Device-touching functions never run their shard: the shadow diff
* can't safely double-execute MMIO/SIO/DMA to validate them, so they
* always fall to the interpreter (the authoritative single path). */
if (c->device_touch) { s_disp_interp++; return 0; }
/* Same-state differential: run native+interp from identical state,
* compare, keep the interp result. Takes precedence over the A/B
* toggle. Verify-budget: once a candidate has passed cleanly enough
* times it's trusted and falls through to normal execution, so the
* diff cost stays bounded (a diverging candidate never reaches the
* budget — it keeps being diff-gated and never runs native live).
*
* VALIDATED-LIVE: live sljit mode (s_sljit_live) is NOT a separate
* "JIT and run blind" path — it routes its sljit SHARDS (dll < 0)
* through the SAME diff gate, so a shard runs native live only after a
* clean verify budget, and run_shadow_diff's interp-first pass computes
* device_touch (device functions get pinned to interp, never double-
* executing I/O — the save-load wedge). gcc candidates (dll >= 0) are
* the trusted tier (validated at dev time) and run native directly;
* they are diffed only in explicit dev diff mode. */
int want_diff = s_diff_mode || (s_sljit_live && c->dll < 0);
if (want_diff && !s_in_shadow && c->diff_passes < OVERLAY_DIFF_BUDGET) {
run_shadow_diff(cpu, c, addr);
return 1;
}
/* A/B: prove whether native EXECUTION is the cause. When off, the
* candidate matched (byte-exact) but we DON'T run native — interp
* handles it. */
if (!s_native_exec) { s_would_run_native++; s_disp_interp++; return 0; }
/* Record into the always-on ring BEFORE the call; mark in-progress
* so a freeze inside this fn is visible at dump time. */
uint32_t slot = s_nring_pos++ & (NRING_CAP - 1u);
s_nring[slot].addr = c->addr;
s_nring[slot].crc = c->crc_code;
s_nring[slot].seq = ++s_nring_seq;
s_nring[slot].returned = 0;
uint32_t prev_inprogress = s_native_inprogress;
s_native_inprogress = c->addr;
s_native_calls_total++;
if (s_active_depth < (int)(sizeof(s_active_stack) / sizeof(s_active_stack[0])))
s_active_stack[s_active_depth++] = i;
s_disp_native++;
/* Delimit this native execution in the interp insn ring (native code
* emits no per-insn entries; markers keep the timeline alignable). */
extern void dirty_ram_log_marker(uint32_t addr, uint32_t tag, int kind);
uint32_t mtag = (uint32_t)s_nring[slot].seq; /* stable across nesting */
dirty_ram_log_marker(c->addr, mtag, 0);
c->fn(cpu);
dirty_ram_log_marker(c->addr, mtag, 1);
if (s_active_depth > 0) s_active_depth--;
s_nring[slot].returned = 1;
s_native_inprogress = prev_inprogress; /* restore (nested calls) */
return 1;
} else {
s_rehash_miss++;
if (c->state == ENTRY_VALID) {
c->state = ENTRY_INVALID;
s_invalidations++;
if (s_valid_count > 0) s_valid_count--;
} else {
c->state = ENTRY_INVALID;
}
s_stale_blocked++;
}
}
s_disp_interp++;
return 0;
}
/* ---- Self-modification of an actively-executing entry (§8.5) ------------ */
/* Lazy re-hash on the NEXT dispatch is too late if a native function modifies
* its own code and continues executing the modified bytes within the same
* activation (native runs the originally-compiled semantics). We can't recover
* the current activation, so we permanently demote that entry to interp. Called
* from memory.c only when the written page is watched. */
void overlay_loader_active_write_check(uint32_t phys, uint32_t size) {
extern uint32_t g_debug_last_store_pc;
uint32_t p = phys & 0x1FFFFFFFu;
for (int d = 0; d < s_active_depth; d++) {
Candidate *c = &s_cand[s_active_stack[d]];
for (int i = 0; i < c->nranges; i++) {
uint32_t lo = c->range_lo[i];
uint32_t hi = lo + c->range_len[i];
if (p < hi && p + size > lo) {
if (c->state != ENTRY_BLACKLIST) {
c->state = ENTRY_BLACKLIST;
s_selfmod++;
if (s_valid_count > 0) s_valid_count--;
s_last_write_pc = g_debug_last_store_pc;
s_last_write_addr = phys;
s_last_write_size = size;
loader_log("blacklist self-mod entry 0x%08X (write 0x%08X)",
c->addr, phys);
}
break;
}
}
}
}
/* ---- Status getters (signatures preserved for debug_server.c) ---------- */
void overlay_loader_get_counters(uint32_t *loads, uint32_t *invalidations,
uint32_t *unregistered,
uint64_t *disp_native, uint64_t *disp_interp,
uint64_t *stale_blocked,
uint32_t *last_write_pc,
uint32_t *last_write_addr,
uint32_t *last_write_size,
int *regions, uint32_t *revalidations) {
if (loads) *loads = (uint32_t)s_ndlls;
if (invalidations) *invalidations = s_invalidations;
if (unregistered) *unregistered = s_no_manifest;
if (disp_native) *disp_native = s_disp_native;
if (disp_interp) *disp_interp = s_disp_interp;
if (stale_blocked) *stale_blocked = s_stale_blocked;
if (last_write_pc) *last_write_pc = s_last_write_pc;
if (last_write_addr) *last_write_addr = s_last_write_addr;
if (last_write_size) *last_write_size = s_last_write_size;
if (regions) *regions = s_ndlls;
if (revalidations) *revalidations = s_revalidations;
}
/* Reload diagnostics. Repurposed for the per-entry model:
* r0_valid -> candidates currently VALID
* r0_writes... -> entries blacklisted (self-mod)
* r0_fn_lo -> total candidates registered
* r0_fn_hi -> DLLs loaded
* r0_crc_live -> last computed code-range crc
* reval_attempts -> code-range hashes computed
* reval_crc_miss -> hashes that did not match
* last_reval_crc -> last computed crc */
void overlay_loader_get_reload_debug(int *r0_valid, uint32_t *r0_writes,
uint32_t *r0_fn_lo, uint32_t *r0_fn_hi,
uint32_t *r0_crc_live,
uint32_t *reval_attempts,
uint32_t *reval_crc_miss,
uint32_t *last_reval_crc) {
if (r0_valid) *r0_valid = s_valid_count;
if (r0_writes) *r0_writes = s_selfmod;
if (r0_fn_lo) *r0_fn_lo = (uint32_t)s_cand_n;
if (r0_fn_hi) *r0_fn_hi = (uint32_t)s_ndlls;
if (r0_crc_live) *r0_crc_live = s_last_crc;
if (reval_attempts) *reval_attempts = s_rehashes;
if (reval_crc_miss) *reval_crc_miss = s_rehash_miss;
if (last_reval_crc) *last_reval_crc = s_last_crc;
}
int overlay_loader_registered_count(void) { return s_valid_count; }
/* sljit Tier-2 shards registered as candidates this session (diagnostics). */
uint32_t overlay_loader_sljit_registered(void) { return s_sljit_registered; }
/* sljit shards superseded by a higher-priority gcc DLL (same content). */
uint32_t overlay_loader_sljit_obsoleted(void) { return s_sljit_obsoleted; }
/* Force a one-shot sljit JIT attempt of the leaf function at `addr` from live
* RAM and register it on success (bypasses the diff-mode gate — a probe). For
* the sljit_try debug command. Returns via the result struct. */
void overlay_loader_sljit_probe(uint32_t addr, OverlaySljitResult *out) {
out->fn = NULL; out->code_lo = 0; out->code_len = 0; out->insns = 0;
uint8_t *ram = memory_get_ram_ptr();
if (!ram) return;
/* Virtual entry so return_pc / jal targets carry the KSEG bits (see
* try_sljit_region); byte offset is still (entry & 0x1FFFFFFF) = phys. */
overlay_sljit_try_compile(addr, ram, 2u * 1024u * 1024u, 0u, out);
if (out->fn)
register_sljit_candidate(addr & 0x1FFFFFFFu, (OverlayFn)out->fn,
out->code_lo, out->code_len);
}
/* ---- Native↔interp execution fingerprint differential (§5-E) ----------- */
/* For each CANDIDATE function execution we record the FULL register file at
* entry and exit (plus the guest cycle), tagged native vs interp. Run once
* native-OFF (all candidates interpreted = oracle) and once native-ON; diff by
* sequence — the first entry whose in-state differs names the exact register
* AND value where the two trajectories part ways, and the cycle field
* quantifies native↔interp cycle-accounting skew over the aligned prefix.
* Logging is purely additive (no control-flow change), driven from the single
* dirty_ram_dispatch chokepoint. */
typedef struct {
uint64_t seq;
uint64_t cycle; /* guest cycle at exit (log time) */
uint32_t addr;
uint32_t in_crc, out_crc;
int native;
uint32_t in_regs[34]; /* r0..r31, hi, lo at entry */
uint32_t out_regs[34]; /* r0..r31, hi, lo at exit */
} FpEnt;
#define FP_CAP (1u << 16) /* ~19 MB with full reg files; ~65K executions */
static FpEnt s_fp[FP_CAP];
static uint64_t s_fp_seq = 0;
int overlay_loader_is_candidate(uint32_t phys) {
return idx_head(phys & 0x1FFFFFFFu) >= 0;
}
/* ---- Same-state native↔interp differential (confident measurement) ------ */
/* At a matched dispatch: snapshot CPU+RAM, run native (discard), restore, run
* interpreter (KEEP — game stays correct), compare under IDENTICAL input state.
* Eliminates manual-nav desync. Interrupts suppressed during both shadow runs
* so the comparison isolates COMPUTATION (and is longjmp-safe). A divergence
* here = a real codegen bug (function + exact register/RAM). Zero divergence =
* computation is correct and the fault is timing/interrupt-ordering. */
#define SHADOW_RAM_SIZE (2u * 1024u * 1024u)
#define SHADOW_SPAD_SIZE 1024u
static uint8_t s_ram0[SHADOW_RAM_SIZE]; /* pre-call main-RAM snapshot */
static uint8_t s_ramN[SHADOW_RAM_SIZE]; /* post-native main-RAM */
static uint8_t s_ramI[SHADOW_RAM_SIZE]; /* post-interp main-RAM (kept) */
static uint8_t s_spad0[SHADOW_SPAD_SIZE]; /* pre-call scratchpad snapshot*/
static uint8_t s_spadI[SHADOW_SPAD_SIZE]; /* post-interp scratchpad (kept)*/
static uint64_t s_shadow_skipped_dev = 0; /* candidates skipped: touch MMIO */
/* s_diff_mode / s_in_shadow declared above (before dispatch). */
typedef struct {
uint64_t seq; uint32_t addr;
int reg; /* first differing gpr index, -1 none */
uint32_t reg_native, reg_interp;
int hi_diff, lo_diff;
int64_t ram_off; /* first differing RAM byte, -1 none */
uint32_t ram_native, ram_interp; /* the differing word */
} ShadowDiv;
#define SDIV_CAP 512
static ShadowDiv s_sdiv[SDIV_CAP];
static int s_sdiv_n = 0;
static uint64_t s_shadow_calls = 0;
static uint64_t s_shadow_divs = 0;
/* One-shot full-state capture of the FIRST divergence: complete native and
* interp register files so the diverging path can be localized. */
static int s_detail_captured = 0;
static uint32_t s_detail_addr = 0;
static uint32_t s_detail_nat_gpr[32], s_detail_int_gpr[32];
static uint32_t s_detail_nat_hi, s_detail_nat_lo, s_detail_int_hi, s_detail_int_lo;
void overlay_loader_set_diff_mode(int on) { s_diff_mode = on ? 1 : 0; }
static void run_shadow_diff(CPUState *cpu, Candidate *c, uint32_t addr) {
extern uint8_t *memory_get_ram_ptr(void);
extern uint8_t *memory_get_scratchpad_ptr(void);
extern int dirty_ram_dispatch(CPUState *cpu, uint32_t addr, uint32_t stop_addr);
extern int g_shadow_mmio_watch; /* memory.c — device-access detector */
extern uint64_t g_shadow_mmio_hits;
uint8_t *ram = memory_get_ram_ptr();
uint8_t *spad = memory_get_scratchpad_ptr();
s_in_shadow = 1;
int saved_supp = s_suppress_irq;
s_suppress_irq = 1; /* isolate computation; longjmp-safe */
/* Validate ONE function at a time: nested OVERLAY calls run via the
* INTERPRETER on BOTH passes (s_native_exec=0). Otherwise the native pass
* dispatches each callee as its own native shard while the interp pass runs
* it interp, so the diff compares the whole call TREE and a callee that bails
* or diverges is misattributed to this candidate (e.g. a contract bail that
* skips THIS function's epilogue -> $sp off by the frame size). Per-function
* isolation proves exactly this shard's codegen vs the interp oracle; whole-
* tree soundness then follows by induction. */
int sv = s_native_exec;
s_native_exec = 0;
CPUState cpu0 = *cpu;
memcpy(s_ram0, ram, SHADOW_RAM_SIZE);
memcpy(s_spad0, spad, SHADOW_SPAD_SIZE);
/* PASS 1 — INTERPRETER, the authoritative single execution, with the device
* detector armed. Running interp FIRST (not native) guarantees device I/O
* happens AT MOST ONCE and only via the trusted path: if this function (or a
* callee) touches ANY MMIO we abandon the native pass entirely — device I/O
* must never be double-executed (one spurious card/SIO/DMA write corrupts
* hardware state and wedges the guest, e.g. the save-load crash). */
uint64_t mmio0 = g_shadow_mmio_hits;
g_shadow_mmio_watch++;
dirty_ram_dispatch(cpu, addr, cpu->gpr[31]);
g_shadow_mmio_watch--;
s_shadow_calls++;
if (g_shadow_mmio_hits != mmio0) {
/* Device-touching: keep the interp result live (already in *cpu/ram/spad),
* mark the candidate so it ALWAYS runs via the interpreter (never its
* shard, never re-diffed). sljit covers the pure-compute majority; device
* functions stay on the interpreter — safe by construction, no double I/O. */
c->device_touch = 1;
s_shadow_skipped_dev++;
g_psx_call_bail = 0;
s_native_exec = sv;
s_suppress_irq = saved_supp;
s_in_shadow = 0;
return;
}
/* Device-free: preserve the interp result, then run the NATIVE shard from the
* same input and compare. No device I/O on either pass (proven clean above). */
CPUState cpuI = *cpu;
memcpy(s_ramI, ram, SHADOW_RAM_SIZE);
memcpy(s_spadI, spad, SHADOW_SPAD_SIZE);
*cpu = cpu0;
memcpy(ram, s_ram0, SHADOW_RAM_SIZE);
memcpy(spad, s_spad0, SHADOW_SPAD_SIZE);
uint32_t stop_ra = cpu->gpr[31]; /* entry $ra = the function's return point */
c->fn(cpu);
/* A shard that ends in a computed `jr rX` (jump table) exits with cpu->pc =
* target rather than running to the return; chain through tail-transfers to
* the same stop the interp used, so we compare FULL native vs FULL interp. */
{
int guard = 0;
while (cpu->pc != 0 && !g_psx_call_bail && guard++ < 8192) {
uint32_t tv = cpu->pc;
if ((tv & 0x1FFFFFFFu) == (stop_ra & 0x1FFFFFFFu)) break; /* returned */
cpu->pc = 0;
if (!dirty_ram_dispatch(cpu, tv, stop_ra)) { cpu->pc = tv; break; }
}
}
CPUState cpuN = *cpu;
memcpy(s_ramN, ram, SHADOW_RAM_SIZE);
/* Compare native (cpuN/s_ramN) vs interp (cpuI/s_ramI) under identical input. */
int reg = -1;
for (int r = 1; r < 32; r++) if (cpuN.gpr[r] != cpuI.gpr[r]) { reg = r; break; }
int hidiff = (cpuN.hi != cpuI.hi), lodiff = (cpuN.lo != cpuI.lo);
int64_t ramoff = -1;
if (memcmp(s_ramN, s_ramI, SHADOW_RAM_SIZE) != 0) {
for (uint32_t a = 0; a < SHADOW_RAM_SIZE; a++)
if (s_ramN[a] != s_ramI[a]) { ramoff = (int64_t)a; break; }
}
if (reg < 0 && !hidiff && !lodiff && ramoff < 0) {
/* Clean pass: credit the verify budget. */
if (c->diff_passes < OVERLAY_DIFF_BUDGET) c->diff_passes++;
} else {
/* Divergence: reset the budget. Promotion to live requires N CONSECUTIVE
* clean passes (the spec's "0 divergences over the budget"), so an
* intermittently-wrong shard can never accumulate enough lucky passes to be
* trusted — it stays diff-gated (interp result kept) and never runs live. */
c->diff_passes = 0;
s_shadow_divs++;
if (!s_detail_captured) {
s_detail_captured = 1;
s_detail_addr = c->addr;
for (int r = 0; r < 32; r++) {
s_detail_nat_gpr[r] = cpuN.gpr[r];
s_detail_int_gpr[r] = cpuI.gpr[r];
}
s_detail_nat_hi = cpuN.hi; s_detail_nat_lo = cpuN.lo;
s_detail_int_hi = cpuI.hi; s_detail_int_lo = cpuI.lo;
}
if (s_sdiv_n < SDIV_CAP) {
ShadowDiv *d = &s_sdiv[s_sdiv_n++];
d->seq = s_shadow_calls; d->addr = c->addr;
d->reg = reg;
d->reg_native = (reg >= 0) ? cpuN.gpr[reg] : 0;
d->reg_interp = (reg >= 0) ? cpuI.gpr[reg] : 0;
d->hi_diff = hidiff; d->lo_diff = lodiff;
d->ram_off = ramoff;
if (ramoff >= 0) {
uint32_t a = (uint32_t)ramoff & ~3u;
d->ram_native = *(uint32_t *)&s_ramN[a];
d->ram_interp = *(uint32_t *)&s_ramI[a];
}
}
}
/* Restore the interp result as the authoritative live state (native discarded).
* A bail raised by the shadow run must never leak into live execution (a
* spurious in-progress unwind wedges the guest). */
*cpu = cpuI;
memcpy(ram, s_ramI, SHADOW_RAM_SIZE);
memcpy(spad, s_spadI, SHADOW_SPAD_SIZE);
g_psx_call_bail = 0;
s_native_exec = sv;
s_suppress_irq = saved_supp;
s_in_shadow = 0;
}
int overlay_loader_dump_shadow_detail(char *out, int cap) {
int n = 0;
n += snprintf(out + n, cap - n,
"{\"captured\":%d,\"addr\":\"0x%08X\",\"regs\":[", s_detail_captured, s_detail_addr);
static const char *rn[32] = {"zero","at","v0","v1","a0","a1","a2","a3",
"t0","t1","t2","t3","t4","t5","t6","t7","s0","s1","s2","s3","s4","s5",
"s6","s7","t8","t9","k0","k1","gp","sp","fp","ra"};
int first = 1;
for (int r = 0; r < 32; r++) {
if (s_detail_nat_gpr[r] == s_detail_int_gpr[r]) continue;
n += snprintf(out + n, cap - n,
"%s{\"r\":%d,\"name\":\"%s\",\"native\":\"0x%08X\",\"interp\":\"0x%08X\"}",
first ? "" : ",", r, rn[r], s_detail_nat_gpr[r], s_detail_int_gpr[r]);
first = 0;
}
n += snprintf(out + n, cap - n, "],\"hi\":{\"native\":\"0x%08X\",\"interp\":\"0x%08X\"},"
"\"lo\":{\"native\":\"0x%08X\",\"interp\":\"0x%08X\"}}",
s_detail_nat_hi, s_detail_int_hi, s_detail_nat_lo, s_detail_int_lo);
return n;
}
int overlay_loader_dump_shadow(char *out, int cap) {
int n = 0;
n += snprintf(out + n, cap - n,
"{\"diff_mode\":%d,\"shadow_calls\":%llu,\"divergences\":%llu,"
"\"skipped_device\":%llu,\"records\":[",
s_diff_mode, (unsigned long long)s_shadow_calls,
(unsigned long long)s_shadow_divs,
(unsigned long long)s_shadow_skipped_dev);
for (int i = 0; i < s_sdiv_n && n < cap - 200; i++) {
ShadowDiv *d = &s_sdiv[i];
n += snprintf(out + n, cap - n,
"%s{\"seq\":%llu,\"addr\":\"0x%08X\",\"reg\":%d,"
"\"reg_native\":\"0x%08X\",\"reg_interp\":\"0x%08X\","
"\"hi\":%d,\"lo\":%d,\"ram_off\":%lld,"
"\"ram_native\":\"0x%08X\",\"ram_interp\":\"0x%08X\"}",
i ? "," : "", (unsigned long long)d->seq, d->addr, d->reg,
d->reg_native, d->reg_interp, d->hi_diff, d->lo_diff,
(long long)d->ram_off, d->ram_native, d->ram_interp);
}
n += snprintf(out + n, cap - n, "]}");
return n;
}
/* Fingerprint over the general registers (r1..r31) + hi/lo. r0 excluded
* (always 0). pc excluded (the return target is trivially equal at exit). */
uint32_t overlay_regs_crc(const CPUState *cpu) {
uint32_t crc = 0xFFFFFFFFu;
crc = crc32_update(crc, (const uint8_t *)&cpu->gpr[1], sizeof(uint32_t) * 31);
crc = crc32_update(crc, (const uint8_t *)&cpu->hi, sizeof(uint32_t));
crc = crc32_update(crc, (const uint8_t *)&cpu->lo, sizeof(uint32_t));
return crc ^ 0xFFFFFFFFu;
}
/* Snapshot r0..r31 + hi/lo into a 34-word buffer (entry-state capture). */
void overlay_regs_snap(uint32_t out[34], const CPUState *cpu) {
memcpy(out, cpu->gpr, sizeof(uint32_t) * 32);
out[32] = cpu->hi;
out[33] = cpu->lo;
}
/* CRC over words 1..33 (r0 excluded — always 0), matching overlay_regs_crc. */
static uint32_t regs34_crc(const uint32_t *r) {
uint32_t crc = 0xFFFFFFFFu;
crc = crc32_update(crc, (const uint8_t *)&r[1], sizeof(uint32_t) * 33);
return crc ^ 0xFFFFFFFFu;
}
void overlay_fp_log(uint32_t addr, const uint32_t *in_regs,
const CPUState *cpu, int native) {
extern uint64_t psx_get_cycle_count(void);
uint64_t s = s_fp_seq++;
FpEnt *e = &s_fp[s & (FP_CAP - 1u)];
e->seq = s; e->cycle = psx_get_cycle_count();
e->addr = addr & 0x1FFFFFFFu; e->native = native;
memcpy(e->in_regs, in_regs, sizeof(e->in_regs));
overlay_regs_snap(e->out_regs, cpu);
e->in_crc = regs34_crc(e->in_regs);
e->out_crc = regs34_crc(e->out_regs);
}
/* Execute `addr` natively if a validated overlay candidate exists, keeping the
* §5-E fingerprint record (same as the dirty_ram_dispatch chokepoint). Called
* from the dirty-RAM interpreter's jal/jalr handlers so native overlay callees
* get the SAME call contract as statically-compiled callees: execute as a
* unit, then the interpreter resumes at the call's return address. Without
* this, the call surfaces to the dispatch loop as a bare pc value; the native
* callee's C-style return (pc==0) then unwinds the loop past the suspended
* caller continuation — the caller's epilogue never runs and its stack frame
* leaks (root cause of the dwarf->overworld native blue screen).
* Returns 1 iff a native candidate ran. */
int overlay_loader_call_native(CPUState *cpu, uint32_t addr) {
if (!s_native_exec) return 0; /* interp mode: keep the legacy inline path */
uint32_t phys = addr & 0x1FFFFFFFu;
if (idx_head(phys) < 0) return 0; /* fast reject: not a candidate */
uint32_t in_regs[34];
overlay_regs_snap(in_regs, cpu);
if (!overlay_loader_dispatch(cpu, addr)) return 0;
overlay_fp_log(addr, in_regs, cpu, 1);
return 1;
}
/* ---- jal/jalr call helper for sljit shards (SLJIT.md §7) ---------------- */
/* A JIT'd shard calls this at every jal/jalr site instead of open-coding the
* call contract. It reproduces the dirty-RAM interpreter's call path EXACTLY
* (see exec_one cases 0x03 / SPECIAL 0x09 and dispatch_nonlocal_call), so a
* shard-issued call behaves identically to an interpreted one — including the
* wild-return / bail unwind that the contract guards (Bug A/C/D family). The
* one principled difference from the interpreter's local-dirty fast path: a
* native shard cannot resume itself block-by-block, so a not-yet-native callee
* is run as a UNIT via psx_dispatch_call (which handles interp/dirty callees to
* the return contract) rather than the interpreter's pc-chain. See overlay_sljit.h. */
int psx_sljit_call(CPUState *cpu, uint32_t target, uint32_t return_pc,
int check_contract) {
uint32_t site_sp = cpu->gpr[29]; /* sp at the call (after the delay slot) */
#ifdef PSX_HAS_GAME_DISPATCH
{
extern int psx_dispatch_game_compiled(CPUState *cpu, uint32_t addr);
cpu->pc = 0;
if (psx_dispatch_game_compiled(cpu, target)) {
if (g_psx_call_bail) return 1;
if (cpu->pc != 0) return 1;
if (check_contract && psx_call_contract(cpu, return_pc, site_sp)) return 1;
return 0;
}
}
#endif
cpu->pc = 0;
if (overlay_loader_call_native(cpu, target)) {
if (g_psx_call_bail) return 1;
if (cpu->pc != 0) return 1;
if (check_contract && psx_call_contract(cpu, return_pc, site_sp)) return 1;
return 0;
}
/* Not a resolved compiled/native unit: dispatch the callee as a unit (the
* interpreter's nonlocal path — handles interp/dirty callees + the return
* contract internally). */
cpu->pc = 0;
psx_dispatch_call(cpu, target, return_pc);
if (g_psx_call_bail) return 1;
if (cpu->pc != 0) return 1;
return 0;
}
/* COP2/GTE helper — mirrors dirty_ram_interp.c case 0x12 + LWC2/SWC2. */
void psx_sljit_cop2(CPUState *cpu, uint32_t insn) {
uint32_t op = (insn >> 26) & 0x3Fu, rs = (insn >> 21) & 0x1Fu;
uint32_t rt = (insn >> 16) & 0x1Fu, rd = (insn >> 11) & 0x1Fu;
if (op == 0x12) { /* COP2 */
uint32_t cop_op = rs;
if (cop_op == 0x00) { cpu->gpr[rt] = gte_read_data(cpu, (uint8_t)rd); cpu->gpr[0] = 0; } /* MFC2 */
else if (cop_op == 0x02) { cpu->gpr[rt] = gte_read_ctrl(cpu, (uint8_t)rd); cpu->gpr[0] = 0; } /* CFC2 */
else if (cop_op == 0x04) { gte_write_data(cpu, (uint8_t)rd, cpu->gpr[rt]); } /* MTC2 */
else if (cop_op == 0x06) { gte_write_ctrl(cpu, (uint8_t)rd, cpu->gpr[rt]); } /* CTC2 */
else if (cop_op & 0x10) { gte_execute(cpu, insn & 0x1FFFFFFu); } /* GTE cmd */
return;
}
int32_t simm = (int32_t)(int16_t)(insn & 0xFFFFu);
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
if (op == 0x32) { gte_write_data(cpu, (uint8_t)rt, cpu->read_word(addr)); } /* LWC2 */
else if (op == 0x3A) { cpu->write_word(addr, gte_read_data(cpu, (uint8_t)rt)); } /* SWC2 */
}
/* Unaligned load/store helper — mirrors dirty_ram_interp.c interp_lwl/lwr/swl/swr
* + cases 0x22/0x26/0x2A/0x2E. */
void psx_sljit_memx(CPUState *cpu, uint32_t insn) {
uint32_t op = (insn >> 26) & 0x3Fu, rs = (insn >> 21) & 0x1Fu, rt = (insn >> 16) & 0x1Fu;
int32_t simm = (int32_t)(int16_t)(insn & 0xFFFFu);
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
uint32_t aligned = addr & ~3u;
uint32_t word = cpu->read_word(aligned);
uint32_t sh = addr & 3u, rtv = cpu->gpr[rt], v;
switch (op) {
case 0x22: /* LWL */
switch (sh) { case 0: v = (rtv & 0x00FFFFFFu) | (word << 24); break;
case 1: v = (rtv & 0x0000FFFFu) | (word << 16); break;
case 2: v = (rtv & 0x000000FFu) | (word << 8); break;
default: v = word; }
cpu->gpr[rt] = v; cpu->gpr[0] = 0; break;
case 0x26: /* LWR */
switch (sh) { case 0: v = word; break;
case 1: v = (rtv & 0xFF000000u) | (word >> 8); break;
case 2: v = (rtv & 0xFFFF0000u) | (word >> 16); break;
default: v = (rtv & 0xFFFFFF00u) | (word >> 24); }
cpu->gpr[rt] = v; cpu->gpr[0] = 0; break;
case 0x2A: /* SWL */
switch (sh) { case 0: word = (word & 0xFFFFFF00u) | (rtv >> 24); break;
case 1: word = (word & 0xFFFF0000u) | (rtv >> 16); break;
case 2: word = (word & 0xFF000000u) | (rtv >> 8); break;
default: word = rtv; }
cpu->write_word(aligned, word); break;
case 0x2E: /* SWR */
switch (sh) { case 0: word = rtv; break;
case 1: word = (word & 0x000000FFu) | (rtv << 8); break;
case 2: word = (word & 0x0000FFFFu) | (rtv << 16); break;
default: word = (word & 0x00FFFFFFu) | (rtv << 24); }
cpu->write_word(aligned, word); break;
}
}
/* Write the whole fingerprint log to a file (no TCP size limit). Returns the
* number of entries written, or -1 on open failure. */
int overlay_loader_write_fp_file(const char *path) {
FILE *f = fopen(path, "w");
if (!f) return -1;
uint64_t total = s_fp_seq;
uint64_t start = (total > FP_CAP) ? (total - FP_CAP) : 0;
fputc('[', f);
int first = 1, count = 0;
for (uint64_t s = start; s < total; s++) {
FpEnt *e = &s_fp[s & (FP_CAP - 1u)];
fprintf(f,
"%s{\"seq\":%llu,\"cycle\":%llu,\"addr\":\"0x%08X\",\"in\":\"0x%08X\","
"\"out\":\"0x%08X\",\"native\":%d,\"in_regs\":[",
first ? "" : ",\n", (unsigned long long)e->seq,
(unsigned long long)e->cycle, e->addr,
e->in_crc, e->out_crc, e->native);
for (int r = 0; r < 34; r++)
fprintf(f, "%s\"0x%08X\"", r ? "," : "", e->in_regs[r]);
fputs("],\"out_regs\":[", f);
for (int r = 0; r < 34; r++)
fprintf(f, "%s\"0x%08X\"", r ? "," : "", e->out_regs[r]);
fputs("]}", f);
first = 0; count++;
}
fputs("]\n", f);
fclose(f);
return count;
}
/* Dump the native-call ring (most-recent first) + the in-progress entry. The
* in_progress field being nonzero means a native function was entered and never
* returned — a freeze INSIDE native code, pointing straight at the suspect. */
int overlay_loader_dump_native_ring(char *out, int cap) {
int n = 0;
n += snprintf(out + n, cap - n,
"{\"native_exec\":%d,\"calls_total\":%llu,\"would_run\":%llu,"
"\"in_progress\":\"0x%08X\",\"recent\":[",
s_native_exec, (unsigned long long)s_native_calls_total,
(unsigned long long)s_would_run_native, s_native_inprogress);
/* Walk backward from the most recent up to 64 entries. */
int shown = 0;
for (uint32_t k = 0; k < NRING_CAP && shown < 64 && n < cap - 120; k++) {
uint32_t idx = (s_nring_pos - 1u - k) & (NRING_CAP - 1u);
if (s_nring[idx].seq == 0) break;
n += snprintf(out + n, cap - n,
"%s{\"addr\":\"0x%08X\",\"crc\":\"0x%08X\",\"seq\":%llu,\"returned\":%d}",
shown ? "," : "", s_nring[idx].addr, s_nring[idx].crc,
(unsigned long long)s_nring[idx].seq, s_nring[idx].returned);
shown++;
}
n += snprintf(out + n, cap - n, "]}");
return n;
}
/* Diagnostic: dump every candidate with its stored vs live hash and generation
* state, so reload behaviour can be inspected directly (Rule 3 — visibility via
* the debug server, not logs). Writes a JSON array into `out`; returns bytes
* written. */
int overlay_loader_dump_candidates(char *out, int cap) {
int n = 0;
n += snprintf(out + n, cap - n, "[");
for (int i = 0; i < s_cand_n && n < cap - 160; i++) {
Candidate *c = &s_cand[i];
uint32_t live = cand_crc(c);
uint32_t sum = cand_gensum(c);
n += snprintf(out + n, cap - n,
"%s{\"addr\":\"0x%08X\",\"state\":%d,\"nranges\":%d,"
"\"crc\":\"0x%08X\",\"live\":\"0x%08X\",\"match\":%d,"
"\"val_gen\":%u,\"gen\":%u,\"dll\":%d,\"diff_passes\":%u}",
i ? "," : "", c->addr, c->state, c->nranges,
c->crc_code, live, (live == c->crc_code) ? 1 : 0,
c->val_gen, sum, c->dll, c->diff_passes);
}
n += snprintf(out + n, cap - n, "]");
return n;
}
void overlay_loader_get_status(int *active, int *registered,
int *regions_checked,
char *cache_dir_out, int cache_dir_len,
char *game_id_out, int game_id_len,
uint32_t *checked_out, int checked_max,
int *checked_written,
uint32_t *last_crc_out, int *last_file_found_out) {
if (active) *active = s_active;
if (registered) *registered = s_valid_count;
if (regions_checked) *regions_checked = s_nchecked;
if (cache_dir_out) strncpy(cache_dir_out, s_cache_dir, (size_t)cache_dir_len - 1);
if (game_id_out) strncpy(game_id_out, s_game_id, (size_t)game_id_len - 1);
if (checked_out && checked_written) {
int n = s_nchecked < checked_max ? s_nchecked : checked_max;
for (int i = 0; i < n; i++) checked_out[i] = s_checked[i];
*checked_written = n;
}
if (last_crc_out) *last_crc_out = s_last_crc;
if (last_file_found_out) *last_file_found_out = s_last_file_found;
}