feat(video): track background and sprite mosaic state
Add vertical mosaic counters and background/sprite sampling regressions.
This commit is contained in:
parent
9d14dd7cf4
commit
45b45e5c52
6 changed files with 1115 additions and 0 deletions
47
tests/mosaic.rs
Normal file
47
tests/mosaic.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
use gba_rust::{
|
||||
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
tests/mosaic/backgrounds.rs
Normal file
212
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
tests/mosaic/demo.rs
Normal file
100
tests/mosaic/demo.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
use super::*;
|
||||
use gba_rust::{
|
||||
input::{Button, Buttons},
|
||||
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_rust::{
|
||||
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
tests/mosaic/objects.rs
Normal file
215
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
tests/mosaic_tracking.rs
Normal file
351
tests/mosaic_tracking.rs
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
use gba_rust::{
|
||||
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)));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue