From 096bcfe314f34920256713d79e0fe605378b209f Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Mon, 5 Oct 2026 12:55:35 +0700 Subject: [PATCH] feat(bios): implement division and integer square root Add Div, DivArm, and Sqrt with emitted ARM algorithms and independent arithmetic references. --- src/bios.rs | 6 ++ src/bios/arithmetic.rs | 57 ++++++++++++ tests/bios.rs | 2 + tests/bios/arithmetic.rs | 191 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+) create mode 100644 src/bios/arithmetic.rs create mode 100644 tests/bios/arithmetic.rs diff --git a/src/bios.rs b/src/bios.rs index 03b4725..c5b5fda 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -1,9 +1,11 @@ //! Original, optional ARM BIOS replacement. No Nintendo firmware is included. //! Supports interrupt waits and memory copy/fill. +//! Includes integer arithmetic services. //! This is a functional subset, not a complete boot ROM or a timing-compatible BIOS. use std::collections::BTreeMap; +mod arithmetic; use crate::{ cpu::Cpu, @@ -110,6 +112,9 @@ pub fn image() -> Vec { (2, "halt"), (4, "intr_wait"), (5, "vblank_wait"), + (6, "div"), + (7, "div_arm"), + (8, "sqrt"), (0x0b, "cpu_set"), (0x0c, "cpu_fast_set"), ] { @@ -203,6 +208,7 @@ pub fn image() -> Vec { ["copy_halfword", "half_loop", "half_fill", "half_fill_loop"], ); + arithmetic::emit(&mut a); a.label("return"); a.emit(0xe10f_3000); // MRS r3,CPSR diff --git a/src/bios/arithmetic.rs b/src/bios/arithmetic.rs new file mode 100644 index 0000000..f3d350c --- /dev/null +++ b/src/bios/arithmetic.rs @@ -0,0 +1,57 @@ +//! Original integer algorithms emitted as ARM instructions, not host calculations. + +use super::ArmImage; + +pub(super) fn emit(a: &mut ArmImage) { + a.label("div_arm"); + a.emit(0xe1a0_2000); // MOV r2,r0 + a.emit(0xe1a0_0001); // MOV r0,r1 + a.emit(0xe1a0_1002); // MOV r1,r2 (exchange input operands) + a.label("div"); + a.emit(0xe351_0000); // CMP r1,#0 + a.branch(0, "invalid_argument"); // Diagnostic instead of firmware's divide-by-zero hang. + a.emit(0xe92d_0070); // STMDB sp!,{r4-r6} (40 bytes total SVC stack use) + a.emit(0xe1a0_4fc0); // MOV r4,r0,ASR #31 (numerator sign) + a.emit(0xe020_5001); // EOR r5,r0,r1 + a.emit(0xe1a0_5fc5); // MOV r5,r5,ASR #31 (quotient sign) + a.emit(0xe350_0000); // CMP r0,#0 + a.emit(0x4260_0000); // RSBMI r0,r0,#0 (unsigned magnitude, including INT_MIN) + a.emit(0xe351_0000); // CMP r1,#0 + a.emit(0x4261_1000); // RSBMI r1,r1,#0 + a.emit(0xe3a0_2000); // MOV r2,#0 (quotient magnitude) + a.emit(0xe3a0_3000); // MOV r3,#0 (remainder magnitude) + a.emit(0xe3a0_6020); // MOV r6,#32 + a.label("div_bit"); + a.emit(0xe1b0_0080); // MOVS r0,r0,LSL #1 (next numerator bit into C) + a.emit(0xe0a3_3003); // ADC r3,r3,r3 (remainder * 2 + bit) + a.emit(0xe153_0001); // CMP r3,r1 (unsigned) + a.emit(0x2043_3001); // SUBCS r3,r3,r1 (preserve comparison carry) + a.emit(0xe0a2_2002); // ADC r2,r2,r2 (quotient * 2 + comparison carry) + a.emit(0xe256_6001); // SUBS r6,r6,#1 + a.branch(1, "div_bit"); + // Patch only the defined outputs in the dispatcher's saved register frame. + a.emit(0xe58d_201c); // STR r2,[sp,#28] (saved r3 = unsigned quotient magnitude) + a.emit(0xe355_0000); // CMP r5,#0 + a.emit(0x1262_2000); // RSBNE r2,r2,#0 (signed quotient) + a.emit(0xe354_0000); // CMP r4,#0 + a.emit(0x1263_3000); // RSBNE r3,r3,#0 (remainder follows numerator sign) + a.emit(0xe58d_2010); // STR r2,[sp,#16] (saved r0) + a.emit(0xe58d_3014); // STR r3,[sp,#20] (saved r1) + a.emit(0xe8bd_0070); // LDMIA sp!,{r4-r6} + a.branch(14, "return"); + + // Restoring base-four square root: exactly 16 iterations for any u32 input. + a.label("sqrt"); + a.emit(0xe3a0_1000); // MOV r1,#0 (root) + a.emit(0xe3a0_2101); // MOV r2,#0x40000000 (highest power of four) + a.label("sqrt_bit"); + a.emit(0xe081_3002); // ADD r3,r1,r2 (trial) + a.emit(0xe150_0003); // CMP r0,r3 + a.emit(0x2040_0003); // SUBCS r0,r0,r3 + a.emit(0xe1a0_10a1); // MOV r1,r1,LSR #1 (comparison carry unchanged) + a.emit(0x2081_1002); // ADDCS r1,r1,r2 + a.emit(0xe1b0_2122); // MOVS r2,r2,LSR #2 + a.branch(1, "sqrt_bit"); + a.emit(0xe58d_1004); // STR r1,[sp,#4] (saved r0 = floor(sqrt(input))) + a.branch(14, "return"); +} diff --git a/tests/bios.rs b/tests/bios.rs index 63dbbd7..bbf4af5 100644 --- a/tests/bios.rs +++ b/tests/bios.rs @@ -12,6 +12,8 @@ const DEST: u32 = 0x0201_0000; const CALLBACK: u32 = 0x0300_0100; const ROM_DATA: u32 = ROM_START + 13 * 4; +#[path = "bios/arithmetic.rs"] +mod arithmetic; fn words(code: &[u32]) -> Vec { code.iter().flat_map(|word| word.to_le_bytes()).collect() diff --git a/tests/bios/arithmetic.rs b/tests/bios/arithmetic.rs new file mode 100644 index 0000000..65bcb91 --- /dev/null +++ b/tests/bios/arithmetic.rs @@ -0,0 +1,191 @@ +use super::*; +use gba_rust::bios::INVALID_ARGUMENT_TRAP; + +fn divide(number: i32, denominator: i32, thumb: bool, service: u8) { + let inputs = if service == 6 { + [number as u32, denominator as u32] + } else { + [denominator as u32, number as u32] + }; + let (mut machine, return_pc) = call(service, thumb, [inputs[0], inputs[1], 0xcafe_babe]); + let before = machine.cpu().clone(); + finish(&mut machine, return_pc); + // Widening avoids host overflow for INT_MIN / -1. + let quotient = i64::from(number) / i64::from(denominator); + let remainder = i64::from(number) % i64::from(denominator); + assert_eq!( + machine.cpu().registers()[0], + quotient as u32, + "{number}/{denominator}" + ); + assert_eq!(machine.cpu().registers()[1], remainder as u32); + assert_eq!(machine.cpu().registers()[3], quotient.unsigned_abs() as u32); + assert_eq!(machine.cpu().registers()[2], before.registers()[2]); + assert_eq!( + &machine.cpu().registers()[4..15], + &before.registers()[4..15] + ); + assert_eq!(machine.cpu().cpsr(), before.cpsr()); +} + +#[test] +fn division_handles_signs_zero_numerator_and_signed_boundaries() { + for service in [6, 7] { + for thumb in [false, true] { + for number in [ + i32::MIN, + i32::MIN + 1, + -1234, + -17, + -1, + 0, + 1, + 17, + 1234, + i32::MAX, + ] { + for denominator in [i32::MIN, -123, -10, -1, 1, 10, 123, i32::MAX] { + divide(number, denominator, thumb, service); + } + } + } + } +} + +#[test] +fn division_matches_wide_integer_reference_for_seeded_inputs() { + let mut state = 0x1a2b_3c4d_u32; + for index in 0..1024 { + state = state.wrapping_mul(1664525).wrapping_add(1013904223); + let number = state as i32; + state = state.wrapping_mul(1664525).wrapping_add(1013904223); + let denominator = (state | 1) as i32; + divide( + number, + denominator, + index & 1 != 0, + if index & 2 == 0 { 6 } else { 7 }, + ); + } +} + +#[test] +fn division_by_zero_is_a_repeatable_diagnostic_not_an_endless_loop() { + for service in [6, 7] { + for thumb in [false, true] { + for number in [0, 1, u32::MAX, 0x8000_0000] { + let operands = if service == 6 { + [number, 0, 0] + } else { + [0, number, 0] + }; + let (mut machine, _) = call(service, thumb, operands); + let error = (0..100) + .find_map(|_| machine.step().err()) + .expect("bounded division diagnostic"); + assert!(matches!( + error, + MachineError::Cpu(CpuError::UnsupportedInstruction { + instruction: INVALID_ARGUMENT_TRAP, + .. + }) + )); + let before = machine.cpu().clone(); + let cycles = machine.cycles(); + let timing = machine.last_timing(); + assert_eq!(machine.step(), Err(error)); + assert_eq!(machine.cpu(), &before); + assert_eq!(machine.cycles(), cycles); + assert_eq!(machine.last_timing(), timing); + } + } + } +} + +fn square_root(value: u32, thumb: bool) { + let (mut machine, return_pc) = call(8, thumb, [value, 0x1234_5678, 0xabcd_ef01]); + let before = machine.cpu().clone(); + finish(&mut machine, return_pc); + let root = u64::from(machine.cpu().registers()[0]); + assert!(root * root <= u64::from(value)); + assert!((root + 1) * (root + 1) > u64::from(value)); + assert!(root <= 0xffff); + assert_eq!( + &machine.cpu().registers()[1..15], + &before.registers()[1..15] + ); + assert_eq!(machine.cpu().cpsr(), before.cpsr()); +} + +#[test] +fn sqrt_handles_perfect_squares_adjacent_values_and_u32_maximum() { + for thumb in [false, true] { + for root in [0_u32, 1, 2, 3, 255, 256, 32767, 32768, 65534, 65535] { + let square = root * root; + if square != 0 { + square_root(square - 1, thumb); + } + square_root(square, thumb); + square_root(square + 1, thumb); + } + square_root(u32::MAX, thumb); + } +} + +#[test] +fn sqrt_result_satisfies_integer_bounds_for_seeded_inputs() { + let mut state = 0x9876_5432_u32; + for index in 0..512 { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + square_root(state, index & 1 != 0); + } +} + +#[test] +fn arithmetic_services_preserve_user_mode_and_interrupt_masks() { + for service in [6, 7, 8] { + for status in [0x10, 0x90, 0x9f, 0xdf] { + for thumb in [false, true] { + let (mut machine, return_pc) = + call_status(service, thumb, [144, 12, 0x55aa], status); + let before = machine.cpu().clone(); + finish(&mut machine, return_pc); + assert_eq!(machine.cpu().cpsr(), before.cpsr()); + assert_eq!(machine.cpu().registers()[2], before.registers()[2]); + assert_eq!( + &machine.cpu().registers()[4..15], + &before.registers()[4..15] + ); + } + } + } +} + +#[test] +fn arithmetic_executes_real_instructions_and_keeps_device_clocks_running() { + let (mut machine, return_pc) = call(6, false, [1234, 10, 0]); + machine.memory_mut().write16(IE, 8).unwrap(); + machine.memory_mut().write16(IME, 1).unwrap(); + machine + .memory_mut() + .write32(TIMER_BASE, 0x00c0_fff0) + .unwrap(); + let start = machine.cycles(); + let mut steps = 0; + loop { + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + steps += 1; + if machine.cpu().pc() == return_pc { + break; + } + assert!(steps < 1000); + } + assert!(steps >= 32 * 7); + assert!(machine.cycles() > start + 16); + assert_eq!(machine.memory().read16(IF).unwrap(), 8); + assert_eq!(machine.cpu().registers()[0], 123); + assert_eq!(machine.cpu().registers()[1], 4); + assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); // Delivery deferred until service return. +}