refactor: split into gameboy-rust workspace with core, demos, and desktop crates
Move the emulator into crates/gba-core, the original demo programs into crates/gba-demos, and the minifb frontend into crates/desktop. The binary is now gameboy-rust. Imports change from gba_rust to gba_core/gba_demos; no behavior changes. gba-core has no dependencies. Its integration tests use gba-demos as a development dependency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a8944d1d5f
commit
f8a0676748
113 changed files with 304 additions and 282 deletions
377
crates/gba-core/tests/affine_backgrounds.rs
Normal file
377
crates/gba-core/tests/affine_backgrounds.rs
Normal 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());
|
||||
}
|
||||
330
crates/gba-core/tests/affine_tracking.rs
Normal file
330
crates/gba-core/tests/affine_tracking.rs
Normal 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
|
||||
);
|
||||
}
|
||||
671
crates/gba-core/tests/bios.rs
Normal file
671
crates/gba-core/tests/bios.rs
Normal 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))
|
||||
);
|
||||
}
|
||||
684
crates/gba-core/tests/bios/affine.rs
Normal file
684
crates/gba-core/tests/bios/affine.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
377
crates/gba-core/tests/bios/angles.rs
Normal file
377
crates/gba-core/tests/bios/angles.rs
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
use super::*;
|
||||
use gba_core::{bios::INVALID_ARGUMENT_TRAP, dma::DMA_BASE};
|
||||
|
||||
/// Wide-integer reference for signed low-word multiplication and floor division.
|
||||
/// This avoids both host signed overflow and the emitted instruction sequence.
|
||||
fn low_signed(value: i64) -> i64 {
|
||||
let bits = value.rem_euclid(1i64 << 32);
|
||||
if bits >= 1i64 << 31 {
|
||||
bits - (1i64 << 32)
|
||||
} else {
|
||||
bits
|
||||
}
|
||||
}
|
||||
|
||||
fn atan_reference(tangent: i32) -> i32 {
|
||||
let a = -low_signed(i64::from(tangent).pow(2)).div_euclid(16384);
|
||||
let mut polynomial = 0xa9i64;
|
||||
for coefficient in [0x390, 0x91c, 0xfb6, 0x16aa, 0x2081, 0x3651, 0xa2f9] {
|
||||
polynomial = low_signed(low_signed(polynomial * a).div_euclid(16384) + coefficient);
|
||||
}
|
||||
low_signed(i64::from(tangent) * polynomial).div_euclid(65536) as i32
|
||||
}
|
||||
|
||||
/// Eight-sector signed-coordinate reference; production uses absolute magnitudes.
|
||||
fn atan2_reference(x: i32, y: i32) -> u32 {
|
||||
if y == 0 {
|
||||
return if x < 0 { 0x8000 } else { 0 };
|
||||
}
|
||||
if x == 0 {
|
||||
return if y < 0 { 0xc000 } else { 0x4000 };
|
||||
}
|
||||
let ratio = |n: i32, d: i32| atan_reference((i64::from(n) * 16384 / i64::from(d)) as i32);
|
||||
let angle = if y > 0 {
|
||||
if x > 0 && x >= y {
|
||||
ratio(y, x)
|
||||
} else if x < 0 && -x >= y {
|
||||
0x8000 + ratio(y, x)
|
||||
} else {
|
||||
0x4000 - ratio(x, y)
|
||||
}
|
||||
} else if x < 0 && -x > -y {
|
||||
0x8000 + ratio(y, x)
|
||||
} else if x > 0 && x >= -y {
|
||||
0x10000 + ratio(y, x)
|
||||
} else {
|
||||
0xc000 - ratio(x, y)
|
||||
};
|
||||
(angle & 0xffff) as u32
|
||||
}
|
||||
|
||||
fn run(service: u8, x: i32, y: i32, thumb: bool) -> u32 {
|
||||
let (mut m, pc) = call(service, thumb, [x as u32, y as u32, 0xcafe_babe]);
|
||||
let before = m.cpu().clone();
|
||||
let guard = bios::SVC_STACK - if service == 9 { 32 } else { 44 };
|
||||
m.memory_mut().write32(guard, 0xa55a_1234).unwrap();
|
||||
finish(&mut m, pc);
|
||||
assert_eq!(&m.cpu().registers()[1..15], &before.registers()[1..15]);
|
||||
assert_eq!(m.cpu().cpsr(), before.cpsr());
|
||||
assert_eq!(m.memory().read32(guard).unwrap(), 0xa55a_1234);
|
||||
let actual = m.cpu().registers()[0];
|
||||
let expected = if service == 9 {
|
||||
atan_reference(x) as u32
|
||||
} else {
|
||||
atan2_reference(x, y)
|
||||
};
|
||||
assert_eq!(actual, expected, "service={service}, x={x}, y={y}");
|
||||
actual
|
||||
}
|
||||
|
||||
fn check_direction(x: i32, y: i32, thumb: bool) {
|
||||
let actual = run(0x0a, x, y, thumb);
|
||||
if x == 0 && y == 0 {
|
||||
return;
|
||||
}
|
||||
let ideal = f64::from(y).atan2(f64::from(x)) * 32768.0 / std::f64::consts::PI;
|
||||
let error = (f64::from(actual) - ideal + 32768.0).rem_euclid(65536.0) - 32768.0;
|
||||
assert!(
|
||||
error.abs() < 3.0,
|
||||
"({x},{y}) actual={actual} ideal={ideal} error={error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arctan_known_fixed_point_values_include_signed_results() {
|
||||
for (input, expected) in [
|
||||
(-16384, -8192),
|
||||
(-8192, -4836),
|
||||
(-1, -1),
|
||||
(0, 0),
|
||||
(1, 0),
|
||||
(8192, 4836),
|
||||
(16384, 8192),
|
||||
] {
|
||||
for thumb in [false, true] {
|
||||
assert_eq!(run(9, input, 0x12345678, thumb) as i32, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arctan_dense_unit_interval_grid_matches_polynomial_and_real_angle() {
|
||||
for input in (-16384..=16384).step_by(16) {
|
||||
let actual = run(9, input, 0, input & 16 != 0) as i32;
|
||||
let ideal = (f64::from(input) / 16384.0).atan() * 32768.0 / std::f64::consts::PI;
|
||||
assert!((f64::from(actual) - ideal).abs() < 2.0, "input={input}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arctan_full_signed_domain_uses_wrapped_products_not_host_float() {
|
||||
for input in (-32768..=32767).step_by(257) {
|
||||
run(9, input, 0, input & 1 != 0);
|
||||
}
|
||||
// Known out-of-unit-range accuracy problems are retained, not corrected with host atan.
|
||||
assert_eq!(run(9, -32768, 0, false) as i32, -5795);
|
||||
assert_eq!(run(9, 32767, 0, true), 5848);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arctan_seeded_inputs_match_wide_integer_reference() {
|
||||
let mut seed = 0x9a2b_14cdu32;
|
||||
for index in 0..1024 {
|
||||
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
|
||||
run(9, i32::from((seed >> 16) as i16), 0, index % 2 == 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_arctan_rounds_down_instead_of_truncating_toward_zero() {
|
||||
for input in [1, 2, 3, 127, 8191, 8192, 16383, 16384] {
|
||||
let positive = run(9, input, 0, false) as i32;
|
||||
let negative = run(9, -input, 0, true) as i32;
|
||||
assert!([0, -1].contains(&(positive + negative)));
|
||||
}
|
||||
assert_eq!(run(9, -1, 0, true), u32::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arctan2_axes_origin_and_diagonals_have_exact_turn_angles() {
|
||||
for thumb in [false, true] {
|
||||
for (x, y, expected) in [
|
||||
(0, 0, 0),
|
||||
(1, 0, 0),
|
||||
(-32768, 0, 0x8000),
|
||||
(0, 32767, 0x4000),
|
||||
(0, -32768, 0xc000),
|
||||
(1, 1, 0x2000),
|
||||
(-1, 1, 0x6000),
|
||||
(-1, -1, 0xa000),
|
||||
(1, -1, 0xe000),
|
||||
(-32768, -32768, 0xa000),
|
||||
(32767, 32767, 0x2000),
|
||||
] {
|
||||
assert_eq!(run(0x0a, x, y, thumb), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arctan2_all_quadrants_and_signed_extremes_match_both_references() {
|
||||
let values = [
|
||||
-32768, -32767, -16384, -1024, -2, -1, 0, 1, 2, 1024, 16384, 32766, 32767,
|
||||
];
|
||||
for x in values {
|
||||
for y in values {
|
||||
check_direction(x, y, (x ^ y) & 1 != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arctan2_sector_boundaries_handle_equal_and_adjacent_magnitudes() {
|
||||
for magnitude in [1, 2, 3, 127, 1024, 16384, 32766] {
|
||||
for delta in [-1, 0, 1] {
|
||||
for sx in [-1, 1] {
|
||||
for sy in [-1, 1] {
|
||||
check_direction(sx * magnitude, sy * (magnitude + delta), delta != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arctan2_seeded_signed_coordinates_match_integer_and_float_references() {
|
||||
let mut seed = 0x5017_e3b9u32;
|
||||
for index in 0..1024 {
|
||||
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
|
||||
let x = i32::from((seed >> 16) as i16);
|
||||
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
|
||||
let y = i32::from((seed >> 16) as i16);
|
||||
check_direction(x, y, index % 2 != 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arctan2_preserves_direction_when_coordinates_scale_equally() {
|
||||
for (x, y) in [
|
||||
(3, 7),
|
||||
(7, 3),
|
||||
(-3, 7),
|
||||
(-7, 3),
|
||||
(-3, -7),
|
||||
(-7, -3),
|
||||
(3, -7),
|
||||
(7, -3),
|
||||
] {
|
||||
let expected = run(0x0a, x, y, false);
|
||||
for scale in [2, 7, 31, 4096] {
|
||||
assert_eq!(run(0x0a, x * scale, y * scale, true), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_system_modes_masks_and_all_nonresult_registers_are_preserved() {
|
||||
for service in [9, 0x0a] {
|
||||
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
|
||||
for thumb in [false, true] {
|
||||
let (mut m, pc) = call_status(
|
||||
service,
|
||||
thumb,
|
||||
[(-1234i32) as u32, 2345, 0xabcd_ef01],
|
||||
status,
|
||||
);
|
||||
let before = m.cpu().clone();
|
||||
finish(&mut m, pc);
|
||||
assert_eq!(&m.cpu().registers()[1..15], &before.registers()[1..15]);
|
||||
assert_eq!(m.cpu().cpsr(), before.cpsr());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_caller_flag_combination_survives_angle_calculation() {
|
||||
for service in [9, 0x0au32] {
|
||||
for flags in 0..16u32 {
|
||||
let mut m = bios::boot(words(&[
|
||||
0xe3a0_0a02, // MOV r0,#0x2000
|
||||
0xe3a0_1901, // MOV r1,#0x4000
|
||||
0xe328_f200 | flags, // MSR CPSR_f,#flags<<28
|
||||
0xef00_0000 | service << 16,
|
||||
0xeaff_fffe,
|
||||
]))
|
||||
.unwrap();
|
||||
reach(&mut m, ROM_START + 12, 100);
|
||||
assert_eq!(m.cpu().cpsr() >> 28, flags);
|
||||
let before = m.cpu().clone();
|
||||
finish(&mut m, ROM_START + 16);
|
||||
assert_eq!(m.cpu().cpsr(), before.cpsr());
|
||||
assert_eq!(&m.cpu().registers()[1..15], &before.registers()[1..15]);
|
||||
assert_eq!(
|
||||
m.cpu().registers()[0],
|
||||
if service == 9 {
|
||||
atan_reference(8192) as u32
|
||||
} else {
|
||||
atan2_reference(8192, 16384)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noncanonical_signed_sixteen_bit_arguments_are_repeatable_diagnostics() {
|
||||
for value in [
|
||||
0x8000,
|
||||
0xffff,
|
||||
0x10000,
|
||||
0xffff_7fff,
|
||||
0x7fff_ffff,
|
||||
0x8000_0000,
|
||||
] {
|
||||
for (service, args) in [
|
||||
(9, [value, 0, 0]),
|
||||
(0x0a, [value, 1, 0]),
|
||||
(0x0a, [1, value, 0]),
|
||||
] {
|
||||
let (mut m, _) = call(service, false, args);
|
||||
let mut failed = false;
|
||||
for _ in 0..150 {
|
||||
let before = m.cpu().clone();
|
||||
let cycles = m.cycles();
|
||||
let timing = m.last_timing();
|
||||
if let Err(error) = m.step() {
|
||||
assert!(matches!(
|
||||
error,
|
||||
MachineError::Cpu(CpuError::UnsupportedInstruction {
|
||||
instruction: INVALID_ARGUMENT_TRAP,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert_eq!(m.cpu(), &before);
|
||||
assert_eq!(m.cycles(), cycles);
|
||||
assert_eq!(m.last_timing(), timing);
|
||||
assert_eq!(m.step(), Err(error));
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(failed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_calls_restore_supervisor_stack_and_do_not_retain_ratio_state() {
|
||||
for service in [9u32, 0x0a] {
|
||||
let mut m = bios::boot(words(&[
|
||||
0xe3a0_0a02, // MOV r0,#0x2000
|
||||
0xe3a0_1901, // MOV r1,#0x4000
|
||||
0xef00_0000 | service << 16,
|
||||
0xeaff_fffb, // B ROM_START
|
||||
]))
|
||||
.unwrap();
|
||||
reach(&mut m, ROM_START, 100);
|
||||
for _ in 0..100 {
|
||||
reach(&mut m, ROM_START + 8, 10);
|
||||
m.step().unwrap();
|
||||
assert_eq!(m.cpu().registers()[13], bios::SVC_STACK);
|
||||
reach(&mut m, ROM_START + 12, 1000);
|
||||
assert_eq!(m.cpu().registers()[13], SYSTEM_STACK);
|
||||
assert_eq!(
|
||||
m.cpu().registers()[0],
|
||||
if service == 9 {
|
||||
atan_reference(8192) as u32
|
||||
} else {
|
||||
atan2_reference(8192, 16384)
|
||||
}
|
||||
);
|
||||
m.step().unwrap();
|
||||
assert_eq!(m.cpu().pc(), ROM_START);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn devices_and_dma_progress_and_pending_irq_waits_until_return() {
|
||||
for service in [9, 0x0a] {
|
||||
let (mut m, pc) = call(service, false, [8192, 16384, 0]);
|
||||
m.step().unwrap(); // Enter SWI before enabling IRQ sources.
|
||||
m.memory_mut().write16(IE, 8).unwrap();
|
||||
m.memory_mut().write16(IME, 1).unwrap();
|
||||
m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap();
|
||||
m.memory_mut().write32(SOURCE, 0x8765_4321).unwrap();
|
||||
m.memory_mut().write32(DMA_BASE, SOURCE).unwrap();
|
||||
m.memory_mut().write32(DMA_BASE + 4, SOURCE + 4).unwrap();
|
||||
m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap();
|
||||
let mut dma = 0;
|
||||
for _ in 0..1000 {
|
||||
if m.cpu().pc() == pc {
|
||||
break;
|
||||
}
|
||||
let step = m.step().unwrap();
|
||||
assert_ne!(step, StepKind::IrqEntry);
|
||||
if matches!(step, StepKind::Dma { channel: 0 }) {
|
||||
dma += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(m.cpu().pc(), pc);
|
||||
assert_eq!(dma, 1);
|
||||
assert_eq!(m.memory().read32(SOURCE + 4).unwrap(), 0x8765_4321);
|
||||
assert_eq!(m.memory().read16(IF).unwrap() & 8, 8);
|
||||
assert_eq!(m.memory().read16(IME).unwrap(), 1);
|
||||
assert_eq!(m.memory().read16(IRQ_FLAGS).unwrap(), 0);
|
||||
assert_eq!(
|
||||
m.cpu().registers()[0],
|
||||
if service == 9 {
|
||||
atan_reference(8192) as u32
|
||||
} else {
|
||||
atan2_reference(8192, 16384)
|
||||
}
|
||||
);
|
||||
assert_eq!(m.step().unwrap(), StepKind::IrqEntry);
|
||||
}
|
||||
}
|
||||
191
crates/gba-core/tests/bios/arithmetic.rs
Normal file
191
crates/gba-core/tests/bios/arithmetic.rs
Normal 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.
|
||||
}
|
||||
418
crates/gba-core/tests/bios/bit_unpack.rs
Normal file
418
crates/gba-core/tests/bios/bit_unpack.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
411
crates/gba-core/tests/bios/differential.rs
Normal file
411
crates/gba-core/tests/bios/differential.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
473
crates/gba-core/tests/bios/huffman.rs
Normal file
473
crates/gba-core/tests/bios/huffman.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
417
crates/gba-core/tests/bios/lz77.rs
Normal file
417
crates/gba-core/tests/bios/lz77.rs
Normal 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);
|
||||
}
|
||||
492
crates/gba-core/tests/bios/ram_reset.rs
Normal file
492
crates/gba-core/tests/bios/ram_reset.rs
Normal 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);
|
||||
}
|
||||
363
crates/gba-core/tests/bios/reset.rs
Normal file
363
crates/gba-core/tests/bios/reset.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
395
crates/gba-core/tests/bios/run_length.rs
Normal file
395
crates/gba-core/tests/bios/run_length.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
442
crates/gba-core/tests/bitmap_modes.rs
Normal file
442
crates/gba-core/tests/bitmap_modes.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
177
crates/gba-core/tests/core.rs
Normal file
177
crates/gba-core/tests/core.rs
Normal 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);
|
||||
}
|
||||
449
crates/gba-core/tests/display.rs
Normal file
449
crates/gba-core/tests/display.rs
Normal 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);
|
||||
}
|
||||
860
crates/gba-core/tests/dma.rs
Normal file
860
crates/gba-core/tests/dma.rs
Normal 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);
|
||||
}
|
||||
61
crates/gba-core/tests/effects.rs
Normal file
61
crates/gba-core/tests/effects.rs
Normal 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();
|
||||
}
|
||||
172
crates/gba-core/tests/effects/colors.rs
Normal file
172
crates/gba-core/tests/effects/colors.rs
Normal 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));
|
||||
}
|
||||
166
crates/gba-core/tests/effects/demo.rs
Normal file
166
crates/gba-core/tests/effects/demo.rs
Normal 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));
|
||||
}
|
||||
176
crates/gba-core/tests/effects/windows.rs
Normal file
176
crates/gba-core/tests/effects/windows.rs
Normal 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));
|
||||
}
|
||||
89
crates/gba-core/tests/exceptions.rs
Normal file
89
crates/gba-core/tests/exceptions.rs
Normal 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);
|
||||
}
|
||||
297
crates/gba-core/tests/graphics.rs
Normal file
297
crates/gba-core/tests/graphics.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
687
crates/gba-core/tests/halt.rs
Normal file
687
crates/gba-core/tests/halt.rs
Normal 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());
|
||||
}
|
||||
272
crates/gba-core/tests/machine.rs
Normal file
272
crates/gba-core/tests/machine.rs
Normal 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());
|
||||
}
|
||||
87
crates/gba-core/tests/memory.rs
Normal file
87
crates/gba-core/tests/memory.rs
Normal 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))
|
||||
);
|
||||
}
|
||||
47
crates/gba-core/tests/mosaic.rs
Normal file
47
crates/gba-core/tests/mosaic.rs
Normal 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]
|
||||
}
|
||||
212
crates/gba-core/tests/mosaic/backgrounds.rs
Normal file
212
crates/gba-core/tests/mosaic/backgrounds.rs
Normal 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));
|
||||
}
|
||||
100
crates/gba-core/tests/mosaic/demo.rs
Normal file
100
crates/gba-core/tests/mosaic/demo.rs
Normal 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);
|
||||
}
|
||||
215
crates/gba-core/tests/mosaic/objects.rs
Normal file
215
crates/gba-core/tests/mosaic/objects.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
351
crates/gba-core/tests/mosaic_tracking.rs
Normal file
351
crates/gba-core/tests/mosaic_tracking.rs
Normal 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)));
|
||||
}
|
||||
}
|
||||
38
crates/gba-core/tests/scanlines.rs
Normal file
38
crates/gba-core/tests/scanlines.rs
Normal 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()
|
||||
}
|
||||
169
crates/gba-core/tests/scanlines/capture.rs
Normal file
169
crates/gba-core/tests/scanlines/capture.rs
Normal 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());
|
||||
}
|
||||
196
crates/gba-core/tests/scanlines/machine.rs
Normal file
196
crates/gba-core/tests/scanlines/machine.rs
Normal 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))
|
||||
);
|
||||
}
|
||||
}
|
||||
155
crates/gba-core/tests/scanlines/rendering.rs
Normal file
155
crates/gba-core/tests/scanlines/rendering.rs
Normal 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]);
|
||||
}
|
||||
306
crates/gba-core/tests/sprite_budget.rs
Normal file
306
crates/gba-core/tests/sprite_budget.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
288
crates/gba-core/tests/sprite_pipeline.rs
Normal file
288
crates/gba-core/tests/sprite_pipeline.rs
Normal 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));
|
||||
}
|
||||
370
crates/gba-core/tests/sprites.rs
Normal file
370
crates/gba-core/tests/sprites.rs
Normal 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);
|
||||
}
|
||||
285
crates/gba-core/tests/sprites/affine.rs
Normal file
285
crates/gba-core/tests/sprites/affine.rs
Normal 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));
|
||||
}
|
||||
479
crates/gba-core/tests/tiles.rs
Normal file
479
crates/gba-core/tests/tiles.rs
Normal 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);
|
||||
}
|
||||
360
crates/gba-core/tests/timers.rs
Normal file
360
crates/gba-core/tests/timers.rs
Normal 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);
|
||||
}
|
||||
169
crates/gba-core/tests/timing.rs
Normal file
169
crates/gba-core/tests/timing.rs
Normal 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);
|
||||
}
|
||||
228
crates/gba-core/tests/window_tracking.rs
Normal file
228
crates/gba-core/tests/window_tracking.rs
Normal 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);
|
||||
}
|
||||
209
crates/gba-core/tests/window_tracking/horizontal.rs
Normal file
209
crates/gba-core/tests/window_tracking/horizontal.rs
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue