Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions runtime/include/audio_trace.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions runtime/include/spu.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion runtime/src/dma.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
38 changes: 38 additions & 0 deletions runtime/src/interrupts.c
Original file line number Diff line number Diff line change
Expand Up @@ -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<<bit)` — identical effect
* on i_stat, plus the trace note (no-op unless the ring is armed). */
/* CAUSE.IP2 is combinational on real hardware: it mirrors the INTC line
* ((I_STAT & I_MASK) != 0) and drops the instant the guest acks I_STAT or
* masks the source. This runtime previously only ever SET bit 10 at
* delivery and never cleared it, leaving a phantom IP2 in COP0.CAUSE. The
* retail kernel's exception dispatcher loops on CAUSE.IP&SR.IM to decide
* whether to service again before returning — a stale IP2 with I_STAT==0
* spins it forever in its event-scan (MOHU Mission 1 weapon-draw freeze:
* kernel event routine 0x1E88 flooding the interp ring, exceptions
* climbing, guest never progressing). Mirror the line at every point it
* can change: raise, I_STAT ack, I_MASK write, and the HLE context
* restore that reloads a saved CAUSE. */
static uint32_t *s_cause_ptr;
void psx_irq_set_cause_ptr(uint32_t *p) { s_cause_ptr = p; }
void psx_irq_refresh_cause_ip2(void)
{
if (!s_cause_ptr) return;
if ((i_stat & i_mask & 0x7FFu) != 0u)
*s_cause_ptr |= (1u << 10);
else
*s_cause_ptr &= ~(1u << 10);
}

void psx_irq_raise(uint32_t bit, uint32_t detail)
{
i_stat |= (1u << bit);
psx_irq_refresh_cause_ip2();
device_trace_note(bit, detail);
}

Expand Down Expand Up @@ -309,11 +332,26 @@ static int should_defer_vblank_for_sio(void) {
return since_progress < VBLANK_DEFER_STALE_CYCLES;
}

/* Mid-dispatch audio pump. The SPU on real hardware is autonomous: it keeps
* consuming samples while the CPU busy-waits. Our audio pump normally runs
* from the main loop between presented frames — a guest busy-wait that never
* completes a frame starves it, freezing voice positions. A game waiting on
* the SPU IRQ (voice playback crossing 0x1F801DA4) then deadlocks: the IRQ
* it waits for needs SPU time that only advances when the wait ends (MOHU
* Mission 1 weapon-draw freeze — kernel TestEvent busy-wait on event class
* 0xF0000009 while voice 0 marches toward the parked IRQ address). This
* VBlank edge demonstrably still fires during such waits, so pump audio
* here too; the pump is guest-cycle-budgeted (delta/768) and same-thread,
* making double-pumping from here + the main loop a no-op. */
static void (*s_midframe_audio_pump)(void);
void psx_set_midframe_audio_pump(void (*fn)(void)) { s_midframe_audio_pump = fn; }

static void fire_vblank_edge(void) {
/* Subtract one VBlank period rather than reset to 0 so cycle overshoot
* carries forward. Prevents long-running blocks from rounding multiple
* VBlanks together. */
cycles_since_vblank -= VBLANK_CYCLES;
if (s_midframe_audio_pump) s_midframe_audio_pump();
dispatch_count = 0;
/* DEQUEUE: this VBlank fired. ENQUEUE: next VBlank scheduled one period out. */
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_VBLANK,
Expand Down
9 changes: 9 additions & 0 deletions runtime/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ 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" void psx_set_midframe_audio_pump(void (*fn)(void));
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,
Expand Down Expand Up @@ -6640,6 +6642,9 @@ int main(int argc, char** argv) {
g_audio_host_rate = have.freq;
audio_trace_set_tap_rate(AUDIO_TAP_HOST, (uint32_t)have.freq);
(void)psx_sdl_audio_resume(sdl_audio_device);
/* Keep SPU time flowing during guest busy-waits: pump from the
* VBlank edge too (guest-cycle-budgeted; see interrupts.c). */
psx_set_midframe_audio_pump([]() { sdl_audio_pump(); });
}
}
#endif
Expand Down Expand Up @@ -6976,6 +6981,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);

Expand Down
4 changes: 4 additions & 0 deletions runtime/src/memory.c
Original file line number Diff line number Diff line change
Expand Up @@ -752,17 +752,21 @@ static void imask_trace_record(uint32_t old_val, uint32_t new_val, uint8_t width
* the freeze heartbeat against g_vblank_raise/deliver counts. */
uint64_t g_vblank_ack_count = 0;

extern void psx_irq_refresh_cause_ip2(void); /* interrupts.c — CAUSE.IP2 mirror */

static void interrupt_write_stat_masked(uint32_t val, uint32_t mask) {
uint32_t ack_mask = mask & 0x7FFu;
uint32_t before = i_stat;
i_stat = (i_stat & ~ack_mask) | (i_stat & val & ack_mask);
if ((before & 1u) && !(i_stat & 1u)) g_vblank_ack_count++; /* VBLANK bit 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 */
Expand Down
71 changes: 70 additions & 1 deletion runtime/src/spu.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
}
Expand All @@ -862,7 +912,26 @@ 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;
}

/* 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);
}
spu_irq_check_range(transfer_addr, 4); /* any SPU-RAM access triggers */
transfer_addr = (transfer_addr + 4) % SPU_RAM_SIZE;
return word;
}

int spu_dma_ready(void) {
Expand Down
7 changes: 7 additions & 0 deletions runtime/src/traps.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down