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
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));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue