From 3ec65040689cd00276330ec787142421da8bb6b0 Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Wed, 5 Aug 2026 04:31:14 +0300 Subject: [PATCH 1/3] spu: implement SPU RAM DMA readback (PSX-SPU-001) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- runtime/include/audio_trace.h | 1 + runtime/include/spu.h | 1 + runtime/src/dma.c | 4 +++- runtime/src/spu.c | 17 +++++++++++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/runtime/include/audio_trace.h b/runtime/include/audio_trace.h index d8cab71dd..6d55b102c 100644 --- a/runtime/include/audio_trace.h +++ b/runtime/include/audio_trace.h @@ -60,6 +60,7 @@ enum { stage 0=post-ADPCM-decode (native rate), stage 1=post-resample+volume (44100). */ AUDIO_EV_SINK_DROP = 10, /* turbo host sink; a=guest SPU frames discarded */ + AUDIO_EV_DMA_READ = 11, /* SPU RAM -> CPU DMA; a=words, b=dest RAM addr */ }; typedef struct { diff --git a/runtime/include/spu.h b/runtime/include/spu.h index b652c816b..5d5a67ebb 100644 --- a/runtime/include/spu.h +++ b/runtime/include/spu.h @@ -102,6 +102,7 @@ void spu_write(uint32_t addr, uint32_t value); /* DMA channel 4 interface */ void spu_dma_write(uint32_t word); +uint32_t spu_dma_read(void); int spu_dma_ready(void); /* CD-ROM XA/CDDA input path. Samples are stereo 44.1 kHz PCM entering the diff --git a/runtime/src/dma.c b/runtime/src/dma.c index 12c2c5072..d203b082c 100644 --- a/runtime/src/dma.c +++ b/runtime/src/dma.c @@ -795,9 +795,11 @@ static uint32_t execute_ch4_spu(void) { channels[4].madr & 0x1FFFFCu); } else { for (uint32_t i = 0; i < total_words; i++) { - psx_write_word(addr, 0); + psx_write_word(addr, spu_dma_read()); addr = (addr + addr_step) & 0x1FFFFCu; } + audio_trace_event(AUDIO_EV_DMA_READ, total_words, + channels[4].madr & 0x1FFFFCu); } channels[4].madr = addr; diff --git a/runtime/src/spu.c b/runtime/src/spu.c index 841ea18d3..aa225d1fa 100644 --- a/runtime/src/spu.c +++ b/runtime/src/spu.c @@ -865,6 +865,23 @@ void spu_dma_write(uint32_t word) { transfer_addr = (transfer_addr + 4) % SPU_RAM_SIZE; } +/* SPU RAM -> CPU RAM (DMA4 read direction, SPUCNT transfer mode 3). + * Gran Turismo carries its cross-EXE GAMESTATUS block ('GTos' magic + + * CRC-CCITT) through SPU RAM across GTMENU->GTMAIN Exec transitions; a + * zero-stubbed readback fails that checksum and the game falls back to + * its cold-boot chain (race load "resets" to the intro FMV). */ +uint32_t spu_dma_read(void) { + uint32_t word = 0; + if (transfer_addr + 3 < SPU_RAM_SIZE) { + word = (uint32_t)spu_ram[transfer_addr] + | ((uint32_t)spu_ram[transfer_addr + 1] << 8) + | ((uint32_t)spu_ram[transfer_addr + 2] << 16) + | ((uint32_t)spu_ram[transfer_addr + 3] << 24); + } + transfer_addr = (transfer_addr + 4) % SPU_RAM_SIZE; + return word; +} + int spu_dma_ready(void) { return 1; } From 8f53468c8ead6270d17752926eb1e386642aad73 Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Wed, 5 Aug 2026 05:06:26 +0300 Subject: [PATCH 2/3] runtime: wire CAUSE.IP2 mirror to interrupts.c for combinational tracking --- runtime/src/main.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 6a0992b06..d95cbb25e 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -153,6 +153,7 @@ extern "C" { /* memory.c */ extern "C" void memory_init(const char* bios_path); extern "C" void memory_set_sr_ptr(const uint32_t *p); +extern "C" void psx_irq_set_cause_ptr(uint32_t *p); extern "C" uint32_t memory_get_bios_checksum(void); extern "C" void dirty_ram_register_text_image(uint32_t phys_lo, const uint8_t *bytes, @@ -6976,6 +6977,10 @@ int main(int argc, char** argv) { /* Let memory subsystem see SR for cache-isolation checks. */ memory_set_sr_ptr(&cpu.cop0[12]); + /* Wire the CAUSE.IP2 mirror (interrupts.c): IP2 is combinational on real + * hardware and must track (I_STAT & I_MASK) through acks/mask writes. */ + psx_irq_set_cause_ptr(&cpu.cop0[13]); + /* Wire debug server to CPU state for register queries. */ debug_server_set_cpu(&cpu); From f1878352b7ff639ba3b57d326b126a1dc566de1b Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Wed, 5 Aug 2026 10:10:52 +0300 Subject: [PATCH 3/3] spu: implement SPU IRQ + CAUSE.IP2 combinational + mid-dispatch audio 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. --- runtime/src/interrupts.c | 38 ++++++++++++++++++++++++++++ runtime/src/main.cpp | 4 +++ runtime/src/memory.c | 4 +++ runtime/src/spu.c | 54 +++++++++++++++++++++++++++++++++++++++- runtime/src/traps.c | 7 ++++++ 5 files changed, 106 insertions(+), 1 deletion(-) diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c index c267a87da..a1e78bce8 100644 --- a/runtime/src/interrupts.c +++ b/runtime/src/interrupts.c @@ -119,9 +119,32 @@ extern uint32_t i_mask; * I_STAT bit so the device-event ring sees every raise from one place with the * exact guest cycle. Pure addition over `i_stat |= (1<0 */ + psx_irq_refresh_cause_ip2(); } static void interrupt_write_mask_masked(uint32_t val, uint32_t mask, uint8_t width) { uint32_t old = i_mask; i_mask = ((i_mask & ~mask) | (val & mask)) & 0x7FFu; imask_trace_record(old, i_mask, width); + psx_irq_refresh_cause_ip2(); } /* Getters for debug server */ diff --git a/runtime/src/spu.c b/runtime/src/spu.c index aa225d1fa..c2a4b056e 100644 --- a/runtime/src/spu.c +++ b/runtime/src/spu.c @@ -23,6 +23,35 @@ static uint8_t spu_ram[SPU_RAM_SIZE]; static uint16_t spu_regs[SPU_REG_COUNT]; static uint32_t transfer_addr; + +/* ---- SPU IRQ (I_STAT bit 9) ---- + * Real hardware raises the SPU interrupt when ANY SPU-RAM access (voice + * ADPCM fetch, FIFO/DMA transfer, CD-audio write) touches the address in + * 0x1F801DA4 (units of 8 bytes), while SPUCNT bit 6 enables it. The flag + * mirrors in SPUSTAT bit 6 and is acknowledged by clearing SPUCNT bit 6. + * Games use this to detect sound-bank upload completion by parking the IRQ + * address at the end of the upload region — MOHU's Mission 1 parks it after + * the weapon sound bank and busy-waits on kernel event class 0xF0000009 + * spec 0x20; without this IRQ the wait never ends (the weapon-draw freeze). + * Modeled triggers: transfer writes/reads (FIFO + DMA4 both directions). + * NOT yet modeled: voice playback fetch crossing the address (streaming + * double-buffer engines) — extend spu_irq_check_range from the voice + * decode loop when a consumer needs it. */ +static uint32_t spu_irq_addr; /* byte address (reg value << 3) */ +static int spu_irq_flag; /* SPUSTAT bit 6 latch, one-shot until re-arm */ + +extern void psx_irq_raise(uint32_t bit, uint32_t detail); + +static void spu_irq_check_range(uint32_t lo, uint32_t len) +{ + uint16_t cnt = spu_regs[(0x1F801DAAu - 0x1F801C00u) >> 1]; + if (!(cnt & 0x0040u)) return; /* SPUCNT.6: IRQ disabled */ + if (spu_irq_flag) return; /* already latched */ + if (spu_irq_addr >= lo && spu_irq_addr < lo + len) { + spu_irq_flag = 1; + psx_irq_raise(9, spu_irq_addr); /* I_STAT bit 9 = SPU */ + } +} static uint32_t key_on_count; static uint64_t render_frames; static uint64_t nonzero_frames; @@ -314,6 +343,12 @@ static void decode_block(SpuVoice *v) { uint32_t addr = v->cur_addr & (SPU_RAM_SIZE - 1u); if (addr + 16u > SPU_RAM_SIZE) addr = 0; + /* Voice ADPCM fetch is an SPU-RAM access: it triggers the SPU IRQ when + * the 16-byte block covers the programmed IRQ address. This is the sync + * technique MOHU's engine waits on at Mission 1 start (event class + * 0xF0000009): a keyed-on voice plays into the parked IRQ address. */ + spu_irq_check_range(addr, 16u); + uint8_t header = spu_ram[addr + 0u]; uint8_t flags = spu_ram[addr + 1u]; int shift = header & 0x0F; @@ -544,6 +579,8 @@ void spu_init(void) { memset(voices, 0, sizeof(voices)); memset(s_events, 0, sizeof(s_events)); transfer_addr = 0; + spu_irq_addr = 0; + spu_irq_flag = 0; key_on_count = 0; render_frames = 0; nonzero_frames = 0; @@ -765,7 +802,8 @@ uint32_t spu_read(uint32_t addr) { * OpenBIOS's shell MOD player waits for (SPUSTAT & 0x7FF) * == 0 after clearing SPUCNT and spun forever. */ uint16_t cnt = spu_regs[reg_index(0x1F801DAAu)]; - return (uint32_t)((cnt & 0x3Fu) | (((cnt >> 5) & 1u) << 7)); + return (uint32_t)((cnt & 0x3Fu) | (((cnt >> 5) & 1u) << 7) + | ((uint32_t)(spu_irq_flag ? 1u : 0u) << 6)); } /* ENDX (end-block-reached latch). Real hw sets bit v when voice * v decodes a block whose flag byte has bit 0; KEYON[v] clears @@ -839,6 +877,17 @@ void spu_write(uint32_t addr, uint32_t value) { key_off((uint32_t)(uint16_t)value << 16); } + if (addr == 0x1F801DA4u) { + spu_irq_addr = (((uint32_t)(uint16_t)value) << 3) + & (SPU_RAM_SIZE - 1u); + } + + if (addr == 0x1F801DAAu) { + /* Clearing SPUCNT bit 6 acknowledges/re-arms the IRQ. */ + if (!((uint16_t)value & 0x0040u)) + spu_irq_flag = 0; + } + if (addr == 0x1F801DA6u) { transfer_addr = ((uint32_t)(uint16_t)value) << 3; if (transfer_addr >= SPU_RAM_SIZE) transfer_addr = 0; @@ -849,6 +898,7 @@ void spu_write(uint32_t addr, uint32_t value) { spu_ram[transfer_addr] = (uint8_t)(value & 0xFF); spu_ram[transfer_addr + 1] = (uint8_t)((value >> 8) & 0xFF); } + spu_irq_check_range(transfer_addr, 2); transfer_addr = (transfer_addr + 2) % SPU_RAM_SIZE; } } @@ -862,6 +912,7 @@ void spu_dma_write(uint32_t word) { spu_ram[transfer_addr + 2] = (uint8_t)((word >> 16) & 0xFF); spu_ram[transfer_addr + 3] = (uint8_t)((word >> 24) & 0xFF); } + spu_irq_check_range(transfer_addr, 4); transfer_addr = (transfer_addr + 4) % SPU_RAM_SIZE; } @@ -878,6 +929,7 @@ uint32_t spu_dma_read(void) { | ((uint32_t)spu_ram[transfer_addr + 2] << 16) | ((uint32_t)spu_ram[transfer_addr + 3] << 24); } + spu_irq_check_range(transfer_addr, 4); /* any SPU-RAM access triggers */ transfer_addr = (transfer_addr + 4) % SPU_RAM_SIZE; return word; } diff --git a/runtime/src/traps.c b/runtime/src/traps.c index 77402276f..d4ac51fd0 100644 --- a/runtime/src/traps.c +++ b/runtime/src/traps.c @@ -340,7 +340,14 @@ static uint32_t psx_restore_context_from_tcb(CPUState* cpu, uint32_t tcb) uint32_t saved_sr = cpu->read_word(save + 140u); cpu->cop0[12] = (saved_sr & 0xFFFFFFC0u) | ((saved_sr >> 2) & 0x0Fu); } + /* Restore the saved CAUSE but re-derive IP2: on hardware the IP bits are + * live interrupt-line state, never memory — restoring a stale IP2 from a + * context saved before the ack re-latches a phantom interrupt. */ cpu->cop0[13] = cpu->read_word(save + 144u); + { + extern void psx_irq_refresh_cause_ip2(void); + psx_irq_refresh_cause_ip2(); + } cpu->gpr[26] = cpu->read_word(save + 128u); psx_assert_no_sentinel_pc("restore_context_from_tcb", tcb, cpu->gpr[26]); thread_ctx_ring_log(cpu, tcb, cpu->gpr[26], 1);