feat(bios): implement division and integer square root

Add Div, DivArm, and Sqrt with emitted ARM algorithms and independent arithmetic references.
This commit is contained in:
0xrsydn 2026-10-05 12:55:35 +07:00
commit 096bcfe314
4 changed files with 256 additions and 0 deletions

View file

@ -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<u8> {
(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<u8> {
["copy_halfword", "half_loop", "half_fill", "half_fill_loop"],
);
arithmetic::emit(&mut a);
a.label("return");
a.emit(0xe10f_3000); // MRS r3,CPSR

57
src/bios/arithmetic.rs Normal file
View file

@ -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");
}

View file

@ -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<u8> {
code.iter().flat_map(|word| word.to_le_bytes()).collect()

191
tests/bios/arithmetic.rs Normal file
View file

@ -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.
}