refactor: split into gameboy-rust workspace with core, demos, and desktop crates
Move the emulator into crates/gba-core, the original demo programs into crates/gba-demos, and the minifb frontend into crates/desktop. The binary is now gameboy-rust. Imports change from gba_rust to gba_core/gba_demos; no behavior changes. gba-core has no dependencies. Its integration tests use gba-demos as a development dependency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a8944d1d5f
commit
f8a0676748
113 changed files with 304 additions and 282 deletions
860
crates/gba-core/tests/dma.rs
Normal file
860
crates/gba-core/tests/dma.rs
Normal file
|
|
@ -0,0 +1,860 @@
|
|||
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},
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue