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,377 @@
use gba_core::{
io::*,
memory::{Memory, MemoryError, OAM_START as OAM, PALETTE_START as PAL, VRAM_START as VRAM},
video::{rgb555_to_rgb888, Framebuffer, VideoError, WIDTH},
};
fn setup(bg: u32, size: u16, wrap: bool) -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.write16(DISPCNT, 2 | (0x100 << bg)).unwrap();
m.write16(
BG0CNT + bg * 2,
0x1000 | (size << 14) | if wrap { 0x2000 } else { 0 },
)
.unwrap();
transform(&mut m, bg, [256, 0, 0, 256], [0, 0]);
m.write16(PAL, 0x4000).unwrap();
for i in 1..=255 {
m.write16(PAL + i * 2, i as u16 * 31).unwrap();
}
// All 256 byte-sized tile numbers, with an asymmetric indexed pattern.
for tile in 0..256 {
for y in 0..8 {
for x in (0..8).step_by(2) {
let lo = index(tile, x, y);
let hi = index(tile, x + 1, y);
m.write16(
VRAM + tile * 64 + y * 8 + x,
u16::from(lo) | (u16::from(hi) << 8),
)
.unwrap();
}
}
}
let tiles = 16_u32 << size;
for y in 0..tiles {
for x in (0..tiles).step_by(2) {
m.write16(
VRAM + 0x8000 + y * tiles + x,
u16::from(tile_number(x, y)) | (u16::from(tile_number(x + 1, y)) << 8),
)
.unwrap();
}
}
m
}
fn tile_number(x: u32, y: u32) -> u8 {
(x.wrapping_mul(17) + y.wrapping_mul(31)) as u8
}
fn index(tile: u32, x: u32, y: u32) -> u8 {
(tile + x + 3 * y) as u8
}
fn transform(m: &mut Memory, bg: u32, coeff: [i16; 4], origin: [i32; 2]) {
let base = BG2PA + (bg - 2) * 16;
for (i, v) in coeff.into_iter().enumerate() {
m.write16(base + i as u32 * 2, v as u16).unwrap();
}
for (i, v) in origin.into_iter().enumerate() {
m.write32(base + 8 + i as u32 * 4, v as u32).unwrap();
}
}
fn frame(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
m.render_frame(&mut f).unwrap();
f
}
fn pixel(m: &Memory, x: usize, y: usize) -> u32 {
frame(m).pixels()[y * WIDTH + x]
}
fn reference(m: &Memory, size: u16, wrap: bool, coeff: [i16; 4], origin: [i32; 2]) {
let f = frame(m);
let size = 128_i64 << size;
for y in 0..160 {
for x in 0..240 {
// Wide-integer reference with explicit 28-bit wrapping and floor division.
let coord = |r: i32, a: i16, b: i16| {
let v = i64::from(r) + i64::from(a) * x as i64 + i64::from(b) * y as i64;
let signed = (v + (1 << 27)).rem_euclid(1 << 28) - (1 << 27);
signed.div_euclid(256)
};
let mut tx = coord(origin[0], coeff[0], coeff[1]);
let mut ty = coord(origin[1], coeff[2], coeff[3]);
let mut color = 0x4000;
if wrap || (tx >= 0 && ty >= 0 && tx < size && ty < size) {
tx = tx.rem_euclid(size);
ty = ty.rem_euclid(size);
let t = tile_number(tx as u32 / 8, ty as u32 / 8);
let i = index(u32::from(t), tx as u32 % 8, ty as u32 % 8);
if i != 0 {
color = u16::from(i) * 31;
}
}
assert_eq!(
f.pixels()[y * WIDTH + x],
rgb555_to_rgb888(color),
"pixel {x},{y}, size {size}, origin={origin:?}, matrix={coeff:?}"
);
}
}
}
#[test]
fn affine_matrices_sizes_wrapping_and_signed_origins_match_wide_reference() {
for bg in [2, 3] {
for size in 0..4 {
for wrap in [false, true] {
let mut m = setup(bg, size, wrap);
for (coeff, origin) in [
([256, 0, 0, 256], [0, 0]),
([0, 256, -256, 0], [-1, 128 * 256]),
([181, 181, -181, 181], [-173, 17551]),
([128, 0, 0, 512], [127, -129]),
([256, 127, 1, -256], [0x07ff_ffff, 0x0800_0000]),
([32767, -32768, -32768, 32767], [-123456, 123456]),
([0; 4], [23 * 256, 17 * 256]),
] {
transform(&mut m, bg, coeff, origin);
reference(&m, size, wrap, coeff, origin);
}
}
}
}
}
#[test]
fn write_only_registers_merge_bytes_halfwords_and_words_and_mask_reference_top_bits() {
let mut m = setup(2, 1, true);
m.write32(BG2PA, 0xff80_0180).unwrap(); // A=1.5, B=-0.5
m.write8(BG2PA, 0x40).unwrap(); // A=1.25, retains high byte
m.write32(BG2PC, 0x0100_0000).unwrap();
m.write32(BG2X, 0xffff_ffff).unwrap();
m.write8(BG2X, 0x80).unwrap(); // -0.5
m.write16(BG2Y, 0x0101).unwrap();
m.write16(BG2Y + 2, 0xf000).unwrap(); // Unused high nibble must be zero internally.
reference(&m, 1, true, [320, -128, 0, 256], [-128, 257]);
for address in (BG2PA..=BG3Y).step_by(4) {
assert_eq!(m.read32(address).unwrap(), 0);
}
assert_eq!(
m.read16(0x0400_0058),
Err(MemoryError::Unmapped(0x0400_0058))
);
assert_eq!(m.cycles(), 0);
}
#[test]
fn affine_bg_ignores_text_scroll_and_color_depth_bit() {
let mut m = setup(2, 1, true);
let before = frame(&m);
m.write32(BG2HOFS, u32::MAX).unwrap();
m.write16(BG2CNT, 0x7080).unwrap(); // Same map/size/wrap, color bit set.
assert_eq!(frame(&m).pixels(), before.pixels());
}
#[test]
fn affine_byte_maps_are_flat_rows_use_tile255_and_have_no_flip_or_bank_bits() {
let mut m = setup(2, 2, false);
m.write16(VRAM + 0x8000 + 64, 0x00ff).unwrap(); // (tileX=0,tileY=1), not next text screen block.
assert_eq!(pixel(&m, 0, 8), rgb555_to_rgb888(255 * 31));
assert_eq!(pixel(&m, 1, 8), rgb555_to_rgb888(0x4000)); // Tile255 texel1 wraps to palette index0.
assert_eq!(pixel(&m, 8, 8), rgb555_to_rgb888(0x4000)); // Adjacent map byte selects tile0.
}
#[test]
fn affine_character_base_and_last_tile_reach_the_end_of_bg_vram() {
let mut m = setup(2, 0, false);
m.write16(BG2CNT, 0x100c).unwrap(); // Character base3, map16.
m.write16(VRAM + 0x8000, 0x00ff).unwrap();
m.write16(VRAM + 0xfffe, 0x0201).unwrap();
transform(&mut m, 2, [256, 0, 0, 256], [6 * 256, 7 * 256]);
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(31));
assert_eq!(pixel(&m, 1, 0), rgb555_to_rgb888(62));
}
#[test]
fn mode1_composes_two_text_layers_and_one_affine_layer_with_priority_ties() {
let mut m = setup(2, 1, true);
m.write16(DISPCNT, 0x701).unwrap();
m.write16(BG0CNT, 0x1804).unwrap(); // Text tile base1, map24, priority0.
m.write16(BG1CNT, 0x1904).unwrap();
m.write16(VRAM + 0x4000, 0x2222).unwrap();
m.write16(VRAM + 0xc800, 1).unwrap();
m.write16(VRAM + 0x4020, 0x3333).unwrap();
m.write16(VRAM, 0x0101).unwrap(); // BG2 affine nonzero index1.
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(2 * 31)); // BG0 wins tie.
m.write16(BG0CNT, 0x1807).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(3 * 31)); // BG1 wins tie over BG2.
m.write16(BG1CNT, 0x1907).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(31));
m.write16(VRAM, 0).unwrap(); // Affine palette index0 reveals text BG0.
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(2 * 31));
}
#[test]
fn mode2_bg3_has_independent_matrix_reference_and_priority() {
let mut m = setup(2, 1, true);
m.write16(DISPCNT, 0xc02).unwrap();
m.write16(BG3CNT, 0x7000).unwrap();
transform(&mut m, 2, [0; 4], [256, 0]);
transform(&mut m, 3, [0; 4], [512, 0]);
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(31)); // BG2 wins tie.
m.write16(BG2CNT, 0x7001).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(62));
m.write16(PAL + 4, 0).unwrap();
assert_eq!(pixel(&m, 0, 0), 0); // Nonzero index with black color is opaque.
}
#[test]
fn mode3_uses_affine_matrix_ignores_overflow_and_keeps_black_opaque() {
let mut m = Memory::new(vec![]).unwrap();
m.write16(DISPCNT, 0x403).unwrap();
m.write16(PAL, 0x03e0).unwrap();
m.write16(VRAM, 0).unwrap();
m.write16(VRAM + 2, 0x001f).unwrap();
m.write16(VRAM + (159 * 240 + 239) * 2, 0x7fff).unwrap();
transform(&mut m, 2, [256, 0, 0, 256], [-256, 0]);
m.write16(BG2CNT, 0x2000).unwrap(); // Ignored bitmap overflow flag.
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
assert_eq!(pixel(&m, 1, 0), 0);
assert_eq!(pixel(&m, 2, 0), 0xff0000);
transform(&mut m, 2, [-256, 0, 0, -256], [239 * 256, 159 * 256]);
assert_eq!(pixel(&m, 0, 0), 0xffffff);
assert_eq!(pixel(&m, 239, 159), 0);
// Hardware reset is a zero matrix, not an implicit identity.
transform(&mut m, 2, [0; 4], [256, 0]);
assert!(frame(&m).pixels().iter().all(|&p| p == 0xff0000));
}
#[test]
fn transformed_backgrounds_composite_with_regular_and_affine_sprites() {
for mode in [1, 2] {
let mut m = setup(2, 1, true);
m.write16(DISPCNT, mode | 0x1440).unwrap();
for i in 0..128 {
m.write16(OAM + i * 8, 0x200).unwrap();
}
m.write16(VRAM, 0x0101).unwrap();
m.write16(VRAM + 0x10000, 0x2222).unwrap();
m.write16(PAL + 0x204, 0x03e0).unwrap();
m.write16(OAM, 0).unwrap();
m.write16(OAM + 4, 0).unwrap();
assert_eq!(pixel(&m, 0, 0), 0x00ff00); // OBJ wins equal BG priority.
m.write16(OAM + 4, 0x400).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(31));
m.write16(OAM, 0x100).unwrap();
m.write16(OAM + 6, 256).unwrap();
m.write16(OAM + 30, 256).unwrap();
m.write16(OAM + 4, 0).unwrap();
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
}
}
#[test]
fn diagnostics_leave_output_unchanged_and_disabled_layers_skip_validation() {
let mut m = setup(2, 3, false);
let mut f = Framebuffer::default();
f.clear(0x001f);
for control in [0xc01, 0xd02, 0xe02] {
m.write16(DISPCNT, control).unwrap();
assert_eq!(
m.render_frame(&mut f),
Err(VideoError::UnsupportedLayers(control))
);
}
m.write16(DISPCNT, 0x402).unwrap();
m.write16(BG2CNT, 0xdf00).unwrap(); // Map31, 1024 pixels.
m.write32(BG2Y, 128 * 256).unwrap(); // Flat row starts outside first64K.
assert_eq!(
m.render_frame(&mut f),
Err(VideoError::UnsupportedMapAddress(0x10000))
);
assert!(f.pixels().iter().all(|&p| p == 0xff0000));
m.write16(DISPCNT, 2).unwrap();
m.render_frame(&mut f).unwrap();
assert!(f.pixels().iter().all(|&p| p == rgb555_to_rgb888(0x4000)));
m.write16(DISPCNT, 0xffff).unwrap();
m.render_frame(&mut f).unwrap();
assert!(f.pixels().iter().all(|&p| p == 0xffffff));
}
#[test]
fn cpu_demo_pans_rotates_zooms_clips_cancels_opposites_and_resets() {
use gba_core::input::{Button, Buttons};
use gba_demos::affine_demo::{AffineDemo, AFFINE_STATE};
let mut demo = AffineDemo::new().unwrap();
let mut f = Framebuffer::default();
assert_eq!(demo.machine().memory().read16(DISPCNT).unwrap(), 0);
assert_eq!(demo.machine().memory().read16(VRAM).unwrap(), 0);
// Signed negative panning and opposite-key cancellation.
demo.frame(
Buttons::default()
.with(Button::Left, true)
.with(Button::Up, true),
&mut f,
)
.unwrap();
demo.frame(Buttons::from_bits(0xf0), &mut f).unwrap();
assert_eq!(
demo.machine().memory().read32(AFFINE_STATE + 4).unwrap() as i32,
-2
);
assert_eq!(
demo.machine().memory().read32(AFFINE_STATE + 8).unwrap() as i32,
-2
);
for bits in 0..8 {
let rotated = bits & 1 != 0;
let zoomed = bits & 2 != 0;
let clipped = bits & 4 != 0;
let before = demo.machine().memory().display_position();
demo.frame(
Buttons::default()
.with(Button::L, rotated)
.with(Button::R, zoomed)
.with(Button::A, clipped),
&mut f,
)
.unwrap();
let a = f64::from(match (rotated, zoomed) {
(false, false) => 256,
(false, true) => 128,
(true, false) => 181,
(true, true) => 90,
}) / 256.0;
let b = if rotated { a } else { 0.0 };
for y in 0..160 {
for x in 0..240 {
let dx = x as f64 - 120.0;
let dy = y as f64 - 80.0;
let tx = (126.0 + a * dx + b * dy).floor() as i32;
let ty = (126.0 - b * dx + a * dy).floor() as i32;
let color = if clipped && (!(0..256).contains(&tx) || !(0..256).contains(&ty)) {
0x4000
} else {
let tx = tx.rem_euclid(256) as usize;
let ty = ty.rem_euclid(256) as usize;
[
0x0260, 0x03a0, 0x7d20, 0x7e80, 0x001f, 0x421f, 0x03ff, 0x7fff,
][((tx / 32 + 2 * (ty / 32)) % 4) * 2 + (tx / 2 + ty / 2) % 2]
};
assert_eq!(
f.pixels()[y * WIDTH + x],
rgb555_to_rgb888(color),
"input {bits}, pixel {x},{y}"
);
}
}
let m = demo.machine().memory();
assert_eq!(m.read32(AFFINE_STATE).unwrap(), bits + 3);
assert_eq!(m.read16(DISPCNT).unwrap(), 0x402);
assert_eq!(m.display_position().vblanks, before.vblanks + 1);
assert_eq!(m.display_position().scanline, 160);
}
demo.frame(
Buttons::default()
.with(Button::Start, true)
.with(Button::Right, true),
&mut f,
)
.unwrap();
assert_eq!(demo.machine().memory().read32(AFFINE_STATE + 4).unwrap(), 0);
assert_eq!(demo.machine().memory().read32(AFFINE_STATE + 8).unwrap(), 0);
}
#[test]
fn snapshot_uses_programmed_origin_without_advancing_or_accumulating_scanline_state() {
let mut m = setup(2, 1, true);
transform(&mut m, 2, [181, 181, -181, 181], [12345, -567]);
let first = frame(&m);
m.advance_cycles(1232 * 57);
let cycles = m.cycles();
assert_eq!(frame(&m).pixels(), first.pixels());
assert_eq!(m.cycles(), cycles);
m.write32(BG2X, 0).unwrap();
assert_ne!(frame(&m).pixels(), first.pixels());
}

View file

@ -0,0 +1,330 @@
use gba_core::{
cpu::Cpu,
display::{CYCLES_PER_FRAME, CYCLES_PER_LINE as LINE, HBLANK_START as HB, VBLANK_START as VB},
dma::DMA_BASE,
input::{Button, Buttons},
io::*,
machine::Machine,
memory::{Memory, PALETTE_START as PAL, ROM_START, VRAM_START as VRAM},
video::{rgb555_to_rgb888 as rgb, Framebuffer, HEIGHT, WIDTH},
};
use gba_demos::affine_raster_demo::{AffineRasterDemo, AFFINE_RASTER_STATE, AFFINE_RASTER_TABLE};
fn color(x: u32, y: u32) -> u16 {
((x & 31) | ((y & 31) << 5) | (((x + y) & 31) << 10)) as u16
}
fn bitmap() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m.write16(DISPCNT, 0x403).unwrap();
m.write16(BG2PA, 256).unwrap();
m.write16(BG2PB, 256).unwrap();
m.write16(BG2PD, 256).unwrap();
for y in 0..160 {
for x in 0..240 {
m.write16(VRAM + (y * 240 + x) * 2, color(x, y)).unwrap();
}
}
m
}
fn finish(m: &mut Memory) -> Framebuffer {
let p = m.display_position();
let phase = u32::from(p.scanline) * LINE + u32::from(p.line_cycle);
m.advance_cycles(VB - phase);
let mut f = Framebuffer::default();
assert!(m.present_frame(&mut f).unwrap());
f
}
fn row(f: &Framebuffer, y: usize, sx: u32, sy: u32) {
assert_eq!(f.pixels()[y * WIDTH], rgb(color(sx, sy)), "row={y}");
}
#[test]
fn coefficient_changes_accumulate_instead_of_multiplying_absolute_screen_y() {
let mut m = bitmap();
m.advance_cycles(2 * LINE + HB);
m.write16(BG2PB, 3 * 256).unwrap();
m.write16(BG2PD, 2 * 256).unwrap();
let f = finish(&mut m);
for (y, x, sy) in [(0, 0, 0), (1, 1, 1), (2, 2, 2), (3, 5, 4), (4, 8, 6)] {
row(&f, y, x, sy);
}
let mut snapshot = Framebuffer::default();
m.render_frame(&mut snapshot).unwrap();
row(&snapshot, 3, 9, 6); // Debug snapshots still use programmed origins.
}
#[test]
fn visible_reference_writes_replace_current_origin_and_only_the_written_axis() {
let mut m = bitmap();
m.advance_cycles(3 * LINE);
m.write32(BG2X, 10 * 256).unwrap();
m.advance_cycles(HB);
m.write32(BG2Y, 20 * 256).unwrap(); // HBlank: override next Y increment.
let f = finish(&mut m);
row(&f, 2, 2, 2);
row(&f, 3, 10, 3);
row(&f, 4, 11, 20);
row(&f, 5, 12, 21);
}
#[test]
fn partial_reference_writes_merge_programmed_not_accumulated_values() {
let mut m = bitmap();
m.write32(BG2X, 0x200).unwrap();
m.advance_cycles(5 * LINE);
m.write8(BG2X, 0x80).unwrap();
let f = finish(&mut m);
row(&f, 4, 6, 4);
row(&f, 5, 2, 5);
row(&f, 6, 3, 6);
assert_eq!(m.read32(BG2X).unwrap(), 0); // Still write-only.
}
#[test]
fn line_zero_reloads_programmed_origins_after_vblank_writes() {
let mut m = bitmap();
finish(&mut m);
m.write32(BG2X, 7 * 256).unwrap();
m.write32(BG2Y, 9 * 256).unwrap();
m.advance_cycles(CYCLES_PER_FRAME - VB);
let f = finish(&mut m);
row(&f, 0, 7, 9);
row(&f, 1, 8, 10);
m.advance_cycles(CYCLES_PER_FRAME - VB);
let f = finish(&mut m);
row(&f, 0, 7, 9); // Not the last frame's accumulated position.
}
#[test]
fn disabled_and_non_affine_modes_pause_tracking_but_forced_blank_does_not() {
for disabled in [3, 0x400, 0x483] {
let mut m = bitmap();
m.advance_cycles(2 * LINE);
m.write16(DISPCNT, disabled).unwrap();
m.advance_cycles(3 * LINE);
m.write16(DISPCNT, 0x403).unwrap();
let f = finish(&mut m);
let origin = if disabled == 0x483 { 5 } else { 2 };
row(&f, 5, origin, origin);
if disabled == 0x483 {
assert_eq!(f.pixels()[2 * WIDTH], 0xffffff);
}
}
}
#[test]
fn window_masks_do_not_pause_internal_tracking() {
let mut m = bitmap();
m.write16(DISPCNT, 0x2403).unwrap();
m.write16(WIN0H, 240).unwrap();
m.write16(WIN0V, 160).unwrap();
m.write16(WININ, 0).unwrap();
m.advance_cycles(5 * LINE);
m.write16(WININ, 4).unwrap();
let f = finish(&mut m);
assert_eq!(f.pixels()[4 * WIDTH], 0);
row(&f, 5, 5, 5);
}
#[test]
fn mosaic_holds_origin_then_uses_current_coefficient_for_block_increment() {
let mut m = bitmap();
m.write16(BG2CNT, 0x40).unwrap();
m.write16(MOSAIC, 0x30).unwrap(); // Four-row vertical blocks.
m.advance_cycles(2 * LINE + HB);
m.write16(BG2PB, 2 * 256).unwrap();
let f = finish(&mut m);
for y in 0..4 {
row(&f, y, 0, 0);
}
for y in 4..8 {
row(&f, y, 8, 4);
}
row(&f, 8, 16, 8);
}
#[test]
fn bg2_and_bg3_track_independently_in_supported_tiled_modes() {
for mode in [1, 2] {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m.write16(DISPCNT, 0x400 | mode).unwrap();
m.write16(BG2CNT, 0x1000).unwrap();
m.write16(BG3CNT, 0x1000).unwrap();
for reg in [BG2PA, BG3PA] {
m.write16(reg, 256).unwrap();
}
m.write16(BG2PB, 256).unwrap();
m.write16(BG3PB, 2 * 256).unwrap();
for index in 1..=8 {
m.write16(PAL + index * 2, color(index, 0)).unwrap();
}
for y in 0..8 {
for x in (0..8).step_by(2) {
m.write16(VRAM + y * 8 + x, (x + 1) as u16 | (((x + 2) as u16) << 8))
.unwrap();
}
}
m.advance_cycles(3 * LINE);
m.write16(DISPCNT, if mode == 2 { 0x802 } else { 0x401 })
.unwrap();
let f = finish(&mut m);
row(&f, 2, 3, 0);
if mode == 2 {
row(&f, 3, 1, 0); // Disabled BG3 did not accumulate.
row(&f, 4, 3, 0);
row(&f, 5, 5, 0);
} else {
row(&f, 4, 5, 0); // BG2 remains affine in mode1.
}
}
}
#[test]
fn capture_enable_does_not_reset_affine_hardware_state() {
let mut m = bitmap();
m.set_scanline_rendering(false);
m.advance_cycles(CYCLES_PER_FRAME); // Batched clock path.
m.set_scanline_rendering(true);
m.advance_cycles(4 * LINE);
m.write32(BG2X, 9 * 256).unwrap();
let captured = finish(&mut m);
row(&captured, 4, 9, 4);
m.set_scanline_rendering(false);
m.advance_cycles(CYCLES_PER_FRAME - VB);
m.set_scanline_rendering(true);
let captured = finish(&mut m);
row(&captured, 0, 9, 0);
}
#[test]
fn hblank_dma_reference_writes_override_next_increment() {
let mut m = bitmap();
for y in 0..160 {
m.write32(0x0200_0000 + y * 4, (y % 16 + 10) * 256).unwrap();
}
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, BG2X).unwrap();
m.write32(DMA_BASE + 8, 0xa640_0001).unwrap(); // Word, fixed destination, repeat HBlank.
m.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.run_until_vblank(1000).unwrap();
let mut f = Framebuffer::default();
assert!(machine.memory().present_frame(&mut f).unwrap());
row(&f, 0, 0, 0);
for y in 1..HEIGHT {
row(&f, y, (y as u32 - 1) % 16 + 10, y as u32);
}
}
#[test]
fn cpu_raster_demo_generates_dma_table_and_all_transform_combinations() {
let mut demo = AffineRasterDemo::new().unwrap();
let mut f = Framebuffer::default();
for index in 0..8 {
let rotated = index & 1 != 0;
let zoomed = index & 2 != 0;
let bypassed = index & 4 != 0;
let buttons = Buttons::default()
.with(Button::L, rotated)
.with(Button::R, zoomed)
.with(Button::A, bypassed)
.with(Button::Left, true)
.with(Button::Up, true);
demo.frame(buttons, &mut f).unwrap();
let m = demo.machine().memory();
let pan = -2 * (index + 1);
assert_eq!(m.read32(AFFINE_RASTER_STATE).unwrap(), index as u32 + 1);
assert_eq!(m.read32(AFFINE_RASTER_STATE + 4).unwrap() as i32, pan);
assert_eq!(m.read32(AFFINE_RASTER_STATE + 8).unwrap() as i32, pan);
assert_eq!(m.read16(DMA_BASE + 10).unwrap(), 0xa240);
assert_eq!(m.captured_vblank(), Some(m.display_position().vblanks));
let a = match (rotated, zoomed) {
(false, false) => 256,
(false, true) => 128,
(true, false) => 181,
(true, true) => 90,
};
let b = if rotated { a } else { 0 };
for y in 0..160 {
let offset = if bypassed {
0
} else if y & 16 == 0 {
512
} else {
-512
};
assert_eq!(
m.read16(AFFINE_RASTER_TABLE + y * 2).unwrap() as i16,
(b + offset) as i16
);
}
for y in 0..HEIGHT {
for x in 0..WIDTH {
let dx = x as i32 - 120;
let dy = y as i32 - 80;
let phase = y as i32 % 32;
let extra = if bypassed {
0
} else {
512 * phase.min(32 - phase)
};
let tx = ((128 + pan) * 256 + a * dx + b * dy + extra)
.div_euclid(256)
.rem_euclid(256) as usize;
let ty = ((128 + pan) * 256 - b * dx + a * dy)
.div_euclid(256)
.rem_euclid(256) as usize;
let tile = (tx / 32 + 2 * (ty / 32)) % 4;
let shade = (tx / 2 + ty / 2) % 2;
let expected = [
0x0260, 0x03a0, 0x7d20, 0x7e80, 0x001f, 0x421f, 0x03ff, 0x7fff,
][tile * 2 + shade];
assert_eq!(
f.pixels()[y * WIDTH + x],
rgb(expected),
"case={index} x={x} y={y}"
);
}
}
}
demo.frame(
Buttons::default()
.with(Button::Left, true)
.with(Button::Right, true),
&mut f,
)
.unwrap();
assert_eq!(
demo.machine()
.memory()
.read32(AFFINE_RASTER_STATE + 4)
.unwrap() as i32,
-16
);
demo.frame(
Buttons::default()
.with(Button::Start, true)
.with(Button::Right, true),
&mut f,
)
.unwrap();
assert_eq!(
demo.machine()
.memory()
.read32(AFFINE_RASTER_STATE + 4)
.unwrap(),
0
);
assert_eq!(
demo.machine()
.memory()
.read32(AFFINE_RASTER_STATE + 8)
.unwrap(),
0
);
}

View file

@ -0,0 +1,671 @@
use gba_core::{
bios::{self, IRQ_FLAGS, IRQ_HANDLER, SYSTEM_STACK, UNSUPPORTED_TRAP},
cpu::{CpuError, InstructionSet, Mode},
display::VBLANK_START,
io::{DISPSTAT, IE, IF, IME, POSTFLG, TIMER_BASE},
machine::{Machine, MachineError, StepKind},
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START, VRAM_START},
};
const SOURCE: u32 = 0x0200_0000;
const DEST: u32 = 0x0201_0000;
const CALLBACK: u32 = 0x0300_0100;
const ROM_DATA: u32 = ROM_START + 13 * 4;
#[path = "bios/affine.rs"]
mod affine;
#[path = "bios/angles.rs"]
mod angles;
#[path = "bios/arithmetic.rs"]
mod arithmetic;
#[path = "bios/bit_unpack.rs"]
mod bit_unpack;
#[path = "bios/differential.rs"]
mod differential;
#[path = "bios/huffman.rs"]
mod huffman;
#[path = "bios/lz77.rs"]
mod lz77;
#[path = "bios/ram_reset.rs"]
mod ram_reset;
#[path = "bios/reset.rs"]
mod reset;
#[path = "bios/run_length.rs"]
mod run_length;
fn words(code: &[u32]) -> Vec<u8> {
code.iter().flat_map(|word| word.to_le_bytes()).collect()
}
fn reach(machine: &mut Machine, pc: u32, max_steps: usize) {
for _ in 0..max_steps {
if machine.cpu().pc() == pc {
return;
}
machine.step().unwrap();
}
panic!(
"did not reach {pc:#x}; pc={:#x}, cpsr={:#x}",
machine.cpu().pc(),
machine.cpu().cpsr()
);
}
/// Boot, load operands and status through CPU instructions, then stop just before SWI.
fn call(service: u8, thumb: bool, operands: [u32; 3]) -> (Machine, u32) {
call_status(service, thumb, operands, 0x1f)
}
fn call_status(service: u8, thumb: bool, operands: [u32; 3], status: u32) -> (Machine, u32) {
call_status_data(service, thumb, operands, status, &[])
}
fn call_status_data(
service: u8,
thumb: bool,
operands: [u32; 3],
status: u32,
data: &[u8],
) -> (Machine, u32) {
let mut code = [
0xe59f_0020, // LDR r0,[pc,#32] (offset 40)
0xe59f_1020,
0xe59f_2020,
0xe3a0_30a3, // MOV r3,#0xa3
0xe3a0_c05c, // MOV r12,#0x5c
0xe328_f480, // MSR CPSR_f,#0x80000000
if thumb { 0xe28f_e001 } else { 0xe1a0_e00f }, // ADD lr,pc,#1 / MOV lr,pc
if thumb { 0xe12f_ff1e } else { 0xe1a0_0000 }, // BX lr / NOP
if thumb {
0xe7fe_0000 | 0xdf00 | u32::from(service)
} else {
0xef00_0000 | u32::from(service) << 16
},
0xeaff_fffe,
operands[0],
operands[1],
operands[2],
];
if status != 0x1f {
code[3] = 0xe321_f000 | status;
} // MSR CPSR_c,#status
let mut rom = words(&code);
assert_eq!(rom.len() as u32, ROM_DATA - ROM_START);
rom.extend_from_slice(data);
let mut machine = bios::boot(rom).unwrap();
reach(&mut machine, ROM_START + 32, 100);
assert_eq!(
machine.cpu().instruction_set(),
if thumb {
InstructionSet::Thumb
} else {
InstructionSet::Arm
}
);
(machine, ROM_START + if thumb { 34 } else { 36 })
}
fn finish(machine: &mut Machine, return_pc: u32) {
machine.step().unwrap(); // Execute SWI, not a host service call.
reach(machine, return_pc, 100_000);
}
/// Original ARM callback: acknowledge IF, record BIOS flags, then return to dispatcher.
fn callback(bus: &mut Memory) {
let code = [
0xe280_0c02, // ADD r0,r0,#0x200 (r0=I/O base from dispatcher)
0xe1d0_10b2, // LDRH r1,[r0,#2]
0xe1c0_10b2, // STRH r1,[r0,#2]
0xe3a0_0403, // MOV r0,#0x03000000
0xe280_0c7f, // ADD r0,r0,#0x7f00
0xe1d0_2fb8, // LDRH r2,[r0,#0xf8]
0xe182_2001, // ORR r2,r2,r1
0xe1c0_2fb8, // STRH r2,[r0,#0xf8]
0xe12f_ff1e, // BX lr
];
for (index, instruction) in code.into_iter().enumerate() {
bus.write32(CALLBACK + index as u32 * 4, instruction)
.unwrap();
}
bus.write32(IRQ_HANDLER, CALLBACK).unwrap();
}
#[test]
fn original_image_is_deterministic_and_boot_enters_arm_system_mode() {
assert_eq!(bios::image().len(), BIOS_SIZE);
assert_eq!(bios::image(), bios::image());
let mut machine = bios::boot(words(&[0xeaff_fffe])).unwrap();
assert_eq!(machine.cpu().mode(), Mode::Supervisor);
assert_eq!(machine.cpu().pc(), 0);
reach(&mut machine, ROM_START, 100);
assert_eq!(machine.cpu().mode(), Mode::System);
assert_eq!(machine.cpu().instruction_set(), InstructionSet::Arm);
assert_eq!(machine.cpu().cpsr(), 0x1f);
assert_eq!(machine.cpu().registers()[13], SYSTEM_STACK);
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 1);
assert_eq!(machine.memory().read32(IRQ_FLAGS).unwrap(), 0);
assert_eq!(machine.memory().read32(IRQ_HANDLER).unwrap(), 0);
assert!(machine.cycles() > 0);
}
#[test]
fn cpu_set_copies_both_widths_from_arm_and_thumb_and_preserves_caller() {
for thumb in [false, true] {
for word in [false, true] {
let (mut machine, return_pc) = call(
0x0b,
thumb,
[SOURCE, DEST, 4 | if word { 1 << 26 } else { 0 }],
);
for index in 0..4 {
machine
.memory_mut()
.write32(SOURCE + index * 4, 0x1234_5678 + index)
.unwrap();
}
let before = machine.cpu().clone();
finish(&mut machine, return_pc);
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
for offset in 0..if word { 16 } else { 8 } {
assert_eq!(
machine.memory().read8(DEST + offset).unwrap(),
machine.memory().read8(SOURCE + offset).unwrap()
);
}
}
}
}
#[test]
fn intr_wait_consumes_preexisting_selected_flags_without_sleep() {
for thumb in [false, true] {
let (mut machine, return_pc) = call(4, thumb, [0, 0x10, 0x1234]);
machine.memory_mut().write16(IRQ_FLAGS, 0x19).unwrap();
let before = machine.cpu().clone();
machine.step().unwrap();
for _ in 0..100 {
if machine.cpu().pc() == return_pc {
break;
}
assert_ne!(machine.step().unwrap(), StepKind::HaltIdle);
}
assert_eq!(machine.cpu().pc(), return_pc);
assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 9);
assert_eq!(machine.memory().read16(IME).unwrap(), 1);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
}
}
#[test]
fn vblank_wait_halts_dispatches_irq_and_returns_through_original_code() {
let (mut machine, return_pc) = call(5, false, [0x123, 0x456, 0x789]);
callback(machine.memory_mut());
machine.memory_mut().write16(DISPSTAT, 8).unwrap();
machine.memory_mut().write16(IE, 1).unwrap();
let before = machine.cpu().clone();
machine.step().unwrap();
let mut slept = false;
let mut irq = 0;
for _ in 0..1000 {
if machine.cpu().pc() == return_pc {
break;
}
match machine.step().unwrap() {
StepKind::HaltIdle => slept = true,
StepKind::IrqEntry => irq += 1,
_ => {}
}
}
assert!(slept);
assert_eq!(irq, 1);
assert_eq!(machine.cpu().pc(), return_pc);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 0);
assert_eq!(machine.memory().read16(IME).unwrap(), 1);
assert!(machine.cycles() > u64::from(VBLANK_START));
}
#[test]
fn unsupported_services_fail_at_an_explicit_instruction_trap() {
for service in [3, 0x0d, 0x19, 0xff] {
let (mut machine, _) = call(service, false, [0, 0, 0]);
let error = (0..100)
.find_map(|_| machine.step().err())
.expect("unsupported SWI must fail");
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: UNSUPPORTED_TRAP,
..
})
));
}
}
#[test]
fn cpu_set_fill_reads_one_value_and_writes_exact_count() {
for thumb in [false, true] {
for word in [false, true] {
let control = 3 | (1 << 24) | if word { 1 << 26 } else { 0 };
let (mut machine, return_pc) = call(0x0b, thumb, [SOURCE, DEST, control]);
machine.memory_mut().write32(SOURCE, 0xaabb_ccdd).unwrap();
for offset in 0..5 {
machine
.memory_mut()
.write32(DEST + offset * 4, 0x9999_9999)
.unwrap();
}
finish(&mut machine, return_pc);
let bytes = if word { 12 } else { 6 };
for offset in 0..bytes {
assert_eq!(
machine.memory().read8(DEST + offset).unwrap(),
[0xdd, 0xcc, 0xbb, 0xaa][(offset % if word { 4 } else { 2 }) as usize]
);
}
assert_eq!(machine.memory().read16(DEST + bytes).unwrap(), 0x9999);
}
}
}
#[test]
fn cpu_fast_set_rounds_up_to_eight_words_for_copy_and_fill() {
for thumb in [false, true] {
for fill in [false, true] {
for count in [1_u32, 7, 8, 9, 15, 16, 17] {
let (mut machine, return_pc) = call(
0x0c,
thumb,
[SOURCE, DEST, count | if fill { 1 << 24 } else { 0 }],
);
let rounded = (count + 7) & !7;
for index in 0..rounded + 1 {
machine
.memory_mut()
.write32(SOURCE + index * 4, 0x1000 + index)
.unwrap();
machine
.memory_mut()
.write32(DEST + index * 4, 0xcccc_cccc)
.unwrap();
}
let before = machine.cpu().clone();
finish(&mut machine, return_pc);
for index in 0..rounded {
assert_eq!(
machine.memory().read32(DEST + index * 4).unwrap(),
0x1000 + if fill { 0 } else { index }
);
}
assert_eq!(
machine.memory().read32(DEST + rounded * 4).unwrap(),
0xcccc_cccc
);
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
}
}
}
}
#[test]
fn fast_copy_reads_each_eight_word_block_before_writing_it() {
let (mut machine, return_pc) = call(0x0c, false, [SOURCE, SOURCE + 4, 16]);
let mut expected: Vec<u32> = (0..17).map(|index| 0x1234 + index).collect();
for (index, value) in expected.iter().enumerate() {
machine
.memory_mut()
.write32(SOURCE + index as u32 * 4, *value)
.unwrap();
}
for start in [0, 8] {
let block = expected[start..start + 8].to_vec();
expected[start + 1..start + 9].copy_from_slice(&block);
}
finish(&mut machine, return_pc);
for (index, value) in expected.into_iter().enumerate() {
assert_eq!(
machine.memory().read32(SOURCE + index as u32 * 4).unwrap(),
value
);
}
}
#[test]
fn swi_callers_outside_user_and_system_modes_are_rejected() {
for mode in [0x11, 0x12, 0x13, 0x17, 0x1b] {
let (mut machine, _) = call_status(0x0b, false, [SOURCE, DEST, 0], mode);
let error = (0..100).find_map(|_| machine.step().err()).unwrap();
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: UNSUPPORTED_TRAP,
..
})
));
}
}
#[test]
fn zero_length_copies_do_not_read_source_or_write_destination() {
for service in [0x0b, 0x0c] {
for control in [0, 1 << 24, 1 << 26, (1 << 24) | (1 << 26), 0xfae0_0000] {
let (mut machine, return_pc) =
call(service, false, [0x0e00_0000, 0x0e00_0000, control]);
finish(&mut machine, return_pc); // Both addresses would fail if accessed.
}
}
}
#[test]
fn copy_count_ignores_reserved_bits_and_fast_set_always_uses_words() {
for service in [0x0b, 0x0c] {
let (mut machine, return_pc) = call(service, false, [SOURCE, DEST, 1 | 0xfae0_0000]);
machine.memory_mut().write32(SOURCE, 0x1234_abcd).unwrap();
finish(&mut machine, return_pc);
assert_eq!(
machine.memory().read32(DEST).unwrap(),
if service == 0x0b { 0xabcd } else { 0x1234_abcd }
);
}
}
#[test]
fn copying_to_video_ram_uses_halfword_and_word_bus_writes() {
for service in [0x0b, 0x0c] {
let (mut machine, return_pc) = call(service, false, [SOURCE, VRAM_START, 3]);
machine.memory_mut().write32(SOURCE, 0x03e0_001f).unwrap();
machine
.memory_mut()
.write32(SOURCE + 4, 0x7fff_7c00)
.unwrap();
finish(&mut machine, return_pc);
assert_eq!(machine.memory().read32(VRAM_START).unwrap(), 0x03e0_001f);
assert_eq!(machine.memory().read16(VRAM_START + 4).unwrap(), 0x7c00);
assert_eq!(
machine.memory().read16(VRAM_START + 6).unwrap(),
if service == 0x0b { 0 } else { 0x7fff }
);
}
}
#[test]
fn copy_addresses_align_down_and_overlaps_use_instruction_order() {
for word in [false, true] {
let (mut machine, return_pc) = call(
0x0b,
false,
[SOURCE + 3, DEST + 3, 1 | if word { 1 << 26 } else { 0 }],
);
machine.memory_mut().write32(SOURCE, 0x1234_abcd).unwrap();
finish(&mut machine, return_pc);
assert_eq!(
machine.memory().read32(DEST).unwrap(),
if word { 0x1234_abcd } else { 0x1234_0000 }
);
}
let (mut machine, return_pc) = call(0x0b, false, [SOURCE, SOURCE + 2, 4]);
machine.memory_mut().write32(SOURCE, 0xabcd_1234).unwrap();
finish(&mut machine, return_pc);
for offset in [0, 2, 4, 6, 8] {
assert_eq!(machine.memory().read16(SOURCE + offset).unwrap(), 0x1234);
}
}
#[test]
fn protected_or_wrapping_source_ranges_return_without_copying() {
for service in [0x0b, 0x0c] {
for source in [0, 0x3ffc, 0x0100_0000, 0x01ff_fffc, 0xffff_fffc] {
let (mut machine, return_pc) = call(service, false, [source, DEST, 2 | (1 << 26)]);
machine.memory_mut().write32(DEST, 0x1234_5678).unwrap();
finish(&mut machine, return_pc);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x1234_5678);
}
}
}
#[test]
fn copy_failure_keeps_earlier_writes_but_not_partial_instruction_changes() {
let (mut machine, _) = call(0x0b, false, [SOURCE, 0x07ff_fffc, 2 | (1 << 26)]);
machine.memory_mut().write32(SOURCE, 0xaabb_ccdd).unwrap();
let mut failed = false;
for _ in 0..100 {
let before = machine.cpu().clone();
let timing = machine.last_timing();
let cycles = machine.cycles();
if let Err(error) = machine.step() {
assert_eq!(
error,
MachineError::Cpu(MemoryError::ReadOnly(0x0800_0000).into())
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.last_timing(), timing);
assert_eq!(machine.memory().read32(0x07ff_fffc).unwrap(), 0xaabb_ccdd);
failed = true;
break;
}
}
assert!(failed);
}
#[test]
fn halt_preserves_registers_and_ime_for_arm_and_thumb_callers() {
for thumb in [false, true] {
let (mut machine, return_pc) = call(2, thumb, [0x1234, 0x5678, 0x9abc]);
machine.memory_mut().write16(IE, 8).unwrap();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_fc18)
.unwrap(); // IRQ after 1,000 cycles.
let before = machine.cpu().clone();
machine.step().unwrap();
let mut slept = false;
for _ in 0..200 {
if machine.cpu().pc() == return_pc {
break;
}
slept |= machine.step().unwrap() == StepKind::HaltIdle;
}
assert!(slept);
assert_eq!(machine.cpu().pc(), return_pc);
assert_eq!(machine.memory().read16(IME).unwrap(), 0);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
}
}
#[test]
fn halt_with_pending_irq_returns_without_an_idle_interval() {
let (mut machine, return_pc) = call(2, false, [1, 2, 3]);
request_timer(machine.memory_mut());
machine.memory_mut().write16(IE, 8).unwrap();
machine.step().unwrap();
for _ in 0..100 {
if machine.cpu().pc() == return_pc {
break;
}
assert_ne!(machine.step().unwrap(), StepKind::HaltIdle);
}
assert_eq!(machine.cpu().pc(), return_pc);
}
fn request_timer(bus: &mut Memory) {
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
bus.advance_cycles(1);
bus.write16(TIMER_BASE + 2, 0).unwrap();
}
#[test]
fn interrupt_wait_has_no_lost_wake_between_flag_check_and_halt() {
for injection_step in 0..100 {
let (mut machine, return_pc) = call(4, false, [0, 8, 0]);
callback(machine.memory_mut());
machine.memory_mut().write16(IE, 8).unwrap();
for step in 0..500 {
if step == injection_step {
request_timer(machine.memory_mut());
}
if machine.cpu().pc() == return_pc {
break;
}
machine.step().unwrap();
}
assert_eq!(
machine.cpu().pc(),
return_pc,
"injection step {injection_step}"
);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 0);
}
}
#[test]
fn discard_old_flags_waits_for_a_new_callback_and_retains_other_flags() {
for thumb in [false, true] {
let (mut machine, return_pc) = call(4, thumb, [1, 8, 0]);
callback(machine.memory_mut());
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().write16(IRQ_FLAGS, 9).unwrap();
for _ in 0..100 {
if machine.halted() {
break;
}
machine.step().unwrap();
}
assert!(machine.halted());
assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 1);
request_timer(machine.memory_mut());
reach(&mut machine, return_pc, 200);
assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 1);
}
}
#[test]
fn unrelated_irq_does_not_complete_a_wait_for_vblank() {
let (mut machine, return_pc) = call(4, false, [1, 1, 0]);
callback(machine.memory_mut());
machine.memory_mut().write16(IE, 9).unwrap();
machine.memory_mut().write16(DISPSTAT, 8).unwrap();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_fc18)
.unwrap();
let mut irqs = 0;
for _ in 0..1000 {
if machine.cpu().pc() == return_pc {
break;
}
if machine.step().unwrap() == StepKind::IrqEntry {
irqs += 1;
machine.memory_mut().write16(TIMER_BASE + 2, 0).unwrap();
}
}
assert_eq!(machine.cpu().pc(), return_pc);
assert_eq!(irqs, 2);
assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 8);
assert!(machine.cycles() > u64::from(VBLANK_START));
}
#[test]
fn wait_services_restore_user_system_and_irq_masked_caller_status() {
for thumb in [false, true] {
for status in [0x10, 0x90, 0x9f, 0xdf] {
let (mut machine, return_pc) = call_status(4, thumb, [0, 8, 0], status);
callback(machine.memory_mut());
machine.memory_mut().write16(IE, 8).unwrap();
let before = machine.cpu().clone();
for _ in 0..100 {
if machine.halted() {
break;
}
machine.step().unwrap();
}
assert!(machine.halted());
request_timer(machine.memory_mut());
reach(&mut machine, return_pc, 200);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(machine.memory().read16(IME).unwrap(), 1);
}
}
}
#[test]
fn missing_or_misaligned_irq_callback_is_an_explicit_trap() {
for pointer in [0, CALLBACK + 1, CALLBACK + 2] {
let (mut machine, _) = call(5, false, [0, 0, 0]);
machine.memory_mut().write32(IRQ_HANDLER, pointer).unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().write16(IME, 1).unwrap();
request_timer(machine.memory_mut());
let error = (0..100).find_map(|_| machine.step().err()).unwrap();
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: UNSUPPORTED_TRAP,
..
})
));
}
}
#[test]
fn repeated_calls_restore_the_supervisor_stack() {
let mut machine = bios::boot(words(&[0xe3a0_2000, 0xef0b_0000, 0xeaff_fffd])).unwrap();
reach(&mut machine, ROM_START + 4, 100);
for _ in 0..100 {
machine.step().unwrap(); // SWI
assert_eq!(machine.cpu().registers()[13], bios::SVC_STACK);
reach(&mut machine, ROM_START + 8, 100);
assert_eq!(machine.cpu().registers()[13], SYSTEM_STACK);
machine.step().unwrap(); // Branch back to SWI.
assert_eq!(machine.cpu().pc(), ROM_START + 4);
}
}
#[test]
fn arm_swi_uses_upper_comment_byte_not_low_byte() {
let mut machine = bios::boot(words(&[0xe3a0_2000, 0xef0b_ff03, 0xeaff_fffe])).unwrap();
reach(&mut machine, ROM_START + 4, 100);
finish(&mut machine, ROM_START + 8); // CpuSet(count=0), not Stop(3).
}
#[test]
fn callback_must_record_bios_flags_not_only_acknowledge_hardware_if() {
let (mut machine, return_pc) = call(4, false, [0, 8, 0]);
callback(machine.memory_mut());
machine
.memory_mut()
.write32(CALLBACK + 12, 0xe12f_ff1e)
.unwrap(); // Return after IF acknowledgement, omit flags.
machine.memory_mut().write16(IE, 8).unwrap();
request_timer(machine.memory_mut());
for _ in 0..300 {
machine.step().unwrap();
}
assert_ne!(machine.cpu().pc(), return_pc);
assert!(machine.halted());
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 0);
}
#[test]
fn supplying_an_image_is_optional_and_does_not_change_memory_new() {
assert_eq!(
Memory::new(vec![]).unwrap().read32(0),
Err(MemoryError::Unmapped(0))
);
let mut bus = Memory::with_bios(vec![], bios::image()).unwrap();
assert_eq!(bus.write32(0, 1), Err(MemoryError::ReadOnly(0)));
assert_eq!(
bus.read8(BIOS_SIZE as u32),
Err(MemoryError::Unmapped(BIOS_SIZE as u32))
);
}

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

View file

@ -0,0 +1,442 @@
use gba_core::{
io::{BG2CNT, BG2PA, BG2X, BG2Y, DISPCNT, GREENSWAP},
memory::{Memory, OAM_START as OAM, PALETTE_START as PAL, VRAM_START as VRAM},
video::{rgb555_to_rgb888, Framebuffer, VideoError, WIDTH},
};
fn setup(mode: u16) -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.write16(DISPCNT, 0x400 | mode).unwrap();
transform(&mut m, [256, 0, 0, 256], [0, 0]);
m.write16(PAL, 0x4000).unwrap();
for i in 1..256 {
m.write16(PAL + i * 2, (i as u16 * 123) & 0x7fff).unwrap();
}
m
}
fn transform(m: &mut Memory, values: [i16; 4], origin: [i32; 2]) {
for (i, v) in values.into_iter().enumerate() {
m.write16(BG2PA + i as u32 * 2, v as u16).unwrap();
}
m.write32(BG2X, origin[0] as u32).unwrap();
m.write32(BG2Y, origin[1] as u32).unwrap();
}
fn frame(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
m.render_frame(&mut f).unwrap();
f
}
fn pixel(m: &Memory, x: usize, y: usize) -> u32 {
frame(m).pixels()[y * WIDTH + x]
}
fn sprite(m: &mut Memory, mode: u16, priority: u16, affine: bool) {
for i in 0..128 {
m.write16(OAM + i * 8, 0x200).unwrap();
}
m.write16(OAM, if affine { 0x100 } else { 0 }).unwrap();
m.write16(OAM + 4, 512 | (priority << 10)).unwrap();
m.write16(OAM + 6, 256).unwrap();
m.write16(OAM + 30, 256).unwrap();
m.write16(VRAM + 0x14000, 0x1111).unwrap();
m.write16(PAL + 0x202, 0x03e0).unwrap();
m.write16(DISPCNT, 0x1440 | mode).unwrap();
}
#[test]
fn mode4_reads_adjacent_bytes_all_indices_and_both_page_boundaries() {
let mut m = setup(4);
for page in [0, 0xa000] {
m.write16(VRAM + page, 0xff01).unwrap();
m.write16(VRAM + page + 238, 0x0203).unwrap();
m.write16(VRAM + page + 240, 0x0405).unwrap();
m.write16(VRAM + page + 0x95fe, 0x0607).unwrap();
m.write16(DISPCNT, 0x404 | if page == 0 { 0 } else { 0x10 })
.unwrap();
for (x, y, index) in [
(0, 0, 1),
(1, 0, 255),
(239, 0, 2),
(0, 1, 5),
(239, 159, 6),
] {
assert_eq!(pixel(&m, x, y), rgb555_to_rgb888((index * 123) & 0x7fff));
}
}
for i in (0..256).step_by(2) {
m.write16(VRAM + i, (i as u16) | ((i as u16 + 1) << 8))
.unwrap();
}
m.write16(DISPCNT, 0x404).unwrap();
let f = frame(&m);
for i in 0..256 {
assert_eq!(
f.pixels()[i],
rgb555_to_rgb888(if i == 0 {
0x4000
} else {
(i as u16 * 123) & 0x7fff
})
);
}
}
#[test]
fn mode4_page_stride_is_40k_and_inactive_page_writes_do_not_change_output() {
let mut m = setup(4);
m.write16(VRAM, 0x0101).unwrap();
m.write16(VRAM + 0x9600, 0x0303).unwrap(); // Padding is not page1.
let before = frame(&m);
m.write16(VRAM + 0xa000, 0x0202).unwrap();
assert_eq!(frame(&m).pixels(), before.pixels());
m.write8(DISPCNT, 0x14).unwrap(); // Byte write selects page1 without losing BG2 enable.
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(246));
m.write16(PAL + 4, 0x001f).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000); // Palette is live, not cached per page.
m.write8(DISPCNT, 4).unwrap();
assert_eq!(frame(&m).pixels(), before.pixels());
}
#[test]
fn mode5_uses_160_by_128_rgb555_pages_and_leaves_remainder_transparent() {
let mut m = setup(5);
for (page, color) in [(0, 0x801f), (0xa000, 0x83e0)] {
m.write16(VRAM + page, color).unwrap();
m.write16(VRAM + page + 318, 0x7c00).unwrap();
m.write16(VRAM + page + 320, 0x7fff).unwrap();
m.write16(VRAM + page + 0x9ffe, color).unwrap();
}
for (control, color) in [(0x405, 0xff0000), (0x415, 0x00ff00)] {
m.write16(DISPCNT, control).unwrap();
let f = frame(&m);
assert_eq!(f.pixels()[0], color);
assert_eq!(f.pixels()[159], 0x0000ff);
assert_eq!(f.pixels()[WIDTH], 0xffffff);
assert_eq!(f.pixels()[127 * WIDTH + 159], color);
for y in 0..160 {
for x in 0..240 {
if x >= 160 || y >= 128 {
assert_eq!(f.pixels()[y * WIDTH + x], rgb555_to_rgb888(0x4000));
}
}
}
}
}
#[test]
fn mode4_zero_index_is_transparent_but_nonzero_black_is_opaque() {
for affine in [false, true] {
let mut m = setup(4);
sprite(&mut m, 4, 3, affine);
m.write16(VRAM, 0x0100).unwrap();
m.write16(PAL + 2, 0).unwrap();
assert_eq!(pixel(&m, 0, 0), 0x00ff00); // Index0 reveals lower-priority sprite.
assert_eq!(pixel(&m, 1, 0), 0); // Black index1 still covers it.
m.write16(BG2CNT, 3).unwrap();
assert_eq!(pixel(&m, 1, 0), 0x00ff00); // OBJ wins equal priority.
}
}
#[test]
fn mode5_black_is_opaque_and_ignores_palette_while_outside_pixels_reveal_objects() {
let mut m = setup(5);
sprite(&mut m, 5, 3, false);
m.write16(VRAM, 0).unwrap();
m.write16(PAL, 0x7fff).unwrap();
assert_eq!(pixel(&m, 0, 0), 0);
m.write32(BG2X, u32::MAX).unwrap(); // Source X=-1/256 => first output column outside.
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
assert_eq!(pixel(&m, 1, 0), 0);
}
#[test]
fn bitmap_sprites_use_upper_obj_area_independent_of_page() {
for mode in [4, 5] {
let mut m = setup(mode);
sprite(&mut m, mode, 0, false);
m.write16(VRAM + 0x10000, 0x1111).unwrap();
m.write16(OAM + 4, 0).unwrap();
assert_ne!(pixel(&m, 0, 0), 0x00ff00); // Lower512 OBJ tiles unavailable.
m.write16(OAM + 4, 512).unwrap();
for page in [0, 0x10] {
m.write16(DISPCNT, 0x1440 | mode | page).unwrap();
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
}
assert_eq!(m.read16(VRAM + 0x14000).unwrap(), 0x1111);
}
}
#[test]
fn bitmap_byte_writes_duplicate_pixels_but_never_modify_obj_tiles() {
for mode in [4, 5] {
let mut m = setup(mode);
for address in [VRAM, VRAM + 0xa001, VRAM + 0x13fff] {
m.write8(address, 0x5a).unwrap();
assert_eq!(m.read16(address & !1).unwrap(), 0x5a5a);
}
m.write16(VRAM + 0x14000, 0x1234).unwrap();
m.write8(VRAM + 0x1c000, 0xff).unwrap(); // OBJ mirror.
assert_eq!(m.read16(VRAM + 0x14000).unwrap(), 0x1234);
}
}
#[test]
fn affine_bitmap_sampling_matches_wide_integer_reference_on_both_pages() {
for mode in [4, 5] {
let mut m = setup(mode);
let (w, h) = if mode == 4 { (240, 160) } else { (160, 128) };
for page in 0..2_u32 {
if mode == 4 {
for offset in (0..w * h).step_by(2) {
let lo = ((offset * 13 + page * 73) & 255) as u16;
let hi = (((offset + 1) * 13 + page * 73) & 255) as u16;
m.write16(VRAM + page * 0xa000 + offset, lo | (hi << 8))
.unwrap();
}
} else {
for offset in 0..w * h {
m.write16(
VRAM + page * 0xa000 + offset * 2,
((offset * 37 + page * 127) & 0xffff) as u16,
)
.unwrap();
}
}
}
for page in 0..2_u32 {
m.write16(DISPCNT, 0x400 | mode | (page as u16 * 16))
.unwrap();
m.write16(BG2CNT, 0xff8f).unwrap(); // Ignored map/size/overflow/color fields; mosaic clear.
for (coeff, origin) in [
([256, 0, 0, 256], [0, 0]),
([181, 181, -181, 181], [-173, 20001]),
([128, 0, 0, 128], [127, -129]),
(
[-256, 0, 0, -256],
[(w as i32 - 1) * 256, (h as i32 - 1) * 256],
),
([256, 73, -129, 512], [-512, 127]),
([0; 4], [256, 256]),
] {
transform(&mut m, coeff, origin);
let f = frame(&m);
for y in 0..160 {
for x in 0..240 {
let tx = (i64::from(origin[0])
+ i64::from(coeff[0]) * x as i64
+ i64::from(coeff[1]) * y as i64)
.div_euclid(256);
let ty = (i64::from(origin[1])
+ i64::from(coeff[2]) * x as i64
+ i64::from(coeff[3]) * y as i64)
.div_euclid(256);
let color = if tx < 0 || ty < 0 || tx >= i64::from(w) || ty >= i64::from(h)
{
0x4000
} else {
let offset = ty as u32 * w + tx as u32;
if mode == 4 {
let i = ((offset * 13 + page * 73) & 255) as u16;
if i == 0 {
0x4000
} else {
(i * 123) & 0x7fff
}
} else {
((offset * 37 + page * 127) & 0x7fff) as u16
}
};
assert_eq!(
f.pixels()[y * WIDTH + x],
rgb555_to_rgb888(color),
"mode {mode}, page {page}, pixel {x},{y}"
);
}
}
}
}
}
}
#[test]
fn mode5_can_scale_to_full_lcd_without_implicit_stretching() {
let mut m = setup(5);
for offset in 0..160 * 128 {
m.write16(VRAM + offset * 2, 0x001f).unwrap();
}
assert_eq!(pixel(&m, 239, 159), rgb555_to_rgb888(0x4000));
transform(&mut m, [170, 0, 0, 204], [0, 0]); // Approximate 160/240 and 128/160 sampling steps.
assert!(frame(&m).pixels().iter().all(|&p| p == 0xff0000));
}
#[test]
fn bitmap_green_swap_disabled_bg_and_forced_blank_remain_consistent() {
for mode in [4, 5] {
let mut m = setup(mode);
m.write16(PAL + 2, 0x001f).unwrap();
m.write16(PAL + 4, 0x03e0).unwrap();
if mode == 4 {
m.write16(VRAM, 0x0201).unwrap();
} else {
m.write32(VRAM, 0x03e0_001f).unwrap();
}
m.write16(GREENSWAP, 1).unwrap();
assert_eq!(&frame(&m).pixels()[..2], &[0xffff00, 0]);
m.write16(DISPCNT, mode | 0x10).unwrap();
assert!(frame(&m)
.pixels()
.iter()
.all(|&p| p == rgb555_to_rgb888(0x4000)));
m.write16(DISPCNT, 0xffff).unwrap();
assert!(frame(&m).pixels().iter().all(|&p| p == 0xffffff));
}
}
fn assert_demo_frame(
f: &Framebuffer,
mode: gba_demos::bitmap_demo::BitmapMode,
pan: (i32, i32),
rotated: bool,
zoomed: bool,
page: usize,
) {
use gba_demos::bitmap_demo::BitmapMode;
let (w, h) = if mode == BitmapMode::Mode4 {
(240, 160)
} else {
(160, 128)
};
let a = f64::from(match (rotated, zoomed) {
(false, false) => 256,
(false, true) => 128,
(true, false) => 181,
(true, true) => 90,
}) / 256.0;
let b = if rotated { a } else { 0.0 };
for y in 0..160 {
for x in 0..240 {
let dx = x as f64 - 120.0;
let dy = y as f64 - 80.0;
let tx = (f64::from(w / 2 + pan.0) + a * dx + b * dy).floor() as i32;
let ty = (f64::from(h / 2 + pan.1) - b * dx + a * dy).floor() as i32;
let color = if tx < 0 || ty < 0 || tx >= w || ty >= h {
0x4000
} else {
let (tx, ty) = (tx as usize, ty as usize);
if mode == BitmapMode::Mode4 {
let i = (tx / 8 + 3 * (ty / 8) + page * 64) % 256;
if i == 0 {
0x4000
} else {
((i % 32) | ((i / 8 % 32) << 5) | ((i * 3 % 32) << 10)) as u16
}
} else {
(((tx / 5 + page * 11) % 32)
| (((ty / 4 + page * 7) % 32) << 5)
| (((tx / 8 + ty / 8 + page * 5) % 32) << 10)) as u16
}
};
assert_eq!(
f.pixels()[y * WIDTH + x],
rgb555_to_rgb888(color),
"{mode:?} page{page}, pixel{x},{y}, pan={pan:?}"
);
}
}
}
#[test]
fn cpu_demos_copy_both_pages_and_flip_during_vblank_every_32_updates() {
use gba_core::input::Buttons;
use gba_demos::bitmap_demo::{BitmapDemo, BitmapMode, BITMAP_STATE};
for mode in [BitmapMode::Mode4, BitmapMode::Mode5] {
let mut demo = BitmapDemo::new(mode).unwrap();
let mut f = Framebuffer::default();
assert_eq!(demo.machine().memory().read16(DISPCNT).unwrap(), 0);
assert_eq!(demo.machine().memory().read16(VRAM + 0xa000).unwrap(), 0);
for updates in 1..=65 {
let before = demo.machine().memory().display_position();
demo.frame(Buttons::default(), &mut f).unwrap();
let page = (updates / 32) % 2;
let m = demo.machine().memory();
assert_eq!(
m.read16(DISPCNT).unwrap(),
0x400 | mode as u16 | (page as u16 * 16)
);
assert_eq!(m.read32(BITMAP_STATE).unwrap(), updates as u32);
assert_eq!(m.display_position().scanline, 160);
if updates > 1 {
assert_eq!(m.display_position().vblanks, before.vblanks + 1);
}
assert_demo_frame(&f, mode, (0, 0), false, false, page);
}
}
}
#[test]
fn cpu_bitmap_controls_pan_rotate_zoom_force_page_and_reset() {
use gba_core::input::{Button, Buttons};
use gba_demos::bitmap_demo::{BitmapDemo, BitmapMode, BITMAP_STATE};
for mode in [BitmapMode::Mode4, BitmapMode::Mode5] {
let mut demo = BitmapDemo::new(mode).unwrap();
let mut f = Framebuffer::default();
demo.frame(
Buttons::default()
.with(Button::Left, true)
.with(Button::Up, true),
&mut f,
)
.unwrap();
demo.frame(Buttons::from_bits(0xf0), &mut f).unwrap();
for bits in 0..8 {
let rotated = bits & 1 != 0;
let zoomed = bits & 2 != 0;
let page = usize::from(bits & 4 != 0);
demo.frame(
Buttons::default()
.with(Button::L, rotated)
.with(Button::R, zoomed)
.with(Button::A, page != 0),
&mut f,
)
.unwrap();
assert_demo_frame(&f, mode, (-2, -2), rotated, zoomed, page);
assert_eq!(
demo.machine().memory().read32(BITMAP_STATE + 4).unwrap() as i32,
-2
);
assert_eq!(
demo.machine().memory().read32(BITMAP_STATE + 8).unwrap() as i32,
-2
);
}
demo.frame(
Buttons::default()
.with(Button::Start, true)
.with(Button::Right, true),
&mut f,
)
.unwrap();
assert_demo_frame(&f, mode, (0, 0), false, false, 0);
assert_eq!(demo.machine().memory().read32(BITMAP_STATE + 4).unwrap(), 0);
assert_eq!(demo.machine().memory().read32(BITMAP_STATE + 8).unwrap(), 0);
}
}
#[test]
fn unsupported_bitmap_layers_leave_frame_unchanged() {
for mode in [4, 5] {
let mut m = setup(mode);
let mut f = Framebuffer::default();
f.clear(0x001f);
for extra in [0x100, 0x200, 0x800] {
let control = 0x410 | mode | extra;
m.write16(DISPCNT, control).unwrap();
assert_eq!(
m.render_frame(&mut f),
Err(VideoError::UnsupportedLayers(control))
);
}
assert!(f.pixels().iter().all(|&p| p == 0xff0000));
assert_eq!(m.cycles(), 0);
}
}

View file

@ -0,0 +1,177 @@
use gba_core::{
cpu::{Cpu, CpuError, InstructionSet},
memory::{Memory, MemoryError, ROM_START},
};
use gba_demos::{demo_program, DEMO_STEPS};
fn program(words: &[u32]) -> Memory {
Memory::new(words.iter().flat_map(|word| word.to_le_bytes()).collect()).unwrap()
}
#[test]
fn demo_computes_values_and_loops() {
let mut memory = Memory::new(demo_program()).unwrap();
let mut cpu = Cpu::new(ROM_START);
let mut saw_thumb = false;
for _ in 0..DEMO_STEPS + 5 {
saw_thumb |= cpu.instruction_set() == InstructionSet::Thumb;
cpu.step(&mut memory).unwrap();
}
assert!(saw_thumb);
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
assert_eq!(&cpu.registers()[..3], &[42, 7, 42]);
assert_eq!(cpu.pc(), ROM_START + 96);
assert_eq!(cpu.registers()[3], 0x0200_0000);
assert_eq!(&cpu.registers()[4..8], &[42, 84, 3528, 42]);
assert_eq!(memory.read32(0x0200_0000).unwrap(), 3528);
assert_eq!(memory.read32(0x0200_0004).unwrap(), 42);
assert_eq!(cpu.registers()[13], 0x0300_0100);
assert_eq!(cpu.registers()[14], ROM_START + 109);
assert_eq!(memory.read32(0x0300_00f8).unwrap(), 42);
assert_eq!(memory.read32(0x0300_00fc).unwrap(), ROM_START + 109);
assert!(cpu.flags().zero);
assert!(cpu.flags().carry);
}
#[test]
fn memory_is_little_endian() {
let memory = program(&[0x1234_5678]);
assert_eq!(memory.read8(ROM_START).unwrap(), 0x78);
assert_eq!(memory.read32(ROM_START).unwrap(), 0x1234_5678);
}
#[test]
fn ram_is_zeroed_writable_and_mirrored() {
let mut memory = Memory::new(vec![]).unwrap();
for (base, size) in [(0x0200_0000, 0x40000), (0x0300_0000, 0x8000)] {
assert_eq!(memory.read32(base).unwrap(), 0);
memory.write8(base, 0x12).unwrap();
memory.write8(base + 1, 0x34).unwrap();
assert_eq!(memory.read32(base + size).unwrap(), 0x3412);
memory.write8(base + size - 1, 0xab).unwrap();
assert_eq!(memory.read8(base + size * 2 - 1).unwrap(), 0xab);
}
}
#[test]
fn cartridge_has_three_windows() {
let memory = program(&[0x1234_5678]);
for address in [0x0800_0000, 0x0a00_0000, 0x0c00_0000] {
assert_eq!(memory.read32(address).unwrap(), 0x1234_5678);
}
}
#[test]
fn invalid_memory_accesses_return_errors() {
let mut memory = program(&[0]);
assert_eq!(memory.read8(0), Err(MemoryError::Unmapped(0)));
assert_eq!(
memory.read32(ROM_START + 1),
Err(MemoryError::Unaligned(ROM_START + 1))
);
assert_eq!(
memory.read8(ROM_START + 4),
Err(MemoryError::Unmapped(ROM_START + 4))
);
assert_eq!(
memory.write8(ROM_START, 1),
Err(MemoryError::ReadOnly(ROM_START))
);
assert_eq!(memory.write8(0, 1), Err(MemoryError::Unmapped(0)));
assert_eq!(
memory.read32(0xffff_fffc),
Err(MemoryError::Unmapped(0xffff_fffc))
);
}
#[test]
fn oversized_rom_is_rejected() {
assert!(matches!(
Memory::new(vec![0; 32 * 1024 * 1024 + 1]),
Err(MemoryError::RomTooLarge(_))
));
}
#[test]
fn rotated_immediate_is_decoded() {
let mut memory = program(&[0xe3a0_0480]); // MOV r0, #0x80000000
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[0], 0x8000_0000);
}
#[test]
fn arithmetic_wraps_at_32_bits() {
let mut memory = program(&[
0xe240_0001, // SUB r0, r0, #1
0xe280_1001, // ADD r1, r0, #1
]);
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[0], u32::MAX);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[1], 0);
}
#[test]
fn reading_pc_as_operand_includes_pipeline_offset() {
let mut memory = program(&[0xe28f_0004]); // ADD r0, pc, #4
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[0], ROM_START + 12);
}
#[test]
fn branches_use_signed_displacement_and_pipeline_offset() {
let mut memory = program(&[
0xea00_0000, // B +8
0xe3a0_0063, // skipped
0xeaff_fffc, // B -8, back to start
]);
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START + 8);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START);
assert_eq!(cpu.registers()[0], 0);
}
#[test]
fn unsupported_instructions_do_not_change_cpu() {
for instruction in [
0xf3a0_0001, // reserved condition
0xe340_0001, // CMP encoding with S=0
0xe3b0_f001, // MOVS pc, #1 (status restore unsupported)
0xe10f_f000, // MRS pc, CPSR
0xe8f0_0001, // LDM user-bank transfer with writeback
0xe100_f090, // SWP with PC destination
0xe14f_0000, // MRS SPSR in System mode
0xe16f_0f10, // CLZ (ARMv5)
0xe12f_ff30, // BLX (ARMv5)
] {
let mut memory = program(&[instruction]);
let mut cpu = Cpu::new(ROM_START);
let before = *cpu.registers();
let flags_before = cpu.flags();
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::UnsupportedInstruction {
address: ROM_START,
instruction
}),
);
assert_eq!(cpu.registers(), &before);
assert_eq!(cpu.flags(), flags_before);
}
}
#[test]
fn fetch_errors_do_not_advance_pc() {
let mut memory = Memory::new(vec![]).unwrap();
let mut cpu = Cpu::new(ROM_START);
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(ROM_START)))
);
assert_eq!(cpu.pc(), ROM_START);
}

View file

@ -0,0 +1,449 @@
use gba_core::{
cpu::{Cpu, Mode},
display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, HBLANK_START, LINES_PER_FRAME, VBLANK_START},
io::{DISPCNT, DISPSTAT, IE, IF, IME, TIMER_BASE, VCOUNT},
machine::{FrameRunError, Machine, StepKind},
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START},
};
fn memory() -> Memory {
Memory::new(vec![]).unwrap()
}
#[test]
fn display_starts_at_line_zero_with_comparison_match_but_no_irq() {
let bus = memory();
assert_eq!(bus.read16(DISPSTAT).unwrap(), 4);
assert_eq!(bus.read16(VCOUNT).unwrap(), 0);
assert_eq!(bus.read16(IF).unwrap(), 0);
assert_eq!(bus.display_position().scanline, 0);
assert_eq!(bus.display_position().line_cycle, 0);
assert_eq!(bus.display_position().frames, 0);
assert_eq!(bus.display_position().vblanks, 0);
assert_eq!(bus.cycles(), 0);
}
#[test]
fn hblank_status_starts_at_1006_not_at_the_last_visible_pixel() {
let mut bus = memory();
bus.advance_cycles(960);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 2, 0);
bus.advance_cycles(45);
assert_eq!(bus.display_position().line_cycle, 1005);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 2, 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 2, 2);
bus.advance_cycles(225);
assert_eq!(bus.read16(VCOUNT).unwrap(), 0);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 2, 2);
bus.advance_cycles(1);
assert_eq!(bus.read16(VCOUNT).unwrap(), 1);
assert_eq!(bus.display_position().line_cycle, 0);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 7, 0);
}
#[test]
fn vblank_starts_at_160_clears_at_227_and_line_count_wraps_at_228() {
let mut bus = memory();
bus.advance_cycles(VBLANK_START - 1);
assert_eq!(bus.read16(VCOUNT).unwrap(), 159);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 3, 2);
bus.advance_cycles(1);
assert_eq!(bus.read16(VCOUNT).unwrap(), 160);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 3, 1);
assert_eq!(bus.display_position().vblanks, 1);
bus.advance_cycles(67 * CYCLES_PER_LINE - 1);
assert_eq!(bus.read16(VCOUNT).unwrap(), 226);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 3, 3);
bus.advance_cycles(1);
assert_eq!(bus.read16(VCOUNT).unwrap(), 227);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 3, 0);
assert_eq!(bus.display_position().frames, 0);
bus.advance_cycles(CYCLES_PER_LINE);
assert_eq!(bus.read32(DISPSTAT).unwrap(), 4); // Compare zero matches again
assert_eq!(bus.display_position().frames, 1);
assert_eq!(bus.display_position().vblanks, 1);
assert_eq!(bus.cycles(), 280_896);
}
#[test]
fn hblank_irq_occurs_on_every_line_including_hidden_lines() {
let mut bus = memory();
bus.write16(DISPSTAT, 0x10).unwrap();
for line in 0..LINES_PER_FRAME {
bus.advance_cycles(HBLANK_START - 1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(VCOUNT).unwrap(), line as u16);
assert_eq!(bus.read16(IF).unwrap(), 2);
bus.write16(IF, 2).unwrap();
bus.advance_cycles(CYCLES_PER_LINE - HBLANK_START);
assert_eq!(bus.read16(IF).unwrap(), 0);
}
assert_eq!(bus.display_position().frames, 1);
}
#[test]
fn vblank_and_compare_requests_latch_without_ie_or_ime_and_acknowledge_independently() {
let mut bus = memory();
bus.write16(DISPSTAT, 0xa028).unwrap(); // VBlank + VCount=160
bus.advance_cycles(VBLANK_START);
assert_eq!(bus.read16(IF).unwrap(), 5);
assert!(!bus.irq_pending());
bus.write16(IE, 5).unwrap();
assert!(!bus.irq_pending());
bus.write32(IME, 1).unwrap();
assert!(bus.irq_pending());
bus.write16(IF, 1).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 4);
assert!(bus.irq_pending());
bus.write16(IF, 4).unwrap();
assert!(!bus.irq_pending());
bus.advance_cycles(0);
bus.advance_cycles(10);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(CYCLES_PER_FRAME - 10);
assert_eq!(bus.read16(IF).unwrap(), 5);
}
#[test]
fn register_masks_read_only_fields_and_word_accesses_are_coherent() {
let mut bus = memory();
bus.advance_cycles(17 * CYCLES_PER_LINE + HBLANK_START);
bus.write32(DISPSTAT, u32::MAX).unwrap();
assert_eq!(bus.read16(DISPSTAT).unwrap(), 0xff3a);
assert_eq!(bus.read16(VCOUNT).unwrap(), 17);
assert_eq!(bus.read32(DISPSTAT).unwrap(), 0x0011_ff3a);
bus.write8(VCOUNT, 200).unwrap();
bus.write8(VCOUNT + 1, 0xff).unwrap();
assert_eq!(bus.read16(VCOUNT).unwrap(), 17);
bus.write8(DISPSTAT, 0).unwrap();
assert_eq!(bus.read16(DISPSTAT).unwrap(), 0xff02);
bus.write8(DISPSTAT + 1, 17).unwrap();
assert_eq!(bus.read16(DISPSTAT).unwrap(), 0x1106);
assert_eq!(bus.read8(DISPSTAT).unwrap(), 6);
assert_eq!(bus.read8(DISPSTAT + 1).unwrap(), 17);
assert_eq!(bus.read8(VCOUNT + 1).unwrap(), 0);
assert_eq!(bus.read16(IF).unwrap(), 0);
assert_eq!(
bus.write16(DISPSTAT + 1, 0),
Err(MemoryError::Unaligned(DISPSTAT + 1))
);
assert_eq!(bus.read16(DISPSTAT).unwrap(), 0x1106);
}
#[test]
fn writing_comparator_can_create_one_immediate_rising_match_request() {
let mut bus = memory();
bus.advance_cycles(7 * CYCLES_PER_LINE);
bus.write16(DISPSTAT, 0x0620).unwrap();
assert_eq!(bus.read16(DISPSTAT).unwrap() & 4, 0);
bus.write8(DISPSTAT + 1, 7).unwrap();
assert_eq!(bus.read16(DISPSTAT).unwrap() & 4, 4);
assert_eq!(bus.read16(IF).unwrap(), 4);
bus.write16(IF, 4).unwrap();
bus.write8(DISPSTAT + 1, 7).unwrap(); // Already matches
bus.write8(DISPSTAT, 0).unwrap();
bus.write8(DISPSTAT, 0x20).unwrap(); // Enabling alone does not create a match edge
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.write8(DISPSTAT + 1, 6).unwrap();
bus.write8(DISPSTAT + 1, 7).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 4);
}
#[test]
fn zero_comparator_irq_waits_for_the_next_line_zero_entry() {
let mut bus = memory();
bus.write16(DISPSTAT, 0x20).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(CYCLES_PER_FRAME - 1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 4);
bus.write16(IF, 4).unwrap();
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 0);
}
#[test]
fn out_of_range_comparator_values_never_match() {
let mut bus = memory();
for compare in 228..=255 {
bus.write16(DISPSTAT, (compare << 8) | 0x20).unwrap();
bus.advance_cycles(CYCLES_PER_FRAME);
assert_eq!(bus.read16(DISPSTAT).unwrap() & 4, 0);
assert_eq!(bus.read16(IF).unwrap(), 0);
}
}
#[test]
fn enabling_vblank_or_hblank_irq_does_not_replay_an_active_status() {
let mut bus = memory();
bus.advance_cycles(VBLANK_START);
bus.write16(DISPSTAT, 8).unwrap();
bus.advance_cycles(0);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(CYCLES_PER_FRAME - 1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 1);
let mut bus = memory();
bus.advance_cycles(HBLANK_START);
bus.write16(DISPSTAT, 0x10).unwrap();
bus.advance_cycles(CYCLES_PER_LINE - 1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 2);
}
#[test]
fn forced_blank_and_display_mode_changes_do_not_stop_or_reset_the_clock() {
let mut bus = memory();
bus.write16(DISPCNT, 0x80).unwrap();
bus.write16(DISPSTAT, 0x18).unwrap();
bus.advance_cycles(VBLANK_START);
let position = bus.display_position();
assert_eq!(bus.read16(IF).unwrap(), 3);
bus.write16(DISPCNT, 0x403).unwrap();
assert_eq!(bus.display_position(), position);
bus.advance_cycles(CYCLES_PER_FRAME);
assert_eq!(bus.display_position().vblanks, 2);
assert_eq!(bus.display_position().scanline, 160);
}
#[test]
fn timer_and_display_events_can_latch_in_the_same_clock_batch() {
let mut bus = memory();
let reload = 65536 - HBLANK_START;
bus.write32(TIMER_BASE, 0x00c0_0000 | reload).unwrap();
bus.write16(DISPSTAT, 0x10).unwrap();
bus.advance_cycles(HBLANK_START);
assert_eq!(bus.read16(IF).unwrap(), 0x0a);
bus.write16(IF, 2).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 8);
}
#[test]
fn bulk_clock_advances_match_a_cycle_by_cycle_reference() {
for compare in [0_u16, 1, 160, 226, 227, 255] {
let mut bus = memory();
bus.write16(DISPSTAT, (compare << 8) | 0x38).unwrap();
let mut line = 0_u16;
let mut cycle = 0_u16;
let mut frames = 0_u64;
let mut vblanks = 0_u64;
for chunk in [
0,
1,
959,
46,
226,
17_777,
CYCLES_PER_FRAME * 2 + 99,
1232,
77,
] {
let mut pending = 0;
for _ in 0..chunk {
cycle += 1;
if cycle == 1006 {
pending |= 2;
}
if cycle == 1232 {
cycle = 0;
line += 1;
if line == 228 {
line = 0;
frames += 1;
}
if line == 160 {
vblanks += 1;
pending |= 1;
}
if line == compare {
pending |= 4;
}
}
}
bus.advance_cycles(chunk);
assert_eq!(
bus.read16(IF).unwrap(),
pending,
"compare {compare}, chunk {chunk}"
);
let flags = u16::from((160..227).contains(&line))
| (u16::from(cycle >= 1006) << 1)
| (u16::from(line == compare) << 2);
assert_eq!(bus.read16(DISPSTAT).unwrap(), (compare << 8) | 0x38 | flags);
let position = bus.display_position();
assert_eq!(
(
position.scanline,
position.line_cycle,
position.frames,
position.vblanks
),
(line, cycle, frames, vblanks)
);
bus.write16(IF, 7).unwrap();
}
}
}
#[test]
fn maximum_batches_preserve_multi_frame_counts_and_do_not_overflow_u32() {
let mut bus = memory();
bus.write16(DISPSTAT, 0xa038).unwrap();
for multiplier in 1..=2_u64 {
bus.advance_cycles(u32::MAX);
let total = multiplier * u64::from(u32::MAX);
let phase = total % u64::from(CYCLES_PER_FRAME);
let position = bus.display_position();
assert_eq!(position.frames, total / u64::from(CYCLES_PER_FRAME));
assert_eq!(
position.vblanks,
(total + u64::from(CYCLES_PER_FRAME - VBLANK_START)) / u64::from(CYCLES_PER_FRAME)
);
assert_eq!(
u64::from(position.scanline),
phase / u64::from(CYCLES_PER_LINE)
);
assert_eq!(
u64::from(position.line_cycle),
phase % u64::from(CYCLES_PER_LINE)
);
assert_eq!(bus.read16(IF).unwrap(), 7);
assert_eq!(bus.cycles(), total);
bus.write16(IF, 7).unwrap();
}
}
#[test]
fn display_sources_enter_an_original_irq_handler_acknowledge_and_return() {
for (control, source, edge) in [
(8, 1, VBLANK_START),
(0x10, 2, HBLANK_START),
(0xa020, 4, VBLANK_START),
] {
let mut bios = vec![0; BIOS_SIZE];
for (index, instruction) in [0xe1c1_20b2_u32, 0xe280_0001, 0xe25e_f004]
.into_iter()
.enumerate()
{
// STRH r2,[r1,#2] (IF); ADD r0,r0,#1; SUBS pc,lr,#4
let offset = 0x18 + index * 4;
bios[offset..offset + 4].copy_from_slice(&instruction.to_le_bytes());
}
let rom = [
0xe3a0_1301_u32,
0xe281_1c02,
0xe3a0_2000 | source,
0xe283_3001,
0xeaff_fffe,
]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect();
let mut bus = Memory::with_bios(rom, bios).unwrap();
let mut cpu = Cpu::new(ROM_START);
for _ in 0..3 {
cpu.step(&mut bus).unwrap();
}
bus.write16(DISPSTAT, control).unwrap();
bus.write16(IE, source as u16).unwrap();
bus.write32(IME, 1).unwrap();
bus.advance_cycles(edge - 1);
let mut machine = Machine::new(cpu, bus);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[3], 1);
assert_eq!(machine.memory().read16(IF).unwrap(), source as u16);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().mode(), Mode::Irq);
assert_eq!(machine.cpu().pc(), 0x18);
assert_eq!(machine.cpu().registers()[14], ROM_START + 20);
for _ in 0..3 {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
assert_eq!(machine.cpu().registers()[0], 1);
assert_eq!(machine.cpu().mode(), Mode::System);
assert_eq!(machine.cpu().pc(), ROM_START + 16);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
}
}
#[test]
fn failed_block_store_does_not_change_window_controls_or_advance_display() {
let rom = [
0xe3a0_2301_u32, // MOV r2,#0x04000000
0xe282_2048, // ADD r2,r2,#0x48 (WININ/WINOUT)
0xe3a0_003f, // MOV r0,#0x3f
0xe3a0_1001, // MOV r1,#1
0xe8a2_001f, // STMIA r2!,{r0-r4}; last word is unmapped 0x04000058
]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect();
let mut bus = Memory::new(rom).unwrap();
let mut cpu = Cpu::new(ROM_START);
for _ in 0..4 {
cpu.step(&mut bus).unwrap();
}
bus.advance_cycles(7 * CYCLES_PER_LINE);
let before = cpu.clone();
let position = bus.display_position();
let mut machine = Machine::new(cpu, bus);
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.memory().display_position(), position);
assert_eq!(machine.memory().read32(gba_core::io::WININ).unwrap(), 0);
assert_eq!(machine.memory().read16(DISPSTAT).unwrap(), 0);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
}
#[test]
fn frame_runner_waits_for_next_vblank_event_not_an_already_set_flag() {
let mut bus = memory();
bus.write32(0x0300_0000, 0xeaff_fffe).unwrap(); // 3-cycle IWRAM self-branch
bus.advance_cycles(VBLANK_START - 2);
let mut machine = Machine::new(Cpu::new(0x0300_0000), bus);
assert_eq!(machine.run_until_vblank(1).unwrap(), 1);
assert_eq!(machine.memory().display_position().line_cycle, 1);
assert_eq!(machine.memory().display_position().vblanks, 1);
let steps = machine
.run_until_vblank((CYCLES_PER_FRAME / 3) as usize)
.unwrap();
assert_eq!(steps, (CYCLES_PER_FRAME / 3) as usize);
assert_eq!(machine.memory().display_position().line_cycle, 1);
assert_eq!(machine.memory().display_position().vblanks, 2);
}
#[test]
fn frame_runner_limits_and_cpu_errors_preserve_completed_progress_only() {
let mut bus = memory();
bus.write32(0x0300_0000, 0xeaff_fffe).unwrap();
let mut machine = Machine::new(Cpu::new(0x0300_0000), bus);
assert_eq!(
machine.run_until_vblank(0),
Err(FrameRunError::StepLimit(0))
);
assert_eq!(machine.cycles(), 0);
assert_eq!(
machine.run_until_vblank(2),
Err(FrameRunError::StepLimit(2))
);
assert_eq!(machine.cycles(), 6);
assert_eq!(machine.memory().display_position().line_cycle, 6);
machine
.memory_mut()
.write32(0x0300_0000, 0xf000_0000)
.unwrap();
let position = machine.memory().display_position();
assert!(matches!(
machine.run_until_vblank(10),
Err(FrameRunError::Cpu(_))
));
assert_eq!(machine.memory().display_position(), position);
assert_eq!(machine.cycles(), 6);
}

View file

@ -0,0 +1,860 @@
use gba_core::{
cpu::{Cpu, Mode},
display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, HBLANK_START, VBLANK_START},
dma::{DmaError, DMA_BASE, DMA_STRIDE},
io::{DISPCNT, DISPSTAT, IE, IF, IME, TIMER_BASE, WAITCNT},
machine::{FrameRunError, Machine, MachineError, StepKind},
memory::{Memory, MemoryError, BIOS_SIZE, PALETTE_START, ROM_START, VRAM_START},
timing::{bus_cycles, AccessKind, AccessWidth},
};
const SOURCE: u32 = 0x0200_0000;
const DEST: u32 = 0x0300_0000;
fn words(values: &[u32]) -> Vec<u8> {
values
.iter()
.flat_map(|value| value.to_le_bytes())
.collect()
}
fn machine() -> Machine {
Machine::new(
Cpu::new(ROM_START),
Memory::new(words(&[0xeaff_fffe])).unwrap(),
)
}
fn base(channel: usize) -> u32 {
DMA_BASE + channel as u32 * DMA_STRIDE
}
fn configure(
bus: &mut Memory,
channel: usize,
source: u32,
destination: u32,
count: u16,
control: u16,
) {
bus.write32(base(channel), source).unwrap();
bus.write32(base(channel) + 4, destination).unwrap();
bus.write32(
base(channel) + 8,
u32::from(count) | (u32::from(control | 0x8000) << 16),
)
.unwrap();
}
fn advance_to(machine: &mut Machine, cycle: u64) {
let elapsed = (cycle - machine.cycles()) as u32;
machine.memory_mut().advance_cycles(elapsed);
}
fn enabled(machine: &Machine, channel: usize) -> bool {
machine.memory().read16(base(channel) + 10).unwrap() & 0x8000 != 0
}
fn units(machine: &mut Machine, channel: usize, count: usize) {
let cpu = machine.cpu().clone();
for _ in 0..count {
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel });
assert_eq!(machine.last_timing().code_cycles, 0);
assert_eq!(machine.cpu(), &cpu);
}
}
#[test]
fn reset_register_layout_masks_and_write_only_readback() {
let mut bus = Memory::new(vec![]).unwrap();
for channel in 0..4 {
for offset in 0..12 {
assert_eq!(bus.read8(base(channel) + offset).unwrap(), 0);
bus.write8(base(channel) + offset, 0xff).unwrap();
}
assert_eq!(bus.read32(base(channel)).unwrap(), 0);
assert_eq!(bus.read32(base(channel) + 4).unwrap(), 0);
assert_eq!(bus.read16(base(channel) + 8).unwrap(), 0);
assert_eq!(
bus.read16(base(channel) + 10).unwrap(),
if channel == 3 { 0xffe0 } else { 0xf7e0 }
);
assert_eq!(
bus.read32(base(channel) + 8).unwrap(),
if channel == 3 {
0xffe0_0000
} else {
0xf7e0_0000
}
);
bus.write8(base(channel) + 11, 0).unwrap();
assert_eq!(bus.read16(base(channel) + 10).unwrap(), 0xe0);
}
assert_eq!(
bus.read8(DMA_BASE - 1),
Err(MemoryError::Unmapped(DMA_BASE - 1))
);
assert_eq!(
bus.read8(DMA_BASE + 48),
Err(MemoryError::Unmapped(DMA_BASE + 48))
);
assert_eq!(
bus.read16(DMA_BASE + 1),
Err(MemoryError::Unaligned(DMA_BASE + 1))
);
assert_eq!(
bus.write32(DMA_BASE + 10, 1),
Err(MemoryError::Unaligned(DMA_BASE + 10))
);
assert_eq!(
bus.read16(DMA_BASE + 0x400),
Err(MemoryError::Unmapped(DMA_BASE + 0x400))
);
}
#[test]
fn immediate_halfword_and_word_copies_pause_cpu_until_completion() {
for channel in 0..4 {
for word in [false, true] {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0x4433_2211).unwrap();
machine
.memory_mut()
.write32(SOURCE + 4, 0x8877_6655)
.unwrap();
configure(
machine.memory_mut(),
channel,
SOURCE,
DEST,
if word { 2 } else { 4 },
if word { 0x400 } else { 0 },
);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0); // Register writes do not execute DMA.
assert_eq!(machine.cycles(), 0);
units(&mut machine, channel, if word { 2 } else { 4 });
assert!(!enabled(&machine, channel));
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x4433_2211);
assert_eq!(machine.memory().read32(DEST + 4).unwrap(), 0x8877_6655);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.cycles(), if word { 16 } else { 18 });
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
}
}
#[test]
fn source_and_destination_address_modes_match_reference_copies() {
for word in [false, true] {
let width = if word { 4 } else { 2 };
for source_mode in 0..3 {
for dest_mode in 0..4 {
let mut machine = machine();
let mut expected = [0_u32; 9];
for index in 0..9 {
machine
.memory_mut()
.write32(SOURCE + index * 4, 0x0123_0123 * (index + 1))
.unwrap();
}
let mut source = SOURCE + 16;
let mut destination = DEST + 16;
// Read the source and simulate the destination independently of DMA state.
let mut expected_bytes = [0_u8; 36];
for _ in 0..4 {
for byte in 0..width {
expected_bytes[(destination - DEST + byte) as usize] =
machine.memory().read8(source + byte).unwrap();
}
source = match source_mode {
0 => source + width,
1 => source - width,
_ => source,
};
destination = match dest_mode {
0 | 3 => destination + width,
1 => destination - width,
_ => destination,
};
}
for (index, chunk) in expected_bytes.chunks_exact(4).enumerate() {
expected[index] = u32::from_le_bytes(chunk.try_into().unwrap());
}
let control = source_mode << 7 | dest_mode << 5 | if word { 0x400 } else { 0 };
configure(machine.memory_mut(), 3, SOURCE + 16, DEST + 16, 4, control);
units(&mut machine, 3, 4);
for (index, value) in expected.into_iter().enumerate() {
assert_eq!(
machine.memory().read32(DEST + index as u32 * 4).unwrap(),
value
);
}
}
}
}
}
#[test]
fn addresses_mask_channel_bits_and_align_to_transfer_width() {
for channel in 0..4 {
for word in [false, true] {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0xaabb_ccdd).unwrap();
let source_high = if channel == 0 {
0xf800_0000
} else {
0xf000_0000
};
let dest_high = if channel == 3 {
0xf000_0000
} else {
0xf800_0000
};
configure(
machine.memory_mut(),
channel,
SOURCE | source_high | 3,
DEST | dest_high | 3,
1,
if word { 0x400 } else { 0 },
);
units(&mut machine, channel, 1);
assert_eq!(
machine.memory().read32(DEST).unwrap(),
if word { 0xaabb_ccdd } else { 0xaabb_0000 }
);
}
}
}
#[test]
fn zero_count_means_channel_maximum_and_count_high_bits_are_masked() {
for (channel, count, expected) in [
(0, 0, 0x4000),
(1, 0xc000, 0x4000),
(2, 0xc003, 3),
(3, 0, 0x10000),
] {
let mut machine = machine();
machine.memory_mut().write16(SOURCE, 0x5a5a).unwrap();
configure(machine.memory_mut(), channel, SOURCE, DEST, count, 0x140); // Both addresses fixed.
units(&mut machine, channel, expected - 1);
assert!(enabled(&machine, channel));
units(&mut machine, channel, 1);
assert!(!enabled(&machine, channel));
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x5a5a);
assert_eq!(machine.cycles(), expected as u64 * 4 + 2);
}
}
#[test]
fn immediate_repeat_completes_once_and_does_not_stall_forever() {
let mut machine = machine();
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, 0x200);
units(&mut machine, 0, 1);
assert!(!enabled(&machine, 0));
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
#[test]
fn address_and_count_latches_change_only_on_enable_edge() {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0x2222_1111).unwrap();
machine.memory_mut().write16(SOURCE + 16, 0x3333).unwrap();
configure(machine.memory_mut(), 2, SOURCE, DEST, 2, 0);
machine.memory_mut().write32(base(2), SOURCE + 16).unwrap();
machine
.memory_mut()
.write32(base(2) + 4, DEST + 16)
.unwrap();
machine.memory_mut().write16(base(2) + 8, 1).unwrap();
machine.memory_mut().write16(base(2) + 10, 0x8000).unwrap(); // Still enabled, not a new edge.
units(&mut machine, 2, 2);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x2222_1111);
assert_eq!(machine.memory().read16(DEST + 16).unwrap(), 0);
machine.memory_mut().write16(base(2) + 10, 0x8000).unwrap();
units(&mut machine, 2, 1);
assert_eq!(machine.memory().read16(DEST + 16).unwrap(), 0x3333);
assert!(!enabled(&machine, 2));
}
#[test]
fn disabling_cancels_pending_dma_and_reenable_reloads_latches() {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0xabcd_1234).unwrap();
configure(machine.memory_mut(), 1, SOURCE, DEST, 2, 0);
units(&mut machine, 1, 1);
machine.memory_mut().write8(base(1) + 11, 0).unwrap();
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.memory().read16(DEST + 2).unwrap(), 0);
machine.memory_mut().write8(base(1) + 11, 0x80).unwrap();
units(&mut machine, 1, 2);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0xabcd_1234);
}
#[test]
fn simultaneous_channels_run_in_priority_order_before_cpu_or_irq() {
let mut machine = machine();
machine.memory_mut().write16(IE, 0x0f00).unwrap();
machine.memory_mut().write16(IME, 1).unwrap();
for channel in (0..4).rev() {
machine
.memory_mut()
.write16(SOURCE + channel as u32 * 2, channel as u16 + 1)
.unwrap();
configure(
machine.memory_mut(),
channel,
SOURCE + channel as u32 * 2,
DEST,
1,
0x4000,
);
}
for channel in 0..4 {
units(&mut machine, channel, 1);
assert_eq!(machine.memory().read16(DEST).unwrap(), channel as u16 + 1);
assert_eq!(
machine.memory().read16(IF).unwrap(),
((1 << (channel + 1)) - 1) << 8
);
}
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
}
#[test]
fn display_request_preempts_lower_priority_dma_at_unit_boundary() {
let mut machine = machine();
machine.memory_mut().write16(SOURCE, 0x1111).unwrap();
machine.memory_mut().write16(SOURCE + 2, 0x3333).unwrap();
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, 0x2000); // HBlank.
configure(machine.memory_mut(), 3, SOURCE + 2, DEST, 3, 0x140); // Immediate fixed fill.
machine.memory_mut().advance_cycles(HBLANK_START - 1);
units(&mut machine, 3, 1); // Crosses HBlank with DMA0 pending.
units(&mut machine, 0, 1);
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x1111);
units(&mut machine, 3, 2); // Resumes the lower-priority block.
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x3333);
assert!(!enabled(&machine, 3));
}
#[test]
fn blank_requests_do_not_depend_on_dispstat_or_forced_blank() {
for (timing, edge) in [(0x1000, VBLANK_START), (0x2000, HBLANK_START)] {
let mut machine = machine();
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, timing);
machine.memory_mut().write16(DISPCNT, 0x80).unwrap();
machine.memory_mut().advance_cycles(edge - 1);
assert_eq!(machine.memory().read16(DISPSTAT).unwrap() & 0x38, 0);
assert_eq!(machine.step().unwrap(), StepKind::Instruction); // Clock crosses edge.
units(&mut machine, 0, 1);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
}
}
#[test]
fn enabling_during_blank_waits_for_next_edge_and_zero_advance_does_not_trigger() {
for (timing, edge, period) in [
(0x1000, VBLANK_START, CYCLES_PER_FRAME),
(0x2000, HBLANK_START, CYCLES_PER_LINE),
] {
let mut machine = machine();
machine.memory_mut().advance_cycles(edge);
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, timing);
machine.memory_mut().advance_cycles(0);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
let elapsed = (machine.cycles() - u64::from(edge)) as u32;
machine.memory_mut().advance_cycles(period - elapsed);
units(&mut machine, 0, 1);
}
}
#[test]
fn hblank_dma_runs_on_all_visible_lines_but_not_hidden_lines() {
let mut machine = machine();
configure(machine.memory_mut(), 1, SOURCE, DEST, 1, 0x2340); // Repeat HBlank, fixed addresses.
machine.memory_mut().write16(DISPSTAT, 0x10).unwrap();
for line in 0..228_u64 {
let edge = line * u64::from(CYCLES_PER_LINE) + u64::from(HBLANK_START);
advance_to(&mut machine, edge);
assert_eq!(machine.memory().read16(IF).unwrap(), 2); // HBlank IRQ still occurs on hidden lines.
machine.memory_mut().write16(IF, 2).unwrap();
if line < 160 {
units(&mut machine, 1, 1);
} else {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
}
let edge = u64::from(CYCLES_PER_FRAME + HBLANK_START);
advance_to(&mut machine, edge);
units(&mut machine, 1, 1);
}
#[test]
fn vblank_repeat_preserves_source_reloads_destination_and_count() {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0x2222_1111).unwrap();
machine
.memory_mut()
.write32(SOURCE + 4, 0x4444_3333)
.unwrap();
configure(machine.memory_mut(), 3, SOURCE, DEST, 2, 0x5260); // VBlank, repeat, dest reload, IRQ.
machine.memory_mut().advance_cycles(VBLANK_START);
units(&mut machine, 3, 2);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x2222_1111);
assert!(enabled(&machine, 3));
assert_eq!(machine.memory().read16(IF).unwrap(), 0x800);
machine.memory_mut().write16(IF, 0x800).unwrap();
assert_eq!(machine.step().unwrap(), StepKind::Instruction); // Same VBlank does not retrigger.
let next = u64::from(VBLANK_START + CYCLES_PER_FRAME);
advance_to(&mut machine, next);
units(&mut machine, 3, 2);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x4444_3333);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x800);
assert!(enabled(&machine, 3));
}
#[test]
fn repeated_increment_destination_continues_and_reload_uses_programmed_values() {
for reload in [false, true] {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0x2222_1111).unwrap();
machine
.memory_mut()
.write32(SOURCE + 4, 0x4444_3333)
.unwrap();
configure(
machine.memory_mut(),
0,
SOURCE,
DEST,
1,
0x1200 | if reload { 0x60 } else { 0 },
);
// Initial count/address are latched. Repeat reloads use newly programmed values.
machine.memory_mut().write16(base(0) + 8, 2).unwrap();
machine
.memory_mut()
.write32(base(0) + 4, DEST + 16)
.unwrap();
machine.memory_mut().advance_cycles(VBLANK_START);
units(&mut machine, 0, 1);
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x1111);
let next = u64::from(VBLANK_START + CYCLES_PER_FRAME);
advance_to(&mut machine, next);
units(&mut machine, 0, 2);
let address = if reload { DEST + 16 } else { DEST + 2 };
assert_eq!(machine.memory().read16(address).unwrap(), 0x2222);
assert_eq!(machine.memory().read16(address + 2).unwrap(), 0x3333);
}
}
#[test]
fn pending_requests_coalesce_and_busy_channel_does_not_queue_repeats() {
let mut machine = machine();
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, 0x1200);
machine.memory_mut().advance_cycles(CYCLES_PER_FRAME * 3);
units(&mut machine, 0, 1);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
let mut machine = self::machine();
configure(machine.memory_mut(), 0, SOURCE, DEST, 100, 0x2200); // Takes 402 cycles, beyond HBlank.
machine.memory_mut().advance_cycles(HBLANK_START);
units(&mut machine, 0, 99);
// Inject more display edges while the block is still active; no queued repeat.
machine.memory_mut().advance_cycles(CYCLES_PER_FRAME * 2);
units(&mut machine, 0, 1);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
#[test]
fn timer_and_display_clocks_continue_during_dma_and_latch_all_irqs() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_ffff)
.unwrap();
machine.memory_mut().write16(DISPSTAT, 0x10).unwrap();
machine.memory_mut().advance_cycles(HBLANK_START - 2);
machine.memory_mut().write16(IF, 0x3fff).unwrap();
configure(machine.memory_mut(), 2, SOURCE, DEST, 1, 0x4000);
let before = machine.cycles();
units(&mut machine, 2, 1);
assert_eq!(machine.cycles(), before + 6);
assert_eq!(
machine.memory().display_position().line_cycle,
HBLANK_START as u16 + 4
);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x40a);
assert!(!machine.memory().irq_pending()); // IE/IME are off, requests still latch.
machine.memory_mut().write16(IE, 0x400).unwrap();
machine.memory_mut().write16(IME, 1).unwrap();
assert!(machine.memory().irq_pending());
}
#[test]
fn dma_completion_irq_enters_handler_acknowledges_and_returns_without_skipping_cpu() {
for channel in 0..4 {
let mask = 1_u16 << (8 + channel);
let mut bios = vec![0; BIOS_SIZE];
bios[0x18..0x24].copy_from_slice(&words(&[
0xe1c1_20b2, // STRH r2,[r1,#2] (IF)
0xe280_0001, // ADD r0,r0,#1
0xe25e_f004, // SUBS pc,lr,#4
]));
let rom = words(&[
0xe59f_1008, // LDR r1,[pc,#8] (IE)
0xe59f_2008, // LDR r2,[pc,#8] (mask)
0xe283_3001, // ADD r3,r3,#1 (interrupted instruction)
0xeaff_fffe,
IE,
u32::from(mask),
]);
let mut bus = Memory::with_bios(rom, bios).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut bus).unwrap();
cpu.step(&mut bus).unwrap();
configure(&mut bus, channel, SOURCE, DEST, 2, 0x4000);
bus.write16(IE, mask).unwrap();
bus.write16(IME, 1).unwrap();
let mut machine = Machine::new(cpu, bus);
units(&mut machine, channel, 1);
assert_eq!(machine.memory().read16(IF).unwrap(), 0); // Not yet complete.
units(&mut machine, channel, 1);
assert_eq!(machine.memory().read16(IF).unwrap(), mask);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().mode(), Mode::Irq);
assert_eq!(machine.cpu().registers()[14], ROM_START + 12);
for _ in 0..3 {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.cpu().mode(), Mode::System);
assert_eq!(machine.cpu().pc(), ROM_START + 8);
assert_eq!(machine.cpu().registers()[0], 1);
assert_eq!(machine.cpu().registers()[3], 0);
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[3], 1);
}
}
#[test]
fn rom_dma_uses_waitcnt_sequential_costs_and_forces_source_increment() {
for channel in 1..4 {
for source_mode in 0..3 {
for (window, waitcnt) in [(0x0800_0000, 0), (0x0a00_0000, 0x7fff), (0x0c00_0000, 0)] {
for word in [false, true] {
let mut bus = Memory::new(words(&[0x2222_1111, 0x4444_3333])).unwrap();
bus.write16(WAITCNT, waitcnt).unwrap();
let width = if word {
AccessWidth::Word
} else {
AccessWidth::Halfword
};
let stride = if word { 4 } else { 2 };
configure(
&mut bus,
channel,
window,
DEST,
2,
source_mode << 7 | if word { 0x400 } else { 0 },
);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
for index in 0..2 {
units(&mut machine, channel, 1);
let expected = bus_cycles(
machine.memory().waitcnt(),
window + index * stride,
width,
if index == 0 {
AccessKind::NonSequential
} else {
AccessKind::Sequential
},
) + 1;
assert_eq!(machine.last_timing().data_cycles, expected);
assert_eq!(
machine.last_timing().internal_cycles,
if index == 0 { 2 } else { 0 }
);
}
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x2222_1111);
if word {
assert_eq!(machine.memory().read32(DEST + 4).unwrap(), 0x4444_3333);
}
}
}
}
}
}
#[test]
fn rom_boundary_forces_nonsequential_timing_in_middle_of_dma() {
let mut machine = Machine::new(
Cpu::new(ROM_START),
Memory::new(vec![0x5a; 0x20004]).unwrap(),
);
configure(machine.memory_mut(), 3, ROM_START + 0x1fffe, DEST, 3, 0);
for expected in [8, 6, 4] {
// First: 5+1+2; boundary: 5+1; then 3+1.
units(&mut machine, 3, 1);
assert_eq!(machine.last_timing().total(), expected);
}
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x5a5a_5a5a);
}
#[test]
fn overlapping_copies_read_each_unit_after_previous_write() {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0x2222_1111).unwrap();
configure(machine.memory_mut(), 0, SOURCE, SOURCE + 2, 3, 0);
units(&mut machine, 0, 3);
for offset in [0, 2, 4, 6] {
assert_eq!(machine.memory().read16(SOURCE + offset).unwrap(), 0x1111);
}
}
#[test]
fn video_palette_mirrors_and_mapped_io_use_the_normal_bus() {
for (destination, alias) in [
(VRAM_START + 0x18000, VRAM_START + 0x10000),
(PALETTE_START + 0x400, PALETTE_START),
(DISPCNT, DISPCNT),
] {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0x0001_0403).unwrap();
configure(machine.memory_mut(), 3, SOURCE, destination, 1, 0x400);
units(&mut machine, 3, 1);
assert_eq!(machine.memory().read32(alias).unwrap(), 0x0001_0403);
}
}
#[test]
fn unsupported_controls_are_explicit_and_leave_state_and_time_unchanged() {
for channel in 0..4 {
for control in [0x180, 0x3000, 0x3800] {
let mut machine = machine();
configure(machine.memory_mut(), channel, SOURCE, DEST, 1, control);
let before = machine.cpu().clone();
let stored = machine.memory().read16(base(channel) + 10).unwrap();
for _ in 0..2 {
assert_eq!(
machine.step(),
Err(MachineError::Dma(DmaError::UnsupportedControl {
channel,
control: stored
}))
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.last_timing().total(), 0);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0);
assert!(enabled(&machine, channel));
}
}
}
let mut machine = machine();
configure(machine.memory_mut(), 3, SOURCE, DEST, 1, 0x800); // DRQ even without special timing.
assert!(matches!(
machine.step(),
Err(MachineError::Dma(DmaError::UnsupportedControl { .. }))
));
}
#[test]
fn invalid_memory_and_dma_register_destinations_are_diagnostics() {
for (source, destination, error) in [
(
0,
DEST,
DmaError::UnsupportedSource {
channel: 3,
address: 0,
},
),
(
0x0e00_0000,
DEST,
DmaError::Memory {
channel: 3,
error: MemoryError::Unmapped(0x0e00_0000),
},
),
(
SOURCE,
ROM_START,
DmaError::Memory {
channel: 3,
error: MemoryError::ReadOnly(ROM_START),
},
),
(
SOURCE,
0x0e00_0000,
DmaError::Memory {
channel: 3,
error: MemoryError::Unmapped(0x0e00_0000),
},
),
(
SOURCE,
DMA_BASE,
DmaError::RegisterDestination {
channel: 3,
address: DMA_BASE,
},
),
] {
let mut machine = machine();
configure(machine.memory_mut(), 3, source, destination, 1, 0x4000);
let before = machine.cpu().clone();
assert_eq!(machine.step(), Err(MachineError::Dma(error)));
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert!(enabled(&machine, 3));
}
}
#[test]
fn failing_later_unit_preserves_earlier_data_but_not_partial_current_unit() {
let mut machine = Machine::new(
Cpu::new(ROM_START),
Memory::new(vec![0x11, 0x22, 0x33]).unwrap(),
);
configure(machine.memory_mut(), 3, ROM_START, DEST, 2, 0x4000);
units(&mut machine, 3, 1);
let timing = machine.last_timing();
let cycles = machine.cycles();
let position = machine.memory().display_position();
for _ in 0..2 {
assert_eq!(
machine.step(),
Err(MachineError::Dma(DmaError::Memory {
channel: 3,
error: MemoryError::Unmapped(ROM_START + 3)
}))
);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x2211);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().display_position(), position);
assert_eq!(machine.last_timing(), timing);
assert!(enabled(&machine, 3));
}
}
#[test]
fn failed_io_write_does_not_partially_change_display_control() {
let mut machine = machine();
machine.memory_mut().write32(SOURCE, 0xffff_ffff).unwrap();
configure(machine.memory_mut(), 3, SOURCE, 0x0400_0058, 1, 0x400);
assert!(machine.step().is_err());
assert_eq!(machine.memory().read16(DISPCNT).unwrap(), 0);
assert_eq!(machine.cycles(), 0);
}
#[test]
fn cpu_can_enable_dma_with_a_combined_count_control_store() {
let rom = words(&[
0xe59f_0010, // LDR r0,[pc,#16] (DMA3CNT)
0xe59f_1010, // LDR r1,[pc,#16] (count/control)
0xe580_1000, // STR r1,[r0]
0xe282_2001, // ADD r2,r2,#1 (must wait for transfer)
0xeaff_fffe,
0xe1a0_0000,
base(3) + 8,
0x8400_0002,
]);
let mut bus = Memory::new(rom).unwrap();
bus.write32(SOURCE, 0x1234_5678).unwrap();
bus.write32(base(3), SOURCE).unwrap();
bus.write32(base(3) + 4, DEST).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
for _ in 0..3 {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
assert_eq!(machine.cpu().pc(), ROM_START + 12);
units(&mut machine, 3, 2);
assert_eq!(machine.memory().read32(DEST).unwrap(), 0x1234_5678);
assert_eq!(machine.cpu().registers()[2], 0);
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[2], 1);
}
#[test]
fn failed_cpu_block_store_cannot_enable_dma_or_change_its_hidden_latches() {
let rom = words(&[
0xe59f_000c, // LDR r0,[pc,#12] (DMA3CNT)
0xe59f_100c, // LDR r1,[pc,#12] (count/control)
0xe8a0_0006, // STMIA r0!,{r1,r2}; second word unmapped.
0xeaff_fffe,
0xe1a0_0000,
base(3) + 8,
0x8400_0001,
]);
let mut bus = Memory::new(rom).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut bus).unwrap();
cpu.step(&mut bus).unwrap();
configure(&mut bus, 3, SOURCE, DEST, 2, 0);
bus.write16(base(3) + 10, 0).unwrap();
let before = cpu.clone();
let mut machine = Machine::new(cpu, bus);
assert!(matches!(machine.step(), Err(MachineError::Cpu(_))));
assert_eq!(machine.cpu(), &before);
assert!(!enabled(&machine, 3));
assert_eq!(machine.cycles(), 0);
machine.memory_mut().write16(base(3) + 10, 0x8000).unwrap();
units(&mut machine, 3, 2); // The count latch is still 2, not 1.
assert!(!enabled(&machine, 3));
}
#[test]
fn cpu_only_and_clock_only_calls_never_execute_dma() {
let mut bus = Memory::new(words(&[0xe280_0001])).unwrap();
bus.write16(SOURCE, 0x1234).unwrap();
configure(&mut bus, 0, SOURCE, DEST, 1, 0);
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut bus).unwrap();
bus.advance_cycles(CYCLES_PER_FRAME);
assert_eq!(cpu.registers()[0], 1);
assert_eq!(bus.read16(DEST).unwrap(), 0);
let mut machine = Machine::new(cpu, bus);
units(&mut machine, 0, 1);
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x1234);
}
#[test]
fn frame_runner_counts_dma_units_obeys_limits_and_reports_dma_errors() {
let mut machine = machine();
configure(machine.memory_mut(), 0, SOURCE, DEST, 3, 0);
assert_eq!(
machine.run_until_vblank(2),
Err(FrameRunError::StepLimit(2))
);
assert_eq!(machine.cpu().pc(), ROM_START);
assert_eq!(machine.cycles(), 10);
machine.memory_mut().advance_cycles(VBLANK_START - 11);
assert_eq!(machine.run_until_vblank(1).unwrap(), 1);
assert_eq!(machine.memory().display_position().scanline, 160);
assert_eq!(machine.cpu().pc(), ROM_START);
configure(machine.memory_mut(), 0, SOURCE, DEST, 1, 0x3000);
assert!(matches!(
machine.run_until_vblank(1),
Err(FrameRunError::Dma(_))
));
}
#[test]
fn vblank_runner_stops_at_event_before_newly_requested_dma_executes() {
let mut machine = machine();
machine.memory_mut().write16(SOURCE, 0x1234).unwrap();
configure(machine.memory_mut(), 3, SOURCE, DEST, 1, 0x1000);
machine.memory_mut().advance_cycles(VBLANK_START - 1);
assert_eq!(machine.run_until_vblank(1).unwrap(), 1);
assert_eq!(machine.memory().read16(DEST).unwrap(), 0);
units(&mut machine, 3, 1);
assert_eq!(machine.memory().read16(DEST).unwrap(), 0x1234);
}

View file

@ -0,0 +1,61 @@
use gba_core::{
io::*,
memory::{Memory, MemoryError, OAM_START as OAM, PALETTE_START as PAL, VRAM_START as VRAM},
video::{rgb555_to_rgb888 as rgb, Framebuffer, HEIGHT, WIDTH},
};
#[path = "effects/colors.rs"]
mod colors;
#[path = "effects/demo.rs"]
mod demo;
#[path = "effects/windows.rs"]
mod windows;
fn setup() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.write16(DISPCNT, 0x700).unwrap();
m.write16(PAL, 0x4210).unwrap();
for (bg, color) in [0x001f, 0x03e0, 0x7c00].into_iter().enumerate() {
let bg = bg as u32;
m.write16(PAL + (bg + 1) * 2, color).unwrap();
m.write16(BG0CNT + bg * 2, (((24 + bg) << 8) | (bg << 2) | bg) as u16)
.unwrap();
for offset in (0..32).step_by(2) {
m.write16(VRAM + bg * 0x4000 + offset, (bg as u16 + 1) * 0x1111)
.unwrap();
}
}
for obj in 0..128 {
m.write16(OAM + obj * 8, 0x200).unwrap();
}
for offset in (0..32).step_by(2) {
m.write16(VRAM + 0x10000 + offset, 0x1111).unwrap();
}
m.write16(PAL + 0x202, 0x7fff).unwrap();
m
}
fn render(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
m.render_frame(&mut f).unwrap();
f
}
fn pixel(m: &Memory, x: usize, y: usize) -> u32 {
render(m).pixels()[y * WIDTH + x]
}
fn object(m: &mut Memory, index: u32, a: u16, b: u16, c: u16) {
m.write16(OAM + index * 8, a).unwrap();
m.write16(OAM + index * 8 + 2, b).unwrap();
m.write16(OAM + index * 8 + 4, c).unwrap();
}
fn window(m: &mut Memory, h: u16, v: u16, inside: u16, outside: u16) {
m.write16(DISPCNT, m.read16(DISPCNT).unwrap() | 0x2000)
.unwrap();
m.write16(WIN0H, h).unwrap();
m.write16(WIN0V, v).unwrap();
m.write16(WININ, inside).unwrap();
m.write16(WINOUT, outside).unwrap();
}

View file

@ -0,0 +1,172 @@
use super::*;
#[test]
fn alpha_uses_five_bit_channels_clamps_coefficients_and_saturates_sums() {
let mut m = setup();
m.write16(BLDCNT, 0x0241).unwrap(); // BG0 with BG1.
for (coefficients, expected) in [
(0, 0),
(0x0808, 0x01ef),
(0x0010, 31),
(0x1000, 0x3e0),
(0x1f1f, 0x3ff),
] {
m.write16(BLDALPHA, coefficients).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(expected));
}
m.write16(PAL + 2, 0x7fff).unwrap();
m.write16(PAL + 4, 0x7fff).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x7fff));
m.write16(PAL + 2, 1).unwrap();
m.write16(PAL + 4, 1).unwrap();
m.write16(BLDALPHA, 0x0808).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(1)); // Sum fractional terms before rounding.
}
#[test]
fn alpha_requires_top_first_target_and_immediate_second_target() {
let mut m = setup();
m.write16(BLDALPHA, 0x0808).unwrap();
for control in [0x0441, 0x0242, 0x0142, 0x2041, 0x0240] {
m.write16(BLDCNT, control).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(31), "control={control:x}");
}
m.write16(BLDCNT, 0x0441).unwrap();
m.write16(VRAM + 0x4000, 0).unwrap(); // Transparent BG1 exposes BG2.
assert_eq!(pixel(&m, 0, 0), rgb(0x3c0f));
assert_eq!(pixel(&m, 4, 0), rgb(31));
}
#[test]
fn windows_remove_layers_before_selecting_blend_targets() {
let mut m = setup();
m.write16(BLDALPHA, 0x0808).unwrap();
m.write16(BLDCNT, 0x0441).unwrap();
window(&mut m, 0x0008, 0x0008, 0x25, 0x3f); // Hide BG1 only inside.
assert_eq!(pixel(&m, 0, 0), rgb(0x3c0f));
assert_eq!(pixel(&m, 8, 0), rgb(31));
}
#[test]
fn brightness_uses_selected_top_layer_and_clamped_write_only_coefficient() {
let mut m = setup();
m.write16(PAL + 2, 0x4210).unwrap();
for (control, coefficient, expected) in [
(0x81, 0, 0x4210),
(0x81, 8, 0x5ef7),
(0x81, 31, 0x7fff),
(0xc1, 8, 0x2108),
(0xc1, 16, 0),
(0xc1, 31, 0),
(0x82, 16, 0x4210),
(0x01, 16, 0x4210),
] {
m.write16(BLDCNT, control).unwrap();
m.write32(BLDY, coefficient).unwrap();
assert_eq!(m.read16(BLDY).unwrap(), 0);
assert_eq!(pixel(&m, 0, 0), rgb(expected));
}
m.write16(BLDCNT, 0x81).unwrap();
m.write8(BLDY, 8).unwrap();
m.write8(BLDY + 1, 0xff).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x5ef7));
}
#[test]
fn backdrop_is_a_target_but_cannot_blend_with_itself() {
let mut m = setup();
m.write16(DISPCNT, 0).unwrap();
m.write16(BLDCNT, 0x2060).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x4210));
m.write16(BLDCNT, 0xa0).unwrap();
m.write16(BLDY, 16).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x7fff));
m.write16(DISPCNT, 0x100).unwrap();
m.write16(BLDCNT, 0x2041).unwrap();
m.write16(BLDALPHA, 0x1000).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x4210)); // Backdrop is not pre-brightened.
}
#[test]
fn object_can_be_first_or_second_target_but_never_blends_with_another_object() {
let mut m = setup();
m.write16(DISPCNT, 0x1100).unwrap();
object(&mut m, 0, 0, 0, 0);
object(&mut m, 1, 0, 0, 0xc00); // Lower priority; never a second OBJ target.
m.write16(BLDALPHA, 0x0808).unwrap();
m.write16(BLDCNT, 0x0150).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x3dff)); // White OBJ + red BG0.
m.write16(BLDCNT, 0x1050).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x7fff)); // No OBJ-to-OBJ blend.
object(&mut m, 0, 0, 0, 0x400); // OBJ0 below BG0; later OBJ still excluded.
m.write16(BLDCNT, 0x1041).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x3dff));
}
#[test]
fn semi_transparent_obj_overrides_mode_and_first_target_selection() {
let mut m = setup();
m.write16(DISPCNT, 0x1100).unwrap();
object(&mut m, 0, 0x400, 0, 0);
m.write16(BLDALPHA, 0x0808).unwrap();
m.write16(BLDY, 16).unwrap();
for mode in 0..4 {
m.write16(BLDCNT, 0x100 | (mode << 6)).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x3dff));
}
window(&mut m, 0x0004, 0x0008, 0x11, 0x31);
assert_eq!(pixel(&m, 0, 0), rgb(0x7fff)); // Window can disable forced alpha.
assert_eq!(pixel(&m, 4, 0), rgb(0x3dff));
}
#[test]
fn semi_transparent_obj_falls_back_to_selected_brightness_without_second_target() {
let mut m = setup();
m.write16(DISPCNT, 0x1100).unwrap();
object(&mut m, 0, 0x400, 0, 0);
m.write16(BLDY, 16).unwrap();
for (control, expected) in [(0xc0, 0x7fff), (0xd0, 0), (0x10, 0x7fff), (0x1d0, 0)] {
m.write16(BLDCNT, control).unwrap();
// Last case has a second target; zero alpha coefficients produce black.
assert_eq!(pixel(&m, 0, 0), rgb(expected));
}
m.write16(BLDALPHA, 0x0808).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x3dff)); // Alpha takes precedence over brightness.
}
#[test]
fn color_effects_work_in_affine_and_all_bitmap_modes() {
for mode in 1..=5 {
let mut m = setup();
m.write16(DISPCNT, 0x400 | mode).unwrap();
m.write16(BG2CNT, 0).unwrap();
m.write16(BG2PA, 256).unwrap();
m.write16(BG2PD, 256).unwrap();
if mode <= 2 {
m.write16(BG2CNT, 0x1000).unwrap(); // Byte map at 0x8000, tile0 at zero.
m.write16(VRAM + 0x8000, 0).unwrap();
m.write16(VRAM, 0x0101).unwrap();
} else {
m.write16(VRAM, if mode == 4 { 0x0101 } else { 31 })
.unwrap();
}
m.write16(BLDCNT, 0x2044).unwrap();
m.write16(BLDALPHA, 0x1000).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x4210), "mode={mode}");
window(&mut m, 0x0001, 0x0001, 4, 0x24);
assert_eq!(pixel(&m, 0, 0), rgb(31));
}
}
#[test]
fn green_swap_follows_effects_and_forced_blank_bypasses_every_effect() {
let mut m = setup();
m.write16(PAL + 2, 0x3e0).unwrap();
m.write16(BLDCNT, 0xc1).unwrap();
m.write16(BLDY, 16).unwrap();
window(&mut m, 0x0001, 0x0001, 1, 0x21);
m.write16(GREENSWAP, 1).unwrap();
assert_eq!(&render(&m).pixels()[..2], &[0, 0xff00]);
m.write16(DISPCNT, 0xffff).unwrap();
assert!(render(&m).pixels().iter().all(|&p| p == 0xffffff));
}

View file

@ -0,0 +1,166 @@
use super::*;
use gba_core::input::{Button, Buttons};
use gba_demos::{
effects_demo::{EffectsDemo, EFFECTS_STATE},
tile_demo::TileDemo,
};
#[test]
fn cpu_demo_moves_wraps_cancels_resets_and_updates_effect_registers_during_vblank() {
let mut demo = EffectsDemo::new().unwrap();
let mut frame = Framebuffer::default();
assert_eq!(demo.machine().memory().read16(DISPCNT).unwrap(), 0);
let mut expected_x = 0u32;
let mut expected_y = 0u32;
let sequence = [
Buttons::default(),
Buttons::default()
.with(Button::Left, true)
.with(Button::Up, true),
Buttons::default()
.with(Button::Right, true)
.with(Button::Down, true),
Buttons::default()
.with(Button::Right, true)
.with(Button::Left, true),
Buttons::default().with(Button::A, true),
Buttons::default().with(Button::B, true),
Buttons::default()
.with(Button::A, true)
.with(Button::B, true),
Buttons::default()
.with(Button::Start, true)
.with(Button::Right, true),
Buttons::default(),
];
for (i, buttons) in sequence.into_iter().enumerate() {
let before = demo.machine().cycles();
demo.frame(buttons, &mut frame).unwrap();
let memory = demo.machine().memory();
// Explicit expected movements keep this independent of the ARM program.
if i == 1 {
expected_x = 510;
expected_y = 510;
}
if i == 2 || i == 7 {
expected_x = 0;
expected_y = 0;
}
assert_eq!(memory.read32(EFFECTS_STATE).unwrap(), i as u32 + 1);
assert_eq!(memory.read32(EFFECTS_STATE + 4).unwrap(), expected_x);
assert_eq!(memory.read32(EFFECTS_STATE + 8).unwrap(), expected_y);
assert_eq!(memory.read16(DISPCNT).unwrap(), 0x3340);
assert_eq!(memory.read16(WININ).unwrap(), 0x1f);
assert_eq!(memory.read16(WINOUT).unwrap(), 0x3f);
assert_eq!(memory.read16(BLDALPHA).unwrap(), 0x808);
assert_eq!(
memory.read16(BLDCNT).unwrap(),
match i {
4 | 6 => 0x251,
5 => 0xff,
_ => 0xbf,
}
);
assert_eq!(memory.display_position().scanline, 160);
assert!(demo.machine().cycles() > before);
// The center is in WIN0 and therefore unchanged by any effect.
assert_eq!(frame.pixels()[80 * WIDTH + 120], rgb(0x7fff));
}
}
#[test]
fn cpu_brightness_demo_matches_tile_scene_inside_and_outside_moving_window() {
let mut demo = EffectsDemo::new().unwrap();
let mut base = TileDemo::new().unwrap();
let mut actual = Framebuffer::default();
let mut original = Framebuffer::default();
// Include wrapped scroll, affine objects, and brightness changes.
for (index, buttons) in [
Buttons::default()
.with(Button::Left, true)
.with(Button::Up, true),
Buttons::default()
.with(Button::B, true)
.with(Button::L, true),
Buttons::default()
.with(Button::B, true)
.with(Button::R, true),
Buttons::default().with(Button::Start, true),
]
.into_iter()
.enumerate()
{
demo.frame(buttons, &mut actual).unwrap();
base.frame(buttons, &mut original).unwrap();
let (left, top) = if index < 3 { (62, 38) } else { (64, 40) };
for y in 0..HEIGHT {
for x in 0..WIDTH {
let old = original.pixels()[y * WIDTH + x];
// Recover the exact five-bit channels from RGB888.
let channels = [(old >> 19) & 31, (old >> 11) & 31, (old >> 3) & 31];
let mut expected = 0u16;
for (channel, shift) in channels.into_iter().zip([0, 5, 10]) {
let changed = if (left..left + 112).contains(&x) && (top..top + 80).contains(&y)
{
channel
} else if index == 1 || index == 2 {
channel - channel / 2
} else {
channel + (31 - channel) / 2
};
expected |= (changed as u16) << shift;
}
assert_eq!(
actual.pixels()[y * WIDTH + x],
rgb(expected),
"frame={index} pixel={x},{y}"
);
}
}
}
}
#[test]
fn cpu_window_demo_retains_active_region_when_bottom_moves_beyond_vcount_range() {
let mut demo = EffectsDemo::new().unwrap();
let mut frame = Framebuffer::default();
for index in 0..54 {
demo.frame(Buttons::default().with(Button::Down, true), &mut frame)
.unwrap();
// x=80 avoids foreground crosses and sprites. It stays inside WIN0's X bounds.
let sy = 2 * (index + 1);
let raw: u16 =
[0x260, 0x3a0, 0x7d20, 0x7e80][((80 / 32 + sy / 32) % 2) * 2 + (80 / 2 + sy / 2) % 2];
let expected = if index == 53 {
// New bottom=228 cannot match any line. Last frame's active flag survives.
raw
} else {
[0, 5, 10].into_iter().fold(0, |value, shift| {
let channel = (raw >> shift) & 31;
value | ((channel + (31 - channel) / 2) << shift)
})
};
assert_eq!(frame.pixels()[80], rgb(expected), "frame={index}");
}
let mut snapshot = Framebuffer::default();
demo.machine().memory().render_frame(&mut snapshot).unwrap();
assert_ne!(snapshot.pixels()[80], frame.pixels()[80]);
}
#[test]
fn dma_word_write_updates_selection_and_coefficients_together() {
use gba_core::{
cpu::Cpu,
dma::DMA_BASE,
machine::{Machine, StepKind},
};
let mut m = setup();
m.write32(0x0200_0000, 0x0808_0241).unwrap();
m.write32(DMA_BASE + 36, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 40, BLDCNT).unwrap();
m.write32(DMA_BASE + 44, 0x8400_0001).unwrap();
let mut machine = Machine::new(Cpu::new(0x0300_0000), m);
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
assert_eq!(machine.memory().read32(BLDCNT).unwrap(), 0x0808_0241);
assert_eq!(pixel(machine.memory(), 0, 0), rgb(0x1ef));
}

View file

@ -0,0 +1,176 @@
use super::*;
#[test]
fn register_masks_byte_merges_and_write_only_reads() {
let mut m = setup();
for (address, mask) in [
(WININ, 0x3f3f),
(WINOUT, 0x3f3f),
(BLDCNT, 0x3fff),
(BLDALPHA, 0x1f1f),
] {
assert_eq!(m.read16(address).unwrap(), 0);
m.write16(address, 0xffff).unwrap();
assert_eq!(m.read16(address).unwrap(), mask);
m.write8(address, 0).unwrap();
assert_eq!(m.read16(address).unwrap(), mask & 0xff00);
m.write8(address + 1, 0).unwrap();
assert_eq!(m.read16(address).unwrap(), 0);
}
for address in [WIN0H, WIN1H, WIN0V, WIN1V, BLDY] {
m.write16(address, 0xffff).unwrap();
assert_eq!(m.read16(address).unwrap(), 0);
}
m.write32(BLDY, u32::MAX).unwrap();
assert_eq!(m.read32(BLDY).unwrap(), 0);
assert_eq!(
m.read8(0x0400_0058),
Err(MemoryError::Unmapped(0x0400_0058))
);
assert_eq!(
m.read8(0x0401_0048),
Err(MemoryError::Unmapped(0x0401_0048))
);
assert_eq!(
m.write16(WININ + 1, 1),
Err(MemoryError::Unaligned(WININ + 1))
);
}
#[test]
fn no_enabled_windows_ignore_zero_masks_but_apply_effects() {
let mut m = setup();
m.write16(BLDCNT, 0xc1).unwrap();
m.write16(BLDY, 16).unwrap();
assert_eq!(pixel(&m, 0, 0), 0);
m.write16(DISPCNT, 0x2700).unwrap(); // Empty WIN0; outside mask is zero.
assert_eq!(pixel(&m, 0, 0), rgb(0x4210));
}
#[test]
fn bounds_are_half_open_and_byte_writes_preserve_other_bound() {
let mut m = setup();
window(&mut m, 0, 0, 1, 2);
m.write8(WIN0H, 30).unwrap();
m.write8(WIN0H + 1, 10).unwrap();
m.write8(WIN0V, 40).unwrap();
m.write8(WIN0V + 1, 20).unwrap();
let f = render(&m);
for y in 0..HEIGHT {
for x in 0..WIDTH {
let inside = (10..30).contains(&x) && (20..40).contains(&y);
assert_eq!(
f.pixels()[y * WIDTH + x],
rgb(if inside { 31 } else { 0x3e0 })
);
}
}
}
#[test]
fn inverted_bounds_wrap_equal_bounds_are_empty_and_offscreen_bounds_clip() {
let mut m = setup();
for (h, v) in [
(0xdc14, 0x960a),
(0xfafa, 0x00ff),
(0xf005, 0xa005),
(0x00ff, 0x00ff),
] {
window(&mut m, h, v, 1, 2);
let f = render(&m);
for y in 0..HEIGHT {
for x in 0..WIDTH {
// Independent modular-distance definition of a half-open interval.
let inside = (x as u8).wrapping_sub((h >> 8) as u8)
< (h as u8).wrapping_sub((h >> 8) as u8)
&& (y as u8).wrapping_sub((v >> 8) as u8)
< (v as u8).wrapping_sub((v >> 8) as u8);
assert_eq!(
f.pixels()[y * WIDTH + x],
rgb(if inside { 31 } else { 0x3e0 })
);
}
}
}
}
#[test]
fn win0_overrides_win1_without_combining_masks() {
let mut m = setup();
window(&mut m, 0x0a1e, 0x0a1e, 0x0200, 4); // WIN0 hides all; WIN1 selects BG1.
m.write16(WIN1H, 0x1428).unwrap();
m.write16(WIN1V, 0x1428).unwrap();
m.write16(DISPCNT, 0x6700).unwrap();
assert_eq!(pixel(&m, 25, 25), rgb(0x4210));
assert_eq!(pixel(&m, 35, 35), rgb(0x03e0));
assert_eq!(pixel(&m, 45, 45), rgb(0x7c00));
m.write16(DISPCNT, 0x6500).unwrap(); // Window cannot enable a globally disabled BG.
assert_eq!(pixel(&m, 35, 35), rgb(0x4210));
}
#[test]
fn window_effect_bit_is_independent_of_layer_bits_and_backdrop_is_always_visible() {
let mut m = setup();
window(&mut m, 0x0008, 0x0008, 1, 0x21);
m.write16(BLDCNT, 0xc1).unwrap();
m.write16(BLDY, 16).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(31));
assert_eq!(pixel(&m, 8, 0), 0);
m.write16(WINOUT, 0x20).unwrap();
m.write16(BLDCNT, 0xa0).unwrap(); // Brighten backdrop.
assert_eq!(pixel(&m, 8, 0), rgb(0x7fff));
}
#[test]
fn object_window_uses_nonzero_texels_not_color_priority_or_visible_obj_mask() {
let mut m = setup();
m.write16(DISPCNT, 0x9700).unwrap();
m.write16(WINOUT, 0x0102).unwrap(); // OBJ region BG0 only, outside BG1 only.
object(&mut m, 0, 0x800, 0, 0xfc00); // Palette15, priority3 are irrelevant.
m.write16(VRAM + 0x10000, 0x1010).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(0x3e0));
assert_eq!(pixel(&m, 1, 0), rgb(31));
assert_eq!(pixel(&m, 8, 0), rgb(0x3e0));
m.write16(DISPCNT, 0x8700).unwrap(); // OBJ master enable is also required.
assert_eq!(pixel(&m, 1, 0), rgb(0x3e0));
m.write16(DISPCNT, 0x1700).unwrap(); // Window OBJ never becomes a visible sprite.
assert_eq!(pixel(&m, 1, 0), rgb(31));
}
#[test]
fn visible_objects_do_not_occlude_object_window_and_rectangles_take_precedence() {
let mut m = setup();
m.write16(DISPCNT, 0x9700).unwrap();
m.write16(WINOUT, 0x0102).unwrap();
object(&mut m, 0, 0, 0, 0); // Occupies the same pixels as the later window OBJ.
object(&mut m, 127, 0x800, 0, 0);
assert_eq!(pixel(&m, 0, 0), rgb(31)); // OBJ mask hides color OBJ, not window coverage.
window(&mut m, 0x0002, 0x0008, 4, 0x0102);
assert_eq!(pixel(&m, 0, 0), rgb(0x7c00));
assert_eq!(pixel(&m, 2, 0), rgb(31));
m.write16(WIN1H, 0x0004).unwrap();
m.write16(WIN1V, 0x0008).unwrap();
m.write16(WININ, 0x0204).unwrap();
m.write16(DISPCNT, 0xf700).unwrap();
assert_eq!(pixel(&m, 2, 0), rgb(0x3e0));
assert_eq!(pixel(&m, 4, 0), rgb(31));
}
#[test]
fn object_window_supports_regular_flips_and_affine_sampling() {
let mut m = setup();
m.write16(DISPCNT, 0x9700).unwrap();
m.write16(WINOUT, 0x0102).unwrap();
for offset in (0..32).step_by(2) {
m.write16(VRAM + 0x10000 + offset, 0).unwrap();
}
m.write16(VRAM + 0x10000, 1).unwrap();
object(&mut m, 0, 0x800, 0x1000, 0);
assert_eq!(pixel(&m, 7, 0), rgb(31));
assert_eq!(pixel(&m, 0, 0), rgb(0x3e0));
object(&mut m, 0, 0x900, 0, 0);
m.write16(OAM + 6, 256).unwrap();
m.write16(OAM + 30, 256).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb(31));
assert_eq!(pixel(&m, 1, 0), rgb(0x3e0));
}

View file

@ -0,0 +1,89 @@
use gba_core::{
cpu::{Cpu, InstructionSet, Mode},
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START},
};
use gba_demos::{demo_bios, exception_demo_program, EXCEPTION_DEMO_STEPS};
#[test]
fn original_demo_enters_supervisor_and_returns_to_arm_and_thumb() {
let mut memory = Memory::with_bios(exception_demo_program(), demo_bios()).unwrap();
let mut cpu = Cpu::new(ROM_START);
let expected = [
(8, Mode::Supervisor, InstructionSet::Arm),
(0x40, Mode::Supervisor, InstructionSet::Arm),
(0x44, Mode::Supervisor, InstructionSet::Arm),
(ROM_START + 4, Mode::System, InstructionSet::Arm),
(ROM_START + 8, Mode::System, InstructionSet::Arm),
(ROM_START + 12, Mode::System, InstructionSet::Thumb),
(8, Mode::Supervisor, InstructionSet::Arm),
(0x40, Mode::Supervisor, InstructionSet::Arm),
(0x44, Mode::Supervisor, InstructionSet::Arm),
(ROM_START + 14, Mode::System, InstructionSet::Thumb),
(ROM_START + 14, Mode::System, InstructionSet::Thumb),
];
assert_eq!(expected.len(), EXCEPTION_DEMO_STEPS);
for (pc, mode, state) in expected {
cpu.step(&mut memory).unwrap();
assert_eq!(
(cpu.pc(), cpu.mode(), cpu.instruction_set()),
(pc, mode, state)
);
}
assert_eq!(cpu.registers()[10], 2);
assert_eq!(cpu.registers()[14], 0); // Supervisor LR did not replace System LR
assert_eq!(cpu.cpsr(), 0x3f);
for _ in 0..5 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(cpu.pc(), ROM_START + 14);
assert_eq!(cpu.registers()[10], 2);
}
#[test]
fn optional_bios_image_is_exactly_16_kib_read_only_and_not_mirrored() {
for size in [0, BIOS_SIZE - 1, BIOS_SIZE + 1] {
assert!(matches!(
Memory::with_bios(vec![], vec![0; size]),
Err(MemoryError::InvalidBiosSize(actual)) if actual == size
));
}
let bios: Vec<u8> = (0..BIOS_SIZE).map(|index| index as u8).collect();
let mut memory = Memory::with_bios(vec![], bios).unwrap();
assert_eq!(memory.read32(0).unwrap(), 0x0302_0100);
assert_eq!(memory.read16(0x3ffe).unwrap(), 0xfffe);
assert_eq!(memory.read8(0x3fff).unwrap(), 255);
for address in [0x4000, 0x0100_0000] {
assert_eq!(memory.read8(address), Err(MemoryError::Unmapped(address)));
assert_eq!(
memory.write8(address, 1),
Err(MemoryError::Unmapped(address))
);
}
assert_eq!(memory.write8(0x3fff, 0), Err(MemoryError::ReadOnly(0x3fff)));
assert_eq!(
memory.write16(0x3ffe, 0),
Err(MemoryError::ReadOnly(0x3ffe))
);
assert_eq!(memory.write32(0, 0), Err(MemoryError::ReadOnly(0)));
assert_eq!(memory.read32(0).unwrap(), 0x0302_0100);
assert_eq!(memory.read16(0x3ffe).unwrap(), 0xfffe);
}
#[test]
fn no_bios_is_mapped_by_default() {
let mut memory = Memory::new(vec![]).unwrap();
assert_eq!(memory.read32(0), Err(MemoryError::Unmapped(0)));
assert_eq!(memory.write32(0, 0), Err(MemoryError::Unmapped(0)));
}
#[test]
fn reset_can_fetch_caller_supplied_vector_code() {
let mut bios = vec![0; BIOS_SIZE];
bios[0..4].copy_from_slice(&0xe3a0_002a_u32.to_le_bytes()); // MOV r0, #42
let mut memory = Memory::with_bios(vec![], bios).unwrap();
let mut cpu = Cpu::at_reset();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[0], 42);
assert_eq!(cpu.pc(), 4);
assert_eq!(cpu.cpsr(), 0xd3);
}

View file

@ -0,0 +1,297 @@
use gba_core::{
input::{Button, Buttons},
io::{BG2PA, BG2PD, DISPCNT, GREENSWAP, KEYINPUT},
memory::{Memory, MemoryError, PALETTE_START, VRAM_START},
timing::{bus_cycles, AccessKind, AccessWidth},
video::{rgb555_to_rgb888, Framebuffer, VideoError, HEIGHT, WIDTH},
};
use gba_demos::graphics_demo::{GraphicsDemo, BACKGROUND, DEMO_STATE, SQUARE_SIZE};
fn mode3() -> Memory {
let mut memory = Memory::new(vec![]).unwrap();
memory.write16(DISPCNT, 0x403).unwrap();
memory.write16(BG2PA, 256).unwrap();
memory.write16(BG2PD, 256).unwrap();
memory
}
fn assert_demo_frame(demo: &GraphicsDemo, frame: &Framebuffer, x: usize, y: usize, color: u16) {
let memory = demo.machine().memory();
assert_eq!(memory.read32(DEMO_STATE + 4).unwrap(), x as u32);
assert_eq!(memory.read32(DEMO_STATE + 8).unwrap(), y as u32);
assert_eq!(memory.read32(DEMO_STATE + 12).unwrap(), u32::from(color));
assert_eq!(memory.read16(DISPCNT).unwrap(), 0x403);
for py in 0..HEIGHT {
for px in 0..WIDTH {
let expected =
if (x..x + SQUARE_SIZE).contains(&px) && (y..y + SQUARE_SIZE).contains(&py) {
color
} else {
BACKGROUND
};
assert_eq!(
frame.pixels()[py * WIDTH + px],
rgb555_to_rgb888(expected),
"pixel {px},{py}"
);
}
}
}
#[test]
fn cpu_program_draws_mode3_and_moves_from_keyinput_without_host_vram_writes() {
let mut demo = GraphicsDemo::new().unwrap();
let mut frame = Framebuffer::default();
assert_eq!(demo.machine().memory().read16(DISPCNT).unwrap(), 0);
assert_eq!(demo.machine().memory().read16(VRAM_START).unwrap(), 0);
let startup_steps = demo.frame(Buttons::default(), &mut frame).unwrap();
assert!(startup_steps > WIDTH * HEIGHT);
assert_demo_frame(&demo, &frame, 112, 72, 0x7fff);
let previous = demo.machine().memory().display_position();
let previous_cycles = demo.machine().cycles();
assert_eq!(previous.scanline, 160);
let steps = demo
.frame(
Buttons::default()
.with(Button::Right, true)
.with(Button::Up, true),
&mut frame,
)
.unwrap();
assert!(steps < startup_steps);
let position = demo.machine().memory().display_position();
assert_eq!(position.scanline, 160);
assert_eq!(position.vblanks, previous.vblanks + 1);
assert_eq!(position.frames, previous.frames + 1);
assert_eq!(
demo.machine().cycles() - previous_cycles,
u64::from(gba_core::display::CYCLES_PER_FRAME) + u64::from(position.line_cycle)
- u64::from(previous.line_cycle)
);
assert_demo_frame(&demo, &frame, 114, 70, 0x7fff);
assert_eq!(demo.machine().memory().read32(DEMO_STATE).unwrap(), 2);
}
#[test]
fn cpu_program_clamps_edges_cancels_opposites_changes_color_and_resets() {
let mut demo = GraphicsDemo::new().unwrap();
let mut frame = Framebuffer::default();
let upper_left = Buttons::default()
.with(Button::Left, true)
.with(Button::Up, true);
for _ in 0..65 {
demo.frame(upper_left, &mut frame).unwrap();
}
assert_demo_frame(&demo, &frame, 0, 0, 0x7fff);
let all_directions = Buttons::from_bits(0xf0);
demo.frame(all_directions, &mut frame).unwrap();
assert_demo_frame(&demo, &frame, 0, 0, 0x7fff);
let lower_right = Buttons::default()
.with(Button::Right, true)
.with(Button::Down, true);
for _ in 0..120 {
demo.frame(lower_right, &mut frame).unwrap();
}
assert_demo_frame(&demo, &frame, 224, 144, 0x7fff);
demo.frame(all_directions, &mut frame).unwrap();
assert_demo_frame(&demo, &frame, 224, 144, 0x7fff);
for (buttons, color) in [
(Buttons::from_bits(2), 0x3e0),
(Buttons::from_bits(3), 0x1f),
(Buttons::default(), 0x7fff),
] {
demo.frame(buttons, &mut frame).unwrap();
assert_demo_frame(&demo, &frame, 224, 144, color);
}
demo.frame(Buttons::default().with(Button::Start, true), &mut frame)
.unwrap();
assert_demo_frame(&demo, &frame, 112, 72, 0x7fff);
}
#[test]
fn mode3_renders_row_major_colors_and_ignores_page_and_pixel_top_bit() {
let mut memory = mode3();
let mut frame = Framebuffer::default();
for (index, color) in [
(0, 0x801f),
(1, 0x03e0),
(WIDTH, 0x7c00),
(WIDTH * HEIGHT - 1, 0x7fff),
] {
memory
.write16(VRAM_START + index as u32 * 2, color)
.unwrap();
}
memory.write16(DISPCNT, 0x413).unwrap(); // Page bit has no effect in Mode 3.
memory.render_frame(&mut frame).unwrap();
assert_eq!(frame.pixels()[0], 0xff0000);
assert_eq!(frame.pixels()[1], 0x00ff00);
assert_eq!(frame.pixels()[WIDTH], 0x0000ff);
assert_eq!(frame.pixels()[WIDTH * HEIGHT - 1], 0xffffff);
assert_eq!(frame.pixels()[2], 0);
assert_eq!(memory.cycles(), 0);
}
#[test]
fn forced_blank_is_white_and_disabled_bg2_uses_palette_backdrop() {
let mut memory = mode3();
let mut frame = Framebuffer::default();
memory.write16(PALETTE_START, 0x3e0).unwrap();
memory.write16(DISPCNT, 3).unwrap();
memory.render_frame(&mut frame).unwrap();
assert!(frame.pixels().iter().all(|&pixel| pixel == 0x00ff00));
memory.write16(DISPCNT, 0x403).unwrap();
memory.render_frame(&mut frame).unwrap();
assert!(frame.pixels().iter().all(|&pixel| pixel == 0)); // Mode 3 black is opaque.
memory.write16(DISPCNT, 0xffff).unwrap();
memory.render_frame(&mut frame).unwrap();
assert!(frame.pixels().iter().all(|&pixel| pixel == 0xffffff));
}
#[test]
fn unsupported_modes_and_layers_report_errors_without_changing_output() {
let mut memory = mode3();
let mut frame = Framebuffer::default();
frame.clear(0x001f);
for mode in [6, 7] {
memory.write16(DISPCNT, 0x400 | mode).unwrap();
assert_eq!(
memory.render_frame(&mut frame),
Err(VideoError::UnsupportedMode(mode as u8))
);
}
for layer in [0x100, 0x200, 0x800] {
memory.write16(DISPCNT, 0x403 | layer).unwrap();
assert_eq!(
memory.render_frame(&mut frame),
Err(VideoError::UnsupportedLayers(0x403 | layer))
);
}
assert!(frame.pixels().iter().all(|&pixel| pixel == 0xff0000));
}
#[test]
fn display_control_masks_cgb_bit_and_green_swap_exchanges_only_green() {
let mut memory = mode3();
memory.write32(DISPCNT, u32::MAX).unwrap();
assert_eq!(memory.read32(DISPCNT).unwrap(), 0x0001_fff7);
memory.write8(DISPCNT, 3).unwrap();
memory.write8(DISPCNT + 1, 4).unwrap();
memory.write8(GREENSWAP + 1, 0xff).unwrap();
assert_eq!(memory.read32(DISPCNT).unwrap(), 0x0001_0403);
memory.write16(VRAM_START, 0x001f).unwrap();
memory.write16(VRAM_START + 2, 0x7fe0).unwrap(); // Green + blue
let mut frame = Framebuffer::default();
memory.render_frame(&mut frame).unwrap();
assert_eq!(frame.pixels()[0], 0xffff00);
assert_eq!(frame.pixels()[1], 0x0000ff);
memory.write8(GREENSWAP, 0).unwrap();
memory.render_frame(&mut frame).unwrap();
assert_eq!(frame.pixels()[0], 0xff0000);
assert_eq!(frame.pixels()[1], 0x00ffff);
}
#[test]
fn palette_and_vram_mirrors_preserve_word_and_halfword_layouts() {
let mut memory = mode3();
memory.write32(PALETTE_START + 0x400, 0x1234_5678).unwrap();
assert_eq!(memory.read32(PALETTE_START).unwrap(), 0x1234_5678);
memory.write16(0x05ff_fffe, 0xabcd).unwrap();
assert_eq!(memory.read16(PALETTE_START + 0x3fe).unwrap(), 0xabcd);
for (mirror, physical) in [
(0x0602_0000, VRAM_START),
(0x0601_8000, VRAM_START + 0x10000),
(0x06ff_fffc, VRAM_START + 0x17ffc),
] {
memory.write32(mirror, 0x1234_5678).unwrap();
assert_eq!(memory.read32(physical).unwrap(), 0x1234_5678);
assert_eq!(memory.read8(mirror).unwrap(), 0x78);
assert_eq!(memory.read16(mirror + 2).unwrap(), 0x1234);
}
memory.write32(VRAM_START + 0x17ffc, 0xabcd_1234).unwrap();
assert_eq!(memory.read32(VRAM_START + 0x1fffc).unwrap(), 0xabcd_1234);
assert_eq!(memory.read32(0x0700_0000).unwrap(), 0); // OAM starts cleared.
}
#[test]
fn video_byte_writes_duplicate_bg_and_palette_but_ignore_obj_area() {
let mut memory = mode3();
for address in [
PALETTE_START,
PALETTE_START + 0x3ff,
VRAM_START,
VRAM_START + 1,
VRAM_START + 0x13fff,
] {
memory.write8(address, 0x5a).unwrap();
assert_eq!(memory.read16(address & !1).unwrap(), 0x5a5a);
}
for address in [
VRAM_START + 0x14000,
VRAM_START + 0x17fff,
VRAM_START + 0x1ffff,
] {
memory.write16(address & !1, 0x1234).unwrap();
memory.write8(address, 0xff).unwrap();
assert_eq!(memory.read16(address & !1).unwrap(), 0x1234);
}
memory.write16(DISPCNT, 0).unwrap(); // Tile-mode OBJ area starts at 64 KiB.
memory.write16(VRAM_START + 0x10000, 0xabcd).unwrap();
memory.write8(VRAM_START + 0x18000, 0).unwrap();
assert_eq!(memory.read16(VRAM_START + 0x10000).unwrap(), 0xabcd);
}
#[test]
fn misaligned_video_writes_leave_all_bytes_unchanged() {
let mut memory = mode3();
for address in [PALETTE_START, VRAM_START] {
memory.write32(address, 0x1234_5678).unwrap();
assert_eq!(
memory.write16(address + 1, 0),
Err(MemoryError::Unaligned(address + 1))
);
assert_eq!(
memory.write32(address + 2, 0),
Err(MemoryError::Unaligned(address + 2))
);
assert_eq!(memory.read32(address).unwrap(), 0x1234_5678);
}
}
#[test]
fn keyinput_exposes_all_ten_buttons_active_low_and_ignores_writes() {
let mut memory = mode3();
assert_eq!(memory.read16(KEYINPUT).unwrap(), 0x3ff);
for bits in 0..=0x3ff {
memory.set_buttons(Buttons::from_bits(bits));
assert_eq!(memory.read16(KEYINPUT).unwrap(), !bits & 0x3ff);
memory.write16(KEYINPUT, 0).unwrap();
assert_eq!(memory.read16(KEYINPUT).unwrap(), !bits & 0x3ff);
}
let buttons = Buttons::from_bits(0xffff)
.with(Button::A, false)
.with(Button::L, false);
assert_eq!(buttons.bits(), 0x1fe);
memory.set_buttons(buttons);
memory.write8(KEYINPUT + 1, 0).unwrap();
assert_eq!(memory.read8(KEYINPUT).unwrap(), 1);
assert_eq!(memory.read8(KEYINPUT + 1).unwrap(), 2);
memory.set_buttons(Buttons::default());
assert_eq!(memory.read16(KEYINPUT).unwrap(), 0x3ff);
assert_eq!(memory.cycles(), 0);
assert_eq!(
memory.read16(KEYINPUT + 2),
Err(MemoryError::Unmapped(KEYINPUT + 2))
); // KEYCNT deferred.
}
#[test]
fn video_bus_widths_have_nominal_16_bit_bus_costs() {
for address in [PALETTE_START, VRAM_START, 0x06ff_fffc] {
for kind in [AccessKind::Sequential, AccessKind::NonSequential] {
assert_eq!(bus_cycles(0, address, AccessWidth::Byte, kind), 1);
assert_eq!(bus_cycles(0, address, AccessWidth::Halfword, kind), 1);
assert_eq!(bus_cycles(0, address, AccessWidth::Word, kind), 2);
}
}
}

View file

@ -0,0 +1,687 @@
use gba_core::{
cpu::{Cpu, InstructionSet, Mode},
display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, HBLANK_START, VBLANK_START},
dma::{DmaError, DMA_BASE},
io::{DISPSTAT, HALTCNT, IE, IF, IME, POSTFLG, TIMER_BASE},
machine::{FrameRunError, Machine, MachineError, StepKind},
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START},
timing::StepTiming,
};
fn words(code: &[u32]) -> Vec<u8> {
code.iter().flat_map(|word| word.to_le_bytes()).collect()
}
fn machine() -> Machine {
Machine::new(
Cpu::new(ROM_START),
Memory::new(words(&[0xe280_0001, 0xeaff_fffe])).unwrap(),
)
}
fn pending_timer(bus: &mut Memory) {
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
bus.advance_cycles(1);
bus.write16(TIMER_BASE + 2, 0).unwrap();
}
fn halt(machine: &mut Machine) {
// Bare bus writes are explicit host/debug setup, not CPU-originated accesses.
machine.memory_mut().write8(HALTCNT, 0).unwrap();
}
fn dma(bus: &mut Memory, control: u16, count: u16) {
bus.write32(DMA_BASE, 0x0200_0000).unwrap();
bus.write32(DMA_BASE + 4, 0x0300_0000).unwrap();
bus.write32(
DMA_BASE + 8,
u32::from(control | 0x8000) << 16 | u32::from(count),
)
.unwrap();
}
// Prepare a real store at entry+8 with operands loaded by CPU instructions.
fn store_machine(
entry: u32,
instruction: u32,
address: u32,
value: u32,
timed_setup: bool,
) -> Machine {
let program = words(&[
0xe59f_1008,
0xe59f_0008,
instruction,
0xe282_2001,
address,
value,
]);
let mut bios = vec![0; BIOS_SIZE];
let mut bus = if entry < BIOS_SIZE as u32 {
bios[entry as usize..entry as usize + program.len()].copy_from_slice(&program);
Memory::with_bios(vec![], bios).unwrap()
} else if entry == ROM_START {
Memory::with_bios(program, bios).unwrap()
} else {
let mut bus = Memory::with_bios(vec![], bios).unwrap();
for (offset, value) in program.into_iter().enumerate() {
bus.write8(entry + offset as u32, value).unwrap();
}
bus
};
let mut cpu = Cpu::new(entry);
for _ in 0..2 {
if timed_setup {
cpu.step_timed(&mut bus).unwrap();
} else {
cpu.step(&mut bus).unwrap();
}
}
Machine::new(cpu, bus)
}
#[test]
fn reset_register_masks_widths_and_reserved_bytes() {
let mut bus = Memory::new(vec![]).unwrap();
assert!(!bus.halted());
assert_eq!(bus.read32(POSTFLG).unwrap(), 0);
bus.write8(POSTFLG, 0xff).unwrap();
assert_eq!(bus.read32(POSTFLG).unwrap(), 1);
assert!(!bus.halted());
bus.write8(POSTFLG, 0xfe).unwrap();
assert_eq!(bus.read8(POSTFLG).unwrap(), 0);
bus.write16(POSTFLG + 2, 0xffff).unwrap();
assert_eq!(bus.read16(POSTFLG + 2).unwrap(), 0);
assert!(!bus.halted());
bus.write16(POSTFLG, 0x7f01).unwrap();
assert!(bus.halted());
assert_eq!(bus.read16(POSTFLG).unwrap(), 1); // HALTCNT is write-only.
assert_eq!(bus.read16(HALTCNT), Err(MemoryError::Unaligned(HALTCNT)));
assert_eq!(
bus.read8(POSTFLG + 4),
Err(MemoryError::Unmapped(POSTFLG + 4))
);
assert_eq!(
bus.read8(POSTFLG + 0x400),
Err(MemoryError::Unmapped(POSTFLG + 0x400))
);
}
#[test]
fn all_halt_values_ignore_low_seven_bits() {
for value in 0..=0x7f {
let mut machine = machine();
machine.memory_mut().write8(HALTCNT, value).unwrap();
assert!(machine.halted());
assert_eq!(machine.memory().read8(HALTCNT).unwrap(), 0);
assert_eq!(machine.cycles(), 0);
}
}
#[test]
fn halt_idle_does_not_fetch_instructions_or_change_cpu_registers() {
let mut machine = Machine::new(Cpu::new(0xdead_beec), Memory::new(vec![]).unwrap());
halt(&mut machine);
let before = machine.cpu().clone();
for cycles in [HBLANK_START, CYCLES_PER_LINE - HBLANK_START, HBLANK_START] {
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.cpu(), &before);
assert_eq!(
machine.last_timing(),
StepTiming {
idle_cycles: cycles,
..StepTiming::default()
}
);
assert!(machine.halted());
}
assert_eq!(machine.cycles(), u64::from(CYCLES_PER_LINE + HBLANK_START));
}
#[test]
fn pending_enabled_request_prevents_sleep_regardless_of_ime() {
for ime in [0, 1] {
let mut machine = machine();
pending_timer(machine.memory_mut());
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().write16(IME, ime).unwrap();
halt(&mut machine);
assert!(!machine.halted());
assert_eq!(
machine.step().unwrap(),
if ime == 0 {
StepKind::Instruction
} else {
StepKind::IrqEntry
}
);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
}
}
#[test]
fn ie_gates_wake_but_ime_does_not_and_wake_is_not_reversed_by_acknowledgement() {
let mut machine = machine();
pending_timer(machine.memory_mut());
machine.memory_mut().write16(IE, 1).unwrap(); // Wrong source.
halt(&mut machine);
assert!(machine.halted());
machine.memory_mut().write16(IME, 1).unwrap();
assert!(machine.halted());
machine.memory_mut().write16(IE, 8).unwrap();
assert!(!machine.halted());
machine.memory_mut().write16(IF, 8).unwrap();
machine.memory_mut().write16(IE, 0).unwrap();
assert!(!machine.halted());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
#[test]
fn timer_wakes_at_exact_overflow_without_ime_and_resumes_next_instruction() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_fff6)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
halt(&mut machine);
let before = machine.cpu().clone();
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, 10);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 10);
assert!(!machine.halted());
assert!(!machine.memory().irq_pending());
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[0], 1);
assert_eq!(machine.cpu().pc(), ROM_START + 4);
}
#[test]
fn cpsr_irq_mask_blocks_handler_but_not_halt_wake() {
let mut bios = vec![0; BIOS_SIZE];
bios[0..4].copy_from_slice(&0xe280_0001_u32.to_le_bytes()); // ADD r0,r0,#1
let mut bus = Memory::with_bios(vec![], bios).unwrap();
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
bus.write16(IE, 8).unwrap();
bus.write16(IME, 1).unwrap();
bus.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::at_reset(), bus);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert!(!machine.halted());
assert!(machine.memory().irq_pending());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().mode(), Mode::Supervisor);
assert_eq!(machine.cpu().registers()[0], 1);
}
#[test]
fn unmasked_wake_enters_irq_before_executing_resumed_instruction() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_ffff)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().write16(IME, 1).unwrap();
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().mode(), Mode::Irq);
assert_eq!(machine.cpu().pc(), 0x18);
assert_eq!(machine.cpu().registers()[14], ROM_START + 4);
assert_eq!(machine.cpu().registers()[0], 0);
}
#[test]
fn wake_irq_returns_to_thumb_without_skipping_instruction() {
let mut rom = words(&[0xe28f_0001, 0xe12f_ff10]); // ADD r0,pc,#1; BX r0
rom.extend(0x2107_u16.to_le_bytes()); // MOV r1,#7
let bios = 0xe25e_f004_u32.to_le_bytes().repeat(BIOS_SIZE / 4); // SUBS pc,lr,#4
let mut bus = Memory::with_bios(rom, bios).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut bus).unwrap();
cpu.step(&mut bus).unwrap();
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
bus.write16(IE, 8).unwrap();
bus.write16(IME, 1).unwrap();
bus.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(cpu, bus);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().spsr(), Some(0x3f));
machine.memory_mut().write16(TIMER_BASE + 2, 0).unwrap();
machine.memory_mut().write16(IF, 8).unwrap();
machine.step().unwrap();
assert_eq!(machine.cpu().instruction_set(), InstructionSet::Thumb);
assert_eq!(machine.cpu().pc(), ROM_START + 8);
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[1], 7);
}
#[test]
fn all_display_interrupt_sources_can_wake_halt_at_their_edges() {
for (control, mask, edge) in [
(8, 1, VBLANK_START),
(0x10, 2, HBLANK_START),
(0x0120, 4, CYCLES_PER_LINE),
] {
let mut machine = machine();
machine.memory_mut().write16(DISPSTAT, control).unwrap();
machine.memory_mut().write16(IE, mask).unwrap();
machine.memory_mut().advance_cycles(edge - 1);
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, 1);
assert_eq!(machine.cycles(), u64::from(edge));
assert!(!machine.halted());
assert_eq!(machine.memory().read16(IF).unwrap(), mask);
}
}
#[test]
fn local_interrupt_enable_is_required_to_generate_a_wake_request() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x0080_ffff)
.unwrap(); // IRQ off.
machine.memory_mut().write16(IE, 0x3fff).unwrap();
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, 1);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert!(machine.halted());
}
#[test]
fn idle_scheduling_accounts_for_timer_prescaler_phase() {
for (selection, divisor) in [1, 64, 256, 1024].into_iter().enumerate() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, ((0xc0 | selection as u32) << 16) | 0xffff)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().advance_cycles(divisor - 1);
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, 1);
assert_eq!(machine.cycles(), u64::from(divisor));
assert!(!machine.halted());
}
}
#[test]
fn cascaded_timer_wakes_only_on_predecessor_overflow() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x0080_fff6)
.unwrap(); // Ten cycles per overflow, IRQ off.
machine
.memory_mut()
.write32(TIMER_BASE + 4, 0x00c4_fffe)
.unwrap(); // Two pulses, IRQ on.
machine.memory_mut().write16(IE, 0x10).unwrap();
halt(&mut machine);
for expected in [true, false] {
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, 10);
assert_eq!(machine.halted(), expected);
}
assert_eq!(machine.memory().read16(IF).unwrap(), 0x10);
assert_eq!(machine.cycles(), 20);
}
#[test]
fn disabled_predecessor_cannot_clock_a_cascaded_timer_during_halt() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE + 4, 0x00c4_ffff)
.unwrap();
machine.memory_mut().write16(IE, 0x10).unwrap();
halt(&mut machine);
for _ in 0..4 {
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert!(machine.halted());
assert_eq!(machine.memory().read16(TIMER_BASE + 4).unwrap(), 0xffff);
}
}
#[test]
fn idle_batches_match_cycle_by_cycle_display_and_timer_execution() {
for seed in 0..16_u32 {
let configure = || {
let mut bus = Memory::new(vec![]).unwrap();
bus.write16(DISPSTAT, ((seed as u16 * 17) << 8) | 0x38)
.unwrap();
for index in 0..4 {
let control = if index != 0 && (seed + index) % 3 == 0 {
0xc4
} else {
0xc0 | ((seed + index) & 3)
};
bus.write32(
TIMER_BASE + index * 4,
control << 16 | (0xffff - (seed * 13 + index * 7)),
)
.unwrap();
}
bus.advance_cycles(seed * 91);
bus.write16(IF, 0x3fff).unwrap();
bus.write8(HALTCNT, 0).unwrap(); // IE=0, so all events are observable without wake.
bus
};
let mut reference = configure();
let mut machine = Machine::new(Cpu::new(ROM_START), configure());
for _ in 0..40 {
machine.memory_mut().write16(IF, 0x3fff).unwrap();
reference.write16(IF, 0x3fff).unwrap();
let mut elapsed = 0;
loop {
reference.advance_cycles(1);
elapsed += 1;
let position = reference.display_position();
if position.line_cycle == 0
|| u32::from(position.line_cycle) == HBLANK_START
|| reference.read16(IF).unwrap() != 0
{
break;
}
assert!(elapsed < CYCLES_PER_LINE);
}
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.last_timing().idle_cycles, elapsed);
assert_eq!(
machine.memory().display_position(),
reference.display_position()
);
assert_eq!(machine.cycles(), reference.cycles());
assert_eq!(
machine.memory().read16(IF).unwrap(),
reference.read16(IF).unwrap()
);
for index in 0..4 {
assert_eq!(
machine.memory().read32(TIMER_BASE + index * 4).unwrap(),
reference.read32(TIMER_BASE + index * 4).unwrap()
);
}
}
}
}
#[test]
fn blank_triggered_dma_continues_during_halt_and_completion_can_wake() {
let mut machine = machine();
machine.memory_mut().write16(0x0200_0000, 0x1234).unwrap();
dma(machine.memory_mut(), 0x5000, 1); // VBlank and completion IRQ.
machine.memory_mut().write16(IE, 0x100).unwrap();
halt(&mut machine);
assert_eq!(machine.run_until_vblank(320).unwrap(), 320);
assert!(machine.halted()); // No display IRQ enabled; DMA has not run yet.
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert_eq!(machine.memory().read16(0x0300_0000).unwrap(), 0x1234);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x100);
assert!(!machine.halted());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
#[test]
fn dma_without_completion_irq_does_not_wake_halt() {
let mut machine = machine();
dma(machine.memory_mut(), 0x2000, 1); // HBlank, no IRQ.
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.cycles(), u64::from(HBLANK_START));
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert!(machine.halted());
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.cycles(), u64::from(CYCLES_PER_LINE));
}
#[test]
fn dma_finishes_before_irq_delivery_after_wake_during_transfer() {
let mut machine = machine();
dma(machine.memory_mut(), 0, 2);
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_ffff)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
machine.memory_mut().write16(IME, 1).unwrap();
halt(&mut machine);
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert!(!machine.halted());
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert_eq!(machine.cpu().pc(), ROM_START);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
}
#[test]
fn unsupported_dma_power_control_write_preserves_halt_and_clock() {
let mut machine = machine();
dma(machine.memory_mut(), 0, 1);
machine.memory_mut().write16(DMA_BASE + 10, 0).unwrap();
machine.memory_mut().write32(DMA_BASE + 4, POSTFLG).unwrap();
machine.memory_mut().write16(DMA_BASE + 10, 0x8000).unwrap();
halt(&mut machine);
assert_eq!(
machine.step(),
Err(MachineError::Dma(DmaError::PowerControlDestination {
channel: 0,
address: POSTFLG
}))
);
assert!(machine.halted());
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0);
}
#[test]
fn byte_halfword_and_word_cpu_stores_enter_halt_only_from_bios() {
for timed in [false, true] {
for entry in [0x100, ROM_START, 0x0300_0000] {
for (instruction, address, value) in [
(0xe5c1_0000, HALTCNT, 0x7f), // STRB
(0xe1c1_00b0, POSTFLG, 1), // STRH
(0xe581_0000, POSTFLG, 0xffff_0001), // STR
] {
let mut machine = store_machine(entry, instruction, address, value, timed);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.halted(), entry < BIOS_SIZE as u32);
assert_eq!(machine.cpu().pc(), entry + 12);
assert_eq!(machine.cpu().registers()[2], 0);
let before = machine.cpu().clone();
if entry < BIOS_SIZE as u32 {
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.cpu(), &before);
} else {
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[2], 1);
}
}
}
}
}
#[test]
fn thumb_halt_stores_use_the_executing_address_for_bios_access() {
for entry in [0x100, ROM_START] {
let mut code = words(&[0xe28f_0001, 0xe12f_ff10]); // Enter Thumb at entry+8.
for instruction in [0x2000_u16, 0x4901, 0x7008, 0x3201] {
// MOV r0,#0; LDR r1,[pc,#4]; STRB r0,[r1]; ADD r2,#1.
code.extend(instruction.to_le_bytes());
}
code.extend(HALTCNT.to_le_bytes());
let mut bios = vec![0; BIOS_SIZE];
let mut bus = if entry < BIOS_SIZE as u32 {
bios[entry as usize..entry as usize + code.len()].copy_from_slice(&code);
Memory::with_bios(vec![], bios).unwrap()
} else {
Memory::new(code).unwrap()
};
let mut cpu = Cpu::new(entry);
for _ in 0..4 {
cpu.step(&mut bus).unwrap();
}
let mut machine = Machine::new(cpu, bus);
assert_eq!(machine.cpu().instruction_set(), InstructionSet::Thumb);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().pc(), entry + 14);
assert_eq!(machine.halted(), entry < BIOS_SIZE as u32);
}
}
#[test]
fn halt_store_finishes_and_can_wake_during_its_own_cycles() {
let mut machine = store_machine(0x100, 0xe5c1_0000, HALTCNT, 0, false);
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_ffff)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().pc(), 0x10c);
assert_eq!(machine.cycles(), 2);
assert!(!machine.halted());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[2], 1);
}
#[test]
fn stop_diagnostics_validate_entire_write_before_postflg_or_halt_changes() {
let mut bus = Memory::new(vec![]).unwrap();
for value in [0x80, 0xff] {
assert_eq!(
bus.write8(HALTCNT, value),
Err(MemoryError::UnsupportedStop)
);
assert_eq!(
bus.write16(POSTFLG, u16::from(value) << 8 | 1),
Err(MemoryError::UnsupportedStop)
);
assert_eq!(
bus.write32(POSTFLG, u32::from(value) << 8 | 1),
Err(MemoryError::UnsupportedStop)
);
assert_eq!(bus.read32(POSTFLG).unwrap(), 0);
assert!(!bus.halted());
assert_eq!(bus.cycles(), 0);
}
}
#[test]
fn bios_stop_error_preserves_cpu_timing_and_clears_access_context() {
let mut machine = store_machine(0x100, 0xe581_0000, POSTFLG, 0x8001, true);
let before = machine.cpu().clone();
assert_eq!(
machine.step(),
Err(MachineError::Cpu(MemoryError::UnsupportedStop.into()))
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.last_timing(), StepTiming::default());
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0);
assert!(!machine.halted());
// A failed non-BIOS instruction must also clear its access context.
let mut machine = store_machine(ROM_START, 0xe581_0000, 0x0400_0058, 0, true);
assert!(machine.step().is_err());
machine.memory_mut().write8(POSTFLG, 1).unwrap();
halt(&mut machine);
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 1);
assert!(machine.halted());
}
#[test]
fn non_bios_stop_write_is_ignored_not_a_diagnostic() {
for entry in [ROM_START, 0x0300_0000] {
let mut machine = store_machine(entry, 0xe581_0000, POSTFLG, 0x8001, false);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert!(!machine.halted());
assert_eq!(machine.memory().read32(POSTFLG).unwrap(), 0);
}
}
#[test]
fn failed_block_store_cannot_partially_halt_or_change_postflg() {
// The first word is mapped; the second is not. Validation precedes all writes.
let mut machine = store_machine(0x100, 0xe8a1_0005, POSTFLG, 1, false); // STMIA r1!,{r0,r2}
let before = machine.cpu().clone();
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert!(!machine.halted());
assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0);
assert_eq!(machine.cycles(), 0);
}
#[test]
fn cpu_only_apis_record_halt_but_do_not_idle_or_enforce_pause() {
for timed in [false, true] {
let mut bios = vec![0; BIOS_SIZE];
bios[..24].copy_from_slice(&words(&[
0xe59f_1008,
0xe3a0_0000,
0xe5c1_0000,
0xe282_2001,
HALTCNT,
0,
]));
let mut bus = Memory::with_bios(vec![], bios).unwrap();
let mut cpu = Cpu::new(0);
for _ in 0..3 {
if timed {
cpu.step_timed(&mut bus).unwrap();
} else {
cpu.step(&mut bus).unwrap();
}
}
assert!(bus.halted());
cpu.step(&mut bus).unwrap(); // CPU-only API intentionally bypasses machine scheduling.
assert_eq!(cpu.registers()[2], 1);
assert_eq!(bus.cycles(), 0);
assert!(bus.halted());
}
}
#[test]
fn frame_runner_remains_bounded_with_no_wake_source_and_reports_idle_cost() {
let mut machine = Machine::new(Cpu::new(ROM_START), Memory::new(vec![]).unwrap());
halt(&mut machine);
assert_eq!(
machine.run_until_vblank(0),
Err(FrameRunError::StepLimit(0))
);
assert_eq!(machine.cycles(), 0);
assert_eq!(
machine.run_until_vblank(319),
Err(FrameRunError::StepLimit(319))
);
assert!(machine.halted());
assert_eq!(machine.run_until_vblank(1).unwrap(), 1);
assert_eq!(machine.cycles(), u64::from(VBLANK_START));
assert_eq!(machine.memory().display_position().line_cycle, 0);
assert!(machine.halted());
assert_eq!(machine.run_until_vblank(456).unwrap(), 456);
assert_eq!(machine.cycles(), u64::from(VBLANK_START + CYCLES_PER_FRAME));
}
#[test]
fn externally_advanced_clock_can_wake_halt_without_cpu_execution() {
let mut machine = machine();
machine
.memory_mut()
.write32(TIMER_BASE, 0x00c0_fff0)
.unwrap();
machine.memory_mut().write16(IE, 8).unwrap();
halt(&mut machine);
let before = machine.cpu().clone();
machine.memory_mut().advance_cycles(16);
assert!(!machine.halted());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.last_timing(), StepTiming::default());
}

View file

@ -0,0 +1,272 @@
use gba_core::{
cpu::{Cpu, CpuError, InstructionSet, Mode},
io::{IE, IF, IME, TIMER_BASE, WAITCNT},
machine::{Machine, MachineError, StepKind},
memory::{Memory, MemoryError, BIOS_SIZE, ROM_START},
};
use gba_demos::timer_demo::{timer_demo, TIMER_DEMO_STEPS};
fn words(values: &[u32]) -> Vec<u8> {
values
.iter()
.flat_map(|value| value.to_le_bytes())
.collect()
}
fn timer(bus: &mut Memory) {
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
bus.write16(IE, 8).unwrap();
bus.write32(IME, 1).unwrap();
}
fn pending_timer(bus: &mut Memory) {
timer(bus);
bus.advance_cycles(1);
bus.write16(TIMER_BASE + 2, 0).unwrap();
}
// Load test operands through real instructions, without exposing CPU setters.
fn prepared(instruction: u32, operands: &[(usize, u32)]) -> Machine {
let offset = (operands.len() as u32 - 1) * 4;
let mut code: Vec<u32> = operands
.iter()
.map(|&(register, _)| 0xe59f_0000 | (register as u32) << 12 | offset)
.collect();
code.push(instruction);
code.extend(operands.iter().map(|&(_, value)| value));
let mut bus = Memory::new(words(&code)).unwrap();
let mut cpu = Cpu::new(ROM_START);
for _ in operands {
cpu.step(&mut bus).unwrap();
}
Machine::new(cpu, bus)
}
#[test]
fn timer_demo_configures_hardware_in_cpu_code_and_handles_exactly_one_irq() {
let mut machine = timer_demo().unwrap();
let mut entries = 0;
for step in 1..=TIMER_DEMO_STEPS {
let kind = machine.step().unwrap();
assert!(machine.last_timing().total() > 0);
if kind == StepKind::IrqEntry {
entries += 1;
assert_eq!(step, 13); // Nominal timing model, not a hardware measurement
assert_eq!(machine.cpu().pc(), 0x18);
assert_eq!(machine.cpu().spsr(), Some(0x1f));
assert_eq!(machine.cpu().registers()[14], ROM_START + 0x30);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
}
}
assert_eq!(entries, 1);
assert_eq!(machine.cycles(), 153);
assert_eq!(machine.cpu().mode(), Mode::System);
assert_eq!(machine.cpu().pc(), ROM_START + 0x2c);
assert_eq!(machine.cpu().registers()[10], 1);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.memory().read16(TIMER_BASE + 2).unwrap(), 0);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xfff4);
for _ in 0..100 {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
assert_eq!(machine.cpu().registers()[10], 1);
}
#[test]
fn overflow_after_instruction_is_sampled_on_next_step() {
let mut bus = Memory::new(words(&[0xe280_0001, 0xe280_0001])).unwrap();
timer(&mut bus);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[0], 1);
assert_eq!(machine.cpu().pc(), ROM_START + 4);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().registers()[0], 1); // Interrupted instruction did not execute
assert_eq!(machine.cpu().registers()[14], ROM_START + 8);
assert_eq!(machine.cycles(), 11); // First ROM access: 8; BIOS IRQ refill: 3
}
#[test]
fn missing_vector_errors_only_after_completed_irq_entry() {
let mut bus = Memory::new(vec![]).unwrap();
pending_timer(&mut bus);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); // No ROM fetch required
let before = machine.cpu().clone();
let cycles = machine.cycles();
assert_eq!(
machine.step(),
Err(MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped(
0x18
))))
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
}
#[test]
fn cpu_mask_defers_irq_until_msr_unmasks_it() {
let mut bios = vec![0; BIOS_SIZE];
bios[0..4].copy_from_slice(&0xe321_f013_u32.to_le_bytes()); // MSR CPSR_c, #SVC (clear I)
let mut bus = Memory::with_bios(vec![], bios).unwrap();
pending_timer(&mut bus);
let mut machine = Machine::new(Cpu::at_reset(), bus);
assert!(machine.memory().irq_pending());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().pc(), 4);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().registers()[14], 8);
assert_eq!(machine.cpu().spsr(), Some(0x13));
}
#[test]
fn ime_and_ie_gate_pending_device_requests_until_cpu_enables_them() {
for register in [IE, IME] {
let value = if register == IE { 8 } else { 1 };
let mut machine = prepared(0xe580_1000, &[(0, register), (1, value)]); // STR r1, [r0]
pending_timer(machine.memory_mut());
machine.memory_mut().write16(register, 0).unwrap();
assert!(!machine.memory().irq_pending());
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert!(machine.memory().irq_pending());
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
}
}
#[test]
fn uncleared_request_reenters_after_irq_return_and_no_fiq_is_generated() {
let bios = 0xe25e_f004_u32.to_le_bytes().repeat(BIOS_SIZE / 4); // SUBS pc, lr, #4
let mut bus = Memory::with_bios(words(&[0xeaff_fffe]), bios).unwrap();
pending_timer(&mut bus);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
for _ in 0..3 {
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().mode(), Mode::Irq);
assert_eq!(machine.step().unwrap(), StepKind::Instruction); // I mask prevents immediate nesting
assert_eq!(machine.cpu().mode(), Mode::System);
assert_eq!(machine.cpu().pc(), ROM_START);
}
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
}
#[test]
fn timer_irq_returns_to_thumb_without_skipping_the_interrupted_instruction() {
let mut rom = words(&[0xe28f_0001, 0xe12f_ff10]); // ADD r0, pc, #1; BX r0
rom.extend(0x2107_u16.to_le_bytes()); // MOV r1, #7 at offset 8
let bios = 0xe25e_f004_u32.to_le_bytes().repeat(BIOS_SIZE / 4);
let mut bus = Memory::with_bios(rom, bios).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut bus).unwrap();
cpu.step(&mut bus).unwrap();
pending_timer(&mut bus);
let mut machine = Machine::new(cpu, bus);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().spsr(), Some(0x3f));
assert_eq!(machine.cpu().registers()[14], ROM_START + 12);
machine.memory_mut().write16(IF, 8).unwrap();
machine.step().unwrap();
assert_eq!(machine.cpu().instruction_set(), InstructionSet::Thumb);
assert_eq!(machine.cpu().pc(), ROM_START + 8);
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[1], 7);
assert_eq!(machine.cpu().pc(), ROM_START + 10);
}
#[test]
fn diagnostics_do_not_advance_timers_or_clock() {
for instruction in [0xf3a0_0001, 0xe590_1000] {
// Unsupported or unmapped LDR
let mut bus = Memory::new(words(&[instruction])).unwrap();
timer(&mut bus);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
let before = machine.cpu().clone();
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xffff);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
}
}
#[test]
fn late_block_store_error_does_not_change_timer_reload_or_enable() {
let mut machine = prepared(
0xe8a0_003e,
&[
(0, TIMER_BASE),
(1, 0x00c0_ffff),
(2, 0x00c0_ffff),
(3, 0x00c0_ffff),
(4, 0x00c0_ffff),
(5, 123),
],
); // STMIA r0!, {r1-r5}; fifth word is unmapped
let before = machine.cpu().clone();
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
for index in 0..4 {
let address = TIMER_BASE + index * 4;
assert_eq!(machine.memory().read32(address).unwrap(), 0);
machine.memory_mut().write16(address + 2, 0x80).unwrap();
assert_eq!(machine.memory().read16(address).unwrap(), 0); // Hidden reload also unchanged
}
}
#[test]
fn late_block_store_error_does_not_acknowledge_if_or_modify_ie() {
let mut machine = prepared(
0xe8a0_001e,
&[(0, IE), (1, 0x0008_ffff), (2, 123), (3, 1), (4, 42)],
);
// IE/IF, WAITCNT, IME are valid. The fourth word at 0x0400020c is not.
pending_timer(machine.memory_mut());
machine.memory_mut().write32(IME, 0).unwrap(); // Let the invalid store execute
let before = machine.cpu().clone();
let cycles = machine.cycles();
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().read32(IE).unwrap(), 0x0008_0008);
assert_eq!(machine.memory().read32(WAITCNT).unwrap(), 0);
assert_eq!(machine.memory().read32(IME).unwrap(), 0);
assert_eq!(machine.last_timing().total(), 0);
}
#[test]
fn late_block_load_error_preserves_cpu_and_devices() {
let mut machine = prepared(0xe8b0_0006, &[(0, IME)]); // LDMIA r0!, {r1, r2}
timer(machine.memory_mut());
let before = machine.cpu().clone();
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xffff);
}
#[test]
fn skipped_condition_charges_code_access_but_not_registers() {
let mut bus = Memory::new(words(&[0x0280_0001])).unwrap(); // ADDEQ r0, r0, #1; Z clear
timer(&mut bus);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[0], 0);
assert_eq!(machine.cpu().pc(), ROM_START + 4);
assert_eq!(machine.cycles(), 8);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
}
#[test]
fn cpu_only_step_does_not_advance_device_time_or_deliver_irq() {
let mut bus = Memory::new(words(&[0xe280_0001])).unwrap();
pending_timer(&mut bus);
let mut cpu = Cpu::new(ROM_START);
let cycles = bus.cycles();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers()[0], 1);
assert_eq!(cpu.mode(), Mode::System);
assert_eq!(bus.cycles(), cycles);
assert!(bus.irq_pending());
}

View file

@ -0,0 +1,87 @@
use gba_core::memory::{Memory, MemoryError, ROM_START};
#[test]
fn halfword_and_word_writes_are_little_endian() {
let mut memory = Memory::new(vec![]).unwrap();
for address in [0x0200_0000, 0x0300_0000] {
memory.write32(address, 0x1234_5678).unwrap();
assert_eq!(memory.read8(address).unwrap(), 0x78);
assert_eq!(memory.read16(address).unwrap(), 0x5678);
assert_eq!(memory.read16(address + 2).unwrap(), 0x1234);
memory.write16(address + 2, 0xabcd).unwrap();
assert_eq!(memory.read32(address).unwrap(), 0xabcd_5678);
}
}
#[test]
fn multi_byte_writes_respect_ram_mirrors_and_region_ends() {
let mut memory = Memory::new(vec![]).unwrap();
for (base, size, end) in [
(0x0200_0000, 0x40000, 0x02ff_ffff),
(0x0300_0000, 0x8000, 0x03ff_ffff),
] {
memory.write32(end - 3, 0xdead_beef).unwrap();
assert_eq!(memory.read32(base + size - 4).unwrap(), 0xdead_beef);
memory.write16(base + size, 0x1234).unwrap();
assert_eq!(memory.read16(base).unwrap(), 0x1234);
}
}
#[test]
fn unaligned_bus_writes_fail_without_modifying_memory() {
let mut memory = Memory::new(vec![]).unwrap();
let address = 0x0200_0000;
memory.write32(address, 0xdead_beef).unwrap();
assert_eq!(
memory.write16(address + 1, 0),
Err(MemoryError::Unaligned(address + 1))
);
for offset in 1..4 {
assert_eq!(
memory.write32(address + offset, 0),
Err(MemoryError::Unaligned(address + offset))
);
}
assert_eq!(memory.read32(address).unwrap(), 0xdead_beef);
assert_eq!(
memory.read16(address + 1),
Err(MemoryError::Unaligned(address + 1))
);
}
#[test]
fn rom_and_unmapped_writes_return_errors_without_partial_changes() {
let mut memory = Memory::new(vec![0x12, 0x34, 0x56, 0x78]).unwrap();
assert_eq!(
memory.write16(ROM_START, 0),
Err(MemoryError::ReadOnly(ROM_START))
);
assert_eq!(
memory.write32(ROM_START, 0),
Err(MemoryError::ReadOnly(ROM_START))
);
assert_eq!(memory.read32(ROM_START).unwrap(), 0x7856_3412);
for address in [0, 0x0400_0058, 0xffff_fffc] {
assert_eq!(
memory.write32(address, 0),
Err(MemoryError::Unmapped(address))
);
assert_eq!(
memory.write16(address, 0),
Err(MemoryError::Unmapped(address))
);
}
}
#[test]
fn truncated_rom_halfword_read_returns_an_error() {
let memory = Memory::new(vec![0x12]).unwrap();
assert_eq!(
memory.read16(ROM_START),
Err(MemoryError::Unmapped(ROM_START + 1))
);
assert_eq!(
memory.read16(0xffff_fffe),
Err(MemoryError::Unmapped(0xffff_fffe))
);
}

View file

@ -0,0 +1,47 @@
use gba_core::{
io::*,
memory::{Memory, MemoryError, OAM_START as OAM, PALETTE_START as PAL, VRAM_START as VRAM},
video::{rgb555_to_rgb888 as rgb, Framebuffer, HEIGHT, WIDTH},
};
#[path = "mosaic/backgrounds.rs"]
mod backgrounds;
#[path = "mosaic/demo.rs"]
mod demo;
#[path = "mosaic/objects.rs"]
mod objects;
fn memory() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
for index in 0..128 {
m.write16(OAM + index * 8, 0x200).unwrap();
}
for index in 0..256 {
let color = (index & 31) | (((index >> 3) & 31) << 5) | (((index * 7) & 31) << 10);
m.write16(PAL + index * 2, color as u16).unwrap();
m.write16(PAL + 0x200 + index * 2, color as u16).unwrap();
}
m
}
fn render(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
m.render_frame(&mut f).unwrap();
f
}
fn object(m: &mut Memory, index: u32, a: u16, b: u16, c: u16) {
m.write16(OAM + index * 8, a).unwrap();
m.write16(OAM + index * 8 + 2, b).unwrap();
m.write16(OAM + index * 8 + 4, c).unwrap();
}
fn fill(m: &mut Memory, address: u32, bytes: u32, value: u16) {
for offset in (0..bytes).step_by(2) {
m.write16(address + offset, value).unwrap();
}
}
fn at(frame: &Framebuffer, x: usize, y: usize) -> u32 {
frame.pixels()[y * WIDTH + x]
}

View file

@ -0,0 +1,212 @@
use super::*;
fn bitmap(mode: u16, page: u16) -> Memory {
let mut m = memory();
m.write16(DISPCNT, 0x400 | mode | (page * 16)).unwrap();
m.write16(BG2PA, 256).unwrap();
m.write16(BG2PD, 256).unwrap();
let (w, h) = if mode == 5 { (160, 128) } else { (240, 160) };
for y in 0..h {
for x in 0..w {
let offset = (y * w + x) as u32;
let index = (x * 3 + y * 7) as u16 & 255;
if mode == 4 {
if x % 2 == 0 {
let next = ((x + 1) * 3 + y * 7) as u16 & 255;
m.write16(VRAM + page as u32 * 0xa000 + offset, index | (next << 8))
.unwrap();
}
} else {
let color =
(x as u16 & 31) | ((y as u16 & 31) << 5) | (((x + y) as u16 & 31) << 10);
m.write16(VRAM + page as u32 * 0xa000 + offset * 2, color)
.unwrap();
}
}
}
m
}
#[test]
fn write_only_register_merges_bytes_and_ignores_upper_word_half() {
let mut m = bitmap(3, 0);
m.write16(BG2CNT, 0x40).unwrap();
m.write32(MOSAIC, 0xffff_0034).unwrap(); // BG 5x4, OBJ1x1.
assert_eq!(m.read32(MOSAIC).unwrap(), 0);
m.write8(MOSAIC + 1, 0xff).unwrap(); // Must retain BG dimensions.
let f = render(&m);
assert_eq!(at(&f, 4, 3), at(&f, 0, 0));
assert_ne!(at(&f, 5, 4), at(&f, 0, 0));
m.write8(MOSAIC, 0).unwrap();
assert_ne!(at(&render(&m), 1, 0), at(&f, 0, 0));
assert_eq!(m.read16(MOSAIC + 2).unwrap(), 0);
assert_eq!(
m.read8(MOSAIC + 0x10000),
Err(MemoryError::Unmapped(MOSAIC + 0x10000))
);
assert_eq!(
m.write16(MOSAIC + 1, 0),
Err(MemoryError::Unaligned(MOSAIC + 1))
);
assert_eq!(m.cycles(), 0);
}
#[test]
fn every_bg_size_and_bitmap_page_repeats_screen_grid_samples() {
for mode in [3, 4, 5] {
for page in 0..=u16::from(mode != 3) {
let mut m = bitmap(mode, page);
let base = render(&m);
m.write16(BG2CNT, 0x40).unwrap();
// Exercise every field value, including asymmetric blocks and partial LCD blocks.
for w in 1..=16 {
let h = 17 - w;
m.write16(MOSAIC, ((h - 1) * 16 + w - 1) as u16).unwrap();
let f = render(&m);
for y in 0..HEIGHT {
for x in 0..WIDTH {
assert_eq!(
at(&f, x, y),
at(&base, x / w * w, y / h * h),
"mode={mode} page={page} size={w}x{h} at={x},{y}"
);
}
}
}
}
}
}
#[test]
fn mosaic_flag_is_independent_of_dimensions_and_one_by_one_is_identity() {
let mut m = bitmap(3, 0);
let base = render(&m);
m.write16(MOSAIC, 0xffff).unwrap();
assert_eq!(render(&m).pixels(), base.pixels()); // Flag is off.
m.write16(BG2CNT, 0x40).unwrap();
m.write16(MOSAIC, 0xff00).unwrap(); // OBJ fields do not affect BG.
assert_eq!(render(&m).pixels(), base.pixels());
m.write16(MOSAIC, 0x000f).unwrap();
let f = render(&m);
assert_eq!(at(&f, 15, 1), at(&base, 0, 1));
assert_ne!(at(&f, 15, 1), at(&base, 0, 0));
}
#[test]
fn text_mosaic_samples_before_scroll_and_tile_flips_in_both_depths() {
for eight_bit in [false, true] {
let mut m = memory();
m.write16(DISPCNT, 0x100).unwrap();
let control = 0x1000 | if eight_bit { 0x80 } else { 0 };
m.write16(BG0CNT, control).unwrap();
m.write16(BG0HOFS, 7).unwrap();
m.write16(BG0VOFS, 5).unwrap();
fill(&mut m, VRAM + 0x8000, 0x800, 0x0c00); // Both tile flips.
for y in 0..8 {
for x in (0..8).step_by(if eight_bit { 2 } else { 4 }) {
let mut value = 0;
for i in 0..if eight_bit { 2 } else { 4 } {
let index = ((x + i + y * 3) % 15) as u16;
value |= index << (i * if eight_bit { 8 } else { 4 });
}
m.write16(
VRAM + (y * if eight_bit { 8 } else { 4 } + x / if eight_bit { 1 } else { 2 })
as u32,
value,
)
.unwrap();
}
}
let base = render(&m);
m.write16(BG0CNT, control | 0x40).unwrap();
m.write16(MOSAIC, 0x54).unwrap(); // 5x6 crosses tile boundaries.
let f = render(&m);
for y in 0..HEIGHT {
for x in 0..WIDTH {
assert_eq!(at(&f, x, y), at(&base, x / 5 * 5, y / 6 * 6));
}
}
}
}
#[test]
fn affine_mosaic_samples_screen_positions_before_signed_transform() {
for mode in [1, 2, 3, 4, 5] {
let mut m = if mode >= 3 { bitmap(mode, 0) } else { memory() };
m.write16(DISPCNT, 0x400 | mode).unwrap();
let control = if mode <= 2 { 0x3000 } else { 0 };
m.write16(BG2CNT, control).unwrap();
if mode <= 2 {
for offset in (0..64).step_by(2) {
m.write16(VRAM + offset, (offset as u16 * 0x101).wrapping_add(0x100))
.unwrap();
}
}
for (reg, value) in [
(BG2PA, 181),
(BG2PB, 91),
(BG2PC, (-181i16) as u16),
(BG2PD, 256),
] {
m.write16(reg, value).unwrap();
}
m.write32(BG2X, (-769i32) as u32).unwrap();
m.write32(BG2Y, 31 * 256 + 127).unwrap();
let base = render(&m);
m.write16(BG2CNT, control | 0x40).unwrap();
m.write16(MOSAIC, 0x76).unwrap();
let f = render(&m);
for y in 0..HEIGHT {
for x in 0..WIDTH {
assert_eq!(
at(&f, x, y),
at(&base, x / 7 * 7, y / 8 * 8),
"mode={mode} pixel={x},{y}"
);
}
}
}
}
#[test]
fn independent_background_flags_preserve_transparency_and_priority() {
let mut m = memory();
m.write16(DISPCNT, 0x300).unwrap();
m.write16(BG0CNT, 0x1040).unwrap();
m.write16(BG1CNT, 0x1105).unwrap();
m.write16(PAL + 2, 31).unwrap();
m.write16(PAL + 4, 0x3e0).unwrap();
fill(&mut m, VRAM, 32, 0x1010);
fill(&mut m, VRAM + 0x4000, 32, 0x2222);
m.write16(MOSAIC, 2).unwrap();
let f = render(&m);
for x in 0..18 {
assert_eq!(at(&f, x, 0), rgb(if x / 3 % 2 == 0 { 0x3e0 } else { 31 }));
}
m.write16(BG0CNT, 0x1000).unwrap();
m.write16(BG1CNT, 0x1145).unwrap();
let f = render(&m);
for x in 0..18 {
assert_eq!(at(&f, x, 0), rgb(if x % 2 == 0 { 0x3e0 } else { 31 }));
}
}
#[test]
fn windows_and_color_effects_use_output_coordinate_not_mosaic_sample() {
let mut m = bitmap(3, 0);
m.write16(BG2CNT, 0x40).unwrap();
m.write16(MOSAIC, 0x77).unwrap();
m.write16(DISPCNT, 0x2403).unwrap();
m.write16(WIN0H, 0x0306).unwrap();
m.write16(WIN0V, 0x0008).unwrap();
m.write16(WININ, 4).unwrap();
m.write16(WINOUT, 0x24).unwrap();
m.write16(BLDCNT, 0x84).unwrap();
m.write16(BLDY, 16).unwrap();
let f = render(&m);
for x in 0..8 {
assert_eq!(at(&f, x, 0), if (3..6).contains(&x) { 0 } else { 0xffffff });
}
m.write16(DISPCNT, 0xffff).unwrap();
assert!(render(&m).pixels().iter().all(|&p| p == 0xffffff));
}

View file

@ -0,0 +1,100 @@
use super::*;
use gba_core::input::{Button, Buttons};
use gba_demos::{
mosaic_demo::{MosaicDemo, MOSAIC_STATE},
tile_demo::TileDemo,
};
#[test]
fn cpu_demo_cycles_all_sizes_and_wraps_without_host_video_writes() {
let mut demo = MosaicDemo::new().unwrap();
let mut frame = Framebuffer::default();
assert_eq!(demo.machine().memory().read16(DISPCNT).unwrap(), 0);
assert_eq!(demo.machine().memory().read16(VRAM).unwrap(), 0);
for update in 1..=129 {
demo.frame(Buttons::default(), &mut frame).unwrap();
let m = demo.machine().memory();
assert_eq!(m.read32(MOSAIC_STATE).unwrap(), update);
assert_eq!(m.read16(DISPCNT).unwrap(), 0x1340);
assert_eq!(m.read16(BG0CNT).unwrap(), 0x1844);
assert_eq!(m.read16(BG1CNT).unwrap(), 0xd0c1);
assert_eq!(m.read16(OAM).unwrap(), 0x1048);
assert_eq!(m.read32(MOSAIC).unwrap(), 0);
assert_eq!(m.display_position().scanline, 160);
let size = (1 + (update / 8) % 16) as usize;
// Check terrain and foreground away from the sprite for every size.
for y in 0..48 {
for x in 0..WIDTH {
let sx = x / size * size;
let sy = y / size * size;
let cross = sx / 8 % 4 == 1 && sy / 8 % 4 == 1 && (sx % 8 == 3 || sy % 8 == 3);
let color = if cross {
0x3ff
} else {
[0x260, 0x3a0, 0x7d20, 0x7e80]
[((sx / 32 + sy / 32) % 2) * 2 + (sx / 2 + sy / 2) % 2]
};
assert_eq!(at(&frame, x, y), rgb(color), "update={update} at={x},{y}");
}
}
}
}
#[test]
fn both_bypasses_restore_original_scene_with_scroll_rotation_zoom_and_reset() {
let mut demo = MosaicDemo::new().unwrap();
let mut base = TileDemo::new().unwrap();
let mut f = Framebuffer::default();
let mut reference = Framebuffer::default();
for i in 0..32 {
let buttons = Buttons::default()
.with(Button::Left, i < 16)
.with(Button::Down, true)
.with(Button::Start, i == 31)
.with(Button::L, (8..24).contains(&i))
.with(Button::R, (16..28).contains(&i));
base.frame(buttons, &mut reference).unwrap();
demo.frame(buttons.with(Button::A, true).with(Button::B, true), &mut f)
.unwrap();
assert_eq!(f.pixels(), reference.pixels());
let m = demo.machine().memory();
assert_ne!(m.read16(OAM).unwrap() & 0x1000, 0);
assert_eq!(m.read16(OAM + 2).unwrap() & 0x1000, 0); // Z no longer flips.
assert_eq!(m.read16(OAM + 4).unwrap() & 0xc00, 0); // X no longer lowers priority.
assert_eq!(
m.read32(MOSAIC_STATE + 4).unwrap(),
base.machine().memory().read32(MOSAIC_STATE + 4).unwrap()
);
}
assert_eq!(demo.machine().memory().read32(MOSAIC_STATE + 4).unwrap(), 0);
assert_eq!(demo.machine().memory().read32(MOSAIC_STATE + 8).unwrap(), 0);
}
#[test]
fn dma_word_write_sets_bg_and_obj_fields_while_unused_halfword_ignores_writes() {
use gba_core::{
cpu::Cpu,
dma::DMA_BASE,
machine::{Machine, StepKind},
};
let mut m = memory();
m.write16(DISPCNT, 0x1140).unwrap();
m.write16(BG0CNT, 0x1040).unwrap();
fill(&mut m, VRAM, 32, 0x1212);
m.write16(PAL + 4, 31).unwrap();
m.write16(PAL + 2, 0x3e0).unwrap();
object(&mut m, 0, 0x1000, 0, 0);
fill(&mut m, VRAM + 0x10000, 32, 0x0101);
m.write16(PAL + 0x202, 0x7c00).unwrap();
m.write32(0x0200_0000, 0xffff_0302).unwrap(); // BG3x1, OBJ4x1.
m.write32(DMA_BASE + 36, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 40, MOSAIC).unwrap();
m.write32(DMA_BASE + 44, 0x8400_0001).unwrap();
let mut machine = Machine::new(Cpu::new(0x0300_0000), m);
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
let f = render(machine.memory());
assert_eq!(at(&f, 3, 0), rgb(0x7c00));
assert_eq!(at(&f, 8, 0), rgb(31)); // BG samples x6, not x8's independent phase.
assert_eq!(at(&f, 9, 0), rgb(0x3e0));
assert_eq!(machine.memory().read32(MOSAIC).unwrap(), 0);
}

View file

@ -0,0 +1,215 @@
use super::*;
fn scene(a: u16, b: u16) -> Memory {
let mut m = memory();
m.write16(DISPCNT, 0x1040).unwrap();
object(&mut m, 0, a | 7, b | 5 | 0x4000, 0); // 16x16 at (5,7).
for y in 0..16 {
for x in (0..16).step_by(2) {
let tile = y / 8 * 2 + x / 8;
let address = VRAM + 0x10000 + tile * 64 + y % 8 * 8 + x % 8;
let first = 1 + (x + y * 5) % 254;
let second = 1 + (x + 1 + y * 5) % 254;
m.write16(address, (first | second << 8) as u16).unwrap();
}
}
m
}
#[test]
fn regular_obj_uses_screen_grid_clamps_partial_first_blocks_and_preserves_flips() {
for flips in [0, 0x1000, 0x2000, 0x3000] {
let mut m = scene(0x2000, flips);
let base = render(&m);
m.write16(OAM, 0x3007).unwrap(); // 8bpp plus mosaic, y7.
for width in 1..=16 {
let height = 17 - width;
m.write16(MOSAIC, (((height - 1) << 12) | ((width - 1) << 8)) as u16)
.unwrap();
let f = render(&m);
for y in 0..32 {
for x in 0..32 {
let expected = if (5..21).contains(&x) && (7..23).contains(&y) {
at(
&base,
(x / width * width).max(5),
(y / height * height).max(7),
)
} else {
0
};
assert_eq!(
at(&f, x, y),
expected,
"flip={flips:x} size={width}x{height} at={x},{y}"
);
}
}
}
}
}
#[test]
fn affine_and_double_size_mosaic_repeat_transformed_samples() {
for doubled in [false, true] {
let mut m = scene(0x2100 | if doubled { 0x200 } else { 0 }, 0);
let matrix: [i16; 4] = if doubled {
[64, 32, -32, 64]
} else {
[128, 64, -64, 128]
};
for (i, value) in matrix.into_iter().enumerate() {
m.write16(OAM + 6 + i as u32 * 8, value as u16).unwrap();
}
let base = render(&m);
m.write16(OAM, 0x3107 | if doubled { 0x200 } else { 0 })
.unwrap();
m.write16(MOSAIC, 0x4500).unwrap(); // OBJ6x5; BG1x1.
let f = render(&m);
let size = if doubled { 32 } else { 16 };
for y in 0..48 {
for x in 0..48 {
let expected = if (5..5 + size).contains(&x) && (7..7 + size).contains(&y) {
at(&base, (x / 6 * 6).max(5), (y / 5 * 5).max(7))
} else {
0
};
assert_eq!(at(&f, x, y), expected, "double={doubled} at={x},{y}");
}
}
}
}
#[test]
fn obj_flag_and_high_byte_are_independent_and_vertical_sampling_wraps_at_screen_top() {
let mut m = scene(0x2000, 0);
let base = render(&m);
m.write16(MOSAIC, 0xffff).unwrap();
assert_eq!(render(&m).pixels(), base.pixels()); // Flag disabled.
m.write16(OAM, 0x3007).unwrap();
m.write8(MOSAIC + 1, 0).unwrap(); // BG15 bits do not affect OBJ.
assert_eq!(render(&m).pixels(), base.pixels());
object(&mut m, 0, 0x20fc, 0x41fc, 0); // 16x16, wraps from (-4,-4).
let base = render(&m);
m.write16(OAM, 0x30fc).unwrap();
m.write16(MOSAIC, 0x3400).unwrap();
let f = render(&m);
for y in 0..12 {
for x in 0..12 {
assert_eq!(at(&f, x, y), at(&base, x / 5 * 5, y / 4 * 4));
}
}
assert_eq!(at(&f, 12, 0), 0);
assert_eq!(at(&f, 0, 12), 0);
}
#[test]
fn transparent_samples_repeat_but_non_mosaic_samples_restart_the_latch() {
let mut m = memory();
m.write16(DISPCNT, 0x1040).unwrap();
m.write16(MOSAIC, 0x0300).unwrap(); // OBJ4x1.
m.write16(PAL + 0x202, 31).unwrap();
fill(&mut m, VRAM + 0x10000, 32, 0x1010); // First sample transparent, second red.
object(&mut m, 0, 0x1000, 0, 0);
assert!(render(&m).pixels().iter().all(|&p| p == 0));
m.write16(VRAM + 0x10000, 0x0101).unwrap();
let f = render(&m);
for x in 0..4 {
assert_eq!(at(&f, x, 0), rgb(31));
}
// A lower-priority non-mosaic sprite fills transparent texels before mosaic.
// Its non-mosaic metadata makes those pixels latch immediately.
fill(&mut m, VRAM + 0x10020, 32, 0x2222);
m.write16(PAL + 0x204, 0x3e0).unwrap();
object(&mut m, 1, 0, 0, 0x401);
let f = render(&m);
assert_eq!(at(&f, 0, 0), rgb(31));
assert_eq!(at(&f, 1, 0), rgb(0x3e0));
}
#[test]
fn horizontal_latch_restarts_for_non_mosaic_and_higher_priority_pixels() {
let mut m = memory();
m.write16(DISPCNT, 0x1040).unwrap();
m.write16(MOSAIC, 0x0f00).unwrap();
for (tile, color) in [(0, 31), (1, 0x3e0), (2, 0x7c00)] {
fill(
&mut m,
VRAM + 0x10000 + tile * 32,
32,
(tile as u16 + 1) * 0x1111,
);
m.write16(PAL + 0x202 + tile * 2, color).unwrap();
}
object(&mut m, 0, 0x1000, 1, 0x800);
object(&mut m, 1, 0x1000, 3, 0x401);
object(&mut m, 2, 0, 5, 2);
let f = render(&m);
assert_eq!(at(&f, 0, 0), 0);
assert_eq!(at(&f, 1, 0), rgb(31));
assert_eq!(at(&f, 3, 0), rgb(0x3e0));
assert_eq!(at(&f, 5, 0), rgb(0x7c00));
assert_eq!(at(&f, 13, 0), 0); // Leaving the canvas resets the latch.
}
#[test]
fn transparent_higher_priority_obj_changes_metadata_without_replacing_color() {
let mut m = memory();
m.write16(DISPCNT, 0x1140).unwrap();
m.write16(BG0CNT, 0x1001).unwrap(); // BG priority1.
fill(&mut m, VRAM, 32, 0x3333);
m.write16(PAL + 6, 0x7c00).unwrap();
fill(&mut m, VRAM + 0x10000, 32, 0x1111);
m.write16(PAL + 0x202, 31).unwrap();
object(&mut m, 0, 0x400, 0, 0x800); // Semi-transparent red, priority2.
object(&mut m, 1, 0x1000, 0, 0x401); // Transparent tile1, priority1, mosaic.
fill(&mut m, VRAM + 0x10040, 32, 0x2222);
object(&mut m, 2, 0, 0, 0x402); // Same priority as metadata; cannot replace red.
m.write16(MOSAIC, 0x0300).unwrap();
m.write16(BLDCNT, 0x100).unwrap();
m.write16(BLDALPHA, 0x0808).unwrap();
assert_eq!(at(&render(&m), 0, 0), rgb(0x3c0f)); // Alpha flag retained, priority now ties BG.
}
#[test]
fn object_window_ignores_mosaic_and_window_edges_do_not_quantize() {
let mut m = memory();
m.write16(DISPCNT, 0x9100).unwrap();
m.write16(BG0CNT, 0x1000).unwrap();
fill(&mut m, VRAM, 32, 0x1111);
m.write16(PAL + 2, 31).unwrap();
fill(&mut m, VRAM + 0x10000, 32, 0x1010);
object(&mut m, 0, 0x1800, 0, 0);
m.write16(MOSAIC, 0xffff).unwrap();
m.write16(WINOUT, 0x0100).unwrap();
let f = render(&m);
for x in 0..8 {
assert_eq!(at(&f, x, 0), if x % 2 == 0 { 0 } else { rgb(31) });
}
m.write16(VRAM + 0x10004, 0x0101).unwrap(); // Next source row remains distinct.
assert_eq!(at(&render(&m), 0, 1), rgb(31));
}
#[test]
fn mosaic_latch_precedes_window_masking_and_semi_transparent_blending() {
let mut m = memory();
m.write16(DISPCNT, 0x3100).unwrap();
m.write16(BG0CNT, 0x1000).unwrap();
fill(&mut m, VRAM, 32, 0x2222);
m.write16(PAL + 4, 0x7c00).unwrap();
m.write16(PAL + 0x202, 31).unwrap();
fill(&mut m, VRAM + 0x10000, 32, 0x0101);
object(&mut m, 0, 0x1400, 0, 0);
m.write16(MOSAIC, 0x0300).unwrap();
m.write16(WIN0H, 0x0001).unwrap();
m.write16(WIN0V, 0x0008).unwrap();
m.write16(WININ, 1).unwrap(); // First pixel masks OBJ but must not reset its latch.
m.write16(WINOUT, 0x31).unwrap();
m.write16(BLDCNT, 0x100).unwrap();
m.write16(BLDALPHA, 0x0808).unwrap();
let f = render(&m);
assert_eq!(at(&f, 0, 0), rgb(0x7c00));
for x in 1..4 {
assert_eq!(at(&f, x, 0), rgb(0x3c0f));
}
}

View file

@ -0,0 +1,351 @@
use gba_core::{
cpu::Cpu,
display::{
CYCLES_PER_FRAME as FRAME, CYCLES_PER_LINE as LINE, HBLANK_START as HB, VBLANK_START as VB,
},
dma::DMA_BASE,
io::*,
machine::Machine,
memory::{Memory, OAM_START as OAM, PALETTE_START as PAL, ROM_START, VRAM_START as VRAM},
video::{rgb555_to_rgb888 as rgb, Framebuffer, HEIGHT, WIDTH},
};
fn color(row: usize) -> u16 {
((row % 64 + 1) * 0x101) as u16
}
fn scene() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m.write16(DISPCNT, 0x1140).unwrap();
m.write16(BG0CNT, 0x10c0).unwrap(); // 8bpp, mosaic, map block16.
for index in 0..64 {
m.write16(PAL + (index + 1) * 2, color(index as usize))
.unwrap();
m.write16(PAL + 0x200 + (index + 1) * 2, color(index as usize))
.unwrap();
}
// Eight text tiles, each with a distinct color per row. The map repeats vertically.
for y in 0..64 {
for x in (0..8).step_by(2) {
m.write16(VRAM + y * 8 + x, (y as u16 + 1) * 0x101).unwrap();
}
}
for y in 0..32 {
for x in 0..32 {
m.write16(VRAM + 0x8000 + (y * 32 + x) * 2, (y % 8) as u16)
.unwrap();
}
}
for index in 0..128 {
m.write16(OAM + index * 8, 0x200).unwrap();
}
m.write16(OAM, 0x3000).unwrap(); // Mosaic 8bpp sprite at (32,0), 16x16.
m.write16(OAM + 2, 0x4020).unwrap();
for y in 0..16 {
for x in (0..16).step_by(2) {
let tile = y / 8 * 2 + x / 8;
m.write16(
VRAM + 0x10000 + tile * 64 + y % 8 * 8 + x % 8,
(y as u16 + 1) * 0x101,
)
.unwrap();
}
}
m
}
fn bitmap() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m.write16(DISPCNT, 0x403).unwrap();
m.write16(BG2CNT, 0x40).unwrap();
m.write16(BG2PA, 256).unwrap();
m.write16(BG2PD, 256).unwrap();
for y in 0..160 {
for x in 0..240 {
m.write16(VRAM + (y * 240 + x) * 2, color(y as usize))
.unwrap();
}
}
m
}
fn present(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
assert!(m.present_frame(&mut f).unwrap());
f
}
fn finish(m: &mut Memory) -> Framebuffer {
let p = m.display_position();
m.advance_cycles(VB - u32::from(p.scanline) * LINE - u32::from(p.line_cycle));
present(m)
}
fn at(f: &Framebuffer, x: usize, y: usize) -> u32 {
f.pixels()[y * WIDTH + x]
}
#[test]
fn all_static_heights_match_snapshots_with_independent_background_and_sprite_sizes() {
for height in 1..=16 {
let mut m = scene();
m.write16(MOSAIC, ((height - 1) << 4) | ((16 - height) << 12))
.unwrap();
let mut expected = Framebuffer::default();
m.render_frame(&mut expected).unwrap();
assert_eq!(
finish(&mut m).pixels(),
expected.pixels(),
"height={height}"
);
}
}
#[test]
fn growing_a_block_preserves_counter_instead_of_realigning_to_screen_y() {
let mut m = scene();
m.write16(MOSAIC, 0x30).unwrap(); // BG4, OBJ1.
m.advance_cycles(5 * LINE + HB);
m.write8(MOSAIC, 0x70).unwrap(); // BG8: phase1 remains phase1.
let f = finish(&mut m);
for y in 0..24 {
let source = if y < 4 {
0
} else if y < 12 {
4
} else if y < 20 {
12
} else {
20
};
assert_eq!(at(&f, 0, y), rgb(color(source)), "row={y}");
if y < 16 {
assert_eq!(at(&f, 32, y), rgb(color(y)));
}
}
let mut snapshot = Framebuffer::default();
m.render_frame(&mut snapshot).unwrap();
assert_eq!(at(&snapshot, 0, 8), rgb(color(8)));
assert_eq!(at(&f, 0, 8), rgb(color(4)));
assert_eq!(present(&m).pixels(), f.pixels());
}
#[test]
fn shrinking_below_current_phase_waits_for_four_bit_wrap() {
let mut m = scene();
m.write16(MOSAIC, 0x70).unwrap();
m.advance_cycles(6 * LINE);
m.write8(MOSAIC, 0x10).unwrap(); // Phase6 > new height2; no immediate reset.
m.advance_cycles(0);
let f = finish(&mut m);
for y in 0..24 {
let source = if y < 16 { 0 } else { y / 2 * 2 };
assert_eq!(at(&f, 0, y), rgb(color(source)), "row={y}");
}
}
#[test]
fn affine_mosaic_advances_by_current_height_when_counter_wraps_not_by_elapsed_rows() {
let mut m = bitmap();
m.write16(MOSAIC, 0x70).unwrap();
m.advance_cycles(6 * LINE + HB);
m.write8(MOSAIC, 0x10).unwrap();
let f = finish(&mut m);
for y in 0..24 {
let source = if y < 16 { 0 } else { 2 + (y - 16) / 2 * 2 };
assert_eq!(at(&f, 0, y), rgb(color(source)), "row={y}");
}
}
#[test]
fn affine_reference_writes_and_coefficients_do_not_reset_shared_mosaic_phase() {
let mut m = bitmap();
m.write16(MOSAIC, 0x70).unwrap();
m.advance_cycles(5 * LINE);
m.write32(BG2Y, 20 * 256).unwrap();
m.write16(BG2PD, 2 * 256).unwrap();
m.write8(MOSAIC, 0x10).unwrap();
let f = finish(&mut m);
for y in 0..20 {
let source = if y < 5 {
0
} else if y < 16 {
20
} else {
24 + (y - 16) / 2 * 4
};
assert_eq!(at(&f, 0, y), rgb(color(source)), "row={y}");
}
}
#[test]
fn mosaic_flags_and_display_enable_do_not_gate_counters() {
for disabled in [false, true] {
let mut m = scene();
m.write16(MOSAIC, 0x7070).unwrap();
m.write16(BG0CNT, 0x1080).unwrap();
m.write16(OAM, 0x2000).unwrap();
if disabled {
m.write16(DISPCNT, 0).unwrap();
}
m.advance_cycles(6 * LINE);
m.write16(MOSAIC, 0x1010).unwrap();
m.write16(BG0CNT, 0x10c0).unwrap();
m.write16(OAM, 0x3000).unwrap();
m.write16(DISPCNT, 0x1140).unwrap();
let f = finish(&mut m);
for y in 6..16 {
assert_eq!(at(&f, 0, y), rgb(color(0)));
// Row6 was prepared on row5, before the attribute/enable write.
let source = if y == 6 && !disabled { 6 } else { 0 };
assert_eq!(at(&f, 32, y), rgb(color(source)));
}
}
}
#[test]
fn forced_blank_does_not_pause_counters() {
let mut m = scene();
m.write16(MOSAIC, 0x7070).unwrap();
m.write16(DISPCNT, 0x11c0).unwrap();
m.advance_cycles(6 * LINE);
m.write16(MOSAIC, 0x1010).unwrap();
m.write16(DISPCNT, 0x1140).unwrap();
let f = finish(&mut m);
assert!(f.pixels()[..6 * WIDTH].iter().all(|&p| p == 0xffffff));
for y in 6..16 {
assert_eq!(at(&f, 0, y), rgb(color(0)));
}
}
#[test]
fn sprite_counter_is_independent_and_clamps_partial_first_blocks_before_transform() {
for affine in [false, true] {
let mut m = scene();
m.write16(OAM, if affine { 0x3103 } else { 0x3003 })
.unwrap(); // y3.
if affine {
m.write16(OAM + 6, 256).unwrap();
m.write16(OAM + 30, 256).unwrap();
}
m.write16(MOSAIC, 0x7030).unwrap(); // BG4, OBJ8.
m.advance_cycles(6 * LINE);
m.write8(MOSAIC + 1, 0x10).unwrap(); // OBJ2 only.
let f = finish(&mut m);
for y in 0..24 {
assert_eq!(at(&f, 0, y), rgb(color(y / 4 * 4)));
let source = if !(3..19).contains(&y) {
y / 4 * 4
} else if y < 16 {
0
} else if y < 18 {
13
} else {
15
};
assert_eq!(at(&f, 32, y), rgb(color(source)), "affine={affine} row={y}");
}
}
}
#[test]
fn object_window_ignores_live_mosaic_phase() {
let mut m = scene();
m.write16(DISPCNT, 0x9140).unwrap();
m.write16(WINOUT, 0x0100).unwrap(); // Show BG only inside OBJ window.
m.write16(BG0CNT, 0x1080).unwrap();
m.write16(OAM, 0x3800).unwrap(); // Mosaic-enabled window sprite.
for y in (1..16).step_by(2) {
for x in (0..16).step_by(2) {
let tile = y / 8 * 2 + x / 8;
m.write16(VRAM + 0x10000 + tile * 64 + y % 8 * 8 + x % 8, 0)
.unwrap();
}
}
m.write16(MOSAIC, 0x7000).unwrap();
m.advance_cycles(6 * LINE);
m.write16(MOSAIC, 0x1000).unwrap();
let f = finish(&mut m);
for y in 0..24 {
assert_eq!(
at(&f, 32, y),
if y < 16 && y % 2 == 0 {
rgb(color(y))
} else {
0
}
);
}
}
#[test]
fn vertical_hold_uses_live_palette_but_sprite_vram_changes_wait_for_preparation() {
let mut m = scene();
m.write16(MOSAIC, 0x7070).unwrap();
m.advance_cycles(3 * LINE + HB);
m.write16(PAL + 2, 31).unwrap();
m.write16(VRAM + 0x10000, 0x0202).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 3), rgb(color(0)));
assert_eq!(at(&f, 0, 4), rgb(31));
assert_eq!(at(&f, 32, 3), rgb(color(0)));
assert_eq!(at(&f, 32, 4), rgb(color(0))); // Prepared before row3 HBlank.
assert_eq!(at(&f, 32, 5), rgb(color(1)));
}
#[test]
fn hblank_dma_size_writes_preserve_phase_and_affect_next_line_transition() {
let mut m = scene();
m.write16(MOSAIC, 0x7070).unwrap();
for y in 0..160 {
m.write16(0x0200_0000 + y * 2, if y < 6 { 0x7070 } else { 0x3010 })
.unwrap();
}
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, MOSAIC).unwrap();
m.write32(DMA_BASE + 8, 0xa240_0001).unwrap();
m.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.run_until_vblank(1000).unwrap();
let f = present(machine.memory());
// Independent recurrence, driven by the table value written after each row.
let (mut bg, mut obj) = (0usize, 0usize);
for y in 0..HEIGHT {
assert_eq!(at(&f, 0, y), rgb(color(y - bg)), "row={y}");
if y < 16 {
assert_eq!(at(&f, 32, y), rgb(color(y - obj)));
}
let (bg_height, obj_height) = if y < 6 { (8, 8) } else { (2, 4) };
bg = if bg + 1 == bg_height {
0
} else {
(bg + 1) % 16
};
obj = if obj + 1 == obj_height {
0
} else {
(obj + 1) % 16
};
}
assert!(machine.halted());
}
#[test]
fn vblank_resets_counters_and_capture_toggles_do_not_reset_them() {
let mut m = scene();
m.set_scanline_rendering(false);
m.write16(MOSAIC, 0x7070).unwrap();
m.advance_cycles(6 * LINE);
m.write16(MOSAIC, 0x1010).unwrap();
m.advance_cycles(VB - 6 * LINE);
m.write16(MOSAIC, 0x5050).unwrap(); // New size during VBlank.
m.advance_cycles(FRAME - VB);
m.set_scanline_rendering(true);
let f = finish(&mut m);
for y in 0..16 {
assert_eq!(at(&f, 0, y), rgb(color(y / 6 * 6)));
assert_eq!(at(&f, 32, y), rgb(color(y / 6 * 6)));
}
}

View file

@ -0,0 +1,38 @@
use gba_core::{
cpu::Cpu,
display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, HBLANK_START, VBLANK_START},
dma::DMA_BASE,
input::{Button, Buttons},
io::*,
machine::{Machine, StepKind},
memory::{Memory, OAM_START as OAM, PALETTE_START as PAL, ROM_START, VRAM_START as VRAM},
video::{rgb555_to_rgb888 as rgb, Framebuffer, VideoError, HEIGHT, WIDTH},
};
use gba_demos::raster_demo::{RasterDemo, RASTER_STATE, RASTER_TABLE};
#[path = "scanlines/capture.rs"]
mod capture;
#[path = "scanlines/machine.rs"]
mod machine;
#[path = "scanlines/rendering.rs"]
mod rendering;
fn memory() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m
}
fn present(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
assert!(m.present_frame(&mut f).unwrap());
f
}
fn at(f: &Framebuffer, x: usize, y: usize) -> u32 {
f.pixels()[y * WIDTH + x]
}
fn word_rom(code: &[u32]) -> Vec<u8> {
code.iter().flat_map(|v| v.to_le_bytes()).collect()
}

View file

@ -0,0 +1,169 @@
use super::*;
#[test]
fn capture_is_optional_and_snapshot_api_remains_independent() {
let mut m = Memory::new(vec![]).unwrap();
m.write16(PAL, 31).unwrap();
m.advance_cycles(CYCLES_PER_FRAME * 2);
let mut f = Framebuffer::default();
f.clear(0x3e0);
assert!(!m.present_frame(&mut f).unwrap());
assert!(f.pixels().iter().all(|&p| p == rgb(0x3e0)));
assert_eq!(m.captured_vblank(), None);
m.render_frame(&mut f).unwrap();
assert!(f.pixels().iter().all(|&p| p == rgb(31)));
let cycles = m.cycles();
m.set_scanline_rendering(true);
assert_eq!(m.cycles(), cycles);
m.advance_cycles(VBLANK_START);
assert_eq!(m.captured_vblank(), Some(3));
assert_eq!(present(&m).pixels(), f.pixels());
}
#[test]
fn hblank_captures_old_row_and_later_palette_writes_do_not_recolor_it() {
let mut m = memory();
m.write16(PAL, 31).unwrap();
m.advance_cycles(HBLANK_START);
m.write16(PAL, 0x3e0).unwrap();
m.advance_cycles(VBLANK_START - HBLANK_START);
let f = present(&m);
for y in 0..HEIGHT {
assert!(f.pixels()[y * WIDTH..(y + 1) * WIDTH]
.iter()
.all(|&p| p == rgb(if y == 0 { 31 } else { 0x3e0 })));
}
m.write16(PAL, 0x7c00).unwrap();
assert_eq!(present(&m).pixels(), f.pixels());
let cycles = m.cycles();
let mut snapshot = Framebuffer::default();
m.render_frame(&mut snapshot).unwrap();
assert!(snapshot.pixels().iter().all(|&p| p == rgb(0x7c00)));
assert_eq!(m.cycles(), cycles);
}
#[test]
fn last_row_capture_does_not_publish_before_vblank_entry() {
let mut m = memory();
m.write16(PAL, 31).unwrap();
let mut f = Framebuffer::default();
f.clear(0x3e0);
m.advance_cycles(159 * CYCLES_PER_LINE + HBLANK_START);
assert!(!m.present_frame(&mut f).unwrap());
m.advance_cycles(CYCLES_PER_LINE - HBLANK_START - 1);
assert!(!m.present_frame(&mut f).unwrap());
assert!(f.pixels().iter().all(|&p| p == rgb(0x3e0)));
m.advance_cycles(1);
assert!(m.present_frame(&mut f).unwrap());
assert_eq!(m.captured_vblank(), Some(1));
assert!(f.pixels().iter().all(|&p| p == rgb(31)));
}
#[test]
fn enabling_mid_frame_discards_partial_frame_and_repeated_enable_preserves_progress() {
for start in [HBLANK_START, 40 * CYCLES_PER_LINE, VBLANK_START - 1] {
let mut m = Memory::new(vec![]).unwrap();
m.advance_cycles(start);
m.set_scanline_rendering(true);
m.write16(PAL, 31).unwrap();
m.advance_cycles(VBLANK_START - start);
assert_eq!(m.captured_vblank(), None);
m.advance_cycles(CYCLES_PER_FRAME - VBLANK_START + HBLANK_START);
m.set_scanline_rendering(true); // Must not reset row0.
m.advance_cycles(VBLANK_START - HBLANK_START);
assert_eq!(m.captured_vblank(), Some(2));
assert!(present(&m).pixels().iter().all(|&p| p == rgb(31)));
m.set_scanline_rendering(false);
assert_eq!(m.captured_vblank(), None);
assert!(!m.present_frame(&mut Framebuffer::default()).unwrap());
}
}
#[test]
fn enable_before_first_hblank_and_during_vblank_can_capture_next_complete_image() {
for start in [0, HBLANK_START - 1, VBLANK_START, CYCLES_PER_FRAME - 1] {
let mut m = Memory::new(vec![]).unwrap();
m.advance_cycles(start);
m.set_scanline_rendering(true);
let target = if start < HBLANK_START {
VBLANK_START
} else {
CYCLES_PER_FRAME + VBLANK_START
};
m.advance_cycles(target - start);
assert_eq!(
m.captured_vblank(),
Some(if start < HBLANK_START { 1 } else { 2 })
);
assert!(m.present_frame(&mut Framebuffer::default()).unwrap());
}
}
#[test]
fn large_batches_publish_latest_frame_and_zero_cycles_have_no_effect() {
let mut m = memory();
m.write16(PAL, 0x4210).unwrap();
m.advance_cycles(CYCLES_PER_FRAME * 3 + VBLANK_START);
assert_eq!(m.captured_vblank(), Some(4));
let before = m.display_position();
let f = present(&m);
m.advance_cycles(0);
m.write16(PAL, 0).unwrap();
assert_eq!(m.display_position(), before);
assert_eq!(present(&m).pixels(), f.pixels());
assert!(f.pixels().iter().all(|&p| p == rgb(0x4210)));
}
#[test]
fn first_render_error_is_reported_at_publication_without_partial_output_then_recovers() {
let mut m = memory();
m.write16(PAL, 31).unwrap();
m.advance_cycles(VBLANK_START);
let mut f = present(&m);
m.advance_cycles(CYCLES_PER_FRAME - VBLANK_START);
m.write16(DISPCNT, 6).unwrap();
m.advance_cycles(HBLANK_START);
m.write16(DISPCNT, 7).unwrap();
m.advance_cycles(CYCLES_PER_LINE);
m.write16(DISPCNT, 0).unwrap();
assert!(m.present_frame(&mut f).unwrap()); // Previous frame remains available while drawing.
m.advance_cycles(VBLANK_START - HBLANK_START - CYCLES_PER_LINE);
assert_eq!(m.captured_vblank(), Some(2));
assert_eq!(m.present_frame(&mut f), Err(VideoError::UnsupportedMode(6)));
assert!(f.pixels().iter().all(|&p| p == rgb(31)));
m.write16(PAL, 0x3e0).unwrap();
m.advance_cycles(CYCLES_PER_FRAME);
assert_eq!(m.captured_vblank(), Some(3));
assert!(m.present_frame(&mut f).unwrap());
assert!(f.pixels().iter().all(|&p| p == rgb(0x3e0)));
}
#[test]
fn split_clock_advance_matches_single_cycle_reference_and_disabled_capture_devices() {
let mut bulk = memory();
let mut single = memory();
let mut disabled = Memory::new(vec![]).unwrap();
for m in [&mut bulk, &mut single, &mut disabled] {
m.write16(DISPSTAT, 0x5038).unwrap();
m.write32(TIMER_BASE, 0x00c0_fff1).unwrap();
m.write16(IE, 15).unwrap();
m.write8(HALTCNT, 0).unwrap();
m.write16(PAL, 0x4210).unwrap();
}
let cycles = CYCLES_PER_FRAME + VBLANK_START + 73;
bulk.advance_cycles(cycles);
disabled.advance_cycles(cycles);
for _ in 0..cycles {
single.advance_cycles(1);
}
for m in [&single, &disabled] {
assert_eq!(bulk.display_position(), m.display_position());
assert_eq!(bulk.cycles(), m.cycles());
assert_eq!(bulk.halted(), m.halted());
for register in [DISPSTAT, IF, TIMER_BASE] {
assert_eq!(bulk.read16(register), m.read16(register));
}
}
assert_eq!(present(&bulk).pixels(), present(&single).pixels());
assert_eq!(bulk.captured_vblank(), single.captured_vblank());
}

View file

@ -0,0 +1,196 @@
use super::*;
#[test]
fn hblank_dma_updates_next_row_while_cpu_stays_halted() {
let mut m = memory();
m.write16(PAL, 31).unwrap();
for i in 0..160 {
m.write16(0x0200_0000 + i * 2, ((i + 1) as u16 & 31) << 5)
.unwrap();
}
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, PAL).unwrap();
m.write32(DMA_BASE + 8, 0xa240_0001).unwrap();
m.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
let cpu = machine.cpu().clone();
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(
machine.memory().display_position().line_cycle,
HBLANK_START as u16
);
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
machine.run_until_vblank(1000).unwrap();
assert_eq!(machine.cpu(), &cpu);
assert!(machine.halted());
let f = present(machine.memory());
for y in 0..HEIGHT {
let color = if y == 0 { 31 } else { (y as u16 & 31) << 5 };
assert!(f.pixels()[y * WIDTH..(y + 1) * WIDTH]
.iter()
.all(|&p| p == rgb(color)));
}
assert_eq!(machine.memory().read16(PAL).unwrap(), 0); // Row159 HBlank DMA ran after its capture.
}
#[test]
fn vblank_dma_cannot_change_the_frame_just_published() {
let mut m = memory();
m.write16(PAL, 31).unwrap();
m.write16(0x0200_0000, 0x3e0).unwrap();
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, PAL).unwrap();
m.write32(DMA_BASE + 8, 0x9000_0001).unwrap();
m.advance_cycles(VBLANK_START);
let mut machine = Machine::new(Cpu::new(ROM_START), m);
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert!(present(machine.memory())
.pixels()
.iter()
.all(|&p| p == rgb(31)));
machine.memory_mut().advance_cycles(CYCLES_PER_FRAME);
assert!(present(machine.memory())
.pixels()
.iter()
.all(|&p| p == rgb(0x3e0)));
}
#[test]
fn cpu_polling_vcount_can_split_the_frame_without_host_video_writes() {
let rom = word_rom(&[
0xe3a0_0301, // MOV r0,#0x04000000
0xe1d0_10b6, // LDRH r1,[r0,#6] (VCOUNT)
0xe351_0050, // CMP r1,#80
0x1aff_fffc, // BNE poll
0xe3a0_2405, // MOV r2,#0x05000000
0xe3a0_3c7c, // MOV r3,#0x7c00
0xe1c2_30b0, // STRH r3,[r2]
0xeaff_fffe,
]);
let mut m = Memory::new(rom).unwrap();
m.set_scanline_rendering(true);
m.write16(PAL, 31).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.run_until_vblank(100_000).unwrap();
let f = present(machine.memory());
for y in 0..HEIGHT {
let color = if y < 80 { 31 } else { 0x7c00 };
assert!(f.pixels()[y * WIDTH..(y + 1) * WIDTH]
.iter()
.all(|&p| p == rgb(color)));
}
}
#[test]
fn instruction_crossing_hblank_uses_committed_write_for_entire_row() {
let rom = word_rom(&[
0xe3a0_0405, // MOV r0,#0x05000000
0xe3a0_1e3e, // MOV r1,#0x3e0
0xe1c0_10b0, // STRH r1,[r0]
]);
let mut m = Memory::new(rom).unwrap();
m.set_scanline_rendering(true);
m.write16(PAL, 31).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.step().unwrap();
machine.step().unwrap();
let elapsed = machine.cycles() as u32;
machine
.memory_mut()
.advance_cycles(HBLANK_START - 1 - elapsed);
machine.step().unwrap();
assert!(machine.cycles() > u64::from(HBLANK_START));
let remaining = VBLANK_START - machine.cycles() as u32;
machine.memory_mut().advance_cycles(remaining);
// This pins the nominal whole-instruction policy, not pixel-accurate timing.
assert_eq!(at(&present(machine.memory()), 0, 0), rgb(0x3e0));
}
#[test]
fn failed_cpu_step_does_not_advance_or_discard_capture_progress() {
let mut m = Memory::new(word_rom(&[0xf000_0000])).unwrap();
m.set_scanline_rendering(true);
m.write16(PAL, 31).unwrap();
m.advance_cycles(HBLANK_START);
let mut machine = Machine::new(Cpu::new(ROM_START), m);
let before = machine.memory().display_position();
let cycles = machine.cycles();
assert!(machine.step().is_err());
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().display_position(), before);
machine.memory_mut().write16(PAL, 0x3e0).unwrap();
machine
.memory_mut()
.advance_cycles(VBLANK_START - HBLANK_START);
let f = present(machine.memory());
assert_eq!(at(&f, 0, 0), rgb(31));
assert_eq!(at(&f, 0, 1), rgb(0x3e0));
}
#[test]
fn cpu_raster_demo_copies_table_and_rearms_hblank_dma_across_frames() {
let mut demo = RasterDemo::new().unwrap();
let mut f = Framebuffer::default();
assert_eq!(demo.machine().memory().read16(RASTER_TABLE).unwrap(), 0);
for update in 1..=40 {
demo.frame(Buttons::default().with(Button::Right, true), &mut f)
.unwrap();
let phase = update % 32;
let m = demo.machine().memory();
assert_eq!(m.read32(RASTER_STATE).unwrap(), update);
assert_eq!(m.read32(RASTER_STATE + 4).unwrap(), phase);
assert_eq!(m.read16(DMA_BASE + 10).unwrap(), 0xa240);
assert_eq!(m.captured_vblank(), Some(m.display_position().vblanks));
assert_eq!(m.display_position().scanline, 160);
for y in 0..HEIGHT {
let n = (y as u16 + phase as u16) % 32;
let color = n + (31 - n) * 32 + (n / 2) * 1024;
assert!(
f.pixels()[y * WIDTH..(y + 1) * WIDTH]
.iter()
.all(|&p| p == rgb(color)),
"update={update} row={y}"
);
}
let mut snapshot = Framebuffer::default();
m.render_frame(&mut snapshot).unwrap();
assert_ne!(snapshot.pixels(), f.pixels()); // Same memory, but no row history in a snapshot.
}
assert_eq!(
demo.machine().memory().read16(RASTER_TABLE).unwrap(),
31 * 32
);
}
#[test]
fn raster_controls_wrap_cancel_opposites_and_reset() {
let mut demo = RasterDemo::new().unwrap();
let mut f = Framebuffer::default();
for (buttons, phase) in [
(Buttons::default().with(Button::Left, true), 31),
(
Buttons::default()
.with(Button::Left, true)
.with(Button::Right, true),
31,
),
(Buttons::default().with(Button::Right, true), 0),
(Buttons::default().with(Button::Right, true), 1),
(
Buttons::default()
.with(Button::Start, true)
.with(Button::Right, true),
0,
),
] {
demo.frame(buttons, &mut f).unwrap();
assert_eq!(
demo.machine().memory().read32(RASTER_STATE + 4).unwrap(),
phase
);
assert_eq!(
at(&f, 0, 0),
rgb(phase as u16 | ((31 - phase as u16) << 5) | ((phase as u16 / 2) << 10))
);
}
}

View file

@ -0,0 +1,155 @@
use super::*;
#[test]
fn static_capture_matches_snapshots_in_every_mode_with_objects_mosaic_windows_and_effects() {
for mode in 0..=5 {
let mut m = memory();
for i in 0..512 {
m.write16(PAL + i * 2, (i as u16).wrapping_mul(0x421))
.unwrap();
}
for i in 0..128 {
m.write16(OAM + i * 8, 0x200).unwrap();
}
for offset in (0..0x14000).step_by(2) {
let value = if mode < 3 {
if offset < 0x4000 {
0x1111
} else {
0
}
} else {
(offset as u16).wrapping_mul(17)
};
m.write16(VRAM + offset, value).unwrap();
}
m.write16(DISPCNT, 0x3440 | mode).unwrap();
m.write16(BG2CNT, 0x1041).unwrap();
m.write16(BG2PA, 181).unwrap();
m.write16(BG2PB, 91).unwrap();
m.write16(BG2PC, (-91i16) as u16).unwrap();
m.write16(BG2PD, 256).unwrap();
m.write32(BG2X, 513).unwrap();
m.write32(BG2Y, 7 * 256).unwrap();
m.write16(OAM, 0x1003).unwrap();
m.write16(OAM + 2, 5).unwrap();
m.write16(OAM + 4, if mode >= 3 { 512 } else { 0 }).unwrap();
let tiles = if mode >= 3 { 0x14000 } else { 0x10000 };
for offset in (0..32).step_by(2) {
m.write16(VRAM + tiles + offset, 0x1111).unwrap();
}
m.write16(MOSAIC, 0x3243).unwrap();
m.write16(WIN0H, 0x0a3c).unwrap();
m.write16(WIN0V, 0x0432).unwrap();
m.write16(WININ, 0x1f).unwrap();
m.write16(WINOUT, 0x3f).unwrap();
m.write16(BLDCNT, 0xbf).unwrap();
m.write16(BLDY, 5).unwrap();
m.write16(GREENSWAP, 1).unwrap();
let mut expected = Framebuffer::default();
m.render_frame(&mut expected).unwrap();
m.advance_cycles(VBLANK_START);
assert_eq!(present(&m).pixels(), expected.pixels(), "mode={mode}");
}
}
#[test]
fn page_palette_vram_and_oam_writes_only_change_rows_not_yet_captured() {
let mut m = memory();
m.write16(DISPCNT, 0x404).unwrap();
m.write16(BG2PA, 256).unwrap();
m.write16(BG2PD, 256).unwrap();
for i in 0..128 {
m.write16(OAM + i * 8, 0x200).unwrap();
}
for offset in (0..38400).step_by(2) {
m.write16(VRAM + offset, 0x0101).unwrap();
m.write16(VRAM + 0xa000 + offset, 0x0202).unwrap();
}
for (index, color) in [(1, 31), (2, 0x3e0), (3, 0x7fff)] {
m.write16(PAL + index * 2, color).unwrap();
}
m.advance_cycles(HBLANK_START); // Row0 page0 red.
m.write16(DISPCNT, 0x414).unwrap();
m.advance_cycles(CYCLES_PER_LINE); // Row1 page1 green.
m.write16(PAL + 4, 0x7c00).unwrap();
m.advance_cycles(CYCLES_PER_LINE); // Row2 same index, now blue.
m.write16(VRAM + 0xa000 + 3 * 240, 0x0303).unwrap();
m.advance_cycles(CYCLES_PER_LINE); // Row3 first two pixels now white.
m.write16(DISPCNT, 0x1454).unwrap();
m.write16(OAM, 0).unwrap();
m.write16(OAM + 4, 512).unwrap();
for offset in (0..32).step_by(2) {
m.write16(VRAM + 0x14000 + offset, 0x1111).unwrap();
}
m.write16(PAL + 0x202, 0).unwrap(); // Opaque black OBJ.
m.advance_cycles(VBLANK_START - (3 * CYCLES_PER_LINE + HBLANK_START));
let f = present(&m);
// Row4's sprites were prepared before the row3 HBlank writes; row5 sees them.
for (y, color) in [
(0, 31),
(1, 0x3e0),
(2, 0x7c00),
(3, 0x7fff),
(4, 0x7c00),
(5, 0),
] {
assert_eq!(at(&f, 0, y), rgb(color));
}
assert_eq!(at(&f, 8, 4), rgb(0x7c00));
assert_eq!(at(&f, 0, 8), rgb(0x7c00));
}
#[test]
fn scroll_mosaic_window_effect_and_forced_blank_changes_apply_per_row() {
let mut m = memory();
m.write16(DISPCNT, 0x100).unwrap();
m.write16(BG0CNT, 0x1000).unwrap();
m.write16(PAL, 0x4210).unwrap();
m.write16(PAL + 2, 31).unwrap();
m.write16(PAL + 4, 0x3e0).unwrap();
for offset in (0..32).step_by(2) {
m.write16(VRAM + offset, 0x2121).unwrap();
}
m.advance_cycles(HBLANK_START);
m.write16(BG0HOFS, 1).unwrap();
m.advance_cycles(CYCLES_PER_LINE);
m.write16(BG0CNT, 0x1040).unwrap();
m.write16(MOSAIC, 1).unwrap();
m.advance_cycles(CYCLES_PER_LINE);
m.write16(DISPCNT, 0x2100).unwrap();
m.write16(WIN0H, 1).unwrap();
// Set the next row's top edge; a missed row-zero edge cannot open the window.
m.write16(WIN0V, 0x03a0).unwrap();
m.write16(WININ, 0).unwrap();
m.write16(WINOUT, 0x21).unwrap();
m.write16(BLDCNT, 0xc1).unwrap();
m.write16(BLDY, 8).unwrap();
m.advance_cycles(CYCLES_PER_LINE);
m.write16(DISPCNT, 0x80).unwrap();
m.advance_cycles(VBLANK_START - (3 * CYCLES_PER_LINE + HBLANK_START));
let f = present(&m);
assert_eq!(&f.pixels()[..2], &[rgb(31), rgb(0x3e0)]);
assert_eq!(&f.pixels()[WIDTH..WIDTH + 2], &[rgb(0x3e0), rgb(31)]);
assert_eq!(&f.pixels()[2 * WIDTH..2 * WIDTH + 2], &[rgb(0x3e0); 2]);
assert_eq!(at(&f, 0, 3), rgb(0x4210));
assert_eq!(at(&f, 1, 3), rgb(0x200));
assert!(f.pixels()[4 * WIDTH..].iter().all(|&p| p == 0xffffff));
}
#[test]
fn green_swap_setting_is_captured_per_row_after_color_composition() {
let mut m = memory();
m.write16(DISPCNT, 0x403).unwrap();
m.write16(BG2PA, 256).unwrap();
m.write16(BG2PD, 256).unwrap();
for row in 0..2 {
m.write32(VRAM + row * 480, 0x03e0_001f).unwrap();
}
m.advance_cycles(HBLANK_START);
m.write16(GREENSWAP, 1).unwrap();
m.advance_cycles(VBLANK_START - HBLANK_START);
let f = present(&m);
assert_eq!(&f.pixels()[..2], &[0xff0000, 0xff00]);
assert_eq!(&f.pixels()[WIDTH..WIDTH + 2], &[0xffff00, 0]);
}

View file

@ -0,0 +1,306 @@
//! Regression tests for the nominal allowance, not hardware fetch cutoffs.
use gba_core::{
cpu::Cpu,
display::{CYCLES_PER_LINE as LINE, VBLANK_START as VB},
dma::DMA_BASE,
io::*,
machine::Machine,
memory::{Memory, OAM_START as OAM, PALETTE_START as PAL, ROM_START, VRAM_START as VRAM},
video::{rgb555_to_rgb888 as rgb, Framebuffer, VideoError, WIDTH},
};
fn scene(free: bool) -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m.write16(DISPCNT, if free { 0x1060 } else { 0x1040 })
.unwrap();
m.write16(PAL, 0x7c00).unwrap();
m.write16(PAL + 0x202, 31).unwrap();
m.write16(PAL + 0x204, 0x3e0).unwrap();
for i in 0..128 {
obj(&mut m, i, 0x200, 0, 0);
}
// Tile zero is transparent. Separate solid 64x64 textures use indices 1/2.
for offset in (0..2048).step_by(2) {
m.write16(VRAM + 0x11000 + offset, 0x1111).unwrap();
m.write16(VRAM + 0x12000 + offset, 0x2222).unwrap();
m.write16(VRAM + 0x14000 + offset, 0x2222).unwrap();
}
m
}
fn obj(m: &mut Memory, index: u32, a: u16, b: u16, c: u16) {
m.write16(OAM + index * 8, a).unwrap();
m.write16(OAM + index * 8 + 2, b).unwrap();
m.write16(OAM + index * 8 + 4, c).unwrap();
}
fn fillers(m: &mut Memory, count: u32) {
for i in 0..count {
obj(m, i, 0, 0xc000, 0);
}
}
fn snapshot(m: &Memory) -> Framebuffer {
let before = (m.cycles(), m.display_position());
let mut f = Framebuffer::default();
m.render_frame(&mut f).unwrap();
assert_eq!((m.cycles(), m.display_position()), before);
f
}
fn finish(m: &mut Memory) -> Framebuffer {
let p = m.display_position();
m.advance_cycles(VB - u32::from(p.scanline) * LINE - u32::from(p.line_cycle));
let mut f = Framebuffer::default();
assert!(m.present_frame(&mut f).unwrap());
f
}
fn at(f: &Framebuffer, x: usize, y: usize) -> u32 {
f.pixels()[y * WIDTH + x]
}
fn prefix(f: &Framebuffer, y: usize, left: usize, width: usize, visible: usize) {
for x in left..left + width {
assert_eq!(
at(f, x, y),
rgb(if x < left + visible { 0x3e0 } else { 0x7c00 }),
"pixel={x},{y}, prefix={visible}"
);
}
}
#[test]
fn regular_prefix_exhausts_allowance_and_omits_later_entries() {
for free in [false, true] {
let mut m = scene(free);
let count = if free { 14 } else { 18 };
fillers(&mut m, count);
obj(&mut m, count, 0, 0xc000 | 80, 256 | 0xc00);
obj(&mut m, count + 1, 0, 0xc000 | 160, 256);
let f = snapshot(&m);
prefix(&f, 0, 80, 64, 58);
prefix(&f, 0, 160, 64, 0);
assert_eq!(finish(&mut m).pixels(), f.pixels());
}
}
#[test]
fn small_sprites_have_no_fixed_thirty_two_object_limit() {
for (free, count, visible) in [(false, 127, 8), (true, 119, 2), (true, 120, 0)] {
let mut m = scene(free);
for i in 0..count {
obj(&mut m, i, 0, 0, 0);
}
obj(&mut m, count, 0, 80, 256);
prefix(&snapshot(&m), 0, 80, 8, visible);
}
}
#[test]
fn affine_setup_and_double_size_use_drawing_canvas_width() {
for free in [false, true] {
for doubled in [false, true] {
let mut m = scene(free);
let count = if free { 14 } else { 18 };
fillers(&mut m, count); // 58 cycles remain.
// Zero matrices sample the opaque center for the entire canvas.
obj(&mut m, count, 0x100, 80, 256); // 10+2*8 = 26.
obj(
&mut m,
count + 1,
if doubled { 0x300 } else { 0x100 },
if doubled { 112 } else { 0x4000 | 112 },
256,
); // 32 cycles remain: setup plus 11 canvas pixels.
let f = snapshot(&m);
prefix(&f, 0, 80, 8, 8);
prefix(&f, 0, 112, 16, 11);
}
}
}
#[test]
fn unfinished_affine_setup_does_not_refund_work_to_regular_objects() {
for free in [false, true] {
let mut m = scene(free);
let count = if free { 14 } else { 18 };
fillers(&mut m, count);
obj(&mut m, count, 0, 0x8000, 0); // 32 cycles.
obj(&mut m, count + 1, 0, 0x4000, 0); // 16 cycles.
obj(&mut m, count + 2, 0x100, 80, 256); // Only setup fits.
obj(&mut m, count + 3, 0, 96, 256);
let f = snapshot(&m);
prefix(&f, 0, 80, 8, 0);
prefix(&f, 0, 96, 8, 0);
}
}
#[test]
fn disabled_off_row_and_fully_offscreen_entries_cost_inspection_only() {
for free in [false, true] {
for (a, b) in [(0x200, 0), (80, 0xc000), (0, 0xc000 | 240)] {
let mut m = scene(free);
for i in 0..100 {
obj(&mut m, i, a, b, 0);
}
let count = if free { 11 } else { 15 };
for i in 100..100 + count {
obj(&mut m, i, 0, 0xc000, 0);
}
obj(&mut m, 100 + count, 0, 0xc000 | 80, 256);
prefix(&snapshot(&m), 0, 80, 64, 50);
}
}
}
#[test]
fn partially_clipped_objects_charge_full_canvas_in_this_nominal_model() {
let mut m = scene(false);
fillers(&mut m, 18);
obj(&mut m, 18, 0, 0xc000 | 508, 256); // Left four pixels are clipped.
prefix(&snapshot(&m), 0, 0, 60, 54);
let mut m = scene(false);
fillers(&mut m, 17);
obj(&mut m, 17, 0, 0xc000 | 238, 256); // Only two pixels are on screen.
obj(&mut m, 18, 0, 0xc000 | 80, 256);
let f = snapshot(&m);
prefix(&f, 0, 238, 2, 2);
prefix(&f, 0, 80, 64, 58);
}
#[test]
fn transparent_semitransparent_and_disabled_obj_window_pixels_still_cost_work() {
for a in [0, 0x400, 0x800] {
let mut m = scene(false);
for i in 0..18 {
obj(&mut m, i, a, 0xc000, if a == 0x800 { 128 } else { 0 });
}
obj(&mut m, 18, 0, 0xc000 | 80, 256);
prefix(&snapshot(&m), 0, 80, 64, 58);
}
}
#[test]
fn oam_order_not_display_priority_allocates_work_even_for_occluded_objects() {
let mut m = scene(false);
for i in 0..18 {
obj(&mut m, i, 0, 0xc000 | 80, 128 | 0xc00);
}
obj(&mut m, 18, 0, 0xc000 | 80, 256);
let f = snapshot(&m);
for x in 80..144 {
assert_eq!(at(&f, x, 0), rgb(if x < 138 { 0x3e0 } else { 31 }));
}
}
#[test]
fn large_affine_and_out_of_texture_samples_use_full_canvas_cost() {
for doubled in [false, true] {
let mut m = scene(false);
let count = if doubled { 4 } else { 8 };
for i in 0..count {
obj(&mut m, i, if doubled { 0x300 } else { 0x100 }, 0xc000, 0);
}
// Most samples are outside the texture. This does not refund work.
m.write16(OAM + 6, 0x7fff).unwrap();
m.write16(OAM + 30, 0x7fff).unwrap();
obj(&mut m, count, 0, 0xc000 | 16, 256);
obj(&mut m, count + 1, 0, 0xc000 | 80, 256);
obj(&mut m, count + 2, 0, 0xc000 | 144, 256);
let f = snapshot(&m);
prefix(&f, 0, 16, 64, 64);
prefix(&f, 0, 80, 64, if doubled { 64 } else { 42 });
prefix(&f, 0, 144, 64, if doubled { 18 } else { 0 });
}
}
#[test]
fn bitmap_restricted_tiles_do_not_refund_sprite_work() {
let mut m = scene(false);
m.write16(DISPCNT, 0x1043).unwrap();
fillers(&mut m, 18); // Tile zero is unavailable to OBJ in bitmap modes.
obj(&mut m, 18, 0, 0xc000 | 80, 512);
prefix(&snapshot(&m), 0, 80, 64, 58);
}
#[test]
fn each_row_gets_an_independent_allowance() {
let mut m = scene(false);
for i in 0..18 {
obj(&mut m, i, 1, 0xc000, 0);
}
obj(&mut m, 18, 0, 0xc000 | 80, 256);
let f = snapshot(&m);
prefix(&f, 0, 80, 64, 64);
prefix(&f, 1, 80, 64, 58);
assert_eq!(finish(&mut m).pixels(), f.pixels());
}
#[test]
fn hblank_free_is_sampled_at_preparation_not_composition() {
for elapsed in [39, 40, 41] {
let mut m = scene(false);
fillers(&mut m, 18);
obj(&mut m, 18, 0, 0xc000 | 80, 256);
m.advance_cycles(elapsed);
m.write16(DISPCNT, 0x1060).unwrap();
prefix(&snapshot(&m), 0, 80, 64, 0);
let f = finish(&mut m);
prefix(&f, 0, 80, 64, 58);
prefix(&f, 1, 80, 64, if elapsed < 40 { 0 } else { 58 });
prefix(&f, 2, 80, 64, 0);
}
}
#[test]
fn hblank_dma_updates_allowance_for_the_following_preparation() {
let mut m = scene(false);
fillers(&mut m, 18);
obj(&mut m, 18, 0, 0xc000 | 80, 256);
for row in 0..160 {
m.write16(
0x0200_0000 + row * 2,
if row % 2 == 0 { 0x1060 } else { 0x1040 },
)
.unwrap();
}
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, DISPCNT).unwrap();
m.write32(DMA_BASE + 8, 0xa240_0001).unwrap();
m.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.run_until_vblank(1000).unwrap();
let mut f = Framebuffer::default();
assert!(machine.memory().present_frame(&mut f).unwrap());
for y in 0..64 {
prefix(&f, y, 80, 64, if y < 2 || y % 2 == 1 { 58 } else { 0 });
}
assert!(machine.halted());
}
#[test]
fn exhausted_work_preserves_development_diagnostics_and_output_atomicity() {
let mut m = scene(false);
fillers(&mut m, 19);
obj(&mut m, 127, 0xc00, 0, 0);
let mut f = Framebuffer::default();
f.clear(0x3e0);
assert!(matches!(
m.render_frame(&mut f),
Err(VideoError::UnsupportedObject { index: 127, .. })
));
assert!(f.pixels().iter().all(|&p| p == rgb(0x3e0)));
m.advance_cycles(VB);
assert!(matches!(
m.present_frame(&mut f),
Err(VideoError::UnsupportedObject { index: 127, .. })
));
assert!(f.pixels().iter().all(|&p| p == rgb(0x3e0)));
for control in [0x40, 0x10c0] {
m.write16(DISPCNT, control).unwrap();
m.render_frame(&mut f).unwrap();
}
}

View file

@ -0,0 +1,288 @@
use gba_core::{
cpu::Cpu,
display::{CYCLES_PER_FRAME as FRAME, CYCLES_PER_LINE as LINE, VBLANK_START as VB},
dma::DMA_BASE,
io::*,
machine::Machine,
memory::{Memory, OAM_START as OAM, PALETTE_START as PAL, ROM_START, VRAM_START as VRAM},
video::{rgb555_to_rgb888 as rgb, Framebuffer, VideoError, WIDTH},
};
fn scene() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m.write16(DISPCNT, 0x1040).unwrap();
m.write16(PAL, 0x7c00).unwrap();
m.write16(PAL + 0x202, 31).unwrap();
m.write16(PAL + 0x204, 0x3e0).unwrap();
for i in 0..128 {
m.write16(OAM + i * 8, 0x200).unwrap();
}
m.write16(OAM, 0).unwrap();
for offset in (0..32).step_by(2) {
m.write16(VRAM + 0x10000 + offset, 0x1111).unwrap();
}
m
}
fn finish(m: &mut Memory) -> Framebuffer {
let p = m.display_position();
m.advance_cycles(VB - u32::from(p.scanline) * LINE - u32::from(p.line_cycle));
present(m)
}
fn present(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
assert!(m.present_frame(&mut f).unwrap());
f
}
fn at(f: &Framebuffer, x: usize, y: usize) -> u32 {
f.pixels()[y * WIDTH + x]
}
#[test]
fn oam_position_writes_obey_preceding_lines_cycle40_boundary() {
for elapsed in [39, 40, 41] {
let mut m = scene();
m.advance_cycles(elapsed);
m.write16(OAM + 2, 16).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 0), rgb(31)); // Reset row0 was already prepared.
let x = if elapsed < 40 { 16 } else { 0 };
assert_eq!(at(&f, x, 1), rgb(31));
assert_eq!(at(&f, 16 - x, 1), rgb(0x7c00));
assert_eq!(at(&f, 16, 2), rgb(31));
}
}
#[test]
fn buffered_indices_keep_vram_history_but_resolve_current_palette() {
let mut m = scene();
m.advance_cycles(40);
for offset in (0..32).step_by(2) {
m.write16(VRAM + 0x10000 + offset, 0x2222).unwrap();
}
m.write16(PAL + 0x202, 0x7fff).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 0), rgb(0x7fff));
assert_eq!(at(&f, 0, 1), rgb(0x7fff));
assert_eq!(at(&f, 0, 2), rgb(0x3e0));
let mut snapshot = Framebuffer::default();
m.render_frame(&mut snapshot).unwrap();
assert_eq!(at(&snapshot, 0, 1), rgb(0x3e0));
assert_eq!(present(&m).pixels(), f.pixels());
}
#[test]
fn affine_matrix_and_tile_samples_are_frozen_at_preparation() {
let mut m = scene();
m.write16(OAM, 0x2100).unwrap(); // Affine, 8bpp.
m.write16(OAM + 6, 256).unwrap();
m.write16(OAM + 30, 256).unwrap();
for y in 0..8 {
for x in (0..8).step_by(2) {
m.write16(
VRAM + 0x10000 + y * 8 + x,
if x < 4 { 0x0101 } else { 0x0202 },
)
.unwrap();
}
}
m.advance_cycles(40);
m.write16(OAM + 6, 0).unwrap(); // PA0 samples the texture center for every X.
let f = finish(&mut m);
assert_eq!(at(&f, 0, 1), rgb(31));
assert_eq!(at(&f, 0, 2), rgb(0x3e0));
}
#[test]
fn semitransparency_is_buffered_but_blend_coefficients_remain_live() {
let mut m = scene();
m.write16(OAM, 0x400).unwrap();
m.advance_cycles(40);
m.write16(OAM, 0).unwrap();
m.write16(BLDCNT, 0x2000).unwrap(); // Backdrop as second target.
m.write16(BLDALPHA, 0x0808).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 1), rgb(0x3c0f));
assert_eq!(at(&f, 0, 2), rgb(31));
}
#[test]
fn obj_window_coverage_is_buffered_even_before_its_display_mask_is_enabled() {
let mut m = scene();
m.write16(OAM, 0x800).unwrap();
m.advance_cycles(40);
m.write16(OAM + 2, 16).unwrap();
m.write16(DISPCNT, 0x9040).unwrap();
m.write16(WINOUT, 0x2000).unwrap();
m.write16(BLDCNT, 0xa0).unwrap();
m.write16(BLDY, 16).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 1), rgb(0x7fff));
assert_eq!(at(&f, 16, 1), rgb(0x7c00));
assert_eq!(at(&f, 0, 2), rgb(0x7c00));
assert_eq!(at(&f, 16, 2), rgb(0x7fff));
}
#[test]
fn horizontal_mosaic_uses_current_size_and_prepared_sprite_metadata() {
let mut m = scene();
m.write16(OAM, 0x1000).unwrap();
for offset in (0..32).step_by(2) {
m.write16(VRAM + 0x10000 + offset, 0x2121).unwrap();
}
m.advance_cycles(40);
m.write16(OAM, 0).unwrap();
m.write16(MOSAIC, 0x0300).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 1, 1), rgb(31));
assert_eq!(at(&f, 1, 2), rgb(0x3e0));
}
#[test]
fn global_enable_gates_preparation_and_current_composition() {
let mut m = scene();
m.write16(DISPCNT, 0x40).unwrap();
m.advance_cycles(40);
m.write16(DISPCNT, 0x1040).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 0), rgb(0x7c00));
assert_eq!(at(&f, 0, 1), rgb(0x7c00));
assert_eq!(at(&f, 0, 2), rgb(31));
let mut m = scene();
m.advance_cycles(40);
m.write16(DISPCNT, 0x40).unwrap();
assert_eq!(at(&finish(&mut m), 0, 1), rgb(0x7c00));
}
#[test]
fn forced_blank_does_not_stop_preparation_in_this_row_model() {
let mut m = scene();
m.write16(DISPCNT, 0x10c0).unwrap();
m.advance_cycles(40);
m.write16(OAM + 2, 16).unwrap();
m.write16(DISPCNT, 0x1040).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 1), rgb(31));
assert_eq!(at(&f, 16, 2), rgb(31));
}
#[test]
fn hblank_dma_oam_changes_wait_until_a_later_preparation_event() {
let mut m = scene();
for row in 0..160 {
m.write16(0x0200_0000 + row * 2, ((row + 1) % 20 * 8) as u16)
.unwrap();
}
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, OAM + 2).unwrap();
m.write32(DMA_BASE + 8, 0xa240_0001).unwrap();
m.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.run_until_vblank(1000).unwrap();
let f = present(machine.memory());
for y in 0usize..8 {
let left = y.saturating_sub(1) * 8;
for x in 0..WIDTH {
assert_eq!(
at(&f, x, y),
rgb(if (left..left + 8).contains(&x) {
31
} else {
0x7c00
}),
"pixel={x},{y}"
);
}
}
assert!(machine.halted());
}
#[test]
fn final_hidden_line_prepares_row_zero_before_frame_wrap() {
for cycle in [39, 40] {
let mut m = scene();
finish(&mut m);
m.advance_cycles(227 * LINE + cycle - VB);
m.write16(OAM + 2, 16).unwrap();
m.advance_cycles(FRAME - 227 * LINE - cycle);
let f = finish(&mut m);
assert_eq!(at(&f, if cycle < 40 { 16 } else { 0 }, 0), rgb(31));
assert_eq!(at(&f, 16, 1), rgb(31));
}
}
#[test]
fn capture_toggles_preserve_prepared_rows_after_large_capture_disabled_batches() {
for frames in [0, 3] {
let mut m = scene();
m.set_scanline_rendering(false);
m.advance_cycles(frames * FRAME + 41);
m.write16(OAM + 2, 16).unwrap();
m.set_scanline_rendering(true);
m.set_scanline_rendering(false);
m.set_scanline_rendering(true);
let f = finish(&mut m);
assert_eq!(at(&f, 0, 0), rgb(31));
assert_eq!(at(&f, 0, 1), rgb(31));
assert_eq!(at(&f, 16, 2), rgb(31));
}
}
#[test]
fn prepared_errors_are_deferred_until_visible_composition_and_recover_next_frame() {
let mut m = scene();
m.advance_cycles(1); // Bootstrap row0 remains valid.
m.write16(OAM, 0xc00).unwrap();
m.advance_cycles(39); // Prepare invalid row1; clock advancement itself succeeds.
m.write16(OAM, 0).unwrap();
m.advance_cycles(VB - 40);
let mut f = Framebuffer::default();
f.clear(0x3e0);
assert!(matches!(
m.present_frame(&mut f),
Err(VideoError::UnsupportedObject { index: 0, .. })
));
assert!(f.pixels().iter().all(|&p| p == rgb(0x3e0)));
m.advance_cycles(FRAME);
assert!(m.present_frame(&mut f).unwrap());
assert_eq!(at(&f, 0, 1), rgb(31));
}
#[test]
fn hidden_preparation_errors_do_not_bypass_global_disable_or_forced_blank() {
for control in [0x40, 0x10c0] {
let mut m = scene();
m.write16(OAM, 0xc00).unwrap();
m.advance_cycles(40);
m.write16(DISPCNT, control).unwrap();
let f = finish(&mut m);
assert_eq!(
at(&f, 0, 1),
if control & 0x80 != 0 {
0xffffff
} else {
rgb(0x7c00)
}
);
}
}
#[test]
fn zero_cycle_advances_and_failed_cpu_steps_do_not_prepare_rows() {
let mut m = scene();
m.advance_cycles(0);
m.write16(OAM + 2, 16).unwrap();
m.advance_cycles(39);
m.write32(0x0300_0000, 0xf000_0000).unwrap();
let mut machine = Machine::new(Cpu::new(0x0300_0000), m);
assert!(machine.step().is_err());
assert_eq!(machine.cycles(), 39);
machine.memory_mut().write16(OAM + 2, 24).unwrap();
let f = finish(machine.memory_mut());
assert_eq!(at(&f, 16, 0), rgb(31));
assert_eq!(at(&f, 24, 1), rgb(31));
}

View file

@ -0,0 +1,370 @@
use gba_core::{
io::{BG0CNT, BG2CNT, DISPCNT, GREENSWAP},
memory::{Memory, MemoryError, OAM_START as OAM, PALETTE_START as PAL, VRAM_START as VRAM},
video::{rgb555_to_rgb888, Framebuffer, VideoError, WIDTH},
};
#[path = "sprites/affine.rs"]
mod affine;
fn setup() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.write16(DISPCNT, 0x1040).unwrap(); // OBJ, 1D, Mode 0
for i in 0..128 {
m.write16(OAM + i * 8, 0x200).unwrap();
}
for (i, color) in [0x4210, 0x001f, 0x03e0, 0x7c00, 0x7fff]
.into_iter()
.enumerate()
{
m.write16(PAL + 0x200 + i as u32 * 2, color).unwrap();
}
m.write16(PAL, 0x4210).unwrap();
m
}
fn obj(m: &mut Memory, index: u32, a: u16, b: u16, c: u16) {
m.write16(OAM + index * 8, a).unwrap();
m.write16(OAM + index * 8 + 2, b).unwrap();
m.write16(OAM + index * 8 + 4, c).unwrap();
}
fn fill(m: &mut Memory, slot: u32, bytes: u32, value: u16) {
for offset in (0..bytes).step_by(2) {
m.write16(VRAM + 0x10000 + ((slot * 32 + offset) & 0x7fff), value)
.unwrap();
}
}
fn render(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
m.render_frame(&mut f).unwrap();
f
}
fn pixel(m: &Memory, x: usize, y: usize) -> u32 {
render(m).pixels()[y * WIDTH + x]
}
#[test]
fn oam_mirrors_reads_all_bytes_and_ignores_only_byte_writes() {
let mut m = setup();
m.write32(OAM, 0xabcd_1234).unwrap();
assert_eq!(m.read32(OAM + 0x400).unwrap(), 0xabcd_1234);
assert_eq!(m.read8(OAM + 1).unwrap(), 0x12);
m.write8(OAM, 0xff).unwrap();
m.write8(OAM + 3, 0xff).unwrap();
assert_eq!(m.read32(OAM).unwrap(), 0xabcd_1234);
m.write32(0x07ff_fffc, 0x7654_3210).unwrap();
assert_eq!(m.read32(OAM + 0x3fc).unwrap(), 0x7654_3210);
m.write16(OAM + 6, 0xface).unwrap(); // Affine parameter slots are ordinary memory.
assert_eq!(m.read16(OAM + 0x406).unwrap(), 0xface);
assert_eq!(m.write16(OAM + 1, 0), Err(MemoryError::Unaligned(OAM + 1)));
assert_eq!(m.write32(OAM + 2, 0), Err(MemoryError::Unaligned(OAM + 2)));
assert_eq!(m.read32(OAM).unwrap(), 0xabcd_1234);
}
#[test]
fn four_bit_sprite_nibbles_banks_zero_transparency_and_opaque_black() {
let mut m = setup();
obj(&mut m, 0, 0, 0, 0xf000);
fill(&mut m, 0, 2, 0x0321);
m.write16(PAL + 0x200 + 241 * 2, 0x001f).unwrap();
m.write16(PAL + 0x200 + 242 * 2, 0x03e0).unwrap();
m.write16(PAL + 0x200 + 243 * 2, 0).unwrap();
m.write16(PAL + 0x200 + 240 * 2, 0x7fff).unwrap();
assert_eq!(
&render(&m).pixels()[..4],
&[0xff0000, 0x00ff00, 0, rgb555_to_rgb888(0x4210)]
);
}
#[test]
fn eight_bit_sprite_ignores_bank_and_uses_obj_palette_not_bg_palette() {
let mut m = setup();
obj(&mut m, 0, 0x2000, 0, 0xf000);
fill(&mut m, 0, 2, 0x00ff);
m.write16(PAL + 0x3fe, 0x03e0).unwrap();
m.write16(PAL + 0x1fe, 0x001f).unwrap();
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
assert_eq!(pixel(&m, 1, 0), rgb555_to_rgb888(0x4210));
}
#[test]
fn every_shape_and_size_covers_exact_rectangle() {
let sizes = [
[(8, 8), (16, 16), (32, 32), (64, 64)],
[(16, 8), (32, 8), (32, 16), (64, 32)],
[(8, 16), (8, 32), (16, 32), (32, 64)],
];
for (shape, dimensions) in sizes.iter().enumerate() {
for (size, &(w, h)) in dimensions.iter().enumerate() {
let mut m = setup();
fill(&mut m, 0, 0x8000, 0x1111);
obj(
&mut m,
0,
((shape as u16) << 14) | 5,
((size as u16) << 14) | 7,
0,
);
let f = render(&m);
for y in 0..160 {
for x in 0..240 {
let expected = if (7..7 + w).contains(&x) && (5..5 + h).contains(&y) {
0xff0000
} else {
rgb555_to_rgb888(0x4210)
};
assert_eq!(
f.pixels()[y * WIDTH + x],
expected,
"shape {shape}, size {size}, pixel {x},{y}"
);
}
}
}
}
}
#[test]
fn tile_row_strides_differ_between_1d_and_2d_in_both_depths() {
for eight in [false, true] {
for one in [false, true] {
let mut m = setup();
m.write16(DISPCNT, if one { 0x1040 } else { 0x1000 })
.unwrap();
obj(&mut m, 0, if eight { 0x2000 } else { 0 }, 0x4000, 0);
let slots = if eight { 2 } else { 1 };
for (slot, index) in [
(0, 1),
(slots, 2),
(if one { 2 * slots } else { 32 }, 3),
(if one { 3 * slots } else { 32 + slots }, 4),
] {
fill(
&mut m,
slot,
slots * 32,
if eight { index * 0x101 } else { index * 0x1111 },
);
}
let f = render(&m);
for (x, y, color) in [
(0, 0, 0xff0000),
(8, 0, 0x00ff00),
(0, 8, 0x0000ff),
(8, 8, 0xffffff),
] {
assert_eq!(f.pixels()[y * WIDTH + x], color, "8bit={eight} 1D={one}");
}
}
}
}
#[test]
fn eight_bit_odd_base_is_preserved_in_1d_but_aligned_in_2d() {
let mut m = setup();
fill(&mut m, 0, 32, 0x0101);
fill(&mut m, 1, 32, 0x0202);
obj(&mut m, 0, 0x2000, 0, 1);
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
m.write16(DISPCNT, 0x1000).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000);
}
#[test]
fn mapping_wraps_horizontal_2d_rows_and_the_32k_obj_area() {
let mut m = setup();
obj(&mut m, 0, 0x4000, 0, 1023); // 16x8
fill(&mut m, 1023, 32, 0x1111);
fill(&mut m, 0, 32, 0x2222);
fill(&mut m, 992, 32, 0x3333);
assert_eq!(pixel(&m, 8, 0), 0x00ff00); // 1D wraps entire area.
m.write16(DISPCNT, 0x1000).unwrap();
assert_eq!(pixel(&m, 8, 0), 0x0000ff); // 2D wraps within last row.
obj(&mut m, 0, 0, 0x4000, 992); // 16x16
assert_eq!(pixel(&m, 0, 8), 0x00ff00); // Next row wraps to first row.
}
#[test]
fn flips_reverse_whole_multi_tile_sprite_not_each_tile() {
for eight in [false, true] {
let mut m = setup();
let slots = if eight { 2 } else { 1 };
for i in 0..4 {
fill(
&mut m,
i * slots,
32 * slots,
if eight {
(i as u16 + 1) * 0x101
} else {
(i as u16 + 1) * 0x1111
},
);
}
for (flips, color) in [
(0, 0xff0000),
(0x1000, 0x00ff00),
(0x2000, 0x0000ff),
(0x3000, 0xffffff),
] {
obj(&mut m, 0, if eight { 0x2000 } else { 0 }, 0x4000 | flips, 0);
assert_eq!(pixel(&m, 0, 0), color);
}
}
}
#[test]
fn screen_coordinates_wrap_and_clip_at_each_edge() {
let mut m = setup();
fill(&mut m, 0, 32, 0x1111);
for (ox, oy) in [(508, 252), (238, 158), (250, 170)] {
obj(&mut m, 0, oy, ox, 0);
let f = render(&m);
for y in 0..160 {
for x in 0..240 {
let inside =
((x + 512 - ox as usize) & 511) < 8 && ((y + 256 - oy as usize) & 255) < 8;
assert_eq!(
f.pixels()[y * WIDTH + x],
if inside {
0xff0000
} else {
rgb555_to_rgb888(0x4210)
}
);
}
}
}
}
#[test]
fn object_priority_wins_then_oam_index_and_transparent_pixels_expose_later_objs() {
let mut m = setup();
fill(&mut m, 0, 32, 0x1111);
fill(&mut m, 1, 32, 0x2222);
obj(&mut m, 0, 0, 0, 0xc00); // First OBJ has lowest BG priority.
obj(&mut m, 127, 0, 0, 1); // Later OBJ has highest BG priority.
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
obj(&mut m, 127, 0, 0, 0xc01); // Equal priority: first OAM entry wins.
assert_eq!(pixel(&m, 0, 0), 0xff0000);
// BG0 priority2 covers both priority3 objects.
m.write16(DISPCNT, 0x1140).unwrap();
m.write16(BG0CNT, 0x1002).unwrap();
m.write16(VRAM, 0x3333).unwrap();
m.write16(PAL + 6, 0x7c00).unwrap();
assert_eq!(pixel(&m, 0, 0), 0x0000ff);
obj(&mut m, 0, 0, 0, 0);
obj(&mut m, 127, 0, 0, 1);
fill(&mut m, 0, 2, 0x0010); // Zero only at first pixel.
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
assert_eq!(pixel(&m, 1, 0), 0xff0000);
}
#[test]
fn sprite_beats_bg_on_equal_priority_but_not_higher_priority_bg() {
let mut m = setup();
obj(&mut m, 0, 0, 0, 0x400);
fill(&mut m, 0, 32, 0x1111);
m.write16(DISPCNT, 0x1140).unwrap();
m.write16(BG0CNT, 0x1001).unwrap();
m.write16(VRAM, 0x2222).unwrap();
m.write16(PAL + 4, 0x03e0).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000);
m.write16(BG0CNT, 0x1000).unwrap();
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
m.write16(VRAM, 0).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000); // Transparent BG reveals sprite.
}
#[test]
fn mode3_uses_bg2_priority_and_only_upper_obj_tile_numbers() {
let mut m = setup();
m.write16(DISPCNT, 0x1443).unwrap();
m.write16(VRAM, 0x03e0).unwrap();
fill(&mut m, 0, 32, 0x1111);
fill(&mut m, 512, 32, 0x1111);
obj(&mut m, 0, 0, 0, 0);
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
obj(&mut m, 0, 0, 0, 512);
assert_eq!(pixel(&m, 0, 0), 0xff0000);
obj(&mut m, 0, 0, 0, 512 | 0x400);
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
m.write16(BG2CNT, 1).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000);
m.write16(DISPCNT, 0x1043).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000); // BG2 disabled.
}
#[test]
fn disabled_objects_ignore_unsupported_bits_and_global_disable_skips_all_objects() {
let mut m = setup();
obj(&mut m, 0, 0xfe00, 0xffff, 0xffff); // Disabled, despite mode/mosaic/shape bits.
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(0x4210));
obj(&mut m, 0, 0xc00, 0, 0); // Prohibited OBJ mode.
m.write16(DISPCNT, 0).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(0x4210));
}
#[test]
fn unsupported_objects_leave_output_unchanged_and_forced_blank_bypasses_errors() {
let mut m = setup();
fill(&mut m, 0, 32, 0x2222);
obj(&mut m, 0, 0, 0, 0);
let mut frame = Framebuffer::default();
frame.clear(0x001f);
for attr in [0xc00, 0xc000, 0xd00, 0x1c00, 0xc300] {
obj(&mut m, 127, attr, 0, 0);
assert!(matches!(
m.render_frame(&mut frame),
Err(VideoError::UnsupportedObject { index: 127, .. })
));
assert!(frame.pixels().iter().all(|&p| p == 0xff0000));
}
m.write16(DISPCNT, 0x1080).unwrap();
m.render_frame(&mut frame).unwrap();
assert!(frame.pixels().iter().all(|&p| p == 0xffffff));
}
#[test]
fn green_swap_applies_after_sprite_composition() {
let mut m = setup();
obj(&mut m, 0, 0, 0, 0);
fill(&mut m, 0, 2, 0x21);
m.write16(GREENSWAP, 1).unwrap();
assert_eq!(&render(&m).pixels()[..2], &[0xffff00, 0]);
}
#[test]
fn oam_bus_uses_one_cycle_for_every_width() {
use gba_core::timing::{bus_cycles, AccessKind, AccessWidth};
for kind in [AccessKind::Sequential, AccessKind::NonSequential] {
for width in [AccessWidth::Byte, AccessWidth::Halfword, AccessWidth::Word] {
assert_eq!(bus_cycles(0, OAM, width, kind), 1);
}
}
}
#[test]
fn dma_copies_words_to_oam_and_reads_them_back() {
use gba_core::{
cpu::Cpu,
dma::DMA_BASE,
machine::{Machine, StepKind},
};
let mut machine = Machine::new(Cpu::new(0x0300_0000), setup());
machine
.memory_mut()
.write32(0x0200_0000, 0x1234_abcd)
.unwrap();
for (source, dest) in [(0x0200_0000, OAM + 0x400), (OAM, 0x0200_0004)] {
let m = machine.memory_mut();
m.write32(DMA_BASE + 36, source).unwrap();
m.write32(DMA_BASE + 40, dest).unwrap();
m.write32(DMA_BASE + 44, 0x8400_0001).unwrap();
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
}
assert_eq!(machine.memory().read32(0x0200_0004).unwrap(), 0x1234_abcd);
}

View file

@ -0,0 +1,285 @@
use super::*;
fn matrix(m: &mut Memory, group: u32, values: [i16; 4]) {
for (i, value) in values.into_iter().enumerate() {
m.write16(OAM + group * 32 + 6 + i as u32 * 8, value as u16)
.unwrap();
}
}
fn texel(x: usize, y: usize) -> u16 {
if (x + y) % 7 == 0 {
0
} else {
((x + 3 * y) % 15 + 1) as u16
}
}
fn pattern(m: &mut Memory, w: usize, h: usize, eight: bool, one: bool) {
let mut bytes = [0_u8; 0x8000];
for y in 0..h {
for x in 0..w {
let slot = (y / 8)
* if one {
w / 8 * if eight { 2 } else { 1 }
} else {
32
}
+ (x / 8) * if eight { 2 } else { 1 };
let p = (y % 8) * 8 + x % 8;
let address = slot * 32 + if eight { p } else { p / 2 };
bytes[address] |= (texel(x, y) as u8) << if eight { 0 } else { (p % 2) * 4 };
}
}
for (i, pair) in bytes.chunks_exact(2).enumerate() {
m.write16(
VRAM + 0x10000 + i as u32 * 2,
u16::from_le_bytes([pair[0], pair[1]]),
)
.unwrap();
}
for i in 1..16 {
m.write16(PAL + 0x200 + i * 2, i as u16 * 0x421).unwrap();
}
}
// Independent floating-point reference. Every coefficient is an exact multiple
// of 1/256, so these small products are exact in f64. Floor defines negative rounding.
fn check_reference(
m: &Memory,
dimensions: (usize, usize),
origin: (usize, usize),
doubled: bool,
values: [i16; 4],
) {
let (w, h) = dimensions;
let (cw, ch) = if doubled { (w * 2, h * 2) } else { (w, h) };
let f = render(m);
for y in 0..160 {
for x in 0..240 {
let lx = (x + 512 - origin.0) % 512;
let ly = (y + 256 - origin.1) % 256;
let mut color = rgb555_to_rgb888(0x4210);
if lx < cw && ly < ch {
let dx = lx as f64 - cw as f64 / 2.0;
let dy = ly as f64 - ch as f64 / 2.0;
let tx = (f64::from(values[0]) / 256.0 * dx
+ f64::from(values[1]) / 256.0 * dy
+ w as f64 / 2.0)
.floor();
let ty = (f64::from(values[2]) / 256.0 * dx
+ f64::from(values[3]) / 256.0 * dy
+ h as f64 / 2.0)
.floor();
if tx >= 0.0 && ty >= 0.0 && tx < w as f64 && ty < h as f64 {
let index = texel(tx as usize, ty as usize);
if index != 0 {
color = rgb555_to_rgb888(index * 0x421);
}
}
}
assert_eq!(
f.pixels()[y * WIDTH + x],
color,
"pixel {x},{y}, size {w}x{h}, double={doubled}, matrix={values:?}"
);
}
}
}
#[test]
fn identity_matches_regular_sprites_for_all_shapes_sizes_depths_and_layouts() {
let sizes = [
[(8, 8), (16, 16), (32, 32), (64, 64)],
[(16, 8), (32, 8), (32, 16), (64, 32)],
[(8, 16), (8, 32), (16, 32), (32, 64)],
];
for (shape, row) in sizes.iter().enumerate() {
for (size, &(w, h)) in row.iter().enumerate() {
for eight in [false, true] {
for one in [false, true] {
let mut m = setup();
m.write16(DISPCNT, if one { 0x1040 } else { 0x1000 })
.unwrap();
pattern(&mut m, w, h, eight, one);
let a = (shape as u16) << 14 | if eight { 0x2000 } else { 0 } | 9;
let b = (size as u16) << 14 | 11;
obj(&mut m, 0, a, b, 0);
let regular = render(&m);
matrix(&mut m, 31, [256, 0, 0, 256]);
obj(&mut m, 0, a | 0x100, b | (31 << 9), 0); // High group bits are not flip flags.
assert_eq!(render(&m).pixels(), regular.pixels());
}
}
}
}
}
#[test]
fn rotations_shears_reflections_and_fractional_scales_match_reference() {
let transforms = [
[0, 256, -256, 0],
[0, -256, 256, 0],
[-256, 0, 0, -256],
[181, 181, -181, 181],
[256, 128, 0, 256],
[-256, 0, 0, 256],
[128, 0, 0, 128],
[512, 0, 0, 512],
[512, 0, 0, 128],
[255, 1, -1, 257],
];
for eight in [false, true] {
for one in [false, true] {
for doubled in [false, true] {
let mut m = setup();
m.write16(DISPCNT, if one { 0x1040 } else { 0x1000 })
.unwrap();
pattern(&mut m, 16, 8, eight, one);
obj(
&mut m,
0,
0x4100 | if doubled { 0x200 } else { 0 } | if eight { 0x2000 } else { 0 } | 17,
23,
0,
);
for values in transforms {
matrix(&mut m, 0, values);
check_reference(&m, (16, 8), (23, 17), doubled, values);
}
}
}
}
}
#[test]
fn double_size_changes_canvas_and_center_without_changing_texture_size() {
let mut m = setup();
pattern(&mut m, 8, 8, false, true);
for doubled in [false, true] {
obj(
&mut m,
0,
0x100 | if doubled { 0x200 } else { 0 } | 20,
30,
0,
);
for values in [[256, 0, 0, 256], [128, 0, 0, 128]] {
matrix(&mut m, 0, values);
check_reference(&m, (8, 8), (30, 20), doubled, values);
}
}
// Identity double-size: texture starts four pixels inside the enlarged box.
matrix(&mut m, 0, [256, 0, 0, 256]);
assert_eq!(pixel(&m, 30, 20), rgb555_to_rgb888(0x4210));
assert_eq!(pixel(&m, 35, 24), rgb555_to_rgb888(texel(1, 0) * 0x421));
}
#[test]
fn zero_singular_and_extreme_signed_matrices_do_not_overflow_or_invert() {
let mut m = setup();
pattern(&mut m, 64, 64, true, true);
obj(&mut m, 0, 0x2300, 0xc000, 0);
for values in [
[0; 4],
[256, 0, 0, 0],
[32767, -32768, -32768, 32767],
[-1, 1, 1, -1],
[1, 0, 0, 1],
] {
matrix(&mut m, 0, values);
check_reference(&m, (64, 64), (0, 0), true, values);
}
}
#[test]
fn all_32_matrix_groups_are_addressed_independently_and_can_be_shared() {
let mut m = setup();
pattern(&mut m, 8, 8, false, true);
for group in 0..32 {
// Distinct coefficients detect both incorrect matrix stride and component stride.
let values = [
256 + group as i16,
group as i16 * 3,
-(group as i16) * 2,
128,
];
matrix(&mut m, group, values);
}
for group in 0..32 {
let values = [
256 + group as i16,
group as i16 * 3,
-(group as i16) * 2,
128,
];
obj(&mut m, 0, 0x100, (group as u16) << 9, 0);
check_reference(&m, (8, 8), (0, 0), false, values);
}
matrix(&mut m, 31, [256, 0, 0, 256]);
obj(&mut m, 0, 0x100, 31 << 9, 0);
obj(&mut m, 127, 0x100, (31 << 9) | 16, 0);
let f = render(&m);
for y in 0..8 {
for x in 0..8 {
assert_eq!(f.pixels()[y * WIDTH + x], f.pixels()[y * WIDTH + x + 16]);
}
}
// Changes to one shared matrix are visible to both sprites on the next snapshot.
matrix(&mut m, 31, [0; 4]);
let f = render(&m);
for y in 0..8 {
for x in 0..8 {
assert_eq!(f.pixels()[y * WIDTH + x], f.pixels()[y * WIDTH + x + 16]);
}
}
}
#[test]
fn affine_canvas_wraps_coordinates_and_clips_without_restarting_transform() {
let mut m = setup();
pattern(&mut m, 32, 64, false, true);
let values = [181, 181, -181, 181];
matrix(&mut m, 0, values);
for (x, y) in [(500, 240), (220, 145), (255, 170)] {
obj(&mut m, 0, 0x8300 | y as u16, 0xc000 | x as u16, 0);
check_reference(&m, (32, 64), (x, y), true, values);
}
}
#[test]
fn affine_transparent_edges_reveal_later_objects_and_obey_bg_priority() {
let mut m = setup();
fill(&mut m, 0, 32, 0x1111);
fill(&mut m, 1, 32, 0x2222);
matrix(&mut m, 0, [512, 0, 0, 512]);
obj(&mut m, 0, 0x100, 0, 0x400);
obj(&mut m, 1, 0, 0, 0x401); // Equal priority retains the first opaque OBJ.
assert_eq!(pixel(&m, 0, 0), 0x00ff00); // Outside transformed texture, later OBJ visible.
assert_eq!(pixel(&m, 4, 4), 0xff0000);
m.write16(DISPCNT, 0x1140).unwrap();
m.write16(BG0CNT, 0x1000).unwrap();
for address in (0..32).step_by(2) {
m.write16(VRAM + address, 0x3333).unwrap();
}
m.write16(PAL + 6, 0x7c00).unwrap();
assert_eq!(pixel(&m, 4, 4), 0x0000ff); // BG priority0 covers selected OBJ priority1.
assert_eq!(pixel(&m, 0, 0), 0x0000ff);
m.write16(BG0CNT, 0x1001).unwrap();
assert_eq!(pixel(&m, 0, 0), 0x00ff00); // OBJ ties BG and wins at transparent edge.
}
#[test]
fn mode3_affine_sprites_keep_bitmap_tile_restrictions() {
let mut m = setup();
m.write16(DISPCNT, 0x1043).unwrap();
matrix(&mut m, 0, [128, 0, 0, 128]);
fill(&mut m, 0, 32, 0x1111);
fill(&mut m, 512, 32, 0x2222);
obj(&mut m, 0, 0x300, 0, 0);
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(0x4210));
obj(&mut m, 0, 0x300, 0, 512);
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
assert_eq!(pixel(&m, 15, 15), 0x00ff00);
assert_eq!(pixel(&m, 16, 16), rgb555_to_rgb888(0x4210));
}

View file

@ -0,0 +1,479 @@
use gba_core::{
io::*,
memory::{Memory, MemoryError, PALETTE_START as PAL, VRAM_START as VRAM},
video::{rgb555_to_rgb888, Framebuffer, VideoError, WIDTH},
};
fn setup() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.write16(DISPCNT, 0x100).unwrap();
m.write16(BG0CNT, 0x1000).unwrap(); // Map at 0x8000, tiles at zero.
for (i, color) in [0x4210, 0x001f, 0x03e0, 0x7c00, 0x7fff]
.into_iter()
.enumerate()
{
m.write16(PAL + i as u32 * 2, color).unwrap();
}
m
}
fn pixel(m: &Memory, x: usize, y: usize) -> u32 {
let mut frame = Framebuffer::default();
m.render_frame(&mut frame).unwrap();
frame.pixels()[y * WIDTH + x]
}
fn tile(m: &mut Memory, address: u32, value: u16, bytes: u32) {
for i in (0..bytes).step_by(2) {
m.write16(VRAM + address + i, value).unwrap();
}
}
#[test]
fn controls_mask_unused_bits_and_merge_byte_and_word_writes() {
let mut m = setup();
m.write32(BG0CNT, u32::MAX).unwrap();
m.write32(BG2CNT, u32::MAX).unwrap();
assert_eq!(m.read32(BG0CNT).unwrap(), 0xdfcf_dfcf);
assert_eq!(m.read32(BG2CNT).unwrap(), 0xffcf_ffcf);
m.write8(BG0CNT, 0x81).unwrap();
m.write8(BG0CNT + 1, 0x10).unwrap();
assert_eq!(m.read16(BG0CNT).unwrap(), 0x1081);
for reg in (BG0HOFS..=BG3VOFS).step_by(2) {
m.write16(reg, 0xffff).unwrap();
assert_eq!(m.read16(reg).unwrap(), 0); // Write-only placeholder, not open bus.
}
assert_eq!(
m.read16(0x0400_0058),
Err(MemoryError::Unmapped(0x0400_0058))
);
}
#[test]
fn four_bit_nibble_order_palette_banks_and_transparent_zero() {
let mut m = setup();
m.write16(VRAM, 0x0321).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000);
assert_eq!(pixel(&m, 1, 0), 0x00ff00);
assert_eq!(pixel(&m, 2, 0), 0x0000ff);
assert_eq!(pixel(&m, 3, 0), rgb555_to_rgb888(0x4210));
m.write16(VRAM + 0x8000, 0xf000).unwrap();
m.write16(PAL + 241 * 2, 0x7fff).unwrap();
m.write16(PAL + 240 * 2, 0x001f).unwrap(); // Bank zero is still transparent.
assert_eq!(pixel(&m, 0, 0), 0xffffff);
assert_eq!(pixel(&m, 3, 0), rgb555_to_rgb888(0x4210));
}
#[test]
fn eight_bit_tiles_use_full_index_and_ignore_palette_bank() {
let mut m = setup();
m.write16(BG0CNT, 0x1080).unwrap();
m.write16(VRAM + 0x8000, 0xf001).unwrap();
m.write16(VRAM + 64, 0x00ff).unwrap();
m.write16(PAL + 510, 0x001f).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000);
assert_eq!(pixel(&m, 1, 0), rgb555_to_rgb888(0x4210));
}
#[test]
fn horizontal_and_vertical_flips_apply_in_both_color_depths() {
for eight in [false, true] {
let mut m = setup();
m.write16(BG0CNT, 0x1000 | if eight { 0x80 } else { 0 })
.unwrap();
let (top, bottom) = if eight {
(0x0201, 0x0403)
} else {
(0x21, 0x43)
};
m.write16(VRAM, top).unwrap();
m.write16(VRAM + if eight { 56 } else { 28 }, bottom)
.unwrap();
for (flip, x, y, expected) in [
(0, 0, 0, 0xff0000),
(0x400, 7, 0, 0xff0000),
(0x800, 0, 0, 0x0000ff),
(0xc00, 6, 0, 0xffffff),
] {
m.write16(VRAM + 0x8000, flip).unwrap();
assert_eq!(pixel(&m, x, y), expected);
}
}
}
#[test]
fn all_map_sizes_use_screen_blocks_not_flat_rows_and_wrap() {
for size in 0..4_u16 {
let mut m = setup();
for i in 1..=4 {
tile(&mut m, i * 32, 0x1111 * i as u16, 32);
}
for block in 0..4 {
tile(&mut m, 0x8000 + block * 0x800, block as u16 + 1, 0x800);
}
m.write16(BG0CNT, 0x1000 | (size << 14)).unwrap();
let width = if size & 1 != 0 { 512 } else { 256 };
let height = if size & 2 != 0 { 512 } else { 256 };
for y in [0, 255, 256, 511] {
for x in [0, 255, 256, 511] {
m.write16(BG0HOFS, x).unwrap();
m.write16(BG0VOFS, y).unwrap();
let block = (x % width) / 256 + (y % height) / 256 * (width / 256);
let colors = [0x001f, 0x03e0, 0x7c00, 0x7fff];
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(colors[block as usize]));
}
}
}
}
#[test]
fn scroll_byte_writes_merge_latches_mask_nine_bits_and_move_subtile_pixels() {
let mut m = setup();
m.write16(BG0CNT, 0x1080).unwrap();
m.write16(VRAM + 2, 0x0201).unwrap();
m.write16(BG0HOFS, 0xffff).unwrap();
m.write8(BG0HOFS, 2).unwrap(); // latch becomes 0x102; 256-wide map wraps to 2.
assert_eq!(pixel(&m, 0, 0), 0xff0000);
m.write8(BG0HOFS + 1, 0xfe).unwrap(); // Only bit zero survives.
assert_eq!(pixel(&m, 1, 0), 0x00ff00);
m.write16(VRAM + 8 + 2, 0x0403).unwrap();
m.write32(BG0HOFS, 0xfe01_fe02).unwrap(); // X=2, Y=1
assert_eq!(pixel(&m, 0, 0), 0x0000ff);
assert_eq!(m.cycles(), 0);
}
#[test]
fn each_background_uses_its_own_scroll_and_control() {
for bg in 0..4 {
let mut m = setup();
m.write16(DISPCNT, 0x100 << bg).unwrap();
m.write16(BG0CNT + bg * 2, 0x1080).unwrap();
m.write16(VRAM + 10, 0x0201).unwrap();
m.write16(BG0HOFS + bg * 4, 2).unwrap();
m.write16(BG0VOFS + bg * 4, 1).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xff0000);
}
}
#[test]
fn priorities_ties_transparency_and_opaque_black_composite_all_four_layers() {
let mut m = setup();
m.write16(DISPCNT, 0xf00).unwrap();
for bg in 0..4 {
m.write16(BG0CNT + bg * 2, ((16 + bg) << 8) as u16).unwrap();
tile(&mut m, 0x8000 + bg * 0x800, bg as u16 + 1, 0x800);
tile(&mut m, (bg + 1) * 32, 0x1111 * (bg as u16 + 1), 32);
}
for bg in 0..4 {
let expected = [0xff0000, 0x00ff00, 0x0000ff, 0xffffff][bg as usize];
assert_eq!(pixel(&m, 0, 0), expected);
// Lower the winning layer's priority so the next wins the tie.
m.write16(BG0CNT + bg * 2, ((16 + bg) << 8) as u16 | 3)
.unwrap();
}
assert_eq!(pixel(&m, 0, 0), 0xff0000); // All priority 3, BG0 wins.
m.write16(PAL + 2, 0).unwrap();
assert_eq!(pixel(&m, 0, 0), 0); // Nonzero index with black palette color is opaque.
tile(&mut m, 32, 0, 32);
assert_eq!(pixel(&m, 0, 0), 0x00ff00);
m.write16(DISPCNT, 0).unwrap();
assert_eq!(pixel(&m, 0, 0), rgb555_to_rgb888(0x4210));
}
#[test]
fn character_base_and_high_tile_numbers_cross_character_blocks() {
for eight in [false, true] {
let mut m = setup();
m.write16(BG0CNT, 0x1004 | if eight { 0x80 } else { 0 })
.unwrap();
m.write16(VRAM + 0x8000, 1023).unwrap();
let address = 0x4000 + 1023 * if eight { 64 } else { 32 };
if eight {
assert!(matches!(
m.render_frame(&mut Framebuffer::default()),
Err(VideoError::UnsupportedTileAddress(_))
));
} else {
tile(&mut m, address, 0x1111, 32);
assert_eq!(pixel(&m, 0, 0), 0xff0000);
}
}
}
#[test]
fn screen_map_outside_supported_bg_area_reports_error_without_wrapping() {
let mut m = setup();
m.write16(BG0CNT, 0x5f04).unwrap(); // Map block31, wide; tile base1.
m.write16(BG0HOFS, 256).unwrap(); // Second block lies outside the BG area.
let mut frame = Framebuffer::default();
frame.clear(0x001f);
assert_eq!(
m.render_frame(&mut frame),
Err(VideoError::UnsupportedMapAddress(0x10000))
);
assert!(frame.pixels().iter().all(|&p| p == 0xff0000));
}
#[test]
fn unsupported_features_leave_output_unchanged_and_forced_blank_bypasses_them() {
let mut m = setup();
let mut frame = Framebuffer::default();
frame.clear(0x001f);
m.write16(DISPCNT, 0x100).unwrap();
m.write16(BG0CNT, 0x100c).unwrap();
m.write16(VRAM + 0x8002, 1023).unwrap(); // Error after some valid pixels.
assert!(matches!(
m.render_frame(&mut frame),
Err(VideoError::UnsupportedTileAddress(_))
));
assert!(frame.pixels().iter().all(|&p| p == 0xff0000));
m.write16(DISPCNT, 0xf180).unwrap();
m.render_frame(&mut frame).unwrap();
assert!(frame.pixels().iter().all(|&p| p == 0xffffff));
}
fn assert_scene(
frame: &Framebuffer,
sx: usize,
sy: usize,
flipped: bool,
behind: bool,
rotated: bool,
zoomed: bool,
) {
for y in 0..160 {
for x in 0..240 {
let fx = (x + sx / 2) % 256;
let fy = (y + sy / 2) % 256;
let bx = (x + sx) % 512;
let by = (y + sy) % 512;
let cross = fx / 8 % 4 == 1 && fy / 8 % 4 == 1 && (fx % 8 == 3 || fy % 8 == 3);
let mut color = if cross {
0x03ff
} else {
[0x0260, 0x03a0, 0x7d20, 0x7e80]
[((bx / 32 + by / 32) % 2) * 2 + (bx / 2 + by / 2) % 2]
};
let affine = rotated || zoomed;
let source = if affine && (104..136).contains(&x) && (64..96).contains(&y) {
let a = f64::from(if rotated {
if zoomed {
90
} else {
181
}
} else {
128
});
let b = if rotated { a } else { 0.0 };
let dx = x as f64 - 120.0;
let dy = y as f64 - 80.0;
Some((
((a * dx + b * dy) / 256.0 + 8.0).floor() as i32,
((-b * dx + a * dy) / 256.0 + 8.0).floor() as i32,
))
} else if !affine && (112..128).contains(&x) && (72..88).contains(&y) {
Some((
if flipped {
127 - x as i32
} else {
x as i32 - 112
},
y as i32 - 72,
))
} else {
None
};
if let Some((tx, ty)) = source.filter(|&(tx, ty)| {
(2..14).contains(&tx) && (1..15).contains(&ty) && !(cross && behind)
}) {
color = if tx == 10 && ty == 7 {
0
} else if tx == 12 && (10..13).contains(&ty) {
0x03ff
} else if (5..10).contains(&ty) {
0x7fff
} else {
0x001f
};
}
assert_eq!(
frame.pixels()[y * WIDTH + x],
rgb555_to_rgb888(color),
"pixel {x},{y} scroll {sx},{sy}"
);
}
}
}
#[test]
fn cpu_tile_demo_copies_assets_scrolls_wraps_cancels_opposites_and_resets() {
use gba_core::input::{Button, Buttons};
use gba_demos::tile_demo::{TileDemo, TILE_STATE};
let mut demo = TileDemo::new().unwrap();
let mut frame = Framebuffer::default();
assert_eq!(demo.machine().memory().read16(VRAM).unwrap(), 0);
assert_eq!(demo.machine().memory().read16(DISPCNT).unwrap(), 0);
let mut updates = 0;
for (buttons, x, y) in [
(Buttons::default(), 0, 0),
(
Buttons::default()
.with(Button::Left, true)
.with(Button::Up, true),
510,
510,
),
(Buttons::from_bits(0xf0), 510, 510),
(
Buttons::default()
.with(Button::Right, true)
.with(Button::Down, true),
0,
0,
),
(
Buttons::default()
.with(Button::Right, true)
.with(Button::Down, true),
2,
2,
),
(
Buttons::default()
.with(Button::Start, true)
.with(Button::Right, true),
0,
0,
),
] {
let before = demo.machine().memory().display_position();
demo.frame(buttons, &mut frame).unwrap();
updates += 1;
assert_scene(&frame, x, y, false, false, false, false);
let m = demo.machine().memory();
assert_eq!(m.read32(TILE_STATE).unwrap(), updates);
assert_eq!(m.read32(TILE_STATE + 4).unwrap(), x as u32);
assert_eq!(m.read32(TILE_STATE + 8).unwrap(), y as u32);
assert_eq!(m.read16(DISPCNT).unwrap(), 0x1340);
assert_eq!(m.display_position().scanline, 160);
if updates > 1 {
assert_eq!(m.display_position().vblanks, before.vblanks + 1);
}
}
}
#[test]
fn cpu_sprite_controls_flip_and_change_priority_through_oam() {
use gba_core::{
input::{Button, Buttons},
memory::OAM_START,
};
use gba_demos::tile_demo::TileDemo;
let mut demo = TileDemo::new().unwrap();
let mut frame = Framebuffer::default();
for _ in 0..16 {
demo.frame(Buttons::default().with(Button::Right, true), &mut frame)
.unwrap();
}
for (flipped, behind) in [
(false, false),
(true, false),
(true, true),
(false, true),
(false, false),
] {
demo.frame(
Buttons::default()
.with(Button::A, flipped)
.with(Button::B, behind),
&mut frame,
)
.unwrap();
assert_scene(&frame, 32, 0, flipped, behind, false, false);
let m = demo.machine().memory();
assert_eq!(m.read16(OAM_START).unwrap(), 72);
assert_eq!(
m.read16(OAM_START + 2).unwrap(),
0x4070 | if flipped { 0x1000 } else { 0 }
);
assert_eq!(
m.read16(OAM_START + 4).unwrap(),
if behind { 0x400 } else { 0 }
);
for i in 1..128 {
assert_eq!(m.read16(OAM_START + i * 8).unwrap(), 0x200);
}
}
}
#[test]
fn cpu_affine_controls_write_matrices_preserve_center_and_return_to_regular_mode() {
use gba_core::{
input::{Button, Buttons},
memory::OAM_START,
};
use gba_demos::tile_demo::TileDemo;
let mut demo = TileDemo::new().unwrap();
let mut frame = Framebuffer::default();
// Put foreground crosses over the sprite so X also tests affine composition.
for _ in 0..16 {
demo.frame(Buttons::default().with(Button::Right, true), &mut frame)
.unwrap();
}
for bits in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0] {
let flipped = bits & 1 != 0;
let behind = bits & 2 != 0;
let rotated = bits & 4 != 0;
let zoomed = bits & 8 != 0;
let buttons = Buttons::default()
.with(Button::A, flipped)
.with(Button::B, behind)
.with(Button::L, rotated)
.with(Button::R, zoomed);
let before = demo.machine().memory().display_position();
demo.frame(buttons, &mut frame).unwrap();
assert_scene(&frame, 32, 0, flipped, behind, rotated, zoomed);
let m = demo.machine().memory();
let affine = rotated || zoomed;
assert_eq!(
m.read16(OAM_START).unwrap(),
if affine { 0x340 } else { 72 }
);
assert_eq!(
m.read16(OAM_START + 2).unwrap(),
if affine {
0x4068
} else {
0x4070 | if flipped { 0x1000 } else { 0 }
}
);
let a: i16 = match (rotated, zoomed) {
(false, false) => 256,
(false, true) => 128,
(true, false) => 181,
(true, true) => 90,
};
let b = if rotated { a } else { 0 };
for (i, value) in [a, b, -b, a].into_iter().enumerate() {
assert_eq!(
m.read16(OAM_START + 6 + i as u32 * 8).unwrap(),
value as u16
);
}
for i in 1..128 {
assert_eq!(m.read16(OAM_START + i * 8).unwrap(), 0x200);
}
assert_eq!(m.display_position().vblanks, before.vblanks + 1);
assert_eq!(m.display_position().scanline, 160);
}
}
#[test]
fn green_swap_runs_after_tile_compositing() {
let mut m = setup();
m.write16(VRAM, 0x21).unwrap();
m.write16(GREENSWAP, 1).unwrap();
assert_eq!(pixel(&m, 0, 0), 0xffff00);
assert_eq!(pixel(&m, 1, 0), 0);
}

View file

@ -0,0 +1,360 @@
use gba_core::{
io::{IE, IF, IME, TIMER_BASE},
memory::{Memory, MemoryError},
};
fn memory() -> Memory {
Memory::new(vec![]).unwrap()
}
#[test]
fn io_reset_values_masks_and_unused_bytes() {
let mut bus = memory();
for address in (TIMER_BASE..TIMER_BASE + 16)
.chain(IE..IE + 4)
.chain(IME..IME + 4)
{
assert_eq!(bus.read8(address).unwrap(), 0);
}
bus.write16(IE, 0xffff).unwrap();
assert_eq!(bus.read16(IE).unwrap(), 0x3fff);
bus.write32(IME, u32::MAX).unwrap();
assert_eq!(bus.read32(IME).unwrap(), 1);
bus.write32(IME, 0xffff_fffe).unwrap();
assert_eq!(bus.read32(IME).unwrap(), 0);
for index in 0..4 {
let address = TIMER_BASE + index * 4 + 2;
bus.write16(address, 0xffff).unwrap();
assert_eq!(
bus.read16(address).unwrap(),
if index == 0 { 0xc3 } else { 0xc7 }
);
}
}
#[test]
fn timer_data_reads_counter_not_reload_and_start_loads_reload() {
let mut bus = memory();
bus.write16(TIMER_BASE, 0x1234).unwrap();
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0);
bus.advance_cycles(100);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0);
bus.write16(TIMER_BASE + 2, 0x80).unwrap();
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1234);
bus.advance_cycles(5);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1239);
bus.write16(TIMER_BASE, 0xabcd).unwrap();
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1239);
bus.write16(TIMER_BASE + 2, 0x80).unwrap(); // No rising enable edge
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1239);
bus.write16(TIMER_BASE + 2, 0).unwrap();
bus.advance_cycles(100);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1239);
bus.write16(TIMER_BASE + 2, 0x80).unwrap();
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xabcd);
}
#[test]
fn combined_word_write_uses_new_reload_before_enabling() {
let mut bus = memory();
for index in 0..4 {
let address = TIMER_BASE + index * 4;
bus.write32(address, 0x0080_abcd).unwrap();
assert_eq!(bus.read32(address).unwrap(), 0x0080_abcd);
}
bus.advance_cycles(1);
for index in 0..4 {
assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xabce);
}
}
#[test]
fn reload_byte_writes_merge_with_hidden_latch_not_counter() {
let mut bus = memory();
bus.write16(TIMER_BASE, 0x1234).unwrap();
bus.write8(TIMER_BASE, 0x56).unwrap();
bus.write8(TIMER_BASE + 2, 0x80).unwrap();
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1256);
bus.advance_cycles(1000);
bus.write8(TIMER_BASE + 1, 0xab).unwrap();
bus.write8(TIMER_BASE + 2, 0).unwrap();
bus.write8(TIMER_BASE + 2, 0x80).unwrap();
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xab56);
}
#[test]
fn all_prescalers_retain_fractional_cycles_between_advances() {
for (selection, divisor) in [1, 64, 256, 1024].into_iter().enumerate() {
let mut bus = memory();
bus.write32(TIMER_BASE, ((0x80 | selection as u32) << 16) | 123)
.unwrap();
bus.advance_cycles(divisor - 1);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 123);
bus.advance_cycles(1);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 124);
bus.advance_cycles(divisor * 3 + divisor - 1);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 127);
bus.advance_cycles(1);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 128);
}
}
#[test]
fn clock_source_changes_and_restart_reset_provisional_phase() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x0081_0000).unwrap(); // /64
bus.advance_cycles(63);
bus.write16(TIMER_BASE + 2, 0xc1).unwrap(); // IRQ change preserves phase
bus.advance_cycles(1);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 1);
bus.advance_cycles(63);
bus.write16(TIMER_BASE + 2, 0x82).unwrap(); // /256 resets phase
bus.advance_cycles(255);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 1);
bus.advance_cycles(1);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 2);
bus.write16(TIMER_BASE + 2, 0).unwrap();
bus.advance_cycles(255);
bus.write16(TIMER_BASE + 2, 0x82).unwrap();
bus.advance_cycles(255);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0);
}
#[test]
fn overflow_reloads_and_latches_request_without_delivery_enables() {
let mut bus = memory();
for index in 0..4 {
bus.write32(TIMER_BASE + index * 4, 0x00c0_fffe).unwrap();
}
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 0x78);
assert!(!bus.irq_pending()); // IE and IME remain zero
for index in 0..4 {
assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xfffe);
}
bus.advance_cycles(5);
assert_eq!(bus.read16(IF).unwrap(), 0x78);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xffff);
}
#[test]
fn overflow_uses_updated_reload_without_resetting_current_counter() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x00c0_fffe).unwrap();
bus.write16(TIMER_BASE, 0x1234).unwrap();
bus.advance_cycles(1);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xffff);
bus.advance_cycles(1);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1234);
assert_eq!(bus.read16(IF).unwrap(), 8);
}
#[test]
fn overflow_with_local_irq_disabled_does_not_set_request() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x0080_ffff).unwrap();
bus.advance_cycles(100);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.write16(TIMER_BASE + 2, 0xc0).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 0); // No retroactive request
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 8);
}
#[test]
fn cascades_count_all_overflows_including_multiple_levels() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x00c0_fffe).unwrap(); // Overflow every 2 cycles
for index in 1..4 {
bus.write32(TIMER_BASE + index * 4, 0x00c7_fffe).unwrap(); // /1024 ignored
}
bus.advance_cycles(15);
assert_eq!(bus.read16(IF).unwrap(), 0x38);
for index in 0..4 {
assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xffff);
}
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 0x78);
for index in 0..4 {
assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xfffe);
}
}
#[test]
fn disabled_cascade_stage_blocks_downstream_pulses() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x0080_ffff).unwrap();
bus.write32(TIMER_BASE + 4, 0x0004_ffff).unwrap(); // Disabled timer 1
bus.write32(TIMER_BASE + 8, 0x00c4_1234).unwrap();
bus.advance_cycles(100_000);
assert_eq!(bus.read16(TIMER_BASE + 8).unwrap(), 0x1234);
assert_eq!(bus.read16(IF).unwrap(), 0);
}
#[test]
fn normal_timer_breaks_cascade_dependency_and_timer_zero_ignores_count_up() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x0084_ffff).unwrap(); // Bit 2 ignored
bus.write32(TIMER_BASE + 8, 0x0080_fffe).unwrap(); // Independent timer 2
bus.write32(TIMER_BASE + 12, 0x00c4_ffff).unwrap();
bus.advance_cycles(2);
assert_eq!(bus.read16(TIMER_BASE + 2).unwrap(), 0x80);
assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xffff);
assert_eq!(bus.read16(IF).unwrap(), 0x40);
}
#[test]
fn bulk_advance_handles_maximum_cycle_count_and_period_one() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
for index in 1..4 {
bus.write32(TIMER_BASE + index * 4, 0x00c4_ffff).unwrap();
}
bus.advance_cycles(u32::MAX);
assert_eq!(bus.cycles(), u64::from(u32::MAX));
for index in 0..4 {
assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xffff);
}
assert_eq!(bus.read16(IF).unwrap(), 0x78);
bus.advance_cycles(u32::MAX);
assert_eq!(bus.cycles(), 2 * u64::from(u32::MAX));
}
#[test]
fn bulk_advance_matches_independent_cycle_by_cycle_reference() {
// A small literal reference: count prescaler ticks and propagate one pulse
// per cycle. Unlike the implementation, this does not use overflow division.
for selection in 0..4 {
for cascade_mask in 0..8 {
let mut bus = memory();
let reloads = [0xfffd_u16, 0xfffe, 0xffff, 0xfffc];
let mut counters = reloads;
let mut phases = [0_u32; 4];
let mut pending = 0_u16;
for (index, reload) in reloads.into_iter().enumerate() {
let cascade = index > 0 && cascade_mask & (1 << (index - 1)) != 0;
let control = 0xc0 | selection | if cascade { 4 } else { 0 };
bus.write32(
TIMER_BASE + index as u32 * 4,
(control << 16) | u32::from(reload),
)
.unwrap();
}
for chunk in [0, 1, 17, 63, 1000, 8193] {
bus.advance_cycles(chunk);
for _ in 0..chunk {
let mut overflow = false;
for index in 0..4 {
let cascade = index > 0 && cascade_mask & (1 << (index - 1)) != 0;
let tick = if cascade {
overflow
} else {
phases[index] += 1;
if phases[index] == [1, 64, 256, 1024][selection as usize] {
phases[index] = 0;
true
} else {
false
}
};
overflow = tick && counters[index] == u16::MAX;
if overflow {
counters[index] = reloads[index];
pending |= 1 << (index + 3);
} else if tick {
counters[index] += 1;
}
}
}
for (index, counter) in counters.into_iter().enumerate() {
assert_eq!(bus.read16(TIMER_BASE + index as u32 * 4).unwrap(), counter);
}
assert_eq!(bus.read16(IF).unwrap(), pending);
}
}
}
}
#[test]
fn interrupt_delivery_requires_both_ie_and_ime_without_clearing_if() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
bus.advance_cycles(1);
for (enable, master, expected) in [
(0, 0, false),
(8, 0, false),
(0, 1, false),
(16, 1, false),
(8, 1, true),
] {
bus.write16(IE, enable).unwrap();
bus.write32(IME, master).unwrap();
assert_eq!(bus.irq_pending(), expected);
assert_eq!(bus.read16(IF).unwrap(), 8);
}
}
#[test]
fn interrupt_acknowledgement_is_write_one_to_clear_for_all_widths() {
let mut bus = memory();
for index in 0..4 {
bus.write32(TIMER_BASE + index * 4, 0x00c0_ffff).unwrap();
}
bus.advance_cycles(1);
bus.write16(IF, 0).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 0x78);
bus.write8(IF + 1, 0xff).unwrap(); // High byte must not clear low flags
assert_eq!(bus.read16(IF).unwrap(), 0x78);
bus.write8(IF, 8).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 0x70);
bus.write16(IF, 0x20).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 0x50);
bus.write32(IE, 0x0010_0078).unwrap(); // IE=0x78, clear only Timer 1
assert_eq!(bus.read32(IE).unwrap(), 0x0040_0078);
bus.write16(IF, 0xffff).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read16(IF).unwrap(), 0x78); // Later events relatch
}
#[test]
fn high_byte_writes_do_not_reset_control_or_modify_low_enable_bits() {
let mut bus = memory();
bus.write32(TIMER_BASE, 0x0080_1234).unwrap();
bus.write8(TIMER_BASE + 3, 0xff).unwrap();
bus.write8(IE, 8).unwrap();
bus.write8(IE + 1, 0xff).unwrap();
assert_eq!(bus.read16(IE).unwrap(), 0x3f08);
bus.write8(IME, 1).unwrap();
for offset in 1..4 {
bus.write8(IME + offset, 0xff).unwrap();
}
assert_eq!(bus.read32(IME).unwrap(), 1);
bus.advance_cycles(1);
assert_eq!(bus.read32(TIMER_BASE).unwrap(), 0x0080_1235);
}
#[test]
fn unknown_io_and_mirrors_stay_unmapped_and_unaligned_writes_have_no_effect() {
let mut bus = memory();
for address in [0x0400_0058, TIMER_BASE + 16, IE + 12, IME + 4, 0x0401_0100] {
assert_eq!(bus.read8(address), Err(MemoryError::Unmapped(address)));
assert_eq!(
bus.write32(address, u32::MAX),
Err(MemoryError::Unmapped(address))
);
}
assert_eq!(
bus.write32(TIMER_BASE + 2, u32::MAX),
Err(MemoryError::Unaligned(TIMER_BASE + 2))
);
assert_eq!(
bus.write16(IF + 1, u16::MAX),
Err(MemoryError::Unaligned(IF + 1))
);
assert_eq!(bus.read32(TIMER_BASE).unwrap(), 0);
assert_eq!(bus.read32(IE).unwrap(), 0);
assert_eq!(bus.cycles(), 0);
}

View file

@ -0,0 +1,169 @@
use gba_core::{
cpu::Cpu,
io::{IE, IF, IME, TIMER_BASE, WAITCNT},
machine::{Machine, StepKind},
memory::{Memory, MemoryError, ROM_START},
timing::{bus_cycles, AccessKind, AccessWidth},
};
#[test]
fn waitcnt_masks_unused_bits_and_supports_all_bus_widths() {
let mut memory = Memory::new(vec![]).unwrap();
assert_eq!(memory.read32(WAITCNT).unwrap(), 0);
memory.write32(WAITCNT, u32::MAX).unwrap();
assert_eq!(memory.read32(WAITCNT).unwrap(), 0x5fff);
memory.write8(WAITCNT, 0x17).unwrap();
memory.write8(WAITCNT + 1, 0x43).unwrap();
assert_eq!(memory.waitcnt(), 0x4317);
memory.write16(WAITCNT + 2, 0xffff).unwrap();
assert_eq!(memory.read32(WAITCNT).unwrap(), 0x4317);
assert_eq!(memory.read16(IE).unwrap(), 0);
assert_eq!(memory.read16(IF).unwrap(), 0);
assert_eq!(memory.cycles(), 0);
assert_eq!(
memory.write32(WAITCNT + 2, 0),
Err(MemoryError::Unaligned(WAITCNT + 2))
);
assert_eq!(memory.waitcnt(), 0x4317);
}
#[test]
fn every_rom_wait_setting_has_the_documented_halfword_and_word_costs() {
for window in 0..3 {
for first in 0..4 {
for second in 0..2 {
let waitcnt = (first << [2, 5, 8][window]) | (second << [4, 7, 10][window]);
let n = [5, 4, 3, 9][first as usize];
let s = if second == 1 { 2 } else { [3, 5, 9][window] };
for mirror in [0, 0x0100_0000] {
let address = ROM_START + window as u32 * 0x0200_0000 + mirror + 0x100;
for (kind, initial) in
[(AccessKind::NonSequential, n), (AccessKind::Sequential, s)]
{
assert_eq!(
bus_cycles(waitcnt, address, AccessWidth::Byte, kind),
initial
);
assert_eq!(
bus_cycles(waitcnt, address, AccessWidth::Halfword, kind),
initial
);
assert_eq!(
bus_cycles(waitcnt, address, AccessWidth::Word, kind),
initial + s
);
}
}
}
}
}
}
#[test]
fn rom_128_kib_boundaries_force_nonsequential_first_halfwords() {
for address in [
0x0800_0000,
0x0802_0000,
0x09fe_0000,
0x0a02_0000,
0x0c02_0000,
] {
let window = ((address >> 25) - 4) as usize;
let s = [3, 5, 9][window];
assert_eq!(
bus_cycles(0, address, AccessWidth::Halfword, AccessKind::Sequential),
5
);
assert_eq!(
bus_cycles(0, address + 1, AccessWidth::Byte, AccessKind::Sequential),
5
);
assert_eq!(
bus_cycles(0, address, AccessWidth::Word, AccessKind::Sequential),
5 + s
);
assert_eq!(
bus_cycles(
0,
address + 2,
AccessWidth::Halfword,
AccessKind::Sequential
),
s
);
}
}
#[test]
fn internal_regions_and_ewram_use_fixed_width_specific_costs() {
for kind in [AccessKind::NonSequential, AccessKind::Sequential] {
for width in [AccessWidth::Byte, AccessWidth::Halfword, AccessWidth::Word] {
for address in [0, 0x0300_0000, 0x03ff_fffc, WAITCNT] {
assert_eq!(bus_cycles(0x5fff, address, width, kind), 1);
}
for address in [0x0200_0000, 0x02ff_fffc] {
assert_eq!(
bus_cycles(0x5fff, address, width, kind),
if width == AccessWidth::Word { 6 } else { 3 }
);
}
}
}
}
#[test]
fn stored_prefetch_phi_and_sram_settings_do_not_change_supported_bus_costs() {
// Explicit limitation: no prefetch queue, PHI output, or SRAM mapping yet.
for width in [AccessWidth::Halfword, AccessWidth::Word] {
for address in [ROM_START, ROM_START + 4, 0x0a00_0004, 0x0c00_0004] {
for kind in [AccessKind::NonSequential, AccessKind::Sequential] {
assert_eq!(
bus_cycles(0, address, width, kind),
bus_cycles(0x5803, address, width, kind)
);
}
}
}
}
#[test]
fn wait_states_change_when_a_timer_interrupt_is_sampled() {
for (waitcnt, entry_step, instructions, cycles) in [(0, 4, 3, 23), (0x18, 6, 5, 24)] {
let mut memory = Memory::new(0xe280_0001_u32.to_le_bytes().repeat(64)).unwrap();
memory.write16(WAITCNT, waitcnt).unwrap();
memory.write32(TIMER_BASE, 0x00c0_ffec).unwrap(); // 20 cycles
memory.write16(IE, 8).unwrap();
memory.write32(IME, 1).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), memory);
assert_eq!(machine.last_timing().total(), 0);
for step in 1..=entry_step {
let event = machine.step().unwrap();
assert_eq!(
event,
if step == entry_step {
StepKind::IrqEntry
} else {
StepKind::Instruction
}
);
}
assert_eq!(machine.cpu().registers()[0], instructions);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.last_timing().code_cycles, 3);
}
}
#[test]
fn failed_machine_step_preserves_previous_timing_breakdown() {
let mut memory = Memory::new(0xe1a0_0000_u32.to_le_bytes().to_vec()).unwrap();
memory.write32(TIMER_BASE, 0x0080_0000).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), memory);
machine.step().unwrap();
let timing = machine.last_timing();
let cycles = machine.cycles();
let counter = machine.memory().read16(TIMER_BASE).unwrap();
assert!(machine.step().is_err());
assert_eq!(machine.last_timing(), timing);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), counter);
}

View file

@ -0,0 +1,228 @@
use gba_core::{
cpu::Cpu,
display::{
CYCLES_PER_FRAME as FRAME, CYCLES_PER_LINE as LINE, HBLANK_START as HB, VBLANK_START as VB,
},
dma::DMA_BASE,
io::*,
machine::Machine,
memory::{Memory, PALETTE_START as PAL, ROM_START},
video::{rgb555_to_rgb888 as rgb, Framebuffer, HEIGHT, WIDTH},
};
#[path = "window_tracking/horizontal.rs"]
mod horizontal;
fn setup(bounds: u16) -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m.write16(DISPCNT, 0x2000).unwrap();
m.write16(PAL, 31).unwrap();
m.write16(WIN0H, 240).unwrap();
m.write16(WIN1H, 240).unwrap();
m.write16(WIN0V, bounds).unwrap();
m.write16(WININ, 0x2020).unwrap();
m.write16(WINOUT, 0).unwrap();
m.write16(BLDCNT, 0xe0).unwrap(); // Darken backdrop only when effects are enabled.
m.write16(BLDY, 16).unwrap();
m
}
fn present(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
assert!(m.present_frame(&mut f).unwrap());
f
}
fn finish(m: &mut Memory) -> Framebuffer {
let position = m.display_position();
let phase = u32::from(position.scanline) * LINE + u32::from(position.line_cycle);
m.advance_cycles(VB - phase);
present(m)
}
fn check_rows(f: &Framebuffer, inside: impl Fn(usize) -> bool) {
for y in 0..HEIGHT {
let expected = if inside(y) { 0 } else { rgb(31) };
assert!(
f.pixels()[y * WIDTH..(y + 1) * WIDTH]
.iter()
.all(|&p| p == expected),
"row={y}"
);
}
}
#[test]
fn ordinary_static_windows_match_debug_snapshots() {
for bounds in [0x00a0, 0x0130, 0x0405, 0x9fa0, 0x0000, 0x0505, 0x00ff] {
let mut m = setup(bounds);
let mut expected = Framebuffer::default();
m.render_frame(&mut expected).unwrap();
assert_eq!(
finish(&mut m).pixels(),
expected.pixels(),
"bounds={bounds:#x}"
);
}
}
#[test]
fn moving_top_after_entry_does_not_close_an_active_window() {
let mut m = setup(0x0210);
m.advance_cycles(4 * LINE + HB);
m.write8(WIN0V + 1, 12).unwrap();
let f = finish(&mut m);
check_rows(&f, |y| (2..16).contains(&y));
let mut snapshot = Framebuffer::default();
m.render_frame(&mut snapshot).unwrap();
check_rows(&snapshot, |y| (12..16).contains(&y));
assert_eq!(present(&m).pixels(), f.pixels()); // Snapshot rendering has no latch side effects.
}
#[test]
fn moving_top_behind_vcount_does_not_open_a_window() {
let mut m = setup(0x0a14);
m.advance_cycles(5 * LINE + HB);
m.write8(WIN0V + 1, 2).unwrap();
check_rows(&finish(&mut m), |_| false);
m.advance_cycles(FRAME - VB);
check_rows(&finish(&mut m), |y| (2..20).contains(&y));
}
#[test]
fn missing_bottom_keeps_window_open_across_frame_wrap() {
let mut m = setup(0x0214);
m.advance_cycles(5 * LINE + HB);
m.write8(WIN0V, 4).unwrap();
check_rows(&finish(&mut m), |y| y >= 2);
m.advance_cycles(FRAME - VB);
check_rows(&finish(&mut m), |y| y < 4); // No unconditional line-zero reset.
}
#[test]
fn writes_before_line_start_take_effect_but_writes_after_it_do_not_replay_edges() {
for before in [false, true] {
let mut m = setup(0x1420);
m.advance_cycles(5 * LINE - u32::from(before));
m.write16(WIN0V, 0x0508).unwrap();
m.advance_cycles(0); // Does not trigger a comparator.
check_rows(&finish(&mut m), |y| before && (5..8).contains(&y));
}
}
#[test]
fn hblank_writes_cannot_replay_the_current_rows_comparator() {
let mut m = setup(0x0208);
m.advance_cycles(3 * LINE + HB);
m.write16(WIN0V, 0x0203).unwrap(); // Bottom already missed this row.
check_rows(&finish(&mut m), |y| y >= 2);
}
#[test]
fn hidden_line_edges_run_and_unreachable_edges_do_not_fire() {
for top in [160, 200, 227, 228, 255] {
let mut m = setup((top << 8) | 8);
check_rows(&finish(&mut m), |_| false); // Reset starts outside; top not reached yet.
m.advance_cycles(FRAME - VB);
check_rows(&finish(&mut m), |y| top < 228 && y < 8);
}
}
#[test]
fn equal_bounds_clear_only_at_the_matching_edge_and_bottom_wins() {
let mut m = setup(0x0214);
m.advance_cycles(4 * LINE);
m.write16(WIN0V, 0x0808).unwrap();
check_rows(&finish(&mut m), |y| (2..8).contains(&y));
let mut m = setup(0x0214);
m.advance_cycles(4 * LINE);
m.write16(WIN0V, 0xffff).unwrap(); // Neither edge is reachable by VCOUNT.
check_rows(&finish(&mut m), |y| y >= 2);
m.advance_cycles(FRAME - VB);
check_rows(&finish(&mut m), |_| true);
}
#[test]
fn window_disable_and_forced_blank_do_not_pause_comparators() {
for control in [0x4000, 0x2080] {
let mut m = setup(0x0210);
m.write16(DISPCNT, control).unwrap();
m.advance_cycles(5 * LINE);
m.write16(DISPCNT, 0x2000).unwrap();
let f = finish(&mut m);
for y in 0..HEIGHT {
let expected = if y < 5 && control & 0x80 != 0 {
0xffffff
} else if (5..16).contains(&y) {
0
} else {
rgb(31)
};
assert_eq!(
f.pixels()[y * WIDTH],
expected,
"control={control:#x} row={y}"
);
}
}
}
#[test]
fn win0_and_win1_keep_independent_state_and_win0_keeps_precedence() {
let mut m = setup(0x0206);
m.write32(WIN0V, 0x0408_0206).unwrap(); // Independent byte merging for both windows.
m.write16(DISPCNT, 0x6000).unwrap();
m.write16(WININ, 0x2000).unwrap(); // WIN0 allows red; WIN1 darkens to black.
m.advance_cycles(3 * LINE + HB);
m.write8(WIN0V + 1, 5).unwrap(); // Does not close WIN0 before row5.
m.advance_cycles(LINE);
m.write8(WIN1V + 1, 7).unwrap(); // Does not close WIN1 before row7.
check_rows(&finish(&mut m), |y| (6..8).contains(&y));
assert_eq!(m.read32(WIN0V).unwrap(), 0); // Registers remain write-only.
}
#[test]
fn hblank_bounds_and_mask_writes_apply_to_subsequent_rows() {
let mut m = setup(0x020a);
m.advance_cycles(4 * LINE + HB);
m.write16(WIN0H, 0x0a14).unwrap();
m.advance_cycles(LINE);
m.write16(WININ, 0).unwrap();
let f = finish(&mut m);
for y in 0..HEIGHT {
for x in 0..WIDTH {
let dark = (2..5).contains(&y) || (y == 5 && (10..20).contains(&x));
assert_eq!(f.pixels()[y * WIDTH + x], if dark { 0 } else { rgb(31) });
}
}
}
#[test]
fn hblank_dma_programs_future_edges_without_recomputing_interval_membership() {
let mut m = setup(0x1020);
for y in 0..160 {
m.write16(0x0200_0000 + y * 2, if y == 0 { 0x0104 } else { 0xc804 })
.unwrap();
}
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, WIN0V).unwrap();
m.write32(DMA_BASE + 8, 0xa240_0001).unwrap();
m.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.run_until_vblank(1000).unwrap();
check_rows(&present(machine.memory()), |y| (1..4).contains(&y));
assert!(machine.halted());
assert_eq!(machine.memory().read16(DMA_BASE + 10).unwrap(), 0xa240);
}
#[test]
fn capture_disabled_still_tracks_hidden_edges_and_enabling_does_not_reset_state() {
let mut m = setup(0xc808);
m.set_scanline_rendering(false);
m.advance_cycles(FRAME); // Hidden row200 opens the window, retained at row0.
m.set_scanline_rendering(true);
m.set_scanline_rendering(true);
check_rows(&finish(&mut m), |y| y < 8);
}

View file

@ -0,0 +1,209 @@
use super::*;
fn scene(horizontal: u16) -> Memory {
let mut m = setup(160);
m.write16(WIN0H, horizontal).unwrap();
m
}
fn check_line(f: &Framebuffer, y: usize, inside: impl Fn(usize) -> bool) {
for x in 0..WIDTH {
assert_eq!(
f.pixels()[y * WIDTH + x],
if inside(x) { 0 } else { rgb(31) },
"pixel={x},{y}"
);
}
}
#[test]
fn moving_left_after_entry_preserves_earlier_pixels_and_active_state() {
let mut m = scene(0x0a50);
m.advance_cycles(4 * 20 + 1);
m.write8(WIN0H + 1, 40).unwrap();
let f = finish(&mut m);
check_line(&f, 0, |x| (10..80).contains(&x));
check_line(&f, 1, |x| (40..80).contains(&x));
let mut snapshot = Framebuffer::default();
m.render_frame(&mut snapshot).unwrap();
check_line(&snapshot, 0, |x| (40..80).contains(&x));
assert_eq!(present(&m).pixels(), f.pixels());
}
#[test]
fn moving_left_behind_current_column_does_not_open_an_inactive_window() {
let mut m = scene(0x6478);
m.advance_cycles(4 * 20);
m.write8(WIN0H + 1, 10).unwrap();
let f = finish(&mut m);
check_line(&f, 0, |_| false);
check_line(&f, 1, |x| (10..120).contains(&x));
}
#[test]
fn missing_right_keeps_window_open_into_the_next_line() {
let mut m = scene(0x0a50);
m.advance_cycles(4 * 30);
m.write8(WIN0H, 5).unwrap();
let f = finish(&mut m);
check_line(&f, 0, |x| x >= 10);
check_line(&f, 1, |x| !(5..10).contains(&x));
}
#[test]
fn exact_four_cycle_sampling_boundaries_and_zero_cycle_advances_are_observable() {
for elapsed in [79, 80, 81, 83] {
let mut m = scene(0x6450);
m.advance_cycles(elapsed);
m.write16(WIN0H, 0x1450).unwrap();
m.advance_cycles(0);
let f = finish(&mut m);
check_line(&f, 0, |x| elapsed <= 80 && (20..80).contains(&x));
check_line(&f, 1, |x| (20..80).contains(&x));
}
}
#[test]
fn equal_bounds_clear_at_the_edge_not_at_register_write() {
for equal in [20, 40] {
let mut m = scene(0x0ac8);
m.advance_cycles(4 * 30);
m.write16(WIN0H, equal * 0x101).unwrap();
let f = finish(&mut m);
check_line(&f, 0, |x| x >= 10 && (equal == 20 || x < 40));
check_line(&f, 1, |x| equal == 20 && x < 20);
check_line(&f, 2, |_| false);
}
}
#[test]
fn offscreen_columns_including_late_hblank_set_next_lines_wrapping_state() {
for left in [240, 251, 252, 255] {
let mut m = scene((left << 8) | 10);
let f = finish(&mut m);
check_line(&f, 0, |_| false); // No previous line at startup.
check_line(&f, 1, |x| x < 10);
check_line(&f, 159, |x| x < 10);
}
}
#[test]
fn hblank_writes_can_cancel_a_late_offscreen_edge_without_changing_captured_row() {
let mut m = scene(0xff0a);
m.advance_cycles(HB);
m.write8(WIN0H + 1, 20).unwrap(); // X255 not reached; X20 already passed.
let f = finish(&mut m);
check_line(&f, 0, |_| false);
check_line(&f, 1, |x| x >= 20);
check_line(&f, 2, |x| !(10..20).contains(&x));
}
#[test]
fn independent_window_histories_preserve_win0_precedence() {
let mut m = scene(0x0a50);
m.write16(WIN1H, 0x1464).unwrap();
m.write16(WIN1V, 160).unwrap();
m.write16(DISPCNT, 0x6000).unwrap();
m.write16(WININ, 0x2000).unwrap(); // WIN0 red, WIN1 black.
m.advance_cycles(4 * 30);
m.write32(WIN0H, 0x1464_3250).unwrap(); // Move WIN0's left edge only.
let f = finish(&mut m);
check_line(&f, 0, |x| (80..100).contains(&x));
check_line(&f, 1, |x| (20..50).contains(&x) || (80..100).contains(&x));
assert_eq!(m.read32(WIN0H).unwrap(), 0);
}
#[test]
fn disabled_windows_and_forced_blank_do_not_pause_horizontal_history() {
for control in [0x4000, 0x2080] {
let mut m = scene(0x0a50);
m.write16(DISPCNT, control).unwrap();
m.advance_cycles(4 * 30);
m.write16(DISPCNT, 0x2000).unwrap();
let f = finish(&mut m);
// Enable/blank bits still sample at HBlank, unlike WIN0H comparator history.
check_line(&f, 0, |x| (10..80).contains(&x));
}
}
#[test]
fn hblank_dma_can_set_an_offscreen_edge_before_the_next_line() {
let mut m = scene(0);
for row in 0..160 {
m.write16(0x0200_0000 + row * 2, 0xff0a).unwrap();
}
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, WIN0H).unwrap();
m.write32(DMA_BASE + 8, 0xa240_0001).unwrap();
m.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.run_until_vblank(1000).unwrap();
let f = present(machine.memory());
check_line(&f, 0, |_| false);
for y in 1..HEIGHT {
check_line(&f, y, |x| x < 10);
}
assert!(machine.halted());
}
#[test]
fn cpu_store_crossing_a_comparator_uses_committed_bounds_not_final_instruction_time() {
for after in [false, true] {
let mut m = scene(0x6450);
let code = [
0xe3a0_0301, // MOV r0,#0x04000000
0xe3a0_1b05, // MOV r1,#0x1400
0xe381_1050, // ORR r1,r1,#0x50
0xe1c0_14b0, // STRH r1,[r0,#0x40] (WIN0H)
];
for (i, value) in code.into_iter().enumerate() {
m.write32(0x0300_0000 + i as u32 * 4, value).unwrap();
}
let mut machine = Machine::new(Cpu::new(0x0300_0000), m);
for _ in 0..3 {
machine.step().unwrap();
}
let cycles = machine.cycles() as u32;
machine
.memory_mut()
.advance_cycles(if after { 81 } else { 79 } - cycles);
machine.step().unwrap();
assert!(machine.cycles() > 80);
let f = finish(machine.memory_mut());
check_line(&f, 0, |x| !after && (20..80).contains(&x));
}
}
#[test]
fn pixel_colors_still_sample_at_hblank_while_window_edges_keep_history() {
let mut m = scene(0x0a50);
m.advance_cycles(4 * 30);
m.write8(WIN0H + 1, 40).unwrap();
m.write16(PAL, 0x7c00).unwrap();
let f = finish(&mut m);
for x in 0..WIDTH {
assert_eq!(
f.pixels()[x],
if (10..80).contains(&x) {
0
} else {
rgb(0x7c00)
}
);
}
}
#[test]
fn capture_enable_preserves_partial_line_history_and_hidden_lines_update_flags() {
let mut m = scene(0x0a50);
m.set_scanline_rendering(false);
m.advance_cycles(4 * 30);
m.write8(WIN0H + 1, 40).unwrap();
m.set_scanline_rendering(true); // Before first HBlank, this frame is still complete.
check_line(&finish(&mut m), 0, |x| (10..80).contains(&x));
m.set_scanline_rendering(false);
m.write16(WIN0H, 0xff0a).unwrap();
m.advance_cycles(FRAME - VB);
m.set_scanline_rendering(true);
check_line(&finish(&mut m), 0, |x| x < 10);
}