gameboy-rust/tests/effects/colors.rs
0xrsydn 9d14dd7cf4 feat(video): track windows and compose color effects
Add persistent window comparators, region masks, alpha blending, and brightness effects.
2026-10-05 12:58:38 +07:00

172 lines
6.1 KiB
Rust

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));
}