Add SWI 0x09 and 0x0a as original ARM routines. Evaluate the fixed-point angle polynomial with wrapped products, reduce ArcTan2 ratios with integer division, and handle axes and quadrants. Require sign-extended 16-bit inputs and preserve non-result caller registers. Add 15 regression tests for fixed-point and floating-point references, signed limits, rounding, quadrant boundaries, caller flags, stack bounds, repeated calls, and DMA/IRQ progress. Validate 671 default and 660 core-only tests in debug and release, plus all 10 native Darwin ARM64 window smoke tests.
377 lines
12 KiB
Rust
377 lines
12 KiB
Rust
use super::*;
|
|
use gba_rust::{bios::INVALID_ARGUMENT_TRAP, dma::DMA_BASE};
|
|
|
|
/// Wide-integer reference for signed low-word multiplication and floor division.
|
|
/// This avoids both host signed overflow and the emitted instruction sequence.
|
|
fn low_signed(value: i64) -> i64 {
|
|
let bits = value.rem_euclid(1i64 << 32);
|
|
if bits >= 1i64 << 31 {
|
|
bits - (1i64 << 32)
|
|
} else {
|
|
bits
|
|
}
|
|
}
|
|
|
|
fn atan_reference(tangent: i32) -> i32 {
|
|
let a = -low_signed(i64::from(tangent).pow(2)).div_euclid(16384);
|
|
let mut polynomial = 0xa9i64;
|
|
for coefficient in [0x390, 0x91c, 0xfb6, 0x16aa, 0x2081, 0x3651, 0xa2f9] {
|
|
polynomial = low_signed(low_signed(polynomial * a).div_euclid(16384) + coefficient);
|
|
}
|
|
low_signed(i64::from(tangent) * polynomial).div_euclid(65536) as i32
|
|
}
|
|
|
|
/// Eight-sector signed-coordinate reference; production uses absolute magnitudes.
|
|
fn atan2_reference(x: i32, y: i32) -> u32 {
|
|
if y == 0 {
|
|
return if x < 0 { 0x8000 } else { 0 };
|
|
}
|
|
if x == 0 {
|
|
return if y < 0 { 0xc000 } else { 0x4000 };
|
|
}
|
|
let ratio = |n: i32, d: i32| atan_reference((i64::from(n) * 16384 / i64::from(d)) as i32);
|
|
let angle = if y > 0 {
|
|
if x > 0 && x >= y {
|
|
ratio(y, x)
|
|
} else if x < 0 && -x >= y {
|
|
0x8000 + ratio(y, x)
|
|
} else {
|
|
0x4000 - ratio(x, y)
|
|
}
|
|
} else if x < 0 && -x > -y {
|
|
0x8000 + ratio(y, x)
|
|
} else if x > 0 && x >= -y {
|
|
0x10000 + ratio(y, x)
|
|
} else {
|
|
0xc000 - ratio(x, y)
|
|
};
|
|
(angle & 0xffff) as u32
|
|
}
|
|
|
|
fn run(service: u8, x: i32, y: i32, thumb: bool) -> u32 {
|
|
let (mut m, pc) = call(service, thumb, [x as u32, y as u32, 0xcafe_babe]);
|
|
let before = m.cpu().clone();
|
|
let guard = bios::SVC_STACK - if service == 9 { 32 } else { 44 };
|
|
m.memory_mut().write32(guard, 0xa55a_1234).unwrap();
|
|
finish(&mut m, pc);
|
|
assert_eq!(&m.cpu().registers()[1..15], &before.registers()[1..15]);
|
|
assert_eq!(m.cpu().cpsr(), before.cpsr());
|
|
assert_eq!(m.memory().read32(guard).unwrap(), 0xa55a_1234);
|
|
let actual = m.cpu().registers()[0];
|
|
let expected = if service == 9 {
|
|
atan_reference(x) as u32
|
|
} else {
|
|
atan2_reference(x, y)
|
|
};
|
|
assert_eq!(actual, expected, "service={service}, x={x}, y={y}");
|
|
actual
|
|
}
|
|
|
|
fn check_direction(x: i32, y: i32, thumb: bool) {
|
|
let actual = run(0x0a, x, y, thumb);
|
|
if x == 0 && y == 0 {
|
|
return;
|
|
}
|
|
let ideal = f64::from(y).atan2(f64::from(x)) * 32768.0 / std::f64::consts::PI;
|
|
let error = (f64::from(actual) - ideal + 32768.0).rem_euclid(65536.0) - 32768.0;
|
|
assert!(
|
|
error.abs() < 3.0,
|
|
"({x},{y}) actual={actual} ideal={ideal} error={error}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn arctan_known_fixed_point_values_include_signed_results() {
|
|
for (input, expected) in [
|
|
(-16384, -8192),
|
|
(-8192, -4836),
|
|
(-1, -1),
|
|
(0, 0),
|
|
(1, 0),
|
|
(8192, 4836),
|
|
(16384, 8192),
|
|
] {
|
|
for thumb in [false, true] {
|
|
assert_eq!(run(9, input, 0x12345678, thumb) as i32, expected);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn arctan_dense_unit_interval_grid_matches_polynomial_and_real_angle() {
|
|
for input in (-16384..=16384).step_by(16) {
|
|
let actual = run(9, input, 0, input & 16 != 0) as i32;
|
|
let ideal = (f64::from(input) / 16384.0).atan() * 32768.0 / std::f64::consts::PI;
|
|
assert!((f64::from(actual) - ideal).abs() < 2.0, "input={input}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn arctan_full_signed_domain_uses_wrapped_products_not_host_float() {
|
|
for input in (-32768..=32767).step_by(257) {
|
|
run(9, input, 0, input & 1 != 0);
|
|
}
|
|
// Known out-of-unit-range accuracy problems are retained, not corrected with host atan.
|
|
assert_eq!(run(9, -32768, 0, false) as i32, -5795);
|
|
assert_eq!(run(9, 32767, 0, true), 5848);
|
|
}
|
|
|
|
#[test]
|
|
fn arctan_seeded_inputs_match_wide_integer_reference() {
|
|
let mut seed = 0x9a2b_14cdu32;
|
|
for index in 0..1024 {
|
|
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
|
|
run(9, i32::from((seed >> 16) as i16), 0, index % 2 == 0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn negative_arctan_rounds_down_instead_of_truncating_toward_zero() {
|
|
for input in [1, 2, 3, 127, 8191, 8192, 16383, 16384] {
|
|
let positive = run(9, input, 0, false) as i32;
|
|
let negative = run(9, -input, 0, true) as i32;
|
|
assert!([0, -1].contains(&(positive + negative)));
|
|
}
|
|
assert_eq!(run(9, -1, 0, true), u32::MAX);
|
|
}
|
|
|
|
#[test]
|
|
fn arctan2_axes_origin_and_diagonals_have_exact_turn_angles() {
|
|
for thumb in [false, true] {
|
|
for (x, y, expected) in [
|
|
(0, 0, 0),
|
|
(1, 0, 0),
|
|
(-32768, 0, 0x8000),
|
|
(0, 32767, 0x4000),
|
|
(0, -32768, 0xc000),
|
|
(1, 1, 0x2000),
|
|
(-1, 1, 0x6000),
|
|
(-1, -1, 0xa000),
|
|
(1, -1, 0xe000),
|
|
(-32768, -32768, 0xa000),
|
|
(32767, 32767, 0x2000),
|
|
] {
|
|
assert_eq!(run(0x0a, x, y, thumb), expected);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn arctan2_all_quadrants_and_signed_extremes_match_both_references() {
|
|
let values = [
|
|
-32768, -32767, -16384, -1024, -2, -1, 0, 1, 2, 1024, 16384, 32766, 32767,
|
|
];
|
|
for x in values {
|
|
for y in values {
|
|
check_direction(x, y, (x ^ y) & 1 != 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn arctan2_sector_boundaries_handle_equal_and_adjacent_magnitudes() {
|
|
for magnitude in [1, 2, 3, 127, 1024, 16384, 32766] {
|
|
for delta in [-1, 0, 1] {
|
|
for sx in [-1, 1] {
|
|
for sy in [-1, 1] {
|
|
check_direction(sx * magnitude, sy * (magnitude + delta), delta != 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn arctan2_seeded_signed_coordinates_match_integer_and_float_references() {
|
|
let mut seed = 0x5017_e3b9u32;
|
|
for index in 0..1024 {
|
|
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
|
|
let x = i32::from((seed >> 16) as i16);
|
|
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
|
|
let y = i32::from((seed >> 16) as i16);
|
|
check_direction(x, y, index % 2 != 0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn arctan2_preserves_direction_when_coordinates_scale_equally() {
|
|
for (x, y) in [
|
|
(3, 7),
|
|
(7, 3),
|
|
(-3, 7),
|
|
(-7, 3),
|
|
(-3, -7),
|
|
(-7, -3),
|
|
(3, -7),
|
|
(7, -3),
|
|
] {
|
|
let expected = run(0x0a, x, y, false);
|
|
for scale in [2, 7, 31, 4096] {
|
|
assert_eq!(run(0x0a, x * scale, y * scale, true), expected);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn user_system_modes_masks_and_all_nonresult_registers_are_preserved() {
|
|
for service in [9, 0x0a] {
|
|
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
|
|
for thumb in [false, true] {
|
|
let (mut m, pc) = call_status(
|
|
service,
|
|
thumb,
|
|
[(-1234i32) as u32, 2345, 0xabcd_ef01],
|
|
status,
|
|
);
|
|
let before = m.cpu().clone();
|
|
finish(&mut m, pc);
|
|
assert_eq!(&m.cpu().registers()[1..15], &before.registers()[1..15]);
|
|
assert_eq!(m.cpu().cpsr(), before.cpsr());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn every_caller_flag_combination_survives_angle_calculation() {
|
|
for service in [9, 0x0au32] {
|
|
for flags in 0..16u32 {
|
|
let mut m = bios::boot(words(&[
|
|
0xe3a0_0a02, // MOV r0,#0x2000
|
|
0xe3a0_1901, // MOV r1,#0x4000
|
|
0xe328_f200 | flags, // MSR CPSR_f,#flags<<28
|
|
0xef00_0000 | service << 16,
|
|
0xeaff_fffe,
|
|
]))
|
|
.unwrap();
|
|
reach(&mut m, ROM_START + 12, 100);
|
|
assert_eq!(m.cpu().cpsr() >> 28, flags);
|
|
let before = m.cpu().clone();
|
|
finish(&mut m, ROM_START + 16);
|
|
assert_eq!(m.cpu().cpsr(), before.cpsr());
|
|
assert_eq!(&m.cpu().registers()[1..15], &before.registers()[1..15]);
|
|
assert_eq!(
|
|
m.cpu().registers()[0],
|
|
if service == 9 {
|
|
atan_reference(8192) as u32
|
|
} else {
|
|
atan2_reference(8192, 16384)
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn noncanonical_signed_sixteen_bit_arguments_are_repeatable_diagnostics() {
|
|
for value in [
|
|
0x8000,
|
|
0xffff,
|
|
0x10000,
|
|
0xffff_7fff,
|
|
0x7fff_ffff,
|
|
0x8000_0000,
|
|
] {
|
|
for (service, args) in [
|
|
(9, [value, 0, 0]),
|
|
(0x0a, [value, 1, 0]),
|
|
(0x0a, [1, value, 0]),
|
|
] {
|
|
let (mut m, _) = call(service, false, args);
|
|
let mut failed = false;
|
|
for _ in 0..150 {
|
|
let before = m.cpu().clone();
|
|
let cycles = m.cycles();
|
|
let timing = m.last_timing();
|
|
if let Err(error) = m.step() {
|
|
assert!(matches!(
|
|
error,
|
|
MachineError::Cpu(CpuError::UnsupportedInstruction {
|
|
instruction: INVALID_ARGUMENT_TRAP,
|
|
..
|
|
})
|
|
));
|
|
assert_eq!(m.cpu(), &before);
|
|
assert_eq!(m.cycles(), cycles);
|
|
assert_eq!(m.last_timing(), timing);
|
|
assert_eq!(m.step(), Err(error));
|
|
failed = true;
|
|
break;
|
|
}
|
|
}
|
|
assert!(failed);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn repeated_calls_restore_supervisor_stack_and_do_not_retain_ratio_state() {
|
|
for service in [9u32, 0x0a] {
|
|
let mut m = bios::boot(words(&[
|
|
0xe3a0_0a02, // MOV r0,#0x2000
|
|
0xe3a0_1901, // MOV r1,#0x4000
|
|
0xef00_0000 | service << 16,
|
|
0xeaff_fffb, // B ROM_START
|
|
]))
|
|
.unwrap();
|
|
reach(&mut m, ROM_START, 100);
|
|
for _ in 0..100 {
|
|
reach(&mut m, ROM_START + 8, 10);
|
|
m.step().unwrap();
|
|
assert_eq!(m.cpu().registers()[13], bios::SVC_STACK);
|
|
reach(&mut m, ROM_START + 12, 1000);
|
|
assert_eq!(m.cpu().registers()[13], SYSTEM_STACK);
|
|
assert_eq!(
|
|
m.cpu().registers()[0],
|
|
if service == 9 {
|
|
atan_reference(8192) as u32
|
|
} else {
|
|
atan2_reference(8192, 16384)
|
|
}
|
|
);
|
|
m.step().unwrap();
|
|
assert_eq!(m.cpu().pc(), ROM_START);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn devices_and_dma_progress_and_pending_irq_waits_until_return() {
|
|
for service in [9, 0x0a] {
|
|
let (mut m, pc) = call(service, false, [8192, 16384, 0]);
|
|
m.step().unwrap(); // Enter SWI before enabling IRQ sources.
|
|
m.memory_mut().write16(IE, 8).unwrap();
|
|
m.memory_mut().write16(IME, 1).unwrap();
|
|
m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap();
|
|
m.memory_mut().write32(SOURCE, 0x8765_4321).unwrap();
|
|
m.memory_mut().write32(DMA_BASE, SOURCE).unwrap();
|
|
m.memory_mut().write32(DMA_BASE + 4, SOURCE + 4).unwrap();
|
|
m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap();
|
|
let mut dma = 0;
|
|
for _ in 0..1000 {
|
|
if m.cpu().pc() == pc {
|
|
break;
|
|
}
|
|
let step = m.step().unwrap();
|
|
assert_ne!(step, StepKind::IrqEntry);
|
|
if matches!(step, StepKind::Dma { channel: 0 }) {
|
|
dma += 1;
|
|
}
|
|
}
|
|
assert_eq!(m.cpu().pc(), pc);
|
|
assert_eq!(dma, 1);
|
|
assert_eq!(m.memory().read32(SOURCE + 4).unwrap(), 0x8765_4321);
|
|
assert_eq!(m.memory().read16(IF).unwrap() & 8, 8);
|
|
assert_eq!(m.memory().read16(IME).unwrap(), 1);
|
|
assert_eq!(m.memory().read16(IRQ_FLAGS).unwrap(), 0);
|
|
assert_eq!(
|
|
m.cpu().registers()[0],
|
|
if service == 9 {
|
|
atan_reference(8192) as u32
|
|
} else {
|
|
atan2_reference(8192, 16384)
|
|
}
|
|
);
|
|
assert_eq!(m.step().unwrap(), StepKind::IrqEntry);
|
|
}
|
|
}
|