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
169
crates/gba-core/tests/scanlines/capture.rs
Normal file
169
crates/gba-core/tests/scanlines/capture.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn capture_is_optional_and_snapshot_api_remains_independent() {
|
||||
let mut m = Memory::new(vec![]).unwrap();
|
||||
m.write16(PAL, 31).unwrap();
|
||||
m.advance_cycles(CYCLES_PER_FRAME * 2);
|
||||
let mut f = Framebuffer::default();
|
||||
f.clear(0x3e0);
|
||||
assert!(!m.present_frame(&mut f).unwrap());
|
||||
assert!(f.pixels().iter().all(|&p| p == rgb(0x3e0)));
|
||||
assert_eq!(m.captured_vblank(), None);
|
||||
m.render_frame(&mut f).unwrap();
|
||||
assert!(f.pixels().iter().all(|&p| p == rgb(31)));
|
||||
let cycles = m.cycles();
|
||||
m.set_scanline_rendering(true);
|
||||
assert_eq!(m.cycles(), cycles);
|
||||
m.advance_cycles(VBLANK_START);
|
||||
assert_eq!(m.captured_vblank(), Some(3));
|
||||
assert_eq!(present(&m).pixels(), f.pixels());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hblank_captures_old_row_and_later_palette_writes_do_not_recolor_it() {
|
||||
let mut m = memory();
|
||||
m.write16(PAL, 31).unwrap();
|
||||
m.advance_cycles(HBLANK_START);
|
||||
m.write16(PAL, 0x3e0).unwrap();
|
||||
m.advance_cycles(VBLANK_START - HBLANK_START);
|
||||
let f = present(&m);
|
||||
for y in 0..HEIGHT {
|
||||
assert!(f.pixels()[y * WIDTH..(y + 1) * WIDTH]
|
||||
.iter()
|
||||
.all(|&p| p == rgb(if y == 0 { 31 } else { 0x3e0 })));
|
||||
}
|
||||
m.write16(PAL, 0x7c00).unwrap();
|
||||
assert_eq!(present(&m).pixels(), f.pixels());
|
||||
let cycles = m.cycles();
|
||||
let mut snapshot = Framebuffer::default();
|
||||
m.render_frame(&mut snapshot).unwrap();
|
||||
assert!(snapshot.pixels().iter().all(|&p| p == rgb(0x7c00)));
|
||||
assert_eq!(m.cycles(), cycles);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_row_capture_does_not_publish_before_vblank_entry() {
|
||||
let mut m = memory();
|
||||
m.write16(PAL, 31).unwrap();
|
||||
let mut f = Framebuffer::default();
|
||||
f.clear(0x3e0);
|
||||
m.advance_cycles(159 * CYCLES_PER_LINE + HBLANK_START);
|
||||
assert!(!m.present_frame(&mut f).unwrap());
|
||||
m.advance_cycles(CYCLES_PER_LINE - HBLANK_START - 1);
|
||||
assert!(!m.present_frame(&mut f).unwrap());
|
||||
assert!(f.pixels().iter().all(|&p| p == rgb(0x3e0)));
|
||||
m.advance_cycles(1);
|
||||
assert!(m.present_frame(&mut f).unwrap());
|
||||
assert_eq!(m.captured_vblank(), Some(1));
|
||||
assert!(f.pixels().iter().all(|&p| p == rgb(31)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enabling_mid_frame_discards_partial_frame_and_repeated_enable_preserves_progress() {
|
||||
for start in [HBLANK_START, 40 * CYCLES_PER_LINE, VBLANK_START - 1] {
|
||||
let mut m = Memory::new(vec![]).unwrap();
|
||||
m.advance_cycles(start);
|
||||
m.set_scanline_rendering(true);
|
||||
m.write16(PAL, 31).unwrap();
|
||||
m.advance_cycles(VBLANK_START - start);
|
||||
assert_eq!(m.captured_vblank(), None);
|
||||
m.advance_cycles(CYCLES_PER_FRAME - VBLANK_START + HBLANK_START);
|
||||
m.set_scanline_rendering(true); // Must not reset row0.
|
||||
m.advance_cycles(VBLANK_START - HBLANK_START);
|
||||
assert_eq!(m.captured_vblank(), Some(2));
|
||||
assert!(present(&m).pixels().iter().all(|&p| p == rgb(31)));
|
||||
m.set_scanline_rendering(false);
|
||||
assert_eq!(m.captured_vblank(), None);
|
||||
assert!(!m.present_frame(&mut Framebuffer::default()).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enable_before_first_hblank_and_during_vblank_can_capture_next_complete_image() {
|
||||
for start in [0, HBLANK_START - 1, VBLANK_START, CYCLES_PER_FRAME - 1] {
|
||||
let mut m = Memory::new(vec![]).unwrap();
|
||||
m.advance_cycles(start);
|
||||
m.set_scanline_rendering(true);
|
||||
let target = if start < HBLANK_START {
|
||||
VBLANK_START
|
||||
} else {
|
||||
CYCLES_PER_FRAME + VBLANK_START
|
||||
};
|
||||
m.advance_cycles(target - start);
|
||||
assert_eq!(
|
||||
m.captured_vblank(),
|
||||
Some(if start < HBLANK_START { 1 } else { 2 })
|
||||
);
|
||||
assert!(m.present_frame(&mut Framebuffer::default()).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_batches_publish_latest_frame_and_zero_cycles_have_no_effect() {
|
||||
let mut m = memory();
|
||||
m.write16(PAL, 0x4210).unwrap();
|
||||
m.advance_cycles(CYCLES_PER_FRAME * 3 + VBLANK_START);
|
||||
assert_eq!(m.captured_vblank(), Some(4));
|
||||
let before = m.display_position();
|
||||
let f = present(&m);
|
||||
m.advance_cycles(0);
|
||||
m.write16(PAL, 0).unwrap();
|
||||
assert_eq!(m.display_position(), before);
|
||||
assert_eq!(present(&m).pixels(), f.pixels());
|
||||
assert!(f.pixels().iter().all(|&p| p == rgb(0x4210)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_render_error_is_reported_at_publication_without_partial_output_then_recovers() {
|
||||
let mut m = memory();
|
||||
m.write16(PAL, 31).unwrap();
|
||||
m.advance_cycles(VBLANK_START);
|
||||
let mut f = present(&m);
|
||||
m.advance_cycles(CYCLES_PER_FRAME - VBLANK_START);
|
||||
m.write16(DISPCNT, 6).unwrap();
|
||||
m.advance_cycles(HBLANK_START);
|
||||
m.write16(DISPCNT, 7).unwrap();
|
||||
m.advance_cycles(CYCLES_PER_LINE);
|
||||
m.write16(DISPCNT, 0).unwrap();
|
||||
assert!(m.present_frame(&mut f).unwrap()); // Previous frame remains available while drawing.
|
||||
m.advance_cycles(VBLANK_START - HBLANK_START - CYCLES_PER_LINE);
|
||||
assert_eq!(m.captured_vblank(), Some(2));
|
||||
assert_eq!(m.present_frame(&mut f), Err(VideoError::UnsupportedMode(6)));
|
||||
assert!(f.pixels().iter().all(|&p| p == rgb(31)));
|
||||
m.write16(PAL, 0x3e0).unwrap();
|
||||
m.advance_cycles(CYCLES_PER_FRAME);
|
||||
assert_eq!(m.captured_vblank(), Some(3));
|
||||
assert!(m.present_frame(&mut f).unwrap());
|
||||
assert!(f.pixels().iter().all(|&p| p == rgb(0x3e0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_clock_advance_matches_single_cycle_reference_and_disabled_capture_devices() {
|
||||
let mut bulk = memory();
|
||||
let mut single = memory();
|
||||
let mut disabled = Memory::new(vec![]).unwrap();
|
||||
for m in [&mut bulk, &mut single, &mut disabled] {
|
||||
m.write16(DISPSTAT, 0x5038).unwrap();
|
||||
m.write32(TIMER_BASE, 0x00c0_fff1).unwrap();
|
||||
m.write16(IE, 15).unwrap();
|
||||
m.write8(HALTCNT, 0).unwrap();
|
||||
m.write16(PAL, 0x4210).unwrap();
|
||||
}
|
||||
let cycles = CYCLES_PER_FRAME + VBLANK_START + 73;
|
||||
bulk.advance_cycles(cycles);
|
||||
disabled.advance_cycles(cycles);
|
||||
for _ in 0..cycles {
|
||||
single.advance_cycles(1);
|
||||
}
|
||||
for m in [&single, &disabled] {
|
||||
assert_eq!(bulk.display_position(), m.display_position());
|
||||
assert_eq!(bulk.cycles(), m.cycles());
|
||||
assert_eq!(bulk.halted(), m.halted());
|
||||
for register in [DISPSTAT, IF, TIMER_BASE] {
|
||||
assert_eq!(bulk.read16(register), m.read16(register));
|
||||
}
|
||||
}
|
||||
assert_eq!(present(&bulk).pixels(), present(&single).pixels());
|
||||
assert_eq!(bulk.captured_vblank(), single.captured_vblank());
|
||||
}
|
||||
196
crates/gba-core/tests/scanlines/machine.rs
Normal file
196
crates/gba-core/tests/scanlines/machine.rs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hblank_dma_updates_next_row_while_cpu_stays_halted() {
|
||||
let mut m = memory();
|
||||
m.write16(PAL, 31).unwrap();
|
||||
for i in 0..160 {
|
||||
m.write16(0x0200_0000 + i * 2, ((i + 1) as u16 & 31) << 5)
|
||||
.unwrap();
|
||||
}
|
||||
m.write32(DMA_BASE, 0x0200_0000).unwrap();
|
||||
m.write32(DMA_BASE + 4, PAL).unwrap();
|
||||
m.write32(DMA_BASE + 8, 0xa240_0001).unwrap();
|
||||
m.write8(HALTCNT, 0).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), m);
|
||||
let cpu = machine.cpu().clone();
|
||||
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
|
||||
assert_eq!(
|
||||
machine.memory().display_position().line_cycle,
|
||||
HBLANK_START as u16
|
||||
);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
|
||||
machine.run_until_vblank(1000).unwrap();
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert!(machine.halted());
|
||||
let f = present(machine.memory());
|
||||
for y in 0..HEIGHT {
|
||||
let color = if y == 0 { 31 } else { (y as u16 & 31) << 5 };
|
||||
assert!(f.pixels()[y * WIDTH..(y + 1) * WIDTH]
|
||||
.iter()
|
||||
.all(|&p| p == rgb(color)));
|
||||
}
|
||||
assert_eq!(machine.memory().read16(PAL).unwrap(), 0); // Row159 HBlank DMA ran after its capture.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vblank_dma_cannot_change_the_frame_just_published() {
|
||||
let mut m = memory();
|
||||
m.write16(PAL, 31).unwrap();
|
||||
m.write16(0x0200_0000, 0x3e0).unwrap();
|
||||
m.write32(DMA_BASE, 0x0200_0000).unwrap();
|
||||
m.write32(DMA_BASE + 4, PAL).unwrap();
|
||||
m.write32(DMA_BASE + 8, 0x9000_0001).unwrap();
|
||||
m.advance_cycles(VBLANK_START);
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), m);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
|
||||
assert!(present(machine.memory())
|
||||
.pixels()
|
||||
.iter()
|
||||
.all(|&p| p == rgb(31)));
|
||||
machine.memory_mut().advance_cycles(CYCLES_PER_FRAME);
|
||||
assert!(present(machine.memory())
|
||||
.pixels()
|
||||
.iter()
|
||||
.all(|&p| p == rgb(0x3e0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_polling_vcount_can_split_the_frame_without_host_video_writes() {
|
||||
let rom = word_rom(&[
|
||||
0xe3a0_0301, // MOV r0,#0x04000000
|
||||
0xe1d0_10b6, // LDRH r1,[r0,#6] (VCOUNT)
|
||||
0xe351_0050, // CMP r1,#80
|
||||
0x1aff_fffc, // BNE poll
|
||||
0xe3a0_2405, // MOV r2,#0x05000000
|
||||
0xe3a0_3c7c, // MOV r3,#0x7c00
|
||||
0xe1c2_30b0, // STRH r3,[r2]
|
||||
0xeaff_fffe,
|
||||
]);
|
||||
let mut m = Memory::new(rom).unwrap();
|
||||
m.set_scanline_rendering(true);
|
||||
m.write16(PAL, 31).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), m);
|
||||
machine.run_until_vblank(100_000).unwrap();
|
||||
let f = present(machine.memory());
|
||||
for y in 0..HEIGHT {
|
||||
let color = if y < 80 { 31 } else { 0x7c00 };
|
||||
assert!(f.pixels()[y * WIDTH..(y + 1) * WIDTH]
|
||||
.iter()
|
||||
.all(|&p| p == rgb(color)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instruction_crossing_hblank_uses_committed_write_for_entire_row() {
|
||||
let rom = word_rom(&[
|
||||
0xe3a0_0405, // MOV r0,#0x05000000
|
||||
0xe3a0_1e3e, // MOV r1,#0x3e0
|
||||
0xe1c0_10b0, // STRH r1,[r0]
|
||||
]);
|
||||
let mut m = Memory::new(rom).unwrap();
|
||||
m.set_scanline_rendering(true);
|
||||
m.write16(PAL, 31).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), m);
|
||||
machine.step().unwrap();
|
||||
machine.step().unwrap();
|
||||
let elapsed = machine.cycles() as u32;
|
||||
machine
|
||||
.memory_mut()
|
||||
.advance_cycles(HBLANK_START - 1 - elapsed);
|
||||
machine.step().unwrap();
|
||||
assert!(machine.cycles() > u64::from(HBLANK_START));
|
||||
let remaining = VBLANK_START - machine.cycles() as u32;
|
||||
machine.memory_mut().advance_cycles(remaining);
|
||||
// This pins the nominal whole-instruction policy, not pixel-accurate timing.
|
||||
assert_eq!(at(&present(machine.memory()), 0, 0), rgb(0x3e0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_cpu_step_does_not_advance_or_discard_capture_progress() {
|
||||
let mut m = Memory::new(word_rom(&[0xf000_0000])).unwrap();
|
||||
m.set_scanline_rendering(true);
|
||||
m.write16(PAL, 31).unwrap();
|
||||
m.advance_cycles(HBLANK_START);
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), m);
|
||||
let before = machine.memory().display_position();
|
||||
let cycles = machine.cycles();
|
||||
assert!(machine.step().is_err());
|
||||
assert_eq!(machine.cycles(), cycles);
|
||||
assert_eq!(machine.memory().display_position(), before);
|
||||
machine.memory_mut().write16(PAL, 0x3e0).unwrap();
|
||||
machine
|
||||
.memory_mut()
|
||||
.advance_cycles(VBLANK_START - HBLANK_START);
|
||||
let f = present(machine.memory());
|
||||
assert_eq!(at(&f, 0, 0), rgb(31));
|
||||
assert_eq!(at(&f, 0, 1), rgb(0x3e0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_raster_demo_copies_table_and_rearms_hblank_dma_across_frames() {
|
||||
let mut demo = RasterDemo::new().unwrap();
|
||||
let mut f = Framebuffer::default();
|
||||
assert_eq!(demo.machine().memory().read16(RASTER_TABLE).unwrap(), 0);
|
||||
for update in 1..=40 {
|
||||
demo.frame(Buttons::default().with(Button::Right, true), &mut f)
|
||||
.unwrap();
|
||||
let phase = update % 32;
|
||||
let m = demo.machine().memory();
|
||||
assert_eq!(m.read32(RASTER_STATE).unwrap(), update);
|
||||
assert_eq!(m.read32(RASTER_STATE + 4).unwrap(), phase);
|
||||
assert_eq!(m.read16(DMA_BASE + 10).unwrap(), 0xa240);
|
||||
assert_eq!(m.captured_vblank(), Some(m.display_position().vblanks));
|
||||
assert_eq!(m.display_position().scanline, 160);
|
||||
for y in 0..HEIGHT {
|
||||
let n = (y as u16 + phase as u16) % 32;
|
||||
let color = n + (31 - n) * 32 + (n / 2) * 1024;
|
||||
assert!(
|
||||
f.pixels()[y * WIDTH..(y + 1) * WIDTH]
|
||||
.iter()
|
||||
.all(|&p| p == rgb(color)),
|
||||
"update={update} row={y}"
|
||||
);
|
||||
}
|
||||
let mut snapshot = Framebuffer::default();
|
||||
m.render_frame(&mut snapshot).unwrap();
|
||||
assert_ne!(snapshot.pixels(), f.pixels()); // Same memory, but no row history in a snapshot.
|
||||
}
|
||||
assert_eq!(
|
||||
demo.machine().memory().read16(RASTER_TABLE).unwrap(),
|
||||
31 * 32
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raster_controls_wrap_cancel_opposites_and_reset() {
|
||||
let mut demo = RasterDemo::new().unwrap();
|
||||
let mut f = Framebuffer::default();
|
||||
for (buttons, phase) in [
|
||||
(Buttons::default().with(Button::Left, true), 31),
|
||||
(
|
||||
Buttons::default()
|
||||
.with(Button::Left, true)
|
||||
.with(Button::Right, true),
|
||||
31,
|
||||
),
|
||||
(Buttons::default().with(Button::Right, true), 0),
|
||||
(Buttons::default().with(Button::Right, true), 1),
|
||||
(
|
||||
Buttons::default()
|
||||
.with(Button::Start, true)
|
||||
.with(Button::Right, true),
|
||||
0,
|
||||
),
|
||||
] {
|
||||
demo.frame(buttons, &mut f).unwrap();
|
||||
assert_eq!(
|
||||
demo.machine().memory().read32(RASTER_STATE + 4).unwrap(),
|
||||
phase
|
||||
);
|
||||
assert_eq!(
|
||||
at(&f, 0, 0),
|
||||
rgb(phase as u16 | ((31 - phase as u16) << 5) | ((phase as u16 / 2) << 10))
|
||||
);
|
||||
}
|
||||
}
|
||||
155
crates/gba-core/tests/scanlines/rendering.rs
Normal file
155
crates/gba-core/tests/scanlines/rendering.rs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn static_capture_matches_snapshots_in_every_mode_with_objects_mosaic_windows_and_effects() {
|
||||
for mode in 0..=5 {
|
||||
let mut m = memory();
|
||||
for i in 0..512 {
|
||||
m.write16(PAL + i * 2, (i as u16).wrapping_mul(0x421))
|
||||
.unwrap();
|
||||
}
|
||||
for i in 0..128 {
|
||||
m.write16(OAM + i * 8, 0x200).unwrap();
|
||||
}
|
||||
for offset in (0..0x14000).step_by(2) {
|
||||
let value = if mode < 3 {
|
||||
if offset < 0x4000 {
|
||||
0x1111
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
(offset as u16).wrapping_mul(17)
|
||||
};
|
||||
m.write16(VRAM + offset, value).unwrap();
|
||||
}
|
||||
m.write16(DISPCNT, 0x3440 | mode).unwrap();
|
||||
m.write16(BG2CNT, 0x1041).unwrap();
|
||||
m.write16(BG2PA, 181).unwrap();
|
||||
m.write16(BG2PB, 91).unwrap();
|
||||
m.write16(BG2PC, (-91i16) as u16).unwrap();
|
||||
m.write16(BG2PD, 256).unwrap();
|
||||
m.write32(BG2X, 513).unwrap();
|
||||
m.write32(BG2Y, 7 * 256).unwrap();
|
||||
m.write16(OAM, 0x1003).unwrap();
|
||||
m.write16(OAM + 2, 5).unwrap();
|
||||
m.write16(OAM + 4, if mode >= 3 { 512 } else { 0 }).unwrap();
|
||||
let tiles = if mode >= 3 { 0x14000 } else { 0x10000 };
|
||||
for offset in (0..32).step_by(2) {
|
||||
m.write16(VRAM + tiles + offset, 0x1111).unwrap();
|
||||
}
|
||||
m.write16(MOSAIC, 0x3243).unwrap();
|
||||
m.write16(WIN0H, 0x0a3c).unwrap();
|
||||
m.write16(WIN0V, 0x0432).unwrap();
|
||||
m.write16(WININ, 0x1f).unwrap();
|
||||
m.write16(WINOUT, 0x3f).unwrap();
|
||||
m.write16(BLDCNT, 0xbf).unwrap();
|
||||
m.write16(BLDY, 5).unwrap();
|
||||
m.write16(GREENSWAP, 1).unwrap();
|
||||
let mut expected = Framebuffer::default();
|
||||
m.render_frame(&mut expected).unwrap();
|
||||
m.advance_cycles(VBLANK_START);
|
||||
assert_eq!(present(&m).pixels(), expected.pixels(), "mode={mode}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_palette_vram_and_oam_writes_only_change_rows_not_yet_captured() {
|
||||
let mut m = memory();
|
||||
m.write16(DISPCNT, 0x404).unwrap();
|
||||
m.write16(BG2PA, 256).unwrap();
|
||||
m.write16(BG2PD, 256).unwrap();
|
||||
for i in 0..128 {
|
||||
m.write16(OAM + i * 8, 0x200).unwrap();
|
||||
}
|
||||
for offset in (0..38400).step_by(2) {
|
||||
m.write16(VRAM + offset, 0x0101).unwrap();
|
||||
m.write16(VRAM + 0xa000 + offset, 0x0202).unwrap();
|
||||
}
|
||||
for (index, color) in [(1, 31), (2, 0x3e0), (3, 0x7fff)] {
|
||||
m.write16(PAL + index * 2, color).unwrap();
|
||||
}
|
||||
m.advance_cycles(HBLANK_START); // Row0 page0 red.
|
||||
m.write16(DISPCNT, 0x414).unwrap();
|
||||
m.advance_cycles(CYCLES_PER_LINE); // Row1 page1 green.
|
||||
m.write16(PAL + 4, 0x7c00).unwrap();
|
||||
m.advance_cycles(CYCLES_PER_LINE); // Row2 same index, now blue.
|
||||
m.write16(VRAM + 0xa000 + 3 * 240, 0x0303).unwrap();
|
||||
m.advance_cycles(CYCLES_PER_LINE); // Row3 first two pixels now white.
|
||||
m.write16(DISPCNT, 0x1454).unwrap();
|
||||
m.write16(OAM, 0).unwrap();
|
||||
m.write16(OAM + 4, 512).unwrap();
|
||||
for offset in (0..32).step_by(2) {
|
||||
m.write16(VRAM + 0x14000 + offset, 0x1111).unwrap();
|
||||
}
|
||||
m.write16(PAL + 0x202, 0).unwrap(); // Opaque black OBJ.
|
||||
m.advance_cycles(VBLANK_START - (3 * CYCLES_PER_LINE + HBLANK_START));
|
||||
let f = present(&m);
|
||||
// Row4's sprites were prepared before the row3 HBlank writes; row5 sees them.
|
||||
for (y, color) in [
|
||||
(0, 31),
|
||||
(1, 0x3e0),
|
||||
(2, 0x7c00),
|
||||
(3, 0x7fff),
|
||||
(4, 0x7c00),
|
||||
(5, 0),
|
||||
] {
|
||||
assert_eq!(at(&f, 0, y), rgb(color));
|
||||
}
|
||||
assert_eq!(at(&f, 8, 4), rgb(0x7c00));
|
||||
assert_eq!(at(&f, 0, 8), rgb(0x7c00));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_mosaic_window_effect_and_forced_blank_changes_apply_per_row() {
|
||||
let mut m = memory();
|
||||
m.write16(DISPCNT, 0x100).unwrap();
|
||||
m.write16(BG0CNT, 0x1000).unwrap();
|
||||
m.write16(PAL, 0x4210).unwrap();
|
||||
m.write16(PAL + 2, 31).unwrap();
|
||||
m.write16(PAL + 4, 0x3e0).unwrap();
|
||||
for offset in (0..32).step_by(2) {
|
||||
m.write16(VRAM + offset, 0x2121).unwrap();
|
||||
}
|
||||
m.advance_cycles(HBLANK_START);
|
||||
m.write16(BG0HOFS, 1).unwrap();
|
||||
m.advance_cycles(CYCLES_PER_LINE);
|
||||
m.write16(BG0CNT, 0x1040).unwrap();
|
||||
m.write16(MOSAIC, 1).unwrap();
|
||||
m.advance_cycles(CYCLES_PER_LINE);
|
||||
m.write16(DISPCNT, 0x2100).unwrap();
|
||||
m.write16(WIN0H, 1).unwrap();
|
||||
// Set the next row's top edge; a missed row-zero edge cannot open the window.
|
||||
m.write16(WIN0V, 0x03a0).unwrap();
|
||||
m.write16(WININ, 0).unwrap();
|
||||
m.write16(WINOUT, 0x21).unwrap();
|
||||
m.write16(BLDCNT, 0xc1).unwrap();
|
||||
m.write16(BLDY, 8).unwrap();
|
||||
m.advance_cycles(CYCLES_PER_LINE);
|
||||
m.write16(DISPCNT, 0x80).unwrap();
|
||||
m.advance_cycles(VBLANK_START - (3 * CYCLES_PER_LINE + HBLANK_START));
|
||||
let f = present(&m);
|
||||
assert_eq!(&f.pixels()[..2], &[rgb(31), rgb(0x3e0)]);
|
||||
assert_eq!(&f.pixels()[WIDTH..WIDTH + 2], &[rgb(0x3e0), rgb(31)]);
|
||||
assert_eq!(&f.pixels()[2 * WIDTH..2 * WIDTH + 2], &[rgb(0x3e0); 2]);
|
||||
assert_eq!(at(&f, 0, 3), rgb(0x4210));
|
||||
assert_eq!(at(&f, 1, 3), rgb(0x200));
|
||||
assert!(f.pixels()[4 * WIDTH..].iter().all(|&p| p == 0xffffff));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn green_swap_setting_is_captured_per_row_after_color_composition() {
|
||||
let mut m = memory();
|
||||
m.write16(DISPCNT, 0x403).unwrap();
|
||||
m.write16(BG2PA, 256).unwrap();
|
||||
m.write16(BG2PD, 256).unwrap();
|
||||
for row in 0..2 {
|
||||
m.write32(VRAM + row * 480, 0x03e0_001f).unwrap();
|
||||
}
|
||||
m.advance_cycles(HBLANK_START);
|
||||
m.write16(GREENSWAP, 1).unwrap();
|
||||
m.advance_cycles(VBLANK_START - HBLANK_START);
|
||||
let f = present(&m);
|
||||
assert_eq!(&f.pixels()[..2], &[0xff0000, 0xff00]);
|
||||
assert_eq!(&f.pixels()[WIDTH..WIDTH + 2], &[0xffff00, 0]);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue