Add Jaguar CD support (CUE/BIN, CDI, ISO) — supersedes #120#201
Open
JoeMatt wants to merge 85 commits into
Open
Add Jaguar CD support (CUE/BIN, CDI, ISO) — supersedes #120#201JoeMatt wants to merge 85 commits into
JoeMatt wants to merge 85 commits into
Conversation
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>
…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>
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>
…Baldies HLE boots
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 insrc/cd/cdrom.c, an HLE CD BIOS path insrc/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$2C00table (verified by disassembling Baldies'$4E18and Primal Rage's$0803E2scanners); 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/callcontinuation 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 optionvirtualjaguar_cd_trace/ envVJ_CD_TRACE=1), and acd_seek_wedgecrash-detect signature so CD hangs self-diagnose in the RetroArch log.Per-title snapshot (headless, 3000 frames)
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_stallsignatures) rather than hanging silently.Known issues / backlog
baldies.cdiloader segfault: pre-existing CDI-parser crash at load time (CUE variant unaffected).Testing
make TEST_EXPORTS=1 test, 56k+ assertions incl. both audio tests) — no cart regressions.$2C00TOC contract (both scanner algorithms, reviewer-reproduced on a real disc), HLE CD_read idempotency (RED 27 corruptions → GREEN 0).🤖 Generated with Claude Code