mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 10:59:16 -04:00
2d915c3fa9
Phase 1 of the cycle-budgeted precise interrupt-take-point fix (root cause: compiled blocks take IRQs only at block edges; HW + the dirty-RAM interpreter take them at the exact instruction — oracle-confirmed at the Tomba 2 logo Timer1 debounce). Design validated with ChatGPT; full spec in PRECISE_IRQ_SLICE.md. This commit lands the additive, no-behavior-change foundation (Task #2) plus the prior session's diagnostic take-PC capture, and a build-hygiene dedup: - cycles_to_next_event() (interrupts.c): single source of truth — min guest-cycle distance to the next DELIVERABLE interrupt (source raises its I_STAT bit AND that bit is unmasked in i_mask). Aggregates VBLANK + per-peripheral queries. - timers/cdrom/dma/sio _cycles_to_irq(i_mask): each peripheral owns its own next-IRQ-distance query; all conservative under-estimates (smaller => slice more => never run past an event). - debug_server: cycles_to_next_event command for live validation. - interrupts.c: take-PC capture into the IRQ_DELIVER event-ring aux (prior session's diagnostic; records the architectural take-PC for cross-backend diff). - overlay_loader.c: remove duplicate psx_native_bad_entry definition (HEAD had it twice; would not link cleanly). Live-validated at the logo: returns the VBLANK distance, correctly excludes the three free-running (unarmed/masked) timers and idle CD/DMA/SIO. Runtime-only, no regen. The two-tier executor (Task #3) that consumes this is next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2275 lines
108 KiB
C
2275 lines
108 KiB
C
#include "overlay_loader.h"
|
||
#include "overlay_api.h"
|
||
#include "code_provider.h"
|
||
#include "overlay_sljit.h"
|
||
#include "overlay_backend.h"
|
||
#include "crc32.h"
|
||
#include "dirty_ram_interp.h"
|
||
#include "interrupts.h"
|
||
#include "debug_server.h"
|
||
#include "psx_cycles.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;
|
||
|
||
/* RECURSION_BUG.md §25 — 1 when the build is continuation-passing (set by a
|
||
* constructor in the generated CPS dispatch). The overlay dispatch + sljit JIT
|
||
* read it to emit/route the CPS tail-transfer contract. Defined here (before
|
||
* overlay_loader_dispatch) so both can see it. 0 = legacy unit-model. */
|
||
int g_psx_cps_mode = 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 16384
|
||
typedef struct { uint32_t addr; uint32_t crc; uint32_t frame; 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;
|
||
extern uint64_t s_frame_count;
|
||
|
||
/* 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; }
|
||
/* Fail-closed native entry guard (root-cause fix for the Tomba/Tomba2 native-
|
||
* overlay "blue screen" wedge). A native overlay function's generated CPS entry-
|
||
* switch calls psx_native_bad_entry when it is entered at a PC that is NOT one of
|
||
* its legal entries (true prologue or a known continuation) -- e.g. when range
|
||
* ownership wrongly resolved a foreign interior PC to this function. Instead of
|
||
* the old `default: break` (which fell through and ran the function from its
|
||
* TOP, corrupting shared CPU/RAM state -- the wedge), the function records the
|
||
* illegal entry and returns WITHOUT executing; overlay_loader_dispatch then
|
||
* routes the requested PC to the sanctioned dirty-RAM interpreter (the bytes at
|
||
* that PC run correctly). NOT a stub/HLE -- the code still runs, via interp. */
|
||
int g_native_bad_entry = 0; /* set by psx_native_bad_entry, consumed by dispatch */
|
||
static uint32_t s_bad_entry_owner = 0; /* the function unit that rejected the PC */
|
||
static uint32_t s_bad_entry_pc = 0; /* the illegal entry PC */
|
||
static uint64_t s_bad_entry_count = 0;
|
||
void psx_native_bad_entry(CPUState *cpu, uint32_t owner, uint32_t pc) {
|
||
(void)cpu;
|
||
g_native_bad_entry = 1;
|
||
s_bad_entry_owner = owner;
|
||
s_bad_entry_pc = pc;
|
||
s_bad_entry_count++;
|
||
}
|
||
void overlay_loader_bad_entry_stats(uint32_t *owner, uint32_t *pc, uint64_t *count) {
|
||
if (owner) *owner = s_bad_entry_owner;
|
||
if (pc) *pc = s_bad_entry_pc;
|
||
if (count) *count = s_bad_entry_count;
|
||
}
|
||
|
||
/* Per-function native-disable (bisection). Functions whose ENTRY phys address is
|
||
* listed here are forced through the sanctioned dirty-RAM interpreter instead of
|
||
* their native shard — exactly as if s_native_exec were off, but for that one
|
||
* function only. This is a DIAGNOSTIC localization knob (not skip/stub/HLE): the
|
||
* function still runs, just via the interpreter. Used to binary-search which
|
||
* compiled overlay function's native execution causes a native-vs-interp
|
||
* divergence. Keyed by phys (addr & 0x1FFFFFFF) so KSEG bits don't matter. */
|
||
#define NATIVE_BLOCK_CAP 64
|
||
static uint32_t s_native_block[NATIVE_BLOCK_CAP];
|
||
static int s_native_block_n = 0;
|
||
static uint64_t s_native_block_hits = 0;
|
||
static int overlay_native_blocked(uint32_t addr) {
|
||
if (s_native_block_n == 0) return 0;
|
||
uint32_t p = addr & 0x1FFFFFFFu;
|
||
for (int i = 0; i < s_native_block_n; i++)
|
||
if (s_native_block[i] == p) { s_native_block_hits++; return 1; }
|
||
return 0;
|
||
}
|
||
int overlay_loader_native_block_add(uint32_t addr) {
|
||
uint32_t p = addr & 0x1FFFFFFFu;
|
||
for (int i = 0; i < s_native_block_n; i++) if (s_native_block[i] == p) return s_native_block_n;
|
||
if (s_native_block_n >= NATIVE_BLOCK_CAP) return -1;
|
||
s_native_block[s_native_block_n++] = p;
|
||
return s_native_block_n;
|
||
}
|
||
void overlay_loader_native_block_clear(void) { s_native_block_n = 0; s_native_block_hits = 0; }
|
||
int overlay_loader_native_block_list(uint32_t *out, int cap) {
|
||
int n = s_native_block_n < cap ? s_native_block_n : cap;
|
||
for (int i = 0; i < n; i++) out[i] = s_native_block[i];
|
||
return s_native_block_n;
|
||
}
|
||
uint64_t overlay_loader_native_block_hits(void) { return s_native_block_hits; }
|
||
|
||
/* CPS interior-continuation dispatch probe (diagnostic, always-on when armed).
|
||
* Records, for a watched interior PC, what the CPS continuation re-entry path
|
||
* (overlay_find_by_range + validate) decides: chosen candidate entry, range
|
||
* count, crc match, and outcome. Used to confirm the wrong-candidate / escape
|
||
* hypothesis for the Tomba 2 FMV freeze (pc=0x50B30). */
|
||
static uint32_t s_cps_probe_pc = 0; /* phys, 0 = disarmed */
|
||
static uint64_t s_cps_probe_count = 0;
|
||
static uint32_t s_cps_probe_found = 0; /* chosen c->addr (0 if none) */
|
||
static int s_cps_probe_ci = -2; /* overlay_find_by_range result */
|
||
static int s_cps_probe_nrange = -1;
|
||
static int s_cps_probe_matched = -1; /* crc match */
|
||
static int s_cps_probe_outcome = -1; /* 0=find<0,1=crc-miss->interp,2=native,3=device,4=blocked */
|
||
static int s_cps_probe_ncand_inrange = 0; /* how many candidates' range contains the PC */
|
||
void overlay_loader_cps_probe_set(uint32_t pc) {
|
||
s_cps_probe_pc = pc & 0x1FFFFFFFu; s_cps_probe_count = 0;
|
||
s_cps_probe_found = 0; s_cps_probe_ci = -2; s_cps_probe_nrange = -1;
|
||
s_cps_probe_matched = -1; s_cps_probe_outcome = -1; s_cps_probe_ncand_inrange = 0;
|
||
}
|
||
void overlay_loader_cps_probe_get(uint32_t *pc, uint64_t *cnt, uint32_t *found,
|
||
int *ci, int *nrange, int *matched, int *outcome,
|
||
int *ncand) {
|
||
if (pc) *pc = s_cps_probe_pc; if (cnt) *cnt = s_cps_probe_count;
|
||
if (found) *found = s_cps_probe_found; if (ci) *ci = s_cps_probe_ci;
|
||
if (nrange) *nrange = s_cps_probe_nrange; if (matched) *matched = s_cps_probe_matched;
|
||
if (outcome) *outcome = s_cps_probe_outcome; if (ncand) *ncand = s_cps_probe_ncand_inrange;
|
||
}
|
||
/* count how many live candidates have a range containing phys (multi-variant check) */
|
||
static int overlay_count_by_range(uint32_t phys) {
|
||
int n = 0;
|
||
for (int i = 0; i < s_cand_n; i++) {
|
||
const Candidate *c = &s_cand[i];
|
||
if (c->state == ENTRY_BLACKLIST) continue;
|
||
for (int r = 0; r < c->nranges; r++)
|
||
if (phys >= c->range_lo[r] && phys < c->range_lo[r] + c->range_len[r]) { n++; break; }
|
||
}
|
||
return n;
|
||
}
|
||
/* 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 uint64_t s_gen_fastpath = 0; /* dispatches that skipped the crc32 */
|
||
/* via the unchanged page-generation */
|
||
/* fast path (overlay-cache v2 P2) */
|
||
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 function. 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.
|
||
*
|
||
* "Same function/content" is keyed on CURRENT LIVE-RAM VALIDITY, not crc_code
|
||
* equality: the two backends hash DIFFERENT byte ranges for the same function
|
||
* (sljit hashes its contiguous JIT fragment [entry, terminator]; the gcc
|
||
* .ranges manifest hashes the recompiler's per-function instruction walk, with
|
||
* interleaved jump-tables/data excised), so their crc_code values legitimately
|
||
* differ even for identical source bytes. The backend-independent test is: this
|
||
* gcc candidate matches live RAM (c->state == VALID, computed above) AND the
|
||
* sljit shard at the same entry also matches live RAM right now — i.e. both
|
||
* describe the CURRENTLY-LIVE variant. An sljit shard for a DIFFERENT overlay
|
||
* variant at this address does NOT match live and is kept (distinct coverage). */
|
||
if (c->state == ENTRY_VALID) {
|
||
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 &&
|
||
cand_crc(o) == o->crc_code) { /* o matches live => same variant */
|
||
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", o->addr);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ---- 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). */
|
||
/* crc_override: 0 => hash the crc from live RAM (an in-session JIT, where the
|
||
* overlay bytes ARE loaded). Non-zero => use it verbatim — a shard RELOADED from
|
||
* the persisted cache at init, where the overlay code may not be in RAM yet; the
|
||
* blob header carries the crc it was compiled from, and dispatch re-hashes live
|
||
* RAM against it so the shard only runs once the identical bytes are present. */
|
||
static void register_sljit_candidate(uint32_t phys, OverlayFn fn,
|
||
uint32_t lo, uint32_t len, uint32_t crc_override) {
|
||
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 = crc_override ? crc_override : cand_crc(c); /* live bytes == JIT'd-from bytes */
|
||
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;
|
||
}
|
||
|
||
/* Persist a freshly-JIT'd shard's serialized LIR to the on-disk cache (defined
|
||
* below, after the cache-path globals). */
|
||
static void persist_sljit_shard(uint32_t entry_phys, uint32_t lo, uint32_t len,
|
||
const void *blob, unsigned long blob_size);
|
||
|
||
#include "overlay_compile_worker.h"
|
||
/* Off-main-thread sljit compile (docs/ASYNC_OVERLAY_COMPILE.md). The background
|
||
* worker JITs from a snapshot and PUBLISHES the shard to the on-disk cache +
|
||
* sets s_async_cache_dirty; the DISPATCH thread rescans-and-registers on its
|
||
* next miss (candidate table stays single-threaded). PSX_SLJIT_SYNC=1 forces
|
||
* the legacy synchronous JIT-on-miss path. */
|
||
static volatile LONG s_async_cache_dirty = 0;
|
||
static int g_sljit_async = 1;
|
||
|
||
/* MASTER SAFETY GATE (2026-06-25): the sljit EMITTER mistranslates some overlay
|
||
* instruction(s). Proven on MMX6: sljit-only save-load softlocks/loopbacks, PURE
|
||
* INTERP works (discriminator), and it reproduces in the SYNCHRONOUS path — so it
|
||
* is the emitter, not the async worker. Until the emitter bug is found + fixed the
|
||
* whole sljit tier is DISABLED by default: no shard READ (scan_sljit_cache_dir),
|
||
* no shard GENERATE (worker / sync try / live gap-fill), every overlay miss falls
|
||
* to the interpreter. gcc (DLL cache + autocompile) and overlay capture are
|
||
* untouched, so priority is gcc > interp and dispatch misses are still recorded
|
||
* for future gcc coverage. PSX_SLJIT_ENABLE=1 re-enables it for emitter-fix
|
||
* testing. See memory mmx6_sljit_shard_malformed. */
|
||
static int g_sljit_tier_enabled = 0;
|
||
|
||
/* 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;
|
||
/* Use the sljit provider specifically (not the active one): gcc is the
|
||
* primary/batch provider with compile_fragment == NULL, so the synchronous
|
||
* JIT-on-miss gap-fill must go through sljit even when gcc is "active". */
|
||
const CodeProvider *cp = code_provider_sljit();
|
||
if (!cp || !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, 0);
|
||
/* Persist the position-independent shard so a later (possibly toolchain-
|
||
* less) session reloads it instead of re-JITing through the interpreter. */
|
||
persist_sljit_shard(phys, frag.code_lo, frag.code_len,
|
||
frag.serialized, frag.serialized_size);
|
||
}
|
||
if (frag.serialized) overlay_sljit_free_serialized(frag.serialized);
|
||
}
|
||
|
||
/* ---- 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;
|
||
}
|
||
|
||
/* Dedup by FILENAME (<addr8>_<crc8>.dll = region+crc). Used so a tcc/ shard for a
|
||
* region already covered by a gcc/ shard is skipped: scan_cache_dir scans gcc/
|
||
* FIRST, so gcc always wins the tie (tier order gcc > tcc). Also subsumes the
|
||
* per-path rescan-idempotence check (same dir => same filename). */
|
||
static int cache_idx_has_basename(const char *fname) {
|
||
for (int i = 0; i < s_cache_idx_count; i++) {
|
||
const char *b = strrchr(s_cache_idx[i].path, '/');
|
||
b = b ? b + 1 : s_cache_idx[i].path;
|
||
if (strcmp(b, fname) == 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);
|
||
/* skip if this region+crc is already indexed (rescan idempotence, AND a
|
||
* higher-priority namespace already covers it — gcc/ scanned before tcc/). */
|
||
if (cache_idx_has_basename(fd.cFileName)) continue;
|
||
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>/cg<codegen-ver>/. The codegen
|
||
* version segment means a build with new emitter output reads a FRESH directory
|
||
* and never picks up a stale DLL (old versions coexist on disk, no migration).
|
||
* The sljit/<arch-abi>/ namespace is reserved (no on-disk blobs: sljit re-JITs
|
||
* from the coverage manifest; see SLJIT.md §5.3). (Pre-1.0: no legacy fallback —
|
||
* older flat / unversioned caches are simply ignored and regenerated.) */
|
||
static void scan_cache_dir(void) {
|
||
char dir[768];
|
||
/* Tier order: scan gcc/ FIRST (highest native priority — the dev/production
|
||
* shards), THEN tcc/ (the toolchain-free fallback). scan_one_cache_dir dedups
|
||
* by filename (region+crc), so a tcc shard for a region a gcc shard already
|
||
* covers is skipped — gcc always wins the tie. */
|
||
snprintf(dir, sizeof(dir), "%s/%s/gcc/%s/cg%d_%08x",
|
||
s_cache_dir, s_game_id, PSX_OVERLAY_ARCH_ABI, PSX_OVERLAY_CODEGEN_VER,
|
||
(unsigned)PSX_OVERLAY_CODEGEN_HASH);
|
||
scan_one_cache_dir(dir);
|
||
snprintf(dir, sizeof(dir), "%s/%s/tcc/%s/cg%d_%08x",
|
||
s_cache_dir, s_game_id, PSX_OVERLAY_ARCH_ABI, PSX_OVERLAY_CODEGEN_VER,
|
||
(unsigned)PSX_OVERLAY_CODEGEN_HASH);
|
||
scan_one_cache_dir(dir);
|
||
}
|
||
|
||
/* ---- Persisted sljit shard cache (Stage 2, SLJIT_PERSIST_CACHE.md) -------- */
|
||
/* JIT'd sljit shards are serialized (position-independent LIR — Stage 1) and
|
||
* written to <cache>/<game_id>/sljit/<arch-abi>/cg<N>/<entry8>_<crc8>.sljit; at
|
||
* init they are deserialized + regenerated for THIS process and registered as
|
||
* dll<0 candidates. The arch-abi + codegen-version + helper-order tags namespace
|
||
* and invalidate stale blobs. Priority is unchanged (gcc > sljit > interp): a gcc
|
||
* DLL covering the same live function still supersedes a reloaded shard via the
|
||
* obsolete path. Safety is unchanged too: a reloaded shard re-earns trust each
|
||
* session through the same per-dispatch crc re-hash + same-state diff gate. */
|
||
#define SLJIT_BLOB_MAGIC 0x534A4C53u /* 'SLJS' */
|
||
#define SLJIT_BLOB_FORMAT_VER 2u /* v2: serialized with debug info (options=0) */
|
||
#define SLJIT_HELPER_ORDER_VER 1u /* bump if the SLJIT_HLP_* order changes */
|
||
|
||
typedef struct {
|
||
uint32_t magic, format_ver, helper_order_ver;
|
||
uint32_t entry_phys, code_lo, code_len, crc_code, blob_size;
|
||
} SljitBlobHeader;
|
||
|
||
static int s_sljit_persist = 0; /* write JIT'd shards to disk */
|
||
static uint32_t s_sljit_reloaded = 0; /* shards loaded from disk at init */
|
||
static uint32_t s_sljit_persist_calls = 0; /* persist_sljit_shard entered */
|
||
static uint32_t s_sljit_persist_writes = 0; /* blobs actually written */
|
||
static int s_persist_dbg = 0; /* last persist exit reason (1..6) */
|
||
static uint32_t s_reload_seen = 0; /* .sljit files matched by the scan */
|
||
static uint32_t s_reload_hdrbad = 0; /* files rejected on header validation */
|
||
static uint32_t s_reload_deserfail = 0; /* files that failed deserialize */
|
||
uint32_t overlay_loader_sljit_reload_seen(void) { return s_reload_seen; }
|
||
uint32_t overlay_loader_sljit_reload_hdrbad(void) { return s_reload_hdrbad; }
|
||
uint32_t overlay_loader_sljit_reload_deserfail(void){ return s_reload_deserfail; }
|
||
uint32_t overlay_loader_sljit_reloaded(void) { return s_sljit_reloaded; }
|
||
uint32_t overlay_loader_sljit_persist_calls(void) { return s_sljit_persist_calls; }
|
||
uint32_t overlay_loader_sljit_persist_writes(void) { return s_sljit_persist_writes; }
|
||
int overlay_loader_sljit_persist_on(void) { return s_sljit_persist; }
|
||
int overlay_loader_sljit_persist_dbg(void) { return s_persist_dbg; }
|
||
|
||
static void sljit_cache_dir(char *buf, size_t n) {
|
||
snprintf(buf, n, "%s/%s/sljit/%s/cg%d_%08x",
|
||
s_cache_dir, s_game_id, PSX_OVERLAY_ARCH_ABI, PSX_OVERLAY_CODEGEN_VER,
|
||
(unsigned)PSX_OVERLAY_CODEGEN_HASH);
|
||
}
|
||
|
||
/* Public: the resolved on-disk sljit shard dir, so the sljit_async debug command
|
||
* can report exactly where the worker writes (no guessing about cache location). */
|
||
void overlay_loader_sljit_cache_dir(char *buf, int n) {
|
||
if (buf && n > 0) sljit_cache_dir(buf, (size_t)n);
|
||
}
|
||
int overlay_loader_sljit_async_on(void) { return g_sljit_async; }
|
||
int overlay_loader_sljit_tier_enabled(void) { return g_sljit_tier_enabled; }
|
||
|
||
#ifdef _WIN32
|
||
static void sljit_mkdir_p(const char *path) {
|
||
char tmp[768];
|
||
snprintf(tmp, sizeof(tmp), "%s", path);
|
||
for (char *p = tmp + 1; *p; p++) {
|
||
if (*p == '/' || *p == '\\') { char sep = *p; *p = '\0'; CreateDirectoryA(tmp, NULL); *p = sep; }
|
||
}
|
||
CreateDirectoryA(tmp, NULL);
|
||
}
|
||
#endif
|
||
|
||
static void persist_sljit_shard(uint32_t entry_phys, uint32_t lo, uint32_t len,
|
||
const void *blob, unsigned long blob_size) {
|
||
#ifdef _WIN32
|
||
s_sljit_persist_calls++;
|
||
if (!s_sljit_persist) { s_persist_dbg = 1; return; }
|
||
if (!blob || blob_size == 0) { s_persist_dbg = 2; return; }
|
||
if (len == 0) { s_persist_dbg = 3; return; }
|
||
const uint8_t *ram = memory_get_ram_ptr();
|
||
if (!ram) { s_persist_dbg = 4; return; }
|
||
/* crc over the live bytes the shard was JIT'd from — identical to cand_crc()
|
||
* for this single range, so the reloaded candidate's crc matches what
|
||
* dispatch re-hashes. */
|
||
uint32_t crc = crc32_update(0xFFFFFFFFu, ram + (lo & 0x1FFFFFFFu), len) ^ 0xFFFFFFFFu;
|
||
char dir[768]; sljit_cache_dir(dir, sizeof(dir));
|
||
sljit_mkdir_p(dir);
|
||
char path[860];
|
||
snprintf(path, sizeof(path), "%s/%08X_%08X.sljit", dir, entry_phys & 0x1FFFFFFFu, crc);
|
||
FILE *f = fopen(path, "wb");
|
||
if (!f) { s_persist_dbg = 5; return; }
|
||
SljitBlobHeader hh;
|
||
hh.magic = SLJIT_BLOB_MAGIC; hh.format_ver = SLJIT_BLOB_FORMAT_VER;
|
||
hh.helper_order_ver = SLJIT_HELPER_ORDER_VER;
|
||
hh.entry_phys = entry_phys & 0x1FFFFFFFu; hh.code_lo = lo & 0x1FFFFFFFu;
|
||
hh.code_len = len; hh.crc_code = crc; hh.blob_size = (uint32_t)blob_size;
|
||
fwrite(&hh, sizeof(hh), 1, f);
|
||
fwrite(blob, 1, blob_size, f);
|
||
fclose(f);
|
||
s_sljit_persist_writes++;
|
||
s_persist_dbg = 6;
|
||
loader_log("sljit shard persisted %08X_%08X [%lu bytes]",
|
||
entry_phys & 0x1FFFFFFFu, crc, blob_size);
|
||
#else
|
||
(void)entry_phys; (void)lo; (void)len; (void)blob; (void)blob_size;
|
||
#endif
|
||
}
|
||
|
||
/* Worker-thread publish (overlay_compile_worker.c): write a freshly-JIT'd shard
|
||
* into the on-disk cache ATOMICALLY (temp + rename, so dispatch's scan never
|
||
* reads a partial file) with an EXPLICIT crc (computed by the worker over the
|
||
* snapshot's compiled range, NOT live RAM), then flag dispatch to rescan. Only
|
||
* the filesystem + read-only-after-init cache-path globals are touched — never
|
||
* the candidate table — so it is safe off the dispatch thread. */
|
||
void overlay_loader_async_publish(uint32_t entry_phys, uint32_t lo, uint32_t len,
|
||
uint32_t crc, const void *blob,
|
||
unsigned long blob_size) {
|
||
#ifdef _WIN32
|
||
if (!s_sljit_persist || !blob || blob_size == 0 || len == 0) return;
|
||
char dir[768]; sljit_cache_dir(dir, sizeof(dir));
|
||
sljit_mkdir_p(dir);
|
||
char path[860], tmp[920];
|
||
snprintf(path, sizeof(path), "%s/%08X_%08X.sljit", dir, entry_phys & 0x1FFFFFFFu, crc);
|
||
snprintf(tmp, sizeof(tmp), "%s.tmp%lu", path, (unsigned long)GetCurrentThreadId());
|
||
FILE *f = fopen(tmp, "wb");
|
||
if (!f) return;
|
||
SljitBlobHeader hh;
|
||
hh.magic = SLJIT_BLOB_MAGIC; hh.format_ver = SLJIT_BLOB_FORMAT_VER;
|
||
hh.helper_order_ver = SLJIT_HELPER_ORDER_VER;
|
||
hh.entry_phys = entry_phys & 0x1FFFFFFFu; hh.code_lo = lo & 0x1FFFFFFFu;
|
||
hh.code_len = len; hh.crc_code = crc; hh.blob_size = (uint32_t)blob_size;
|
||
int wok = (fwrite(&hh, sizeof hh, 1, f) == 1) &&
|
||
(fwrite(blob, 1, blob_size, f) == blob_size);
|
||
fclose(f);
|
||
if (!wok) { remove(tmp); return; }
|
||
if (!MoveFileExA(tmp, path, MOVEFILE_REPLACE_EXISTING)) { remove(tmp); return; }
|
||
s_sljit_persist_writes++; /* telemetry-only; benign cross-thread incr */
|
||
InterlockedExchange(&s_async_cache_dirty, 1);
|
||
#else
|
||
(void)entry_phys; (void)lo; (void)len; (void)crc; (void)blob; (void)blob_size;
|
||
#endif
|
||
}
|
||
|
||
/* Idempotency for the on-miss rescan: a dispatch-thread-only set of .sljit
|
||
* filenames already deserialized+registered, so re-running scan_sljit_cache_dir
|
||
* after a worker publish never double-registers. */
|
||
#define MAX_LOADED_BLOBS 8192
|
||
static char s_loaded_blobs[MAX_LOADED_BLOBS][24]; /* "XXXXXXXX_XXXXXXXX.sljit" */
|
||
static int s_loaded_blob_n;
|
||
static int blob_already_loaded(const char *name) {
|
||
for (int i = 0; i < s_loaded_blob_n; i++)
|
||
if (!strcmp(s_loaded_blobs[i], name)) return 1;
|
||
return 0;
|
||
}
|
||
static void blob_mark_loaded(const char *name) {
|
||
if (s_loaded_blob_n < MAX_LOADED_BLOBS) {
|
||
strncpy(s_loaded_blobs[s_loaded_blob_n], name, 23);
|
||
s_loaded_blobs[s_loaded_blob_n][23] = '\0';
|
||
s_loaded_blob_n++;
|
||
}
|
||
}
|
||
|
||
/* Reload persisted shards at init: deserialize + regenerate + register. The crc
|
||
* comes from the blob header (overlay RAM may not be loaded yet; dispatch
|
||
* re-hashes live RAM against it before ever running the shard). */
|
||
static void scan_sljit_cache_dir(void) {
|
||
#ifdef _WIN32
|
||
char dir[768]; sljit_cache_dir(dir, sizeof(dir));
|
||
char pattern[860];
|
||
snprintf(pattern, sizeof(pattern), "%s/*.sljit", dir);
|
||
WIN32_FIND_DATAA fd;
|
||
HANDLE fh = FindFirstFileA(pattern, &fd);
|
||
if (fh == INVALID_HANDLE_VALUE) return;
|
||
do {
|
||
s_reload_seen++;
|
||
if (blob_already_loaded(fd.cFileName)) continue; /* idempotent rescan */
|
||
char full[900];
|
||
snprintf(full, sizeof(full), "%s/%s", dir, fd.cFileName);
|
||
FILE *f = fopen(full, "rb");
|
||
if (!f) continue;
|
||
SljitBlobHeader hd;
|
||
if (fread(&hd, sizeof(hd), 1, f) != 1) { fclose(f); s_reload_hdrbad++; continue; }
|
||
if (hd.magic != SLJIT_BLOB_MAGIC || hd.format_ver != SLJIT_BLOB_FORMAT_VER ||
|
||
hd.helper_order_ver != SLJIT_HELPER_ORDER_VER ||
|
||
hd.blob_size == 0 || hd.blob_size > (4u * 1024u * 1024u)) { fclose(f); s_reload_hdrbad++; continue; }
|
||
void *blob = malloc(hd.blob_size);
|
||
if (!blob) { fclose(f); continue; }
|
||
size_t got = fread(blob, 1, hd.blob_size, f);
|
||
fclose(f);
|
||
if (got != hd.blob_size) { free(blob); s_reload_hdrbad++; continue; }
|
||
OverlaySljitFn fn = overlay_sljit_deserialize(blob, hd.blob_size);
|
||
free(blob);
|
||
if (!fn) { s_reload_deserfail++; continue; }
|
||
register_sljit_candidate(hd.entry_phys, (OverlayFn)fn,
|
||
hd.code_lo, hd.code_len, hd.crc_code);
|
||
s_sljit_reloaded++;
|
||
blob_mark_loaded(fd.cFileName);
|
||
} while (FindNextFileA(fh, &fd));
|
||
FindClose(fh);
|
||
if (s_sljit_reloaded) loader_log("sljit cache: reloaded %u shard(s)", s_sljit_reloaded);
|
||
#endif
|
||
}
|
||
|
||
/* 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 psx_check_interrupts_at(CPUState *cpu, uint32_t resume_pc);
|
||
extern void gte_execute(CPUState *cpu, uint32_t cmd);
|
||
extern int 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;
|
||
static uint32_t s_irq_budget_cycles = 0; /* 0 = off; otherwise guest cycles/check */
|
||
static uint64_t s_irq_last_check_cycle = UINT64_MAX;
|
||
static int s_irq_suppress_cdrom_only = 0;
|
||
static int s_irq_post_dispatch_pump = 0;
|
||
static int s_irq_defer_cdrom = 0;
|
||
|
||
extern uint32_t i_stat;
|
||
extern uint32_t i_mask;
|
||
|
||
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 int overlay_irq_suppressed_now(void) {
|
||
if (s_suppress_irq) {
|
||
if (s_irq_ratelimit == 0) { s_irq_suppressed++; return 1; }
|
||
if ((++s_irq_callcount % s_irq_ratelimit) != 0) { s_irq_suppressed++; return 1; }
|
||
}
|
||
if (s_irq_suppress_cdrom_only) {
|
||
uint32_t pending = i_stat & i_mask;
|
||
if (pending == (1u << IRQ_CDROM)) {
|
||
s_irq_suppressed++;
|
||
return 1;
|
||
}
|
||
}
|
||
if (s_irq_budget_cycles != 0) {
|
||
uint64_t now = psx_get_cycle_count();
|
||
if (s_irq_last_check_cycle != UINT64_MAX &&
|
||
now - s_irq_last_check_cycle < (uint64_t)s_irq_budget_cycles) {
|
||
s_irq_suppressed++;
|
||
return 1;
|
||
}
|
||
s_irq_last_check_cycle = now;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static void overlay_ci_wrapper(CPUState *cpu) {
|
||
if (overlay_irq_suppressed_now()) return;
|
||
if (s_irq_defer_cdrom && (i_stat & (1u << IRQ_CDROM))) {
|
||
uint32_t saved_cd = i_stat & (1u << IRQ_CDROM);
|
||
i_stat &= ~(1u << IRQ_CDROM);
|
||
psx_check_interrupts(cpu);
|
||
i_stat |= saved_cd;
|
||
return;
|
||
}
|
||
psx_check_interrupts(cpu);
|
||
}
|
||
|
||
static void overlay_ci_at_wrapper(CPUState *cpu, uint32_t resume_pc) {
|
||
if (overlay_irq_suppressed_now()) return;
|
||
if (s_irq_defer_cdrom && (i_stat & (1u << IRQ_CDROM))) {
|
||
uint32_t saved_cd = i_stat & (1u << IRQ_CDROM);
|
||
i_stat &= ~(1u << IRQ_CDROM);
|
||
psx_check_interrupts_at(cpu, resume_pc);
|
||
i_stat |= saved_cd;
|
||
return;
|
||
}
|
||
psx_check_interrupts_at(cpu, resume_pc);
|
||
}
|
||
|
||
static int overlay_irq_budget_blocks_now(void) {
|
||
if (s_irq_budget_cycles == 0) return 0;
|
||
uint64_t now = psx_get_cycle_count();
|
||
if (s_irq_last_check_cycle != UINT64_MAX &&
|
||
now - s_irq_last_check_cycle < (uint64_t)s_irq_budget_cycles) {
|
||
s_irq_suppressed++;
|
||
return 1;
|
||
}
|
||
s_irq_last_check_cycle = now;
|
||
return 0;
|
||
}
|
||
|
||
static void overlay_post_dispatch_irq_pump(CPUState *cpu) {
|
||
if (!s_irq_post_dispatch_pump) return;
|
||
if (overlay_irq_budget_blocks_now()) return;
|
||
if (cpu->pc != 0u) psx_check_interrupts_at(cpu, cpu->pc);
|
||
else 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.check_interrupts_at = overlay_ci_at_wrapper;
|
||
{ extern void psx_advance_cycles(uint32_t cycles);
|
||
s_callbacks.advance_cycles = psx_advance_cycles; }
|
||
s_callbacks.gte_execute = gte_execute;
|
||
s_callbacks.psx_syscall = psx_syscall;
|
||
s_callbacks.psx_native_bad_entry = psx_native_bad_entry;
|
||
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;
|
||
/* Widescreen hooks (ABI v3): overlay-emitted psx_ws_* calls forward to
|
||
* the runtime's live widescreen state (gpu.c). */
|
||
{
|
||
extern int psx_ws_backdrop_x(int x);
|
||
extern int psx_ws_x_margin(void);
|
||
extern void psx_ws_sprite_tag(CPUState *cpu);
|
||
extern uint32_t psx_ws_backdrop_value(uint32_t orig, int is_end, int window_cols); /* ABI v4 */
|
||
s_callbacks.ws_backdrop_x = psx_ws_backdrop_x;
|
||
s_callbacks.ws_x_margin = psx_ws_x_margin;
|
||
s_callbacks.ws_sprite_tag = psx_ws_sprite_tag;
|
||
s_callbacks.ws_backdrop_value = psx_ws_backdrop_value;
|
||
}
|
||
}
|
||
|
||
/* ---- 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();
|
||
/* Persisted sljit shard cache (Stage 2): the cache is enabled (this init only
|
||
* runs when [runtime] overlay_cache is on), so persist JIT'd shards and reload
|
||
* any from a prior session now. Reloaded shards register as dll<0 candidates
|
||
* and re-earn trust via the normal per-dispatch crc + diff gate. */
|
||
{ const char *e = getenv("PSX_SLJIT_ENABLE"); if (e && *e && *e != '0') g_sljit_tier_enabled = 1; }
|
||
if (g_sljit_tier_enabled) {
|
||
s_sljit_persist = 1;
|
||
scan_sljit_cache_dir();
|
||
{ const char *e = getenv("PSX_SLJIT_SYNC"); if (e && *e && *e != '0') g_sljit_async = 0; }
|
||
if (g_sljit_async) overlay_compile_worker_start(); /* off-thread JIT */
|
||
} else {
|
||
/* Tier disabled (default): no read, no generate. persist off so the worker
|
||
* publish + persist_sljit_shard are inert even if reached. gcc > interp. */
|
||
s_sljit_persist = 0;
|
||
loader_log("sljit tier DISABLED (emitter bug; gcc>interp). PSX_SLJIT_ENABLE=1 to re-enable");
|
||
}
|
||
/* Pre-seed native-block bisection list from PSX_NATIVE_BLOCK (comma/space
|
||
* separated hex addrs). Lets us route init-time overlay functions through the
|
||
* sanctioned dirty-RAM interpreter from the FIRST boot instruction — the
|
||
* runtime overlay_native_block cmd can only be set post-boot, too late for
|
||
* once-only init functions. Same diagnostic knob, just seedable at launch. */
|
||
{
|
||
const char *nb = getenv("PSX_NATIVE_BLOCK");
|
||
if (nb && *nb) {
|
||
const char *p = nb;
|
||
while (*p) {
|
||
while (*p == ' ' || *p == ',' || *p == '\t') p++;
|
||
if (!*p) break;
|
||
uint32_t a = (uint32_t)strtoul(p, NULL, 0);
|
||
if (a) overlay_loader_native_block_add(a);
|
||
while (*p && *p != ' ' && *p != ',' && *p != '\t') p++;
|
||
}
|
||
loader_log("PSX_NATIVE_BLOCK seeded %d native-block entr%s from '%s'",
|
||
s_native_block_n, s_native_block_n == 1 ? "y" : "ies", nb);
|
||
}
|
||
}
|
||
/* Boot-time full-interp override (diagnostic): PSX_OVERLAY_NATIVE_OFF=1 forces
|
||
* native overlay execution off from the first instruction, so a pristine
|
||
* interpreter reference can be captured without racing post-boot cmds. Same
|
||
* effect as the overlay_native_off cmd, but seeded at launch. */
|
||
{
|
||
const char *no = getenv("PSX_OVERLAY_NATIVE_OFF");
|
||
if (no && *no && *no != '0') {
|
||
s_native_exec = 0;
|
||
loader_log("PSX_OVERLAY_NATIVE_OFF set: native overlay exec OFF from boot");
|
||
}
|
||
}
|
||
{
|
||
const char *sup = getenv("PSX_OVERLAY_IRQ_SUPPRESS");
|
||
const char *rl = getenv("PSX_OVERLAY_IRQ_RATELIMIT");
|
||
if (sup && *sup && *sup != '0') {
|
||
overlay_loader_set_irq_suppress(1, 0);
|
||
loader_log("PSX_OVERLAY_IRQ_SUPPRESS set: native overlay IRQ checks suppressed from boot");
|
||
} else if (rl && *rl) {
|
||
uint32_t n = (uint32_t)strtoul(rl, NULL, 0);
|
||
if (n == 0) n = 1;
|
||
overlay_loader_set_irq_suppress(1, n);
|
||
loader_log("PSX_OVERLAY_IRQ_RATELIMIT set: native overlay IRQ checks every %u call(s)", n);
|
||
}
|
||
}
|
||
{
|
||
const char *budget = getenv("PSX_OVERLAY_IRQ_BUDGET");
|
||
if (budget && *budget) {
|
||
uint32_t n = (uint32_t)strtoul(budget, NULL, 0);
|
||
s_irq_budget_cycles = n;
|
||
s_irq_last_check_cycle = UINT64_MAX;
|
||
loader_log("PSX_OVERLAY_IRQ_BUDGET set: native overlay IRQ checks every %u guest cycle(s)", n);
|
||
}
|
||
}
|
||
{
|
||
const char *no_cd = getenv("PSX_OVERLAY_IRQ_NO_CDROM");
|
||
if (no_cd && *no_cd && *no_cd != '0') {
|
||
s_irq_suppress_cdrom_only = 1;
|
||
loader_log("PSX_OVERLAY_IRQ_NO_CDROM set: native overlay CDROM-only IRQ checks suppressed");
|
||
}
|
||
}
|
||
{
|
||
const char *defer_cd = getenv("PSX_OVERLAY_IRQ_DEFER_CDROM");
|
||
if (defer_cd && *defer_cd && *defer_cd != '0') {
|
||
s_irq_defer_cdrom = 1;
|
||
loader_log("PSX_OVERLAY_IRQ_DEFER_CDROM set: native overlay CDROM IRQ delivery deferred");
|
||
}
|
||
}
|
||
{
|
||
const char *post = getenv("PSX_OVERLAY_IRQ_POST_PUMP");
|
||
if (post && *post && *post != '0') {
|
||
s_irq_post_dispatch_pump = 1;
|
||
s_irq_last_check_cycle = UINT64_MAX;
|
||
loader_log("PSX_OVERLAY_IRQ_POST_PUMP set: native overlay dispatch-return IRQ pump enabled");
|
||
}
|
||
}
|
||
{
|
||
const char *diff = getenv("PSX_OVERLAY_DIFF");
|
||
if (diff && *diff && *diff != '0') {
|
||
s_diff_mode = 1;
|
||
loader_log("PSX_OVERLAY_DIFF set: native/interp shadow diff ON from boot");
|
||
}
|
||
}
|
||
/* Live-mode policy is applied later via overlay_loader_apply_live_policy(),
|
||
* AFTER code_provider_init resolves the backend (the default depends on it). */
|
||
s_active = 1;
|
||
}
|
||
|
||
/* Apply the sljit live-execution policy once the Tier-2 backend is resolved
|
||
* (call AFTER code_provider_init). Default: live ON when the resolved backend is
|
||
* sljit (the toolchain-less production model — JIT on miss, VALIDATE via the
|
||
* same-state diff, promote to native only on a clean verify budget; device +
|
||
* diverging shards stay on the interpreter), OFF otherwise. The
|
||
* PSX_OVERLAY_SLJIT_LIVE env var overrides either way (0 forces off, nonzero
|
||
* forces on) — a dev escape hatch. NOTE: "live" here is VALIDATED-live, not the
|
||
* old blind path; see overlay_loader_dispatch's diff gate. */
|
||
void overlay_loader_apply_live_policy(void) {
|
||
/* DEFAULT-ON (validated): sljit is live whenever it is AVAILABLE, so it fills
|
||
* gcc-absent overlay regions even on a gcc dev box (gcc > sljit > interp).
|
||
* "Live" is VALIDATED-live — shards still route through the same-state diff
|
||
* gate (see overlay_loader_dispatch) and device-touching / diverging shards
|
||
* stay on the interpreter, so default-on cannot reintroduce the save-load
|
||
* wedge. The PSX_OVERLAY_SLJIT_LIVE env var still overrides (0 forces off). */
|
||
int live = (code_provider_sljit() != NULL) ? 1 : 0;
|
||
/* Mutually exclusive (user preference, RECURSION_BUG.md §25): if gcc is the
|
||
* active backend, do NOT run sljit live. The gcc autocompile produces
|
||
* full-coverage CPS DLLs in the background; the gap before they land is the
|
||
* interpreter (relatively fast), not sljit leaf shards. So gcc boxes are
|
||
* gcc>interp; toolchain-less boxes are sljit>interp. PSX_OVERLAY_SLJIT_LIVE
|
||
* still overrides for testing. */
|
||
if (overlay_backend_active() == OVERLAY_BACKEND_GCC) live = 0;
|
||
const char *e = getenv("PSX_OVERLAY_SLJIT_LIVE");
|
||
if (e && *e) live = (*e != '0') ? 1 : 0;
|
||
/* MASTER GATE: the sljit tier is disabled by default (emitter bug) — never go
|
||
* live, regardless of backend/env, until PSX_SLJIT_ENABLE=1. */
|
||
if (!g_sljit_tier_enabled) live = 0;
|
||
s_sljit_live = live;
|
||
loader_log("sljit live policy: active_backend=%s sljit_avail=%d env=%s -> live=%d",
|
||
overlay_backend_name(overlay_backend_active()),
|
||
code_provider_sljit() != NULL, e && *e ? e : "(unset)", live);
|
||
}
|
||
|
||
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.
|
||
*
|
||
* CRITICAL: the capture clamps each region to its WINDOW — kernel
|
||
* [0, DIRTY_RAM_KERNEL_WINDOW_END) or overlay [OVERLAY_REGION_FLOOR, end) —
|
||
* so a DLL's region_start is the first dirty page AT OR ABOVE the window
|
||
* floor. But the dirty run is contiguous ACROSS the static-EXE gap between
|
||
* those windows: the boot EXE image (0x10000..floor) was written at load
|
||
* time, so its pages are dirty too. Walking back without the same floor
|
||
* clamp overshoots through the EXE region and yields a region_start (e.g.
|
||
* 0x10000) that NO DLL filename matches — so the overlay's DLLs never load
|
||
* and its functions interpret forever (the dominant slowness). Clamp the
|
||
* walkback to the window floor so region_start matches the capture. */
|
||
uint32_t floor_pg = (phys < DIRTY_RAM_KERNEL_WINDOW_END)
|
||
? 0u
|
||
: (OVERLAY_REGION_FLOOR / page_sz);
|
||
uint32_t pg = phys / page_sz;
|
||
while (pg > floor_pg) {
|
||
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 ---------------------------------------------------------- */
|
||
|
||
/* CPS (§25): find a non-blacklisted candidate whose code range CONTAINS phys —
|
||
* i.e. phys is a continuation / return point INSIDE an overlay function, not its
|
||
* registered entry. Under continuation-passing, an overlay function tail-
|
||
* transfers after each block, so a callee returns to a mid-function address that
|
||
* idx_head() (entry-keyed) misses; we re-enter the owning function with
|
||
* cpu->pc = that address so its entry-switch routes to the right block. Returns
|
||
* the candidate index, or -1. */
|
||
static int overlay_find_by_range(uint32_t phys) {
|
||
for (int i = 0; i < s_cand_n; i++) {
|
||
const Candidate *c = &s_cand[i];
|
||
if (c->state == ENTRY_BLACKLIST) continue;
|
||
for (int r = 0; r < c->nranges; r++) {
|
||
uint32_t lo = c->range_lo[r];
|
||
if (phys >= lo && phys < lo + c->range_len[r]) return i;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
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)) {
|
||
/* Pick up shards the background compile worker just published (idempotent
|
||
* rescan; runs only when a publish has flipped the dirty flag). */
|
||
if (g_sljit_tier_enabled && g_sljit_async && InterlockedExchange(&s_async_cache_dirty, 0)) {
|
||
scan_sljit_cache_dir();
|
||
head = idx_head(phys);
|
||
}
|
||
if (head < 0) {
|
||
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 && g_sljit_tier_enabled && (s_diff_mode || s_sljit_live) && !s_in_shadow &&
|
||
code_provider_sljit() != NULL) {
|
||
/* head < 0 == no gcc DLL (or any candidate) registered for this region
|
||
* after the cache-load attempt above => gcc-absent => sljit fills the
|
||
* gap (priority gcc > sljit > interp). */
|
||
if (g_sljit_async) {
|
||
/* OFF-THREAD: snapshot the region bytes + enqueue for the worker;
|
||
* interpret THIS pass. The shard registers on a later miss via the
|
||
* rescan above — no synchronous JIT spike on the dispatch thread. */
|
||
if (!sljit_already_tried(phys)) {
|
||
extern int dirty_ram_is_dirty(uint32_t phys);
|
||
uint8_t *ram = memory_get_ram_ptr();
|
||
if (ram && dirty_ram_is_dirty(phys)) { /* only JIT runtime code */
|
||
sljit_mark_tried(phys);
|
||
uint32_t snap = 8u * 1024u; /* SLJIT_MAX_FRAG_INSNS*4 */
|
||
if (phys + snap > 2u * 1024u * 1024u)
|
||
snap = 2u * 1024u * 1024u - phys;
|
||
uint32_t crc = crc32_update(0xFFFFFFFFu, ram + phys, snap)
|
||
^ 0xFFFFFFFFu;
|
||
overlay_compile_worker_enqueue(addr, phys, ram + phys, snap, crc);
|
||
}
|
||
}
|
||
} else {
|
||
try_sljit_region(addr); /* legacy synchronous path (PSX_SLJIT_SYNC=1) */
|
||
head = idx_head(phys);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* CPS (§25) continuation re-entry: phys is not an overlay ENTRY, but it may
|
||
* be a return point inside an already-registered overlay function (the
|
||
* caller's continuation after a tail-transferred call). Re-enter that
|
||
* function natively with cpu->pc = addr so its entry-switch routes to the
|
||
* continuation block — without this the continuation falls to the interp and
|
||
* native coverage stops at the first call. gcc-DLL candidates are the trusted
|
||
* tier (diff-validated at their entry; the continuation is the same DLL code),
|
||
* so they run native directly here; device-touch funcs still go to interp. */
|
||
if (head < 0 && g_psx_cps_mode) {
|
||
int ci = overlay_find_by_range(phys);
|
||
int _probe = (s_cps_probe_pc && phys == s_cps_probe_pc);
|
||
if (_probe) {
|
||
s_cps_probe_count++;
|
||
s_cps_probe_ci = ci;
|
||
s_cps_probe_ncand_inrange = overlay_count_by_range(phys);
|
||
s_cps_probe_found = (ci >= 0) ? s_cand[ci].addr : 0;
|
||
s_cps_probe_nrange = (ci >= 0) ? s_cand[ci].nranges : -1;
|
||
s_cps_probe_matched = -1;
|
||
s_cps_probe_outcome = (ci < 0) ? 0 : -1;
|
||
}
|
||
if (ci >= 0) {
|
||
Candidate *c = &s_cand[ci];
|
||
/* Generation-gated validation (overlay-cache v2 P2) — same contract
|
||
* as the main chain: skip the crc32 when no watched code page changed
|
||
* since this entry was last confirmed VALID. */
|
||
uint32_t gen = cand_gensum(c);
|
||
int matched;
|
||
if (c->state == ENTRY_VALID && gen == c->val_gen) {
|
||
matched = 1;
|
||
s_gen_fastpath++;
|
||
} else {
|
||
uint32_t live = cand_crc(c);
|
||
s_rehashes++;
|
||
s_last_crc = live;
|
||
c->val_gen = gen;
|
||
matched = (live == c->crc_code);
|
||
}
|
||
if (_probe) s_cps_probe_matched = matched;
|
||
if (matched) {
|
||
if (c->state != ENTRY_VALID) { c->state = ENTRY_VALID; s_valid_count++; }
|
||
if (c->device_touch) { if (_probe) s_cps_probe_outcome = 3; s_disp_interp++; return 0; }
|
||
if (!s_native_exec || overlay_native_blocked(c->addr) || overlay_native_blocked(addr))
|
||
{ if (_probe) s_cps_probe_outcome = 4; s_would_run_native++; s_disp_interp++; return 0; }
|
||
if (_probe) s_cps_probe_outcome = 2;
|
||
uint32_t slot = s_nring_pos++ & (NRING_CAP - 1u);
|
||
s_nring[slot].addr = addr;
|
||
s_nring[slot].crc = c->crc_code;
|
||
s_nring[slot].frame = (uint32_t)s_frame_count;
|
||
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++] = ci;
|
||
s_disp_native++;
|
||
cpu->pc = addr; /* route the func's entry-switch to the block */
|
||
c->fn(cpu);
|
||
overlay_post_dispatch_irq_pump(cpu);
|
||
if (s_active_depth > 0) s_active_depth--;
|
||
s_nring[slot].returned = 1;
|
||
s_native_inprogress = prev_inprogress;
|
||
if (g_native_bad_entry) { /* foreign interior entry: fail closed to interp */
|
||
g_native_bad_entry = 0;
|
||
s_disp_native--; s_disp_interp++;
|
||
return 0; /* cpu->pc was restored to the requested PC */
|
||
}
|
||
return 1;
|
||
}
|
||
if (_probe) s_cps_probe_outcome = 1;
|
||
/* stale code bytes: fall through to the interpreter */
|
||
}
|
||
}
|
||
|
||
for (int i = head; i >= 0; i = s_cand[i].next) {
|
||
Candidate *c = &s_cand[i];
|
||
if (c->state == ENTRY_BLACKLIST) continue;
|
||
|
||
/* Generation-gated validation (overlay-cache v2 P2). The ONLY way this
|
||
* entry's compiled code bytes can change is a write to one of its watched
|
||
* code pages — and every CPU/DMA store funnels through the single store
|
||
* chokepoint, which bumps overlay_page_gen for watched pages (memory.c).
|
||
* So if the page-generation sum is unchanged since we last confirmed this
|
||
* entry VALID, the bytes are PROVABLY unchanged and we run native without
|
||
* re-hashing. We fall back to the full crc32 only when the generation
|
||
* moved (or the entry isn't known-valid) — which also covers
|
||
* reload-on-return: a write-away-then-back bumps the gen, forcing a
|
||
* re-hash that re-confirms the byte-exact match before any native call.
|
||
* This removes the per-dispatch crc32 of the whole function body from the
|
||
* hot path for stable code (the warm-cache common case). Correctness is
|
||
* identical to the old unconditional hash; only redundant hashing is cut. */
|
||
uint32_t gen = cand_gensum(c);
|
||
int matched;
|
||
if (c->state == ENTRY_VALID && gen == c->val_gen) {
|
||
matched = 1; /* no watched write since validation */
|
||
s_gen_fastpath++;
|
||
} else {
|
||
uint32_t live = cand_crc(c);
|
||
s_rehashes++;
|
||
s_last_crc = live;
|
||
c->val_gen = gen;
|
||
matched = (live == c->crc_code);
|
||
}
|
||
if (matched) {
|
||
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. The per-function blocklist forces the same interp
|
||
* routing for one function only (bisection localization). */
|
||
if (!s_native_exec || overlay_native_blocked(c->addr))
|
||
{ 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].frame = (uint32_t)s_frame_count;
|
||
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);
|
||
overlay_post_dispatch_irq_pump(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) */
|
||
if (g_native_bad_entry) { /* foreign interior entry: fail closed to interp */
|
||
g_native_bad_entry = 0;
|
||
s_disp_native--; s_disp_interp++;
|
||
return 0; /* cpu->pc was restored to the requested PC */
|
||
}
|
||
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; }
|
||
|
||
/* Dispatches that ran native via the unchanged-page-generation fast path,
|
||
* i.e. skipped the per-dispatch code-range crc32 (overlay-cache v2 P2). High
|
||
* values relative to reval_attempts mean the warm-cache hot path is cheap. */
|
||
uint64_t overlay_loader_gen_fastpath(void) { return s_gen_fastpath; }
|
||
|
||
/* 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, 0);
|
||
persist_sljit_shard(addr & 0x1FFFFFFFu, out->code_lo, out->code_len,
|
||
out->serialized, out->serialized_size);
|
||
}
|
||
if (out->serialized) { overlay_sljit_free_serialized(out->serialized); out->serialized = NULL; }
|
||
}
|
||
|
||
/* ---- 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 void psx_dispatch_call(CPUState *cpu, uint32_t addr, uint32_t return_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);
|
||
/* CPS shards exit with cpu->pc set to the next tail target. Chain through
|
||
* the normal dispatcher to the original caller return, with s_native_exec=0
|
||
* above so nested overlay calls still run through the interpreter on both
|
||
* passes. dirty_ram_dispatch alone cannot follow clean/static BIOS targets
|
||
* and creates false shadow divergences for tail-transfer-heavy functions. */
|
||
{
|
||
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;
|
||
psx_dispatch_call(cpu, tv, stop_ra);
|
||
}
|
||
}
|
||
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
|
||
/* Skip the compiled game function if its page is dirty (an overlay overwrote it
|
||
* after the game-start baseline -> compiled code is stale); fall through to the
|
||
* native/interp paths which run the live overlay. Clean pages run compiled. */
|
||
if (!dirty_ram_is_dirty(target & 0x1FFFFFFFu)) {
|
||
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 entries kept in the diagnostic ring. */
|
||
int shown = 0;
|
||
for (uint32_t k = 0; k < NRING_CAP && n < cap - 140; 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\",\"frame\":%u,\"seq\":%llu,\"returned\":%d}",
|
||
shown ? "," : "", s_nring[idx].addr, s_nring[idx].crc, s_nring[idx].frame,
|
||
(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;
|
||
}
|