mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 02:32:02 -04:00
fdbd2b515faef73ef3796e13e90a3eb283bc445a
29 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
cc8b17e555 | Add Tomba EXE inspection tools | ||
|
|
f75159c8d6 |
SPU ADSR + free-voice CURVOL fix: chime fans across all 24 voices
Root cause: BIOS music engine reads voice register 6 (byte offset 0x0C, CURVOL / ADSR_LEVEL) to find a free voice; recomp returned the never- written spu_regs value (always 0), so the engine saw every voice as silent and over-recycled voices 0-3 instead of fanning across all 24. After fix, recomp KEYON-per-voice counts match Beetle exactly (e.g. v0 2/2, v18 2/2, v22 1/1). Also: - Full ADSR envelope (Attack/Decay/Sustain/Release) ported from Beetle's CalcVCDelta/RunEnvelope. KEYOFF now triggers Release-phase decay rather than immediate silencing. - End-without-loop triggers Release (matches RunDecoder), not active=0. - ENDX latch at 0x1F801D9C/9E now reports actual block-end-reached state instead of active-mask. - SPU event ring (KEYON/KEYOFF/END_STOP/END_LOOP) on both backends with frame-stamped per-voice payload; 1 M entries each (32 MB) for chime + game-scene headroom. Beetle-side env>0 filter so silent-loop noise doesn't bury the audible-voice timeline (matches recomp behavior where deactivated voices don't fire further events). - spu_voices / spu_events / spu_events_reset debug-server commands on both ports. send_fmt buffer kept single-shot to avoid extra newlines. - tools/_spu_ring_diff.py: frame-aligned event-ring diff with per-voice count summary; chunk-count cap removed (was aborting reads at 98 KB). - tools/_spu_capture.py, _spu_probe.py, _probe_one.py: targeted probes used during diagnosis. Local-only beetle-psx change (gitignored): same env>0 filter at mednafen/psx/spu.cpp around line 345/352. Rebuild beetle-psx static lib if reapplying. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
97f30d2a76 | Preserve PSX memory-card debug state | ||
|
|
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> |
||
|
|
6889f5c5d2 |
recompiler: seed missing modal_stub function (BFC1A1BC), add unknown_dispatch_log ring
Modal close on YES/NO COPY confirm now works on recomp. Cause: function
discovery (Ghidra + transitive walk) missed the function at 0xBFC1A1BC
which the BIOS shell calls indirectly during modal close. The 5 nearby
"missing" PCs the diagnostic identified are all in the same gap from
0xBFC1A1B8 to 0xBFC1A3B8; seeding 0xBFC1A1BC discovers the rest
transitively.
Status: modal closes cleanly on X. Actual copy execution still has
downstream missing-function-discovery issues (3 more PCs hit
psx_unknown_dispatch on copy: 0xBFC09890, 0xBFC0989C, 0xBFC02918)
plus likely additional bugs that softlock when those are seeded.
Those remain for follow-up; this commit is the modal-close milestone.
This is a BANDAID per user permission. The proper fix is to improve
function discovery so missed functions like 0xBFC1A1BC are found
automatically. Ghidra didn't see this function, and our transitive
walk didn't reach it either.
Tooling additions:
- psx_unknown_dispatch_record() ring buffer + unknown_dispatch_log
debug command. Replaces the prior file-based miss log per
CLAUDE.md §3 (no log files, ever). Always-on; queryable summary
(per-target counts) or tail (last N entries with addr/phys/ra/a0/a1).
- WTRACE_MAX_RANGES bumped from 16 -> 64 (boot defaults were
crowding out investigative arms).
- DirtyRamBlockLogEntry +a0/a1 fields, ring CAP 16K -> 4M
(previous 28ms eviction made retroactive press-window analysis
impossible).
Diagnostic scripts (kept):
tools/_unknown_dispatch.py — query unknown_dispatch_log
tools/_dispatch_check_v3.py — was a PC ever dispatched (RAM->ROM map)
tools/_dispatch_tail.py — last N dispatched targets
tools/_freeze_diag2.py — softlock investigator
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
e88e9a0233 |
phase 4: fix mc_flag persistence so multi-sector card reads work
Before: only sector 0 read succeeded; sector ≥1 reads aborted at 2 bytes
because mc_flag was loaded from mc_slots[].flag on every 0x81 select but
never saved back. The clear in MC_CMD lived only in the working static, so
every reselect re-loaded the stale 0x08 ("new card / changed battery") init
value, and the BIOS bailed every multi-sector op as "card was just inserted,
re-init."
After: BIOS shell memcard menu reads the full 15-sector directory area for
both slots. Real save filenames decode (e.g. "BISLPS-01234SAVE00") and per
card icons render in the bottom panes — the Phase 4 success criterion.
Also bundled this session:
- debug_server: B9D0..B9F0 added to always-on wtrace ranges; fn_dump_parse
default window scoped to 1M most-recent entries (filter scans against the
full 128M ring used to freeze the debug server thread for many seconds).
- ISSUES.md + diagnostic tools/*.py written during the audit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
87d99d4f1d |
seeds: 5 dispatch_miss entries for chain writer PCs (vertical slice partial)
Adds the Beetle-discovered chain writer/init PCs as dispatch_miss seeds
so the recompiler emits standalone functions for them at their RAM
destination addresses (via the existing Phase 1e ROM↔RAM alias rule
RAM = ROM - 0xBFC10000 + 0x500):
0xBFC14A54 → 0x00004F54 (chain BUSY-writer)
0xBFC158D0 → 0x00005DD0 (chain init slot-0 BUSY)
0xBFC158D8 → 0x00005DD8 (chain init slot-1 BUSY)
0xBFC159F4 → 0x00005EF4 (chain step 0x02-OPEN writer — KEY)
0xBFC15AA8 → 0x00005FA8 (chain step 0x04-OPEN writer — KEY)
Post-regen dispatch_table contains all 5 as standalone entries. e.g.
func_00005EF4 is a clean 3-instruction leaf (sb $t3, 0($a3); v0=1; jr ra).
Validation (full output in docs/CASE_A_AOT_GAP.md "Vertical slice result"):
- ✓ dispatch_table contains all 5 entries
- ✗ none of the 5 are reached at runtime — chain still doesn't dispatch
- ✗ [0x80007510..0x7570] all-zero — the chain init function FUN_bfc158a8
(= func_00005DA8) never runs in our recomp during this session, gates
haven't even reached the initial 0x01 BUSY state
- ✓ shell IS on memcard screen ([0x80066948] = 0x32)
The seed addition is correct and necessary work — it removes one of two
layers of "writer is unreachable". The remaining layer is upstream:
nothing in our recomp causes FUN_bfc14b00 / FUN_bfc158a8 to run during
the memcard flow. That's a different category from "chain runs but
fails on byte N" and is not chased here per Case A directive.
Beetle wtrace also revealed the writers at 0x5EF4 / 0x5FA8 are tiny
leaf utilities called from BIOS code in BFC08{B98,C94,D08,F28} — NOT
chain-step jump-table targets as initially assumed. The chain step
tables at RAM 0x6c70/0x6c98/0x6ccc point at 0x52xx / 0x56xx-0x5Axx /
0x5Bxx-0x5Dxx ranges. Memo updated accordingly.
Includes 4 new validation scripts under tools/ used during this audit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f8841ead30 |
case A: Beetle wtrace + recomp verification → memcard blocked on AOT gap
Adds a RAM-write trace hook to mednafen-PSX (SB/SH/SW handlers fire g_psxrecomp_wtrace_cb after each write), a ring buffer + TCP commands in psx-beetleoracle, and verification scripts that drive CROSS in both Beetle and recomp and diff the gate-byte writers. Conclusion (docs/CASE_A_AOT_GAP.md): the [0x7568+slot] LSB-clear writers are at RAM PCs 0x5EF4 / 0x5FA8 (and 0x4F54 sets BUSY). Recomp never dispatches any of those PCs, never executes the 0x4F00..0x4FFF or 0x5800..0x5FFF RAM windows, and never writes the gate. Selective coverage pattern (interp reaches 0xCF0/0x52xx/0x6xxx but not 0x4Fxx/0x5xxx) is install/dispatch path gap, not interpreter bug. PLAN.md:941 already names this work as a hard gate. Beetle-side modifications live inside the vendored beetle-psx/ git clone; docs/beetle_wtrace_hook.patch captures the diff so they survive a re-clone. Memcard symptom debugging stops here. Next: trace-generated AOT RAM-code manifest + recompiler ingestion (Phases B–F). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1b59103b3c |
runtime: card-read summary + card-data-writes diagnostic rings
Two passive, always-on rings for auditing memory-card reads end-to-end without arming/timing probes: card_read_summary (non-evicting, first 32 successful reads): Captured at the MC_READ_END transition in sio_process_byte. Records slot/cmd/sector/checksum/data_idx/first 16 bytes/cycle/caller func. TCP cmds: card_read_summary, card_read_summary_reset. card_data_writes (4096 entries, rolling tail): Arm side: sio_read(SIO_RX_DATA) sets a pending flag while the card protocol is in MC_READ_DATA..MC_READ_END. Check side: psx_write_byte/half/word capture the next RAM write whose new_val matches the armed RX byte, then disarm. Records dest addr / value / store_pc / func / mc_idx / slot / cycle. TCP cmds: card_data_writes, card_data_writes_reset. Helpers: tools/probe_card_reads.py tools/probe_card_writes.py tools/probe_card_writes_raw.py tools/peek_cards.py Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
479c51934a |
runtime: drive SIO TX via cycle-paced shifter
TX_DATA writes now route through the SIO0 shifter under SIO_MODEL_CYCLE_PACED. Pad and card share the single-byte shifter and TX buffer; bus owner is tagged on the first byte of each SELECT cycle. sio_advance() is driven from psx_advance_cycles() so the shifter and ACK pipeline progress on the guest CPU clock without touching the legacy IRQ countdown. The SELECT-deassert and TX_EN 1->0 paths now restore TX_RDY|TX_EMPTY when they kill an in-flight shifter. Without this, sio_stat retained the TX-clear bits set by SHIFT_START and the next handler busy-waited forever on TX_RDY=0 inside the exception, deadlocking the BIOS. Watchdog dump captured the deadlock at chain handler 0x445C with stat=0x0000 across 4 seconds of PC samples. Diagnostics added (passive/gated): - starvation_ring records SIO MMIO events + periodic PC samples, watchdog dumps to JSONL on TCP-poll starvation - starvation_walk.py queries the dump (last/last_change/why_idle) Validation: shell-only sanity stable; one CROSS press no longer hangs; mc_read_done climbs from 0 -> 30+ with mc_aborts=0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
101fd2e1a9 |
feat: 46 GTE library seeds + DuckStation oracle SW readback + debug tooling
Add 46 seeds for the libgte helper functions at BFC34E00-BFC36D00. These wrap individual GTE operations (RTPS, RTPT, NCLIP, MVMVA, etc.) and were never discovered because they're called via JALR function pointers, not direct JAL. Coverage: 52→114 gte_execute, 166→429 LWC2/SWC2. Emitted functions: 1178→1214. Seeds: 960→1006. Fix DuckStation oracle VRAM readback by setting UseSoftwareRendererForReadbacks=true in settings.ini. Add debug/analysis tools: GTE audit, VRAM stitching, rapid polling, GPU info, divergence comparison, jump table dumps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
957256134b |
duckstation oracle: fix headless boot — rcc target + first-run wizard
Root-caused the post-rebuild boot failure: 1. MSBuild's duckstation-qt target does NOT include the Qt resource compile. The separate duckstation-qt-rcc target produces build/bin/resources/duckstation-qt.rcc (~740 KB). Without it the binary shows a "duckstation-qt.rcc could not be loaded" dialog and exits. build.sh now builds both targets in one MSBuild invocation. 2. A first-time GUI launch is also required so DuckStation's setup wizard can write out a working settings.ini. Our auto-generated settings.ini (with SetupWizardIncomplete=false etc.) is insufficient — the wizard records other fields (probably Graphics API init, controller profile, achievement opt-out) that are needed for headless emulation to start. Thanks to mstanley for running the wizard interactively. After those two fixes, verified end-to-end: - `python3 tools/duckstation/launch.py -bios -nogui -fastboot` runs - port 4371 binds - read_ram 0x00079D9C returns an incrementing VSync counter - read_ram 0x000DFEE0 returns 0x8005A5BC (VBlank chain handler — patch live) - pc_break_list responds (proves my compile-but-untested pc_break family from the uncommitted additions is now tested via embedded binary) tools/duckstation/launch.py — detached-spawn wrapper (uses CREATE_NEW_PROCESS_GROUP only, no DETACHED_PROCESS so the GUI stays visible on first-run). Moved from tools/launch_ds_detached.py to live alongside setup.sh / build.sh / README.md. README updated to replace the "known issue" section with the correct recovery steps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
6a4f1d027b |
duckstation oracle: document post-rebuild boot-failure investigation
Follow-up to the restructure commit. Attempted to verify end-to-end `-bios -nogui -fastboot` behaviour on the rebuilt binary; it does not start BIOS emulation regardless of launch-flag permutations. Process runs stably at ~34 MB but `PSXRecompDebug::Initialize(4371)` is never reached because the call sits inside `System::Initialize` which returns `false` silently. Verified (by this session's investigation) as NOT the cause: - patch present in binary (`tools/check_ds_patch.py` finds PSXRecompDebug, pc_break, read_ram, psxrecomp_debug_server literals) - BIOS file md5 matches DuckStation's known SCPH-1001 hash - prebuilt deps extracted + `_IMPORT_PREFIX` paths rewritten across 27 files - settings.ini has all the obvious gates flipped (BIOS path, setup-wizard bypass, auto-update disabled, explicit region, resources + translations restored next to exe) - portable.txt in place, submodule pinned to upstream ffb33c281 Still unknown: whether it's a GPU/Vulkan/D3D init path failing under -nogui on this machine's environment, or a baked path in the prebuilt deps we didn't rewrite. Documented in tools/duckstation/README.md for pickup next session. Also adds tools/check_ds_patch.py — verifies the patch is compiled into the binary via string-search of marker symbols. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
685687f36f |
Restructure duckstation as submodule + patch; salvage TCP docs + debug_client
Recovery + restructure after rm -rf destroyed duckstation/build/ earlier this session. Adopts the nesrecomp pattern (see F:/Projects/nesrecomp): pristine upstream as submodule, our changes as a patch file that gets auto-applied by a setup script. Submodule: - duckstation/ now pins stenzek/duckstation@ffb33c281 (release-20260328 era) - .gitmodules records the pin; .gitignore stops ignoring the submodule path but still ignores build/ and dep/prebuilt/ (the submodule's own .gitignore also handles these) Patch + setup: - tools/duckstation/psxrecomp_oracle.patch (1187 lines, 7 files) captures our TCP debug-server on port 4371 plus the unfinished pc_break family of commands (with the VirtualMemoryAddress compile fix) - tools/duckstation/setup.sh clones submodule, fetches + verifies + extracts prebuilt Windows deps, rewrites stale absolute paths baked into the prebuilt CMake metadata, applies the oracle patch. Idempotent. - tools/duckstation/build.sh configures CMake (VS 17 2022) + runs MSBuild, then provisions bin/ with portable marker, BIOS copy, and settings.ini post-fixes - tools/duckstation/README.md documents the layout, the regeneration flow, and the known headless-boot configuration issue TCP documentation (salvaged from v3): - TCP_COMMANDS.md: full command table with native (4370) / duckstation (4371) / both columns, reflecting actual v4 implementation including wtrace_* and pc_break/pc_hit_last families - tools/debug_client.py: unified CLI from v3 (ports 4370/4371 match v4) - DEBUG.md: merged v3's END-TO-END COMPLETION RULE, Stale Hypothesis Rule, TOOLING RULE, and TOOL LIFECYCLE RULE sections into v4's existing loop Phase 4 diagnostic artefacts from this session: - generated/relocation_proofs/vblank_registrar/ — corrected VBlank-stall diagnosis. Prior handoff claimed the shell VBlank handler was never installed; live TCP inspection of both runtime and DuckStation oracle proved handler IS installed at 0x800DFEE0. Real gap is one level up in the kernel IRQ chain at *(0xA000E014). - tools/duck_query*.py, duck_search.py, disasm_ram.py, scan_*.py — focused probes used to establish the above; reusable against either server CLAUDE.md §2 and new §15 document the submodule-based oracle setup and the "never edit submodule, regenerate patch instead" rule. Known issues (tracked in tools/duckstation/README.md): - Fresh settings.ini doesn't complete BIOS boot under -nogui; one-time launch with GUI is required to warm up the settings before headless mode works Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
7c40d33db3 |
Phase 1 complete: recompiler proven correct on SCPH1001.BIN
Phase 1a (boot slice): 30 instructions at 0xBFC00000, compiles clean. Phase 1b (instruction coverage): 54 opcodes implemented in strict_translator, 44,629 walker-reachable instructions covered, 0 missing. Phase 1c (function discovery): 76 functions from 2 seeds, 2,988 instructions, 199 call edges, 27 indirect jumps. All validated through StrictTranslator. Phase 1d (indirect control flow): 27 sites classified — 21 A0/B0/C0 dispatch stubs, 4 computed calls, 2 computed tail calls. Ghidra cross-referenced with 0 disagreements. Phase 1e (relocation): ROM-to-RAM copy at FUN_bfc00420 proven with both Ghidra (static) and DuckStation (runtime). 69 address aliases, 68/69 function entries match byte-for-byte in RAM. Normalization rule documented. Includes PRINCIPLES.md and DEBUG.md adapted from recomp-template. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |