gameboy-rust/crates/gba-core/tests/bios/differential.rs
0xrsydn f8a0676748 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>
2026-10-05 16:09:46 +07:00

411 lines
14 KiB
Rust

use super::*;
use gba_core::{bios::INVALID_ARGUMENT_TRAP, dma::DMA_BASE, memory::OAM_START};
fn filtered(service: u8, samples: &[u16]) -> (Vec<u8>, Vec<u8>) {
let halfword = service == 0x18;
let mask = if halfword { 65535i64 } else { 255 };
let length = samples.len() * if halfword { 2 } else { 1 };
let mut source = ((length as u32) << 8 | if halfword { 0x82 } else { 0x81 })
.to_le_bytes()
.to_vec();
let mut expected = Vec::new();
let mut previous = 0i64;
// Encode differences from original samples, rather than duplicating decoder accumulation.
for &sample in samples {
assert!(i64::from(sample) <= mask);
let difference = ((i64::from(sample) - previous) & mask) as u16;
if halfword {
source.extend(difference.to_le_bytes());
expected.extend(sample.to_le_bytes());
} else {
source.push(difference as u8);
expected.push(sample as u8);
}
previous = i64::from(sample);
}
(source, expected)
}
fn prepare(service: u8, thumb: bool, dest: u32, data: &[u8]) -> (Machine, u32) {
call_status_data(service, thumb, [ROM_DATA, dest, 0x55aa_1234], 0x1f, data)
}
fn complete(m: &mut Machine, pc: u32) {
m.step().unwrap();
reach(m, pc, 4_000_000);
}
fn verify(service: u8, thumb: bool, dest: u32, data: &[u8], expected: &[u8]) {
let (mut m, pc) = prepare(service, thumb, dest, data);
let before = m.cpu().clone();
let end = dest + expected.len() as u32;
for address in [(dest - 1) & !1, end & !1] {
m.memory_mut().write16(address, 0xa55a).unwrap();
}
m.memory_mut()
.write32(bios::IRQ_STACK, 0x5a5a_a5a5)
.unwrap();
complete(&mut m, pc);
for (index, byte) in expected.iter().enumerate() {
assert_eq!(
m.memory().read8(dest + index as u32).unwrap(),
*byte,
"service={service:x}, byte={index}"
);
}
for address in [dest - 1, end] {
assert_eq!(
m.memory().read8(address).unwrap(),
if address & 1 == 0 { 0x5a } else { 0xa5 }
);
}
assert_eq!(m.memory().read32(bios::IRQ_STACK).unwrap(), 0x5a5a_a5a5);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
}
fn fail(m: &mut Machine) -> MachineError {
for _ in 0..2000 {
let before = m.cpu().clone();
let cycles = m.cycles();
let timing = m.last_timing();
if let Err(error) = m.step() {
assert_eq!(m.cpu(), &before);
assert_eq!(m.cycles(), cycles);
assert_eq!(m.last_timing(), timing);
assert_eq!(m.step(), Err(error.clone()));
return error;
}
}
panic!("invalid filter did not produce a bounded diagnostic");
}
fn assert_invalid(error: MachineError) {
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: INVALID_ARGUMENT_TRAP,
..
})
));
}
#[test]
fn first_unit_is_original_and_following_units_are_deltas() {
for service in [0x16, 0x17, 0x18] {
let (data, expected) = filtered(service, &[10, 11, 12, 13, 14, 15]);
if service != 0x18 {
assert_eq!(&data[4..], &[10, 1, 1, 1, 1, 1]);
}
for thumb in [false, true] {
verify(
service,
thumb,
if service == 0x16 {
DEST
} else {
VRAM_START + 4
},
&data,
&expected,
);
}
}
}
#[test]
fn byte_filters_cover_every_value_and_wrap_in_both_directions() {
let mut samples: Vec<_> = (0..=255u16).collect();
samples.extend((0..=255u16).rev());
samples.extend([255, 0, 255, 128, 0, 127, 0, 0]);
for service in [0x16, 0x17] {
let (data, expected) = filtered(service, &samples);
for thumb in [false, true] {
verify(
service,
thumb,
if service == 0x16 {
DEST + 1
} else {
VRAM_START + 4
},
&data,
&expected,
);
}
}
}
#[test]
fn halfword_filter_covers_every_sample_and_uses_byte_length() {
let samples: Vec<_> = (0..=65535u32)
.map(|i| i.wrapping_mul(40503) as u16)
.collect();
let (data, expected) = filtered(0x18, &samples);
assert_eq!(&data[..4], &[0x82, 0, 0, 2]); // 131072 bytes, not 65536 units.
verify(0x18, true, DEST, &data, &expected);
}
#[test]
fn modular_accumulation_survives_full_register_wrap_and_large_byte_lengths() {
for (service, length) in [(0x16, 65536usize), (0x17, 65536), (0x18, 65538 * 2)] {
let mut data = ((length as u32) << 8 | if service == 0x18 { 0x82 } else { 0x81 })
.to_le_bytes()
.to_vec();
data.extend(std::iter::repeat_n(255, length));
let expected: Vec<u8> = if service == 0x18 {
(1..=length / 2)
.flat_map(|i| (((i as u64 * 65535) % 65536) as u16).to_le_bytes())
.collect()
} else {
(1..=length)
.map(|i| ((i as u64 * 255) % 256) as u8)
.collect()
};
verify(service, false, DEST, &data, &expected);
}
}
#[test]
fn seeded_original_samples_round_trip_through_all_filters() {
let mut seed = 0x415c_a927u32;
for case in 0..64 {
for service in [0x16, 0x17, 0x18] {
let samples: Vec<_> = (0..(case + 1) * 2)
.map(|_| {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
(seed >> 16) as u16 & if service == 0x18 { 65535 } else { 255 }
})
.collect();
let (data, expected) = filtered(service, &samples);
verify(service, case % 2 == 0, DEST, &data, &expected);
}
}
}
#[test]
fn smallest_outputs_and_odd_byte_output_use_exact_boundaries() {
for samples in [vec![0x12], vec![0x12, 0x34, 0x56]] {
let (data, expected) = filtered(0x16, &samples);
verify(0x16, true, DEST + 1, &data, &expected);
}
let (data, expected) = filtered(0x17, &[0x12, 0x34]);
verify(0x17, false, DEST, &data, &expected);
let (data, expected) = filtered(0x18, &[0xabcd]);
verify(0x18, true, VRAM_START + 4, &data, &expected);
}
#[test]
fn sources_can_be_read_from_either_work_ram_region() {
for service in [0x16, 0x17, 0x18] {
let (data, expected) = filtered(service, &[0, 255, 128, 1]);
for source in [SOURCE, 0x0300_0100] {
let (mut m, pc) = call(service, false, [source, DEST, 0]);
for (index, byte) in data.iter().enumerate() {
m.memory_mut().write8(source + index as u32, *byte).unwrap();
}
complete(&mut m, pc);
for (index, byte) in expected.iter().enumerate() {
assert_eq!(m.memory().read8(DEST + index as u32).unwrap(), *byte);
}
}
}
}
#[test]
fn caller_status_masks_and_registers_are_restored() {
for service in [0x16, 0x17, 0x18] {
let (data, _) = filtered(service, &[0x12, 0x34, 0xff, 0]);
for thumb in [false, true] {
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
let (mut m, pc) =
call_status_data(service, thumb, [ROM_DATA, DEST, 0xa55a_1234], status, &data);
let before = m.cpu().clone();
complete(&mut m, pc);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
}
}
}
}
#[test]
fn zero_length_reads_no_payload_and_does_not_access_destination() {
for service in [0x16, 0x17, 0x18] {
let (data, _) = filtered(service, &[]);
let (mut m, pc) = prepare(service, false, 0x0e00_0000, &data);
complete(&mut m, pc);
}
}
#[test]
fn all_incorrect_type_or_unit_headers_are_rejected_before_output() {
for service in [0x16, 0x17, 0x18] {
for header in 0u8..=255 {
if header == if service == 0x18 { 0x82 } else { 0x81 } {
continue;
}
let (mut m, _) = prepare(service, false, DEST, &[header, 4, 0, 0]);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
}
}
#[test]
fn protected_sources_alignment_odd_lengths_and_output_wrap_are_diagnostics() {
for service in [0x16, 0x17, 0x18] {
let (data, _) = filtered(service, &[0x12, 0x34]);
for source in [
0,
0x3ffc,
0x0100_0000,
ROM_DATA + 1,
ROM_DATA + 2,
ROM_DATA + 3,
] {
let (mut m, _) = call_status_data(service, false, [source, DEST, 0], 0x1f, &data);
assert_invalid(fail(&mut m));
}
let (mut m, _) = prepare(service, false, 0xffff_fffe, &data);
assert_invalid(fail(&mut m));
if service == 0x16 {
continue;
}
let (mut m, _) = prepare(service, false, DEST + 1, &data);
assert_invalid(fail(&mut m));
let mut odd = data.clone();
odd[1] = 3;
let (mut m, _) = prepare(service, true, DEST, &odd);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
}
#[test]
fn every_truncated_source_prefix_returns_a_memory_diagnostic() {
for service in [0x16, 0x17, 0x18] {
let (data, _) = filtered(service, &[0x12, 0x34, 0x56, 0x78]);
for length in 0..data.len() {
let (mut m, _) = prepare(service, false, DEST, &data[..length]);
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::Unmapped(ROM_DATA + length as u32).into()),
"service={service:x}, length={length}"
);
}
}
}
#[test]
fn failed_source_reads_keep_completed_units_but_not_pending_halfwords() {
for service in [0x16, 0x17, 0x18] {
let (mut data, _) = filtered(service, &[0x12, 0x34, 0x56, 0x78]);
// Three byte samples or one complete halfword plus one byte are available.
data.truncate(7);
let (mut m, _) = prepare(service, false, DEST, &data);
m.memory_mut().write32(DEST, 0xcccc_cccc).unwrap();
assert!(matches!(
fail(&mut m),
MachineError::Cpu(CpuError::Memory(_))
));
assert_eq!(
m.memory().read32(DEST).unwrap(),
match service {
0x16 => 0xcc56_3412,
0x17 => 0xcccc_3412,
_ => 0xcccc_0012,
}
);
}
}
#[test]
fn destination_failure_preserves_preceding_completed_halfword() {
for service in [0x17, 0x18] {
let (data, _) = filtered(service, &[0x12, 0x34, 0x56, 0x78]);
let (mut m, _) = prepare(service, false, 0x07ff_fffe, &data);
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into())
);
assert_eq!(
m.memory().read16(0x07ff_fffe).unwrap(),
if service == 0x17 { 0x3412 } else { 0x12 }
);
}
}
#[test]
fn output_uses_normal_byte_and_halfword_video_bus_rules() {
for service in [0x16, 0x17, 0x18] {
let (data, _) = filtered(
service,
if service == 0x18 {
&[0x3412]
} else {
&[0x12, 0x34]
},
);
for dest in [VRAM_START, OAM_START] {
let (mut m, pc) = prepare(service, false, dest, &data);
m.memory_mut().write16(dest, 0xcccc).unwrap();
complete(&mut m, pc);
assert_eq!(
m.memory().read16(dest).unwrap(),
if service != 0x16 {
0x3412
} else if dest == OAM_START {
0xcccc
} else {
0x3434
}
);
}
}
}
#[test]
fn declared_end_does_not_consume_trailing_payload() {
for service in [0x16, 0x17, 0x18] {
let (mut data, expected) = filtered(service, &[0x12, 0x34]);
data.extend([0xff; 8]);
verify(service, false, DEST, &data, &expected);
}
}
#[test]
fn dma_and_timers_continue_while_filter_masks_cpu_irq_delivery() {
for service in [0x16, 0x17, 0x18] {
let (data, expected) = filtered(service, &[0x55; 32]);
let (mut m, pc) = prepare(service, false, DEST, &data);
m.step().unwrap(); // Enter SWI before requesting IRQs.
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..5000 {
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);
for (index, byte) in expected.iter().enumerate() {
assert_eq!(m.memory().read8(DEST + index as u32).unwrap(), *byte);
}
}
}