overlay: restore overlay DLL cache + capture (faithful port of codex/cache-discovery)

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>
This commit is contained in:
Matthew Stanley
2026-06-07 12:32:19 -07:00
parent d22c9cec42
commit b2929aa504
26 changed files with 3288 additions and 108 deletions
+111
View File
@@ -0,0 +1,111 @@
# PSXRecomp Overlay System — Feature Reference
## Overview
PSX games use **overlays**: regions of game code that are loaded from disc into a fixed
RAM window at runtime via CD DMA. The same physical addresses are reused by different
overlay images throughout the game. The static recompiler only sees the boot EXE; it
cannot see overlay code that does not exist at compile time.
The overlay system has two layers, B and A, that together solve this.
---
## Layer B — Capture and static compilation (IMPLEMENTED, B-1 done / B-2 in progress)
### B-1: Overlay capture (runtime, done)
`overlay_capture.c` hooks every CD DMA completion into game-code RAM
(`dest < 0x1C0000`). On each unique load address, it stores the DMA-time bytes
(unpatched copy, taken before the game can modify them) in an in-process set.
On clean process exit, `overlay_capture_write_json()` writes
`build-dev/overlay_captures.json` alongside the executable. The JSON contains:
- `load_addr` — virtual address where the overlay loaded (e.g. `0x800E7000`)
- `size` — byte count of the full dirty region
- `bytes_b64` — base64 of the DMA-time image (zero-filled for scatter-load gaps)
- `seeds` — per-PC interpreter hit addresses (execution-verified entry points)
The `seeds` list is how we know which addresses inside the overlay are real function
entry points. They come from `dirty_ram_interp`'s PC table: addresses the interpreter
actually branched to during gameplay.
### B-2: Static overlay compilation (build-time, in progress)
`compile_overlays.py` reads `overlay_captures.json`, wraps the bytes in a fake PS-EXE
header, runs `psxrecomp-game` to recompile, post-processes the generated C for
standalone compilation, and produces a C file that is compiled into the binary.
In **static mode** (B-2), the output is a `.c` file placed in `generated/overlays.c`
that the CMake build includes directly. No DLL, no runtime loading, no CRC matching.
The overlay functions join the static dispatch table exactly like game EXE functions.
`game.toml` gains an `[[overlays]]` section that points to the captures file; CMake
includes `generated/overlays.c` when it exists.
---
## Layer A — Dynamic DLL loading (runtime, SHELVED)
### What it was supposed to do
A-1 was the "live" complement to B: while B produces a binary rebuild, A-1 lets a
user contribute a new overlay by dropping a compiled DLL into a cache directory.
The runtime detects the overlay firing in `dirty_ram_dispatch`, finds the matching
DLL in the cache, loads it with `LoadLibrary`, and wires the overlay functions into
a dynamic dispatch hash table — all without a rebuild.
The idea was that contributors could run `compile_overlays.py`, drop the DLL in
`build-dev/cache/<game-id>/`, and immediately have compiled overlay code.
### What we built
- `overlay_loader.c` — dynamic dispatch hash table (16 384 slots), DLL load + PE
export enumeration, `overlay_loader_dispatch` called from `dirty_ram_dispatch`
- `overlay_api.h` — shared ABI: `OverlayCallbacks` struct, `overlay_init` entry point
- `compile_overlays.py` DLL path — generates `{addr}_{crc}.dll` in the cache dir
- `overlay_loader_status` TCP debug command
### Why it was shelved
**Load Game regression.** When a DLL exists in the cache, loading a save triggers
the overlay dispatch, which loads the DLL, which runs the compiled overlay code.
This crashed consistently. Clearing the DLL cache restores stable behavior. We never
confirmed whether the compiled code itself is correct (the crash analysis was blocked
by the plumbing issues below).
**CRC key instability.** We tried three different naming schemes before landing on
`{addr}_{dma_crc}.dll`:
1. CRC of live RAM — wrong: scatter-load gaps contain old data in live RAM but zeros
in the DMA-assembled image, so the CRC never matched.
2. Address-only — no collision safety.
3. DMA-CRC (from `overlay_capture_get_region_crc`) — correct in theory, but the DMA
blocks are not in `s_entries` after a fast-boot restore (no DMA fired), so the
function hashed a zeroed image and produced the wrong CRC.
**Final fix attempted:** scan the cache dir at init for `{addr}_*.dll` files and
build a `region_start → path` index. This avoids recomputing the CRC at dispatch
time. The strlen check in the scanner had an off-by-one (22 vs 21) that was fixed,
but the Load Game crash was not diagnosed before shelving.
### What remains for A-1 to ship
1. Confirm the generated DLL code is actually correct (same question as B-2).
Debug with `B476006F_patched.c` in `build-dev/cache/SCUS-94236/`.
2. Understand why Load Game crashes with the DLL active.
3. Possibly audit `patch_generated_c` in `compile_overlays.py` for incorrect
cross-call patching or missing extern declarations.
The infrastructure is complete. The blocker is code correctness, not plumbing.
---
## Tomba overlay map (known)
| Region | Load addr | Size | Seeds | Status |
|----------------|--------------|---------|-------|---------------------|
| `000E7000` | `0x800E7000` | 364 544 | 66 | Captured, DLL built, shelved |
More overlays will appear as gameplay progresses past Dwarf Village.
+86 -79
View File
@@ -1,7 +1,6 @@
# Overlay Discovery
**Date:** 2026-05-29
**Branch:** `overlay-discovery`
**Updated:** 2026-05-30
**Phase gate:** Phase 5+ (game EXE loaded, dirty-RAM hits observed in game address space)
---
@@ -24,133 +23,141 @@ interpretation of overlay code.
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 complete compiled MIPS modules on the disc filesystem,
loaded to fixed RAM addresses. Their bytes are known ahead of time; the recompiler just hasn't
seen them yet. This system fixes that.
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 and will not match any disc file hash. The overlay system
ignores them and they fall through to dirty-RAM as today. Fix missed heuristics by improving
heuristics; fix overlays by this system.
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 — Persistent map → static precompilation
### Layer B — Build-time pre-compilation
**Runtime side:** whenever CD DMA completes a load into RAM, record `(disc_file_hash, load_addr,
size)` to a per-game log file on disk. This is cheap — a hash computation and a file append.
**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:** `recompiler/recomp.py` reads the log file. For each entry, it locates the
matching file on the disc image by hash, disassembles it at the recorded load address, and
emits a compiled C function set alongside the main EXE output. The overlay's functions are added
to the static dispatch table at their load addresses.
**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.
**Result:** any overlay logged in a previous run is statically compiled into the next build. No
runtime overhead on subsequent loads.
**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:**
1. Hash the written bytes.
2. Check the on-disk compiled cache (`cache/<SCUS_ID>/<hash>.dll`).
- Cache hit → `LoadLibrary` the cached DLL, register its functions in the live dispatch table
for `[load_addr, load_addr + size)`. No interpretation ever needed.
- Cache miss → fall through to dirty-RAM interpretation. Queue 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 (next room visit, next level reload), the cache
hit path fires instead.
**On CD DMA completion into game-code RAM:**
**Result:** within a single playthrough, each overlay compiles once on first encounter. Every
subsequent encounter in that session, and in all future sessions, hits the cache.
1. Hash the written bytes.
2. Check `cache/<SCUS_ID>/<hash>.dll`.
- **Cache hit** → `LoadLibrary` the 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.
**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 path (on 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]
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 is the last-resort fallback that fires exactly once per undiscovered
overlay.
rebuild happens. Dirty-RAM fires exactly once per undiscovered overlay.
---
## Game identification
Cache and log files are namespaced by SCUS/SLUS/SCES product code, read from `SYSTEM.CNF` on the
disc image at startup (`BOOT=cdrom:\SCUS_941.60;1` → key `SCUS-94160`).
Cache and capture files are namespaced by SCUS/SLUS/SCES product code, read from `SYSTEM.CNF`:
```
cache/
SCUS-94160/ ← Tomba (NTSC-U)
SCUS-94236/ ← Tomba (NTSC-U)
<hash>.dll
<hash>.dll
SCES-01234/ ← different region, separate cache
...
logs/
SCUS-94160/
overlay_map.jsonl ← append-only: {hash, load_addr, size, disc_file}
SCUS-94236/
overlay_map.jsonl
overlay_captures.json ← written at exit, next to the executable
```
Different regional versions get separate caches automatically, which is correct since overlay
load addresses may differ between regions.
---
## Cache invalidation
A cached DLL is keyed by the hash of the overlay bytes. The hash IS the identity. If a disc file
changes (different dump, different region), the hash changes and the old cache entry is simply
never matched — it sits unused. No explicit invalidation needed.
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.
---
## Integration point
The natural hook is the CD-ROM DMA completion callback in `runtime/src/cdrom.c`. This is where
bytes land in RAM. After the DMA write:
1. Compute hash of the destination range.
2. Log `(hash, load_addr, size, source_file)` for Layer B.
3. Check Layer A cache. If hit, register the DLL's functions in the live dispatch table.
4. If miss, mark the range dirty (existing dirty-RAM path) and enqueue background compilation.
---
## Implementation phases
| Phase | Work |
|---|---|
| B-1 | CD DMA completion hook logs `(hash, load_addr, size)` to `logs/<SCUS_ID>/overlay_map.jsonl` |
| B-2 | `recomp.py` reads the log and emits overlay functions alongside the main EXE; adds entries to dispatch table |
| B-3 | Build-time validation: for each log entry, verify disc file hash matches before compiling |
| A-1 | On DMA completion, check `cache/<SCUS_ID>/<hash>.dll`; if present, `LoadLibrary` + register |
| A-2 | Background thread: emit C for newly-seen overlay, compile to DLL, write to cache |
| A-3 | On next load of same overlay, Layer A cache hit fires instead of dirty-RAM |
| 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 can ship before A-1. The system is useful with only Layer B (just requires a
rebuild after each new playthrough). Layer A is the quality-of-life layer that eliminates the
"rebuild required" friction.
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.
---
## Open questions
## Scatter-load discovery (Tomba)
- Does Tomba use named disc files (ISO 9660) or raw LBA sector reads for overlay loads? Named
files make Layer B trivial (hash the file, record the name). Raw LBA reads require tracing the
sector number through to the ISO to recover the file identity. Determine by inspecting
dirty-RAM dispatch miss addresses: if they cluster at 2-3 fixed load addresses, it's classic
file-based overlays. If they scatter, it may be more ad-hoc.
Tomba's CD DMA log (captured 2026-05-30) confirmed the scatter-load format:
- `LoadLibrary` / `dlopen` infrastructure does not currently exist in the runtime. Layer A
requires it. Layer B does not. Implement B first; defer A until B has real coverage data.
- Every sector pair: `12-byte block descriptor` (always to a header buffer at `0x973C0`) +
`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.
+201
View File
@@ -0,0 +1,201 @@
# Overlay System — Implementation Plan
**Written:** 2026-05-30
**Status:** Active
---
## Goal
Make PSX overlay code transparent to the user. Shipped builds run pre-compiled
overlays at native speed (Layer B). Unknown overlays fall back to the interpreter
once, compile in the background, and run as native code from the second encounter
onward (Layer A). Users contribute new coverage automatically by playing.
---
## Data structures
### Capture set
A hash map keyed by `load_addr`. Write-once: a given load address is inserted
exactly once regardless of how many times that overlay loads during a session.
```c
typedef enum {
OVERLAY_STATE_QUEUED = 0, /* in compilation queue, not yet started */
OVERLAY_STATE_COMPILING = 1, /* background thread is working on it */
OVERLAY_STATE_COMPILED = 2, /* DLL written to cache, dispatch patched */
} OverlayState;
typedef struct OverlayEntry {
uint32_t load_addr;
uint32_t size;
uint8_t *bytes; /* heap copy of the DMA'd bytes */
OverlayState state; /* atomic, guards queue dedup */
struct OverlayEntry *next; /* intrusive hash chain */
} OverlayEntry;
```
### Compilation queue
A simple FIFO of `OverlayEntry *` pointers into the capture set. Does not own
data. Background thread drains it. Never contains duplicates — the state machine
on the capture set entry prevents duplicate pushes.
---
## State machine (per entry)
```
DMA completion (first time, load_addr not in set)
→ alloc entry, copy bytes
→ state = QUEUED
→ push pointer onto queue
DMA completion (load_addr already in set)
→ state == QUEUED → no-op (already waiting to compile)
→ state == COMPILING → no-op (being compiled now, interp handles it)
→ state == COMPILED → no-op (dispatch table has it, interp won't fire)
Background thread (dequeues entry)
→ state = COMPILING
→ emit C for bytes at load_addr
→ compile to DLL → write cache/<game_id>/<hash>.dll
→ LoadLibrary + register functions in live dispatch table
→ state = COMPILED
```
The `NEW→QUEUED` transition and the queue push are done under a single mutex
lock so a concurrent DMA completion on a second thread cannot push the same
entry twice.
---
## Implementation phases
### B-1 — Capture + JSON dump *(prerequisite for everything)*
**Files:** `runtime/src/overlay_capture.c` + `runtime/include/overlay_capture.h`
- `overlay_capture_init(const char *game_id)` — called at game handoff
- `overlay_capture_on_dma(uint32_t load_addr, uint32_t size, const uint8_t *bytes)` — called from `dma.c` `execute_ch3_cdrom` when `load_start < 0x1C0000`
- `overlay_capture_write_json(const char *path)` — writes `overlay_captures.json` next to the executable; called from `main.cpp` at clean exit
- JSON schema per entry:
```json
{ "load_addr": "0xA3000", "size": 208896, "bytes_b64": "...", "seeds": [] }
```
(`seeds` is empty at capture time; filled in by the ingestion script from per_pc data)
**Hook in `dma.c`:** existing `execute_ch3_cdrom` already has `load_start` and
`total_words`. Add one call after the existing cd_dma_log push:
```c
if (!step && load_start < 0x1C0000u)
overlay_capture_on_dma(load_start, total_words * 4u, ram + load_start);
```
**Hook in `main.cpp`:** at clean shutdown (SDL quit, window close) call
`overlay_capture_write_json`. Retail and dev builds both do this.
---
### B-2 — psxrecomp-game overlay compilation
**Files:** `recompiler/src/overlay_compiler.cpp`, extended `game.toml` schema
`game.toml` gains an `[[overlays]]` array:
```toml
[[overlays]]
load_addr = "0xA3000"
capture = "captures/A3000.bin"
seeds = ["0xA3140", "0xA52C0"]
```
`psxrecomp-game` reads each entry, disassembles the MIPS bytes at `load_addr`,
traces from seeds, emits C functions, and appends to the game's dispatch table.
These functions are linked into the shipped binary at zero runtime cost.
---
### B-3 — Ingestion script
**File:** `tools/ingest_overlays.py`
```
python3 tools/ingest_overlays.py \
--captures overlay_captures.json \
--per-pc logs/SCUS-94236/per_pc.json \
--out-dir captures/ \
--toml game.toml
```
- Reads `overlay_captures.json` (from user submission or own playthrough)
- Reads `per_pc.json` for executed addresses within each overlay range
- Writes `captures/<load_addr_hex>.bin`
- Appends new `[[overlays]]` blocks to `game.toml` (skips already-present entries)
After running, `psxrecomp-game --config game.toml` re-emits the dispatch table
with the new overlays compiled in.
---
### A-1 — Cache DLL check on DMA completion
**Files:** `runtime/src/overlay_capture.c` (extended)
On DMA completion, after the capture-set insert:
- Compute `hash = crc32(bytes, size)`
- Check `cache/<game_id>/<hash>.dll` exists
- If yes: `LoadLibrary` → enumerate exported functions → register each in the
live dispatch table for `[load_addr, load_addr + size)` → set state COMPILED
- If no: proceed to queue for background compilation (A-2)
---
### A-2 — Background compiler thread
**Files:** `runtime/src/overlay_compiler_thread.c`
A single long-lived thread started at game handoff:
```
loop:
wait on queue semaphore
dequeue OverlayEntry*
set state = COMPILING
emit_c_for_overlay(entry) // calls into recompiler lib
compile_c_to_dll(entry) // invokes GCC/Clang subprocess
LoadLibrary + register dispatch
set state = COMPILED
```
The recompiler is embedded in the runtime as a static library for dev builds.
For retail builds, the compiler subprocess path uses the system C compiler if
present, or skips to "write JSON only" mode if not (Layer A gracefully degrades
to JSON-only on systems without a toolchain).
---
## Build order
1. **B-1** — capture set + JSON dump (unblocks everything)
2. **B-2** — `psxrecomp-game` overlay compilation (unblocks B-3)
3. **B-3** — ingestion script (completes the contribution loop)
4. **A-1** — cache DLL check (fast path for returning users)
5. **A-2** — background compiler thread (self-healing on first run)
B-1 through B-3 are useful independently: they close the developer feedback
loop and give users a way to contribute without any runtime compilation.
A-1 and A-2 are the quality-of-life layer.
---
## What already exists
| Component | File | Status |
|---|---|---|
| Dirty-RAM interpreter (last-resort fallback) | `runtime/src/dirty_ram_interp.c` | Done |
| CD DMA ring log (`cd_read_log` TCP cmd) | `runtime/src/dma.c` | Done (filter + 65536 ring, 2026-05-30) |
| Dispatch table infrastructure | `runtime/src/full_function_emitter.cpp` | Done |
| Recompiler core (MIPS→C) | `recompiler/src/` | Done |
| `game.toml` config loader | `recompiler/src/config_loader.cpp` | Done |
+2
View File
@@ -34,6 +34,7 @@ struct CodeGenConfig {
bool emit_line_numbers; // Include original address as comments
bool optimize_zero_reg; // Optimize away $zero assignments
bool use_switch_for_blocks; // Use switch instead of goto labels
bool split_mid_function_targets; // Split branch targets into funcs for legacy main-EXE analysis
std::string indent; // Indentation string (default: " ")
CodeGenConfig()
@@ -41,6 +42,7 @@ struct CodeGenConfig {
, emit_line_numbers(true)
, optimize_zero_reg(true)
, use_switch_for_blocks(false)
, split_mid_function_targets(true)
, indent(" ") {}
};
+5
View File
@@ -35,6 +35,11 @@ public:
// Scan entire executable for function boundaries
FunctionAnalysisResult analyze();
// Analyze only explicit entry points and direct JAL targets reachable from
// them. This is for runtime-loaded overlays where scanning the whole blob
// treats data and basic-block targets as standalone functions.
FunctionAnalysisResult analyze_exact_entries(const std::vector<uint32_t>& entries);
// Add a forced entry point address that is treated as a function start
// even if it has no standard ADDIU $sp prologue. The function will be
// included in the analysis result with has_prologue = false.
+5 -1
View File
@@ -1573,7 +1573,11 @@ std::string CodeGenerator::generate_file(
// hit `psx_unknown_dispatch`. Surfaced by Phase B2 audit. The 16-pass
// cap is a safety limit; in practice each pass strictly reduces the
// remaining target set so convergence is fast.
{
//
// Overlay exact mode disables this: overlay branch targets are basic-block
// labels, not callable function entries, so splitting them into standalone
// functions produces broken dispatch — the mid-function-seed failure mode.
if (config_.split_mid_function_targets) {
std::set<uint32_t> cfgs_to_scan; // Which CFGs to scan (empty = all)
uint32_t exe_start = exe_.header.load_address;
uint32_t exe_end = exe_.end_address();
+3
View File
@@ -61,6 +61,9 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
if (runtime.contains("fast_boot")) {
rt.fast_boot = toml::find<bool>(runtime, "fast_boot");
}
if (runtime.contains("overlay_cache")) {
rt.overlay_cache = toml::find<bool>(runtime, "overlay_cache");
}
return rt;
}
+6
View File
@@ -47,6 +47,12 @@ struct RuntimeConfig {
// subsequent launches, skipping BIOS execution entirely. Default off;
// enable per-game in [runtime]. Snapshot is keyed on BIOS SHA256 + entry_pc.
bool fast_boot = false;
// overlay_cache: enable the overlay DLL cache + capture (Layer A). Off by
// default. When true the runtime scans cache/<game_id>/ for precompiled
// overlay DLLs (loaded ahead of the dirty-RAM interpreter) and records
// overlay bytes to overlay_captures.json for offline compilation.
bool overlay_cache = false;
};
// One entry from [[recompiler.bios_vectors]].
+376
View File
@@ -1,6 +1,7 @@
#include "function_analysis.h"
#include <algorithm>
#include <map>
#include <queue>
#include <set>
#include <fmt/format.h>
@@ -217,6 +218,381 @@ bool FunctionAnalyzer::is_likely_data_section(uint32_t start_addr, uint32_t end_
return false;
}
namespace {
enum class ExactCfKind {
Normal,
Branch,
Jump,
Jal,
JrRa,
JrOther,
Jalr,
};
struct ExactCf {
ExactCfKind kind = ExactCfKind::Normal;
uint32_t target = 0;
};
struct ExactWalkResult {
std::set<uint32_t> visited;
std::set<uint32_t> direct_jal_targets;
std::set<uint32_t> jump_table_targets;
uint32_t jr_ra_count = 0;
};
static bool exact_is_jr_ra_word(uint32_t instr) {
return instr == 0x03E00008u;
}
static bool exact_is_addiu_sp_neg(uint32_t instr) {
uint32_t opcode = (instr >> 26) & 0x3Fu;
uint32_t rs = (instr >> 21) & 0x1Fu;
uint32_t rt = (instr >> 16) & 0x1Fu;
int16_t imm = static_cast<int16_t>(instr & 0xFFFFu);
return opcode == 0x09u && rs == 29u && rt == 29u && imm < 0;
}
static bool exact_is_valid_mips_word(uint32_t instr) {
if (instr == 0xFFFFFFFFu || instr == 0xFFFFFFFDu) return false;
uint32_t opcode = (instr >> 26) & 0x3Fu;
uint32_t funct = instr & 0x3Fu;
uint32_t rt = (instr >> 16) & 0x1Fu;
if (opcode == 0x00u) {
switch (funct) {
case 0x00u: case 0x02u: case 0x03u: case 0x04u:
case 0x06u: case 0x07u: case 0x08u: case 0x09u:
case 0x0Cu: case 0x0Du:
case 0x10u: case 0x11u: case 0x12u: case 0x13u:
case 0x18u: case 0x19u: case 0x1Au: case 0x1Bu:
case 0x20u: case 0x21u: case 0x22u: case 0x23u:
case 0x24u: case 0x25u: case 0x26u: case 0x27u:
case 0x2Au: case 0x2Bu:
return true;
default:
return false;
}
}
if (opcode == 0x01u) {
return rt == 0x00u || rt == 0x01u || rt == 0x10u || rt == 0x11u;
}
switch (opcode) {
case 0x02u: case 0x03u: case 0x04u: case 0x05u:
case 0x06u: case 0x07u:
case 0x08u: case 0x09u: case 0x0Au: case 0x0Bu:
case 0x0Cu: case 0x0Du: case 0x0Eu: case 0x0Fu:
case 0x10u: case 0x12u:
case 0x20u: case 0x21u: case 0x22u: case 0x23u:
case 0x24u: case 0x25u: case 0x26u:
case 0x28u: case 0x29u: case 0x2Au: case 0x2Bu:
case 0x2Eu:
case 0x30u: case 0x32u: case 0x38u: case 0x3Au:
return true;
default:
return false;
}
}
static uint32_t exact_branch_target(uint32_t pc, uint32_t instr) {
int16_t imm = static_cast<int16_t>(instr & 0xFFFFu);
return pc + 4u + (static_cast<int32_t>(imm) << 2);
}
static uint32_t exact_jump_target(uint32_t pc, uint32_t instr) {
return ((pc + 4u) & 0xF0000000u) | ((instr & 0x03FFFFFFu) << 2);
}
static ExactCf exact_classify_cf(uint32_t pc, uint32_t instr) {
ExactCf cf;
uint32_t opcode = (instr >> 26) & 0x3Fu;
uint32_t funct = instr & 0x3Fu;
uint32_t rs = (instr >> 21) & 0x1Fu;
if (opcode == 0x00u && funct == 0x08u) {
cf.kind = (rs == 31u) ? ExactCfKind::JrRa : ExactCfKind::JrOther;
return cf;
}
if (opcode == 0x00u && funct == 0x09u) {
cf.kind = ExactCfKind::Jalr;
return cf;
}
if (opcode == 0x02u) {
cf.kind = ExactCfKind::Jump;
cf.target = exact_jump_target(pc, instr);
return cf;
}
if (opcode == 0x03u) {
cf.kind = ExactCfKind::Jal;
cf.target = exact_jump_target(pc, instr);
return cf;
}
if (opcode == 0x01u || (opcode >= 0x04u && opcode <= 0x07u) ||
(opcode >= 0x14u && opcode <= 0x17u)) {
cf.kind = ExactCfKind::Branch;
cf.target = exact_branch_target(pc, instr);
return cf;
}
return cf;
}
} // namespace
FunctionAnalysisResult FunctionAnalyzer::analyze_exact_entries(const std::vector<uint32_t>& entries) {
FunctionAnalysisResult result;
result.total_instructions = 0;
result.jr_ra_count = 0;
result.prologue_count = 0;
result.call_discovered_count = 0;
result.state_continuation_count = 0;
fmt::print("\n=== Exact Overlay Function Analysis ===\n\n");
auto in_exe = [&](uint32_t addr) {
return addr >= exe_.header.load_address && addr < exe_.end_address() && (addr & 3u) == 0;
};
auto callable_direct_jal_target = [&](uint32_t addr) {
auto word_opt = exe_.read_word(addr);
if (!word_opt.has_value() || !exact_is_valid_mips_word(*word_opt)) return false;
if (addr == exe_.header.load_address) return true;
if (addr >= exe_.header.load_address + 8u) {
auto prev8_opt = exe_.read_word(addr - 8u);
if (prev8_opt.has_value() && exact_is_jr_ra_word(*prev8_opt)) return true;
}
if (exact_is_addiu_sp_neg(*word_opt)) {
if (addr < exe_.header.load_address + 4u) return true;
auto prev_opt = exe_.read_word(addr - 4u);
if (!prev_opt.has_value()) return true;
return exact_classify_cf(addr - 4u, *prev_opt).kind == ExactCfKind::Normal;
}
return false;
};
auto find_jump_table_targets = [&](uint32_t entry, uint32_t hard_cap,
uint32_t jr_pc, uint32_t jr_rs) {
std::vector<uint32_t> targets;
uint32_t lw_base = 0xFFu;
int32_t lw_offset = 0;
uint32_t addu_cand[2] = {0xFFu, 0xFFu};
uint32_t lui_val = 0;
int16_t addiu_val[2] = {0, 0};
bool found_addiu[2] = {false, false};
bool found_lui = false;
uint32_t table_count = 0;
for (int back = 1; back <= 40; back++) {
uint32_t scan_addr = jr_pc - static_cast<uint32_t>(back * 4);
if (scan_addr < entry) break;
auto scan_opt = exe_.read_word(scan_addr);
if (!scan_opt.has_value()) break;
uint32_t instr = *scan_opt;
uint32_t op = (instr >> 26) & 0x3Fu;
uint32_t rs = (instr >> 21) & 0x1Fu;
uint32_t rt = (instr >> 16) & 0x1Fu;
uint32_t rd = (instr >> 11) & 0x1Fu;
uint32_t fn = instr & 0x3Fu;
if (op == 0x23u && rt == jr_rs && lw_base == 0xFFu) {
lw_base = rs;
lw_offset = static_cast<int32_t>(static_cast<int16_t>(instr & 0xFFFFu));
continue;
}
if (op == 0x00u && fn == 0x21u && rd == lw_base && lw_base != 0xFFu &&
addu_cand[0] == 0xFFu) {
addu_cand[0] = rs;
addu_cand[1] = rt;
continue;
}
if (op == 0x09u && addu_cand[0] != 0xFFu) {
for (int c = 0; c < 2; c++) {
if (!found_addiu[c] && addu_cand[c] != 0xFFu &&
rs == addu_cand[c] && rt == addu_cand[c]) {
addiu_val[c] = static_cast<int16_t>(instr & 0xFFFFu);
found_addiu[c] = true;
break;
}
}
continue;
}
if (op == 0x0Fu && addu_cand[0] != 0xFFu && !found_lui) {
for (int c = 0; c < 2; c++) {
if (addu_cand[c] != 0xFFu && rt == addu_cand[c]) {
lui_val = (instr & 0xFFFFu) << 16;
if (!found_addiu[c]) addiu_val[c] = 0;
addiu_val[0] = addiu_val[c];
found_addiu[0] = found_addiu[c];
found_lui = true;
break;
}
}
continue;
}
if (op == 0x0Bu && table_count == 0) {
table_count = instr & 0xFFFFu;
continue;
}
if (found_lui && table_count != 0) break;
}
if (!found_lui || table_count == 0 || table_count >= 512) return targets;
uint32_t table_base = lui_val +
(found_addiu[0] ? static_cast<uint32_t>(static_cast<int32_t>(addiu_val[0])) : 0u) +
static_cast<uint32_t>(lw_offset);
for (uint32_t i = 0; i < table_count; i++) {
auto entry_opt = exe_.read_word(table_base + i * 4u);
if (!entry_opt.has_value()) continue;
uint32_t target = *entry_opt;
if (target >= entry && target < hard_cap && in_exe(target)) {
targets.push_back(target);
}
}
return targets;
};
auto walk = [&](uint32_t entry, uint32_t hard_cap) {
ExactWalkResult wr;
std::queue<uint32_t> work;
work.push(entry);
auto in_function = [&](uint32_t addr) {
return in_exe(addr) && addr >= entry && addr < hard_cap;
};
while (!work.empty()) {
uint32_t pc = work.front();
work.pop();
if (wr.visited.count(pc) || !in_function(pc)) continue;
auto word_opt = exe_.read_word(pc);
if (!word_opt.has_value()) continue;
uint32_t instr = *word_opt;
wr.visited.insert(pc);
ExactCf cf = exact_classify_cf(pc, instr);
uint32_t delay = pc + 4u;
switch (cf.kind) {
case ExactCfKind::Normal:
if (in_function(pc + 4u)) work.push(pc + 4u);
break;
case ExactCfKind::Branch:
if (in_function(delay)) wr.visited.insert(delay);
if (in_function(pc + 8u)) work.push(pc + 8u);
if (in_function(cf.target)) work.push(cf.target);
break;
case ExactCfKind::Jump:
if (in_function(delay)) wr.visited.insert(delay);
if (in_function(cf.target)) work.push(cf.target);
break;
case ExactCfKind::Jal:
if (in_function(delay)) wr.visited.insert(delay);
if (in_function(pc + 8u)) work.push(pc + 8u);
if (in_exe(cf.target) && callable_direct_jal_target(cf.target)) {
wr.direct_jal_targets.insert(cf.target);
}
break;
case ExactCfKind::Jalr:
if (in_function(delay)) wr.visited.insert(delay);
if (in_function(pc + 8u)) work.push(pc + 8u);
break;
case ExactCfKind::JrRa:
if (in_function(delay)) wr.visited.insert(delay);
wr.jr_ra_count++;
break;
case ExactCfKind::JrOther:
if (in_function(delay)) wr.visited.insert(delay);
{
uint32_t jr_rs = (instr >> 21) & 0x1Fu;
for (uint32_t target : find_jump_table_targets(entry, hard_cap, pc, jr_rs)) {
wr.jump_table_targets.insert(target);
if (in_function(target)) work.push(target);
}
}
break;
}
}
return wr;
};
std::set<uint32_t> known_entries;
std::queue<uint32_t> pending;
for (uint32_t entry : entries) {
if (!in_exe(entry)) continue;
if (known_entries.insert(entry).second) pending.push(entry);
}
size_t explicit_count = known_entries.size();
fmt::print("Explicit entries: {}\n", explicit_count);
std::set<uint32_t> processed;
while (!pending.empty()) {
uint32_t entry = pending.front();
pending.pop();
if (processed.count(entry)) continue;
processed.insert(entry);
auto next_it = known_entries.upper_bound(entry);
uint32_t hard_cap = (next_it != known_entries.end()) ? *next_it : exe_.end_address();
ExactWalkResult wr = walk(entry, hard_cap);
for (uint32_t target : wr.direct_jal_targets) {
if (known_entries.insert(target).second) pending.push(target);
}
}
result.call_discovered_count = static_cast<int>(known_entries.size() - explicit_count);
fmt::print("Direct-JAL entries: {}\n", result.call_discovered_count);
fmt::print("Total exact entries: {}\n\n", known_entries.size());
std::vector<uint32_t> starts_vec(known_entries.begin(), known_entries.end());
std::sort(starts_vec.begin(), starts_vec.end());
for (size_t i = 0; i < starts_vec.size(); i++) {
uint32_t entry = starts_vec[i];
uint32_t hard_cap = (i + 1 < starts_vec.size()) ? starts_vec[i + 1] : exe_.end_address();
ExactWalkResult wr = walk(entry, hard_cap);
if (wr.visited.empty()) continue;
Function func;
func.start_addr = entry;
func.end_addr = *wr.visited.rbegin() + 4u;
if (func.end_addr > hard_cap) func.end_addr = hard_cap;
if (func.end_addr <= func.start_addr) func.end_addr = func.start_addr + 4u;
func.size = func.end_addr - func.start_addr;
func.name = fmt::format("func_{:08X}", func.start_addr);
auto first_instr = exe_.read_word(func.start_addr);
if (first_instr.has_value()) {
int32_t stack_size;
func.has_prologue = is_prologue(*first_instr, stack_size);
func.stack_frame_size = func.has_prologue ? stack_size : 0;
if (func.has_prologue) result.prologue_count++;
} else {
func.has_prologue = false;
func.stack_frame_size = 0;
}
func.has_epilogue = wr.jr_ra_count > 0;
func.is_data_section = false;
result.jr_ra_count += static_cast<int>(wr.jr_ra_count);
result.total_instructions += static_cast<int>(wr.visited.size());
result.functions.push_back(func);
}
return result;
}
FunctionAnalysisResult FunctionAnalyzer::analyze() {
FunctionAnalysisResult result;
result.total_instructions = 0;
+16 -3
View File
@@ -40,6 +40,7 @@ int main(int argc, char** argv) {
std::string extra_funcs_storage; // lifetime anchor for the .c_str() below
const char* extra_funcs_path = nullptr;
bool inspect_mode = false;
bool overlay_exact_mode = false;
std::filesystem::path out_dir = "generated";
if (!config_path.empty()) {
@@ -72,6 +73,8 @@ int main(int argc, char** argv) {
* with psxrecomp-bios and project scripts. */
} else if (arg == "--inspect") {
inspect_mode = true;
} else if (arg == "--overlay-exact") {
overlay_exact_mode = true;
}
}
}
@@ -197,7 +200,11 @@ int main(int argc, char** argv) {
fmt::print("Performing function analysis...\n");
PSXRecomp::FunctionAnalyzer analyzer(*exe);
analyzer.add_forced_entry(exe->header.initial_pc);
std::vector<uint32_t> exact_entries;
exact_entries.push_back(exe->header.initial_pc);
if (!overlay_exact_mode) {
analyzer.add_forced_entry(exe->header.initial_pc);
}
// Forced entry points: functions called from the dispatch table but not
// automatically detected by the heuristic-based function analysis.
@@ -213,7 +220,10 @@ int main(int argc, char** argv) {
if (line.empty() || line[0] == '#') continue;
uint32_t addr = (uint32_t)std::strtoul(line.c_str(), nullptr, 16);
if (addr >= 0x80010000u && addr < 0x80200000u) {
analyzer.add_forced_entry(addr);
exact_entries.push_back(addr);
if (!overlay_exact_mode) {
analyzer.add_forced_entry(addr);
}
extra_count++;
}
}
@@ -223,7 +233,9 @@ int main(int argc, char** argv) {
}
}
auto analysis_result = analyzer.analyze();
auto analysis_result = overlay_exact_mode
? analyzer.analyze_exact_entries(exact_entries)
: analyzer.analyze();
// Print summary statistics
fmt::print("\n=== Function Analysis Summary ===\n\n");
@@ -344,6 +356,7 @@ int main(int argc, char** argv) {
PSXRecomp::CodeGenConfig codegen_config;
codegen_config.emit_comments = true;
codegen_config.emit_line_numbers = true;
codegen_config.split_mid_function_targets = !overlay_exact_mode;
// Load per-game annotations: annotations/<exe_stem>_annotations.csv
// Silently skipped if the file doesn't exist.
+3
View File
@@ -15,6 +15,9 @@ void cdrom_set_speed(int divisor);
void cdrom_set_game_speed(int divisor);
/* Called by fntrace on first game-range dispatch; switches to game speed. */
void cdrom_notify_game_started(void);
/* LBA of the most recent SetLoc command (-1 if none yet). Used by the DMA
* ring buffer to correlate each sector transfer with its disc position. */
int cdrom_get_setloc_lba(void);
/* MMIO read/write (0x1F801800-0x1F801803) */
uint32_t cdrom_read(uint32_t addr);
+4 -1
View File
@@ -56,13 +56,16 @@ extern const char *g_dirty_ram_last_unsupported_reason;
* stubs actually fire — a single noisy spurious-dispatch site can mask
* a legitimate handler that never runs. This open-addressed table keys
* on the entry PC of each interpreted block. */
#define DIRTY_RAM_PC_TABLE_SIZE 64
#define DIRTY_RAM_PC_TABLE_SIZE 65536
typedef struct {
uint32_t pc; /* entry PC, 0 = empty slot */
uint64_t hits; /* number of times dispatched here */
uint64_t insns; /* total instructions executed across hits */
} DirtyRamPcEntry;
extern DirtyRamPcEntry g_dirty_ram_pc_table[DIRTY_RAM_PC_TABLE_SIZE];
/* Companion table: every PC the interpreter actually executes (not just block
* entries). overlay_capture uses both to report execution-verified seeds. */
extern DirtyRamPcEntry g_dirty_ram_exec_pc_table[DIRTY_RAM_PC_TABLE_SIZE];
/* Block-entry ring buffer. Records every dispatch into dirty RAM with the
* caller's RA at entry, plus argument context — answers
+44
View File
@@ -0,0 +1,44 @@
#ifndef OVERLAY_API_H
#define OVERLAY_API_H
/* Shared ABI between psx-runtime.exe and overlay DLLs.
*
* After LoadLibrary, the runtime calls overlay_init() with this struct.
* The DLL stores the pointers and routes all cross-module calls through them.
*/
#include "cpu_state.h"
#include <stdint.h>
typedef struct {
/* Core dispatch: routes call_by_address() and out-of-overlay jal */
void (*dispatch_call)(CPUState *cpu, uint32_t addr, uint32_t ra);
/* Interrupt check: called after every function return in overlay */
void (*check_interrupts)(CPUState *cpu);
/* GTE coprocessor 2 execution */
void (*gte_execute)(CPUState *cpu, uint32_t cmd);
/* MIPS syscall (break/syscall instructions) */
void (*psx_syscall)(CPUState *cpu, uint32_t code);
/* Unresolved dispatch target */
void (*psx_unknown_dispatch)(CPUState *cpu, uint32_t addr, uint32_t phys);
/* Debug instrumentation: called at every function entry (may be NULL) */
void (*log_call_entry)(uint32_t func_addr);
} OverlayCallbacks;
#ifdef __cplusplus
extern "C" {
#endif
/* Exported by every overlay DLL. Call once after LoadLibrary. */
#ifdef _WIN32
__declspec(dllexport)
#else
__attribute__((visibility("default")))
#endif
void overlay_init(const OverlayCallbacks *cbs);
#ifdef __cplusplus
}
#endif
#endif /* OVERLAY_API_H */
+62
View File
@@ -0,0 +1,62 @@
#ifndef OVERLAY_CAPTURE_H
#define OVERLAY_CAPTURE_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* overlay_capture — B-1 implementation of the overlay capture set.
*
* On every CD DMA completion into game-code RAM (dest < 0x1C0000), the runtime
* calls overlay_capture_on_dma(). Each unique load_addr is stored exactly once
* in a write-once set. At clean process exit, overlay_capture_write_json()
* writes overlay_captures.json next to the executable for user contribution.
*
* The set never overwrites: a second DMA to the same load_addr is a no-op.
* This is correct because PSX overlays always load the same bytes to the same
* address; recording once is sufficient.
*/
/* Call once at startup (before the run loop) with the exe directory.
* Sets the output path for overlay_captures.json. Capture is not yet
* active — it activates automatically on the first post-game DMA transfer. */
void overlay_capture_set_out_dir(const char *out_dir);
/* Legacy init alias kept for compatibility; same as set_out_dir. */
void overlay_capture_init(const char *out_dir);
/* Enable/disable overlay capture. Off by default; main() enables it only when
* the overlay cache is turned on in [runtime] config. When disabled,
* overlay_capture_on_dma() and overlay_capture_write_json() are no-ops. */
void overlay_capture_set_enabled(int enabled);
/* Call from dma.c execute_ch3_cdrom() after every forward CH3 transfer with
* load_start < 0x1C0000 and fntrace_is_game_started().
* load_addr is the physical RAM destination, size is byte count,
* bytes points to the RAM buffer at that address (already written by DMA). */
void overlay_capture_on_dma(uint32_t load_addr, uint32_t size,
const uint8_t *bytes);
/* Write overlay_captures.json to the directory supplied at init time.
* Safe to call even if no overlays were captured (writes nothing).
* Called from shutdown_runtime() in main.cpp. */
void overlay_capture_write_json(void);
/* Returns number of unique overlays captured so far. */
int overlay_capture_count(void);
/* Compute CRC32 of the DMA-time bytes for the region [region_start,
* region_start+region_size). Zero-filled for gaps between DMA blocks,
* exactly as overlay_captures.json bytes_b64 is assembled. Use this
* in overlay_loader to build a consistent DLL filename without reading
* live RAM (which has scatter-load gap contamination). */
uint32_t overlay_capture_get_region_crc(uint32_t region_start,
uint32_t region_size);
#ifdef __cplusplus
}
#endif
#endif /* OVERLAY_CAPTURE_H */
+58
View File
@@ -0,0 +1,58 @@
#ifndef OVERLAY_LOADER_H
#define OVERLAY_LOADER_H
/* overlay_loader — A-1 runtime overlay DLL cache and dynamic dispatch.
*
* On CD DMA completion (overlay_capture_on_dma), the loader checks whether
* a cached DLL exists for the overlay bytes. If it does, it LoadLibrary's
* the DLL, calls overlay_init() to wire callbacks, enumerates func_XXXXXXXX
* exports, and registers each in the dynamic dispatch table.
*
* dirty_ram_dispatch calls overlay_loader_dispatch() before the interpreter,
* so compiled overlay functions get priority over interpretation.
*/
#include "cpu_state.h"
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Called at game handoff to set the cache root directory and game ID.
* cache_dir: absolute path to the cache root (e.g. "build-dev/cache")
* game_id: product code (e.g. "SCUS-94236") */
void overlay_loader_init(const char *cache_dir, const char *game_id);
/* Called from overlay_capture_on_dma after the capture-set insert.
* Computes CRC32 of bytes, checks cache, loads DLL if present.
* load_addr: physical RAM address, size/bytes: the transferred data. */
void overlay_loader_check_cache(uint32_t load_addr, uint32_t size,
const uint8_t *bytes);
/* Called from dirty_ram_dispatch before the interpreter.
* Returns 1 and calls the compiled function if addr is registered,
* 0 if not found (fall through to interpreter). */
int overlay_loader_dispatch(CPUState *cpu, uint32_t addr);
/* Returns number of functions currently registered in the dynamic table. */
int overlay_loader_registered_count(void);
/* Returns full loader state for TCP diagnostics. */
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);
/* Most recent loader event string (DLL load success/failure). Surfaced via
* the overlay_loader_status TCP command — no stderr logging (Rule 3). */
const char *overlay_loader_last_msg(void);
#ifdef __cplusplus
}
#endif
#endif /* OVERLAY_LOADER_H */
+13
View File
@@ -92,6 +92,8 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/starvation_ring.c
${PSXRECOMP_ROOT}/runtime/src/card_read_summary.c
${PSXRECOMP_ROOT}/runtime/src/card_data_writes.c
${PSXRECOMP_ROOT}/runtime/src/overlay_capture.c
${PSXRECOMP_ROOT}/runtime/src/overlay_loader.c
${PSXRECOMP_ROOT}/recompiler/src/config_loader.cpp
)
@@ -112,6 +114,7 @@ function(psxrecomp_add_runtime_target target)
set(oneValueArgs
GAME_GENERATED_FULL_C
GAME_GENERATED_DISPATCH_C
GAME_OVERLAY_STATIC_C
DEBUG_PORT
WINDOW_TITLE
DEFAULT_BIOS_PATH
@@ -147,6 +150,13 @@ function(psxrecomp_add_runtime_target target)
list(APPEND generated_sources "${PSXRT_GAME_GENERATED_DISPATCH_C}")
set(has_game_dispatch TRUE)
endif()
# Layer B: statically-compiled overlay dispatch. Inert unless a game
# provides a generated overlays_static.c — no target sets this yet.
if(PSXRT_GAME_OVERLAY_STATIC_C AND EXISTS "${PSXRT_GAME_OVERLAY_STATIC_C}")
set_source_files_properties("${PSXRT_GAME_OVERLAY_STATIC_C}" PROPERTIES GENERATED TRUE)
list(APPEND generated_sources "${PSXRT_GAME_OVERLAY_STATIC_C}")
set(has_overlay_dispatch TRUE)
endif()
if(PSXRT_ORACLE)
set(mode_source ${PSXRECOMP_ROOT}/runtime/src/psx_interpreter.c)
@@ -201,6 +211,9 @@ function(psxrecomp_add_runtime_target target)
if(has_game_dispatch)
target_compile_definitions(${target} PRIVATE PSX_HAS_GAME_DISPATCH=1)
endif()
if(has_overlay_dispatch)
target_compile_definitions(${target} PRIVATE PSX_HAS_OVERLAY_DISPATCH=1)
endif()
# PSX_DEBUG_TOOLS option declared at the top of runtime.cmake so it's
# also visible to psx-beetle / non-runtime-helper targets.
+4
View File
@@ -75,6 +75,7 @@ static uint64_t sector_history_seq;
/* Seek target */
static uint8_t seek_min, seek_sec, seek_sect;
static int s_setloc_lba = -1; /* LBA captured at SetLoc time */
/* Read state */
static int reading;
@@ -138,6 +139,8 @@ void cdrom_notify_game_started(void) {
g_disc_speed_divisor = g_game_divisor;
}
int cdrom_get_setloc_lba(void) { return s_setloc_lba; }
/* Minimum cycles between CD-ROM IRQs in fast modes. Must be enough for the
* interrupt handler to save state, check the IRQ flag, process data, and
* return. Too low → interrupt fires before the previous one is processed →
@@ -659,6 +662,7 @@ static void exec_command(uint8_t cmd) {
seek_min = bcd_to_bin(param_fifo[0]);
seek_sec = bcd_to_bin(param_fifo[1]);
seek_sect = bcd_to_bin(param_fifo[2]);
s_setloc_lba = msf_to_lba(seek_min, seek_sec, seek_sect);
}
response_push(stat_reg);
set_irq(CDIRQ_ACK);
+85
View File
@@ -8,6 +8,7 @@
* so TCP.md and DEBUG.md are reusable across projects.
*/
#include "debug_server.h"
#include "overlay_loader.h"
#include "cpu_state.h"
#include "dma.h"
#include "gpu.h"
@@ -7214,6 +7215,88 @@ static void handle_fn_exit_dump(int id, const char *json) {
free(buf);
}
/* cd_read_log: dump the CD DMA transfer ring. Each entry records the
* setloc LBA, destination RAM address, and byte count for one forward
* channel-3 DMA transfer. Use with overlay_dump to map overlay regions
* back to their disc positions for the extract_overlays.py disc scanner.
*
* Parameters: "tail" (int, default 256) — how many recent entries to return.
*/
static void handle_cd_read_log(int id, const char *json)
{
extern uint32_t cd_dma_log_get_total(void);
extern void cd_dma_log_get_entry(uint32_t idx, int *lba,
uint32_t *dest, uint32_t *size);
int tail = json_get_int(json, "tail", 256);
uint32_t total = cd_dma_log_get_total();
uint32_t cap = 65536;
uint32_t avail = total < cap ? total : cap;
if ((uint32_t)tail > avail) tail = (int)avail;
uint32_t start_idx = total > (uint32_t)tail ? total - (uint32_t)tail : 0;
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%u,\"count\":%d,\"entries\":[",
id, total, tail);
int first = 1;
for (uint32_t i = start_idx; i < total; i++) {
int lba; uint32_t dest, size;
cd_dma_log_get_entry(i, &lba, &dest, &size);
if (lba < 0) continue;
send_fmt("%s{\"lba\":%d,\"dest\":\"0x%08X\",\"size\":%u}",
first ? "" : ",", lba, dest, size);
first = 0;
}
send_fmt("]}\n");
}
/* overlay_loader_status: report the dynamic overlay DLL loader state —
* whether active, how many functions are registered, which regions have
* been checked, and the most recent load event. Rule-3 inspection path for
* the loader (it does no stderr logging). */
static void handle_overlay_loader_status(int id, const char *json)
{
(void)json;
int active = 0, registered = 0, nchecked = 0, nwritten = 0;
int file_found = 0;
uint32_t last_crc = 0;
char cache_dir[512] = {0}, game_id[64] = {0};
uint32_t checked[8] = {0};
overlay_loader_get_status(&active, &registered, &nchecked,
cache_dir, sizeof(cache_dir),
game_id, sizeof(game_id),
checked, 8, &nwritten,
&last_crc, &file_found);
/* Escape backslashes in cache_dir for JSON */
char esc_dir[768] = {0};
for (int si = 0, di = 0; cache_dir[si] && di < (int)sizeof(esc_dir)-2; si++) {
if (cache_dir[si] == '\\') esc_dir[di++] = '\\';
esc_dir[di++] = cache_dir[si];
}
const char *msg = overlay_loader_last_msg();
char esc_msg[512] = {0};
for (int si = 0, di = 0; msg[si] && di < (int)sizeof(esc_msg)-2; si++) {
if (msg[si] == '\\' || msg[si] == '"') esc_msg[di++] = '\\';
esc_msg[di++] = msg[si];
}
char buf[1536];
int n = snprintf(buf, sizeof(buf),
"{\"id\":%d,\"ok\":true,\"active\":%d,\"registered\":%d,"
"\"regions_checked\":%d,\"last_crc\":\"0x%08X\",\"file_found\":%d,"
"\"cache_dir\":\"%s\",\"game_id\":\"%s\",\"last_msg\":\"%s\"",
id, active, registered, nchecked, last_crc, file_found,
esc_dir, game_id, esc_msg);
if (nwritten > 0) {
n += snprintf(buf + n, sizeof(buf) - n, ",\"checked\":[");
for (int i = 0; i < nwritten; i++)
n += snprintf(buf + n, sizeof(buf) - n,
"%s\"0x%08X\"", i ? "," : "", checked[i]);
n += snprintf(buf + n, sizeof(buf) - n, "]");
}
snprintf(buf + n, sizeof(buf) - n, "}\n");
send_fmt("%s", buf);
}
/* overlay_dump: extract RAM regions that dirty_ram has marked executable
* above a threshold physical address. Writes <crc32>.bin files to a
* caller-supplied directory and returns a JSON manifest.
@@ -7746,6 +7829,8 @@ static const CmdEntry s_commands[] = {
{ "fn_entry_tail", handle_fn_entry_tail },
{ "fn_exit_dump", handle_fn_exit_dump },
{ "overlay_dump", handle_overlay_dump },
{ "cd_read_log", handle_cd_read_log },
{ "overlay_loader_status", handle_overlay_loader_status },
{ NULL, NULL }
};
+84 -2
View File
@@ -52,6 +52,7 @@ uint32_t g_dirty_ram_last_unsupported_insn = 0;
const char *g_dirty_ram_last_unsupported_reason = NULL;
DirtyRamPcEntry g_dirty_ram_pc_table[DIRTY_RAM_PC_TABLE_SIZE] = {0};
DirtyRamPcEntry g_dirty_ram_exec_pc_table[DIRTY_RAM_PC_TABLE_SIZE] = {0};
DirtyRamBlockLogEntry g_dirty_ram_block_log[DIRTY_RAM_BLOCK_LOG_CAP] = {0};
uint64_t g_dirty_ram_block_log_seq = 0;
@@ -67,17 +68,29 @@ extern uint64_t s_frame_count;
* the working set of install-stub PCs is tiny (handful), so this stays
* O(1) in practice. Returns NULL if the table is full — caller treats
* that as "stop tracking" rather than failing. */
static DirtyRamPcEntry *pc_table_get_or_insert(uint32_t pc) {
static DirtyRamPcEntry *pc_table_get_or_insert_in(DirtyRamPcEntry *table, uint32_t pc) {
uint32_t h = (pc * 2654435761u) & (DIRTY_RAM_PC_TABLE_SIZE - 1);
for (uint32_t i = 0; i < DIRTY_RAM_PC_TABLE_SIZE; i++) {
uint32_t idx = (h + i) & (DIRTY_RAM_PC_TABLE_SIZE - 1);
DirtyRamPcEntry *e = &g_dirty_ram_pc_table[idx];
DirtyRamPcEntry *e = &table[idx];
if (e->pc == pc) return e;
if (e->pc == 0) { e->pc = pc; return e; }
}
return NULL;
}
static DirtyRamPcEntry *pc_table_get_or_insert(uint32_t pc) {
return pc_table_get_or_insert_in(g_dirty_ram_pc_table, pc);
}
/* Record every PC the interpreter executes (not just block entries) so
* overlay_capture can report execution-verified seeds for the region. */
static void exec_pc_table_record(uint32_t pc) {
DirtyRamPcEntry *e = pc_table_get_or_insert_in(g_dirty_ram_exec_pc_table,
pc & 0x1FFFFFFFu);
if (e) e->hits++;
}
/* From debug_server.c — keep our outer-frame attribution coherent. */
extern uint32_t g_debug_current_func_addr;
extern uint32_t g_debug_last_store_pc;
@@ -233,6 +246,54 @@ static int is_local_dirty_target(uint32_t target) {
return phys >= 0x00098000u && dirty_ram_is_dirty(phys);
}
/* A dirty-RAM target only deserves interpretation if its first word decodes
* as a plausible MIPS instruction. A scatter-loaded overlay leaves data
* bytes in dirty pages; jumping into those and interpreting them as code is
* how the original cache build produced black/blue screens. When the target
* word doesn't look like code, fall back to the normal dispatch path. */
static int dirty_ram_word_looks_decodable(uint32_t insn) {
if (insn == 0xFFFFFFFFu || insn == 0xFFFFFFFDu) return 0;
uint32_t op = op_field(insn);
uint32_t fn = funct_field(insn);
uint32_t rt = rt_field(insn);
if (op == 0x00u) {
switch (fn) {
case 0x00u: case 0x02u: case 0x03u: case 0x04u:
case 0x06u: case 0x07u: case 0x08u: case 0x09u:
case 0x0Cu: case 0x0Du:
case 0x10u: case 0x11u: case 0x12u: case 0x13u:
case 0x18u: case 0x19u: case 0x1Au: case 0x1Bu:
case 0x20u: case 0x21u: case 0x22u: case 0x23u:
case 0x24u: case 0x25u: case 0x26u: case 0x27u:
case 0x2Au: case 0x2Bu:
return 1;
default:
return 0;
}
}
if (op == 0x01u) {
return rt == 0x00u || rt == 0x01u || rt == 0x10u || rt == 0x11u;
}
switch (op) {
case 0x02u: case 0x03u: case 0x04u: case 0x05u:
case 0x06u: case 0x07u:
case 0x08u: case 0x09u: case 0x0Au: case 0x0Bu:
case 0x0Cu: case 0x0Du: case 0x0Eu: case 0x0Fu:
case 0x10u: case 0x12u:
case 0x20u: case 0x21u: case 0x22u: case 0x23u:
case 0x24u: case 0x25u: case 0x26u:
case 0x28u: case 0x29u: case 0x2Au: case 0x2Bu:
case 0x2Eu:
case 0x30u: case 0x32u: case 0x38u: case 0x3Au:
return 1;
default:
return 0;
}
}
static int dispatch_nonlocal_call(CPUState *cpu, uint32_t target,
uint32_t return_pc,
uint32_t *next_pc_out) {
@@ -276,6 +337,7 @@ static void exec_delay_slot(CPUState *cpu, uint32_t pc) {
}
static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
exec_pc_table_record(pc);
uint32_t phys = pc & 0x1FFFFFFFu;
uint32_t insn = fetch_word(phys);
uint32_t opc = op_field(insn);
@@ -466,6 +528,9 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
if (!is_local_dirty_target(target)) {
return dispatch_nonlocal_call(cpu, target, return_pc, next_pc_out);
}
if (!dirty_ram_word_looks_decodable(fetch_word(target & 0x1FFFFFFFu))) {
return dispatch_nonlocal_call(cpu, target, return_pc, next_pc_out);
}
cpu->pc = target;
return 1;
}
@@ -687,6 +752,23 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr) {
if (psx_dispatch_game_compiled(cpu, addr)) return 1;
#endif
/* B-2: statically-compiled overlay functions (generated/overlays_static.c).
* Inert unless a game provides an overlays_static.c at build time. */
#ifdef PSX_HAS_OVERLAY_DISPATCH
{
extern int psx_overlay_dispatch(CPUState *cpu, uint32_t addr);
if (psx_overlay_dispatch(cpu, addr)) return 1;
}
#endif
/* A-1: dynamically-loaded overlay DLL functions, checked before the
* interpreter. No-op (returns 0) until overlay_loader_init() runs, which
* only happens when the overlay cache is enabled in config. */
{
extern int overlay_loader_dispatch(CPUState *cpu, uint32_t addr);
if (overlay_loader_dispatch(cpu, addr)) return 1;
}
if (!dirty_ram_is_dirty(phys)) return 0;
/* Reset soft-fail state at block entry. */
+53
View File
@@ -17,6 +17,7 @@
#include "dirty_ram_interp.h"
#include "gpu.h"
#include "mdec.h"
#include "overlay_capture.h"
#include "spu.h"
#include <stdint.h>
#include <stdio.h>
@@ -49,11 +50,43 @@ typedef struct {
uint32_t total_words;
uint32_t remaining_words;
uint32_t cycles_accum;
uint32_t start_addr; /* madr at transfer start (CD overlay capture) */
} DMAAsyncChannel;
static DMAAsyncChannel mdec_async[2];
static DMAAsyncChannel cdrom_async;
/* ---- CD DMA transfer log ---- */
/* Every forward CH3 DMA that lands below 0x1C0000 (game data region) records
* (setloc_lba, dest_addr, size). Transfers to 0x1C0000+ are FMV/streaming
* buffers and are excluded to keep the ring focused on overlay loads.
* Surfaced via the cd_read_log TCP command; pairs with overlay_dump to map
* overlay regions back to disc positions for extract_overlays.py. */
#define CD_DMA_LOG_CAP 65536
typedef struct { int lba; uint32_t dest; uint32_t size; } CdDmaEntry;
static CdDmaEntry cd_dma_log[CD_DMA_LOG_CAP];
static uint32_t cd_dma_log_head = 0;
static uint32_t cd_dma_log_total = 0;
static void cd_dma_log_push(int lba, uint32_t dest, uint32_t size) {
cd_dma_log[cd_dma_log_head % CD_DMA_LOG_CAP].lba = lba;
cd_dma_log[cd_dma_log_head % CD_DMA_LOG_CAP].dest = dest;
cd_dma_log[cd_dma_log_head % CD_DMA_LOG_CAP].size = size;
cd_dma_log_head = (cd_dma_log_head + 1) % CD_DMA_LOG_CAP;
cd_dma_log_total++;
}
uint32_t cd_dma_log_get_total(void) { return cd_dma_log_total; }
void cd_dma_log_get_entry(uint32_t idx, int *lba, uint32_t *dest, uint32_t *size) {
uint32_t cap = CD_DMA_LOG_CAP;
uint32_t oldest = cd_dma_log_total > cap ? cd_dma_log_total - cap : 0;
if (idx < oldest || idx >= cd_dma_log_total) { *lba = -1; return; }
uint32_t slot = idx % cap;
*lba = cd_dma_log[slot].lba;
*dest = cd_dma_log[slot].dest;
*size = cd_dma_log[slot].size;
}
#define DMA_MDEC_IN_CYCLES_PER_WORD 1u
#define DMA_MDEC_OUT_CYCLES_PER_WORD 14u
#define DMA_GPU_CYCLES_PER_WORD 1u
@@ -247,6 +280,7 @@ static void start_async_cdrom_transfer(void) {
a->total_words = transfer_word_count(3);
a->remaining_words = a->total_words;
a->cycles_accum = 0;
a->start_addr = channels[3].madr & 0x1FFFFCu;
if (a->total_words == 0) {
cancel_async_transfer(3);
@@ -255,6 +289,25 @@ static void start_async_cdrom_transfer(void) {
}
static void finish_async_cdrom_transfer(uint32_t final_addr) {
DMAAsyncChannel *a = &cdrom_async;
uint32_t step = (channels[3].chcr >> 1) & 1u;
uint32_t load_start = a->start_addr;
uint32_t size = a->total_words * 4u;
/* Log and capture game-data transfers only.
* Transfers to 0x1C0000+ are FMV/streaming buffers; skip them. */
if (!step && size > 0 && load_start < 0x1C0000u) {
int lba = cdrom_get_setloc_lba();
if (lba >= 0) cd_dma_log_push(lba, load_start, size);
/* B-1: capture overlay bytes into the write-once capture set.
* overlay_capture_on_dma auto-activates after game handoff and is a
* no-op unless the overlay cache is enabled in config. */
extern uint8_t *memory_get_ram_ptr(void);
uint8_t *ram = memory_get_ram_ptr();
overlay_capture_on_dma(load_start, size, ram + load_start);
}
channels[3].madr = final_addr;
cancel_async_transfer(3);
complete_transfer(3);
+13
View File
@@ -10,6 +10,8 @@
#include "cdrom.h"
#include "fntrace.h"
#include "boot_state.h"
#include "overlay_capture.h"
#include "overlay_loader.h"
#include "gpu.h"
#include "sio.h"
#include "spu.h"
@@ -468,6 +470,7 @@ static void close_controller(void);
static void shutdown_runtime(void) {
memcard_flush_all();
overlay_capture_write_json();
if (sdl_audio_device) {
SDL_ClearQueuedAudio(sdl_audio_device);
SDL_CloseAudioDevice(sdl_audio_device);
@@ -1106,6 +1109,16 @@ int main(int argc, char** argv) {
if (gc.runtime.has_disc_speed) disc_speed = gc.runtime.disc_speed;
game_entry_pc = gc.entry_pc;
fast_boot = gc.runtime.fast_boot;
/* Overlay DLL cache (Layer A). Off unless enabled in [runtime];
* when on, capture overlay bytes and scan cache/<game_id>/ for
* precompiled overlay DLLs. */
if (gc.runtime.overlay_cache) {
std::filesystem::path exe_dir = exe_dir_from_argv(argv[0]);
std::string cache_dir = (exe_dir / "cache").string();
overlay_capture_set_out_dir(exe_dir.string().c_str());
overlay_capture_set_enabled(1);
overlay_loader_init(cache_dir.c_str(), game_id.c_str());
}
std::fprintf(stdout, "psxrecomp: loaded game config %s (%s, %s)\n",
game_config_path, game_name.c_str(), game_id.c_str());
} catch (const std::exception& ex) {
+285
View File
@@ -0,0 +1,285 @@
#include "overlay_capture.h"
#include "overlay_loader.h"
#include "dirty_ram_interp.h"
#include "crc32.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
/* ---- Capture set --------------------------------------------------------
* Tracks which physical addresses have received CD DMA loads since game
* handoff. Used for dedup (state machine) and A-layer compilation queue.
* The JSON output uses dirty_ram regions, not individual DMA blocks.
* -------------------------------------------------------------------------*/
typedef enum {
OV_QUEUED = 0, /* captured, not yet compiled */
OV_COMPILING = 1, /* background thread working on it */
OV_COMPILED = 2, /* DLL written, dispatch table patched */
} OvState;
typedef struct {
uint32_t load_addr; /* physical RAM address */
uint32_t size; /* byte count of this DMA block */
uint8_t *bytes; /* unpatched copy taken at DMA time */
OvState state;
} OvEntry;
#define MAX_OVERLAYS 4096
static OvEntry s_entries[MAX_OVERLAYS];
static int s_count = 0;
static char s_out_dir[512];
static int s_active = 0;
static int s_enabled = 0; /* config gate; off unless overlay cache enabled */
/* ---- Base64 encoder ----------------------------------------------------- */
static const char k_b64[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static void write_b64(FILE *f, const uint8_t *data, size_t len)
{
size_t i;
for (i = 0; i < len; i += 3) {
uint32_t b = (uint32_t)data[i] << 16;
if (i + 1 < len) b |= (uint32_t)data[i + 1] << 8;
if (i + 2 < len) b |= (uint32_t)data[i + 2];
fputc(k_b64[(b >> 18) & 0x3F], f);
fputc(k_b64[(b >> 12) & 0x3F], f);
fputc(i + 1 < len ? k_b64[(b >> 6) & 0x3F] : '=', f);
fputc(i + 2 < len ? k_b64[(b ) & 0x3F] : '=', f);
}
}
/* ---- Public API --------------------------------------------------------- */
void overlay_capture_set_out_dir(const char *out_dir)
{
strncpy(s_out_dir, out_dir, sizeof(s_out_dir) - 1);
s_out_dir[sizeof(s_out_dir) - 1] = '\0';
}
void overlay_capture_init(const char *out_dir)
{
overlay_capture_set_out_dir(out_dir);
}
void overlay_capture_set_enabled(int enabled)
{
s_enabled = enabled ? 1 : 0;
}
void overlay_capture_on_dma(uint32_t load_addr, uint32_t size,
const uint8_t *bytes)
{
int i;
OvEntry *e;
extern int fntrace_is_game_started(void);
if (!s_enabled) return; /* overlay cache disabled in config */
if (size == 0) return;
/* Auto-activate on the first post-game-handoff DMA. */
if (!s_active) {
if (!fntrace_is_game_started()) return;
s_active = 1;
}
/* Dedup by load_addr. */
for (i = 0; i < s_count; i++) {
if (s_entries[i].load_addr == load_addr)
return;
}
if (s_count >= MAX_OVERLAYS) return;
e = &s_entries[s_count++];
e->load_addr = load_addr;
e->size = size;
e->state = OV_QUEUED;
e->bytes = (uint8_t *)malloc(size);
if (e->bytes) memcpy(e->bytes, bytes, size);
/* A-1: check if a compiled DLL exists in cache for these bytes. */
overlay_loader_check_cache(load_addr, size, bytes);
/* A-2: push onto compilation queue (to be added in A-2). */
}
/* ---- JSON output — assembled dirty_ram regions -------------------------- */
void overlay_capture_write_json(void)
{
extern uint32_t dirty_ram_get_bitmap_word(uint32_t word_index);
extern uint32_t dirty_ram_get_bitmap_word_count(void);
char path[600];
FILE *f;
uint32_t bw, page_sz, lo_page;
uint32_t page, run_start;
int in_run, first_region;
if (!s_active) return;
/* Use the same lo threshold as overlay_dump: only above 0x98000. */
uint32_t lo_phys = 0x00098000u;
bw = dirty_ram_get_bitmap_word_count();
page_sz = 4096u;
lo_page = lo_phys / page_sz;
snprintf(path, sizeof(path), "%s/overlay_captures.json", s_out_dir);
f = fopen(path, "w");
if (!f) return;
fprintf(f, "[\n");
in_run = 0;
run_start = 0;
first_region = 1;
for (page = lo_page; page <= bw * 32u; page++) {
int dirty = 0;
if (page < bw * 32u) {
uint32_t word = dirty_ram_get_bitmap_word(page >> 5);
dirty = (word >> (page & 31u)) & 1u;
}
if (dirty && !in_run) {
in_run = 1;
run_start = page;
} else if (!dirty && in_run) {
uint32_t phys = run_start * page_sz;
uint32_t size = (page - run_start) * page_sz;
uint32_t virt = 0x80000000u | (phys & 0x1FFFFFu);
int j;
in_run = 0;
/* Seeds: only per-PC interpreter hits — execution-verified. */
#define MAX_CAPTURE_PCS DIRTY_RAM_PC_TABLE_SIZE
uint32_t *executed = (uint32_t *)malloc(
MAX_CAPTURE_PCS * sizeof(uint32_t));
uint32_t *dispatch = (uint32_t *)malloc(
MAX_CAPTURE_PCS * sizeof(uint32_t));
int nexec = 0;
int ndisp = 0;
if (!executed || !dispatch) {
free(executed);
free(dispatch);
continue;
}
for (j = 0; j < DIRTY_RAM_PC_TABLE_SIZE; j++) {
DirtyRamPcEntry *pe = &g_dirty_ram_pc_table[j];
if (pe->pc == 0 || pe->hits == 0) continue;
if (pe->pc < phys || pe->pc >= phys + size) continue;
dispatch[ndisp++] = pe->pc;
}
for (j = 0; j < DIRTY_RAM_PC_TABLE_SIZE; j++) {
DirtyRamPcEntry *pe = &g_dirty_ram_exec_pc_table[j];
if (pe->pc == 0 || pe->hits == 0) continue;
if (pe->pc < phys || pe->pc >= phys + size) continue;
executed[nexec++] = pe->pc;
}
/* Skip pure-data regions — nothing to compile. */
if (ndisp == 0 && nexec == 0) {
free(executed);
free(dispatch);
continue;
}
/* Assemble unpatched bytes from DMA-time captures.
* Start with a zeroed image, then blit each stored DMA block
* into its correct offset. This gives pre-patch bytes, not
* the potentially-modified bytes now in live RAM. */
uint8_t *image = (uint8_t *)calloc(size, 1);
if (!image) {
free(executed);
free(dispatch);
continue;
}
for (j = 0; j < s_count; j++) {
OvEntry *blk = &s_entries[j];
if (!blk->bytes) continue;
if (blk->load_addr < phys || blk->load_addr >= phys + size) continue;
uint32_t off = blk->load_addr - phys;
uint32_t copy_sz = blk->size;
if (off + copy_sz > size) copy_sz = size - off;
memcpy(image + off, blk->bytes, copy_sz);
}
if (!first_region) fprintf(f, ",\n");
first_region = 0;
fprintf(f, " {\n");
fprintf(f, " \"schema\": \"psxrecomp overlay capture v2\",\n");
fprintf(f, " \"load_addr\": \"0x%08X\",\n", virt);
fprintf(f, " \"size\": %u,\n", size);
fprintf(f, " \"bytes_b64\": \"");
write_b64(f, image, size);
free(image);
fprintf(f, "\",\n");
fprintf(f, " \"executed_pcs\": [");
for (j = 0; j < nexec; j++) {
uint32_t seed_virt = 0x80000000u | (executed[j] & 0x1FFFFFu);
if (j > 0) fprintf(f, ", ");
fprintf(f, "\"0x%08X\"", seed_virt);
}
fprintf(f, "],\n");
fprintf(f, " \"dispatch_entry_pcs\": [");
for (j = 0; j < ndisp; j++) {
uint32_t seed_virt = 0x80000000u | (dispatch[j] & 0x1FFFFFu);
if (j > 0) fprintf(f, ", ");
fprintf(f, "\"0x%08X\"", seed_virt);
}
fprintf(f, "],\n");
fprintf(f, " \"function_entry_pcs\": [],\n");
fprintf(f, " \"seeds\": [");
for (j = 0; j < ndisp; j++) {
uint32_t seed_virt = 0x80000000u | (dispatch[j] & 0x1FFFFFu);
if (j > 0) fprintf(f, ", ");
fprintf(f, "\"0x%08X\"", seed_virt);
}
fprintf(f, "]\n");
fprintf(f, " }");
free(executed);
free(dispatch);
}
}
fprintf(f, "\n]\n");
fclose(f);
}
int overlay_capture_count(void)
{
return s_count;
}
uint32_t overlay_capture_get_region_crc(uint32_t region_start,
uint32_t region_size)
{
int j;
uint8_t *image = (uint8_t *)calloc(region_size, 1);
if (!image) return 0;
for (j = 0; j < s_count; j++) {
OvEntry *blk = &s_entries[j];
if (!blk->bytes) continue;
if (blk->load_addr < region_start ||
blk->load_addr >= region_start + region_size) continue;
uint32_t off = blk->load_addr - region_start;
uint32_t copy_sz = blk->size;
if (off + copy_sz > region_size) copy_sz = region_size - off;
memcpy(image + off, blk->bytes, copy_sz);
}
uint32_t crc = crc32_compute(image, region_size);
free(image);
return crc;
}
+365
View File
@@ -0,0 +1,365 @@
#include "overlay_loader.h"
#include "overlay_api.h"
#include "crc32.h"
#include "interrupts.h"
#include "debug_server.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
#else
# include <dlfcn.h>
#endif
/* ---- Dynamic dispatch hash table --------------------------------------- */
/* Open-addressed hash map: physical addr → compiled function pointer.
* Sized at 16384 slots — enough for ~10 overlays × ~500 functions each
* at <50% load factor. */
#define DYNTAB_CAP 16384u
#define DYNTAB_MASK (DYNTAB_CAP - 1u)
typedef void (*OverlayFn)(CPUState *);
typedef struct {
uint32_t addr; /* physical address, 0 = empty */
OverlayFn fn;
} DynEntry;
static DynEntry s_table[DYNTAB_CAP];
static int s_count = 0;
static void dyntab_insert(uint32_t phys, OverlayFn fn)
{
uint32_t h = (phys * 2654435761u) & DYNTAB_MASK;
uint32_t i;
for (i = 0; i < DYNTAB_CAP; i++) {
uint32_t idx = (h + i) & DYNTAB_MASK;
if (s_table[idx].addr == 0 || s_table[idx].addr == phys) {
if (s_table[idx].addr == 0) s_count++;
s_table[idx].addr = phys;
s_table[idx].fn = fn;
return;
}
}
/* Table full — shouldn't happen at <50% load */
}
static OverlayFn dyntab_lookup(uint32_t phys)
{
uint32_t h = (phys * 2654435761u) & DYNTAB_MASK;
uint32_t i;
for (i = 0; i < DYNTAB_CAP; i++) {
uint32_t idx = (h + i) & DYNTAB_MASK;
if (s_table[idx].addr == 0) return NULL;
if (s_table[idx].addr == phys) return s_table[idx].fn;
}
return NULL;
}
/* ---- Global state ------------------------------------------------------ */
static char s_cache_dir[512];
static char s_game_id[64];
static int s_active = 0;
/* Rule 3: no stderr logging. The most recent loader event is recorded here
* and surfaced through the `overlay_loader_status` TCP command instead of
* being printed. */
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 ------------------------------- */
/* Scanned once at init from {cache_dir}/{game_id}/{8hex}_{8hex}.dll files.
* Avoids recomputing the DMA CRC at dispatch time (which fails after
* fast_boot because DMA callbacks haven't fired to fill s_entries). */
#define CACHE_IDX_CAP 256
typedef struct { uint32_t region_start; char path[768]; } CacheEntry;
static CacheEntry s_cache_idx[CACHE_IDX_CAP];
static int s_cache_idx_count = 0;
static void scan_cache_dir(void)
{
#ifdef _WIN32
char pattern[768];
snprintf(pattern, sizeof(pattern), "%s/%s/*_*.dll",
s_cache_dir, s_game_id);
WIN32_FIND_DATAA fd;
HANDLE h = FindFirstFileA(pattern, &fd);
if (h == INVALID_HANDLE_VALUE) return;
do {
if (strlen(fd.cFileName) != 21) continue; /* 8+1+8+4 = 21 */
uint32_t addr = (uint32_t)strtoul(fd.cFileName, NULL, 16);
if (addr == 0) continue;
if (s_cache_idx_count >= CACHE_IDX_CAP) break;
CacheEntry *e = &s_cache_idx[s_cache_idx_count++];
e->region_start = addr;
snprintf(e->path, sizeof(e->path), "%s/%s/%s",
s_cache_dir, s_game_id, fd.cFileName);
} while (FindNextFileA(h, &fd));
FindClose(h);
#endif
}
/* ---- Runtime callbacks wired into overlay DLLs via overlay_init() ------ */
extern void psx_dispatch_call(CPUState *cpu, uint32_t addr, uint32_t ra);
extern void psx_check_interrupts(CPUState *cpu);
extern void gte_execute(CPUState *cpu, uint32_t cmd);
extern void psx_syscall(CPUState *cpu, uint32_t code);
extern void psx_unknown_dispatch(CPUState *cpu, uint32_t addr, uint32_t phys);
extern void debug_server_log_call_entry(uint32_t func_addr);
static OverlayCallbacks s_callbacks;
static void init_callbacks(void)
{
s_callbacks.dispatch_call = psx_dispatch_call;
s_callbacks.check_interrupts = psx_check_interrupts;
s_callbacks.gte_execute = gte_execute;
s_callbacks.psx_syscall = psx_syscall;
s_callbacks.psx_unknown_dispatch = psx_unknown_dispatch;
s_callbacks.log_call_entry = debug_server_log_call_entry;
}
/* ---- DLL loading and export enumeration -------------------------------- */
#ifdef _WIN32
static int load_overlay_dll(const char *dll_path, uint32_t load_addr_virt)
{
HMODULE dll = LoadLibraryA(dll_path);
if (!dll) {
loader_log("LoadLibrary(%s) failed: %lu", dll_path, GetLastError());
return 0;
}
/* Call overlay_init to wire the runtime callbacks. */
typedef void (*InitFn)(const OverlayCallbacks *);
InitFn init_fn = (InitFn)GetProcAddress(dll, "overlay_init");
if (!init_fn) {
loader_log("no overlay_init in %s", dll_path);
FreeLibrary(dll);
return 0;
}
init_fn(&s_callbacks);
/* Enumerate PE export table for func_XXXXXXXX symbols. */
BYTE *base = (BYTE *)dll;
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(dll);
return 0;
}
IMAGE_EXPORT_DIRECTORY *exp =
(IMAGE_EXPORT_DIRECTORY *)(base + exp_dd->VirtualAddress);
DWORD *names = (DWORD *)(base + exp->AddressOfNames);
WORD *ordinals = (WORD *)(base + exp->AddressOfNameOrdinals);
DWORD *funcs = (DWORD *)(base + exp->AddressOfFunctions);
int registered = 0;
for (DWORD i = 0; i < exp->NumberOfNames; i++) {
const char *name = (const char *)(base + names[i]);
/* Match "func_XXXXXXXX" — exactly 13 chars */
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]);
uint32_t phys = addr & 0x1FFFFFFFu;
dyntab_insert(phys, fn);
registered++;
}
loader_log("loaded %s -> %d functions registered", dll_path, registered);
return registered;
}
#else
static int load_overlay_dll(const char *dll_path, uint32_t load_addr_virt)
{
void *dll = dlopen(dll_path, RTLD_NOW | RTLD_LOCAL);
if (!dll) {
loader_log("dlopen(%s) failed: %s", dll_path, dlerror());
return 0;
}
typedef void (*InitFn)(const OverlayCallbacks *);
InitFn init_fn = (InitFn)dlsym(dll, "overlay_init");
if (!init_fn) {
loader_log("no overlay_init in %s", dll_path);
dlclose(dll);
return 0;
}
init_fn(&s_callbacks);
/* On non-Windows we can't easily enumerate exports without libelf/BFD.
* Fall back to scanning the known address range: probe GetProcAddress
* equivalents by constructing names from the seed list stored in the
* overlay capture. For now, register nothing — the interpreter handles
* it until proper export enumeration is implemented. */
loader_log("%s loaded (posix export scan TODO)", dll_path);
return 0;
}
#endif
/* ---- Public API -------------------------------------------------------- */
void overlay_loader_init(const char *cache_dir, const char *game_id)
{
strncpy(s_cache_dir, cache_dir, sizeof(s_cache_dir) - 1);
strncpy(s_game_id, game_id, sizeof(s_game_id) - 1);
init_callbacks();
scan_cache_dir();
s_active = 1;
}
void overlay_loader_check_cache(uint32_t load_addr, uint32_t size,
const uint8_t *bytes)
{
/* Block-level check is not used — the DLL CRC is over the full assembled
* region, not individual DMA blocks. Cache loading is deferred to
* overlay_loader_dispatch on the first dispatch miss. */
(void)load_addr; (void)size; (void)bytes;
}
/* ---- Lazy region cache check ------------------------------------------- */
/* On the first dispatch miss for any address in a dirty region, we compute
* the CRC32 of the full assembled region from RAM and look for a matching
* DLL. This fires AFTER the game has fully loaded the overlay and called
* into it — by that point all DMA blocks have landed in RAM. */
#define MAX_CHECKED 64
static uint32_t s_checked[MAX_CHECKED];
static int s_nchecked = 0;
static uint32_t s_last_crc = 0;
static int s_last_file_found = 0;
static int already_checked(uint32_t region_start) {
int i;
for (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;
}
static void try_load_region(uint32_t phys)
{
extern uint32_t dirty_ram_get_bitmap_word(uint32_t word_index);
extern uint32_t dirty_ram_get_bitmap_word_count(void);
extern uint8_t *memory_get_ram_ptr(void);
uint32_t page_sz = 4096u;
uint32_t bw = dirty_ram_get_bitmap_word_count();
char dll_path[768];
/* Walk backward to find region start. */
uint32_t pg = phys / page_sz;
while (pg > 0) {
uint32_t pp = pg - 1;
if (!((dirty_ram_get_bitmap_word(pp >> 5) >> (pp & 31u)) & 1u)) break;
pg = pp;
}
uint32_t region_start = pg * page_sz;
if (already_checked(region_start)) return;
mark_checked(region_start);
/* Walk forward to find region end. */
uint32_t pg2 = phys / page_sz + 1;
while (pg2 < bw * 32u &&
((dirty_ram_get_bitmap_word(pg2 >> 5) >> (pg2 & 31u)) & 1u))
pg2++;
uint32_t region_size = (pg2 - pg) * page_sz;
/* Look up the compiled DLL by region_start in the cache index.
* The index was built at init by scanning for {addr}_{crc}.dll files,
* so we never need to recompute the CRC from RAM or DMA blocks. */
s_last_crc = 0;
s_last_file_found = 0;
(void)region_size;
int ci;
const char *found_path = NULL;
for (ci = 0; ci < s_cache_idx_count; ci++) {
if (s_cache_idx[ci].region_start == region_start) {
found_path = s_cache_idx[ci].path;
break;
}
}
if (!found_path) return;
strncpy(dll_path, found_path, sizeof(dll_path) - 1);
s_last_file_found = 1;
load_overlay_dll(dll_path, 0x80000000u | (region_start & 0x1FFFFFFFu));
}
int overlay_loader_dispatch(CPUState *cpu, uint32_t addr)
{
uint32_t phys = addr & 0x1FFFFFFFu;
OverlayFn fn = dyntab_lookup(phys);
if (fn) { fn(cpu); return 1; }
/* First miss for this region: check if a DLL is cached. */
if (s_active && phys >= 0x98000u)
try_load_region(phys);
/* Retry after potential DLL load. */
fn = dyntab_lookup(phys);
if (fn) { fn(cpu); return 1; }
return 0;
}
int overlay_loader_registered_count(void)
{
return s_count;
}
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_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;
}
File diff suppressed because it is too large Load Diff
+70 -22
View File
@@ -2,13 +2,15 @@
"""
dump_overlays.py — Extract executed overlay regions from a live runtime.
Queries the overlay_dump TCP command which:
1. Scans the dirty_ram bitmap for contiguous dirty pages above --lo
2. Writes each region as <crc32>.bin in --dir
3. Returns a JSON manifest
Queries overlay_dump (dirty_ram bitmap scan) and cd_read_log (CD DMA
transfer ring) to produce overlay_map.jsonl entries with:
crc32 — CRC32 of the RAM bytes (for verification)
load_addr — physical RAM address where the overlay lives
size — byte count of the region
start_lba — disc LBA of the first sector of this overlay file
Then writes logs/<game_id>/overlay_map.jsonl from the manifest so the
build tool (B-2) has (load_addr, size, crc32, bytes_file) for each overlay.
The start_lba lets extract_overlays.py seek directly to the correct disc
position without brute-force scanning the whole disc image.
Usage:
python3 tools/dump_overlays.py [--port PORT] [--lo PHYS] [--dir DIR] [--out JSONL]
@@ -41,32 +43,72 @@ def send_cmd(port, cmd, **kwargs):
s.close()
return json.loads(data.decode())
def find_lba_for_region(cd_log, phys_addr, size):
"""Return the disc start_lba for an overlay region if known, else -1.
Groups consecutive CD DMA entries (same setloc_lba, dest advancing by
sector size) and returns the lba of the group whose dest range covers
phys_addr for at least `size` bytes.
"""
SECTOR = 2048
entries = cd_log.get('entries', [])
if not entries:
return -1
# Build file-load groups: consecutive entries where dest advances by 2048
# and lba is the same (all sectors of one file share the SetLoc LBA).
groups = []
cur = None
for e in entries:
dest = int(e['dest'], 16)
lba = e['lba']
sz = e['size']
if cur and cur['lba'] == lba and dest == cur['end']:
cur['end'] += sz
cur['total'] += sz
else:
if cur:
groups.append(cur)
cur = {'lba': lba, 'start': dest, 'end': dest + sz, 'total': sz}
if cur:
groups.append(cur)
# Find a group whose start <= phys_addr and end >= phys_addr + size
for g in groups:
if g['start'] <= phys_addr and g['end'] >= phys_addr + size:
# LBA offset for the exact start of the overlay within this file
offset_sectors = (phys_addr - g['start']) // SECTOR
return g['lba'] + offset_sectors
# Partial overlap — return the group start LBA as best guess
if g['start'] <= phys_addr < g['end']:
offset_sectors = (phys_addr - g['start']) // SECTOR
return g['lba'] + offset_sectors
return -1
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--port', type=int, default=4470)
ap.add_argument('--lo', default='0x98000',
help='Low physical address threshold (default: 0x98000)')
ap.add_argument('--dir', default='logs/SCUS-94236/overlays',
help='Directory for .bin files (created if missing)')
ap.add_argument('--out', default='logs/SCUS-94236/overlay_map.jsonl',
help='Output JSONL manifest path')
ap.add_argument('--lo', default='0x98000')
ap.add_argument('--dir', default='logs/SCUS-94236/overlays')
ap.add_argument('--out', default='logs/SCUS-94236/overlay_map.jsonl')
args = ap.parse_args()
os.makedirs(args.dir, exist_ok=True)
os.makedirs(os.path.dirname(args.out) or '.', exist_ok=True)
print(f'Querying overlay_dump on port {args.port} (lo={args.lo}, dir={args.dir})...')
print('Querying overlay_dump...')
result = send_cmd(args.port, 'overlay_dump', lo=args.lo, dir=args.dir)
if not result.get('ok'):
print(f'ERROR: {result.get("error")}')
sys.exit(1)
print(f'ERROR: {result.get("error")}'); sys.exit(1)
regions = result.get('regions', [])
print(f'Found {len(regions)} overlay region(s).')
# Read existing JSONL to avoid duplicate entries
print('Querying cd_read_log...')
cd_log = send_cmd(args.port, 'cd_read_log', tail=4096)
print(f'Found {len(regions)} overlay region(s), {cd_log.get("total", 0)} CD DMA entries')
# Load existing manifest to avoid duplicates
existing = set()
if os.path.exists(args.out):
with open(args.out, encoding='utf-8') as f:
@@ -81,19 +123,25 @@ def main():
with open(args.out, 'a', encoding='utf-8') as f:
for r in regions:
key = (r['crc32'], r['addr'])
phys_addr = int(r['addr'], 16)
lba = find_lba_for_region(cd_log, phys_addr, r['size'])
lba_str = f'0x{lba:X}' if lba >= 0 else 'unknown'
if key in existing:
print(f' skip {r["addr"]} {r["size"]} bytes {r["crc32"]} (already logged)')
print(f' skip {r["addr"]} {r["size"]:>8} bytes lba={lba_str} (already logged)')
continue
entry = {
'crc32': r['crc32'],
'load_addr': r['addr'],
'size': r['size'],
'bytes_file': f'overlays/{r["file"]}',
'start_lba': lba_str,
}
f.write(json.dumps(entry) + '\n')
existing.add(key)
new_count += 1
print(f' new {r["addr"]} {r["size"]} bytes {r["crc32"]} -> {r["file"]}')
print(f' new {r["addr"]} {r["size"]:>8} bytes lba={lba_str} crc32={r["crc32"]}')
print(f'\n{new_count} new overlay(s) appended to {args.out}')
+289
View File
@@ -0,0 +1,289 @@
#!/usr/bin/env python3
"""
extract_overlays.py — Scan a PSX disc image for overlay byte regions.
Reads overlay_map.jsonl (committed metadata: crc32, load_addr, size).
Scans the disc via ISO 9660 file enumeration, computing CRC32 of each
file's bytes. When a file matches an overlay entry, writes its bytes to
overlays/<crc32>.bin for the recompiler (B-2).
Falls back to sector-by-sector scan for any overlays not found via ISO.
Supports:
BIN/CUE MODE2/2352 — standard PSX disc rip (2352 bytes/sector)
ISO 2048 bytes/sector — less common but handled
Usage:
python3 tools/extract_overlays.py \\
--disc <path/to/game.cue | game.bin | game.iso> \\
--manifest <overlay_map.jsonl> \\
--out <output_dir>
Outputs:
<out>/<CRC32HEX>.bin for each matched overlay
"""
import sys
import os
import re
import struct
import argparse
import binascii
# ---------------------------------------------------------------------------
# Disc sector reader
# ---------------------------------------------------------------------------
SECTOR_RAW = 2352 # MODE2/2352
SECTOR_DATA = 2048 # user data per sector
DATA_OFFSET = 24 # offset of user data in raw MODE2 sector
class DiscReader:
"""Reads user-data sectors from a PSX BIN/CUE or ISO image."""
def __init__(self, bin_path, raw=True):
self.f = open(bin_path, 'rb')
self.raw = raw # True = MODE2/2352, False = 2048-byte ISO
self.sec_sz = SECTOR_RAW if raw else SECTOR_DATA
fsize = os.path.getsize(bin_path)
self.total = fsize // self.sec_sz
def close(self):
self.f.close()
def read_sector_data(self, lba):
"""Return 2048 user-data bytes for the given LBA."""
offset = lba * self.sec_sz
self.f.seek(offset)
raw = self.f.read(self.sec_sz)
if len(raw) < self.sec_sz:
return b'\x00' * SECTOR_DATA
if self.raw:
return raw[DATA_OFFSET: DATA_OFFSET + SECTOR_DATA]
return raw[:SECTOR_DATA]
def read_file_bytes(self, lba, size):
"""Read exactly `size` bytes starting at `lba`."""
sectors_needed = (size + SECTOR_DATA - 1) // SECTOR_DATA
data = b''
for i in range(sectors_needed):
data += self.read_sector_data(lba + i)
return data[:size]
# ---------------------------------------------------------------------------
# CUE parser — extracts BIN path and track mode
# ---------------------------------------------------------------------------
def parse_cue(cue_path):
"""Return (bin_path, is_raw) from a .cue file."""
cue_dir = os.path.dirname(os.path.abspath(cue_path))
bin_path = None
is_raw = True # default: MODE2/2352
with open(cue_path, encoding='utf-8', errors='replace') as f:
for line in f:
m = re.match(r'\s*FILE\s+"([^"]+)"\s+BINARY', line, re.IGNORECASE)
if m:
bin_path = os.path.join(cue_dir, m.group(1))
m = re.match(r'\s*TRACK\s+\d+\s+(MODE\d/(\d+))', line, re.IGNORECASE)
if m:
is_raw = (int(m.group(2)) == SECTOR_RAW)
if bin_path is None:
raise ValueError(f'No BINARY FILE found in {cue_path}')
return bin_path, is_raw
# ---------------------------------------------------------------------------
# ISO 9660 directory walker
# ---------------------------------------------------------------------------
def _read_pvd(disc, lba=16):
"""Return root dir LBA and size from the Primary Volume Descriptor."""
data = disc.read_sector_data(lba)
if data[0] != 1 or data[1:6] != b'CD001':
raise ValueError('Sector 16 is not an ISO 9660 PVD')
root = data[156:190]
root_lba = struct.unpack_from('<I', root, 2)[0]
root_size = struct.unpack_from('<I', root, 10)[0]
return root_lba, root_size
def _iter_dir(disc, lba, size):
"""Yield (name, file_lba, file_size, is_dir) for each entry in a dir."""
raw = disc.read_file_bytes(lba, size)
pos = 0
while pos < len(raw):
rec_len = raw[pos]
if rec_len == 0:
# Skip to next sector boundary
pos = ((pos // SECTOR_DATA) + 1) * SECTOR_DATA
continue
if pos + rec_len > len(raw):
break
rec = raw[pos: pos + rec_len]
flags = rec[25]
is_dir = bool(flags & 0x02)
elba = struct.unpack_from('<I', rec, 2)[0]
esize = struct.unpack_from('<I', rec, 10)[0]
name_len = rec[32]
name = rec[33: 33 + name_len].decode('ascii', errors='replace')
name = name.split(';')[0] # strip version suffix
if name not in ('', '\x00', '\x01'):
yield name, elba, esize, is_dir
pos += rec_len
def enumerate_files(disc, dir_lba=None, dir_size=None, prefix=''):
"""Recursively yield (path, lba, size) for every file on the disc."""
if dir_lba is None:
dir_lba, dir_size = _read_pvd(disc)
for name, lba, size, is_dir in _iter_dir(disc, dir_lba, dir_size):
path = f'{prefix}/{name}' if prefix else name
if is_dir:
yield from enumerate_files(disc, lba, size, path)
else:
yield path, lba, size
# ---------------------------------------------------------------------------
# Overlay manifest
# ---------------------------------------------------------------------------
def load_manifest(jsonl_path):
import json
entries = []
with open(jsonl_path, encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line:
continue
e = json.loads(line)
lba_raw = e.get('start_lba', 'unknown')
entries.append({
'crc32': int(e['crc32'], 16),
'load_addr': int(e['load_addr'], 16),
'size': int(e['size']),
'start_lba': int(lba_raw, 16) if lba_raw != 'unknown' else -1,
})
return entries
def crc32_of(data):
return binascii.crc32(data) & 0xFFFFFFFF
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--disc', required=True,
help='Path to .cue, .bin, or .iso disc image')
ap.add_argument('--manifest', required=True,
help='overlay_map.jsonl with (crc32, load_addr, size)')
ap.add_argument('--out', default='overlays',
help='Output directory for .bin files (default: overlays/)')
args = ap.parse_args()
os.makedirs(args.out, exist_ok=True)
# Open disc
disc_path = args.disc
if disc_path.lower().endswith('.cue'):
bin_path, is_raw = parse_cue(disc_path)
else:
bin_path = disc_path
is_raw = not disc_path.lower().endswith('.iso')
print(f'Disc : {bin_path} ({"MODE2/2352" if is_raw else "ISO 2048"})')
disc = DiscReader(bin_path, raw=is_raw)
overlays = load_manifest(args.manifest)
print(f'Manifest: {len(overlays)} overlay(s) to find')
print()
# Build lookup by crc32
remaining = {o['crc32']: o for o in overlays}
found = {}
# --- Pass 0: Direct LBA extraction (fastest — uses cd_read_log data) ---
print('Pass 0: direct LBA extraction (from cd_read_log)...')
for crc, o in list(remaining.items()):
lba = o.get('start_lba', -1)
if lba < 0:
continue
size = o['size']
data = disc.read_file_bytes(lba, size)
actual_crc = crc32_of(data)
if actual_crc == crc:
remaining.pop(crc)
out_name = f'{crc:08X}.bin'
out_path = os.path.join(args.out, out_name)
with open(out_path, 'wb') as f:
f.write(data)
found[crc] = (f'lba:{lba}(direct)', lba, size)
print(f' FOUND lba={lba} load=0x{o["load_addr"]:08X}'
f' size={size} -> {out_name}')
else:
print(f' MISMATCH lba={lba} expected=0x{crc:08X}'
f' got=0x{actual_crc:08X} (will try ISO/brute-force)')
# --- Pass 1: ISO 9660 file scan (fast) ---
print('Pass 1: ISO 9660 file enumeration...')
try:
for path, lba, size in enumerate_files(disc):
if not remaining:
break
# Try exact file size and also overlay-truncated size
for try_size in sorted({size, *[o['size'] for o in remaining.values()]}):
if try_size > size + SECTOR_DATA:
continue # file too small to cover this overlay
data = disc.read_file_bytes(lba, try_size)
crc = crc32_of(data[:try_size])
if crc in remaining:
o = remaining.pop(crc)
out_name = f'{crc:08X}.bin'
out_path = os.path.join(args.out, out_name)
with open(out_path, 'wb') as f:
f.write(data[:o['size']])
found[crc] = (path, lba, size)
print(f' FOUND {path:40s} load=0x{o["load_addr"]:08X}'
f' size={o["size"]} -> {out_name}')
break
except Exception as e:
print(f' ISO scan error: {e}')
# --- Pass 2: brute-force sector scan for anything not found ---
if remaining:
print()
print(f'Pass 2: brute-force sector scan for {len(remaining)} unfound overlay(s)...')
for ov_crc, ov in list(remaining.items()):
sz = ov['size']
nsec = (sz + SECTOR_DATA - 1) // SECTOR_DATA
print(f' Scanning for crc32=0x{ov_crc:08X} size={sz} ({nsec} sectors)...',
flush=True)
found_it = False
for lba in range(disc.total - nsec + 1):
data = disc.read_file_bytes(lba, sz)
if crc32_of(data) == ov_crc:
remaining.pop(ov_crc)
out_name = f'{ov_crc:08X}.bin'
out_path = os.path.join(args.out, out_name)
with open(out_path, 'wb') as f:
f.write(data)
found[ov_crc] = (f'lba:{lba}', lba, sz)
print(f' FOUND lba={lba} load=0x{ov["load_addr"]:08X}'
f' size={sz} -> {out_name}')
found_it = True
break
if not found_it:
print(f' NOT FOUND')
disc.close()
print()
print(f'Results: {len(found)} found {len(remaining)} not found')
if remaining:
print('Not found (play through more content and re-run dump_overlays.py):')
for crc, o in remaining.items():
print(f' 0x{crc:08X} load=0x{o["load_addr"]:08X} size={o["size"]}')
else:
print('All overlays extracted successfully.')
if __name__ == '__main__':
main()