mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 18:52:11 -04:00
e6ce5006b4
The repository root carried seven markdown files that were not onboarding
material, plus two that had been superseded by the code they described. This
sorts them: reader-facing docs to docs/, working notes to docs/internal/, and
the two superseded ones merged into the docs that replaced them.
Moved to docs/: WIDESCREEN.md, ENHANCEMENTS.md, TCP_COMMANDS.md
Moved to docs/internal/: PRINCIPLES.md, ISSUES.md, DEBUG.md, GTE_LM_FIX.md,
and from docs/: bios-hardening.md, overlay-status.md,
overlay-plan.md, overlay-recompilation-design.md,
overlay-discovery.md, TOMBA2_EXC_CONSULT_RESPONSE.md,
CASE_A_AOT_GAP.md, audit_inventory.md
Moved to docs/internal/upstream/: UPSTREAM_2026-08-20.md
The root keeps only what belongs there: README.md, CONTRIBUTING.md, LICENSE,
CLAUDE.md, THIRD_PARTY_ATTRIBUTION.md, RELEASE_NOTES.md, the dotfiles and
psxrecomp_cli.py.
MERGES
SPLITGEN_MIGRATION.md folded into docs/BUILDING.md as "Split generated C". Its
migration instructions were stale in a way that mattered: it told every game
repo to hand-edit its CMakeLists and add a file(GLOB ...) over
<serial>_full*.c, but runtime/runtime.cmake now does that itself. GEN_FULL_GLOB
is a multi-value argument of psxrecomp_add_game_runtime(); the loop at
runtime.cmake:1985 makes each pattern absolute against CMAKE_CURRENT_SOURCE_DIR,
expands it with file(GLOB), and concatenates the hits into
GAME_GENERATED_FULL_C. So a title names the pattern and gets however many shards
a regeneration produced. The rewritten section describes that, the two-step
fallback when the globs match nothing (GEN_FULL_FALLBACK, else <serial>_full.c
derived from the dispatch marker by substituting _dispatch.c), and the one
caveat the code actually has: that file(GLOB) is plain, not CONFIGURE_DEPENDS,
so a regeneration which changes the shard COUNT needs a re-configure to be seen.
psxrecomp-toml-readme.md folded into docs/GAME_PROJECT_SETUP.md as "EXE-only
autofill (psxrecomp-toml)", placed after "Disc autofill (probe_disc.py)" and
before "Project Studio" so the two autofill paths sit together. Its documented
flags were checked against the usage text in recompiler/src/main_toml.cpp:28-42
and against the argument parser at :150-165 -- all seven options match with no
drift, so the flag list is reproduced as-is. Added a pointer that probe_disc.py
is preferable when a cue exists, since that tool does the same EXE-header work
plus disc identity and netplay gates.
FUNCTIONAL FIXES
tools/gen_tcp_commands.py wrote to REPO/"TCP_COMMANDS.md". After the move that
path does not exist, so the generator would have created a new stray root file
and --check would have failed against it forever. Retargeted to
REPO/"docs"/"TCP_COMMANDS.md", along with the seven docstring and message
strings that name the path.
Running it surfaced two further problems, both fixed here:
* The doc was genuinely stale -- pre-existing drift, not introduced by this
commit (the moved file is byte-identical to origin/master, and no
debug-server command table is touched here). Four commands registered since
the last regeneration were missing from the index -- cdrom_timing_dump,
savestate_status, ws_ui_groups, xprobe_watch -- and scanline had gained a
backend. Regenerated: 307 -> 311 commands, and the change is confined to the
AUTOGEN block (the hand-written head and tail are byte-identical to master).
tools/gen_tcp_commands.py --check now exits 0.
* DOC.write_text() has no newline argument, so Python translated every newline
to os.linesep. On Windows that rewrote all 585 lines of the file to CRLF and
turned a four-line index update into a whole-file diff -- measured: 589 CRLF
/ 0 LF after a run, against 0 CRLF / 585 LF in the repo. Now written through
an explicit newline="" handle, verified to leave the file at 0 CRLF.
runtime/include/debug_server.h:9 and runtime/src/debug_server.c:8 cited "TCP.md
and DEBUG.md". TCP.md has never existed in this repo under that name; both now
name docs/TCP_COMMANDS.md and docs/internal/DEBUG.md.
LINK AND REFERENCE FIXES
Every inbound reference to a moved file was located by grepping each basename
across the whole tracked tree, not just the markdown. Markdown hrefs in
README.md, CONTRIBUTING.md, docs/ARCHITECTURE.md, docs/COMPILING_OVERLAYS.md,
docs/AOT_OVERLAY_PLAN.md, docs/RELOCATION_MANIFEST_FORMAT.md,
docs/config_schema.md, docs/OVERLAY_XREF.md, docs/TCP_COMMANDS.md and several
docs/internal/ files now point at the new locations; where two files became
siblings under docs/internal/ the path was shortened rather than lengthened
(bios-hardening.md's reference to overlay-discovery.md, for instance).
docs/COMPILING_OVERLAYS.md's "in this folder" phrasing was wrong once two of the
three files it listed moved, so it now names internal/ explicitly.
24 source-comment mentions of ENHANCEMENTS.md sections G1.x across 19 files
(recompiler, runtime, cmake) were retargeted to docs/ENHANCEMENTS.md with the
section numbers untouched. docs/SHADOW_ENHANCEMENTS.md is a DIFFERENT file and
was deliberately excluded.
20 rationale strings citing docs/CASE_A_AOT_GAP.md across the four
recompiler/seeds/*.json files were updated. Done as a byte-level substring
replacement rather than a JSON round-trip: two of those files are stored with
CRLF endings and two with LF, and re-serializing them reformatted 5,700 lines.
The byte-level version changes exactly 20 lines and each file still parses.
References that look similar but point elsewhere were deliberately left alone,
and are worth naming so a future pass does not "fix" them:
* "recomp-template PRINCIPLES.md" / "NES recomp template PRINCIPLES.md" in
CLAUDE.md:43, docs/SHADOW_ENHANCEMENTS.md:28,
docs/internal/HLE_SCHEDULER_CARVEOUT_PLAN.md:5,
runtime/src/debug_server.c:4934 and tools/parity_diff.py:8 are a different
repository's file. Only the five that mean this repo's PRINCIPLES.md were
updated.
* Every remaining bare ISSUES.md mention is a GAME repo's tracker
(MegaManX6Recomp 2/7/8/9, TombaRecomp 6), not this repo's.
* docs/disc-speed.md:4,54,56 and docs/internal/bios-hardening.md:5 name
overlay-discovery as a BRANCH, not a file, so no path applies.
INDEXES
README.md's "New here?" table gains rows for docs/WIDESCREEN.md,
docs/ENHANCEMENTS.md and docs/TCP_COMMANDS.md. docs/internal/README.md gains
sections indexing all fourteen documents that moved in, plus an "Upstream
contributor reviews" section describing upstream/ and the new sweep file, plus a
"Removed" bullet for CYCLE_TIMING_ARCH.md (deleted in the previous commit)
pointing at FAITHFUL_TIMING_PLAN.md, CYCLE_MODEL_BEETLE.md and
PRECISE_IRQ_SLICE.md -- following the SLJIT_PERSIST_CACHE.md precedent already
in that file.
docs/ENHANCEMENTS.md:7 claimed framework bugs live in "game repos' ISSUES.md
until a framework tracker exists". A framework tracker does exist; it now says
so and names the Beads epic.
VERIFICATION
A link checker was written to scan every tracked *.md for relative links,
resolving each against the tree being scanned. Baseline at origin/master: 129
files, 137 relative links, 3 broken. After both commits: 127 files, 159 relative
links, 3 broken -- the SAME three, all pre-existing and untouched here:
docs/DATA_SHARDS.md -> INSTANT_LOADS_PARKED.md (never existed)
docs/DATA_SHARDS.md -> INSTANT_LOADS_SPIKE2.md (never existed)
docs/NETPLAY.md -> ../lib/recomp-net/README.md (inside a submodule)
Zero new broken links. The checker's own coverage was checked against a raw grep
for markdown link openers: the 3-link difference is C++ lambdas inside code
blocks, correctly not treated as links.
Also verified: py_compile over all 113 tracked tools/*.py plus psxrecomp_cli.py;
all four seed JSONs still parse; the test-registration guard still reports 123
test files all registered; recompiler/tests/test_coverage_vault.py still passes;
and no file in this commit had its line endings flipped (checked mechanically
across all 50 modified files, after two files were caught and redone).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
202 lines
6.7 KiB
Markdown
202 lines
6.7 KiB
Markdown
# 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 |
|