gameboy-rust/crates/gba-core/tests/halt.rs
0xrsydn f8a0676748 refactor: split into gameboy-rust workspace with core, demos, and desktop crates
Move the emulator into crates/gba-core, the original demo programs into
crates/gba-demos, and the minifb frontend into crates/desktop. The binary
is now gameboy-rust. Imports change from gba_rust to gba_core/gba_demos;
no behavior changes.

gba-core has no dependencies. Its integration tests use gba-demos as a
development dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:09:46 +07:00

687 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_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());
}