use alloc::string::ToString;
use alloc::sync::Arc;
use alloc::vec::Vec;
use super::*;
use crate::core::clock::{ClockForest, Rational};
use crate::core::device::Deferred;
use crate::core::space::{RamStore, Region, RequesterId, UnassignedPolicy};
use crate::core::state::{MachineShape, Migrations, StateReader, StateWriter};
use crate::core::wire::{Wire, WireId, WireIdAllocator, WireSink};
fn new_ppu() -> (NesPpu, Arc<RamStore>, Arc<RamStore>) {
let mut props = Props::new();
props.insert("warmup", false);
let ppu = NesPpu::new(&props).expect("properties are valid");
let chr = Arc::new(RamStore::new(0x2000));
let nt = Arc::new(RamStore::new(0x1000));
let space = AddressSpace::new("ppu", 14).with_unassigned(UnassignedPolicy::ZEROS);
space
.topology()
.map(Arc::new(Region::ram("chr", Arc::clone(&chr))), 0x0000)
.expect("chr fits");
space
.topology()
.map(
Arc::new(
Region::mirror(
"nametables",
Arc::new(Region::ram("nt", Arc::clone(&nt))),
0x1000,
)
.expect("mirror fits"),
),
0x2000,
)
.expect("nametables fit");
ppu.attach_bus(Arc::new(space));
(ppu, chr, nt)
}
fn seek(ppu: &NesPpu, scanline: u16, dot: u16) {
ppu.with_engine(|e| {
e.scanline = scanline;
e.dot = dot;
});
}
fn draw_two_frames(ppu: &NesPpu) {
ppu.advance_by(DOTS_PER_FRAME * 2 - u64::from(DOTS_PER_SCANLINE) * 22);
assert_eq!(ppu.position(), (SCREEN_HEIGHT as u16, 0));
}
fn enable_rendering(ppu: &NesPpu) {
ppu.write_register(
PPUMASK,
MASK_BG | MASK_SPRITE | MASK_BG_LEFT | MASK_SPRITE_LEFT,
);
}
#[test]
fn three_dots_per_cpu_cycle_exactly() {
let mut forest = ClockForest::new();
let master = forest
.add_oscillator("master", Rational::new(236_250_000, 11).unwrap())
.unwrap();
let cpu = forest.add_domain("cpu", master, 1, 12).unwrap();
let dots = add_clock_domain(&mut forest, master).unwrap();
assert_eq!(forest.convert_ticks(cpu, dots, 1).unwrap(), 3);
assert_eq!(forest.convert_ticks(cpu, dots, 29_780).unwrap(), 89_340);
}
#[test]
fn an_even_frame_is_341_by_262_dots() {
let (ppu, _, _) = new_ppu();
ppu.advance_by(DOTS_PER_FRAME);
assert_eq!(ppu.frame(), 1);
assert_eq!(ppu.position(), (0, 0));
assert_eq!(DOTS_PER_FRAME, 89_342);
}
#[test]
fn an_odd_frame_skips_a_dot_when_rendering() {
let (ppu, _, _) = new_ppu();
enable_rendering(&ppu);
ppu.advance_by(DOTS_PER_FRAME); assert_eq!(ppu.frame(), 1);
assert_eq!(ppu.position(), (0, 0));
ppu.advance_by(DOTS_PER_FRAME - 1); assert_eq!(ppu.frame(), 2);
assert_eq!(ppu.position(), (0, 0));
}
#[test]
fn the_odd_frame_skip_needs_rendering_enabled() {
let (ppu, _, _) = new_ppu();
ppu.advance_by(DOTS_PER_FRAME);
ppu.advance_by(DOTS_PER_FRAME);
assert_eq!(ppu.frame(), 2);
assert_eq!(ppu.position(), (0, 0));
}
#[derive(Debug, Default)]
struct Recorder {
levels: crate::core::sync::Mutex<Vec<bool>>,
}
impl WireSink for Recorder {
fn set_level(&self, _src: WireId, _line: u32, level: Level) {
self.levels.lock().push(level.is_high());
}
}
fn with_nmi(ppu: &NesPpu) -> Arc<Recorder> {
let ids = WireIdAllocator::new();
let id = ids.alloc();
let sink = Arc::new(Recorder::default());
let wire = Wire::builder()
.source(id)
.sink(Arc::clone(&sink) as Arc<dyn WireSink>, 0)
.build_shared();
ppu.attach_nmi(WireSource::new(wire, id));
sink
}
#[test]
fn the_vblank_flag_is_set_at_dot_1_of_scanline_241() {
let (ppu, _, _) = new_ppu();
seek(&ppu, VBLANK_SCANLINE, 0);
ppu.advance_by(1); assert_eq!(ppu.with_engine(|e| e.status) & STATUS_VBLANK, 0);
ppu.advance_by(1); assert_eq!(ppu.with_engine(|e| e.status) & STATUS_VBLANK, STATUS_VBLANK);
ppu.advance_by(3);
assert_eq!(ppu.read_register(PPUSTATUS) & STATUS_VBLANK, STATUS_VBLANK);
assert_eq!(
ppu.with_engine(|e| e.status) & STATUS_VBLANK,
0,
"cleared by the read"
);
}
#[test]
fn the_vblank_flag_is_cleared_at_dot_1_of_the_pre_render_line() {
let (ppu, _, _) = new_ppu();
seek(&ppu, VBLANK_SCANLINE, 1);
ppu.advance_by(1);
ppu.with_engine(|e| e.status |= STATUS_SPRITE0 | STATUS_OVERFLOW);
seek(&ppu, PRE_RENDER_SCANLINE, 0);
ppu.advance_by(1);
assert_ne!(ppu.with_engine(|e| e.status) & STATUS_VBLANK, 0);
ppu.advance_by(1); assert_eq!(
ppu.with_engine(|e| e.status) & (STATUS_VBLANK | STATUS_SPRITE0 | STATUS_OVERFLOW),
0
);
}
#[test]
fn reading_2002_one_dot_before_the_set_suppresses_the_flag_entirely() {
let (ppu, _, _) = new_ppu();
let nmi = with_nmi(&ppu);
ppu.write_register(PPUCTRL, CTRL_NMI);
seek(&ppu, VBLANK_SCANLINE, 1);
assert_eq!(ppu.read_register(PPUSTATUS) & STATUS_VBLANK, 0);
ppu.advance_by(3);
assert_eq!(ppu.with_engine(|e| e.status) & STATUS_VBLANK, 0);
assert!(
!nmi.levels.lock().iter().any(|high| *high),
"no NMI may be requested this frame"
);
}
#[test]
fn reading_2002_on_the_set_dot_returns_the_flag_and_suppresses_the_nmi() {
for dot in [2u16, 3] {
let (ppu, _, _) = new_ppu();
let nmi = with_nmi(&ppu);
ppu.write_register(PPUCTRL, CTRL_NMI);
seek(&ppu, VBLANK_SCANLINE, 1);
ppu.advance_by(u64::from(dot - 1)); assert_eq!(ppu.position(), (VBLANK_SCANLINE, dot));
let status = ppu.read_register(PPUSTATUS);
assert_eq!(status & STATUS_VBLANK, STATUS_VBLANK, "dot {dot}");
assert!(ppu.with_engine(|e| e.suppress_nmi), "dot {dot}");
ppu.advance_by(u64::from(DOTS_PER_SCANLINE) * 19);
assert!(
!nmi.levels.lock().iter().any(|high| *high),
"dot {dot}: the NMI must stay suppressed for the frame"
);
}
}
#[test]
fn a_read_four_dots_after_the_set_is_too_late_to_suppress() {
let (ppu, _, _) = new_ppu();
let nmi = with_nmi(&ppu);
ppu.write_register(PPUCTRL, CTRL_NMI);
seek(&ppu, VBLANK_SCANLINE, 1);
ppu.advance_by(3);
assert_eq!(ppu.position(), (VBLANK_SCANLINE, 4));
assert_eq!(nmi.levels.lock().as_slice(), &[true]);
ppu.read_register(PPUSTATUS);
assert_eq!(nmi.levels.lock().as_slice(), &[true, false]);
}
#[test]
fn enabling_nmi_mid_vblank_requests_one_immediately() {
let (ppu, _, _) = new_ppu();
let nmi = with_nmi(&ppu);
seek(&ppu, VBLANK_SCANLINE, 1);
ppu.advance_by(10);
assert!(nmi.levels.lock().is_empty(), "NMI output is still off");
ppu.write_register(PPUCTRL, CTRL_NMI);
assert_eq!(nmi.levels.lock().as_slice(), &[true]);
ppu.write_register(PPUCTRL, 0);
ppu.write_register(PPUCTRL, CTRL_NMI);
assert_eq!(nmi.levels.lock().as_slice(), &[true, false, true]);
}
#[test]
fn the_nmi_falls_when_vblank_ends() {
let (ppu, _, _) = new_ppu();
let nmi = with_nmi(&ppu);
ppu.write_register(PPUCTRL, CTRL_NMI);
seek(&ppu, VBLANK_SCANLINE, 1);
ppu.advance_by(3);
assert_eq!(nmi.levels.lock().as_slice(), &[true]);
seek(&ppu, PRE_RENDER_SCANLINE, 0);
ppu.advance_by(2);
assert_eq!(nmi.levels.lock().as_slice(), &[true, false]);
}
#[test]
fn the_2007_read_buffer_delays_by_one_access() {
let (ppu, _, nt) = new_ppu();
nt.write_u8(0x000, 0x11).unwrap();
nt.write_u8(0x001, 0x22).unwrap();
ppu.write_register(PPUADDR, 0x20);
ppu.write_register(PPUADDR, 0x00);
let dummy = ppu.read_register(PPUDATA);
assert_eq!(dummy, 0x00, "the first read returns the stale buffer");
assert_eq!(ppu.read_register(PPUDATA), 0x11);
assert_eq!(ppu.read_register(PPUDATA), 0x22);
}
#[test]
fn a_palette_read_is_not_buffered_but_still_fills_the_buffer() {
let (ppu, _, nt) = new_ppu();
nt.write_u8(0xf01, 0x5a).unwrap();
ppu.poke_palette(0x3f01, 0x21);
ppu.write_register(PPUADDR, 0x3f);
ppu.write_register(PPUADDR, 0x01);
assert_eq!(ppu.read_register(PPUDATA) & 0x3f, 0x21);
ppu.write_register(PPUADDR, 0x20);
ppu.write_register(PPUADDR, 0x00);
assert_eq!(ppu.read_register(PPUDATA), 0x5a);
}
#[test]
fn the_registers_mirror_every_eight_bytes_to_3fff() {
let (ppu, _, _) = new_ppu();
let port = ppu.port();
let mut byte = [0u8; 1];
ppu.with_engine(|e| e.status |= STATUS_VBLANK);
port.read(0x3ffa - REGISTER_BASE, &mut byte, MemAttrs::DEFAULT)
.unwrap();
assert_eq!(byte[0] & STATUS_VBLANK, STATUS_VBLANK);
assert_eq!(ppu.with_engine(|e| e.status) & STATUS_VBLANK, 0);
}
#[test]
fn the_write_toggle_is_shared_and_2002_clears_it() {
let (ppu, _, _) = new_ppu();
ppu.write_register(PPUADDR, 0x21); assert!(ppu.with_engine(|e| e.w));
ppu.read_register(PPUSTATUS);
assert!(!ppu.with_engine(|e| e.w));
ppu.write_register(PPUADDR, 0x2c);
ppu.write_register(PPUADDR, 0x00);
assert_eq!(ppu.with_engine(|e| e.v), 0x2c00);
}
#[test]
fn scroll_writes_land_in_t_and_x_exactly() {
let (ppu, _, _) = new_ppu();
ppu.write_register(PPUSCROLL, 0b1010_1011); assert_eq!(ppu.with_engine(|e| e.t) & 0x001f, 0b10101);
assert_eq!(ppu.with_engine(|e| e.x), 0b011);
ppu.write_register(PPUSCROLL, 0b0110_1101); let t = ppu.with_engine(|e| e.t);
assert_eq!((t >> 5) & 0x1f, 0b01101);
assert_eq!((t >> 12) & 0x07, 0b101);
}
#[test]
fn the_2006_high_write_clears_bit_14() {
let (ppu, _, _) = new_ppu();
ppu.write_register(PPUSCROLL, 0);
ppu.write_register(PPUSCROLL, 0xff); ppu.write_register(PPUADDR, 0xff); assert_eq!(ppu.with_engine(|e| e.t) & 0x7f00, 0x3f00);
}
#[test]
fn open_bus_fills_the_unused_bits_of_2002_and_decays() {
let mut props = Props::new();
props.insert("warmup", false);
props.insert("open-bus-decay-dots", 100u64);
let ppu = NesPpu::new(&props).unwrap();
ppu.attach_bus(Arc::new(AddressSpace::new("ppu", 14)));
ppu.write_register(PPUMASK, 0x1f);
assert_eq!(ppu.read_register(PPUSTATUS) & 0x1f, 0x1f);
assert_eq!(ppu.read_register(PPUCTRL), 0x1f);
ppu.advance_by(200);
assert_eq!(
ppu.read_register(PPUSTATUS) & 0x1f,
0x00,
"the charge decayed"
);
}
#[test]
fn a_debug_read_of_2002_has_no_side_effects() {
let (ppu, _, _) = new_ppu();
ppu.with_engine(|e| e.status |= STATUS_VBLANK);
ppu.write_register(PPUADDR, 0x21); let port = ppu.port();
let mut byte = [0u8; 1];
port.read(2, &mut byte, MemAttrs::DEBUG).unwrap();
assert_eq!(byte[0] & STATUS_VBLANK, STATUS_VBLANK);
assert_eq!(ppu.with_engine(|e| e.status) & STATUS_VBLANK, STATUS_VBLANK);
assert!(ppu.with_engine(|e| e.w), "the toggle must not have moved");
}
#[test]
fn a_debug_read_of_2007_does_not_advance_the_address() {
let (ppu, _, nt) = new_ppu();
nt.write_u8(0, 0x77).unwrap();
ppu.write_register(PPUADDR, 0x20);
ppu.write_register(PPUADDR, 0x00);
let port = ppu.port();
let mut byte = [0u8; 1];
port.read(7, &mut byte, MemAttrs::DEBUG).unwrap();
assert_eq!(ppu.with_engine(|e| e.v), 0x2000);
assert_eq!(ppu.read_register(PPUDATA), 0x00);
}
#[test]
fn the_warmup_lockout_ignores_early_writes() {
let ppu = NesPpu::new(&Props::new()).unwrap();
ppu.attach_bus(Arc::new(AddressSpace::new("ppu", 14)));
ppu.write_register(PPUCTRL, CTRL_NMI);
assert_eq!(ppu.with_engine(|e| e.ctrl), 0, "too early");
ppu.with_engine(|e| e.dots = WARMUP_DOTS);
ppu.write_register(PPUCTRL, CTRL_NMI);
assert_eq!(ppu.with_engine(|e| e.ctrl), CTRL_NMI);
ppu.with_engine(|e| e.dots = 0);
ppu.write_register(OAMADDR, 0x40);
assert_eq!(ppu.oam_addr(), 0x40);
}
#[test]
fn the_sprite_palette_zeros_alias_the_background_ones() {
let (ppu, _, _) = new_ppu();
for (sprite, background) in [
(0x3f10, 0x3f00),
(0x3f14, 0x3f04),
(0x3f18, 0x3f08),
(0x3f1c, 0x3f0c),
] {
ppu.poke_palette(sprite, 0x2a);
assert_eq!(ppu.peek_palette(background), 0x2a, "{sprite:#x}");
ppu.poke_palette(background, 0x15);
assert_eq!(ppu.peek_palette(sprite), 0x15, "{sprite:#x}");
}
ppu.poke_palette(0x3f01, 0x01);
ppu.poke_palette(0x3f11, 0x02);
assert_eq!(ppu.peek_palette(0x3f01), 0x01);
}
#[test]
fn palette_ram_repeats_through_3fff() {
let (ppu, _, _) = new_ppu();
ppu.poke_palette(0x3f03, 0x30);
assert_eq!(ppu.peek_palette(0x3fe3), 0x30);
assert_eq!(ppu.peek_palette(0x3f23), 0x30);
}
#[test]
fn palette_entries_are_six_bits() {
let (ppu, _, _) = new_ppu();
ppu.poke_palette(0x3f00, 0xff);
assert_eq!(ppu.peek_palette(0x3f00), 0x3f);
}
#[test]
fn a_palette_read_reports_the_top_two_bits_as_open_bus() {
let (ppu, _, _) = new_ppu();
ppu.poke_palette(0x3f00, 0x0f);
ppu.write_register(PPUMASK, 0xc0); ppu.write_register(PPUADDR, 0x3f);
ppu.write_register(PPUADDR, 0x00);
ppu.write_register(PPUSTATUS, 0xc0); assert_eq!(ppu.read_register(PPUDATA), 0xc0 | 0x0f);
}
fn plain_background(chr: &RamStore, nt: &RamStore, tile: u8) {
for row in 0..8u64 {
chr.write_u8(u64::from(tile) * 16 + row, 0xff).unwrap(); chr.write_u8(u64::from(tile) * 16 + 8 + row, 0x00).unwrap(); }
for index in 0..0x3c0u64 {
nt.write_u8(index, tile).unwrap();
}
}
#[test]
fn the_background_pipeline_draws_a_uniform_screen() {
let (ppu, chr, nt) = new_ppu();
plain_background(&chr, &nt, 1);
ppu.poke_palette(0x3f00, 0x0f); ppu.poke_palette(0x3f01, 0x21); ppu.write_register(PPUMASK, MASK_BG | MASK_BG_LEFT);
draw_two_frames(&ppu);
for x in [0usize, 1, 7, 8, 128, 255] {
assert_eq!(ppu.pixel(x, 100).unwrap().index(), 0x21, "x = {x}");
}
}
#[test]
fn the_left_column_mask_hides_the_first_eight_pixels() {
let (ppu, chr, nt) = new_ppu();
plain_background(&chr, &nt, 1);
ppu.poke_palette(0x3f00, 0x0f);
ppu.poke_palette(0x3f01, 0x21);
ppu.write_register(PPUMASK, MASK_BG); draw_two_frames(&ppu);
assert_eq!(ppu.pixel(0, 100).unwrap().index(), 0x0f);
assert_eq!(ppu.pixel(7, 100).unwrap().index(), 0x0f);
assert_eq!(ppu.pixel(8, 100).unwrap().index(), 0x21);
}
#[test]
fn fine_x_shifts_the_background_left() {
let (ppu, chr, nt) = new_ppu();
for row in 0..8u64 {
chr.write_u8(16 + row, 0x80).unwrap();
chr.write_u8(16 + 8 + row, 0x00).unwrap();
}
for index in 0..0x3c0u64 {
nt.write_u8(index, 1).unwrap();
}
ppu.poke_palette(0x3f00, 0x0f);
ppu.poke_palette(0x3f01, 0x21);
ppu.write_register(PPUMASK, MASK_BG | MASK_BG_LEFT);
ppu.write_register(PPUSCROLL, 3); ppu.write_register(PPUSCROLL, 0);
draw_two_frames(&ppu);
assert_eq!(ppu.pixel(5, 100).unwrap().index(), 0x21);
assert_eq!(ppu.pixel(4, 100).unwrap().index(), 0x0f);
}
#[test]
fn greyscale_masks_the_palette_index() {
let (ppu, chr, nt) = new_ppu();
plain_background(&chr, &nt, 1);
ppu.poke_palette(0x3f01, 0x21);
ppu.write_register(PPUMASK, MASK_BG | MASK_BG_LEFT | MASK_GREYSCALE);
draw_two_frames(&ppu);
assert_eq!(ppu.pixel(100, 100).unwrap().index(), 0x20);
}
#[test]
fn emphasis_travels_with_the_pixel() {
let (ppu, _, _) = new_ppu();
ppu.poke_palette(0x3f00, 0x0f);
ppu.write_register(PPUMASK, MASK_EMPHASIS_R | MASK_EMPHASIS_B);
draw_two_frames(&ppu);
assert_eq!(ppu.pixel(10, 10).unwrap().emphasis(), 0b101);
}
#[test]
fn rendering_disabled_with_v_in_the_palette_shows_that_entry() {
let (ppu, _, _) = new_ppu();
ppu.poke_palette(0x3f00, 0x0f);
ppu.poke_palette(0x3f05, 0x16);
ppu.write_register(PPUADDR, 0x3f);
ppu.write_register(PPUADDR, 0x05);
draw_two_frames(&ppu);
assert_eq!(ppu.pixel(10, 10).unwrap().index(), 0x16);
}
#[test]
fn coarse_x_wraps_into_the_next_nametable() {
let (ppu, _, _) = new_ppu();
ppu.with_engine(|e| e.v = 0x001f);
enable_rendering(&ppu);
seek(&ppu, 0, 8);
ppu.advance_by(1);
assert_eq!(ppu.with_engine(|e| e.v), 0x0400);
}
#[test]
fn y_increment_wraps_at_row_29_and_at_row_31() {
let (ppu, _, _) = new_ppu();
enable_rendering(&ppu);
ppu.with_engine(|e| e.v = 0x7000 | (29 << 5));
seek(&ppu, 0, 256);
ppu.advance_by(1);
assert_eq!(ppu.with_engine(|e| e.v) & 0x0be0, 0x0800, "toggles bit 11");
ppu.with_engine(|e| e.v = 0x7000 | (31 << 5));
seek(&ppu, 0, 256);
ppu.advance_by(1);
assert_eq!(ppu.with_engine(|e| e.v) & 0x0be0, 0x0000, "no toggle");
}
#[test]
fn dot_257_copies_the_horizontal_bits_and_280_to_304_the_vertical_ones() {
let (ppu, _, _) = new_ppu();
enable_rendering(&ppu);
ppu.with_engine(|e| {
e.v = 0;
e.t = 0x7fff;
});
seek(&ppu, 0, 257);
ppu.advance_by(1);
assert_eq!(ppu.with_engine(|e| e.v), 0x041f);
ppu.with_engine(|e| e.v = 0);
seek(&ppu, PRE_RENDER_SCANLINE, 280);
ppu.advance_by(1);
assert_eq!(ppu.with_engine(|e| e.v), 0x7be0);
}
#[test]
fn writing_2007_while_rendering_moves_the_scroll_counters() {
let (ppu, _, _) = new_ppu();
enable_rendering(&ppu);
seek(&ppu, 0, 100);
ppu.with_engine(|e| e.v = 0x0000);
ppu.write_register(PPUDATA, 0x00);
assert_eq!(ppu.with_engine(|e| e.v), 0x1001);
}
fn opaque_sprite(chr: &RamStore, tile: u8) {
for row in 0..8u64 {
chr.write_u8(u64::from(tile) * 16 + row, 0xff).unwrap();
chr.write_u8(u64::from(tile) * 16 + 8 + row, 0x00).unwrap();
}
}
#[test]
fn a_sprite_is_drawn_one_line_below_its_y_byte() {
let (ppu, chr, _) = new_ppu();
opaque_sprite(&chr, 1);
ppu.poke_palette(0x3f00, 0x0f);
ppu.poke_palette(0x3f11, 0x27);
ppu.poke_oam(0, 50); ppu.poke_oam(1, 1); ppu.poke_oam(2, 0); ppu.poke_oam(3, 40); ppu.write_register(PPUMASK, MASK_SPRITE | MASK_SPRITE_LEFT);
draw_two_frames(&ppu);
assert_eq!(ppu.pixel(40, 50).unwrap().index(), 0x0f, "not on line 50");
assert_eq!(ppu.pixel(40, 51).unwrap().index(), 0x27);
assert_eq!(ppu.pixel(47, 58).unwrap().index(), 0x27);
assert_eq!(ppu.pixel(48, 58).unwrap().index(), 0x0f);
}
#[test]
fn sprites_never_appear_on_scanline_zero() {
let (ppu, chr, _) = new_ppu();
opaque_sprite(&chr, 1);
ppu.poke_palette(0x3f00, 0x0f);
ppu.poke_palette(0x3f11, 0x27);
ppu.poke_oam(0, 0xff); ppu.poke_oam(1, 1);
ppu.poke_oam(3, 0);
ppu.write_register(PPUMASK, MASK_SPRITE | MASK_SPRITE_LEFT);
draw_two_frames(&ppu);
assert_eq!(ppu.pixel(0, 0).unwrap().index(), 0x0f);
}
#[test]
fn an_8x16_sprite_takes_its_bank_from_the_tile_byte() {
let (ppu, chr, _) = new_ppu();
for row in 0..8u64 {
chr.write_u8(0x1000 + 0x02 * 16 + row, 0xff).unwrap();
chr.write_u8(0x1000 + 0x03 * 16 + row, 0x00).unwrap();
chr.write_u8(0x1000 + 0x03 * 16 + 8 + row, 0xff).unwrap();
}
ppu.poke_palette(0x3f00, 0x0f);
ppu.poke_palette(0x3f11, 0x27); ppu.poke_palette(0x3f12, 0x28); ppu.poke_oam(0, 50);
ppu.poke_oam(1, 0x03); ppu.poke_oam(2, 0);
ppu.poke_oam(3, 40);
ppu.write_register(PPUCTRL, CTRL_SPRITE_16);
ppu.write_register(PPUMASK, MASK_SPRITE | MASK_SPRITE_LEFT);
draw_two_frames(&ppu);
assert_eq!(ppu.pixel(40, 51).unwrap().index(), 0x27, "top half");
assert_eq!(ppu.pixel(40, 59).unwrap().index(), 0x28, "bottom half");
assert_eq!(ppu.pixel(40, 67).unwrap().index(), 0x0f, "16 rows only");
}
#[test]
fn sprite_flipping_mirrors_the_pattern() {
let (ppu, chr, _) = new_ppu();
chr.write_u8(16, 0xff).unwrap();
for row in 1..8u64 {
chr.write_u8(16 + row, 0x80).unwrap();
}
ppu.poke_palette(0x3f00, 0x0f);
ppu.poke_palette(0x3f11, 0x27);
ppu.poke_oam(0, 50);
ppu.poke_oam(1, 1);
ppu.poke_oam(2, SPRITE_FLIP_X | SPRITE_FLIP_Y);
ppu.poke_oam(3, 40);
ppu.write_register(PPUMASK, MASK_SPRITE | MASK_SPRITE_LEFT);
draw_two_frames(&ppu);
assert_eq!(ppu.pixel(47, 58).unwrap().index(), 0x27);
assert_eq!(ppu.pixel(40, 58).unwrap().index(), 0x27);
assert_eq!(ppu.pixel(40, 51).unwrap().index(), 0x0f);
}
#[test]
fn only_eight_sprites_are_drawn_and_the_ninth_sets_overflow() {
let (ppu, chr, _) = new_ppu();
opaque_sprite(&chr, 1);
ppu.poke_palette(0x3f00, 0x0f);
ppu.poke_palette(0x3f11, 0x27);
for index in 0..9u8 {
ppu.poke_oam(index * 4, 50);
ppu.poke_oam(index * 4 + 1, 1);
ppu.poke_oam(index * 4 + 2, 0);
ppu.poke_oam(index * 4 + 3, index * 8);
}
ppu.write_register(PPUMASK, MASK_SPRITE | MASK_SPRITE_LEFT);
draw_two_frames(&ppu);
assert_eq!(
ppu.pixel(56, 51).unwrap().index(),
0x27,
"the eighth is drawn"
);
assert_eq!(ppu.pixel(64, 51).unwrap().index(), 0x0f, "the ninth is not");
assert_ne!(ppu.with_engine(|e| e.status) & STATUS_OVERFLOW, 0);
}
#[test]
fn the_overflow_bug_reads_a_tile_byte_as_a_y_coordinate() {
let (ppu, chr, _) = new_ppu();
opaque_sprite(&chr, 1);
for index in 0..8u8 {
ppu.poke_oam(index * 4, 50);
ppu.poke_oam(index * 4 + 1, 1);
ppu.poke_oam(index * 4 + 2, 0);
ppu.poke_oam(index * 4 + 3, index * 8);
}
for index in 8..64u8 {
ppu.poke_oam(index * 4, 0xf0);
ppu.poke_oam(index * 4 + 1, 0xf0);
ppu.poke_oam(index * 4 + 2, 0xf0);
ppu.poke_oam(index * 4 + 3, 0xf0);
}
ppu.poke_oam(9 * 4 + 1, 50);
ppu.write_register(PPUMASK, MASK_SPRITE | MASK_SPRITE_LEFT);
draw_two_frames(&ppu);
assert_ne!(
ppu.with_engine(|e| e.status) & STATUS_OVERFLOW,
0,
"a tile byte in range must trip the buggy overflow search"
);
}
#[test]
fn the_overflow_flag_stays_clear_with_eight_sprites_and_nothing_in_range() {
let (ppu, chr, _) = new_ppu();
opaque_sprite(&chr, 1);
for index in 0..8u8 {
ppu.poke_oam(index * 4, 50);
ppu.poke_oam(index * 4 + 1, 1);
ppu.poke_oam(index * 4 + 2, 0);
ppu.poke_oam(index * 4 + 3, index * 8);
}
for index in 8..64u8 {
for byte in 0..4u8 {
ppu.poke_oam(index * 4 + byte, 0xf0);
}
}
ppu.write_register(PPUMASK, MASK_SPRITE | MASK_SPRITE_LEFT);
draw_two_frames(&ppu);
assert_eq!(ppu.with_engine(|e| e.status) & STATUS_OVERFLOW, 0);
}
#[test]
fn sprite_priority_is_by_oam_index_then_by_the_priority_bit() {
let (ppu, chr, nt) = new_ppu();
opaque_sprite(&chr, 1);
opaque_sprite(&chr, 2);
plain_background(&chr, &nt, 3);
for row in 0..8u64 {
chr.write_u8(3 * 16 + row, 0x00).unwrap();
chr.write_u8(3 * 16 + 8 + row, 0x00).unwrap();
}
ppu.poke_palette(0x3f00, 0x0f);
ppu.poke_palette(0x3f11, 0x27); ppu.poke_palette(0x3f15, 0x28); ppu.poke_oam(0, 50);
ppu.poke_oam(1, 1);
ppu.poke_oam(2, 0);
ppu.poke_oam(3, 40);
ppu.poke_oam(4, 50);
ppu.poke_oam(5, 2);
ppu.poke_oam(6, 1); ppu.poke_oam(7, 40);
ppu.write_register(
PPUMASK,
MASK_BG | MASK_SPRITE | MASK_BG_LEFT | MASK_SPRITE_LEFT,
);
draw_two_frames(&ppu);
assert_eq!(ppu.pixel(40, 51).unwrap().index(), 0x27, "lower index wins");
}
#[test]
fn a_behind_sprite_loses_to_opaque_background() {
let (ppu, chr, nt) = new_ppu();
opaque_sprite(&chr, 1);
plain_background(&chr, &nt, 2);
opaque_sprite(&chr, 2);
ppu.poke_palette(0x3f00, 0x0f);
ppu.poke_palette(0x3f01, 0x11); ppu.poke_palette(0x3f11, 0x27);
ppu.poke_oam(0, 50);
ppu.poke_oam(1, 1);
ppu.poke_oam(2, SPRITE_BEHIND);
ppu.poke_oam(3, 40);
ppu.write_register(
PPUMASK,
MASK_BG | MASK_SPRITE | MASK_BG_LEFT | MASK_SPRITE_LEFT,
);
draw_two_frames(&ppu);
assert_eq!(ppu.pixel(40, 51).unwrap().index(), 0x11);
}
fn sprite_zero_scene(ppu: &NesPpu, chr: &RamStore, nt: &RamStore, x: u8, y: u8) {
opaque_sprite(chr, 1);
plain_background(chr, nt, 1);
ppu.poke_palette(0x3f00, 0x0f);
ppu.poke_palette(0x3f01, 0x11);
ppu.poke_palette(0x3f11, 0x27);
ppu.poke_oam(0, y);
ppu.poke_oam(1, 1);
ppu.poke_oam(2, 0);
ppu.poke_oam(3, x);
}
#[test]
fn sprite_zero_hits_where_both_layers_are_opaque() {
let (ppu, chr, nt) = new_ppu();
sprite_zero_scene(&ppu, &chr, &nt, 40, 50);
ppu.write_register(
PPUMASK,
MASK_BG | MASK_SPRITE | MASK_BG_LEFT | MASK_SPRITE_LEFT,
);
draw_two_frames(&ppu);
assert_ne!(ppu.with_engine(|e| e.status) & STATUS_SPRITE0, 0);
}
#[test]
fn sprite_zero_never_hits_at_x_255() {
let (ppu, chr, nt) = new_ppu();
sprite_zero_scene(&ppu, &chr, &nt, 255, 50);
ppu.write_register(
PPUMASK,
MASK_BG | MASK_SPRITE | MASK_BG_LEFT | MASK_SPRITE_LEFT,
);
draw_two_frames(&ppu);
assert_eq!(ppu.with_engine(|e| e.status) & STATUS_SPRITE0, 0);
}
#[test]
fn sprite_zero_never_hits_in_a_clipped_left_column() {
let (ppu, chr, nt) = new_ppu();
sprite_zero_scene(&ppu, &chr, &nt, 0, 50);
ppu.write_register(PPUMASK, MASK_BG | MASK_SPRITE | MASK_BG_LEFT);
draw_two_frames(&ppu);
assert_eq!(ppu.with_engine(|e| e.status) & STATUS_SPRITE0, 0);
}
#[test]
fn sprite_zero_never_hits_with_a_layer_disabled() {
let (ppu, chr, nt) = new_ppu();
sprite_zero_scene(&ppu, &chr, &nt, 40, 50);
ppu.write_register(PPUMASK, MASK_SPRITE | MASK_SPRITE_LEFT); draw_two_frames(&ppu);
assert_eq!(ppu.with_engine(|e| e.status) & STATUS_SPRITE0, 0);
}
#[test]
fn sprite_zero_never_hits_on_a_transparent_background_pixel() {
let (ppu, chr, nt) = new_ppu();
sprite_zero_scene(&ppu, &chr, &nt, 40, 50);
for row in 0..8u64 {
chr.write_u8(16 + row, 0x00).unwrap();
}
ppu.write_register(
PPUMASK,
MASK_BG | MASK_SPRITE | MASK_BG_LEFT | MASK_SPRITE_LEFT,
);
draw_two_frames(&ppu);
assert_eq!(ppu.with_engine(|e| e.status) & STATUS_SPRITE0, 0);
}
#[test]
fn the_sprite_zero_flag_lands_one_dot_after_the_pixel() {
let (ppu, chr, nt) = new_ppu();
sprite_zero_scene(&ppu, &chr, &nt, 0, 50);
ppu.write_register(
PPUMASK,
MASK_BG | MASK_SPRITE | MASK_BG_LEFT | MASK_SPRITE_LEFT,
);
ppu.advance_by(DOTS_PER_FRAME); while ppu.position() != (51, 0) {
ppu.advance_by(1);
}
ppu.advance_by(1); assert_eq!(ppu.with_engine(|e| e.status) & STATUS_SPRITE0, 0);
ppu.advance_by(1); assert_eq!(
ppu.with_engine(|e| e.status) & STATUS_SPRITE0,
0,
"not yet visible"
);
ppu.advance_by(1); assert_ne!(ppu.with_engine(|e| e.status) & STATUS_SPRITE0, 0);
}
#[test]
fn the_unimplemented_attribute_bits_read_back_as_zero() {
let (ppu, _, _) = new_ppu();
ppu.write_register(OAMADDR, 2);
ppu.write_register(OAMDATA, 0xff);
ppu.write_register(OAMADDR, 2);
assert_eq!(ppu.read_register(OAMDATA), 0xe3);
}
#[test]
fn writing_2004_while_rendering_bumps_oamaddr_without_storing() {
let (ppu, _, _) = new_ppu();
enable_rendering(&ppu);
seek(&ppu, 10, 100);
ppu.write_register(OAMADDR, 0x10);
ppu.write_register(OAMDATA, 0x5a);
assert_eq!(ppu.peek_oam(0x10), 0x00, "OAM is busy being evaluated");
assert_eq!(ppu.oam_addr(), 0x14, "only the high six bits moved");
}
#[test]
fn reading_2004_during_the_secondary_oam_clear_returns_ff() {
let (ppu, _, _) = new_ppu();
enable_rendering(&ppu);
ppu.poke_oam(0, 0x12);
seek(&ppu, 10, 30);
assert_eq!(ppu.read_register(OAMDATA), 0xff);
seek(&ppu, 10, 100);
assert_eq!(ppu.read_register(OAMDATA), 0x12);
}
#[test]
fn oam_dma_bytes_go_through_the_2004_path() {
let (ppu, _, _) = new_ppu();
ppu.write_register(OAMADDR, 0);
for index in 0..4u8 {
ppu.oam_dma_write(0x40 + index);
}
assert_eq!(ppu.peek_oam(0), 0x40);
assert_eq!(ppu.peek_oam(2), 0x42 & SPRITE_ATTR_IMPLEMENTED);
assert_eq!(ppu.oam_addr(), 4);
}
#[test]
fn a_high_oamaddr_corrupts_the_first_eight_oam_bytes_at_rendering_start() {
let (ppu, _, _) = new_ppu();
for index in 0..8u8 {
ppu.poke_oam(index, 0x00);
ppu.poke_oam(0x20 + index, 0xa0 + index);
}
enable_rendering(&ppu);
ppu.write_register(OAMADDR, 0x20);
seek(&ppu, PRE_RENDER_SCANLINE, 0);
ppu.advance_by(1);
for index in 0..8u8 {
let expected = if index & 3 == 2 {
(0xa0 + index) & SPRITE_ATTR_IMPLEMENTED
} else {
0xa0 + index
};
assert_eq!(ppu.peek_oam(index), expected, "byte {index}");
}
}
#[test]
fn a_misaligned_oamaddr_reinterprets_oam_bytes() {
let (ppu, chr, _) = new_ppu();
opaque_sprite(&chr, 1);
ppu.poke_oam(0, 50);
ppu.poke_oam(1, 1);
ppu.poke_oam(2, 0);
ppu.poke_oam(3, 40);
ppu.poke_oam(4, 50);
ppu.poke_oam(5, 1);
ppu.poke_oam(6, 0);
ppu.poke_oam(7, 60);
ppu.write_register(PPUMASK, MASK_SPRITE | MASK_SPRITE_LEFT);
ppu.advance_by(DOTS_PER_FRAME);
while ppu.position() != (40, 330) {
ppu.advance_by(1);
}
ppu.write_register(OAMADDR, 3);
ppu.advance_by(u64::from(DOTS_PER_SCANLINE));
assert_eq!(ppu.position(), (41, 330));
ppu.with_engine(|e| {
assert_eq!(e.eval_base, 3);
assert_eq!(&e.secondary_oam[..4], &[40, 50, 1, 0]);
assert_eq!(e.eval_found, 1);
});
}
#[test]
fn realize_fails_without_a_bus() {
let ppu = NesPpu::new(&Props::new()).unwrap();
let mut deferred = Deferred::new();
let mut ctx = RealizeCtx::new("ppu", RequesterId(1), &mut deferred);
let err = ppu.realize(&mut ctx).unwrap_err().to_string();
assert!(err.contains("attach_bus"), "{err}");
}
#[test]
fn realize_announces_the_nmi_line() {
let (ppu, _, _) = new_ppu();
let nmi = with_nmi(&ppu);
let mut deferred = Deferred::new();
let mut ctx = RealizeCtx::new("ppu", RequesterId(1), &mut deferred);
ppu.realize(&mut ctx).unwrap();
assert_eq!(nmi.levels.lock().as_slice(), &[] as &[bool]);
assert!(!ppu.with_engine(|e| e.nmi_active()));
}
#[test]
fn a_cold_reset_returns_every_register_to_its_documented_value() {
let (ppu, _, _) = new_ppu();
ppu.write_register(PPUCTRL, 0xff);
ppu.write_register(PPUMASK, 0xff);
ppu.advance_by(1000);
ppu.reset(ResetKind::Cold);
ppu.with_engine(|e| {
assert_eq!(e.ctrl, 0);
assert_eq!(e.mask, 0);
assert_eq!(e.status, 0);
assert_eq!(e.v, 0);
assert_eq!(e.t, 0);
assert_eq!(e.dots, 0);
});
}
fn state_hash(ppu: &NesPpu) -> u64 {
let mut w = StateWriter::new(MachineShape::new());
{
let mut chunk = w
.chunk("/ppu", NES_PPU_CLASS.name, NES_PPU_CLASS.version)
.unwrap();
ppu.save(&mut chunk).unwrap();
}
let bytes = w.to_vec().unwrap();
let mut hash = 0xcbf2_9ce4_8422_2325u64;
for byte in bytes {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
#[test]
fn a_mid_frame_snapshot_round_trips_including_the_shift_registers() {
let (ppu, chr, nt) = new_ppu();
plain_background(&chr, &nt, 1);
ppu.poke_palette(0x3f01, 0x21);
enable_rendering(&ppu);
ppu.advance_by(DOTS_PER_FRAME + 100 * u64::from(DOTS_PER_SCANLINE) + 123);
assert_ne!(ppu.with_engine(|e| e.bg_shift_lo), 0, "a tile is in flight");
let mut w = StateWriter::new(MachineShape::new());
{
let mut chunk = w
.chunk("/ppu", NES_PPU_CLASS.name, NES_PPU_CLASS.version)
.unwrap();
ppu.save(&mut chunk).unwrap();
}
let bytes = w.to_vec().unwrap();
let before = state_hash(&ppu);
let (restored, chr2, nt2) = new_ppu();
plain_background(&chr2, &nt2, 1);
let reader = StateReader::new(&bytes).unwrap();
let chunk = reader
.load(
"/ppu",
NES_PPU_CLASS.name,
NES_PPU_CLASS.version,
&Migrations::new(),
)
.unwrap();
restored.load(&mut chunk.reader()).unwrap();
assert_eq!(state_hash(&restored), before);
ppu.advance_by(5000);
restored.advance_by(5000);
assert_eq!(state_hash(&restored), state_hash(&ppu));
}
#[test]
fn a_short_chunk_is_rejected() {
let (ppu, _, _) = new_ppu();
let mut reader = ChunkReader::new(&[0u8; 4]);
assert!(ppu.load(&mut reader).is_err());
}
#[test]
fn the_framebuffer_is_256_by_240_indexed_pixels() {
let (ppu, _, _) = new_ppu();
ppu.with_framebuffer(|fb| assert_eq!(fb.len(), FRAMEBUFFER_LEN));
assert!(ppu.pixel(255, 239).is_some());
assert!(ppu.pixel(256, 0).is_none());
assert!(ppu.pixel(0, 240).is_none());
}
#[test]
fn a_pixel_packs_an_index_and_emphasis() {
let pixel = Pixel::new(0x3f, 0b101);
assert_eq!(pixel.index(), 0x3f);
assert_eq!(pixel.emphasis(), 0b101);
assert_eq!(Pixel::new(0xff, 0xff), Pixel::new(0x3f, 0b111));
}
#[test]
fn the_register_block_takes_byte_accesses_only() {
let (ppu, _, _) = new_ppu();
let port = ppu.port();
let mut wide = [0u8; 2];
assert!(port.read(0, &mut wide, MemAttrs::DEFAULT).is_err());
assert!(port.write(0, &[0, 0], MemAttrs::DEFAULT).is_err());
assert_eq!(port.constraints().max, Width::U8);
}
#[test]
fn a_debug_write_is_refused_rather_than_guessed_at() {
let (ppu, _, _) = new_ppu();
let port = ppu.port();
assert!(port.write(0, &[0xff], MemAttrs::DEBUG).is_err());
assert_eq!(ppu.with_engine(|e| e.ctrl), 0);
}