gameboy-rust/crates/gba-core/tests/mosaic/backgrounds.rs
0xrsydn f8a0676748 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>
2026-10-05 16:09:46 +07:00

212 lines
7.3 KiB
Rust

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