The oracle-accurate load wait-state (=4) deterministically wedged Tomba 2 boot in the BIOS shell (handle 104 -> 1672-stride GUI table index, no upper bound -> wild ptr 0x8013B608 -> RAM corruption -> pc=0). Faithful-core cascade: an accurate CPU cost exposed an inaccurate device timing. Root cause (confirmed via sio_irq_dump, not the memcard path the handoff guessed): the proximate runaway is 100% controller (pad) polling. The pad fast-path armed the access-paced sio_irq_countdown=SIO_IRQ_DELAY_PAD(4), decremented once per SIO register access (sio_tick only ever called cycles=0), so pad ACK->IRQ7 was access-count-paced, not guest-cycle-paced. Under the faster (accurate) CPU it fired at the wrong guest-cycle phase vs the cycle-paced timers/VBLANK, diverging the BIOS pad-detect state machine. Fix: pad fast-path now arms the guest-cycle-paced ack scheduler (sio_pending_ack/sio_ack_remaining = BAUD+ACK = 1258 cyc, driven by sio_advance <- psx_advance_cycles), identical to the already-faithful card path. Pad and card ACK timing are now both CPU-speed-independent. Validated: load=4 boots past the wedge to the intro FMV (screenshot, frame 11k+ stable, reproduced); ruler #1 unchanged modulo the known load-ReadFudge gap. Runtime-only, no regen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4.7 KiB
RESOLVED: load=4 boot wedge — faithful guest-cycle pad ACK timing
Branch wt/tomba2-load-accuracy. When PSX_RAM_READ_WAIT_CYCLES=4 (the
Beetle-oracle-accurate single-load value, vs the old DuckStation 6), Tomba 2's
boot deterministically wedged in the BIOS shell. Root-caused and FIXED
2026-06-27. This is the faithful-core cascade exactly as CLAUDE.md predicts:
an accurate CPU cost exposed an inaccurate device timing.
Symptom (was reproducible, deterministic)
- pc=0 / halt;
total_checksfrozen (~134,756,056),current_funcin the BIOS shell (0x1FC2CE28 / 0x1FC2C3E0 — the exact landing varies by a few-cycle observer-effect, axis-7),i_stat0x80,i_mask0x0D. - Wedge mechanism (live-verified registers + disasm): a BIOS-shell GUI/object
builder indexes a 1672-byte-stride table at 0x80110EC8 by a handle
s1(s1*1672 + base) with only a lower-bound (bgez) guard, no upper bound. At the wedges1 = 0x68 = 104→v0 = 0x80110EC8 + 104*1672 = 0x8013B608, a wild pointer; the struct copy corrupts RAM → downstream pc=0. (RAM at the shell funcs was zeroed by the time of the halt — post-mortem RAM is unreliable; the always-on rings are the ground truth.)
CONFIRMED root cause (step A — validated, not hypothesised)
The proximate runaway immediately before the wedge is continuous controller
(pad) polling, NOT memcard enumeration as previously hypothesised. Live
evidence (sio_irq_dump on the wedged runtime): the last 150+ SIO IRQs are
100% source=pad, delay=4, active_device=PAD, mc_state=0 (card idle).
The unfaithfulness (source-confirmed, cross-ref Beetle frontio/sio + axis5 doc
D1/Fix-6): the pad fast-path in sio_write (sio.c:1386) bypassed the
guest-cycle-paced shifter and armed sio_irq_countdown = SIO_IRQ_DELAY_PAD (=4),
which sio_tick(0) decrements once per SIO register access (sio_tick is only
ever called with cycles=0 — see memory.c:389/498/589, interrupts.c:268). So pad
ACK→IRQ7 timing was access-count-paced, not guest-cycle-paced. The card path
(cycle-paced shifter via sio_advance ← psx_advance_cycles) was already
faithful; only the pad path was not.
Causal chain: with the faithful/faster CPU (load=4) the pad ACK IRQ fires at a different guest-cycle phase relative to the cycle-paced timers/VBLANK than it did at load=6, diverging the BIOS pad-detection state machine and ultimately producing the out-of-range handle 104. (The handoff's "memcard save-file enumeration count" attribution was wrong — it is the SIO controller path, a sub-case of the same axis-5 device-timing weakness.)
FIX (step B — faithful, validated)
runtime/src/sio.c, pad fast-path ACK arm: replaced the access-paced
sio_irq_countdown = SIO_IRQ_DELAY_PAD with the guest-cycle-paced ack
scheduler the card path uses:
sio_pending_ack = 1;
sio_ack_remaining = SIO_BAUD_CYCLES_DEFAULT + SIO_ACK_CYCLES_DEFAULT; /* 1088+170 */
sio_pending_ack_irq_en = 1;
g_sio_timing_active = 1;
sio_advance() (driven by psx_advance_cycles() per instruction) now delivers
the pad IRQ after a fixed guest-cycle delay (one byte-shift + ack pulse =
1258 cyc, identical to the card path), independent of how many SIO register
reads the CPU issues while it waits. Runtime-only change — no regen.
Validation
- load=4 boots past the wedge to the intro FMV (screenshot verified, pixels on screen — Rule 5), reaching frame 11,000+ stably (wedge was frame 2189, total_checks frozen). The same FMV the load=6 branch reached.
- Ruler #1 (BIOS EvCB-search [c5c→ca4], native 4500 vs Beetle 4382): native 54 vs Beetle 56, steady −2 = the known unmodeled load ReadFudge gap on the load=4 branch (a listed remaining Stage-2 component), NOT a regression — the SIO change cannot alter CPU instruction cost, and native being lower rules out a spurious mid-window IRQ.
Remaining completeness follow-up (recommended, not faithfulness-blocking)
The pad fast-path is now faithful but is still a separate code path from the card shifter. axis5 Fix-6 / the unfinished "1.0e-e2" (comment at sio.c:1868) specs fully removing the pad fast-path so pad bytes flow through the unified cycle-paced shifter (modeling the byte-shift-in-progress STAT transitions too). That is a path-unification refactor; it needs pad input validation at the title/menu (not just boot-to-FMV) before adopting. Both pad and card ACK timing are already guest-cycle-paced, so there is no remaining fake/access-paced site in the shipping (SIO_MODEL_CYCLE_PACED) config.
Cross-title note: the pad path is shared — regen+smoke Tomba1/MMX6/Ape before any cross-title merge of this branch (Rule -1: breaking other titles is acceptable; they regenerate).