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
172
crates/gba-core/tests/effects/colors.rs
Normal file
172
crates/gba-core/tests/effects/colors.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn alpha_uses_five_bit_channels_clamps_coefficients_and_saturates_sums() {
|
||||
let mut m = setup();
|
||||
m.write16(BLDCNT, 0x0241).unwrap(); // BG0 with BG1.
|
||||
for (coefficients, expected) in [
|
||||
(0, 0),
|
||||
(0x0808, 0x01ef),
|
||||
(0x0010, 31),
|
||||
(0x1000, 0x3e0),
|
||||
(0x1f1f, 0x3ff),
|
||||
] {
|
||||
m.write16(BLDALPHA, coefficients).unwrap();
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(expected));
|
||||
}
|
||||
m.write16(PAL + 2, 0x7fff).unwrap();
|
||||
m.write16(PAL + 4, 0x7fff).unwrap();
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(0x7fff));
|
||||
m.write16(PAL + 2, 1).unwrap();
|
||||
m.write16(PAL + 4, 1).unwrap();
|
||||
m.write16(BLDALPHA, 0x0808).unwrap();
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(1)); // Sum fractional terms before rounding.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alpha_requires_top_first_target_and_immediate_second_target() {
|
||||
let mut m = setup();
|
||||
m.write16(BLDALPHA, 0x0808).unwrap();
|
||||
for control in [0x0441, 0x0242, 0x0142, 0x2041, 0x0240] {
|
||||
m.write16(BLDCNT, control).unwrap();
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(31), "control={control:x}");
|
||||
}
|
||||
m.write16(BLDCNT, 0x0441).unwrap();
|
||||
m.write16(VRAM + 0x4000, 0).unwrap(); // Transparent BG1 exposes BG2.
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(0x3c0f));
|
||||
assert_eq!(pixel(&m, 4, 0), rgb(31));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_remove_layers_before_selecting_blend_targets() {
|
||||
let mut m = setup();
|
||||
m.write16(BLDALPHA, 0x0808).unwrap();
|
||||
m.write16(BLDCNT, 0x0441).unwrap();
|
||||
window(&mut m, 0x0008, 0x0008, 0x25, 0x3f); // Hide BG1 only inside.
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(0x3c0f));
|
||||
assert_eq!(pixel(&m, 8, 0), rgb(31));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn brightness_uses_selected_top_layer_and_clamped_write_only_coefficient() {
|
||||
let mut m = setup();
|
||||
m.write16(PAL + 2, 0x4210).unwrap();
|
||||
for (control, coefficient, expected) in [
|
||||
(0x81, 0, 0x4210),
|
||||
(0x81, 8, 0x5ef7),
|
||||
(0x81, 31, 0x7fff),
|
||||
(0xc1, 8, 0x2108),
|
||||
(0xc1, 16, 0),
|
||||
(0xc1, 31, 0),
|
||||
(0x82, 16, 0x4210),
|
||||
(0x01, 16, 0x4210),
|
||||
] {
|
||||
m.write16(BLDCNT, control).unwrap();
|
||||
m.write32(BLDY, coefficient).unwrap();
|
||||
assert_eq!(m.read16(BLDY).unwrap(), 0);
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(expected));
|
||||
}
|
||||
m.write16(BLDCNT, 0x81).unwrap();
|
||||
m.write8(BLDY, 8).unwrap();
|
||||
m.write8(BLDY + 1, 0xff).unwrap();
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(0x5ef7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backdrop_is_a_target_but_cannot_blend_with_itself() {
|
||||
let mut m = setup();
|
||||
m.write16(DISPCNT, 0).unwrap();
|
||||
m.write16(BLDCNT, 0x2060).unwrap();
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(0x4210));
|
||||
m.write16(BLDCNT, 0xa0).unwrap();
|
||||
m.write16(BLDY, 16).unwrap();
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(0x7fff));
|
||||
m.write16(DISPCNT, 0x100).unwrap();
|
||||
m.write16(BLDCNT, 0x2041).unwrap();
|
||||
m.write16(BLDALPHA, 0x1000).unwrap();
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(0x4210)); // Backdrop is not pre-brightened.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_can_be_first_or_second_target_but_never_blends_with_another_object() {
|
||||
let mut m = setup();
|
||||
m.write16(DISPCNT, 0x1100).unwrap();
|
||||
object(&mut m, 0, 0, 0, 0);
|
||||
object(&mut m, 1, 0, 0, 0xc00); // Lower priority; never a second OBJ target.
|
||||
m.write16(BLDALPHA, 0x0808).unwrap();
|
||||
m.write16(BLDCNT, 0x0150).unwrap();
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(0x3dff)); // White OBJ + red BG0.
|
||||
m.write16(BLDCNT, 0x1050).unwrap();
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(0x7fff)); // No OBJ-to-OBJ blend.
|
||||
object(&mut m, 0, 0, 0, 0x400); // OBJ0 below BG0; later OBJ still excluded.
|
||||
m.write16(BLDCNT, 0x1041).unwrap();
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(0x3dff));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn semi_transparent_obj_overrides_mode_and_first_target_selection() {
|
||||
let mut m = setup();
|
||||
m.write16(DISPCNT, 0x1100).unwrap();
|
||||
object(&mut m, 0, 0x400, 0, 0);
|
||||
m.write16(BLDALPHA, 0x0808).unwrap();
|
||||
m.write16(BLDY, 16).unwrap();
|
||||
for mode in 0..4 {
|
||||
m.write16(BLDCNT, 0x100 | (mode << 6)).unwrap();
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(0x3dff));
|
||||
}
|
||||
window(&mut m, 0x0004, 0x0008, 0x11, 0x31);
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(0x7fff)); // Window can disable forced alpha.
|
||||
assert_eq!(pixel(&m, 4, 0), rgb(0x3dff));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn semi_transparent_obj_falls_back_to_selected_brightness_without_second_target() {
|
||||
let mut m = setup();
|
||||
m.write16(DISPCNT, 0x1100).unwrap();
|
||||
object(&mut m, 0, 0x400, 0, 0);
|
||||
m.write16(BLDY, 16).unwrap();
|
||||
for (control, expected) in [(0xc0, 0x7fff), (0xd0, 0), (0x10, 0x7fff), (0x1d0, 0)] {
|
||||
m.write16(BLDCNT, control).unwrap();
|
||||
// Last case has a second target; zero alpha coefficients produce black.
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(expected));
|
||||
}
|
||||
m.write16(BLDALPHA, 0x0808).unwrap();
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(0x3dff)); // Alpha takes precedence over brightness.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_effects_work_in_affine_and_all_bitmap_modes() {
|
||||
for mode in 1..=5 {
|
||||
let mut m = setup();
|
||||
m.write16(DISPCNT, 0x400 | mode).unwrap();
|
||||
m.write16(BG2CNT, 0).unwrap();
|
||||
m.write16(BG2PA, 256).unwrap();
|
||||
m.write16(BG2PD, 256).unwrap();
|
||||
if mode <= 2 {
|
||||
m.write16(BG2CNT, 0x1000).unwrap(); // Byte map at 0x8000, tile0 at zero.
|
||||
m.write16(VRAM + 0x8000, 0).unwrap();
|
||||
m.write16(VRAM, 0x0101).unwrap();
|
||||
} else {
|
||||
m.write16(VRAM, if mode == 4 { 0x0101 } else { 31 })
|
||||
.unwrap();
|
||||
}
|
||||
m.write16(BLDCNT, 0x2044).unwrap();
|
||||
m.write16(BLDALPHA, 0x1000).unwrap();
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(0x4210), "mode={mode}");
|
||||
window(&mut m, 0x0001, 0x0001, 4, 0x24);
|
||||
assert_eq!(pixel(&m, 0, 0), rgb(31));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn green_swap_follows_effects_and_forced_blank_bypasses_every_effect() {
|
||||
let mut m = setup();
|
||||
m.write16(PAL + 2, 0x3e0).unwrap();
|
||||
m.write16(BLDCNT, 0xc1).unwrap();
|
||||
m.write16(BLDY, 16).unwrap();
|
||||
window(&mut m, 0x0001, 0x0001, 1, 0x21);
|
||||
m.write16(GREENSWAP, 1).unwrap();
|
||||
assert_eq!(&render(&m).pixels()[..2], &[0, 0xff00]);
|
||||
m.write16(DISPCNT, 0xffff).unwrap();
|
||||
assert!(render(&m).pixels().iter().all(|&p| p == 0xffffff));
|
||||
}
|
||||
166
crates/gba-core/tests/effects/demo.rs
Normal file
166
crates/gba-core/tests/effects/demo.rs
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
use super::*;
|
||||
use gba_core::input::{Button, Buttons};
|
||||
use gba_demos::{
|
||||
effects_demo::{EffectsDemo, EFFECTS_STATE},
|
||||
tile_demo::TileDemo,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn cpu_demo_moves_wraps_cancels_resets_and_updates_effect_registers_during_vblank() {
|
||||
let mut demo = EffectsDemo::new().unwrap();
|
||||
let mut frame = Framebuffer::default();
|
||||
assert_eq!(demo.machine().memory().read16(DISPCNT).unwrap(), 0);
|
||||
let mut expected_x = 0u32;
|
||||
let mut expected_y = 0u32;
|
||||
let sequence = [
|
||||
Buttons::default(),
|
||||
Buttons::default()
|
||||
.with(Button::Left, true)
|
||||
.with(Button::Up, true),
|
||||
Buttons::default()
|
||||
.with(Button::Right, true)
|
||||
.with(Button::Down, true),
|
||||
Buttons::default()
|
||||
.with(Button::Right, true)
|
||||
.with(Button::Left, true),
|
||||
Buttons::default().with(Button::A, true),
|
||||
Buttons::default().with(Button::B, true),
|
||||
Buttons::default()
|
||||
.with(Button::A, true)
|
||||
.with(Button::B, true),
|
||||
Buttons::default()
|
||||
.with(Button::Start, true)
|
||||
.with(Button::Right, true),
|
||||
Buttons::default(),
|
||||
];
|
||||
for (i, buttons) in sequence.into_iter().enumerate() {
|
||||
let before = demo.machine().cycles();
|
||||
demo.frame(buttons, &mut frame).unwrap();
|
||||
let memory = demo.machine().memory();
|
||||
// Explicit expected movements keep this independent of the ARM program.
|
||||
if i == 1 {
|
||||
expected_x = 510;
|
||||
expected_y = 510;
|
||||
}
|
||||
if i == 2 || i == 7 {
|
||||
expected_x = 0;
|
||||
expected_y = 0;
|
||||
}
|
||||
assert_eq!(memory.read32(EFFECTS_STATE).unwrap(), i as u32 + 1);
|
||||
assert_eq!(memory.read32(EFFECTS_STATE + 4).unwrap(), expected_x);
|
||||
assert_eq!(memory.read32(EFFECTS_STATE + 8).unwrap(), expected_y);
|
||||
assert_eq!(memory.read16(DISPCNT).unwrap(), 0x3340);
|
||||
assert_eq!(memory.read16(WININ).unwrap(), 0x1f);
|
||||
assert_eq!(memory.read16(WINOUT).unwrap(), 0x3f);
|
||||
assert_eq!(memory.read16(BLDALPHA).unwrap(), 0x808);
|
||||
assert_eq!(
|
||||
memory.read16(BLDCNT).unwrap(),
|
||||
match i {
|
||||
4 | 6 => 0x251,
|
||||
5 => 0xff,
|
||||
_ => 0xbf,
|
||||
}
|
||||
);
|
||||
assert_eq!(memory.display_position().scanline, 160);
|
||||
assert!(demo.machine().cycles() > before);
|
||||
// The center is in WIN0 and therefore unchanged by any effect.
|
||||
assert_eq!(frame.pixels()[80 * WIDTH + 120], rgb(0x7fff));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_brightness_demo_matches_tile_scene_inside_and_outside_moving_window() {
|
||||
let mut demo = EffectsDemo::new().unwrap();
|
||||
let mut base = TileDemo::new().unwrap();
|
||||
let mut actual = Framebuffer::default();
|
||||
let mut original = Framebuffer::default();
|
||||
// Include wrapped scroll, affine objects, and brightness changes.
|
||||
for (index, buttons) in [
|
||||
Buttons::default()
|
||||
.with(Button::Left, true)
|
||||
.with(Button::Up, true),
|
||||
Buttons::default()
|
||||
.with(Button::B, true)
|
||||
.with(Button::L, true),
|
||||
Buttons::default()
|
||||
.with(Button::B, true)
|
||||
.with(Button::R, true),
|
||||
Buttons::default().with(Button::Start, true),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
demo.frame(buttons, &mut actual).unwrap();
|
||||
base.frame(buttons, &mut original).unwrap();
|
||||
let (left, top) = if index < 3 { (62, 38) } else { (64, 40) };
|
||||
for y in 0..HEIGHT {
|
||||
for x in 0..WIDTH {
|
||||
let old = original.pixels()[y * WIDTH + x];
|
||||
// Recover the exact five-bit channels from RGB888.
|
||||
let channels = [(old >> 19) & 31, (old >> 11) & 31, (old >> 3) & 31];
|
||||
let mut expected = 0u16;
|
||||
for (channel, shift) in channels.into_iter().zip([0, 5, 10]) {
|
||||
let changed = if (left..left + 112).contains(&x) && (top..top + 80).contains(&y)
|
||||
{
|
||||
channel
|
||||
} else if index == 1 || index == 2 {
|
||||
channel - channel / 2
|
||||
} else {
|
||||
channel + (31 - channel) / 2
|
||||
};
|
||||
expected |= (changed as u16) << shift;
|
||||
}
|
||||
assert_eq!(
|
||||
actual.pixels()[y * WIDTH + x],
|
||||
rgb(expected),
|
||||
"frame={index} pixel={x},{y}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_window_demo_retains_active_region_when_bottom_moves_beyond_vcount_range() {
|
||||
let mut demo = EffectsDemo::new().unwrap();
|
||||
let mut frame = Framebuffer::default();
|
||||
for index in 0..54 {
|
||||
demo.frame(Buttons::default().with(Button::Down, true), &mut frame)
|
||||
.unwrap();
|
||||
// x=80 avoids foreground crosses and sprites. It stays inside WIN0's X bounds.
|
||||
let sy = 2 * (index + 1);
|
||||
let raw: u16 =
|
||||
[0x260, 0x3a0, 0x7d20, 0x7e80][((80 / 32 + sy / 32) % 2) * 2 + (80 / 2 + sy / 2) % 2];
|
||||
let expected = if index == 53 {
|
||||
// New bottom=228 cannot match any line. Last frame's active flag survives.
|
||||
raw
|
||||
} else {
|
||||
[0, 5, 10].into_iter().fold(0, |value, shift| {
|
||||
let channel = (raw >> shift) & 31;
|
||||
value | ((channel + (31 - channel) / 2) << shift)
|
||||
})
|
||||
};
|
||||
assert_eq!(frame.pixels()[80], rgb(expected), "frame={index}");
|
||||
}
|
||||
let mut snapshot = Framebuffer::default();
|
||||
demo.machine().memory().render_frame(&mut snapshot).unwrap();
|
||||
assert_ne!(snapshot.pixels()[80], frame.pixels()[80]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dma_word_write_updates_selection_and_coefficients_together() {
|
||||
use gba_core::{
|
||||
cpu::Cpu,
|
||||
dma::DMA_BASE,
|
||||
machine::{Machine, StepKind},
|
||||
};
|
||||
let mut m = setup();
|
||||
m.write32(0x0200_0000, 0x0808_0241).unwrap();
|
||||
m.write32(DMA_BASE + 36, 0x0200_0000).unwrap();
|
||||
m.write32(DMA_BASE + 40, BLDCNT).unwrap();
|
||||
m.write32(DMA_BASE + 44, 0x8400_0001).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(0x0300_0000), m);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
|
||||
assert_eq!(machine.memory().read32(BLDCNT).unwrap(), 0x0808_0241);
|
||||
assert_eq!(pixel(machine.memory(), 0, 0), rgb(0x1ef));
|
||||
}
|
||||
176
crates/gba-core/tests/effects/windows.rs
Normal file
176
crates/gba-core/tests/effects/windows.rs
Normal 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));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue