Add SWI 0x16, 0x17, and 0x18 as original ARM routines. Reconstruct samples with modular addition and byte or halfword output. Validate headers, alignment, lengths, and address ranges; preserve earlier completed writes on failure. Add 17 regression tests for sample round trips, wraparound, large byte lengths, caller state, partial failures, bus behavior, and DMA/IRQ progress. Validate 635 default and 624 core-only tests in debug and release, plus all 10 native Darwin ARM64 window smoke tests.
411 lines
14 KiB
Rust
411 lines
14 KiB
Rust
use super::*;
|
|
use gba_rust::{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);
|
|
}
|
|
}
|
|
}
|