fix(cd): populate TOC track durations -- Primal Rage CD music plays

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 <[email protected]>
This commit is contained in:
Joseph MattielloandClaude Fable 5 committed 2026-07-16 01:48:27 -04:00
1 parent 0f22facd50
commit 746cf4a7a5
8 files changed
+153 -8

No files matched your search

@@ -1,4 +1,33 @@
# Primal Rage CD-audio silence — diagnosis in progress
# Primal Rage CD-audio silence — RESOLVED (2026-07-16)
**Root cause:** the boot-stub TOC at $2C00 left bytes [5..7] of every
track entry (track duration as MSF) zeroed. Primal Rage's music player
(RAM $3A2D6, transient overlay) computes its DSP playback countdown from
exactly those bytes — `[5]*4500 + [6]*75 + [7]` sectors — and stores it
to the synth-DSP's sector counter at $F1B278. With duration 0 the DSP's
per-588-sample countdown ($F1B23A) underflowed on the first sector,
set the done flag at $F1B27C, and the 68K service loop ($3A3CC) sent
mailbox cmd 2 (mix OFF) within ~13 ms of cmd 1 (mix ON). The mix gate
(bank-0 r20) was never the problem — the 68K opened it every attempt and
immediately closed it because the track "ended".
**Fix:** `CDIntfGetTrackDuration()` (lengthLBA minus pregap, as MSF) now
fills TOC bytes [5..7] in both TOC writers (jagcd_bios.c boot stub,
jagcd_hle.c). Also reverted the Pause/Pause-Release DSA response to
$0400: the game's own CD driver ($BE42) masks the response high byte and
treats only $04xx (error-status, code $00 = none) as success — $0400 is
the Philips-protocol completion ack, not an error.
**Verified:** headless BIOS-mode run — mailbox cmd 1 arrives, no cmd 2
follows, LRXD reads go from 5 to 900 000+ with live waveform data, and
per-second host audio RMS holds 1 000–4 500 from music start (was 0).
Old "68K enable-gate" hypothesis below is obsolete: Primal Rage never
sets INT1 C_JERENA at all (all INT1 writes are $100/$101); its CD flow
is poll-driven, no 68K CD interrupt needed.
---
Historical notes (pre-resolution):
**State (2026-07-15, late):** the entire playback mechanism is decoded
end-to-end; the remaining unknown is the 68K's precondition for enabling
+37
View File
@@ -1170,6 +1170,43 @@ uint8_t CDIntfGetTrackInfo(uint32_t track, uint32_t offset)
}
}
// Returns one byte of the track's playing duration as MSF (offset 0/1/2 =
// minutes/seconds/frames). Duration is a sector-count delta, NOT a disc
// position, so no 150-frame lead-in offset applies. The pregap (startLBA
// to dataLBA) is excluded: playback and the $2C00 TOC both start at
// INDEX 01. Games size their CD-audio playback from this: Primal Rage's
// music player loads TOC bytes [5..7] (length MSF) into its DSP sector
// countdown at $F1B278 -- a zero length there stops the track after one
// sector (~13 ms).
uint8_t CDIntfGetTrackDuration(uint32_t track, uint32_t offset)
{
uint32_t pregap, durLBA;
uint8_t m, s, f;
if (!disc.loaded || track < 1 || track > disc.numTracks)
return 0;
pregap = disc.tracks[track - 1].dataLBA
? (disc.tracks[track - 1].dataLBA - disc.tracks[track - 1].startLBA)
: 0;
durLBA = (disc.tracks[track - 1].lengthLBA > pregap)
? (disc.tracks[track - 1].lengthLBA - pregap)
: 0;
MSFFromLBA(durLBA, &m, &s, &f);
switch (offset)
{
case 0:
return m;
case 1:
return s;
case 2:
return f;
default:
return 0;
}
}
// Returns the session number (1-based) for a given track
uint8_t CDIntfGetTrackSession(uint32_t track)
{
+1
View File
@@ -70,6 +70,7 @@ uint32_t CDIntfGetCurrentDrive(void);
const uint8_t * CDIntfGetDriveName(uint32_t driveNum);
uint8_t CDIntfGetSessionInfo(uint32_t session, uint32_t offset);
uint8_t CDIntfGetTrackInfo(uint32_t track, uint32_t offset);
uint8_t CDIntfGetTrackDuration(uint32_t track, uint32_t offset);
uint8_t CDIntfGetTrackSession(uint32_t track);
// Returns true if the given disc-image LBA falls within a session 2 track
+13 -7
View File
@@ -1336,13 +1336,19 @@ void CDROMWriteWord(uint32_t offset, uint16_t data, uint32_t who/*=UNKNOWN*/)
* $70nn echo and the game wedged polling for it. */
uint16_t resp;
if ((data & 0xFF00) == 0x0400 || (data & 0xFF00) == 0x0500)
/* Pause / Pause-Release complete with Found ($01) in the
* Philips CDD-family DSA protocol (mister_ground_truth.h
* response table has no $04xx/$05xx completion; $04 is the
* ERROR response). Returning the old $0400 fallthrough made
* Primal Rage read "error" after Unpause and instantly
* re-Pause -- CD-audio test silent, match start blocked. */
resp = 0x0100;
/* Pause / Pause-Release complete with the error-status word
* $04nn, nn = error code, $00 = none -- i.e. $0400 is the
* generic "done, no error" ack in the Philips CDD-family DSA
* protocol, NOT an error indicator. Ground truth: Primal
* Rage's own 68K CD driver (RAM $BE42, from the Atari CD
* library) masks the response to its high byte and treats
* ONLY $04xx as success for Stop/Mute/Pause/Unpause-class
* commands, and its multi-word collector ($C20C) uses an
* $04xx word as the end-of-response terminator. A previous
* change made these complete with Found ($0100) after
* misreading mister_ground_truth.h's DSA_RSP_ERROR -- that
* made the game flag every Pause/Unpause as failed. */
resp = 0x0400;
else if ((data & 0xFF00) == 0x1500)
resp = 0x1700 | (data & 0xFF); /* Mode Status */
else if ((data & 0xFF00) == 0x1800)
+8
View File
@@ -147,6 +147,14 @@ static bool bios_instruction_hook(uint32_t m68kPC)
* 1-based sessions. */
jaguarMainRAM[tocAddr + 4] =
(uint8_t)((tsess >= 1) ? (tsess - 1) : 0);
/* bytes[5..7] = track duration as MSF. Primal Rage's
* music player multiplies these into a sector count
* for its DSP playback countdown ($F1B278); zeroes
* here made every CD-audio track "finish" after one
* sector and the game silenced the mix instantly. */
jaguarMainRAM[tocAddr + 5] = CDIntfGetTrackDuration(t, 0);
jaguarMainRAM[tocAddr + 6] = CDIntfGetTrackDuration(t, 1);
jaguarMainRAM[tocAddr + 7] = CDIntfGetTrackDuration(t, 2);
maxTrack = (uint8_t)t;
}
+5
View File
@@ -150,6 +150,11 @@ static void HLEPopulateTOC(uint32_t addr)
jaguarMainRAM[off + 2] = CDIntfGetTrackInfo(t, 1);
jaguarMainRAM[off + 3] = CDIntfGetTrackInfo(t, 2);
jaguarMainRAM[off + 4] = (uint8_t)((sess >= 1) ? (sess - 1) : 0);
/* bytes[5..7] = track duration as MSF -- games size CD-audio
* playback from these (Primal Rage DSP countdown at $F1B278). */
jaguarMainRAM[off + 5] = CDIntfGetTrackDuration(t, 0);
jaguarMainRAM[off + 6] = CDIntfGetTrackDuration(t, 1);
jaguarMainRAM[off + 7] = CDIntfGetTrackDuration(t, 2);
if (sess >= 2 || t >= numTracks - 4)
HLE_LOG("TOC: track %2u session=%u(0-based %u) MSF=%02u:%02u:%02u at $%04X\n",
+46
View File
@@ -17,12 +17,14 @@
#include "dsp.h"
#include "dsp_acc40.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "dac.h"
#include "gpu.h"
#include "jaguar.h"
#include "jerry.h"
#include "log.h"
#include "m68000/m68kinterface.h"
#include "settings.h"
@@ -574,6 +576,38 @@ void DSPWriteWord(uint32_t offset, uint16_t data, uint32_t who/*=UNKNOWN*/)
if ((offset >= DSP_WORK_RAM_BASE) && (offset < DSP_WORK_RAM_BASE+0x2000))
{
/* CDDA-DIAG: see DSPWriteLong -- Primal Rage synth mailbox.
* Rate-capped: other titles may use this RAM range as data. */
if (offset >= 0xF1B270 && offset <= 0xF1B277)
{
static uint32_t mboxWrites = 0;
mboxWrites++;
if (mboxWrites <= 40 || (mboxWrites % 10000) == 0)
LOG_INF("[CDDA] DSP mailbox write.w $%06X = $%04X who=%u 68kpc=$%06X\n",
offset, data, who, m68k_get_reg(NULL, M68K_REG_PC));
/* One-shot main-RAM snapshot at the mix-ON edge so the transient
* music-player overlay around the writer PC can be disassembled.
* Enabled only when VJ_CDDA_SNAPDIR is set (diagnostic builds). */
if (offset == 0xF1B276 && data == 0x0001)
{
const char *dir = getenv("VJ_CDDA_SNAPDIR");
static int snapped = 0;
if (dir && !snapped)
{
char path[1024];
FILE *f;
snapped = 1;
snprintf(path, sizeof(path), "%s/mixon_mainram.bin", dir);
f = fopen(path, "wb");
if (f)
{
fwrite(jaguarMainRAM, 1, 0x200000, f);
fclose(f);
LOG_INF("[CDDA] snapshot: %s\n", path);
}
}
}
}
offset -= DSP_WORK_RAM_BASE;
dsp_ram_8[offset] = data >> 8;
dsp_ram_8[offset+1] = data & 0xFF;
@@ -613,6 +647,18 @@ void DSPWriteLong(uint32_t offset, uint32_t data, uint32_t who/*=UNKNOWN*/)
if (offset >= DSP_WORK_RAM_BASE && offset <= DSP_WORK_RAM_BASE + 0x1FFF)
{
/* CDDA-DIAG: Primal Rage's synth-DSP command mailbox lives at
* $F1B274 (cmd 1 = CD mix ON, 2 = OFF, 3 = reset, 4 = exit) --
* log external writes so we can see who opens the mix gate.
* Rate-capped: other titles may use this RAM range as data. */
if (offset >= 0xF1B270 && offset <= 0xF1B277)
{
static uint32_t mboxWritesL = 0;
mboxWritesL++;
if (mboxWritesL <= 40 || (mboxWritesL % 10000) == 0)
LOG_INF("[CDDA] DSP mailbox write $%06X = $%08X who=%u 68kpc=$%06X\n",
offset, data, who, m68k_get_reg(NULL, M68K_REG_PC));
}
offset -= DSP_WORK_RAM_BASE;
SET32(dsp_ram_8, offset, data);
//CC only!
+13
View File
@@ -260,6 +260,7 @@
#include "event.h"
#include "gpu.h"
#include "jaguar.h"
#include "log.h"
#include "m68000/m68kinterface.h"
#include "op.h"
#include "perf_counters.h"
@@ -1246,6 +1247,18 @@ void TOMWriteByte(uint32_t offset, uint8_t data, uint32_t who)
offset &= 0x3FFF;
if (offset == INT1)
TOMClearPendingIRQs(data);
if (offset == INT1 + 1)
{
/* CDDA-DIAG: INT1 bit 4 (C_JERENA) is the TOM-side gate for
* JERRY -> 68K delivery (BUTCH CD IRQs route through it, see
* cdrom.c BUTCHExec). Edges are rare; log unconditionally. */
static uint8_t tomPrevInt1Ena = 0;
if ((tomPrevInt1Ena ^ data) & 0x10)
LOG_INF("[CDDA] INT1 C_JERENA %s (INT1=$%02X who=%u 68kpc=$%06X)\n",
(data & 0x10) ? "ON" : "OFF", data, who,
m68k_get_reg(NULL, M68K_REG_PC));
tomPrevInt1Ena = data;
}
tomRam8[offset] = data;
if (offset == INT1 || offset == (INT1 + 1))
TOMAssertEnabledIRQs();