Commit Graph

14 Commits

Author SHA1 Message Date
Matthew Stanley 696ffbd1f6 Persist BIOS memory-card copies + dev/prod memcard layout
The COPY/DELETE flows were silently no-oping the SIO write protocol:
60 dispatches per copy were missing the 3-instruction predicate fn at
0xBFC0989C (the WRITE-command sibling of 0xBFC09890), so the BIOS's
write-handler fork test returned junk via uninitialized v0 and the
BIOS skipped sending 0x57 over SIO entirely. The in-memory card cache
appeared updated (visual UI worked, leave/re-enter showed copies) but
nothing reached memcard_write_sector or the .mcd files.

Also reorganize memcard directory resolution: production builds put
cards next to the exe; dev builds (CMake build tree detected) keep
them in the project root next to bios/ so a runtime rebuild doesn't
delete them. --memcard-dir still overrides.
2026-05-09 13:15:33 -07:00
Matthew Stanley 9b332ba5be Persist memcards and add SPU audio output 2026-05-09 06:32:22 -07:00
Matthew Stanley cf87fd8fc6 crash_trace: structured post-mortem dump (mirrors SMW recomp pattern)
New runtime/src/crash_trace.c writes psx_last_run_report.json with:
  reason / timestamp / frame / last_func_addr / last_store_pc
  full CPU register state (all 32 GPRs + hi/lo/sr/cause/epc)
  dispatch_ring tail (last 256 dispatched func entries)
  unknown_dispatch tail (last 50 entries with addr/phys/ra/a0/a1)
  dirty_block_log tail (last 100 entries)
  Windows SEH info (when triggered from SEH filter)

Triggered from:
  - SIGSEGV / SIGABRT signal handlers
  - Windows SetUnhandledExceptionFilter (catches access violations)
  - atexit
  - psx_unknown_dispatch fail-fast path (set PSX_FAIL_FAST_UNKNOWN_DISPATCH=1)
  - trap_crash (existing fatal trap path)

File overwrites per dump (last-write-wins). Per CLAUDE.md §3 this is not
a log — it's a one-shot final state snapshot for crashes the running
TCP server cannot intercept.

Pattern mirrors F:/Projects/SuperMarioWorldRecomp/src/post_mortem.c.

Used the new infrastructure to capture a controlled crash with
0xBFC0989C re-seeded. Crash dump revealed the chain:
  - JAL at 0xBFC09788 is jalr $s3, NOT jalr $a0
  - $s3 was set in func_1FC09598 prologue: $s3 = $a2 (callee saves arg)
  - Caller at 0xBFC093C4 in func_1FC092EC: $a2 = $s3 - 128 (delay slot)
  - func_1FC092EC at 0xBFC09314: $s3 = $a3 (callee saves arg)
  So real bug is upstream-of-upstream caller passing $a3 = 131 (0x83)
  to func_1FC092EC, propagating down to a misaligned function pointer.
  The 0xA000E1E8 we suspected as a function-pointer table was a
  red herring — $a0 in the loop body is loaded from $fp+s1*4 (the
  ARG to pass to the called fn), not the call target.

0xBFC0989C reverted out of seed list (was diagnostic-only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 23:45:33 -07:00
Matthew Stanley 29ebe255bf recomp: safe-state seeds (02918+09890) + turbo mode + Beetle-sourced card2.mcd
Outcomes:
  - YES/NO COPY confirm modal closes correctly on CROSS press (returns to
    COPY menu — matches Beetle behavior)
  - No crash, no freeze
  - Memory card 2 visible with a known-good save (sourced from Beetle's
    dummy.1.mcr; backup of prior card2.mcd kept as .bak.before_beetle_copy)
  - TAB held = turbo mode (skips VRAM->ARGB conversion + SDL_RenderPresent
    every frame except every 30th); useful for navigation cycles

Remaining bug:
  - The actual save data is NOT being copied. Source card content is not
    written to destination. card1.mcd hash unchanged after a copy attempt.
  - 0xBFC0989C still hits psx_unknown_dispatch 60x per copy. Bisect
    confirmed seeding 0xBFC0989C alone causes a misaligned-dispatch crash
    from caller 0xBFC09790 (function-pointer table at runtime addr
    0xA000E1E8 indexed by predicate result, gives garbage target 0x3).
    Seeding both 0xBFC09890 + 0xBFC0989C causes a softlock instead of crash.
    Therefore 0xBFC0989C is intentionally NOT seeded; the copy data path
    short-circuits because the predicate's stale-v0 result fails to enter
    the actual write code.

Status of the function-discovery-miss bug class:
  - VALIDATED for 0xBFC1A1BC (commit 6889f5c) — bandaid worked, modal
    closes
  - PARTIALLY VALIDATED for the BFC09xxx area — adding 0xBFC09890 alone is
    safe but doesn't unblock the copy. 0xBFC0989C cannot be safely seeded
    because it's likely a mid-function label, not a function entry. The
    proper fix requires recompiling the full function containing 0xBFC09790
    (the caller of these predicates) and ensuring the function-pointer
    table at runtime address 0xA000E1E8 is populated with valid recompiled
    function targets.

Turbo mode: hold TAB on the SDL window. Suppresses 29/30 frame presents,
keeps simulated CPU advancing at full speed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:52:55 -07:00
Matthew Stanley 7a12a0baac two-process refactor: replace embedded oracle with independent psx-runtime + psx-beetle
Switches diagnostic methodology from a single binary running both backends
in lockstep (psx-beetleoracle.exe) to two independent processes:
  psx-runtime.exe (recomp only, port 4370)
  psx-beetle.exe  (Beetle libretro only, port 4380)

Both expose the same JSON wire protocol for the common debug commands
(read_ram, press, set_input, clear_input, pad_status, screenshot_file,
wtrace_*, fntrace_* on Beetle). Cross-process comparison is done by
querying both ports from a tool, NOT by sharing input or memory.

Why: the embedded-oracle setup required keyboard input to drive both
backends in lockstep. Once their state diverged, the same keypress
drove them to different screens and "compare on the same press" became
unreliable. Two independent processes lets each backend be navigated
to its own state and queried on its own timeline.

Removed:
  - runtime/src/beetle_psx_bridge.cpp (cannibalised into beetle_libretro.cpp)
  - runtime/src/duckstation_bridge.cpp
  - runtime/src/psx_oracle_cmds.c
  - runtime/src/psx_oracle_stub.c
  - runtime/include/psx_oracle_backend.h
  - psx-beetleoracle and psx-dsoracle cmake targets
  - 22 dual-backend tools (emu_read_ram / find_first_divergence / etc)

Added:
  - runtime/src/beetle_main.cpp     (SDL window + libretro driver + keyboard)
  - runtime/src/beetle_libretro.cpp (libretro callbacks + wtrace/fntrace/sio rings)
  - runtime/src/beetle_debug_server.c (TCP server, port 4380, same wire protocol)
  - psx-beetle cmake target

Also bumps tooling caps surfaced during modal investigation:
  - DIRTY_RAM_BLOCK_LOG_CAP: 16K -> 4M (28ms eviction -> ~7s retention)
  - WTRACE_MAX_RANGES:        16  -> 64  (boot defaults filled the prior cap)
  - DirtyRamBlockLogEntry: added a0/a1 fields for richer dispatch context

CLAUDE.md section 16 rewritten for the new methodology. New memory note
locks the direction so future sessions don't reach for the deleted
embedded-oracle commands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:06:44 -07:00
Matthew Stanley 7c9ea5f397 debug: card txn ring + SIO IRQ ring + EvCB walk ring + diff/nav tools
Three new always-on diagnostic rings, plus tools to drive Beetle in
parallel and toggle the SDL window between ours and Beetle for visual
validation. Built to support deterministic root-cause analysis of the
memcard directory-load failure.

Card transaction ring (sio.c, sio.h): one entry per 0x81-rooted card
protocol run with full TX/RX bytes, cmd, sector, ack count, terminal
state, end_reason classification (success/abort_reselect/abort_reset/
abort_slot/abort_bad_cmd/abort_other). 4096-entry cap. Surfaced via
card_txn_dump TCP cmd. Live (in-progress) txn included separately.

SIO IRQ #7 delivery ring (sio.c, sio.h): every IRQ #7 fire with byte
seq linkage, mc_state at fire, active_device, ctrl, counter_7514,
i_stat before/after, source classification (card/pad), slot, applied
delay. 4096-entry cap. Surfaced via sio_irq_dump.

EvCB walk ring (debug_server.c): captures full kernel EvCB table
(class/status/spec/mode/fhandler) on every DeliverEvent entry. Reads
ToT layout per nocash: 0x120=ptr, 0x124=size. Exit detection tracked
via direct RA-watch (shadow stack unreliable due to
psx_restore_state_escape longjmp). Surfaced via evcb_snapshot,
evcb_walk_dump, evcb_walk_stats.

card_txn_diff (psx_oracle_cmds.c): aligns our structured txn ring
against Beetle's 64K SIO ring split into transactions, finds first
byte-level divergence, returns context window. SIO_FFD_MAX_BYTES
bumped 16K -> 64K so historical Beetle reads remain in scope.

emu_press (psx_oracle_cmds.c, beetle_psx_bridge.cpp): drives Beetle's
pad input for navigation parity with our `press` cmd. 180-frame cycles
verified to navigate Beetle to MC screen.

F7 toggle (main.cpp): swaps SDL window display source between ours
and Beetle's framebuffer for visual A/B comparison without restart.

Hygiene: 162 stale probe artifacts moved into probes/. .gitignore
extended for probes/ + root-level *.json/*.png/*.bmp/*.txt scratch.
Runtime data (card*.mcd, dummy.*.mcr) intentionally not covered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:04:37 -07:00
Matthew Stanley 82aeca98a7 feat: embed Beetle PSX libretro core as golden oracle for RAM comparison
Adds a new psx-beetleoracle build target that embeds beetle-psx-libretro
(mednafen PSX core) alongside the recompiled BIOS. Both execute the same
SCPH1001.BIN and advance in lock-step, enabling byte-level RAM comparison
via the TCP debug server.

New infrastructure:
- psx_oracle_backend.h: abstract oracle backend interface (init, run_frame,
  get_ram, get_vram, get_scratchpad, read_byte/word, get_cpu_regs, sync_to_state)
- beetle_psx_bridge.cpp: libretro bridge with CPU register access via
  PSX_CPU->GetRegister(), VRAM via GPU_get_vram(), scratchpad via ScratchRAM
- psx_oracle_cmds.c: TCP commands — find_first_divergence, emu_read_ram,
  emu_read_vram, emu_read_scratchpad, emu_cpu_regs, emu_step, emu_sync,
  emu_sync_press
- psx_oracle_stub.c: no-op stubs for non-oracle builds
- tools/oracle_diff.py: standalone Python RAM comparison tool
- tools/build_oracle.sh: build script for beetle-psx + oracle target

Also includes: SIO card probe diagnostic counters (mc_probes, mc_acks,
mc_cmds, tx_writes), enhanced dispatch miss logging with register context,
memory_get_ram_ptr() for oracle RAM comparison.

Build: cd beetle-psx && make platform=mingw_x86_64 STATIC_LINKING=1 HAVE_LIGHTREC=0
       cd runtime/build && cmake --build . --target psx-beetleoracle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:15:01 -07:00
Matthew Stanley 53ca0d54d1 fix: relocate $ra to RAM addresses for shell/kernel code regions
The recompiler set $ra to ROM addresses (0xBFC18xxx) for jal/jalr in
shell-region functions. On real hardware, $ra uses RAM addresses
(0x80030xxx) because the shell runs from RAM. This caused handler/
callback tables to contain ROM addresses instead of RAM, breaking
pointer comparisons that gate the BIOS shell's 3D rendering pipeline.

Also adds:
- tools/find_divergence.py: golden-oracle RAM comparison tool that
  diffs recomp (port 4370) vs DuckStation (port 4371) byte-by-byte
  using persistent TCP connections
- gte_exec counter in oracle VSync output for interpreter validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 17:04:26 -07:00
Matthew Stanley 70d39c475e fix: oracle JR/JALR delay slots + RestoreState longjmp-return + VSync mechanism seeds
Oracle interpreter:
- JR and JALR were missing delay slot execution (exec_one recursive call),
  causing the entire boot sequence to malfunction — C0 table never populated,
  oracle stuck at ~65K instructions. Now boots to shell and VSync incrementer
  hit 272 times in 100M instructions (counter = 271).
- Added NULL-PC halt trap (jr to address 0 = corrupt function pointer).
- Reworked oracle main loop: counts VSync hits, steps past breakpoints,
  dumps trace ring correctly (ring-relative indices), reads VSync counter
  and init flags at end of run.

Code generator (longjmp-return):
- RestoreState (A0:0x14) loads $ra from a save buffer via $a0, so jr $ra
  must set cpu->pc for the dispatch loop to tail-call to the restored
  address. Detected by: first instruction is `lw $ra, 0($a0)` (0x8C9F0000).
- Applied in both code_generator.cpp (block-based) and full_function_emitter.cpp
  (linear emitter used by psxrecomp-bios.exe).

Dispatch seeds (+5 new, 832→837):
- 0xBFC421D8: RestoreState longjmp target (mid-function in func_1FC42160)
- 0xBFC4231C: SaveState v0=0 init body (callback table setup)
- 0xBFC42370: func_1FC42160 epilogue (init flag gate branch target)
- 0xBFC42910: post-callback cleanup (was dispatch miss)
- 0xBFC42BF0: shell_register_callback (callback table was empty without it)

Runtime survivability:
- psx_unaligned_access: survivable ADEL with diagnostic dump (was exit(1))
- DMA ch2 linked-list: break on cycle instead of exit(1)
- NULL dispatch (addr=0): silently absorb in psx_unknown_dispatch

Regenerated BIOS: 1056 dispatch entries (was 1051).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 00:21:18 -07:00
Matthew Stanley 08ef235e63 fix: oracle interpreter — undefined opcodes as NOP + interrupt injection + VBlank tick
- Undefined SPECIAL/opcode encodings treated as NOP (R3000 doesn't trap)
- Added VBlank scheduling + interrupt injection to interpreter loop
  (fires exception at SR/I_STAT/I_MASK conditions, same as interrupts.c)
- Extended memory control register range to 0x1F80103C
- Added boot trace + PC breakpoint diagnostics

Found: BIOS C0-table lookup returns NULL function ptr at instruction
~65K → jr $t0 where $t0=0 → NOP slide through zero RAM → stuck.
The interpreter needs to handle this gracefully to boot past kernel
init. DuckStation doesn't have this issue because its C0 table is
populated by the time the call happens.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 22:52:31 -07:00
Matthew Stanley e1dae7fcbd feat: native PSX oracle — R3000A interpreter + dual CMake targets
Adds psx-oracle.exe alongside psx-runtime.exe, following the Genesis
recomp hybrid pattern (two targets from shared sources):

- psx-runtime: recompiled BIOS (stub_interpreter.c), port 4370
- psx-oracle:  R3000A interpreter (psx_interpreter.c), port 4371

The interpreter implements the full MIPS I instruction set:
  ALU, shifts, mult/div, branches, jumps, loads/stores (incl LWL/LWR),
  COP0 (MFC0/MTC0/RFE), COP2/GTE (delegated to gte.cpp), SYSCALL/BREAK.

Includes 1M-entry per-instruction trace ring and 64 PC breakpoints —
the tooling gap that blocked VSync counter diagnosis (DuckStation's
TCP debug server only had 2-3 breakpoint slots, no PC breakpoints).

Also extends memory control register range to 0x1F80103C (was 0x1F801020)
for the interpreter's early BIOS init path.

Verified: oracle boots from BFC00000 and correctly executes the first
20+ BIOS instructions. Hits unknown SPECIAL func at PC=0x68 later in
boot — instruction coverage issue to debug next session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 21:34:28 -07:00
Matthew Stanley a16e3a4408 Add CDROM controller and SPU register sink, wire into MMIO
CDROM (adapted from v3, 487 lines):
- Removed event_deliver() — HLE disease. IRQ delivery is now purely
  via i_stat bit 2; the recompiled BIOS exception handler processes
  CDROM events natively through its own EvCB table.
- Removed CPUState* from all function signatures — hardware registers
  don't need CPU state.
- Removed hard-coded RAM writes (0x800DEE54 etc.) — BIOS kernel state
  manipulation disguised as hardware sim.
- Removed all fprintf.
- Kept: register banking, command processing, sector reads, DMA interface.

SPU (new, 89 lines, register map from v2 reference):
- Accepts all SPU register reads/writes at 0x1F801C00-0x1F801FFF
- 512KB SPU RAM with transfer address tracking and FIFO writes
- DMA4 write interface for bulk transfers
- SPUSTAT returns 0 (ready, not busy)
- No audio synthesis — placeholder for SDL_audio integration later

Memory.c: replaced inline spu_regs[] array and stub CDROM handlers
with calls to the proper cdrom.c and spu.c modules.

Validated: BIOS boots identically (16,859 GP0 writes, 1,149 draws).
No regression from previous commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:20:12 -07:00
Matthew Stanley fba58d76b3 Phase 4 interrupt fixes: RFE, GPR save/restore, DMA IRQ edge-trigger
Three interrupt delivery bugs fixed that were blocking shell progression:

1. RFE after handler exit: The exception handler's normal exit path
   (jmpbuf longjmp at 0xBFC10944) does not execute RFE. In our model
   the handler returns to psx_check_interrupts, not to the WaitEvent
   caller. Without RFE, IEc stays 0 forever after the first interrupt.
   Fix: apply RFE in psx_check_interrupts if IEc is still 0 after the
   handler returns.

2. GPR save/restore around handler dispatch: The handler's jmpbuf exit
   restores callee-saved registers from the kernel jmpbuf (intended for
   a non-local jump to WaitEvent's caller). In our model this corrupts
   the interrupted code's registers. Fix: save all GPRs before handler
   dispatch and restore them after, matching real hardware where the
   interrupted code's registers are preserved via TCB save/restore.

3. VBlank tick gated on !in_exception: The handler runs thousands of
   block leaders, each calling psx_check_interrupts. Without gating,
   dispatch_count advances inside the handler, firing new VBlanks that
   stack up and starve the main code. Fix: only increment dispatch_count
   and fire VBlank/timer ticks when not in exception handling.

4. DMA IRQ edge-triggered: update_master_irq() was level-triggered,
   re-asserting i_stat bit 3 on every call while master_flag=1. Combined
   with no BIOS callback to acknowledge DMA IRQs, this caused an
   interrupt storm. Fix: edge-detect master_flag 0->1 transition, and
   suppress DMA completion IRQs entirely since transfers are synchronous.

Also adds GPU debug diagnostics: GP0 command counters (nop/fill/draw/
env/copy), draw area/offset in gpu_state, vram_peek debug command.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:29:03 -07:00
Matthew Stanley a1165d51b7 Phase 2 complete: BIOS boots to GPUSTAT read
Full recompilation pipeline working:
- Recompiler emits 799 functions (46k instructions) from SCPH1001.BIN
- Tail-call trampoline in dispatch loop prevents stack overflow
- Timer registers (3 counters, mode, target) implemented
- Interrupt check at labels + dispatch boundaries with vblank scheduling
- SYSCALL 1/2/3 (Enter/ExitCriticalSection, ReturnFromException) handled
- EPC correctly set from SYSCALL instruction address

BIOS boot reaches first GPU hardware access:
  UNIMPLEMENTED MMIO READ32 (GPUSTAT) @ 0x1F801814

Phase 3 (Sony logo) starts here: GPU, DMA, display pipeline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:03:46 -07:00