refactor: split into gameboy-rust workspace with core, demos, and desktop crates

Move the emulator into crates/gba-core, the original demo programs into
crates/gba-demos, and the minifb frontend into crates/desktop. The binary
is now gameboy-rust. Imports change from gba_rust to gba_core/gba_demos;
no behavior changes.

gba-core has no dependencies. Its integration tests use gba-demos as a
development dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
0xrsydn 2026-10-05 16:07:32 +07:00
commit f8a0676748
113 changed files with 304 additions and 282 deletions

View file

@ -0,0 +1,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));
}