Review of #561. Guest time now has two entry points (psx_cycles.h): the
CPU charge psx_cpu_charge() (instruction base, load fudge/region/completion,
i-cache refills, BIOS HLE costs), which applies [timing] guest_cycle_scale,
and the unscaled device advance psx_advance_cycles() (DMA, idle skips,
mul/div and GTE deadline waits). The emitters' block-mode charges emit
psx_cpu_charge; overlay DLLs accumulate it raw and publish through the
cpu_charge callback (was advance_cycles, same slot) into the same host
function, so nothing is scaled at the DLL boundary. The batched
psx_cyc_charge path, the dirty-RAM interpreter, psx_cyc_readmem, i-cache
refills and bios_hle.c all charge through it. At scale 1 it is exactly
psx_advance_cycles.
Gate in psxrecomp: [timing] guest_cycle_scale_gate, RAM predicates judged
at every VBlank edge (deterministic, works online, no mod needed), plus
psx_mod_set_guest_cycle_scale_gate() in mod_plugins.h for custom logic or
to tie the scale to a mod (guest_cycle_scale_gated = true; every mod
session reset shuts it). The carried fraction and both gate states are a
boot_state section (BS_SEC_GCS, written only when the scale is not 1), so
savestates and the netplay rollback snapshot re-simulate the same cycles.
psx_guest_cycle_scale_gate_test pins tier identity, the gates and rollback
re-simulation against the real psx_cycles.c.
OpenBIOS regenerated (emitter stamp); retail SCPH1001 is not.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes Ape Escape terrain seams: CPU-built subdivision midpoints and lwl/lwr-copied
grid corners drew native next to precise neighbours. Adds pgxp_tri_ring/pgxp_store_ring.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reconciled with #470 (PGXP): gpu.c precise vertices take PGXP only under geometry correction and count it, then native-wide edge recovery; the PGXP triangle stat stays PGXP-only. Stubs and tests from both kept; stamps recomputed.
Master moved the textured-dot test stubs into gpu_exec_stubs.inc; the PGXP stubs move there too. pgxp_note_rect_bypass stub takes (int). Stamps recomputed.
2163035e and 85c88d26 changed config_loader.cpp (a fingerprint input) without restamping; a full regen is byte-identical, so only the stamps move.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The draw-distance clamp changes the emitter sources. Both BIOS backends
regenerate byte-identically (no clamp sites outside a game config), so
only the fingerprints move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
recompiler/src/config_loader.cpp is an input of the BIOS emitter
fingerprint, and this branch adds the [video] PGXP keys to it, so the
committed stamps no longer matched. A release-style configure
(PSXRECOMP_BIOS_STALE_FATAL) and tools/ci/check_bios_stamps.sh refused the
tree. tools/regen_bios.sh for OpenBIOS and SCPH-1001 regenerated both
backends byte-identically; only the two .emitter.sha stamps change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preserve current segmented dispatch, active-plan mod hooks and software scale limits. Extend function filters through the active plan, retain the last-store pointer with overlay ABI 25, and keep default config fingerprints stable. Refresh BIOS emitter stamps after byte-identical regeneration and reconcile regression fixtures.
Git for Windows checks text files out with CRLF, so the windows-2022 release
job hashed a byte-identical tree to a different value and BIOS_STALE_FATAL
refused the committed backends there while Linux and macOS passed. Stripping
CR before hashing makes the fingerprint line-ending independent (verified:
an LF tree and a CRLF copy of it now agree). Stamps rewritten to the new
canonical value; the generated C is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Decision (Alex, 2026-09-30): the recompiled BIOS carries the same risk as
the recompiled game, which titles already commit, so the retail backend is
compiled in rather than built on the player's machine. The retail image is
still never shipped: the runtime needs its data at run time and accepts only
the exact image the backend was built from, so the player supplies their
own dump as before.
- generated/{OpenBIOS,SCPH1001}_{full,dispatch}.c and their .emitter.sha
stamps are tracked (.gitignore un-ignores exactly those); regenerated with
the current emitter.
- bios_emitter_fingerprint.sh no longer hashes the ROM image: the profile's
SHA-256 pin stands for it and the emitter refuses any other image, and CI
has no dump, so hashing it made every CI configure read "stale".
- runtime.cmake: the staleness check covers every LINKED stem and
-DPSXRECOMP_BIOS_STALE_FATAL=ON makes a stale stamp fail configure.
- game-release.yml: no CI-side OpenBIOS emission (generate_openbios.sh is
gone); configure with BIOS_STALE_FATAL and assert both backends linked.
check_generated.sh asserts the submodule carries both backends tracked
and no dump is tracked anywhere.
- The player-side module build stays for a known image a build does not
link (SCPH-5552).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The `generated/` tree contains C and JSON produced by the recompiler from
SCPH1001.BIN — derivative of copyrighted Sony BIOS code. It is regenerated
from a user-supplied BIOS on local build; not committed.
Mirrors the convention used by nesrecomp and snesrecomp.
Also covers release-stage/ and *.zip so local release packaging artifacts
stay out of the tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
The COPY/DELETE flows were silently no-oping the SIO write protocol:
60 dispatches per copy were missing the 3-instruction predicate fn at
0xBFC0989C (the WRITE-command sibling of 0xBFC09890), so the BIOS's
write-handler fork test returned junk via uninitialized v0 and the
BIOS skipped sending 0x57 over SIO entirely. The in-memory card cache
appeared updated (visual UI worked, leave/re-enter showed copies) but
nothing reached memcard_write_sector or the .mcd files.
Also reorganize memcard directory resolution: production builds put
cards next to the exe; dev builds (CMake build tree detected) keep
them in the project root next to bios/ so a runtime rebuild doesn't
delete them. --memcard-dir still overrides.
Outcomes:
- YES/NO COPY confirm modal closes correctly on CROSS press (returns to
COPY menu — matches Beetle behavior)
- No crash, no freeze
- Memory card 2 visible with a known-good save (sourced from Beetle's
dummy.1.mcr; backup of prior card2.mcd kept as .bak.before_beetle_copy)
- TAB held = turbo mode (skips VRAM->ARGB conversion + SDL_RenderPresent
every frame except every 30th); useful for navigation cycles
Remaining bug:
- The actual save data is NOT being copied. Source card content is not
written to destination. card1.mcd hash unchanged after a copy attempt.
- 0xBFC0989C still hits psx_unknown_dispatch 60x per copy. Bisect
confirmed seeding 0xBFC0989C alone causes a misaligned-dispatch crash
from caller 0xBFC09790 (function-pointer table at runtime addr
0xA000E1E8 indexed by predicate result, gives garbage target 0x3).
Seeding both 0xBFC09890 + 0xBFC0989C causes a softlock instead of crash.
Therefore 0xBFC0989C is intentionally NOT seeded; the copy data path
short-circuits because the predicate's stale-v0 result fails to enter
the actual write code.
Status of the function-discovery-miss bug class:
- VALIDATED for 0xBFC1A1BC (commit 6889f5c) — bandaid worked, modal
closes
- PARTIALLY VALIDATED for the BFC09xxx area — adding 0xBFC09890 alone is
safe but doesn't unblock the copy. 0xBFC0989C cannot be safely seeded
because it's likely a mid-function label, not a function entry. The
proper fix requires recompiling the full function containing 0xBFC09790
(the caller of these predicates) and ensuring the function-pointer
table at runtime address 0xA000E1E8 is populated with valid recompiled
function targets.
Turbo mode: hold TAB on the SDL window. Suppresses 29/30 frame presents,
keeps simulated CPU advancing at full speed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
SUB (SPECIAL funct 0x22) was the last non-FPU unsupported opcode.
Treated identically to SUBU — PSX R3000A has no overflow trap in
practice. Unblocks function at BFC33CC0.
Emitted: 1086 (was 1085). Skipped: 1 (COP1/FPU only). Unsupported
instructions: 2 (was 4).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>