feat(machine): schedule display DMA IRQs and HALT

Integrate nominal bus timing, device clocks, display events, DMA transfers, keypad state, and interrupt delivery.
This commit is contained in:
0xrsydn 2026-10-05 12:55:35 +07:00
commit bb6784615a
10 changed files with 3172 additions and 0 deletions

449
tests/display.rs Normal file
View file

@ -0,0 +1,449 @@
use gba_rust::{
cpu::{Cpu, Mode},
display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, HBLANK_START, LINES_PER_FRAME, VBLANK_START},
io::{DISPCNT, DISPSTAT, IE, IF, IME, TIMER_BASE, VCOUNT},
machine::{FrameRunError, Machine, StepKind},
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START},
};
fn memory() -> Memory {
Memory::new(vec![]).unwrap()
}
#[test]
fn display_starts_at_line_zero_with_comparison_match_but_no_irq() {
let bus = memory();
assert_eq!(bus.read16(DISPSTAT).unwrap(), 4);
assert_eq!(bus.read16(VCOUNT).unwrap(), 0);
assert_eq!(bus.read16(IF).unwrap(), 0);
assert_eq!(bus.display_position().scanline, 0);
assert_eq!(bus.display_position().line_cycle, 0);
assert_eq!(bus.display_position().frames, 0);
assert_eq!(bus.display_position().vblanks, 0);
assert_eq!(bus.cycles(), 0);
}
#[test]
fn hblank_status_starts_at_1006_not_at_the_last_visible_pixel() {
let mut bus = memory();
bus.advance_cycles(960);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 2, 0);
bus.advance_cycles(45);
assert_eq!(bus.display_position().line_cycle, 1005);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 2, 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 2, 2);
bus.advance_cycles(225);
assert_eq!(bus.read16(VCOUNT).unwrap(), 0);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 2, 2);
bus.advance_cycles(1);
assert_eq!(bus.read16(VCOUNT).unwrap(), 1);
assert_eq!(bus.display_position().line_cycle, 0);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 7, 0);
}
#[test]
fn vblank_starts_at_160_clears_at_227_and_line_count_wraps_at_228() {
let mut bus = memory();
bus.advance_cycles(VBLANK_START - 1);
assert_eq!(bus.read16(VCOUNT).unwrap(), 159);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 3, 2);
bus.advance_cycles(1);
assert_eq!(bus.read16(VCOUNT).unwrap(), 160);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 3, 1);
assert_eq!(bus.display_position().vblanks, 1);
bus.advance_cycles(67 * CYCLES_PER_LINE - 1);
assert_eq!(bus.read16(VCOUNT).unwrap(), 226);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 3, 3);
bus.advance_cycles(1);
assert_eq!(bus.read16(VCOUNT).unwrap(), 227);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 3, 0);
assert_eq!(bus.display_position().frames, 0);
bus.advance_cycles(CYCLES_PER_LINE);
assert_eq!(bus.read32(DISPSTAT).unwrap(), 4); // Compare zero matches again
assert_eq!(bus.display_position().frames, 1);
assert_eq!(bus.display_position().vblanks, 1);
assert_eq!(bus.cycles(), 280_896);
}
#[test]
fn hblank_irq_occurs_on_every_line_including_hidden_lines() {
let mut bus = memory();
bus.write16(DISPSTAT, 0x10).unwrap();
for line in 0..LINES_PER_FRAME {
bus.advance_cycles(HBLANK_START - 1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(VCOUNT).unwrap(), line as u16);
assert_eq!(bus.read16(IF).unwrap(), 2);
bus.write16(IF, 2).unwrap();
bus.advance_cycles(CYCLES_PER_LINE - HBLANK_START);
assert_eq!(bus.read16(IF).unwrap(), 0);
}
assert_eq!(bus.display_position().frames, 1);
}
#[test]
fn vblank_and_compare_requests_latch_without_ie_or_ime_and_acknowledge_independently() {
let mut bus = memory();
bus.write16(DISPSTAT, 0xa028).unwrap(); // VBlank + VCount=160
bus.advance_cycles(VBLANK_START);
assert_eq!(bus.read16(IF).unwrap(), 5);
assert!(!bus.irq_pending());
bus.write16(IE, 5).unwrap();
assert!(!bus.irq_pending());
bus.write32(IME, 1).unwrap();
assert!(bus.irq_pending());
bus.write16(IF, 1).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 4);
assert!(bus.irq_pending());
bus.write16(IF, 4).unwrap();
assert!(!bus.irq_pending());
bus.advance_cycles(0);
bus.advance_cycles(10);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(CYCLES_PER_FRAME - 10);
assert_eq!(bus.read16(IF).unwrap(), 5);
}
#[test]
fn register_masks_read_only_fields_and_word_accesses_are_coherent() {
let mut bus = memory();
bus.advance_cycles(17 * CYCLES_PER_LINE + HBLANK_START);
bus.write32(DISPSTAT, u32::MAX).unwrap();
assert_eq!(bus.read16(DISPSTAT).unwrap(), 0xff3a);
assert_eq!(bus.read16(VCOUNT).unwrap(), 17);
assert_eq!(bus.read32(DISPSTAT).unwrap(), 0x0011_ff3a);
bus.write8(VCOUNT, 200).unwrap();
bus.write8(VCOUNT + 1, 0xff).unwrap();
assert_eq!(bus.read16(VCOUNT).unwrap(), 17);
bus.write8(DISPSTAT, 0).unwrap();
assert_eq!(bus.read16(DISPSTAT).unwrap(), 0xff02);
bus.write8(DISPSTAT + 1, 17).unwrap();
assert_eq!(bus.read16(DISPSTAT).unwrap(), 0x1106);
assert_eq!(bus.read8(DISPSTAT).unwrap(), 6);
assert_eq!(bus.read8(DISPSTAT + 1).unwrap(), 17);
assert_eq!(bus.read8(VCOUNT + 1).unwrap(), 0);
assert_eq!(bus.read16(IF).unwrap(), 0);
assert_eq!(
bus.write16(DISPSTAT + 1, 0),
Err(MemoryError::Unaligned(DISPSTAT + 1))
);
assert_eq!(bus.read16(DISPSTAT).unwrap(), 0x1106);
}
#[test]
fn writing_comparator_can_create_one_immediate_rising_match_request() {
let mut bus = memory();
bus.advance_cycles(7 * CYCLES_PER_LINE);
bus.write16(DISPSTAT, 0x0620).unwrap();
assert_eq!(bus.read16(DISPSTAT).unwrap() & 4, 0);
bus.write8(DISPSTAT + 1, 7).unwrap();
assert_eq!(bus.read16(DISPSTAT).unwrap() & 4, 4);
assert_eq!(bus.read16(IF).unwrap(), 4);
bus.write16(IF, 4).unwrap();
bus.write8(DISPSTAT + 1, 7).unwrap(); // Already matches
bus.write8(DISPSTAT, 0).unwrap();
bus.write8(DISPSTAT, 0x20).unwrap(); // Enabling alone does not create a match edge
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.write8(DISPSTAT + 1, 6).unwrap();
bus.write8(DISPSTAT + 1, 7).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 4);
}
#[test]
fn zero_comparator_irq_waits_for_the_next_line_zero_entry() {
let mut bus = memory();
bus.write16(DISPSTAT, 0x20).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(CYCLES_PER_FRAME - 1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 4);
bus.write16(IF, 4).unwrap();
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 0);
}
#[test]
fn out_of_range_comparator_values_never_match() {
let mut bus = memory();
for compare in 228..=255 {
bus.write16(DISPSTAT, (compare << 8) | 0x20).unwrap();
bus.advance_cycles(CYCLES_PER_FRAME);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 4, 0);
assert_eq!(bus.read16(IF).unwrap(), 0);
}
}
#[test]
fn enabling_vblank_or_hblank_irq_does_not_replay_an_active_status() {
let mut bus = memory();
bus.advance_cycles(VBLANK_START);
bus.write16(DISPSTAT, 8).unwrap();
bus.advance_cycles(0);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(CYCLES_PER_FRAME - 1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 1);
let mut bus = memory();
bus.advance_cycles(HBLANK_START);
bus.write16(DISPSTAT, 0x10).unwrap();
bus.advance_cycles(CYCLES_PER_LINE - 1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 2);
}
#[test]
fn forced_blank_and_display_mode_changes_do_not_stop_or_reset_the_clock() {
let mut bus = memory();
bus.write16(DISPCNT, 0x80).unwrap();
bus.write16(DISPSTAT, 0x18).unwrap();
bus.advance_cycles(VBLANK_START);
let position = bus.display_position();
assert_eq!(bus.read16(IF).unwrap(), 3);
bus.write16(DISPCNT, 0x403).unwrap();
assert_eq!(bus.display_position(), position);
bus.advance_cycles(CYCLES_PER_FRAME);
assert_eq!(bus.display_position().vblanks, 2);
assert_eq!(bus.display_position().scanline, 160);
}
#[test]
fn timer_and_display_events_can_latch_in_the_same_clock_batch() {
let mut bus = memory();
let reload = 65536 - HBLANK_START;
bus.write32(TIMER_BASE, 0x00c0_0000 | reload).unwrap();
bus.write16(DISPSTAT, 0x10).unwrap();
bus.advance_cycles(HBLANK_START);
assert_eq!(bus.read16(IF).unwrap(), 0x0a);
bus.write16(IF, 2).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 8);
}
#[test]
fn bulk_clock_advances_match_a_cycle_by_cycle_reference() {
for compare in [0_u16, 1, 160, 226, 227, 255] {
let mut bus = memory();
bus.write16(DISPSTAT, (compare << 8) | 0x38).unwrap();
let mut line = 0_u16;
let mut cycle = 0_u16;
let mut frames = 0_u64;
let mut vblanks = 0_u64;
for chunk in [
0,
1,
959,
46,
226,
17_777,
CYCLES_PER_FRAME * 2 + 99,
1232,
77,
] {
let mut pending = 0;
for _ in 0..chunk {
cycle += 1;
if cycle == 1006 {
pending |= 2;
}
if cycle == 1232 {
cycle = 0;
line += 1;
if line == 228 {
line = 0;
frames += 1;
}
if line == 160 {
vblanks += 1;
pending |= 1;
}
if line == compare {
pending |= 4;
}
}
}
bus.advance_cycles(chunk);
assert_eq!(
bus.read16(IF).unwrap(),
pending,
"compare {compare}, chunk {chunk}"
);
let flags = u16::from((160..227).contains(&line))
| (u16::from(cycle >= 1006) << 1)
| (u16::from(line == compare) << 2);
assert_eq!(bus.read16(DISPSTAT).unwrap(), (compare << 8) | 0x38 | flags);
let position = bus.display_position();
assert_eq!(
(
position.scanline,
position.line_cycle,
position.frames,
position.vblanks
),
(line, cycle, frames, vblanks)
);
bus.write16(IF, 7).unwrap();
}
}
}
#[test]
fn maximum_batches_preserve_multi_frame_counts_and_do_not_overflow_u32() {
let mut bus = memory();
bus.write16(DISPSTAT, 0xa038).unwrap();
for multiplier in 1..=2_u64 {
bus.advance_cycles(u32::MAX);
let total = multiplier * u64::from(u32::MAX);
let phase = total % u64::from(CYCLES_PER_FRAME);
let position = bus.display_position();
assert_eq!(position.frames, total / u64::from(CYCLES_PER_FRAME));
assert_eq!(
position.vblanks,
(total + u64::from(CYCLES_PER_FRAME - VBLANK_START)) / u64::from(CYCLES_PER_FRAME)
);
assert_eq!(
u64::from(position.scanline),
phase / u64::from(CYCLES_PER_LINE)
);
assert_eq!(
u64::from(position.line_cycle),
phase % u64::from(CYCLES_PER_LINE)
);
assert_eq!(bus.read16(IF).unwrap(), 7);
assert_eq!(bus.cycles(), total);
bus.write16(IF, 7).unwrap();
}
}
#[test]
fn display_sources_enter_an_original_irq_handler_acknowledge_and_return() {
for (control, source, edge) in [
(8, 1, VBLANK_START),
(0x10, 2, HBLANK_START),
(0xa020, 4, VBLANK_START),
] {
let mut bios = vec![0; BIOS_SIZE];
for (index, instruction) in [0xe1c1_20b2_u32, 0xe280_0001, 0xe25e_f004]
.into_iter()
.enumerate()
{
// STRH r2,[r1,#2] (IF); ADD r0,r0,#1; SUBS pc,lr,#4
let offset = 0x18 + index * 4;
bios[offset..offset + 4].copy_from_slice(&instruction.to_le_bytes());
}
let rom = [
0xe3a0_1301_u32,
0xe281_1c02,
0xe3a0_2000 | source,
0xe283_3001,
0xeaff_fffe,
]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect();
let mut bus = Memory::with_bios(rom, bios).unwrap();
let mut cpu = Cpu::new(ROM_START);
for _ in 0..3 {
cpu.step(&mut bus).unwrap();
}
bus.write16(DISPSTAT, control).unwrap();
bus.write16(IE, source as u16).unwrap();
bus.write32(IME, 1).unwrap();
bus.advance_cycles(edge - 1);
let mut machine = Machine::new(cpu, bus);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[3], 1);
assert_eq!(machine.memory().read16(IF).unwrap(), source as u16);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().mode(), Mode::Irq);
assert_eq!(machine.cpu().pc(), 0x18);
assert_eq!(machine.cpu().registers()[14], ROM_START + 20);
for _ in 0..3 {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
assert_eq!(machine.cpu().registers()[0], 1);
assert_eq!(machine.cpu().mode(), Mode::System);
assert_eq!(machine.cpu().pc(), ROM_START + 16);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
}
}
#[test]
fn failed_block_store_does_not_change_window_controls_or_advance_display() {
let rom = [
0xe3a0_2301_u32, // MOV r2,#0x04000000
0xe282_2048, // ADD r2,r2,#0x48 (WININ/WINOUT)
0xe3a0_003f, // MOV r0,#0x3f
0xe3a0_1001, // MOV r1,#1
0xe8a2_001f, // STMIA r2!,{r0-r4}; last word is unmapped 0x04000058
]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect();
let mut bus = Memory::new(rom).unwrap();
let mut cpu = Cpu::new(ROM_START);
for _ in 0..4 {
cpu.step(&mut bus).unwrap();
}
bus.advance_cycles(7 * CYCLES_PER_LINE);
let before = cpu.clone();
let position = bus.display_position();
let mut machine = Machine::new(cpu, bus);
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.memory().display_position(), position);
assert_eq!(machine.memory().read32(gba_rust::io::WININ).unwrap(), 0);
assert_eq!(machine.memory().read16(DISPSTAT).unwrap(), 0);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
}
#[test]
fn frame_runner_waits_for_next_vblank_event_not_an_already_set_flag() {
let mut bus = memory();
bus.write32(0x0300_0000, 0xeaff_fffe).unwrap(); // 3-cycle IWRAM self-branch
bus.advance_cycles(VBLANK_START - 2);
let mut machine = Machine::new(Cpu::new(0x0300_0000), bus);
assert_eq!(machine.run_until_vblank(1).unwrap(), 1);
assert_eq!(machine.memory().display_position().line_cycle, 1);
assert_eq!(machine.memory().display_position().vblanks, 1);
let steps = machine
.run_until_vblank((CYCLES_PER_FRAME / 3) as usize)
.unwrap();
assert_eq!(steps, (CYCLES_PER_FRAME / 3) as usize);
assert_eq!(machine.memory().display_position().line_cycle, 1);
assert_eq!(machine.memory().display_position().vblanks, 2);
}
#[test]
fn frame_runner_limits_and_cpu_errors_preserve_completed_progress_only() {
let mut bus = memory();
bus.write32(0x0300_0000, 0xeaff_fffe).unwrap();
let mut machine = Machine::new(Cpu::new(0x0300_0000), bus);
assert_eq!(
machine.run_until_vblank(0),
Err(FrameRunError::StepLimit(0))
);
assert_eq!(machine.cycles(), 0);
assert_eq!(
machine.run_until_vblank(2),
Err(FrameRunError::StepLimit(2))
);
assert_eq!(machine.cycles(), 6);
assert_eq!(machine.memory().display_position().line_cycle, 6);
machine
.memory_mut()
.write32(0x0300_0000, 0xf000_0000)
.unwrap();
let position = machine.memory().display_position();
assert!(matches!(
machine.run_until_vblank(10),
Err(FrameRunError::Cpu(_))
));
assert_eq!(machine.memory().display_position(), position);
assert_eq!(machine.cycles(), 6);
}

860
tests/dma.rs Normal file
View file

@ -0,0 +1,860 @@
use gba_rust::{
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);
}

687
tests/halt.rs Normal file
View file

@ -0,0 +1,687 @@
use gba_rust::{
cpu::{Cpu, InstructionSet, Mode},
display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, HBLANK_START, VBLANK_START},
dma::{DmaError, DMA_BASE},
io::{DISPSTAT, HALTCNT, IE, IF, IME, POSTFLG, TIMER_BASE},
machine::{FrameRunError, Machine, MachineError, StepKind},
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START},
timing::StepTiming,
};
fn words(code: &[u32]) -> Vec<u8> {
code.iter().flat_map(|word| word.to_le_bytes()).collect()
}
fn machine() -> Machine {
Machine::new(
Cpu::new(ROM_START),
Memory::new(words(&[0xe280_0001, 0xeaff_fffe])).unwrap(),
)
}
fn pending_timer(bus: &mut Memory) {
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
bus.advance_cycles(1);
bus.write16(TIMER_BASE + 2, 0).unwrap();
}
fn halt(machine: &mut Machine) {
// Bare bus writes are explicit host/debug setup, not CPU-originated accesses.
machine.memory_mut().write8(HALTCNT, 0).unwrap();
}
fn dma(bus: &mut Memory, control: u16, count: u16) {
bus.write32(DMA_BASE, 0x0200_0000).unwrap();
bus.write32(DMA_BASE + 4, 0x0300_0000).unwrap();
bus.write32(
DMA_BASE + 8,
u32::from(control | 0x8000) << 16 | u32::from(count),
)
.unwrap();
}
// Prepare a real store at entry+8 with operands loaded by CPU instructions.
fn store_machine(
entry: u32,
instruction: u32,
address: u32,
value: u32,
timed_setup: bool,
) -> Machine {
let program = words(&[
0xe59f_1008,
0xe59f_0008,
instruction,
0xe282_2001,
address,
value,
]);
let mut bios = vec![0; BIOS_SIZE];
let mut bus = if entry < BIOS_SIZE as u32 {
bios[entry as usize..entry as usize + program.len()].copy_from_slice(&program);
Memory::with_bios(vec![], bios).unwrap()
} else if entry == ROM_START {
Memory::with_bios(program, bios).unwrap()
} else {
let mut bus = Memory::with_bios(vec![], bios).unwrap();
for (offset, value) in program.into_iter().enumerate() {
bus.write8(entry + offset as u32, value).unwrap();
}
bus
};
let mut cpu = Cpu::new(entry);
for _ in 0..2 {
if timed_setup {
cpu.step_timed(&mut bus).unwrap();
} else {
cpu.step(&mut bus).unwrap();
}
}
Machine::new(cpu, bus)
}
#[test]
fn reset_register_masks_widths_and_reserved_bytes() {
let mut bus = Memory::new(vec![]).unwrap();
assert!(!bus.halted());
assert_eq!(bus.read32(POSTFLG).unwrap(), 0);
bus.write8(POSTFLG, 0xff).unwrap();
assert_eq!(bus.read32(POSTFLG).unwrap(), 1);
assert!(!bus.halted());
bus.write8(POSTFLG, 0xfe).unwrap();
assert_eq!(bus.read8(POSTFLG).unwrap(), 0);
bus.write16(POSTFLG + 2, 0xffff).unwrap();
assert_eq!(bus.read16(POSTFLG + 2).unwrap(), 0);
assert!(!bus.halted());
bus.write16(POSTFLG, 0x7f01).unwrap();
assert!(bus.halted());
assert_eq!(bus.read16(POSTFLG).unwrap(), 1); // HALTCNT is write-only.
assert_eq!(bus.read16(HALTCNT), Err(MemoryError::Unaligned(HALTCNT)));
assert_eq!(
bus.read8(POSTFLG + 4),
Err(MemoryError::Unmapped(POSTFLG + 4))
);
assert_eq!(
bus.read8(POSTFLG + 0x400),
Err(MemoryError::Unmapped(POSTFLG + 0x400))
);
}
#[test]
fn all_halt_values_ignore_low_seven_bits() {
for value in 0..=0x7f {
let mut machine = machine();
machine.memory_mut().write8(HALTCNT, value).unwrap();
assert!(machine.halted());
assert_eq!(machine.memory().read8(HALTCNT).unwrap(), 0);
assert_eq!(machine.cycles(), 0);
}
}
#[test]
fn halt_idle_does_not_fetch_instructions_or_change_cpu_registers() {
let mut machine = Machine::new(Cpu::new(0xdead_beec), Memory::new(vec![]).unwrap());
halt(&mut machine);
let before = machine.cpu().clone();
for cycles in [HBLANK_START, CYCLES_PER_LINE - HBLANK_START, HBLANK_START] {
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.cpu(), &before);
assert_eq!(
machine.last_timing(),
StepTiming {
idle_cycles: cycles,
..StepTiming::default()
}
);
assert!(machine.halted());
}
assert_eq!(machine.cycles(), u64::from(CYCLES_PER_LINE + HBLANK_START));
}
#[test]
fn pending_enabled_request_prevents_sleep_regardless_of_ime() {
for ime in [0, 1] {
let mut machine = machine();
pending_timer(machine.memory_mut());
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().write16(IME, ime).unwrap();
halt(&mut machine);
assert!(!machine.halted());
assert_eq!(
machine.step().unwrap(),
if ime == 0 {
StepKind::Instruction
} else {
StepKind::IrqEntry
}
);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
}
}
#[test]
fn ie_gates_wake_but_ime_does_not_and_wake_is_not_reversed_by_acknowledgement() {
let mut machine = machine();
pending_timer(machine.memory_mut());
machine.memory_mut().write16(IE, 1).unwrap(); // Wrong source.
halt(&mut machine);
assert!(machine.halted());
machine.memory_mut().write16(IME, 1).unwrap();
assert!(machine.halted());
machine.memory_mut().write16(IE, 8).unwrap();
assert!(!machine.halted());
machine.memory_mut().write16(IF, 8).unwrap();
machine.memory_mut().write16(IE, 0).unwrap();
assert!(!machine.halted());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
#[test]
fn timer_wakes_at_exact_overflow_without_ime_and_resumes_next_instruction() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_fff6)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
halt(&mut machine);
let before = machine.cpu().clone();
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, 10);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 10);
assert!(!machine.halted());
assert!(!machine.memory().irq_pending());
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[0], 1);
assert_eq!(machine.cpu().pc(), ROM_START + 4);
}
#[test]
fn cpsr_irq_mask_blocks_handler_but_not_halt_wake() {
let mut bios = vec![0; BIOS_SIZE];
bios[0..4].copy_from_slice(&0xe280_0001_u32.to_le_bytes()); // ADD r0,r0,#1
let mut bus = Memory::with_bios(vec![], bios).unwrap();
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
bus.write16(IE, 8).unwrap();
bus.write16(IME, 1).unwrap();
bus.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::at_reset(), bus);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert!(!machine.halted());
assert!(machine.memory().irq_pending());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().mode(), Mode::Supervisor);
assert_eq!(machine.cpu().registers()[0], 1);
}
#[test]
fn unmasked_wake_enters_irq_before_executing_resumed_instruction() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_ffff)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().write16(IME, 1).unwrap();
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().mode(), Mode::Irq);
assert_eq!(machine.cpu().pc(), 0x18);
assert_eq!(machine.cpu().registers()[14], ROM_START + 4);
assert_eq!(machine.cpu().registers()[0], 0);
}
#[test]
fn wake_irq_returns_to_thumb_without_skipping_instruction() {
let mut rom = words(&[0xe28f_0001, 0xe12f_ff10]); // ADD r0,pc,#1; BX r0
rom.extend(0x2107_u16.to_le_bytes()); // MOV r1,#7
let bios = 0xe25e_f004_u32.to_le_bytes().repeat(BIOS_SIZE / 4); // SUBS pc,lr,#4
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();
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
bus.write16(IE, 8).unwrap();
bus.write16(IME, 1).unwrap();
bus.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(cpu, bus);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().spsr(), Some(0x3f));
machine.memory_mut().write16(TIMER_BASE + 2, 0).unwrap();
machine.memory_mut().write16(IF, 8).unwrap();
machine.step().unwrap();
assert_eq!(machine.cpu().instruction_set(), InstructionSet::Thumb);
assert_eq!(machine.cpu().pc(), ROM_START + 8);
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[1], 7);
}
#[test]
fn all_display_interrupt_sources_can_wake_halt_at_their_edges() {
for (control, mask, edge) in [
(8, 1, VBLANK_START),
(0x10, 2, HBLANK_START),
(0x0120, 4, CYCLES_PER_LINE),
] {
let mut machine = machine();
machine.memory_mut().write16(DISPSTAT, control).unwrap();
machine.memory_mut().write16(IE, mask).unwrap();
machine.memory_mut().advance_cycles(edge - 1);
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, 1);
assert_eq!(machine.cycles(), u64::from(edge));
assert!(!machine.halted());
assert_eq!(machine.memory().read16(IF).unwrap(), mask);
}
}
#[test]
fn local_interrupt_enable_is_required_to_generate_a_wake_request() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x0080_ffff)
.unwrap(); // IRQ off.
machine.memory_mut().write16(IE, 0x3fff).unwrap();
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, 1);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert!(machine.halted());
}
#[test]
fn idle_scheduling_accounts_for_timer_prescaler_phase() {
for (selection, divisor) in [1, 64, 256, 1024].into_iter().enumerate() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, ((0xc0 | selection as u32) << 16) | 0xffff)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().advance_cycles(divisor - 1);
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, 1);
assert_eq!(machine.cycles(), u64::from(divisor));
assert!(!machine.halted());
}
}
#[test]
fn cascaded_timer_wakes_only_on_predecessor_overflow() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x0080_fff6)
.unwrap(); // Ten cycles per overflow, IRQ off.
machine
.memory_mut()
.write32(TIMER_BASE + 4, 0x00c4_fffe)
.unwrap(); // Two pulses, IRQ on.
machine.memory_mut().write16(IE, 0x10).unwrap();
halt(&mut machine);
for expected in [true, false] {
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, 10);
assert_eq!(machine.halted(), expected);
}
assert_eq!(machine.memory().read16(IF).unwrap(), 0x10);
assert_eq!(machine.cycles(), 20);
}
#[test]
fn disabled_predecessor_cannot_clock_a_cascaded_timer_during_halt() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE + 4, 0x00c4_ffff)
.unwrap();
machine.memory_mut().write16(IE, 0x10).unwrap();
halt(&mut machine);
for _ in 0..4 {
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert!(machine.halted());
assert_eq!(machine.memory().read16(TIMER_BASE + 4).unwrap(), 0xffff);
}
}
#[test]
fn idle_batches_match_cycle_by_cycle_display_and_timer_execution() {
for seed in 0..16_u32 {
let configure = || {
let mut bus = Memory::new(vec![]).unwrap();
bus.write16(DISPSTAT, ((seed as u16 * 17) << 8) | 0x38)
.unwrap();
for index in 0..4 {
let control = if index != 0 && (seed + index) % 3 == 0 {
0xc4
} else {
0xc0 | ((seed + index) & 3)
};
bus.write32(
TIMER_BASE + index * 4,
control << 16 | (0xffff - (seed * 13 + index * 7)),
)
.unwrap();
}
bus.advance_cycles(seed * 91);
bus.write16(IF, 0x3fff).unwrap();
bus.write8(HALTCNT, 0).unwrap(); // IE=0, so all events are observable without wake.
bus
};
let mut reference = configure();
let mut machine = Machine::new(Cpu::new(ROM_START), configure());
for _ in 0..40 {
machine.memory_mut().write16(IF, 0x3fff).unwrap();
reference.write16(IF, 0x3fff).unwrap();
let mut elapsed = 0;
loop {
reference.advance_cycles(1);
elapsed += 1;
let position = reference.display_position();
if position.line_cycle == 0
|| u32::from(position.line_cycle) == HBLANK_START
|| reference.read16(IF).unwrap() != 0
{
break;
}
assert!(elapsed < CYCLES_PER_LINE);
}
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, elapsed);
assert_eq!(
machine.memory().display_position(),
reference.display_position()
);
assert_eq!(machine.cycles(), reference.cycles());
assert_eq!(
machine.memory().read16(IF).unwrap(),
reference.read16(IF).unwrap()
);
for index in 0..4 {
assert_eq!(
machine.memory().read32(TIMER_BASE + index * 4).unwrap(),
reference.read32(TIMER_BASE + index * 4).unwrap()
);
}
}
}
}
#[test]
fn blank_triggered_dma_continues_during_halt_and_completion_can_wake() {
let mut machine = machine();
machine.memory_mut().write16(0x0200_0000, 0x1234).unwrap();
dma(machine.memory_mut(), 0x5000, 1); // VBlank and completion IRQ.
machine.memory_mut().write16(IE, 0x100).unwrap();
halt(&mut machine);
assert_eq!(machine.run_until_vblank(320).unwrap(), 320);
assert!(machine.halted()); // No display IRQ enabled; DMA has not run yet.
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert_eq!(machine.memory().read16(0x0300_0000).unwrap(), 0x1234);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x100);
assert!(!machine.halted());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
#[test]
fn dma_without_completion_irq_does_not_wake_halt() {
let mut machine = machine();
dma(machine.memory_mut(), 0x2000, 1); // HBlank, no IRQ.
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.cycles(), u64::from(HBLANK_START));
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert!(machine.halted());
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.cycles(), u64::from(CYCLES_PER_LINE));
}
#[test]
fn dma_finishes_before_irq_delivery_after_wake_during_transfer() {
let mut machine = machine();
dma(machine.memory_mut(), 0, 2);
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_ffff)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().write16(IME, 1).unwrap();
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert!(!machine.halted());
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert_eq!(machine.cpu().pc(), ROM_START);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
}
#[test]
fn unsupported_dma_power_control_write_preserves_halt_and_clock() {
let mut machine = machine();
dma(machine.memory_mut(), 0, 1);
machine.memory_mut().write16(DMA_BASE + 10, 0).unwrap();
machine.memory_mut().write32(DMA_BASE + 4, POSTFLG).unwrap();
machine.memory_mut().write16(DMA_BASE + 10, 0x8000).unwrap();
halt(&mut machine);
assert_eq!(
machine.step(),
Err(MachineError::Dma(DmaError::PowerControlDestination {
channel: 0,
address: POSTFLG
}))
);
assert!(machine.halted());
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0);
}
#[test]
fn byte_halfword_and_word_cpu_stores_enter_halt_only_from_bios() {
for timed in [false, true] {
for entry in [0x100, ROM_START, 0x0300_0000] {
for (instruction, address, value) in [
(0xe5c1_0000, HALTCNT, 0x7f), // STRB
(0xe1c1_00b0, POSTFLG, 1), // STRH
(0xe581_0000, POSTFLG, 0xffff_0001), // STR
] {
let mut machine = store_machine(entry, instruction, address, value, timed);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.halted(), entry < BIOS_SIZE as u32);
assert_eq!(machine.cpu().pc(), entry + 12);
assert_eq!(machine.cpu().registers()[2], 0);
let before = machine.cpu().clone();
if entry < BIOS_SIZE as u32 {
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.cpu(), &before);
} else {
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[2], 1);
}
}
}
}
}
#[test]
fn thumb_halt_stores_use_the_executing_address_for_bios_access() {
for entry in [0x100, ROM_START] {
let mut code = words(&[0xe28f_0001, 0xe12f_ff10]); // Enter Thumb at entry+8.
for instruction in [0x2000_u16, 0x4901, 0x7008, 0x3201] {
// MOV r0,#0; LDR r1,[pc,#4]; STRB r0,[r1]; ADD r2,#1.
code.extend(instruction.to_le_bytes());
}
code.extend(HALTCNT.to_le_bytes());
let mut bios = vec![0; BIOS_SIZE];
let mut bus = if entry < BIOS_SIZE as u32 {
bios[entry as usize..entry as usize + code.len()].copy_from_slice(&code);
Memory::with_bios(vec![], bios).unwrap()
} else {
Memory::new(code).unwrap()
};
let mut cpu = Cpu::new(entry);
for _ in 0..4 {
cpu.step(&mut bus).unwrap();
}
let mut machine = Machine::new(cpu, bus);
assert_eq!(machine.cpu().instruction_set(), InstructionSet::Thumb);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().pc(), entry + 14);
assert_eq!(machine.halted(), entry < BIOS_SIZE as u32);
}
}
#[test]
fn halt_store_finishes_and_can_wake_during_its_own_cycles() {
let mut machine = store_machine(0x100, 0xe5c1_0000, HALTCNT, 0, false);
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_ffff)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().pc(), 0x10c);
assert_eq!(machine.cycles(), 2);
assert!(!machine.halted());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[2], 1);
}
#[test]
fn stop_diagnostics_validate_entire_write_before_postflg_or_halt_changes() {
let mut bus = Memory::new(vec![]).unwrap();
for value in [0x80, 0xff] {
assert_eq!(
bus.write8(HALTCNT, value),
Err(MemoryError::UnsupportedStop)
);
assert_eq!(
bus.write16(POSTFLG, u16::from(value) << 8 | 1),
Err(MemoryError::UnsupportedStop)
);
assert_eq!(
bus.write32(POSTFLG, u32::from(value) << 8 | 1),
Err(MemoryError::UnsupportedStop)
);
assert_eq!(bus.read32(POSTFLG).unwrap(), 0);
assert!(!bus.halted());
assert_eq!(bus.cycles(), 0);
}
}
#[test]
fn bios_stop_error_preserves_cpu_timing_and_clears_access_context() {
let mut machine = store_machine(0x100, 0xe581_0000, POSTFLG, 0x8001, true);
let before = machine.cpu().clone();
assert_eq!(
machine.step(),
Err(MachineError::Cpu(MemoryError::UnsupportedStop.into()))
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.last_timing(), StepTiming::default());
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0);
assert!(!machine.halted());
// A failed non-BIOS instruction must also clear its access context.
let mut machine = store_machine(ROM_START, 0xe581_0000, 0x0400_0058, 0, true);
assert!(machine.step().is_err());
machine.memory_mut().write8(POSTFLG, 1).unwrap();
halt(&mut machine);
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 1);
assert!(machine.halted());
}
#[test]
fn non_bios_stop_write_is_ignored_not_a_diagnostic() {
for entry in [ROM_START, 0x0300_0000] {
let mut machine = store_machine(entry, 0xe581_0000, POSTFLG, 0x8001, false);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert!(!machine.halted());
assert_eq!(machine.memory().read32(POSTFLG).unwrap(), 0);
}
}
#[test]
fn failed_block_store_cannot_partially_halt_or_change_postflg() {
// The first word is mapped; the second is not. Validation precedes all writes.
let mut machine = store_machine(0x100, 0xe8a1_0005, POSTFLG, 1, false); // STMIA r1!,{r0,r2}
let before = machine.cpu().clone();
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert!(!machine.halted());
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0);
assert_eq!(machine.cycles(), 0);
}
#[test]
fn cpu_only_apis_record_halt_but_do_not_idle_or_enforce_pause() {
for timed in [false, true] {
let mut bios = vec![0; BIOS_SIZE];
bios[..24].copy_from_slice(&words(&[
0xe59f_1008,
0xe3a0_0000,
0xe5c1_0000,
0xe282_2001,
HALTCNT,
0,
]));
let mut bus = Memory::with_bios(vec![], bios).unwrap();
let mut cpu = Cpu::new(0);
for _ in 0..3 {
if timed {
cpu.step_timed(&mut bus).unwrap();
} else {
cpu.step(&mut bus).unwrap();
}
}
assert!(bus.halted());
cpu.step(&mut bus).unwrap(); // CPU-only API intentionally bypasses machine scheduling.
assert_eq!(cpu.registers()[2], 1);
assert_eq!(bus.cycles(), 0);
assert!(bus.halted());
}
}
#[test]
fn frame_runner_remains_bounded_with_no_wake_source_and_reports_idle_cost() {
let mut machine = Machine::new(Cpu::new(ROM_START), Memory::new(vec![]).unwrap());
halt(&mut machine);
assert_eq!(
machine.run_until_vblank(0),
Err(FrameRunError::StepLimit(0))
);
assert_eq!(machine.cycles(), 0);
assert_eq!(
machine.run_until_vblank(319),
Err(FrameRunError::StepLimit(319))
);
assert!(machine.halted());
assert_eq!(machine.run_until_vblank(1).unwrap(), 1);
assert_eq!(machine.cycles(), u64::from(VBLANK_START));
assert_eq!(machine.memory().display_position().line_cycle, 0);
assert!(machine.halted());
assert_eq!(machine.run_until_vblank(456).unwrap(), 456);
assert_eq!(machine.cycles(), u64::from(VBLANK_START + CYCLES_PER_FRAME));
}
#[test]
fn externally_advanced_clock_can_wake_halt_without_cpu_execution() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_fff0)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
halt(&mut machine);
let before = machine.cpu().clone();
machine.memory_mut().advance_cycles(16);
assert!(!machine.halted());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.last_timing(), StepTiming::default());
}

272
tests/machine.rs Normal file
View file

@ -0,0 +1,272 @@
use gba_rust::{
cpu::{Cpu, CpuError, InstructionSet, Mode},
io::{IE, IF, IME, TIMER_BASE, WAITCNT},
machine::{Machine, MachineError, StepKind},
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START},
timer_demo::{timer_demo, TIMER_DEMO_STEPS},
};
fn words(values: &[u32]) -> Vec<u8> {
values
.iter()
.flat_map(|value| value.to_le_bytes())
.collect()
}
fn timer(bus: &mut Memory) {
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
bus.write16(IE, 8).unwrap();
bus.write32(IME, 1).unwrap();
}
fn pending_timer(bus: &mut Memory) {
timer(bus);
bus.advance_cycles(1);
bus.write16(TIMER_BASE + 2, 0).unwrap();
}
// Load test operands through real instructions, without exposing CPU setters.
fn prepared(instruction: u32, operands: &[(usize, u32)]) -> Machine {
let offset = (operands.len() as u32 - 1) * 4;
let mut code: Vec<u32> = operands
.iter()
.map(|&(register, _)| 0xe59f_0000 | (register as u32) << 12 | offset)
.collect();
code.push(instruction);
code.extend(operands.iter().map(|&(_, value)| value));
let mut bus = Memory::new(words(&code)).unwrap();
let mut cpu = Cpu::new(ROM_START);
for _ in operands {
cpu.step(&mut bus).unwrap();
}
Machine::new(cpu, bus)
}
#[test]
fn timer_demo_configures_hardware_in_cpu_code_and_handles_exactly_one_irq() {
let mut machine = timer_demo().unwrap();
let mut entries = 0;
for step in 1..=TIMER_DEMO_STEPS {
let kind = machine.step().unwrap();
assert!(machine.last_timing().total() > 0);
if kind == StepKind::IrqEntry {
entries += 1;
assert_eq!(step, 13); // Nominal timing model, not a hardware measurement
assert_eq!(machine.cpu().pc(), 0x18);
assert_eq!(machine.cpu().spsr(), Some(0x1f));
assert_eq!(machine.cpu().registers()[14], ROM_START + 0x30);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
}
}
assert_eq!(entries, 1);
assert_eq!(machine.cycles(), 153);
assert_eq!(machine.cpu().mode(), Mode::System);
assert_eq!(machine.cpu().pc(), ROM_START + 0x2c);
assert_eq!(machine.cpu().registers()[10], 1);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.memory().read16(TIMER_BASE + 2).unwrap(), 0);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xfff4);
for _ in 0..100 {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
assert_eq!(machine.cpu().registers()[10], 1);
}
#[test]
fn overflow_after_instruction_is_sampled_on_next_step() {
let mut bus = Memory::new(words(&[0xe280_0001, 0xe280_0001])).unwrap();
timer(&mut bus);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[0], 1);
assert_eq!(machine.cpu().pc(), ROM_START + 4);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().registers()[0], 1); // Interrupted instruction did not execute
assert_eq!(machine.cpu().registers()[14], ROM_START + 8);
assert_eq!(machine.cycles(), 11); // First ROM access: 8; BIOS IRQ refill: 3
}
#[test]
fn missing_vector_errors_only_after_completed_irq_entry() {
let mut bus = Memory::new(vec![]).unwrap();
pending_timer(&mut bus);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); // No ROM fetch required
let before = machine.cpu().clone();
let cycles = machine.cycles();
assert_eq!(
machine.step(),
Err(MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped(
0x18
))))
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
}
#[test]
fn cpu_mask_defers_irq_until_msr_unmasks_it() {
let mut bios = vec![0; BIOS_SIZE];
bios[0..4].copy_from_slice(&0xe321_f013_u32.to_le_bytes()); // MSR CPSR_c, #SVC (clear I)
let mut bus = Memory::with_bios(vec![], bios).unwrap();
pending_timer(&mut bus);
let mut machine = Machine::new(Cpu::at_reset(), bus);
assert!(machine.memory().irq_pending());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().pc(), 4);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().registers()[14], 8);
assert_eq!(machine.cpu().spsr(), Some(0x13));
}
#[test]
fn ime_and_ie_gate_pending_device_requests_until_cpu_enables_them() {
for register in [IE, IME] {
let value = if register == IE { 8 } else { 1 };
let mut machine = prepared(0xe580_1000, &[(0, register), (1, value)]); // STR r1, [r0]
pending_timer(machine.memory_mut());
machine.memory_mut().write16(register, 0).unwrap();
assert!(!machine.memory().irq_pending());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert!(machine.memory().irq_pending());
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
}
}
#[test]
fn uncleared_request_reenters_after_irq_return_and_no_fiq_is_generated() {
let bios = 0xe25e_f004_u32.to_le_bytes().repeat(BIOS_SIZE / 4); // SUBS pc, lr, #4
let mut bus = Memory::with_bios(words(&[0xeaff_fffe]), bios).unwrap();
pending_timer(&mut bus);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
for _ in 0..3 {
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().mode(), Mode::Irq);
assert_eq!(machine.step().unwrap(), StepKind::Instruction); // I mask prevents immediate nesting
assert_eq!(machine.cpu().mode(), Mode::System);
assert_eq!(machine.cpu().pc(), ROM_START);
}
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
}
#[test]
fn timer_irq_returns_to_thumb_without_skipping_the_interrupted_instruction() {
let mut rom = words(&[0xe28f_0001, 0xe12f_ff10]); // ADD r0, pc, #1; BX r0
rom.extend(0x2107_u16.to_le_bytes()); // MOV r1, #7 at offset 8
let bios = 0xe25e_f004_u32.to_le_bytes().repeat(BIOS_SIZE / 4);
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();
pending_timer(&mut bus);
let mut machine = Machine::new(cpu, bus);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().spsr(), Some(0x3f));
assert_eq!(machine.cpu().registers()[14], ROM_START + 12);
machine.memory_mut().write16(IF, 8).unwrap();
machine.step().unwrap();
assert_eq!(machine.cpu().instruction_set(), InstructionSet::Thumb);
assert_eq!(machine.cpu().pc(), ROM_START + 8);
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[1], 7);
assert_eq!(machine.cpu().pc(), ROM_START + 10);
}
#[test]
fn diagnostics_do_not_advance_timers_or_clock() {
for instruction in [0xf3a0_0001, 0xe590_1000] {
// Unsupported or unmapped LDR
let mut bus = Memory::new(words(&[instruction])).unwrap();
timer(&mut bus);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
let before = machine.cpu().clone();
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xffff);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
}
}
#[test]
fn late_block_store_error_does_not_change_timer_reload_or_enable() {
let mut machine = prepared(
0xe8a0_003e,
&[
(0, TIMER_BASE),
(1, 0x00c0_ffff),
(2, 0x00c0_ffff),
(3, 0x00c0_ffff),
(4, 0x00c0_ffff),
(5, 123),
],
); // STMIA r0!, {r1-r5}; fifth word is unmapped
let before = machine.cpu().clone();
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
for index in 0..4 {
let address = TIMER_BASE + index * 4;
assert_eq!(machine.memory().read32(address).unwrap(), 0);
machine.memory_mut().write16(address + 2, 0x80).unwrap();
assert_eq!(machine.memory().read16(address).unwrap(), 0); // Hidden reload also unchanged
}
}
#[test]
fn late_block_store_error_does_not_acknowledge_if_or_modify_ie() {
let mut machine = prepared(
0xe8a0_001e,
&[(0, IE), (1, 0x0008_ffff), (2, 123), (3, 1), (4, 42)],
);
// IE/IF, WAITCNT, IME are valid. The fourth word at 0x0400020c is not.
pending_timer(machine.memory_mut());
machine.memory_mut().write32(IME, 0).unwrap(); // Let the invalid store execute
let before = machine.cpu().clone();
let cycles = machine.cycles();
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().read32(IE).unwrap(), 0x0008_0008);
assert_eq!(machine.memory().read32(WAITCNT).unwrap(), 0);
assert_eq!(machine.memory().read32(IME).unwrap(), 0);
assert_eq!(machine.last_timing().total(), 0);
}
#[test]
fn late_block_load_error_preserves_cpu_and_devices() {
let mut machine = prepared(0xe8b0_0006, &[(0, IME)]); // LDMIA r0!, {r1, r2}
timer(machine.memory_mut());
let before = machine.cpu().clone();
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xffff);
}
#[test]
fn skipped_condition_charges_code_access_but_not_registers() {
let mut bus = Memory::new(words(&[0x0280_0001])).unwrap(); // ADDEQ r0, r0, #1; Z clear
timer(&mut bus);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[0], 0);
assert_eq!(machine.cpu().pc(), ROM_START + 4);
assert_eq!(machine.cycles(), 8);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
}
#[test]
fn cpu_only_step_does_not_advance_device_time_or_deliver_irq() {
let mut bus = Memory::new(words(&[0xe280_0001])).unwrap();
pending_timer(&mut bus);
let mut cpu = Cpu::new(ROM_START);
let cycles = bus.cycles();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers()[0], 1);
assert_eq!(cpu.mode(), Mode::System);
assert_eq!(bus.cycles(), cycles);
assert!(bus.irq_pending());
}

169
tests/timing.rs Normal file
View file

@ -0,0 +1,169 @@
use gba_rust::{
cpu::Cpu,
io::{IE, IF, IME, TIMER_BASE, WAITCNT},
machine::{Machine, StepKind},
memory::{Memory, MemoryError, ROM_START},
timing::{bus_cycles, AccessKind, AccessWidth},
};
#[test]
fn waitcnt_masks_unused_bits_and_supports_all_bus_widths() {
let mut memory = Memory::new(vec![]).unwrap();
assert_eq!(memory.read32(WAITCNT).unwrap(), 0);
memory.write32(WAITCNT, u32::MAX).unwrap();
assert_eq!(memory.read32(WAITCNT).unwrap(), 0x5fff);
memory.write8(WAITCNT, 0x17).unwrap();
memory.write8(WAITCNT + 1, 0x43).unwrap();
assert_eq!(memory.waitcnt(), 0x4317);
memory.write16(WAITCNT + 2, 0xffff).unwrap();
assert_eq!(memory.read32(WAITCNT).unwrap(), 0x4317);
assert_eq!(memory.read16(IE).unwrap(), 0);
assert_eq!(memory.read16(IF).unwrap(), 0);
assert_eq!(memory.cycles(), 0);
assert_eq!(
memory.write32(WAITCNT + 2, 0),
Err(MemoryError::Unaligned(WAITCNT + 2))
);
assert_eq!(memory.waitcnt(), 0x4317);
}
#[test]
fn every_rom_wait_setting_has_the_documented_halfword_and_word_costs() {
for window in 0..3 {
for first in 0..4 {
for second in 0..2 {
let waitcnt = (first << [2, 5, 8][window]) | (second << [4, 7, 10][window]);
let n = [5, 4, 3, 9][first as usize];
let s = if second == 1 { 2 } else { [3, 5, 9][window] };
for mirror in [0, 0x0100_0000] {
let address = ROM_START + window as u32 * 0x0200_0000 + mirror + 0x100;
for (kind, initial) in
[(AccessKind::NonSequential, n), (AccessKind::Sequential, s)]
{
assert_eq!(
bus_cycles(waitcnt, address, AccessWidth::Byte, kind),
initial
);
assert_eq!(
bus_cycles(waitcnt, address, AccessWidth::Halfword, kind),
initial
);
assert_eq!(
bus_cycles(waitcnt, address, AccessWidth::Word, kind),
initial + s
);
}
}
}
}
}
}
#[test]
fn rom_128_kib_boundaries_force_nonsequential_first_halfwords() {
for address in [
0x0800_0000,
0x0802_0000,
0x09fe_0000,
0x0a02_0000,
0x0c02_0000,
] {
let window = ((address >> 25) - 4) as usize;
let s = [3, 5, 9][window];
assert_eq!(
bus_cycles(0, address, AccessWidth::Halfword, AccessKind::Sequential),
5
);
assert_eq!(
bus_cycles(0, address + 1, AccessWidth::Byte, AccessKind::Sequential),
5
);
assert_eq!(
bus_cycles(0, address, AccessWidth::Word, AccessKind::Sequential),
5 + s
);
assert_eq!(
bus_cycles(
0,
address + 2,
AccessWidth::Halfword,
AccessKind::Sequential
),
s
);
}
}
#[test]
fn internal_regions_and_ewram_use_fixed_width_specific_costs() {
for kind in [AccessKind::NonSequential, AccessKind::Sequential] {
for width in [AccessWidth::Byte, AccessWidth::Halfword, AccessWidth::Word] {
for address in [0, 0x0300_0000, 0x03ff_fffc, WAITCNT] {
assert_eq!(bus_cycles(0x5fff, address, width, kind), 1);
}
for address in [0x0200_0000, 0x02ff_fffc] {
assert_eq!(
bus_cycles(0x5fff, address, width, kind),
if width == AccessWidth::Word { 6 } else { 3 }
);
}
}
}
}
#[test]
fn stored_prefetch_phi_and_sram_settings_do_not_change_supported_bus_costs() {
// Explicit limitation: no prefetch queue, PHI output, or SRAM mapping yet.
for width in [AccessWidth::Halfword, AccessWidth::Word] {
for address in [ROM_START, ROM_START + 4, 0x0a00_0004, 0x0c00_0004] {
for kind in [AccessKind::NonSequential, AccessKind::Sequential] {
assert_eq!(
bus_cycles(0, address, width, kind),
bus_cycles(0x5803, address, width, kind)
);
}
}
}
}
#[test]
fn wait_states_change_when_a_timer_interrupt_is_sampled() {
for (waitcnt, entry_step, instructions, cycles) in [(0, 4, 3, 23), (0x18, 6, 5, 24)] {
let mut memory = Memory::new(0xe280_0001_u32.to_le_bytes().repeat(64)).unwrap();
memory.write16(WAITCNT, waitcnt).unwrap();
memory.write32(TIMER_BASE, 0x00c0_ffec).unwrap(); // 20 cycles
memory.write16(IE, 8).unwrap();
memory.write32(IME, 1).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), memory);
assert_eq!(machine.last_timing().total(), 0);
for step in 1..=entry_step {
let event = machine.step().unwrap();
assert_eq!(
event,
if step == entry_step {
StepKind::IrqEntry
} else {
StepKind::Instruction
}
);
}
assert_eq!(machine.cpu().registers()[0], instructions);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.last_timing().code_cycles, 3);
}
}
#[test]
fn failed_machine_step_preserves_previous_timing_breakdown() {
let mut memory = Memory::new(0xe1a0_0000_u32.to_le_bytes().to_vec()).unwrap();
memory.write32(TIMER_BASE, 0x0080_0000).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), memory);
machine.step().unwrap();
let timing = machine.last_timing();
let cycles = machine.cycles();
let counter = machine.memory().read16(TIMER_BASE).unwrap();
assert!(machine.step().is_err());
assert_eq!(machine.last_timing(), timing);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), counter);
}