Retain independent known data across DMA blocks. Duplicate mapped halfword sources and select the destination lane for blocked sources below work RAM. Keep cold history diagnostic, commit only successful units, and preserve separate CPU/BIOS history rules. Add 13 original regressions and pinned source research. CPU resume ownership, general DMA open bus, and IWRAM DMA lane effects remain explicit limits. Validated on Darwin arm64: 930 workspace and 861 core/demo tests in debug and release, 39 Python tests, native ROM windows, ten window smoke modes, fmt, clippy, and rustdoc. Public ARM/Thumb/memory/BIOS reports match the previous results exactly.
863 lines
31 KiB
Rust
863 lines
31 KiB
Rust
use gba_core::{
|
|
cpu::{Cpu, Mode},
|
|
display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, HBLANK_START, VBLANK_START},
|
|
dma::{DmaError, DMA_BASE, DMA_STRIDE},
|
|
io::{DISPCNT, DISPSTAT, IE, IF, IME, TIMER_BASE, WAITCNT},
|
|
machine::{FrameRunError, Machine, MachineError, StepKind},
|
|
memory::{Memory, MemoryError, BIOS_SIZE, PALETTE_START, ROM_START, VRAM_START},
|
|
timing::{bus_cycles, AccessKind, AccessWidth},
|
|
};
|
|
|
|
#[path = "dma/latch.rs"]
|
|
mod latch;
|
|
|
|
const SOURCE: u32 = 0x0200_0000;
|
|
const DEST: u32 = 0x0300_0000;
|
|
|
|
fn words(values: &[u32]) -> Vec<u8> {
|
|
values
|
|
.iter()
|
|
.flat_map(|value| value.to_le_bytes())
|
|
.collect()
|
|
}
|
|
|
|
fn machine() -> Machine {
|
|
Machine::new(
|
|
Cpu::new(ROM_START),
|
|
Memory::new(words(&[0xeaff_fffe])).unwrap(),
|
|
)
|
|
}
|
|
|
|
fn base(channel: usize) -> u32 {
|
|
DMA_BASE + channel as u32 * DMA_STRIDE
|
|
}
|
|
|
|
fn configure(
|
|
bus: &mut Memory,
|
|
channel: usize,
|
|
source: u32,
|
|
destination: u32,
|
|
count: u16,
|
|
control: u16,
|
|
) {
|
|
bus.write32(base(channel), source).unwrap();
|
|
bus.write32(base(channel) + 4, destination).unwrap();
|
|
bus.write32(
|
|
base(channel) + 8,
|
|
u32::from(count) | (u32::from(control | 0x8000) << 16),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn advance_to(machine: &mut Machine, cycle: u64) {
|
|
let elapsed = (cycle - machine.cycles()) as u32;
|
|
machine.memory_mut().advance_cycles(elapsed);
|
|
}
|
|
|
|
fn enabled(machine: &Machine, channel: usize) -> bool {
|
|
machine.memory().read16(base(channel) + 10).unwrap() & 0x8000 != 0
|
|
}
|
|
|
|
fn units(machine: &mut Machine, channel: usize, count: usize) {
|
|
let cpu = machine.cpu().clone();
|
|
for _ in 0..count {
|
|
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel });
|
|
assert_eq!(machine.last_timing().code_cycles, 0);
|
|
assert_eq!(machine.cpu(), &cpu);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn reset_register_layout_masks_and_write_only_readback() {
|
|
let mut bus = Memory::new(vec![]).unwrap();
|
|
for channel in 0..4 {
|
|
for offset in 0..12 {
|
|
assert_eq!(bus.read8(base(channel) + offset).unwrap(), 0);
|
|
bus.write8(base(channel) + offset, 0xff).unwrap();
|
|
}
|
|
assert_eq!(bus.read32(base(channel)).unwrap(), 0);
|
|
assert_eq!(bus.read32(base(channel) + 4).unwrap(), 0);
|
|
assert_eq!(bus.read16(base(channel) + 8).unwrap(), 0);
|
|
assert_eq!(
|
|
bus.read16(base(channel) + 10).unwrap(),
|
|
if channel == 3 { 0xffe0 } else { 0xf7e0 }
|
|
);
|
|
assert_eq!(
|
|
bus.read32(base(channel) + 8).unwrap(),
|
|
if channel == 3 {
|
|
0xffe0_0000
|
|
} else {
|
|
0xf7e0_0000
|
|
}
|
|
);
|
|
bus.write8(base(channel) + 11, 0).unwrap();
|
|
assert_eq!(bus.read16(base(channel) + 10).unwrap(), 0xe0);
|
|
}
|
|
assert_eq!(
|
|
bus.read8(DMA_BASE - 1),
|
|
Err(MemoryError::Unmapped(DMA_BASE - 1))
|
|
);
|
|
assert_eq!(
|
|
bus.read8(DMA_BASE + 48),
|
|
Err(MemoryError::Unmapped(DMA_BASE + 48))
|
|
);
|
|
assert_eq!(
|
|
bus.read16(DMA_BASE + 1),
|
|
Err(MemoryError::Unaligned(DMA_BASE + 1))
|
|
);
|
|
assert_eq!(
|
|
bus.write32(DMA_BASE + 10, 1),
|
|
Err(MemoryError::Unaligned(DMA_BASE + 10))
|
|
);
|
|
assert_eq!(
|
|
bus.read16(DMA_BASE + 0x400),
|
|
Err(MemoryError::Unmapped(DMA_BASE + 0x400))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn immediate_halfword_and_word_copies_pause_cpu_until_completion() {
|
|
for channel in 0..4 {
|
|
for word in [false, true] {
|
|
let mut machine = machine();
|
|
machine.memory_mut().write32(SOURCE, 0x4433_2211).unwrap();
|
|
machine
|
|
.memory_mut()
|
|
.write32(SOURCE + 4, 0x8877_6655)
|
|
.unwrap();
|
|
configure(
|
|
machine.memory_mut(),
|
|
channel,
|
|
SOURCE,
|
|
DEST,
|
|
if word { 2 } else { 4 },
|
|
if word { 0x400 } else { 0 },
|
|
);
|
|
assert_eq!(machine.memory().read32(DEST).unwrap(), 0); // Register writes do not execute DMA.
|
|
assert_eq!(machine.cycles(), 0);
|
|
units(&mut machine, channel, if word { 2 } else { 4 });
|
|
assert!(!enabled(&machine, channel));
|
|
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x4433_2211);
|
|
assert_eq!(machine.memory().read32(DEST + 4).unwrap(), 0x8877_6655);
|
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
|
assert_eq!(machine.cycles(), if word { 16 } else { 18 });
|
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn source_and_destination_address_modes_match_reference_copies() {
|
|
for word in [false, true] {
|
|
let width = if word { 4 } else { 2 };
|
|
for source_mode in 0..3 {
|
|
for dest_mode in 0..4 {
|
|
let mut machine = machine();
|
|
let mut expected = [0_u32; 9];
|
|
for index in 0..9 {
|
|
machine
|
|
.memory_mut()
|
|
.write32(SOURCE + index * 4, 0x0123_0123 * (index + 1))
|
|
.unwrap();
|
|
}
|
|
let mut source = SOURCE + 16;
|
|
let mut destination = DEST + 16;
|
|
// Read the source and simulate the destination independently of DMA state.
|
|
let mut expected_bytes = [0_u8; 36];
|
|
for _ in 0..4 {
|
|
for byte in 0..width {
|
|
expected_bytes[(destination - DEST + byte) as usize] =
|
|
machine.memory().read8(source + byte).unwrap();
|
|
}
|
|
source = match source_mode {
|
|
0 => source + width,
|
|
1 => source - width,
|
|
_ => source,
|
|
};
|
|
destination = match dest_mode {
|
|
0 | 3 => destination + width,
|
|
1 => destination - width,
|
|
_ => destination,
|
|
};
|
|
}
|
|
for (index, chunk) in expected_bytes.chunks_exact(4).enumerate() {
|
|
expected[index] = u32::from_le_bytes(chunk.try_into().unwrap());
|
|
}
|
|
let control = source_mode << 7 | dest_mode << 5 | if word { 0x400 } else { 0 };
|
|
configure(machine.memory_mut(), 3, SOURCE + 16, DEST + 16, 4, control);
|
|
units(&mut machine, 3, 4);
|
|
for (index, value) in expected.into_iter().enumerate() {
|
|
assert_eq!(
|
|
machine.memory().read32(DEST + index as u32 * 4).unwrap(),
|
|
value
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn addresses_mask_channel_bits_and_align_to_transfer_width() {
|
|
for channel in 0..4 {
|
|
for word in [false, true] {
|
|
let mut machine = machine();
|
|
machine.memory_mut().write32(SOURCE, 0xaabb_ccdd).unwrap();
|
|
let source_high = if channel == 0 {
|
|
0xf800_0000
|
|
} else {
|
|
0xf000_0000
|
|
};
|
|
let dest_high = if channel == 3 {
|
|
0xf000_0000
|
|
} else {
|
|
0xf800_0000
|
|
};
|
|
configure(
|
|
machine.memory_mut(),
|
|
channel,
|
|
SOURCE | source_high | 3,
|
|
DEST | dest_high | 3,
|
|
1,
|
|
if word { 0x400 } else { 0 },
|
|
);
|
|
units(&mut machine, channel, 1);
|
|
assert_eq!(
|
|
machine.memory().read32(DEST).unwrap(),
|
|
if word { 0xaabb_ccdd } else { 0xaabb_0000 }
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn zero_count_means_channel_maximum_and_count_high_bits_are_masked() {
|
|
for (channel, count, expected) in [
|
|
(0, 0, 0x4000),
|
|
(1, 0xc000, 0x4000),
|
|
(2, 0xc003, 3),
|
|
(3, 0, 0x10000),
|
|
] {
|
|
let mut machine = machine();
|
|
machine.memory_mut().write16(SOURCE, 0x5a5a).unwrap();
|
|
configure(machine.memory_mut(), channel, SOURCE, DEST, count, 0x140); // Both addresses fixed.
|
|
units(&mut machine, channel, expected - 1);
|
|
assert!(enabled(&machine, channel));
|
|
units(&mut machine, channel, 1);
|
|
assert!(!enabled(&machine, channel));
|
|
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x5a5a);
|
|
assert_eq!(machine.cycles(), expected as u64 * 4 + 2);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn immediate_repeat_completes_once_and_does_not_stall_forever() {
|
|
let mut machine = machine();
|
|
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, 0x200);
|
|
units(&mut machine, 0, 1);
|
|
assert!(!enabled(&machine, 0));
|
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
|
}
|
|
|
|
#[test]
|
|
fn address_and_count_latches_change_only_on_enable_edge() {
|
|
let mut machine = machine();
|
|
machine.memory_mut().write32(SOURCE, 0x2222_1111).unwrap();
|
|
machine.memory_mut().write16(SOURCE + 16, 0x3333).unwrap();
|
|
configure(machine.memory_mut(), 2, SOURCE, DEST, 2, 0);
|
|
machine.memory_mut().write32(base(2), SOURCE + 16).unwrap();
|
|
machine
|
|
.memory_mut()
|
|
.write32(base(2) + 4, DEST + 16)
|
|
.unwrap();
|
|
machine.memory_mut().write16(base(2) + 8, 1).unwrap();
|
|
machine.memory_mut().write16(base(2) + 10, 0x8000).unwrap(); // Still enabled, not a new edge.
|
|
units(&mut machine, 2, 2);
|
|
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x2222_1111);
|
|
assert_eq!(machine.memory().read16(DEST + 16).unwrap(), 0);
|
|
machine.memory_mut().write16(base(2) + 10, 0x8000).unwrap();
|
|
units(&mut machine, 2, 1);
|
|
assert_eq!(machine.memory().read16(DEST + 16).unwrap(), 0x3333);
|
|
assert!(!enabled(&machine, 2));
|
|
}
|
|
|
|
#[test]
|
|
fn disabling_cancels_pending_dma_and_reenable_reloads_latches() {
|
|
let mut machine = machine();
|
|
machine.memory_mut().write32(SOURCE, 0xabcd_1234).unwrap();
|
|
configure(machine.memory_mut(), 1, SOURCE, DEST, 2, 0);
|
|
units(&mut machine, 1, 1);
|
|
machine.memory_mut().write8(base(1) + 11, 0).unwrap();
|
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
|
assert_eq!(machine.memory().read16(DEST + 2).unwrap(), 0);
|
|
machine.memory_mut().write8(base(1) + 11, 0x80).unwrap();
|
|
units(&mut machine, 1, 2);
|
|
assert_eq!(machine.memory().read32(DEST).unwrap(), 0xabcd_1234);
|
|
}
|
|
|
|
#[test]
|
|
fn simultaneous_channels_run_in_priority_order_before_cpu_or_irq() {
|
|
let mut machine = machine();
|
|
machine.memory_mut().write16(IE, 0x0f00).unwrap();
|
|
machine.memory_mut().write16(IME, 1).unwrap();
|
|
for channel in (0..4).rev() {
|
|
machine
|
|
.memory_mut()
|
|
.write16(SOURCE + channel as u32 * 2, channel as u16 + 1)
|
|
.unwrap();
|
|
configure(
|
|
machine.memory_mut(),
|
|
channel,
|
|
SOURCE + channel as u32 * 2,
|
|
DEST,
|
|
1,
|
|
0x4000,
|
|
);
|
|
}
|
|
for channel in 0..4 {
|
|
units(&mut machine, channel, 1);
|
|
assert_eq!(machine.memory().read16(DEST).unwrap(), channel as u16 + 1);
|
|
assert_eq!(
|
|
machine.memory().read16(IF).unwrap(),
|
|
((1 << (channel + 1)) - 1) << 8
|
|
);
|
|
}
|
|
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
|
|
}
|
|
|
|
#[test]
|
|
fn display_request_preempts_lower_priority_dma_at_unit_boundary() {
|
|
let mut machine = machine();
|
|
machine.memory_mut().write16(SOURCE, 0x1111).unwrap();
|
|
machine.memory_mut().write16(SOURCE + 2, 0x3333).unwrap();
|
|
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, 0x2000); // HBlank.
|
|
configure(machine.memory_mut(), 3, SOURCE + 2, DEST, 3, 0x140); // Immediate fixed fill.
|
|
machine.memory_mut().advance_cycles(HBLANK_START - 1);
|
|
units(&mut machine, 3, 1); // Crosses HBlank with DMA0 pending.
|
|
units(&mut machine, 0, 1);
|
|
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x1111);
|
|
units(&mut machine, 3, 2); // Resumes the lower-priority block.
|
|
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x3333);
|
|
assert!(!enabled(&machine, 3));
|
|
}
|
|
|
|
#[test]
|
|
fn blank_requests_do_not_depend_on_dispstat_or_forced_blank() {
|
|
for (timing, edge) in [(0x1000, VBLANK_START), (0x2000, HBLANK_START)] {
|
|
let mut machine = machine();
|
|
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, timing);
|
|
machine.memory_mut().write16(DISPCNT, 0x80).unwrap();
|
|
machine.memory_mut().advance_cycles(edge - 1);
|
|
assert_eq!(machine.memory().read16(DISPSTAT).unwrap() & 0x38, 0);
|
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction); // Clock crosses edge.
|
|
units(&mut machine, 0, 1);
|
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn enabling_during_blank_waits_for_next_edge_and_zero_advance_does_not_trigger() {
|
|
for (timing, edge, period) in [
|
|
(0x1000, VBLANK_START, CYCLES_PER_FRAME),
|
|
(0x2000, HBLANK_START, CYCLES_PER_LINE),
|
|
] {
|
|
let mut machine = machine();
|
|
machine.memory_mut().advance_cycles(edge);
|
|
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, timing);
|
|
machine.memory_mut().advance_cycles(0);
|
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
|
let elapsed = (machine.cycles() - u64::from(edge)) as u32;
|
|
machine.memory_mut().advance_cycles(period - elapsed);
|
|
units(&mut machine, 0, 1);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn hblank_dma_runs_on_all_visible_lines_but_not_hidden_lines() {
|
|
let mut machine = machine();
|
|
configure(machine.memory_mut(), 1, SOURCE, DEST, 1, 0x2340); // Repeat HBlank, fixed addresses.
|
|
machine.memory_mut().write16(DISPSTAT, 0x10).unwrap();
|
|
for line in 0..228_u64 {
|
|
let edge = line * u64::from(CYCLES_PER_LINE) + u64::from(HBLANK_START);
|
|
advance_to(&mut machine, edge);
|
|
assert_eq!(machine.memory().read16(IF).unwrap(), 2); // HBlank IRQ still occurs on hidden lines.
|
|
machine.memory_mut().write16(IF, 2).unwrap();
|
|
if line < 160 {
|
|
units(&mut machine, 1, 1);
|
|
} else {
|
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
|
}
|
|
}
|
|
let edge = u64::from(CYCLES_PER_FRAME + HBLANK_START);
|
|
advance_to(&mut machine, edge);
|
|
units(&mut machine, 1, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn vblank_repeat_preserves_source_reloads_destination_and_count() {
|
|
let mut machine = machine();
|
|
machine.memory_mut().write32(SOURCE, 0x2222_1111).unwrap();
|
|
machine
|
|
.memory_mut()
|
|
.write32(SOURCE + 4, 0x4444_3333)
|
|
.unwrap();
|
|
configure(machine.memory_mut(), 3, SOURCE, DEST, 2, 0x5260); // VBlank, repeat, dest reload, IRQ.
|
|
machine.memory_mut().advance_cycles(VBLANK_START);
|
|
units(&mut machine, 3, 2);
|
|
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x2222_1111);
|
|
assert!(enabled(&machine, 3));
|
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0x800);
|
|
machine.memory_mut().write16(IF, 0x800).unwrap();
|
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction); // Same VBlank does not retrigger.
|
|
let next = u64::from(VBLANK_START + CYCLES_PER_FRAME);
|
|
advance_to(&mut machine, next);
|
|
units(&mut machine, 3, 2);
|
|
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x4444_3333);
|
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0x800);
|
|
assert!(enabled(&machine, 3));
|
|
}
|
|
|
|
#[test]
|
|
fn repeated_increment_destination_continues_and_reload_uses_programmed_values() {
|
|
for reload in [false, true] {
|
|
let mut machine = machine();
|
|
machine.memory_mut().write32(SOURCE, 0x2222_1111).unwrap();
|
|
machine
|
|
.memory_mut()
|
|
.write32(SOURCE + 4, 0x4444_3333)
|
|
.unwrap();
|
|
configure(
|
|
machine.memory_mut(),
|
|
0,
|
|
SOURCE,
|
|
DEST,
|
|
1,
|
|
0x1200 | if reload { 0x60 } else { 0 },
|
|
);
|
|
// Initial count/address are latched. Repeat reloads use newly programmed values.
|
|
machine.memory_mut().write16(base(0) + 8, 2).unwrap();
|
|
machine
|
|
.memory_mut()
|
|
.write32(base(0) + 4, DEST + 16)
|
|
.unwrap();
|
|
machine.memory_mut().advance_cycles(VBLANK_START);
|
|
units(&mut machine, 0, 1);
|
|
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x1111);
|
|
let next = u64::from(VBLANK_START + CYCLES_PER_FRAME);
|
|
advance_to(&mut machine, next);
|
|
units(&mut machine, 0, 2);
|
|
let address = if reload { DEST + 16 } else { DEST + 2 };
|
|
assert_eq!(machine.memory().read16(address).unwrap(), 0x2222);
|
|
assert_eq!(machine.memory().read16(address + 2).unwrap(), 0x3333);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pending_requests_coalesce_and_busy_channel_does_not_queue_repeats() {
|
|
let mut machine = machine();
|
|
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, 0x1200);
|
|
machine.memory_mut().advance_cycles(CYCLES_PER_FRAME * 3);
|
|
units(&mut machine, 0, 1);
|
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
|
|
|
let mut machine = self::machine();
|
|
configure(machine.memory_mut(), 0, SOURCE, DEST, 100, 0x2200); // Takes 402 cycles, beyond HBlank.
|
|
machine.memory_mut().advance_cycles(HBLANK_START);
|
|
units(&mut machine, 0, 99);
|
|
// Inject more display edges while the block is still active; no queued repeat.
|
|
machine.memory_mut().advance_cycles(CYCLES_PER_FRAME * 2);
|
|
units(&mut machine, 0, 1);
|
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
|
}
|
|
|
|
#[test]
|
|
fn timer_and_display_clocks_continue_during_dma_and_latch_all_irqs() {
|
|
let mut machine = machine();
|
|
machine
|
|
.memory_mut()
|
|
.write32(TIMER_BASE, 0x00c0_ffff)
|
|
.unwrap();
|
|
machine.memory_mut().write16(DISPSTAT, 0x10).unwrap();
|
|
machine.memory_mut().advance_cycles(HBLANK_START - 2);
|
|
machine.memory_mut().write16(IF, 0x3fff).unwrap();
|
|
configure(machine.memory_mut(), 2, SOURCE, DEST, 1, 0x4000);
|
|
let before = machine.cycles();
|
|
units(&mut machine, 2, 1);
|
|
assert_eq!(machine.cycles(), before + 6);
|
|
assert_eq!(
|
|
machine.memory().display_position().line_cycle,
|
|
HBLANK_START as u16 + 4
|
|
);
|
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0x40a);
|
|
assert!(!machine.memory().irq_pending()); // IE/IME are off, requests still latch.
|
|
machine.memory_mut().write16(IE, 0x400).unwrap();
|
|
machine.memory_mut().write16(IME, 1).unwrap();
|
|
assert!(machine.memory().irq_pending());
|
|
}
|
|
|
|
#[test]
|
|
fn dma_completion_irq_enters_handler_acknowledges_and_returns_without_skipping_cpu() {
|
|
for channel in 0..4 {
|
|
let mask = 1_u16 << (8 + channel);
|
|
let mut bios = vec![0; BIOS_SIZE];
|
|
bios[0x18..0x24].copy_from_slice(&words(&[
|
|
0xe1c1_20b2, // STRH r2,[r1,#2] (IF)
|
|
0xe280_0001, // ADD r0,r0,#1
|
|
0xe25e_f004, // SUBS pc,lr,#4
|
|
]));
|
|
let rom = words(&[
|
|
0xe59f_1008, // LDR r1,[pc,#8] (IE)
|
|
0xe59f_2008, // LDR r2,[pc,#8] (mask)
|
|
0xe283_3001, // ADD r3,r3,#1 (interrupted instruction)
|
|
0xeaff_fffe,
|
|
IE,
|
|
u32::from(mask),
|
|
]);
|
|
let mut bus = Memory::with_bios(rom, bios).unwrap();
|
|
let mut cpu = Cpu::new(ROM_START);
|
|
cpu.step(&mut bus).unwrap();
|
|
cpu.step(&mut bus).unwrap();
|
|
configure(&mut bus, channel, SOURCE, DEST, 2, 0x4000);
|
|
bus.write16(IE, mask).unwrap();
|
|
bus.write16(IME, 1).unwrap();
|
|
let mut machine = Machine::new(cpu, bus);
|
|
units(&mut machine, channel, 1);
|
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0); // Not yet complete.
|
|
units(&mut machine, channel, 1);
|
|
assert_eq!(machine.memory().read16(IF).unwrap(), mask);
|
|
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
|
|
assert_eq!(machine.cpu().mode(), Mode::Irq);
|
|
assert_eq!(machine.cpu().registers()[14], ROM_START + 12);
|
|
for _ in 0..3 {
|
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
|
}
|
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
|
assert_eq!(machine.cpu().mode(), Mode::System);
|
|
assert_eq!(machine.cpu().pc(), ROM_START + 8);
|
|
assert_eq!(machine.cpu().registers()[0], 1);
|
|
assert_eq!(machine.cpu().registers()[3], 0);
|
|
machine.step().unwrap();
|
|
assert_eq!(machine.cpu().registers()[3], 1);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rom_dma_uses_waitcnt_sequential_costs_and_forces_source_increment() {
|
|
for channel in 1..4 {
|
|
for source_mode in 0..3 {
|
|
for (window, waitcnt) in [(0x0800_0000, 0), (0x0a00_0000, 0x7fff), (0x0c00_0000, 0)] {
|
|
for word in [false, true] {
|
|
let mut bus = Memory::new(words(&[0x2222_1111, 0x4444_3333])).unwrap();
|
|
bus.write16(WAITCNT, waitcnt).unwrap();
|
|
let width = if word {
|
|
AccessWidth::Word
|
|
} else {
|
|
AccessWidth::Halfword
|
|
};
|
|
let stride = if word { 4 } else { 2 };
|
|
configure(
|
|
&mut bus,
|
|
channel,
|
|
window,
|
|
DEST,
|
|
2,
|
|
source_mode << 7 | if word { 0x400 } else { 0 },
|
|
);
|
|
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
|
for index in 0..2 {
|
|
units(&mut machine, channel, 1);
|
|
let expected = bus_cycles(
|
|
machine.memory().waitcnt(),
|
|
window + index * stride,
|
|
width,
|
|
if index == 0 {
|
|
AccessKind::NonSequential
|
|
} else {
|
|
AccessKind::Sequential
|
|
},
|
|
) + 1;
|
|
assert_eq!(machine.last_timing().data_cycles, expected);
|
|
assert_eq!(
|
|
machine.last_timing().internal_cycles,
|
|
if index == 0 { 2 } else { 0 }
|
|
);
|
|
}
|
|
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x2222_1111);
|
|
if word {
|
|
assert_eq!(machine.memory().read32(DEST + 4).unwrap(), 0x4444_3333);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rom_boundary_forces_nonsequential_timing_in_middle_of_dma() {
|
|
let mut machine = Machine::new(
|
|
Cpu::new(ROM_START),
|
|
Memory::new(vec![0x5a; 0x20004]).unwrap(),
|
|
);
|
|
configure(machine.memory_mut(), 3, ROM_START + 0x1fffe, DEST, 3, 0);
|
|
for expected in [8, 6, 4] {
|
|
// First: 5+1+2; boundary: 5+1; then 3+1.
|
|
units(&mut machine, 3, 1);
|
|
assert_eq!(machine.last_timing().total(), expected);
|
|
}
|
|
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x5a5a_5a5a);
|
|
}
|
|
|
|
#[test]
|
|
fn overlapping_copies_read_each_unit_after_previous_write() {
|
|
let mut machine = machine();
|
|
machine.memory_mut().write32(SOURCE, 0x2222_1111).unwrap();
|
|
configure(machine.memory_mut(), 0, SOURCE, SOURCE + 2, 3, 0);
|
|
units(&mut machine, 0, 3);
|
|
for offset in [0, 2, 4, 6] {
|
|
assert_eq!(machine.memory().read16(SOURCE + offset).unwrap(), 0x1111);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn video_palette_mirrors_and_mapped_io_use_the_normal_bus() {
|
|
for (destination, alias) in [
|
|
(VRAM_START + 0x18000, VRAM_START + 0x10000),
|
|
(PALETTE_START + 0x400, PALETTE_START),
|
|
(DISPCNT, DISPCNT),
|
|
] {
|
|
let mut machine = machine();
|
|
machine.memory_mut().write32(SOURCE, 0x0001_0403).unwrap();
|
|
configure(machine.memory_mut(), 3, SOURCE, destination, 1, 0x400);
|
|
units(&mut machine, 3, 1);
|
|
assert_eq!(machine.memory().read32(alias).unwrap(), 0x0001_0403);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unsupported_controls_are_explicit_and_leave_state_and_time_unchanged() {
|
|
for channel in 0..4 {
|
|
for control in [0x180, 0x3000, 0x3800] {
|
|
let mut machine = machine();
|
|
configure(machine.memory_mut(), channel, SOURCE, DEST, 1, control);
|
|
let before = machine.cpu().clone();
|
|
let stored = machine.memory().read16(base(channel) + 10).unwrap();
|
|
for _ in 0..2 {
|
|
assert_eq!(
|
|
machine.step(),
|
|
Err(MachineError::Dma(DmaError::UnsupportedControl {
|
|
channel,
|
|
control: stored
|
|
}))
|
|
);
|
|
assert_eq!(machine.cpu(), &before);
|
|
assert_eq!(machine.cycles(), 0);
|
|
assert_eq!(machine.last_timing().total(), 0);
|
|
assert_eq!(machine.memory().read32(DEST).unwrap(), 0);
|
|
assert!(enabled(&machine, channel));
|
|
}
|
|
}
|
|
}
|
|
let mut machine = machine();
|
|
configure(machine.memory_mut(), 3, SOURCE, DEST, 1, 0x800); // DRQ even without special timing.
|
|
assert!(matches!(
|
|
machine.step(),
|
|
Err(MachineError::Dma(DmaError::UnsupportedControl { .. }))
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_memory_and_dma_register_destinations_are_diagnostics() {
|
|
for (source, destination, error) in [
|
|
(
|
|
0,
|
|
DEST,
|
|
DmaError::UnsupportedSource {
|
|
channel: 3,
|
|
address: 0,
|
|
},
|
|
),
|
|
(
|
|
0x0e00_0000,
|
|
DEST,
|
|
DmaError::Memory {
|
|
channel: 3,
|
|
error: MemoryError::Unmapped(0x0e00_0000),
|
|
},
|
|
),
|
|
(
|
|
SOURCE,
|
|
ROM_START,
|
|
DmaError::Memory {
|
|
channel: 3,
|
|
error: MemoryError::ReadOnly(ROM_START),
|
|
},
|
|
),
|
|
(
|
|
SOURCE,
|
|
0x0e00_0000,
|
|
DmaError::Memory {
|
|
channel: 3,
|
|
error: MemoryError::Unmapped(0x0e00_0000),
|
|
},
|
|
),
|
|
(
|
|
SOURCE,
|
|
DMA_BASE,
|
|
DmaError::RegisterDestination {
|
|
channel: 3,
|
|
address: DMA_BASE,
|
|
},
|
|
),
|
|
] {
|
|
let mut machine = machine();
|
|
configure(machine.memory_mut(), 3, source, destination, 1, 0x4000);
|
|
let before = machine.cpu().clone();
|
|
assert_eq!(machine.step(), Err(MachineError::Dma(error)));
|
|
assert_eq!(machine.cpu(), &before);
|
|
assert_eq!(machine.cycles(), 0);
|
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
|
assert!(enabled(&machine, 3));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn failing_later_unit_preserves_earlier_data_but_not_partial_current_unit() {
|
|
let mut machine = Machine::new(
|
|
Cpu::new(ROM_START),
|
|
Memory::new(vec![0x11, 0x22, 0x33]).unwrap(),
|
|
);
|
|
configure(machine.memory_mut(), 3, ROM_START, DEST, 2, 0x4000);
|
|
units(&mut machine, 3, 1);
|
|
let timing = machine.last_timing();
|
|
let cycles = machine.cycles();
|
|
let position = machine.memory().display_position();
|
|
for _ in 0..2 {
|
|
assert_eq!(
|
|
machine.step(),
|
|
Err(MachineError::Dma(DmaError::Memory {
|
|
channel: 3,
|
|
error: MemoryError::Unmapped(ROM_START + 3)
|
|
}))
|
|
);
|
|
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x2211);
|
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
|
assert_eq!(machine.cycles(), cycles);
|
|
assert_eq!(machine.memory().display_position(), position);
|
|
assert_eq!(machine.last_timing(), timing);
|
|
assert!(enabled(&machine, 3));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn failed_io_write_does_not_partially_change_display_control() {
|
|
let mut machine = machine();
|
|
machine.memory_mut().write32(SOURCE, 0xffff_ffff).unwrap();
|
|
configure(machine.memory_mut(), 3, SOURCE, 0x0400_0058, 1, 0x400);
|
|
assert!(machine.step().is_err());
|
|
assert_eq!(machine.memory().read16(DISPCNT).unwrap(), 0);
|
|
assert_eq!(machine.cycles(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn cpu_can_enable_dma_with_a_combined_count_control_store() {
|
|
let rom = words(&[
|
|
0xe59f_0010, // LDR r0,[pc,#16] (DMA3CNT)
|
|
0xe59f_1010, // LDR r1,[pc,#16] (count/control)
|
|
0xe580_1000, // STR r1,[r0]
|
|
0xe282_2001, // ADD r2,r2,#1 (must wait for transfer)
|
|
0xeaff_fffe,
|
|
0xe1a0_0000,
|
|
base(3) + 8,
|
|
0x8400_0002,
|
|
]);
|
|
let mut bus = Memory::new(rom).unwrap();
|
|
bus.write32(SOURCE, 0x1234_5678).unwrap();
|
|
bus.write32(base(3), SOURCE).unwrap();
|
|
bus.write32(base(3) + 4, DEST).unwrap();
|
|
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
|
for _ in 0..3 {
|
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
|
}
|
|
assert_eq!(machine.cpu().pc(), ROM_START + 12);
|
|
units(&mut machine, 3, 2);
|
|
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x1234_5678);
|
|
assert_eq!(machine.cpu().registers()[2], 0);
|
|
machine.step().unwrap();
|
|
assert_eq!(machine.cpu().registers()[2], 1);
|
|
}
|
|
|
|
#[test]
|
|
fn failed_cpu_block_store_cannot_enable_dma_or_change_its_hidden_latches() {
|
|
let rom = words(&[
|
|
0xe59f_000c, // LDR r0,[pc,#12] (DMA3CNT)
|
|
0xe59f_100c, // LDR r1,[pc,#12] (count/control)
|
|
0xe8a0_0006, // STMIA r0!,{r1,r2}; second word unmapped.
|
|
0xeaff_fffe,
|
|
0xe1a0_0000,
|
|
base(3) + 8,
|
|
0x8400_0001,
|
|
]);
|
|
let mut bus = Memory::new(rom).unwrap();
|
|
let mut cpu = Cpu::new(ROM_START);
|
|
cpu.step(&mut bus).unwrap();
|
|
cpu.step(&mut bus).unwrap();
|
|
configure(&mut bus, 3, SOURCE, DEST, 2, 0);
|
|
bus.write16(base(3) + 10, 0).unwrap();
|
|
let before = cpu.clone();
|
|
let mut machine = Machine::new(cpu, bus);
|
|
assert!(matches!(machine.step(), Err(MachineError::Cpu(_))));
|
|
assert_eq!(machine.cpu(), &before);
|
|
assert!(!enabled(&machine, 3));
|
|
assert_eq!(machine.cycles(), 0);
|
|
machine.memory_mut().write16(base(3) + 10, 0x8000).unwrap();
|
|
units(&mut machine, 3, 2); // The count latch is still 2, not 1.
|
|
assert!(!enabled(&machine, 3));
|
|
}
|
|
|
|
#[test]
|
|
fn cpu_only_and_clock_only_calls_never_execute_dma() {
|
|
let mut bus = Memory::new(words(&[0xe280_0001])).unwrap();
|
|
bus.write16(SOURCE, 0x1234).unwrap();
|
|
configure(&mut bus, 0, SOURCE, DEST, 1, 0);
|
|
let mut cpu = Cpu::new(ROM_START);
|
|
cpu.step(&mut bus).unwrap();
|
|
bus.advance_cycles(CYCLES_PER_FRAME);
|
|
assert_eq!(cpu.registers()[0], 1);
|
|
assert_eq!(bus.read16(DEST).unwrap(), 0);
|
|
let mut machine = Machine::new(cpu, bus);
|
|
units(&mut machine, 0, 1);
|
|
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x1234);
|
|
}
|
|
|
|
#[test]
|
|
fn frame_runner_counts_dma_units_obeys_limits_and_reports_dma_errors() {
|
|
let mut machine = machine();
|
|
configure(machine.memory_mut(), 0, SOURCE, DEST, 3, 0);
|
|
assert_eq!(
|
|
machine.run_until_vblank(2),
|
|
Err(FrameRunError::StepLimit(2))
|
|
);
|
|
assert_eq!(machine.cpu().pc(), ROM_START);
|
|
assert_eq!(machine.cycles(), 10);
|
|
machine.memory_mut().advance_cycles(VBLANK_START - 11);
|
|
assert_eq!(machine.run_until_vblank(1).unwrap(), 1);
|
|
assert_eq!(machine.memory().display_position().scanline, 160);
|
|
assert_eq!(machine.cpu().pc(), ROM_START);
|
|
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, 0x3000);
|
|
assert!(matches!(
|
|
machine.run_until_vblank(1),
|
|
Err(FrameRunError::Dma(_))
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn vblank_runner_stops_at_event_before_newly_requested_dma_executes() {
|
|
let mut machine = machine();
|
|
machine.memory_mut().write16(SOURCE, 0x1234).unwrap();
|
|
configure(machine.memory_mut(), 3, SOURCE, DEST, 1, 0x1000);
|
|
machine.memory_mut().advance_cycles(VBLANK_START - 1);
|
|
assert_eq!(machine.run_until_vblank(1).unwrap(), 1);
|
|
assert_eq!(machine.memory().read16(DEST).unwrap(), 0);
|
|
units(&mut machine, 3, 1);
|
|
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x1234);
|
|
}
|