Skip to content

Update from feature/jaguar-cd-support#10

Draft
github-actions[bot] wants to merge 84 commits into
masterfrom
feature/jaguar-cd-support
Draft

Update from feature/jaguar-cd-support#10
github-actions[bot] wants to merge 84 commits into
masterfrom
feature/jaguar-cd-support

Conversation

@github-actions

Copy link
Copy Markdown

This is an automated pull request to update from branch feature/jaguar-cd-support

@github-actions

github-actions Bot commented Apr 29, 2026

Copy link
Copy Markdown
Author

Regression: macos-arm64

Regression Test Results

ROM Status Details Diff
jagniccc 🆕 NEW no baseline -
yarc 🆕 NEW no baseline -
jagniccc (determinism) ✅ PASS identical across runs -
yarc (determinism) ✅ PASS identical across runs -
jagniccc (frameskip) ✅ PASS skip=0 matches skip=3 -
yarc (frameskip) ✅ PASS skip=0 matches skip=3 -
jagniccc (save state) ✅ PASS round-trip matches -
yarc (save state) ✅ PASS round-trip matches -
jagniccc (rewind) ✅ PASS rewind matches -
yarc (rewind) ✅ PASS rewind matches -

Platform: Darwin arm64

Updated by CI at 2026-07-14T07:45:56.906Z

@github-actions

github-actions Bot commented Apr 29, 2026

Copy link
Copy Markdown
Author

Regression: linux-x64

Regression Test Results

ROM Status Details Diff
jagniccc 🆕 NEW no baseline -
yarc 🆕 NEW no baseline -
jagniccc (determinism) ✅ PASS identical across runs -
yarc (determinism) ✅ PASS identical across runs -
jagniccc (frameskip) ✅ PASS skip=0 matches skip=3 -
yarc (frameskip) ✅ PASS skip=0 matches skip=3 -
jagniccc (save state) ✅ PASS round-trip matches -
yarc (save state) ✅ PASS round-trip matches -
jagniccc (rewind) ✅ PASS rewind matches -
yarc (rewind) ✅ PASS rewind matches -

Platform: Linux x86_64

Updated by CI at 2026-07-14T07:46:45.477Z

@github-actions

github-actions Bot commented Apr 29, 2026

Copy link
Copy Markdown
Author

Regression: linux-arm64

Regression Test Results

ROM Status Details Diff
jagniccc 🆕 NEW no baseline -
yarc 🆕 NEW no baseline -
jagniccc (determinism) ✅ PASS identical across runs -
yarc (determinism) ✅ PASS identical across runs -
jagniccc (frameskip) ✅ PASS skip=0 matches skip=3 -
yarc (frameskip) ✅ PASS skip=0 matches skip=3 -
jagniccc (save state) ✅ PASS round-trip matches -
yarc (save state) ✅ PASS round-trip matches -
jagniccc (rewind) ✅ PASS rewind matches -
yarc (rewind) ✅ PASS rewind matches -

Platform: Linux aarch64

Updated by CI at 2026-07-14T07:47:03.077Z

JoeMatt and others added 11 commits May 6, 2026 20:37
Bumps the actions-minor-patch group with 1 update: [cirrus-actions/rebase](https://github.com/cirrus-actions/rebase).


Updates `cirrus-actions/rebase` from 1.4 to 1.8
- [Release notes](https://github.com/cirrus-actions/rebase/releases)
- [Commits](cirrus-actions/rebase@1.4...1.8)

---
updated-dependencies:
- dependency-name: cirrus-actions/rebase
  dependency-version: '1.8'
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [actions/labeler](https://github.com/actions/labeler) from 5 to 6.
- [Release notes](https://github.com/actions/labeler/releases)
- [Commits](actions/labeler@v5...v6)

---
updated-dependencies:
- dependency-name: actions/labeler
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](actions/cache@v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](codecov/codecov-action@v5...v6)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
…ns/develop/codecov/codecov-action-6

ci: Bump codecov/codecov-action from 5 to 6
…ns/develop/actions/cache-5

ci: Bump actions/cache from 4 to 5
…ns/develop/actions/labeler-6

ci: Bump actions/labeler from 5 to 6
…ns/develop/actions/upload-artifact-7

ci: Bump actions/upload-artifact from 4 to 7
…ns/develop/actions-minor-patch-4f0f6e5aaa

ci: Bump cirrus-actions/rebase from 1.4 to 1.8 in the actions-minor-patch group
@JoeMatt
JoeMatt force-pushed the feature/jaguar-cd-support branch from 806f321 to 77b0675 Compare May 7, 2026 02:19
JoeMatt and others added 15 commits May 6, 2026 23:44
Signed-off-by: Joseph Mattiello <git@joemattiello.com>
PR libretro#170 (closed without merge) shipped a test_audio_clipping pass on
Iron Soldier 2 that read "0 saturated samples / RMS=521" — which is
silence, not fixed audio.  The clipping test couldn't tell those
apart: it only flags loud-broken output (saturation density, run
length, sustained loud RMS), so a "fix" that drops RMS to zero
sailed through.

test_audio_presence is the missing counterpart.  It runs N frames,
captures the post-onset window, and asserts:

- first_audio_frame is reached (some |s| > 32 seen)
- window RMS lies in [floor, ceiling] -- per-ROM tunables via CLI
- longest run of near-zero stereo frames < max_zero_run_pct of window

Wired into `make test` against Iron Soldier 1 (boots straight to a
music-on title, develop measures RMS ~1175).  Floor 200 catches the
silencing-regression class, ceiling 25000 catches the loud-broken
class.  Same skip-if-missing pattern as the rest of the audio tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PR libretro#170 lesson: a clipping-only test missed the silencing regression
where Iron Soldier 2 went from 17% saturated → RMS=521 (silent).
Document `test_audio_clipping` + `test_audio_presence` as a pair, and
add a "Audio / DSP work — required tests" section that spells out
the required runs before declaring an audio change done.  Also pin
the rule into the sub-agent guidelines so worktree agents don't ship
the same masked-failure pattern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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 libretro#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; libretro#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 libretro#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 libretro#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.
altiereslima and others added 5 commits June 5, 2026 11:20
The fast blitter's DCOMPEN+!CMPDST path inhibited writes only where the
source pixel was zero. The hardware data comparator (DATACOMP) also
inhibits where the source equals PATTERNDATA -- the colour-key the
accurate gate-level path already implements (dcomp = patd ^ srcd, see
MiSTer comp_ctrl.v). Blits keying transparency on a non-zero colour drew
their transparent pixels as a solid colour instead of letting the
background show through.

Add the PATTERNDATA equality test alongside the existing source-zero
test (inhibit on srcdata == 0 || srcdata == PATTERNDATA) in both
data-movement directions. The source-zero test is KEPT, so the
pixel-level transparency the accurate path was augmented to match
(develop's COMP_CTRL zero-mask) is preserved -- no regression for the
source-zero sprite-blit family. BCOMPEN keeps its single-bit
"transparent on 0" test. The COMP_CTRL comment is updated to note the
fast path now keys on both zero and PATD.

Repro: Theme Park's grass tiles key transparency on colour 0x04; with
the fast blitter those pixels were drawn as CLUT[4] instead of being
transparent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-and-dcompen-key

Fix/blitter a2 wrap and dcompen key
…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.
JoeMatt and others added 14 commits July 13, 2026 05:09
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>
…ristic

CD_read ($303C) is a discrete, stateless read: D0 (packed MSF) fully
specifies the source position, A0/A1 the destination range, D1 the sync
marker locked onto after the seek. A re-issued byte-identical CD_read
re-seeks to the SAME position and reproduces the SAME data -- there is no
per-call continuation pointer. (Holds under both readings of the real
service: stateless-idempotent, or a BIOS that streams internally; a repeat
with an identical MSF is a restart from the same position either way.)

The removed heuristic (hle_next_lba = scanLBA + s writeback + the
"repeated read -- resuming from LBA" block, extended by 4844f6b into the
raw-fallback and counter-ID paths) was a tuned guess, not a disassembled
contract -- its comment admitted it "mimics the I2S stream real hardware
would still be feeding." The +3/call Iron Soldier 2 exhibited is just
s = ceil(0x14FE / 2352) = 3 sectors per transfer.

Trace, IS2 (hle), identical D0=$00320001 D1=$4438410B dest=$00CB00 every call:
  BEFORE: read 1 finds the sync block at LBA 224857; each identical repeat
          resumes +3 (224860, 224863, ...), misses the sync block, and
          streams raw garbage into $00CB00 -- corrupting the good first read.
  AFTER:  every identical repeat re-finds the sync block at LBA 224857 and
          transfers the same correct bytes; dest is stable.

RED/GREEN: test/test_cd_hle_idempotent.c counts destination-region content
changes after first population -- 27 (RED) -> 0 (GREEN).

Matrix (current HEAD, before vs after at matched frame budgets): IS2 hle,
Baldies hle, Primal Rage hle/bios, BrainDead 13 hle, Dragon's Lair hle,
Baldies bios all unchanged. bios rows structurally unaffected (heuristic is
HLE-path only). IS2 first read now stays correct/idempotent but IS2 does NOT
boot past the loop -- a downstream game-logic re-read remains (separate
defect); not re-tuned to force a boot. "Baldies shares the heuristic"
hypothesis NOT supported by the trace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ge log level; run idempotency test in suite

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants