Separate Running, Halt, and Stop states. Complete the entering instruction before freezing CPU, DMA, timers, display, and externally supplied clock advances. Wake STOP through the live enabled keypad condition without creating a clock-off IF request. Implement original ARM SWI 0x03, preserve caller state, and expose zero-cycle StopIdle plus stopped frame-runner results. Retain DMA state, timer phase, and captured frames across sleep. Document keypad-only wake and unverified oscillator/edge timing. Add core, BIOS, and demo regression tests; preserve BIOS-only access and failed-store isolation. Validated on Darwin arm64 with debug/release workspace and core/demo suites, Clippy, rustdoc, ten native window smoke tests, and terminal demos.
683 lines
24 KiB
Rust
683 lines
24 KiB
Rust
use gba_core::{
|
|
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_writes_support_all_widths_without_becoming_halt() {
|
|
for value in 0x80..=0xff {
|
|
for width in [1, 2, 4] {
|
|
let mut bus = Memory::new(vec![]).unwrap();
|
|
match width {
|
|
1 => bus.write8(HALTCNT, value).unwrap(),
|
|
2 => bus.write16(POSTFLG, u16::from(value) << 8 | 1).unwrap(),
|
|
_ => bus.write32(POSTFLG, u32::from(value) << 8 | 1).unwrap(),
|
|
}
|
|
assert_eq!(bus.read32(POSTFLG).unwrap(), u32::from(width != 1));
|
|
assert!(bus.stopped());
|
|
assert!(!bus.halted());
|
|
assert_eq!(bus.cycles(), 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn bios_stop_store_finishes_before_freezing_and_access_context_is_cleared() {
|
|
let mut machine = store_machine(0x100, 0xe581_0000, POSTFLG, 0x8001, true);
|
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
|
assert_eq!(machine.cpu().pc(), 0x10c);
|
|
assert_eq!(machine.cycles(), u64::from(machine.last_timing().total()));
|
|
assert!(machine.cycles() > 0);
|
|
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 1);
|
|
assert!(machine.stopped());
|
|
assert!(!machine.halted());
|
|
assert_eq!(machine.step().unwrap(), StepKind::StopIdle);
|
|
assert_eq!(machine.last_timing(), StepTiming::default());
|
|
// 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);
|
|
assert!(!machine.stopped());
|
|
}
|
|
}
|
|
|
|
#[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());
|
|
}
|