gameboy-rust/crates/gba-core/tests/sprite_pipeline.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

288 lines
8.9 KiB
Rust

use gba_core::{
cpu::Cpu,
display::{CYCLES_PER_FRAME as FRAME, CYCLES_PER_LINE as LINE, VBLANK_START as VB},
dma::DMA_BASE,
io::*,
machine::Machine,
memory::{Memory, OAM_START as OAM, PALETTE_START as PAL, ROM_START, VRAM_START as VRAM},
video::{rgb555_to_rgb888 as rgb, Framebuffer, VideoError, WIDTH},
};
fn scene() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_scanline_rendering(true);
m.write16(DISPCNT, 0x1040).unwrap();
m.write16(PAL, 0x7c00).unwrap();
m.write16(PAL + 0x202, 31).unwrap();
m.write16(PAL + 0x204, 0x3e0).unwrap();
for i in 0..128 {
m.write16(OAM + i * 8, 0x200).unwrap();
}
m.write16(OAM, 0).unwrap();
for offset in (0..32).step_by(2) {
m.write16(VRAM + 0x10000 + offset, 0x1111).unwrap();
}
m
}
fn finish(m: &mut Memory) -> Framebuffer {
let p = m.display_position();
m.advance_cycles(VB - u32::from(p.scanline) * LINE - u32::from(p.line_cycle));
present(m)
}
fn present(m: &Memory) -> Framebuffer {
let mut f = Framebuffer::default();
assert!(m.present_frame(&mut f).unwrap());
f
}
fn at(f: &Framebuffer, x: usize, y: usize) -> u32 {
f.pixels()[y * WIDTH + x]
}
#[test]
fn oam_position_writes_obey_preceding_lines_cycle40_boundary() {
for elapsed in [39, 40, 41] {
let mut m = scene();
m.advance_cycles(elapsed);
m.write16(OAM + 2, 16).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 0), rgb(31)); // Reset row0 was already prepared.
let x = if elapsed < 40 { 16 } else { 0 };
assert_eq!(at(&f, x, 1), rgb(31));
assert_eq!(at(&f, 16 - x, 1), rgb(0x7c00));
assert_eq!(at(&f, 16, 2), rgb(31));
}
}
#[test]
fn buffered_indices_keep_vram_history_but_resolve_current_palette() {
let mut m = scene();
m.advance_cycles(40);
for offset in (0..32).step_by(2) {
m.write16(VRAM + 0x10000 + offset, 0x2222).unwrap();
}
m.write16(PAL + 0x202, 0x7fff).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 0), rgb(0x7fff));
assert_eq!(at(&f, 0, 1), rgb(0x7fff));
assert_eq!(at(&f, 0, 2), rgb(0x3e0));
let mut snapshot = Framebuffer::default();
m.render_frame(&mut snapshot).unwrap();
assert_eq!(at(&snapshot, 0, 1), rgb(0x3e0));
assert_eq!(present(&m).pixels(), f.pixels());
}
#[test]
fn affine_matrix_and_tile_samples_are_frozen_at_preparation() {
let mut m = scene();
m.write16(OAM, 0x2100).unwrap(); // Affine, 8bpp.
m.write16(OAM + 6, 256).unwrap();
m.write16(OAM + 30, 256).unwrap();
for y in 0..8 {
for x in (0..8).step_by(2) {
m.write16(
VRAM + 0x10000 + y * 8 + x,
if x < 4 { 0x0101 } else { 0x0202 },
)
.unwrap();
}
}
m.advance_cycles(40);
m.write16(OAM + 6, 0).unwrap(); // PA0 samples the texture center for every X.
let f = finish(&mut m);
assert_eq!(at(&f, 0, 1), rgb(31));
assert_eq!(at(&f, 0, 2), rgb(0x3e0));
}
#[test]
fn semitransparency_is_buffered_but_blend_coefficients_remain_live() {
let mut m = scene();
m.write16(OAM, 0x400).unwrap();
m.advance_cycles(40);
m.write16(OAM, 0).unwrap();
m.write16(BLDCNT, 0x2000).unwrap(); // Backdrop as second target.
m.write16(BLDALPHA, 0x0808).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 1), rgb(0x3c0f));
assert_eq!(at(&f, 0, 2), rgb(31));
}
#[test]
fn obj_window_coverage_is_buffered_even_before_its_display_mask_is_enabled() {
let mut m = scene();
m.write16(OAM, 0x800).unwrap();
m.advance_cycles(40);
m.write16(OAM + 2, 16).unwrap();
m.write16(DISPCNT, 0x9040).unwrap();
m.write16(WINOUT, 0x2000).unwrap();
m.write16(BLDCNT, 0xa0).unwrap();
m.write16(BLDY, 16).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 1), rgb(0x7fff));
assert_eq!(at(&f, 16, 1), rgb(0x7c00));
assert_eq!(at(&f, 0, 2), rgb(0x7c00));
assert_eq!(at(&f, 16, 2), rgb(0x7fff));
}
#[test]
fn horizontal_mosaic_uses_current_size_and_prepared_sprite_metadata() {
let mut m = scene();
m.write16(OAM, 0x1000).unwrap();
for offset in (0..32).step_by(2) {
m.write16(VRAM + 0x10000 + offset, 0x2121).unwrap();
}
m.advance_cycles(40);
m.write16(OAM, 0).unwrap();
m.write16(MOSAIC, 0x0300).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 1, 1), rgb(31));
assert_eq!(at(&f, 1, 2), rgb(0x3e0));
}
#[test]
fn global_enable_gates_preparation_and_current_composition() {
let mut m = scene();
m.write16(DISPCNT, 0x40).unwrap();
m.advance_cycles(40);
m.write16(DISPCNT, 0x1040).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 0), rgb(0x7c00));
assert_eq!(at(&f, 0, 1), rgb(0x7c00));
assert_eq!(at(&f, 0, 2), rgb(31));
let mut m = scene();
m.advance_cycles(40);
m.write16(DISPCNT, 0x40).unwrap();
assert_eq!(at(&finish(&mut m), 0, 1), rgb(0x7c00));
}
#[test]
fn forced_blank_does_not_stop_preparation_in_this_row_model() {
let mut m = scene();
m.write16(DISPCNT, 0x10c0).unwrap();
m.advance_cycles(40);
m.write16(OAM + 2, 16).unwrap();
m.write16(DISPCNT, 0x1040).unwrap();
let f = finish(&mut m);
assert_eq!(at(&f, 0, 1), rgb(31));
assert_eq!(at(&f, 16, 2), rgb(31));
}
#[test]
fn hblank_dma_oam_changes_wait_until_a_later_preparation_event() {
let mut m = scene();
for row in 0..160 {
m.write16(0x0200_0000 + row * 2, ((row + 1) % 20 * 8) as u16)
.unwrap();
}
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, OAM + 2).unwrap();
m.write32(DMA_BASE + 8, 0xa240_0001).unwrap();
m.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), m);
machine.run_until_vblank(1000).unwrap();
let f = present(machine.memory());
for y in 0usize..8 {
let left = y.saturating_sub(1) * 8;
for x in 0..WIDTH {
assert_eq!(
at(&f, x, y),
rgb(if (left..left + 8).contains(&x) {
31
} else {
0x7c00
}),
"pixel={x},{y}"
);
}
}
assert!(machine.halted());
}
#[test]
fn final_hidden_line_prepares_row_zero_before_frame_wrap() {
for cycle in [39, 40] {
let mut m = scene();
finish(&mut m);
m.advance_cycles(227 * LINE + cycle - VB);
m.write16(OAM + 2, 16).unwrap();
m.advance_cycles(FRAME - 227 * LINE - cycle);
let f = finish(&mut m);
assert_eq!(at(&f, if cycle < 40 { 16 } else { 0 }, 0), rgb(31));
assert_eq!(at(&f, 16, 1), rgb(31));
}
}
#[test]
fn capture_toggles_preserve_prepared_rows_after_large_capture_disabled_batches() {
for frames in [0, 3] {
let mut m = scene();
m.set_scanline_rendering(false);
m.advance_cycles(frames * FRAME + 41);
m.write16(OAM + 2, 16).unwrap();
m.set_scanline_rendering(true);
m.set_scanline_rendering(false);
m.set_scanline_rendering(true);
let f = finish(&mut m);
assert_eq!(at(&f, 0, 0), rgb(31));
assert_eq!(at(&f, 0, 1), rgb(31));
assert_eq!(at(&f, 16, 2), rgb(31));
}
}
#[test]
fn prepared_errors_are_deferred_until_visible_composition_and_recover_next_frame() {
let mut m = scene();
m.advance_cycles(1); // Bootstrap row0 remains valid.
m.write16(OAM, 0xc00).unwrap();
m.advance_cycles(39); // Prepare invalid row1; clock advancement itself succeeds.
m.write16(OAM, 0).unwrap();
m.advance_cycles(VB - 40);
let mut f = Framebuffer::default();
f.clear(0x3e0);
assert!(matches!(
m.present_frame(&mut f),
Err(VideoError::UnsupportedObject { index: 0, .. })
));
assert!(f.pixels().iter().all(|&p| p == rgb(0x3e0)));
m.advance_cycles(FRAME);
assert!(m.present_frame(&mut f).unwrap());
assert_eq!(at(&f, 0, 1), rgb(31));
}
#[test]
fn hidden_preparation_errors_do_not_bypass_global_disable_or_forced_blank() {
for control in [0x40, 0x10c0] {
let mut m = scene();
m.write16(OAM, 0xc00).unwrap();
m.advance_cycles(40);
m.write16(DISPCNT, control).unwrap();
let f = finish(&mut m);
assert_eq!(
at(&f, 0, 1),
if control & 0x80 != 0 {
0xffffff
} else {
rgb(0x7c00)
}
);
}
}
#[test]
fn zero_cycle_advances_and_failed_cpu_steps_do_not_prepare_rows() {
let mut m = scene();
m.advance_cycles(0);
m.write16(OAM + 2, 16).unwrap();
m.advance_cycles(39);
m.write32(0x0300_0000, 0xf000_0000).unwrap();
let mut machine = Machine::new(Cpu::new(0x0300_0000), m);
assert!(machine.step().is_err());
assert_eq!(machine.cycles(), 39);
machine.memory_mut().write16(OAM + 2, 24).unwrap();
let f = finish(machine.memory_mut());
assert_eq!(at(&f, 16, 0), rgb(31));
assert_eq!(at(&f, 24, 1), rgb(31));
}