Skip to content

spu: implement SPU IRQ + CAUSE.IP2 combinational + mid-dispatch audio pump - #102

Closed
Alexbeav wants to merge 3 commits into
mstan:masterfrom
Alexbeav:pr/spu-irq-cause-ip2
Closed

spu: implement SPU IRQ + CAUSE.IP2 combinational + mid-dispatch audio pump#102
Alexbeav wants to merge 3 commits into
mstan:masterfrom
Alexbeav:pr/spu-irq-cause-ip2

Conversation

@Alexbeav

@Alexbeav Alexbeav commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • SPU IRQ (spu.c): implement I_STAT bit 9 raise when any SPU-RAM access touches the address in SpuSetIrqAddr (0x1F801DA4), gated by SPUCNT bit 6, with SPUSTAT bit 6 mirror and SPUCNT.6-clear acknowledge. Triggers on FIFO writes, DMA4 read/write transfers, and voice ADPCM block fetches.
  • CAUSE.IP2 combinational (interrupts.c, memory.c, traps.c): IP2 is a live mirror of the INTC line, not a latched bit. Refresh at every I_STAT ack, I_MASK write, IRQ raise, and HLE context restore.
  • Mid-dispatch audio pump (interrupts.c, main.cpp): pump SPU time from the VBlank edge (guest-cycle-budgeted, delta/768) so the autonomous SPU doesn't freeze during CPU busy-waits.
  • SPU RAM DMA readback (spu.c, dma.c): add spu_dma_read() for DMA4 read-direction transfers — previously the ch4 SPU read path returned zeros.

Problem

Three gaps in the SPU/IRQ subsystem caused busy-wait deadlocks during sequenced audio synchronisation:

  1. Missing SPU IRQ. The runtime never raised I_STAT bit 9. Games that park the IRQ address at the end of a sound-bank upload and wait on kernel event class 0xF0000009 spec 0x20 (TestEvent) deadlock permanently — the wait needs the IRQ, and the IRQ was never modelled.
  2. Stale CAUSE.IP2. IP2 was set at delivery and never cleared, leaving a phantom bit in COP0.CAUSE. The retail kernel's exception dispatcher loops on CAUSE.IP & SR.IM to decide whether to service again — with I_STAT clear but IP2 still set, it spins forever in its event-scan.
  3. SPU clock starvation. The audio pump ran only from the main loop between presented frames. A guest busy-wait that never completes a frame freezes SPU time, so a voice that needs to play into the parked IRQ address never advances — deadlocking the very IRQ-wait that was supposed to unblock.
  4. Zero-fill SPU DMA read. DMA4 read transfers returned zeros, breaking multi-EXE titles that carry cross-EXE state through SPU RAM.

Validation

  • Medal of Honor Underground (SLUS-01270): resolves Mission 1 weapon-draw freeze. The game keys-on a voice and busy-waits in a kernel TestEvent loop for SPU IRQ delivery (~1.6 s on hardware). Before these fixes: zero dispatch-count runs, wedge within seconds. After: 5+ minutes of clean 60 fps gameplay, pistol rendered, player movement normal.
  • Gran Turismo (SCUS-94194): SPU DMA readback fix resolves the GTMENU→GTMAIN Exec transition — the game's GAMESTATUS block (~46 KB, GTos magic + CRC-CCITT) passes checksum verification across the EXE boundary. Previously the zero stub failed, causing a cold-boot fallback.
  • Existing titles: no regression expected — the SPU IRQ path is purely additive (IRQ-disabled by default, acknowledge idle), CAUSE.IP2 is derived from the same I_STAT/I_MASK state the runtime already tracks, and the audio pump is guest-cycle-budgeted with same-thread double-pump being a no-op.

Hardware basis

  • SPU IRQ address register (0x1F801DA4), SPUCNT.6 (IRQ enable), SPUSTAT.6 (IRQ flag): PSX-SPX documentation, Nocash PSX specs §SPU.
  • CAUSE.IP2 combinational behaviour: real hardware mirrors the INTC line continuously; IP bits are not latched in COP0.CAUSE across I_STAT/I_MASK changes. Observed in Beetle PSX (libretro) LLE trace.
  • SPU autonomy: the SPU core runs independently of the CPU; ADPCM decode and voice playback advance even during CPU busy-wait loops. Confirmed via Beetle oracle comparison — identical CD command storm, identical SPU RAM state at the wedge point.

Add spu_dma_read() for DMA4 read-direction transfers (SPUCNT transfer
mode 3). Previously the ch4 SPU read path wrote zeros — this is a
critical gap for multi-EXE titles that stash cross-EXE state in SPU RAM.

Proven: Gran Turismo carries its GAMESTATUS block ('GTos' magic +
CRC-CCITT over ~46KB) through SPU RAM across GTMENU->GTMAIN Exec
transitions. The zero stub failed the checksum, causing the game to
chain-exec gtos.exe (cold-boot fallback — presenting as the race-load
'reset to intro FMV' bug). With the fix, Arcade race loads and plays.

Also emit AUDIO_EV_DMA_READ event for cross-title probing and wire the
write path's audio_trace_event for symmetry.
… pump

Three related hardware-correctness fixes that together resolve
busy-wait deadlocks during sequenced audio sync:

1. SPU IRQ (spu.c): Real hardware raises I_STAT bit 9 when any
   SPU-RAM access touches the address in 0x1F801DA4, gated by
   SPUCNT.6. Adds triggers for FIFO writes, DMA4 r/w transfers,
   and voice ADPCM block fetches. Acknowledge via SPUCNT.6 clear.

2. CAUSE.IP2 combinational (interrupts.c, memory.c, traps.c):
   IP2 is a live mirror of the INTC line on hardware, not a
   latched bit. Refresh at every I_STAT ack, I_MASK write, IRQ
   raise, and HLE context restore. Prevents phantom IP2 from
   spinning the kernel exception dispatcher.

3. Mid-dispatch audio pump (interrupts.c, main.cpp): The SPU is
   autonomous -- it keeps consuming samples during CPU busy-waits.
   Pump audio from the VBlank edge (guest-cycle-budgeted) so a
   guest waiting on the SPU IRQ doesn't starve SPU time.

Validated on Medal of Honor Underground (SLUS-01270): resolves
Mission 1 weapon-draw freeze where the game busy-waits on kernel
TestEvent for SPU IRQ delivery during voice playback.
mstan added a commit that referenced this pull request Aug 6, 2026
spu: reverb, noise, sweeps and SPU IRQ (closes #103); CAUSE.IP2 combinational (supersedes #102)
@mstan

mstan commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Hello! Apologies for taking the time to check on this. Very grateful for the contributions.

The CAUSE.IP2 finding was very valuable. I checked it against the Beetle oracle and it holds: IP2 mirrors the interrupt line and we were latching it and never clearing it. I've merged that, your SPU RAM DMA readback, and the mid-dispatch audio pump as part of #104, which also implements the reverb/noise/sweeps that #103 needed, so I'm closing this as superseded rather than stale. You're credited as co-author on the commit

As a heads up, two changes I made on the way, for transparency: there was a second place latching IP2 that this missed (psx_interpreter.c, the interpreter delivery path), and the audio pump needed gating — calling it unconditionally from the VBlank edge bypasses our turbo-load mute, which would have played time-compressed audio on every load. I also couldn't reproduce the MOHU/Gran Turismo validation since we don't have those discs, so I verified the hardware behavior instead (the BIOS boot was actually a good candidate to test against).

Going to close this PR for now since the work got folded into 104, but wanted to follow up and thank you and ensure you know you're properly attributed on the work carried forward there. Thank you and take care!

@mstan mstan closed this Aug 6, 2026
ClaudioVarandas pushed a commit to ClaudioVarandas/psxrecomp that referenced this pull request Aug 18, 2026
…inational

Closes the four gaps named in issue mstan#103 and lands the separable, verified
parts of PR mstan#102. See docs/internal/SPU_FIDELITY_103.md for the per-piece
disposition and the oracle-verification queue.

SPU DSP (issue mstan#103), clean-room from hardware documentation:

- Reverb: the documented 22050 Hz engine — vLIN/vRIN input, same-side and
  crossed different-side IIR reflections, 4-tap comb early echo, both
  all-pass filters, vLOUT/vROUT output, and the work-area address advance.
  Gated on SPUCNT bit 7 for WRITES only; reads, IRQ checks, address advance
  and output always run, as on hardware. Addressing is confined to
  [mBASE, 0x80000). Per-voice sends from EON, CD send from SPUCNT bits 0+2.
- Noise: 16-bit LFSR with the documented parity feedback, clocked at the
  SPUCNT bits 8-13 rate; NON voices substitute it for their ADPCM sample.
- Volume sweeps: all 24 voice L/R plus main L/R. Bit 15 selects sweep vs
  direct; sweep is a live envelope on the same rate machinery as ADSR, and a
  guest read of a sweeping register returns the live level.
- Capture buffers: CD L/R and voice 1/3 output written to SPU RAM, so a game
  that parks the IRQ address there still gets its interrupt.
- SPU IRQ (I_STAT bit 9): checked at every SPU RAM access class — FIFO, DMA
  both directions, ADPCM block fetch, capture writes, reverb work-area
  accesses — gated on SPUCNT bit 6, mirrored in SPUSTAT bit 6, acked by
  clearing SPUCNT bit 6.

The CD-only fast path in spu_render was removed rather than extended: the
SPU now has per-frame work that must run in every path, and an FMV (XA audio
with zero active voices) is exactly the CD-with-reverb case.

This is a clean-room implementation from the psx-spx/nocash register map and
documented algorithm. Beetle's source was deliberately not consulted: it is
GPL-2.0-or-later and this project is PolyForm Noncommercial. PR mstan#16 was
parked for exactly that reason and PR mstan#13's reverb was separately wrong (it
gated on bit 15 of dAPF1, an address offset, instead of SPUCNT bit 7). The
one place this knowingly deviates from hardware is the 22.05 -> 44.1 kHz
reconstruction filter, which the documentation does not specify; it is
isolated in a single function and flagged for oracle comparison.

CAUSE.IP2 (from PR mstan#102):

IP2 is combinational on R3000A — it mirrors the interrupt line and falls the
instant the guest acks I_STAT or masks the source. It was being OR'd in at
delivery and never cleared, in TWO places (the compiled path and
psx_interpreter.c, the latter missed by PR mstan#102), leaving a phantom pending
interrupt that can spin a kernel dispatcher looping on CAUSE.IP & SR.IM.
psx_irq_refresh_cause_ip2() is now the sole writer of bit 10, refreshed at
raise, I_STAT ack, I_MASK write, HLE context restore and power-on. Verified
against the Beetle oracle's irq.cpp/cpu.cpp rather than asserted, and
covered by test_cause_ip2_combinational.

Mid-dispatch audio pump (from PR mstan#102), with a fix: routed through a gate
that mirrors sdl_audio_update's mute/sink decision. Pumping unconditionally
from the VBlank edge, as submitted, would have pushed real audio during every
turbo-load hard mute and defeated the freeze-in-place mute model.

SPU RAM DMA readback (from PR mstan#102): DMA4 in the SPU->RAM direction wrote
literal zeros; SPU RAM is readable memory and titles carry state through it
across an Exec boundary.

Also fixes an unrelated build race found on the way: tools/embed_spirv.py
wrote its intermediate SPIR-V to <source>.spv, so the two runtime targets
that embed the same shaders raced on one path and one deleted the file the
other was reading.

Co-Authored-By: Alexandros Mandravillis <Alexbeav@live.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants