Reinstates the runtime-loaded overlay DLL cache (Layer A) and the offline overlay compilation pipeline that were built on the pruned codex/cache-discovery branch (tip 2bbafc2) and never merged. Ported onto current master; rebuilt rather than history-restored. Runtime (off by default, opt in via [runtime] overlay_cache = true): - overlay_loader.c/.h, overlay_api.h: CRC32-keyed DLL cache at cache/<game_id>/<region>_<crc>.dll. On a dirty-RAM dispatch miss the loader LoadLibrary's the matching DLL, wires runtime callbacks via overlay_init(), enumerates func_XXXXXXXX exports, and dispatches them BEFORE the interpreter. - overlay_capture.c/.h: write-once capture set; records overlay DMA bytes and writes overlay_captures.json (execution-verified seeds) at clean exit. - dma.c: capture + cd_dma_log hook at CD DMA completion (async model). - cdrom.c: cdrom_get_setloc_lba() for disc-position correlation. - dirty_ram_interp.c: g_dirty_ram_exec_pc_table (exec-verified PCs), a decodability guard on JAL-into-dirty targets, and the overlay dispatch hooks. - debug_server.c: cd_read_log + overlay_loader_status TCP commands. - config_loader: [runtime] overlay_cache flag (default false). Recompiler (compile_overlays.py / extract_overlays.py drive these): - main_psx.cpp --overlay-exact: analyze only explicit entries + direct JAL targets and set split_mid_function_targets=false, so overlay basic-block targets are NOT split into bogus standalone functions (the mid-function-seed failure mode that produced black-on-load / blue-on-new-game). - function_analysis.cpp: analyze_exact_entries(); code_generator.cpp: gate the mid-function split pre-pass behind split_mid_function_targets (default true, so normal main-EXE codegen is unchanged - Tomba still regens to 2854 funcs). Deltas from the original: save states dropped (never worked); overlay enable is runtime-config-gated (off by default) instead of a build-time define; loader stderr logging converted to the overlay_loader_status TCP path (CLAUDE.md Rule 3). Recovered from _backups/2026-06-06-consolidation/psxrecomp-all-PRE.bundle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7.0 KiB
Overlay Discovery
Updated: 2026-05-30 Phase gate: Phase 5+ (game EXE loaded, dirty-RAM hits observed in game address space)
Problem
PSX games load code from disc at runtime into RAM. The main EXE on disc does not contain these overlays — they arrive via CD-ROM DMA during gameplay. The static recompiler sees only the main EXE at build time; it cannot know "at address 0x80090000, after area 2 loads, there will be function X" without actually running the game.
The dirty-RAM interpreter handles these hits today, but it is slow and has no coverage guarantee. This doc describes a self-improving two-layer system that eventually eliminates all runtime interpretation of overlay code.
Distinction from BIOS dirty-RAM
BIOS dirty-RAM (Rule 18 in CLAUDE.md) is code the BIOS assembles in-place at boot — e.g. the 4-instruction SIO handler written to RAM 0xCF0. That code never existed on disc.
Game overlays are different: they are compiled MIPS modules loaded to fixed RAM addresses. Their bytes are known after they land in RAM; the recompiler just hasn't seen them yet. This system fixes that.
Missed heuristics (static-EXE functions the recompiler failed to identify) are orthogonal: they exist in the main EXE at build time. Fix missed heuristics by improving heuristics; fix overlays by this system.
Why disc extraction is not used
The naive approach — scan the disc image for overlay bytes by CRC — does not work for Tomba and
many other PSX titles. Investigation confirmed that Tomba stores overlays in a scatter-load
format: each disc sector is a (12-byte block descriptor, 2048-byte data block) pair, where
the descriptor tells the loader which RAM address to write the data block to. The resulting RAM
content is not present anywhere on disc as a contiguous byte range, so no CRC or brute-force scan
can find it.
The universal correct primitive is to capture bytes at DMA time — intercepting them as they land in RAM, after all scatter-reassembly and decompression. This works for:
- Scatter-load formats (Tomba, others)
- Game-specific LZ or RLE compression (bytes arrive decompressed via DMA)
- Direct file loads (trivially captured)
Architecture
Two layers, composing. As Layer B grows, Layer A fires less. Eventually Layer A goes silent.
Layer B is a manifest of Layer A's output: the same (load_addr, bytes, seeds) data, committed
to the repo and baked into the binary ahead of time.
Layer B — Build-time pre-compilation
Runtime side (capture): on every CD DMA completion into game-code RAM, the runtime appends
(load_addr, size, bytes) to a ring buffer. On process exit it writes
overlay_captures.json automatically — no user action required. The user discovers the file
exists and can attach it to a GitHub issue.
Build-time side: psxrecomp-game reads game.toml [[overlays]] entries, each pointing to
a captured .bin file and a list of seed entry points. It disassembles the MIPS bytes at
load_addr, emits C functions, and adds them to the static dispatch table. The shipped binary
has those overlays pre-compiled at zero runtime cost.
Coverage grows with every release as developer playthroughs and user contributions feed new captures into the build.
Layer A — Runtime background compilation with persistent cache
On CD DMA completion into game-code RAM:
- Hash the written bytes.
- Check
cache/<SCUS_ID>/<hash>.dll.- Cache hit →
LoadLibrarythe DLL, register its functions in the live dispatch table for[load_addr, load_addr + size). Interpreter never fires for this overlay. - Cache miss → fall through to dirty-RAM interpretation. Enqueue a background thread to
compile this overlay: emit C for the bytes at
load_addr, invoke the C compiler, write the DLL to cache. On the next load of this overlay the cache hit fires.
- Cache hit →
Result: within a single session, each new overlay is interpreted once and compiled once. Every subsequent encounter — same session or future sessions — dispatches to native code.
A user who plays through the entire game accumulates a complete cache. From that point forward the game runs entirely as compiled native code, never touching the interpreter.
How they compose
Dispatch (branch to overlay address):
1. Static dispatch table [Layer B] → precompiled function zero runtime cost
2. Compiled cache [Layer A] → LoadLibrary'd function one-time per session after first
3. Dirty-RAM interpreter → fallback first-ever encounter only
Layer B coverage grows with every rebuild. Layer A covers the gap within a session before the rebuild happens. Dirty-RAM fires exactly once per undiscovered overlay.
Game identification
Cache and capture files are namespaced by SCUS/SLUS/SCES product code, read from SYSTEM.CNF:
cache/
SCUS-94236/ ← Tomba (NTSC-U)
<hash>.dll
logs/
SCUS-94236/
overlay_map.jsonl
overlay_captures.json ← written at exit, next to the executable
Cache invalidation
A cached DLL is keyed by the hash of the overlay bytes. The hash IS the identity. If a disc dump differs between users or regions, the hash differs and the old cache entry is simply never matched. No explicit invalidation needed.
When Layer B statically compiles an overlay, Layer A's cached DLL for that hash becomes unreachable (static dispatch wins at step 1). It can be pruned post-build but is benign if left.
Implementation phases
| Phase | Work | Status |
|---|---|---|
| — | Dirty-RAM interpreter (Layer A last-resort fallback) | Done |
| B-1 | CD DMA byte capture ring (always-on, retail-safe, dest < 0x1C0000 filter) |
To build |
| B-1 | Automatic overlay_captures.json written at process exit |
To build |
| B-2 | game.toml [[overlays]] + psxrecomp-game overlay compilation |
To build |
| B-3 | Contribution ingestion script: JSON → .bin + game.toml entries |
To build |
| A-1 | On DMA completion, check cache DLL; if present, LoadLibrary + register |
To build |
| A-2 | Background thread: emit C, compile to DLL, write to cache | To build |
| A-3 | On next overlay encounter, A-1 fires instead of dirty-RAM | Follows from A-1/A-2 |
B-1 through B-3 ship before A-1. The system is useful with only Layer B (requires a rebuild after each new playthrough contribution). Layer A eliminates the "rebuild required" friction and makes the game self-healing for any user.
Scatter-load discovery (Tomba)
Tomba's CD DMA log (captured 2026-05-30) confirmed the scatter-load format:
- Every sector pair:
12-byte block descriptor(always to a header buffer at0x973C0) +2048-byte data block(scattered to the address the descriptor specifies). - A single ISO file load produces DMA entries with non-contiguous destination addresses.
- The 12-byte descriptor encodes the block's destination; the loader dispatches accordingly.
This format is game-specific but the DMA capture approach handles it transparently: the captured bytes are the post-scatter RAM contents, which is exactly the execution form the recompiler needs.