From 04e45996770b6fa5df8353f0c7b503184c5e967b Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Mon, 5 Oct 2026 12:55:35 +0700 Subject: [PATCH] feat(bios): add original boot IRQ waits and memory services Generate original ARM firmware for minimal boot, IRQ callbacks, HALT, IntrWait, CpuSet, and CpuFastSet. --- src/bios.rs | 321 +++++++++++++++++++++++++ tests/bios.rs | 651 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 972 insertions(+) create mode 100644 src/bios.rs create mode 100644 tests/bios.rs diff --git a/src/bios.rs b/src/bios.rs new file mode 100644 index 0000000..03b4725 --- /dev/null +++ b/src/bios.rs @@ -0,0 +1,321 @@ +//! Original, optional ARM BIOS replacement. No Nintendo firmware is included. +//! Supports interrupt waits and memory copy/fill. +//! This is a functional subset, not a complete boot ROM or a timing-compatible BIOS. + +use std::collections::BTreeMap; + + +use crate::{ + cpu::Cpu, + io::{HALTCNT, IME, POSTFLG}, + machine::Machine, + memory::{Memory, MemoryError, BIOS_SIZE, ROM_START}, +}; + +pub const SYSTEM_STACK: u32 = 0x0300_7f00; +pub const IRQ_STACK: u32 = 0x0300_7fa0; +pub const SVC_STACK: u32 = 0x0300_7fe0; +pub const IRQ_FLAGS: u32 = 0x0300_7ff8; +pub const IRQ_HANDLER: u32 = 0x0300_7ffc; +/// An intentional undefined instruction used for unsupported SWIs/vectors and invalid IRQ pointers. +pub const UNSUPPORTED_TRAP: u32 = 0xe7f0_00f0; +/// A separate diagnostic trap for division by zero or invalid unpacking/decompression arguments. +pub const INVALID_ARGUMENT_TRAP: u32 = 0xe7f0_00f1; + +/// Start at reset with our optional firmware. Call Machine::step to execute boot. +/// Boot initializes three stacks, POSTFLG, and the IRQ communication words, then +/// enters ROM_START in ARM System mode. It does not validate a cartridge header, +/// reproduce Nintendo's boot process, or provide file loading. +pub fn boot(rom: Vec) -> Result { + Ok(Machine::new( + Cpu::at_reset(), + Memory::with_bios(rom, image())?, + )) +} + +/// Build a deterministic 16 KiB image from original ARM instructions. +/// SWIs use bits 16–23 of the ARM immediate, or the Thumb immediate byte. +/// Services support User/System callers and preserve caller status. Registers +/// other than each arithmetic service's documented outputs are preserved. +/// IRQ callbacks must preserve r4–r11, acknowledge IF, update IRQ_FLAGS for wait +/// services, and return with BX lr. Nested IRQs and SWIs from callbacks are unsupported. +/// Service routines use the Supervisor stack; exact firmware stack layout and +/// undocumented BIOS side effects are not reproduced. +pub fn image() -> Vec { + let mut a = ArmImage::default(); + for vector in [ + "reset", + "unsupported", + "swi", + "unsupported", + "unsupported", + "unsupported", + "irq", + "unsupported", + ] { + a.branch(14, vector); + } + + a.label("reset"); + a.emit(0xe321_f0d3); // MSR CPSR_c,#SVC|I|F + a.literal(13, SVC_STACK); + a.emit(0xe321_f0d2); // MSR CPSR_c,#IRQ|I|F + a.literal(13, IRQ_STACK); + a.emit(0xe321_f0df); // MSR CPSR_c,#System|I|F + a.literal(13, SYSTEM_STACK); + a.emit(0xe3a0_0000); // MOV r0,#0 + a.literal(1, IRQ_FLAGS); + a.emit(0xe581_0000); // STR r0,[r1] (flags and reserved upper halfword) + a.emit(0xe581_0004); // STR r0,[r1,#4] (callback pointer) + a.literal(1, IME); + a.emit(0xe581_0000); // STR r0,[r1] (disable IRQ delivery during minimal boot) + a.literal(1, POSTFLG); + a.emit(0xe3a0_0001); // MOV r0,#1 + a.emit(0xe5c1_0000); // STRB r0,[r1] + a.emit(0xe321_f01f); // MSR CPSR_c,#System + a.literal(15, ROM_START); // LDR pc,=ROM_START + + a.label("irq"); + a.emit(0xe92d_500f); // STMDB sp!,{r0-r3,r12,lr} + a.literal(1, IRQ_HANDLER); + a.emit(0xe591_1000); // LDR r1,[r1] + a.emit(0xe351_0000); // CMP r1,#0 + a.branch(0, "unsupported"); + a.emit(0xe311_0003); // TST r1,#3 (hardware callback is word-aligned ARM) + a.branch(1, "unsupported"); + a.emit(0xe3a0_0301); // MOV r0,#0x04000000 (callback convention) + a.emit(0xe1a0_e00f); // MOV lr,pc (return after BX) + a.emit(0xe12f_ff11); // BX r1 + a.emit(0xe8bd_500f); // LDMIA sp!,{r0-r3,r12,lr} + a.emit(0xe25e_f004); // SUBS pc,lr,#4 + + a.label("swi"); + a.emit(0xe92d_500f); // STMDB sp!,{r0-r3,r12,lr} + a.emit(0xe14f_3000); // MRS r3,SPSR + a.emit(0xe92d_0008); // STMDB sp!,{r3} (save caller status across IRQs) + // Keep stack usage bounded within the default 64-byte Supervisor area. + // In particular, reject a nested SWI from an IRQ callback during a wait. + a.emit(0xe203_c01f); // AND r12,r3,#mode mask + a.emit(0xe35c_0010); // CMP r12,#User + a.branch(0, "decode_swi"); + a.emit(0xe35c_001f); // CMP r12,#System + a.branch(1, "unsupported"); + a.label("decode_swi"); + a.emit(0xe313_0020); // TST r3,#Thumb + a.emit(0x115e_c0b2); // LDRHNE r12,[lr,#-2] + a.emit(0x051e_c004); // LDREQ r12,[lr,#-4] + a.emit(0x01a0_c82c); // MOVEQ r12,r12,LSR #16 + a.emit(0xe20c_c0ff); // AND r12,r12,#0xff + for (number, target) in [ + (2, "halt"), + (4, "intr_wait"), + (5, "vblank_wait"), + (0x0b, "cpu_set"), + (0x0c, "cpu_fast_set"), + ] { + a.emit(0xe35c_0000 | number); // CMP r12,#service + a.branch(0, target); + } + a.branch(14, "unsupported"); + + a.label("halt"); + a.literal(1, HALTCNT); + a.emit(0xe3a0_0000); // MOV r0,#0 + a.emit(0xe5c1_0000); // STRB r0,[r1] + a.branch(14, "return"); + + a.label("vblank_wait"); + a.emit(0xe3a0_0001); // MOV r0,#1 (discard old flags) + a.emit(0xe3a0_1001); // MOV r1,#VBlank + a.label("intr_wait"); + a.literal(12, IME); + a.emit(0xe3a0_3001); // MOV r3,#1 + a.emit(0xe58c_3000); // STR r3,[r12] (IME=1) + a.literal(2, IRQ_FLAGS); + a.emit(0xe350_0000); // CMP r0,#0 + a.branch(0, "wait_unmask"); + a.emit(0xe1d2_30b0); // LDRH r3,[r2] + a.emit(0xe1c3_3001); // BIC r3,r3,r1 + a.emit(0xe1c2_30b0); // STRH r3,[r2] + a.label("wait_unmask"); + a.emit(0xe10f_3000); // MRS r3,CPSR + a.emit(0xe3c3_3080); // BIC r3,r3,#I + a.emit(0xe121_f003); // MSR CPSR_c,r3 + a.label("wait_loop"); + // Mask IME while checking flags and entering HALT. A new hardware request + // still wakes HALT, but cannot be acknowledged between the check and sleep. + a.emit(0xe3a0_3000); // MOV r3,#0 + a.emit(0xe58c_3000); // STR r3,[r12] (IME=0) + a.emit(0xe1d2_30b0); // LDRH r3,[r2] + a.emit(0xe113_0001); // TST r3,r1 + a.branch(1, "wait_done"); + a.literal(0, HALTCNT); + a.emit(0xe3a0_3000); // MOV r3,#0 + a.emit(0xe5c0_3000); // STRB r3,[r0] (HALT) + a.emit(0xe3a0_3001); // MOV r3,#1 + a.emit(0xe58c_3000); // STR r3,[r12] (IME=1, allow IRQ callback to record flags) + a.branch(14, "wait_loop"); + a.label("wait_done"); + a.emit(0xe1c3_3001); // BIC r3,r3,r1 (consume selected flags only) + a.emit(0xe1c2_30b0); // STRH r3,[r2] + a.emit(0xe3a0_3001); // MOV r3,#1 + a.emit(0xe58c_3000); // STR r3,[r12] + a.branch(14, "return"); + + a.label("cpu_set"); + emit_count(&mut a); + a.emit(0xe312_0301); // TST r2,#0x04000000 (word width) + a.branch(1, "copy_word"); + a.branch(14, "copy_halfword"); + a.label("cpu_fast_set"); + emit_count(&mut a); + a.emit(0xe283_3007); // ADD r3,r3,#7 + a.emit(0xe3c3_3007); // BIC r3,r3,#7 (round up to eight words) + emit_copy_checks(&mut a, true); + a.emit(0xe92d_0ff0); // STMDB sp!,{r4-r11} (60 bytes total SVC stack use) + a.emit(0xe312_0401); // TST r2,#0x01000000 + a.branch(0, "fast_copy_loop"); + a.emit(0xe590_4000); // LDR r4,[r0] (read fill value once) + for register in 5..=11 { + a.emit(0xe1a0_0004 | register << 12); + } // MOV rN,r4 + a.label("fast_fill_loop"); + a.emit(0xe8a1_0ff0); // STMIA r1!,{r4-r11} + a.emit(0xe253_3008); // SUBS r3,r3,#8 + a.branch(1, "fast_fill_loop"); + a.branch(14, "fast_done"); + a.label("fast_copy_loop"); + a.emit(0xe8b0_0ff0); // LDMIA r0!,{r4-r11} + a.emit(0xe8a1_0ff0); // STMIA r1!,{r4-r11} + a.emit(0xe253_3008); // SUBS r3,r3,#8 + a.branch(1, "fast_copy_loop"); + a.label("fast_done"); + a.emit(0xe8bd_0ff0); // LDMIA sp!,{r4-r11} + a.branch(14, "return"); + emit_copy( + &mut a, + true, + ["copy_word", "word_loop", "word_fill", "word_fill_loop"], + ); + emit_copy( + &mut a, + false, + ["copy_halfword", "half_loop", "half_fill", "half_fill_loop"], + ); + + + a.label("return"); + a.emit(0xe10f_3000); // MRS r3,CPSR + a.emit(0xe383_3080); // ORR r3,r3,#I (protect status/stack restoration) + a.emit(0xe121_f003); // MSR CPSR_c,r3 + a.emit(0xe8bd_0008); // LDMIA sp!,{r3} + a.emit(0xe169_f003); // MSR SPSR_fc,r3 + a.emit(0xe8bd_500f); // LDMIA sp!,{r0-r3,r12,lr} + a.emit(0xe1b0_f00e); // MOVS pc,lr + a.label("unsupported"); + a.emit(UNSUPPORTED_TRAP); + a.label("invalid_argument"); + a.emit(INVALID_ARGUMENT_TRAP); + a.finish() +} + +fn emit_count(a: &mut ArmImage) { + a.emit(0xe1a0_3582); // MOV r3,r2,LSL #11 + a.emit(0xe1a0_35a3); // MOV r3,r3,LSR #11 (low 21 bits) +} + +fn emit_copy_checks(a: &mut ArmImage, word: bool) { + a.emit(0xe353_0000); // CMP r3,#0 + a.branch(0, "return"); + a.emit(if word { 0xe3c0_0003 } else { 0xe3c0_0001 }); // BIC r0,r0,#alignment mask + a.emit(if word { 0xe3c1_1003 } else { 0xe3c1_1001 }); // BIC r1,r1,#alignment mask + // Reject protected source ranges and wrapping end addresses before writing. + a.emit(0xe350_0402); // CMP r0,#0x02000000 + a.branch(3, "return"); // BLO + a.emit(if word { 0xe090_c103 } else { 0xe090_c083 }); // ADDS r12,r0,r3,LSL #2/#1 + a.branch(2, "return"); // BCS (address overflow) + a.emit(0xe35c_0402); // CMP r12,#0x02000000 + a.branch(3, "return"); +} + +fn emit_copy(a: &mut ArmImage, word: bool, labels: [&'static str; 4]) { + let [entry, copy_loop, fill, fill_loop] = labels; + a.label(entry); + emit_copy_checks(a, word); + a.emit(0xe312_0401); // TST r2,#0x01000000 (fixed source) + a.branch(1, fill); + a.label(copy_loop); + a.emit(if word { 0xe490_2004 } else { 0xe0d0_20b2 }); // LDR/LDRH r2,[r0],#width + a.emit(if word { 0xe481_2004 } else { 0xe0c1_20b2 }); // STR/STRH r2,[r1],#width + a.emit(0xe253_3001); // SUBS r3,r3,#1 + a.branch(1, copy_loop); + a.branch(14, "return"); + a.label(fill); + a.emit(if word { 0xe590_2000 } else { 0xe1d0_20b0 }); // Read fixed source once. + a.label(fill_loop); + a.emit(if word { 0xe481_2004 } else { 0xe0c1_20b2 }); + a.emit(0xe253_3001); + a.branch(1, fill_loop); + a.branch(14, "return"); +} + +/// Small image builder for branch/literal fixups. Not a general ARM assembler. +#[derive(Default)] +struct ArmImage { + code: Vec, + labels: BTreeMap<&'static str, usize>, + branches: Vec<(usize, u32, &'static str, bool)>, + literals: Vec<(usize, u32, u32)>, +} + +impl ArmImage { + fn emit(&mut self, instruction: u32) { + self.code.push(instruction); + } + + fn label(&mut self, name: &'static str) { + assert!( + self.labels.insert(name, self.code.len()).is_none(), + "duplicate BIOS label" + ); + } + + fn branch(&mut self, condition: u32, target: &'static str) { + self.branches + .push((self.code.len(), condition, target, false)); + self.emit(0); + } + + fn call(&mut self, target: &'static str) { + self.branches.push((self.code.len(), 14, target, true)); + self.emit(0); + } + + fn literal(&mut self, register: u32, value: u32) { + self.literals.push((self.code.len(), register, value)); + self.emit(0); + } + + fn finish(mut self) -> Vec { + for (index, condition, target, link) in self.branches { + let offset = self.labels[target] as i32 - index as i32 - 2; + assert!((-0x80_0000..0x80_0000).contains(&offset)); + self.code[index] = condition << 28 + | 0x0a00_0000 + | (u32::from(link) << 24) + | (offset as u32 & 0x00ff_ffff); + } + for (index, register, value) in self.literals { + let offset = (self.code.len() as i32 - index as i32 - 2) * 4; + assert!(offset.unsigned_abs() <= 0xfff); + self.code[index] = (if offset < 0 { 0xe51f_0000 } else { 0xe59f_0000 }) + | register << 12 + | offset.unsigned_abs(); + self.code.push(value); + } + let mut bytes: Vec = self.code.into_iter().flat_map(u32::to_le_bytes).collect(); + assert!(bytes.len() <= BIOS_SIZE); + bytes.resize(BIOS_SIZE, 0); + bytes + } +} diff --git a/tests/bios.rs b/tests/bios.rs new file mode 100644 index 0000000..63dbbd7 --- /dev/null +++ b/tests/bios.rs @@ -0,0 +1,651 @@ +use gba_rust::{ + bios::{self, IRQ_FLAGS, IRQ_HANDLER, SYSTEM_STACK, UNSUPPORTED_TRAP}, + cpu::{CpuError, InstructionSet, Mode}, + display::VBLANK_START, + io::{DISPSTAT, IE, IF, IME, POSTFLG, TIMER_BASE}, + machine::{Machine, MachineError, StepKind}, + memory::{Memory, MemoryError, BIOS_SIZE, ROM_START, VRAM_START}, +}; + +const SOURCE: u32 = 0x0200_0000; +const DEST: u32 = 0x0201_0000; +const CALLBACK: u32 = 0x0300_0100; +const ROM_DATA: u32 = ROM_START + 13 * 4; + + +fn words(code: &[u32]) -> Vec { + code.iter().flat_map(|word| word.to_le_bytes()).collect() +} + +fn reach(machine: &mut Machine, pc: u32, max_steps: usize) { + for _ in 0..max_steps { + if machine.cpu().pc() == pc { + return; + } + machine.step().unwrap(); + } + panic!( + "did not reach {pc:#x}; pc={:#x}, cpsr={:#x}", + machine.cpu().pc(), + machine.cpu().cpsr() + ); +} + +/// Boot, load operands and status through CPU instructions, then stop just before SWI. +fn call(service: u8, thumb: bool, operands: [u32; 3]) -> (Machine, u32) { + call_status(service, thumb, operands, 0x1f) +} + +fn call_status(service: u8, thumb: bool, operands: [u32; 3], status: u32) -> (Machine, u32) { + call_status_data(service, thumb, operands, status, &[]) +} + +fn call_status_data( + service: u8, + thumb: bool, + operands: [u32; 3], + status: u32, + data: &[u8], +) -> (Machine, u32) { + let mut code = [ + 0xe59f_0020, // LDR r0,[pc,#32] (offset 40) + 0xe59f_1020, + 0xe59f_2020, + 0xe3a0_30a3, // MOV r3,#0xa3 + 0xe3a0_c05c, // MOV r12,#0x5c + 0xe328_f480, // MSR CPSR_f,#0x80000000 + if thumb { 0xe28f_e001 } else { 0xe1a0_e00f }, // ADD lr,pc,#1 / MOV lr,pc + if thumb { 0xe12f_ff1e } else { 0xe1a0_0000 }, // BX lr / NOP + if thumb { + 0xe7fe_0000 | 0xdf00 | u32::from(service) + } else { + 0xef00_0000 | u32::from(service) << 16 + }, + 0xeaff_fffe, + operands[0], + operands[1], + operands[2], + ]; + if status != 0x1f { + code[3] = 0xe321_f000 | status; + } // MSR CPSR_c,#status + let mut rom = words(&code); + assert_eq!(rom.len() as u32, ROM_DATA - ROM_START); + rom.extend_from_slice(data); + let mut machine = bios::boot(rom).unwrap(); + reach(&mut machine, ROM_START + 32, 100); + assert_eq!( + machine.cpu().instruction_set(), + if thumb { + InstructionSet::Thumb + } else { + InstructionSet::Arm + } + ); + (machine, ROM_START + if thumb { 34 } else { 36 }) +} + +fn finish(machine: &mut Machine, return_pc: u32) { + machine.step().unwrap(); // Execute SWI, not a host service call. + reach(machine, return_pc, 100_000); +} + +/// Original ARM callback: acknowledge IF, record BIOS flags, then return to dispatcher. +fn callback(bus: &mut Memory) { + let code = [ + 0xe280_0c02, // ADD r0,r0,#0x200 (r0=I/O base from dispatcher) + 0xe1d0_10b2, // LDRH r1,[r0,#2] + 0xe1c0_10b2, // STRH r1,[r0,#2] + 0xe3a0_0403, // MOV r0,#0x03000000 + 0xe280_0c7f, // ADD r0,r0,#0x7f00 + 0xe1d0_2fb8, // LDRH r2,[r0,#0xf8] + 0xe182_2001, // ORR r2,r2,r1 + 0xe1c0_2fb8, // STRH r2,[r0,#0xf8] + 0xe12f_ff1e, // BX lr + ]; + for (index, instruction) in code.into_iter().enumerate() { + bus.write32(CALLBACK + index as u32 * 4, instruction) + .unwrap(); + } + bus.write32(IRQ_HANDLER, CALLBACK).unwrap(); +} + +#[test] +fn original_image_is_deterministic_and_boot_enters_arm_system_mode() { + assert_eq!(bios::image().len(), BIOS_SIZE); + assert_eq!(bios::image(), bios::image()); + let mut machine = bios::boot(words(&[0xeaff_fffe])).unwrap(); + assert_eq!(machine.cpu().mode(), Mode::Supervisor); + assert_eq!(machine.cpu().pc(), 0); + reach(&mut machine, ROM_START, 100); + assert_eq!(machine.cpu().mode(), Mode::System); + assert_eq!(machine.cpu().instruction_set(), InstructionSet::Arm); + assert_eq!(machine.cpu().cpsr(), 0x1f); + assert_eq!(machine.cpu().registers()[13], SYSTEM_STACK); + assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 1); + assert_eq!(machine.memory().read32(IRQ_FLAGS).unwrap(), 0); + assert_eq!(machine.memory().read32(IRQ_HANDLER).unwrap(), 0); + assert!(machine.cycles() > 0); +} + +#[test] +fn cpu_set_copies_both_widths_from_arm_and_thumb_and_preserves_caller() { + for thumb in [false, true] { + for word in [false, true] { + let (mut machine, return_pc) = call( + 0x0b, + thumb, + [SOURCE, DEST, 4 | if word { 1 << 26 } else { 0 }], + ); + for index in 0..4 { + machine + .memory_mut() + .write32(SOURCE + index * 4, 0x1234_5678 + index) + .unwrap(); + } + let before = machine.cpu().clone(); + finish(&mut machine, return_pc); + assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]); + assert_eq!(machine.cpu().cpsr(), before.cpsr()); + for offset in 0..if word { 16 } else { 8 } { + assert_eq!( + machine.memory().read8(DEST + offset).unwrap(), + machine.memory().read8(SOURCE + offset).unwrap() + ); + } + } + } +} + +#[test] +fn intr_wait_consumes_preexisting_selected_flags_without_sleep() { + for thumb in [false, true] { + let (mut machine, return_pc) = call(4, thumb, [0, 0x10, 0x1234]); + machine.memory_mut().write16(IRQ_FLAGS, 0x19).unwrap(); + let before = machine.cpu().clone(); + machine.step().unwrap(); + for _ in 0..100 { + if machine.cpu().pc() == return_pc { + break; + } + assert_ne!(machine.step().unwrap(), StepKind::HaltIdle); + } + assert_eq!(machine.cpu().pc(), return_pc); + assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 9); + assert_eq!(machine.memory().read16(IME).unwrap(), 1); + assert_eq!(machine.cpu().cpsr(), before.cpsr()); + assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]); + } +} + +#[test] +fn vblank_wait_halts_dispatches_irq_and_returns_through_original_code() { + let (mut machine, return_pc) = call(5, false, [0x123, 0x456, 0x789]); + callback(machine.memory_mut()); + machine.memory_mut().write16(DISPSTAT, 8).unwrap(); + machine.memory_mut().write16(IE, 1).unwrap(); + let before = machine.cpu().clone(); + machine.step().unwrap(); + let mut slept = false; + let mut irq = 0; + for _ in 0..1000 { + if machine.cpu().pc() == return_pc { + break; + } + match machine.step().unwrap() { + StepKind::HaltIdle => slept = true, + StepKind::IrqEntry => irq += 1, + _ => {} + } + } + assert!(slept); + assert_eq!(irq, 1); + assert_eq!(machine.cpu().pc(), return_pc); + assert_eq!(machine.cpu().cpsr(), before.cpsr()); + assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]); + assert_eq!(machine.memory().read16(IF).unwrap(), 0); + assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 0); + assert_eq!(machine.memory().read16(IME).unwrap(), 1); + assert!(machine.cycles() > u64::from(VBLANK_START)); +} + +#[test] +fn unsupported_services_fail_at_an_explicit_instruction_trap() { + for service in [0, 1, 3, 9, 0x0a, 0x13, 0xff] { + let (mut machine, _) = call(service, false, [0, 0, 0]); + let error = (0..100) + .find_map(|_| machine.step().err()) + .expect("unsupported SWI must fail"); + assert!(matches!( + error, + MachineError::Cpu(CpuError::UnsupportedInstruction { + instruction: UNSUPPORTED_TRAP, + .. + }) + )); + } +} + +#[test] +fn cpu_set_fill_reads_one_value_and_writes_exact_count() { + for thumb in [false, true] { + for word in [false, true] { + let control = 3 | (1 << 24) | if word { 1 << 26 } else { 0 }; + let (mut machine, return_pc) = call(0x0b, thumb, [SOURCE, DEST, control]); + machine.memory_mut().write32(SOURCE, 0xaabb_ccdd).unwrap(); + for offset in 0..5 { + machine + .memory_mut() + .write32(DEST + offset * 4, 0x9999_9999) + .unwrap(); + } + finish(&mut machine, return_pc); + let bytes = if word { 12 } else { 6 }; + for offset in 0..bytes { + assert_eq!( + machine.memory().read8(DEST + offset).unwrap(), + [0xdd, 0xcc, 0xbb, 0xaa][(offset % if word { 4 } else { 2 }) as usize] + ); + } + assert_eq!(machine.memory().read16(DEST + bytes).unwrap(), 0x9999); + } + } +} + +#[test] +fn cpu_fast_set_rounds_up_to_eight_words_for_copy_and_fill() { + for thumb in [false, true] { + for fill in [false, true] { + for count in [1_u32, 7, 8, 9, 15, 16, 17] { + let (mut machine, return_pc) = call( + 0x0c, + thumb, + [SOURCE, DEST, count | if fill { 1 << 24 } else { 0 }], + ); + let rounded = (count + 7) & !7; + for index in 0..rounded + 1 { + machine + .memory_mut() + .write32(SOURCE + index * 4, 0x1000 + index) + .unwrap(); + machine + .memory_mut() + .write32(DEST + index * 4, 0xcccc_cccc) + .unwrap(); + } + let before = machine.cpu().clone(); + finish(&mut machine, return_pc); + for index in 0..rounded { + assert_eq!( + machine.memory().read32(DEST + index * 4).unwrap(), + 0x1000 + if fill { 0 } else { index } + ); + } + assert_eq!( + machine.memory().read32(DEST + rounded * 4).unwrap(), + 0xcccc_cccc + ); + assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]); + assert_eq!(machine.cpu().cpsr(), before.cpsr()); + } + } + } +} + +#[test] +fn fast_copy_reads_each_eight_word_block_before_writing_it() { + let (mut machine, return_pc) = call(0x0c, false, [SOURCE, SOURCE + 4, 16]); + let mut expected: Vec = (0..17).map(|index| 0x1234 + index).collect(); + for (index, value) in expected.iter().enumerate() { + machine + .memory_mut() + .write32(SOURCE + index as u32 * 4, *value) + .unwrap(); + } + for start in [0, 8] { + let block = expected[start..start + 8].to_vec(); + expected[start + 1..start + 9].copy_from_slice(&block); + } + finish(&mut machine, return_pc); + for (index, value) in expected.into_iter().enumerate() { + assert_eq!( + machine.memory().read32(SOURCE + index as u32 * 4).unwrap(), + value + ); + } +} + +#[test] +fn swi_callers_outside_user_and_system_modes_are_rejected() { + for mode in [0x11, 0x12, 0x13, 0x17, 0x1b] { + let (mut machine, _) = call_status(0x0b, false, [SOURCE, DEST, 0], mode); + let error = (0..100).find_map(|_| machine.step().err()).unwrap(); + assert!(matches!( + error, + MachineError::Cpu(CpuError::UnsupportedInstruction { + instruction: UNSUPPORTED_TRAP, + .. + }) + )); + } +} + +#[test] +fn zero_length_copies_do_not_read_source_or_write_destination() { + for service in [0x0b, 0x0c] { + for control in [0, 1 << 24, 1 << 26, (1 << 24) | (1 << 26), 0xfae0_0000] { + let (mut machine, return_pc) = + call(service, false, [0x0e00_0000, 0x0e00_0000, control]); + finish(&mut machine, return_pc); // Both addresses would fail if accessed. + } + } +} + +#[test] +fn copy_count_ignores_reserved_bits_and_fast_set_always_uses_words() { + for service in [0x0b, 0x0c] { + let (mut machine, return_pc) = call(service, false, [SOURCE, DEST, 1 | 0xfae0_0000]); + machine.memory_mut().write32(SOURCE, 0x1234_abcd).unwrap(); + finish(&mut machine, return_pc); + assert_eq!( + machine.memory().read32(DEST).unwrap(), + if service == 0x0b { 0xabcd } else { 0x1234_abcd } + ); + } +} + +#[test] +fn copying_to_video_ram_uses_halfword_and_word_bus_writes() { + for service in [0x0b, 0x0c] { + let (mut machine, return_pc) = call(service, false, [SOURCE, VRAM_START, 3]); + machine.memory_mut().write32(SOURCE, 0x03e0_001f).unwrap(); + machine + .memory_mut() + .write32(SOURCE + 4, 0x7fff_7c00) + .unwrap(); + finish(&mut machine, return_pc); + assert_eq!(machine.memory().read32(VRAM_START).unwrap(), 0x03e0_001f); + assert_eq!(machine.memory().read16(VRAM_START + 4).unwrap(), 0x7c00); + assert_eq!( + machine.memory().read16(VRAM_START + 6).unwrap(), + if service == 0x0b { 0 } else { 0x7fff } + ); + } +} + +#[test] +fn copy_addresses_align_down_and_overlaps_use_instruction_order() { + for word in [false, true] { + let (mut machine, return_pc) = call( + 0x0b, + false, + [SOURCE + 3, DEST + 3, 1 | if word { 1 << 26 } else { 0 }], + ); + machine.memory_mut().write32(SOURCE, 0x1234_abcd).unwrap(); + finish(&mut machine, return_pc); + assert_eq!( + machine.memory().read32(DEST).unwrap(), + if word { 0x1234_abcd } else { 0x1234_0000 } + ); + } + let (mut machine, return_pc) = call(0x0b, false, [SOURCE, SOURCE + 2, 4]); + machine.memory_mut().write32(SOURCE, 0xabcd_1234).unwrap(); + finish(&mut machine, return_pc); + for offset in [0, 2, 4, 6, 8] { + assert_eq!(machine.memory().read16(SOURCE + offset).unwrap(), 0x1234); + } +} + +#[test] +fn protected_or_wrapping_source_ranges_return_without_copying() { + for service in [0x0b, 0x0c] { + for source in [0, 0x3ffc, 0x0100_0000, 0x01ff_fffc, 0xffff_fffc] { + let (mut machine, return_pc) = call(service, false, [source, DEST, 2 | (1 << 26)]); + machine.memory_mut().write32(DEST, 0x1234_5678).unwrap(); + finish(&mut machine, return_pc); + assert_eq!(machine.memory().read32(DEST).unwrap(), 0x1234_5678); + } + } +} + +#[test] +fn copy_failure_keeps_earlier_writes_but_not_partial_instruction_changes() { + let (mut machine, _) = call(0x0b, false, [SOURCE, 0x07ff_fffc, 2 | (1 << 26)]); + machine.memory_mut().write32(SOURCE, 0xaabb_ccdd).unwrap(); + let mut failed = false; + for _ in 0..100 { + let before = machine.cpu().clone(); + let timing = machine.last_timing(); + let cycles = machine.cycles(); + if let Err(error) = machine.step() { + assert_eq!( + error, + MachineError::Cpu(MemoryError::ReadOnly(0x0800_0000).into()) + ); + assert_eq!(machine.cpu(), &before); + assert_eq!(machine.cycles(), cycles); + assert_eq!(machine.last_timing(), timing); + assert_eq!(machine.memory().read32(0x07ff_fffc).unwrap(), 0xaabb_ccdd); + failed = true; + break; + } + } + assert!(failed); +} + +#[test] +fn halt_preserves_registers_and_ime_for_arm_and_thumb_callers() { + for thumb in [false, true] { + let (mut machine, return_pc) = call(2, thumb, [0x1234, 0x5678, 0x9abc]); + machine.memory_mut().write16(IE, 8).unwrap(); + machine + .memory_mut() + .write32(TIMER_BASE, 0x00c0_fc18) + .unwrap(); // IRQ after 1,000 cycles. + let before = machine.cpu().clone(); + machine.step().unwrap(); + let mut slept = false; + for _ in 0..200 { + if machine.cpu().pc() == return_pc { + break; + } + slept |= machine.step().unwrap() == StepKind::HaltIdle; + } + assert!(slept); + assert_eq!(machine.cpu().pc(), return_pc); + assert_eq!(machine.memory().read16(IME).unwrap(), 0); + assert_eq!(machine.memory().read16(IF).unwrap(), 8); + assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]); + assert_eq!(machine.cpu().cpsr(), before.cpsr()); + } +} + +#[test] +fn halt_with_pending_irq_returns_without_an_idle_interval() { + let (mut machine, return_pc) = call(2, false, [1, 2, 3]); + request_timer(machine.memory_mut()); + machine.memory_mut().write16(IE, 8).unwrap(); + machine.step().unwrap(); + for _ in 0..100 { + if machine.cpu().pc() == return_pc { + break; + } + assert_ne!(machine.step().unwrap(), StepKind::HaltIdle); + } + assert_eq!(machine.cpu().pc(), return_pc); +} + +fn request_timer(bus: &mut Memory) { + bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap(); + bus.advance_cycles(1); + bus.write16(TIMER_BASE + 2, 0).unwrap(); +} + +#[test] +fn interrupt_wait_has_no_lost_wake_between_flag_check_and_halt() { + for injection_step in 0..100 { + let (mut machine, return_pc) = call(4, false, [0, 8, 0]); + callback(machine.memory_mut()); + machine.memory_mut().write16(IE, 8).unwrap(); + for step in 0..500 { + if step == injection_step { + request_timer(machine.memory_mut()); + } + if machine.cpu().pc() == return_pc { + break; + } + machine.step().unwrap(); + } + assert_eq!( + machine.cpu().pc(), + return_pc, + "injection step {injection_step}" + ); + assert_eq!(machine.memory().read16(IF).unwrap(), 0); + assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 0); + } +} + +#[test] +fn discard_old_flags_waits_for_a_new_callback_and_retains_other_flags() { + for thumb in [false, true] { + let (mut machine, return_pc) = call(4, thumb, [1, 8, 0]); + callback(machine.memory_mut()); + machine.memory_mut().write16(IE, 8).unwrap(); + machine.memory_mut().write16(IRQ_FLAGS, 9).unwrap(); + for _ in 0..100 { + if machine.halted() { + break; + } + machine.step().unwrap(); + } + assert!(machine.halted()); + assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 1); + request_timer(machine.memory_mut()); + reach(&mut machine, return_pc, 200); + assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 1); + } +} + +#[test] +fn unrelated_irq_does_not_complete_a_wait_for_vblank() { + let (mut machine, return_pc) = call(4, false, [1, 1, 0]); + callback(machine.memory_mut()); + machine.memory_mut().write16(IE, 9).unwrap(); + machine.memory_mut().write16(DISPSTAT, 8).unwrap(); + machine + .memory_mut() + .write32(TIMER_BASE, 0x00c0_fc18) + .unwrap(); + let mut irqs = 0; + for _ in 0..1000 { + if machine.cpu().pc() == return_pc { + break; + } + if machine.step().unwrap() == StepKind::IrqEntry { + irqs += 1; + machine.memory_mut().write16(TIMER_BASE + 2, 0).unwrap(); + } + } + assert_eq!(machine.cpu().pc(), return_pc); + assert_eq!(irqs, 2); + assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 8); + assert!(machine.cycles() > u64::from(VBLANK_START)); +} + +#[test] +fn wait_services_restore_user_system_and_irq_masked_caller_status() { + for thumb in [false, true] { + for status in [0x10, 0x90, 0x9f, 0xdf] { + let (mut machine, return_pc) = call_status(4, thumb, [0, 8, 0], status); + callback(machine.memory_mut()); + machine.memory_mut().write16(IE, 8).unwrap(); + let before = machine.cpu().clone(); + for _ in 0..100 { + if machine.halted() { + break; + } + machine.step().unwrap(); + } + assert!(machine.halted()); + request_timer(machine.memory_mut()); + reach(&mut machine, return_pc, 200); + assert_eq!(machine.cpu().cpsr(), before.cpsr()); + assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]); + assert_eq!(machine.memory().read16(IME).unwrap(), 1); + } + } +} + +#[test] +fn missing_or_misaligned_irq_callback_is_an_explicit_trap() { + for pointer in [0, CALLBACK + 1, CALLBACK + 2] { + let (mut machine, _) = call(5, false, [0, 0, 0]); + machine.memory_mut().write32(IRQ_HANDLER, pointer).unwrap(); + machine.memory_mut().write16(IE, 8).unwrap(); + machine.memory_mut().write16(IME, 1).unwrap(); + request_timer(machine.memory_mut()); + let error = (0..100).find_map(|_| machine.step().err()).unwrap(); + assert!(matches!( + error, + MachineError::Cpu(CpuError::UnsupportedInstruction { + instruction: UNSUPPORTED_TRAP, + .. + }) + )); + } +} + +#[test] +fn repeated_calls_restore_the_supervisor_stack() { + let mut machine = bios::boot(words(&[0xe3a0_2000, 0xef0b_0000, 0xeaff_fffd])).unwrap(); + reach(&mut machine, ROM_START + 4, 100); + for _ in 0..100 { + machine.step().unwrap(); // SWI + assert_eq!(machine.cpu().registers()[13], bios::SVC_STACK); + reach(&mut machine, ROM_START + 8, 100); + assert_eq!(machine.cpu().registers()[13], SYSTEM_STACK); + machine.step().unwrap(); // Branch back to SWI. + assert_eq!(machine.cpu().pc(), ROM_START + 4); + } +} + +#[test] +fn arm_swi_uses_upper_comment_byte_not_low_byte() { + let mut machine = bios::boot(words(&[0xe3a0_2000, 0xef0b_ff03, 0xeaff_fffe])).unwrap(); + reach(&mut machine, ROM_START + 4, 100); + finish(&mut machine, ROM_START + 8); // CpuSet(count=0), not Stop(3). +} + +#[test] +fn callback_must_record_bios_flags_not_only_acknowledge_hardware_if() { + let (mut machine, return_pc) = call(4, false, [0, 8, 0]); + callback(machine.memory_mut()); + machine + .memory_mut() + .write32(CALLBACK + 12, 0xe12f_ff1e) + .unwrap(); // Return after IF acknowledgement, omit flags. + machine.memory_mut().write16(IE, 8).unwrap(); + request_timer(machine.memory_mut()); + for _ in 0..300 { + machine.step().unwrap(); + } + assert_ne!(machine.cpu().pc(), return_pc); + assert!(machine.halted()); + assert_eq!(machine.memory().read16(IF).unwrap(), 0); + assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 0); +} + +#[test] +fn supplying_an_image_is_optional_and_does_not_change_memory_new() { + assert_eq!( + Memory::new(vec![]).unwrap().read32(0), + Err(MemoryError::Unmapped(0)) + ); + let mut bus = Memory::with_bios(vec![], bios::image()).unwrap(); + assert_eq!(bus.write32(0, 1), Err(MemoryError::ReadOnly(0))); + assert_eq!( + bus.read8(BIOS_SIZE as u32), + Err(MemoryError::Unmapped(BIOS_SIZE as u32)) + ); +}