// SPDX-License-Identifier: MIT // Copyright (c) 2026 Raphael Amorim const { Gpu } = import("gpu"); const { irqRaise, IC_DMA } = import("irq"); const { Vec } = import("std/collections"); const { Bus } = import("bus"); const { Cdrom } = import("cdrom"); const { Mdec } = import("mdec"); const { spuWrite16, spuRead16 } = import("spu"); const { Disc } = import("disc"); const SPU_TFIFO_OFF: u32 = 0x1A8; // d[0..20] = 7 channels × 3 regs (MADR/BCR/CHCR) // d[21] = DPCR // d[22] = DICR // d[23..28]= per-channel "done" pulse flags // d[29] = SPU DMA (ch4) remaining cycles before completion. const DMA_STATE_WORDS: u32 = 30; const F_SPU_REMAIN: u32 = 29; const F_MADR: u32 = 0; const F_BCR: u32 = 1; const F_CHCR: u32 = 2; const CHCR_TDIR: u32 = 0x00000001; const CHCR_STEP: u32 = 0x00000002; const CHCR_SYNC: u32 = 0x00000600; const CHCR_BUSY: u32 = 0x01000000; const CHCR_TRIG: u32 = 0x10000000; const DICR_FORCE: u32 = 0x00008000; const DICR_FLAGS: u32 = 0x7F000000; const DICR_IRQEN: u32 = 0x00800000; const DICR_IRQSI: u32 = 0x80000000; // Owned DMA-state buffer. d[21] = DPCR is pre-set to its reset default // (0x07654321). Auto-drops with the Bus. pub fn createDma() Vec(u32) { var b: Vec(u32) = Vec(u32).filled(0, DMA_STATE_WORDS); b[21] = 0x07654321; return b; } pub fn chBase(ch: u32) u32 { return ch * 3; } pub fn ramRead32(bus: Bus, addr: u32) u32 { const off: u32 = addr & 0x001FFFFC; var p: *mut[] u8 = bus.ram.ptr; return (p[off] as u32) | ((p[off + 1] as u32) << 8) | ((p[off + 2] as u32) << 16) | ((p[off + 3] as u32) << 24); } pub fn ramWrite32(bus: Bus, addr: u32, val: u32) { const off: u32 = addr & 0x001FFFFC; var p: *mut[] u8 = bus.ram.ptr; p[off] = (val & 0xFF) as u8; p[off + 1] = ((val >> 8) & 0xFF) as u8; p[off + 2] = ((val >> 16) & 0xFF) as u8; p[off + 3] = ((val >> 24) & 0xFF) as u8; } pub fn chcrHw1(ch: u32) u32 { if (ch == 6) { return 0x00000002; } return 0x00000000; } pub fn chcrHw0(ch: u32) u32 { if (ch == 6) { return 0x50000002; } return 0x71770703; } pub fn dmaRead32(d: *mut[] u32, off: u32) u32 { if (off < 0x70) { const ch: u32 = (off >> 4) & 0x7; const reg: u32 = (off >> 2) & 0x3; match (reg) { 0 { return d[chBase(ch) + F_MADR]; } 1 { return d[chBase(ch) + F_BCR]; } 2 { var cr: u32 = d[chBase(ch) + F_CHCR]; cr = cr | chcrHw1(ch); cr = cr & chcrHw0(ch); return cr; } _ { return 0; } } } if (off == 0x70) { return d[21]; } if (off == 0x74) { return d[22]; } return 0; } pub fn dmaRead16(d: *mut[] u32, off: u32) u32 { const shift: u32 = (off & 2) * 8; return (dmaRead32(d, off & 0xFFFFFFFC) >> shift) & 0xFFFF; } pub fn dmaRead8(d: *mut[] u32, off: u32) u32 { const shift: u32 = (off & 3) * 8; return (dmaRead32(d, off & 0xFFFFFFFC) >> shift) & 0xFF; } pub fn dmaWriteDicr(d: *mut[] u32, val: u32) { const ack: u32 = val & DICR_FLAGS; var flags: u32 = d[22] & DICR_FLAGS; flags = (flags & (~ack)) & DICR_FLAGS; var nd: u32 = d[22] & 0x80000000; nd = nd | flags; nd = nd | (val & 0xFFFFFF); d[22] = nd; } pub fn dmaWrite32(d: *mut[] u32, bus: Bus, cop0: *mut[] u32, off: u32, val: u32) { if (off < 0x70) { const ch: u32 = (off >> 4) & 0x7; const reg: u32 = (off >> 2) & 0x3; match (reg) { 0 { d[chBase(ch) + F_MADR] = val; } 1 { d[chBase(ch) + F_BCR] = val; } 2 { d[chBase(ch) + F_CHCR] = val; dmaTrigger(d, bus, cop0, ch); } _ {} } return; } match (off) { 0x70 { d[21] = val; } 0x74 { dmaWriteDicr(d, val); } _ {} } } pub fn dmaWrite16(d: *mut[] u32, bus: Bus, cop0: *mut[] u32, off: u32, val: u32) { match (off) { 0x74 { dmaWriteDicr(d, val & 0xFFFF); return; } 0x76 { dmaWriteDicr(d, (val & 0xFFFF) << 16); return; } _ {} } dmaWrite32(d, bus, cop0, off, val & 0xFFFF); } pub fn dmaWrite8(d: *mut[] u32, bus: Bus, cop0: *mut[] u32, off: u32, val: u32) { match (off) { 0x74 { dmaWriteDicr(d, val & 0xFF); return; } 0x75 { dmaWriteDicr(d, (val & 0xFF) << 8); return; } 0x76 { dmaWriteDicr(d, (val & 0xFF) << 16); return; } 0x77 { dmaWriteDicr(d, (val & 0xFF) << 24); return; } _ {} } dmaWrite32(d, bus, cop0, off, val & 0xFF); } pub fn dmaTrigger(d: *mut[] u32, bus: Bus, cop0: *mut[] u32, ch: u32) { match (ch) { 0 { dmaDoMdecIn(d, bus); } 1 { dmaDoMdecOut(d, bus); } 2 { dmaDoGpu(d, bus); } 3 { dmaDoCdrom(d, bus); } 4 { dmaDoSpu(d, bus); } 6 { dmaDoOtc(d, bus); } _ {} } } pub fn chcrBusy(d: *mut[] u32, ch: u32) bool { return (d[chBase(ch) + F_CHCR] & CHCR_BUSY) != 0; } pub fn chcrTdir(d: *mut[] u32, ch: u32) bool { return (d[chBase(ch) + F_CHCR] & CHCR_TDIR) != 0; } pub fn chcrSync(d: *mut[] u32, ch: u32) u32 { return (d[chBase(ch) + F_CHCR] & CHCR_SYNC) >> 9; } pub fn chcrStep(d: *mut[] u32, ch: u32) u32 { if ((d[chBase(ch) + F_CHCR] & CHCR_STEP) != 0) { return 0xFFFFFFFC; } return 4; } pub fn bcrSize(d: *mut[] u32, ch: u32) u32 { return d[chBase(ch) + F_BCR] & 0xFFFF; } pub fn bcrBlocks(d: *mut[] u32, ch: u32) u32 { return (d[chBase(ch) + F_BCR] >> 16) & 0xFFFF; } pub fn dmaClear(d: *mut[] u32, ch: u32) { d[chBase(ch) + F_CHCR] = d[chBase(ch) + F_CHCR] & (~(CHCR_BUSY | CHCR_TRIG)); d[chBase(ch) + F_BCR] = 0; } pub fn dmaDoGpu(d: *mut[] u32, bus: Bus) { if (!chcrBusy(d, 2)) { return; } match (chcrSync(d, 2)) { 0 | 1 { dmaDoGpuRequest(d, bus); } 2 { dmaDoGpuLinked(d, bus); } _ {} } dmaClear(d, 2); d[25] = 1; } pub fn dmaDoGpuRequest(d: *mut[] u32, bus: Bus) { const size: u32 = bcrSize(d, 2) * bcrBlocks(d, 2); const step: u32 = chcrStep(d, 2); var addr: u32 = d[chBase(2) + F_MADR]; if (chcrTdir(d, 2)) { var i: u32 = 0; while (i < size) { const data: u32 = ramRead32(bus, addr); bus.gpu.gp0(bus.vram.ptr, data); addr = addr + step; i = i + 1; } } else { // GPU->RAM (C0 transfer): drain GPUREAD per word into RAM. // Each word reads GPUREAD at 0x1F801810, which // pulls from VRAM via the active C0 state machine. // Our previous stub wrote zeros, which broke // games that DMA-copy VRAM back to RAM (font caches, frame // composites, etc.). var i: u32 = 0; while (i < size) { const data: u32 = bus.gpu.read32(bus.vram.ptr, 0); ramWrite32(bus, addr, data); addr = addr + step; i = i + 1; } } d[chBase(2) + F_MADR] = addr; } pub fn dmaDoGpuLinked(d: *mut[] u32, bus: Bus) { var hdr: u32 = ramRead32(bus, d[chBase(2) + F_MADR]); var addr: u32 = d[chBase(2) + F_MADR]; const step: u32 = chcrStep(d, 2); var timeout: u32 = 16384; while (timeout > 0) { var size: u32 = hdr >> 24; while (size > 0) { addr = (addr + step) & 0x001FFFFC; const cmd: u32 = ramRead32(bus, addr); bus.gpu.gp0(bus.vram.ptr, cmd); size = size - 1; } addr = hdr & 0xFFFFFF; if (addr == 0xFFFFFF) { return; } hdr = ramRead32(bus, addr); timeout = timeout - 1; } } pub fn dmaDoMdecIn(d: *mut[] u32, bus: Bus) { if (!chcrBusy(d, 0)) { return; } const size: u32 = bcrSize(d, 0) * bcrBlocks(d, 0); const step: u32 = chcrStep(d, 0); var addr: u32 = d[chBase(0) + F_MADR]; var i: u32 = 0; while (i < size) { const w: u32 = ramRead32(bus, addr); bus.mdec.write32(bus.mdecIn.ptr, bus.mdecOut.ptr, 0, w); addr = addr + step; i = i + 1; } d[chBase(0) + F_MADR] = addr; d[chBase(0) + F_CHCR] = 0; d[chBase(0) + F_BCR] = 0; d[23] = size; } pub fn dmaDoMdecOut(d: *mut[] u32, bus: Bus) { if (!chcrBusy(d, 1)) { return; } const size: u32 = bcrSize(d, 1) * bcrBlocks(d, 1); const step: u32 = chcrStep(d, 1); var addr: u32 = d[chBase(1) + F_MADR]; var i: u32 = 0; while (i < size) { const w: u32 = bus.mdec.read32(bus.mdecOut.ptr, 0); ramWrite32(bus, addr, w); addr = addr + step; i = i + 1; } d[chBase(1) + F_MADR] = addr; d[chBase(1) + F_CHCR] = 0; d[chBase(1) + F_BCR] = 0; d[24] = size; } // CD-ROM -> RAM transfer. Each "word" in BCR.size is 4 bytes from the // data FIFO at 0x1F801802. Streaming goes through // cdromReadData(), popping one byte at a time and packing into a // little-endian word. After the transfer, ack via the d[26] flag so // dmaUpdate raises the channel-3 IRQ if enabled. pub fn dmaDoCdrom(d: *mut[] u32, bus: Bus) { if (!chcrBusy(d, 3)) { return; } // CD->RAM is SyncMode 0 (burst: BCR low 16 = word count) or SyncMode 1 // (block: low = block size in words, high = block count; total = // size*blocks). The old code used only bcrSize, so a SyncMode-1 read // transferred ONE block instead of all of them. var size: u32 = 0; match (chcrSync(d, 3)) { 0 { size = bcrSize(d, 3); if (size == 0) { size = 0x10000; } } 1 { size = bcrSize(d, 3) * bcrBlocks(d, 3); } _ { return; } } var addr: u32 = d[chBase(3) + F_MADR]; const step: u32 = chcrStep(d, 3); var i: u32 = 0; while (i < size) { const b0: u32 = bus.cdrom.readData(); const b1: u32 = bus.cdrom.readData(); const b2: u32 = bus.cdrom.readData(); const b3: u32 = bus.cdrom.readData(); const w: u32 = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24); ramWrite32(bus, addr, w); addr = addr + step; i = i + 1; } d[chBase(3) + F_MADR] = addr; d[chBase(3) + F_CHCR] = 0; d[chBase(3) + F_BCR] = 0; d[26] = 1; } // SPU DMA (channel 4). Each 32-bit word is piped through // the SPU TFIFO register at 0x1F801DA8 as two 16-bit halves, low first. // Reverse direction reads two halves from TFIFO and packs them. Previously // we drop the data - meaning Musashi/HM ADPCM banks were never reaching // SPU RAM and the BIOS "wait for SPU drain" loops would never advance. pub fn dmaDoSpu(d: *mut[] u32, bus: Bus) { if (!chcrBusy(d, 4)) { return; } // Sync mode 0 (burst/immediate): BCR low 16 = word count, hi 16 = 0. // Sync mode 1 (block-to-DMA-request): BCR low 16 = block size (words), // BCR hi 16 = block count. Treat as a flat word count for the loop - // ps1-tests testDMAReadToRamSyncMode0 was getting 0 transfers because // blocks was 0 in mode 0. var words: u32 = 0; match (chcrSync(d, 4)) { 0 { words = bcrSize(d, 4); if (words == 0) { words = 0x10000; } } 1 { words = bcrSize(d, 4) * bcrBlocks(d, 4); } _ { return; } } const step: u32 = chcrStep(d, 4); var addr: u32 = d[chBase(4) + F_MADR]; if (chcrTdir(d, 4)) { var i: u32 = 0; while (i < words) { const data: u32 = ramRead32(bus, addr); spuWrite16(bus.spu.ptr, bus.spuram.ptr, SPU_TFIFO_OFF, data & 0xFFFF); spuWrite16(bus.spu.ptr, bus.spuram.ptr, SPU_TFIFO_OFF, (data >> 16) & 0xFFFF); addr = addr + step; i = i + 1; } } else { var i: u32 = 0; while (i < words) { const lo: u32 = spuRead16(bus.spu.ptr, bus.spuram.ptr, SPU_TFIFO_OFF); const hi: u32 = spuRead16(bus.spu.ptr, bus.spuram.ptr, SPU_TFIFO_OFF); ramWrite32(bus, addr, lo | (hi << 16)); addr = addr + step; i = i + 1; } } d[chBase(4) + F_MADR] = addr; d[chBase(4) + F_CHCR] = 0; d[chBase(4) + F_BCR] = 0; d[27] = 1; } pub fn dmaDoOtc(d: *mut[] u32, bus: Bus) { if ((d[21] & 0x08000000) == 0) { return; } const cr: u32 = d[chBase(6) + F_CHCR]; if ((cr & CHCR_TRIG) == 0) { return; } if ((cr & CHCR_BUSY) == 0) { return; } var size: u32 = bcrSize(d, 6); if (size == 0) { size = 0x10000; } var addr: u32 = d[chBase(6) + F_MADR]; var i: u32 = size; while (i > 0) { var next: u32 = 0xFFFFFF; if (i != 1) { next = (addr - 4) & 0xFFFFFF; } ramWrite32(bus, addr, next); addr = addr - 4; i = i - 1; } d[chBase(6) + F_MADR] = addr; d[chBase(6) + F_CHCR] = 0; d[chBase(6) + F_BCR] = 0; d[28] = size; } pub fn dmaTickSpu(d: *mut[] u32, delta: u32) { if (d[F_SPU_REMAIN] == 0) { return; } if (delta >= d[F_SPU_REMAIN]) { d[F_SPU_REMAIN] = 0; d[chBase(4) + F_CHCR] = d[chBase(4) + F_CHCR] & (~CHCR_BUSY); d[27] = 32; } else { d[F_SPU_REMAIN] = d[F_SPU_REMAIN] - delta; } } pub fn dmaUpdate(d: *mut[] u32, ic: *mut[] u32, cop0: *mut[] u32) { if (d[23] != 0) { d[23] = d[23] - 1; if (d[23] == 0 && (d[22] & 0x00010000) != 0) { d[22] = d[22] | 0x01000000; } } if (d[24] != 0) { d[24] = d[24] - 1; if (d[24] == 0 && (d[22] & 0x00020000) != 0) { d[22] = d[22] | 0x02000000; } } if (d[25] != 0) { d[25] = 0; if ((d[22] & 0x00040000) != 0) { d[22] = d[22] | 0x04000000; } } if (d[26] != 0) { d[26] = 0; if ((d[22] & 0x00080000) != 0) { d[22] = d[22] | 0x08000000; } } if (d[27] != 0) { d[27] = 0; if ((d[22] & 0x00100000) != 0) { d[22] = d[22] | 0x10000000; } } if (d[28] != 0) { d[28] = 0; if ((d[22] & 0x00400000) != 0) { d[22] = d[22] | 0x40000000; } } const prev: bool = (d[22] & DICR_IRQSI) != 0; const irqEn: bool = (d[22] & DICR_IRQEN) != 0; const forceB: bool = (d[22] & DICR_FORCE) != 0; const flagsB: bool = (d[22] & DICR_FLAGS) != 0; const signal: bool = forceB || (flagsB && irqEn); if (signal && !prev) { irqRaise(ic, cop0, IC_DMA); } d[22] = d[22] & (~DICR_IRQSI); if (signal) { d[22] = d[22] | DICR_IRQSI; } }