mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
4799 lines
212 KiB
C
4799 lines
212 KiB
C
#include "overlay_loader.h"
|
||
#include "overlay_api.h"
|
||
#include "overlay_path_canon.h"
|
||
#include "code_provider.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 "lockstep.h"
|
||
#include "overlay_posix.h"
|
||
#include <stdint.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <stdio.h>
|
||
#include <stdarg.h>
|
||
#include <ctype.h>
|
||
|
||
extern void overlay_watch_set_range(uint32_t phys, uint32_t len);
|
||
|
||
#ifdef _WIN32
|
||
# define WIN32_LEAN_AND_MEAN
|
||
# include <windows.h>
|
||
#else
|
||
# include <unistd.h>
|
||
#endif
|
||
|
||
#ifdef _WIN32
|
||
# define OVERLAY_SHARED_EXT ".dll"
|
||
# define OVERLAY_SHARED_EXT_LEN 4
|
||
#else
|
||
# define OVERLAY_SHARED_EXT ".so"
|
||
# define OVERLAY_SHARED_EXT_LEN 3
|
||
#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 *);
|
||
typedef void (*OverlayFlushFn)(void);
|
||
|
||
#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 */
|
||
uint8_t tier; /* gcc=2, tcc=1, unknown=0 */
|
||
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). */
|
||
#ifndef PSX_NO_DEBUG_TOOLS
|
||
uint32_t native_rank; /* first-seen identity rank for bisection */
|
||
#endif
|
||
} 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
|
||
|
||
/* MMX6's clean play-free cache now carries 17,506 on-disk manifest identities
|
||
* across observed byte variants, above the old 16,384 lifetime ceiling. Lazy
|
||
* loading means this is a potential long-session population, not proof that one
|
||
* observed run loaded all of them; every encountered variant does remain loaded.
|
||
* Keep enough headroom that coverage cannot silently become interpreter-only. */
|
||
/* Executable loader tests need a tiny table to prove alias-at-cap behavior.
|
||
* Keep the override private to this translation unit: changing overlay_api.h
|
||
* would roll the production shard codegen hash and its publisher contract. */
|
||
#ifdef PSX_OVERLAY_TEST_CANDIDATE_CAP
|
||
#define CAND_CAP PSX_OVERLAY_TEST_CANDIDATE_CAP
|
||
#else
|
||
#define CAND_CAP PSX_OVERLAY_CANDIDATE_CAP
|
||
#endif
|
||
static Candidate s_cand[CAND_CAP];
|
||
static int s_cand_n = 0;
|
||
|
||
/* CPS continuation lookup. A full candidate-table scan at every tail transfer
|
||
* scales catastrophically once a warmed cache contains hundreds of variant
|
||
* DLLs. Index candidates by the 4 KiB RAM pages touched by their code ranges;
|
||
* a continuation then examines only candidates that could contain its PC. */
|
||
#define RANGE_PAGE_COUNT (2u * 1024u * 1024u / 4096u)
|
||
#define RANGE_LINK_CAP (CAND_CAP * 8)
|
||
typedef struct { int cand, next; } RangeLink;
|
||
static int s_range_page_head[RANGE_PAGE_COUNT];
|
||
static int s_range_page_tail[RANGE_PAGE_COUNT];
|
||
static RangeLink s_range_links[RANGE_LINK_CAP];
|
||
static int s_range_link_n = 0;
|
||
static int s_range_index_overflow = 0;
|
||
|
||
/* Cache the selected owner of hot CPS continuation PCs. The cached candidate
|
||
* is still generation/CRC validated on every lookup; this only avoids walking
|
||
* hundreds of historical, currently-invalid owners in a reused region. */
|
||
#define RANGE_PC_CACHE_CAP 16384u
|
||
#define RANGE_PC_CACHE_MASK (RANGE_PC_CACHE_CAP - 1u)
|
||
typedef struct { uint32_t phys, generation; int cand; } RangePcCache;
|
||
static RangePcCache s_range_pc_cache[RANGE_PC_CACHE_CAP];
|
||
static uint32_t s_range_candidate_generation = 1u;
|
||
|
||
static void range_index_add_candidate(int ci) {
|
||
Candidate *c = &s_cand[ci];
|
||
uint8_t seen[RANGE_PAGE_COUNT / 8] = {0};
|
||
for (int r = 0; r < c->nranges; r++) {
|
||
uint32_t lo = c->range_lo[r];
|
||
uint32_t hi = lo + c->range_len[r] - 1u;
|
||
uint32_t p0 = lo >> 12, p1 = hi >> 12;
|
||
if (p0 >= RANGE_PAGE_COUNT) continue;
|
||
if (p1 >= RANGE_PAGE_COUNT) p1 = RANGE_PAGE_COUNT - 1u;
|
||
for (uint32_t p = p0; p <= p1; p++) {
|
||
uint8_t bit = (uint8_t)(1u << (p & 7u));
|
||
if (seen[p >> 3] & bit) continue;
|
||
seen[p >> 3] |= bit;
|
||
if (s_range_link_n >= RANGE_LINK_CAP) {
|
||
/* Never leave lookup correctness dependent on a partial index.
|
||
* overlay_find_by_range() falls back to the original ordered
|
||
* candidate scan if a future title exceeds this sizing guess. */
|
||
s_range_index_overflow = 1;
|
||
return;
|
||
}
|
||
int li = s_range_link_n++;
|
||
s_range_links[li].cand = ci;
|
||
s_range_links[li].next = -1;
|
||
if (s_range_page_head[p] < 0) s_range_page_head[p] = li;
|
||
else s_range_links[s_range_page_tail[p]].next = li;
|
||
s_range_page_tail[p] = li;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* RECURSION_BUG.md §25 — 1 when the build is continuation-passing (set by a
|
||
* constructor in the generated CPS dispatch). The overlay dispatch reads 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 (CAND_CAP * 2u)
|
||
#define IDX_MASK (IDX_CAP - 1u)
|
||
typedef struct { uint32_t addr; int head; } IdxSlot;
|
||
static IdxSlot s_idx[IDX_CAP];
|
||
/* Presence-only exact-entry index for the interpreter's common local-transfer
|
||
* negative check. Full byte/bundle validation remains in dispatch. */
|
||
static uint32_t s_exact_entry_bitmap[DIRTY_RAM_EXEC_BITMAP_WORDS];
|
||
|
||
static void exact_entry_set(uint32_t phys) {
|
||
phys &= 0x1FFFFFFFu;
|
||
if (phys < 2u * 1024u * 1024u && (phys & 3u) == 0u) {
|
||
uint32_t word = phys >> 2;
|
||
s_exact_entry_bitmap[word >> 5] |= 1u << (word & 31u);
|
||
}
|
||
}
|
||
|
||
static int exact_entry_has(uint32_t phys) {
|
||
phys &= 0x1FFFFFFFu;
|
||
if (phys >= 2u * 1024u * 1024u || (phys & 3u) != 0u) return 0;
|
||
uint32_t word = phys >> 2;
|
||
return (s_exact_entry_bitmap[word >> 5] >> (word & 31u)) & 1u;
|
||
}
|
||
|
||
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 */
|
||
/* Sampler accessor (phase_profile hot-function histogram): the innermost native
|
||
* candidate's registered entry. Read off-thread; a torn read is harmless. */
|
||
uint32_t overlay_loader_native_inprogress(void) { return s_native_inprogress; }
|
||
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;
|
||
/* Optional diagnostic filter: when non-zero, shadow-diff only this candidate
|
||
* entry while every other validated candidate follows its normal live route. */
|
||
static uint32_t s_diff_addr = 0;
|
||
static int s_in_shadow = 0;
|
||
/* Candidate whose shadow NATIVE pass is currently executing (NULL outside it).
|
||
* Set ONLY around run_shadow_diff's native pass — never during the interp pass,
|
||
* which must stay pure interp. Lets the CPS continuation re-entry path below
|
||
* run THIS candidate's own interiors natively inside its shadow run (so the
|
||
* diff exercises the whole function, not just the first CPS segment), while
|
||
* every other dispatch inside the shadow stays on the interpreter. */
|
||
static const void *s_shadow_cand = NULL;
|
||
/* Longjmp-escape hardening. run_shadow_diff saves the pre-shadow values of
|
||
* s_native_exec / s_suppress_irq here (in addition to its locals) so that
|
||
* overlay_loader_shadow_escape_fixup() can restore them if an exception
|
||
* longjmp ever unwinds through a live shadow frame (should be impossible —
|
||
* the dispatch gate refuses to start a shadow while in an exception dispatch —
|
||
* but an escape must fail LOUD and self-heal, not silently kill the
|
||
* instrument). s_shadow_epoch is the exception-setjmp epoch at shadow start:
|
||
* a longjmp targeting a frame with epoch <= s_shadow_epoch escapes the shadow;
|
||
* a larger epoch is an exception armed INSIDE the shadow (contained). */
|
||
static int s_shadow_saved_native_exec = 0;
|
||
static int s_shadow_saved_supp = 0;
|
||
static int s_shadow_saved_mmio_watch = 0;
|
||
static int s_shadow_saved_exec_phase = 0;
|
||
static int s_shadow_scheduler_bail = 0;
|
||
static int s_shadow_saved_call_bail = 0;
|
||
static uint32_t s_shadow_saved_debug_func = 0;
|
||
static uint32_t s_shadow_saved_irq_calls = 0;
|
||
static uint64_t s_shadow_saved_irq_supp = 0;
|
||
static uint64_t s_shadow_epoch = 0;
|
||
static uint32_t s_shadow_escapes = 0;
|
||
static uint32_t s_shadow_escapes_native = 0;
|
||
/* (sljit removed 2026-07-15: the deprecated in-process JIT tier and its
|
||
* live-execution mode are gone; overlay gaps fall to the interpreter.) */
|
||
static void run_shadow_diff(CPUState *cpu, Candidate *c, uint32_t addr);
|
||
|
||
/* A real interpreter-pass ChangeThread is authoritative and escapes before a
|
||
* native validation pass starts. Therefore a scheduler switch attempted while
|
||
* s_shadow_cand is armed is necessarily native-only divergence. Traps calls
|
||
* this before mutating TCB/RAM or longjmping; force the speculative call to
|
||
* unwind normally so run_shadow_diff can restore the interpreter snapshot. */
|
||
int overlay_loader_shadow_native_thread_switch_bail(void) {
|
||
if (!s_in_shadow || s_shadow_cand == NULL) return 0;
|
||
s_shadow_scheduler_bail = 1;
|
||
g_psx_call_bail = 1;
|
||
return 1;
|
||
}
|
||
|
||
static int cand_range_contains(const Candidate *c, uint32_t phys) {
|
||
for (int r = 0; r < c->nranges; r++)
|
||
if (phys >= c->range_lo[r] && phys < c->range_lo[r] + c->range_len[r])
|
||
return 1;
|
||
return 0;
|
||
}
|
||
|
||
static int cand_ranges_overlap(const Candidate *a, const Candidate *b) {
|
||
for (int ar = 0; ar < a->nranges; ar++) {
|
||
uint32_t alo = a->range_lo[ar], ahi = alo + a->range_len[ar];
|
||
for (int br = 0; br < b->nranges; br++) {
|
||
uint32_t blo = b->range_lo[br], bhi = blo + b->range_len[br];
|
||
if (alo < bhi && blo < ahi) return 1;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* An explicit entry filter owns its whole code envelope, including separately
|
||
* seeded continuation candidates. This prevents an IRQ resume at a mid-function
|
||
* PC from bypassing the diagnostic quarantine and running native live. The scan
|
||
* is intentionally diagnostic-only (s_diff_addr != 0). */
|
||
static int cand_selected_for_diff(const Candidate *c) {
|
||
if (!s_diff_addr) return 1;
|
||
uint32_t target = s_diff_addr & 0x1FFFFFFFu;
|
||
if (c->addr == target || cand_range_contains(c, target)) return 1;
|
||
for (int i = 0; i < s_cand_n; i++) {
|
||
const Candidate *owner = &s_cand[i];
|
||
if (owner->addr == target && cand_ranges_overlap(c, owner)) return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* ---- Load freeze (rollback resim / selfcheck) --------------------------
|
||
* Overlay DLL registration is host-only and not part of boot_state. A lazy
|
||
* load mid-resim-pass#1 that is visible at the start of pass#2 changes
|
||
* native-vs-interp BB boundaries and forks MotK wait-loop GPRs at matched
|
||
* guest clocks. */
|
||
static int s_load_freeze = 0;
|
||
|
||
void overlay_loader_set_load_freeze(int freeze)
|
||
{
|
||
s_load_freeze = freeze ? 1 : 0;
|
||
}
|
||
int overlay_loader_load_frozen(void) { return s_load_freeze; }
|
||
|
||
static int overlay_loads_allowed(void)
|
||
{
|
||
if (s_load_freeze)
|
||
return 0;
|
||
{
|
||
extern int psx_netplay_is_resimulating(void);
|
||
if (psx_netplay_is_resimulating())
|
||
return 0;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* ---- Counters (surfaced via overlay_loader_status) --------------------- */
|
||
static int s_ndlls = 0; /* DLLs LoadLibrary'd */
|
||
static uint64_t s_load_total_us = 0;
|
||
static uint64_t s_load_max_us = 0;
|
||
static uint64_t s_load_last_us = 0;
|
||
static int s_valid_count = 0; /* candidates currently VALID */
|
||
static uint64_t s_disp_native = 0;
|
||
static uint64_t s_disp_interp = 0;
|
||
/* Small diagnostic-only native-owner sampler. A logical softlock can continue
|
||
* presenting at 60 Hz while executing a tiny bad native loop, so FPS alone is
|
||
* not correctness evidence. This is enabled only with the existing runtime
|
||
* perf environment switch and is drained once per telemetry interval. */
|
||
#define NATIVE_HOT_CAP 256u
|
||
typedef struct { uint32_t pc; uint64_t calls; } NativeHot;
|
||
static NativeHot s_native_hot[NATIVE_HOT_CAP];
|
||
static int s_native_hot_enabled = 0;
|
||
|
||
#ifndef PSX_NO_DEBUG_TOOLS
|
||
/* Correctness localization without speculative/double guest execution. When
|
||
* PSX_NATIVE_RANK_LIMIT is present, each concrete loaded candidate is assigned
|
||
* a first-seen rank. Candidates through the limit execute normally; later ones
|
||
* fall back to the authoritative interpreter. Binary-searching the limit finds
|
||
* the first candidate identity required for a deterministic softlock while
|
||
* preserving exactly one guest execution path per run. Debug builds only. */
|
||
static uint32_t s_native_rank_limit = UINT32_MAX;
|
||
static uint32_t s_native_rank_next = 0;
|
||
static uint64_t s_native_rank_blocked = 0;
|
||
#endif
|
||
|
||
static void native_hot_note(uint32_t pc) {
|
||
if (!s_native_hot_enabled) return;
|
||
uint32_t slot = ((pc >> 2) * 2654435761u) & (NATIVE_HOT_CAP - 1u);
|
||
NativeHot *h = &s_native_hot[slot];
|
||
if (h->pc != pc) { h->pc = pc; h->calls = 0; }
|
||
h->calls++;
|
||
}
|
||
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 uint64_t s_diffgate_interp = 0; /* CPS interior re-entries sent to the */
|
||
/* interp because their candidate is */
|
||
/* still inside the diff verify budget */
|
||
static uint32_t s_last_crc = 0;
|
||
static uint32_t s_no_manifest = 0; /* exports skipped (no manifest range)*/
|
||
static uint64_t s_cand_overflow = 0; /* registrations dropped at CAND_CAP */
|
||
static uint64_t s_pair_aliases = 0; /* validated complete pairs deduped */
|
||
static uint32_t s_selfmod = 0; /* entries blacklisted (self-mod) */
|
||
static uint32_t s_last_write_pc = 0;
|
||
static uint32_t s_last_write_addr = 0;
|
||
static uint32_t s_last_write_size = 0;
|
||
|
||
/* ABI v11 cycle batching. Each DLL owns a small pending-cycle accumulator.
|
||
* While its code is active, memory stores ask this export to commit cycles
|
||
* before the guest-visible write; block/device boundaries flush through the
|
||
* DLL glue directly. */
|
||
static OverlayFlushFn s_dll_flush[4096];
|
||
OverlayFlushFn g_overlay_flush_pending_cycles = NULL;
|
||
|
||
static OverlayFlushFn overlay_flush_enter(const Candidate *c) {
|
||
OverlayFlushFn prev = g_overlay_flush_pending_cycles;
|
||
if (prev) prev();
|
||
g_overlay_flush_pending_cycles =
|
||
(c->dll >= 0 && c->dll < (int)(sizeof(s_dll_flush) / sizeof(s_dll_flush[0])))
|
||
? s_dll_flush[c->dll] : NULL;
|
||
return prev;
|
||
}
|
||
|
||
static void overlay_flush_leave(OverlayFlushFn prev) {
|
||
if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles();
|
||
g_overlay_flush_pending_cycles = prev;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
#ifdef PSX_HAS_OVERLAY_DISPATCH
|
||
/* Static-overlay validation uses the same exact-code-range contract as the
|
||
* dynamic DLL loader. Generated code passes immutable {phys_lo, len} pairs and
|
||
* the CRC of the bytes it was compiled from. A page-generation cache keeps the
|
||
* hot path O(number of ranges), without re-hashing unchanged code each call. */
|
||
#define STATIC_MATCH_CACHE_CAP 4096u
|
||
typedef struct {
|
||
const uint32_t *ranges;
|
||
uint32_t count;
|
||
uint32_t expected_crc;
|
||
uint32_t gen_sum;
|
||
int matches;
|
||
} StaticMatchCache;
|
||
|
||
static StaticMatchCache s_static_match_cache[STATIC_MATCH_CACHE_CAP];
|
||
static uint64_t s_static_match_rehashes = 0;
|
||
static uint64_t s_static_match_crc_misses = 0;
|
||
static uint64_t s_static_match_gen_fastpath = 0;
|
||
|
||
int psx_overlay_static_code_matches(const uint32_t *lo_len_pairs,
|
||
uint32_t count,
|
||
uint32_t expected_crc) {
|
||
const uint8_t *ram = memory_get_ram_ptr();
|
||
if (!ram || !lo_len_pairs || count == 0u || count > 4096u) {
|
||
s_static_match_crc_misses++;
|
||
return 0;
|
||
}
|
||
|
||
uint32_t gen_sum = 0;
|
||
for (uint32_t i = 0; i < count; i++) {
|
||
uint32_t lo = lo_len_pairs[i * 2u] & 0x1FFFFFFFu;
|
||
uint32_t len = lo_len_pairs[i * 2u + 1u];
|
||
if (len == 0u || lo >= 2u * 1024u * 1024u ||
|
||
len > 2u * 1024u * 1024u - lo) {
|
||
s_static_match_crc_misses++;
|
||
return 0;
|
||
}
|
||
gen_sum += overlay_watch_pagegen_sum(lo, len);
|
||
}
|
||
|
||
uintptr_t raw = (uintptr_t)lo_len_pairs;
|
||
uint32_t slot = (uint32_t)(((raw >> 4) ^ (raw >> 19) ^ expected_crc ^
|
||
(count * 0x9E3779B9u)) &
|
||
(STATIC_MATCH_CACHE_CAP - 1u));
|
||
StaticMatchCache *entry = NULL;
|
||
for (uint32_t probe = 0; probe < STATIC_MATCH_CACHE_CAP; probe++) {
|
||
StaticMatchCache *candidate =
|
||
&s_static_match_cache[(slot + probe) & (STATIC_MATCH_CACHE_CAP - 1u)];
|
||
if (!candidate->ranges ||
|
||
(candidate->ranges == lo_len_pairs &&
|
||
candidate->count == count &&
|
||
candidate->expected_crc == expected_crc)) {
|
||
entry = candidate;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (entry && entry->ranges && entry->gen_sum == gen_sum) {
|
||
s_static_match_gen_fastpath++;
|
||
return entry->matches;
|
||
}
|
||
|
||
uint32_t crc = 0xFFFFFFFFu;
|
||
for (uint32_t i = 0; i < count; i++) {
|
||
uint32_t lo = lo_len_pairs[i * 2u] & 0x1FFFFFFFu;
|
||
uint32_t len = lo_len_pairs[i * 2u + 1u];
|
||
crc = crc32_update(crc, ram + lo, len);
|
||
}
|
||
crc ^= 0xFFFFFFFFu;
|
||
int matches = (crc == expected_crc) ? 1 : 0;
|
||
s_static_match_rehashes++;
|
||
if (!matches) s_static_match_crc_misses++;
|
||
|
||
if (entry) {
|
||
entry->ranges = lo_len_pairs;
|
||
entry->count = count;
|
||
entry->expected_crc = expected_crc;
|
||
entry->gen_sum = gen_sum;
|
||
entry->matches = matches;
|
||
}
|
||
return matches;
|
||
}
|
||
|
||
void overlay_loader_static_match_stats(uint64_t *rehashes,
|
||
uint64_t *crc_misses,
|
||
uint64_t *gen_fastpath) {
|
||
if (rehashes) *rehashes = s_static_match_rehashes;
|
||
if (crc_misses) *crc_misses = s_static_match_crc_misses;
|
||
if (gen_fastpath) *gen_fastpath = s_static_match_gen_fastpath;
|
||
}
|
||
#endif
|
||
|
||
/* ---- Per-DLL code-range manifest --------------------------------------- */
|
||
/* Strict v2 line format emitted by tools/compile_overlays.py beside each DLL:
|
||
* P <pair_id_hex> optional DLL/manifest publication identity
|
||
* F <entry_hex> <crc_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; /* parser validity bit; authoritative CRC is required */
|
||
uint32_t lo[MAX_CODE_RANGES];
|
||
uint32_t len[MAX_CODE_RANGES];
|
||
int n;
|
||
} ManFn;
|
||
|
||
#define OVERLAY_RAM_SIZE (2u * 1024u * 1024u)
|
||
#define MANIFEST_LINE_MAX 128u
|
||
#define MANIFEST_PHYSICAL_LINE_MAX 159u
|
||
#define MANIFEST_PROVENANCE_PREFIX "# psxrecomp overlay provenance "
|
||
enum {
|
||
MANIFEST_PROVENANCE_AMBIGUOUS = -1,
|
||
MANIFEST_PROVENANCE_AUTHORITY = 0,
|
||
MANIFEST_PROVENANCE_HOSTED = 1,
|
||
MANIFEST_PROVENANCE_ORPHAN = 2
|
||
};
|
||
|
||
static int man_structurally_valid(const ManFn *m) {
|
||
if (!m || !m->has_crc || m->n < 1 || m->n > MAX_CODE_RANGES)
|
||
return 0;
|
||
if ((m->entry & 0xFFE00000u) != 0x80000000u) return 0;
|
||
uint32_t entry = m->entry & 0x1FFFFFFFu;
|
||
if ((entry & 3u) != 0u || entry >= OVERLAY_RAM_SIZE) return 0;
|
||
int entry_covered = 0;
|
||
for (int r = 0; r < m->n; r++) {
|
||
if ((m->lo[r] & 0xFFE00000u) != 0x80000000u) return 0;
|
||
uint32_t lo = m->lo[r] & 0x1FFFFFFFu;
|
||
uint32_t len = m->len[r];
|
||
if ((lo & 3u) != 0u || (len & 3u) != 0u || len < 4u ||
|
||
lo >= OVERLAY_RAM_SIZE || len > OVERLAY_RAM_SIZE - lo)
|
||
return 0;
|
||
if (entry >= lo && entry - lo <= len - 4u) entry_covered = 1;
|
||
for (int p = 0; p < r; p++) {
|
||
uint32_t prev_lo = m->lo[p] & 0x1FFFFFFFu;
|
||
uint32_t prev_hi = prev_lo + m->len[p];
|
||
uint32_t hi = lo + len;
|
||
if (lo < prev_hi && prev_lo < hi) return 0;
|
||
}
|
||
}
|
||
return entry_covered;
|
||
}
|
||
|
||
static int manifest_is_space(unsigned char c) {
|
||
return c == ' ' || c == '\t' || c == '\r' || c == '\v' || c == '\f';
|
||
}
|
||
|
||
static const char *manifest_skip_space(const char *p) {
|
||
while (*p && manifest_is_space((unsigned char)*p)) p++;
|
||
return p;
|
||
}
|
||
|
||
static int manifest_hex_field(const char **cursor, int max_digits,
|
||
uint64_t *out) {
|
||
const char *p = manifest_skip_space(*cursor);
|
||
uint64_t value = 0;
|
||
int digits = 0;
|
||
while (isxdigit((unsigned char)*p)) {
|
||
int nibble;
|
||
if (*p >= '0' && *p <= '9') nibble = *p - '0';
|
||
else if (*p >= 'a' && *p <= 'f') nibble = *p - 'a' + 10;
|
||
else nibble = *p - 'A' + 10;
|
||
if (++digits > max_digits) return 0;
|
||
value = (value << 4) | (uint64_t)nibble;
|
||
p++;
|
||
}
|
||
if (digits == 0 || (*p && !manifest_is_space((unsigned char)*p))) return 0;
|
||
*cursor = p;
|
||
*out = value;
|
||
return 1;
|
||
}
|
||
|
||
static int manifest_record_end(const char *cursor) {
|
||
return *manifest_skip_space(cursor) == '\0';
|
||
}
|
||
|
||
static ManFn *parse_manifest(const char *path, int *out_n,
|
||
uint64_t *out_pair_id,
|
||
int *out_has_pair_id,
|
||
int *out_provenance) {
|
||
*out_n = 0;
|
||
if (out_pair_id) *out_pair_id = 0;
|
||
if (out_has_pair_id) *out_has_pair_id = 0;
|
||
if (out_provenance) *out_provenance = MANIFEST_PROVENANCE_AUTHORITY;
|
||
FILE *f = fopen(path, "rb");
|
||
if (!f) return NULL;
|
||
int cap = 1024, n = 0;
|
||
int invalid = 0, pair_seen = 0, provenance_seen = 0;
|
||
int provenance = MANIFEST_PROVENANCE_AUTHORITY;
|
||
ManFn *arr = (ManFn *)malloc(sizeof(ManFn) * cap);
|
||
if (!arr) { fclose(f); return NULL; }
|
||
char line[MANIFEST_PHYSICAL_LINE_MAX + 1u];
|
||
ManFn *cur = NULL;
|
||
for (;;) {
|
||
size_t line_len = 0;
|
||
int ch = 0, too_long = 0;
|
||
while ((ch = fgetc(f)) != '\n' && ch != EOF) {
|
||
if (line_len + 1 < sizeof(line)) line[line_len++] = (char)ch;
|
||
else too_long = 1;
|
||
}
|
||
if (ch == EOF && line_len == 0 && !too_long) break;
|
||
line[line_len] = '\0';
|
||
size_t semantic_len = line_len;
|
||
if (semantic_len > 0 && line[semantic_len - 1] == '\r')
|
||
semantic_len--;
|
||
size_t provenance_prefix_len = strlen(MANIFEST_PROVENANCE_PREFIX);
|
||
if (semantic_len >= provenance_prefix_len &&
|
||
memcmp(line, MANIFEST_PROVENANCE_PREFIX,
|
||
provenance_prefix_len) == 0) {
|
||
const char *value = line + provenance_prefix_len;
|
||
size_t value_len = semantic_len - provenance_prefix_len;
|
||
int parsed_provenance = MANIFEST_PROVENANCE_AMBIGUOUS;
|
||
if (value_len == strlen("hosted-v1") &&
|
||
memcmp(value, "hosted-v1", value_len) == 0)
|
||
parsed_provenance = MANIFEST_PROVENANCE_HOSTED;
|
||
else if (value_len == strlen("orphan-v1") &&
|
||
memcmp(value, "orphan-v1", value_len) == 0)
|
||
parsed_provenance = MANIFEST_PROVENANCE_ORPHAN;
|
||
provenance_seen++;
|
||
provenance = provenance_seen == 1
|
||
? parsed_provenance : MANIFEST_PROVENANCE_AMBIGUOUS;
|
||
}
|
||
const char *record = manifest_skip_space(line);
|
||
int recognized = ((*record == 'P' || *record == 'F' || *record == 'R') &&
|
||
(record[1] == '\0' ||
|
||
manifest_is_space((unsigned char)record[1])));
|
||
int embedded_nul = memchr(line, '\0', line_len) != NULL;
|
||
int non_ascii = 0;
|
||
for (size_t i = 0; i < line_len; i++)
|
||
if ((unsigned char)line[i] >= 0x80u) { non_ascii = 1; break; }
|
||
size_t record_width = 0;
|
||
while (record_width < line_len && line[record_width] != '\r')
|
||
record_width++;
|
||
if (too_long || embedded_nul || non_ascii ||
|
||
record_width > MANIFEST_LINE_MAX) {
|
||
invalid = 1;
|
||
continue;
|
||
}
|
||
if (!recognized) continue;
|
||
if (*record == 'P') {
|
||
const char *cursor = record + 1;
|
||
uint64_t parsed = 0;
|
||
if (manifest_hex_field(&cursor, 16, &parsed) &&
|
||
manifest_record_end(cursor)) {
|
||
if (pair_seen) invalid = 1;
|
||
pair_seen = 1;
|
||
if (out_pair_id) *out_pair_id = parsed;
|
||
if (out_has_pair_id) *out_has_pair_id = 1;
|
||
} else invalid = 1;
|
||
} else if (*record == 'F') {
|
||
const char *cursor = record + 1;
|
||
uint64_t parsed_e = 0, parsed_crc = 0;
|
||
if (manifest_hex_field(&cursor, 8, &parsed_e) &&
|
||
manifest_hex_field(&cursor, 8, &parsed_crc) &&
|
||
manifest_record_end(cursor)) {
|
||
if (n >= cap) {
|
||
cap *= 2;
|
||
ManFn *na = (ManFn *)realloc(arr, sizeof(ManFn) * cap);
|
||
if (!na) { invalid = 1; break; }
|
||
arr = na;
|
||
}
|
||
cur = &arr[n++];
|
||
cur->entry = ((uint32_t)parsed_e & 0x1FFFFFFFu) | 0x80000000u;
|
||
cur->crc = (uint32_t)parsed_crc;
|
||
cur->has_crc = 1;
|
||
cur->n = 0;
|
||
} else invalid = 1;
|
||
} else if (*record == 'R' && cur) {
|
||
const char *cursor = record + 1;
|
||
uint64_t parsed_lo = 0, parsed_len = 0;
|
||
if (manifest_hex_field(&cursor, 8, &parsed_lo) &&
|
||
manifest_hex_field(&cursor, 8, &parsed_len) &&
|
||
manifest_record_end(cursor) &&
|
||
cur->n < MAX_CODE_RANGES && parsed_len > 0) {
|
||
cur->lo[cur->n] = (uint32_t)parsed_lo;
|
||
cur->len[cur->n] = (uint32_t)parsed_len;
|
||
cur->n++;
|
||
} else invalid = 1;
|
||
}
|
||
}
|
||
if (!invalid && (n == 0 || !cur || !man_structurally_valid(cur))) invalid = 1;
|
||
fclose(f);
|
||
for (int i = 0; i < n; i++) {
|
||
uint32_t entry = arr[i].entry & 0x1FFFFFFFu;
|
||
int contains_entry = 0;
|
||
if (!arr[i].has_crc || arr[i].n <= 0) invalid = 1;
|
||
for (int r = 0; r < arr[i].n; r++) {
|
||
uint32_t lo = arr[i].lo[r] & 0x1FFFFFFFu;
|
||
uint64_t hi = (uint64_t)lo + arr[i].len[r];
|
||
if (lo >= OVERLAY_RAM_SIZE ||
|
||
arr[i].len[r] > OVERLAY_RAM_SIZE - lo)
|
||
invalid = 1;
|
||
if ((uint64_t)entry >= lo && (uint64_t)entry < hi)
|
||
contains_entry = 1;
|
||
}
|
||
if (!contains_entry) invalid = 1;
|
||
for (int j = 0; j < i; j++)
|
||
if ((arr[j].entry & 0x1FFFFFFFu) == entry) invalid = 1;
|
||
}
|
||
if (invalid || n == 0) {
|
||
free(arr);
|
||
return NULL;
|
||
}
|
||
if (out_provenance) *out_provenance = provenance;
|
||
*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;
|
||
}
|
||
|
||
/* 1 = supported R3000A control transfer with a delay slot, 0 = ordinary
|
||
* instruction, -1 = reserved/unsupported branch encoding that native shards
|
||
* must never claim. */
|
||
static int mips_control_kind(uint32_t instr) {
|
||
uint32_t op = instr >> 26;
|
||
if (op == 0u) {
|
||
uint32_t funct = instr & 0x3Fu;
|
||
return funct == 0x08u || funct == 0x09u; /* JR / JALR */
|
||
}
|
||
if (op == 0x01u) { /* REGIMM */
|
||
uint32_t rt = (instr >> 16) & 0x1Fu;
|
||
return (rt == 0x00u || rt == 0x01u ||
|
||
rt == 0x10u || rt == 0x11u) ? 1 : -1;
|
||
}
|
||
if (op == 0x02u || op == 0x03u ||
|
||
(op >= 0x04u && op <= 0x07u)) /* J/JAL/branches */
|
||
return 1;
|
||
if (op >= 0x14u && op <= 0x17u) return -1; /* branch-likely: reserved */
|
||
if (op >= 0x10u && op <= 0x13u &&
|
||
((instr >> 21) & 0x1Fu) == 0x08u) return -1; /* unsupported BCz* */
|
||
return 0;
|
||
}
|
||
|
||
static int ranges_contain_word(const uint32_t *lo_list,
|
||
const uint32_t *len_list, int n,
|
||
uint32_t phys) {
|
||
if ((phys & 3u) != 0u || phys > (2u * 1024u * 1024u) - 4u) return 0;
|
||
for (int r = 0; r < n; r++) {
|
||
uint32_t lo = lo_list[r] & 0x1FFFFFFFu;
|
||
uint32_t len = len_list[r];
|
||
if (len >= 4u && phys >= lo && phys - lo <= len - 4u) return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* Defense in depth for caches generated before guarded capture/codegen. Called
|
||
* only after the range CRC matches. Scan every hashed instruction so a missing
|
||
* slot at any range edge, an unsupported branch form, or a nested transfer in a
|
||
* delay slot rejects the whole native identity. */
|
||
static int ranges_delay_slots_hashed(const uint32_t *lo_list,
|
||
const uint32_t *len_list, int n) {
|
||
const uint8_t *ram = memory_get_ram_ptr();
|
||
const uint32_t ram_size = 2u * 1024u * 1024u;
|
||
if (!ram || n < 1 || n > MAX_CODE_RANGES) return 0;
|
||
for (int r = 0; r < n; r++) {
|
||
uint32_t lo = lo_list[r] & 0x1FFFFFFFu;
|
||
uint32_t len = len_list[r];
|
||
if ((lo & 3u) != 0u || (len & 3u) != 0u || len < 4u ||
|
||
lo >= ram_size || len > ram_size - lo)
|
||
return 0;
|
||
for (uint32_t pc = lo; pc < lo + len; pc += 4u) {
|
||
uint32_t instr = (uint32_t)ram[pc] |
|
||
((uint32_t)ram[pc + 1u] << 8) |
|
||
((uint32_t)ram[pc + 2u] << 16) |
|
||
((uint32_t)ram[pc + 3u] << 24);
|
||
int kind = mips_control_kind(instr);
|
||
if (kind < 0) return 0;
|
||
if (kind > 0) {
|
||
uint32_t slot = pc + 4u;
|
||
if (!ranges_contain_word(lo_list, len_list, n, slot)) return 0;
|
||
uint32_t slot_instr = (uint32_t)ram[slot] |
|
||
((uint32_t)ram[slot + 1u] << 8) |
|
||
((uint32_t)ram[slot + 2u] << 16) |
|
||
((uint32_t)ram[slot + 3u] << 24);
|
||
if (mips_control_kind(slot_instr) != 0) return 0;
|
||
}
|
||
}
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* Complete-pair deduplication. A generated P record binds the DLL source,
|
||
* manifest provenance, and normalized F/R records. The pair id alone is not
|
||
* enough for aliasing: retain the canonical parsed manifest and require exact
|
||
* semantic equality as collision defense. Compiler tiers remain separate so
|
||
* a gcc image never silently stands in for a tcc image (or vice versa).
|
||
*
|
||
* Only a fully registered pair enters this table. Its DLL handle, function
|
||
* pointers, and cycle-flush callback remain the sole owners for process life;
|
||
* a later identical physical cache pair is preflighted, closed, and recorded
|
||
* as a satisfied path without consuming Candidate slots. */
|
||
#define LOADED_PAIR_CAP 4096
|
||
#define LOAD_PAIR_ALIAS (-1)
|
||
typedef struct {
|
||
uint64_t pair_id;
|
||
ManFn *man;
|
||
int man_n;
|
||
int tier;
|
||
int provenance;
|
||
} LoadedPair;
|
||
static LoadedPair s_loaded_pairs[LOADED_PAIR_CAP];
|
||
static int s_loaded_pair_n = 0;
|
||
|
||
static int manifest_pair_equal(const ManFn *a, int a_n,
|
||
const ManFn *b, int b_n) {
|
||
if (!a || !b || a_n != b_n) return 0;
|
||
for (int i = 0; i < a_n; i++) {
|
||
if (a[i].entry != b[i].entry || a[i].crc != b[i].crc ||
|
||
a[i].has_crc != b[i].has_crc || a[i].n != b[i].n)
|
||
return 0;
|
||
for (int r = 0; r < a[i].n; r++)
|
||
if ((a[i].lo[r] & 0x1FFFFFFFu) !=
|
||
(b[i].lo[r] & 0x1FFFFFFFu) ||
|
||
a[i].len[r] != b[i].len[r])
|
||
return 0;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
static int man_delay_slots_hashed(const ManFn *m) {
|
||
return ranges_delay_slots_hashed(m->lo, m->len, m->n);
|
||
}
|
||
|
||
static int cand_delay_slots_hashed(const Candidate *c) {
|
||
return ranges_delay_slots_hashed(c->range_lo, c->range_len, c->nranges);
|
||
}
|
||
|
||
static int loaded_pair_find(int tier, int provenance, uint64_t pair_id,
|
||
const ManFn *man, int man_n) {
|
||
if (provenance == MANIFEST_PROVENANCE_AMBIGUOUS) return -1;
|
||
for (int i = 0; i < s_loaded_pair_n; i++) {
|
||
const LoadedPair *p = &s_loaded_pairs[i];
|
||
if (p->tier == tier && p->provenance == provenance &&
|
||
p->pair_id == pair_id &&
|
||
manifest_pair_equal(p->man, p->man_n, man, man_n))
|
||
return i;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
static void loaded_pair_commit(int tier, int provenance, uint64_t pair_id,
|
||
const ManFn *man, int man_n) {
|
||
if (!man || man_n <= 0 ||
|
||
provenance == MANIFEST_PROVENANCE_AMBIGUOUS ||
|
||
s_loaded_pair_n >= LOADED_PAIR_CAP ||
|
||
loaded_pair_find(tier, provenance, pair_id, man, man_n) >= 0)
|
||
return;
|
||
ManFn *copy = (ManFn *)malloc(sizeof(*copy) * (size_t)man_n);
|
||
if (!copy) return; /* Safe fallback: later twins register independently. */
|
||
memcpy(copy, man, sizeof(*copy) * (size_t)man_n);
|
||
LoadedPair *p = &s_loaded_pairs[s_loaded_pair_n++];
|
||
p->pair_id = pair_id;
|
||
p->man = copy;
|
||
p->man_n = man_n;
|
||
p->tier = tier;
|
||
p->provenance = provenance;
|
||
}
|
||
|
||
/* ---- Candidate registration -------------------------------------------- */
|
||
|
||
static void loader_log(const char *fmt, ...); /* defined below */
|
||
static int cand_register(uint32_t phys, OverlayFn fn, const ManFn *m, int dll,
|
||
int tier) {
|
||
if (s_cand_n >= CAND_CAP) {
|
||
s_cand_overflow++;
|
||
return 0;
|
||
}
|
||
if (!man_structurally_valid(m)) return 0;
|
||
int idx = s_cand_n++;
|
||
Candidate *c = &s_cand[idx];
|
||
c->addr = phys;
|
||
c->fn = fn;
|
||
c->dll = dll;
|
||
c->tier = (uint8_t)tier;
|
||
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. */
|
||
c->crc_code = m->crc;
|
||
c->val_gen = cand_gensum(c);
|
||
c->state = (cand_crc(c) == c->crc_code && cand_delay_slots_hashed(c))
|
||
? ENTRY_VALID : ENTRY_INVALID;
|
||
/* Keep higher-priority compiler tiers first without discarding additive
|
||
* artifacts. New same-tier repairs precede older ones; lower-tier TCC
|
||
* remains available if every GCC candidate fails live-byte validation. */
|
||
int head = idx_head(phys);
|
||
if (head < 0 || s_cand[head].tier <= c->tier) {
|
||
c->next = head;
|
||
idx_set_head(phys, idx);
|
||
} else {
|
||
int prev = head;
|
||
while (s_cand[prev].next >= 0 &&
|
||
s_cand[s_cand[prev].next].tier > c->tier)
|
||
prev = s_cand[prev].next;
|
||
c->next = s_cand[prev].next;
|
||
s_cand[prev].next = idx;
|
||
}
|
||
exact_entry_set(phys);
|
||
range_index_add_candidate(idx);
|
||
s_range_candidate_generation++;
|
||
if (s_range_candidate_generation == 0u) s_range_candidate_generation = 1u;
|
||
if (c->state == ENTRY_VALID) s_valid_count++;
|
||
/* (sljit removed 2026-07-15: the gcc-DLL-obsoletes-older-sljit-shard block
|
||
* lived here. With no sljit shards ever registered (dll == -1), there is
|
||
* nothing to obsolete; the normal content-keyed chain dispatch stands.) */
|
||
return 1;
|
||
}
|
||
|
||
/* (sljit removed 2026-07-15: register_sljit_candidate, the JIT-on-miss memo,
|
||
* the async compile-worker glue, the master tier gate, and try_sljit_region
|
||
* lived here. Overlay misses now fall to the interpreter.) */
|
||
|
||
/* ---- Global state ------------------------------------------------------ */
|
||
|
||
static char s_cache_dir[512];
|
||
static char s_game_id[64];
|
||
static uint32_t s_config_hash;
|
||
static int s_active = 0;
|
||
|
||
/* Canonical (filesystem-resolved) form of s_cache_dir, computed lazily on the
|
||
* first live publication — the root is guaranteed to exist by then (the
|
||
* compiler just wrote a DLL into it). cache_path_has_root() is sound ONLY on
|
||
* canonical inputs: raw pipe text can smuggle `..` segments or a junction
|
||
* past a lexical prefix compare. */
|
||
static char s_cache_root_canon[768];
|
||
static int s_cache_root_canon_ok = 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 ----------------------------- */
|
||
|
||
/* 256 -> 4096 (2026-07-03): Tomba2's cache crossed 256 DLLs and the index
|
||
* TRUNCATED SILENTLY — every entry past the cap was invisible to the loader
|
||
* (region ran interpreted forever, no diagnostic). Found by the ABI-sweep
|
||
* negative test. scan_one_cache_dir now shouts if even 4096 is hit. */
|
||
#define CACHE_IDX_CAP 4096
|
||
enum { CACHE_TIER_UNKNOWN = 0, CACHE_TIER_TCC = 1, CACHE_TIER_GCC = 2 };
|
||
typedef struct {
|
||
uint32_t region_start;
|
||
uint32_t logical_crc;
|
||
uint64_t mtime;
|
||
int func_count;
|
||
int indexed_func_count;
|
||
uint8_t tier;
|
||
uint8_t manifest_ok;
|
||
uint8_t load_failed;
|
||
int resident;
|
||
int capacity_suppressed;
|
||
char path[768];
|
||
} CacheEntry;
|
||
static CacheEntry s_cache_idx[CACHE_IDX_CAP];
|
||
static int s_cache_idx_count = 0;
|
||
static int s_capacity_warm_dropped = 0;
|
||
static void overlay_image_warm_cancel(int ci);
|
||
static void overlay_image_warm_release(int ci);
|
||
static void overlay_image_warm_drop_all(void);
|
||
|
||
static int cache_entry_suppress_for_shortfall(int ci, int needed) {
|
||
if (ci < 0 || ci >= s_cache_idx_count || needed <= 0) return 0;
|
||
CacheEntry *e = &s_cache_idx[ci];
|
||
if (e->capacity_suppressed) return 1;
|
||
if (needed <= CAND_CAP - s_cand_n) return 0;
|
||
if (s_cand_n >= CAND_CAP && !s_capacity_warm_dropped) {
|
||
/* No further DLL can ever register in this process. Release every
|
||
* speculative image-map reference, including bundles never dispatched. */
|
||
overlay_image_warm_drop_all();
|
||
s_capacity_warm_dropped = 1;
|
||
} else {
|
||
/* Smaller bundles may still fit. Release only this impossible image
|
||
* and leave warming enabled for candidates within the remaining cap. */
|
||
overlay_image_warm_cancel(ci);
|
||
overlay_image_warm_release(ci);
|
||
}
|
||
e->capacity_suppressed = 1;
|
||
s_cand_overflow += (uint64_t)needed;
|
||
loader_log("*** CANDIDATE TABLE SHORTFALL (%d): suppressing %s (%d "
|
||
"manifest candidates, %d slots remain); native coverage is "
|
||
"falling back to the interpreter", CAND_CAP, e->path, needed,
|
||
CAND_CAP - s_cand_n);
|
||
return 1;
|
||
}
|
||
|
||
/* Candidates are process-lifetime objects, so reaching CAND_CAP is permanent.
|
||
* Suppress each not-yet-loaded bundle once instead of repeatedly mapping and
|
||
* unmapping it on every dispatch miss. The durable overflow count includes the
|
||
* manifest identities that this short-circuit prevents from registering. */
|
||
static int cache_entry_suppress_at_capacity(int ci) {
|
||
if (ci < 0 || ci >= s_cache_idx_count) return 0;
|
||
CacheEntry *e = &s_cache_idx[ci];
|
||
if (e->capacity_suppressed) return 1;
|
||
if (s_cand_n < CAND_CAP) return 0;
|
||
int dropped = e->indexed_func_count > 0
|
||
? e->indexed_func_count : e->func_count;
|
||
if (dropped == 0) dropped = 1;
|
||
return cache_entry_suppress_for_shortfall(ci, dropped);
|
||
}
|
||
|
||
static int path_component_eq(const char *path, const char *wanted) {
|
||
size_t wanted_len = strlen(wanted);
|
||
const char *p = path;
|
||
while (*p) {
|
||
while (*p == '/' || *p == '\\') p++;
|
||
const char *start = p;
|
||
while (*p && *p != '/' && *p != '\\') p++;
|
||
size_t len = (size_t)(p - start);
|
||
if (len == wanted_len) {
|
||
size_t i = 0;
|
||
for (; i < len; i++) {
|
||
unsigned char a = (unsigned char)start[i];
|
||
unsigned char b = (unsigned char)wanted[i];
|
||
if (tolower(a) != tolower(b)) break;
|
||
}
|
||
if (i == len) return 1;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static int cache_tier_from_path(const char *path) {
|
||
if (path_component_eq(path, "gcc")) return CACHE_TIER_GCC;
|
||
if (path_component_eq(path, "tcc")) return CACHE_TIER_TCC;
|
||
return CACHE_TIER_UNKNOWN;
|
||
}
|
||
|
||
static int cache_name_is_immutable(const char *name) {
|
||
if (!name) return 0;
|
||
size_t len = strlen(name);
|
||
/* Current publishers commit a recoverable region/CRC pair. Retain support
|
||
* for additive artifact-suffixed pairs produced by earlier cache tools. */
|
||
if (len == 17u + OVERLAY_SHARED_EXT_LEN) return name[8] == '_';
|
||
return len == 26u + OVERLAY_SHARED_EXT_LEN &&
|
||
name[8] == '_' && name[17] == '_';
|
||
}
|
||
|
||
static int cache_path_char_equal(unsigned char a, unsigned char b) {
|
||
#ifdef _WIN32
|
||
if ((a == '/' || a == '\\') && (b == '/' || b == '\\')) return 1;
|
||
return tolower(a) == tolower(b);
|
||
#else
|
||
return a == b;
|
||
#endif
|
||
}
|
||
|
||
static int cache_path_equal(const char *a, const char *b) {
|
||
if (!a || !b) return 0;
|
||
while (*a && *b) {
|
||
if (!cache_path_char_equal((unsigned char)*a, (unsigned char)*b))
|
||
return 0;
|
||
a++;
|
||
b++;
|
||
}
|
||
return *a == '\0' && *b == '\0';
|
||
}
|
||
|
||
static int cache_path_has_root(const char *path, const char *root) {
|
||
if (!path || !root) return 0;
|
||
size_t root_len = strlen(root);
|
||
while (root_len > 0u &&
|
||
(root[root_len - 1u] == '/' || root[root_len - 1u] == '\\'))
|
||
root_len--;
|
||
for (size_t i = 0; i < root_len; i++) {
|
||
if (!path[i] ||
|
||
!cache_path_char_equal((unsigned char)path[i],
|
||
(unsigned char)root[i]))
|
||
return 0;
|
||
}
|
||
return path[root_len] == '/' || path[root_len] == '\\';
|
||
}
|
||
|
||
#ifdef _WIN32
|
||
/* Image-only, bounded mapping for small recovery/fragment DLLs. Windows can
|
||
* spend ~100 ms in its loader/security path on the first LoadLibrary, enough to
|
||
* drain audio even though guest execution is otherwise at full speed. Map the
|
||
* dynamic-text-base fragments on a worker before streamed code first uses them.
|
||
* The emulation thread still performs overlay_init, export registration, and
|
||
* live-byte validation.
|
||
*
|
||
* This deliberately is NOT a boot-time preload: Tomba's mature vault contains
|
||
* hundreds of historical fragments and retaining all of them consumed ~1.8 GB.
|
||
* Only one cache region's small fragments are mapped. */
|
||
typedef struct {
|
||
volatile LONG state; /* 0 idle, 1 mapping, 2 ready, 3 cancelled, -1 failed */
|
||
HMODULE handle;
|
||
} ImageWarm;
|
||
typedef struct {
|
||
int cache_idx;
|
||
char path[768];
|
||
} ImageWarmJob;
|
||
typedef struct {
|
||
int count;
|
||
ImageWarmJob jobs[1];
|
||
} ImageWarmBatch;
|
||
static ImageWarm s_image_warm[CACHE_IDX_CAP];
|
||
volatile LONG g_overlay_image_warm_loaded = 0;
|
||
volatile LONG g_overlay_image_warm_pending = 0;
|
||
static int s_image_warm_enabled = 1;
|
||
static ULONGLONG s_image_warm_not_before_ms = 0;
|
||
|
||
static DWORD WINAPI overlay_image_warm_main(LPVOID unused) {
|
||
ImageWarmBatch *batch = (ImageWarmBatch *)unused;
|
||
ULONGLONG now = GetTickCount64();
|
||
if (now < s_image_warm_not_before_ms)
|
||
Sleep((DWORD)(s_image_warm_not_before_ms - now));
|
||
for (int i = 0; i < batch->count; i++) {
|
||
ImageWarmJob *job = &batch->jobs[i];
|
||
int ci = job->cache_idx;
|
||
if (InterlockedCompareExchange(&s_image_warm[ci].state, 0, 0) != 1) {
|
||
InterlockedDecrement(&g_overlay_image_warm_pending);
|
||
continue;
|
||
}
|
||
HMODULE h = LoadLibraryA(job->path);
|
||
s_image_warm[ci].handle = h;
|
||
if (InterlockedCompareExchange(&s_image_warm[ci].state,
|
||
h ? 2 : -1, 1) == 1) {
|
||
if (h) InterlockedIncrement(&g_overlay_image_warm_loaded);
|
||
} else {
|
||
/* The emulation thread needed this image before the batch reached
|
||
* it and cancelled the speculative reference. */
|
||
s_image_warm[ci].handle = NULL;
|
||
if (h) FreeLibrary(h);
|
||
InterlockedExchange(&s_image_warm[ci].state, 0);
|
||
}
|
||
InterlockedDecrement(&g_overlay_image_warm_pending);
|
||
}
|
||
free(batch);
|
||
return 0;
|
||
}
|
||
|
||
static void overlay_image_warm_init(void) {
|
||
const char *e = getenv("PSX_OVERLAY_IMAGE_WARM");
|
||
if (e && (e[0] == '0' || e[0] == 'n' || e[0] == 'N'))
|
||
s_image_warm_enabled = 0;
|
||
/* Leave BIOS/game initialization alone, but finish before the first
|
||
* streamed replacement of the dynamic text base. */
|
||
s_image_warm_not_before_ms = GetTickCount64() + 5000u;
|
||
}
|
||
|
||
static void overlay_image_warm_queue(const int *indices, int count) {
|
||
/* Candidate storage is process-lifetime. Once full, no warmed image can
|
||
* ever be registered, including a queue attempted later during init. */
|
||
if (!s_image_warm_enabled || s_capacity_warm_dropped ||
|
||
s_cand_n >= CAND_CAP || count <= 0) return;
|
||
size_t bytes = sizeof(ImageWarmBatch) +
|
||
(size_t)(count - 1) * sizeof(ImageWarmJob);
|
||
ImageWarmBatch *batch = (ImageWarmBatch *)malloc(bytes);
|
||
if (!batch) return;
|
||
batch->count = 0;
|
||
for (int i = 0; i < count; i++) {
|
||
int ci = indices[i];
|
||
if (ci < 0 || ci >= s_cache_idx_count) continue;
|
||
if (s_cache_idx[ci].capacity_suppressed) continue;
|
||
if (InterlockedCompareExchange(&s_image_warm[ci].state, 1, 0) != 0)
|
||
continue;
|
||
ImageWarmJob *job = &batch->jobs[batch->count++];
|
||
job->cache_idx = ci;
|
||
snprintf(job->path, sizeof(job->path), "%s", s_cache_idx[ci].path);
|
||
InterlockedIncrement(&g_overlay_image_warm_pending);
|
||
}
|
||
if (batch->count == 0) { free(batch); return; }
|
||
HANDLE h = CreateThread(NULL, 0, overlay_image_warm_main, batch, 0, NULL);
|
||
if (!h) {
|
||
for (int i = 0; i < batch->count; i++) {
|
||
InterlockedDecrement(&g_overlay_image_warm_pending);
|
||
InterlockedExchange(&s_image_warm[batch->jobs[i].cache_idx].state, 0);
|
||
}
|
||
free(batch);
|
||
return;
|
||
}
|
||
SetThreadPriority(h, THREAD_PRIORITY_BELOW_NORMAL);
|
||
CloseHandle(h);
|
||
}
|
||
|
||
static void overlay_image_warm_cancel(int ci) {
|
||
if (ci < 0 || ci >= CACHE_IDX_CAP) return;
|
||
InterlockedCompareExchange(&s_image_warm[ci].state, 3, 1);
|
||
}
|
||
|
||
static void overlay_image_warm_release(int ci) {
|
||
if (ci < 0 || ci >= CACHE_IDX_CAP) return;
|
||
if (InterlockedCompareExchange(&s_image_warm[ci].state, 0, 0) != 2) return;
|
||
HMODULE h = s_image_warm[ci].handle;
|
||
s_image_warm[ci].handle = NULL;
|
||
InterlockedExchange(&s_image_warm[ci].state, 0);
|
||
if (h) FreeLibrary(h); /* load_overlay_dll retained its own reference */
|
||
}
|
||
static void overlay_image_warm_drop_all(void) {
|
||
for (int ci = 0; ci < s_cache_idx_count; ci++) {
|
||
overlay_image_warm_cancel(ci);
|
||
overlay_image_warm_release(ci);
|
||
}
|
||
}
|
||
#else
|
||
long g_overlay_image_warm_loaded = 0;
|
||
long g_overlay_image_warm_pending = 0;
|
||
static void overlay_image_warm_init(void) {}
|
||
static void overlay_image_warm_queue(const int *indices, int count)
|
||
{ (void)indices; (void)count; }
|
||
static void overlay_image_warm_cancel(int ci) { (void)ci; }
|
||
static void overlay_image_warm_release(int ci) { (void)ci; }
|
||
static void overlay_image_warm_drop_all(void) {}
|
||
#endif
|
||
|
||
static void overlay_image_warm_seed_boot_text(void);
|
||
|
||
/* Manifest-only cache index. Parsing the small .ranges sidecars up front lets
|
||
* a dispatch hash the live code bytes and LoadLibrary only the exact matching
|
||
* variant. This is the scalable alternative to trial-loading hundreds of DLLs
|
||
* that share one reused RAM region. */
|
||
#define LAZY_MAN_CAP (CAND_CAP * 2)
|
||
#define LAZY_ENTRY_CAP (CAND_CAP * 2u)
|
||
#define LAZY_ENTRY_MASK (LAZY_ENTRY_CAP - 1u)
|
||
#define LAZY_RANGE_LINK_CAP (LAZY_MAN_CAP * 8)
|
||
typedef struct {
|
||
int cache_idx;
|
||
ManFn fn;
|
||
uint32_t val_gen;
|
||
uint8_t state; /* 0xFF unknown, else ENTRY_VALID/INVALID */
|
||
int next_entry;
|
||
int next_bundle;
|
||
} LazyMan;
|
||
static LazyMan s_lazy_man[LAZY_MAN_CAP];
|
||
static int s_lazy_man_n = 0;
|
||
static int s_lazy_man_overflow = 0;
|
||
static int s_lazy_entry_head[LAZY_ENTRY_CAP];
|
||
static int s_lazy_entry_tail[LAZY_ENTRY_CAP];
|
||
static int s_lazy_bundle_head[CACHE_IDX_CAP];
|
||
static int s_lazy_page_head[RANGE_PAGE_COUNT];
|
||
static int s_lazy_page_tail[RANGE_PAGE_COUNT];
|
||
static RangeLink s_lazy_range_links[LAZY_RANGE_LINK_CAP];
|
||
static int s_lazy_range_link_n = 0;
|
||
|
||
/* A direct-mapped negative cache for the final "no live native owner" result.
|
||
* Hot uncovered PCs otherwise repeat manifest/range-chain discovery on every
|
||
* branch into the interpreter. The entry is valid only for the current RAM
|
||
* code generation and loader generation: executable writes, DLL publication,
|
||
* and cache rescans all make old misses disappear immediately. */
|
||
#define LAZY_MISS_CACHE_CAP 16384u
|
||
#define LAZY_MISS_CACHE_MASK (LAZY_MISS_CACHE_CAP - 1u)
|
||
typedef struct {
|
||
uint32_t phys;
|
||
uint32_t code_gen;
|
||
uint32_t watched_code_gen;
|
||
uint32_t loader_gen;
|
||
} LazyMissEntry;
|
||
static LazyMissEntry s_lazy_miss_cache[LAZY_MISS_CACHE_CAP];
|
||
static uint32_t s_lazy_loader_gen = 1;
|
||
static uint32_t s_lazy_watched_code_gen = 1;
|
||
extern uint32_t g_dirty_ram_code_gen;
|
||
|
||
static void lazy_miss_invalidate_loader(void) {
|
||
if (++s_lazy_loader_gen == 0) {
|
||
memset(s_lazy_miss_cache, 0, sizeof(s_lazy_miss_cache));
|
||
s_lazy_loader_gen = 1;
|
||
}
|
||
}
|
||
|
||
static int lazy_miss_cached(uint32_t phys) {
|
||
LazyMissEntry *e = &s_lazy_miss_cache[(phys >> 2) & LAZY_MISS_CACHE_MASK];
|
||
return e->phys == phys && e->code_gen == g_dirty_ram_code_gen &&
|
||
e->watched_code_gen == s_lazy_watched_code_gen &&
|
||
e->loader_gen == s_lazy_loader_gen;
|
||
}
|
||
|
||
static void lazy_miss_record(uint32_t phys) {
|
||
LazyMissEntry *e = &s_lazy_miss_cache[(phys >> 2) & LAZY_MISS_CACHE_MASK];
|
||
e->phys = phys;
|
||
e->code_gen = g_dirty_ram_code_gen;
|
||
e->watched_code_gen = s_lazy_watched_code_gen;
|
||
e->loader_gen = s_lazy_loader_gen;
|
||
}
|
||
|
||
void overlay_loader_note_code_write(void) {
|
||
if (++s_lazy_watched_code_gen == 0) {
|
||
memset(s_lazy_miss_cache, 0, sizeof(s_lazy_miss_cache));
|
||
s_lazy_watched_code_gen = 1;
|
||
}
|
||
}
|
||
|
||
static void rebuild_lazy_manifest_index(void) {
|
||
memset(s_exact_entry_bitmap, 0, sizeof(s_exact_entry_bitmap));
|
||
s_lazy_man_n = 0;
|
||
s_lazy_man_overflow = 0;
|
||
s_lazy_range_link_n = 0;
|
||
for (uint32_t i = 0; i < LAZY_ENTRY_CAP; i++) {
|
||
s_lazy_entry_head[i] = -1;
|
||
s_lazy_entry_tail[i] = -1;
|
||
}
|
||
for (int i = 0; i < CACHE_IDX_CAP; i++) s_lazy_bundle_head[i] = -1;
|
||
for (uint32_t i = 0; i < RANGE_PAGE_COUNT; i++) {
|
||
s_lazy_page_head[i] = -1;
|
||
s_lazy_page_tail[i] = -1;
|
||
}
|
||
for (int ci = 0; ci < s_cache_idx_count; ci++) {
|
||
s_cache_idx[ci].func_count = 0;
|
||
s_cache_idx[ci].indexed_func_count = 0;
|
||
s_cache_idx[ci].manifest_ok = 0;
|
||
char path[800];
|
||
snprintf(path, sizeof(path), "%s", s_cache_idx[ci].path);
|
||
size_t n = strlen(path);
|
||
if (n < OVERLAY_SHARED_EXT_LEN ||
|
||
strcmp(path + n - OVERLAY_SHARED_EXT_LEN, OVERLAY_SHARED_EXT) != 0)
|
||
continue;
|
||
snprintf(path + n - OVERLAY_SHARED_EXT_LEN,
|
||
sizeof(path) - (n - OVERLAY_SHARED_EXT_LEN), ".ranges");
|
||
int man_n = 0;
|
||
ManFn *man = parse_manifest(path, &man_n, NULL, NULL, NULL);
|
||
if (!man) continue;
|
||
s_cache_idx[ci].manifest_ok = 1;
|
||
s_cache_idx[ci].func_count = man_n;
|
||
for (int mi = 0; mi < man_n; mi++) {
|
||
if (!man[mi].has_crc || man[mi].n <= 0) continue;
|
||
uint32_t entry = man[mi].entry & 0x1FFFFFFFu;
|
||
exact_entry_set(entry);
|
||
uint32_t bucket = (entry * 2654435761u) & LAZY_ENTRY_MASK;
|
||
|
||
if (s_lazy_man_n >= LAZY_MAN_CAP) {
|
||
s_lazy_man_overflow = 1;
|
||
break;
|
||
}
|
||
int li = s_lazy_man_n++;
|
||
LazyMan *lm = &s_lazy_man[li];
|
||
lm->cache_idx = ci;
|
||
lm->fn = man[mi];
|
||
lm->val_gen = 0;
|
||
lm->state = 0xFFu;
|
||
lm->next_bundle = s_lazy_bundle_head[ci];
|
||
s_lazy_bundle_head[ci] = li;
|
||
s_cache_idx[ci].indexed_func_count++;
|
||
/* Exact-entry chains historically insert at the head, so later
|
||
* cache entries win. Keep that order distinct from range ownership
|
||
* below, which is oldest-first. */
|
||
lm->next_entry = s_lazy_entry_head[bucket];
|
||
s_lazy_entry_head[bucket] = li;
|
||
if (s_lazy_entry_tail[bucket] < 0) s_lazy_entry_tail[bucket] = li;
|
||
|
||
uint8_t seen[RANGE_PAGE_COUNT / 8] = {0};
|
||
for (int r = 0; r < lm->fn.n; r++) {
|
||
uint32_t lo = lm->fn.lo[r] & 0x1FFFFFFFu;
|
||
uint32_t hi = lo + lm->fn.len[r] - 1u;
|
||
/* Unloaded manifests must participate in the same page
|
||
* generations as registered DLL candidates. Otherwise a CPU
|
||
* copy can turn INVALID bytes into a match while both the
|
||
* LazyMan state and final-miss cache remain stale forever. */
|
||
overlay_watch_set_range(lo, lm->fn.len[r]);
|
||
uint32_t p0 = lo >> 12, p1 = hi >> 12;
|
||
if (p0 >= RANGE_PAGE_COUNT) continue;
|
||
if (p1 >= RANGE_PAGE_COUNT) p1 = RANGE_PAGE_COUNT - 1u;
|
||
for (uint32_t p = p0; p <= p1; p++) {
|
||
uint8_t bit = (uint8_t)(1u << (p & 7u));
|
||
if (seen[p >> 3] & bit) continue;
|
||
seen[p >> 3] |= bit;
|
||
if (s_lazy_range_link_n >= LAZY_RANGE_LINK_CAP) {
|
||
s_lazy_man_overflow = 1;
|
||
break;
|
||
}
|
||
int ri = s_lazy_range_link_n++;
|
||
s_lazy_range_links[ri].cand = li;
|
||
s_lazy_range_links[ri].next = -1;
|
||
if (s_lazy_page_head[p] < 0) s_lazy_page_head[p] = ri;
|
||
else s_lazy_range_links[s_lazy_page_tail[p]].next = ri;
|
||
s_lazy_page_tail[p] = ri;
|
||
}
|
||
if (s_lazy_man_overflow) break;
|
||
}
|
||
if (s_lazy_man_overflow) break;
|
||
}
|
||
free(man);
|
||
if (s_lazy_man_overflow) break;
|
||
}
|
||
for (int i = 0; i < s_cand_n; i++) exact_entry_set(s_cand[i].addr);
|
||
}
|
||
|
||
static int cache_idx_has_path(const char *path) {
|
||
for (int i = 0; i < s_cache_idx_count; i++)
|
||
if (cache_path_equal(s_cache_idx[i].path, path)) return 1;
|
||
return 0;
|
||
}
|
||
|
||
/* Cache discovery is idempotent only by full path. Logical identities may have
|
||
* multiple immutable artifacts; validation and tier-aware selection happen
|
||
* after every artifact is indexed. */
|
||
static int cache_filename_equal(const char *a, const char *b) {
|
||
#ifdef _WIN32
|
||
return _stricmp(a, b) == 0;
|
||
#else
|
||
return strcmp(a, b) == 0;
|
||
#endif
|
||
}
|
||
|
||
/* A BIOS-resident shard is generated from an exact-BIOS-hash manifest and has
|
||
* a synthetic page envelope rather than a runtime dirty-run envelope. Its
|
||
* region filename therefore cannot participate in the ordinary region-start
|
||
* lookup. compile_overlays.py publishes this explicit sidecar only for those
|
||
* shards. Loading it early merely registers candidates: every dispatch still
|
||
* passes the ordinary per-function live-byte CRC guard, so a different BIOS or
|
||
* boot-time patch fails closed to the interpreter. */
|
||
#define BIOS_RESIDENT_MARKER_SCHEMA "psxrecomp bios resident shard v1"
|
||
static int cache_path_is_bios_resident(const char *dll_path) {
|
||
char path[800];
|
||
snprintf(path, sizeof(path), "%s", dll_path);
|
||
size_t n = strlen(path);
|
||
if (n < OVERLAY_SHARED_EXT_LEN ||
|
||
strcmp(path + n - OVERLAY_SHARED_EXT_LEN, OVERLAY_SHARED_EXT) != 0)
|
||
return 0;
|
||
snprintf(path + n - OVERLAY_SHARED_EXT_LEN,
|
||
sizeof(path) - (n - OVERLAY_SHARED_EXT_LEN), ".resident");
|
||
FILE *f = fopen(path, "rb");
|
||
if (!f) return 0;
|
||
char buf[256] = {0};
|
||
size_t got = fread(buf, 1, sizeof(buf) - 1, f);
|
||
fclose(f);
|
||
buf[got] = '\0';
|
||
return strstr(buf, BIOS_RESIDENT_MARKER_SCHEMA) != NULL;
|
||
}
|
||
|
||
static void refresh_bios_resident_flags(void) {
|
||
for (int i = 0; i < s_cache_idx_count; i++)
|
||
s_cache_idx[i].resident =
|
||
cache_path_is_bios_resident(s_cache_idx[i].path);
|
||
}
|
||
|
||
/* Canonical cache arch-abi tag (caches are namespaced per backend AND per
|
||
* target so a Windows-x64 gcc DLL and, later, a same-OS arm64 build 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; }
|
||
|
||
#ifndef _WIN32
|
||
static int add_posix_cache_file(const PsxOverlayCacheFile *file, void *opaque) {
|
||
int tier = opaque ? *(const int *)opaque : CACHE_TIER_UNKNOWN;
|
||
/* Legacy 8_8 pairs were published by a replace protocol that could leave a
|
||
* new manifest beside an old DLL. Keep them on disk for Python seed
|
||
* migration, but never let them suppress or execute instead of a bound
|
||
* immutable 8_8_8 repair. */
|
||
if (!cache_name_is_immutable(file->name)) return 0;
|
||
if (s_cache_idx_count >= CACHE_IDX_CAP) {
|
||
loader_log("*** CACHE INDEX FULL (%d): further DLLs near %s are being "
|
||
"IGNORED — their regions will run interpreted. Raise "
|
||
"CACHE_IDX_CAP.", CACHE_IDX_CAP, file->path);
|
||
return 1;
|
||
}
|
||
if (cache_idx_has_path(file->path)) return 0;
|
||
CacheEntry *e = &s_cache_idx[s_cache_idx_count++];
|
||
e->region_start = file->region_start;
|
||
e->logical_crc = file->content_crc;
|
||
e->mtime = file->mtime;
|
||
e->func_count = 0;
|
||
e->indexed_func_count = 0;
|
||
e->tier = (uint8_t)tier;
|
||
e->manifest_ok = 0;
|
||
e->load_failed = 0;
|
||
e->resident = cache_path_is_bios_resident(file->path);
|
||
e->capacity_suppressed = 0;
|
||
snprintf(e->path, sizeof(e->path), "%s", file->path);
|
||
return 0;
|
||
}
|
||
#endif
|
||
|
||
/* 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, int tier) {
|
||
#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 {
|
||
uint32_t addr = 0, crc = 0;
|
||
if (!psx_overlay_cache_name_parse(fd.cFileName, &addr, &crc)) continue;
|
||
if (!cache_name_is_immutable(fd.cFileName)) continue;
|
||
(void)crc;
|
||
if (s_cache_idx_count >= CACHE_IDX_CAP) {
|
||
/* Never-again (the silent-256 truncation): overflowing the index
|
||
* means real native coverage is being IGNORED. Shout once. */
|
||
loader_log("*** CACHE INDEX FULL (%d): further DLLs in %s are being "
|
||
"IGNORED — their regions will run interpreted. Raise "
|
||
"CACHE_IDX_CAP.", CACHE_IDX_CAP, dir);
|
||
break;
|
||
}
|
||
char full[768];
|
||
snprintf(full, sizeof(full), "%s/%s", dir, fd.cFileName);
|
||
/* Same full path means a rescan duplicate. Same logical name with a
|
||
* different artifact suffix is an additive repair and is retained. */
|
||
if (cache_idx_has_path(full)) continue;
|
||
CacheEntry *e = &s_cache_idx[s_cache_idx_count++];
|
||
e->region_start = addr;
|
||
e->logical_crc = crc;
|
||
e->mtime = ((uint64_t)fd.ftLastWriteTime.dwHighDateTime << 32) |
|
||
(uint64_t)fd.ftLastWriteTime.dwLowDateTime;
|
||
e->func_count = 0;
|
||
e->indexed_func_count = 0;
|
||
e->tier = (uint8_t)tier;
|
||
e->manifest_ok = 0;
|
||
e->load_failed = 0;
|
||
e->capacity_suppressed = 0;
|
||
snprintf(e->path, sizeof(e->path), "%s", full);
|
||
e->resident = cache_path_is_bios_resident(full);
|
||
} while (FindNextFileA(h, &fd));
|
||
FindClose(h);
|
||
#else
|
||
psx_overlay_posix_scan_cache_dir(dir, add_posix_cache_file, &tier);
|
||
#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).
|
||
* (Pre-1.0: no legacy fallback — older flat / unversioned caches are simply
|
||
* ignored and regenerated.) */
|
||
/* Hardening (never-again for the silent cg-tag read≠write drift): when the loader
|
||
* finds ZERO shards under its OWN cg-tag, check whether a SIBLING cg<...> folder in
|
||
* the same tier holds shards. If so, the autocompile wrote to a different codegen
|
||
* hash than this build reads — every overlay will silently fall to the interpreter
|
||
* ("why is it slow"). This is USUALLY a mismatched overlay_autocompile_cmd
|
||
* --recompiler / --runtime-include (e.g. a cross-build: runtime from one framework
|
||
* checkout, autocompile pointed at another). Shout it once, loudly, with both tags,
|
||
* so it can never again be diagnosed as generic slowness. */
|
||
static void warn_on_cgtag_mismatch(const char *tier) {
|
||
#ifdef _WIN32
|
||
char base[768], pattern[900];
|
||
snprintf(base, sizeof base, "%s/%s/%s/%s",
|
||
s_cache_dir, s_game_id, tier, PSX_OVERLAY_ARCH_ABI);
|
||
char expect[64];
|
||
snprintf(expect, sizeof expect, "cg%d_%08x_gc%08x",
|
||
PSX_OVERLAY_CODEGEN_VER, (unsigned)PSX_OVERLAY_CODEGEN_HASH,
|
||
(unsigned)s_config_hash);
|
||
snprintf(pattern, sizeof pattern, "%s/cg*", base);
|
||
WIN32_FIND_DATAA fd;
|
||
HANDLE h = FindFirstFileA(pattern, &fd);
|
||
if (h == INVALID_HANDLE_VALUE) return;
|
||
do {
|
||
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) continue;
|
||
if (strcmp(fd.cFileName, expect) == 0) continue; /* our own tag */
|
||
char dllpat[900]; WIN32_FIND_DATAA fd2;
|
||
snprintf(dllpat, sizeof dllpat, "%s/%s/*_*.dll", base, fd.cFileName);
|
||
HANDLE h2 = FindFirstFileA(dllpat, &fd2);
|
||
if (h2 != INVALID_HANDLE_VALUE) { /* sibling tag HAS shards */
|
||
FindClose(h2);
|
||
loader_log("*** OVERLAY CACHE HASH MISMATCH: this build reads %s/%s but "
|
||
"shards exist under %s/%s. The autocompile is writing to a "
|
||
"DIFFERENT codegen/config hash than this runtime reads -> ALL overlays "
|
||
"run INTERPRETED (slow). Fix overlay_autocompile_cmd's "
|
||
"--recompiler/--runtime-include to match THIS build's framework.",
|
||
tier, expect, tier, fd.cFileName);
|
||
}
|
||
} while (FindNextFileA(h, &fd));
|
||
FindClose(h);
|
||
#else
|
||
char base[768], expect[64], found[256];
|
||
snprintf(base, sizeof base, "%s/%s/%s/%s",
|
||
s_cache_dir, s_game_id, tier, PSX_OVERLAY_ARCH_ABI);
|
||
snprintf(expect, sizeof expect, "cg%d_%08x_gc%08x",
|
||
PSX_OVERLAY_CODEGEN_VER, (unsigned)PSX_OVERLAY_CODEGEN_HASH,
|
||
(unsigned)s_config_hash);
|
||
if (psx_overlay_posix_find_other_cache_tag(base, expect, found,
|
||
sizeof(found))) {
|
||
loader_log("*** OVERLAY CACHE HASH MISMATCH: this build reads %s/%s but "
|
||
"shards exist under %s/%s. The autocompile is writing to a "
|
||
"DIFFERENT codegen/config hash than this runtime reads -> ALL overlays "
|
||
"run INTERPRETED (slow). Fix overlay_autocompile_cmd's "
|
||
"--recompiler/--runtime-include to match THIS build's framework.",
|
||
tier, expect, tier, found);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
static void cache_idx_remove_path(const char *path) {
|
||
for (int i = 0; i < s_cache_idx_count; i++) {
|
||
if (cache_path_equal(s_cache_idx[i].path, path)) {
|
||
s_cache_idx[i] = s_cache_idx[--s_cache_idx_count];
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
#ifndef _WIN32
|
||
typedef struct PosixAbiSweep {
|
||
int purged;
|
||
int kept;
|
||
} PosixAbiSweep;
|
||
|
||
static int posix_abi_sweep_file(const PsxOverlayCacheFile *file, void *opaque) {
|
||
PosixAbiSweep *sweep = (PosixAbiSweep *)opaque;
|
||
if (!cache_name_is_immutable(file->name)) return 0;
|
||
char error[256] = {0};
|
||
void *handle = psx_overlay_posix_library_open(file->path, error, sizeof(error));
|
||
int abi = 0;
|
||
if (handle) {
|
||
typedef int (*AbiFn)(void);
|
||
AbiFn abi_fn = (AbiFn)psx_overlay_posix_library_symbol(handle, "overlay_abi");
|
||
abi = abi_fn ? abi_fn() : 0;
|
||
psx_overlay_posix_library_close(handle);
|
||
}
|
||
if (handle && abi == PSX_OVERLAY_ABI_TAG) {
|
||
sweep->kept++;
|
||
return 0;
|
||
}
|
||
|
||
if (handle)
|
||
loader_log("ABI preflight rejecting %s: dll=0x%X runtime=0x%X",
|
||
file->path, abi, PSX_OVERLAY_ABI_TAG);
|
||
else
|
||
loader_log("ABI preflight rejecting unloadable %s: %s",
|
||
file->path, error);
|
||
/* Do not delete by canonical pathname after inspecting a loaded handle:
|
||
* an atomic publisher may already have replaced that path with a new file. */
|
||
cache_idx_remove_path(file->path);
|
||
sweep->purged++;
|
||
return 0;
|
||
}
|
||
#endif
|
||
|
||
/* ---- ABI pre-flight sweep (batch purge) ----------------------------------
|
||
* A contract-ABI bump (e.g. v9 -> v10) invalidates EVERY cached DLL at once.
|
||
* The lazy path handled that per-dispatch: try_load_region -> LoadLibrary ->
|
||
* ABI reject -> DeleteFile -> retry next dispatch — thousands of file ops on
|
||
* the EMULATION thread (which owns the window pump). On a large cache this
|
||
* presented as a "(Not Responding)" black window for minutes (MMX6, 136-DLL
|
||
* cache, the v10 migration, 2026-07-03 regression gate).
|
||
*
|
||
* Instead, sweep at scan time: load each indexed DLL, check overlay_abi, and
|
||
* drop mismatches from the in-memory index. Never delete by canonical path
|
||
* after loading: a concurrent atomic publisher may have replaced that name. A marker
|
||
* file (.abi_<tag>.ok) per cache dir records a completed sweep, so healthy
|
||
* caches skip the sweep entirely on later boots — steady-state cost is one
|
||
* stat per dir. Autocompile only ever writes current-ABI DLLs, so the marker
|
||
* stays truthful; the per-load ABI gate in load_overlay_dll remains as
|
||
* defense in depth. */
|
||
static void abi_preflight_sweep(const char *dir) {
|
||
#ifdef _WIN32
|
||
char marker[900];
|
||
snprintf(marker, sizeof marker, "%s/.abi_%08x.ok", dir, (unsigned)PSX_OVERLAY_ABI_TAG);
|
||
if (GetFileAttributesA(marker) != INVALID_FILE_ATTRIBUTES) return; /* swept */
|
||
|
||
/* Enumerate the DIRECTORY, not the index: the index is capacity-bounded
|
||
* and dedup-filtered, so sweeping it alone can leave stale files behind
|
||
* while the marker claims a complete sweep (found by the negative test —
|
||
* the planted v6 DLL survived behind the old 256-entry index cap). */
|
||
int purged = 0, kept = 0;
|
||
char pattern[900];
|
||
snprintf(pattern, sizeof pattern, "%s/*_*.dll", dir);
|
||
WIN32_FIND_DATAA fd;
|
||
HANDLE hf = FindFirstFileA(pattern, &fd);
|
||
if (hf != INVALID_HANDLE_VALUE) {
|
||
do {
|
||
if (!cache_name_is_immutable(fd.cFileName)) continue;
|
||
char full[900];
|
||
snprintf(full, sizeof full, "%s/%s", dir, fd.cFileName);
|
||
HMODULE h = LoadLibraryA(full);
|
||
if (h) {
|
||
typedef int (*AbiFn)(void);
|
||
AbiFn abi_fn = (AbiFn)GetProcAddress(h, "overlay_abi");
|
||
int abi = abi_fn ? abi_fn() : 0;
|
||
FreeLibrary(h);
|
||
if (abi == PSX_OVERLAY_ABI_TAG) { kept++; continue; }
|
||
}
|
||
/* Drop only the index entry. Deleting `full` here can delete a new
|
||
* shard atomically swapped in after LoadLibrary returned the old
|
||
* handle. The compiler owns on-disk replacement. */
|
||
purged++;
|
||
for (int i = 0; i < s_cache_idx_count; i++) {
|
||
if (cache_path_equal(s_cache_idx[i].path, full)) {
|
||
s_cache_idx[i] = s_cache_idx[--s_cache_idx_count];
|
||
break;
|
||
}
|
||
}
|
||
} while (FindNextFileA(hf, &fd));
|
||
FindClose(hf);
|
||
}
|
||
if (purged)
|
||
loader_log("abi preflight: rejected %d stale DLL(s), kept %d in %s",
|
||
purged, kept, dir);
|
||
/* Rejected files remain until the compiler replaces them, so do not create
|
||
* a marker that would skip their in-memory rejection next boot. */
|
||
if (!purged) {
|
||
HANDLE m = CreateFileA(marker, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
|
||
FILE_ATTRIBUTE_NORMAL, NULL);
|
||
if (m != INVALID_HANDLE_VALUE) CloseHandle(m);
|
||
}
|
||
#else
|
||
char marker[900];
|
||
snprintf(marker, sizeof marker, "%s/.abi_%08x.ok", dir,
|
||
(unsigned)PSX_OVERLAY_ABI_TAG);
|
||
if (access(marker, F_OK) == 0) return;
|
||
|
||
PosixAbiSweep sweep = {0};
|
||
psx_overlay_posix_scan_cache_dir(dir, posix_abi_sweep_file, &sweep);
|
||
if (sweep.purged)
|
||
loader_log("abi preflight: rejected %d stale DLL(s), kept %d in %s",
|
||
sweep.purged, sweep.kept, dir);
|
||
if (!sweep.purged) {
|
||
FILE *m = fopen(marker, "wb");
|
||
if (m) fclose(m);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
static void scan_cache_dir(void) {
|
||
char dir[768];
|
||
/* Index both tiers and every immutable artifact. Runtime selection prefers
|
||
* usable GCC over TCC; an invalid GCC artifact cannot suppress a valid TCC
|
||
* fallback merely because its filename was enumerated first. */
|
||
snprintf(dir, sizeof(dir), "%s/%s/gcc/%s/cg%d_%08x_gc%08x",
|
||
s_cache_dir, s_game_id, PSX_OVERLAY_ARCH_ABI, PSX_OVERLAY_CODEGEN_VER,
|
||
(unsigned)PSX_OVERLAY_CODEGEN_HASH, (unsigned)s_config_hash);
|
||
scan_one_cache_dir(dir, CACHE_TIER_GCC);
|
||
abi_preflight_sweep(dir);
|
||
snprintf(dir, sizeof(dir), "%s/%s/tcc/%s/cg%d_%08x_gc%08x",
|
||
s_cache_dir, s_game_id, PSX_OVERLAY_ARCH_ABI, PSX_OVERLAY_CODEGEN_VER,
|
||
(unsigned)PSX_OVERLAY_CODEGEN_HASH, (unsigned)s_config_hash);
|
||
scan_one_cache_dir(dir, CACHE_TIER_TCC);
|
||
abi_preflight_sweep(dir);
|
||
|
||
refresh_bios_resident_flags();
|
||
rebuild_lazy_manifest_index();
|
||
|
||
/* Never-again guard: if we loaded NOTHING but wrong-hash shards exist, shout. */
|
||
if (s_cache_idx_count == 0) {
|
||
warn_on_cgtag_mismatch("gcc");
|
||
warn_on_cgtag_mismatch("tcc");
|
||
}
|
||
|
||
/* Opt-in startup/rescan inventory from PR #13. Current master deliberately
|
||
* keeps routine loader events off stderr, so retain that policy unless the
|
||
* operator explicitly requests the diagnostic. */
|
||
const char *inventory = getenv("PSX_OVERLAY_CACHE_INVENTORY");
|
||
if (inventory && *inventory && *inventory != '0') {
|
||
fprintf(stderr, "overlay cache scan: %d shared library file(s) indexed "
|
||
"[arch=%s]\n", s_cache_idx_count, PSX_OVERLAY_ARCH_ABI);
|
||
for (int i = 0; i < s_cache_idx_count; i++) {
|
||
const char *base = strrchr(s_cache_idx[i].path, '/');
|
||
base = base ? base + 1 : s_cache_idx[i].path;
|
||
fprintf(stderr, " [%d] %s (region=0x%08X, functions=%d%s)\n",
|
||
i, base, s_cache_idx[i].region_start,
|
||
s_cache_idx[i].indexed_func_count,
|
||
s_cache_idx[i].resident ? ", BIOS-resident" : "");
|
||
}
|
||
fflush(stderr);
|
||
}
|
||
}
|
||
|
||
/* (sljit removed 2026-07-15: the persisted sljit shard cache — blob header,
|
||
* persist/publish writers, on-disk reload scan, and their debug accessors —
|
||
* lived here. No shards are produced or reloaded anymore.) */
|
||
|
||
static uint32_t lazy_man_crc(const ManFn *m); /* defined with lazy matching */
|
||
|
||
/* True if the cache holds a usable DLL for this region/logical image CRC.
|
||
* Filename identity alone is insufficient: a structurally valid but stale or
|
||
* unsafe manifest must not suppress additive recapture of its own repair. */
|
||
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].manifest_ok &&
|
||
!s_cache_idx[i].load_failed &&
|
||
s_cache_idx[i].region_start == region_start &&
|
||
s_cache_idx[i].logical_crc == crc &&
|
||
s_cache_idx[i].func_count > 0 &&
|
||
s_cache_idx[i].indexed_func_count == s_cache_idx[i].func_count) {
|
||
int seen = 0, usable = 1;
|
||
for (int li = s_lazy_bundle_head[i]; li >= 0;
|
||
li = s_lazy_man[li].next_bundle) {
|
||
seen++;
|
||
if (lazy_man_crc(&s_lazy_man[li].fn) != s_lazy_man[li].fn.crc ||
|
||
!man_delay_slots_hashed(&s_lazy_man[li].fn)) {
|
||
usable = 0;
|
||
break;
|
||
}
|
||
}
|
||
if (usable && seen == s_cache_idx[i].func_count) 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 int psx_interrupt_delivery_needed(const CPUState *cpu);
|
||
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_fn(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;
|
||
|
||
/* Nested call-unit depth (Ape memcard native<->interp resume-desync fix).
|
||
*
|
||
* When the dirty interpreter (or a shard) issues a guest jal/jalr to a callee
|
||
* run as a UNIT — an overlay-native shard (overlay_loader_call_native) OR a
|
||
* non-local dirty/kernel routine (dispatch_nonlocal_call -> psx_dispatch_call) —
|
||
* that callee must be ATOMIC w.r.t. the cooperative thread switch, exactly like
|
||
* statically-compiled code (psx_dispatch_impl checks interrupts ONLY at the
|
||
* outermost dispatch return; a nested callee never interrupts before its caller
|
||
* runs the post-call continuation). Both the overlay CI wrappers AND the dirty
|
||
* per-transfer IRQ pumps, however, checked interrupts at EVERY block/transfer
|
||
* regardless of nesting; an IRQ + cooperative ChangeThread landing inside such a
|
||
* nested unit suspended the interrupted thread with an INCONSISTENT snapshot
|
||
* (resume PC at the caller's post-call point, sp still mid-callee) -> a leaked
|
||
* stack frame that compounded across cooperative cycles into a smeared jumptable
|
||
* index (the Ape "Checking MEMORY CARD" softlock/fatal). While this depth is >0
|
||
* both backends defer the IRQ check; the callee runs to completion, then the
|
||
* enclosing top-level dirty pump / outermost dispatch return delivers the IRQ at
|
||
* a consistent (pc, sp) boundary. The TOP-LEVEL flow (depth 0) still pumps IRQs,
|
||
* so the consumer's poll loop and long overlay game-loops keep their
|
||
* responsiveness. Incremented only when the A/B toggle is on (below). */
|
||
int g_call_unit_depth = 0;
|
||
|
||
/* A/B toggle for the nested-unit IRQ deferral (PSX_OVERLAY_UNIT_DEFER, default
|
||
* ON). Gates the depth INCREMENTS (so =0 leaves g_call_unit_depth at 0 and
|
||
* nothing defers — the pre-fix behavior), letting us attribute a behavior change
|
||
* to this fix without a rebuild. */
|
||
int overlay_unit_defer_enabled(void) {
|
||
static int cached = -1;
|
||
if (cached < 0) {
|
||
const char *e = getenv("PSX_OVERLAY_UNIT_DEFER");
|
||
cached = (e && (e[0] == '0' || e[0] == 'n' || e[0] == 'N')) ? 0 : 1;
|
||
}
|
||
return cached;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/* Overlay CI-wrapper attribution (post-load freeze): early returns never enter
|
||
* psx_check_interrupts, so s_irq_path_entry stays flat while cycles still
|
||
* advance inside native overlay / call-unit regions. */
|
||
static uint64_t s_ci_skip_unit;
|
||
static uint64_t s_ci_skip_supp;
|
||
static uint64_t s_ci_skip_none;
|
||
static uint64_t s_ci_skip_sr;
|
||
static uint64_t s_ci_skip_deliv;
|
||
static uint64_t s_ci_enter;
|
||
|
||
void overlay_loader_get_ci_skip_diag(uint64_t *unit, uint64_t *supp,
|
||
uint64_t *none, uint64_t *sr,
|
||
uint64_t *deliv, uint64_t *enter) {
|
||
if (unit) *unit = s_ci_skip_unit;
|
||
if (supp) *supp = s_ci_skip_supp;
|
||
if (none) *none = s_ci_skip_none;
|
||
if (sr) *sr = s_ci_skip_sr;
|
||
if (deliv) *deliv = s_ci_skip_deliv;
|
||
if (enter) *enter = s_ci_enter;
|
||
}
|
||
int overlay_loader_call_unit_depth(void) { return g_call_unit_depth; }
|
||
|
||
static int overlay_irq_suppressed_now(void) {
|
||
/* Differential replay (and its authoritative interpreter pass) is atomic.
|
||
* Never let a previously armed rate-limit punch a real IRQ into a shadow. */
|
||
if (s_in_shadow) { s_irq_suppressed++; return 1; }
|
||
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) {
|
||
/* Defer while inside a nested call unit — a callee must not interrupt
|
||
* mid-call (static-call atomicity). See g_call_unit_depth. */
|
||
if (g_call_unit_depth > 0) { s_ci_skip_unit++; return; }
|
||
if (overlay_irq_suppressed_now()) { s_ci_skip_supp++; return; }
|
||
/* psx_advance_cycles() has already raised every device edge due at this
|
||
* block. Avoid entering the full scheduler/diagnostic path when COP0 could
|
||
* not take the IRQ anyway. FMV polling loops can execute this edge millions
|
||
* of times while an INTC bit is pending but IEc is deliberately clear. */
|
||
if ((i_stat & i_mask) == 0) { s_ci_skip_none++; return; }
|
||
if ((cpu->cop0[12] & ((1u << 10) | 1u)) != ((1u << 10) | 1u)) {
|
||
s_ci_skip_sr++;
|
||
return;
|
||
}
|
||
if (!psx_interrupt_delivery_needed(cpu)) { s_ci_skip_deliv++; return; }
|
||
s_ci_enter++;
|
||
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 int overlay_idle_note_is_internal_or_return(const CPUState *cpu,
|
||
uint32_t resume_pc) {
|
||
uint32_t phys = resume_pc & 0x1FFFFFFFu;
|
||
if ((resume_pc & 0x1FFFFFFFu) == (cpu->gpr[31] & 0x1FFFFFFFu)) return 1;
|
||
if (s_active_depth <= 0) return 0;
|
||
Candidate *c = &s_cand[s_active_stack[s_active_depth - 1]];
|
||
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 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static void overlay_ci_at_wrapper(CPUState *cpu, uint32_t resume_pc) {
|
||
/* Defer while inside a nested call unit (see g_call_unit_depth): suspending
|
||
* here would save resume_pc at the callee's block leader while the enclosing
|
||
* dirty caller expects an atomic unit — the resume-desync bug. */
|
||
if (g_call_unit_depth > 0) { s_ci_skip_unit++; return; }
|
||
if (overlay_irq_suppressed_now()) { s_ci_skip_supp++; return; }
|
||
if ((i_stat & i_mask) == 0) { s_ci_skip_none++; return; }
|
||
if ((cpu->cop0[12] & ((1u << 10) | 1u)) != ((1u << 10) | 1u)) {
|
||
s_ci_skip_sr++;
|
||
return;
|
||
}
|
||
if (!psx_interrupt_delivery_needed(cpu)) { s_ci_skip_deliv++; return; }
|
||
s_ci_enter++;
|
||
extern int g_idle_note_suppress;
|
||
int suppress_idle_note = overlay_idle_note_is_internal_or_return(cpu, resume_pc);
|
||
if (suppress_idle_note) g_idle_note_suppress++;
|
||
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;
|
||
if (suppress_idle_note) g_idle_note_suppress--;
|
||
return;
|
||
}
|
||
psx_check_interrupts_at(cpu, resume_pc);
|
||
if (suppress_idle_note) g_idle_note_suppress--;
|
||
}
|
||
|
||
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;
|
||
/* Never deliver an IRQ inside a shadow run: the handler would ack device
|
||
* state (I_STAT) that survives the sandbox while the guest state it ran on
|
||
* is rolled back — a lost interrupt. Shadow runs are IRQ-suppressed by
|
||
* design (s_suppress_irq in run_shadow_diff); this pump must match. */
|
||
if (s_in_shadow) return;
|
||
if (overlay_irq_budget_blocks_now()) return;
|
||
if (cpu->pc != 0u) psx_check_interrupts_at(cpu, cpu->pc);
|
||
else psx_check_interrupts(cpu);
|
||
}
|
||
|
||
/* Probe wrapper (mmx6_card_load_regression_state): record shard-side calls to
|
||
* xprobe-watched targets (before + after) so overlay-DLL call-outs are visible
|
||
* in the xprobe `watched` dump the way interp JAL/JALR sites already are. */
|
||
extern void dirty_ram_xprobe_call_note(CPUState *cpu, uint32_t target, uint32_t ra, uint8_t phase);
|
||
extern int g_exec_phase; /* wall-time sampler phase (dirty_ram_interp.c) */
|
||
static void overlay_dispatch_call_probed(CPUState *cpu, uint32_t addr, uint32_t ra) {
|
||
dirty_ram_xprobe_call_note(cpu, addr, ra, 0);
|
||
int prev_phase = g_exec_phase;
|
||
g_exec_phase = 3; /* compiled route; dirty/native callees re-tag inside */
|
||
psx_dispatch_call(cpu, addr, ra);
|
||
g_exec_phase = prev_phase;
|
||
dirty_ram_xprobe_call_note(cpu, addr, ra, 1);
|
||
}
|
||
|
||
static void init_callbacks(void) {
|
||
extern void psx_restore_state_escape(void);
|
||
extern void psx_rfe_mark_escape(void);
|
||
s_callbacks.dispatch_call = overlay_dispatch_call_probed;
|
||
s_callbacks.check_interrupts = overlay_ci_wrapper;
|
||
s_callbacks.check_interrupts_at = overlay_ci_at_wrapper;
|
||
/* Address of the header inline → out-of-line copy in this TU (host side). */
|
||
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;
|
||
#ifdef PSX_NO_DEBUG_TOOLS
|
||
/* Generated DLLs NULL-check this callback. Avoid a production call/return
|
||
* at every guest function entry when the callee would immediately no-op. */
|
||
s_callbacks.log_call_entry = NULL;
|
||
#else
|
||
s_callbacks.log_call_entry = debug_server_log_call_entry_fn;
|
||
#endif
|
||
s_callbacks.psx_restore_state_escape = psx_restore_state_escape;
|
||
/* Return-from-exception mark (ABI v12): overlay `rfe` ops forward here. */
|
||
s_callbacks.rfe_mark_escape = psx_rfe_mark_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;
|
||
}
|
||
/* Faithful-timing functions (ABI v9): overlay code built with
|
||
* PSX_ENABLE_BLOCK_CYCLES emits these; forward to the runtime's real impls so
|
||
* native overlays charge cycles on the SAME timeline as the interp/BIOS. */
|
||
{
|
||
/* Word/half fast paths are static inline in psx_cyc.h; overlays get the
|
||
* out-of-line slow entry points (same guest timing, MMIO-safe). */
|
||
extern uint32_t psx_cyc_load_word_slow(CPUState*, uint32_t, uint32_t, uint32_t);
|
||
extern uint16_t psx_cyc_load_half_slow(CPUState*, uint32_t, uint32_t, uint32_t);
|
||
extern uint8_t psx_cyc_load_byte(CPUState*, uint32_t, uint32_t, uint32_t);
|
||
extern uint32_t psx_cyc_lwc2_read(CPUState*, uint32_t);
|
||
extern void psx_icache_fetch_fn(CPUState*, uint32_t);
|
||
extern void psx_muldiv_set(CPUState*, uint32_t);
|
||
extern void psx_muldiv_stall(CPUState*);
|
||
extern uint32_t psx_mult_latency_s(uint32_t);
|
||
extern uint32_t psx_mult_latency_u(uint32_t);
|
||
extern void psx_gte_stall(CPUState*);
|
||
extern void psx_gte_read(CPUState*, uint32_t);
|
||
extern int psx_slice_block_impl(CPUState*, uint32_t, uint32_t, int);
|
||
s_callbacks.cyc_load_word = psx_cyc_load_word_slow;
|
||
s_callbacks.cyc_load_half = psx_cyc_load_half_slow;
|
||
s_callbacks.cyc_load_byte = psx_cyc_load_byte;
|
||
s_callbacks.cyc_lwc2_read = psx_cyc_lwc2_read;
|
||
s_callbacks.icache_fetch = psx_icache_fetch_fn;
|
||
s_callbacks.muldiv_set = psx_muldiv_set;
|
||
s_callbacks.muldiv_stall = psx_muldiv_stall;
|
||
s_callbacks.mult_latency_s = psx_mult_latency_s;
|
||
s_callbacks.mult_latency_u = psx_mult_latency_u;
|
||
s_callbacks.gte_stall = psx_gte_stall;
|
||
s_callbacks.gte_read = psx_gte_read;
|
||
s_callbacks.slice_block = psx_slice_block_impl;
|
||
/* ABI v10: GTE special-register accessors — the emitter emits direct
|
||
* calls for flag/IR/derived GTE regs (mfc2/cfc2/mtc2/ctc2); a GTE-heavy
|
||
* overlay DLL cannot link without these forwarded. */
|
||
{
|
||
extern uint32_t gte_read_data(CPUState*, uint8_t);
|
||
extern uint32_t gte_read_ctrl(CPUState*, uint8_t);
|
||
extern void gte_write_data(CPUState*, uint8_t, uint32_t);
|
||
extern void gte_write_ctrl(CPUState*, uint8_t, uint32_t);
|
||
s_callbacks.gte_read_data = gte_read_data;
|
||
s_callbacks.gte_read_ctrl = gte_read_ctrl;
|
||
s_callbacks.gte_write_data = gte_write_data;
|
||
s_callbacks.gte_write_ctrl = gte_write_ctrl;
|
||
}
|
||
/* ABI v12: rfe escape marker — emitted at every `rfe`, including in
|
||
* overlay-resident exception-context code; mutates the runtime's
|
||
* shared RFE-pending state so it must forward. */
|
||
{
|
||
extern void psx_rfe_mark_escape(void);
|
||
s_callbacks.rfe_mark_escape = psx_rfe_mark_escape;
|
||
}
|
||
/* ABI v13: typed native-wide signed player-X bound — the emitter
|
||
* rewrites configured signed_x_bound LUI sites (Einhander player
|
||
* bounds) into psx_ws_player_x_bound() calls, including in
|
||
* overlay-resident code; the clamp reads the host's live widescreen
|
||
* state (gpu.c) so the DLL must forward. */
|
||
{
|
||
extern int32_t psx_ws_player_x_bound(int32_t vanilla);
|
||
s_callbacks.ws_player_x_bound = psx_ws_player_x_bound;
|
||
}
|
||
/* ABI v14: GTE precision-store tracker — the emitter emits a direct
|
||
* gte_precision_store_word() call for every swc2 (GTE store-word),
|
||
* including in overlay-resident code; it mutates the host's stateful
|
||
* sub-pixel projection cache (gte.cpp) so the DLL must forward. */
|
||
{
|
||
extern void gte_precision_store_word(uint32_t addr, uint8_t reg);
|
||
s_callbacks.gte_precision_store_word = gte_precision_store_word;
|
||
}
|
||
{
|
||
extern int32_t psx_ws_depth_bound(int32_t imm);
|
||
s_callbacks.ws_depth_bound = psx_ws_depth_bound;
|
||
}
|
||
{
|
||
extern int32_t psx_ws_plane_nx(int32_t nx);
|
||
s_callbacks.ws_plane_nx = psx_ws_plane_nx;
|
||
}
|
||
{
|
||
extern uint32_t psx_ws_xclip_bound(uint32_t vanilla);
|
||
s_callbacks.ws_xclip_bound = psx_ws_xclip_bound;
|
||
}
|
||
{
|
||
extern uint32_t psx_ws_cull_keep_result(uint32_t vanilla,
|
||
uint32_t forced);
|
||
s_callbacks.ws_cull_keep_result = psx_ws_cull_keep_result;
|
||
}
|
||
{
|
||
extern uint32_t psx_ws_aspect_cone_result(
|
||
uint32_t site, uint32_t vanilla, uint32_t object,
|
||
int32_t x, int32_t z, int32_t y);
|
||
s_callbacks.ws_aspect_cone_result =
|
||
psx_ws_aspect_cone_result;
|
||
}
|
||
{
|
||
extern uint32_t psx_ws_angle_widen(uint32_t vanilla);
|
||
s_callbacks.ws_angle_widen = psx_ws_angle_widen;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ---- DLL loading and export enumeration -------------------------------- */
|
||
|
||
#ifdef _WIN32
|
||
typedef HMODULE OverlayLibraryHandle;
|
||
#else
|
||
typedef void *OverlayLibraryHandle;
|
||
#endif
|
||
|
||
struct OverlayPreparedImage {
|
||
OverlayLibraryHandle handle;
|
||
char path[768];
|
||
};
|
||
|
||
#ifdef _WIN32
|
||
static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll,
|
||
OverlayLibraryHandle prepared,
|
||
uint64_t manifest_pair_id,
|
||
int manifest_has_pair_id,
|
||
int manifest_provenance, int tier) {
|
||
HMODULE h = prepared ? prepared : 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). The compiler, not this
|
||
* loaded handle, owns safe on-disk replacement. */
|
||
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 so autocompile can replace it for THIS build. */
|
||
if (abi != PSX_OVERLAY_ABI_TAG) {
|
||
loader_log("ABI/flavor mismatch in %s: dll=0x%X runtime=0x%X — rejecting "
|
||
"without pathname deletion", dll_path, abi,
|
||
PSX_OVERLAY_ABI_TAG);
|
||
FreeLibrary(h);
|
||
return 0;
|
||
}
|
||
typedef uint64_t (*PairIdFn)(void);
|
||
PairIdFn pair_fn = (PairIdFn)GetProcAddress(h, "overlay_pair_id");
|
||
int dll_has_pair_id = pair_fn != NULL;
|
||
uint64_t dll_pair_id = pair_fn ? pair_fn() : 0;
|
||
if (dll_has_pair_id != manifest_has_pair_id ||
|
||
(dll_has_pair_id && dll_pair_id != manifest_pair_id)) {
|
||
loader_log("DLL/manifest pair mismatch in %s -- rejecting without "
|
||
"deleting (publication may be in progress)", dll_path);
|
||
FreeLibrary(h);
|
||
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;
|
||
}
|
||
/* Match the offline pair validator atomically: every manifest F must have
|
||
* its exact callable export before any Candidate slot is consumed. */
|
||
for (int i = 0; i < man_n; i++) {
|
||
char name[32];
|
||
snprintf(name, sizeof(name), "func_%08X", man[i].entry);
|
||
if (!GetProcAddress(h, name)) {
|
||
loader_log("manifest/export mismatch in %s: missing %s -- "
|
||
"rejecting whole DLL", dll_path, name);
|
||
FreeLibrary(h);
|
||
return 0;
|
||
}
|
||
}
|
||
OverlayFlushFn flush_fn =
|
||
(OverlayFlushFn)GetProcAddress(h, "overlay_flush_cycles");
|
||
if (!flush_fn) {
|
||
loader_log("no ABI-v11 cycle flush export in %s", dll_path);
|
||
FreeLibrary(h);
|
||
return 0;
|
||
}
|
||
|
||
/* Validate the whole physical pair before treating it as an alias. The
|
||
* canonical pair already owns its initialized handle and callbacks, so the
|
||
* redundant image must never run overlay_init or publish function pointers. */
|
||
if (manifest_has_pair_id && loaded_pair_find(
|
||
tier, manifest_provenance, manifest_pair_id, man, man_n) >= 0) {
|
||
s_pair_aliases++;
|
||
loader_log("validated duplicate overlay pair in %s -- reusing canonical "
|
||
"candidate bundle", dll_path);
|
||
FreeLibrary(h);
|
||
return LOAD_PAIR_ALIAS;
|
||
}
|
||
if (dll < 0 || dll >= (int)(sizeof(s_dll_flush) / sizeof(s_dll_flush[0]))) {
|
||
loader_log("overlay DLL owner table full for %s", dll_path);
|
||
FreeLibrary(h);
|
||
return 0;
|
||
}
|
||
|
||
/* Candidate publication is all-or-nothing for pair dedup authority. The
|
||
* offline publisher normally makes this impossible; fail closed if an
|
||
* externally modified cache would leave only part of a bundle resident. */
|
||
if (man_n > CAND_CAP - s_cand_n) {
|
||
s_cand_overflow += (uint64_t)man_n;
|
||
loader_log("*** CANDIDATE TABLE SHORTFALL (%d): rejecting complete %s "
|
||
"pair (%d manifest candidates); native coverage is falling "
|
||
"back to the interpreter", CAND_CAP, dll_path, man_n);
|
||
FreeLibrary(h);
|
||
return 0;
|
||
}
|
||
|
||
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;
|
||
}
|
||
init_fn(&s_callbacks);
|
||
s_dll_flush[dll] = flush_fn;
|
||
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;
|
||
uint64_t overflow_before = s_cand_overflow;
|
||
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; }
|
||
registered += cand_register(addr & 0x1FFFFFFFu, fn, m, dll, tier);
|
||
}
|
||
if (registered == man_n && manifest_has_pair_id)
|
||
loaded_pair_commit(tier, manifest_provenance,
|
||
manifest_pair_id, man, man_n);
|
||
if (s_cand_overflow != overflow_before)
|
||
loader_log("*** CANDIDATE TABLE FULL (%d): loaded %s -> %d registered, "
|
||
"%llu dropped; native coverage is falling back to the "
|
||
"interpreter. Raise CAND_CAP.", CAND_CAP, dll_path,
|
||
registered,
|
||
(unsigned long long)(s_cand_overflow - overflow_before));
|
||
else
|
||
loader_log("loaded %s -> %d candidates (%u no-manifest)",
|
||
dll_path, registered, s_no_manifest);
|
||
if (registered == 0) {
|
||
/* load_one_dll deliberately leaves a zero-candidate path retryable. Do
|
||
* not leak a LoadLibrary reference on every retry after CAND_CAP is
|
||
* exhausted (or when every export lacks a usable manifest record). */
|
||
s_dll_flush[dll] = NULL;
|
||
FreeLibrary(h);
|
||
}
|
||
return registered;
|
||
}
|
||
#else
|
||
static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll,
|
||
OverlayLibraryHandle prepared,
|
||
uint64_t manifest_pair_id,
|
||
int manifest_has_pair_id,
|
||
int manifest_provenance, int tier) {
|
||
char error[256] = {0};
|
||
void *h = prepared ? prepared
|
||
: psx_overlay_posix_library_open(dll_path, error,
|
||
sizeof(error));
|
||
if (!h) { loader_log("dlopen(%s) failed: %s", dll_path, error); return 0; }
|
||
/* ABI gate (see the _WIN32 branch). */
|
||
typedef int (*AbiFn)(void);
|
||
AbiFn abi_fn = (AbiFn)psx_overlay_posix_library_symbol(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 "
|
||
"without pathname deletion", dll_path, abi,
|
||
PSX_OVERLAY_ABI_TAG);
|
||
psx_overlay_posix_library_close(h);
|
||
return 0;
|
||
}
|
||
typedef uint64_t (*PairIdFn)(void);
|
||
PairIdFn pair_fn = (PairIdFn)psx_overlay_posix_library_symbol(
|
||
h, "overlay_pair_id");
|
||
int dll_has_pair_id = pair_fn != NULL;
|
||
uint64_t dll_pair_id = pair_fn ? pair_fn() : 0;
|
||
if (dll_has_pair_id != manifest_has_pair_id ||
|
||
(dll_has_pair_id && dll_pair_id != manifest_pair_id)) {
|
||
loader_log("DLL/manifest pair mismatch in %s -- rejecting without "
|
||
"deleting (publication may be in progress)", dll_path);
|
||
psx_overlay_posix_library_close(h);
|
||
return 0;
|
||
}
|
||
typedef void (*InitFn)(const OverlayCallbacks *);
|
||
InitFn init_fn = (InitFn)psx_overlay_posix_library_symbol(h, "overlay_init");
|
||
if (!init_fn) {
|
||
loader_log("no overlay_init in %s", dll_path);
|
||
psx_overlay_posix_library_close(h);
|
||
return 0;
|
||
}
|
||
for (int i = 0; i < man_n; i++) {
|
||
if (!psx_overlay_posix_library_entry(h, man[i].entry)) {
|
||
loader_log("manifest/export mismatch in %s: missing func_%08X -- "
|
||
"rejecting whole DLL", dll_path, man[i].entry);
|
||
psx_overlay_posix_library_close(h);
|
||
return 0;
|
||
}
|
||
}
|
||
OverlayFlushFn flush_fn = (OverlayFlushFn)psx_overlay_posix_library_symbol(
|
||
h, "overlay_flush_cycles");
|
||
if (!flush_fn) {
|
||
loader_log("no ABI-v11 cycle flush export in %s", dll_path);
|
||
psx_overlay_posix_library_close(h);
|
||
return 0;
|
||
}
|
||
if (manifest_has_pair_id && loaded_pair_find(
|
||
tier, manifest_provenance, manifest_pair_id, man, man_n) >= 0) {
|
||
s_pair_aliases++;
|
||
loader_log("validated duplicate overlay pair in %s -- reusing canonical "
|
||
"candidate bundle", dll_path);
|
||
psx_overlay_posix_library_close(h);
|
||
return LOAD_PAIR_ALIAS;
|
||
}
|
||
if (dll < 0 || dll >= (int)(sizeof(s_dll_flush) / sizeof(s_dll_flush[0]))) {
|
||
loader_log("overlay DLL owner table full for %s", dll_path);
|
||
psx_overlay_posix_library_close(h);
|
||
return 0;
|
||
}
|
||
if (man_n > CAND_CAP - s_cand_n) {
|
||
s_cand_overflow += (uint64_t)man_n;
|
||
loader_log("*** CANDIDATE TABLE SHORTFALL (%d): rejecting complete %s "
|
||
"pair (%d manifest candidates); native coverage is falling "
|
||
"back to the interpreter", CAND_CAP, dll_path, man_n);
|
||
psx_overlay_posix_library_close(h);
|
||
return 0;
|
||
}
|
||
init_fn(&s_callbacks);
|
||
s_dll_flush[dll] = flush_fn;
|
||
|
||
/* ELF/Mach-O do not expose a portable export-table walker. The range
|
||
* manifest is already the loader's authority, so resolve only its exact
|
||
* callable entries. Missing symbols stay interpreted rather than acquiring
|
||
* a guessed extent. */
|
||
int registered = 0;
|
||
uint64_t overflow_before = s_cand_overflow;
|
||
for (int i = 0; i < man_n; i++) {
|
||
ManFn *m = &man[i];
|
||
if (m->entry == 0 || m->n == 0) continue;
|
||
OverlayFn fn = (OverlayFn)psx_overlay_posix_library_entry(h, m->entry);
|
||
if (!fn) continue;
|
||
registered += cand_register(m->entry & 0x1FFFFFFFu, fn, m, dll, tier);
|
||
}
|
||
if (registered == man_n && manifest_has_pair_id)
|
||
loaded_pair_commit(tier, manifest_provenance,
|
||
manifest_pair_id, man, man_n);
|
||
if (s_cand_overflow != overflow_before)
|
||
loader_log("*** CANDIDATE TABLE FULL (%d): loaded %s -> %d registered, "
|
||
"%llu dropped; native coverage is falling back to the "
|
||
"interpreter. Raise CAND_CAP.", CAND_CAP, dll_path,
|
||
registered,
|
||
(unsigned long long)(s_cand_overflow - overflow_before));
|
||
else
|
||
loader_log("loaded %s -> %d manifest candidates", dll_path, registered);
|
||
if (registered == 0) {
|
||
s_dll_flush[dll] = NULL;
|
||
psx_overlay_posix_library_close(h);
|
||
}
|
||
/* A successful handle intentionally stays open for process lifetime: the
|
||
* registered function and flush pointers refer into it. Rescans are
|
||
* idempotent through s_loaded_paths and never acquire a second reference. */
|
||
return registered;
|
||
}
|
||
#endif
|
||
|
||
/* ---- Public API -------------------------------------------------------- */
|
||
|
||
static int dll_already_loaded(const char *path);
|
||
static int load_one_dll(const char *dll_path, OverlayLibraryHandle prepared);
|
||
|
||
static int load_bios_resident_shards(void) {
|
||
int bundles = 0, functions = 0;
|
||
for (int i = 0; i < s_cache_idx_count; i++) {
|
||
CacheEntry *e = &s_cache_idx[i];
|
||
if (!e->resident || e->func_count <= 0 ||
|
||
e->indexed_func_count != e->func_count ||
|
||
e->capacity_suppressed || dll_already_loaded(e->path))
|
||
continue;
|
||
int loaded = load_one_dll(e->path, NULL);
|
||
if (loaded > 0) { bundles++; functions += loaded; }
|
||
}
|
||
if (bundles > 0)
|
||
loader_log("preloaded %d exact-guarded BIOS resident shard(s), %d candidates",
|
||
bundles, functions);
|
||
return functions;
|
||
}
|
||
|
||
void overlay_loader_init(const char *cache_dir, const char *game_id,
|
||
uint32_t config_hash) {
|
||
{
|
||
const char *perf = getenv("PSX_RUNTIME_PERF_DIAG");
|
||
s_native_hot_enabled = perf && perf[0] && perf[0] != '0';
|
||
}
|
||
for (uint32_t p = 0; p < RANGE_PAGE_COUNT; p++) {
|
||
s_range_page_head[p] = -1;
|
||
s_range_page_tail[p] = -1;
|
||
}
|
||
s_range_link_n = 0;
|
||
s_range_index_overflow = 0;
|
||
for (uint32_t i = 0; i < RANGE_PC_CACHE_CAP; i++)
|
||
s_range_pc_cache[i].cand = -1;
|
||
strncpy(s_cache_dir, cache_dir, sizeof(s_cache_dir) - 1);
|
||
strncpy(s_game_id, game_id, sizeof(s_game_id) - 1);
|
||
s_config_hash = config_hash;
|
||
s_cache_root_canon_ok = 0; /* re-resolve for the (re)assigned root */
|
||
/* data shards persist under the same unified cache root (data_shards.c) */
|
||
{ extern void ds_init(const char*, const char*); ds_init(cache_dir, game_id); }
|
||
init_callbacks();
|
||
scan_cache_dir();
|
||
load_bios_resident_shards();
|
||
overlay_image_warm_init();
|
||
overlay_image_warm_seed_boot_text();
|
||
/* (sljit removed 2026-07-15: the persisted-shard reload + off-thread compile
|
||
* worker were started here. Overlay misses fall to the interpreter.) */
|
||
/* 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);
|
||
}
|
||
}
|
||
#ifndef PSX_NO_DEBUG_TOOLS
|
||
{
|
||
const char *limit = getenv("PSX_NATIVE_RANK_LIMIT");
|
||
if (limit && *limit) {
|
||
unsigned long long n = strtoull(limit, NULL, 0);
|
||
s_native_rank_limit = n >= UINT32_MAX ? UINT32_MAX - 1u : (uint32_t)n;
|
||
s_native_rank_next = 0;
|
||
s_native_rank_blocked = 0;
|
||
fprintf(stdout,
|
||
"psxrecomp: PSX_NATIVE_RANK_LIMIT=%u: "
|
||
"first-seen candidate bisection ON\n",
|
||
s_native_rank_limit);
|
||
}
|
||
}
|
||
#endif
|
||
/* 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");
|
||
}
|
||
const char *diff_addr = getenv("PSX_OVERLAY_DIFF_ADDR");
|
||
if (diff_addr && *diff_addr) {
|
||
s_diff_addr = (uint32_t)strtoul(diff_addr, NULL, 0);
|
||
loader_log("PSX_OVERLAY_DIFF_ADDR set: shadow diff restricted to 0x%08X",
|
||
s_diff_addr);
|
||
}
|
||
}
|
||
s_active = 1;
|
||
}
|
||
|
||
/* (sljit removed 2026-07-15: overlay_loader_apply_live_policy resolved the
|
||
* sljit live-execution policy once the Tier-2 backend was known. With the tier
|
||
* gone there is no live policy to apply; the interpreter is the overlay-miss
|
||
* floor unconditionally.) */
|
||
|
||
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) -------- */
|
||
|
||
/* 64 -> 256 (2026-07-06): the dirty boot-text window adds per-variant regions
|
||
* below FLOOR; a full memo silently stops caching new region checks. */
|
||
#define MAX_CHECKED 256
|
||
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;
|
||
}
|
||
|
||
void overlay_loader_clear_lazy_miss(void)
|
||
{
|
||
lazy_miss_invalidate_loader();
|
||
s_nchecked = 0;
|
||
}
|
||
|
||
void overlay_loader_resync_validation_after_restore(void)
|
||
{
|
||
/* Host-only validation memos. Page gens were already bumped by
|
||
* overlay_watch_invalidate_after_ram_restore; force every candidate off
|
||
* the gen fast-path (including ENTRY_INVALID + gen==val_gen skips, and
|
||
* nranges==0 bodies whose gensum never moves). Static-match cache is the
|
||
* same class for AOT game/BIOS overlays. */
|
||
int i;
|
||
for (i = 0; i < s_cand_n; i++)
|
||
s_cand[i].val_gen ^= 0x80000000u;
|
||
#ifdef PSX_HAS_OVERLAY_DISPATCH
|
||
memset(s_static_match_cache, 0, sizeof(s_static_match_cache));
|
||
#endif
|
||
}
|
||
|
||
/* 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 || !overlay_loads_allowed()) return;
|
||
scan_cache_dir();
|
||
load_bios_resident_shards();
|
||
s_nchecked = 0;
|
||
lazy_miss_invalidate_loader();
|
||
}
|
||
|
||
/* 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. */
|
||
/* The warmed vault and runtime-discovered variants can exceed the old 512-DLL
|
||
* ceiling; once dll_already_loaded() lost track, a rescan could double-register
|
||
* candidates. Match the cache-index capacity used by the loader. */
|
||
#define MAX_LOADED_DLLS 4096
|
||
static char s_loaded_paths[MAX_LOADED_DLLS][768];
|
||
static int s_nloaded_paths = 0;
|
||
|
||
#ifndef PSX_NO_DEBUG_TOOLS
|
||
static int native_rank_allows(Candidate *c, uint32_t pc) {
|
||
if (s_native_rank_limit == UINT32_MAX) return 1;
|
||
if (c->native_rank == 0) {
|
||
c->native_rank = ++s_native_rank_next;
|
||
const char *path = (c->dll >= 0 && c->dll < s_nloaded_paths)
|
||
? s_loaded_paths[c->dll] : "<loading>";
|
||
fprintf(stdout,
|
||
"psxrecomp: native-rank rank=%u candidate=%d owner=0x%08X "
|
||
"pc=0x%08X crc=0x%08X dll=%s frame=%llu cycle=%llu allow=%d\n",
|
||
c->native_rank, (int)(c - s_cand), c->addr, pc,
|
||
c->crc_code, path, (unsigned long long)s_frame_count,
|
||
(unsigned long long)psx_get_cycle_count(),
|
||
c->native_rank <= s_native_rank_limit);
|
||
}
|
||
if (c->native_rank <= s_native_rank_limit) return 1;
|
||
s_native_rank_blocked++;
|
||
return 0;
|
||
}
|
||
#endif
|
||
|
||
static int dll_already_loaded(const char *path) {
|
||
for (int i = 0; i < s_nloaded_paths; i++)
|
||
if (cache_path_equal(s_loaded_paths[i], path)) return 1;
|
||
return 0;
|
||
}
|
||
|
||
static void overlay_library_close(OverlayLibraryHandle handle) {
|
||
if (!handle) return;
|
||
#ifdef _WIN32
|
||
FreeLibrary(handle);
|
||
#else
|
||
psx_overlay_posix_library_close(handle);
|
||
#endif
|
||
}
|
||
|
||
/* Consumes prepared on every path. A successful registration intentionally
|
||
* retains that reference for process lifetime, matching the ordinary loader. */
|
||
static int load_one_dll(const char *dll_path,
|
||
OverlayLibraryHandle prepared) {
|
||
/* Fail CLOSED at the tracking cap, BEFORE registering anything: past this
|
||
* point dll_already_loaded() would lose track of the DLL and a later
|
||
* rescan could double-register its candidates (stale-chain execution).
|
||
* Refusing keeps the region interpreted -- same contract as CACHE_IDX_CAP. */
|
||
if (s_nloaded_paths >= MAX_LOADED_DLLS) {
|
||
loader_log("*** LOADED-DLL TABLE FULL (%d): refusing %s -- its region "
|
||
"stays interpreted. Raise MAX_LOADED_DLLS.",
|
||
MAX_LOADED_DLLS, dll_path);
|
||
overlay_library_close(prepared);
|
||
return 0;
|
||
}
|
||
int cache_idx = -1;
|
||
int tier = cache_tier_from_path(dll_path);
|
||
for (int ci = 0; ci < s_cache_idx_count; ci++) {
|
||
if (cache_path_equal(s_cache_idx[ci].path, dll_path)) {
|
||
cache_idx = ci;
|
||
tier = s_cache_idx[ci].tier;
|
||
break;
|
||
}
|
||
}
|
||
#ifdef _WIN32
|
||
LARGE_INTEGER q0, q1, qf;
|
||
QueryPerformanceCounter(&q0);
|
||
#endif
|
||
/* 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 >= OVERLAY_SHARED_EXT_LEN &&
|
||
strcmp(ranges_path + plen - OVERLAY_SHARED_EXT_LEN, OVERLAY_SHARED_EXT) == 0)
|
||
snprintf(ranges_path + plen - OVERLAY_SHARED_EXT_LEN,
|
||
sizeof(ranges_path) - (plen - OVERLAY_SHARED_EXT_LEN), ".ranges");
|
||
|
||
int man_n = 0;
|
||
uint64_t manifest_pair_id = 0;
|
||
int manifest_has_pair_id = 0;
|
||
int manifest_provenance = MANIFEST_PROVENANCE_AUTHORITY;
|
||
ManFn *man = parse_manifest(ranges_path, &man_n,
|
||
&manifest_pair_id,
|
||
&manifest_has_pair_id,
|
||
&manifest_provenance);
|
||
if (!man || man_n == 0) {
|
||
loader_log("no/empty manifest %s — DLL left to interpreter", ranges_path);
|
||
free(man);
|
||
overlay_library_close(prepared);
|
||
return 0;
|
||
}
|
||
/* A complete known pair remains a zero-slot operation even when no new
|
||
* bundle of this size can fit. Parse and compare before durably suppressing
|
||
* the cache entry; otherwise safe aliases become unreachable at the cap. */
|
||
int known_alias = manifest_has_pair_id && loaded_pair_find(
|
||
tier, manifest_provenance, manifest_pair_id, man, man_n) >= 0;
|
||
if (cache_idx >= 0 && !known_alias) {
|
||
int suppressed = s_cand_n >= CAND_CAP
|
||
? cache_entry_suppress_at_capacity(cache_idx)
|
||
: cache_entry_suppress_for_shortfall(cache_idx, man_n);
|
||
if (suppressed) {
|
||
free(man);
|
||
overlay_library_close(prepared);
|
||
return 0;
|
||
}
|
||
}
|
||
int registered = load_overlay_dll(dll_path, man, man_n, s_ndlls, prepared,
|
||
manifest_pair_id,
|
||
manifest_has_pair_id,
|
||
manifest_provenance, tier);
|
||
#ifdef _WIN32
|
||
QueryPerformanceCounter(&q1);
|
||
QueryPerformanceFrequency(&qf);
|
||
uint64_t elapsed_us = qf.QuadPart > 0
|
||
? (uint64_t)((q1.QuadPart - q0.QuadPart) * 1000000LL / qf.QuadPart) : 0;
|
||
s_load_last_us = elapsed_us;
|
||
s_load_total_us += elapsed_us;
|
||
if (elapsed_us > s_load_max_us) s_load_max_us = elapsed_us;
|
||
#endif
|
||
free(man);
|
||
if (registered == 0) return 0;
|
||
|
||
/* The DLL may publish other PCs that were previously negative-cached. */
|
||
lazy_miss_invalidate_loader();
|
||
|
||
/* Unconditional: the fail-closed gate at entry guarantees room, so the
|
||
* tracking table can never silently lose a loaded DLL again. */
|
||
strncpy(s_loaded_paths[s_nloaded_paths], dll_path, 767);
|
||
s_loaded_paths[s_nloaded_paths][767] = '\0';
|
||
s_nloaded_paths++;
|
||
if (registered > 0) s_ndlls++;
|
||
return registered;
|
||
}
|
||
|
||
static int load_one_dll_prepared(const char *dll_path,
|
||
OverlayLibraryHandle prepared) {
|
||
/* load_one_dll performs lazy_miss_invalidate_loader(); keep this wrapper
|
||
* name for the prepared-publication contract. */
|
||
return load_one_dll(dll_path, prepared);
|
||
}
|
||
|
||
static int published_root_canonical(void) {
|
||
if (s_cache_root_canon_ok) return 1;
|
||
if (!overlay_path_canonicalize(s_cache_dir, s_cache_root_canon,
|
||
sizeof(s_cache_root_canon))) {
|
||
/* Not cached as a failure: the root may simply not exist yet; the
|
||
* next publication retries. Rejecting until it resolves is the
|
||
* fail-closed direction. */
|
||
loader_log("publication rejected: cache root %s does not "
|
||
"canonicalize", s_cache_dir);
|
||
return 0;
|
||
}
|
||
return s_cache_root_canon_ok = 1;
|
||
}
|
||
|
||
/* Validates dll_path and writes its CANONICAL form into canon; every later
|
||
* consumer (LoadLibrary, .ranges derivation, loaded-path tracking) uses the
|
||
* canonical string, never the raw pipe text. */
|
||
static int published_path_valid(const char *dll_path, char *canon,
|
||
size_t canon_cap) {
|
||
if (!s_active || !dll_path || !dll_path[0]) return 0;
|
||
/* The compiler command is trusted local code, but keep its native-library
|
||
* output confined to the cache root the framework injected — resolved
|
||
* through the filesystem on BOTH sides, exactly as the OS loader will
|
||
* resolve it, so `..`/short-name/reparse forms cannot slip a lexical
|
||
* prefix check. */
|
||
if (!published_root_canonical()) return 0;
|
||
if (!overlay_path_canonicalize(dll_path, canon, canon_cap)) return 0;
|
||
if (!cache_path_has_root(canon, s_cache_root_canon)) return 0;
|
||
const char *base = strrchr(canon, '/');
|
||
const char *win_base = strrchr(canon, '\\');
|
||
if (!base || (win_base && win_base > base)) base = win_base;
|
||
base = base ? base + 1 : canon;
|
||
uint32_t addr = 0, crc = 0;
|
||
if (!psx_overlay_cache_name_parse(base, &addr, &crc)) return 0;
|
||
if (!cache_name_is_immutable(base)) return 0;
|
||
(void)addr;
|
||
(void)crc;
|
||
return 1;
|
||
}
|
||
|
||
OverlayPreparedImage *overlay_loader_prepare_published(const char *dll_path) {
|
||
char canon[768];
|
||
if (!published_path_valid(dll_path, canon, sizeof(canon))) return NULL;
|
||
OverlayPreparedImage *image =
|
||
(OverlayPreparedImage *)calloc(1, sizeof(*image));
|
||
if (!image) return NULL;
|
||
#ifdef _WIN32
|
||
/* Guard handle: GENERIC_READ with only READ shared blocks rename/replace/
|
||
* delete of the validated file until LoadLibrary has mapped it, and the
|
||
* canonical identity is re-derived from the object actually held — so a
|
||
* swap between validation and mapping cannot survive the re-check.
|
||
* (Anything already writable inside the cache root itself is outside this
|
||
* boundary by design; see the threat-model note above cand_register.) */
|
||
HANDLE guard = CreateFileA(canon, GENERIC_READ, FILE_SHARE_READ, NULL,
|
||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
||
if (guard == INVALID_HANDLE_VALUE) {
|
||
free(image);
|
||
return NULL;
|
||
}
|
||
if (!overlay_path_canonicalize_handle(guard, canon, sizeof(canon)) ||
|
||
!cache_path_has_root(canon, s_cache_root_canon)) {
|
||
CloseHandle(guard);
|
||
free(image);
|
||
return NULL;
|
||
}
|
||
snprintf(image->path, sizeof(image->path), "%s", canon);
|
||
image->handle = LoadLibraryA(canon);
|
||
CloseHandle(guard);
|
||
#else
|
||
snprintf(image->path, sizeof(image->path), "%s", canon);
|
||
char error[256] = {0};
|
||
image->handle = psx_overlay_posix_library_open(canon, error,
|
||
sizeof(error));
|
||
#endif
|
||
if (!image->handle) {
|
||
free(image);
|
||
return NULL;
|
||
}
|
||
return image;
|
||
}
|
||
|
||
int overlay_loader_commit_published(OverlayPreparedImage *image) {
|
||
if (!image) return 0;
|
||
if (!overlay_loads_allowed()) {
|
||
overlay_loader_discard_prepared(image);
|
||
return 0;
|
||
}
|
||
OverlayLibraryHandle handle = image->handle;
|
||
image->handle = NULL;
|
||
int loaded = 0;
|
||
char canon[768];
|
||
/* image->path is already canonical (prepare stored the handle-derived
|
||
* form); re-validating is an idempotent freshness check. */
|
||
if (published_path_valid(image->path, canon, sizeof(canon)) &&
|
||
!dll_already_loaded(canon))
|
||
loaded = load_one_dll_prepared(canon, handle);
|
||
else
|
||
overlay_library_close(handle);
|
||
free(image);
|
||
return loaded;
|
||
}
|
||
|
||
void overlay_loader_discard_prepared(OverlayPreparedImage *image) {
|
||
if (!image) return;
|
||
overlay_library_close(image->handle);
|
||
image->handle = NULL;
|
||
free(image);
|
||
}
|
||
|
||
static int lazy_man_contains(const ManFn *m, uint32_t phys) {
|
||
if ((m->entry & 0x1FFFFFFFu) == phys) return 1;
|
||
for (int r = 0; r < m->n; r++) {
|
||
uint32_t lo = m->lo[r] & 0x1FFFFFFFu;
|
||
if (phys >= lo && phys < lo + m->len[r]) return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static uint32_t lazy_man_crc(const ManFn *m) {
|
||
const uint8_t *ram = memory_get_ram_ptr();
|
||
uint32_t crc = 0xFFFFFFFFu;
|
||
for (int r = 0; r < m->n; r++)
|
||
crc = crc32_update(crc, ram + (m->lo[r] & 0x1FFFFFFFu), m->len[r]);
|
||
return crc ^ 0xFFFFFFFFu;
|
||
}
|
||
|
||
static uint32_t lazy_man_gensum(const ManFn *m) {
|
||
uint32_t sum = 0;
|
||
for (int r = 0; r < m->n; r++)
|
||
sum += overlay_watch_pagegen_sum(m->lo[r] & 0x1FFFFFFFu, m->len[r]);
|
||
return sum;
|
||
}
|
||
|
||
static int lazy_man_matches(LazyMan *lm) {
|
||
if (!man_structurally_valid(&lm->fn)) {
|
||
lm->state = ENTRY_INVALID;
|
||
return 0;
|
||
}
|
||
uint32_t gen = lazy_man_gensum(&lm->fn);
|
||
if (lm->state == ENTRY_VALID && lm->val_gen == gen) return 1;
|
||
if (lm->state == ENTRY_INVALID && lm->val_gen == gen) return 0;
|
||
uint32_t live = lazy_man_crc(&lm->fn);
|
||
lm->val_gen = gen;
|
||
s_last_crc = live;
|
||
lm->state = (live == lm->fn.crc && man_delay_slots_hashed(&lm->fn))
|
||
? ENTRY_VALID : ENTRY_INVALID;
|
||
return lm->state == ENTRY_VALID;
|
||
}
|
||
|
||
/* A CPS DLL can directly own internal tail targets, so one matching entry does
|
||
* not prove the rest of that captured snapshot is current. Validate every
|
||
* manifested function before preferring a broad bundle. This permits one
|
||
* coherent snapshot to replace many synchronous incremental LoadLibrary calls
|
||
* at a stage transition without substituting a stale CPS owner. */
|
||
static int lazy_bundle_matches(int ci) {
|
||
if (ci < 0 || ci >= s_cache_idx_count || ci >= CACHE_IDX_CAP) return 0;
|
||
if (s_cache_idx[ci].func_count <= 0 ||
|
||
s_cache_idx[ci].indexed_func_count != s_cache_idx[ci].func_count)
|
||
return 0;
|
||
int seen = 0;
|
||
for (int li = s_lazy_bundle_head[ci]; li >= 0;
|
||
li = s_lazy_man[li].next_bundle) {
|
||
seen++;
|
||
if (!lazy_man_matches(&s_lazy_man[li])) return 0;
|
||
}
|
||
return seen == s_cache_idx[ci].func_count;
|
||
}
|
||
|
||
static int lazy_candidate_preferred(int li, int current) {
|
||
if (current < 0) return 1;
|
||
const CacheEntry *a = &s_cache_idx[s_lazy_man[li].cache_idx];
|
||
const CacheEntry *b = &s_cache_idx[s_lazy_man[current].cache_idx];
|
||
if (a->tier != b->tier) return a->tier > b->tier;
|
||
if (a->mtime != b->mtime) return a->mtime > b->mtime;
|
||
return strcmp(a->path, b->path) > 0;
|
||
}
|
||
|
||
/* Prefer GCC over TCC even when the TCC option happens to be a broader bundle.
|
||
* Within one tier, a fully coherent bundle remains stronger than a partial
|
||
* region-local function match. */
|
||
static int lazy_choose_complete_or_fallback(int complete, int fallback) {
|
||
if (complete < 0) return fallback;
|
||
if (fallback < 0) return complete;
|
||
const CacheEntry *a = &s_cache_idx[s_lazy_man[complete].cache_idx];
|
||
const CacheEntry *b = &s_cache_idx[s_lazy_man[fallback].cache_idx];
|
||
if (a->tier != b->tier) return a->tier > b->tier ? complete : fallback;
|
||
return complete;
|
||
}
|
||
|
||
/* A streamed variant is not byte-coherent until immediately before its first
|
||
* call, too late to hide Windows' first image-map cost. Map every small bundle
|
||
* for this ONE base after boot. Tomba's mature vault has 141 here versus 712 in
|
||
* the rejected whole-cache preload. Mapping never registers a candidate; the
|
||
* normal manifest/live-byte gate still decides whether it can execute. */
|
||
static void overlay_image_warm_seed_boot_text(void) {
|
||
int indices[CACHE_IDX_CAP];
|
||
int count = 0;
|
||
for (int ci = 0; ci < s_cache_idx_count; ci++) {
|
||
if (s_cache_idx[ci].region_start != DIRTY_RAM_KERNEL_WINDOW_END ||
|
||
s_cache_idx[ci].capacity_suppressed ||
|
||
s_cache_idx[ci].func_count <= 0 ||
|
||
s_cache_idx[ci].func_count > 8)
|
||
continue;
|
||
indices[count++] = ci;
|
||
}
|
||
overlay_image_warm_queue(indices, count);
|
||
}
|
||
|
||
static int lazy_is_loadable(int li, uint32_t region_start, uint32_t phys,
|
||
int require_region_start) {
|
||
if (li < 0 || li >= s_lazy_man_n) return 0;
|
||
LazyMan *lm = &s_lazy_man[li];
|
||
int ci = lm->cache_idx;
|
||
if (ci < 0 || ci >= s_cache_idx_count) return 0;
|
||
return (!require_region_start ||
|
||
s_cache_idx[ci].region_start == region_start) &&
|
||
!s_cache_idx[ci].load_failed &&
|
||
!s_cache_idx[ci].capacity_suppressed &&
|
||
!dll_already_loaded(s_cache_idx[ci].path) &&
|
||
lazy_man_contains(&lm->fn, phys) && lazy_man_matches(lm);
|
||
}
|
||
|
||
static int lazy_better_bundle(int li, int best) {
|
||
(void)li;
|
||
/* Candidate order is semantic for continuation-passing bundles: two DLLs
|
||
* can contain a byte-identical entry while resolving its internal tail
|
||
* targets against different region snapshots. Preserve the manifest index's
|
||
* established order and publish only its first live match. Comparing bundle
|
||
* size or timestamp can silently substitute an incompatible owner. */
|
||
return best < 0;
|
||
}
|
||
|
||
static int lazy_load_selected(int li) {
|
||
if (li < 0 || li >= s_lazy_man_n) return 0;
|
||
int ci = s_lazy_man[li].cache_idx;
|
||
if (ci < 0 || ci >= s_cache_idx_count || ci >= CACHE_IDX_CAP ||
|
||
s_cache_idx[ci].load_failed ||
|
||
s_cache_idx[ci].capacity_suppressed ||
|
||
dll_already_loaded(s_cache_idx[ci].path) ||
|
||
s_cache_idx[ci].func_count <= 0) return 0;
|
||
/* If proactive warming has not reached this fragment yet, prefer the
|
||
* historical synchronous path over running a potentially hot function in
|
||
* the interpreter. The worker drops its speculative reference safely. */
|
||
overlay_image_warm_cancel(ci);
|
||
s_last_file_found = 1;
|
||
int loaded = load_one_dll(s_cache_idx[ci].path, NULL) > 0;
|
||
if (!loaded) s_cache_idx[ci].load_failed = 1;
|
||
overlay_image_warm_release(ci);
|
||
return loaded;
|
||
}
|
||
|
||
static int try_load_region(uint32_t phys) {
|
||
extern uint32_t dirty_ram_get_bitmap_word(uint32_t word_index);
|
||
|
||
if (!overlay_loads_allowed())
|
||
return 0;
|
||
|
||
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, KERNEL_WINDOW_END), dirty boot-text [KERNEL_WINDOW_END, FLOOR), or
|
||
* overlay [FLOOR, end) — so a DLL's region_start is the first dirty page
|
||
* AT OR ABOVE the window floor. The walkback must apply the SAME clamp,
|
||
* chosen by PAGE (the capture windows are page-granular and FLOOR need
|
||
* not be page-aligned): an overlay-region walk stopping anywhere below
|
||
* FLOOR's page, or a boot-text walk crossing into the kernel window,
|
||
* yields a region_start that NO DLL filename matches — the overlay's
|
||
* DLLs never load and its functions interpret forever. */
|
||
uint32_t kern_pg = DIRTY_RAM_KERNEL_WINDOW_END / page_sz;
|
||
uint32_t ovl_pg = OVERLAY_REGION_FLOOR / page_sz;
|
||
uint32_t pg = phys / page_sz;
|
||
uint32_t floor_pg = (pg < kern_pg) ? 0u
|
||
: (pg < ovl_pg) ? kern_pg
|
||
: ovl_pg;
|
||
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)) mark_checked(region_start);
|
||
|
||
int select_attempts = 0;
|
||
retry_artifact:
|
||
;
|
||
/* Prefer the broadest fully coherent snapshot. If none has every function
|
||
* live, retain the historical first matching-function fallback. */
|
||
int best = -1;
|
||
int fallback = -1;
|
||
uint32_t bucket = (phys * 2654435761u) & LAZY_ENTRY_MASK;
|
||
for (int li = s_lazy_entry_head[bucket]; li >= 0;
|
||
li = s_lazy_man[li].next_entry) {
|
||
if ((s_lazy_man[li].fn.entry & 0x1FFFFFFFu) != phys) continue;
|
||
/* This function is reached only for a real runtime dispatch at phys
|
||
* (external entry or CPS continuation). The manifest supplies exact
|
||
* code identity, and cross-region recovery below additionally requires
|
||
* every directly-owned function in the CPS bundle to match. The dirty
|
||
* bitmap is sticky across streamed
|
||
* variants, so walkback can recover an older adjacent run's base
|
||
* (e.g. 0x106000 instead of an exact live 0x108000 shard). Rejecting
|
||
* the exact entry on that heuristic strands valid DLLs forever. */
|
||
if (!lazy_is_loadable(li, region_start, phys, 0)) continue;
|
||
int ci = s_lazy_man[li].cache_idx;
|
||
/* Cross-region recovery is safe only for a fully coherent CPS bundle.
|
||
* A partial exact-function match can have snapshot-specific internal
|
||
* tails, so retain it as fallback only when the heuristic base agrees. */
|
||
if (s_cache_idx[ci].region_start == region_start &&
|
||
lazy_candidate_preferred(li, fallback))
|
||
fallback = li;
|
||
/* Entry chains are newest-first/semantic. Preserve their established
|
||
* order instead of substituting a larger historical bundle. */
|
||
if (lazy_bundle_matches(ci) && lazy_candidate_preferred(li, best))
|
||
best = li;
|
||
}
|
||
|
||
uint32_t page = phys >> 12;
|
||
/* An exact live manifest is sufficient and semantically stronger than an
|
||
* enclosing range. Scanning every range link after finding one revalidated
|
||
* hundreds of broad historical bundles at streamed-stage transitions. */
|
||
if (fallback < 0 && page < RANGE_PAGE_COUNT) {
|
||
for (int ri = s_lazy_page_head[page]; ri >= 0;
|
||
ri = s_lazy_range_links[ri].next) {
|
||
int li = s_lazy_range_links[ri].cand;
|
||
if ((s_lazy_man[li].fn.entry & 0x1FFFFFFFu) == phys) continue;
|
||
/* Non-exact range ownership remains region-narrowed: unlike an
|
||
* exact manifested entry, an interior PC alone is not a unique
|
||
* identity for a reused-address CPS bundle. */
|
||
if (!lazy_is_loadable(li, region_start, phys, 1)) continue;
|
||
if (lazy_candidate_preferred(li, fallback)) fallback = li;
|
||
int ci = s_lazy_man[li].cache_idx;
|
||
if (lazy_bundle_matches(ci) && lazy_candidate_preferred(li, best))
|
||
best = li;
|
||
}
|
||
}
|
||
int selected = lazy_choose_complete_or_fallback(best, fallback);
|
||
if (selected < 0) return 0;
|
||
if (lazy_load_selected(selected)) return 1;
|
||
/* An unloadable/corrupt GCC artifact must not suppress an older immutable
|
||
* repair or the TCC fallback. lazy_load_selected marks the failed cache
|
||
* entry, so a bounded re-selection chooses the next live candidate. */
|
||
if (++select_attempts < s_cache_idx_count) goto retry_artifact;
|
||
return 0;
|
||
}
|
||
|
||
/* O(1)-bucket discriminator between a real manifested function entry and an
|
||
* interior CPS continuation. The latter must use its already-loaded range owner
|
||
* first (the Whoopee 0x107624 fix); the former must be allowed to publish its
|
||
* exact DLL even when a broader conservative owner contains the same PC. */
|
||
static int lazy_has_exact_entry(uint32_t phys) {
|
||
/* Same overlay-off hazard as overlay_find_by_range: the lazy entry index
|
||
* (s_lazy_entry_head) is -1-initialized only when overlay_cache is enabled;
|
||
* with overlay off it is zero-initialized and the link traversal below
|
||
* spins forever. Fail closed (no lazy entry) when the loader is inactive. */
|
||
if (!s_active) return 0;
|
||
uint32_t bucket = (phys * 2654435761u) & LAZY_ENTRY_MASK;
|
||
for (int li = s_lazy_entry_head[bucket]; li >= 0;
|
||
li = s_lazy_man[li].next_entry) {
|
||
if ((s_lazy_man[li].fn.entry & 0x1FFFFFFFu) == phys)
|
||
return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* ---- 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 range_candidate_matches(int i, uint32_t phys) {
|
||
Candidate *c = &s_cand[i];
|
||
if (c->state == ENTRY_BLACKLIST) return 0;
|
||
int contains = 0;
|
||
uint32_t first_range_lo = UINT32_MAX;
|
||
for (int r = 0; r < c->nranges; r++) {
|
||
uint32_t lo = c->range_lo[r];
|
||
if (lo < first_range_lo) first_range_lo = lo;
|
||
if (phys >= lo && phys < lo + c->range_len[r]) contains = 1;
|
||
}
|
||
/* Interior hosted aliases intentionally share their canonical host's full
|
||
* CRC/range identity, but their generated wrapper may contain only the
|
||
* blocks reachable from the alias. They are valid exact dispatch entries,
|
||
* never authoritative owners for an arbitrary CPS continuation elsewhere
|
||
* in the host range. Prefer the rooted host; if it is unavailable, fail
|
||
* closed to the interpreter instead of restarting through an alias. */
|
||
if (!contains || c->addr != first_range_lo) return 0;
|
||
|
||
/* A reused address can have several range-owning variants. Select the
|
||
* one whose compiled code bytes match live RAM instead of returning the
|
||
* first range hit and letting one stale variant mask every later match. */
|
||
uint32_t gen = cand_gensum(c);
|
||
if (c->state == ENTRY_VALID && gen == c->val_gen) {
|
||
s_gen_fastpath++;
|
||
return 1;
|
||
}
|
||
if (c->state == ENTRY_INVALID && gen == c->val_gen)
|
||
return 0; /* known mismatch, no watched write */
|
||
uint32_t live = cand_crc(c);
|
||
s_rehashes++;
|
||
s_last_crc = live;
|
||
c->val_gen = gen;
|
||
if (live == c->crc_code && cand_delay_slots_hashed(c)) {
|
||
if (c->state != ENTRY_VALID) {
|
||
c->state = ENTRY_VALID;
|
||
s_valid_count++;
|
||
}
|
||
return 1;
|
||
}
|
||
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++;
|
||
return 0;
|
||
}
|
||
|
||
static int range_candidate_preferred(int candidate, int current) {
|
||
if (current < 0) return 1;
|
||
if (s_cand[candidate].tier != s_cand[current].tier)
|
||
return s_cand[candidate].tier > s_cand[current].tier;
|
||
return candidate > current; /* newest same-tier repair */
|
||
}
|
||
|
||
static int overlay_find_by_range(uint32_t phys) {
|
||
/* Definitive guard for the overlay-off case: the range page index
|
||
* (s_range_page_head) is only initialized to -1 by overlay_loader_init,
|
||
* which runs solely when overlay_cache is enabled. With overlay off it is
|
||
* zero-initialized (0 = a valid index, not the -1 sentinel), so the link
|
||
* traversal below never terminates. Fail closed (no candidate) when the
|
||
* loader is inactive — there are no candidates to find anyway. */
|
||
if (!s_active) return -1;
|
||
uint32_t page = phys >> 12;
|
||
if (page >= RANGE_PAGE_COUNT) return -1;
|
||
|
||
RangePcCache *pc = &s_range_pc_cache[
|
||
(phys * 2654435761u) & RANGE_PC_CACHE_MASK];
|
||
if (pc->generation == s_range_candidate_generation &&
|
||
pc->cand >= 0 && pc->phys == phys &&
|
||
range_candidate_matches(pc->cand, phys))
|
||
return pc->cand;
|
||
|
||
int best = -1;
|
||
if (s_range_index_overflow) {
|
||
for (int i = 0; i < s_cand_n; i++)
|
||
if (range_candidate_matches(i, phys) &&
|
||
range_candidate_preferred(i, best))
|
||
best = i;
|
||
} else {
|
||
for (int li = s_range_page_head[page]; li >= 0;
|
||
li = s_range_links[li].next) {
|
||
int i = s_range_links[li].cand;
|
||
if (range_candidate_matches(i, phys) &&
|
||
range_candidate_preferred(i, best))
|
||
best = i;
|
||
}
|
||
}
|
||
pc->phys = phys;
|
||
pc->generation = s_range_candidate_generation;
|
||
pc->cand = best;
|
||
return best;
|
||
}
|
||
|
||
int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) {
|
||
uint32_t phys = addr & 0x1FFFFFFFu;
|
||
/* Overlay dispatch is a no-op when the overlay loader is inactive
|
||
* (overlay_cache=false): there are no candidates to match, so this must
|
||
* return 0 (dispatch to interp). This guard is also a HARD SAFETY:
|
||
* overlay_loader_init() — the ONLY place the range index s_range_page_head[]
|
||
* is initialized to -1 (empty) — runs only when overlay_cache is enabled
|
||
* (main.cpp). With overlay off, that array stays zero-initialized (0 is a
|
||
* VALID index, not the -1 sentinel), so the CPS continuation range lookup
|
||
* below (gated on g_psx_cps_mode, independent of s_active) would traverse a
|
||
* garbage index: s_range_page_head[page]=0 -> s_range_links[0].next=0 -> the
|
||
* loop never terminates (infinite spin in range_candidate_matches). Every
|
||
* overlay-off + CPS game hit this and wedged at boot before any game code ran
|
||
* (found via Ape Escape, the only overlay-off title). Fail closed here. */
|
||
if (!s_active) return 0;
|
||
if (overlay_cache_window_contains(phys) && lazy_miss_cached(phys)) {
|
||
s_disp_interp++;
|
||
return 0;
|
||
}
|
||
int lazy_loaded = 0;
|
||
retry_candidates:
|
||
int head = idx_head(phys);
|
||
int loaded_range_ci = -1;
|
||
int lazy_exact = 0;
|
||
int exact_needs_load = 0;
|
||
if (head < 0 && s_active && overlay_cache_window_contains(phys)) {
|
||
lazy_exact = head < 0 && lazy_has_exact_entry(phys);
|
||
/* A CPS continuation is normally not a registered function ENTRY. Its
|
||
* already-loaded range owner must be checked before lazy discovery:
|
||
* try_load_region otherwise scans the 12K+ manifest index and may load
|
||
* another duplicate bundle on every hot continuation edge. Whoopee's
|
||
* 0x80107624 path paid that cost thousands of times per frame. Exact
|
||
* manifested entries are different: a broader range owner must not mask
|
||
* their valid cached DLL (Tomba FMV's 0x80106424/0x80106688 case). */
|
||
if (head < 0 && g_psx_cps_mode)
|
||
loaded_range_ci = overlay_find_by_range(phys);
|
||
exact_needs_load = lazy_exact &&
|
||
(loaded_range_ci < 0 || s_cand[loaded_range_ci].device_touch);
|
||
if (head < 0 && (loaded_range_ci < 0 || exact_needs_load) &&
|
||
!lazy_loaded && try_load_region(phys)) {
|
||
lazy_loaded = 1;
|
||
goto retry_candidates;
|
||
}
|
||
/* (sljit removed 2026-07-15: the JIT-on-miss gap-fill — off-thread
|
||
* enqueue or synchronous compile — ran here. Misses fall to interp.) */
|
||
}
|
||
|
||
/* 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 = loaded_range_ci >= 0 ? loaded_range_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 && cand_delay_slots_hashed(c));
|
||
}
|
||
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; }
|
||
/* Diff instrument — same contract as the entry chain's want_diff
|
||
* gate below. A continuation re-entry must NOT run native blind
|
||
* while its candidate is still inside the verify budget: CPS
|
||
* interiors dominate dispatch, so without this gate ~all native
|
||
* execution bypassed the diff harness (shadow_calls=1 over 51K
|
||
* frames while gen_fastpath took ~600M hits). Interiors are
|
||
* validated transitively: the entry-level shadow diff runs the
|
||
* WHOLE function natively (in_own_shadow below), so until the
|
||
* candidate passes its budget its interiors take the interpreter
|
||
* (the authority). Inside the candidate's OWN shadow native pass
|
||
* interiors DO run native — that IS the diff exercising the
|
||
* continuation blocks; the blocklist is not consulted there,
|
||
* matching the entry gate (which shadow-diffs blocked candidates
|
||
* too — the blocklist blocks LIVE native, not the sandbox). */
|
||
int in_own_shadow = (s_in_shadow && (const void *)c == s_shadow_cand);
|
||
if (!in_own_shadow) {
|
||
int want_diff = s_diff_mode && cand_selected_for_diff(c);
|
||
if (want_diff && addr < 0x10000u) want_diff = 0;
|
||
if (want_diff && (s_diff_addr || c->diff_passes < OVERLAY_DIFF_BUDGET)) {
|
||
if (_probe) s_cps_probe_outcome = 5;
|
||
s_diffgate_interp++;
|
||
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; }
|
||
#ifndef PSX_NO_DEBUG_TOOLS
|
||
if (!native_rank_allows(c, addr))
|
||
{ if (_probe) s_cps_probe_outcome = 7; s_would_run_native++; s_disp_interp++; return 0; }
|
||
#endif
|
||
}
|
||
if (_probe) s_cps_probe_outcome = 2;
|
||
/* Native-call ring: ALWAYS-ON (production too). Each entry names
|
||
* the requested PC and the candidate CRC that claimed it — the
|
||
* only record that can attribute a wrong-variant native run on
|
||
* a production binary (Tomba 2 splash reload loop). */
|
||
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++;
|
||
native_hot_note(c->addr);
|
||
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 */
|
||
{
|
||
int prev_phase = g_exec_phase;
|
||
OverlayFlushFn prev_flush = overlay_flush_enter(c);
|
||
g_exec_phase = 2;
|
||
c->fn(cpu);
|
||
overlay_flush_leave(prev_flush);
|
||
g_exec_phase = prev_phase;
|
||
}
|
||
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 if (c->state == ENTRY_INVALID && gen == c->val_gen) {
|
||
/* This exact byte identity already failed after the most recent
|
||
* watched write. Re-hashing it on every dispatch makes reused
|
||
* variant chains scale with cache history instead of live code. */
|
||
continue;
|
||
} else {
|
||
uint32_t live = cand_crc(c);
|
||
s_rehashes++;
|
||
s_last_crc = live;
|
||
c->val_gen = gen;
|
||
matched = (live == c->crc_code && cand_delay_slots_hashed(c));
|
||
}
|
||
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).
|
||
* gcc/DLL candidates are the trusted tier (validated at dev time) and
|
||
* run native directly; they are diffed only in explicit dev diff mode
|
||
* (PSX_OVERLAY_DIFF / overlay_diff cmd). */
|
||
int want_diff = s_diff_mode && cand_selected_for_diff(c);
|
||
/* Kernel-window candidates (call gates 0xA0/0xB0/0xC0 and the RAM
|
||
* kernel) are NOT shadow-diffable: the gates tail-jump via RAM
|
||
* tables into kernel SERVICES whose behavior depends on scheduler/
|
||
* event state and can block (TestEvent/WaitEvent) — the two passes
|
||
* legitimately run different instruction paths and every call
|
||
* "diverges" (observed: 2016/2017 calls, all 0xB0, flooding the
|
||
* ring). The diff harness validates OVERLAY shard codegen against
|
||
* the interp oracle; kernel gates stay on their normal route. */
|
||
if (want_diff && addr < 0x10000u) want_diff = 0;
|
||
if (want_diff && !s_in_shadow &&
|
||
(s_diff_addr || c->diff_passes < OVERLAY_DIFF_BUDGET)) {
|
||
/* NEVER start a shadow inside an exception dispatch: the guest's
|
||
* ReturnFromException longjmps to the setjmp frame BELOW us
|
||
* (psx_check_interrupts), unwinding run_shadow_diff WITHOUT its
|
||
* epilogue — s_in_shadow/s_native_exec/s_suppress_irq stay stuck
|
||
* and the diff instrument is dead for the rest of the run
|
||
* (observed: shadow_calls=1 over 51K frames). Exceptions ENTERED
|
||
* during a shadow are contained — their setjmp is armed inside
|
||
* the shadow frame — so gating the START is structurally
|
||
* sufficient (every psx_exception_longjmp site is guarded by
|
||
* psx_get_in_exception()). The candidate still must not run
|
||
* native unvalidated: route to the interpreter (the authority);
|
||
* it gets diffed at its next non-exception dispatch. */
|
||
extern int psx_get_in_exception(void);
|
||
if (psx_get_in_exception()) {
|
||
s_diffgate_interp++;
|
||
s_disp_interp++;
|
||
return 0;
|
||
}
|
||
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; }
|
||
#ifndef PSX_NO_DEBUG_TOOLS
|
||
if (!native_rank_allows(c, addr))
|
||
{ s_would_run_native++; s_disp_interp++; return 0; }
|
||
#endif
|
||
|
||
/* Record into the always-on ring BEFORE the call; mark in-progress
|
||
* so a freeze inside this fn is visible at dump time. */
|
||
/* Native-call ring: ALWAYS-ON (see the range-chain site). */
|
||
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++;
|
||
native_hot_note(c->addr);
|
||
|
||
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). */
|
||
#ifndef PSX_NO_DEBUG_TOOLS
|
||
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);
|
||
#endif
|
||
{
|
||
int prev_phase = g_exec_phase;
|
||
OverlayFlushFn prev_flush = overlay_flush_enter(c);
|
||
g_exec_phase = 2;
|
||
c->fn(cpu);
|
||
overlay_flush_leave(prev_flush);
|
||
g_exec_phase = prev_phase;
|
||
}
|
||
overlay_post_dispatch_irq_pump(cpu);
|
||
#ifndef PSX_NO_DEBUG_TOOLS
|
||
dirty_ram_log_marker(c->addr, mtag, 1);
|
||
#endif
|
||
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++;
|
||
}
|
||
}
|
||
|
||
/* Publish at most one cached DLL and retry this same dispatch once. This
|
||
* preserves additive variant coverage without turning one guest transition
|
||
* into an unbounded synchronous LoadLibrary loop. */
|
||
if (!lazy_loaded && s_active && overlay_cache_window_contains(phys) &&
|
||
try_load_region(phys)) {
|
||
lazy_loaded = 1;
|
||
goto retry_candidates;
|
||
}
|
||
|
||
if (overlay_cache_window_contains(phys)) lazy_miss_record(phys);
|
||
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;
|
||
}
|
||
|
||
void overlay_loader_get_load_timing(uint64_t *total_us, uint64_t *max_us,
|
||
uint64_t *last_us) {
|
||
if (total_us) *total_us = s_load_total_us;
|
||
if (max_us) *max_us = s_load_max_us;
|
||
if (last_us) *last_us = s_load_last_us;
|
||
}
|
||
|
||
void overlay_loader_take_hot_native(uint32_t *pc, uint64_t *calls) {
|
||
uint32_t best_pc = 0;
|
||
uint64_t best_calls = 0;
|
||
if (s_native_hot_enabled) {
|
||
for (uint32_t i = 0; i < NATIVE_HOT_CAP; i++) {
|
||
if (s_native_hot[i].calls > best_calls) {
|
||
best_pc = s_native_hot[i].pc;
|
||
best_calls = s_native_hot[i].calls;
|
||
}
|
||
s_native_hot[i].calls = 0;
|
||
}
|
||
}
|
||
if (pc) *pc = best_pc;
|
||
if (calls) *calls = best_calls;
|
||
}
|
||
|
||
/* 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; }
|
||
|
||
/* 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; }
|
||
int overlay_loader_range_link_count(void) { return s_range_link_n; }
|
||
int overlay_loader_range_index_overflow(void) { return s_range_index_overflow; }
|
||
int overlay_loader_lazy_manifest_count(void) { return s_lazy_man_n; }
|
||
int overlay_loader_lazy_manifest_overflow(void) { return s_lazy_man_overflow; }
|
||
uint64_t overlay_loader_candidate_overflow(void) { return s_cand_overflow; }
|
||
uint64_t overlay_loader_pair_aliases(void) { return s_pair_aliases; }
|
||
|
||
/* (sljit removed 2026-07-15: overlay_loader_sljit_obsoleted and the
|
||
* overlay_loader_sljit_probe one-shot JIT helper lived here.) */
|
||
|
||
/* ---- 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) {
|
||
if (!s_active) return 0;
|
||
return exact_entry_has(phys);
|
||
}
|
||
|
||
int overlay_fp_enabled(void) {
|
||
static int enabled = -1;
|
||
if (enabled < 0) {
|
||
const char *e = getenv("PSX_OVERLAY_FP_LOG");
|
||
enabled = (e && e[0] && e[0] != '0') ? 1 : 0;
|
||
}
|
||
return enabled;
|
||
}
|
||
|
||
/* ---- 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], s_ramN[SHADOW_RAM_SIZE], s_ramI[SHADOW_RAM_SIZE];
|
||
static uint8_t s_spad0[SHADOW_SPAD_SIZE], s_spadI[SHADOW_SPAD_SIZE];
|
||
static uint64_t s_shadow_skipped_dev = 0; /* unsafe/incomplete traces skipped */
|
||
/* s_diff_mode / s_in_shadow declared above (before dispatch). */
|
||
|
||
typedef struct {
|
||
uint64_t seq; uint32_t addr;
|
||
int kind; /* architectural/trace difference class */
|
||
int index; /* register/array index, or -1 */
|
||
uint64_t cycle_native, cycle_interp;
|
||
uint32_t trace_ops, replay_ops;
|
||
int trace_kind;
|
||
uint32_t trace_pc, trace_addr, trace_expected, trace_actual;
|
||
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_get_shadow_summary(uint64_t *calls, uint64_t *divergences,
|
||
uint32_t *first_divergence_pc) {
|
||
if (calls) *calls = s_shadow_calls;
|
||
if (divergences) *divergences = s_shadow_divs;
|
||
if (first_divergence_pc)
|
||
*first_divergence_pc = s_detail_captured ? s_detail_addr : 0;
|
||
}
|
||
|
||
void overlay_loader_set_diff_mode(int on) { s_diff_mode = on ? 1 : 0; }
|
||
|
||
static void run_shadow_diff_legacy(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();
|
||
|
||
extern uint64_t psx_exception_setjmp_epoch(void);
|
||
s_in_shadow = 1;
|
||
s_shadow_epoch = psx_exception_setjmp_epoch();
|
||
int saved_supp = s_suppress_irq;
|
||
s_suppress_irq = 1; /* isolate computation; longjmp-safe */
|
||
s_shadow_saved_supp = saved_supp; /* escape-fixup mirror (see decl) */
|
||
s_shadow_saved_mmio_watch = g_shadow_mmio_watch;
|
||
s_shadow_saved_exec_phase = g_exec_phase;
|
||
s_shadow_scheduler_bail = 0;
|
||
/* 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;
|
||
s_shadow_saved_native_exec = sv; /* escape-fixup mirror (see decl) */
|
||
|
||
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). 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 */
|
||
/* Arm the own-interior native route for the NATIVE pass only (see
|
||
* s_shadow_cand decl): the candidate's CPS continuation re-entries run
|
||
* native so the diff exercises every block of the function, not just the
|
||
* first segment (nested CALLS still run interp on both passes —
|
||
* s_native_exec=0 above). Never armed during the interp pass, which must
|
||
* stay pure interp. */
|
||
s_shadow_cand = c;
|
||
/* Preserve the authoritative interpreter's projection provenance while
|
||
* making every host-only cache read miss and every cache write a no-op for
|
||
* the speculative native pass. */
|
||
gte_precision_speculative_begin();
|
||
{
|
||
int prev_phase = g_exec_phase;
|
||
OverlayFlushFn prev_flush = overlay_flush_enter(c);
|
||
g_exec_phase = 2;
|
||
c->fn(cpu);
|
||
overlay_flush_leave(prev_flush);
|
||
g_exec_phase = prev_phase;
|
||
}
|
||
/* 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;
|
||
int prev_phase = g_exec_phase;
|
||
g_exec_phase = 3; /* compiled route; dirty/native callees re-tag inside */
|
||
psx_dispatch_call(cpu, tv, stop_ra);
|
||
g_exec_phase = prev_phase;
|
||
}
|
||
}
|
||
s_shadow_cand = NULL;
|
||
gte_precision_speculative_end();
|
||
CPUState cpuN = *cpu;
|
||
memcpy(s_ramN, ram, SHADOW_RAM_SIZE);
|
||
int scheduler_bail = s_shadow_scheduler_bail;
|
||
|
||
/* 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 (!scheduler_bail && 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 (scheduler_bail && c->state != ENTRY_BLACKLIST) {
|
||
/* The interpreter pass completed without switching threads, so a
|
||
* native-only scheduler attempt proves control-flow divergence.
|
||
* Permanently keep this shard off the live path. */
|
||
c->state = ENTRY_BLACKLIST;
|
||
if (s_valid_count > 0) s_valid_count--;
|
||
loader_log("blacklist shadow scheduler divergence 0x%08X", c->addr);
|
||
}
|
||
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;
|
||
}
|
||
/* Ring-flood guard: one persistently-diverging site must not evict
|
||
* every other site's records (512x 0xB0 drowned the 0xF514 hunt).
|
||
* Cap records per address; the divergence COUNTER still increments. */
|
||
int addr_recs = 0;
|
||
for (int i = 0; i < s_sdiv_n; i++)
|
||
if (s_sdiv[i].addr == c->addr && ++addr_recs >= 16) break;
|
||
if (s_sdiv_n < SDIV_CAP && addr_recs < 16) {
|
||
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;
|
||
}
|
||
|
||
enum {
|
||
SHDIFF_NONE = 0, SHDIFF_PC, SHDIFF_GPR, SHDIFF_HI, SHDIFF_LO,
|
||
SHDIFF_COP0, SHDIFF_GTE_DATA, SHDIFF_GTE_CTRL,
|
||
SHDIFF_MULDIV_TS, SHDIFF_GTE_TS, SHDIFF_READ_ABSORB,
|
||
SHDIFF_READ_WHICH, SHDIFF_READ_FUDGE, SHDIFF_LD_WHICH,
|
||
SHDIFF_LD_ABSORB, SHDIFF_CYCLE, SHDIFF_TRACE, SHDIFF_SETUP,
|
||
SHDIFF_SCHED
|
||
};
|
||
|
||
static int shadow_cpu_diff(const CPUState *native, const CPUState *interp,
|
||
int *index, uint32_t *nv, uint32_t *iv) {
|
||
#define SHCMP_SCALAR(kind_, field_) do { \
|
||
if (native->field_ != interp->field_) { \
|
||
*index = -1; *nv = (uint32_t)native->field_; *iv = (uint32_t)interp->field_; \
|
||
return (kind_); \
|
||
} \
|
||
} while (0)
|
||
#define SHCMP_ARRAY(kind_, field_, count_) do { \
|
||
for (int _i = 0; _i < (count_); _i++) { \
|
||
if (native->field_[_i] != interp->field_[_i]) { \
|
||
*index = _i; *nv = (uint32_t)native->field_[_i]; \
|
||
*iv = (uint32_t)interp->field_[_i]; return (kind_); \
|
||
} \
|
||
} \
|
||
} while (0)
|
||
if (native->pc != interp->pc) {
|
||
*index = -1; *nv = native->pc; *iv = interp->pc; return SHDIFF_PC;
|
||
}
|
||
SHCMP_ARRAY(SHDIFF_GPR, gpr, 32);
|
||
SHCMP_SCALAR(SHDIFF_HI, hi);
|
||
SHCMP_SCALAR(SHDIFF_LO, lo);
|
||
SHCMP_ARRAY(SHDIFF_COP0, cop0, 32);
|
||
SHCMP_ARRAY(SHDIFF_GTE_DATA, gte_data, 32);
|
||
SHCMP_ARRAY(SHDIFF_GTE_CTRL, gte_ctrl, 32);
|
||
SHCMP_SCALAR(SHDIFF_MULDIV_TS, muldiv_ts_done);
|
||
SHCMP_SCALAR(SHDIFF_GTE_TS, gte_ts_done);
|
||
SHCMP_ARRAY(SHDIFF_READ_ABSORB, read_absorb, 33);
|
||
SHCMP_SCALAR(SHDIFF_READ_WHICH, read_absorb_which);
|
||
SHCMP_SCALAR(SHDIFF_READ_FUDGE, read_fudge);
|
||
SHCMP_SCALAR(SHDIFF_LD_WHICH, ld_which_t);
|
||
SHCMP_SCALAR(SHDIFF_LD_ABSORB, ld_absorb);
|
||
#undef SHCMP_ARRAY
|
||
#undef SHCMP_SCALAR
|
||
*index = -1; *nv = 0; *iv = 0;
|
||
return SHDIFF_NONE;
|
||
}
|
||
|
||
/* Authoritative interpreter RECORD + side-effect-free native REPLAY.
|
||
* Memory reads are replayed and writes verified/no-op, the replay clock advances
|
||
* without servicing devices, and GTE visual/diagnostic sidecars are sandboxed.
|
||
* Thus RAM/MMIO/devices mutate exactly once while the comparison covers the full
|
||
* CPU architectural and timing state plus the ordered memory-operation trace. */
|
||
static void run_shadow_diff(CPUState *cpu, Candidate *c, uint32_t addr) {
|
||
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 gte_replay_side_effects_begin(void);
|
||
extern void gte_replay_side_effects_end(void);
|
||
extern uint32_t g_debug_current_func_addr;
|
||
extern uint64_t psx_exception_setjmp_epoch(void);
|
||
extern int g_shadow_mmio_watch;
|
||
|
||
s_in_shadow = 1;
|
||
s_shadow_epoch = psx_exception_setjmp_epoch();
|
||
int saved_supp = s_suppress_irq;
|
||
int saved_native = s_native_exec;
|
||
s_suppress_irq = 1;
|
||
s_native_exec = 0;
|
||
s_shadow_saved_supp = saved_supp;
|
||
s_shadow_saved_native_exec = saved_native;
|
||
s_shadow_saved_mmio_watch = g_shadow_mmio_watch;
|
||
s_shadow_saved_exec_phase = g_exec_phase;
|
||
s_shadow_saved_call_bail = g_psx_call_bail;
|
||
s_shadow_saved_debug_func = g_debug_current_func_addr;
|
||
s_shadow_saved_irq_calls = s_irq_callcount;
|
||
s_shadow_saved_irq_supp = s_irq_suppressed;
|
||
s_shadow_scheduler_bail = 0;
|
||
|
||
CPUState cpu0 = *cpu;
|
||
uint64_t cycle0 = psx_get_cycle_count();
|
||
uint32_t stop_ra = cpu0.gpr[31];
|
||
uint32_t record_ops = 0, replay_ops = 0;
|
||
int saw_exception = 0;
|
||
int icache_record_ok = psx_icache_shadow_record_begin();
|
||
int record_owned = 0;
|
||
int record_ok = icache_record_ok && ls_shadow_record_begin();
|
||
if (record_ok) {
|
||
record_owned = 1;
|
||
dirty_ram_dispatch(cpu, addr, stop_ra);
|
||
record_ok = ls_shadow_record_end(&record_ops, &saw_exception);
|
||
record_owned = 0;
|
||
} else {
|
||
dirty_ram_dispatch(cpu, addr, stop_ra);
|
||
}
|
||
s_shadow_calls++;
|
||
|
||
CPUState cpuI = *cpu;
|
||
uint64_t cycleI = psx_get_cycle_count();
|
||
if (!record_ok || saw_exception) {
|
||
s_shadow_skipped_dev++;
|
||
if (record_owned) ls_shadow_abort();
|
||
psx_icache_shadow_abort();
|
||
s_native_exec = saved_native;
|
||
s_suppress_irq = saved_supp;
|
||
s_in_shadow = 0;
|
||
return;
|
||
}
|
||
|
||
CPUState cpuN = cpu0;
|
||
int trace_kind = 0;
|
||
uint32_t trace_pc = 0, trace_addr = 0, trace_expected = 0, trace_actual = 0;
|
||
int replay_owned = 0;
|
||
int replay_ok = ls_shadow_replay_begin();
|
||
if (replay_ok) replay_owned = 1;
|
||
int icache_ok = replay_ok && psx_icache_shadow_replay_begin();
|
||
int clock_ok = icache_ok && psx_cycle_replay_begin(cycle0);
|
||
int gte_ok = clock_ok && gte_replay_side_effects_begin();
|
||
uint32_t saved_debug_func = g_debug_current_func_addr;
|
||
uint32_t saved_irq_callcount = s_irq_callcount;
|
||
uint64_t saved_irq_suppressed = s_irq_suppressed;
|
||
int saved_bail = g_psx_call_bail;
|
||
|
||
if (gte_ok) {
|
||
s_shadow_cand = c;
|
||
{
|
||
int prev_phase = g_exec_phase;
|
||
OverlayFlushFn prev_flush = overlay_flush_enter(c);
|
||
g_exec_phase = 2;
|
||
c->fn(&cpuN);
|
||
overlay_flush_leave(prev_flush);
|
||
g_exec_phase = prev_phase;
|
||
}
|
||
{
|
||
int guard = 0;
|
||
while (cpuN.pc != 0 && !g_psx_call_bail && guard++ < 8192) {
|
||
uint32_t tv = cpuN.pc;
|
||
if ((tv & 0x1FFFFFFFu) == (stop_ra & 0x1FFFFFFFu)) break;
|
||
cpuN.pc = 0;
|
||
int prev_phase = g_exec_phase;
|
||
g_exec_phase = 3;
|
||
psx_dispatch_call(&cpuN, tv, stop_ra);
|
||
g_exec_phase = prev_phase;
|
||
}
|
||
}
|
||
s_shadow_cand = NULL;
|
||
}
|
||
int scheduler_bail = s_shadow_scheduler_bail;
|
||
|
||
if (gte_ok) gte_replay_side_effects_end();
|
||
uint64_t cycleN = clock_ok ? psx_cycle_replay_end() : cycle0;
|
||
psx_icache_shadow_replay_end();
|
||
if (replay_ok) {
|
||
replay_ok = ls_shadow_replay_end(&replay_ops, &trace_kind, &trace_pc,
|
||
&trace_addr, &trace_expected, &trace_actual);
|
||
replay_owned = 0;
|
||
} else if (replay_owned) {
|
||
ls_shadow_abort();
|
||
replay_owned = 0;
|
||
}
|
||
psx_icache_shadow_abort();
|
||
g_debug_current_func_addr = saved_debug_func;
|
||
s_irq_callcount = saved_irq_callcount;
|
||
s_irq_suppressed = saved_irq_suppressed;
|
||
g_psx_call_bail = saved_bail;
|
||
|
||
int index = -1;
|
||
uint32_t native_value = 0, interp_value = 0;
|
||
int kind = (!gte_ok || !replay_ok) ? SHDIFF_TRACE :
|
||
shadow_cpu_diff(&cpuN, &cpuI, &index, &native_value, &interp_value);
|
||
if (kind == SHDIFF_NONE && cycleN - cycle0 != cycleI - cycle0) {
|
||
kind = SHDIFF_CYCLE;
|
||
native_value = (uint32_t)(cycleN - cycle0);
|
||
interp_value = (uint32_t)(cycleI - cycle0);
|
||
}
|
||
if (kind == SHDIFF_NONE && scheduler_bail) kind = SHDIFF_SCHED;
|
||
|
||
if (kind == SHDIFF_NONE) {
|
||
if (c->diff_passes < OVERLAY_DIFF_BUDGET) c->diff_passes++;
|
||
} else {
|
||
c->diff_passes = 0;
|
||
c->device_touch = 1; /* fail closed: all later calls stay interpreted */
|
||
s_shadow_divs++;
|
||
if (scheduler_bail && c->state != ENTRY_BLACKLIST) {
|
||
/* The interpreter pass completed without switching threads, so a
|
||
* native-only scheduler attempt proves control-flow divergence.
|
||
* Permanently keep this shard off the live path. */
|
||
c->state = ENTRY_BLACKLIST;
|
||
if (s_valid_count > 0) s_valid_count--;
|
||
loader_log("blacklist shadow scheduler divergence 0x%08X", c->addr);
|
||
}
|
||
if (!s_detail_captured) {
|
||
s_detail_captured = 1;
|
||
s_detail_addr = c->addr;
|
||
memcpy(s_detail_nat_gpr, cpuN.gpr, sizeof s_detail_nat_gpr);
|
||
memcpy(s_detail_int_gpr, cpuI.gpr, sizeof s_detail_int_gpr);
|
||
s_detail_nat_hi = cpuN.hi; s_detail_nat_lo = cpuN.lo;
|
||
s_detail_int_hi = cpuI.hi; s_detail_int_lo = cpuI.lo;
|
||
}
|
||
int addr_recs = 0;
|
||
for (int i = 0; i < s_sdiv_n; i++)
|
||
if (s_sdiv[i].addr == c->addr && ++addr_recs >= 16) break;
|
||
if (s_sdiv_n < SDIV_CAP && addr_recs < 16) {
|
||
ShadowDiv *d = &s_sdiv[s_sdiv_n++];
|
||
memset(d, 0, sizeof *d);
|
||
d->seq = s_shadow_calls; d->addr = c->addr;
|
||
d->kind = kind; d->index = index;
|
||
d->cycle_native = cycleN - cycle0;
|
||
d->cycle_interp = cycleI - cycle0;
|
||
d->trace_ops = record_ops; d->replay_ops = replay_ops;
|
||
d->trace_kind = trace_kind; d->trace_pc = trace_pc;
|
||
d->trace_addr = trace_addr; d->trace_expected = trace_expected;
|
||
d->trace_actual = trace_actual;
|
||
d->reg = kind == SHDIFF_GPR ? index : -1;
|
||
d->reg_native = native_value; d->reg_interp = interp_value;
|
||
d->hi_diff = kind == SHDIFF_HI; d->lo_diff = kind == SHDIFF_LO;
|
||
d->ram_off = -1;
|
||
}
|
||
}
|
||
|
||
s_native_exec = saved_native;
|
||
s_suppress_irq = saved_supp;
|
||
s_in_shadow = 0;
|
||
}
|
||
|
||
/* A nonlocal scheduler bail during the speculative native replay pass (see
|
||
* overlay_loader_shadow_native_thread_switch_bail) means the interpreter pass
|
||
* — authoritative and already complete by the time the native replay runs —
|
||
* did NOT itself switch threads at this point, so a native-only switch attempt
|
||
* proves the native shard's control flow diverges. This is stronger than an
|
||
* ordinary register/RAM mismatch: permanently blacklist the shard rather than
|
||
* merely resetting its verify budget, mirroring the legacy comparator's
|
||
* handling of the same signal. */
|
||
static void shadow_escape_cleanup(void) {
|
||
extern int g_shadow_mmio_watch;
|
||
if (s_shadow_cand != NULL) {
|
||
Candidate *escaped = (Candidate *)s_shadow_cand;
|
||
escaped->diff_passes = 0;
|
||
escaped->device_touch = 1; /* native escape is unverified: fail closed */
|
||
}
|
||
{ extern void gte_replay_side_effects_end(void); gte_replay_side_effects_end(); }
|
||
(void)psx_cycle_replay_end();
|
||
psx_icache_shadow_abort();
|
||
ls_shadow_abort();
|
||
s_shadow_escapes++;
|
||
if (s_shadow_cand != NULL) {
|
||
s_shadow_escapes_native++;
|
||
gte_precision_speculative_end();
|
||
}
|
||
/* The interpreter pass arms the MMIO detector around dirty dispatch. A
|
||
* nonlocal scheduler/exception escape can skip the matching decrement. */
|
||
g_shadow_mmio_watch = s_shadow_saved_mmio_watch;
|
||
s_in_shadow = 0;
|
||
s_shadow_cand = NULL;
|
||
s_native_exec = s_shadow_saved_native_exec;
|
||
s_suppress_irq = s_shadow_saved_supp;
|
||
g_exec_phase = s_shadow_saved_exec_phase;
|
||
g_psx_call_bail = s_shadow_saved_call_bail;
|
||
{ extern uint32_t g_debug_current_func_addr;
|
||
g_debug_current_func_addr = s_shadow_saved_debug_func; }
|
||
s_irq_callcount = s_shadow_saved_irq_calls;
|
||
s_irq_suppressed = s_shadow_saved_irq_supp;
|
||
}
|
||
|
||
static void shadow_escape_flush_cycles(void) {
|
||
/* A host unwind can skip a candidate call's normal epilogue. Commit any
|
||
* cycles accumulated before the callback and clear the active DLL store
|
||
* hook so later static/interpreted writes cannot address stale context. */
|
||
if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles();
|
||
g_overlay_flush_pending_cycles = NULL;
|
||
}
|
||
|
||
/* Called by deferred_exception_longjmp() (interrupts.c) before it unwinds.
|
||
* If the longjmp target frame predates a live shadow run (target_epoch <=
|
||
* s_shadow_epoch), the unwind blows through run_shadow_diff and its epilogue
|
||
* never runs — restore the shadow-scoped globals here and count the escape.
|
||
* The dispatch gate (no shadow start while in an exception dispatch) makes
|
||
* this structurally unreachable; if the counters ever move, that invariant is
|
||
* broken and must be investigated. An escape during the NATIVE pass
|
||
* (s_shadow_cand set) additionally means speculative native state leaked into
|
||
* the live timeline — counted separately; it must stay 0. A longjmp to a
|
||
* frame armed AFTER shadow start (target_epoch > s_shadow_epoch) is an
|
||
* exception contained inside the shadow: it lands inside the shadow frame and
|
||
* the shadow continues — the flags must NOT be touched. */
|
||
void overlay_loader_shadow_escape_fixup(uint64_t target_epoch) {
|
||
shadow_escape_flush_cycles();
|
||
if (!s_in_shadow) return;
|
||
if (target_epoch > s_shadow_epoch) return; /* contained: leave armed */
|
||
shadow_escape_cleanup();
|
||
}
|
||
|
||
/* The deterministic HLE scheduler uses a different jmp_buf and therefore has
|
||
* no exception-setjmp epoch. Its landing calls this after every structured
|
||
* escape; a live shadow frame was necessarily abandoned in full. */
|
||
void overlay_loader_shadow_scheduler_escape_fixup(void) {
|
||
shadow_escape_flush_cycles();
|
||
if (s_in_shadow) shadow_escape_cleanup();
|
||
}
|
||
|
||
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,\"interior_gated\":%llu,"
|
||
"\"in_shadow\":%d,\"native_exec\":%d,"
|
||
"\"escapes\":%u,\"escapes_native\":%u,\"records\":[",
|
||
s_diff_mode, (unsigned long long)s_shadow_calls,
|
||
(unsigned long long)s_shadow_divs,
|
||
(unsigned long long)s_shadow_skipped_dev,
|
||
(unsigned long long)s_diffgate_interp,
|
||
s_in_shadow, s_native_exec,
|
||
s_shadow_escapes, s_shadow_escapes_native);
|
||
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\",\"kind\":%d,\"index\":%d,\"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\","
|
||
"\"cycle_native\":%llu,\"cycle_interp\":%llu,"
|
||
"\"trace_ops\":%u,\"replay_ops\":%u,\"trace_kind\":%d,"
|
||
"\"trace_pc\":\"0x%08X\",\"trace_addr\":\"0x%08X\","
|
||
"\"trace_expected\":\"0x%08X\",\"trace_actual\":\"0x%08X\"}",
|
||
i ? "," : "", (unsigned long long)d->seq, d->addr,
|
||
d->kind, d->index, 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,
|
||
(unsigned long long)d->cycle_native,
|
||
(unsigned long long)d->cycle_interp,
|
||
d->trace_ops, d->replay_ops, d->trace_kind,
|
||
d->trace_pc, d->trace_addr, d->trace_expected, d->trace_actual);
|
||
}
|
||
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_active || !s_native_exec)
|
||
return 0; /* inactive/interp mode: keep the legacy inline path */
|
||
uint32_t phys = addr & 0x1FFFFFFFu;
|
||
if (idx_head(phys) < 0 && !lazy_has_exact_entry(phys))
|
||
return 0; /* neither a registered nor an exact cached entry */
|
||
uint32_t in_regs[34];
|
||
int fp = overlay_fp_enabled();
|
||
if (fp) overlay_regs_snap(in_regs, cpu);
|
||
/* Run the callee as an atomic UNIT: IRQ delivery is deferred inside it (both
|
||
* the overlay CI wrappers and the dirty IRQ pumps check g_call_unit_depth) so
|
||
* a cooperative ChangeThread cannot suspend the interrupted thread mid-callee
|
||
* with an inconsistent (resume_pc, sp) snapshot. Restore (not just decrement)
|
||
* so a bail/longjmp-out unwinds the depth correctly; the scheduler landing
|
||
* also resets it to 0 as a backstop. Gated by the A/B toggle. */
|
||
int prev_unit_depth = g_call_unit_depth;
|
||
if (overlay_unit_defer_enabled()) g_call_unit_depth = prev_unit_depth + 1;
|
||
int ran = overlay_loader_dispatch(cpu, addr);
|
||
g_call_unit_depth = prev_unit_depth;
|
||
if (!ran) return 0;
|
||
if (fp) overlay_fp_log(addr, in_regs, cpu, 1);
|
||
return 1;
|
||
}
|
||
|
||
/* (sljit removed 2026-07-15: the JIT-shard host helpers psx_sljit_call /
|
||
* psx_sljit_cop2 / psx_sljit_memx lived here. They were called only by sljit-
|
||
* emitted shards via the cpu->sljit_helpers table, which is no longer wired.) */
|
||
|
||
/* 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;
|
||
}
|
||
|
||
/* Focused form for live miss diagnosis. The full candidate table can exceed the
|
||
* debug command's response buffer once a game has accumulated many variants;
|
||
* filtering by entry keeps every candidate at a reused PC visible. */
|
||
int overlay_loader_dump_candidates_at(uint32_t addr, char *out, int cap) {
|
||
uint32_t phys = addr & 0x1FFFFFFFu;
|
||
int n = 0;
|
||
int first = 1;
|
||
n += snprintf(out + n, cap - n, "[");
|
||
for (int i = 0; i < s_cand_n && n < cap - 180; i++) {
|
||
Candidate *c = &s_cand[i];
|
||
if (c->addr != phys) continue;
|
||
uint32_t live = cand_crc(c);
|
||
uint32_t sum = cand_gensum(c);
|
||
n += snprintf(out + n, cap - n,
|
||
"%s{\"index\":%d,\"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,"
|
||
"\"device_touch\":%d}",
|
||
first ? "" : ",", 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, c->device_touch);
|
||
first = 0;
|
||
}
|
||
n += snprintf(out + n, cap - n, "]");
|
||
return n;
|
||
}
|
||
|
||
/* Focused view of manifest-only candidates that have not been loaded yet. */
|
||
int overlay_loader_dump_lazy_at(uint32_t addr, char *out, int cap) {
|
||
uint32_t phys = addr & 0x1FFFFFFFu;
|
||
uint32_t pg = phys >> 12;
|
||
uint32_t kern_pg = DIRTY_RAM_KERNEL_WINDOW_END >> 12;
|
||
uint32_t ovl_pg = OVERLAY_REGION_FLOOR >> 12;
|
||
uint32_t floor_pg = pg < kern_pg ? 0u : (pg < ovl_pg ? kern_pg : ovl_pg);
|
||
extern uint32_t dirty_ram_get_bitmap_word(uint32_t word_index);
|
||
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 recovered = pg << 12;
|
||
uint32_t bucket = (phys * 2654435761u) & LAZY_ENTRY_MASK;
|
||
int n = snprintf(out, cap, "{\"recovered_region\":\"0x%08X\",\"entries\":[",
|
||
recovered);
|
||
int first = 1;
|
||
for (int li = s_lazy_entry_head[bucket]; li >= 0 && n < cap - 320;
|
||
li = s_lazy_man[li].next_entry) {
|
||
LazyMan *lm = &s_lazy_man[li];
|
||
if ((lm->fn.entry & 0x1FFFFFFFu) != phys) continue;
|
||
int ci = lm->cache_idx;
|
||
const char *base = strrchr(s_cache_idx[ci].path, '/');
|
||
base = base ? base + 1 : s_cache_idx[ci].path;
|
||
uint32_t live = lazy_man_crc(&lm->fn);
|
||
n += snprintf(out + n, cap - n,
|
||
"%s{\"li\":%d,\"ci\":%d,\"region\":\"0x%08X\","
|
||
"\"file\":\"%s\",\"funcs\":%d,\"indexed\":%d,"
|
||
"\"loaded\":%d,\"crc\":\"0x%08X\",\"live\":\"0x%08X\","
|
||
"\"match\":%d,\"contains\":%d}",
|
||
first ? "" : ",", li, ci, s_cache_idx[ci].region_start,
|
||
base, s_cache_idx[ci].func_count, s_cache_idx[ci].indexed_func_count,
|
||
dll_already_loaded(s_cache_idx[ci].path), lm->fn.crc, live,
|
||
live == lm->fn.crc, lazy_man_contains(&lm->fn, phys));
|
||
first = 0;
|
||
}
|
||
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;
|
||
}
|