mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 02:56:36 -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>
708 lines
32 KiB
Markdown
708 lines
32 KiB
Markdown
# PSXRecomp v4 — Open Issues
|
||
|
||
## Issue #1 — Per-card directory load never runs in recomp (Phase 4 blocker)
|
||
|
||
**Status:** open, root cause narrowed
|
||
**Date opened:** 2026-05-03
|
||
**Phase:** 4 (BIOS shell + memory-card screen)
|
||
|
||
### Symptom
|
||
|
||
Recomp and Beetle both reach the same MEMORY CARD menu screen with the
|
||
same correct PSX rendering. **Beetle renders 3 save-block icons in
|
||
CARD 1 + 1 face icon in CARD 2; recomp renders zero icons.** The
|
||
divergence is the per-card directory load — Beetle reads it from the
|
||
on-disk card files, recomp does not.
|
||
|
||
(Note: the rainbow buttons are the actual PSX BIOS rendering — both
|
||
sides have it. Not a bug.)
|
||
|
||
### Hard data — gate-mode write distribution
|
||
|
||
Captured side-by-side, both runtimes driven into the memcard screen
|
||
with a 240-frame CROSS hold via `press_into_card_view*.py`:
|
||
|
||
| Mode | Beetle | Recomp | Notes |
|
||
|------|--------|--------|-------|
|
||
| 0x01 | 122 | 21 | chain reset / BUSY clear |
|
||
| 0x02 | **96** | 16 | R-step install (read-step) |
|
||
| 0x04 | 2 | 6 | step-3 install |
|
||
| 0x08 | 22 | 3 | D-step install (detect) |
|
||
| 0x21 | **0** | **6** | init+flag — recomp-only path |
|
||
|
||
R-step (mode 0x02) caller breakdown:
|
||
|
||
| caller (`ra`) | Beetle | Recomp |
|
||
|----|----|----|
|
||
| 0xBFC08F28 (icon-load loop) | **40** | **0** |
|
||
| 0xBFC09250 | **16** | **0** |
|
||
| 0xBFC08D08 | 30 | 6 |
|
||
| 0xBFC08A0C | 8 | 4 |
|
||
| 0xBFC08B98 | 2 | 6 |
|
||
|
||
**Recomp never reaches the icon-load loop.** That is the divergence.
|
||
|
||
### What we proved is *not* the cause
|
||
|
||
- `mem[0x80007520]` ("chain-restart discriminator") is **not** the
|
||
divergence. Both sides have it at 0 in steady state. The earlier
|
||
hypothesis that recomp=0 / Beetle=non-zero at the decision point was
|
||
wrong — both sides clear it in the JAL-delay-slot at PC `0xBFC14CE0`
|
||
(= RAM 0x51E0) every 2 frames before calling `func_00004D6C`.
|
||
- The texture corruption on the menu buttons is correct PSX
|
||
rendering, not a bug.
|
||
- The per-slot FP table at `mem[0x7528+slot*4]` IS swapped
|
||
dynamically by recomp too (between `0x5B64`, `0x5688`, `0x51F4`) —
|
||
the swap mechanism works. Beetle just performs additional swaps from
|
||
callers `BFC08F28` and `BFC09250` that recomp never reaches.
|
||
- `func_00006380` (writes `mem[0x75C0]=1`) and the B-table trampolines
|
||
at `BFC0DA30` / `BFC0DA40` (B0:0x58, B0:0x4D `_card_status`) DO run in
|
||
recomp — `BFC0DA30` 620×, `BFC0DA40` 618×. The trampoline machinery
|
||
works for those.
|
||
|
||
### Suspected root cause (not yet confirmed)
|
||
|
||
Recomp **never enters `func_1FC08B3C`** — the per-card directory load
|
||
function. The icon-load loop at PC `0xBFC08F1C..0xBFC08F7C` lives
|
||
inside this function. The loop does `jal 0xBFC0DA00` (= B0:0x4F
|
||
`_card_read`) once per directory entry; Beetle hits this 20× per
|
||
session.
|
||
|
||
Wtrace activity that *appears* to put recomp inside `func_1FC08B3C`
|
||
(mode 0x02 writes with `ra=0xBFC08B98`) is reached via the FP-table
|
||
swap path, not via the icon loop. The wider function entry at
|
||
`label_BFC08B3C` is never logged in `fn_entry_dump` for the entire
|
||
[0x1FC08000, 0x1FC0A000) range across 134M captured ring entries.
|
||
|
||
`func_1FC08B3C` IS in the recomp dispatch table (entry 5449,
|
||
`{ 0x1FC08B3Cu, func_1FC08B3C }`) plus 20+ continuation entries. So
|
||
the function is recompiled and dispatchable; it's the *caller* that
|
||
isn't reaching it.
|
||
|
||
### Open questions for the next session
|
||
|
||
1. **What calls `func_1FC08B3C`?** Find the static caller (or callers)
|
||
of ROM `0xBFC08B3C`. Use Ghidra `xrefs 0xBFC08B3C` to enumerate.
|
||
2. **Does recomp ever reach `func_000005E0` (B0 dispatcher) and
|
||
`func_000005C4` (A0 dispatcher)?** These live in the recomp
|
||
dispatch table; if recomp doesn't enter them, B-table dispatch is
|
||
broken. Was about to check this when the runtime froze on a
|
||
270M-entry `fn_entry_dump`.
|
||
3. **Counter-mismatch hypothesis:** in `func_00004D6C`, the FP-call
|
||
chooses between several outcomes based on `mem[0x7514]`-1.
|
||
`func_00005B64` (the dispatcher FP) returns -1 when counter=0,
|
||
forcing the 0x21 path. Both sides clear `mem[0x7514]` in the same
|
||
place every 2 frames, but Beetle's writes show counter cycles
|
||
1-2-3-4-5-6 inside a single window (132 cycles × 6 calls), while
|
||
recomp's data showed cycles up to 13 with different clear sources.
|
||
The cycle structure differs — needs deeper analysis.
|
||
|
||
### Diagnostic infrastructure built / fixed this session
|
||
|
||
- `tools/summarize_gate_modes.py` — tallies gate writes by value
|
||
(mode 0x01/0x02/0x04/0x08/0x21) per caller, recomp + beetle
|
||
- `tools/_dump_gate_seq.py` — dump gate writes in seq order with
|
||
`pc/fn/ra/w` fields (extends earlier handoff tool)
|
||
- `tools/_dump_addrs.py` — group wtrace by addr, show distinct
|
||
writers per addr
|
||
- `tools/_dump_7520_beetle.py` — filter beetle wtrace by addr range
|
||
- `tools/_dump_ram_words.py` — RAM dump as 4-byte words, both backends
|
||
- `tools/_read_7520.py` — sample mem[0x7520], 0x755A, 0x7568
|
||
- `tools/_screenshot_meta.py` — call screenshot/screenshot_file/
|
||
emu_screenshot
|
||
- `tools/check_pc_dispatch.py` — group `fn_entry_dump` by `func_addr`
|
||
in a range
|
||
- `tools/press_into_card_view_recomp.py` — mirror Beetle's
|
||
press_into_card_view but for recomp
|
||
- `tools/arm_gate_trace.py` extended with `[0x7520..0x7524)`,
|
||
`[0x7514..0x7518)`, `[0x7528..0x7538)`, `[0x7258..0x7260)` ranges
|
||
|
||
### Tooling fix needed (CLAUDE.md Rule 15)
|
||
|
||
`fn_entry_dump` iterates the entire ring (up to 270M entries) before
|
||
applying the addr filter, which **freezes the debug server thread for
|
||
seconds-to-minutes** on a populated ring. Both `handle_fn_entry_dump`
|
||
and `handle_fn_exit_dump` in `runtime/src/debug_server.c` need the
|
||
addr filter applied early, plus a `seq_lo/seq_hi` window cap, so a
|
||
filtered query for a small range returns immediately.
|
||
|
||
### Files with new memories on disk
|
||
|
||
- `memory/MEMORY.md` (will be updated next session with the
|
||
"0x7520 is NOT the divergence — directory-load function never
|
||
entered" finding)
|
||
|
||
### Update 2026-05-03 (continued)
|
||
|
||
**Static call chain to the directory load (via Ghidra xrefs):**
|
||
|
||
```
|
||
????
|
||
-> FUN_bfc24640 / FUN_bfc24a48 / FUN_bfc1cdd8 (3 of the 5 _card_load callers)
|
||
jal 0xB005A9B0 (= jal 0xBFC42BB0 = B0:0x15 _card_load)
|
||
-> FUN_bfc0dac0 (kernel B0:0x15 trampoline body)
|
||
jr $t2 with $t1=0x15 → kernel B0 dispatcher
|
||
-> _card_load (B0:0x15 body in kernel image)
|
||
calls FUN_bfc09914
|
||
-> FUN_bfc09914
|
||
FUN_bfc08b3c(0) ; load card-1 directory
|
||
FUN_bfc08b3c(0x10); load card-2 directory
|
||
-> FUN_bfc08b3c (the icon-load loop at BFC08F1C..BFC08F7C)
|
||
jal 0xBFC0DA00 (= B0:0x4F _card_read) per directory entry
|
||
```
|
||
|
||
**On recomp NONE of `FUN_bfc24640`, `FUN_bfc24a48`, `FUN_bfc1cdd8`,
|
||
`FUN_bfc09914`, `FUN_bfc08b3c`, `FUN_bfc0c2e8` are ever called.**
|
||
The shell never reaches the call sites. Per
|
||
`memory/phase4_b0_trampoline_findings.md`: "The shell has a conditional
|
||
code path for card init. On DuckStation, the shell enters the card
|
||
detection path. In our runtime, this condition fails, so card init is
|
||
skipped." This is the same blocker.
|
||
|
||
**Static caller of FUN_bfc0c2e8 (BootInitMemcards) is NOT static.** It
|
||
has zero static xrefs, and the literal `0xBFC0C2E8` does not appear
|
||
anywhere in ROM. It's invoked via a kernel function pointer table
|
||
populated at runtime.
|
||
|
||
**Update late 2026-05-03 — actual call path identified via Beetle
|
||
fn_trace:**
|
||
|
||
Beetle's fn_trace ring (with `0x1FC09914` armed as a target) captures:
|
||
- `seq=904 JR caller=0x000005D8 ra=0x8003202C` jumping to FUN_bfc09914
|
||
- caller PC `0x000005D8` is INSIDE the kernel A0/B0 dispatcher area
|
||
(A0 dispatcher entry is `0x000005C4`, B0 dispatcher is `0x000005E0`)
|
||
- `ra=0x8003202C` means the JAL that started this chain was at
|
||
`0x80032024` — which translates to ROM `0xBFC1A024`
|
||
- Disassembly at ROM 0xBFC1A024:
|
||
```
|
||
bfc1a000: li $t8, 1
|
||
bfc1a004: bne $t9, $zero, 0xbfc1a040 ; SKIP if already initialized
|
||
bfc1a008: sw $t8, -0x5880($at) ; (delay slot) set init flag
|
||
bfc1a00c: jal 0xb005a960 ; B0:0x4A InitCARD($a0=1)
|
||
bfc1a010: li $a0, 1
|
||
bfc1a014: jal 0xb005a970 ; B0:0x4B StartCARD($a0=0)
|
||
bfc1a018: nop
|
||
bfc1a01c: jal 0xb005a980 ; B0:0x5B ChangeClearPad
|
||
bfc1a020: clear $a0
|
||
bfc1a024: jal 0xb005a990 ; A0:0x70 (_card_load equivalent)
|
||
bfc1a028: nop
|
||
bfc1a02c: jal 0xb005a9a0 ; A0:0xAD
|
||
bfc1a030: li $a0, 1
|
||
bfc1a034: li $t0, 1
|
||
bfc1a038: lui $at, 0x8006
|
||
bfc1a03c: sw $t0, 0x66f8($at) ; mark mem[0x800666F8]=1 (init done)
|
||
bfc1a040: lw $ra, ...
|
||
```
|
||
- Shell calls `jal 0xB005A990` at PC `0xBFC1A024`. The trampoline at
|
||
`0xBFC42990` is **A0:0x70**, which the kernel A-table dispatches to
|
||
`FUN_bfc09914` (the directory loader caller).
|
||
- So the actual chain is **A0:0x70**, not B0:0x15. The 5 static
|
||
callers of `FUN_bfc0c2e8` (FUN_bfc24640 etc.) are red herrings —
|
||
they handle COPY/DELETE actions, not boot-time card load.
|
||
|
||
**Recomp blocker:** the shell function `FUN_bfc1a000` is reached via
|
||
indirect dispatch (no static caller in ROM). It is in recomp's
|
||
dispatch table (`{0x1FC1A000u, func_1FC1A000}`), but the shell never
|
||
dispatches to it. OR — the shell DOES dispatch to it, but the
|
||
`bne $t9, $zero, 0xBFC1A040` branch at PC 0xBFC1A004 skips the init
|
||
because `$t9 != 0` somehow.
|
||
|
||
**Confirmed already-known issue.** This matches
|
||
`memory/phase4_b0_trampoline_findings.md`: "The shell has a
|
||
conditional code path for card init. ... In our runtime, this
|
||
condition fails, so card init is skipped and the shell goes directly
|
||
to the main menu (state 0x37)."
|
||
|
||
### Concrete next step
|
||
|
||
1. Find what calls `FUN_bfc1a000` on Beetle. Arm `0x1FC1A000` in
|
||
Beetle fn_trace and capture the caller. (One JAL produces the
|
||
chain.)
|
||
2. On recomp, check whether `func_1FC1A000` is ever entered. If yes:
|
||
capture `$t9` value at entry. If $t9!=0, find what set it.
|
||
If no: trace the upstream call chain to find why the JAL to
|
||
`func_1FC1A000` doesn't fire.
|
||
3. Fix the recompiler's emit / generator for whatever instruction or
|
||
branch is producing the divergent state. NEVER hand-patch the
|
||
generated code or hand-call `func_1FC1A000`.
|
||
|
||
### Update 2026-05-03 (continued, late session)
|
||
|
||
**Step 1's address range was wrong** — `0x1FC1A000` is a label
|
||
INSIDE `func_1FC19FD0`, not the function entry. Querying for the
|
||
correct entry `0x1FC19FD0` shows recomp DOES enter:
|
||
- `func_1FC19FD0` (BootInitMemcardsShell) at frame 617/2344 with
|
||
`$a0=0x00F000F0` (matches the AND-mask gate at BFC19FDC).
|
||
- The full init chain runs: `mem[0x800666F8]` (init done flag) is set
|
||
to `1`, meaning all 5 inner JALs (InitCARD, StartCARD,
|
||
ChangeClearPad, A0:0x70, A0:0xAD) executed.
|
||
- `FUN_bfc09914` is entered at frame 617/2344 with `$a0=0`
|
||
(`ra=0x8003202C` — confirming the JAL chain from BootInitMemcardsShell).
|
||
- `FUN_bfc08b3c` is entered **12 times** across both slots:
|
||
frame 2344/2350 (initial calls from FUN_bfc09914 with `ra=BFC09934`,
|
||
`BFC0993C`), then frames 2372/2385/2426/2433 (subsequent calls from
|
||
`FUN_bfc089c0` `ra=BFC08A90`).
|
||
|
||
**Beetle fn_trace confirms the call chain**:
|
||
```
|
||
JALR @ 0x8004673C → 0x80031FD0 (BootInitMemcardsShell)
|
||
$a0=0x00F000F0, $a1=0x00F000F0, ra=0x80030968
|
||
```
|
||
The caller PC `0x8004673C` is inside a function at shell-RAM
|
||
`0x800466B8` (= ROM `0xBFC2E6B8`), entered via `JAL` from PC
|
||
`0x80030960`. The function reads a function-pointer table at
|
||
`mem[$a3 + idx*0x88 + 0x34]` and dispatches devices. For card
|
||
init, `$t2` = pointer to BootInitMemcardsShell. Recomp ALSO reaches
|
||
this function (per `func_1FC2E6B8` having entries) — the BIOS shell
|
||
init chain IS running on recomp end-to-end.
|
||
|
||
**The actual root cause is one level deeper.** `FUN_bfc08b3c`
|
||
runs but **bails out in the 15-sector init loop before reaching the
|
||
icon-load loop** at PC `0xBFC08F1C..BFC08F7C`:
|
||
|
||
- `_card_read` (B0:0x4F via trampoline `BFC0DA00`) is called 32 times
|
||
on recomp — `ra=BFC08B98` (sector 0 from FUN_bfc08b3c entry),
|
||
`ra=BFC08D08` (sectors 1+ from the 15-sector loop), `ra=BFC08A0C`
|
||
(FUN_bfc089c0's first read).
|
||
- Sector 0 read SUCCEEDS — `mem[0xA000BE48]` shows `'M' 'C' 0 0 …
|
||
0 0 0 0x0E` — the MC magic AND a valid XOR checksum (XOR of bytes
|
||
0..126 = 0x0E = byte 127). The buffer is correctly filled by
|
||
recomp's SIO/card simulation for sector 0.
|
||
- The 15-sector loop runs only ~4 iterations per slot before bailing
|
||
to `LAB_bfc08f8c`. Confirmed: `mem[0xA000B9E8..]=0xFFFFFFFF` is
|
||
exactly the bail-clear pattern (`for (i=0; i<0x14; i++) puVar10[i]=
|
||
0xFFFFFFFF`). And `mem[0xA000BA88]=0` is the bail-clear for the
|
||
directory area.
|
||
- The validator `FUN_bfc08720` is **never entered** — confirming the
|
||
bail happens BEFORE the checksum check, in the read-gate:
|
||
|
||
```c
|
||
iVar6 = FUN_bfc0da00(slot, sector, buf);
|
||
if ((iVar6 != 1) || (iVar6 = FUN_bfc09144(), iVar6 == 0)) {
|
||
goto LAB_bfc08f8c; // ← bail
|
||
}
|
||
iVar6 = FUN_bfc08720(buf); // ← never called
|
||
```
|
||
|
||
Either:
|
||
- (a) `_card_read` returns `!= 1` for sector ≥ 1 (i.e. recomp's SIO
|
||
card simulation only completes sector-0 reads correctly), OR
|
||
- (b) `FUN_bfc09144` returns 0 — meaning one of the error flags
|
||
`mem[0xA000B9D4..B9E0]` was set instead of the success flag
|
||
`mem[0xA000B9D0]`. The error flags are set by the recomp's
|
||
SIO IRQ / chain coordinator on read failure.
|
||
|
||
**Concrete next step (next session):** capture `_card_read` return
|
||
values via `fn_exit_dump` of `0x1FC0DA00` while card init is happening
|
||
(window from frame ~1029-1100), and capture the wtrace lifecycle of
|
||
`mem[0xA000B9D0]` and `mem[0xA000B9D4..B9E0]` during the same window
|
||
to determine which flag fires for the failing sector.
|
||
|
||
**Or simpler**: The fix is in `runtime/src/memcard.c` /
|
||
`runtime/src/sio.c`. The SIO/card simulation handles sector 0
|
||
correctly but fails on sector ≥ 1. The issue is in hardware
|
||
simulation, NOT recompiler/codegen. Inspect how the runtime simulates
|
||
the multi-sector read protocol.
|
||
|
||
**This session's findings invalidate the handoff's primary
|
||
hypothesis** (`recomp never enters func_1FC1A000`). The shell init
|
||
chain IS reaching directory load. The blocker is the runtime's SIO
|
||
card simulation failing sector ≥ 1 reads. Per the handoff: "Cycle-paced
|
||
SIO works well enough for card detection. Sector-0 detection works."
|
||
That hypothesis is correct, but sector ≥ 1 is the new blocker.
|
||
|
||
**Failed attempt: 67 trampoline seeds (regressed shell main-menu
|
||
transition).** I added all 67 missing A0/B0/C0 trampolines (BFC0D8E0..
|
||
BFC0D940, BFC16550..BFC16750, BFC42xxx including BFC42BB0=B0:0x15) to
|
||
`recompiler/seeds/dispatch_miss_seeds.json`, regenerated, rebuilt.
|
||
Result: recomp never advanced past the MAIN MENU — pressing CROSS no
|
||
longer transitioned to the memcard menu. Reverted seeds via
|
||
`git checkout HEAD -- recompiler/seeds/`. Conclusion: dispatching the
|
||
shell trampolines through recompiled C functions broke shell flow.
|
||
The existing dirty_ram_dispatch handles them correctly; do not reseed.
|
||
|
||
**Tool fix landed:** `runtime/src/debug_server.c::fn_dump_parse` now
|
||
defaults to a 1M-entry sliding window instead of the full 128M ring,
|
||
preventing the multi-second freeze that previously made `fn_entry_dump`
|
||
unusable on populated rings.
|
||
|
||
### Next session — laser focus
|
||
|
||
The blocker is "find what kernel/shell function call chain *should*
|
||
invoke `FUN_bfc0c2e8` (BootInitMemcards) at boot, and why recomp's
|
||
shell skips it." Suggested approach:
|
||
|
||
1. Find the kernel function pointer table that contains
|
||
`0xBFC0C2E8`. The address must appear *somewhere* — maybe in a
|
||
table populated at runtime by `lui+addiu` instructions in another
|
||
function. Search Ghidra for `BFC0` `C2E8` halfword pairs.
|
||
2. Look at SystemInit / boot chain (around `0xBFC00150` reset
|
||
vector). Compare DuckStation/Beetle's static fn_trace through boot
|
||
vs recomp's — find where the divergence first occurs.
|
||
3. The goal is a single decision point in shell init code where
|
||
recomp takes the "skip card init" branch and Beetle takes the
|
||
"do card init" branch. Compare register state at that branch.
|
||
|
||
---
|
||
|
||
## Issue #2 — `fn_entry_dump` / `fn_exit_dump` freeze the debug server on populated rings
|
||
|
||
**Status:** open
|
||
**Date opened:** 2026-05-03
|
||
|
||
`runtime/src/debug_server.c::handle_fn_entry_dump` and
|
||
`handle_fn_exit_dump` walk every entry in the 64K-cap ring (or rather
|
||
the entire `[seq_lo, seq_hi)` window, which can be the full ring)
|
||
applying the addr filter only AFTER constructing the per-entry buffer
|
||
position. With 270M cumulative entries and a 1GB output buffer, a
|
||
filtered query for a 0x200-byte addr range still iterates 134M
|
||
entries before returning anything. Debug server is single-threaded so
|
||
all other commands stall (including `ping`).
|
||
|
||
**Fix:** apply `addr_lo/hi` filter as the first check inside the
|
||
loop before any string formatting; cap `seq_hi - seq_lo` to a
|
||
sensible default (e.g. 2M) when the caller doesn't pass an explicit
|
||
window.
|
||
|
||
---
|
||
|
||
## Issue #3 — BIOS "PlayStation" disc-detected screen is missing the PS logo glyph
|
||
|
||
**Status:** open
|
||
**Date opened:** 2026-05-11
|
||
**Phase:** 4 (BIOS shell render)
|
||
|
||
### Symptom
|
||
|
||
After the Sony logo, the BIOS shows the second boot screen (the
|
||
"PlayStation" disc-detected screen rendered when a disc is present).
|
||
On this screen the top region — which on real hardware shows the
|
||
stylized PS logo bitmap — is blank. The text below it ("PlayStation",
|
||
license string, etc.) renders correctly.
|
||
|
||
Cosmetic only; boot continues, the disc loads, and Tomba's FMVs start.
|
||
Logged here so we don't lose track of it once the FMV cluster is fixed.
|
||
|
||
### Likely areas
|
||
|
||
- GPU VRAM upload for the logo bitmap (CopyRectangle / CPU->VRAM
|
||
transfer) may be dropping a region. The other tiles on the same
|
||
screen render, so it isn't a wholesale VRAM/clut wipe.
|
||
- Or the logo is sourced from a CD raw-sector / sector-header data
|
||
path that the current `cdrom.c` whole-sector mode doesn't expose
|
||
the way the BIOS expects.
|
||
|
||
### Concrete next step
|
||
|
||
Take a Beetle screenshot of the same boot screen as oracle, diff the
|
||
VRAM region the logo lives in (Beetle vs recomp at the same frame),
|
||
and walk back from the missing pixels via `wtrace` on the source
|
||
RAM/VRAM coordinates.
|
||
|
||
---
|
||
|
||
## Issue #5 — Mid-function split targets not registered in dispatch table
|
||
|
||
**Status:** open, audit-surfaced
|
||
**Date opened:** 2026-05-12
|
||
**Phase:** platform (recompiler emit, full_function_emitter path)
|
||
**Bug class:** same shape as 2026-05-12 jump-table cross-function fix
|
||
(`full_function_emitter.cpp:777`).
|
||
|
||
### Symptom
|
||
|
||
When the recompiler emits a branch/jump whose target is a
|
||
mid-function address (neither a known block in the current CFG nor a
|
||
known function start), it emits `call_by_address(cpu, 0xX); return;`
|
||
to defer dispatch to the runtime — but does NOT register `0xX` in the
|
||
dispatch table. At runtime, the dispatch binary-search misses, the
|
||
target page isn't dirty, so `psx_unknown_dispatch` fires.
|
||
|
||
### Affected emit sites in `recompiler/src/code_generator.cpp`
|
||
|
||
| Line | Pattern |
|
||
|------|--------------------------------------------------------|
|
||
| 970 | conditional-branch taken-arm to mid-func target |
|
||
| 981 | conditional-branch fall-through to mid-func target |
|
||
| 998 | unconditional jump to mid-func target |
|
||
|
||
All three should call `register_cross_function_target(branch_target)`
|
||
the same way the jump-table emitter does (line 777 of
|
||
`full_function_emitter.cpp`).
|
||
|
||
### Evidence — Tomba audit, 2 manifestations
|
||
|
||
From `codegen_audit_game.py --config game.toml`:
|
||
|
||
```
|
||
[2] literal call_by_address targets: 333 unique, 459 sites
|
||
dispatch table size: 1921 entries
|
||
targets in declared code regions: 2
|
||
targets in RAM (dirty-RAM interpreter domain): 331
|
||
in-code targets MISSING from dispatch: 2
|
||
0x800905E4 (1 site)
|
||
0x80090600 (1 site)
|
||
```
|
||
|
||
Both addresses appear in `generated/SCUS_942.36_full.c` with comments
|
||
`/* taken: split (mid-func) */` and `/* not taken: split (mid-func) */`
|
||
— the recompiler EXPLICITLY knows these are mid-function splits but
|
||
fails to register them.
|
||
|
||
The 331 RAM-domain targets are not bugs — they're runtime-loaded
|
||
overlays correctly handled by `dirty_ram_dispatch`.
|
||
|
||
### Concrete next step
|
||
|
||
1. Add `register_cross_function_target(branch_target)` at the three
|
||
call sites in `code_generator.cpp` (lines 970, 981, 998).
|
||
2. Regen Tomba.
|
||
3. Re-run `codegen_audit_game.py` — expect "in-code call_by_address
|
||
misses: 0".
|
||
4. Re-run on BIOS to confirm no regression.
|
||
|
||
---
|
||
|
||
## Issue #4 — 7 unemitted GTE / COP2 instructions in BIOS Shell code
|
||
|
||
**Status:** open, audit-surfaced
|
||
**Date opened:** 2026-05-12
|
||
**Phase:** 4 (BIOS shell render)
|
||
**Likely related to:** Issue #3 (missing PS logo on disc-detected screen)
|
||
|
||
### Symptom
|
||
|
||
The Phase B1 `gte_audit` (now generic, code-region-filtered) reports
|
||
4 missing `gte_execute` emits and 3 missing LWC2/SWC2 emits in the
|
||
BIOS Shell code region. None of the affected PCs appear anywhere in
|
||
`generated/SCPH1001_full.c`.
|
||
|
||
| PC | Word | Class |
|
||
|-------------|--------------|----------------|
|
||
| 0xBFC34FF8 | 0x4A480012 | GTE MVMVA |
|
||
| 0xBFC3502C | 0x4A480012 | GTE MVMVA |
|
||
| 0xBFC35064 | 0x4A480012 | GTE MVMVA |
|
||
| 0xBFC350C4 | 0x4A480012 | GTE MVMVA |
|
||
| (3 sites) | LWC2 / SWC2 | GTE load/store |
|
||
|
||
These were previously masked by 73 data-region false positives in the
|
||
old unfiltered tool. The B1 code-region filter surfaced them.
|
||
|
||
### Likely cause
|
||
|
||
MVMVA is matrix-vector-multiply-and-add — the BIOS only uses GTE/3D
|
||
math in two narrow places: the boot logo intro and the
|
||
disc-detected/PlayStation-logo screen. We boot past both, but
|
||
**Issue #3's missing PS-logo glyph is consistent with these
|
||
unemitted MVMVA + LWC2/SWC2 sites**: if the recompiler skipped the
|
||
3D math that draws the logo, the logo would render as nothing or
|
||
garbage but the surrounding text would be fine — exactly Issue #3's
|
||
symptom.
|
||
|
||
Two possible root causes (not yet distinguished):
|
||
- **Discovery gap:** the function containing these PCs was never
|
||
identified, so nothing got emitted for it.
|
||
- **Emit gap:** the function was discovered but the recompiler
|
||
skipped these specific instructions when translating.
|
||
|
||
### Concrete next step
|
||
|
||
Check whether ANY PC near `0xBFC34FF8` appears in
|
||
`generated/SCPH1001_full.c`. If neighbors are emitted but the GTE
|
||
sites aren't, it's an emit gap (fix in code_generator.cpp). If
|
||
neighbors are absent too, it's a discovery gap (fix in function
|
||
discovery seeds). Either way, then close Issue #3 alongside.
|
||
|
||
## Issue #7 — sljit live execution is unvalidated (pure-live save-load wedge)
|
||
|
||
**Status:** CLOSED — superseded, not fixed. The sljit Tier-2 backend this issue
|
||
describes was removed wholesale in `5b7e69b4` (2026-07-15), which deleted
|
||
`lib/sljit/`, `overlay_sljit.{c,h}` and the sljit paths in `code_provider.c` /
|
||
`overlay_loader.c`. The toolchain-free role it filled is now served by the
|
||
bundled TinyCC tier (introduced `c2a3f6ad`, 2026-06-25), which compiles to a DLL
|
||
through a subprocess rather than JITting in-process, so it does not share this
|
||
defect's live-execution/save-load shape. Nothing here is actionable; retained as
|
||
history.
|
||
**Date opened:** 2026-06-15
|
||
**Date closed:** 2026-07-15 (by removal of the subsystem)
|
||
**Area:** overlay Tier-2 sljit backend (`runtime/src/overlay_loader.c`,
|
||
`overlay_sljit.c`, `code_provider.c`, `overlay_sljit.c` resolution)
|
||
|
||
### Symptom
|
||
|
||
With `PSX_OVERLAY_SLJIT_LIVE=1` (the prototype toolchain-less production path),
|
||
save/load **wedges the guest**: `reason=atexit, pc=0`, dispatch looping a
|
||
BIOS/kernel address (`0xB0`/`0x650`). Reproduces only in live mode — the
|
||
dev differential path (`overlay_diff_on`) is clean (0 divergences / 395 shadow
|
||
calls). Flagged in commit `81cf21b` as Known Bug #1.
|
||
|
||
### Root cause (NOT the emitter)
|
||
|
||
The MIPS→sljit emitter + block-local register allocator are **proven correct**
|
||
this session: 66/66 emitter unit, 22/22 `$sp`-balance probe, and byte-identical to
|
||
the pre-regalloc emitter across 313 overlay functions
|
||
(`runtime/tests/sljit_{emit_test,sp_probe,offline_diff}.c`). The crash is in the
|
||
**live-execution wiring**, which bypasses the validation harness:
|
||
|
||
- `run_shadow_diff` (the device-touch detector + diff gate) is gated on
|
||
`s_diff_mode`. Live mode sets `s_sljit_live`, not `s_diff_mode`, so in live mode
|
||
**the diff never runs.** Therefore:
|
||
- `device_touch` is **never computed** for a live shard → a device-touching
|
||
function runs its shard and **double-executes** its SIO/memory-card/DMA I/O
|
||
against real hardware state, corrupting the kernel byte-handler path → the
|
||
`atexit, pc=0`, kernel-`0xB0/0x650` livelock. *(Known Bug #3 — leading cause.)*
|
||
- Shards run with `diff_passes == 0`, i.e. **completely unvalidated**.
|
||
*(Known Bug #2.)*
|
||
- Separately, the verify budget counts **cumulative** (not consecutive) clean
|
||
passes — `diff_passes` never resets on a divergence — so even in dev mode an
|
||
intermittently-wrong shard could reach budget on lucky passes.
|
||
|
||
The historical "`$sp` off by 0x18 at 0x12E478" crash was a **different, already-fixed
|
||
bug** (diff over-scoping the call tree, fixed by per-function isolation in
|
||
`81cf21b`) — not this one, and not a codegen bug.
|
||
|
||
### Fix (SLJIT.md §11 Phase A) — runtime-only, no regen
|
||
|
||
Collapse blind-live into validated-live: route live-mode shards through the diff
|
||
gate (`(s_diff_mode || s_sljit_live)` at the dispatch gate), so `device_touch` is
|
||
computed and a shard runs native live ONLY after a clean verify budget; and reset
|
||
`diff_passes` to 0 on any divergence (consecutive-clean = "0 divergences"). After
|
||
this there is no blind path: device + diverging shards stay on the interpreter.
|
||
|
||
### Related gaps (same area, tracked in SLJIT.md §11 Phases B–C)
|
||
|
||
- **gcc>sljit precedence not enforced** (LIFO chain order can put an sljit shard
|
||
ahead of a gcc shard for the same region). → Phase B.
|
||
- **Non-dev machine never triggers sljit generation in normal play** (on-miss hook
|
||
gated behind a dead env-only flag; `auto` resolves to gcc on every machine because
|
||
`autocompile_configured()` tests for a config *string*, not a real toolchain). →
|
||
Phase C.
|
||
|
||
### End-to-end success criteria (per `docs/internal/DEBUG.md` end-to-end rule)
|
||
|
||
`PSX_OVERLAY_SLJIT_LIVE=1` (or `auto`→sljit on a toolchain-less box): play through
|
||
save-load, FMV, menus, and combat with **0 divergences, 0 wedges**, shards
|
||
validated-then-promoted, device functions correctly pinned to the interpreter.
|
||
"Diff is clean" or "shard validated" is NOT success until the full save-load loop
|
||
closes live.
|
||
|
||
---
|
||
|
||
## Issue #8 — Tomba2 P0: wild-jump FAIL-FAST fatal after coverage-wave native promotion
|
||
|
||
**Status:** OPEN — root-cause hunt in flight, currently blocked on Issue #9
|
||
**Date opened:** 2026-07-06 (carried from session handoff)
|
||
**Area:** overlay native execution (codegen OR dispatch/call-contract layer)
|
||
**Rule:** root-cause via first-divergence; do NOT ship a block/disable.
|
||
`overlay_native_block` on 0x80073328 acceptable for ATTRIBUTION only.
|
||
|
||
### Symptom
|
||
|
||
Two FAIL-FAST unknown-dispatch fatals in attract soaks (~15–20 min apart per
|
||
run) after the 2026-07-06 entry-based-coverage wave promoted new PCs to native
|
||
(the convergence fixes work; they exposed a latent defect):
|
||
|
||
- **Crash 1:** addr=ra=0x0000F514 (zeroed kernel heap, page not dirty),
|
||
4 seconds after 0x80073328 went native for the first time (its only-new-entry
|
||
DLL `00038000_0A16928B` built 09:04:14). 0x80073328 fired ra=0x80139400 every
|
||
2 frames all scene.
|
||
- **Crash 2:** addr=0x009D6830, ra=0x00000001 (beyond 4× RAM mirror), pre-fatal
|
||
register soup: target==ra; interp dispatches into 0x13xxxx drivers with
|
||
garbage args (a0=0xDF73E104, a1=0xE2000318 = raw GP0 word as pointer).
|
||
- Both: same kernel event-delivery dispatch chain (…0x20D4, 0x650, 0x2104)
|
||
immediately before; a0=0x800F42E0 (persistent struct) at both fatals.
|
||
Corruption PRECEDES the fatal; fail-fast catches the landing.
|
||
|
||
Evidence: memory `tomba2_f514_wildjump_first_native.md` + prior session's
|
||
scratchpad `crash_f514/` (psx_crash.txt, psx_last_run_report.json,
|
||
psx_freeze_heartbeat.json).
|
||
|
||
### Suspect set
|
||
|
||
The post-08:44 coverage wave DLLs in
|
||
`build-t2/cache/SCUS-94454/gcc/win-x64/cg4_d44a063d/`: new region DLLs +
|
||
island fragments (incl. F 80073328 in `00038000_0A16928B`; F 80106D7C /
|
||
F 80106E58 / F 80024548; jump-table-ladder aliases F 8011040C/43C/45C/478/4CC
|
||
in `00106000_1675417D`).
|
||
|
||
### Plan (two independent paths)
|
||
|
||
1. **Instrumented path (preferred):** finish the shadow-diff segment redesign
|
||
(Issue #9), soak with `PSX_OVERLAY_DIFF=1` from boot. Expect either a named
|
||
divergence record (→ shard codegen defect, fix recompiler) or clean segments
|
||
everywhere + crash persists (→ exit/call-contract class, bugA/bugD family —
|
||
fix the dispatch contract layer).
|
||
2. **Bisection path (no instrument needed):** quarantine halves of the
|
||
post-08:44 DLL wave out of the cache dir, ~20-min attract soak per round
|
||
(crash repro interval). First single-candidate probe: `overlay_native_block`
|
||
on 0x80073328 (attribution only, never a fix).
|
||
|
||
---
|
||
|
||
## Issue #9 — Overlay shadow-diff instrument is structurally unsound (never engages)
|
||
|
||
**Status:** ROOT-CAUSED; partial fixes in tree (uncommitted, `_wt-tomba2-ipr`);
|
||
segment-granular redesign DESIGNED but NOT implemented
|
||
**Date opened:** 2026-07-06
|
||
**Area:** `runtime/src/overlay_loader.c` (run_shadow_diff + dispatch gates),
|
||
`runtime/src/dirty_ram_interp.c`, `runtime/src/interrupts.c`, `runtime/src/traps.c`
|
||
**Blocks:** Issue #8 path 1. Full detail + design in memory
|
||
`tomba2_shadow_diff_unsound_segment_redesign.md`.
|
||
|
||
### Symptom
|
||
|
||
With `overlay_diff_on` / `PSX_OVERLAY_DIFF=1`, shadow_calls=1 over 51K+ frames
|
||
(two independent runs) — the diff instrument validates essentially nothing while
|
||
dispatch_native runs millions of calls. The game LOOKS fine, so the failure is
|
||
silent.
|
||
|
||
### Root cause chain (three stacked defects, found in order)
|
||
|
||
1. **CPS interior re-entry path had no diff gate** — continuation re-entries
|
||
(the dominant dispatch class under CPS) ran native blind in diff mode.
|
||
FIXED in tree: gate mirrors the entry chain (`interior_gated` counter in
|
||
`overlay_shadow_dump`).
|
||
2. **Longjmp-escape hazard** — a shadow started inside an exception dispatch is
|
||
unwound by the guest RFE's longjmp (setjmp frame below run_shadow_diff);
|
||
epilogue skipped; s_in_shadow/s_native_exec/s_suppress_irq stuck forever.
|
||
HARDENED in tree: `g_exc_setjmp_epoch` (interrupts.c) +
|
||
`overlay_loader_shadow_escape_fixup()` at all 3 longjmp sites; dump fields
|
||
`escapes`/`escapes_native` must stay 0.
|
||
3. **THE WEDGE (design flaw, predates this session): run_shadow_diff never
|
||
terminates for non-returning functions.** Interp pass = one
|
||
dirty_ram_dispatch call (returns at first exit-to-elsewhere, NOT at stop_ra);
|
||
native pass chases via psx_dispatch_call until pc==stop_ra — NEVER for a
|
||
main-loop-shaped function. The live game then runs FOREVER inside the
|
||
abandoned shadow native pass: **speculative native state becomes the live
|
||
timeline**, IRQ-suppress stuck on, all downstream native disabled
|
||
(native_exec=0). Verified live both runs: in_shadow=1, native_exec=0,
|
||
escapes=0, wedge at frame ~1000. ⇒ Any pace/behavior data from a diff-mode
|
||
run is garbage; the instrument was never capable of validating this game.
|
||
|
||
### The fix (designed, ready to implement): CPS segment-granular diff
|
||
|
||
Diff exactly ONE `c->fn` invocation extent (entry/interior PC → first
|
||
jal/jalr/jr/region-exit). Verified against generated DLL C: CPS codegen
|
||
surfaces at EVERY call/return/indirect jump (no local jump-table switches), so
|
||
a lean interp segment runner (in dirty_ram_interp.c; must intercept
|
||
jal/jalr/jr/j BEFORE exec_one — exec_one nests calls inline) can run an
|
||
identical extent. Both passes bounded by construction; no chasing, no fiber/RFE
|
||
exposure; validates interior re-entries directly; compares pc (boundary target)
|
||
as a first-class divergence signal. Abandon (no diff, interp state stands) on:
|
||
syscall/break/rfe, unsupported insn, insn cap, MMIO (device_touch as today).
|
||
Asymmetry guards: psx_slice_block no-fire in shadow (currently moot — slicing
|
||
default OFF); cycle-counter snapshot/restore around the native pass;
|
||
post-dispatch IRQ pump shadow guard (already in tree).
|
||
|
||
### Uncommitted worktree state (this session, runtime-only, cg tag intact)
|
||
|
||
- `overlay_loader.c`: CPS interior diff gate + `interior_gated`; in-exception
|
||
shadow-start detour (superseded by segment design — remove when implementing);
|
||
`s_shadow_cand` own-interior native route for shadow native pass; escape-fixup
|
||
mirrors + `overlay_loader_shadow_escape_fixup()`; shadow dump fields
|
||
`in_shadow`/`native_exec`/`escapes`/`escapes_native`; IRQ pump shadow guard.
|
||
- `interrupts.c`: `g_exc_setjmp_epoch` + getter; fixup calls in
|
||
deferred_exception_longjmp + psx_rfe_escape_check.
|
||
- `traps.c`: fixup call at the deferred-honor longjmp site.
|
||
- All inert when diff mode is off; normal play unaffected. Builds clean.
|