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

@ -1,297 +0,0 @@
use gba_rust::{
graphics_demo::{GraphicsDemo, BACKGROUND, DEMO_STATE, SQUARE_SIZE},
input::{Button, Buttons},
io::{BG2PA, BG2PD, DISPCNT, GREENSWAP, KEYINPUT},
memory::{Memory, MemoryError, PALETTE_START, VRAM_START},
timing::{bus_cycles, AccessKind, AccessWidth},
video::{rgb555_to_rgb888, Framebuffer, VideoError, HEIGHT, WIDTH},
};
fn mode3() -> Memory {
let mut memory = Memory::new(vec![]).unwrap();
memory.write16(DISPCNT, 0x403).unwrap();
memory.write16(BG2PA, 256).unwrap();
memory.write16(BG2PD, 256).unwrap();
memory
}
fn assert_demo_frame(demo: &GraphicsDemo, frame: &Framebuffer, x: usize, y: usize, color: u16) {
let memory = demo.machine().memory();
assert_eq!(memory.read32(DEMO_STATE + 4).unwrap(), x as u32);
assert_eq!(memory.read32(DEMO_STATE + 8).unwrap(), y as u32);
assert_eq!(memory.read32(DEMO_STATE + 12).unwrap(), u32::from(color));
assert_eq!(memory.read16(DISPCNT).unwrap(), 0x403);
for py in 0..HEIGHT {
for px in 0..WIDTH {
let expected =
if (x..x + SQUARE_SIZE).contains(&px) && (y..y + SQUARE_SIZE).contains(&py) {
color
} else {
BACKGROUND
};
assert_eq!(
frame.pixels()[py * WIDTH + px],
rgb555_to_rgb888(expected),
"pixel {px},{py}"
);
}
}
}
#[test]
fn cpu_program_draws_mode3_and_moves_from_keyinput_without_host_vram_writes() {
let mut demo = GraphicsDemo::new().unwrap();
let mut frame = Framebuffer::default();
assert_eq!(demo.machine().memory().read16(DISPCNT).unwrap(), 0);
assert_eq!(demo.machine().memory().read16(VRAM_START).unwrap(), 0);
let startup_steps = demo.frame(Buttons::default(), &mut frame).unwrap();
assert!(startup_steps > WIDTH * HEIGHT);
assert_demo_frame(&demo, &frame, 112, 72, 0x7fff);
let previous = demo.machine().memory().display_position();
let previous_cycles = demo.machine().cycles();
assert_eq!(previous.scanline, 160);
let steps = demo
.frame(
Buttons::default()
.with(Button::Right, true)
.with(Button::Up, true),
&mut frame,
)
.unwrap();
assert!(steps < startup_steps);
let position = demo.machine().memory().display_position();
assert_eq!(position.scanline, 160);
assert_eq!(position.vblanks, previous.vblanks + 1);
assert_eq!(position.frames, previous.frames + 1);
assert_eq!(
demo.machine().cycles() - previous_cycles,
u64::from(gba_rust::display::CYCLES_PER_FRAME) + u64::from(position.line_cycle)
- u64::from(previous.line_cycle)
);
assert_demo_frame(&demo, &frame, 114, 70, 0x7fff);
assert_eq!(demo.machine().memory().read32(DEMO_STATE).unwrap(), 2);
}
#[test]
fn cpu_program_clamps_edges_cancels_opposites_changes_color_and_resets() {
let mut demo = GraphicsDemo::new().unwrap();
let mut frame = Framebuffer::default();
let upper_left = Buttons::default()
.with(Button::Left, true)
.with(Button::Up, true);
for _ in 0..65 {
demo.frame(upper_left, &mut frame).unwrap();
}
assert_demo_frame(&demo, &frame, 0, 0, 0x7fff);
let all_directions = Buttons::from_bits(0xf0);
demo.frame(all_directions, &mut frame).unwrap();
assert_demo_frame(&demo, &frame, 0, 0, 0x7fff);
let lower_right = Buttons::default()
.with(Button::Right, true)
.with(Button::Down, true);
for _ in 0..120 {
demo.frame(lower_right, &mut frame).unwrap();
}
assert_demo_frame(&demo, &frame, 224, 144, 0x7fff);
demo.frame(all_directions, &mut frame).unwrap();
assert_demo_frame(&demo, &frame, 224, 144, 0x7fff);
for (buttons, color) in [
(Buttons::from_bits(2), 0x3e0),
(Buttons::from_bits(3), 0x1f),
(Buttons::default(), 0x7fff),
] {
demo.frame(buttons, &mut frame).unwrap();
assert_demo_frame(&demo, &frame, 224, 144, color);
}
demo.frame(Buttons::default().with(Button::Start, true), &mut frame)
.unwrap();
assert_demo_frame(&demo, &frame, 112, 72, 0x7fff);
}
#[test]
fn mode3_renders_row_major_colors_and_ignores_page_and_pixel_top_bit() {
let mut memory = mode3();
let mut frame = Framebuffer::default();
for (index, color) in [
(0, 0x801f),
(1, 0x03e0),
(WIDTH, 0x7c00),
(WIDTH * HEIGHT - 1, 0x7fff),
] {
memory
.write16(VRAM_START + index as u32 * 2, color)
.unwrap();
}
memory.write16(DISPCNT, 0x413).unwrap(); // Page bit has no effect in Mode 3.
memory.render_frame(&mut frame).unwrap();
assert_eq!(frame.pixels()[0], 0xff0000);
assert_eq!(frame.pixels()[1], 0x00ff00);
assert_eq!(frame.pixels()[WIDTH], 0x0000ff);
assert_eq!(frame.pixels()[WIDTH * HEIGHT - 1], 0xffffff);
assert_eq!(frame.pixels()[2], 0);
assert_eq!(memory.cycles(), 0);
}
#[test]
fn forced_blank_is_white_and_disabled_bg2_uses_palette_backdrop() {
let mut memory = mode3();
let mut frame = Framebuffer::default();
memory.write16(PALETTE_START, 0x3e0).unwrap();
memory.write16(DISPCNT, 3).unwrap();
memory.render_frame(&mut frame).unwrap();
assert!(frame.pixels().iter().all(|&pixel| pixel == 0x00ff00));
memory.write16(DISPCNT, 0x403).unwrap();
memory.render_frame(&mut frame).unwrap();
assert!(frame.pixels().iter().all(|&pixel| pixel == 0)); // Mode 3 black is opaque.
memory.write16(DISPCNT, 0xffff).unwrap();
memory.render_frame(&mut frame).unwrap();
assert!(frame.pixels().iter().all(|&pixel| pixel == 0xffffff));
}
#[test]
fn unsupported_modes_and_layers_report_errors_without_changing_output() {
let mut memory = mode3();
let mut frame = Framebuffer::default();
frame.clear(0x001f);
for mode in [6, 7] {
memory.write16(DISPCNT, 0x400 | mode).unwrap();
assert_eq!(
memory.render_frame(&mut frame),
Err(VideoError::UnsupportedMode(mode as u8))
);
}
for layer in [0x100, 0x200, 0x800] {
memory.write16(DISPCNT, 0x403 | layer).unwrap();
assert_eq!(
memory.render_frame(&mut frame),
Err(VideoError::UnsupportedLayers(0x403 | layer))
);
}
assert!(frame.pixels().iter().all(|&pixel| pixel == 0xff0000));
}
#[test]
fn display_control_masks_cgb_bit_and_green_swap_exchanges_only_green() {
let mut memory = mode3();
memory.write32(DISPCNT, u32::MAX).unwrap();
assert_eq!(memory.read32(DISPCNT).unwrap(), 0x0001_fff7);
memory.write8(DISPCNT, 3).unwrap();
memory.write8(DISPCNT + 1, 4).unwrap();
memory.write8(GREENSWAP + 1, 0xff).unwrap();
assert_eq!(memory.read32(DISPCNT).unwrap(), 0x0001_0403);
memory.write16(VRAM_START, 0x001f).unwrap();
memory.write16(VRAM_START + 2, 0x7fe0).unwrap(); // Green + blue
let mut frame = Framebuffer::default();
memory.render_frame(&mut frame).unwrap();
assert_eq!(frame.pixels()[0], 0xffff00);
assert_eq!(frame.pixels()[1], 0x0000ff);
memory.write8(GREENSWAP, 0).unwrap();
memory.render_frame(&mut frame).unwrap();
assert_eq!(frame.pixels()[0], 0xff0000);
assert_eq!(frame.pixels()[1], 0x00ffff);
}
#[test]
fn palette_and_vram_mirrors_preserve_word_and_halfword_layouts() {
let mut memory = mode3();
memory.write32(PALETTE_START + 0x400, 0x1234_5678).unwrap();
assert_eq!(memory.read32(PALETTE_START).unwrap(), 0x1234_5678);
memory.write16(0x05ff_fffe, 0xabcd).unwrap();
assert_eq!(memory.read16(PALETTE_START + 0x3fe).unwrap(), 0xabcd);
for (mirror, physical) in [
(0x0602_0000, VRAM_START),
(0x0601_8000, VRAM_START + 0x10000),
(0x06ff_fffc, VRAM_START + 0x17ffc),
] {
memory.write32(mirror, 0x1234_5678).unwrap();
assert_eq!(memory.read32(physical).unwrap(), 0x1234_5678);
assert_eq!(memory.read8(mirror).unwrap(), 0x78);
assert_eq!(memory.read16(mirror + 2).unwrap(), 0x1234);
}
memory.write32(VRAM_START + 0x17ffc, 0xabcd_1234).unwrap();
assert_eq!(memory.read32(VRAM_START + 0x1fffc).unwrap(), 0xabcd_1234);
assert_eq!(memory.read32(0x0700_0000).unwrap(), 0); // OAM starts cleared.
}
#[test]
fn video_byte_writes_duplicate_bg_and_palette_but_ignore_obj_area() {
let mut memory = mode3();
for address in [
PALETTE_START,
PALETTE_START + 0x3ff,
VRAM_START,
VRAM_START + 1,
VRAM_START + 0x13fff,
] {
memory.write8(address, 0x5a).unwrap();
assert_eq!(memory.read16(address & !1).unwrap(), 0x5a5a);
}
for address in [
VRAM_START + 0x14000,
VRAM_START + 0x17fff,
VRAM_START + 0x1ffff,
] {
memory.write16(address & !1, 0x1234).unwrap();
memory.write8(address, 0xff).unwrap();
assert_eq!(memory.read16(address & !1).unwrap(), 0x1234);
}
memory.write16(DISPCNT, 0).unwrap(); // Tile-mode OBJ area starts at 64 KiB.
memory.write16(VRAM_START + 0x10000, 0xabcd).unwrap();
memory.write8(VRAM_START + 0x18000, 0).unwrap();
assert_eq!(memory.read16(VRAM_START + 0x10000).unwrap(), 0xabcd);
}
#[test]
fn misaligned_video_writes_leave_all_bytes_unchanged() {
let mut memory = mode3();
for address in [PALETTE_START, VRAM_START] {
memory.write32(address, 0x1234_5678).unwrap();
assert_eq!(
memory.write16(address + 1, 0),
Err(MemoryError::Unaligned(address + 1))
);
assert_eq!(
memory.write32(address + 2, 0),
Err(MemoryError::Unaligned(address + 2))
);
assert_eq!(memory.read32(address).unwrap(), 0x1234_5678);
}
}
#[test]
fn keyinput_exposes_all_ten_buttons_active_low_and_ignores_writes() {
let mut memory = mode3();
assert_eq!(memory.read16(KEYINPUT).unwrap(), 0x3ff);
for bits in 0..=0x3ff {
memory.set_buttons(Buttons::from_bits(bits));
assert_eq!(memory.read16(KEYINPUT).unwrap(), !bits & 0x3ff);
memory.write16(KEYINPUT, 0).unwrap();
assert_eq!(memory.read16(KEYINPUT).unwrap(), !bits & 0x3ff);
}
let buttons = Buttons::from_bits(0xffff)
.with(Button::A, false)
.with(Button::L, false);
assert_eq!(buttons.bits(), 0x1fe);
memory.set_buttons(buttons);
memory.write8(KEYINPUT + 1, 0).unwrap();
assert_eq!(memory.read8(KEYINPUT).unwrap(), 1);
assert_eq!(memory.read8(KEYINPUT + 1).unwrap(), 2);
memory.set_buttons(Buttons::default());
assert_eq!(memory.read16(KEYINPUT).unwrap(), 0x3ff);
assert_eq!(memory.cycles(), 0);
assert_eq!(
memory.read16(KEYINPUT + 2),
Err(MemoryError::Unmapped(KEYINPUT + 2))
); // KEYCNT deferred.
}
#[test]
fn video_bus_widths_have_nominal_16_bit_bus_costs() {
for address in [PALETTE_START, VRAM_START, 0x06ff_fffc] {
for kind in [AccessKind::Sequential, AccessKind::NonSequential] {
assert_eq!(bus_cycles(0, address, AccessWidth::Byte, kind), 1);
assert_eq!(bus_cycles(0, address, AccessWidth::Halfword, kind), 1);
assert_eq!(bus_cycles(0, address, AccessWidth::Word, kind), 2);
}
}
}