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
|
|
@ -1,330 +0,0 @@
|
|||
use gba_rust::{
|
||||
affine_raster_demo::{AffineRasterDemo, AFFINE_RASTER_STATE, AFFINE_RASTER_TABLE},
|
||||
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},
|
||||
};
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue