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:
parent
156f1eb839
commit
bb6784615a
10 changed files with 3172 additions and 0 deletions
449
tests/display.rs
Normal file
449
tests/display.rs
Normal 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
860
tests/dma.rs
Normal 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
687
tests/halt.rs
Normal 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
272
tests/machine.rs
Normal 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
169
tests/timing.rs
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue