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
377
crates/gba-core/tests/bios/angles.rs
Normal file
377
crates/gba-core/tests/bios/angles.rs
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
use super::*;
|
||||
use gba_core::{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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue