Skip to content

Add Jaguar CD support (CUE/BIN, CDI, ISO) — supersedes #120#201

Open
JoeMatt wants to merge 85 commits into
developfrom
feature/jaguar-cd-support
Open

Add Jaguar CD support (CUE/BIN, CDI, ISO) — supersedes #120#201
JoeMatt wants to merge 85 commits into
developfrom
feature/jaguar-cd-support

Conversation

@JoeMatt

@JoeMatt JoeMatt commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Supersedes #120 (same work — branch moved from the Provenance-Emu fork to the libretro org) and #109.

Experimental Jaguar CD support: image loading (CUE/BIN, CDI, ISO) in src/cd/cdintf.c, BUTCH/DSA/FIFO emulation in src/cd/cdrom.c, an HLE CD BIOS path in src/cd/jagcd_hle.c, real-BIOS boot with auth bypass, and a large CD test suite. CHD was dropped deliberately: Jaguar discs hide data in pregaps/odd session layouts and CHD's pregap normalization corrupts the offsets.

Status (2026-07-14 revival)

The branch is synced with develop (blitter/bus-arbiter/crash-detect/DSP-IRQ work all in) and this round landed three root-cause fixes, each derived from disassembly/trace evidence and adversarially reviewed:

  • fix(cd): route BUTCH CD interrupt to GPU IRQ1, not IRQ0 — the CD BIOS installs its CD-data ISR at $F03010 (GPU IRQ1 vector, JERRY source per JTRM) and enables/acks only IRQ1; we were asserting IRQ0, so the ISR never ran and the FIFO never filled. Reverts the unverified April routing change (3279b30). With this, the real-BIOS data path actually streams disc data.
  • fix(cd): rewrite $2C00 TOC to the track-indexed layout — game boot stubs scan the $2C00 table (verified by disassembling Baldies' $4E18 and Primal Rage's $0803E2 scanners); our injected table had a zero-longword marker that terminated the scan and never stamped the session key byte. New layout: entry for track N at $2C00+N*8, [track#][MSF min/sec/frm][session-1], validated by a contract test that emulates both scanner algorithms against live RAM.
  • fix(cd): make HLE CD_read idempotent — deleted a fabricated +3 sectors/call continuation heuristic that made repeated identical reads drift and corrupt RAM.

Plus: a 21-row boot matrix (docs/cd-boot-matrix.md, test/tools/cd_boot_matrix.sh) as the per-title regression gate, a DSA/seek/FIFO trace ring (core option virtualjaguar_cd_trace / env VJ_CD_TRACE=1), and a cd_seek_wedge crash-detect signature so CD hangs self-diagnose in the RetroArch log.

Per-title snapshot (headless, 3000 frames)

Title HLE boot Real BIOS boot
BrainDead 13 GAME_CODE GAME_CODE (calls BIOS CD_read)
Baldies (CUE) GAME_CODE live CD streaming (was ILLEGAL-halt crash)
Primal Rage GAME_CODE BOOT_STUB, streams data, sentinel handoff pending
Highlander / Iron Soldier 2 GAME_CODE / loops BIOS service band, transfer engine live
Dragon's Lair / Space Ace / Battle Morph / Hover Strike GAME_CODE BIOS intro / early wedge

No title is fully in-game yet under the real BIOS; HLE mode gets most titles into game code. All remaining failures wedge loudly (cd_seek_wedge / video_stall signatures) rather than hanging silently.

Known issues / backlog

  1. BIOS-mode streaming wall: boot stubs stream real data but the load handoff never completes (Primal Rage sentinel scan, Baldies post-TOC). Next diagnosis target.
  2. Battle Morph derailment: PC lands in data before any CD I/O; TOC link tested negative — needs its own diagnosis.
  3. HLE IS2 re-read loop: CD_read is now idempotent, but a downstream game-logic loop remains.
  4. baldies.cdi loader segfault: pre-existing CDI-parser crash at load time (CUE variant unaffected).
  5. Cosmetic: some pre-fix matrix rows in the doc's table are superseded by the appended re-run notes.

Testing

  • Full suite green (make TEST_EXPORTS=1 test, 56k+ assertions incl. both audio tests) — no cart regressions.
  • New tests: BUTCH→GPU IRQ1 routing contract (RED→GREEN), $2C00 TOC contract (both scanner algorithms, reviewer-reproduced on a real disc), HLE CD_read idempotency (RED 27 corruptions → GREEN 0).
  • Boot matrix re-run per fix; no title regressed at any step.
  • RetroArch visual spot-check on real hardware images is the remaining manual gate before merge.

🤖 Generated with Claude Code

JoeMatt and others added 30 commits May 7, 2026 20:52
Brings in the CUE/BIN parser, BUTCH emulation, HLE/cart/bios boot
strategies, and BootConfig settings glue from
claude/add-jaguar-cd-support-hOzev (PR #109), repointed to the new
src/cd and src/core layout. The build now links the CD subsystem
but no path activates it -- bootConfig.strategy is NULL and
JaguarCDHLEActive() returns false. libretro.c CD detection and
src/core/jaguar.c CD reset are deferred to Tier 2.

C89 fixes applied to satisfy scripts/c89-lint.sh: hoisted for-loop
counters (HLEHandleCDRead phase loop, HLEPopulateCartBuffer, ATRI
sync block, cdrom.c HLETransferTick), moved mid-block decls to block
tops in cdrom.c (BUTCHExec, NM93C14 opcode/addr decode, seek-redirect
discTotal) and cdintf.c (CDIntfGetTrackInfo MSF locals), pulled
HLEHandleCDRead's sentinelIsAscii/fallback*/phase_starts/wasRedirected
to the function header, and converted designated initializers in
cd_boot_strategy_{hle,cart,bios} to positional.

VJSettings layout: kept hardwareTypeNTSC/useJaguarBIOS/useFastBlitter
as the leading three fields so test/test_hle_bios.c (which redeclares
the struct with just those three) still resolves them via dlsym at
matching offsets.

external_cd_bios[] / cd_bios_loaded_externally are emitted as weak
fallback definitions in src/cd/jagcd_bios.c; libretro.c will provide
the real definitions in Tier 2 and override them.

JaguarInstallCDAuthBypass() is forward-declared in cdintf.c; it is
defined in jagcd_bios.c so cdintf can arm the BNE-NOP patch when the
BIOS reads into an inter-session gap.

Files imported:
- src/cd/cdintf.{c,h}    -- CUE/BIN parser
- src/cd/cdrom.{c,h}     -- BUTCH emulation
- src/cd/jagcd_{bios,cart,hle}.c, jagcd_{boot,hle}.h  -- strategies
- src/core/settings.{c,h} -- BootConfig + ResolveBootConfig
- Makefile.common        -- register the new objects

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Detects .cue/.cdi/.iso content in retro_load_game, scans the system
directory for a CD BIOS image, opens the disc image via CDIntfOpenImage,
calls ResolveBootConfig to pick the boot strategy, and dispatches via
bootConfig.strategy->boot(info). Adds virtualjaguar_cd_bios_type and
virtualjaguar_cd_boot_mode core options. Extends the EEPROM save buffer
to include the CD EEPROM (cdrom_eeprom_ram[64]).

The Tier 1 weak symbols for external_cd_bios[] and
cd_bios_loaded_externally are now overridden by strong libretro.c
definitions. Cart-only flow is unchanged when no CD content is loaded.

Also propagates JaguarLoadFile failures out of cart_boot (so the
'unknown headerless BIN rejected' contract still holds) and folds the
post-reset reload of RAM-loaded executables (ABS/COFF/JAGSERVER) into
the cart strategy. Exposes cdrom_eeprom_ram so the save buffer can pack
both EEPROMs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wires the M68KInstructionHook to dispatch JaguarCDHLEHook and the active
CDBootStrategy's instruction_hook, and invokes the strategy's reset()
from JaguarReset so HLE/BIOS per-run state (auth-bypass flag, boot-stub
flag, HLE active flag) clears alongside the rest of the machine.

The cart strategy keeps its existing path: cart_instruction_hook and
cart_reset are no-ops, so non-CD content is unaffected.  CD HLE/BIOS
strategies (already imported by Tier 1) now actually fire.

Tier 1+2 already exposed everything else this needs:
  - jagcd_hle.c defines JaguarCDHLEHook + cd_boot_strategy_hle
  - jagcd_bios.c defines bios_instruction_hook + JaguarInstallCDAuthBypass
  - jagcd_cart.c defines the cart no-op strategy
  - cdrom.c handles BUTCH FIFO drain via HLETransferTick (no GPU-side
    intercept needed; #109's gpu.c HLE path was an alternate route)
  - jerry.c already routes BUTCH I2S into the DAC (Tier 1 imported)
  - libretro.c populates bootConfig.strategy via ResolveBootConfig

Hunks from #109 not ported, with reasoning:
  - src/m68000/m68kinterface.c MULL/DIVL: their version is buggier
    (truncates 64-bit overflow detection, mishandles unsigned q sign,
    drops Dh != Dl guard).  Our existing code is correct; keep ours.
  - src/tom/gpu.c +263 lines: 95% diagnostic tracing (gpuStartCount,
    GPU_TRACE, gpu_isr_phase, gpu_ram_8 dumps).  The functional CD
    intercept (JaguarCDHLEGPUDataPhase) is reachable from cdrom.c's
    HLE path; the GPU-side intercept is an alternate route we don't
    need.  Skipping to preserve our IRQ priority/dispatch (Test 9d).
  - src/tom/tom.c: their tom.c has no CD-specific changes vs ours;
    the diff was rename + cosmetic.
  - src/jerry/eeprom.{c,h}: cdrom_eeprom_ram[] already lives in
    cdrom.c with libretro.c handling save/restore.  No header tweak
    needed.
  - src/core/file.c: ours has more code (raw-binary homebrew load
    address inference); their version is older.  Keep ours.
  - src/core/log.h: identical content (just inline vs INLINE; ours
    is more portable for MSVC).

No test changes (Tier 4) and no libretro.c changes (Tier 2 already
did those).  test_hle_bios stays at 211/211, make test 0 failures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brings in #109's test infrastructure for the CD subsystem. Per the
Tier 5 triage report, redundant general-hardware suites were dropped;
overlapping cherry-picks deferred to a follow-up.

Imported and wired into make test (84 new passing asserts):
  test/test_butch_cd.c, test_bios_config.c, test_boot_config.c,
  test_audio_dac.c

Built but not auto-run (private-ROM gated; surface real per-game
boot regressions when discs are present):
  test/test_cd_boot.c, test_cd_hle_boot.c, test_cd_bios_boot.c,
  test_blitter.c

Framework + helpers + tooling: test_framework.h, cd_assertions.h,
mister_ground_truth.h, dump_pc.c, heap_search.c, headless.py, and
test/tools/{analyze_cd_roms,bios_disasm,disasm_gpu_isr}.py.

Dropped (redundant with our coverage in test_hle_bios, test_dsp_ops,
test_gpu_ops, test_m68k_ops, test_blitter_simd):
  test_dsp_instructions, test_gpu_controlflow, test_gpu_instructions,
  test_m68k_instructions, test_timers, test_irq, test_gpu_ctrl,
  test_video_modes, test_memory_map, test_gpu_irq.

Cherry-picks deferred to a follow-up:
  resolution-derivation cases from test_video_modes ->test_hle_bios,
  gpu_ctrl_bus_hog_readback / gpu_flags_int_enable_all_five from
  test_gpu_ctrl -> test_hle_bios, BUTCH cases from test_irq ->
  test_butch_cd, line_buffer cases from test_memory_map ->
  test_hle_bios.

All imports pass scripts/c89-lint.sh (decls hoisted). test_hle_bios
unchanged at 211; full suite OK: 0 test(s) failed.
…-ID shortcut

Two bugs in jagcd_hle.c CD_read:

1. The "sentinel NOT found — read raw" fallback hard-coded scanLBA = lba
   (the original requested seek), which silently undid the
   streaming-continuation logic that advances startLBA past previously
   transferred sectors on repeated calls. Result: a game polling the same
   CD_read got the same 1 MB on loop forever. Now uses startLBA so
   continuation works on the fallback path too.

2. When D1's top 16 bits are zero (e.g. Space Ace passes D1=$00000001
   as a transfer ID, not a sync pattern), the sentinel scan finds
   millions of false-positive `\0\0\0\x01` matches across the disc but
   never accepts a real sync block — then falls back to "read raw"
   anyway, after burning 4 M log lines and several seconds of CPU per
   call. Now short-circuits the scan when D1 is clearly a counter and
   streams raw from startLBA directly.

Space Ace's HLE_LOG output drops from ~4.3 M lines to ~43 K (100x).
Primal Rage's behaviour unchanged (it uses real ASCII sentinels —
DDL9/DDL5 — which find proper sync blocks). Full HLE sweep still
5/9 pass; full make test green.

The user-visible "screen goes black" failure on Space Ace is upstream
of these fixes — the game now gets fresh sectors per call but quickly
runs past end-of-disc into zero-fill. Tracking that as part of a
broader CD-subsystem audit now that the CPU/GPU/DSP/IRQ baseline is
solid.
load_external_cd_bios() previously only matched the .j64 variants of
the World retail and developer CD BIOS plus the lowercase
jaguarcd_bios variants. Real-world libretro frontends ship the BIOS
under names like "Jaguar CD BIOS.rom" or with the libretro [BIOS] tag
in .rom/.bin form. Add those filenames so the BIOS is picked up
without renaming.
cdrom.c:593 was asserting GPUIRQ_DSP (=1, vector $F03010) but the CD
BIOS installs its CD-data ISR at GPU IRQ0 ($F03000) - the EXT1/CPU
line. The mismatch meant any BUTCH-triggered IRQ landed on the DSP
vector with no handler. Fix to GPUIRQ_CPU.

Identified by the CD-IRQ-chain audit (no behaviour change without
the BUTCHExec scheduler tick - see follow-up commit).
BUTCHExec was fully written but never called from anywhere in the
live engine, so BUTCH never asserted IRQs to the GPU on any path.
The HLE shortcuts (jagcd_hle.c sentinel scan, cdrom.c:335
HLETransferTick) compensated by bypassing BUTCH entirely.

Tick BUTCHExec once per halfline when bootConfig.isCDGame is true.
Halfline cadence (~32 us) is much coarser than real BUTCH I2S timing
but matches our existing event-queue resolution; the FIFO-fill state
machine progresses one transition per call.

Real-BIOS path can now fire its CD ISR. The HLE shortcuts remain
load-bearing because of the documented PTRPOS divergence
(cdrom.c:319-333) - that fix is separate.

Adds test_butch_gpu_irq_line_mapping to test_hle_bios.c pinning the
GPUSetIRQLine(line, ASSERT) -> gpu_control bit (6+line) mapping for
both lines BUTCH could plausibly target, regression-locking the
GPUIRQ_CPU vs GPUIRQ_DSP fix from the previous commit.
… dual-delivery

Two destructive paths surfaced when Path A wired BUTCHExec into the
scheduler:

1. The "after N seeks without HLE progress, fall back to native FIFO
   IRQ" mechanism (HLE_FALLBACK_THRESHOLD) was harmless when BUTCH
   was dormant but actively destructive once BUTCHExec ticks: the
   fallback unleashed FIFO IRQs which run the BIOS GPU ISR's
   fifo_read path, which writes to the wrong RAM address (PTRPOS
   divergence, cdrom.c:319-333). Pin hleActive = (strategy==bios)
   permanently. HLETransferTick remains the canonical CD-data path
   for both strategies until the GPU-side PTRPOS bug is fixed.

2. The dual JERRY-EXT1 delivery — JERRYSetPendingIRQ() AND
   m68k_set_irq(2) — fired the 68K IRQ2 vector during transfers when
   the CD BIOS hadn't yet installed its EXT1 trampoline (Hover
   Strike, Primal Rage stack-corruption to PC=$22xxxxxx). The CD
   BIOS clears BUTCH bit 0 before issuing CD_read so the 68K side is
   dormant during transfers; the GPU side (BUTCH -> JERRY EXT1
   latch -> GPU IRQ0) is the path that matters. Drop the m68k_set_irq
   call but keep the JERRYSetPendingIRQ pending bit so JINTCTRL reads
   continue to see the latched source.

Sweep deltas vs pre-Path-A baseline (which passed 7/9 via fake
HLETransferTick path):
  BIOS: 6/9 actually executing the BIOS code path (was 5/9 with
        Path-A regression, was 7/9 fake before Path A).
  HLE:  5/9 (unchanged).
  Hover Strike: recovered to PASS.
  Primal Rage:  still fails but the wedge moved deeper into the BIOS
                path (227 unique PCs, 16 KB RAM payload before the
                $220DC09E garbage-PC). Tracking separately.
Adds per-disc visibility into actual CD subsystem activity. Previously
the harness only sampled 68K PC, which can't distinguish "BIOS wedged
in CD spin-loop" from "BIOS booted game code." Now the per-disc line
includes GPU PC, GPU IRQ0 (CD ISR) count, GPU IRQ3 (OP IRQ) count,
BUTCH diag counters (butchExec/fifoIRQs/dsaIRQs/fifoReads/seeks/
globalDisabled), and HLE transfer bytes.

Implementation:
- Adds gpu_irq0_count and gpu_irq3_count globals to gpu.c, declared
  in gpu.h. Incremented in GPUSetIRQLine on ASSERT_LINE; reset in
  GPUReset alongside other GPU state. Pure observability — no
  behavioural side effects.
- Adds CDROMDiagGetCounters() accessor to cdrom.c/cdrom.h that
  exposes the existing static diag_* counters and hleTransferBytes
  to harnesses. Avoids dropping `static` on the whole set.
- cd_assertions.h: new cd_diag_snapshot struct + cd_diag_capture()
  helper that reads the accessor via dlsym (alongside gpu_irq0_count,
  gpu_irq3_count, GPUGetPC). Both HLE and BIOS harnesses capture the
  snapshot before unload and emit a [DIAG] line per disc.

C89/GNU89 clean (declarations at top of block).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Removed 6 patches in jagcd_bios.c (4 RAM stomps, BNE-NOP auth-bypass + helper, DSP completion-flag stomp), TryReadAuthRedirect in cdintf.c, and HLETransferTick + support machinery in cdrom.c.

HLE 5/9 unchanged. BIOS 6/9 -> 7/9 (Primal Rage flips PASS, was wedging at gpu_pc=$22002200). make test green.
The CD BIOS GPU CD ISR reads DSCNTRL after handling a DSARX response
and the inline comment in the embedded BIOS listing (cdrom.c:1643)
documents this read as 'Clears DSA pending interrupt'. Real BUTCH
hardware acks the DSA pending latch on a CPU/GPU read of DSCNTRL.

Without this, dsaResponseReady stays sticky after the first seek and
BUTCHExec re-asserts GPU IRQ0 every halfline indefinitely (Primal Rage
trace shows 2.9 M GPU IRQ0 firings across 6 K frames, all DSARX, while
the game's poll-loop at \$0050E2 waits for game state that never
advances because the GPU is permanently re-entering its DSARX handler).

The fix doesn't (yet) flip Primal Rage from harness wedge to harness
PASS — the GPU still parks at gpu_pc=\$F03060 long enough for the user-
visible boot to reach the publisher logo and stall there. That's a
separate bug; this commit is the correct hardware semantics regardless.
Audited HLE-side hacks the same way as a96556b did the BIOS side.

REMOVE: single-match sentinel fallback (unreachable once multi-phase
scan + raw-from-startLBA fallback are both present).

REMOVE: per-CD_read $800000 cart-space mirror. HLEPopulateCartBuffer
already covers BrainDead 13's cart-scan at boot; the per-read mirror
was just redundant write traffic.

REMOVE: 10 jump-table no-op intercepts (CD_I2S_ENABLE, CD_SPIN_UP,
CD_STOP_DRIVE, CD_SET_VOL_*, CD_PAUSE, CD_UNPAUSE, CD_FIFO_DISABLE,
CD_HW_RESET, CD_SET_DAC_MODE). HLEInstallJumpTable pre-fills these
slots with RTS so falling through is a clean no-op naturally.

CHANGE: defer CD_poll completion by one tick. Some boot stubs depend
on observing not-done at least once before done (BrainDead 13's HLE
poll loop misses an init step otherwise). Return A0=0 the first time
we see hle_read_pending and clear it; subsequent polls return the
GPU data area pointer as before.

Sweep: HLE 5/9 -> 6/9 (BrainDead 13 PASS). BIOS 7/9 unchanged.
Combined coverage: 8/9 boot via at least one strategy. make test green.

Kept (still load-bearing): counter-ID shortcut (perf), multi-phase
sentinel scan (Highlander), sentinel scan as a whole (Highlander +
Primal Rage), ATRI sync block writeback (boot-stub PC diversity for
BrainDead 13/Baldies), streaming continuation (Iron Soldier 2's
repeated-CD_read pattern).
- Add CDROM*/BUTCH*/GetRamPtr/bootConfig/cd_boot_strategy_*/
  ResolveBootConfig to the test export lists (link-test.T,
  exports-test.list) so test/test_butch_cd, test/test_bios_config,
  and test/test_boot_config can dlsym the CD subsystem.
- Make retro_get_memory_size(SAVE_RAM) return EEPROM_SAVE_SIZE
  (128 bytes) for cart-only loads and (EEPROM_SAVE_SIZE +
  CD_EEPROM_SAVE_SIZE) (256 bytes) only when jaguar_cd_mode is
  active.  Keeps cart save format byte-compatible with develop and
  un-breaks test/test_eeprom_lifecycle:first_load_sram_available.
The unconditional JaguarReset() after retro_load_game's strategy->boot()
call was wiping out HLE CD state: it re-randomized RAM (clobbering the
boot stub injected at $004000+), reset TOM/JERRY/GPU/DSP/CDROM, and
called the strategy reset hook a second time, undoing the entire HLE
setup the cd_boot_hle path had just performed.  Symptom: every CD HLE
disc booted to the injected entry PC, then the 68K immediately wandered
into garbage and saturated the test harness's 256-PC unique-PC ceiling
(test_cd_hle_boot regressed 6/9 -> 0/9).

Cart and CD strategies already perform their own JaguarReset() at the
right point in their boot sequence, so the only path that genuinely
needs the extra reset+reload is the RAM-loaded executable case
(.abs/.cof/JagServer) where the load buffer must be re-applied after
RAM is randomized.  Move the JaguarReset() inside the RAM-loaded
branch and drop the unconditional one.

After fix: test_cd_hle_boot 6/9 (matches pre-rebase tip 806f321);
test_cd_bios_boot stays 7/9; make test exits 0.
…ename

develop's DSP cleanup renamed DSPHandleIRQs to DSPHandleIRQsNP and removed
the old export (docs/emulation-bug-hunt-todos.md:126). test_framework.h
(added by the CD branch's own Tier 4 commits) still declared and
LOAD_SYM'd the old name, producing a harmless "dlsym failed" WARN in
test_butch_cd / test_cd_hle_boot on every run. The pointer was never
called anywhere in the CD test suite, so this is dead code stranded by
the merge -- drop the struct field and the LOAD_SYM line.

make test: still exits 0, all subsystems 0 failed, no more
DSPHandleIRQs warnings.
test/tools/cd_boot_matrix.sh runs every local CD title through
test_cd_hle_boot and test_cd_bios_boot (3000 frames, 120s wall-clock cap
per title x mode), classifies each run into the boot-stage taxonomy
(LOAD_FAIL -> BIOS_INTRO -> BOOT_STUB -> GAME_CODE -> MENU -> IN_GAME)
from the harnesses' own structured output (final_pc bands, PC-SET,
crash_detect watchdog lines), and writes docs/cd-boot-matrix.md.

Rows append incrementally with a resume guard (already-recorded
title x mode combos are skipped) and CD_MATRIX_MAX_RUNS chunking, so an
interrupted sweep picks up where it left off.

Baseline (21 rows, 9 CUE x 2 modes + baldies.cdi x 2 + 1 ISO doc row):
HLE 9 GAME_CODE + 1 LOAD_FAIL; BIOS 4 BIOS_INTRO, 1 BOOT_STUB,
1 GAME_CODE, 2 ambiguous BIOS-service-band, 1 pc_escape, 1 HARNESS_HANG,
1 LOAD_FAIL, 1 ISO BIOS_INTRO (documented .iso limitation). No title
reaches a verified MENU/IN_GAME headlessly; every future CD fix must
move rows forward, never backward.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up to 529ed38, three fixes:

1. docs "How to re-run" now documents ALL env knobs -- CD_MATRIX_MAX_RUNS
   (the chunking cap that makes long sweeps survivable), CD_MATRIX_LOGDIR,
   CD_MATRIX_OUT -- plus the recommended chunked invocation pattern and how
   to force re-runs of specific rows (delete them; resume guard refills).

2. Ambiguous BIOS-service-band ($0-$3FFF) classification no longer claims
   a check that never ran: the harness only prints [PC-SET] when
   unique_pcs <= 32, so absence of the line now yields "PC-SET suppressed,
   unique_pcs>32 -- game handoff undetermined" instead of the false
   checked-negative "no game-band PCs seen". Iron Soldier 2 bios row
   (unique_pcs=34) re-run and corrected accordingly; Highlander bios row
   re-run with the explicit checked-negative wording.

3. GAME_CODE calls made from PC-SET are now self-auditing: game-band
   entries ($090000-$1FFFFF) are quoted in the row's evidence column.
   BrainDead 13 bios row re-run and now carries its disambiguating
   "PC-SET game-band: $124342 $124346 $12434C" inline.

Also: moved the dated notes section above the results table so the resume
guard's append-at-end always lands inside the table (the three re-run rows
had been appended after the notes), and added a matching "## Results"
heading to fresh-file generation.

Only the three affected title x mode combos were re-run (resume guard);
the other 18 rows are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two mechanisms account for all four BIOS-mode failure classes, each cited
with trace lines / register counters / disasm addresses:

- Mechanism A (classes 1+3, Primal Rage/Highlander CONFIRMED, IS2 confirmed
  root): seek completes but the BIOS never sets I2CNTRL bit 2
  (i2s_fifo_enabled) -- writes $0001/$0011 at PC $365E -- so BUTCHExec's
  bit-2 FIFO-fill gate never opens and the guest polls forever. Guest never
  issues the write (stored correctly), not a dropped write; no GPU/DSP ISR
  sets it either.
- Mechanism B (classes 2+4, Battle Morph/Baldies): 68K exception vectors are
  left PRNG garbage in CD-BIOS mode (jaguar.c:857 vector-stub block gated
  !useJaguarBIOS), so an exception at frame 437 vectors through garbage and
  pc-escapes. Pre-CD-read (seeks=0) -> wrong-LBA/TOC hypothesis rejected.

Also: IS2 (hle) LBAs are correct (MSF->LBA maps into track 12) -- the loop
is a jagcd_hle repeated-read continuation bug, not wrong-LBA.

Docs-only; temporary cdrom.c trace counters used for evidence were reverted.
Review fixes to the Task 5 diagnosis section (docs-only):

- CRITICAL: Mechanism A verdict split -- the FIFO-never-fills deadlock
  remains CONFIRMED (airtight), but attribution is now UNCONFIRMED: the
  write counters prove no processor wrote I2CNTRL bit 2, not that the
  guest never intended to. The CD BIOS uploads a GPU IRQ0 CD-ISR
  (jaguar.c:762 asserts IRQ0 to drive it), and the observed signature
  (556k IRQ0 fires, gpu_run=1, DSA response never consumed, zero I2CNTRL
  writes) equally fits that guest ISR stalling under our GPU/IRQ
  emulation. Removed the "rules out a GPU/DSP ISR" over-claim; added
  Task-6 candidate 1: disassemble the uploaded GPU CD-ISR and resolve
  this against JTRM/MiSTer BEFORE any FIFO-gate loosening.
- IMPORTANT: frame-437 trigger is statically knowable -- prev_pc $8020A2
  is CD BIOS ROM; static disasm of the exception class is a stated Task-6
  requirement before trusting RTE-stub vector population (correct for an
  expected interrupt, a mask for a genuine fault). Noted the unexplained
  same-prev_pc / different-target detail (Battle Morph $8CA88044 vs
  Baldies $A7E08FA0).
- MINOR: named the reverted [T5-DIAG] counter placement (common
  CDROMWriteWord/Byte handlers -- the all-processor choke point).
New ROM-free register-level test (synthetic 16-sector CUE/BIN): mimic the
CD BIOS setup (G_FLAGS=INT_ENA1 only, BUTCH master+RBUF enable, DSA seek
$10/$11/$12), tick BUTCHExec past the seek delay, and require G_CTRL
INT_LAT1 latched with dispatch to $F03010 — the vector where the CD BIOS
installs its CD-data ISR (JTRM: vector = int# x 16, int 1 = DSP/JERRY).
Fails on current code (IRQ0 latched, permanently masked); fixed by the
follow-up commit. Exports _CDIntf* in the TEST_EXPORTS ABI only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…O transfer engine now runs

The CD BIOS's uploaded GPU CD-data ISR was waiting for GPU IRQ 1: its
entry stub is installed at $F03010 (the IRQ1 vector — JTRM vector =
int# x 16, int 1 = DSP/JERRY source), it enables only G_FLAGS INT_ENA1
($20), acks via INT_CLR1, and re-arms the JERRY external latch
(J_INT=$0101) in its epilogue — BUTCH's eint enters through JERRY
(MiSTer butch.v line 83). BUTCHExec asserted GPUIRQ_CPU (IRQ0, vector
$F03000) instead, which the BIOS never enables, so the latch stayed
permanently masked: instrumented Primal Rage run showed zero GPU IRQ
dispatches ever (latch=$01 vs enable=$02), zero GPU BUTCH reads, zero
I2CNTRL writes. The ISR's I2CNTRL bit-2 setter (BSET #2 on its
BUTCH-bit-13 branch) never executed — the Mechanism-A FIFO-never-fills
boot deadlock (Primal Rage / Highlander / Iron Soldier 2, bios mode).

One-line fix: GPUSetIRQLine(GPUIRQ_DSP, ASSERT_LINE). Post-fix, the ISR
consumes the DSA $0100 response on its first dispatch (dsaIRQs
556791 -> 1), sets I2CNTRL bit 2 (i2sEn=1), and the FIFO fill/IRQ/drain
engine streams sectors (block 116618 -> 118131 in 1500 frames).

Matrix: 3 Mechanism-A rows + 2 control rows re-run. Targets now show a
live transfer engine (Primal Rage gpu_pc=$F03010 in-ISR, Highlander
fifo_drains 0 -> 54); stage advance past BOOT_STUB is blocked by a
separate downstream mechanism (boot-stub sentinel scan never matches)
documented in docs/cd-boot-matrix.md. Controls unchanged.

The cdrom.c bit-2 FIFO gate itself matches butch.v (i2s_fifo_enabled,
bit 2) and is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d commit

Review follow-up for 73dbc18 (Approved, 1 Important + 2 Minor):
- HLE rows re-run (Primal Rage, Iron Soldier 2, BrainDead 13): all
  unchanged vs baseline — BUTCHExec ticks in HLE mode too, so the
  IRQ0->IRQ1 reroute needed HLE coverage; no spurious $F03010
  dispatches. Rows re-recorded via the matrix resume guard.
- Provenance note: the reroute functionally reverts 3279b30
  (2026-04-29), whose "$F03000 = EXT1" premise was unverified and
  self-declared no-behaviour-change without the BUTCHExec tick.
- BrainDead 13 (bios) control-row gpu_pc drift ($F03270 -> $F03276,
  same poll band) called out explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ssing IRQ vector

Task 6B carried out the REQUIRED instruction-level disasm before trusting
RTE-stub vector population, and it overturns Mechanism B's premise for both
target titles. The fault is a SYNCHRONOUS CPU trap the loaded boot stub
executes on its own error path, not an async interrupt with an uninstalled
handler:

- Baldies: op $4AFC (68000 ILLEGAL) at $004D54, reached via
  `jsr $4E18; tst.l d0; bpl; ILLEGAL`. $4E18 is a TOC scanner over $2C08
  that returns d0=-1 because our injected $2C00 TOC (src/cd/jagcd_bios.c)
  puts a zero first longword at $2C08 (session marker byte at +4) — the
  scanner's first `tst.l d2` sees 0 and bails. Root cause = TOC format
  (project_cd_toc_format.md), not vectors.
- Battle Morph: op $AFDE (Line-A) at $0507E2, which is inside a DATA region
  ($78xx run) — PC derailed into data; uncharacterized derailment.

prev_pc=$8020A2 is a red herring (once-per-frame sampling of a benign BIOS
copy loop). Different escape targets = different vectors (4 vs 10) plus
frame-granular sampling landing mid-runaway.

Populating vectors MASKS: vector 4 = plain RTE returns to $004D54 and
re-loops ILLEGAL forever (pc_escape -> wedge) while a "vectors populated"
test passes (PR#170 pattern). Real fix = the $2C00 TOC layout, a separate
mechanism. Task 6B: BLOCKED with artifacts, no src change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per Task 6B review (Approved, one Important gap): the Mechanism B section
named only defect (1) (marker slot's zero first longword at $2C08 ends the
$4E18 scan). Add:

- Defect (2): no injected entry carries the byte[4] key the scanner matches
  (jagcd_bios.c writes only bytes [0..3] = [track][min][sec][frm]) — so
  un-zeroing the marker alone still returns d0=-1.
- The format-model question (per-entry session/type byte at [4] vs
  standalone marker slot), to be resolved against MiSTer ground truth.
- The two-scanner requirement (Baldies $4E18 occurrence-count scan AND
  Primal Rage $0803E2 marker-then-NEXT-entry scan) and the still-unknown
  found-path payload.
- Preempt the jagcd_hle.c:934-949 skip-2 ILLEGAL-handler alternative: it
  equally masks (stub continues past its deliberate halt with d0=-1).
- Crisp frame-437 statement: BIOS reaches the $050176 injection hook at
  ~frame 437; each stub faults within its first frame after injection.
- Correct the RTE analysis to cover Battle Morph: Line-A (vector 10) also
  stacks the faulting PC on a 68000, so plain RTE re-executes $AFDE = wedge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The boot-stub $2C00 TOC scan was failing because our injected table used a
sequential layout with a standalone session-marker slot, incompatible with
what the game boot stubs (and the real BIOS DSP TOC writer) expect. Baldies'
$4E18 scanner hit the marker's zero first longword ($2C08), treated it as
end-of-table, returned -1, and executed a deliberate ILLEGAL halt; no track
entry ever carried the byte[4] session key it matches against, so the scan
could not succeed past the marker either.

Boot-mode determination = outcome (c): $2C00 is PRNG RAM-fill garbage at the
injection point (getenv-gated dump, reverted), i.e. the real BIOS DSP/TOC
writer never runs under our auth-bypass + boot-stub-injection boot, so the
injected table must itself be correct.

Reconstructed the authoritative layout from the BIOS DSP/TOC writer
(disassembled at ROM $808BE8 in the retail CD BIOS):
  - entry for track N at $2C00 + N*8 ($808CB4 lsl.w #3; $808CBE store byte[0])
  - byte[4] = 0-based session number ($808CC2 move.b d7,$4(a0,d5.w))
  - byte[1..3] = start MSF (full-TOC words $62/$63/$64)
  - $2C00 is a header (byte[2]=minTrack, byte[3]=maxTrack); the two boot-stub
    scanners start at $2C08 and skip it.

This layout satisfies BOTH documented scanners by construction:
  - Baldies $4E18: scans from $2C08, terminates on a zero first longword,
    matches byte[4] against session key 1 (0-based data session).
  - Primal Rage $0803E2 (disassembled from the injected stub this task):
    finds the first byte[4]==1 entry, advances to the NEXT entry, reads its
    MSF -> now the faithful firstDataTrack+1 (was one track early).

Fixed in both injectors: the real-BIOS path (jagcd_bios.c) and the HLE path's
own HLEPopulateTOC (jagcd_hle.c), which carried the identical defect.

TDD: new run-gated test/test_cd_toc_contract.c reads the live $2C00 out of RAM
after boot and runs both scanner algorithms in C, asserting each lands on the
data-session track (Primal Rage on the exact firstDataTrack+1 with matching
MSF). RED (pre-fix): Baldies scan returns -1; $2C08 holds the zero-longword
marker. GREEN (post-fix): both scanners pass on Baldies and Primal Rage, bios
and HLE modes.

Matrix: Baldies (bios) advances from HARNESS_HANG (ILLEGAL halt) to PASS in the
BIOS CD-read poll band with the CD stream live; Primal Rage and Battle Morph
(bios) unchanged (their blockers are downstream/distinct mechanisms, not the
TOC scan); controls (BrainDead 13, Dragon's Lair, Highlander bios) and HLE rows
(Primal Rage, IS2) byte-identical. make TEST_EXPORTS=1 test exit 0
(56103/56103); c89-lint clean. No IRQ routing, vector-stub, HLE +3-LBA, or
savestate changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up to the $2C00 TOC fix (evidence-quality, no behavior change):

1. Writer provenance: the $808BE8 routine previously labeled "the BIOS
   DSP/TOC writer" is a 68K routine in the BIOS ROM (all disassembled
   instructions are 68K; it polls BUTCH DSA responses directly at $DFFF0A),
   not the DSP code the BIOS uploads to $F1B000. The prior project belief
   (DSP code queries the drive via BUTCH $14xx) most plausibly describes a
   different stage of the same pipeline (DSP = drive-level transport; this
   68K routine consumes the $60-$64 DSA words and builds $2C00), but the
   disasm does not confirm the DSP stage either way — stated honestly in
   docs/cd-boot-matrix.md. The layout's real validation anchors are the two
   independently disassembled game boot-stub scanners plus the
   reviewer-reproduced GREEN on the real Baldies disc; track-indexed vs the
   old sequential placement is byte-identical for contiguous track numbering,
   so the load-bearing fixes are the removed zero-longword marker slot and
   the byte[4] session stamp. Source comments in jagcd_bios.c / jagcd_hle.c /
   test_cd_toc_contract.c corrected (repo policy: no known-wrong hardware
   provenance in comments).

2. Contract-test scope: noted in the test header, at the assertion site, and
   in the matrix doc that the Primal Rage MSF assertion compares the injected
   table against CDIntfGetTrackInfo — the injector's own source — proving
   copy fidelity into the scanner-visible layout, NOT seek-target/LBA
   correctness (H1-class pregap/offset bugs would still pass), and that
   Primal Rage's marker-then-NEXT-entry path is not yet exercised end-to-end
   in-game (title blocked downstream).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JoeMatt and others added 19 commits July 15, 2026 23:47
…ic layer

Two parts, aimed at the CD-DA silence investigation
(docs/cd-diagnosis/primal-rage-cdda-diagnosis.md):

1. BUTCH interrupts now also deliver 68K IPL2 when software enables the
   JERRY external interrupt -- gated on TOMIRQEnabled(IRQ_DSP) &&
   JERRYIRQEnabled(IRQ2_EXTERNAL), mirroring the JERRY timer callbacks,
   and EDGE-triggered on the condition onset (level delivery per halfline
   was an IRQ storm that wall-clock-hung four matrix titles). Boot flows
   leave the enables clear, so boot behavior is unchanged; measured
   headless: Primal Rage enables JINTCTRL ext (68K $364C) but TOM's DSP
   int stays off, so it polls the pending bit instead -- delivery stays
   dormant there, correct per hardware.

2. [CDDA] diagnostic layer (rate-limited LOG_INF, visible in RetroArch
   device logs): audio-flow DSA commands ($01/$04/$05/$15/$51/$70 with
   I2CNTRL state), SMODE master/slave switches, JINTCTRL ext-enable
   edges (with 68K PC), BUTCH IRQ edges with all gate states, BUTCH->SSI
   sample delivery counter, LRXD read counter (the CD-mix gate signal),
   and the Primal Rage DSP command mailbox ($F1B274). Remove the layer
   once CD-DA is resolved.

Verified: full suite green, 18/18 BUTCH contract tests, Dragon's Lair /
Space Ace 3600-frame bios visual runs unchanged (no error screens, no
watchdogs), Primal Rage headless behavior unchanged. Boot matrix runs
were confounded by concurrent iOS builds on the same checkout; device
testing is the point of this build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hrough

Device-traced on Primal Rage: navigating to the CD-audio test sends
$0400 (Pause) / $0500 (Pause-Release), whose DS_DATA responses fell
through to the $0400 default -- the DSA ERROR response (butch.v response
table; the command table has no $04xx/$05xx completion of their own, and
the Philips CDD-family protocol completes both with Found $01).  The
game read "error" after every Unpause and re-Paused within a tick: CD
audio test silent, match start blocked behind the audio cue.

Queue $0100 (Found) at command time for both, alongside the existing
single-word response group.  cdPlaying side effects unchanged (pause
keeps position, FIFO already gates on cdPlaying).

Full suite green; 18/18 BUTCH contract tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of the Primal Rage CD-audio silence: the $2C00 boot-stub TOC
left bytes [5..7] (track duration as MSF) zeroed in every entry.  The
game's music player computes its DSP playback countdown from those bytes
([5]*4500 + [6]*75 + [7] sectors -> $F1B278); with duration 0 the DSP
flagged end-of-track ($F1B27C) after one sector and the 68K turned the
CD mix off ~13 ms after turning it on.  New CDIntfGetTrackDuration()
(lengthLBA minus pregap) fills the bytes in both TOC writers (BIOS boot
stub + HLE).

Also revert Pause/Pause-Release DSA completion to $0400: the game's own
CD driver (RAM $BE42) masks the response to its high byte and accepts
ONLY $04xx -- the Philips error-status word with code $00 (none) is the
generic success ack, not an error.  The previous change ($0100 Found)
misread the MiSTer response table and made every Pause/Unpause read as
failed.

Diagnostics added to the [CDDA] layer while chasing this: INT1 C_JERENA
edge log (tom.c), DSP mailbox write log + env-gated RAM snapshot
(dsp.c, VJ_CDDA_SNAPDIR).

Verified: headless BIOS-mode Primal Rage -- mix-ON mailbox cmd sticks,
LRXD reads 5 -> 900k+ with live music data, host audio RMS 1000-4500
sustained from music start (was 0).  Full make test green; CD boot
sweeps: HLE 6/9 identical to pre-change baseline (same 3 pre-existing
failures), BIOS 8/9 (baseline 7/9, only documented Battle Morph
pc_escape remains).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… DSP stream

The FIFO data path (GPU/68K reads of $DFFF24) and the slave-mode SSI
path (BUTCH -> JERRY I2S -> DSP LRXD/RRXD) shared one sector cursor
(cdBuf/cdBufPtr/block).  On real hardware the drive outputs a single
stream that both sinks observe -- consumers don't advance the disc.
With the shared cursor, every GPU-ISR FIFO word-read stole 2 bytes out
of the DSP's stream and vice versa.

Device-traced on Iron Soldier 2 (mission load): the game streams data
through the DSP I2S port in slave mode while its GPU ISR drains the
FIFO half-full interrupts (~350/s) -- the DSP received a gap-riddled
stream, sector validation failed, and the game re-seeked the same
blocks forever ($1509/$150A every ~5 s): audio looping, next screen
never loading, video_stall at the mission-load screen.

The SSI path now has its own ssiBuf/ssiBufPtr/ssiBlock, initialized at
seek with the same one-word capture skew (the skew belongs to BUTCH's
I2S capture, not the consumer).  The FIFO path's matrix-validated
behavior is untouched.  New fields appended to the CDROM savestate.

Verified: full make test green; HLE boot sweep 6/9 and BIOS sweep 8/9
identical to pre-change baseline; Primal Rage CD music timeline
unchanged (RMS 1000-4500 from music start).  The failing IS2 phase
needs controller input, so the final check is on-device.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ition

The SSI head streamed disc data unconditionally: through Pause/Stop,
during seeks, and past track ends into data sectors.  A game with its
DSP CD mix enabled plays those data sectors as full-scale noise --
device-reported as "loud clipped static" under music and SFX (Primal
Rage, Baldies, Iron Soldier 2 garbage-audio loop).

A real drive outputs silence and holds position while paused, stopped,
or seeking; the I2S bit clock keeps running.  Gate the sample fetch on
cdPlaying / seekDelay: deliver zeros without advancing the head, but
keep the DSP SSI interrupt firing (the synth engine is clocked off it
in slave mode).

Verified: make test green, HLE sweep 6/9 and BIOS sweep 8/9 at
baseline, Primal Rage attract music timeline unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Writing the I2S serial-clock register (SCLK, $F1A150) invoked
JERRYI2SCallback() directly, asserting DSPIRQ_SSI inside the very DSP
store instruction that programmed the clock. On real hardware the SSI
raises its first word-strobe interrupt only after a complete I2S word
has been shifted (32 bits x 2 phases x (SCLK+1) system clocks), so DSP
init code that enables I2S interrupts and then programs SCLK always
finishes its register setup before the first interrupt can arrive.

The synchronous assert broke the Jaguar CD titles that re-upload a DSP
audio module mid-boot (park DSP -> upload -> D_PC -> DSPGO): the new
module's init wrote SCLK and was vectored into the new I2S handler with
the OLD module's register values still live. The handler mixed a CD
sample through a stale pointer (r13) straight over its own instruction
stream at $F1B07C, the stomped word $FFFF later executed as
ADDQMOD 31,r31 (stack pointer += $1F -> odd, past the end of DSP RAM),
and every subsequent interrupt return popped wavetable garbage and
jumped to the sign-extended sample value $FFFFCE03 -- the recurring
dsp_pc_escape / video_stall seen on Dragon's Lair (BIOS, device) and
Highlander (HLE, cd_boot_matrix frame 128).

Fix: restart the I2S timer chain one full I2S frame period out
(JERRYRescheduleI2S) instead of calling the callback at t=0. Steady-
state period math is unchanged; slave-mode BUTCH delivery starts one
22.68us period later.

Verified:
- Highlander HLE: dsp_pc_escape $FFFFCE03 at frame 128 -> gone (400f)
- Dragon's Lair BIOS: was video_stall f1008 baseline -> now renders
  2400 frames, audio RMS 1234, no crash-detect signatures
- Highlander BIOS: was video_stall f920 -> now renders 2400 frames,
  audio RMS 1526, no dsp escape (separate known cd_seek_wedge remains)
- Primal Rage HLE/BIOS signatures identical to documented baselines
- make TEST_EXPORTS=1 test exit 0; audio pair: IS1 presence RMS 1175.7
  (= develop baseline), Atari Karts clipping negative control PASS

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… dylib

The per-binary test rules only exist in the Makefile's TEST_EXPORTS=1
branch.  A bare 'make test/test_cd_hle_boot' fell through to GNU make's
built-in %:%.c rule, which links with the core's -dynamiclib LDFLAGS
and produces an unexecutable shared library.  cd_boot_matrix.sh built
its harnesses exactly that way, so every row of a fresh matrix
regeneration came back '?' with "cannot execute binary file" in the
per-run logs.

Add a loud pattern-rule guard in the non-TEST_EXPORTS branch, and make
cd_boot_matrix.sh build with TEST_EXPORTS=1 and re-link when an
existing file isn't a real executable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full 21-row regeneration after fix(jerry) b63fb7d (SCLK write no longer
fires the SSI interrupt synchronously). Highlander (hle) loses its
dsp_pc_escape $FFFFCE03 frame-128 signature; Space Ace (hle) recovers
from the machine-load HARNESS_HANG flake; all other 19 rows are
byte-identical, including Dragon's Lair (bios) and Highlander (bios).
cd_visual_verify confirms both ReadySoft-class bios titles render 3000
frames with audio (DL RMS 1234, Highlander RMS 1526). Remaining open
bug documented: Highlander (bios) intermittent cd_seek_wedge at frames
1743 / 2704 with disc activity resuming after each signature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… fixes

9 of 21 rows improved, no regressions. Highlights: Highlander HLE's
dsp_pc_escape $FFFFCE03 cleared (b63fb7d), Dragon's Lair BIOS
video_stall cleared, Hover Strike / Iron Soldier 2 / Space Ace BIOS
rows now reach GAME_CODE with clean watchdogs, Highlander BIOS
progresses from video_stall to the known cd_seek_wedge. Dated re-run
notes appended per the doc's convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- harness --press FRAME:BUTTON[:HOLD] (repeatable) schedules joypad
  presses by frame, enough to navigate menus into gameplay headlessly;
  programmatic tests get harness_press() / a harness_input_cb hook
- test/tools/cd_wedge_probe.c: framebuffer-freeze detector that dumps
  68K registers, the pcQueue traceback ring, CD counters + trace ring,
  and a RAM hexdump around the stuck PC; exits 42 when a wedge is
  caught so a retry loop can tell "caught" from "ran clean"
- VJ_HARNESS_LOG_DEBUG=1 passes LOG_DBG through the harness logger
  (the CD HLE per-call trace logs at that level)
- export CDTrace* in the test ABI (exports-test.list, link-test.T)

This tooling captured the Hover Strike mission-load lockup: 68K halted
executing $FFFF-filled RAM inside an HLE CD_read destination window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HLE CD_read delivered the whole transfer inside one 68K instruction.
Games issue overlay loads whose destination covers code that is still
executing (Hover Strike's LVL6 load spans $05D340-$1F0000 while its
loader runs at $1B2FD2-$1B4378 inside that window, placed there by an
earlier 1MB read): on real hardware the drive streams at 352,800 B/s,
so the game tracks the write pointer and jumps into the new code long
before the stream reaches its own address. Instant delivery broke that
sequencing and the 68K ended up executing $FFFF-filled RAM and
double-faulting -- the intermittent "lockup when skipping cutscenes /
starting a mission from the load menu" reports.

CD_read now only arms a stream (sentinel scan unchanged);
JaguarCDHLEStreamTick(), driven per halfline next to BUTCHExec, copies
~11.2 bytes/halfline (150 double-speed sectors/s x 2352 B) and updates
the GPU data area write pointer as it goes. Completion side effects
(DSP done flag $F1B4C8, $FF pad, ATRI sync block) fire when the last
byte lands. CD_poll reports not-done while a stream is in flight, and
a byte-identical CD_read re-issued mid-stream (Iron Soldier 2's
poll-retry idiom) keeps the in-flight transfer.

Validation (at 874330f base; matrix re-run on this rev follows):
- Hover Strike wedge repro (scripted d-pad+A on the mission planet):
  2/2 wedges before, 0/16 after across a 60-frame press-timing jitter;
  the fatal LVL6 read streams to completion and the game keeps running
- make TEST_EXPORTS=1 test green
- CD boot matrix: zero regressions, three rows improved -- Baldies hle
  0/1 FAIL -> 1/1 GAME_CODE, Space Ace hle HARNESS_HANG -> 1/1
  GAME_CODE, Hover Strike hle not_loop 0 -> 1

Known limitation: stream state is not serialized into savestates (same
as the rest of the HLE CD_read statics); a savestate taken mid-load
resumes with the transfer dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sume

The Object Processor treated every GPU object (type 2) as a hard stop:
it latched the phrase into OB, raised GPU IRQ3, and abandoned the rest
of the display list for that halfline. On hardware the OP halts only
until the GPU's service routine writes the object flag OBF ($F00026),
then continues with the next sequential phrase (single-phrase object,
no link field). MAME models the same suspend/resume-at +8.

Primal Rage (Jaguar CD) gates a GPU object to halflines >= 352 with a
BRANCH object and places the arena-floor bitmaps after it, so the
bottom third of every fight scene rendered black (device report:
"looks like narrow widescreen"). Fight-scene coverage goes from 61%
to 91% non-black; intros/menus are unchanged.

Notes:
- The JTRM's YPOS activation condition for GPU objects is not honored
  by the silicon: games gate the object with BRANCH objects and carry
  stale YPOS values (yarc rebuilds its list heights from a GPU object
  reached via BRANCH VC==506 whose YPOS never matches; gating on YPOS
  blacked it out entirely). Fire whenever reached.
- "Halt until OBF" is modeled by running the GPU inline (bounded to
  ~5 halflines of GPU cycles) until the TOM write path flags the OBF
  write. GPU idle, IRQ3 disabled, or no release keeps the legacy
  stop-for-this-line behavior.
- New contract test test/tools/test_op_gpu_object.c (wired into
  make test): synthetic OP list + hand-assembled GPU IRQ3 ISR that
  writes OBF; asserts both the halt-unserviced and release-resume
  paths against the TEST_EXPORTS core.

Validation: make test clean, acid suite 0 regressions vs BASELINE,
yarc framebuffer-integrity restored, scripted Primal Rage fight run
renders full-height fighters + floor + foreground.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Zero regressions; Baldies hle 0/1 FAIL -> 1/1 GAME_CODE (its load
sequencing also relied on observing streaming progress).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…not steal them from the 68K

Queued DSA responses (Set Mode, Set DAC, STOP, seek-complete) were made
visible on BUTCH bit 13 in the same instant the command word was written
to DS_DATA. If the 68K's timeslice ended in the few-instruction window
between writing a command and its first poll iteration, the game's GPU
CD ISR - entered for a routine FIFO half-full service, near-continuous
while streaming - saw bit 13 already set, acked DSCNTRL and consumed the
response from DS_DATA. The 68K then polled bit 13 forever.

Device-traced on Primal Rage (bios boot): the attract loop's CDDA
hand-off sends $1501/$7001 and blocks on the $70nn echo in a wait loop
at $3544 (poll BUTCH bit 13). Four attract cycles won the race; the
fifth lost it ~103 s in - screen permanently black from frame ~6180,
cd_seek_wedge firing, never recovering out to 30k frames.

On real hardware a response word arrives hundreds of microseconds after
the command (DSA serial link + drive MCU turnaround), so it is never
already pending while that ISR runs. Model that: queued responses become
visible only in BUTCHExec after DSA_RESPONSE_DELAY_TICKS (4 halflines,
~127 us). JaguarExecuteNew runs the 68K slice first after every event
boundary, so a polling 68K now always sees its response before the GPU
ISR can run; a GPU-consumed response with no 68K waiter (this game's
normal $12xx seek flow) works exactly as before, one delay later. A
DSCNTRL ack with words still queued re-arms the delay instead of
stranding them.

Savestate: CDROM chunk gains the DSA queue + delay counter (the queue
was never serialized); STATE_VERSION 3 -> 4.

Validation:
- Primal Rage (bios) attract loops indefinitely across 12k frames
  (title screen + attract cards render on the second loop).
- cd_boot_matrix: 21/21 title x mode rows identical to bfbcd6a baseline
  (one transient Battle Morph HARNESS_HANG reproduced as CPU contention,
  back to baseline pc_escape standalone).
- make TEST_EXPORTS=1 test exit 0; audio pair unchanged (Skyhammer/IS2
  still expected-fail loud, Iron Soldier 1 presence in envelope);
  sram_test.sh 5/5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In slave mode (Jaguar CD titles) the DSP I2S ISR is clocked by
JERRYI2SCallback at a fixed 22.675737 us (44100 Hz), but the DAC
resampler derived its ring-consumption ratio from SCLK regardless of
SMODE.  Games leave SCLK at the reset value 19 (20.8 kHz), so the
resampler consumed under half of each frame's LTXD/RTXD samples and
discarded the rest at the DACPrepareFrame ring reset -- a 60 Hz chop
plus octave-down playback over ALL slave-mode DSP output, CD music and
synth SFX alike.  Device-reported as 'crunched / over volume clipped /
static sounding' on Primal Rage, Baldies and Iron Soldier 2 CD.

Measured on Primal Rage (BIOS mode): game runs SMODE=$14 (slave) with
SCLK=19 -> ratio 0.4328 vs the 0.9187 the delivery rate requires.

Master-mode titles are unaffected: audible carts must set
SMODE_INTERNAL (the master-mode branch is the only one that asserts
the I2S interrupt), and that path still derives the rate from SCLK.
SMODE writes now also refresh the ratio immediately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three defects in SetSSIWordsXmittedFromButch(), the feed for the DSP
CD-audio mix ISR's LRXD/RRXD reads:

- Channels were swapped: bytes [ptr+2..3] went to LRXD and [ptr+0..1]
  to RRXD.  Red Book interleaves LEFT first ([4i+0..1] left LE,
  [4i+2..3] right LE).
- The head inherited the BUTCH FIFO one-word capture skew
  (ssiBufPtr = 2 at seek).  That skew belongs to BUTCH's FIFO entry
  assembly on the data path (cdBufPtr, unchanged); the drive frames
  the audio word stream on sample boundaries.  With advance-then-read
  the skew masked the channel swap for the first sector, then the
  wrap reset flipped the phase and every later sector played with
  L/R truly swapped.
- ssiBlock was not advanced past the sector preloaded at seek time,
  so the first sector played twice (~27 ms stutter after every seek).

The head now reads sample-aligned from byte 0, refills eagerly on
exhaustion (keeps ButchIsReadyToSend() true across sector boundaries),
and starts the disc reload at block + 1.

test(cd): add test/test_cd_ssi_stream -- seeks an audio track over the
DSA protocol, ticks BUTCHExec to completion, then compares 1764
delivered samples against the disc image bytes across 3 sector
boundaries, plus pause-silence/hold and exact-position resume.  RED on
the pre-fix core (first sample already skewed+swapped), GREEN now.
Gated on VJ_SSI_DISC like the other commercial-disc tests; lrxd/rrxd
and SetSSIWordsXmittedFromButch join the test-only export ABI.

Verified: make TEST_EXPORTS=1 test exits 0; test_audio_clipping
(Karts negative control PASS, Skyhammer/IS2 cart still expected-fail,
unchanged) and test_audio_presence (Iron Soldier 1 in envelope) both
pass; contract test green on Primal Rage, Iron Soldier 2 CD, Baldies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e heap

Primal Rage in HLE boot mode wedged forever at the Probe logo with fully
silent audio (video_stall frame 530, 68K parked in its 'NRL1' exception
trap). Two root causes, found by diffing VJ_CD_TRACE between hle and bios
runs and disassembling the wedge site:

1. Heap corruption by CD_read side-writes. Games allocate CD-read buffers
   from their own heap (Primal Rage: first-fit free list at $1FB750,
   {next,len} node 8 bytes before each block). The free-list remainder
   node sits just past the allocation, and the HLE CD_read's 2352-byte
   boot-sector mirror at dest+count+64 overwrote it. The game's heap
   validation walk then chased $FFFFFFFF -> address error (vector 3 at
   $039788) -> vectors 2-11 handler pushes 'NRL1' and halts. The mirror
   is removed; the cart-space copy (and the 64-byte ATRI block + 8-byte
   $FFFF padding, which fit inside allocation slack) stay.

2. No CD->I2S streaming path in HLE. After a CD_read, real hardware
   leaves the drive playing one sector past the transferred data; the
   game then calls CD_I2S_enable + CD_set_DAC_mode with SMODE in slave
   mode and the drive streams audio over I2S->SSI into the DSP. Both
   vectors were RTS stubs, and with the SSI head unprimed
   ButchIsReadyToSend() stayed false — the DSP received zero slave-mode
   SSI interrupts, so its entire audio driver (music AND synth SFX) was
   unclocked. New CDROMHLEStartAudio()/CDROMHLESetAudioPlaying() prime
   the SSI head and drive the existing BUTCH->SSI delivery; jagcd_hle.c
   tracks the post-read LBA and hooks CD_I2S_ENABLE (start),
   CD_STOP_DRIVE/CD_PAUSE (stop), CD_UNPAUSE (resume). A bit-31
   just-seek CD_read now also starts playback at the requested timecode,
   per the Jaguar CD-ROM manual p.8 CDDA-play API.

Also adds VJ_CD_TRACE_LIVE=1: log each CD trace event as it is pushed
(the 256-entry ring dump only fired on cd_seek_wedge, hiding early boot
divergence).

Result: Primal Rage HLE boots to title + attract with music (audio RMS
~1332, onset frame 421; was 0). Full test suite passes; CD boot matrix
unchanged for every other title/mode.

NOTE: committed unsigned (1Password SSH signer unreachable from this
session) — amend/re-sign before pushing if signed history is required.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Primal Rage arena floor)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the op TOM Object Processor (sprites/bitmaps) label Jul 17, 2026
JoeMatt and others added 10 commits July 16, 2026 23:03
Instrumentation ahead of the FMV scene-sequencing work: log any traffic
on SBCNTRL/SUBDATA/SUBDATB/SB_TIME (currently RAM-backed no-ops) and
edges on BUTCH interrupt-enable bits 2 (subcode frame-time) / 3
(SB_TIME time-match).

Negative result already established with this layer: Dragon's Lair,
Space Ace, and BrainDead 13 generate ZERO subcode register traffic in
BIOS mode across 2400-4500 headless frames, with and without --press
input -- the ReadySoft-class engines really do dead-reckon the disc
schedule (48-bit clock in RAM, GPU-timer incremented) rather than read
subcode position.  The FMV scene-jump bug is therefore schedule drift,
not missing subcode; subcode emulation stays unbuilt until a title
actually arms it (this diagnostic will catch the first one that does).

Log-only change.  Note: test_cd_hle_idempotent currently fails on this
branch from the earlier streamed-CD_read merge (pre-existing, verified
on unmodified HEAD; tracked separately).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-current bank

Per the JTRM ("Systolic Matrix Multiplies"), MMULT's packed vector
operand is always read from the SECONDARY register bank — bank 1, an
absolute reference — while our GPU and DSP cores read
*_alternate_reg, i.e. "whichever bank is not currently selected".

The two coincide whenever the mixer runs in interrupt service (IMASK
forces bank 0 current, so alternate == bank 1), which is why most
titles never noticed.  But an audio engine that mixes in mainline
code with REGPAGE=1 selects bank 1 as its CURRENT bank, loads the
sample vector there, and issues MMULT — our emulation then multiplied
the volume matrix by bank 0, the I2S interrupt handler's pointers and
masks ($F1A14C, $FFFF, $F000...), producing rail-to-rail square waves:
the "loud crunched/clipped music" class.

Confirmed by disassembling Baldies' DSP mixer (output stage at
$F1C50A: mmult -> sharq #7 -> sat16s) and tracing operands at runtime:
FLAGS=$4466 (REGPAGE=1, IMASK=0), vector regs contained bank-0
addresses instead of the just-loaded samples.

Fixes (all measured, BIOS-mode 60 s captures, 48 kHz):
- Baldies (CD): menu/game music 60-95% saturated, RMS ~28000 ->
  0.00% saturated, RMS ~800, proper stereo
- Skyhammer: clipping signature gone (0% sat, RMS 3080) — was an
  expected-fail in test_audio_clipping since the test was added
- Iron Soldier 2: same (0% sat, RMS 2599, was 17% saturated)

Baseline update called out per CLAUDE.md: the Makefile known-broken
manifest drops --expect-clipping for Skyhammer and IS2 — they now
assert clean audio so any regression flips them red again.

No change to: Iron Soldier 1 presence envelope (RMS 1175.7, matches
develop baseline), Baldies intro FMV (sample-identical audio, video
renders), Primal Rage HLE CD audio (0% sat), regression suite
determinism/frameskip/savestate/rewind (8/8).  make TEST_EXPORTS=1
test passes except the pre-existing test_cd_hle_idempotent failure,
which reproduces identically without this change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…— fixes Baldies/Skyhammer/IS2 clipped audio
…streamed fill.

Signed-off-by: Joseph Mattiello <git@joemattiello.com>
…s Baldies HLE black screen

Baldies booted to a black screen in HLE mode: the 68K spun forever at
$5FE0C waiting for its frame counter ($60CC0), which its own GPU OP
ISR increments — except the 4-byte increment at $F030C0 had been
zeroed.  The zeroing came from the game's engine init doing
`clr.l (a0)` on whatever A0 its last BIOS call left behind.  On real
hardware that scratch-clears past the loaded data; in HLE our CD_poll
stub had planted the GPU-RAM struct pointer ($F030C0) in A0, aiming
the clear straight at the game's own GPU code.

The real resident BIOS was disassembled live (BIOS-mode session,
functions at $3610/$3624/$3310/$31CA/$3078) and its register ABI
replicated:

- CD_poll ($304E) returns A0 = [struct+0], the ADVANCING destination
  pointer, and A1 = [struct+8], the ISR error status — not the struct
  pointer and not a progress count.  Boot stubs abort the load when
  A1 is nonzero (Baldies $4DA2) and poll until A0 passes their end
  address, so both halves matter.
- [struct+0] models the real GPU ISR's pre-decremented FIFO write
  pointer: armed at dest-4, and on completion it OVERSHOOTS past the
  requested end by up to one 32-byte FIFO batch (module code drains
  while ptr <= end).  Primal Rage exits its poll loop on
  `cmpa.l a6,a0 / blt` with a6 = end — an exact stop at end-4 or end
  never terminates it; the overshoot is load-bearing ABI.
- [struct+8] is zeroed (real CD_read $366A); it is an error field,
  not a byte count.
- CD_read ($303C) leaves A0 = dest-4 (the subq.l #4,a0 at $362C is
  visible to callers).  Applied only to accepted data reads: audio-
  mode plays that we reject (Highlander retries its CDDA call every
  frame against our unmodeled audio path) would otherwise accumulate
  -4s into a live game pointer.

Result: Baldies HLE now boots to its animated title menu with clean
music (RMS ~800, 0% saturation, matches BIOS mode).  HLE boot sweep
(VJ_TEST_CD_FRAMES=900): 8/9 PASS — Baldies, Battle Morph,
BrainDead 13, Dragon's Lair, Highlander, Hover Strike, Primal Rage,
Space Ace; Iron Soldier 2's failure is pre-existing (identical
signature without this change).  make TEST_EXPORTS=1 test exits 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stops stomping game GPU code

Two related fixes for BIOS-mode CD titles:

1. Hover Strike B-skip lockup (deterministic at press frames
   150/200/550/650): GPUSetIRQLine latched+dispatched interrupts while
   the GPU was halted (GPUGO=0).  A stale DSA-response latch captured
   during the driver's stopped-GPU maintenance window dispatched at
   restart before the new GPU program's r31 stack init, pushing the
   return address into GPU code (the PIT stub's movei operand at
   $F03114) — the game's schedule clock at $5DC2E froze and the 68K
   polled forever.  A halted RISC samples nothing: asserts are now
   dropped while stopped (the source's level re-edges after restart),
   GPUHandleIRQs bails while halted, and the G_CTRL write handler
   applies GO before processing the CPU->GPU INT0 strobe so a single
   GO|INT0 write still works.  The BUTCH GPU latch assert is now
   edge-paced (once per service cycle, re-armed by the ISR's BUTCH+2
   ack write) instead of once per BUTCHExec tick — per-tick assert
   kept a latch pending at every instant, per-onset starves transfers,
   and edges arriving during a stopped window stay armed so the first
   tick after restart re-asserts them.

2. Myst BIOS boot crash (GPU flies to $ACC5A186 at the post-load
   Pause): the BIOS strategy's auth-magic hook wrote $03D0DEAD to
   $F03000 on EVERY 68K execution of PC=$005E40.  That address is
   BIOS-phase code, but Myst loads its game code at $5000-$1D380 and
   hot paths cross $5E40 — each crossing overwrote the first two words
   of Myst's GPU IRQ0 handler stub (`movei #$F0D000,r0 / jump (r0)`;
   Myst uses IRQ0 as its 68K->GPU command doorbell, G_CTRL=$05 kicks).
   The corrupted slot dispatched through stale r0 into garbage.  The
   hook is now gated on the boot stub not yet having been injected —
   after BIOS->game handoff the game owns that RAM.  Myst now boots
   and runs its command loop (it renders black in headless — separate
   issue, likely its CDDA-driven attract flow; the crash is gone).

Verified: Hover Strike B-press sweep 150/200/350/550/650 all clean;
Myst BIOS 1500 frames no crash signature; full suite passes (56103);
CD boot matrix 9 titles x 2 modes no regressions, Baldies HLE improved
0/1 -> 1/1.  test_hle_bios 9d/9d-cd and test_butch_cd DSA-routing test
updated to pin the new halted-GPU semantics (latch mechanics now run
with GPUGO=1; new sub-assert pins assert-drop while halted).

Tooling: cd_wedge_probe gains --ram-dump / --snap / --snap-prefix
(full main-RAM + GPU-RAM snapshots) and fixes the jaguarMainRAM
pointer-variable dereference bug that made all prior RAM dumps read
host heap instead of emulated RAM.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…head past them

Battle Morph (BIOS mode) hung with the 68K executing wild addresses
($8xDFFFxx BUTCH-mirror reads) right after boot-stub injection.  Two
defects in the boot-stub shortcut, both title-size dependent:

1. The hook injected the boot executable and then let the 68K resume
   at $050176, relying on the BIOS's own `JSR $80000.l` there.  Battle
   Morph's executable is $65290 bytes loaded at $4400 — ending at
   $69690, past $050176 — so the injection overwrote that BIOS RAM
   code and the 68K executed injected DATA.  The hook now redirects
   the 68K PC straight to $080000 (the game entry, or our trampoline
   when loadAddr differs); the BIOS JSR return address was never used
   — CD games do not return to the BIOS.  (Myst's $18380-byte exe at
   $5000 ends at $1D380, which is why it survived.)

2. The real BIOS leaves the drive head parked just past the boot
   executable it streamed; Battle Morph resumes reading with a bare
   Pause/Unpause pair and NO seek, so data must flow from there.  Our
   shortcut left the head at block 0 (session-1 audio).  New
   CDROMSetHeadPosition() parks the head at the exe-end LBA (exposed
   via CDIntfGetBootStubEndLBA) with the data-path capture-skew
   framing, paused, after every injection.

Battle Morph BIOS now boots into game code and streams its intro FMV
from the correct disc position (blocks 15701+).  It still renders
black — same open display-path bucket as Myst — but the wild-PC hang
class is gone.

Verified: full suite passes; Hover Strike B-skip sweep clean;
Myst BIOS boots with no crash signature; BrainDead 13 BIOS still
renders (visual canary).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…$15)

The real CD BIOS's DSP module programs JERRY's serial clock during boot
(SCLK=$13 ~20 kHz, SMODE=$15 INTERNAL/master — visible as
"SMODE $0000 -> $0015" in every BIOS-mode log), and the cart-HLE reset
block replicates that for cartridges — but the CD-HLE boot path skipped
it (no cart inserted, so JaguarReset's HLE block never runs; jagcd_hle
re-creates vectors/auth-magic itself and forgot I2S).  Without a
running I2S clock JERRY never fires SSI interrupts, and games whose DSP
engine idles on an interrupt-set event mask never advance: Battle
Morph's DSP main loop spins forever on r22==0 at $F1B580 (its ISRs set
event bits 24-27 that the loop dispatches on).  With the fix its DSP
engine runs.

Battle Morph still shows a black screen: its 68K code at $4400-$69690
gets shredded 8-bytes-per-24 at runtime by blitter row-blits
(A1f=$42A3: pitch=3, intended dest the $1C0xxx framebuffer) whose
computed addresses land at $4408+ — a blitter A1 addressing bug,
tracked separately as the next investigation.

Tooling: cd_wedge_probe snapshots now also dump DSP RAM
($F1B000-$F1CFFF) alongside main/GPU RAM.

Verified: full suite passes; Hover Strike B-skip sweep clean; Battle
Morph DSP event loop confirmed running post-fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ilent one

Philia (BIOS mode) froze mid-game: it seeks to the block the drive is
already streaming ($1006/$1105/$123E -> LBA 27287 while at 27287),
which hits the redundant-seek guard.  The guard exists so boot-stub
CD_read retry loops don't restart the seek state machine (cycling
dsaResponseReady kept BUTCH bit 13 set and masked the FIFO-data path),
but it also swallowed the DSA response entirely.  Real hardware
answers every $12xx Goto with Found ($0100), motion or not.  Philia's
GPU driver waits for that seek-complete IRQ, stops draining the FIFO,
and the game wedges (cd_seek_wedge with seek_starts == seek_dones and
frozen drains, straight from the device log).

Queue the $0100 through DSAQueuePush in the guard branch: paced
delivery like any immediate DSA answer, no seekDelay restart, stream
and cdBufPtr untouched — the guard's original purpose is preserved.

Verified: full suite passes; Hover Strike B-skip sweep and Myst BIOS
boot clean; boot-matrix chunk all 1/1 with Battle Morph BIOS moving
0/1 HARNESS_HANG -> 1/1 GAME_CODE.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bios Embedded BIOS / boot stubs / HLE BIOS 📖 documentation build Makefile / Makefile.common / scripts/ cd Jaguar CD (BUTCH/cdrom/cdintf) ci/cd 🤖 Nothing to see, just CI/CD. core Core orchestration (jaguar.c, event, memory, settings) dsp JERRY DSP (audio coprocessor) gpu TOM GPU (graphics RISC) libretro-glue libretro.c API surface + core options m68k Motorola 68000 (UAE-derived main CPU) op TOM Object Processor (sprites/bitmaps) tests test harnesses, regression baselines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants