feat(video): track windows and compose color effects

Add persistent window comparators, region masks, alpha blending, and brightness effects.
This commit is contained in:
0xrsydn 2026-10-05 12:55:35 +07:00
commit 9d14dd7cf4
9 changed files with 1408 additions and 0 deletions

View file

@ -0,0 +1,209 @@
use super::*;
fn scene(horizontal: u16) -> Memory {
let mut m = setup(160);
m.write16(WIN0H, horizontal).unwrap();
m
}
fn check_line(f: &Framebuffer, y: usize, inside: impl Fn(usize) -> bool) {
for x in 0..WIDTH {
assert_eq!(
f.pixels()[y * WIDTH + x],
if inside(x) { 0 } else { rgb(31) },
"pixel={x},{y}"
);
}
}
#[test]
fn moving_left_after_entry_preserves_earlier_pixels_and_active_state() {
let mut m = scene(0x0a50);
m.advance_cycles(4 * 20 + 1);
m.write8(WIN0H + 1, 40).unwrap();
let f = finish(&mut m);
check_line(&f, 0, |x| (10..80).contains(&x));
check_line(&f, 1, |x| (40..80).contains(&x));
let mut snapshot = Framebuffer::default();
m.render_frame(&mut snapshot).unwrap();
check_line(&snapshot, 0, |x| (40..80).contains(&x));
assert_eq!(present(&m).pixels(), f.pixels());
}
#[test]
fn moving_left_behind_current_column_does_not_open_an_inactive_window() {
let mut m = scene(0x6478);
m.advance_cycles(4 * 20);
m.write8(WIN0H + 1, 10).unwrap();
let f = finish(&mut m);
check_line(&f, 0, |_| false);
check_line(&f, 1, |x| (10..120).contains(&x));
}
#[test]
fn missing_right_keeps_window_open_into_the_next_line() {
let mut m = scene(0x0a50);
m.advance_cycles(4 * 30);
m.write8(WIN0H, 5).unwrap();
let f = finish(&mut m);
check_line(&f, 0, |x| x >= 10);
check_line(&f, 1, |x| !(5..10).contains(&x));
}
#[test]
fn exact_four_cycle_sampling_boundaries_and_zero_cycle_advances_are_observable() {
for elapsed in [79, 80, 81, 83] {
let mut m = scene(0x6450);
m.advance_cycles(elapsed);
m.write16(WIN0H, 0x1450).unwrap();
m.advance_cycles(0);
let f = finish(&mut m);
check_line(&f, 0, |x| elapsed <= 80 && (20..80).contains(&x));
check_line(&f, 1, |x| (20..80).contains(&x));
}
}
#[test]
fn equal_bounds_clear_at_the_edge_not_at_register_write() {
for equal in [20, 40] {
let mut m = scene(0x0ac8);
m.advance_cycles(4 * 30);
m.write16(WIN0H, equal * 0x101).unwrap();
let f = finish(&mut m);
check_line(&f, 0, |x| x >= 10 && (equal == 20 || x < 40));
check_line(&f, 1, |x| equal == 20 && x < 20);
check_line(&f, 2, |_| false);
}
}
#[test]
fn offscreen_columns_including_late_hblank_set_next_lines_wrapping_state() {
for left in [240, 251, 252, 255] {
let mut m = scene((left << 8) | 10);
let f = finish(&mut m);
check_line(&f, 0, |_| false); // No previous line at startup.
check_line(&f, 1, |x| x < 10);
check_line(&f, 159, |x| x < 10);
}
}
#[test]
fn hblank_writes_can_cancel_a_late_offscreen_edge_without_changing_captured_row() {
let mut m = scene(0xff0a);
m.advance_cycles(HB);
m.write8(WIN0H + 1, 20).unwrap(); // X255 not reached; X20 already passed.
let f = finish(&mut m);
check_line(&f, 0, |_| false);
check_line(&f, 1, |x| x >= 20);
check_line(&f, 2, |x| !(10..20).contains(&x));
}
#[test]
fn independent_window_histories_preserve_win0_precedence() {
let mut m = scene(0x0a50);
m.write16(WIN1H, 0x1464).unwrap();
m.write16(WIN1V, 160).unwrap();
m.write16(DISPCNT, 0x6000).unwrap();
m.write16(WININ, 0x2000).unwrap(); // WIN0 red, WIN1 black.
m.advance_cycles(4 * 30);
m.write32(WIN0H, 0x1464_3250).unwrap(); // Move WIN0's left edge only.
let f = finish(&mut m);
check_line(&f, 0, |x| (80..100).contains(&x));
check_line(&f, 1, |x| (20..50).contains(&x) || (80..100).contains(&x));
assert_eq!(m.read32(WIN0H).unwrap(), 0);
}
#[test]
fn disabled_windows_and_forced_blank_do_not_pause_horizontal_history() {
for control in [0x4000, 0x2080] {
let mut m = scene(0x0a50);
m.write16(DISPCNT, control).unwrap();
m.advance_cycles(4 * 30);
m.write16(DISPCNT, 0x2000).unwrap();
let f = finish(&mut m);
// Enable/blank bits still sample at HBlank, unlike WIN0H comparator history.
check_line(&f, 0, |x| (10..80).contains(&x));
}
}
#[test]
fn hblank_dma_can_set_an_offscreen_edge_before_the_next_line() {
let mut m = scene(0);
for row in 0..160 {
m.write16(0x0200_0000 + row * 2, 0xff0a).unwrap();
}
m.write32(DMA_BASE, 0x0200_0000).unwrap();
m.write32(DMA_BASE + 4, WIN0H).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());
check_line(&f, 0, |_| false);
for y in 1..HEIGHT {
check_line(&f, y, |x| x < 10);
}
assert!(machine.halted());
}
#[test]
fn cpu_store_crossing_a_comparator_uses_committed_bounds_not_final_instruction_time() {
for after in [false, true] {
let mut m = scene(0x6450);
let code = [
0xe3a0_0301, // MOV r0,#0x04000000
0xe3a0_1b05, // MOV r1,#0x1400
0xe381_1050, // ORR r1,r1,#0x50
0xe1c0_14b0, // STRH r1,[r0,#0x40] (WIN0H)
];
for (i, value) in code.into_iter().enumerate() {
m.write32(0x0300_0000 + i as u32 * 4, value).unwrap();
}
let mut machine = Machine::new(Cpu::new(0x0300_0000), m);
for _ in 0..3 {
machine.step().unwrap();
}
let cycles = machine.cycles() as u32;
machine
.memory_mut()
.advance_cycles(if after { 81 } else { 79 } - cycles);
machine.step().unwrap();
assert!(machine.cycles() > 80);
let f = finish(machine.memory_mut());
check_line(&f, 0, |x| !after && (20..80).contains(&x));
}
}
#[test]
fn pixel_colors_still_sample_at_hblank_while_window_edges_keep_history() {
let mut m = scene(0x0a50);
m.advance_cycles(4 * 30);
m.write8(WIN0H + 1, 40).unwrap();
m.write16(PAL, 0x7c00).unwrap();
let f = finish(&mut m);
for x in 0..WIDTH {
assert_eq!(
f.pixels()[x],
if (10..80).contains(&x) {
0
} else {
rgb(0x7c00)
}
);
}
}
#[test]
fn capture_enable_preserves_partial_line_history_and_hidden_lines_update_flags() {
let mut m = scene(0x0a50);
m.set_scanline_rendering(false);
m.advance_cycles(4 * 30);
m.write8(WIN0H + 1, 40).unwrap();
m.set_scanline_rendering(true); // Before first HBlank, this frame is still complete.
check_line(&finish(&mut m), 0, |x| (10..80).contains(&x));
m.set_scanline_rendering(false);
m.write16(WIN0H, 0xff0a).unwrap();
m.advance_cycles(FRAME - VB);
m.set_scanline_rendering(true);
check_line(&finish(&mut m), 0, |x| x < 10);
}