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