fix(jerry): SCLK write no longer fires the SSI interrupt synchronously

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 <[email protected]>
This commit is contained in:
Joseph MattielloandClaude Fable 5 committed 2026-07-16 18:28:52 -04:00
1 parent 874330f4ce
commit b63fb7d903
3 files changed
+32 -1

No files matched your search

+4 -1
View File
@@ -274,7 +274,10 @@ void DACWriteWord(uint32_t offset, uint16_t data, uint32_t who)
DACUpdateSCLKRate();
JERRYI2SInterruptTimer = -1;
RemoveCallback(JERRYI2SCallback);
JERRYI2SCallback();
/* Restart the timer chain one I2S frame period out; a synchronous
* JERRYI2SCallback() here would assert the SSI interrupt inside
* the very instruction that wrote SCLK (see jerry.c). */
JERRYRescheduleI2S();
}
else if (offset == SMODE + 2)
{
+27
View File
@@ -289,6 +289,33 @@ void JERRYPIT2Callback(void)
}
/* Restart the I2S interrupt timer chain WITHOUT asserting an interrupt
* now. Called on SCLK writes (src/jerry/dac.c). On real hardware the
* SSI raises its first word-strobe interrupt only after a complete I2S
* word has been shifted -- 32 bits x 2 clock phases x (SCLK+1) system
* clocks -- never in the same instruction that programmed the clock.
* Invoking JERRYI2SCallback() synchronously here vectored the DSP into
* its I2S handler in the middle of the store that wrote SCLK, before
* the DSP module's init code had set up the handler's pointer
* registers; the handler then wrote CD samples through stale pointers
* over its own code (Highlander / Dragon's Lair CD crash: dsp_pc
* escape to $FFFFCE03 after a module re-upload). */
void JERRYRescheduleI2S(void)
{
double usecs;
jerryI2SCycles = 32 * (2 * (*sclk + 1));
if (*smode & SMODE_INTERNAL)
usecs = (double)jerryI2SCycles
* (vjs.hardwareTypeNTSC ? RISC_CYCLE_IN_USEC : RISC_CYCLE_PAL_IN_USEC);
else
usecs = 22.675737;
SetCallbackTime(JERRYI2SCallback, usecs, EVENT_JERRY);
}
void JERRYI2SCallback(void)
{
// We don't have to divide the RISC clock rate by this--the reason is a bit
+1
View File
@@ -46,6 +46,7 @@ void JERRYSetPendingIRQ(int irq);
// This should stay inside this file, but it's here for now...
// Need to set up an interface function so that this can go back
void JERRYI2SCallback(void);
void JERRYRescheduleI2S(void);
// External variables