69 Commits

Author SHA1 Message Date
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 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 3ef8b72791 Fix BIOS delete modal delay-slot dispatch 2026-05-08 23:08:29 -07:00
Matthew Stanley 122dc49f42 Fix BIOS memory card copy writes 2026-05-08 20:50:31 -07:00
Matthew Stanley 97f30d2a76 Preserve PSX memory-card debug state 2026-05-07 17:57:34 -07:00
Matthew Stanley a9ab4d914d recompiler: seed 50 missing trampolines + extend shell relocation bound
Codegen audit (tools/_codegen_audit.py) revealed two related dispatch-miss
classes after the predicate-init fix.

(1) Discovery missed the A0/B0/C0 syscall trampolines at BFC428D0-BFC42C00.
    These are 8-16 byte stubs of the form
        li t2, 0xa0/b0/c0
        jr t2
        _li t1, FUN_ID
    that wrap kernel syscall dispatch. Ghidra had no xrefs to them (callers
    reach them via patterns auto-analysis didn't trace), so they never made
    it into ghidra_function_starts.json and from there into seeds. The
    recompiler decoded JALs targeting them (visible as `psx_dispatch(cpu,
    0xB005A8F0u)` etc. in regen output) but with no dispatch table entry,
    these calls hit psx_unknown_dispatch and silently no-op'd.

    For the YES/NO confirm modal, this likely manifested as the modal's
    CROSS handler calling e.g. TestEvent (B0:0xB at BFC42A00) and getting
    no result, leaving the modal in indeterminate state.

    Added 50 explicit seeds for BFC428D0..BFC42C00 (trampolines) plus four
    small kernel/libgte stubs (BFC304AC, BFC30584, BFC33C90, BFC33D1C —
    `jr ra; _li v0, X` return-constant helpers) which were also dispatch
    misses via tail-call paths.

(2) full_function_emitter.cpp's relocate_j_target/relocate_ra clamped the
    shell relocation range at phys 0x1FC427FF, but the runtime dispatcher's
    normalize() (SCPH1001_dispatch.c) handles shell up to phys 0x1FC42FFF.
    This left JALs in BFC42800-BFC42FFF un-relocated — the JAL target kept
    ROM PC[31:28]=0xB instead of being remapped to RAM PC[31:28]=0x8.
    Concrete example: `jal 0xB005A910` at BFC4282C, where the intended
    runtime target 0x8005A910 (a trampoline at RAM offset 0x2A910 = ROM
    BFC42910) became 0xB005A910 (KSEG1 mirror of RAM 0x005A910 — random
    address far outside any dispatch-table entry).

    Extended bound to 0x1FC42FFF in both lambdas to match the runtime.

Audit results: direct dispatch misses 47 -> 0, tail-call misses 5 -> 1.
Remaining 1 is RAM 0x00000CF0 (dynamic install stub, handled by
dirty_ram_dispatch per Rule 18).

Awaiting user repro to confirm YES/NO modal action handlers now fire.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:56:32 -07:00
Matthew Stanley e54e63131c recompiler: fix chained-branch delay-slot predicate-init UB
When a `beq+delay-slot` terminator's delay-slot address is also a basic-block
label (target of an external `b BFCxxxx` from a sibling branch), the previously
emitted C placed the variable declaration BEFORE the next block label and the
dangling if-goto AFTER it:

  label_BFC2185C:
      capture brA, brB
      int psx_taken_BFC2185C = (brA == brB);     // declaration
  label_BFC21860:                                 // goto target
      cpu->gpr[1] = ...;                          // delay-slot c_code
      if (psx_taken_BFC2185C) goto label_BFC21800; // ← dangling

When control reaches `label_BFC21860` via a goto from elsewhere (e.g., the
COPY branch ends with `b 0xbfc21860`), C semantics make `psx_taken_BFC2185C`
indeterminate because its declaration with initializer was skipped. On MinGW,
stack garbage is non-zero → the if-goto fires unconditionally → spurious
cross-branch jump → infinite loop in DELETE branch (BFC21800).

Concretely this caused the memcard COPY confirm-modal freeze: pressing CROSS
on a save-block target should push widget 0xA modal, but the COPY branch
would push 0xA correctly, then on `b 0xbfc21860` cleanup the dangling
if-goto would fire spuriously and jump into the DELETE branch, which keeps
re-pushing widget 0xB and reverting state.

Fix: in the first pass, collect every branch terminator address and emit
`int psx_taken_BFCxxxx = 0;` at function entry. Change the terminator-emit
from declaration to assignment. Now external gotos that bypass the
assignment see the zero default and the dangling if-goto correctly does
not fire.

Regenerated `generated/SCPH1001_full.c`: 6083 zero-init declarations,
all `int psx_taken_*` at terminator sites converted to assignments.

User-verified: COPY confirm-modal now appears. (YES/NO button/close
behavior still broken — likely additional bugs in the same class or
adjacent codegen edge cases; tracked separately.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:28:26 -07:00
Matthew Stanley 77e66f9da3 phase1.0e-d: per-block guest cycle accounting in codegen
Codegen-side foundation for cycle-driven peripheral timing per
docs/CYCLE_TIMING_ARCH.md. Each emitted basic block now calls
psx_advance_cycles(N) where N is the conservative 1-cycle-per-MIPS-
instruction count for the block (sum from leader to next leader,
including delay slot).

Generic per-instruction emit, no card-specific paths. Macro-gated by
PSX_ENABLE_BLOCK_CYCLES so generated output is comparable on/off.

This slice does NOT wire psx_advance_cycles to peripheral advance.
Reason: sio_tick has a legacy IRQ-countdown body that decrements
unconditionally per call; calling it from psx_advance_cycles (which
fires per block, ~100M+ times/sec) drains that countdown in
microseconds and breaks pad timing. A future slice with a peripheral-
only sio_advance (no legacy side effect) will wire SIO. Card byte-15
abort persists for now — expected; the fix needs that peripheral wiring.

Files:
- recompiler/src/full_function_emitter.cpp:
  - precompute block_cycles map (instr count between leaders)
  - emit psx_advance_cycles(<N>u) under #ifdef PSX_ENABLE_BLOCK_CYCLES
    at every block leader, before psx_check_interrupts
  - emit extern decl in generated header
- generated/SCPH1001_full.c: regenerated; 12794 PSX_ENABLE_BLOCK_CYCLES
  guards, one per block leader
- runtime/CMakeLists.txt: PSX_ENABLE_BLOCK_CYCLES=1 added to both
  psx-runtime and psx-beetleoracle target_compile_definitions
- runtime/src/psx_cycles.c: psx_advance_cycles accumulates counter only;
  peripheral dispatch deferred to next slice (rationale in comment)

Validation against 5d4d4bd baseline:
- Build clean both targets
- t=20s tx_writes=928, t=30s tx_writes=5000 (matches baseline)
- pad arm audit 5056/5056 armed, no_ack=0
- 4x CROSS press all succeed
- card txn distribution unchanged: txns reach READ_DATA, "MC" bytes
  flow at txn 0 bytes 10-11 (0x4D 0x43)
- psx_cycle_count grows: 1.72B (t=20s), 2.33B (t=30s), 10.04B
  post-CROSS — proves codegen emit live and per-block accounting works
- Macro=0 build (verified before macro flip): output identical to
  baseline

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 20:03:07 -07:00
Matthew Stanley 5b3c7dd08d phase4: freeze_check diagnostic + accumulated overnight infra
Today's session work
- runtime/src/debug_server.c: freeze_check TCP cmd snapshots all liveness
  counters (PC, exception entries, exception_reentry_blocks, dirty_ram
  blocks/insns, fn_entry total + top-K histogram, SIO IRQ pending +
  countdown, sio_stat/i_stat/i_mask, mc_max_state) for tight-loop
  diagnosis when the runtime hangs
- runtime/src/debug_server.c: wtrace slot 12 covering [0x72F0..0x72F4]
  (data-byte counter); measured peak=1 confirming setup_data_phase resets
  the counter mid-cycle
- runtime/src/interrupts.c: exception_entries_total + exception_reentry_blocks
  counters + psx_get_freeze_diag accessor
- runtime/src/sio.c: sio_get_freeze_diag accessor for IRQ pacing internals
- memory entries: phase4_card_gate_setup_writes_2026_04_28 (refutes the
  prior \"missing [0x7568+slot] clearer\" framing — setup functions write
  LSB=0 directly), phase4_72f0_one_byte_per_vblank_2026_04_28 (peak=1
  measurement)

Overnight session work bundled in
- runtime/src/dirty_ram_interp.{c,h}: small MIPS interpreter for
  install-at-runtime kernel RAM stubs (Rule 18; e.g., SIO data-byte
  handler the BIOS writes to RAM 0xCF0)
- recompiler/src/full_function_emitter.{cpp,h}: 3057 jal/jalr continuation
  return labels added to dispatch table (continuation fix); branch
  fall-through fix for conditional-branch delay slots
- recompiler/src/strict_translator.cpp: matching translator updates
- generated/: regenerated SCPH1001 dispatch + full C output reflecting
  the recompiler changes
- runtime/include/debug_server.h, src/memory.c: card_txn ring + SIO IRQ
  ring + EvCB walk ring + per-PC dirty_ram counters + always-on wtrace
  coverage for [0x755A], [0x7514], [0x7528], [0x7520], [0x74A4],
  [0x75C0..C8], [0x74B8..C0]
- recompiler/tests/, runtime/tests/: SIO/card protocol unit tests
- tools/: 60+ Python diagnostic scripts (chain_trace analysis, SIO trace
  diff, EvCB walker, wtrace dumpers, etc.)
- docs/: psx_bios_disasm reference + dynamic_handler_install rationale
- OVERNIGHT_2026_04_28.md, SUMMARY_20260412_2.md: session logs

Phase 4 status (still blocked, root cause better understood)
- BIOS card chain runs through R-handlers correctly (chain_trace shows
  R1..R11 cycling 26x); shell IS retriggering _card_read
- Install handler at ROM 0xBFC15F1C increments [0x72F0] correctly per
  SIO IRQ; gate at <0x80 / <0x7F is correct
- Bottleneck: SIO IRQ countdown is access-paced, fires ~1× per VBlank
  vs ~128× needed; setup_data_phase (called from outer coord on VBlank)
  zeroes [0x72F0] before counter reaches 128
- Both attempted fixes today caused freezes:
  * VBlank deferral on [0x75C0]==1 — pending VBlank already in i_stat
    delivered anyway; tighter dispatch gate also locked up
  * Cycle-paced sio_tick at rates 50, 200 (and gated on [0x75C0]==1) —
    runtime freezes once data phase activates, root cause unidentified
- delay=5 narrows the threshold (peak=2 vs peak=1, no freeze) but
  doesn't get to 0x80
- Real fix likely requires deeper instrumentation of the freeze (call
  stack / dispatch state at the moment of TCP starvation)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 21:29:19 -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 66c9f21f83 feat: MTC2/MFC2 special register handling + screenshot_file command
MTC2 to registers 15 (SXYP FIFO push), 28 (IRGB decompose), and 30
(LZCS leading-zero count) now routes through gte_write_data() for
correct side effects. MFC2 from registers 28/29 (IRGB/ORGB packed
read) routes through gte_read_data(). All other GTE register
transfers remain plain assignments for performance.

Added screenshot_file debug command: writes psx_screenshot.bmp to
disk (640x478 24-bit BMP), convertible to PNG for visual inspection.

Verified: BIOS shell renders correctly — main menu with Memory Card
and CD Player options, Memory Card management screen with CARD 1/2
labels, EXIT/COPY/COPY ALL/DELETE menu. Missing: 3D card slot
graphics (requires GTE 3D transform pipeline investigation).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 14:00:16 -07:00
Matthew Stanley 80a27545dc feat: COP2/GTE instruction support in strict translator
The strict translator rejected opcode 0x12 (COP2) as "not implemented",
causing all GTE functions to be skipped during emission. The code
generator already handled COP2 — only the strict translator was missing.

Added to strict_translator.cpp:
- MFC2/CFC2: move from GTE data/control registers
- MTC2/CTC2: move to GTE data/control registers
- GTE commands (bit 25 set): route through gte_execute()
- LWC2 (opcode 0x32): load word to GTE data register
- SWC2 (opcode 0x3A): store word from GTE data register

Added gte_execute extern declaration to full_function_emitter.cpp
for both dispatch and full file headers.

Result: 0 dispatch misses (was 5 COP2/GTE + cascading CARD driver).
Emitted functions: 1085 (was 1056). Unsupported instructions: 4 (was 34).
Only 2 skipped functions remain (COP1/FPU + SUB opcode).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 12:46:58 -07:00
Matthew Stanley 1a0213ec5e fix: RestoreState escape mechanism — VSync counter now increments
The kernel's inline RestoreState (at BFC1094C inside the exception
handler func_00000C80) restored all registers and did `jr $ra`, but
the generated `return;` went back to psx_check_interrupts instead of
dispatching to the restored $ra (0xBFC421D8 — the VSync callback
loop). Then psx_check_interrupts overwrote all GPRs from the
pre-exception save, wiping RestoreState's changes. VSync counter
was permanently stuck at 0.

Fix has three parts:

1. runtime/src/interrupts.c: Add psx_restore_state_escape() which
   longjmps with code 2 (vs code 1 for ReturnFromException).
   psx_check_interrupts now loops: code 2 re-dispatches to cpu->pc
   (still in exception context), code 1 exits, code 0 is normal.

2. recompiler full_function_emitter: Broaden longjmp-return detection
   from first-instruction-only to scanning ALL instructions for the
   exact encoding 0x8C9F0000 (lw $ra, 0($a0)). This catches both
   standalone RestoreState (func_1FC0227C) and the kernel inline
   copy (func_00000C80 at BFC10964).

3. Generated jr $ra terminators in matched functions now emit:
   cpu->pc = cpu->gpr[31]; psx_restore_state_escape(); return;

Result: VSync counter increments steadily (~256/vblank), zero crashes,
shell renders Memory Card / CD Player screen with no ADEL faults.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 23:23:25 -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 b3833aa253 fix: exception handler chain walk + shell reaches Memory Card/CD-ROM screen
Root cause: the recompiler split the kernel exception handler at the
jalr $s1 (chain handler dispatch) into func_00000C80 and func_00000E10.
The chain-walker continuation at BFC10910 never executed — only the
first handler in chain 0 was ever called.  I_STAT bits 0 (VBlank) and
2 (CDROM) were never acknowledged, causing an interrupt livelock.

Fixes:
- Emitter jalr/jal safety-net: when the return address falls outside
  the current function, emit a continuation psx_dispatch call after
  the jalr dispatch.  This bridges split functions at indirect calls.
- Merged exception handler: removed BFC10910 seed so the chain walker
  continuation is part of func_00000C80 (C fall-through after jalr).
- 0xBFC10910 dispatch special case in psx_unknown_dispatch: chain
  handlers return to $ra=BFC10910 via jr $ra (compiled as `return;`);
  the dispatch loop picks up the $ra value — handled as a no-op since
  the continuation already ran via C fall-through.
- Post-exception adaptive cooldown: if the handler didn't clear any
  I_STAT bits, give main code 500 blocks before re-firing.  Prevents
  VBlank livelock when no handler claims the interrupt.
- SIO byte write handler in mmio_write8 (was missing, caused crash).
- GPU→RAM DMA ch2 stub: fills target with zeros, completes transfer.
- Dispatch trace (debug_server_trace_dispatch) added to emitter
  template so it survives regeneration.

Result: shell device detection completes, StartPad dispatched, chain
E014 populated, GPU actively rendering the Memory Card / CD-ROM
screen.  VSync counter still 0 (handler installed but not yet
incrementing — next investigation).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 18:13:51 -07:00
Matthew Stanley 8e81afd111 fix: conditional branch fall-through + Tier 1 reverse debugger
Recompiler bug fix: full_function_emitter treated conditional branch
delay slots at function boundaries as fully resolved control flow,
suppressing the fall-through tail call for the "not taken" path.
This broke 15 cross-function fall-throughs across the BIOS, most
critically the find-free-EvCB-slot function (BFC11800) whose search
loop was silently skipped — causing OpenEvent to return garbage handles
(0xF100E1E8) instead of valid slot indices (0xF1000000..0xF1000005).

Fix: only suppress fall-through for unconditional flow (j/jr/jal/jalr/rfe).
Conditional branches (beq/bne/blez/bgtz/bltz/bgez) now always emit
`cpu->pc = next_addr; return;` for the not-taken path.

Also included:
- Tier 1 reverse debugger: 1M-entry write trace ring (32 MB),
  64K-entry MMIO trace ring (2 MB), 8-slot multi-range address
  filtering, function attribution via g_debug_current_func_addr.
  6 new TCP commands: wtrace_add/del/ranges/stats, mmio_dump/clear.
- 3 new dispatch seeds for functions exposed by fall-through fix
  (SIO epilogue, exception chain walker, GTE init).
- Diagnostic tools: analyze_sio_trace, decode_insn, decode_jt, mmio_tally.
- Pattern 5 trampoline fix + PLAN.md Phase 4 status update.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 14:23:09 -07:00
Matthew Stanley 6e2926c52e fix: resolve MIPS jump tables with RAM-to-ROM address translation
The BIOS shell code (ROM 0xBFC18000+) runs from RAM at 0x80030000+.
Jump tables in shell functions store RAM-space target addresses, but
at recompile time only the ROM data is available.

Three fixes:
1. function_discovery.cpp: detect jump table pattern (SLTIU/SLL/LUI/
   ADDU/LW/JR) during function scanning, translate RAM table addresses
   to ROM, push case targets onto BFS work queue so case body code is
   included in the function's instruction set.
2. full_function_emitter.cpp: detect same pattern at jr emission time,
   read table from ROM with RAM-to-ROM mapping, emit C switch/goto
   instead of cpu->pc=reg;return for resolved tables.
3. code_generator.cpp: add ram_to_rom() helper for the existing jump
   table detection (used by a different code path).

Result: 13 jump tables resolved across the BIOS, +6 functions discovered,
+2825 instructions included. Dispatch miss at 0x80058564 (memcard SIO
handler) eliminated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:40:46 -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