Skip to content

Add Jaguar CD support with CUE/BIN and CHD image loading#109

Open
JoeMatt wants to merge 31 commits intolibretro:masterfrom
Provenance-Emu:claude/add-jaguar-cd-support-hOzev
Open

Add Jaguar CD support with CUE/BIN and CHD image loading#109
JoeMatt wants to merge 31 commits intolibretro:masterfrom
Provenance-Emu:claude/add-jaguar-cd-support-hOzev

Conversation

@JoeMatt
Copy link
Copy Markdown
Collaborator

@JoeMatt JoeMatt commented Apr 16, 2026

This pull request integrates the libchdr library as a dependency, enabling CHD disc image support in the project. It adds the library's source code, build configuration, and comprehensive cross-platform CI workflows. The most important changes are summarized below.

Build System Integration and Source Inclusion:

  • The Makefile.common is updated to include libchdr as a dependency, adding its include paths, source files, and necessary compiler flags for CHD support. ([[1]](https://github.com/libretro/virtualjaguar-libretro/pull/109/files#diff-f190d29e2cad4f32539c8b73d8af07c77f0aac63b5c6553c06b117c08a70b494R2), [[2]](https://github.com/libretro/virtualjaguar-libretro/pull/109/files#diff-f190d29e2cad4f32539c8b73d8af07c77f0aac63b5c6553c06b117c08a70b494R13-R19), [[3]](https://github.com/libretro/virtualjaguar-libretro/pull/109/files#diff-f190d29e2cad4f32539c8b73d8af07c77f0aac63b5c6553c06b117c08a70b494R59-R78))

libchdr Library Addition:

  • The full libchdr source code and dependencies are added under deps/libchdr, including license, README, and all necessary files to build and use the library. ([[1]](https://github.com/libretro/virtualjaguar-libretro/pull/109/files#diff-abd5ac3bb94c64e105483f5faf36fa21ebe709a176890bead73b2901d88ce55eR1-R7), [[2]](https://github.com/libretro/virtualjaguar-libretro/pull/109/files#diff-1b8ba7656932a95a1543b3d24df279d2aa5602ab27c62d896c51cd9b55c15cc6R1-R24), [[3]](https://github.com/libretro/virtualjaguar-libretro/pull/109/files#diff-053801adbf44631113c7c7d4a686235a4a256e402db546f444bc8e79b104ca37R1-R3), [[4]](https://github.com/libretro/virtualjaguar-libretro/pull/109/files#diff-b906f2c0d7e47818baed3f8a4a02da10dc2b0b763b30d0db06a363109c68287cR1-R181), [[5]](https://github.com/libretro/virtualjaguar-libretro/pull/109/files#diff-c5c7fc96865e9fed47725023ad159ba5e5868298365c42f8dc0653bd0c0e0470R1-R341))

Build System and Configuration for libchdr:

  • A CMake build system is introduced for libchdr, supporting options for static/shared builds, system libraries, and build optimizations. ([deps/libchdr/CMakeLists.txtR1-R172](https://github.com/libretro/virtualjaguar-libretro/pull/109/files#diff-33f96b07c6b48c3495a5fb6a8ab937c028c9060c04f93108c2318b66d2f7b9eaR1-R172))

Continuous Integration (CI) Enhancements:

  • Cross-platform CI workflows are added for libchdr, covering CMake builds on major platforms (Linux, macOS, Windows), MSYS2 environments, BSD/Haiku/OmniOS, Nintendo Switch, and PlayStation Vita. ([[1]](https://github.com/libretro/virtualjaguar-libretro/pull/109/files#diff-d73a63b10a4819b37a9db04c94bfbcbc2d815740f527208817e23b868fb1040bR1-R19), [[2]](https://github.com/libretro/virtualjaguar-libretro/pull/109/files#diff-bbfe3353020b26283c4dcf8d4f5538360f0690caf0a0cec92a710c8ae381c529R1-R36), [[3]](https://github.com/libretro/virtualjaguar-libretro/pull/109/files#diff-ffc351a06445bfa406becbe0fbe3ec5386f351a5088f74d2f8f668bb4136dec1R1-R45), [[4]](https://github.com/libretro/virtualjaguar-libretro/pull/109/files#diff-50f09d4b3c600489a54bfabf0bc622bef16d071b5df9f219fb3330427f521608R1-R17), [[5]](https://github.com/libretro/virtualjaguar-libretro/pull/109/files#diff-c51f1585a38196e56ac6551307e4fce42e171120386984e1cbdcaa4d9c87cdb4R1-R17))

These changes collectively enable robust CHD image support, ensure build reliability across platforms, and make future maintenance and integration of libchdr straightforward.

@JoeMatt JoeMatt self-assigned this Apr 16, 2026
Copilot AI review requested due to automatic review settings April 16, 2026 03:12
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Jaguar CD disc image support to the core by integrating libchdr (for CHD), implementing CUE/BIN parsing and sector reads, and updating the libretro frontend to boot via the Jaguar CD BIOS when .cue/.chd content is loaded.

Changes:

  • Added CD image loading APIs and implemented CUE/BIN + CHD sector reading in cdintf.*.
  • Updated CDROM/Butch emulation paths and added core option for selecting Retail vs Developer CD BIOS.
  • Vendored deps/libchdr and wired it into the build via Makefile.common (plus upstream CMake/CI files included under deps/libchdr).

Reviewed changes

Copilot reviewed 65 out of 69 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/settings.h Adds settings fields/enums for enabling CD BIOS and selecting CD BIOS type.
src/cdrom.c Updates Butch status/interrupt behavior and sector/audio delivery reads.
src/cdintf.h Expands the CD interface API and adds disc image load/unload helpers and data structures.
src/cdintf.c Implements CUE/BIN parsing, CHD parsing/reads, and image-backed sector reads.
libretro_core_options.h Adds core option to select Retail vs Developer Jaguar CD BIOS.
libretro.c Enables .cue/.chd loading, boots via CD BIOS, and loads cart ROMs from path when needed.
Makefile.common Adds libchdr include paths/defines and compiles vendored libchdr sources.
deps/libchdr/unity.c Unity build translation unit for libchdr + bundled deps sources.
deps/libchdr/src/link.T Version-script for symbol visibility when building shared libchdr.
deps/libchdr/src/libchdr_huffman.c Vendored libchdr implementation (Huffman).
deps/libchdr/src/libchdr_flac.c Vendored libchdr implementation (FLAC wrapper).
deps/libchdr/src/libchdr_codec_zstd.c Vendored libchdr codec implementation (Zstd).
deps/libchdr/src/libchdr_codec_zlib.c Vendored libchdr codec implementation (Zlib/miniz).
deps/libchdr/src/libchdr_codec_lzma.c Vendored libchdr codec implementation (LZMA).
deps/libchdr/src/libchdr_codec_huff.c Vendored libchdr codec implementation (Huffman).
deps/libchdr/src/libchdr_codec_flac.c Vendored libchdr codec implementation (FLAC).
deps/libchdr/src/libchdr_codec_cdzs.c Vendored libchdr CD codec implementation (CD + Zstd).
deps/libchdr/src/libchdr_codec_cdzl.c Vendored libchdr CD codec implementation (CD + Zlib).
deps/libchdr/src/libchdr_codec_cdlz.c Vendored libchdr CD codec implementation (CD + LZMA).
deps/libchdr/src/libchdr_codec_cdfl.c Vendored libchdr CD codec implementation (CD + FLAC).
deps/libchdr/src/libchdr_bitstream.c Vendored libchdr bitstream implementation.
deps/libchdr/pkg-config.pc.in pkg-config template for standalone libchdr builds.
deps/libchdr/include/libchdr/macros.h Vendored libchdr public header (macros).
deps/libchdr/include/libchdr/huffman.h Vendored libchdr public header (Huffman).
deps/libchdr/include/libchdr/flac.h Vendored libchdr public header (FLAC).
deps/libchdr/include/libchdr/coretypes.h Vendored libchdr public header (core file callbacks/types).
deps/libchdr/include/libchdr/codec_zstd.h Vendored libchdr public header (Zstd codec).
deps/libchdr/include/libchdr/codec_zlib.h Vendored libchdr public header (Zlib codec).
deps/libchdr/include/libchdr/codec_lzma.h Vendored libchdr public header (LZMA codec).
deps/libchdr/include/libchdr/codec_huff.h Vendored libchdr public header (Huffman codec).
deps/libchdr/include/libchdr/codec_flac.h Vendored libchdr public header (FLAC codec).
deps/libchdr/include/libchdr/codec_cdzs.h Vendored libchdr public header (CDZS codec).
deps/libchdr/include/libchdr/codec_cdzl.h Vendored libchdr public header (CDZL codec).
deps/libchdr/include/libchdr/codec_cdlz.h Vendored libchdr public header (CDLZ codec).
deps/libchdr/include/libchdr/codec_cdfl.h Vendored libchdr public header (CDFL codec).
deps/libchdr/include/libchdr/chdconfig.h Vendored libchdr public header (feature config).
deps/libchdr/include/libchdr/chd.h Vendored libchdr public header (CHD API).
deps/libchdr/include/libchdr/cdrom.h Vendored libchdr public header (CD constants/helpers).
deps/libchdr/include/libchdr/bitstream.h Vendored libchdr public header (bitstream).
deps/libchdr/deps/zstd-1.5.7/zstd_errors.h Bundled Zstd error definitions for libchdr.
deps/libchdr/deps/zstd-1.5.7/CMakeLists.txt CMake build file for bundled zstd static lib.
deps/libchdr/deps/miniz-3.1.1/CMakeLists.txt CMake build file for bundled miniz static lib.
deps/libchdr/deps/lzma-25.01/src/LzmaDec.c LZMA decoder compilation unit wrapper for bundled SDK.
deps/libchdr/deps/lzma-25.01/include/real/LzmaDec.h Bundled LZMA SDK header.
deps/libchdr/deps/lzma-25.01/include/real/7zTypes.h Bundled LZMA SDK types header.
deps/libchdr/deps/lzma-25.01/include/LzmaDec.h Namespacing wrapper header to avoid symbol collisions.
deps/libchdr/deps/lzma-25.01/LICENSE Bundled LZMA SDK license.
deps/libchdr/deps/lzma-25.01/CMakeLists.txt CMake build file for bundled LZMA static lib (incl. optional asm).
deps/libchdr/deps/lzma-25.01/Asm/x86/7zAsm.asm Bundled LZMA x86 asm macros.
deps/libchdr/deps/lzma-25.01/Asm/arm64/7zAsm.S Bundled LZMA arm64 asm macros.
deps/libchdr/README.md libchdr README (vendored).
deps/libchdr/LICENSE.txt libchdr license (vendored).
deps/libchdr/CMakeLists.txt Standalone libchdr CMake build definition.
deps/libchdr/.gitignore Ignore build artifacts within vendored libchdr directory.
deps/libchdr/.github/workflows/vita.yml Vendored libchdr CI workflow (Vita).
deps/libchdr/.github/workflows/switch.yml Vendored libchdr CI workflow (Switch).
deps/libchdr/.github/workflows/msys2.yml Vendored libchdr CI workflow (MSYS2).
deps/libchdr/.github/workflows/cross-platform-actions.yml Vendored libchdr CI workflow (BSD/Haiku/OmniOS).
deps/libchdr/.github/workflows/cmake.yml Vendored libchdr CI workflow (Linux/macOS/Windows).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread libretro.c Outdated
Comment on lines +1026 to +1037
int64_t fileSize;

rfseek(romFile, 0, SEEK_END);
fileSize = rftell(romFile);
rfseek(romFile, 0, SEEK_SET);

romData = (uint8_t *)malloc(fileSize);
if (romData)
{
rfread(romData, 1, fileSize, romFile);
JaguarLoadFile(romData, fileSize);
free(romData);
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When loading cartridge ROMs from info->path, fileSize = rftell(romFile) isn’t validated. If rftell fails (returns < 0) or returns an unexpectedly large value, this can lead to a huge/invalid malloc and subsequent read/overflow behavior. Add error checks for fileSize <= 0 (and optionally a sane max size), and verify rfread returned the expected number of bytes before calling JaguarLoadFile.

Copilot uses AI. Check for mistakes.
Comment thread src/cdintf.c Outdated
Comment on lines +718 to +732
// Calculate the file position
// The track's fileOffset tells us where track data starts in the file.
// Then we add the offset for the requested sector within the track.
filePos = (int64_t)(sector - track->startLBA) * sectorSize + track->fileOffset;

// For single-BIN CUE sheets, all tracks are in the same file and fileOffset
// accounts for the absolute position. But for multi-index tracks where INDEX 01
// is the actual start, fileOffset is based on INDEX 01's MSF offset.
// Simpler approach: single BIN file, sectors are sequential.
// File position = sector * sectorSize (for single-file BIN)
filePos = (int64_t)sector * sectorSize;

rfseek((RFILE *)disc.binFile, filePos, SEEK_SET);
bytesRead = rfread(buffer, 1, 2352, (RFILE *)disc.binFile);

Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CDIntfReadBlock() calculates filePos using track->fileOffset/track->startLBA, but then immediately overwrites it with filePos = (int64_t)sector * sectorSize;, which makes the earlier track-based calculation dead code and breaks any CUE that relies on INDEX offsets/pregaps. Additionally, the read always requests 2352 bytes regardless of sectorSize, which will mis-handle MODE1/2048 (and similar) tracks. Use a single, consistent addressing scheme (either track-relative using fileOffset + (sector-startLBA)*sectorSize, or explicitly constrain support to raw-2352 images) and ensure the read length matches the on-disk sector size (padding/expanding to 2352 as needed).

Copilot uses AI. Check for mistakes.
Comment thread src/cdintf.c
Comment on lines +159 to +170
// Build full path
if (dir[0])
snprintf(currentBinFile, sizeof(currentBinFile), "%s%s", dir, binName);
else
snprintf(currentBinFile, sizeof(currentBinFile), "%s", binName);

// If we don't have a bin path set yet, set it as the primary
if (!disc.binPath[0])
snprintf(disc.binPath, sizeof(disc.binPath), "%s", currentBinFile);

fileOffset = 0;
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ParseCueSheet() tracks the current FILE entry in currentBinFile, but only ever stores/opens disc.binPath (the first FILE) and never updates fileOffset based on previous files. As a result, multi-BIN CUE sheets (one BIN per track) will parse but read from the wrong file/offset. Either explicitly reject multi-FILE CUEs (return false with an error) or extend the track model to store/open per-track file paths/handles and compute offsets across files.

Copilot uses AI. Check for mistakes.
Comment thread src/cdrom.c Outdated
Comment on lines +243 to +272
// bit 12: Command to CD drive pending (trans buffer empty if 1)
// Always set when we're ready for commands
butchRead |= (1 << 12);

// bit 13: Response from CD drive pending (rec buffer full if 1)
// Set when we have a response ready (always ready in our emulation)
butchRead |= (1 << 13);

// Store the read-side status
cdRam[BUTCH + 2] = (butchRead >> 8) & 0xFF;
cdRam[BUTCH + 3] = butchRead & 0xFF;

// Generate interrupts through JERRY -> GPU path
// Butch interrupts route through JERRY EXT1 to the GPU
if (butchRead & 0x3E00) // Any interrupt flag pending
{
// Check if any enabled interrupt has a pending flag
bool shouldIRQ = false;

if ((butchWrite & 0x02) && (butchRead & (1 << 9))) // FIFO half-full
shouldIRQ = true;
if ((butchWrite & 0x20) && (butchRead & (1 << 13))) // DSARX (response ready)
shouldIRQ = true;

if (shouldIRQ)
{
// Route through JERRY to GPU via EXT1 interrupt
// The GPU ISR at JERRY_ISR handles Butch interrupts
DSPSetIRQLine(DSPIRQ_EXT1, ASSERT_LINE);
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BUTCHExec() sets the “response pending” flag (bit 13) unconditionally and then raises DSPIRQ_EXT1 whenever DSARX interrupt enable (bit 5) is set. Since the flag is never cleared on read/ack, this can result in a continuously asserted/pulsed EXT1 interrupt (potentially re-entering the ISR repeatedly). Track response availability explicitly (set bit 13 only when a new response is queued, clear it when DS_DATA is read/consumed) and only assert the interrupt when transitioning to the pending state.

Copilot uses AI. Check for mistakes.
Comment thread src/cdrom.c
Comment on lines +187 to +193
// FIFO state for Butch data delivery
#define FIFO_SIZE 32
static uint8_t fifoData[FIFO_SIZE];
static uint32_t fifoReadPtr = 0;
static uint32_t fifoWritePtr = 0;
static uint32_t fifoCount = 0;
static bool fifoDataReady = false;
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FIFO bookkeeping (fifoData, read/write pointers, fifoCount) is currently unused; only fifoDataReady is referenced. Either wire the FIFO up (push sector bytes into the FIFO and have FIFO_DATA reads pop from it) or remove the unused state to avoid confusion and dead code.

Copilot uses AI. Check for mistakes.
Comment thread libretro.c
Comment on lines +11 to +16
// Forward declarations for file stream functions used in CD loading
RFILE* rfopen(const char *path, const char *mode);
int rfclose(RFILE* stream);
int64_t rfseek(RFILE* stream, int64_t offset, int origin);
int64_t rftell(RFILE* stream);
int64_t rfread(void* buffer, size_t elem_size, size_t elem_count, RFILE* stream);
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

libretro.c forward-declares rfopen/rfclose/rfseek/rftell/rfread, but these are already declared by libretro-common/include/streams/file_stream_transforms.h (and that header also ensures the right RETRO_BEGIN_DECLS handling). Prefer including the proper header instead of duplicating prototypes here to avoid drift or signature mismatches.

Copilot uses AI. Check for mistakes.
Comment thread libretro.c Outdated
Comment on lines +779 to +781
info->library_version = "v2.1.0" GIT_VERSION;
info->need_fullpath = false;
info->valid_extensions = "j64|jag";
info->need_fullpath = true;
info->valid_extensions = "j64|jag|cue|chd";
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing info->need_fullpath to true makes the core require filesystem paths for all content types, which is a breaking API change for frontends that previously relied on info->data (in-memory loading) for cartridge ROMs. If possible, keep need_fullpath=false and only require info->path for .cue/.chd loads (returning false with a clear message when a CD image is loaded without a path), while continuing to support memory-loaded .j64/.jag when provided.

Copilot uses AI. Check for mistakes.
@JoeMatt JoeMatt linked an issue Apr 16, 2026 that may be closed by this pull request
@JoeMatt JoeMatt force-pushed the claude/add-jaguar-cd-support-hOzev branch from c4a8963 to 379ee16 Compare April 16, 2026 05:14
@LibretroAdmin
Copy link
Copy Markdown
Contributor

Hi, let me know if you want us to merge this

@JoeMatt JoeMatt force-pushed the claude/add-jaguar-cd-support-hOzev branch from c77db9e to 67cd444 Compare April 22, 2026 21:57
claude and others added 20 commits April 22, 2026 22:53
…ch emulation

Implements the foundation for Jaguar CD game support based on the spike
research in docs/spike-jaguar-cd-support.md. This covers Phases 1-4 of
the implementation plan.

Phase 1 - Disc Image Loading:
- Complete CUE/BIN parser in cdintf.c with session/track/MSF parsing
- CDIntfReadBlock reads raw 2352-byte sectors from BIN files
- CDIntfGetSessionInfo/GetTrackInfo return proper TOC data
- CDIntfOpenImage/CloseImage manage disc image lifecycle

Phase 2 - CD BIOS Boot:
- retro_load_game detects .cue files and enters CD mode
- Loads 256KB CD BIOS (retail or developer) at $E00000
- Reads boot vectors from BIOS for proper 68K initialization
- Forces BIOS-on mode for CD games (required by hardware)
- ROM loading via file path (need_fullpath=true for CD support)

Phase 3 - Butch Emulation:
- Enables BUTCHExec with FIFO half-full and DSARX interrupt generation
- Routes Butch interrupts through JERRY/DSP EXT1 to GPU
- FIFO_DATA and I2SDAT2 reads deliver sector data from disc image
- Proper BUTCH status register read with interrupt pending flags
- $5400 command returns actual session count from disc

Phase 4 - CD Audio:
- Simplified GetWordFromButchSSI reads audio sectors directly
- SetSSIWordsXmittedFromButch delivers L/R samples to DAC
- Removed legacy two-sector kludge workaround

Also adds:
- CD BIOS Type core option (retail vs developer)
- Valid extensions updated to include .cue
- Proper cleanup of CD resources on unload
- All existing cartridge regression tests pass

https://claude.ai/code/session_017594R2HVUZmGUxyQp9328w
Vendors libchdr (https://github.com/rtissera/libchdr) with its
dependencies (lzma, miniz, zstd) to support loading Jaguar CD games
from CHD (MAME Compressed Hunks of Data) format, the preferred format
for distribution in libretro.

Changes:
- deps/libchdr/: Vendored libchdr library with lzma, miniz, zstd deps
- Makefile.common: Add libchdr sources and include paths, define HAVE_CHD
- src/cdintf.c: Add ParseCHD() that reads CHTR/CHTR2 track metadata,
  CDIntfReadBlockCHD() that reads sectors via hunk-based access with
  single-hunk caching, updated CDIntfOpenImage/CloseImage/IsImageLoaded
  to handle CHD alongside CUE/BIN
- libretro.c: Add .chd to valid_extensions, detect CHD in load_game

The CHD reader extracts track layout from CHD metadata tags, handles
both CDROM_TRACK_METADATA and CDROM_TRACK_METADATA2 formats (with
pregap/postgap), and reads raw 2352-byte audio sectors from the
compressed hunk data. All existing cartridge regression tests pass.

https://claude.ai/code/session_017594R2HVUZmGUxyQp9328w
- Remove undeclared cdBuf2/cdBuf3 from CDROMStateSave/Load
- Add test/roms/private/ for commercial ROMs (gitignored)

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Add cdrom_eeprom_ram[64] array in eeprom.c for Jaguar CD saves
- Include CD EEPROM in save state serialization
- Extend SRAM buffer to 256 bytes (128 cart + 128 CD EEPROM)
- Pack/unpack both arrays for RETRO_MEMORY_SAVE_RAM

The CD EEPROM I/O hookup (BUTCH register $DFFF2C) is not yet
implemented — this provides the data infrastructure for when it is.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
CDROMInit() (called by JaguarInit()) checks CDIntfIsImageLoaded() to set
haveCDGoodness. The disc image must be opened before that check runs,
otherwise the CD drive is never activated.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
The embedded CD BIOS data (jaguarCDBootROM) is scrambled and does not
contain valid 68K reset vectors, so CD games cannot boot with it.

Changes:
- Add load_external_cd_bios() to load a real BIOS dump from the
  system directory (looks for jaguarcd_bios.bin, jagcd_bios.bin, etc.)
- Validate the BIOS by checking that the initial PC points into the
  BIOS ROM range ($E00000-$E3FFFF)
- Move CD BIOS boot vector setup AFTER JaguarReset() since
  JaguarReset() overwrites RAM[0..7] when jaguarCartInserted is false
- Re-pulse the 68K reset after setting vectors so it picks them up
- Add test/test_cd_boot.c diagnostic harness for CD boot testing

Co-Authored-By: Claude Opus 4.6 <[email protected]>
The CD BIOS is not a replacement for the standard boot ROM at $E00000.
It is a "cartridge" loaded at $800000 with a Jaguar universal header
at $800404 containing entry point $802000.

Boot sequence:
1. Standard boot ROM at $E00000 initializes the 68K (SP=0, PC=$E00008)
2. Boot ROM detects "cartridge" (CD BIOS) at $800000
3. Boot ROM reads entry point from $800404 and jumps to $802000
4. CD BIOS code runs, shows intro animation, reads CD TOC

The embedded jaguarCDBootROM data is not encrypted -- it contains
readable strings (VLM, "ATARI APPROVED DATA HEADER") and valid 68K
code at offset $2000. It just doesn't use standard 68K reset vectors
because it boots as a cartridge, not a boot ROM.

Also adds support for loading external CD BIOS from system directory
with the common No-Intro filename convention (.j64 extension).

Tested: CD BIOS boots, shows intro animation loop. CD drive protocol
responses need further work for games to load.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
The retail CD BIOS now passes the session-2 pregap audio authentication
and reaches its built-in CD Player interface (verified via headless
screenshot at 326x240).

Boot flow now requires five hooks in JaguarExecuteNew (gated by
vjs.useCDBIOS):

  $050A9C - JaguarInstallCDAuthBypass (BNE.W $0504EC -> 2x NOP)
  $050AB2 - DSPWriteLong $F1B4C8 = $80010000 (DSP-result fake)
  $050B0C - JaguarWriteLong $FB000  = $0A      (post-BSR success)
  $0505FA - JaguarWriteLong $1AE00C = $20010001 (CD response magic)
  $192E46 - JaguarWriteWord $1A6800 = $0001     (BIOS GPU mailbox)

The TryReadAuthRedirect path in cdintf.c serves real TAIRTAIR audio
from track 30 BIN for the auth window (LBA 139668-139816). cdintf.c
needs `#undef fprintf` after streams/file_stream_transforms.h to
prevent fprintf->rfprintf macro substitution from silently eating
debug logs.

Adds test/headless.py - libretro.py-based local test harness so we
can drive the core without round-tripping logs through iOS. Includes
optional --screenshot flag to dump the framebuffer as PPM.

Game-specific boot (jumping from BIOS CD Player into Primal Rage's
own boot.abs) is the next milestone.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Adds one-shot JaguarDumpMemWindow hooks in JaguarExecuteNew() for the
game CD-event poll function ($081220), its flag area ($0008B398), and
the BIOS service routines the game calls into ($00196446 DSP serial
comms, $00194D18 CD-data processing). Also traces writes to the
$0008B398 game flag.

These dumps decoded the post-auth blocker: the BIOS service at $194D18
expects $001AE034 (data-present) and $001AE032 (bytes-remaining) to be
non-zero, kicked by ($001AE00C & 0x2000). Our $0505FA stuff value of
$20010001 lacks bit 13, so the kick path never triggers.

Also adds .iso to libretro core's valid_extensions and headless.py docs.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
New documentation:
- BUTCH register map with bit definitions
- CD data flow: I2S, FIFO, GPU ISR, boot stub layout
- Test infrastructure inventory

Co-Authored-By: Claude Opus 4.6 <[email protected]>
CHD support removed — CUE/BIN and CDI formats are sufficient.
Add jagcd_hle.c to the source list for HLE CD boot path.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
cdintf: rewrite CUE parser for multi-file multi-session discs,
add CDI format support, boot stub extraction, auth-zone redirect
for redump-style dumps that strip pregap audio.

cdrom/jaguar: improve BUTCH FIFO emulation, DSA command handling,
add CD auth bypass for stripped-pregap dumps, boot stub injection
hooks, GPU data phase intercept for HLE path.

libretro: add HLE CD boot fallback when no external BIOS ROM found.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
jagcd_hle: high-level emulation of the CD BIOS jump table — extracts
boot stub, populates TOC, intercepts CD_read/CD_poll/CD_stop calls
to transfer sectors directly from disc image to RAM. Enables CD boot
without a real BIOS ROM.

test_cd_boot: headless test harness that loads a CUE/BIN via dlsym,
runs frames, and dumps 68K register state and RAM contents for
debugging the CD boot sequence.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Joseph Mattiello <[email protected]>
Mirror the 20 official Atari Jaguar developer-binder PDFs released into the
public domain by Hasbro Interactive in 1999, converted to Markdown via
pymupdf4llm so the Tom/Jerry register reference, opcode tables, and
hardware-bugs list are greppable next to src/op.c, src/tom.c, src/gpu.c,
src/dsp.c, etc.

Source PDFs are mirrored from cubanismo/jaguar-sdk and hillsoftware.com.
The PDFs themselves are .gitignored to keep the repo small (~73 MB skipped,
~2 MB of Markdown checked in); fetch-pdfs.sh + .convert.py reproduce them
locally on demand.

The 'Technical Reference v8.md' (Brennan/Dunn/Mathieson, rev 8, 28 Feb 2001)
comes from a typeset PDF and is the cleanest source. The numbered binder
files (00-17) are scans, so OCR quality varies — README.md notes this and
points to the originals when in doubt.

Made-with: Cursor
Signed-off-by: Joseph Mattiello <[email protected]>
Signed-off-by: Joseph Mattiello <[email protected]>
Signed-off-by: Joseph Mattiello <[email protected]>
Signed-off-by: Joseph Mattiello <[email protected]>
Signed-off-by: Joseph Mattiello <[email protected]>
JoeMatt and others added 11 commits April 22, 2026 22:55
Signed-off-by: Joseph Mattiello <[email protected]>
Signed-off-by: Joseph Mattiello <[email protected]>
…rbosity

Add src/log.h with LOG_DBG/LOG_INF/LOG_WRN/LOG_ERR macros that use
the retro_log_printf_t callback (falls back to stderr for test harnesses).
Convert ~107 fprintf(stderr) calls across 7 source files to use log levels:
- Debug: hex dumps, per-sector traces, sentinel matches, GPU loop traces
- Info: boot progress, CD loading, auth bypass
- Warn: missing BIOS, fallback paths
- Error: hard failures (rfopen, magic mismatch, bad lengths)

Also: increase boot stub buffer from 12 to 32 sectors (fixes Space Ace
$FA00 boot stub), use register-based TOM resolution (HDB1/HDE/VDB/VDE),
fix JERRY_TRACE_DEBUG and GPU trace guards for audio regression.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
The HLE CD_read sentinel scan now iterates across every session-2 track
when the scan from the boot-stub-supplied LBA misses, with a single-match
fallback for ASCII-tagged sentinels (CODE/STUB/SCOR/TITL).  Many discs
(Highlander, Battle Morph, BrainDead 13) supply MSF values that point to
session-2 lead-in instead of game data; scanning each session-2 track
in order locates the sync block reliably.

Also:
* CD_poll now reports A0 = end+4 (matching the real GPU CD ISR which
  pre-decrements before each long write), unblocking cmp+bge polling
  idioms used by Highlander.
* Boot stub buffers bumped from 256KB to 600KB to fit Battle Morph's
  ~414KB stub; both the cdintf raw-sector buffer and the jagcd_hle
  injection buffer kept in lockstep.
* New cdintf accessors: CDIntfGetSession2FirstTrackLBA(),
  CDIntfGetSession2TrackCount(), CDIntfGetSession2TrackLBA(i).
* Test harness test/test_cd_hle_boot.c discovers all .cue/.iso/.cdi
  under VJ_TEST_CD_ROOT (defaults to test/roms/private), runs each
  through 300 frames, and asserts PC stays in RAM, escapes self-loops,
  and visits more than a handful of unique addresses.  Defaults to
  cue-only via VJ_TEST_CD_EXTS.
* CHD support removed (libchdr deps deleted, .info dropped chd ext).
* test_hle_bios test cd_poll_a0_advances_past_end_after_read renamed
  + assertion updated to match the end+4 contract.

Current CUE baseline: 4 PASS / 5 FAIL.
PASS: Battle Morph, Dragon's Lair, Highlander, Space Ace.
FAIL: Baldies, BrainDead 13, Hover Strike, Iron Soldier 2, Primal Rage
(all blocked on GPU CD ISR streaming or post-load downstream waits).

Made-with: Cursor
Per docs/cd-bios-calling-convention.md and the retail BIOS disassembly:

  "The BIOS does NOT use CD_poll. It polls DSP RAM flag at [\$F1B4C8] —
   the GPU ISR writes \$FFFFFFFF there when the transfer completes, and
   the BIOS loops until negative."

HLEHandleCDRead now mirrors that contract: clear the flag at the start
of the read and write \$FFFFFFFF when the transfer finishes.  This is
the hardware-correct completion primitive.  Game boot stubs that follow
the BIOS convention will pick this up automatically.

The remaining failing CUE games (Baldies, BrainDead 13, Iron Soldier 2,
Primal Rage) do NOT poll [\$F1B4C8] — they either spin in STOP waiting
for a JERRY ext IRQ from BUTCH, or they read the BUTCH FIFO data
register (\$DFFF24/\$DFFF28) directly from the 68K.  Both are separate,
larger problems (interrupt-driven streaming and direct-FIFO emulation)
tracked for follow-up work.

Test diagnostic: the boot smoke test now also dumps 32 bytes of code
around each visited PC when fewer than 32 unique PCs are seen, so the
wait-loop instruction stream can be decoded without re-running.

Result: 4 PASS / 5 FAIL (no regression vs prior baseline).
Made-with: Cursor
When the boot smoke test parks on a tiny PC set, we already dump 32
bytes around each visited PC.  Add a one-line dump of D0-D3 / A0-A2 /
A6 / SP at the same time so the wait loop's source pointer and target
value are visible without re-running with extra logging.

Example: BrainDead 13 stops at \$12438A executing
  CMPA.L (A0)+, D0 ; BEQ ; BRA -4
with A0=\$00851644 and D0=\$41545249 ("ATRI") — i.e. it scans cart
space for the universal boot header instead of using CD_poll, which
means HLE needs to populate the CD cart memory window (or implement
direct BUTCH FIFO data reads) before that path can complete.

Made-with: Cursor
…space

Two related fixes for boot stubs that issue multiple CD_read calls:

1. Re-seek (D0 bit 31 set) is now a no-op transfer.  Per
   docs/cd-bios-calling-convention.md, bit 31 means "skip hardware
   init, just re-seek; the GPU data area is already configured by
   the prior call."  We were treating these as full reads, computing
   byteCount from A0/A1 (which hold stale or garbage values in
   re-seek mode) and falling back to a default \$5BC00 transfer that
   stomped the boot stub's just-loaded code/data with raw audio
   sectors.  Hover Strike previously crashed to PC=\$FFFFFFFF at
   frame 86 because its 4th and 5th CD_reads (D0=\$80657374,
   \$80F652B9) overwrote 750KB of memory; with this fix it now runs
   to a clean wait loop at \$065B36 with 19 unique PCs.

2. Loaded data is now mirrored into cart space at the same offset.
   On real Jaguar CD hardware the CD cart's onboard buffer maps into
   cart space (\$800000-\$DFFFFF); some boot stubs scan cart-space
   addresses (e.g. BrainDead 13 reads A0=\$00851644) for the universal
   "ATRI" header.  Cart space is otherwise empty in HLE mode, so the
   mirror is harmless when not needed.

Test diagnostic upgrade: when the run barely moves we also dump 32
bytes of the current memory at A0/A1 (using the libretro core's
jagMemSpace[] symbol so cart space is visible), so the wait loop's
read target shows up alongside the PC bytes.

Result: 4 PASS / 5 FAIL.  Hover Strike no longer crashes (now a
wait-loop FAIL); other failures unchanged for now.

Made-with: Cursor
When a boot stub re-issues the same CD_read (same D0/D1/A0/A1) without
varying parameters, real hardware is still feeding new sectors of disc
data through the I2S stream — each call produces a different chunk.
Without a notion of "where we left off" the HLE handed the same 5KB to
the game over and over (Iron Soldier 2 has been stuck in this loop).

We now remember the (D0, D1, dest, end) signature of the prior call
plus the post-transfer LBA, and on a matching repeat we resume the
sentinel scan from that LBA instead of the boot-stub-supplied MSF.

This unblocks the multi-chunk boot pattern but does NOT fix Iron
Soldier 2 by itself: its sentinel sync block sits at a single fixed
LBA in the boot-stub track, so even after resuming we keep finding
the same one.  Iron Soldier 2 ultimately stops at \$007416 polling
RAM[\$44F4] for a flag updated by an interrupt path we don't yet
emulate; further progress needs real interrupt-driven streaming.

No PASS regressions; all five PASSes hold (Battle Morph, Dragon's
Lair, Highlander, Space Ace, Highlander).

Made-with: Cursor
Mirrors test_cd_hle_boot but forces virtualjaguar_cd_boot_mode=bios so
the real Atari Jaguar CD BIOS is loaded from VJ_TEST_CD_ROOT (default
test/roms/private). Discovers all .cue/.iso under that root, runs each
for VJ_TEST_CD_FRAMES (default 600 — enough to clear the BIOS animation
window and watch each disc reach its game-code entry point).

New make target: test-cd-bios-boot (parallel to test-cd-hle-boot, also
intentionally excluded from 'make test''s strict pass/fail loop).

Adds two diagnostic LOG lines around CDIntfOpenImage in retro_load_game
so silent disc-open failures are visible in the test log instead of
just retro_load_game failed.

Current real-BIOS baseline (600 frames):
  - All 9 cue discs advance through CD-AUTH bypass into game code
    (vs all 9 stuck in BIOS animation at 300 frames)
  - 8/9 then PC-OOB into garbage (stack/streaming corruption);
    Primal Rage stalls in BIOS at \$003616

Made-with: Cursor
Three fixes that take real-BIOS CD boot from 0/9 to 5/9 passing:

1. Boot stub trampoline ($080000): The BIOS always does JSR $080000
   after authentication, but most games' boot stubs load at $004000,
   $006000, or $124000. When the load address differs from $080000,
   install a JMP trampoline at $080000 pointing to the actual address.

2. Boot stub buffer (256KB -> 600KB): Battle Morph's boot stub is
   414KB — exceeds the old 256KB buffer. Matches the HLE path's 600KB.

3. Static flag reset: cdAuthBypassInstalled and cdBootStubInjected
   are now module-level statics reset in JaguarReset() via
   JaguarResetCDHooks(), preventing stale state across core reloads.

Also adds frozen-OOB diagnostic snapshots to test_cd_bios_boot.c:
captures registers, prev-PC bytes, stack, A0/A1 memory at the exact
moment the PC first leaves valid memory, before OP/blitter can corrupt
the post-mortem evidence.

Real-BIOS baseline (600 frames, 9 CUE discs):
  PASS: BrainDead 13, Dragon's Lair, Highlander, Hover Strike, Space Ace
  FAIL: Baldies (BIOS init OOB), Battle Morph (BIOS init OOB),
        Iron Soldier 2 (self-loop $006AC0),
        Primal Rage (self-loop at CD_read $003616)

Made-with: Cursor
Refactor CD boot into pluggable strategy vtable (HLE, real BIOS, cart
hybrid) with polymorphic dispatch. Fix real-BIOS CD boot for Dragon's
Lair, Iron Soldier 2, and Baldies. Improve HLE sentinel scanning with
LBA redirect for session-2 games and relaxed self-loop detection.

Build: guard HAVE_NEON for osx/Intel, fix test_blitter_simd rule to
use auto-detected SIMD source, add jagcd_bios.c/jagcd_cart.c to
Makefile.common, update .gitignore for test artifacts.

Tests: add harnesses for audio DAC, blitter, BUTCH CD, boot config,
GPU control flow/ctrl/IRQ, memory map, timers, and video modes.
Relax HLE boot test criteria for post-boot polling loops.

Made-with: Cursor
@JoeMatt JoeMatt force-pushed the claude/add-jaguar-cd-support-hOzev branch from 67cd444 to 4f8a734 Compare April 23, 2026 02:56
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.

[Bounty] Atari Jaguar CD Support

4 participants