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:
0xrsydn 2026-10-05 16:07:32 +07:00
commit f8a0676748
113 changed files with 304 additions and 282 deletions

View file

@ -0,0 +1,684 @@
use super::*;
use gba_core::{
bios::INVALID_ARGUMENT_TRAP,
dma::DMA_BASE,
io::{BG2PA, DISPCNT},
memory::{OAM_START as OAM, PALETTE_START as PAL},
video::{rgb555_to_rgb888 as rgb, Framebuffer},
};
const DATA: u32 = ROM_START + 23 * 4;
#[derive(Clone, Copy)]
struct Bg {
texture: [i32; 2],
screen: [i16; 2],
scale: [i16; 2],
angle: u16,
}
#[derive(Clone, Copy)]
struct Obj {
scale: [i16; 2],
angle: u16,
}
fn bg_bytes(records: &[Bg]) -> Vec<u8> {
records
.iter()
.flat_map(|r| {
let mut bytes = Vec::new();
for v in r.texture {
bytes.extend(v.to_le_bytes());
}
for v in r.screen.into_iter().chain(r.scale) {
bytes.extend(v.to_le_bytes());
}
bytes.extend(r.angle.to_le_bytes());
bytes.extend([0xa5, 0x5a]); // Ignored structure padding.
bytes
})
.collect()
}
fn obj_bytes(records: &[Obj]) -> Vec<u8> {
records
.iter()
.flat_map(|r| {
let mut bytes = Vec::new();
for v in r.scale {
bytes.extend(v.to_le_bytes());
}
bytes.extend(r.angle.to_le_bytes());
bytes.extend([0xa5, 0x5a]);
bytes
})
.collect()
}
// Four-argument call harness. Seed r4-r12 to check nonzero register preservation.
fn prepare(service: u8, thumb: bool, args: [u32; 4], status: u32, data: &[u8]) -> (Machine, u32) {
let mut code: Vec<u32> = (0..4).map(|r| 0xe59f_0044 | r << 12).collect();
for r in 4..=12 {
code.push(0xe3a0_0040 | r << 12 | r);
}
code.extend([
0xe321_f000 | status,
0xe328_f480, // MSR CPSR_f,#N
if thumb { 0xe28f_e001 } else { 0xe1a0_e00f },
if thumb { 0xe12f_ff1e } else { 0xe1a0_0000 },
if thumb {
0xe7fe_df00 | u32::from(service)
} else {
0xef00_0000 | u32::from(service) << 16
},
0xeaff_fffe,
]);
code.extend(args);
let mut rom = words(&code);
assert_eq!(rom.len() as u32 + ROM_START, DATA);
rom.extend(data);
let mut m = bios::boot(rom).unwrap();
reach(&mut m, ROM_START + 68, 100);
(m, ROM_START + if thumb { 70 } else { 72 })
}
fn complete(m: &mut Machine, pc: u32) {
let before = m.cpu().clone();
m.memory_mut()
.write32(bios::IRQ_STACK, 0x1234_5678)
.unwrap();
m.step().unwrap();
reach(m, pc, 3_000_000);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
assert_eq!(m.memory().read32(bios::IRQ_STACK).unwrap(), 0x1234_5678);
}
// Independent floating-point table generation and wide-integer matrix arithmetic.
fn coefficients(scale: [i16; 2], angle: u16) -> [i16; 4] {
let phase = usize::from(angle >> 8);
let sine = |index: usize| {
((index as f64 * std::f64::consts::TAU / 256.0).sin() * 16384.0).trunc() as i64
};
let s = sine(phase);
let c = sine((phase + 64) % 256);
let convert = |scale: i16, v: i64| (i64::from(scale) * v).div_euclid(16384) as i16;
[
convert(scale[0], c),
convert(scale[0], s),
convert(scale[1], s),
convert(scale[1], c),
]
}
fn bg_reference(r: Bg) -> Vec<u8> {
let [pa, sxsin, pc, pd] = coefficients(r.scale, r.angle);
let x = i64::from(r.texture[0]) - i64::from(pa) * i64::from(r.screen[0])
+ i64::from(sxsin) * i64::from(r.screen[1]);
let y = i64::from(r.texture[1])
- i64::from(pc) * i64::from(r.screen[0])
- i64::from(pd) * i64::from(r.screen[1]);
let mut bytes: Vec<_> = [pa, sxsin.wrapping_neg(), pc, pd]
.into_iter()
.flat_map(i16::to_le_bytes)
.collect();
bytes.extend((x as u32).to_le_bytes());
bytes.extend((y as u32).to_le_bytes());
bytes
}
fn verify_bg(records: &[Bg], thumb: bool, dest: u32) {
let (mut m, pc) = prepare(
0x0e,
thumb,
[DATA, dest, records.len() as u32, 0xdead_beef],
0x1f,
&bg_bytes(records),
);
let end = dest + records.len() as u32 * 16;
for address in [dest - 4, end] {
m.memory_mut().write32(address, 0xa55a_1234).unwrap();
}
complete(&mut m, pc);
let expected: Vec<_> = records.iter().flat_map(|r| bg_reference(*r)).collect();
for (i, byte) in expected.iter().enumerate() {
assert_eq!(
m.memory().read8(dest + i as u32).unwrap(),
*byte,
"byte={i}"
);
}
for address in [dest - 4, end] {
assert_eq!(m.memory().read32(address).unwrap(), 0xa55a_1234);
}
}
fn verify_obj(records: &[Obj], thumb: bool, dest: u32, stride: u32) {
let data = obj_bytes(records);
let span = records.len() as u32 * stride * 4;
let (mut m, pc) = prepare(
0x0f,
thumb,
[DATA, dest, records.len() as u32, stride],
0x1f,
&data,
);
let mut expected = vec![0xa55au16; span as usize / 2];
for (i, r) in records.iter().enumerate() {
let [pa, sxsin, pc, pd] = coefficients(r.scale, r.angle);
for (j, value) in [pa, sxsin.wrapping_neg(), pc, pd].into_iter().enumerate() {
expected[(i * 4 + j) * stride as usize / 2] = value as u16;
}
}
for offset in (0..span).step_by(2) {
m.memory_mut().write16(dest + offset, 0xa55a).unwrap();
}
for address in [dest - 2, dest + span] {
m.memory_mut().write16(address, 0x1357).unwrap();
}
complete(&mut m, pc);
for (i, value) in expected.iter().enumerate() {
assert_eq!(
m.memory().read16(dest + i as u32 * 2).unwrap(),
*value,
"halfword={i}"
);
}
for address in [dest - 2, dest + span] {
assert_eq!(m.memory().read16(address).unwrap(), 0x1357);
}
}
fn fail(m: &mut Machine) -> MachineError {
for _ in 0..3000 {
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 affine call did not produce a bounded diagnostic");
}
fn assert_invalid(error: MachineError) {
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: INVALID_ARGUMENT_TRAP,
..
})
));
}
#[test]
fn background_identity_translates_texture_and_display_centers() {
let r = Bg {
texture: [0x12345, -0x12345],
screen: [12, -34],
scale: [256, 256],
angle: 0,
};
let expected = bg_reference(r);
assert_eq!(&expected[..8], &[0, 1, 0, 0, 0, 0, 0, 1]);
for thumb in [false, true] {
verify_bg(&[r], thumb, DEST);
}
}
#[test]
fn object_cardinal_rotations_write_packed_and_oam_matrices() {
let records: Vec<_> = [0, 0x4000, 0x8000, 0xc000]
.into_iter()
.map(|angle| Obj {
scale: [256, 512],
angle,
})
.collect();
for thumb in [false, true] {
verify_obj(&records, thumb, DEST, 2);
verify_obj(&records, thumb, OAM + 6, 8);
}
}
#[test]
fn every_angle_phase_matches_independent_matrix_and_origin_references() {
let records: Vec<_> = (0..256u16)
.map(|phase| Bg {
texture: [0x123456, -0x234567],
screen: [-123, 234],
scale: [257, -513],
angle: phase << 8,
})
.collect();
verify_bg(&records, false, DEST);
let objects: Vec<_> = records
.iter()
.map(|r| Obj {
scale: r.scale,
angle: r.angle,
})
.collect();
verify_obj(&objects, true, DEST, 8);
}
#[test]
fn low_angle_byte_and_record_padding_do_not_change_results() {
let records: Vec<_> = (0..256u16)
.map(|low| Bg {
texture: [-12345, 98765],
screen: [45, -67],
scale: [319, 711],
angle: 0x2300 | low,
})
.collect();
let expected = bg_reference(records[0]);
assert!(records.iter().all(|r| bg_reference(*r) == expected));
verify_bg(&records, true, DEST);
let objects: Vec<_> = records
.iter()
.map(|r| Obj {
scale: r.scale,
angle: r.angle,
})
.collect();
verify_obj(&objects, false, DEST, 2);
}
#[test]
fn signed_scales_zero_reflections_and_halfword_wrap_match_reference() {
let mut records = Vec::new();
for sx in [-32768, -32767, -257, -1, 0, 1, 127, 256, 32767] {
for sy in [-32768, -1, 0, 1, 256, 32767] {
for angle in [0, 0x100, 0x4000, 0x8000, 0xc000, 0xff00] {
records.push(Bg {
texture: [i32::MIN, i32::MAX],
screen: [i16::MIN, i16::MAX],
scale: [sx, sy],
angle,
});
}
}
}
verify_bg(&records, false, DEST);
let objects: Vec<_> = records
.iter()
.map(|r| Obj {
scale: r.scale,
angle: r.angle,
})
.collect();
verify_obj(&objects, true, DEST, 2);
}
#[test]
fn pb_negation_occurs_after_signed_rounding() {
assert_eq!(coefficients([1, 1], 0x100), [0, 0, 0, 0]);
assert_eq!(coefficients([-1, -1], 0x100), [-1, -1, -1, -1]);
let records = [
Obj {
scale: [1, 1],
angle: 0x100,
},
Obj {
scale: [-1, -1],
angle: 0x100,
},
];
verify_obj(&records, false, DEST, 2); // PB must be 0 then +1, not -1 then 0.
}
#[test]
fn seeded_origins_and_matrices_match_wide_integer_wraparound() {
let mut state = 0x817b_53e9u32;
let mut next = || {
state = state.wrapping_mul(1664525).wrapping_add(1013904223);
state
};
let records: Vec<_> = (0..512)
.map(|_| Bg {
texture: [next() as i32, next() as i32],
screen: [next() as i16, next() as i16],
scale: [next() as i16, next() as i16],
angle: next() as u16,
})
.collect();
verify_bg(&records, true, DEST);
}
#[test]
fn custom_even_strides_preserve_gaps_between_coefficients_and_records() {
let records = [
Obj {
scale: [256, 128],
angle: 0x1200,
},
Obj {
scale: [-257, 513],
angle: 0xa500,
},
Obj {
scale: [0, -1],
angle: 0xffff,
},
];
for stride in [2, 4, 6, 8, 16, 32, 1024] {
verify_obj(&records, false, DEST, stride);
}
}
#[test]
fn zero_count_skips_all_buffer_and_stride_validation() {
for service in [0x0e, 0x0f] {
let (mut m, pc) = prepare(service, true, [1, 0xffff_ffff, 0, 1], 0x1f, &[]);
complete(&mut m, pc);
}
}
#[test]
fn alignment_and_protected_sources_are_rejected_before_output() {
for service in [0x0e, 0x0f] {
for source in [0, 0x3ffc, 0x0100_0000, DATA + 1, DATA + 3] {
let (mut m, _) = prepare(service, false, [source, DEST, 1, 2], 0x1f, &[0; 20]);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
let (mut m, _) = prepare(service, false, [DATA, DEST + 1, 1, 2], 0x1f, &[0; 20]);
assert_invalid(fail(&mut m));
}
for (source, dest) in [(DATA + 2, DEST), (DATA, DEST + 2)] {
let (mut m, _) = prepare(0x0e, false, [source, dest, 1, 2], 0x1f, &[0; 24]);
assert_invalid(fail(&mut m));
}
let mut data = vec![0, 0];
data.extend(obj_bytes(&[Obj {
scale: [256, 256],
angle: 0,
}]));
let (mut m, pc) = prepare(0x0f, false, [DATA + 2, DEST + 2, 1, 2], 0x1f, &data);
complete(&mut m, pc); // OBJ only requires halfword alignment.
assert_eq!(m.memory().read16(DEST + 2).unwrap(), 256);
}
#[test]
fn invalid_stride_batch_sizes_and_wrapping_ranges_are_diagnostics() {
for stride in [0, 1, 3, 0x4000_0000, 0xffff_fffe] {
let (mut m, _) = prepare(0x0f, false, [DATA, DEST, 1, stride], 0x1f, &[]);
assert_invalid(fail(&mut m));
}
for service in [0x0e, 0x0f] {
for (source, dest, count, stride) in [
(DATA, DEST, u32::MAX, 2),
(0xffff_fff8, DEST, 1, 2),
(DATA, 0xffff_fff8, 1, 2),
(DATA, DEST, 0x1000_0000, 32),
] {
let (mut m, _) = prepare(service, false, [source, dest, count, stride], 0x1f, &[]);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
}
}
#[test]
fn last_record_padding_need_not_be_mapped() {
for service in [0x0e, 0x0f] {
let mut data = if service == 0x0e {
bg_bytes(&[Bg {
texture: [0, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}])
} else {
obj_bytes(&[Obj {
scale: [256, 256],
angle: 0,
}])
};
data.truncate(data.len() - 2);
let (mut m, pc) = prepare(service, false, [DATA, DEST, 1, 2], 0x1f, &data);
complete(&mut m, pc);
assert_eq!(m.memory().read16(DEST).unwrap(), 256);
}
}
#[test]
fn later_source_failure_preserves_first_record_but_not_partial_new_output() {
for service in [0x0e, 0x0f] {
let mut data = if service == 0x0e {
bg_bytes(
&[Bg {
texture: [0, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}; 2],
)
} else {
obj_bytes(
&[Obj {
scale: [256, 256],
angle: 0,
}; 2],
)
};
data.truncate(data.len() - 3); // Missing last angle byte in second record.
let span = if service == 0x0e { 16 } else { 8 };
let (mut m, _) = prepare(service, false, [DATA, DEST, 2, 2], 0x1f, &data);
m.memory_mut().write32(DEST + span, 0xcccc_cccc).unwrap();
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::Unmapped(DATA + data.len() as u32).into())
);
assert_eq!(m.memory().read16(DEST).unwrap(), 256);
assert_eq!(m.memory().read16(DEST + 6).unwrap(), 256);
assert_eq!(m.memory().read32(DEST + span).unwrap(), 0xcccc_cccc);
}
}
#[test]
fn destination_failure_keeps_preceding_halfword_stores() {
for service in [0x0e, 0x0f] {
let data = if service == 0x0e {
bg_bytes(&[Bg {
texture: [0, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}])
} else {
obj_bytes(&[Obj {
scale: [256, 256],
angle: 0,
}])
};
let (mut m, _) = prepare(service, false, [DATA, 0x07ff_fffc, 1, 2], 0x1f, &data);
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into())
);
assert_eq!(m.memory().read32(0x07ff_fffc).unwrap(), 256);
}
}
#[test]
fn both_work_ram_regions_can_supply_records() {
for service in [0x0e, 0x0f] {
let data = if service == 0x0e {
bg_bytes(&[Bg {
texture: [0, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}])
} else {
obj_bytes(&[Obj {
scale: [256, 256],
angle: 0,
}])
};
for source in [SOURCE, 0x0300_0100] {
let (mut m, pc) = prepare(service, true, [source, DEST, 1, 2], 0x1f, &[]);
for (i, byte) in data.iter().enumerate() {
m.memory_mut().write8(source + i as u32, *byte).unwrap();
}
complete(&mut m, pc);
assert_eq!(m.memory().read16(DEST).unwrap(), 256);
assert_eq!(m.memory().read16(DEST + 6).unwrap(), 256);
}
}
}
#[test]
fn user_system_status_masks_and_nonzero_caller_registers_are_preserved() {
for service in [0x0e, 0x0f] {
let data = if service == 0x0e {
bg_bytes(&[Bg {
texture: [-12345, 67890],
screen: [-123, 321],
scale: [-257, 519],
angle: 0xa501,
}])
} else {
obj_bytes(&[Obj {
scale: [-257, 519],
angle: 0xa501,
}])
};
for thumb in [false, true] {
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
let (mut m, pc) = prepare(service, thumb, [DATA, DEST, 1, 8], status, &data);
complete(&mut m, pc);
}
}
}
}
#[test]
fn background_service_can_write_live_affine_registers_and_render() {
let data = bg_bytes(&[Bg {
texture: [256, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}]);
let (mut m, pc) = prepare(0x0e, false, [DATA, BG2PA, 1, 0], 0x1f, &data);
m.memory_mut().write16(DISPCNT, 0x0403).unwrap();
m.memory_mut().write16(VRAM_START, 31).unwrap();
m.memory_mut().write16(VRAM_START + 2, 0x3e0).unwrap();
complete(&mut m, pc);
// Affine registers are write-only; verify their effect through rendering.
let mut f = Framebuffer::default();
m.memory().render_frame(&mut f).unwrap();
assert_eq!(f.pixels()[0], rgb(0x3e0));
}
#[test]
fn object_service_writes_oam_matrix_without_changing_sprite_attributes() {
let data = obj_bytes(&[Obj {
scale: [-256, 256],
angle: 0,
}]);
let (mut m, pc) = prepare(0x0f, true, [DATA, OAM + 6, 1, 8], 0x1f, &data);
m.memory_mut().write16(DISPCNT, 0x1040).unwrap();
m.memory_mut().write16(PAL, 0x7c00).unwrap();
m.memory_mut().write16(PAL + 0x202, 31).unwrap();
m.memory_mut().write16(PAL + 0x204, 0x3e0).unwrap();
for i in 0..128 {
m.memory_mut().write16(OAM + i * 8, 0x200).unwrap();
}
m.memory_mut().write16(OAM, 0x100).unwrap();
for y in 0..8 {
for x in (0..8).step_by(4) {
m.memory_mut()
.write16(
VRAM_START + 0x10000 + y * 4 + x / 2,
if x == 0 { 0x1111 } else { 0x2222 },
)
.unwrap();
}
}
complete(&mut m, pc);
assert_eq!(m.memory().read16(OAM).unwrap(), 0x100);
assert_eq!(m.memory().read16(OAM + 8).unwrap(), 0x200);
let mut f = Framebuffer::default();
m.memory().render_frame(&mut f).unwrap();
assert_eq!(f.pixels()[1], rgb(0x3e0));
assert_eq!(f.pixels()[7], rgb(31));
}
#[test]
fn output_uses_normal_halfword_and_word_video_bus_writes() {
verify_bg(
&[Bg {
texture: [0x1234, -0x1234],
screen: [0, 0],
scale: [256, 512],
angle: 0,
}],
false,
VRAM_START + 4,
);
verify_obj(
&[Obj {
scale: [256, 512],
angle: 0x1234,
}],
true,
PAL + 4,
2,
);
}
#[test]
fn dma_and_timers_progress_while_services_defer_irq_delivery() {
for service in [0x0e, 0x0f] {
let data = if service == 0x0e {
bg_bytes(&[Bg {
texture: [0, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}])
} else {
obj_bytes(&[Obj {
scale: [256, 256],
angle: 0,
}])
};
let (mut m, pc) = prepare(service, false, [DATA, DEST, 1, 2], 0x1f, &data);
m.step().unwrap();
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.memory().read16(DEST).unwrap(), 256);
assert_eq!(m.step().unwrap(), StepKind::IrqEntry);
}
}

View 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);
}
}

View file

@ -0,0 +1,191 @@
use super::*;
use gba_core::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.
}

View file

@ -0,0 +1,418 @@
use super::*;
use gba_core::{
bios::INVALID_ARGUMENT_TRAP,
dma::DMA_BASE,
memory::{OAM_START, PALETTE_START},
};
const INFO: u32 = SOURCE + 0x1000;
fn info(m: &mut Machine, length: u16, source_width: u8, dest_width: u8, offset: u32) {
m.memory_mut().write16(INFO, length).unwrap();
m.memory_mut().write8(INFO + 2, source_width).unwrap();
m.memory_mut().write8(INFO + 3, dest_width).unwrap();
m.memory_mut().write32(INFO + 4, offset).unwrap();
}
fn prepare(
thumb: bool,
destination: u32,
data: &[u8],
sw: u8,
dw: u8,
offset: u32,
) -> (Machine, u32) {
let (mut m, pc) = call_status_data(0x10, thumb, [ROM_DATA, destination, INFO], 0x1f, data);
info(&mut m, data.len().try_into().unwrap(), sw, dw, offset);
(m, pc)
}
/// Expand individual bits and repack words independently of the emitted ARM loops.
fn reference(data: &[u8], sw: u8, dw: u8, offset: u32) -> Vec<u32> {
let mut bits = Vec::new();
for byte in data {
for position in (0..8).step_by(sw as usize) {
let mut value = 0u64;
for bit in 0..sw {
value += u64::from((byte >> (position + bit)) & 1) << bit;
}
if value != 0 || offset & 0x8000_0000 != 0 {
value += u64::from(offset & 0x7fff_ffff);
}
assert!(value < (1u64 << dw));
for bit in 0..dw {
bits.push((value >> bit) & 1);
}
}
}
assert_eq!(bits.len() % 32, 0);
bits.chunks_exact(32)
.map(|chunk| {
chunk
.iter()
.enumerate()
.fold(0, |word, (bit, v)| word | (*v as u32) << bit)
})
.collect()
}
fn complete(m: &mut Machine, pc: u32) {
m.step().unwrap();
reach(m, pc, 3_000_000);
}
fn verify(thumb: bool, dest: u32, data: &[u8], sw: u8, dw: u8, offset: u32) {
let expected = reference(data, sw, dw, offset);
let (mut m, pc) = prepare(thumb, dest, data, sw, dw, offset);
let before = m.cpu().clone();
let end = dest + expected.len() as u32 * 4;
for address in [dest - 4, end, bios::IRQ_STACK] {
m.memory_mut().write32(address, 0xa55a_1234).unwrap();
}
complete(&mut m, pc);
for (index, word) in expected.iter().enumerate() {
assert_eq!(
m.memory().read32(dest + index as u32 * 4).unwrap(),
*word,
"word={index}, sw={sw}, dw={dw}"
);
}
for address in [dest - 4, end, bios::IRQ_STACK] {
assert_eq!(m.memory().read32(address).unwrap(), 0xa55a_1234);
}
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..3000 {
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 unpack did not produce a bounded diagnostic");
}
fn assert_invalid(error: MachineError) {
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: INVALID_ARGUMENT_TRAP,
..
})
));
}
#[test]
fn all_supported_width_pairs_and_input_bytes_match_bit_reference() {
let data: Vec<_> = (0..=255).collect();
for sw in [1, 2, 4, 8] {
for dw in [1, 2, 4, 8, 16, 32].into_iter().filter(|dw| *dw >= sw) {
for thumb in [false, true] {
verify(
thumb,
if thumb { VRAM_START + 4 } else { DEST },
&data,
sw,
dw,
0,
);
}
}
}
}
#[test]
fn source_units_and_destination_units_are_least_significant_first() {
let (mut m, pc) = prepare(false, DEST, &[0b1011_0001], 1, 4, 0);
complete(&mut m, pc);
assert_eq!(m.memory().read32(DEST).unwrap(), 0x1011_0001);
let (mut m, pc) = prepare(true, DEST, &[0xe4], 2, 8, 0);
complete(&mut m, pc);
assert_eq!(m.memory().read32(DEST).unwrap(), 0x0302_0100);
}
#[test]
fn offsets_apply_to_nonzero_units_and_optionally_to_zero() {
for zero in [0, 0x8000_0000] {
for offset in [0, 1, 0x1234, 0x7fff_ffff] {
verify(false, DEST, &[0, 1, 0xfe, 0xff], 8, 32, offset | zero);
}
verify(true, VRAM_START + 4, &[0x10, 0x21], 4, 8, 0x80 | zero);
}
}
#[test]
fn seeded_width_offset_and_data_combinations_match_reference() {
let mut seed = 0x615c_ae39u32;
for case in 0..96 {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
let sw = 1u8 << (seed & 3);
let choices: Vec<u8> = [1, 2, 4, 8, 16, 32]
.into_iter()
.filter(|dw| *dw >= sw)
.collect();
let dw = choices[(seed >> 8) as usize % choices.len()];
let capacity = ((1u64 << dw) - (1u64 << sw)).min(0x7fff_ffff);
let bias =
((seed as u64) % (capacity + 1)) as u32 | if case % 2 == 0 { 0x8000_0000 } else { 0 };
let data: Vec<_> = (0..20)
.map(|_| {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
(seed >> 24) as u8
})
.collect();
verify(case % 2 != 0, DEST, &data, sw, dw, bias);
}
}
#[test]
fn maximum_source_byte_count_is_unsigned_and_fully_processed() {
let data: Vec<_> = (0..65535u32).map(|i| i as u8).collect();
// Keep the descriptor in IWRAM: the output fills almost all EWRAM.
let (mut m, pc) = call_status_data(0x10, false, [ROM_DATA, SOURCE, 0x0300_0100], 0x1f, &data);
m.memory_mut().write32(0x0300_0100, 0x2008_ffff).unwrap();
m.memory_mut().write32(0x0300_0104, 0).unwrap();
m.memory_mut()
.write32(SOURCE + 65535 * 4, 0xa55a_1234)
.unwrap();
complete(&mut m, pc);
for (index, byte) in data.iter().enumerate() {
assert_eq!(
m.memory().read32(SOURCE + index as u32 * 4).unwrap(),
u32::from(*byte)
);
}
assert_eq!(m.memory().read32(SOURCE + 65535 * 4).unwrap(), 0xa55a_1234);
}
#[test]
fn byte_aligned_sources_work_in_rom_and_both_ram_regions() {
for source in [
ROM_DATA + 1,
ROM_DATA + 2,
ROM_DATA + 3,
SOURCE + 1,
0x0300_0103,
] {
let mut data = vec![0xcc; 3];
data.extend([0x12, 0x34, 0x56, 0x78]);
let (mut m, pc) = call_status_data(0x10, true, [source, DEST, INFO], 0x1f, &data);
info(&mut m, 4, 8, 8, 0);
let expected = if source >= ROM_DATA {
&data[(source - ROM_DATA) as usize..(source - ROM_DATA) as usize + 4]
} else {
&[0x12, 0x34, 0x56, 0x78]
};
if source < ROM_DATA {
for (index, byte) in expected.iter().enumerate() {
m.memory_mut().write8(source + index as u32, *byte).unwrap();
}
}
complete(&mut m, pc);
assert_eq!(
m.memory().read32(DEST).unwrap(),
u32::from_le_bytes(expected.try_into().unwrap())
);
}
}
#[test]
fn unpack_descriptor_can_be_in_cartridge_rom() {
let data = [4, 0, 8, 8, 0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78];
let (mut m, pc) = call_status_data(0x10, false, [ROM_DATA + 8, DEST, ROM_DATA], 0x1f, &data);
complete(&mut m, pc);
assert_eq!(m.memory().read32(DEST).unwrap(), 0x7856_3412);
}
#[test]
fn caller_registers_status_and_stack_boundaries_are_preserved() {
for thumb in [false, true] {
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
let (mut m, pc) =
call_status_data(0x10, thumb, [ROM_DATA, DEST, INFO], status, &[0x12, 0x34]);
info(&mut m, 2, 4, 8, 0x10);
let before = m.cpu().clone();
m.memory_mut()
.write32(bios::IRQ_STACK, 0x1234_5678)
.unwrap();
complete(&mut m, pc);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
assert_eq!(m.memory().read32(bios::IRQ_STACK).unwrap(), 0x1234_5678);
}
}
}
#[test]
fn zero_length_does_not_access_source_or_destination_after_validation() {
let (mut m, pc) = call(0x10, false, [0x0e00_0000, 0x0e00_0000, INFO]);
info(&mut m, 0, 1, 32, 0);
complete(&mut m, pc);
}
#[test]
fn invalid_widths_and_narrowing_produce_diagnostics() {
for width in 0..=255u8 {
if ![1, 2, 4, 8].contains(&width) {
let (mut m, _) = prepare(false, DEST, &[0; 4], width, 32, 0);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
if ![1, 2, 4, 8, 16, 32].contains(&width) {
let (mut m, _) = prepare(true, DEST, &[0; 4], 1, width, 0);
assert_invalid(fail(&mut m));
}
}
for (sw, dw) in [(2, 1), (4, 1), (4, 2), (8, 1), (8, 2), (8, 4)] {
let (mut m, _) = prepare(false, DEST, &[0; 4], sw, dw, 0);
assert_invalid(fail(&mut m));
}
}
#[test]
fn alignment_protected_source_and_wrapping_ranges_are_rejected() {
for (source, dest, descriptor) in [
(0, DEST, INFO),
(0x3fff, DEST, INFO),
(0x0100_0000, DEST, INFO),
(ROM_DATA, DEST + 1, INFO),
(ROM_DATA, DEST + 2, INFO),
(ROM_DATA, DEST + 3, INFO),
(ROM_DATA, DEST, INFO + 1),
(ROM_DATA, DEST, INFO + 2),
(ROM_DATA, DEST, INFO + 3),
(0xffff_fffe, DEST, INFO),
(ROM_DATA, 0xffff_fffc, INFO),
] {
let (mut m, _) = call_status_data(0x10, false, [source, dest, descriptor], 0x1f, &[1; 4]);
info(&mut m, 4, 8, 32, 0);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
}
#[test]
fn incomplete_output_words_are_rejected_before_source_reads() {
for (length, sw, dw) in [(1, 8, 8), (2, 8, 8), (3, 8, 8), (1, 4, 8), (6, 8, 8)] {
let (mut m, _) = call(0x10, false, [0x0e00_0000, DEST, INFO]);
info(&mut m, length, sw, dw, 0);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
}
#[test]
fn offset_overflow_keeps_completed_words_but_discards_pending_word() {
// First four values fit; a later 255+1 does not fit eight destination bits.
let (mut m, _) = prepare(false, DEST, &[1, 2, 3, 4, 5, 0xff, 7, 8], 8, 8, 1);
m.memory_mut().write32(DEST + 4, 0xcccc_cccc).unwrap();
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0x0504_0302);
assert_eq!(m.memory().read32(DEST + 4).unwrap(), 0xcccc_cccc);
// Offsets on transparent zero units are checked only when the flag requests them.
verify(true, DEST, &[0; 4], 8, 8, 256);
let (mut m, _) = prepare(true, DEST, &[0; 4], 8, 8, 0x8000_0100);
assert_invalid(fail(&mut m));
}
#[test]
fn truncated_descriptors_and_sources_return_memory_diagnostics() {
let descriptor = [4, 0, 8, 8, 0, 0, 0, 0];
for length in 0..8 {
let (mut m, _) = call_status_data(
0x10,
false,
[SOURCE, DEST, ROM_DATA],
0x1f,
&descriptor[..length],
);
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::Unmapped(ROM_DATA + length as u32).into())
);
}
for length in 0..8 {
let (mut m, _) = prepare(false, DEST, &vec![0x12; length], 8, 8, 0);
info(&mut m, 8, 8, 8, 0);
m.memory_mut().write32(DEST, 0xcccc_cccc).unwrap();
m.memory_mut().write32(DEST + 4, 0xcccc_cccc).unwrap();
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::Unmapped(ROM_DATA + length as u32).into())
);
assert_eq!(
m.memory().read32(DEST).unwrap(),
if length >= 4 {
0x1212_1212
} else {
0xcccc_cccc
}
);
assert_eq!(m.memory().read32(DEST + 4).unwrap(), 0xcccc_cccc);
}
}
#[test]
fn destination_failure_preserves_completed_word() {
let (mut m, _) = prepare(
false,
0x07ff_fffc,
&[0x12, 0x34, 0x56, 0x78, 1, 2, 3, 4],
8,
8,
0,
);
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into())
);
assert_eq!(m.memory().read32(0x07ff_fffc).unwrap(), 0x7856_3412);
}
#[test]
fn word_stores_support_palette_oam_and_video_memory() {
for dest in [PALETTE_START + 4, OAM_START + 4, VRAM_START + 4] {
verify(false, dest, &[0xe4, 0x1b], 2, 4, 0);
}
}
#[test]
fn timers_and_dma_continue_while_irq_delivery_is_masked() {
let (mut m, pc) = prepare(false, DEST, &[0x12; 16], 8, 32, 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, 0xa55a_1234).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(), 0xa55a_1234);
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 in 0..16 {
assert_eq!(m.memory().read32(DEST + index * 4).unwrap(), 0x12);
}
}

View file

@ -0,0 +1,411 @@
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);
}
}
}

View file

@ -0,0 +1,473 @@
use super::*;
use gba_core::{
bios::INVALID_ARGUMENT_TRAP,
dma::DMA_BASE,
memory::{OAM_START, PALETTE_START},
};
use std::collections::{BTreeMap, VecDeque};
#[derive(Clone)]
enum Tree {
Leaf(u8),
Branch(Box<Tree>, Box<Tree>),
}
fn branch(left: Tree, right: Tree) -> Tree {
Tree::Branch(Box::new(left), Box::new(right))
}
fn pair() -> Tree {
branch(Tree::Leaf(0x12), Tree::Leaf(0x34))
}
fn balanced(values: &[u8]) -> Tree {
if values.len() == 1 {
return Tree::Leaf(values[0]);
}
let half = values.len() / 2;
branch(balanced(&values[..half]), balanced(&values[half..]))
}
fn chain(depth: u8) -> Tree {
(0..depth).rev().fold(Tree::Leaf(depth), |next, value| {
branch(next, Tree::Leaf(value))
})
}
/// Test-only tree serializer and path encoder. Expected output comes directly
/// from requested symbols; no production traversal code is used to decode it.
fn encode(width: u8, tree: &Tree, symbols: &[u8]) -> (Vec<u8>, Vec<u8>) {
fn paths(tree: &Tree, prefix: &mut Vec<bool>, codes: &mut BTreeMap<u8, Vec<bool>>) {
match tree {
Tree::Leaf(value) => {
assert!(codes.insert(*value, prefix.clone()).is_none());
}
Tree::Branch(left, right) => {
prefix.push(false);
paths(left, prefix, codes);
prefix.pop();
prefix.push(true);
paths(right, prefix, codes);
prefix.pop();
}
}
}
let mut codes = BTreeMap::new();
paths(tree, &mut Vec::new(), &mut codes);
let mut table = vec![0, 0]; // Size byte and root.
let mut queue = VecDeque::from([(1usize, tree)]);
while let Some((index, node)) = queue.pop_front() {
match node {
Tree::Leaf(value) => table[index] = *value,
Tree::Branch(left, right) => {
let children = table.len();
let offset = (children - (index & !1)) / 2 - 1;
assert!(offset < 64);
table[index] = offset as u8
| if matches!(**left, Tree::Leaf(_)) {
0x80
} else {
0
}
| if matches!(**right, Tree::Leaf(_)) {
0x40
} else {
0
};
table.extend([0, 0]);
queue.push_back((children, left));
queue.push_back((children + 1, right));
}
}
}
while table.len() % 4 != 0 {
table.push(0);
}
assert!(table.len() <= 512);
table[0] = (table.len() / 2 - 1) as u8;
let expected: Vec<u8> = if width == 4 {
assert!(symbols.iter().all(|v| *v < 16));
assert_eq!(symbols.len() % 2, 0);
symbols
.chunks_exact(2)
.map(|pair| pair[0] | pair[1] << 4)
.collect()
} else {
symbols.to_vec()
};
assert_eq!(expected.len() % 4, 0);
let mut data = (((expected.len() as u32) << 8) | 0x20 | u32::from(width))
.to_le_bytes()
.to_vec();
data.extend(table);
let bits: Vec<bool> = symbols
.iter()
.flat_map(|v| codes[v].iter().copied())
.collect();
for chunk in bits.chunks(32) {
let word = chunk
.iter()
.enumerate()
.fold(0u32, |word, (i, bit)| word | u32::from(*bit) << (31 - i));
data.extend(word.to_le_bytes());
}
(data, expected)
}
fn prepare(thumb: bool, dest: u32, data: &[u8]) -> (Machine, u32) {
call_status_data(0x13, thumb, [ROM_DATA, dest, 0xa55a_1234], 0x1f, data)
}
fn complete(m: &mut Machine, pc: u32) {
m.step().unwrap();
reach(m, pc, 8_000_000);
}
fn verify(thumb: bool, dest: u32, data: &[u8], expected: &[u8]) {
let (mut m, pc) = prepare(thumb, dest, data);
let before = m.cpu().clone();
let end = dest + expected.len() as u32;
for address in [dest - 4, end, bios::IRQ_STACK] {
m.memory_mut().write32(address, 0xa55a_1234).unwrap();
}
complete(&mut m, pc);
for (index, byte) in expected.iter().enumerate() {
assert_eq!(
m.memory().read8(dest + index as u32).unwrap(),
*byte,
"byte={index}"
);
}
for address in [dest - 4, end, bios::IRQ_STACK] {
assert_eq!(m.memory().read32(address).unwrap(), 0xa55a_1234);
}
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..10000 {
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 Huffman stream did not produce a bounded diagnostic");
}
fn assert_invalid(error: MachineError) {
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: INVALID_ARGUMENT_TRAP,
..
})
));
}
#[test]
fn documented_huff_example_decodes_msb_first_words_from_arm_and_thumb() {
let tree = branch(Tree::Leaf(b'f'), branch(Tree::Leaf(b'H'), Tree::Leaf(b'u')));
let (data, expected) = encode(8, &tree, b"Huff");
assert_eq!(
data,
[0x28, 4, 0, 0, 3, 0x80, b'f', 0xc0, b'H', b'u', 0, 0, 0, 0, 0, 0xb0]
);
for thumb in [false, true] {
verify(thumb, DEST, &data, &expected);
}
}
#[test]
fn nibble_symbols_pack_low_nibble_first_into_output_words() {
let tree = branch(Tree::Leaf(2), Tree::Leaf(13));
let (data, expected) = encode(4, &tree, &[2, 13, 13, 2, 2, 2, 13, 13]);
assert_eq!(expected, [0xd2, 0x2d, 0x22, 0xdd]);
assert_eq!(&data[8..], &0x6300_0000u32.to_le_bytes());
verify(true, VRAM_START + 4, &data, &expected);
}
#[test]
fn balanced_trees_cover_every_byte_and_nibble_value() {
for base in [0u8, 128] {
let symbols: Vec<_> = (0..128u8).map(|n| base + n).collect();
let (data, expected) = encode(8, &balanced(&symbols), &symbols);
verify(false, DEST, &data, &expected);
verify(true, VRAM_START + 4, &data, &expected);
}
let symbols: Vec<_> = (0..16u8).collect();
let (data, expected) = encode(4, &balanced(&symbols), &symbols);
verify(true, DEST, &data, &expected);
}
#[test]
fn maximum_tree_and_deep_paths_preserve_node_state_across_input_words() {
let (data, expected) = encode(8, &chain(255), &[255, 254, 0, 127]);
assert_eq!(data[4], 255); // 512-byte size/table section; 511 actual nodes.
verify(false, DEST, &data, &expected);
verify(true, VRAM_START + 4, &data, &expected);
}
#[test]
fn maximum_child_offset_and_unused_tree_padding_are_supported() {
let mut data = vec![0x28, 4, 0, 0];
let mut table = vec![0xff; 512]; // Unused bytes are not decoded or validated.
table[0] = 255;
table[1] = 0xff; // Both leaves, offset 63 -> section bytes 128 and 129.
table[128] = 0x12;
table[129] = 0x34;
data.extend(table);
data.extend(0x6000_0000u32.to_le_bytes()); // 0,1,1,0
verify(false, DEST, &data, &[0x12, 0x34, 0x34, 0x12]);
}
#[test]
fn seeded_variable_length_paths_match_original_symbols() {
let mut seed = 0x392b_175eu32;
for case in 0..48 {
let width = if case % 2 == 0 { 4 } else { 8 };
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
let depth = 2 + (seed % if width == 4 { 14 } else { 62 }) as u8;
let symbols: Vec<_> = (0..64)
.map(|_| {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
(seed % (u32::from(depth) + 1)) as u8
})
.collect();
let (data, expected) = encode(width, &chain(depth), &symbols);
verify(case % 3 == 0, DEST, &data, &expected);
}
}
#[test]
fn output_byte_length_uses_upper_header_bits_for_both_symbol_widths() {
for width in [4, 8] {
let tree = branch(Tree::Leaf(1), Tree::Leaf(2));
let symbols: Vec<_> = (0..65536 * (8 / width as usize))
.map(|i| if i % 3 == 0 { 1 } else { 2 })
.collect();
let (data, expected) = encode(width, &tree, &symbols);
assert_eq!(&data[..4], &[0x20 | width, 0, 0, 1]);
verify(false, DEST, &data, &expected);
}
}
#[test]
fn streams_can_be_read_from_both_work_ram_regions() {
let (data, expected) = encode(8, &pair(), &[0x12, 0x34, 0x12, 0x34]);
for source in [SOURCE, 0x0300_0100] {
let (mut m, pc) = call(0x13, true, [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 user_system_status_masks_and_caller_registers_are_restored() {
let (data, _) = encode(8, &pair(), &[0x12; 4]);
for thumb in [false, true] {
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
let (mut m, pc) =
call_status_data(0x13, thumb, [ROM_DATA, DEST, 0x1234_5678], 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_output_length_does_not_read_tree_or_bitstream() {
for header in [0x24, 0x28] {
let (mut m, pc) = prepare(false, 0x0e00_0000, &[header, 0, 0, 0]);
complete(&mut m, pc);
}
}
#[test]
fn unsupported_type_or_symbol_width_headers_are_rejected() {
for header in 0u8..=255 {
if [0x24, 0x28].contains(&header) {
continue;
}
let (mut m, _) = prepare(false, DEST, &[header, 4, 0, 0]);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
}
#[test]
fn source_destination_length_and_bitstream_alignment_are_checked() {
let (data, _) = encode(8, &pair(), &[0x12; 4]);
for (source, dest) in [
(0, DEST),
(0x3ffc, DEST),
(0x0100_0000, DEST),
(ROM_DATA + 1, DEST),
(ROM_DATA + 2, DEST),
(ROM_DATA + 3, DEST),
(ROM_DATA, DEST + 1),
(ROM_DATA, DEST + 2),
(ROM_DATA, DEST + 3),
(ROM_DATA, 0xffff_fffc),
] {
let (mut m, _) = call_status_data(0x13, false, [source, dest, 0], 0x1f, &data);
assert_invalid(fail(&mut m));
}
for length in [1, 2, 3, 5, 6, 7] {
let (mut m, _) = prepare(true, DEST, &[0x28, length, 0, 0]);
assert_invalid(fail(&mut m));
}
for size in [0, 2, 128, 254] {
let (mut m, _) = prepare(false, DEST, &[0x28, 4, 0, 0, size]);
assert_invalid(fail(&mut m)); // Table end must be aligned, not rounded silently.
}
}
#[test]
fn traversed_out_of_table_children_fail_without_reading_them() {
for root in [0xc1, 0xc2, 0xff, 0] {
// Size=1 permits only root + two bytes. Root=0 treats a child as another
// internal node; its next edge must also stay inside the declared table.
let data = [0x28, 4, 0, 0, 1, root, 0, 0, 0, 0, 0, 0];
let (mut m, _) = prepare(false, DEST, &data);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
}
#[test]
fn invalid_nibble_leaves_discard_pending_word_but_keep_prior_output() {
let mut data = vec![0x24, 8, 0, 0, 1, 0xc0, 2, 0x10];
data.extend(0x0040_0000u32.to_le_bytes()); // Eight left leaves, then left, then invalid right.
let (mut m, _) = prepare(true, DEST, &data);
m.memory_mut().write32(DEST + 4, 0xcccc_cccc).unwrap();
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0x2222_2222);
assert_eq!(m.memory().read32(DEST + 4).unwrap(), 0xcccc_cccc);
}
#[test]
fn unused_invalid_leaf_does_not_block_valid_decoding() {
let data = [0x24, 4, 0, 0, 1, 0xc0, 2, 0xff, 0, 0, 0, 0];
verify(false, DEST, &data, &[0x22; 4]);
}
#[test]
fn truncated_headers_tables_and_words_return_memory_diagnostics() {
let (data, _) = encode(8, &pair(), &[0x12; 4]);
for length in 0..data.len() {
let (mut m, _) = prepare(false, DEST, &data[..length]);
// Input words are loaded before the first node. Missing table storage
// therefore faults at the declared bitstream address, not at a tree pre-scan.
let address = ROM_DATA
+ if (5..8).contains(&length) {
8
} else {
length as u32
};
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::Unmapped(address).into()),
"length={length}"
);
}
}
#[test]
fn input_word_refill_failure_does_not_commit_incomplete_output() {
for width in [4, 8] {
let symbols = vec![3; if width == 4 { 24 } else { 12 }];
let (mut data, _) = encode(width, &chain(3), &symbols);
let bitstream = 4 + (usize::from(data[4]) + 1) * 2;
data.truncate(bitstream + 4); // Only 32 of the required branch bits exist.
let (mut m, _) = prepare(false, DEST, &data);
for offset in [0, 4, 8] {
m.memory_mut().write32(DEST + offset, 0xcccc_cccc).unwrap();
}
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::Unmapped(ROM_DATA + data.len() as u32).into())
);
let word = if width == 4 { 0x3333_3333 } else { 0x0303_0303 };
assert_eq!(m.memory().read32(DEST).unwrap(), word);
assert_eq!(
m.memory().read32(DEST + 4).unwrap(),
if width == 4 { 0xcccc_cccc } else { word }
);
assert_eq!(m.memory().read32(DEST + 8).unwrap(), 0xcccc_cccc);
}
}
#[test]
fn destination_failure_preserves_preceding_completed_word() {
let (data, _) = encode(8, &pair(), &[0x12; 8]);
let (mut m, _) = prepare(false, 0x07ff_fffc, &data);
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into())
);
assert_eq!(m.memory().read32(0x07ff_fffc).unwrap(), 0x1212_1212);
}
#[test]
fn word_output_works_in_video_palette_and_object_memory() {
let (data, expected) = encode(8, &pair(), &[0x12, 0x34, 0x34, 0x12]);
for dest in [VRAM_START + 4, PALETTE_START + 4, OAM_START + 4] {
verify(false, dest, &data, &expected);
}
}
#[test]
fn declared_output_end_ignores_remaining_bits_and_trailing_words() {
let (mut data, expected) = encode(8, &pair(), &[0x12; 4]);
data[8..12].copy_from_slice(&0x0fff_ffffu32.to_le_bytes());
data.extend([0xff; 4]);
verify(true, DEST, &data, &expected);
}
#[test]
fn device_clocks_and_dma_continue_while_huffman_masks_irq_delivery() {
let (data, expected) = encode(8, &pair(), &[0x12; 32]);
let (mut m, pc) = prepare(false, DEST, &data);
m.step().unwrap();
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);
}
}

View file

@ -0,0 +1,417 @@
use super::*;
use gba_core::bios::INVALID_ARGUMENT_TRAP;
#[derive(Clone, Copy)]
enum Token {
Literal(u8),
Reference { distance: usize, length: usize },
}
/// Test-only token encoder and byte-vector reference. Neither decodes the emitted ARM code.
fn stream(tokens: &[Token]) -> (Vec<u8>, Vec<u8>) {
let mut compressed = vec![0x10, 0, 0, 0];
let mut expected = Vec::new();
for group in tokens.chunks(8) {
let flags_index = compressed.len();
compressed.push(0);
for (index, token) in group.iter().enumerate() {
match *token {
Token::Literal(value) => {
compressed.push(value);
expected.push(value);
}
Token::Reference { distance, length } => {
assert!((1..=4096).contains(&distance));
assert!((3..=18).contains(&length));
assert!(distance <= expected.len());
compressed[flags_index] |= 0x80 >> index;
compressed.push((((length - 3) << 4) | ((distance - 1) >> 8)) as u8);
compressed.push((distance - 1) as u8);
for _ in 0..length {
expected.push(expected[expected.len() - distance]);
}
}
}
}
}
let length = expected.len() as u32;
compressed[..4].copy_from_slice(&(length << 8 | 0x10).to_le_bytes());
(compressed, expected)
}
fn prepare(service: u8, thumb: bool, destination: u32, compressed: &[u8]) -> (Machine, u32) {
call_status_data(
service,
thumb,
[ROM_DATA, destination, 0x55aa_1234],
0x1f,
compressed,
)
}
fn complete(machine: &mut Machine, return_pc: u32) {
machine.step().unwrap();
reach(machine, return_pc, 500_000);
}
fn verify(service: u8, thumb: bool, destination: u32, compressed: &[u8], expected: &[u8]) {
let (mut machine, return_pc) = prepare(service, thumb, destination, compressed);
let before = machine.cpu().clone();
// Halfword sentinels also work in VRAM without byte-write duplication.
machine
.memory_mut()
.write16((destination - 2) & !1, 0xa55a)
.unwrap();
let after = (destination + expected.len() as u32 + 1) & !1;
machine.memory_mut().write16(after, 0xa55a).unwrap();
for address in [destination - 1, destination + expected.len() as u32] {
machine.memory_mut().write16(address & !1, 0xa55a).unwrap();
}
machine
.memory_mut()
.write32(bios::IRQ_STACK, 0x5a5a_a5a5)
.unwrap();
complete(&mut machine, return_pc);
for (index, value) in expected.iter().enumerate() {
assert_eq!(
machine.memory().read8(destination + index as u32).unwrap(),
*value,
"output byte {index}"
);
}
assert_eq!(
machine.memory().read16((destination - 2) & !1).unwrap(),
0xa55a
);
assert_eq!(machine.memory().read16(after).unwrap(), 0xa55a);
for address in [destination - 1, destination + expected.len() as u32] {
assert_eq!(
machine.memory().read8(address).unwrap(),
if address & 1 == 0 { 0x5a } else { 0xa5 }
);
}
assert_eq!(
machine.memory().read32(bios::IRQ_STACK).unwrap(),
0x5a5a_a5a5
);
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
}
fn fail(machine: &mut Machine) -> MachineError {
for _ in 0..1000 {
let before = machine.cpu().clone();
let cycles = machine.cycles();
let timing = machine.last_timing();
if let Err(error) = machine.step() {
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.last_timing(), timing);
assert_eq!(machine.step(), Err(error.clone()));
return error;
}
}
panic!("invalid stream did not produce a bounded diagnostic");
}
fn assert_invalid(error: MachineError) {
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: INVALID_ARGUMENT_TRAP,
..
})
));
}
#[test]
fn literals_cross_flag_groups_in_arm_and_thumb_for_both_write_widths() {
let tokens: Vec<_> = (0..34).map(|value| Token::Literal(value * 7)).collect();
let (compressed, expected) = stream(&tokens);
for thumb in [false, true] {
verify(0x11, thumb, DEST, &compressed, &expected);
verify(0x12, thumb, VRAM_START + 4, &compressed, &expected);
verify(0x12, thumb, DEST, &compressed, &expected); // Halfword variant also works in RAM.
}
}
#[test]
fn literal_and_reference_flags_are_read_most_significant_bit_first() {
let tokens = [
Token::Literal(b'A'),
Token::Literal(b'B'),
Token::Reference {
distance: 2,
length: 3,
},
Token::Literal(b'C'),
Token::Reference {
distance: 4,
length: 4,
},
Token::Literal(b'D'),
Token::Literal(b'E'),
Token::Reference {
distance: 3,
length: 6,
},
Token::Reference {
distance: 8,
length: 4,
},
Token::Literal(b'F'),
Token::Literal(b'G'),
];
let (compressed, expected) = stream(&tokens);
assert_eq!(compressed[4], 0x29); // References in token positions 2,4,7.
assert_eq!(expected.len() % 2, 0);
verify(0x11, false, DEST, &compressed, &expected);
verify(0x12, true, VRAM_START + 4, &compressed, &expected);
}
#[test]
fn overlapping_references_support_every_run_length() {
for length in 3..=18 {
let mut tokens = vec![
Token::Literal(0x12),
Token::Literal(0xab),
Token::Reference {
distance: 2,
length,
},
];
if length % 2 != 0 {
tokens.push(Token::Literal(0x99));
}
let (compressed, expected) = stream(&tokens);
verify(0x11, false, DEST + 1, &compressed, &expected); // Byte destination need not be aligned.
verify(0x12, true, VRAM_START + 4, &compressed, &expected);
}
}
#[test]
fn wram_variant_supports_distance_one_runs() {
let (compressed, expected) = stream(&[
Token::Literal(0x5a),
Token::Reference {
distance: 1,
length: 18,
},
]);
verify(0x11, false, DEST, &compressed, &expected);
verify(0x11, true, DEST + 1, &compressed, &expected);
}
#[test]
fn full_twelve_bit_displacements_reach_back_4096_bytes() {
for distance in [4095, 4096] {
let mut tokens: Vec<_> = (0..4096)
.map(|index| Token::Literal((index * 37 % 251) as u8))
.collect();
tokens.push(Token::Reference {
distance,
length: 18,
});
let (compressed, expected) = stream(&tokens);
verify(0x11, false, DEST, &compressed, &expected);
verify(0x12, true, VRAM_START + 4, &compressed, &expected);
}
}
#[test]
fn seeded_valid_streams_match_independent_token_expansion() {
let mut state = 0x1234_abcd_u32;
for seed in 0..16 {
let mut tokens = vec![Token::Literal(0x13), Token::Literal(0x57)];
let mut length = 2;
for _ in 0..40 {
state = state.wrapping_mul(1664525).wrapping_add(1013904223);
if state & 3 == 0 {
tokens.push(Token::Literal((state >> 16) as u8));
length += 1;
} else {
let run = 3 + (state >> 8) as usize % 16;
let distance = 2 + (state >> 16) as usize % (length - 1).min(4095);
tokens.push(Token::Reference {
distance,
length: run,
});
length += run;
}
}
if length % 2 != 0 {
tokens.push(Token::Literal(0));
}
let (compressed, expected) = stream(&tokens);
verify(0x11, seed & 1 == 0, DEST, &compressed, &expected);
verify(0x12, seed & 1 != 0, VRAM_START + 4, &compressed, &expected);
}
}
#[test]
fn compressed_sources_can_be_read_from_either_work_ram_region() {
let (compressed, expected) = stream(&[
Token::Literal(0x55),
Token::Literal(0xaa),
Token::Reference {
distance: 2,
length: 4,
},
]);
for source in [SOURCE, 0x0300_0100] {
for service in [0x11, 0x12] {
let (mut machine, return_pc) = call(service, false, [source, DEST, 0]);
for (offset, byte) in compressed.iter().enumerate() {
machine
.memory_mut()
.write8(source + offset as u32, *byte)
.unwrap();
}
complete(&mut machine, return_pc);
for (offset, byte) in expected.iter().enumerate() {
assert_eq!(machine.memory().read8(DEST + offset as u32).unwrap(), *byte);
}
}
}
}
#[test]
fn decompression_restores_user_mode_masks_and_all_caller_registers() {
let (compressed, _) = stream(&[
Token::Literal(0x55),
Token::Literal(0xaa),
Token::Reference {
distance: 2,
length: 4,
},
]);
for service in [0x11, 0x12] {
for thumb in [false, true] {
for status in [0x10, 0x90, 0x9f, 0xdf] {
let (mut machine, return_pc) = call_status_data(
service,
thumb,
[ROM_DATA, DEST, 0x12345678],
status,
&compressed,
);
let before = machine.cpu().clone();
complete(&mut machine, return_pc);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
}
}
}
}
#[test]
fn zero_output_length_does_not_read_flags_or_touch_destination() {
for service in [0x11, 0x12] {
let (mut machine, return_pc) = prepare(service, false, 0x0e00_0000, &[0x10, 0, 0, 0]);
complete(&mut machine, return_pc); // Destination and next source byte are unmapped.
}
}
#[test]
fn bad_headers_alignment_and_wrapping_destinations_are_rejected() {
for header in [0_u32, 0x0200, 0x0211, 0x0220, 0x02ff] {
let (mut machine, _) = prepare(0x11, false, DEST, &header.to_le_bytes());
assert_invalid(fail(&mut machine));
assert_eq!(machine.memory().read32(DEST).unwrap(), 0);
}
for source in [0, 0x3ffc, ROM_DATA + 1, ROM_DATA + 2, ROM_DATA + 3] {
let (mut machine, _) =
call_status_data(0x11, false, [source, DEST, 0], 0x1f, &[0x10, 4, 0, 0]);
assert_invalid(fail(&mut machine));
}
let (mut machine, _) = prepare(0x12, false, 0xffff_fffe, &[0x10, 4, 0, 0]);
assert_invalid(fail(&mut machine));
}
#[test]
fn references_before_output_or_beyond_declared_length_are_rejected() {
for compressed in [
vec![0x10, 3, 0, 0, 0x80, 0, 0], // No preceding output.
vec![0x10, 4, 0, 0, 0x40, b'A', 0, 1], // Only one preceding byte, distance two.
vec![0x10, 3, 0, 0, 0x40, b'A', 0, 0], // Length three exceeds two remaining bytes.
] {
let (mut machine, _) = prepare(0x11, false, DEST, &compressed);
machine.memory_mut().write32(DEST, 0xcccc_cccc).unwrap();
assert_invalid(fail(&mut machine));
assert_eq!(
machine.memory().read8(DEST).unwrap(),
if compressed[4] == 0x80 { 0xcc } else { b'A' }
);
assert_eq!(machine.memory().read8(DEST + 1).unwrap(), 0xcc);
}
}
#[test]
fn vram_variant_rejects_odd_sizes_odd_destinations_and_distance_one() {
for (destination, compressed) in [
(VRAM_START + 4, vec![0x10, 1, 0, 0, 0, 0x77]),
(VRAM_START + 5, vec![0x10, 2, 0, 0, 0, 0x77, 0x88]),
(VRAM_START + 4, vec![0x10, 4, 0, 0, 0x40, 0x77, 0, 0]),
] {
let (mut machine, _) = prepare(0x12, true, destination, &compressed);
machine
.memory_mut()
.write32(VRAM_START + 4, 0xcccc_cccc)
.unwrap();
assert_invalid(fail(&mut machine));
assert_eq!(
machine.memory().read32(VRAM_START + 4).unwrap(),
0xcccc_cccc
);
}
}
#[test]
fn truncated_rom_inputs_produce_memory_diagnostics() {
for compressed in [
vec![],
vec![0x10, 2],
vec![0x10, 2, 0, 0],
vec![0x10, 2, 0, 0, 0],
vec![0x10, 3, 0, 0, 0x80, 0],
] {
let (mut machine, _) = prepare(0x11, false, DEST, &compressed);
assert_eq!(
fail(&mut machine),
MachineError::Cpu(MemoryError::Unmapped(ROM_DATA + compressed.len() as u32).into())
);
}
}
#[test]
fn halfword_output_never_commits_a_lone_pending_byte_on_failure() {
for service in [0x11, 0x12] {
let compressed = [0x10, 2, 0, 0, 0, b'A']; // Second literal is missing.
let (mut machine, _) = prepare(service, false, DEST, &compressed);
machine.memory_mut().write16(DEST, 0xcccc).unwrap();
assert!(matches!(
fail(&mut machine),
MachineError::Cpu(CpuError::Memory(_))
));
assert_eq!(
machine.memory().read16(DEST).unwrap(),
if service == 0x11 { 0xcc41 } else { 0xcccc }
);
}
}
#[test]
fn destination_failure_preserves_preceding_completed_halfword() {
let (compressed, _) = stream(&[
Token::Literal(0x12),
Token::Literal(0x34),
Token::Literal(0x56),
Token::Literal(0x78),
]);
let (mut machine, _) = prepare(0x12, false, 0x07ff_fffe, &compressed);
assert_eq!(
fail(&mut machine),
MachineError::Cpu(MemoryError::ReadOnly(0x0800_0000).into())
);
assert_eq!(machine.memory().read16(0x07ff_fffe).unwrap(), 0x3412);
}

View file

@ -0,0 +1,492 @@
use super::*;
use gba_core::{
bios::INVALID_ARGUMENT_TRAP,
dma::{DmaError, DMA_BASE, DMA_STRIDE},
input::Buttons,
io::{
BG0CNT, BG2PA, BG2PD, BG3PA, BG3PD, BLDALPHA, BLDCNT, DISPCNT, GREENSWAP, KEYINPUT,
WAITCNT, WININ, WINOUT,
},
memory::{OAM_START, PALETTE_START},
video::Framebuffer,
};
const REGIONS: [(u32, u32, u32); 5] = [
(1, 0x0200_0000, 0x40000),
(2, 0x0300_0000, 0x7e00),
(4, PALETTE_START, 0x400),
(8, VRAM_START, 0x18000),
(16, OAM_START, 0x400),
];
const MARKER: u32 = 0xa55a_c33c;
fn literal(code: &mut Vec<u32>, register: u32, value: u32) {
code.extend([0xe59f_0000 | register << 12, 0xea00_0000, value]);
}
fn prepare(flags: u32, thumb: bool, status: u32) -> (Machine, u32) {
prepare_at(flags, thumb, status, None)
}
fn prepare_at(flags: u32, thumb: bool, status: u32, location: Option<u32>) -> (Machine, u32) {
let mut code = Vec::new();
literal(&mut code, 0, flags);
for r in 1..=12 {
code.push(0xe3a0_0080 | r << 12 | r);
}
code.extend([0xe328_f20f, 0xe321_f000 | status]);
let swi_pc = if let Some(address) = location {
literal(&mut code, 14, address | u32::from(thumb));
code.push(0xe12f_ff1e);
address
} else {
code.extend([
if thumb { 0xe28f_e001 } else { 0xe1a0_e00f },
if thumb { 0xe12f_ff1e } else { 0xe1a0_0000 },
]);
let address = ROM_START + code.len() as u32 * 4;
code.extend([if thumb { 0xe7fe_df01 } else { 0xef01_0000 }, 0xeaff_fffe]);
address
};
let mut m = bios::boot(words(&code)).unwrap();
if let Some(address) = location {
m.memory_mut()
.write32(address, if thumb { 0xe7fe_df01 } else { 0xef01_0000 })
.unwrap();
m.memory_mut().write32(address + 4, 0xeaff_fffe).unwrap();
}
reach(&mut m, swi_pc, 200);
(m, swi_pc + if thumb { 2 } else { 4 })
}
fn complete(m: &mut Machine, pc: u32) {
let before = m.cpu().clone();
m.step().unwrap();
reach(m, pc, 400_000);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
assert_eq!(m.memory().read16(DISPCNT).unwrap(), 0x80);
}
fn samples(start: u32, bytes: u32) -> [u32; 4] {
[start, start + 4, start + bytes / 2, start + bytes - 4]
}
fn seed_samples(m: &mut Machine) {
for (_, start, bytes) in REGIONS {
for address in samples(start, bytes) {
m.memory_mut().write32(address, MARKER).unwrap();
}
}
}
fn check_samples(m: &Machine, flags: u32) {
for (bit, start, bytes) in REGIONS {
for address in samples(start, bytes) {
assert_eq!(
m.memory().read32(address).unwrap(),
if flags & bit == 0 { MARKER } else { 0 },
"flags={flags:#x}, address={address:#x}"
);
}
}
}
#[test]
fn each_memory_flag_clears_its_entire_region_and_no_other_region() {
for thumb in [false, true] {
for (flag, _, _) in REGIONS {
let (mut m, pc) = prepare(flag, thumb, 0x1f);
for (_, start, bytes) in REGIONS {
for address in (start..start + bytes).step_by(4) {
m.memory_mut().write32(address, MARKER ^ address).unwrap();
}
}
complete(&mut m, pc);
for (bit, start, bytes) in REGIONS {
for address in (start..start + bytes).step_by(4) {
assert_eq!(
m.memory().read32(address).unwrap(),
if bit == flag { 0 } else { MARKER ^ address },
"flag={flag}, address={address:#x}"
);
}
}
}
}
}
#[test]
fn all_memory_flag_combinations_are_independent_from_arm_and_thumb() {
for thumb in [false, true] {
for flags in 0..32 {
let (mut m, pc) = prepare(flags, thumb, 0x1f);
seed_samples(&mut m);
complete(&mut m, pc);
check_samples(&m, flags);
}
}
}
#[test]
fn zero_flags_still_force_blank_and_clear_other_display_control_bits() {
for thumb in [false, true] {
let (mut m, pc) = prepare(0, thumb, 0x1f);
seed_samples(&mut m);
m.memory_mut().write16(DISPCNT, 0x1403).unwrap();
m.memory_mut().write16(BG0CNT, 0x1234).unwrap();
complete(&mut m, pc);
check_samples(&m, 0);
assert_eq!(m.memory().read16(BG0CNT).unwrap(), 0x1204);
let mut frame = Framebuffer::default();
m.memory().render_frame(&mut frame).unwrap();
assert!(frame.pixels().iter().all(|p| *p == 0xffffff));
}
}
#[test]
fn reserved_high_bits_are_ignored_and_original_argument_is_restored() {
for flags in [
0x8000_0000,
0xffff_ff00,
0x1234_5604,
0xffff_ff80,
0xffff_ff9f,
] {
for thumb in [false, true] {
let (mut m, pc) = prepare(flags, thumb, 0x1f);
seed_samples(&mut m);
complete(&mut m, pc);
check_samples(&m, flags);
}
}
}
#[test]
fn internal_ram_clear_preserves_bios_work_area_except_common_swi_frame() {
for thumb in [false, true] {
let (mut m, pc) = prepare(0x9f, thumb, 0x1f);
for address in (0x0300_7e00..0x0300_8000).step_by(4) {
m.memory_mut().write32(address, MARKER ^ address).unwrap();
}
complete(&mut m, pc);
for address in (0x0300_7e00..0x0300_8000).step_by(4) {
if (bios::SVC_STACK - 28..bios::SVC_STACK).contains(&address) {
continue;
}
assert_eq!(
m.memory().read32(address).unwrap(),
MARKER ^ address,
"{address:#x}"
);
}
assert_eq!(m.memory().read32(0x0300_7dfc).unwrap(), 0);
}
}
#[test]
fn all_serial_sound_requests_fail_before_forced_blank_or_memory_clear() {
for thumb in [false, true] {
for flags in (0..256).filter(|flags| flags & 0x60 != 0) {
let (mut m, _) = prepare(flags, thumb, 0x1f);
seed_samples(&mut m);
m.memory_mut().write16(DISPCNT, 0x0403).unwrap();
for _ in 0..1000 {
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));
break;
}
}
assert!(m.step().is_err());
assert_eq!(m.memory().read16(DISPCNT).unwrap(), 0x0403);
check_samples(&m, 0);
}
}
}
#[test]
fn caller_registers_flags_modes_and_interrupt_masks_are_preserved() {
for thumb in [false, true] {
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
for flags in [0, 4, 0x80] {
let (mut m, pc) = prepare(flags, thumb, status);
assert!(m.cpu().registers()[1..13].iter().all(|r| *r != 0));
complete(&mut m, pc);
}
}
}
}
#[test]
fn io_reset_clears_readable_controls_but_keeps_input_postflg_and_green_swap() {
for flags in [0x80, 0x9f] {
let (mut m, pc) = prepare(flags, false, 0x9f);
m.memory_mut().set_buttons(Buttons::from_bits(0x251));
let keys = m.memory().read16(KEYINPUT).unwrap();
m.memory_mut().write16(GREENSWAP, 1).unwrap();
for address in [
BG0CNT,
BG0CNT + 2,
BG0CNT + 4,
BG0CNT + 6,
WININ,
WINOUT,
BLDCNT,
BLDALPHA,
WAITCNT,
IE,
] {
m.memory_mut().write16(address, 0xffff).unwrap();
}
m.memory_mut().write16(DISPSTAT, 0x7b38).unwrap();
m.memory_mut().write16(IME, 1).unwrap();
// Overflow queues IF, independently of CPU execution.
m.memory_mut().write32(TIMER_BASE, 0x00c0_ffff).unwrap();
m.memory_mut().advance_cycles(1);
assert_ne!(m.memory().read16(IF).unwrap(), 0);
let cycles = m.cycles();
complete(&mut m, pc);
for address in [
BG0CNT,
BG0CNT + 2,
BG0CNT + 4,
BG0CNT + 6,
WININ,
WINOUT,
BLDCNT,
BLDALPHA,
WAITCNT,
IE,
IF,
IME,
] {
assert_eq!(m.memory().read16(address).unwrap(), 0, "{address:#x}");
}
assert_eq!(m.memory().read16(DISPSTAT).unwrap() & 0xff38, 0);
assert_eq!(m.memory().read16(KEYINPUT).unwrap(), keys);
assert_eq!(m.memory().read16(POSTFLG).unwrap(), 1);
assert_eq!(m.memory().read16(GREENSWAP).unwrap(), 1);
assert!(m.cycles() > cycles);
assert!(!m.halted());
}
}
#[test]
fn io_reset_clears_write_only_display_state_and_restores_both_identity_matrices() {
let (mut m, pc) = prepare(0x80, true, 0x1f);
let mut reference = Memory::new(vec![]).unwrap();
for (_, start, bytes) in [REGIONS[2], REGIONS[3]] {
for offset in (0..bytes).step_by(2) {
let value = ((offset * 37 + 1) ^ (offset >> 4)) as u16;
m.memory_mut().write16(start + offset, value).unwrap();
reference.write16(start + offset, value).unwrap();
}
}
for address in (BG0CNT..0x0400_0058).step_by(2) {
m.memory_mut().write16(address, 0x5353).unwrap();
}
complete(&mut m, pc);
for address in [BG2PA, BG2PD, BG3PA, BG3PD] {
reference.write16(address, 256).unwrap();
}
for control in [
0x0100, 0x0200, 0x0400, 0x0800, 0x0402, 0x0802, 0x0403, 0x2403, 0x4403, 0x6403,
] {
for bus in [m.memory_mut(), &mut reference] {
bus.write16(DISPCNT, control).unwrap();
// Enable mosaic in each background without altering its offset/matrix.
for address in (BG0CNT..=BG0CNT + 6).step_by(2) {
bus.write16(address, 0x40).unwrap();
}
}
let mut actual = Framebuffer::default();
let mut expected = Framebuffer::default();
m.memory().render_frame(&mut actual).unwrap();
reference.render_frame(&mut expected).unwrap();
assert_eq!(actual.pixels(), expected.pixels(), "DISPCNT={control:#x}");
}
}
#[test]
fn timer_reset_stops_counters_clears_reload_and_does_not_replace_counter_state() {
let (mut m, pc) = prepare(0x80, false, 0x1f);
for timer in 0..4 {
m.memory_mut()
.write32(TIMER_BASE + timer * 4, 0x0080_4321)
.unwrap();
}
complete(&mut m, pc);
for timer in 0..4 {
let address = TIMER_BASE + timer * 4;
assert_eq!(m.memory().read16(address + 2).unwrap(), 0);
let counter = m.memory().read16(address).unwrap();
assert!(counter >= 0x4321);
m.memory_mut().advance_cycles(1000);
assert_eq!(m.memory().read16(address).unwrap(), counter);
m.memory_mut().write16(address + 2, 0x80).unwrap();
assert_eq!(m.memory().read16(address).unwrap(), 0); // Reload latch was cleared.
m.memory_mut().write16(address + 2, 0).unwrap();
}
}
#[test]
fn dma_reset_disables_channels_and_clears_write_only_address_and_count_latches() {
for channel in 0..4 {
let (mut m, pc) = prepare(0x80, false, 0x1f);
let base = DMA_BASE + channel as u32 * DMA_STRIDE;
m.memory_mut().write32(base, SOURCE).unwrap();
m.memory_mut().write32(base + 4, DEST).unwrap();
m.memory_mut().write32(base + 8, 0x9200_0001).unwrap(); // Repeating VBlank, count one.
complete(&mut m, pc);
assert_eq!(m.memory().read16(base + 10).unwrap(), 0);
// With only control re-enabled, the source must now be zero.
m.memory_mut().write16(base + 10, 0x8000).unwrap();
assert_eq!(
m.step(),
Err(MachineError::Dma(DmaError::UnsupportedSource {
channel,
address: 0
}))
);
m.memory_mut().write16(base + 10, 0).unwrap();
m.memory_mut().write32(base, SOURCE).unwrap();
m.memory_mut().write16(base + 10, 0x8000).unwrap();
assert_eq!(
m.step(),
Err(MachineError::Dma(DmaError::Memory {
channel,
error: MemoryError::ReadOnly(0)
}))
);
m.memory_mut().write16(base + 10, 0).unwrap();
m.memory_mut().write32(base + 4, DEST).unwrap();
m.memory_mut().write32(SOURCE, MARKER).unwrap();
m.memory_mut().write16(base + 10, 0x8500).unwrap(); // Fixed source, words; count remains zero.
assert_eq!(m.step().unwrap(), StepKind::Dma { channel });
assert_eq!(m.step().unwrap(), StepKind::Dma { channel });
assert_eq!(m.memory().read32(DEST).unwrap(), MARKER);
assert_eq!(m.memory().read32(DEST + 4).unwrap(), MARKER);
// Count zero expands to the channel maximum, not the stale count of one.
assert_ne!(m.memory().read16(base + 10).unwrap() & 0x8000, 0);
m.memory_mut().write16(base + 10, 0).unwrap();
}
}
#[test]
fn memory_only_reset_keeps_io_and_allows_dma_timers_and_deferred_irq() {
for thumb in [false, true] {
let (mut m, pc) = prepare(4, thumb, 0x1f);
let before = m.cpu().clone();
m.step().unwrap();
m.memory_mut().write16(BG0CNT, 0x41).unwrap();
m.memory_mut().write16(WAITCNT, 0x4317).unwrap();
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, MARKER).unwrap();
m.memory_mut().write32(DMA_BASE, SOURCE).unwrap();
m.memory_mut().write32(DMA_BASE + 4, DEST).unwrap();
m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap();
let mut dma = 0;
for _ in 0..4000 {
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!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
assert_eq!(dma, 1);
assert_eq!(m.memory().read32(DEST).unwrap(), MARKER);
for (address, value) in [
(BG0CNT, 0x41),
(WAITCNT, 0x4317),
(IE, 8),
(IME, 1),
(TIMER_BASE + 2, 0xc0),
] {
assert_eq!(m.memory().read16(address).unwrap(), value);
}
assert_ne!(m.memory().read16(IF).unwrap() & 8, 0);
assert_eq!(m.step().unwrap(), StepKind::IrqEntry);
}
}
#[test]
fn clear_is_visible_one_word_at_a_time_and_uses_normal_video_bus_stores() {
for flag in [1, 2, 4, 8, 16] {
let (mut m, pc) = prepare(flag, false, 0x1f);
let (_, start, _) = REGIONS.into_iter().find(|r| r.0 == flag).unwrap();
m.memory_mut().write32(start, MARKER).unwrap();
m.memory_mut().write32(start + 4, MARKER).unwrap();
m.step().unwrap();
for _ in 0..1000 {
if m.memory().read32(start).unwrap() == 0 {
break;
}
m.step().unwrap();
}
assert_eq!(m.memory().read32(start).unwrap(), 0);
assert_eq!(m.memory().read32(start + 4).unwrap(), MARKER);
assert_eq!(m.memory().read16(DISPCNT).unwrap(), 0x80);
assert!(m.cpu().pc() < BIOS_SIZE as u32);
reach(&mut m, pc, 400_000);
assert_eq!(m.memory().read32(start + 4).unwrap(), 0);
}
}
#[test]
fn ram_callers_can_return_but_selected_ram_code_is_really_erased() {
for thumb in [false, true] {
for (location, clear_flag) in [(SOURCE + 0x100, 1), (0x0300_1000, 2)] {
for flags in [4, clear_flag] {
let (mut m, pc) = prepare_at(flags, thumb, 0x1f, Some(location));
complete(&mut m, pc);
assert_eq!(
m.memory().read32(location).unwrap(),
if flags == clear_flag {
0
} else if thumb {
0xe7fe_df01
} else {
0xef01_0000
}
);
}
}
}
}
#[test]
fn cleared_oam_is_zero_not_the_disabled_sprite_encoding() {
let (mut m, pc) = prepare(16, false, 0x1f);
m.memory_mut().write16(OAM_START, 0x200).unwrap();
m.memory_mut().write16(PALETTE_START + 0x202, 31).unwrap();
m.memory_mut()
.write16(VRAM_START + 0x10000, 0x1111)
.unwrap();
complete(&mut m, pc);
assert_eq!(m.memory().read16(OAM_START).unwrap(), 0);
m.memory_mut().write16(DISPCNT, 0x1000).unwrap();
let mut frame = Framebuffer::default();
m.memory().render_frame(&mut frame).unwrap();
assert_eq!(frame.pixels()[0], 0xff0000);
}

View file

@ -0,0 +1,363 @@
use super::*;
use gba_core::{
dma::DMA_BASE,
io::{BG0CNT, DISPCNT, WAITCNT},
memory::{OAM_START, PALETTE_START},
};
const FLAG: u32 = 0x0300_7ffa;
const CLEAR_START: u32 = 0x0300_7e00;
const CLEAR_END: u32 = 0x0300_8000;
fn restart(m: &mut Machine, flag: u8) {
m.memory_mut().write8(FLAG, flag).unwrap();
m.step().unwrap(); // Enter the real SWI vector.
reach(m, if flag == 0 { ROM_START } else { SOURCE }, 2000);
}
fn assert_restart_state(m: &Machine, target: u32) {
assert_eq!(m.cpu().pc(), target);
assert_eq!(m.cpu().mode(), Mode::System);
assert_eq!(m.cpu().instruction_set(), InstructionSet::Arm);
assert_eq!(m.cpu().cpsr(), 0x9f); // Deterministic policy: IRQ masked, FIQ clear, NZCV clear.
assert_eq!(&m.cpu().registers()[..13], &[0; 13]);
assert_eq!(m.cpu().registers()[13], SYSTEM_STACK);
assert_eq!(m.cpu().registers()[14], target);
}
fn assert_cleared(m: &Machine) {
for address in CLEAR_START..CLEAR_END {
assert_eq!(m.memory().read8(address).unwrap(), 0, "{address:#x}");
}
}
// Inline literal with a branch over its data. No host register mutation.
fn literal(code: &mut Vec<u32>, register: u32, value: u32) {
code.extend([0xe59f_0000 | register << 12, 0xea00_0000, value]);
}
fn dirty_sp(mode: u32) -> u32 {
0x0300_7000 + mode * 32
}
fn dirty_lr(mode: u32) -> u32 {
0xa100_0000 | mode
}
fn seeded_call(thumb: bool, status: u32, location: Option<u32>) -> Machine {
let mut code = Vec::new();
for mode in [0x11, 0x12, 0x13, 0x17, 0x1b] {
code.push(0xe321_f0c0 | mode); // Switch bank with IRQ/FIQ masked.
literal(&mut code, 13, dirty_sp(mode));
literal(&mut code, 14, dirty_lr(mode));
literal(&mut code, 0, 0xb000_00df);
code.push(0xe169_f000); // MSR SPSR_fc,r0
if mode == 0x11 {
for r in 8..=12 {
code.push(0xe3a0_00e0 | r << 12 | r); // Distinct FIQ high registers.
}
}
}
code.push(0xe321_f0df);
literal(&mut code, 13, 0x0300_7dc0);
for r in 0..=12 {
code.push(0xe3a0_0080 | r << 12 | r);
}
code.extend([0xe328_f20f, 0xe321_f000 | status]); // NZCV set; caller mode/masks.
let swi_pc = if let Some(address) = location {
literal(&mut code, 14, address | u32::from(thumb));
code.push(0xe12f_ff1e);
address
} else {
code.extend([
if thumb { 0xe28f_e001 } else { 0xe1a0_e00f },
if thumb { 0xe12f_ff1e } else { 0xe1a0_0000 },
]);
let address = ROM_START + code.len() as u32 * 4;
code.extend([if thumb { 0xe7fe_df00 } else { 0xef00_0000 }, 0xeaff_fffe]);
address
};
let mut m = bios::boot(words(&code)).unwrap();
if let Some(address) = location {
m.memory_mut()
.write32(address, if thumb { 0xe7fe_df00 } else { 0xef00_0000 })
.unwrap();
m.memory_mut().write32(address + 4, 0xeaff_fffe).unwrap();
}
reach(&mut m, swi_pc, 500);
m
}
#[test]
fn every_flag_byte_selects_rom_or_ram_from_arm_and_thumb() {
for thumb in [false, true] {
for flag in 0..=255 {
let (mut m, _) = call(0, thumb, [0x123, 0x456, 0x789]);
// Only the byte at 7ffa selects the target, not surrounding bytes.
m.memory_mut().write32(IRQ_FLAGS, 0xffff_ffff).unwrap();
restart(&mut m, flag);
assert_restart_state(&m, if flag == 0 { ROM_START } else { SOURCE });
assert_cleared(&m);
}
}
}
#[test]
fn clears_exactly_top_512_bytes_and_keeps_other_memory() {
for thumb in [false, true] {
let (mut m, _) = call(0, thumb, [0, 0, 0]);
for address in (0x0300_0000..CLEAR_END).step_by(4) {
m.memory_mut()
.write32(address, address ^ 0xa55a_c33c)
.unwrap();
}
let sentinels = [
SOURCE,
SOURCE + 0x3fffc,
PALETTE_START,
PALETTE_START + 0x3fc,
VRAM_START,
VRAM_START + 0x17ffc,
OAM_START,
OAM_START + 0x3fc,
];
for address in sentinels {
m.memory_mut().write32(address, 0x1357_2468).unwrap();
}
let rom_first = m.memory().read32(ROM_START).unwrap();
restart(&mut m, 0);
assert_cleared(&m);
for address in (0x0300_0000..CLEAR_START).step_by(4) {
assert_eq!(m.memory().read32(address).unwrap(), address ^ 0xa55a_c33c);
}
for address in sentinels {
assert_eq!(m.memory().read32(address).unwrap(), 0x1357_2468);
}
assert_eq!(m.memory().read32(ROM_START).unwrap(), rom_first);
}
}
#[test]
fn resets_nonzero_registers_and_status_for_user_system_callers() {
for thumb in [false, true] {
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
for flag in [0, 1] {
let mut m = seeded_call(thumb, status, None);
assert!(m.cpu().registers()[..13].iter().all(|r| *r != 0));
assert_eq!(m.cpu().cpsr() & 0xf000_0000, 0xf000_0000);
restart(&mut m, flag);
assert_restart_state(&m, if flag == 0 { ROM_START } else { SOURCE });
assert_cleared(&m);
}
}
}
}
#[test]
fn reinitializes_supervisor_irq_stacks_links_and_saved_status() {
for thumb in [false, true] {
let mut m = seeded_call(thumb, 0x1f, None);
for (i, mode) in [0xd3, 0xd2, 0xdf].into_iter().enumerate() {
m.memory_mut()
.write32(SOURCE + i as u32 * 4, 0xe321_f000 | mode)
.unwrap();
}
restart(&mut m, 1);
for (mode, stack) in [
(Mode::Supervisor, bios::SVC_STACK),
(Mode::Irq, bios::IRQ_STACK),
] {
m.step().unwrap();
assert_eq!(m.cpu().mode(), mode);
assert_eq!(m.cpu().registers()[13], stack);
assert_eq!(m.cpu().registers()[14], 0);
assert_eq!(m.cpu().spsr(), Some(0));
}
m.step().unwrap();
assert_eq!(m.cpu().mode(), Mode::System);
assert_eq!(m.cpu().registers()[13], SYSTEM_STACK);
assert_eq!(m.cpu().registers()[14], SOURCE);
assert_cleared(&m);
}
}
#[test]
fn unrelated_banked_registers_and_saved_status_are_preserved() {
let mut m = seeded_call(false, 0x1f, None);
for (i, mode) in [0x11, 0x17, 0x1b].into_iter().enumerate() {
m.memory_mut()
.write32(SOURCE + i as u32 * 4, 0xe321_f0c0 | mode)
.unwrap();
}
restart(&mut m, 1);
for mode in [Mode::Fiq, Mode::Abort, Mode::Undefined] {
m.step().unwrap();
assert_eq!(m.cpu().mode(), mode);
assert_eq!(m.cpu().registers()[13], dirty_sp(mode as u32));
assert_eq!(m.cpu().registers()[14], dirty_lr(mode as u32));
assert_eq!(m.cpu().spsr(), Some(0xb000_00df));
if mode == Mode::Fiq {
assert_eq!(&m.cpu().registers()[8..13], &[0xe8, 0xe9, 0xea, 0xeb, 0xec]);
} else {
assert_eq!(&m.cpu().registers()[8..13], &[0; 5]);
}
}
}
#[test]
fn can_restart_from_work_ram_including_code_in_erased_area() {
for thumb in [false, true] {
for location in [SOURCE + 0x100, 0x0300_1000, CLEAR_START, 0x03ff_fe00] {
let mut m = seeded_call(thumb, 0x1f, Some(location));
restart(&mut m, 1);
assert_restart_state(&m, SOURCE);
assert_cleared(&m);
if location < CLEAR_START {
assert_eq!(
m.memory().read32(location).unwrap(),
if thumb { 0xe7fe_df00 } else { 0xef00_0000 }
);
}
}
}
}
#[test]
fn ram_restart_executes_arm_code_and_can_call_bios_again() {
for thumb in [false, true] {
let mut m = seeded_call(thumb, 0x10, None);
// This program is only valid as ARM: Sqrt(81), then a self-branch.
for (i, instruction) in [0xe3a0_0051, 0xef08_0000, 0xeaff_fffe]
.into_iter()
.enumerate()
{
m.memory_mut()
.write32(SOURCE + i as u32 * 4, instruction)
.unwrap();
}
restart(&mut m, 1);
reach(&mut m, SOURCE + 8, 1000);
assert_eq!(m.cpu().registers()[0], 9);
assert_eq!(m.cpu().registers()[13], SYSTEM_STACK);
assert_eq!(m.cpu().registers()[14], SOURCE);
assert_eq!(m.cpu().cpsr(), 0x9f);
}
}
#[test]
fn does_not_reset_io_or_device_clocks() {
for thumb in [false, true] {
let (mut m, _) = call(0, thumb, [0, 0, 0]);
for (address, value) in [
(DISPCNT, 0x0403),
(BG0CNT, 0x0141),
(WAITCNT, 0x4317),
(DISPSTAT, 0x7b38),
(IE, 0x1234),
(IME, 1),
] {
m.memory_mut().write16(address, value).unwrap();
}
m.memory_mut().write32(TIMER_BASE, 0x0080_0000).unwrap();
let timer_before = m.memory().read16(TIMER_BASE).unwrap();
let cycles = m.cycles();
restart(&mut m, 0);
for (address, value) in [
(DISPCNT, 0x0403),
(BG0CNT, 0x0141),
(WAITCNT, 0x4317),
(IE, 0x1234),
(IME, 1),
(POSTFLG, 1),
(TIMER_BASE + 2, 0x80),
] {
assert_eq!(m.memory().read16(address).unwrap(), value, "{address:#x}");
}
assert_eq!(m.memory().read16(DISPSTAT).unwrap() & 0xff38, 0x7b38);
let elapsed = m.cycles() - cycles;
assert!(elapsed > 512 && elapsed < 65536);
assert_eq!(
m.memory().read16(TIMER_BASE).unwrap(),
timer_before.wrapping_add(elapsed as u16)
);
assert!(!m.halted());
}
}
#[test]
fn dma_timers_and_pending_irq_continue_but_irq_stays_masked_after_restart() {
for thumb in [false, true] {
for ime in [0, 1] {
let (mut m, _) = call(0, thumb, [0, 0, 0]);
m.memory_mut().write8(FLAG, 1).unwrap();
m.memory_mut().write32(SOURCE, 0xeaff_fffe).unwrap();
m.step().unwrap();
// Queue IRQ and DMA only after SWI entry; otherwise IRQ can win first.
m.memory_mut().write16(IE, 8).unwrap();
m.memory_mut().write16(IME, ime).unwrap();
m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap();
m.memory_mut().write32(DEST, 0x8765_4321).unwrap();
m.memory_mut().write32(DMA_BASE, DEST).unwrap();
m.memory_mut().write32(DMA_BASE + 4, DEST + 4).unwrap();
m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap();
let mut dma = 0;
for _ in 0..2000 {
if m.cpu().pc() == SOURCE {
break;
}
let step = m.step().unwrap();
assert_ne!(step, StepKind::IrqEntry);
if matches!(step, StepKind::Dma { channel: 0 }) {
dma += 1;
}
}
assert_restart_state(&m, SOURCE);
assert_eq!(dma, 1);
assert_eq!(m.memory().read32(DEST + 4).unwrap(), 0x8765_4321);
assert_eq!(m.memory().read16(IF).unwrap() & 8, 8);
assert_eq!(m.memory().read16(IME).unwrap(), ime);
assert_cleared(&m);
assert_eq!(m.step().unwrap(), StepKind::Instruction);
assert_eq!(m.cpu().pc(), SOURCE);
}
}
}
#[test]
fn reset_is_not_an_atomic_host_memory_clear() {
let (mut m, _) = call(0, false, [0, 0, 0]);
for address in (CLEAR_START..CLEAR_END).step_by(4) {
m.memory_mut().write32(address, 0xa55a_c33c).unwrap();
}
m.memory_mut().write8(FLAG, 0).unwrap();
m.step().unwrap();
for _ in 0..1000 {
if m.memory().read32(CLEAR_START).unwrap() == 0 {
break;
}
m.step().unwrap();
}
assert_eq!(m.memory().read32(CLEAR_START).unwrap(), 0);
assert_eq!(m.memory().read32(CLEAR_START + 4).unwrap(), 0xa55a_c33c);
assert_eq!(m.memory().read32(IRQ_HANDLER).unwrap(), 0xa55a_c33c);
assert!(m.cpu().pc() < BIOS_SIZE as u32);
reach(&mut m, ROM_START, 2000);
assert_cleared(&m);
}
#[test]
fn exception_mode_callers_still_receive_a_diagnostic() {
for status in [0x12, 0x13] {
let (mut m, _) = call_status(0, false, [0, 0, 0], status);
m.memory_mut().write32(CLEAR_START, 0x1357_2468).unwrap();
let error = (0..100).find_map(|_| m.step().err()).unwrap();
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: UNSUPPORTED_TRAP,
..
})
));
assert_eq!(m.memory().read32(CLEAR_START).unwrap(), 0x1357_2468);
}
}

View file

@ -0,0 +1,395 @@
use super::*;
use gba_core::{bios::INVALID_ARGUMENT_TRAP, dma::DMA_BASE, memory::OAM_START};
#[derive(Clone)]
enum Block {
Literal(Vec<u8>),
Repeat(u8, usize),
}
/// Encode structured blocks and independently expand their expected bytes.
fn stream(blocks: &[Block]) -> (Vec<u8>, Vec<u8>) {
let mut encoded = vec![0x30, 0, 0, 0];
let mut expected = Vec::new();
for block in blocks {
match block {
Block::Literal(bytes) => {
assert!((1..=128).contains(&bytes.len()));
encoded.push((bytes.len() - 1) as u8);
encoded.extend(bytes);
expected.extend(bytes);
}
Block::Repeat(byte, count) => {
assert!((3..=130).contains(count));
encoded.extend([0x80 | (*count - 3) as u8, *byte]);
expected.extend(std::iter::repeat_n(*byte, *count));
}
}
}
encoded[..4].copy_from_slice(&((expected.len() as u32) << 8 | 0x30).to_le_bytes());
(encoded, expected)
}
fn prepare(service: u8, thumb: bool, destination: u32, data: &[u8]) -> (Machine, u32) {
call_status_data(
service,
thumb,
[ROM_DATA, destination, 0xa55a_1234],
0x1f,
data,
)
}
fn complete(m: &mut Machine, return_pc: u32) {
m.step().unwrap();
reach(m, return_pc, 2_000_000);
}
fn verify(service: u8, thumb: bool, destination: u32, data: &[u8], expected: &[u8]) {
let (mut m, return_pc) = prepare(service, thumb, destination, data);
let before = m.cpu().clone();
let end = destination + expected.len() as u32;
// Halfword sentinels support both RAM and video memory.
for address in [(destination - 1) & !1, end & !1] {
m.memory_mut().write16(address, 0xa55a).unwrap();
}
// The 60-byte SVC frame must not touch the IRQ stack boundary at SVC_SP-64.
m.memory_mut()
.write32(bios::IRQ_STACK, 0x5a5a_a5a5)
.unwrap();
complete(&mut m, return_pc);
for (index, byte) in expected.iter().enumerate() {
assert_eq!(
m.memory().read8(destination + index as u32).unwrap(),
*byte,
"byte={index}"
);
}
for address in [destination - 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 stream did not reach a bounded diagnostic");
}
fn assert_invalid(error: MachineError) {
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: INVALID_ARGUMENT_TRAP,
..
})
));
}
#[test]
fn every_block_control_value_matches_reference_in_arm_and_thumb() {
for flag in 0u8..=255 {
let count = usize::from(flag & 127) + if flag & 128 == 0 { 1 } else { 3 };
let block = if flag & 128 == 0 {
Block::Literal((0..count).map(|i| (i * 37) as u8).collect())
} else {
Block::Repeat(flag, count)
};
let mut blocks = vec![block];
// The final literal can complete a halfword started by the prior block.
if count % 2 != 0 {
blocks.push(Block::Literal(vec![0x42]));
}
let (data, expected) = stream(&blocks);
assert_eq!(data[4], flag);
for thumb in [false, true] {
verify(0x14, thumb, DEST + 1, &data, &expected);
verify(0x15, thumb, VRAM_START + 4, &data, &expected);
}
}
}
#[test]
fn repeated_values_cover_all_bytes_and_cross_halfword_boundaries() {
let blocks: Vec<_> = (0..=255).map(|byte| Block::Repeat(byte, 3)).collect();
let (data, expected) = stream(&blocks);
verify(0x14, false, DEST, &data, &expected);
verify(0x15, true, DEST, &data, &expected);
verify(0x15, false, VRAM_START + 4, &data, &expected);
}
#[test]
fn seeded_mixed_streams_match_independent_block_expansion() {
let mut seed = 0x83ca_206bu32;
for case in 0..64 {
let mut blocks = Vec::new();
let mut length = 0;
for _ in 0..32 {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
let repeat = seed & 128 != 0;
let count = ((seed >> 8) & 127) as usize + if repeat { 3 } else { 1 };
blocks.push(if repeat {
Block::Repeat((seed >> 24) as u8, count)
} else {
Block::Literal(
(0..count)
.map(|i| (seed.wrapping_add(i as u32 * 71) >> 3) as u8)
.collect(),
)
});
length += count;
}
if length % 2 != 0 {
blocks.push(Block::Literal(vec![0xab]));
}
let (data, expected) = stream(&blocks);
verify(0x14, case % 2 == 0, DEST, &data, &expected);
verify(0x15, case % 2 != 0, VRAM_START + 4, &data, &expected);
}
}
#[test]
fn output_length_uses_all_twenty_four_header_bits() {
let mut blocks = vec![Block::Repeat(0x5a, 130); 504];
blocks.push(Block::Repeat(0xa5, 16)); // 65,536 bytes, crossing the 16-bit count boundary.
let (data, expected) = stream(&blocks);
assert_eq!(expected.len(), 65536);
assert_eq!(&data[..4], &[0x30, 0, 0, 1]);
verify(0x14, false, DEST, &data, &expected);
verify(0x15, true, DEST, &data, &expected);
}
#[test]
fn source_can_be_in_either_work_ram_region() {
let (data, expected) = stream(&[Block::Literal(vec![1, 2, 3]), Block::Repeat(0xaa, 3)]);
for source in [SOURCE, 0x0300_0100] {
for service in [0x14, 0x15] {
let (mut m, return_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, return_pc);
for (index, byte) in expected.iter().enumerate() {
assert_eq!(m.memory().read8(DEST + index as u32).unwrap(), *byte);
}
}
}
}
#[test]
fn user_system_status_and_all_caller_registers_are_restored() {
let (data, _) = stream(&[Block::Repeat(0x88, 3), Block::Literal(vec![0x12])]);
for service in [0x14, 0x15] {
for thumb in [false, true] {
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
let (mut m, return_pc) =
call_status_data(service, thumb, [ROM_DATA, DEST, 0x12345678], status, &data);
let before = m.cpu().clone();
complete(&mut m, return_pc);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
}
}
}
}
#[test]
fn zero_length_reads_no_blocks_and_does_not_access_destination() {
for service in [0x14, 0x15] {
let (mut m, return_pc) = prepare(service, false, 0x0e00_0000, &[0x30, 0, 0, 0]);
complete(&mut m, return_pc);
}
}
#[test]
fn bad_type_source_alignment_protection_and_output_wrap_are_diagnostics() {
for service in [0x14, 0x15] {
for header in [0u32, 0x0410, 0x0420, 0x0431, 0x04ff] {
let (mut m, _) = prepare(service, false, DEST, &header.to_le_bytes());
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
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, &[0x30, 4, 0, 0]);
assert_invalid(fail(&mut m));
}
let (mut m, _) = prepare(service, false, 0xffff_fffe, &[0x30, 4, 0, 0]);
assert_invalid(fail(&mut m));
}
}
#[test]
fn halfword_variant_rejects_odd_size_or_address_before_writing() {
for (destination, data) in [
(VRAM_START + 4, vec![0x30, 3, 0, 0, 0x80, 0x77]),
(VRAM_START + 5, vec![0x30, 4, 0, 0, 0x81, 0x77]),
(VRAM_START + 5, vec![0x30, 0, 0, 0]),
] {
let (mut m, _) = prepare(0x15, true, destination, &data);
m.memory_mut().write32(VRAM_START + 4, 0xcccc_cccc).unwrap();
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(VRAM_START + 4).unwrap(), 0xcccc_cccc);
}
let (data, expected) = stream(&[Block::Repeat(0x77, 3)]);
verify(0x14, true, DEST + 1, &data, &expected);
}
#[test]
fn oversized_blocks_fail_before_payload_reads_or_writes() {
for service in [0x14, 0x15] {
for flag in [1, 0x7f, 0x80, 0xff] {
// One literal succeeds, then the next block exceeds one remaining byte.
// No payload follows the invalid block: its bounds check must fail first.
let data = [0x30, 2, 0, 0, 0, 0x12, flag];
let (mut m, _) = prepare(service, false, DEST, &data);
m.memory_mut().write16(DEST, 0xcccc).unwrap();
assert_invalid(fail(&mut m));
assert_eq!(
m.memory().read16(DEST).unwrap(),
if service == 0x14 { 0xcc12 } else { 0xcccc }
);
}
}
}
#[test]
fn every_truncated_input_prefix_returns_a_memory_diagnostic() {
let (data, _) = stream(&[
Block::Literal(vec![0x12, 0x34, 0x56]),
Block::Repeat(0x78, 3),
]);
for service in [0x14, 0x15] {
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 incomplete_halfwords_are_not_committed_after_source_failure() {
for service in [0x14, 0x15] {
// Three of four literal bytes are available. One halfword can be committed.
let data = [0x30, 4, 0, 0, 3, 0x12, 0x34, 0x56];
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(),
if service == 0x14 {
0xcc56_3412
} else {
0xcccc_3412
}
);
}
}
#[test]
fn destination_failure_preserves_earlier_completed_writes() {
let (data, _) = stream(&[Block::Literal(vec![0x12, 0x34, 0x56, 0x78])]);
let (mut m, _) = prepare(0x15, false, 0x07ff_fffe, &data);
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into())
);
assert_eq!(m.memory().read16(0x07ff_fffe).unwrap(), 0x3412);
}
#[test]
fn declared_end_ignores_padding_and_trailing_invalid_blocks() {
let (mut data, expected) = stream(&[Block::Repeat(0xab, 4)]);
data.extend([0x7f, 0xff, 0x80]); // Invalid if read as another block.
for service in [0x14, 0x15] {
verify(service, true, DEST, &data, &expected);
}
}
#[test]
fn byte_and_halfword_variants_use_normal_video_bus_write_rules() {
let (data, _) = stream(&[Block::Literal(vec![0x12, 0x34])]);
for service in [0x14, 0x15] {
for destination in [VRAM_START, OAM_START] {
let (mut m, return_pc) = prepare(service, false, destination, &data);
m.memory_mut().write16(destination, 0xcccc).unwrap();
complete(&mut m, return_pc);
let expected = if service == 0x15 {
0x3412
} else if destination == OAM_START {
0xcccc
} else {
0x3434
};
assert_eq!(m.memory().read16(destination).unwrap(), expected);
}
}
}
#[test]
fn device_clocks_and_dma_continue_while_service_masks_irq_delivery() {
let (data, expected) = stream(&[Block::Repeat(0x55, 130)]);
for service in [0x14, 0x15] {
let (mut m, return_pc) = prepare(service, false, DEST, &data);
m.step().unwrap(); // Enter Supervisor mode before requesting any IRQ.
let start = m.cycles();
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() == return_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(), return_pc);
assert_eq!(dma, 1);
assert!(m.cycles() - start > 16);
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);
}
}
}