Commit Graph

24 Commits

Author SHA1 Message Date
Matthew Stanley 0d15e2bd4e phase1.0c-v2: cycle-paced sio_tick + dispatch quantum behind active-guard
Adds the cycle-paced sio_tick body, the sio_tick_quantum() wrapper, and
the pace_state TCP telemetry handler. Crucial change vs. failed 1.0c:
the dispatch-loop quantum tick is gated by g_sio_timing_active (volatile
int), so on the psx_check_interrupts hot path the cost is one load + one
branch when no SIO time is pending — not a full function call into the
cycle-paced loop.

In 1.0c-v2 the TX path is still synchronous and nothing arms shift_active
or pending_ack, so g_sio_timing_active stays 0. Phase 1.0d will set it
when arming the shifter and clear it when the shifter+buffer+ack are all
empty.

Files:
- sio.h: SIO_MODEL_CYCLE_PACED moved to header (default 1); declares
  g_sio_timing_active (extern volatile int) and sio_tick_quantum()
- sio.c: defines g_sio_timing_active=0; adds cycle-paced helpers
  (sio_consume_ack_event, sio_fire_ack_irq, sio_handle_shift_complete);
  sio_tick gains a cycle-paced loop that runs only if shift_active or
  pending_ack is set (currently never), then falls through to the legacy
  IRQ countdown body which remains the live IRQ delivery path; adds
  sio_get_pace_state(uint64_t out[16]) accessor
- interrupts.c: in psx_check_interrupts !in_exception block, calls
  sio_tick_quantum only if g_sio_timing_active is set
- debug_server.c: new pace_state TCP handler exposing the snapshot

Validation against 64e0194 baseline:
- t=20s/30s TCP responsive; pad arm audit 2840/2840 no_ack=0
- 4x CROSS succeeds
- card txn distribution unchanged (10 txns reach bytes=15 ts=10)
- txn 0 still reads "MC" magic at bytes 10-11 (0x4D 0x43)
- pace_state: timing_active=0, all dynamic fields 0 throughout

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 17:04:14 -07:00
Matthew Stanley 8b0fc34714 phase4: SELECT-deassert protocol reset + slot-state instrumentation
Real card protocol restarts on every fresh 0x81 — SELECT deassert is
a bus-wide reset signal. Earlier model preserved mc_state across
deassert via mc_save_slot, which caused every subsequent 0x81 to land
on a stale non-IDLE saved state and trigger abort_reselect bookkeeping.
Audit confirmed the BIOS-visible RX was still correct (the abort path
itself reset mc_state), but the retained sector/cmd/counter context
across logically-independent transactions was wrong per no$psx.

Clear volatile protocol state on SELECT deassert (state, cmd, sector,
sector_msb, sector_lsb, data_idx, checksum on both slots). Preserve
mc_slots[].flag (persistent "new card" metadata) and mc_slots[].data
(sector buffer, harmless to retain).

Result on a 4-press boot run: 6 transactions reach READ_DATA (ts=10)
and successfully read the "MC" header magic (RX bytes 0x4D 0x43);
remaining txns continue as 4-byte presence pings. Pre-fix: 0 reads
of card data ever; post-fix: card protocol can complete the setup
phase and enter data phase.

The byte-15 abort that follows is a separate blocker — kernel chain
dispatcher interleaves pad polling into the card data read. Tracked
for next session via the chain dispatcher disassembly.

Also includes:
- Slot-state capture in SioTraceEntry + sio_trace JSON emission
- dirty_ram_interp: replace mid-block fprintf+fflush with a counter
  (CLAUDE.md §3 — synchronous Windows console I/O at high rate
  starves the dispatch loop)
- Passive Python TCP probes (raw_tcp, trace_audit, trace_range,
  txn_summary, txn_dist, txn_bytes, drs_filter, readram_test,
  step_audit{,2}, verify_card_buffer, wtrace_filter)
- docs/MEMCARD_AUDIT_2026_04_28.md (prior session's 7-layer audit)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:46:26 -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 3971af1cc3 debug: pad_status TCP cmd + sio_card_protocol_active() accessor
pad_status reports both runtimes' current pad word side-by-side so
input dispatch symmetry can be validated empirically. Verified that
the existing `press` cmd already drives Beetle (via
psx_oracle_run_frame in main.cpp's per-frame loop); the bouncing-vs-
stable behavior the operator observed was downstream of input
(shell-state-machine pacing differences), not an input dispatch
asymmetry.

sio_card_protocol_active() accessor returns 1 if any slot has an
in-flight memcard protocol exchange. Building block for a future
fix to defer VBlank-driven pad polling while a card read is in
progress (root cause of the directory-load failure — see memory
phase4_real_root_cause_d3_to_d4 for the byte-level ours-vs-Beetle
divergence proof and the failed fix attempts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 16:38:59 -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 188bdc860d fn-trace: bump shadow stack depth 256 -> 4096
256 was too small under heavy fall-through dispatch (depth always
saturated at 256, exit tracking broken). 4096 keeps the stack
non-saturated for normal call patterns and allows fn_exit to
correctly pair with fn_entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:41:07 -07:00
Matthew Stanley 84180e2a31 debug: function ENTRY + EXIT trace rings with shadow call stack
Two always-on parallel ring buffers hooked off psx_dispatch via
debug_server_trace_dispatch:

- fn_entry: every dispatch in CALL ORDER with target, $ra, $a0..$a3,
  $t1 (B0/A0/C0 fn index when target is a BIOS vector), shadow-stack
  depth, frame
- fn_exit: every function in FINISH ORDER with $v0, $v1, plus
  entry_seq link back to its paired entry

Shadow call stack classifies each dispatch as:
- RETURN — target matches some open frame's saved RA → pop frames as
  exited, record exit values
- TAIL CALL — $ra unchanged from previous dispatch → replace top
  frame in place (keeps the stack bounded under heavy fall-through
  dispatch, which is how chain handlers tail-jump between states)
- NEW CALL — $ra changed (fresh JAL) → push new frame

When the 256-frame stack overflows, oldest frame is rotated out
(unbounded recursion / longjmp pathology) so tracking continues.

TCP commands:
- fn_filter {lo, hi}    set [lo, hi) phys addr filter
- fn_clear              reset both rings + shadow stack
- fn_stats              entry/exit totals, stack depth, overflows
- fn_entry_dump {count, seq_lo, seq_hi, addr_lo, addr_hi}
- fn_exit_dump  {count, seq_lo, seq_hi, addr_lo, addr_hi}

Validation: with filter set to BIOS card area 0x4000..0x6000, captured
state-1..state-11 each ran 4 times during one press-X cycle, all with
ra=0x4DDC (chain dispatcher's jalr return). Also revealed THREE
intermediate functions (0x59D0, 0x5A08, 0x5A48) called between state
addresses via tail-jump — visibility we did not have before.

Memory: 32 MB per ring × 2 = 64 MB heap-allocated at debug_server_init.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:33:37 -07:00
Matthew Stanley 8a694af69c debug: always-on wtrace coverage for card-chain state addrs
Per global rule: use ring buffers, never time/attach. Add static
always-on wtrace coverage at boot for the addresses that determine
card-chain progress, so we can answer "when was X written?" by
querying the ring instead of racing live samples.

Newly always-watched (range 5..9, total ranges 7→10, MAX 8→16):
- 0x7514..18: chain counter (shared by detection + read chains)
- 0x7528..30: per-slot chain handler ptr (=0x5688 read, =0x5B64 detect)
- 0x755A:     chain abort flag (D1/state-1 set, outer coord checks)
- 0x7520:     read-chain success flag (state-3 sets, cascade reads)
- 0x74A4:     chain status flag

This let us deterministically prove (without racing samples) that:
- read chain DOES get installed at 0x7528 briefly on press X (seq 12754)
- counter DOES advance 0→11 successfully during read-chain runs
- but resets at counter=11 via func_00005000's BFC14B90 abort path
- reset triggers because state-1 set 0x755A=1 and chain never
  reaches state-13 to clear it

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:00:26 -07:00
Matthew Stanley 3d364091e7 fix(memcard): clear FLAG byte after read — the four-day stall
This is the bug. Single line. Four days lost.

# The fuck-up

`runtime/src/sio.c` returned `mc_flag` (init 0x08) on every 0x52 (read
command) but NEVER cleared it. Only writes cleared the flag.

Per no$psx spec: real PSX cards return FLAG=0x08 ONCE — first read
after fresh-battery / power-cycle / write — then clear it to 0x00 for
all subsequent reads.

# Why this stalled directory loading

BIOS state-3 chain handler (func_0000579C / 0xBFC152DC) tests bit 3:

    andi $t1, $a0, 0x8
    beq  $t1, $0, BFC15650    # bit 3 NOT set: success (v0=0, advance)
                              # bit 3 SET:     "fresh card" (v0=-1)

Our card returned 0x08 forever, so EVERY read attempt landed in the
"fresh card" path: state-3 returned v0=-1, dispatcher's $v0 cascade
(BFC1499C) reset the chain counter, infinite loop. The directory
literally could not be read.

Beetle's card sim returns 0x00 in steady-state. That was the
canonical first divergence per recomp-template/NES/PRINCIPLES.md #3.

# Why we couldn't find it for four days

We didn't have a transaction-aligned SIO byte diff. The existing
sio_trace dumps were timing-dependent and didn't survive the
press-X window. We were chasing symptoms (dispatch misses, missing
state-12/13 seeds, chain counter reset paths, RAM divergences at
0x74F8 / 0x7560) — all real but all DOWNSTREAM of this one byte.

Per principle "BUILD TOOLS, NOT GUESSWORK" we built
`sio_first_divergence` (transaction-aligned ours-vs-Beetle byte diff,
splits each ring at 0x81 select, compares same-position TX/RX),
which immediately reported byte 1 of the read transaction:

    ours:    tx=0x52 rx=0x08
    beetle:  tx=0x52 rx=0x00

Two-line fix in mc_process_byte's MC_CMD case: clear `mc_flag = 0x00`
after returning it.

# Measured impact

Before:
- chain_trace state-3 returned v0=-1 every cycle
- 0x7514 counter cycled 0->1->2->3->4 then reset (BFC1499C path)
- mc_max_state stuck at 10 with mc_aborts ~30
- Both runtimes stuck at MAIN MENU shell selector

After:
- chain_trace states 1..11 all return v0=0, run 14+ times each
- counter no longer resets at state-3
- Both runtimes navigate to MEMORY CARD screen (matching layout)
- byte-1 SIO RX divergence eliminated (both return 0x00)

Memory cards still don't show save data (state-12/13 read-data
handlers don't fire because outer counter never reaches >=5 to enter
the read-chain path at BFC1588C). That's a separate downstream
divergence which the new diff tool will pinpoint next.

# Other artifacts in this commit

- recompiler/seeds/phase2_ghidra_seeds.json: added state-12 (0x5A58)
  and state-13 (0x5AB0) chain entries from no$psx-spec read-data
  handler ranges. Necessary for state-12/13 to dispatch when the
  outer chain finally enters the read-chain path. Dispatch table
  4295 -> 4297 entries, no dispatch misses.
- runtime/src/psx_oracle_cmds.c: new sio_first_divergence TCP cmd —
  splits each side's SIO ring into card-protocol transactions
  (0x81-start, 0x01 or next 0x81 ends), compares the most recent K
  transactions byte-by-byte to find first (matching TX, differing RX)
  or transaction-length mismatch.
- runtime/src/debug_server.c: chain_trace ring buffer + mc_status
  TCP cmd for chain-state visibility.
- runtime/src/memcard.c + memcard.h: memcard_debug_info() accessor.
- runtime/include/sio.h + sio.c: MC_READ_MSB_ECHO/LSB_ECHO states
  per no$psx address-handshake spec.
- generated/SCPH1001_*.c: regen with seed additions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 00:46:31 -07:00
Matthew Stanley 711d22cff3 fix: per-slot card SIO state + card-specific IRQ delay
The SIO model had a single global mc_state shared between both card slots,
which caused 100% probe abort rate when the BIOS alternates between slot 0
and slot 1: each new 0x81 probe on the other slot would abort the in-flight
state from the previous slot.

Changes:
- Per-slot McSlotState saved/restored on slot switch via mc_save_slot/mc_load_slot
- 0x01 (pad select) preserves card state instead of aborting it
- SIO_IRQ_DELAY split into PAD (4) and CARD (8) — card delay tuned so the
  SIO chain walker can dispatch func_00004D6C before next idle path tick
  clears the protocol counter

Diagnostic tooling:
- I_MASK[7] trace ring buffer with bit-7 set/clear counts and caller info
- Counter (0x7514) field added to SIO trace so card protocol state machine
  progress is visible
- check_card.py inspects card directory contents

Runtime evidence: mc_aborts went from 100% → 0% (when slots don't overlap).
mc_max_state advanced from 1 (only MC_CMD) to 8 (MC_READ_DATA). dispatch_check
confirms _new_card AND its body 0x5688 are dispatched. Detection callback
(func_00005D94 at BFC15894) verified to clear v0=0 before return — continue
path of func_00004D6C IS being taken (133 I_MASK[7] sets observed).

Remaining: 0x7550 stays NULL despite _new_card being dispatched. Either
shell passes NULL buffer or early-return path of _new_card triggers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:36:15 -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 4054dc1c08 fix: DMA ch2 GPU→RAM now reads GPUREAD instead of writing zeros
The VRAM-to-RAM DMA path (channel 2, direction 0) was stubbed to fill
the destination buffer with zeros. This broke all offscreen VRAM
readback — the BIOS shell draws spiral/splash textures to an offscreen
VRAM region, then DMA-transfers the rendered pixels back to RAM for
later use as texture atlas entries. With the stub, all texture buffers
stayed zero, producing invisible button backgrounds.

Fix: call gpu_read_gpuread() for each DMA word instead of writing 0.

Also includes: CDROM GetID two-phase response fix, GPU raw texture
mode fix (opcode bit 0 instead of color==0x808080), debug server
A0/C0 upload history commands with CPU context capture, and extended
write trace ranges.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 14:30:57 -07:00
Matthew Stanley c4398aa35a debug: add DF8/EB4 write trace ranges for Phase 4.5 3D investigation
Track writes to 0x80079EB0-0x80079EC0 (EB4 status byte area) and
0x80079DF0-0x80079E04 (DF8/DFC/DF0 gate variables) at startup.
This traces the root cause chain: CDROM status → EB4 → DF8 → 3D gate.

Key findings from trace:
- DF8 set to 1 at frame 322 (init), NEVER cleared
- EB4 set to 0x10 (CDSTAT_SHELL) at frame 335 by func_1FC413B0
- func_800393FC(55,6) times out during boot (state doesn't reach 55)
- Root cause: events for state machine not delivered during boot init

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 01:05:11 -07:00
Matthew Stanley 5c6e00b00f feat: 5 new shell seeds + extended default write trace to 0xE300
Seeds BFC41D18, BFC41DC8, BFC41DFC, BFC41E14, BFC210F0 discovered
via DuckStation oracle TCB comparison. Extended default wtrace range
from 0xE1F0 to 0xE300 to cover EvCB divergence area at boot time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 18:08:37 -07:00
Matthew Stanley 3b9ac6b84b perf: read_ram 16KB cap + 4KB divergence chunks + send_fmt 64KB buffer
- debug_server.c: increase read_ram from 256 bytes to 16KB max,
  use dynamic allocation for hex buffer; increase send_fmt buffer
  from 16KB to 64KB to hold large responses
- find_divergence.py: use 4KB chunks (was 256), add total diff byte
  count, remove run-length cap, remove hardcoded VSync exclusion,
  add data length verification on reads

512KB paused scan: 128s (was 1393s with 256-byte chunks)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 17:50:47 -07:00
Matthew Stanley bd6dfa12da fix: memcard absent detection + raw texture bug + GTE exec counter + quad capture
- memcard.c: stop auto-creating blank cards when .mcd files don't
  exist; missing file now correctly means "no card in slot"
- gpu.c: fix setup_textured_draw raw texture flag (was ? 0 : 0,
  now ? 1 : 0 for identity-modulation bypass)
- gpu.c: add shaded quad vertex capture mechanism for Phase 4.5
  debug (capture_quads / get_quads debug commands)
- gte.cpp: add gte_exec_count to track total GTE compute commands
  executed (exposed via gte_state debug command)
- debug_server.c: wire up capture_quads, get_quads commands and
  gte_exec field in gte_state response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 15:33:19 -07:00
Matthew Stanley 00cc73f20e feat: GPU semi-transparency + 85 GTE library seeds + debug commands
GPU fixes:
- Mono tri/quad/rect/dot/8x8/lines: route through sw_renderer with
  semi-transparency support (bit 25 extraction + sw_set_semi_transparency)
- Add 16x16 mono sprite (0x78-0x7B) and 1x1 textured dot (0x6C-0x6F)
- Add per-opcode execution counter (gpu_get_opcode_count)

GTE library: batch-discovered 85 functions in BFC33000-BFC36000
including SetRotMatrix, SetTransMatrix, and full transform pipeline.
Emitted: 1173 (was 1086). Seeds: 955. Zero dispatch misses.

Debug commands: gpu_opcodes, gte_state, screenshot_file (BMP to disk).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 14:25:34 -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 2767018b46 fix: SIO pad detection + input override + CARD driver seeds
Root cause: BIOS pad handler polls I_STAT bit 7 waiting for SIO ACK
IRQ. sio_tick() was only called from psx_check_interrupts and SIO
register accesses, but I_STAT reads go through memory.c which never
called sio_tick(). The IRQ countdown was stuck during the polling
loop, causing the BIOS to time out and report "no pad found" (0xFF).

Additionally, sio_tick() in psx_check_interrupts fired the IRQ too
early — during BIOS delay loops BEFORE I_STAT bit 7 was cleared —
so the BIOS cleared it and never saw it re-appear.

Fixes:
- memory.c: Call sio_tick() on I_STAT reads (32/16/8-bit paths)
- interrupts.c: Remove sio_tick() from psx_check_interrupts
- debug_server.c: Don't clear input override on TCP disconnect
  (was wiping override before next VBlank could read it)
- debug_server.c: sio_state reports real pad_buttons via getter
- sio.c/sio.h: Add sio_get_pad_buttons() for debug server
- seeds: Add 20 new seeds for shell input and CARD driver functions

Result: Pad buffer at 0x80083E30 shows status=0x00 (pad found),
type=0x41 (digital), buttons reflect keyboard/override state.
Only 5 COP2/GTE dispatch misses remain (Phase 5).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 11:22:39 -07:00
Matthew Stanley 70e7e7aea2 fix: enable DMA completion IRQ + DICR byte access + 5 VBlank seeds
Root cause of VSync counter stuck at 0: DMA completion IRQs were
deliberately suppressed in dma.c, preventing the shell's chain-E004
handler (func_1FC40628) from ever seeing I_STAT bit 3.  The handler
gates all VBlank callback processing on DMA interrupt presence.

Changes:
- dma.c: directly assert i_stat bit 3 on DMA ch2/ch6 completion
  (bypass DICR edge detection which gets stuck because the shell
  handler only acknowledges ch3 in DICR, not ch2/ch6)
- dma.c: expose dma_get_dicr()/dma_get_dpcr() for debug server
- memory.c: add DMA register range (0x1F801080-0x1F8010FF) to
  mmio_read8/write8/read16/write16 — fixes crash on DICR byte read
- debug_server.c: add DPCR/DICR to irq_state command
- seeds: add 0xBFC42BA0 (shell_vblank_dispatch), 0xBFC38AF0 (shell
  hot function, 178 misses), 0xBFC137D0 (kernel RAM), 0xBFC42BE0
  (shell_vblank_process), 0xBFC4226C (mid-function callback loop)
- regen: 1051 dispatch entries (was 1046)

Verified: chain-E004 handler + verify now dispatch.  VSync counter
still 0 because the VBlank callback loop inside func_1FC42160 is
gated by an init-done flag and the mid-function entry mechanism is
not yet resolved.  New EvCB crash at BFC12A84 exposed by DMA IRQ —
next session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 20:06:15 -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 e8094affb4 Phase 4: seed VBlank chain handler + add RAM write-trace
EvCB write-trace instrumentation confirmed OpenEvent is never called for
any event class during boot, not only VBlank. Static ROM analysis found
that the only instruction in SCPH1001.BIN that stores to the VSync
counter at 0x80079D9C lives in an unrecompiled 15-instruction function
at 0xBFC425BC (identified as the VBlank chain handler: increments the
counter, optionally calls the user callback at *0x800DFF40). Added as a
seed so the function is now compiled and present in the dispatch table.

Recompile alone does not unblock the boot — no JAL, literal, or
lui 0xBFC4 exists in ROM that would load this address, so the kernel
registrar that enqueues it on the root-counter chain is itself missing
and needs to be found (next session, via DuckStation cross-reference).

Changes:
 - recompiler/seeds: +0xBFC425BC (vblank_chain_handler), 825 seeds total
 - debug_server: wtrace_range / wtrace_dump / wtrace_clear TCP commands,
   1024-entry ring buffer capturing (addr, old, new, ra, width); default
   range 0x0000E000..0x0000E1F0 active from process start so early-boot
   writes are captured before a TCP client can attach
 - memory.c: inline bounds check + hook call in psx_write_{word,half,byte}
 - tools: print_wtrace.py, sample_ra.py, sample_ra_long.py,
   scan_f2_sites.py — investigation helpers

Runtime behaviour unchanged (still hangs in VSync spin at Sony logo) —
the seed is a prerequisite; the upstream registrar is the next bug.

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