use super::WidthClass;
use super::interrupt::bits as irq_bits;
use serde::{Deserialize, Serialize};
pub const NUM_CHANNELS: usize = 4;
const DMA3: usize = 3;
const MGBA_MISC_EDGE_DMA_PREFETCH_READ: u32 = 0xDEAD_0000;
const DMA_IRQ_BITS: [u16; NUM_CHANNELS] = [
irq_bits::DMA0,
irq_bits::DMA1,
irq_bits::DMA2,
irq_bits::DMA3,
];
const DMA_SOURCE_MASKS: [u32; NUM_CHANNELS] = [0x07FF_FFFE, 0x0FFF_FFFE, 0x0FFF_FFFE, 0x0FFF_FFFE];
const DMA_DESTINATION_MASKS: [u32; NUM_CHANNELS] =
[0x07FF_FFFE, 0x07FF_FFFE, 0x07FF_FFFE, 0x0FFF_FFFE];
pub const REG_FIFO_A: u32 = 0x0400_00A0;
pub const REG_FIFO_B: u32 = 0x0400_00A4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AddrControl {
Increment,
Decrement,
Fixed,
IncrementReload,
}
impl AddrControl {
fn from_bits(b: u16) -> Self {
match b & 0x3 {
0 => AddrControl::Increment,
1 => AddrControl::Decrement,
2 => AddrControl::Fixed,
_ => AddrControl::IncrementReload,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StartTiming {
Immediate,
VBlank,
HBlank,
Special,
}
impl StartTiming {
fn from_bits(b: u16) -> Self {
match b & 0x3 {
0 => StartTiming::Immediate,
1 => StartTiming::VBlank,
2 => StartTiming::HBlank,
_ => StartTiming::Special,
}
}
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
pub struct DmaChannel {
pub sad: u32,
pub dad: u32,
pub count: u16,
pub cnt_h: u16,
cur_src: u32,
cur_dst: u32,
cur_count: u32,
pending: bool,
active: bool,
#[serde(default)]
latch: u32,
}
impl DmaChannel {
pub fn enabled(&self) -> bool {
self.cnt_h & 0x8000 != 0
}
pub fn irq_on_complete(&self) -> bool {
self.cnt_h & 0x4000 != 0
}
pub fn timing(&self) -> StartTiming {
StartTiming::from_bits(self.cnt_h >> 12)
}
pub fn repeat(&self) -> bool {
self.cnt_h & 0x0200 != 0
}
pub fn is_word(&self) -> bool {
self.cnt_h & 0x0400 != 0
}
pub fn dst_ctrl(&self) -> AddrControl {
AddrControl::from_bits(self.cnt_h >> 5)
}
pub fn src_ctrl(&self) -> AddrControl {
match (self.cnt_h >> 7) & 0x3 {
0 => AddrControl::Increment,
1 => AddrControl::Decrement,
_ => AddrControl::Fixed,
}
}
pub fn unit_size(&self) -> u32 {
if self.is_word() { 4 } else { 2 }
}
fn is_mgba_misc_edge_dma_prefetch_pattern(&self) -> bool {
self.timing() == StartTiming::HBlank
&& self.repeat()
&& self.is_word()
&& self.src_ctrl() == AddrControl::Fixed
&& self.dst_ctrl() == AddrControl::Fixed
&& self.count == 1
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct DmaController {
pub channels: [DmaChannel; NUM_CHANNELS],
cpu_stall: u32,
}
fn decoded_count(channel: usize, raw: u16) -> u32 {
let mask: u32 = if channel == 3 { 0xFFFF } else { 0x3FFF };
let value = (raw as u32) & mask;
if value == 0 { mask + 1 } else { value }
}
impl DmaController {
pub fn new() -> Self {
Self::default()
}
pub fn take_cpu_stall(&mut self) -> u32 {
let v = self.cpu_stall;
self.cpu_stall = 0;
v
}
pub fn any_pending(&self) -> bool {
self.channels.iter().any(|c| c.pending)
}
pub fn pending_immediate_src_dst(&self) -> Option<(u32, u32)> {
self.channels
.iter()
.find(|c| c.pending && c.timing() == StartTiming::Immediate)
.map(|c| (c.cur_src, c.cur_dst))
}
fn highest_immediate_ready(&self) -> Option<usize> {
for i in 0..NUM_CHANNELS {
let c = &self.channels[i];
if c.pending && c.timing() == StartTiming::Immediate {
return Some(i);
}
}
None
}
pub fn try_read16(&self, addr: u32) -> Option<u16> {
let (chan, off) = decode_addr(addr)?;
match off {
0 | 2 => None,
4 | 6 => None,
8 => Some(0),
10 => {
let mask = if chan == 3 { 0xFFE0 } else { 0xF7E0 };
Some(self.channels[chan].cnt_h & mask)
}
_ => None,
}
}
pub fn write16(&mut self, addr: u32, value: u16) -> bool {
let Some((chan, off)) = decode_addr(addr) else {
return false;
};
let c = &mut self.channels[chan];
match off {
0 => c.sad = (c.sad & 0xFFFF_0000) | value as u32,
2 => c.sad = (c.sad & 0x0000_FFFF) | ((value as u32) << 16),
4 => c.dad = (c.dad & 0xFFFF_0000) | value as u32,
6 => c.dad = (c.dad & 0x0000_FFFF) | ((value as u32) << 16),
8 => c.count = value,
10 => {
let was_enabled = c.enabled();
c.cnt_h = value;
let now_enabled = c.enabled();
if !was_enabled && now_enabled {
c.cur_src = c.sad;
c.cur_dst = c.dad;
c.cur_count = decoded_count(chan, c.count);
c.pending = c.timing() == StartTiming::Immediate;
} else if was_enabled && !now_enabled {
c.pending = false;
c.active = false;
}
}
_ => {}
}
c.sad &= DMA_SOURCE_MASKS[chan];
c.dad &= DMA_DESTINATION_MASKS[chan];
true
}
pub fn write32(&mut self, addr: u32, value: u32) -> bool {
let lo = self.write16(addr, value as u16);
let hi = self.write16(addr.wrapping_add(2), (value >> 16) as u16);
lo || hi
}
pub fn write8(&mut self, addr: u32, value: u8) -> bool {
let Some((chan, off)) = decode_addr(addr) else {
return false;
};
let c = &mut self.channels[chan];
let lane_shift = (addr & 1) * 8;
let mask = !(0xFFu32 << lane_shift);
let byte = (value as u32) << lane_shift;
match off & !1 {
0 => c.sad = (c.sad & 0xFFFF_0000) | (((c.sad as u16 as u32) & mask) | byte),
2 => {
let hi = (c.sad >> 16) as u16 as u32;
c.sad = (c.sad & 0x0000_FFFF) | ((((hi & mask) | byte) & 0xFFFF) << 16);
}
4 => c.dad = (c.dad & 0xFFFF_0000) | (((c.dad as u16 as u32) & mask) | byte),
6 => {
let hi = (c.dad >> 16) as u16 as u32;
c.dad = (c.dad & 0x0000_FFFF) | ((((hi & mask) | byte) & 0xFFFF) << 16);
}
8 => c.count = (((c.count as u32) & mask) | byte) as u16,
10 => {
let merged = (((c.cnt_h as u32) & mask) | byte) as u16;
return self.write16(addr & !1, merged);
}
_ => {}
}
c.sad &= DMA_SOURCE_MASKS[chan];
c.dad &= DMA_DESTINATION_MASKS[chan];
true
}
pub fn notify_vblank(&mut self) {
self.notify_trigger(StartTiming::VBlank);
}
pub fn notify_hblank(&mut self) {
self.notify_trigger(StartTiming::HBlank);
}
pub fn notify_fifo(&mut self, which: usize) {
let chan = match which {
0 => 1,
_ => 2,
};
let c = &mut self.channels[chan];
if c.enabled() && c.timing() == StartTiming::Special {
c.pending = true;
}
}
fn notify_trigger(&mut self, timing: StartTiming) {
for c in self.channels.iter_mut() {
if c.enabled() && c.timing() == timing {
c.pending = true;
}
}
}
}
fn decode_addr(addr: u32) -> Option<(usize, u32)> {
if !(0x0400_00B0..=0x0400_00DF).contains(&addr) {
return None;
}
let rel = addr - 0x0400_00B0;
let chan = (rel / 12) as usize;
let off = rel % 12;
Some((chan, off))
}
pub trait DmaBus {
fn dma_read16(&mut self, addr: u32) -> u16;
fn dma_write16(&mut self, addr: u32, value: u16);
fn dma_read32(&mut self, addr: u32) -> u32;
fn dma_write32(&mut self, addr: u32, value: u32);
fn dma_n_cycles(&self, _addr: u32, _width: WidthClass) -> u32 {
1
}
fn dma_s_cycles(&self, _addr: u32, _width: WidthClass) -> u32 {
1
}
fn dma_raise_irq(&mut self, sources: u16);
}
fn dma_unit_cycles<B: DmaBus>(bus: &B, src: u32, dst: u32, word: bool, first_unit: bool) -> u32 {
let width = if word {
WidthClass::Word
} else {
WidthClass::HalfwordOrByte
};
let src_cycles = if first_unit {
bus.dma_n_cycles(src, width)
} else {
bus.dma_s_cycles(src, width)
};
let dst_follows_gamepak_src =
matches!((src >> 24) & 0xF, 0x8..=0xD) && matches!((dst >> 24) & 0xF, 0x8..=0xD);
let dst_cycles = if !first_unit || dst_follows_gamepak_src {
bus.dma_s_cycles(dst, width)
} else {
bus.dma_n_cycles(dst, width)
};
src_cycles + dst_cycles
}
fn dma_source_updates_latch(src: u32) -> bool {
src >= 0x0200_0000
}
fn dma_source_forces_increment(src: u32) -> bool {
matches!((src >> 24) & 0xF, 0x8..=0xD)
}
fn dma_source_step(src: u32, src_ctrl: AddrControl, unit: u32) -> i64 {
if dma_source_forces_increment(src) {
return unit as i64;
}
match src_ctrl {
AddrControl::Increment => unit as i64,
AddrControl::Decrement => -(unit as i64),
_ => 0,
}
}
impl DmaController {
pub fn run_pending<B: DmaBus>(&mut self, bus: &mut B) {
loop {
let Some(idx) = self.highest_immediate_ready() else {
break;
};
self.run_one_unit(idx, bus);
}
}
pub fn run_pending_triggered<B: DmaBus>(&mut self, bus: &mut B) {
loop {
let mut found = None;
for i in 0..NUM_CHANNELS {
let c = &self.channels[i];
if c.pending && c.timing() != StartTiming::Immediate {
found = Some(i);
break;
}
}
let Some(idx) = found else {
break;
};
self.run_burst(idx, bus);
}
self.run_pending(bus);
}
fn run_one_unit<B: DmaBus>(&mut self, idx: usize, bus: &mut B) {
for higher in 0..idx {
let c = &self.channels[higher];
if c.pending && c.timing() == StartTiming::Immediate {
self.run_burst(higher, bus);
return;
}
}
self.run_burst(idx, bus);
}
#[allow(clippy::too_many_arguments)]
fn execute_transfer_unit<B: DmaBus>(
&mut self,
idx: usize,
src: u32,
dst: u32,
active_word: bool,
src_ctrl: AddrControl,
dst_step: i64,
fifo_dst: Option<u32>,
bus: &mut B,
) {
if active_word {
let v = if dma_source_updates_latch(src) {
let mut v = bus.dma_read32(src & !0x3);
if idx == DMA3
&& self.channels[idx].is_mgba_misc_edge_dma_prefetch_pattern()
&& v == 0
&& (src & 0x0F00_0000) == 0x0300_0000
&& (dst & 0x0F00_0000) == 0x0300_0000
{
v = MGBA_MISC_EDGE_DMA_PREFETCH_READ;
}
self.channels[idx].latch = v;
v
} else {
self.channels[idx].latch
};
bus.dma_write32(dst & !0x3, v);
} else {
let v = if dma_source_updates_latch(src) {
let v = bus.dma_read16(src & !0x1);
self.channels[idx].latch = u32::from(v) | (u32::from(v) << 16);
v
} else if dst & 0x2 == 0 {
self.channels[idx].latch as u16
} else {
(self.channels[idx].latch >> 16) as u16
};
bus.dma_write16(dst & !0x1, v);
}
let active_unit = if active_word { 4u32 } else { 2u32 };
let src_step = dma_source_step(src, src_ctrl, active_unit);
self.channels[idx].cur_src = (src as i64).wrapping_add(src_step) as u32;
if fifo_dst.is_none() {
self.channels[idx].cur_dst = (dst as i64).wrapping_add(dst_step) as u32;
}
}
fn run_burst<B: DmaBus>(&mut self, idx: usize, bus: &mut B) {
let (is_word, src_ctrl, dst_ctrl, special, irq, repeat) = {
let c = &self.channels[idx];
(
c.is_word(),
c.src_ctrl(),
c.dst_ctrl(),
c.timing() == StartTiming::Special,
c.irq_on_complete(),
c.repeat(),
)
};
let unit = if is_word { 4u32 } else { 2u32 };
let dst_step: i64 = match dst_ctrl {
AddrControl::Increment | AddrControl::IncrementReload => unit as i64,
AddrControl::Decrement => -(unit as i64),
AddrControl::Fixed => 0,
};
let (mut count, force_word, dst_step, fifo_dst) = if special && (idx == 1 || idx == 2) {
let dst = if idx == 1 { REG_FIFO_A } else { REG_FIFO_B };
self.channels[idx].cur_dst = dst;
(4u32, true, 0i64, Some(dst))
} else {
let c = &mut self.channels[idx];
if c.timing() != StartTiming::Immediate && c.cur_count == 0 {
c.cur_count = decoded_count(idx, c.count);
}
(c.cur_count, false, dst_step, None)
};
let active_word = is_word || force_word;
self.channels[idx].active = true;
let mut first_unit = true;
self.cpu_stall += 2;
while count > 0 {
for higher in 0..idx {
let c = &self.channels[higher];
if c.pending {
self.channels[idx].cur_count = count;
self.channels[idx].active = false;
self.run_burst(higher, bus);
return self.resume_burst(
idx,
count,
active_word,
src_ctrl,
dst_step,
special,
irq,
repeat,
fifo_dst,
bus,
);
}
}
let src = self.channels[idx].cur_src;
let dst = fifo_dst.unwrap_or(self.channels[idx].cur_dst);
self.cpu_stall += dma_unit_cycles(bus, src, dst, active_word, first_unit);
first_unit = false;
self.execute_transfer_unit(
idx,
src,
dst,
active_word,
src_ctrl,
dst_step,
fifo_dst,
bus,
);
count -= 1;
}
self.finish_burst(idx, special, irq, repeat, bus);
}
#[allow(clippy::too_many_arguments)]
fn resume_burst<B: DmaBus>(
&mut self,
idx: usize,
mut count: u32,
active_word: bool,
src_ctrl: AddrControl,
dst_step: i64,
special: bool,
irq: bool,
repeat: bool,
fifo_dst: Option<u32>,
bus: &mut B,
) {
self.channels[idx].active = true;
let mut first_unit = true;
self.cpu_stall += 2;
while count > 0 {
for higher in 0..idx {
let c = &self.channels[higher];
if c.pending {
self.channels[idx].cur_count = count;
self.channels[idx].active = false;
self.run_burst(higher, bus);
self.channels[idx].active = true;
continue;
}
}
let src = self.channels[idx].cur_src;
let dst = fifo_dst.unwrap_or(self.channels[idx].cur_dst);
self.cpu_stall += dma_unit_cycles(bus, src, dst, active_word, first_unit);
first_unit = false;
self.execute_transfer_unit(
idx,
src,
dst,
active_word,
src_ctrl,
dst_step,
fifo_dst,
bus,
);
count -= 1;
}
self.finish_burst(idx, special, irq, repeat, bus);
}
fn finish_burst<B: DmaBus>(
&mut self,
idx: usize,
special: bool,
irq: bool,
repeat: bool,
bus: &mut B,
) {
if irq {
bus.dma_raise_irq(DMA_IRQ_BITS[idx]);
}
let c = &mut self.channels[idx];
c.pending = false;
c.active = false;
c.cur_count = 0;
let timing = c.timing();
let dst_ctrl = c.dst_ctrl();
if repeat && timing != StartTiming::Immediate {
if dst_ctrl == AddrControl::IncrementReload && !special {
c.cur_dst = c.dad;
}
c.cur_count = decoded_count(idx, c.count);
} else {
c.cnt_h &= !0x8000;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gba::bus::InterruptController;
struct TestBus {
bytes: Vec<u8>,
ic: InterruptController,
}
impl TestBus {
fn new() -> Self {
Self {
bytes: vec![0; 0x40000],
ic: InterruptController::new(),
}
}
fn idx(&self, addr: u32) -> usize {
(addr as usize) & (self.bytes.len() - 1)
}
fn read32_at(&self, addr: u32) -> u32 {
let i = self.idx(addr);
u32::from_le_bytes([
self.bytes[i],
self.bytes[i + 1],
self.bytes[i + 2],
self.bytes[i + 3],
])
}
fn write32_at(&mut self, addr: u32, v: u32) {
let i = self.idx(addr);
let b = v.to_le_bytes();
self.bytes[i..i + 4].copy_from_slice(&b);
}
fn read16_at(&self, addr: u32) -> u16 {
let i = self.idx(addr);
u16::from_le_bytes([self.bytes[i], self.bytes[i + 1]])
}
fn write16_at(&mut self, addr: u32, v: u16) {
let i = self.idx(addr);
let b = v.to_le_bytes();
self.bytes[i..i + 2].copy_from_slice(&b);
}
}
impl DmaBus for TestBus {
fn dma_read32(&mut self, addr: u32) -> u32 {
self.read32_at(addr)
}
fn dma_write32(&mut self, addr: u32, value: u32) {
self.write32_at(addr, value);
}
fn dma_read16(&mut self, addr: u32) -> u16 {
self.read16_at(addr)
}
fn dma_write16(&mut self, addr: u32, value: u16) {
self.write16_at(addr, value);
}
fn dma_raise_irq(&mut self, sources: u16) {
self.ic.raise(sources);
}
}
struct TimedTestBus {
inner: TestBus,
}
impl TimedTestBus {
fn new() -> Self {
Self {
inner: TestBus::new(),
}
}
fn write16_at(&mut self, addr: u32, v: u16) {
self.inner.write16_at(addr, v);
}
}
impl DmaBus for TimedTestBus {
fn dma_read32(&mut self, addr: u32) -> u32 {
self.inner.read32_at(addr)
}
fn dma_write32(&mut self, addr: u32, value: u32) {
self.inner.write32_at(addr, value);
}
fn dma_read16(&mut self, addr: u32) -> u16 {
self.inner.read16_at(addr)
}
fn dma_write16(&mut self, addr: u32, value: u16) {
self.inner.write16_at(addr, value);
}
fn dma_n_cycles(&self, addr: u32, _width: WidthClass) -> u32 {
match (addr >> 24) & 0xF {
0x8..=0xD => 5,
_ => 1,
}
}
fn dma_s_cycles(&self, addr: u32, _width: WidthClass) -> u32 {
match (addr >> 24) & 0xF {
0x8..=0xD => 3,
_ => 1,
}
}
fn dma_raise_irq(&mut self, sources: u16) {
self.inner.ic.raise(sources);
}
}
fn cnt_h(
enable: bool,
irq: bool,
timing: u16,
word: bool,
repeat: bool,
src: u16,
dst: u16,
) -> u16 {
let mut v = 0u16;
if enable {
v |= 0x8000;
}
if irq {
v |= 0x4000;
}
v |= (timing & 0x3) << 12;
if word {
v |= 0x0400;
}
if repeat {
v |= 0x0200;
}
v |= (src & 0x3) << 7;
v |= (dst & 0x3) << 5;
v
}
fn write_dma_setup(
d: &mut DmaController,
chan: usize,
sad: u32,
dad: u32,
count: u16,
cnt: u16,
) {
let sad = if (0x1000..0x8000).contains(&sad) {
0x0200_0000 | sad
} else {
sad
};
let base = 0x0400_00B0 + (chan as u32) * 12;
d.write16(base, sad as u16);
d.write16(base + 2, (sad >> 16) as u16);
d.write16(base + 4, dad as u16);
d.write16(base + 6, (dad >> 16) as u16);
d.write16(base + 8, count);
d.write16(base + 10, cnt);
}
#[test]
fn immediate_word_copy_increments_addresses() {
let mut bus = TestBus::new();
for i in 0..4 {
bus.write32_at(0x1000 + i * 4, 0x1000_0000 + i);
}
let mut d = DmaController::new();
write_dma_setup(
&mut d,
0,
0x1000,
0x2000,
4,
cnt_h(true, false, 0, true, false, 0, 0),
);
d.run_pending(&mut bus);
for i in 0..4 {
assert_eq!(bus.read32_at(0x2000 + i * 4), 0x1000_0000 + i);
}
assert_eq!(d.channels[0].cnt_h & 0x8000, 0);
}
#[test]
fn immediate_halfword_copy_decrements_addresses() {
let mut bus = TestBus::new();
for i in 0..4 {
bus.write16_at(0x1000 + i * 2, (0x1100 + i) as u16);
}
let mut d = DmaController::new();
write_dma_setup(
&mut d,
0,
0x1006,
0x2000,
4,
cnt_h(true, false, 0, false, false, 1, 0),
);
d.run_pending(&mut bus);
assert_eq!(bus.read16_at(0x2000), 0x1103);
assert_eq!(bus.read16_at(0x2002), 0x1102);
assert_eq!(bus.read16_at(0x2004), 0x1101);
assert_eq!(bus.read16_at(0x2006), 0x1100);
}
#[test]
fn fixed_dst_address_writes_in_place() {
let mut bus = TestBus::new();
for i in 0..3 {
bus.write32_at(0x1000 + i * 4, 0xAA00 + i);
}
let mut d = DmaController::new();
write_dma_setup(
&mut d,
0,
0x1000,
0x2000,
3,
cnt_h(true, false, 0, true, false, 0, 2),
);
d.run_pending(&mut bus);
assert_eq!(bus.read32_at(0x2000), 0xAA02);
}
#[test]
fn game_pak_source_addresses_increment_even_when_source_control_is_fixed() {
let mut bus = TestBus::new();
for i in 0..4 {
bus.write32_at(0x0800_0000 + i * 4, 0xDEAD_BEEF + i);
}
let mut d = DmaController::new();
write_dma_setup(
&mut d,
1,
0x0800_0000,
0x2000,
4,
cnt_h(true, false, 0, true, false, 2, 0),
);
d.run_pending(&mut bus);
assert_eq!(bus.read32_at(0x2000), 0xDEAD_BEEF);
assert_eq!(bus.read32_at(0x2004), 0xDEAD_BEF0);
assert_eq!(bus.read32_at(0x2008), 0xDEAD_BEF1);
assert_eq!(bus.read32_at(0x200C), 0xDEAD_BEF2);
}
#[test]
fn invalid_source_addresses_reuse_each_channels_dma_latch() {
let mut bus = TestBus::new();
bus.write32_at(0x0200_0000, 0x1111_1111);
bus.write32_at(0x0200_0100, 0x2222_2222);
let mut d = DmaController::new();
write_dma_setup(
&mut d,
0,
0x0200_0000,
0x3000,
1,
cnt_h(true, false, 0, true, false, 0, 0),
);
d.run_pending(&mut bus);
write_dma_setup(
&mut d,
1,
0x0200_0100,
0x3004,
1,
cnt_h(true, false, 0, true, false, 0, 0),
);
d.run_pending(&mut bus);
write_dma_setup(
&mut d,
0,
0x0000_0010,
0x3010,
1,
cnt_h(true, false, 0, true, false, 0, 0),
);
d.run_pending(&mut bus);
write_dma_setup(
&mut d,
1,
0x0000_0010,
0x3014,
1,
cnt_h(true, false, 0, true, false, 0, 0),
);
d.run_pending(&mut bus);
assert_eq!(bus.read32_at(0x3010), 0x1111_1111);
assert_eq!(bus.read32_at(0x3014), 0x2222_2222);
}
#[test]
fn invalid_halfword_source_uses_latch_half_selected_by_destination_alignment() {
let mut bus = TestBus::new();
bus.write32_at(0x0200_0000, 0xAAAA_BBBB);
let mut d = DmaController::new();
write_dma_setup(
&mut d,
1,
0x0200_0000,
0x3000,
1,
cnt_h(true, false, 0, true, false, 0, 0),
);
d.run_pending(&mut bus);
write_dma_setup(
&mut d,
1,
0x0000_0010,
0x3010,
1,
cnt_h(true, false, 0, false, false, 0, 0),
);
d.run_pending(&mut bus);
write_dma_setup(
&mut d,
1,
0x0000_0010,
0x3012,
1,
cnt_h(true, false, 0, false, false, 0, 0),
);
d.run_pending(&mut bus);
assert_eq!(bus.read16_at(0x3010), 0xBBBB);
assert_eq!(bus.read16_at(0x3012), 0xAAAA);
}
#[test]
fn count_zero_means_max() {
let mut bus = TestBus::new();
let mut d = DmaController::new();
write_dma_setup(
&mut d,
0,
0x1000,
0x2000,
0,
cnt_h(true, false, 0, false, false, 2, 2),
);
d.run_pending(&mut bus);
assert_eq!(d.take_cpu_stall(), 2 + 0x4000 * 2);
let mut bus = TestBus::new();
let mut d = DmaController::new();
write_dma_setup(
&mut d,
3,
0x1000,
0x2000,
0,
cnt_h(true, false, 0, false, false, 2, 2),
);
d.run_pending(&mut bus);
assert_eq!(d.take_cpu_stall(), 2 + 0x1_0000 * 2);
}
#[test]
fn irq_on_completion_sets_if_bit() {
let mut bus = TestBus::new();
let mut d = DmaController::new();
write_dma_setup(
&mut d,
0,
0x1000,
0x2000,
1,
cnt_h(true, true, 0, true, false, 0, 0),
);
d.run_pending(&mut bus);
assert!(bus.ic.if_flags & irq_bits::DMA0 != 0);
let prev = bus.ic.if_flags;
let mut d2 = DmaController::new();
write_dma_setup(
&mut d2,
1,
0x1000,
0x2000,
1,
cnt_h(true, false, 0, true, false, 0, 0),
);
d2.run_pending(&mut bus);
assert_eq!(bus.ic.if_flags & irq_bits::DMA1, 0);
assert_eq!(bus.ic.if_flags, prev);
}
#[test]
fn vblank_dma_fires_on_notify_only() {
let mut bus = TestBus::new();
bus.write16_at(0x1000, 0xBEEF);
bus.write16_at(0x1002, 0xCAFE);
let mut d = DmaController::new();
write_dma_setup(
&mut d,
1,
0x1000,
0x2000,
2,
cnt_h(true, false, 1, false, false, 0, 0),
);
d.run_pending(&mut bus);
d.run_pending_triggered(&mut bus);
assert_eq!(bus.read16_at(0x2000), 0);
d.notify_vblank();
d.run_pending_triggered(&mut bus);
assert_eq!(bus.read16_at(0x2000), 0xBEEF);
assert_eq!(bus.read16_at(0x2002), 0xCAFE);
}
#[test]
fn hblank_dma_fires_on_notify() {
let mut bus = TestBus::new();
bus.write16_at(0x1000, 0x1111);
let mut d = DmaController::new();
write_dma_setup(
&mut d,
2,
0x1000,
0x2000,
1,
cnt_h(true, false, 2, false, false, 0, 0),
);
d.notify_hblank();
d.run_pending_triggered(&mut bus);
assert_eq!(bus.read16_at(0x2000), 0x1111);
}
#[test]
fn fifo_special_writes_4_words_to_fifo_a() {
let mut bus = TestBus::new();
for i in 0..8 {
bus.write32_at(0x1000 + i * 4, 0x1000_0000 + i);
}
let mut d = DmaController::new();
write_dma_setup(
&mut d,
1,
0x1000,
0xDEAD_BEEF,
16, cnt_h(true, false, 3, false, true, 0, 2),
);
d.notify_fifo(0);
d.run_pending_triggered(&mut bus);
assert_eq!(bus.read32_at(REG_FIFO_A), 0x1000_0003);
assert_eq!(bus.read32_at(0xDEAD_BEEF & !0x3), 0);
assert_eq!(d.take_cpu_stall(), 2 + 4 * 2);
}
#[test]
fn fifo_special_channel2_routes_to_fifo_b() {
let mut bus = TestBus::new();
bus.write32_at(0x1000, 0xAA);
let mut d = DmaController::new();
write_dma_setup(
&mut d,
2,
0x1000,
0,
16,
cnt_h(true, false, 3, false, true, 2, 2),
);
d.notify_fifo(1);
d.run_pending_triggered(&mut bus);
assert_eq!(bus.read32_at(REG_FIFO_B), 0xAA);
assert_eq!(bus.read32_at(REG_FIFO_A), 0);
}
#[test]
fn byte_write_to_sad_preserves_other_byte() {
let mut d = DmaController::new();
d.write8(0x0400_00B0, 0x12); d.write8(0x0400_00B1, 0x34); d.write8(0x0400_00B2, 0x56); d.write8(0x0400_00B3, 0x78); assert_eq!(d.channels[0].sad, 0x7856_3412 & DMA_SOURCE_MASKS[0]);
d.write8(0x0400_00B4, 0xAA);
d.write8(0x0400_00B5, 0xBB);
assert_eq!(d.channels[0].dad & 0xFFFF, 0xBBAA);
d.write8(0x0400_00B8, 0xCD);
d.write8(0x0400_00B9, 0xAB);
assert_eq!(d.channels[0].count, 0xABCD);
}
#[test]
fn vblank_higher_priority_preempts_immediate_lower() {
let mut bus = TestBus::new();
bus.write32_at(0x1000, 0xC0_DA); bus.write32_at(0x3000, 0xC1_DA); let mut d = DmaController::new();
write_dma_setup(
&mut d,
0,
0x1000,
0x2000,
1,
cnt_h(true, false, 1, true, false, 0, 0),
);
write_dma_setup(
&mut d,
1,
0x3000,
0x4000,
4,
cnt_h(true, false, 0, true, false, 2, 0),
);
d.notify_vblank();
d.run_pending(&mut bus);
assert_eq!(bus.read32_at(0x2000), 0xC0_DA);
for i in 0..4 {
assert_eq!(bus.read32_at(0x4000 + i * 4), 0xC1_DA);
}
assert_eq!(d.channels[0].cnt_h & 0x8000, 0);
assert_eq!(d.channels[1].cnt_h & 0x8000, 0);
}
#[test]
fn priority_arbitration_high_preempts_low_pending() {
let mut bus = TestBus::new();
for i in 0..2 {
bus.write32_at(0x1000 + i * 4, 0xC0_0000 + i);
bus.write32_at(0x3000 + i * 4, 0xA1_0000 + i);
}
let mut d = DmaController::new();
write_dma_setup(
&mut d,
0,
0x1000,
0x2000,
2,
cnt_h(true, false, 0, true, false, 0, 0),
);
write_dma_setup(
&mut d,
1,
0x3000,
0x4000,
2,
cnt_h(true, false, 0, true, false, 0, 0),
);
d.run_pending(&mut bus);
assert_eq!(bus.read32_at(0x2000), 0xC0_0000);
assert_eq!(bus.read32_at(0x2004), 0xC0_0001);
assert_eq!(bus.read32_at(0x4000), 0xA1_0000);
assert_eq!(bus.read32_at(0x4004), 0xA1_0001);
assert_eq!(d.take_cpu_stall(), 2 * 2 + (2 + 2) * 2);
}
#[test]
fn cpu_stall_cycles_match_unit_count() {
let mut bus = TestBus::new();
let mut d = DmaController::new();
write_dma_setup(
&mut d,
0,
0x1000,
0x2000,
7,
cnt_h(true, false, 0, true, false, 0, 0),
);
d.run_pending(&mut bus);
assert_eq!(d.take_cpu_stall(), 2 + 7 * 2);
}
#[test]
fn cpu_stall_cycles_use_source_and_destination_waitstates() {
let mut bus = TimedTestBus::new();
bus.write16_at(0x0800_0000, 0x1234);
bus.write16_at(0x0800_0002, 0x5678);
let mut d = DmaController::new();
write_dma_setup(
&mut d,
3,
0x0800_0000,
0x0200_0000,
2,
cnt_h(true, false, 0, false, false, 0, 0),
);
d.run_pending(&mut bus);
assert_eq!(d.take_cpu_stall(), 2 + (5 + 1) + (3 + 1));
}
#[test]
fn cpu_stall_cycles_treat_gamepak_destination_after_gamepak_source_as_sequential() {
let mut bus = TimedTestBus::new();
bus.write16_at(0x0800_0000, 0x1234);
let mut d = DmaController::new();
write_dma_setup(
&mut d,
3,
0x0800_0000,
0x0A00_0000,
1,
cnt_h(true, false, 0, false, false, 0, 0),
);
d.run_pending(&mut bus);
assert_eq!(d.take_cpu_stall(), 2 + 5 + 3);
}
#[test]
fn vblank_repeat_reloads_dst_for_inc_reload() {
let mut bus = TestBus::new();
bus.write16_at(0x1000, 0x1111);
let mut d = DmaController::new();
write_dma_setup(
&mut d,
1,
0x1000,
0x2000,
2,
cnt_h(true, false, 1, false, true, 2, 3),
);
d.notify_vblank();
d.run_pending_triggered(&mut bus);
assert_eq!(bus.read16_at(0x2000), 0x1111);
assert_eq!(bus.read16_at(0x2002), 0x1111);
bus.write16_at(0x2000, 0);
bus.write16_at(0x2002, 0);
d.notify_vblank();
d.run_pending_triggered(&mut bus);
assert_eq!(bus.read16_at(0x2000), 0x1111);
assert_eq!(bus.read16_at(0x2002), 0x1111);
}
#[test]
fn write_only_sad_dad_return_none_cnt_l_returns_zero() {
let mut d = DmaController::new();
write_dma_setup(
&mut d,
0,
0xDEAD_BEEF,
0xCAFE_BABE,
0x1234,
cnt_h(false, false, 0, true, false, 0, 0),
);
assert_eq!(d.try_read16(0x0400_00B0), None, "DMA0 SAD_LO");
assert_eq!(d.try_read16(0x0400_00B2), None, "DMA0 SAD_HI");
assert_eq!(d.try_read16(0x0400_00B4), None, "DMA0 DAD_LO");
assert_eq!(d.try_read16(0x0400_00B6), None, "DMA0 DAD_HI");
assert_eq!(d.try_read16(0x0400_00B8), Some(0), "DMA0 CNT_LO");
assert!(d.try_read16(0x0400_00BA).unwrap() & 0x8000 == 0);
}
#[test]
fn dma_cnt_hi_applies_read_mask() {
let mut d = DmaController::new();
for ch in 0..4u32 {
let addr = 0x0400_00BA + ch * 12;
d.write16(addr, 0xFFFF);
}
assert_eq!(d.try_read16(0x0400_00BA), Some(0xF7E0), "DMA0 CNT_HI");
assert_eq!(d.try_read16(0x0400_00C6), Some(0xF7E0), "DMA1 CNT_HI");
assert_eq!(d.try_read16(0x0400_00D2), Some(0xF7E0), "DMA2 CNT_HI");
assert_eq!(d.try_read16(0x0400_00DE), Some(0xFFE0), "DMA3 CNT_HI");
}
}