Key merges: event-horizon acceleration + device-batch advancement as
the new Lever-A axis (E2); data-shards temporal-write-visibility flaw
+ verify-only shadow mode gate (E4); never-early+catch-up reclassified
as drive-model correctness, not acceleration (E5); no universally safe
guest-visible CD speedup (risk ladder, per-game traits); state-
transition cache as the only true near-zero, Phase-2 (E10); 12-session
burn-down order with gates and kill criteria.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Frame: wall = guest_time x host-cost/guest-sec; turbo makes the window
emulation-bound, so Lever A (host cost: data shards, coverage) is safe
and multiplies with turbo; Lever B (guest CD latency) is the wedge zone;
Lever C (skip guest work) is the proven hardlock class. Burn-down E0-E7
with gates and kill criteria. ChatGPT-consult merge section pending.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User-facing guide for turning overlay_captures.json into native code:
developer-first framing (ship a full gcc cache), the manual gcc cache
build, the --static bake-into-binary path, and the staleness guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the repo approachable for outside contributors and an IDE integrator:
- New contributor docs:
- docs/EXECUTION_MODEL.md — semi-technical tour of the static / native-overlay
/ interpreter execution model and how coverage gaps get plugged.
- docs/ARCHITECTURE.md — component-level view (recompiler, runtime, LLE+HLE
BIOS, overlay backend static->gcc->tcc, renderers, oracle model).
- docs/BUILDING.md — accurate dependency table, per-platform prereqs, build
steps, and the submodule + local-junction framework-linking setup.
- CONTRIBUTING.md — distilled core rules, verification expectations, PR flow.
- Declutter root: move ~14 internal dev-logs into docs/internal/ (history
preserved via rename) with an index README; repoint CLAUDE.md references.
- README: correct build recipe, LLE+HLE + overlay-backend summary, links to the
new docs.
- code_provider.c: update the stale backend comment to the real static->gcc->tcc
resolution (dead SLJIT.md reference removed).
- config_schema.md: drop the obsolete "once Phase A2/A3 ships --config" note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Games live in their own repositories in this project (TombaRecomp,
MegaManX6Recomp, ...); the framework holds only the shared recompiler +
runtime, never a game's config/assets. A contributor PR landed Kula World
(SCES-01000) game files directly in the framework, which is inconsistent
with that layout — that content belongs in the contributor's own game
repo. Remove it:
- games/kula/{game.toml,seeds.txt}
- docs/kula-world-findings.md, docs/{MACOS,WINDOWS}_BUILD_KULA.md
- docs/assets/kula-world-*.png
- README "This fork — Kula World" + macOS branding sections
- runtime/CMakeLists.txt kula-runtime target
The general framework improvements those commits also carried are KEPT
(they benefit every game): KUSEG-addressed PS-X EXE header acceptance,
dirty-RAM interpreter interrupt-poll-at-entry, main-RAM 4x mirror /
GetID-from-serial / CD-status / CD-version fixes, and the Linux/macOS
POSIX ports. Source comments citing Kula as the motivating case are left
in place as provenance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace absolute F:/C: paths with repo-relative (../TombaRecomp, bios/, beetle-psx/)
or placeholders (<desktop>/<psxref>/<nesrecomp>/<psxrecomp-v4>) across accuracy docs,
WIDESCREEN/SHADOW/RECURSION_BUG/config_schema/ISSUES/README, and tools/*.py|.ps1.
Removed a personal C:\Users\Matthew\Desktop path (ISSUES.md). Left CLAUDE.md/PLAN.md
sibling-project + version-history paths intact (intentional layout documentation).
Two interlocking pieces from the Tsumu HUD/menu-label investigation:
- GP0 ring builder attribution (gpu.h/gpu.c/debug_server.c): op-0x80
entries now carry a bounded guest-stack unwind (bld[6]) + copy-time
$sp (csp); gpu_frame_dump emits func/ra per entry and csp+bld[] for
copies. This cracked the HUD compose: whole label strips are
MoveImaged from the glyph sheet (src 834/852,352 -> HUD cache y=32).
- [[vram_patch]] (text_xlate.cpp/.h + gpu.c hook): third translation
layer for text that exists only as pre-rendered pixels in an
uploaded texture asset (Tsumu HUD labels, menus, prompts live in
the glyph-sheet image; the 144-glyph font has no katakana at all).
On every GP0 0xA0 upload completion the configured rects are
verified halfword-for-halfword against expected JP pixels and only
then rewritten with the target-language pixels via the renderer
facade (software/GL/supersample coherent). Re-applies on every
re-upload; verify-fail leaves foreign assets untouched. Debug:
xlate stats gains vram_patches/vram_applied; new "vpatch"
force-apply subcommand for config iteration without rebuild.
Verified on-screen: HUD "STAGE: 1-1 / BATTERY: 100", pause menu
(CONTINUE/RESTART/GIVE UP/CHANGE CONTROLS, TARGET BATT), title-menu
strips. docs/STRING_TRANSLATION.md Appendix Z documents the model,
mechanism, authoring recipe, and remaining strip backlog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the LOAD-GAME attempt outcome and why the HUD can't be finished
headless: both memcards are empty (0 used blocks) so LOAD GAME reaches no
stage; NEW GAME still hits the intractable name grid; and the attract
demo's HUD is composed once at stage-load and only displayed (never
per-frame-drawn as glyph quads in the full 554-3754 GP0 capture; its
values are static so there is no dynamic draw to intercept). The drawer
(0x8005443C) + font-tile scheme are cracked; capturing the HUD label's
own glyph quads needs real gameplay (dynamic HUD) reachable only via a
real save or driving past the name grid on an interactive build. No hook
shipped (no discriminator captured -> would risk non-HUD quads; Rule 5).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live GP0-ring capture (on the name-entry screen, same per-glyph drawer
class as the HUD) cracked the mechanism for the eventual HUD index-remap:
- Drawer PC 0x8005443C draws one textured 0x2C quad per glyph via direct
GP0, building prims in game RAM ~0x80092xxx; U,V into texpage 0x1E
selects the font tile (CLUT 0x7C10).
- Texpage 0x1E holds hiragana AND Latin (the name-entry grid draws A-Z
through this same drawer), so the Latin tile U,Vs the remap needs are
readable straight off the grid's letter quads.
- Remaining blocker: catch the HUD's own stage-load compose to get its
katakana source prims, then rewrite their U,V to Latin tiles (or hook
0x8005443C to remap HUD-position glyph quads). Gated only on reaching a
fresh stage-load (blind grid-confirm navigation didn't land headless; a
reachable build clears it). No fragile partial shipped (Rule 5).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Corrects and refines the Bucket-1/3 findings with live GPU evidence:
- CLEAR!! banner: ALREADY renders in stylized Latin ("CLEAR!!") — verified
on-screen in a gameplay stage. Not Japanese; no translation needed. The
earlier "resister/baked-sprite" assumption was wrong.
- In-game HUD (ステージ/バッテリー): pinned the render model via the always-on
GP0 ring (gpu_frame_dump). The game composites — ~6 draw prims/frame,
rebuilding only the animated foreground (character/blocks, ~8 textured 0x2C
quads at screen y>=31, via BIOS DMA from an OT at ~0x138xxx). The HUD (screen
y~8) is composed ONCE at stage-load, not per-frame, and has no static SJIS
source (RAM-scan proof). Remapping it needs catching that stage-load compose
(a fresh stage load while observing) and a scoped blit/tile intercept — not a
source-patch. NOT shipped: no clean pixel-verified remap this pass, and a
blanket VRAM font swap would corrupt katakana elsewhere (Rule 5, no hacks).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records the outcome of the per-glyph translation pass (Buckets 1-3):
- SHIPPED: stage/level/tutorial names (source-patch, f00631e/31ab3c2);
name-entry prompt -> "YOUR NAME?" (source-patch, 31ba92d); name-entry
Latin input confirmed NATIVE (grid already draws the font's A-Z/0-9).
- RESISTERS (not shipped, no fake): the in-game HUD (ステージ/バッテリー)
and the CLEAR!! banner are composed per-glyph at runtime with NO static
source address. Proven: a live 2 MB RAM scan finds ステージ/バッテリー
only inside static message sentences (never a standalone HUD label,
no ステージ: template); クリア/CLEAR/CLEAR exist nowhere as a
standalone banner; and every disc graphic TIM was extracted+rendered —
the only baked text is the already-English TSUMU light logo. So the
source-patch (which needs a static address) cannot reach them; they
require the scoped per-glyph sprite-draw index-remap hook (Appendix Y),
a separate larger effort. Includes the full disc asset map.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records the concrete findings for the deferred per-glyph HUD/menu/title
localization: the font glyph-index map (Latin A-Z=73-98, 0-9=105-114;
fullwidth Latin glyphs present, no injection needed), the static LE-SJIS
label storage in fixed-width space-padded slots (level names 0x80070755+,
tutorial/mode labels 0x80070300+), and the cleanest hook design (load-time
source patch: rewrite a label's katakana/hiragana SJIS to fullwidth-Latin
so the existing per-glyph routine draws English).
Not shipped: the verification bar is a rendered-English screenshot, and the
target screens (mode/level-select, in-game HUD) are unreachable in the
headless build — the attract never auto-plays the demo, and reaching them
needs completing the name-entry glyph grid (blind grid nav to the "End"
cell did not land). Committing a RAM-patch without that screenshot would be
unverified coverage. Remaining resisters (HUD runtime counter; title/CLEAR
sprites) documented.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fold the hard-won runtime findings into STRING_TRANSLATION.md: the
message-table text is little-endian 16-bit Shift-JIS (EXE menu labels are
big-endian); FE-FF/FF-FF/30-FC control framing; message text is
string-pointer-drawn (interceptable) but HUD/title/menu are per-glyph
sprites (need a separate glyph-index hook); struct-embedded NUL records
crash if replaced (apply gated to 0xFFFF-framed messages by default);
capture quality gate; verification status; open items for full coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implementation-ready design for capturing PSX source strings from memory
and substituting translated bytes on the fly, so JP-only titles (Tsumu
Light SLPS-02253 first) can be played in English or any language.
Adapts the author's proven n64recomp prior art (Pocket Monsters Stadium
JP): always-on capture ring keyed by FNV-1a64 content hash, a per-title
multilingual table, and a draw-time apply hook that repoints the string
arg at guest scratch so the original renderer draws the replacement (no
regen, unbounded length, dynamic args preserved).
Grounds the PSX design in real findings: Tsumu text is Shift-JIS (2-byte
fullwidth + 1-byte halfwidth kana) with 0xFFFF-framed message records;
in-EXE text block ~0x80070900-0x80072000 via pointer table at 0x80071474;
menu labels at 0x80010C5C/0x80010C94; font already has fullwidth Latin
(English renders with zero glyph injection). Hooks onto the psx_dispatch
chokepoint (fntrace_record census + g_psx_bios_hle_hook precedent), so no
per-function codegen and works in Release.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Windows->Linux POSIX port left the recompiler and runtime almost
portable; two small platform gaps remained on macOS (AppleClang, arm64),
both guarded so Windows/Linux are unaffected:
- runtime/src/autocompile.c: add #include <stdlib.h> for getenv. glibc and
windows.h leak that declaration transitively; the macOS SDK does not, so
AppleClang errored on the implicit declaration.
- recompiler/src/main_bios.cpp: the C-compiler probe used ">NUL" (Windows
null device). On POSIX that misdirects the redirect and drops a stray
"NUL" file in the cwd; now #ifdef'd to /dev/null.
The ucontext fiber backend already carried the _XOPEN_SOURCE/_DARWIN_C_SOURCE
guards it needs; SDL2 (Homebrew) selects the Metal backend automatically.
Verified end to end on macOS 15.5 / Apple Silicon: BIOS -> KULA WORLD title
-> menu -> 1 Player -> a 3D level that rolls under control.
Adds docs/MACOS_BUILD_KULA.md and a macOS gameplay screenshot; README notes
the port. Ignores the local dist/ .app bundle (recompiled-BIOS derivative).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Step-by-step instructions for an AI agent to reproduce the working Kula
World recomp on Windows: prerequisites (MSYS2 mingw64 + SDL2), clone the
branch, user-supplied BIOS/disc, extract the boot EXE, build the
recompiler, generate BIOS + game C, build the kula-runtime target, run,
controls, and an honest verified/unverified status list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
Adds this fork's Kula World (SCES-01000) end-to-end target to the README with
a title-screen screenshot and a summary of the five runtime bugs fixed during
bring-up (four of them game-agnostic framework improvements): the dirty-RAM
interpreter interrupt-latency fix, the main-RAM mirror, GetID region
derivation, CD status bits, and the SCPH-1001 CD controller version.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
ROOT CAUSE of the Kula World black-screen wedge: a guest wait loop (libcd's
CdSync spin) that lives entirely in dirty RAM re-enters the dirty-RAM
interpreter once per short (~7-insn) block. The interpreter's only interrupt
poll was gated by the PER-INVOCATION (insns_executed & 0xFFF) counter, which
short blocks never reach — and when the loop exits to dispatch each iteration
that path isn't reached at all. So the interpreter ran millions of
instructions with psx_check_interrupts never called, while SR (IEc+IM2) and
Cause (IP2) fully satisfied the MIPS interrupt-take condition and i_stat had
CDROM+VBLANK pending. The CD IRQ that sets libcd's wait-flag was never
serviced; the loop spun ~14s per cycle until a rare timeout re-dispatched.
Fix: poll interrupts at the interpreter's per-invocation entry, gated by a
global invocation counter (every 64 entries), cpu->pc set for a coherent
resume — the interpreter analogue of a static block-leader poll.
RESULT: Kula World now boots to its title screen and menu. Diagnosed with the
self-stop trap + gdb sampling of SR/Cause/i_stat + the interrupt gate globals
(in_exception, total_checks) + g_dirty_ram_insns_run, which isolated 'CPU
spinning in the interpreter without polling interrupts'. General fix: any
dirty-RAM spin-loop now yields to interrupts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
With the boot fixed, live state during the game's retry loop shows the CDROM
interrupt is raised (i_stat bit2) AND enabled (i_mask bit2) and the CPU takes
exceptions (VBLANK cleared each frame), yet the CDROM bit is never cleared and
the response FIFO never drained (irq_flag=0x03, response_read=0). The game's
libcd CD interrupt handler in the BIOS IntRP chain is not servicing the CD
IRQ, so CdSync never completes. Distinct from the CD-response-value bugs fixed
today; a BIOS-IRQ-chain / libcd-handler matter. Documents the next step and
the full session tally in docs/kula-world-findings.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
The shell CD-init (ROM 0x1DF50) sets kernel flag [0xA000DFFC]=1 when the CD
controller version high byte (Test sub-cmd 0x20) is >= 0x95; the boot CD-open
(0xBFC0D570) then issues a ReadTOC gated on that flag. We returned the
PSone-era 0x97 0x01 0x10 0xC2 (copied from beetle, which hardcodes it) — wrong
for the SCPH-1001 BIOS, whose 1994 sub-CPU reports 0x94 0x09 0x19 0xC0. With
the correct value the flag stays clear, the spurious frame-890 ReadTOC is
gone, and the boot command stream matches the oracle exactly. Found via
self-stop trap + fntrace-all + gdb watchpoint on [0xDFFC] + capstone tracing
the flag setter and its version check.
Also adds a symmetric self-stop trap to psx-beetle's CD-command hook so the
oracle can be frozen at the same boot point for state diffs.
Honest status: this closes the boot-flow divergence but the game still wedges
downstream in its OWN CD-init retry (GetStat/Init loop from game pc 0x63A14);
that is a separate issue, documented in docs/kula-world-findings.md round 6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
Adds PSX_FNTRACE_ALL=1 (records every dispatch from power-on) and documents
the full mechanistic chain of the wedge (docs/kula-world-findings.md round 5):
The frame-890 ReadTOC that breaks the game's CD reads is gated by kernel flag
[0xA000DFFC]: the CD-open function 0xBFC0D570 does 'beqz [0xDFFC], skip;
jal ReadTOC'. On ours [0xDFFC]=1 -> ReadTOC -> wedge; on the oracle it stays
0 -> reads continue. The flag is set to 1 by func_1FC1DF50 (a CD control
routine) called from shell code at ra=0x80036024. The shells run the same
verbatim bytes but diverge early (ours 0x35000-0x39000 issuing Test/ReadTOC,
oracle 0x30D00-0x33200 issuing Init/GetTN), so a branch input differs. Traced
via self-stop trap + fntrace-all + gdb watchpoint on [0xDFFC] + capstone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
Extends psxrecomp_cdcmd_cb_t (and the beetle_cdcmd_trace ring + cdrom_cmd_dump
JSON) with the guest PC at dispatch, read via PS_CPU::GetRegister(GSREG_PC).
Lets the boot-command-stream oracle-diff localize the first divergent
command's call site (kernel vs shell vs game) instead of only the command
byte — the next step in isolating the early-boot branch divergence behind
Kula World bug 3. Builds clean; behavior-inert when no listener is attached.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
Bit 2 is ADPBUSY (XA-ADPCM playback in progress) and must idle at 0; we
raised it unconditionally under a mistaken 'ADPCM empty' reading. Bit 7
(BUSYSTS: command written, first response not yet ready) was never raised;
now mapped to the queued-command window, mirroring beetle's
PendingCommandCounter/phase busy window. Verified against beetle cdc.cpp
Read(A==0), which sets bit 2 never and bit 7 during command latency.
Boot-diff verification shows this does NOT change our boot command stream
(the early-boot branch divergence vs the oracle persists — documented in
docs/kula-world-findings.md round 4, with the next sharp step: add guest
PC to the oracle's cdcmd hook and diff the first divergent call site).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
New debug tool: PSX_CD_TRAP_CMD=<byte> [PSX_CD_TRAP_NTH=<n>] SIGSTOPs the
process the instant the Nth matching CD command dispatches, so a debugger
attaches to the exact moment with zero run-speed cost (a gdb conditional
breakpoint on exec_command slows the emu ~100x and never reaches the wedge).
Used it to pin Kula bug 3 one level deeper: the pre-wedge ReadTOC at frame
~890 is issued by the BIOS BOOT FLOW itself (guest stack shows only
Main/StartKernel-chain ROM addresses, zero game frames) — the boot's
post-EXE-load disc re-check. And the full command-history dump shows our
boot diverges from the oracle's from the VERY FIRST command: the oracle's
kernel opens with Init/Init/GetTN/GetID and never issues Test or ReadTOC;
ours opens with the shell's Test[20]/GetID/ReadTOC check and re-checks
after the EXE load. Same SCPH1001 bytes, different path — an early-boot
branch divergence (recompiled-BIOS or hardware-sim input), documented in
docs/kula-world-findings.md for the first-divergence session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
Full oracle command trace confirms it NEVER issues ReadTOC; ours does, from
the BIOS (Pause->ReadTOC->GetID) at frame 890, with clean pre-read stat
(0x22, no error/shell bits). Same BIOS bytes on both, so our runtime reaching
the disc-identify path is a state-driven BRANCH divergence, not a CD-sim
error: likely a kernel 'disc already identified' flag left unset, or a
recompiler-level game-logic divergence. Next: instruction-level
first-divergence hunt around frame 890.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
Adds a CD-command trace to psx-beetle (beetle_cdcmd_trace_hook.patch:
callback in cdc.cpp's command dispatch, wired through beetle_libretro.cpp
into cdrom_cmd_dump / cdrom_cmd_reset on the oracle debug server). This is
the two-process oracle-diff the setup is built for, now working on Linux.
Live-diff result (docs/kula-world-findings.md, bug 3): the oracle's game
reads level data with a clean Setloc->SeekL->Setmode->ReadN->Pause per
sector (SeekL x18, ReadN x22, GetlocL x89). Our runtime's game, in the
wedge, issues NEITHER SeekL NOR ReadN — only GetStat/Init/Setloc/Pause,
an error/retry branch the oracle never enters.
Crucial: our runtime DID deliver 307 sectors during early boot (ReadN +
data delivery work). Reads only stop at frame ~890 — the BIOS mid-game
region revalidation (ReadTOC+GetID). The region fix made that PASS, but
the post-ReadTOC CD state still stops the game resuming reads. Prime
suspect: our ReadTOC (case 0x1E) is a bare stat+COMPLETE stub that does
not model drive repositioning / the track table the game reads to
recompute its seek. Documented for the next session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
The live trace shows the game's CD-init state machine (GetStat/Init then
Setloc/Pause, ~2.5s cadence) receives CORRECT responses at every step —
stat=0x02 (healthy idle drive), INT3 ACK + INT2 COMPLETE both fire and the
game's ISR processes them — yet it retries forever. So bug 3 is NOT a
wrong-response-value bug like bug 2; the game is waiting on a timing or
event-completion condition its init check never sees satisfied. Documents
two next-session paths: (1) rebuild psx-beetle with a CD-command hook and
live-diff the boot->demo sequence, (2) find the game's init-complete branch
condition and check our INT2-COMPLETE delivery timing against it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
Records the two fixed bugs (RAM mirror, GetID region) and characterizes
the open third wedge: the game's own CD driver loops Setloc(PVD)/Pause
without ever reading the directory, so the demo level never loads and
the screen stays black. The oracle loads it, so the divergence is
concrete and localized for the next oracle-diff session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
beetle_libretro.cpp targets the C++ beetle-psx tree; upstream master has
been converted to plain C and no longer compiles against it. Documented
the working recipe: pin beetle-psx at 5759277b (the wtrace patch's base,
located via the patch's own blob hash), apply beetle_wtrace_hook.patch,
and apply the new beetle_sio_trace_hook.patch — FrontIO::SetSIOTraceCallback
(fires once per completed SIO byte exchange), a hook beetle_libretro.cpp
required that was missing from the committed patch set.
Verified on Linux/Xvfb: psx-beetle boots Kula World to demo gameplay and
serves ping/screenshot over the shared TCP debug protocol, alongside
kula-runtime — the two-process oracle setup from CLAUDE.md §16 now works
in this environment. Runtime-created per-game .mcr files are gitignored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
Completeness layer for the Tomba 2 FMV-freeze fix (the discovery/ownership side
of "containment is not ownership, ownership is not entry legality"). In overlay
discovery (main_psx.cpp --overlay), after walking the seed roots, every DIRECT
CALL (jal) target that lands strictly INSIDE an already-discovered function is
registered as an ALIAS ENTRY of that host via the existing alias machinery
(materialize_alias_groups). So when a host's discovered range absorbs a separate
routine (e.g. the crt0 `jal 0x50B08; break` falling through into the init-array
at 0x89788), the host's entry-switch now routes a dispatch of that PC to the
correct absorbed interior block and runs it NATIVE — instead of depending on the
PC having been captured, or falling closed to the interpreter.
Derived purely from static jal evidence (jal encodes a code target, so it never
mints code from data); does not depend on execution capture. Preferred over an
explicit range-clip because the absorbed code IS the right code — it just needed
a routable entry. The static two-phase FunctionDiscovery already enforces
hard_cap = next-entry, so only the overlay path needed this.
VERIFIED: regenerated cache (codegen cg4_42de29a8) emits `case 0x80089788u` in
func_800896E0; cps_probe for 0x89788 drops 1021 -> 0 (now main-chain native, not
the broken CPS find-by-range default); freeze stays gone, 60fps sustained. Pairs
with the fail-closed entry guard (bd60381), which remains the safety net for any
genuinely-unrouted interior PC.
Framework-wide: regression-test Tomba 1 / MMX6 / Ape before any master merge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record the implemented fix and verification at the top of the freeze writeup:
overlay CPS entry-switch fails closed on a foreign interior PC -> sanctioned
dirty-RAM interpreter (ABI v6). Verified native passes the freeze (frame
1823 -> 18001+, 60fps, i_mask=0x0D, reval_crc_miss=0). Notes the optional B/C
completeness layer (clip overlay ownership range in the capture-driven overlay
discovery path) and that the static two-phase discovery already enforces
hard_cap=next-entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds overlay_cps_probe (debug cmd) to record what the CPS interior-continuation
dispatch decides for a watched PC. Used it to CONFIRM the root cause:
func_80050B08 calls jal 0x89788 (init-array). 0x89788 isn't a registered entry,
so overlay_find_by_range(0x89788) runs -- 6 loaded candidates have a range
containing it -- and returns func_800896E0 (the crt0 bootstrap), whose range
wrongly spans 0x89788. 0x89788 is not a case in its entry-switch, so default
re-runs the bootstrap from the top: InitHeap/A0 spin, VBLANK-masked deadlock.
func_800896E0's range covers 0x89788 because function_discovery.cpp:493-499
treats BREAK as fall-through; the bootstrap's "jal 0x50B08; break" (break guards
the main-returned case) falls through at 0x89784 into the separate function at
0x89788, absorbing it. So the early break-no-op finding WAS the root -- damage is
at discovery/range time, not runtime break execution. Interp is immune (executes
0x89788 bytes directly).
Fix options + recommendation (clip function range at next entry / don't cross a
function boundary on break fall-through) documented in
docs/TOMBA2_NATIVE_FMV_FREEZE.md. Diagnostic only; no fix applied yet (needs
regression on Tomba1/MMX6/Ape). Workaround: overlay_native_block 0x80050B08.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a phys-keyed blocklist in overlay_loader.c that forces a named overlay
function through the sanctioned dirty-RAM interpreter (it still RUNS — not
skip/stub/HLE), as if s_native_exec were off but for that one function only.
Exposed as the `overlay_native_block` debug command (add/clear/report). This is
the localization tool for native<->interp divergences (e.g. the Tomba 2 FMV
freeze): binary-search which compiled overlay function's native execution flips
the bug, without rebuilding or losing candidate visibility. Runtime/tool-only.
Used it to localize the Tomba 2 end-of-Whoopee-Camp FMV freeze to
func_80050B08's OWN native body (blocking it passes the freeze with callees
still native; all 33 jal targets + continuations verified clean). Narrowed to
the main-loop region with a strong interior-entry suspect at 0x80050CE8.
Findings in docs/TOMBA2_NATIVE_FMV_FREEZE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Whoopee-Camp-end freeze with the warm overlay cache is a DETERMINISTIC
codegen divergence, not a timing race -- proven via the sandboxed
native<->interp shadow-compare (overlay_diff/shadow_dump): both engines
run from identical CPU+RAM state, no IRQ/timing, and native still diverges.
First divergence (overlay_shadow_detail) is fn 0x800896E0: native t1=0xE4
(=0x39<<2, the A0 dispatcher`s sll t1,t1,2) / t0=0xBFC01E24 (A0_table[0x39],
InitHeap) vs interp t1=0x39 / t0=0x80000000. Native wrongly routes through
the A0:0x39 dispatch path interp doesn`t take -> re-inits every spin-loop
iteration -> endless A0-vector dispatch with VBLANK masked -> deadlock.
Also documents a separate latent CLASS bug: the recompiler does not model
the R3000A load-delay slot (strict_translator.cpp:830) while the dirty
interp does; 50 load-delay hazards found in this game`s overlays (0
control-flow) -- not this freeze, but a real divergence class to class-fix.
Same ~frame-1828 hang a prior session believed fixed by 47f3f15; it was
masked by interp fallback (audit-failed region), the clean warm cache
re-exposes it. Exact diverging instruction + recompiler fix still pending.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
P1+P2 done (c5efd70). Decoupled post-loop fragment pass; validated by probe
(clean mid-entry island for 0x80083094), full run (34/47 executed orphans at
0x38000 incl 0x80083094 as isolated shards, despite that region's normal compile
audit-failing), and a fresh runtime run (20 DLLs / 264 native / crc_miss=0 — no
divergence). Open: clean good-variant warm for end-to-end FMV/title speed (messy
accumulated captures audit-fail the region, losing hot NORMAL funcs); optionally
extend isolated fragments to executed normal funcs; P3 host recovery optional.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Architecture review verdict for the FMV-driver coverage gap: DON'T recover the
host. The regression was mixing speculative recovered-host roots into the trusted
all-roots region DLL (shared failure domain). Fix = three isolated build classes
(REGION_TRUSTED / RECOVERED_HOST_SPECULATIVE / INTERIOR_ENTRY_FRAGMENT); for the
FMV, compile an interior-entry ISLAND that enters at the executed PC 0x80083094
over a worklist-discovered CFG (not linear-to-boundary), its own DLL, keyed by
covered ranges + covered-byte hash. Mid-function start is safe iff codegen makes
no function-entry assumptions (no synthesized frame; out-of-island branches are
dispatcher exits, not audit failures). Phased plan recorded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Attempted orphan-interior host recovery (walk back to the host start, root it so
the FMV driver 0x80083094 compiles). It REGRESSES: adding the recovered host as a
root to the region's shared seed list (even gated to executed/resident orphans)
makes the recompiler emit C that fails the strict generated-C audit, and because
all roots in a region compile into ONE DLL, one failure skips the whole region ->
the previously-good 0x38000 region stopped compiling. Reverted to clean
checkpoint. Robust path = isolate each recovered host (+ its interior aliases)
into its OWN compile unit / DLL so a bad recovery fails alone (loader already
content-matches per function across DLLs), or audit-validate each host before
including it. Dedicated effort; recorded for next session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Records the three fixes that made native overlay execution stable through boot
(region_start load fix bb62b20, alias-rollup 47f3f15, timer write8 6004ced) and
the remaining FMV-speed blocker: the hot FMV driver 0x80083094 is a
DISPATCH_INTERIOR orphan (no discovered host -> not compilable -> interp-only).
Plus the next-step plan: recover the host by walking backward from the interior
PC to a callable-start signature, root it, and let the interior become an
alias-entry into it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The slowness root cause (region_start walkback overshoot) is fixed (bb62b20):
native funcs 14 -> 790. Remaining blocker = a native-vs-interp divergence at
0x80051FA4 that hangs the boot when native runs.
0x80051FA4 is a CPS CONTINUATION point inside host 0x80051F80, and the recompiler
emits BOTH a full host func_80051F80 AND a separate epilogue-only alias body
func_80051FA4 -- with the SAME .ranges range (R 80051F80 34) and SAME code_crc
(5CE0B05E). So two candidates cover identical bytes and a continuation to 0x51FA4
can resolve to the epilogue-only alias instead of the full host. Hypothesis:
aliases/interior entries should roll up to (dedup into) the parent host shard.
Recorded for the recompiler fix + ChatGPT architecture review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit of the current overlay-cache subsystem (loader, capture, autocompile,
compile tool) plus the design target. Key finding: the correctness model is
sound (dispatch re-validates live RAM vs per-function code_crc every call, so
no stale execution) -- the problems are efficiency: whole-region-CRC build
keying causes redundant DLLs, a per-dispatch full CRC32 taxes warm code, and
capture/dlopen/JIT run on the main thread. Phased plan P1-P5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fixed CD command trace shows single-sector data reads (PVD16->root22->subdir373) all work; the
game stalls AFTER the directory read at the FILE-LOAD DECISION, issuing no further CD command. The
CD-audio/XA streaming is still missing (oracle DMA3-streams, recomp doesn't) but the game never
reaches it, so it's downstream, not the gate. Corrected blocker: a state the game tests after the
dir read diverges (no SetLoc+ReadN for the next file). Supersedes the premature 'CD streaming root
cause' conclusion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>