Something went wrong. Try again.
A Playstation One (PS1) emulator fully written in Jam
Something went wrong. Try again.
jamstation dma.jam
15 kB · 480 lines
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481// 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; }}