Commit Graph

128 Commits

Author SHA1 Message Date
Matthew Stanley aef852e11b SIO alias dispatch + instant disc speed + game miss collector
SIO handler alias (0x0CF0 → func_0000641C):
  Runtime capture showed BIOS installs lui+addiu+jalr to RAM 0x641C at 0x0CF0.
  New [[recompiler.bios_aliases]] TOML mechanism emits a one-liner wrapper and
  registers it in the dispatch table. Eliminates 30K dirty_ram blocks/session.

Instant disc speed (disc_speed = "instant" in game.toml [runtime]):
  cdrom_set_speed(0) collapses all seek/read delays to 1 cycle. Correct INT
  sequence still fires; no artificial wait. "2x" and "4x" divisors available.
  Default remains authentic 1x. Tomba set to instant.

Persistent game miss collector (tools/collect_game_misses.py):
  Appends newly-seen dirty_ram dispatch misses in the game text range to a
  file (TombaRecomp/seeds/dirty_ram_misses.txt) for later seeding.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 23:42:16 -07:00
Matthew Stanley f7258927ae Seed verified BIOS kernel function 0xBFC15F1C (SIO/PAD interrupt handler)
Runtime measurement showed this function (RAM 0x641C) and its internal
branch targets (0x6430, 0x6444, 0x6458, 0x650C, 0x6514, 0x658C, 0x6594)
collectively generating ~50K dirty_ram_dispatch blocks/session. The address
is confirmed as a true function start via jalr call site at 0xBFC15F10.

Seeding the entry point only; CFG analysis discovers all internal labels.
Result: blocks_run dropped from 119,804 to 29,692 (75% reduction).

Also adds tools/probe_dirty_ram.py for live dirty_ram measurement via the
TCP debug server.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 23:29:05 -07:00
Matthew Stanley 5804a5a351 Add static BIOS vector dispatch for A0/B0/C0
Replaces dirty_ram_interp interpretation of BIOS vector trampolines with
statically-generated C switch handlers registered in the dispatch table.

For SCPH1001.BIN (a fixed binary), the A0/B0/C0 function pointer tables
are known at recompile time: A0 is pre-populated in ROM at 0xBFC04300,
B0 and C0 in the kernel image at 0xBFC10374 and 0xBFC10174 respectively.
The recompiler reads each table from the ROM binary and emits a switch
function (bios_vec_A0/B0/C0) registered as dispatch table entries for
RAM addresses 0xA0/0xB0/0xC0.

For kernel entries (ROM-encoded), calls go directly to the static C function
via the switch: zero interpretation overhead. For Shell-patched entries
(runtime-installed by the Shell at B0:0x32+ etc.), a RAM-table fallback reads
the live pointer via cpu->read_word and dispatches through psx_dispatch_call:
one memory read + dispatch instead of a 6-instruction trampoline interpretation.

New TOML fields in [[recompiler.bios_vectors]]:
  ram_addr, index_reg, table_rom_addr, table_count, table_ram_addr (opt.)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 22:34:43 -07:00
Matthew Stanley bd60a359fc Controller: fall back to first available, add Steam Remote Play hints
- open_configured_controller: two-pass scan — first tries the configured
  device index, then falls back to the first available game controller.
  Fixes Steam Remote Play where the virtual Xbox controller appears at an
  unpredictable SDL joystick index, causing silent failure when device=0
  in input.ini doesn't match.
- SDL_HINT_JOYSTICK_HIDAPI=1 / SDL_HINT_JOYSTICK_RAWINPUT=0 set before
  SDL_Init so Steam Input's virtual controller is enumerated via HIDAPI
  rather than the raw Windows HID path, making it visible as a proper
  SDL_GameController.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 21:40:36 -07:00
Matt Stanley fdf3656b2a Merge pull request #3 from mstan/release-build
Release build: v4 rename, release/debug split, resizable window, fiber fix
2026-05-29 21:31:52 -07:00
Matthew Stanley 5c87ee2837 Fix psx_fiber_convert_thread on Windows: drop sentinel, use ERROR_ALREADY_FIBER
The 0x1E00000000000000 TEB sentinel for "not yet a fiber" is MSVC-
internal. MinGW's GetCurrentFiber() before ConvertThreadToFiber may
return a different unspecified TEB value, causing the sentinel check
to silently skip ConvertThreadToFiber. The thread stays as a non-fiber
and the first SwitchToFiber crashes (SIGSEGV at frame 714, CD-boot).

Fix: call ConvertThreadToFiber unconditionally and detect the already-
converted case via GetLastError() == ERROR_ALREADY_FIBER, then fall
back to GetCurrentFiber(). This matches the pre-abstraction behavior
in the old traps.c which gated on s_main_fiber == NULL.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 21:29:40 -07:00
Matthew Stanley af90bd7123 Release/debug split: auto-disable debug tools, no console, resizable window
- PSX_DEBUG_TOOLS now defaults OFF for Release/MinSizeRel, ON for
  Debug/RelWithDebInfo. A plain -DCMAKE_BUILD_TYPE=Release produces a
  production binary with no TCP server and no per-block recording.
  Explicit -DPSX_DEBUG_TOOLS=ON/OFF overrides the default.
- Release MSVC: /SUBSYSTEM:WINDOWS + /ENTRY:mainCRTStartup — no console
  window appears when launching the exe from Explorer/desktop.
- Release MinGW: -mwindows for the same effect.
- SDL_WINDOW_RESIZABLE added so the window can be dragged to any size;
  SDL_RenderSetLogicalSize(640,480) already letterboxes correctly.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 21:16:49 -07:00
Matthew Stanley 6f7fcdc89b Remove all psxrecomp-v4 naming — rename to psxrecomp throughout
The "v4" suffix was a holdover from iterative project restarts.
There is now one psxrecomp; all identifiers, include guards, cmake
variables, function names, log strings, schema tags, and tool paths
drop the v4 suffix.

Changes:
- PSXRECOMP_V4_ROOT → PSXRECOMP_ROOT (cmake)
- PSXRECOMP_V4_RUNTIME_SOURCES/_INCLUDE_DIRS/_BIOS_GENERATED → no V4
- psxrecomp_v4_add_runtime_target → psxrecomp_add_runtime_target
- PSXRECOMP_V4_*_H include guards → PSXRECOMP_*_H
- "psxrecomp-v4" log/window strings → "psxrecomp"
- Schema version strings in recompiler JSON output
- Tool script hardcoded absolute paths updated

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 21:14:30 -07:00
Matt Stanley 7d4eee715e Merge pull request #2: Native macOS/Linux support (ucontext thread scheduler, fullscreen, build fixes)
Native macOS/Linux support (ucontext thread scheduler, fullscreen, build fixes)
2026-05-29 21:06:37 -07:00
Matthew Stanley b9716154f0 Add native macOS/Linux support (ucontext thread scheduler, fullscreen, build fixes)
The recompiler and runtime now build and run natively on macOS (Apple
Silicon & Intel) and Linux, alongside Windows. The recompiled BIOS boots to
the PlayStation logo, and Tomba boots into gameplay with working memory-card
save/load — the previously Windows-only path.

Core change — the BIOS thread scheduler:
- runtime/src/psx_fiber.{h,c} (new): one cooperative-fiber API, backed by
  Win32 Fibers on Windows and ucontext (makecontext/swapcontext) on POSIX,
  with a GetCurrentFiber analog (psx_fiber_current).
- traps.c: the BIOS thread switcher now uses psx_fiber_* on every platform.
  The old non-Windows setjmp/longjmp fallback is removed — it ran target
  threads nested on the caller's C stack and so couldn't resume an arbitrary
  suspended thread, which hung the BIOS CD-boot WaitEvent handoff. Real
  per-thread stacks (fibers/ucontext) fix it.
- interrupts.c: the exception-deferral path (g_exception_owner_fiber,
  deferred_exception_longjmp) is now fiber-based on all platforms via the
  same abstraction, instead of #ifdef _WIN32.

Cross-platform build fixes:
- recompiler/CMakeLists.txt: vendored fmt 9.1.0 force-enables consteval on
  Apple clang (the __cpp_consteval clause overrides its own Apple exclusion),
  which clang 21 then rejects; force FMT_CONSTEVAL empty on non-MSVC.
- runtime.cmake: add psx_fiber.c; add pkg-config's SDL2_LIBRARY_DIRS so
  -lSDL2 resolves outside default paths (Homebrew /opt/homebrew/lib).
- crash_trace.c: include <stdarg.h> (not pulled in transitively off Windows).
- main.cpp: the forced "opengl" render-driver hint (a Windows-GDI-freeze
  workaround) is now Windows-only so macOS uses Metal; create the renderer
  with PRESENTVSYNC; SDL_RenderSetLogicalSize(640,480) so fullscreen
  letterboxes; add an F11 / Alt+Enter / Cmd+F fullscreen toggle.

Verified: SCPH1001 BIOS boots to the Sony logo on Apple Silicon, and Tomba!
(SCUS-94236) boots into gameplay with memory-card saves persisting across
relaunches. Gamepad (SDL_GameController) was already cross-platform.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 18:53:22 -07:00
Matthew Stanley 4706680c1c README: add PSXRecomp logo at top
Tight-crop (1031x315) so the header isn't dominated by empty grey
above/below the logo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:31:11 -07:00
Matthew Stanley 092c9fc0f6 release: add controller config and BIOS package v0.1.0-alpha 2026-05-17 22:25:50 -07:00
Matthew Stanley f97833f1ed Fix stretched UI textured quad seams 2026-05-17 22:14:34 -07:00
Matthew Stanley 0c5b892904 Fix Tomba visual rendering issues 2026-05-17 21:58:55 -07:00
Matthew Stanley 4068518b04 runtime: add release asset picker 2026-05-17 17:16:14 -07:00
Matthew Stanley 291e10f00d checkpoint: document Tomba gameplay milestone 2026-05-17 16:49:01 -07:00
Matthew Stanley bd582d84fe runtime: emulate DualShock config pad replies 2026-05-17 16:38:35 -07:00
Matthew Stanley f6c15de880 checkpoint: options/new-game diagnostics handoff 2026-05-17 11:29:26 -07:00
Matthew Stanley 9684b31393 gitignore: untrack duckstation (local-only oracle, not a project dep)
The DuckStation oracle (mstan/duckstation@psxrecomp on GitHub) is
maintained separately for psxrecomp-v4 debugging. It is not a
build dependency of psxrecomp-v4, the runtime, or the recompiler.
Clone the fork manually if you need an oracle.

Removes the (stale) submodule entry that briefly pointed at upstream
DuckStation, deletes the empty .gitmodules, and adds /duckstation/
to .gitignore so a local clone in that location is invisible to the
project.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:20:39 -07:00
Matthew Stanley fdbd2b515f runtime: cross-fiber longjmp fix + decouple vblank-present from MMIO read
Two distinct bugs were causing OPTIONS-black hard freezes after 12K-65K
frames:

1. interrupts.c + traps.c: exception_jmpbuf is a single global. setjmp
   captures the running fiber, but longjmp may be requested from a
   different fiber via psx_exception_longjmp / psx_restore_state_escape
   during chain-handler dispatch. Cross-fiber longjmp restores RSP to
   the wrong stack while Win32 still tracks the calling fiber, corrupting
   fiber state until SwitchToFiber eventually hangs.

   Fix: track g_exception_owner_fiber at setjmp time. New
   deferred_exception_longjmp checks the current fiber; if it's not the
   owner, sets a pending flag and SwitchToFiber's back to the owner.
   psx_change_thread_fiber's post-SwitchToFiber path observes the flag
   and executes the longjmp on the correct stack.

2. gpu.c: gpu_read_gpustat fired gpu_vblank_tick() after 1000 polls,
   which invoked sdl_vblank_present (SDL_Delay -> Sleep) from inside
   an MMIO read code path. That meant recompiled MIPS spent wall time
   in Sleep deep inside the fiber call tree -- confusing to reason
   about and a contributor to fiber/longjmp interactions.

   Fix: synthetic VBLANK from a poll-storm only updates emulation state
   (LCF + I_STAT). The host-side render+sleep happens only at the proper
   cycle-paced VBLANK trigger in psx_check_interrupts.

Also adds: dirty_block ring slice to the freeze auto-dump, Win32 main-
thread stack capture for wedge_kind=1 hard freezes (SuspendThread +
StackWalk64 + SymFromAddr), and dbghelp link.

Validated: TombaRecomp psx-runtime ran 30+ minutes / 105K+ frames at
OPTIONS with no hard freeze. Previous freezes consistently hit at
11900, 20493, and 64596 frames.

Diagnostic tooling under tools/_options_session/ that produced this
diagnosis is included.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:49:13 -07:00
Matthew Stanley 747410abe9 runtime: SDL OpenGL renderer (fix freeze) + heartbeat-always-on + bisect hooks
The actual fix for TombaRecomp/ISSUES.md #6 ("Not Responding"
freezes). Earlier claim that SDL_RENDERER_SOFTWARE fixed it
(commit 9eb4ab3) was wrong — based on a too-short stress test
that didn't reach the freeze threshold. The corrected story:

  1. SDL_RENDERER_ACCELERATED → froze.
  2. SDL_RENDERER_SOFTWARE (Windows GDI backend) → still froze.
  3. SDL_HINT_RENDER_DRIVER=opengl + ACCELERATED → no freeze
     after 11+ min / 40k+ frames, well past every prior failure.

Why it intermittently triggered: the FMV-speed fix (b486c13)
raised guest cycle throughput, which raised the GDI present
rate enough to trip the underlying Windows GDI hang. Before
that fix the workload stayed below threshold.

Three changes in this commit:

  * runtime/src/main.cpp + runtime/src/beetle_main.cpp:
    switch from SDL_RENDERER_SOFTWARE to OpenGL via
    SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl").

  * runtime/src/main.cpp: PSX_SDL_NO_AUDIO and PSX_SDL_NO_RENDER
    compile-flag guards added for bisection. Pass via -DCMAKE_C_FLAGS
    when needed; default behavior is unchanged.

  * runtime/src/freeze_heartbeat.c:
      - Always-on regardless of PSX_NO_DEBUG_TOOLS. This thread
        was the key diagnostic — it survives main-thread stalls
        because it runs in its own thread, so its ring captures
        pre-freeze state when the TCP server can't respond.
      - Added a 64-entry pre-freeze history ring (~6 s window)
        to the heartbeat JSON. Each entry snapshots frame_count,
        psx_cycle_count, exc_reentry_blocks, dirty_ram_insns,
        current_func, last_store_pc, i_stat, sio_stat/ctrl,
        in_exception, mc_max_state. When all those columns stop
        advancing while wall_clock keeps ticking, that's the
        stall point. This signal proved the freeze is in HOST
        code (not guest), which redirected the investigation
        from "fix the recomp" to "fix the SDL/Windows path."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:16:20 -07:00
Matthew Stanley 3dce5e43ce runtime: PSX_DEBUG_TOOLS CMake flag + freeze heartbeat thread
Two observability additions, both gated by the new
-DPSX_DEBUG_TOOLS=OFF flag for production builds.

1. Freeze heartbeat thread (`runtime/src/freeze_heartbeat.c`):
   Background thread spawned at startup that snapshots runtime
   state (frame_count, psx_cycle_count, in_exception, exception
   counters, sio/mc state, i_stat/i_mask, dirty_ram counters) to
   `psx_freeze_heartbeat.json` every 100 ms. Survives main-thread
   freezes — writer is independent of the SDL loop and the TCP
   debug server. Atomic file replace via .tmp + MoveFileEx so a
   reader never catches a half-written JSON.

   Already paid off: captured a freeze pattern (frame_count stuck,
   psx_cycle_count stuck, in_exception=0, current_func in
   install-stub kernel code) that the TCP server couldn't show
   because its accept queue was wedged by the same stalled main
   thread.

2. PSX_DEBUG_TOOLS CMake flag (`runtime/runtime.cmake`):
   - Default ON: builds with the TCP debug server, heartbeat
     thread, and per-block recording (debug_server_log_call_entry
     fires for every recompiled function entry).
   - OFF (`cmake -DPSX_DEBUG_TOOLS=OFF`): production build. The
     hot-path log functions early-return; TCP server doesn't
     start; heartbeat doesn't spawn. The ring buffer arrays still
     exist in the binary (their definitions live in
     debug_server.c) but no writes happen so the demand-paged
     memory stays committed at a much lower level (~77 MB at
     idle vs ~245 MB in debug mode).

   The flag propagates to psx-beetle via the option declared at
   the top of runtime.cmake.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:36:37 -07:00
Matthew Stanley 04a5490894 runtime/debug_server: raise listen() backlog to 16
Pure observability change — not a fix claim.

Both psx-runtime (was 1) and psx-beetle (was 4) had small accept-
queue backlogs. When the SDL main thread stalls (the exact state
we want to probe during a freeze investigation), the kernel
queues the first pending connect and rejects subsequent attempts
with RST. From the client side this looks like ConnectionRefused,
making it impossible to diagnose the stalled process from another
shell.

Backlog 16 lets a handful of probes queue up. Main thread will
eventually accept them in order if it ever resumes. If the
main thread is permanently dead the probes still fail, but at
least the failure mode changes from "looks like the server is
gone" to "the queue is genuinely deep + still stalled."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 20:45:08 -07:00
Matthew Stanley 7faaba24f7 runtime/debug_server: surface dispatch-miss count in ping response
NES recomp template PRINCIPLES.md §13a calls a dispatch miss a
"SILENT GAME-BREAKING BUG" and mandates checking
`dispatch_misses.log` after every game run. We don't write that log
(no log files per CLAUDE.md §3) — we have an in-memory ring +
`unknown_dispatch_log` TCP command instead. But nothing routinely
surfaces the count, so silent misses can accumulate across sessions
unnoticed.

Now `ping` includes `dispatch_miss_total` and `dispatch_miss_unique`
fields. Every routine health check reveals if discovery has gaps.
0 = healthy. Non-zero on any session = stop and add the addresses
to game.toml / regen before continuing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 20:09:25 -07:00
Matthew Stanley 2aeb4db4e5 runtime/gpu: GP1(0x10) subcommand 7 returns GPU version
Tomba's `ResetGraph` (FUN_8005E694 at 0x8005E694) calls
`FUN_80061620` to autodetect the GPU video mode. That function
writes `GP1(0x10000007)` ("Get GPU info", subcommand 7 = GPU
version) and reads back GPUREAD; if the low 24 bits equal 2 it
returns 3 (the byte stored at `0x80090C9C`).

Our `gp1_get_info` was treating subcommand 7 as "leave latch
unchanged" (a misleading DuckStation-derived note in the source).
So GPUREAD held stale junk, the `== 2` check failed, and
`0x80090C9C` ended up 0 instead of 3 — landing the OPTIONS draw
code on a no-draw config table entry that produces 11 GP0 commands
per frame with zero draw primitives.

Mednafen-psx (`beetle-psx/mednafen/psx/gpu.cpp:1212`) returns 2
hardcoded for subcommand 7 — that's the GPU-version constant real
hardware returns. We now do the same, plus subcommand 8 = 0 (also
matches mednafen), and mask the subcommand with `& 0x0F` instead of
`& 0x07` to match mednafen's decode.

Verified: runtime's `0x80090C9C` now reads `0x03` at boot,
matching beetle. OPTIONS UI is still black so more downstream
divergences remain — but this is a real root cause and a step
forward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 19:25:53 -07:00
Matthew Stanley 9eb4ab3f21 runtime: SDL software renderer only
The SDL accelerated renderer (`SDL_RENDERER_ACCELERATED`) lets
`SDL_RenderPresent` block inside the GPU driver, which has been
reproducibly hanging the SDL main thread mid-frame after extended
uptime. Both psx-runtime and psx-beetle showed the same signature:
process alive, debug socket dead, partial framebuffer (top half
rendered, bottom half stale) — a classic GPU-driver-side hang.

Switching both binaries to `SDL_RENDERER_SOFTWARE` removes the GPU
driver from the path. Verified under stress: five consecutive
`wtrace_dump count=200000` (~6.2 s each, 31 s cumulative
main-thread block) without a single freeze. Beetle, which had been
freezing at ~15 min of plain title-screen idle with constant 90 MB
RSS (so memory pressure was definitively not the cause), now stays
responsive indefinitely.

Cost: a few percent more CPU. Worth it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 19:25:42 -07:00
Matthew Stanley 4161c854d2 runtime/beetle: add --disc <path> for psx-beetle.exe
psx-beetle previously hard-coded "dummy.cue" in the BIOS system dir
and ejected the disc on init so the BIOS booted to its shell. That
covered BIOS-only testing but not "what should this game look like at
this frame" comparisons against psx-runtime.

Add `beetle_init_with_disc(bios_path, disc_path)`: if disc_path is
non-NULL, retro_load_game() loads that real disc and the disc stays
inserted (BIOS proceeds straight into game boot). NULL keeps the
existing dummy-cue + eject behavior. beetle_init() is now a wrapper
preserving the old signature.

beetle_main.cpp learns `--disc <path>` CLI flag.

Verified: `psx-beetle.exe bios/SCPH1001.BIN --disc tomba.cue` boots
Tomba through the SCEA splash on the SDL window. Debug server on port
4380 responds (ping returns frame counter), so the same wire protocol
that psx-runtime exposes on 4470 is now available for side-by-side
state comparison while both binaries run the same game.

Use case driving this: Issue #5 OPTIONS-black SIO protocol divergence
diagnosis — need beetle as authoritative reference for what bytes the
card should send Tomba.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:22:29 -07:00
Matthew Stanley b486c13c3a runtime: fix VBlank pacing to use guest cycles, not dispatch count
VBlank firing in psx_check_interrupts was gated on dispatch_count
(block-dispatch count, ~5-6 cycles each), not on guest cycles.
50000 dispatches ≈ 289k cycles per VBlank, vs real PSX 564480
cycles per VBlank. Effect: game-internal time ran at ~51% of real
PSX rate, breaking every cycle-paced game flow including FMV.

This was the root cause of TombaRecomp ISSUES.md #4 — FMV played
at 6.7 fps because Tomba's FMV state machine is driven by DMA Ch1
(MDEC-out done) IRQ, which in turn fires once per full FMV-frame
pipeline iteration, which is bounded by cycle-paced steps
(CDROM sector ready, MDEC decode "complete", DMA transfer).
At 51% guest-time rate, a Tomba-designed 15-fps FMV becomes 7-8 fps.

Fix:
- New VBLANK_CYCLES = 564480 (33.8688 MHz / 60 Hz NTSC).
- Add interrupts_advance_cycles(cycles) called from
  psx_advance_cycles() so the cycle counter for VBlank gating
  tracks emitted guest cycles.
- Change the trigger from `dispatch_count >= VBLANK_INTERVAL` to
  `cycles_since_vblank >= VBLANK_CYCLES`.
- On VBlank fire, subtract one period (not reset to 0) so cycle
  overshoot from long blocks carries forward.
- dispatch_count is kept for the SIO card-protocol stale check —
  that uses total_checks, which is the real load-bearing field
  there, dispatch_count itself is now incidental.

Verified:
- Cycles/VBlank: 289k -> 564k (exactly VBLANK_CYCLES).
- Cycles/sec: 20.6 M -> 41 M (121% of real PSX; slight wall-clock
  overshoot to investigate, but game-internal pacing now correct).
- FMV decode rate: 0.111 -> 0.250 decodes/VBlank = 15.0 fps exactly,
  matching Tomba's designed FMV rate.
- Measured via tools/_mdec_pace.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:09:11 -07:00
Matthew Stanley f42c597090 runtime/debug_server: remove pause / step / run_to_frame entirely
Per CLAUDE.md global rule #2 (and direct user instruction): pause-and-
inspect is the wrong observation primitive for this codebase. It
synthesizes a snapshot at probe time rather than reading the system's
own continuously-recorded history. It also accidentally became a
freeze vector — a Python client that sent `pause` without `continue`
left s_paused=1 with no client to clear it, so the runtime spun in
debug_server_wait_if_paused forever and looked frozen.

Replacement is the ring buffers that already exist:
  - fn_entry_dump / fn_entry_tail   what code ran
  - wtrace_dump                     what memory was written
  - gpu_frame_dump frame=N          what GP0 commands were issued
  - mdec_trace                      MDEC events
  - sio_trace / sio_pc_trace        SIO history
  - frame_range / get_frame         per-frame state snapshots

Changes:
- handle_pause, handle_continue, handle_step, handle_run_to_frame
  now return send_err with a migration message pointing at the
  appropriate ring. The state variables (s_paused, s_step_count,
  s_run_to) stay declared and zero-valued so freeze_check fields keep
  reporting them as 0 (protocol stability).
- debug_server_wait_if_paused is now a no-op (kept exported so
  main.cpp's vblank callback stays unchanged).
- The post-frame `if (s_step_count > 0)` / `if (s_run_to > 0)` hooks
  are gated behind `if (0)` — dead code, kept as a comment marker.
- debug_server_dirty_break_maybe_pause no longer sets s_paused; it
  still records the hit (target, ra, regs, sp, frame) so
  dirty_break_state can be queried for the latest event.
- Reverted the client-disconnect auto-clear-of-pause logic added in
  40e9b79 — no longer needed, pause is gone.

Verified: pause/step/continue all return the migration error,
frame_count advances steadily (~50 fps) for 30 seconds, no freeze.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:47:18 -07:00
Matthew Stanley 40e9b79fb7 runtime/debug_server: fix "frozen" runtime — auto-unpause on client disconnect
Root cause of the apparent "freeze" observed after issuing debug
commands: tools/_dbg.py and similar one-shot Python clients open a
fresh TCP connection per command, send their request, read the reply,
then close. If the command was `pause` or `step`, the runtime is left
with s_paused=1 and no client to issue `continue`. The runtime then
spins in debug_server_wait_if_paused() forever — SDL window stops
updating, frame counter stuck — and the user reads this as "frozen"
even though the process is healthy.

The legitimate "open a persistent connection, pause, investigate,
continue" workflow keeps the socket open across all of that, so the
disconnect-then-clear behavior doesn't affect it.

Fix: when the client disconnects, clear s_paused / s_step_count /
s_run_to. Verified by sending pause via _dbg.py and watching the
frame counter keep advancing on the next freeze_check query.

Visibility: freeze_check now reports frame_count, paused, step_count,
run_to, client_connected. A "frozen" state can now be classified
without guessing — paused=1 = leftover-pause, paused=0 + frame_count
stuck = real game-loop wedge.

Ring cap reductions:
- WRITE_TRACE_CAP   4M -> 256K   (-360 MB worst-case)
- SIO_PC_TRACE_CAP  4M -> 256K
- MMIO_TRACE_CAP    4M -> 256K
- FN_TRACE_CAP      1M -> 256K
- FN_EXIT_TRACE_CAP 4M -> 256K

At 60Hz and ~thousand events/frame each ring still covers ~4 sec —
plenty for the diagnostic flows we actually run. Worst-case runtime
RSS drops from ~1 GB potential to ~100 MB.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:43:13 -07:00
Matthew Stanley 94a829904f Revert "runtime/mdec: fast IDCT (FFmpeg simple_idct, ~16x fewer mults)"
This reverts commit e5a4e9643c.
2026-05-13 13:19:00 -07:00
Matthew Stanley e5a4e9643c runtime/mdec: fast IDCT (FFmpeg simple_idct, ~16x fewer mults)
Replace the O(N^3) reference IDCT in idct_block (1024 multiplies per
8x8 block) with FFmpeg's simple_idct butterfly decomposition (~64
multiplies per block). Both row and column passes ported to produce
int16_t output so the rest of mdec.c (to_output_u8,
append_color_macroblock, append_luma_block) keeps working unchanged.

Source: FFmpeg libavcodec/simple_idct_template.c (BIT_DEPTH=8),
LGPL-2.1+. Previously imported into legacy runner/src/fmv_player.cpp
on the legacy-2026-05-11 branch; salvaged into v4's hardware-emulated
MDEC path. Attribution preserved in the source comment.

Side effect: the new IDCT computes cosines from the W1..W7 constants
directly and no longer depends on mdec.scale. The prior implementation
treated SET_SCALE input as the cosine matrix; real PSX hardware ignores
SET_SCALE for IDCT (it only seeds the dequant tables), so the new code
matches hardware more closely as well.

Verified visually: Tomba boots through Sony splash + Whoopee Camp FMV;
intro FMV reaches the mountain-valley scene with correct colors. No
visual regression vs the prior implementation.

This addresses one piece of TombaRecomp ISSUES.md #3.D / FMV-speed
goal. Whether the IDCT was the actual FMV bottleneck still needs an
A/B timing test; this is a clean CPU win regardless.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:07:49 -07:00
Matthew Stanley 3cfcdaba17 runtime: per-frame GP0 command ring buffer
Always-on ring captures every GP0 command (header + first 6 payload
words) stamped with the frame number issued. Hooked at
gp0_execute_command() for fixed-length commands and at the polyline
start path for variable-length ones. ~70 MB at 1 048 576 entries
(34 bytes/entry); lazy-allocated on first record.

New debug-server commands:
- gpu_ring_stats: total/capacity/oldest+newest frame in buffer
- gpu_frame_dump frame=N count=M: per-frame GP0 entries with raw words

Designed per CLAUDE.md ring-buffer rule (CLAUDE.md global #2):
capture is continuous; observers query a window of interest
post-hoc. No arming, no timed attach.

Use case driving this: fuzzy menu text bug on Tomba title screen
(TombaRecomp ISSUES.md #3 'D'). Lifetime opcode counts couldn't
distinguish which 0x64/0x2C calls draw the menu vs the static
text. With per-frame dump we can now show that frame N has 88 GP0
commands and isolate the 6 0x2C entries that render with degenerate
top-right vertices (= the fuzzy text).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:25:29 -07:00
Matthew Stanley 2f709ee542 Phase B: config-driven unified runtime + BIOS shell boot restored
- Config loader (recompiler/src/config_loader.{h,cpp}): single source of
  truth for both BIOS and game runtime settings (debug_port, window_title,
  memcard_dir, disc path). psxrecomp-bios and psxrecomp-game both accept
  --config <toml>; runtime takes --game <toml> (no more CLI overrides).
- runtime/src/main.cpp: replaced --disc/--memcard-dir/--game-root CLI
  overrides with --game <toml>; game.toml is the single source of truth.
  Discless launch (no args) goes to BIOS shell.
- runtime/src/dma.c: fixed update_master_irq() re-asserting I_STAT bit 3
  on every DICR read. BIOS shell DMA handler at 0x80058628 only clears
  DICR bit 27 (ch3 flag), so level-sensitive re-assert pinned the kernel
  in the exception loop. Bit 3 is now edge-triggered (one assert per DMA
  completion). Discless boot now reaches MAIN MENU / MEMORY CARD / CD
  PLAYER. With-disc Tomba boots through to Whoopee Camp splash.
- Audit tooling: codegen_audit.py (BIOS), codegen_audit_game.py (game),
  audit_config.py (config loader for tools), verify_audit_schema.py.
  Replaces older tools/_codegen_audit.py probe.
- Docs: CURRENT_STATE.md (project state), UNIFICATION_PLAN.md (Phase C
  translator merge plan), docs/config_schema.md (TOML schema reference),
  docs/audit_inventory.md (audit tier inventory).
- .gitignore: tools/_*.py convention for ad-hoc scratch probes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 01:31:27 -07:00
Matthew Stanley 185162428e runtime + recompiler: boot through main menu, Load Game enter/exit
Recompiler: register jump-table targets with the cross-function
continuation pass before filtering local goto labels — BIOS jump
tables can legally branch into another discovered function's body,
so the prior local-only check dropped valid emit-side edges.

Dirty-RAM interpreter:
  - Dispatch non-local JAL/JALR targets back through psx_dispatch_call
    instead of forcing them into the interpreter when the target sits
    in static-recompiled code (only targets in dirty pages stay local).
  - New always-on flow + per-instruction ring buffers (insn_log,
    flow_log) capturing reg state, current_tcb, and task fields for
    post-hoc analysis. Block log gains t0/t1/t2.
  - guard_yields counter; richer last_unsupported state (entry PC,
    entry RA/SP, insn count) for dead-end diagnosis.

SIO: ACK is now visible across a small read window (2 STAT reads)
matching BIOS poll patterns that re-read STAT after CTRL_ACK clears
IRQ but expect ACK to still latch. Per-channel ack_irq_en captured
at shift start so a CTRL write that disables ACK_IRQ_EN mid-shift
doesn't suppress the firing. sio_init now zeros all paced-model
state. Read/write paths poll the debug server every ~1k accesses
so long pad/card spins don't starve diagnostics.

Memcard: auto-format slot files on first init when missing, so
the BIOS shell sees a present-but-empty card instead of "no card".

Runtime main: SDL nearest-neighbor texture scaling (no bilinear
blur on the integer-scaled framebuffer).

Debug server: large expansion of ring-buffer queries and dump
commands feeding the new insn/flow logs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 10:34:22 -07:00
Matthew Stanley 07dab8fd34 chore: add GitHub Actions release workflow
Adapted from SuperMarioWorldRecomp's release.yml. Triggers on tag push
(v*) and workflow_dispatch. Builds psx-runtime in Release on
windows-latest under MSYS2 mingw64, stages the exe + SDL2 + mingw
runtime DLLs + LICENSE + a release README + bios/saves placeholders,
zips as psxrecomp-v2-windows-x64.zip, and attaches it to a GitHub
Release on tag builds.

Caveat: the recompiler step needs a real BIOS to operate on, which CI
does not have. The current workflow is a scaffold — it builds the
runtime artefact correctly but the recompile step will fail unless
the BIOS is supplied via a secret or a separate ingest step. Wire up
end-to-end before relying on tag builds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:08:47 -07:00
Matthew Stanley 6a657733ec docs: rewrite README for v2 (in-place rewrite, public-facing)
Replaces the Phase-1-internal milestone notes with a user-facing README
suitable for the GitHub repo's new role: in-place v2 of mstan/psxrecomp.

Calls out explicitly:
- CD-ROM / disc-based game loading is untested and likely non-functional
  in psx-runtime today. BIOS-only memcard management is the supported
  use case.
- MDEC video decode is slow / partial.
- SPU/GPU/GTE coverage is partial.

Includes setup (release binary path + build-from-source path), keyboard
map, memcard-management walkthrough, and v1 reference (legacy-2026-05-11
branch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:08:15 -07:00
Matthew Stanley 802e0fb16d chore: stop tracking BIOS-derivative generated/ output
The `generated/` tree contains C and JSON produced by the recompiler from
SCPH1001.BIN — derivative of copyrighted Sony BIOS code. It is regenerated
from a user-supplied BIOS on local build; not committed.

Mirrors the convention used by nesrecomp and snesrecomp.

Also covers release-stage/ and *.zip so local release packaging artifacts
stay out of the tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:06:40 -07:00
Matthew Stanley a9e707e605 chore: add PolyForm Noncommercial 1.0.0 LICENSE
Matches the license used by the v1 repo. Copyright preserved as
'2026 Matthew Stan'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:06:17 -07:00
Matthew Stanley 88a7ecdcaa runtime: fix Tomba FMV presentation audio and skip 2026-05-11 14:34:17 -07:00
Matthew Stanley 4e1d6f0b71 rabbitizer: drop unused upstream metadata, bindings, docs, and tests
Vendored rabbitizer ships with Python bindings, Rust bindings, an
upstream Makefile, packaging files, docs, tests, and an upstream
changelog. Our CMake build consumes only the C/C++ sources, headers,
and instruction tables:

  recompiler/lib/rabbitizer/
    include/        (kept — C headers)
    cplusplus/      (kept — C++ headers + sources)
    src/            (kept — C sources)
    tables/         (kept — instruction tables)
    LICENSE         (kept — MIT requires the copyright notice)

Everything else is upstream project scaffolding that has never been
referenced by our build. Removed:

  CHANGELOG.md, README.md, Cargo.toml, Makefile, MANIFEST.in,
  pyproject.toml, setup.py
  docs/                (upstream usage docs + GTE macros)
  tests/               (upstream's own test suite)
  rust/                (Rust language bindings)
  rabbitizer/          (Python language bindings + .pyi stubs)

MIT obligations remain satisfied: the LICENSE file with the
Decompollaborate copyright notice is unchanged. No source files,
headers, or instruction tables were touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:42:14 -07:00
Matthew Stanley cf05a0ab1f runtime + recompiler: Tomba boot through BIOS into Whoopee Camp FMV
Recompiler — `recompiler/src/function_analysis.cpp` now stops a
function's end_addr at its mapped `jr $ra` + delay slot instead of
stretching through trailing data. Fixes Tomba's 0x80076DB0, a real
136-byte function that was being emitted as a psx_unknown_dispatch
stub because data after it poisoned the boundary heuristic.
`code_generator.cpp` emits `func_<next>(cpu); return;` as a tail
call when a mid-function seed split leaves the physical fall-through
in a different C function, plus a `debug_server_log_call_entry` at
every function entry. `full_function_emitter.cpp` splits dispatch
into `psx_dispatch_impl(cpu, addr, stop_addr)` with public
`psx_dispatch` and new `psx_dispatch_call(cpu, addr, return_addr)`
so JAL/JALR into RAM stubs can return cleanly without growing the
native stack.

Runtime — `runtime/src/dma.c`/`include/dma.h`/`src/memory.c` now
expose `dma_write_masked()`; the 16-bit and 8-bit mmio paths route
DICR/DPCR/CHCR partial writes through it so a partial DICR write no
longer accidentally clears unrelated channels' pending IRQ flags,
and CHCR only triggers DMA when the written mask actually includes
the start bit. Unified `complete_transfer(ch)` across all channels,
removed `prev_master_flag` edge detection.

MDEC (`runtime/src/mdec.c`, `include/mdec.h`) gains a full debug
surface: `MDECDebugState` snapshot, an always-on 4096-entry
`MDECDebugEvent` ring with 11 event kinds (reset, ctrl_write,
cmd_begin/done, decode_done, dma_in/out start/end, output_drained,
read_underflow), and `mdec_state` / `mdec_trace` / `mdec_trace_clear`
debug-server commands. DMA ch0/1 emit start/end events around MDEC
transfers.

CDROM (`runtime/src/cdrom.c`, `include/cdrom.h`) — read stream
separated from one-shot pending state (`read_cmd`, `read_delay`,
`start_read_stream`, `stop_read_stream`, `deliver_read_sector`);
`cdrom_state`/`cdrom_trace` report the new fields.

Thread/fiber tracing — `traps.c`'s Win32-fiber TCB save/restore now
emits 12 thread-event kinds; new `ThreadTraceEntry` ring (64 K) plus
`thread_trace` / `thread_trace_clear` debug-server commands report
both live CPU regs and the target TCB's full saved register block.

wtrace / mmio rings — `WriteTraceEntry` grew (cpu_pc, sp, v0/v1/a0-a3/
t0/t1); capacity 1 M -> 4 M. `MmioTraceEntry` grew (pc, cpu_pc, sp,
a0-a3, sr, epc, i_stat, i_mask). Both dump commands accept
`count`/`newest=1` for reverse iteration. `restore_trace` and
`dirty_block_log` emit extended register fields (a2/a3/sp).

Misc — `interrupts.c` polls debug-server every 16K checks so it
stays responsive without a separate thread. `psx_runtime.h`'s
`call_by_address` routes through `psx_dispatch_call`. Dirty-RAM
interp JAL/JALR consult `psx_dispatch_game_compiled` when the game
is wired in. iso_reader gains the entry points needed by the game
disc path. New `tools/_tomba_probe.py` for port-4470 health checks.

Generated — regenerated `SCPH1001_dispatch.c` against the new
`psx_dispatch_impl` shape.

ISSUES.md — opened Issue #3 (BIOS PS-logo glyph missing on the
disc-detected screen, cosmetic).

.gitignore — added `recompiler/build-asan*/`, `build-checked/`,
`__pycache__/`.

Validated end-to-end: BIOS boots, disc detected, Whoopee Camp FMV
plays with audio. Frame count advances through ~40K frames without
freezes. `unknown_dispatch_log.total = 0`. Remaining FMV cluster
issues (1/4-size render, FMV-2 audio missing, no skip) tracked
under TombaRecomp/ISSUES.md.

Co-Authored-By: Codex (OpenAI) <noreply@openai.com>
2026-05-11 12:34:49 -07:00
Matthew Stanley 632e5dc662 runtime: BIOS thread switching via Win32 fibers + debug_server CDROM/dirty-RAM
traps.c — ThreadWaitFrame setjmp stack plus a Win32-fiber-based
HostThreadFiber pool for BIOS thread context switches. TCB save/restore
maps SR low-4-bits → low-6-bits via the standard PSX exception entry
shift. Backs the BIOS OpenTh/ChangeTh/ReturnFromException paths that
the kernel uses to schedule its own threads on top of the recompiled
exception handler.

debug_server.c — new TCP commands:
- cdrom_state: snapshot of index/stat/irq regs, FIFOs, pending cmd,
  seek/read MSF, i_stat.
- cdrom_trace: paginated dump of the cdrom_trace ring (kind/addr/val/
  func/pc/frame + state snapshot per entry).
- dirty_ram_unsupported: counts and last PC/insn/reason for opcodes
  the dirty-RAM interpreter rejected.
- dirty_ram_stats now also exports the full bitmap as
  dirty_bitmap_words[] (matches the bitmap promotion in this branch).

Both surfaces are query-only against always-on ring buffers, no
log files or stderr emission — consistent with the
no-stdio-debug rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:51:01 -07:00
Matthew Stanley b96f2a7098 runtime: MDEC + DMA channels 0/1 + full-RAM dirty bitmap
New runtime/src/mdec.c + runtime/include/mdec.h: MDEC register port
and DMA word ports. Reads command stream from DMA channel 0 (in);
emits decoded macroblocks via DMA channel 1 (out). mdec_init wired
into main.cpp init sequence.

dma.c — execute_ch0_mdec_in and execute_ch1_mdec_out implementations
for MDEC pipes. transfer_word_count handles sync mode 1 block-count
form (BCR upper half * BCR lower half) plus default block size.
complete_transfer(ch) clears active/start bits and raises DICR + i_stat
bit 3. Channels 2/3/4 still default.

memory.c — dirty-RAM bitmap promoted from u32 (16-page kernel-only) to
DIRTY_RAM_BITMAP_WORDS covering all 2 MB RAM. CD-ROM DMA flags overlay
load ranges via new dirty_ram_mark_executable_range(phys, len), without
treating arbitrary data writes as executable. CPU writes still only
mark the kernel-RAM code region (0..0xFFFF) as before.

dirty_ram_interp.c — opcode coverage extended for game-side install
stubs: MFHI/MTHI/MFLO/MTLO/COP0 MFC0/MTC0/RFE. Add
psx_dispatch_game_compiled hook gated on PSX_HAS_GAME_DISPATCH so
recompiled game EXE entries can take precedence over interpretation
on RAM PCs that are also covered by static codegen.

dirty_ram_interp.h — expose dirty_ram_get_bitmap_word(i) +
_word_count, dirty_ram_mark_executable_range, and the
last_unsupported_pc/insn/reason for the debug_server query landed
in a later commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:50:51 -07:00
Matthew Stanley b4b5d223e2 runtime: cycle-paced timers + CDROM with sector header + trace ring
timers.c — timers_advance(cycles) replaces per-frame timers_tick().
Per-timer fractional accumulator + proper divisors:
- Timer 0 dotclock approx (cycles/5)
- Timer 1 HBlank clock (cycles/2146, ~263 HBlanks per NTSC frame)
- Timer 2 SYSCLK or SYSCLK/8 per mode register bit 8..9
Target/overflow flags fire IRQ when bit 4/5 of mode is set.

cdrom.c — cdrom_advance(cycles) replaces per-frame cdrom_tick().
Sector pacing from CPU clock: 451584 cycles at 1x, 225792 at 2x
(SetMode bit 7). Whole-sector mode bit 5 prepends a 12-byte BCD-MSF
header to the data buffer; SECTOR_BUFFER_SIZE = 12 + 2048.

cdrom_trace ring: always-on CDROMTraceEntry[1<<16] capturing every
register write, IRQ raise/mask, sector load, and command dispatch
with the current func/PC/frame snapshot. Query via the new
debug_server commands landed in a later commit.

psx_cycles.c — psx_advance_cycles now drives cdrom_advance and
timers_advance in addition to sio_advance.

interrupts.c — legacy per-frame timers_tick + cdrom_tick now gated
behind #ifndef PSX_ENABLE_BLOCK_CYCLES, so block-cycle builds use
the cycle-paced path exclusively.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:50:39 -07:00
Matthew Stanley 0683e114d2 runtime: SDL2 discovery + game-dispatch hook (B1 finish)
Extend psxrecomp_v4_add_runtime_target():
- SDL2 discovery via MSVC dev-pkg layout (sdl2-msvc/SDL2-*) or pkg-config
  next to the C compiler on mingw. Centralizes what was inline in
  runtime/CMakeLists.txt before the runtime-as-library refactor.
- Track PSXRT_GAME_GENERATED_FULL_C / DISPATCH_C and emit
  PSX_HAS_GAME_DISPATCH=1 when a game-EXE codegen output is wired in.
- Register runtime/src/mdec.c as a default runtime source.

New runtime/include/psx_runtime.h: call_by_address(cpu, addr) thin
wrapper over psx_dispatch — call site that generated game-EXE code
will use for indirect jumps after psxrecomp-game emits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:50:29 -07:00
Matthew Stanley 48d9971f26 generated: regen against new discovery + seeds
Driven by:
- recompiler state-continuation pass (Pass 2.6)
- 3 new Tomba boot dispatch_miss seeds
- 4 BANDAID seeds retired (validated against memcard COPY/DELETE/persist)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:50:25 -07:00
Matthew Stanley 3b22728082 seeds: Tomba boot dispatch_miss + retire 4 BANDAID entries
Add 3 dispatch_miss seeds for Tomba boot path:
- 0xBFC04910: BIOS init routine (0xBFC048D0..0xBFC04ABC, ret at 0xBFC04A98)
- 0xBFC04850: BIOS callback-list init (0xBFC0472C..0xBFC048D0, ret at 0xBFC048C8)
- 0xBFC079F8: tiny BIOS table-leaf (sw a0,0(sp); jr ra; clear v0)

Retire 4 prior BANDAID seeds covered by the new state-continuation pass
and dispatch-miss heuristics:
- 0xBFC02918 card_copy_dispatch_miss
- 0xBFC09890 card_cmd_is_not_read
- 0xBFC0989C card_cmd_is_not_write
- 0xBFC1A1BC modal_stub_dispatch_miss

Validated by re-running BIOS shell with full suite of memcard ops
(COPY ALL, DELETE, persist-across-restart) — all green on master HEAD
plus the new recompiler discovery passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:50:19 -07:00
Matthew Stanley 25474410d0 recompiler: state-continuation discovery + psxrecomp-game build target
Add Pass 2.6 to FunctionAnalyzer: detect JAL targets whose callee starts
with `sw $ra, imm($a0)` (SaveState-style context-save helpers) and force
the post-delay-slot PC into the dispatch table. Recovers continuation
entries that aren't reached by JAL prologue or jr $ra discovery.

Extend find_function_start to recognize `lui rt; lw rt, off(rt)` 8 bytes
before jr $ra as a prologue marker. Rewrite analyze_function block-build
from std::set boundaries to a sorted std::vector + binary_search.

main_psx CLI: --seeds, --out-dir, --strict, --inspect; out_dir support.
Add psxrecomp-game executable target alongside PSXRecomp.

code_generator: reg_name() now emits cpu->gpr[N] consistently (matching
what the rest of the codegen already emitted — the old reg_names[] table
returning cpu->v0/cpu->ra was unreachable because CPUState has no named
fields, only gpr[32]). LWL/LWR/SWL/SWR moved from cpu->lwl(...) member
form to psx_lwl(cpu, ...) free-function form for game-EXE codegen reuse.

Remove a stray fmt::print left in build_basic_blocks that spammed
stdout on every regen — per the no-stdio-debug rule (memory v4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:50:01 -07:00
Matthew Stanley 1c189530ba Add reusable runtime target plumbing 2026-05-10 15:54:15 -07:00