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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>