use crate::{Ppu, PpuCartMemorySpace, Register};
#[derive(Default)]
struct MockCartRead;
impl PpuCartMemorySpace for MockCartRead {
fn read(&mut self, addr: u16) -> u8 {
let data = addr as u8;
data.wrapping_add(6)
}
fn write(&mut self, _data: u8, _addr: u16) {
}
}
#[derive(Default)]
struct NotifyCounter {
notify_count: u32,
last_old: u16,
last_new: u16,
}
impl PpuCartMemorySpace for NotifyCounter {
fn read(&mut self, _addr: u16) -> u8 {
0
}
fn write(&mut self, _data: u8, _addr: u16) {}
fn notify_addr_change(&mut self, old_addr: u16, new_addr: u16) {
self.notify_count += 1;
self.last_old = old_addr;
self.last_new = new_addr;
}
}
#[test]
pub fn read_palette_data() {
let mut ppu = Ppu::new();
let mut cart = MockCartRead::default();
let cart_ref_mut = &mut cart;
for addr in 0x3F00..0x3F20 {
let addr_low = addr as u8;
let addr_high = (addr >> 8) as u8;
ppu.write_ppu_register(addr_high, Register::PpuAddr, cart_ref_mut);
ppu.write_ppu_register(addr_low, Register::PpuAddr, cart_ref_mut);
let palette_data = ppu.read_ppu_register(Register::Data, cart_ref_mut);
let expected_palette_data = ppu.palette.read((addr as usize) & 0x1F);
assert_eq!(palette_data, expected_palette_data);
}
}
#[test]
pub fn read_buffered_data() {
let mut ppu = Ppu::new();
let mut cart = MockCartRead::default();
let cart_ref_mut = &mut cart;
for addr in 0x0000..0x3F00_u16 {
let addr_low = addr as u8;
let addr_high = (addr >> 8) as u8;
ppu.write_ppu_register(addr_high, Register::PpuAddr, cart_ref_mut);
ppu.write_ppu_register(addr_low, Register::PpuAddr, cart_ref_mut);
let buffered_data = ppu.read_ppu_register(Register::Data, cart_ref_mut);
let expected_buffered_data = (addr + 6) as u8;
assert_eq!(
buffered_data, expected_buffered_data,
"addr={addr:#06x}: buffered_data={buffered_data:#04x}, expected={expected_buffered_data:#04x}"
);
}
}
#[test]
pub fn read_palette_data_mirrors() {
let mut ppu = Ppu::new();
let mut cart = MockCartRead::default();
let cart_ref_mut = &mut cart;
for addr in 0x3F20..0x3FFF {
let addr_low = addr as u8;
let addr_high = (addr >> 8) as u8;
ppu.write_ppu_register(addr_high, Register::PpuAddr, cart_ref_mut);
ppu.write_ppu_register(addr_low, Register::PpuAddr, cart_ref_mut);
let palette_data = ppu.read_ppu_register(Register::Data, cart_ref_mut);
let expected_palette_data = ppu.palette.read((addr as usize) & 0x1F);
assert_eq!(palette_data, expected_palette_data);
}
}
#[test]
pub fn ppudata_read_triggers_a12_rise_on_increment() {
let mut ppu = Ppu::new();
let mut cart = NotifyCounter::default();
let cart_ref_mut = &mut cart;
ppu.write_ppu_register(0x0F, Register::PpuAddr, cart_ref_mut);
ppu.write_ppu_register(0xFF, Register::PpuAddr, cart_ref_mut);
ppu.write_ppu_register(0x00, Register::PpuControl, cart_ref_mut);
let _ = ppu.read_ppu_register(Register::Data, cart_ref_mut);
assert_eq!(
cart.notify_count, 1,
"expected one notify on $0FFF -> $1000"
);
assert_eq!(cart.last_old, 0x0FFF);
assert_eq!(cart.last_new, 0x1000);
}
#[test]
pub fn ppudata_write_triggers_a12_rise_on_increment() {
let mut ppu = Ppu::new();
let mut cart = NotifyCounter::default();
let cart_ref_mut = &mut cart;
ppu.write_ppu_register(0x0F, Register::PpuAddr, cart_ref_mut);
ppu.write_ppu_register(0xFF, Register::PpuAddr, cart_ref_mut);
ppu.write_ppu_register(0x00, Register::PpuControl, cart_ref_mut);
ppu.write_ppu_register(0xAA, Register::Data, cart_ref_mut);
assert_eq!(
cart.notify_count, 1,
"expected one notify on $0FFF -> $1000"
);
assert_eq!(cart.last_old, 0x0FFF);
assert_eq!(cart.last_new, 0x1000);
}
#[test]
pub fn ppudata_increment_within_a12_range_does_not_notify() {
let mut ppu = Ppu::new();
let mut cart = NotifyCounter::default();
let cart_ref_mut = &mut cart;
ppu.write_ppu_register(0x00, Register::PpuAddr, cart_ref_mut);
ppu.write_ppu_register(0x50, Register::PpuAddr, cart_ref_mut);
let _ = ppu.read_ppu_register(Register::Data, cart_ref_mut);
assert_eq!(cart.notify_count, 0, "no rising edge when A12 stays low");
}
#[test]
pub fn ppudata_increment_that_falls_a12_does_not_notify() {
let mut ppu = Ppu::new();
let mut cart = NotifyCounter::default();
let cart_ref_mut = &mut cart;
ppu.write_ppu_register(0x1F, Register::PpuAddr, cart_ref_mut);
ppu.write_ppu_register(0xFE, Register::PpuAddr, cart_ref_mut);
let _ = ppu.read_ppu_register(Register::Data, cart_ref_mut);
assert_eq!(cart.notify_count, 0, "A12 stays high; no rising edge");
}
const STATUS_SPRITE_OVERFLOW: u8 = 0b0010_0000;
const STATUS_SPRITE_ZERO_HIT: u8 = 0b0100_0000;
const ATTR_FRONT_OF_BG: u8 = 0b0000_0000;
#[derive(Default)]
struct NoopCart;
impl PpuCartMemorySpace for NoopCart {
fn read(&mut self, _addr: u16) -> u8 {
0
}
fn write(&mut self, _data: u8, _addr: u16) {}
}
fn oam_write(
ppu: &mut Ppu,
cart: &mut impl PpuCartMemorySpace,
sprite_index: u8,
y: u8,
tile: u8,
attr: u8,
x: u8,
) {
let mut noop = NoopCart;
let addr = sprite_index * 4;
ppu.write_ppu_register(addr, Register::OamAddr, &mut noop);
ppu.write_ppu_register(y, Register::OamData, &mut noop);
ppu.write_ppu_register(tile, Register::OamData, &mut noop);
ppu.write_ppu_register(attr, Register::OamData, &mut noop);
ppu.write_ppu_register(x, Register::OamData, &mut noop);
let _ = cart;
}
#[test]
fn sprite_overflow_flag_is_set_when_nine_in_range_sprites_present() {
let mut ppu = Ppu::new();
let mut cart = NoopCart::default();
ppu.write_ppu_register(0x00, Register::PpuControl, &mut cart);
ppu.write_ppu_register(0x18, Register::PpuMask, &mut cart);
ppu.write_ppu_register(0x20, Register::PpuControl, &mut cart);
for i in 0..9u8 {
oam_write(
&mut ppu,
&mut cart,
i,
113 + i,
0x00,
ATTR_FRONT_OF_BG,
i * 2,
);
}
let total_cycles = (1 + 129) * 341;
for _ in 0..total_cycles {
ppu.run_cycle(&mut cart);
}
let status = ppu.read_ppu_register(Register::PpuStatus, &mut cart);
assert_ne!(
status & STATUS_SPRITE_OVERFLOW,
0,
"SPRITE_OVERFLOW bit 5 should be set when 9 in-range sprites \
are present on the scanline. Status = {status:#04x}."
);
}
#[test]
fn sprite_zero_hit_set_when_sprite0_opaque_overlaps_opaque_bg() {
let mut ppu = Ppu::new();
let mut cart = OpaqueBgCart::default();
ppu.write_ppu_register(0x00, Register::PpuControl, &mut cart);
ppu.write_ppu_register(0x1E, Register::PpuMask, &mut cart);
oam_write(&mut ppu, &mut cart, 0, 0x00, 0x00, ATTR_FRONT_OF_BG, 0x00);
let total_cycles = 2 * 341 + 256;
for _ in 0..total_cycles {
ppu.run_cycle(&mut cart);
}
let status = ppu.read_ppu_register(Register::PpuStatus, &mut cart);
assert_ne!(
status & STATUS_SPRITE_ZERO_HIT,
0,
"SPRITE_ZERO_HIT bit 6 should be set after sprite 0 overlaps an \
opaque background pixel. Status = {status:#04x}."
);
}
#[derive(Default)]
struct OpaqueBgCart;
impl PpuCartMemorySpace for OpaqueBgCart {
fn read(&mut self, _addr: u16) -> u8 {
0xFF
}
fn write(&mut self, _data: u8, _addr: u16) {}
}
#[derive(Default)]
struct DistinctNametableCart;
impl PpuCartMemorySpace for DistinctNametableCart {
fn read(&mut self, addr: u16) -> u8 {
if (0x2000..=0x2FFF).contains(&addr) {
0xAB
} else {
0x00
}
}
fn write(&mut self, _data: u8, _addr: u16) {}
}
#[test]
fn first_ppudata_read_returns_value_at_address_not_zero() {
let mut ppu = Ppu::new();
let mut cart = DistinctNametableCart::default();
ppu.write_ppu_register(0x20, Register::PpuAddr, &mut cart);
ppu.write_ppu_register(0x00, Register::PpuAddr, &mut cart);
let first = ppu.read_ppu_register(Register::Data, &mut cart);
assert_eq!(
first, 0xAB,
"First $2007 read at $2000 should return the value at $2000 \
(the nametable byte), not 0. Got {first:#04x}."
);
}