PPU implementation with sprites

This commit is contained in:
2026-08-09 23:27:39 -05:00
parent 5d48780ad5
commit cda0a7ddb4
3 changed files with 237 additions and 30 deletions
+208 -20
View File
@@ -1,12 +1,20 @@
use crate::cartridge::Cartridge;
use crate::mapper::Mirroring;
// 4-byte OAM entry: Y, tile, attribute, X
#[derive(Clone, Copy, Default)]
struct Sprite {
y: u8,
tile: u8,
attr: u8,
x: u8,
}
pub struct Ppu {
ctrl: u8, // $2000
mask: u8, // $2001
status: u8, // $2002
oamaddr: u8, // $2003
oam: [u8; 256], // $2004
ctrl: u8, // $2000
mask: u8, // $2001
status: u8, // $2002
oamaddr: u8, // $2003
oam: [u8; 256], // $2004
scanline: u32,
cycle: u32,
framebuffer: [u8; 256 * 240],
@@ -24,10 +32,16 @@ pub struct Ppu {
tile_latch: u8,
attr_latch: u8,
write_latch: bool, // shared W toggle for $2005/$2006, reset by $2002 read
vram_addr: u16, // 14-bit
data_buffer: u8, // $2007 read-buffer
vram_addr: u16, // 14-bit
data_buffer: u8, // $2007 read-buffer
vram: [u8; 0x1000], // 4KB nametables (FourScreen support)
palette: [u8; 32],
sprite_scanline: [Sprite; 8], // 8 sprites selected for the current scanline
sprite_count: u8, // how many were selected (up to 9 for overflow)
sprite_shifter_lo: [u8; 8], // per-sprite pattern bitplanes
sprite_shifter_hi: [u8; 8],
sprite_zero_hit_possible: bool, // sprite 0 is in this scanline's set
sprite_zero_being_rendered: bool, // sprite 0 has a non-transparent pixel here
}
impl Ppu {
@@ -50,9 +64,16 @@ impl Ppu {
self.status &= !0x80;
}
let rendering = self.mask & 0x08 != 0;
let rendering = self.mask & 0x18 != 0; // bg OR sprites
// Background pipeline: visible + pre-render scanlines only
// Pre-render line start: clear sprite flags + shifters (reference: -1, cycle 1)
if self.scanline == 261 && self.cycle == 1 {
self.status &= !0x60; // sprite overflow + sprite zero hit
self.sprite_shifter_lo = [0; 8];
self.sprite_shifter_hi = [0; 8];
}
// Background + sprite pipeline: visible + pre-render scanlines only
if rendering && (self.scanline < 240 || self.scanline == 261) {
// Shifters shift ONLY during the fetch windows (reference timing)
if (self.cycle >= 2 && self.cycle < 258) || (self.cycle >= 321 && self.cycle < 338) {
@@ -60,6 +81,11 @@ impl Ppu {
self.bg_fetch(cart);
}
// Sprite X countdown + shifter shifting (reference: cycle 1-257)
if self.cycle >= 1 && self.cycle < 258 {
self.sprite_shift();
}
// End of visible scanline: advance vertical position
if self.cycle == 256 && (self.scanline < 240 || self.scanline == 261) {
self.increment_vertical();
@@ -68,6 +94,7 @@ impl Ppu {
if self.cycle == 257 && (self.scanline < 240 || self.scanline == 261) {
self.load_shifters();
self.copy_horizontal();
self.sprite_evaluate(); // pick sprites for the next scanline
}
// Idle nametable fetches at the end of the line
if (self.cycle == 338 || self.cycle == 340) && (self.scanline < 240 || self.scanline == 261) {
@@ -77,11 +104,15 @@ impl Ppu {
if self.scanline == 261 && self.cycle >= 280 && self.cycle < 305 {
self.copy_vertical();
}
// Load the 8 selected sprites' patterns into shifters for the next scanline
if self.cycle == 340 && (self.scanline < 240 || self.scanline == 261) {
self.sprite_fetch_patterns(cart);
}
}
// Compose + store pixel (visible only, x = cycle - 1)
if rendering && self.scanline < 240 && self.cycle >= 1 && self.cycle <= 256 {
self.render_bg_pixel(cart);
self.render_pixel(cart); // was render_bg_pixel
}
// advance timing
@@ -112,6 +143,12 @@ impl Ppu {
tile_latch: 0, attr_latch: 0,
write_latch: false, vram_addr: 0, data_buffer: 0,
vram: [0; 0x1000], palette: [0; 32],
sprite_scanline: [Sprite::default(); 8],
sprite_count: 0,
sprite_shifter_lo: [0; 8],
sprite_shifter_hi: [0; 8],
sprite_zero_hit_possible: false,
sprite_zero_being_rendered: false,
}
}
@@ -271,6 +308,107 @@ impl Ppu {
}
}
pub fn oam_dma(&mut self, ram: &[u8; 0x800], page: u8) {
for i in 0..256u16 {
let src = (((page as u16) << 8) + i) & 0x7FF; // CPU RAM mirrors every 2KB
self.oam[i as usize] = ram[src as usize];
}
}
// Pick the up-to-8 sprites that overlap the NEXT scanline (cycle 257).
fn sprite_evaluate(&mut self) {
self.sprite_count = 0;
self.sprite_zero_hit_possible = false;
self.sprite_scanline = [Sprite::default(); 8];
// The scanline these sprites will be drawn on (pre-render -> scanline 0)
let target = if self.scanline == 261 { 0 } else { self.scanline as i32 + 1 };
let height: i32 = if self.ctrl & 0x20 != 0 { 16 } else { 8 };
let mut entry: usize = 0;
while entry < 64 && self.sprite_count < 9 {
let y = self.oam[entry * 4] as i32;
let diff = target - y;
if diff >= 0 && diff < height {
if self.sprite_count < 8 {
if entry == 0 {
self.sprite_zero_hit_possible = true;
}
let i = self.sprite_count as usize;
self.sprite_scanline[i] = Sprite {
y: self.oam[entry * 4],
tile: self.oam[entry * 4 + 1],
attr: self.oam[entry * 4 + 2],
x: self.oam[entry * 4 + 3],
};
}
self.sprite_count += 1;
}
entry += 1;
}
// sprite overflow flag (status bit 5)
self.status = (self.status & !0x20) | if self.sprite_count > 8 { 0x20 } else { 0 };
}
// Decrement sprite X until the sprite's screen position, then shift its pattern.
fn sprite_shift(&mut self) {
if self.mask & 0x10 == 0 { return; } // sprite rendering disabled
for i in 0..(self.sprite_count.min(8) as usize) {
if self.sprite_scanline[i].x > 0 {
self.sprite_scanline[i].x -= 1;
} else {
self.sprite_shifter_lo[i] <<= 1;
self.sprite_shifter_hi[i] <<= 1;
}
}
}
// Fetch the pattern rows for the 8 selected sprites (cycle 340).
fn sprite_fetch_patterns(&mut self, cart: &mut Cartridge) {
let n = self.sprite_count.min(8) as usize;
for i in 0..n {
let s = self.sprite_scanline[i];
let row = (self.scanline as i32 + 1 - s.y as i32) & 0x0F; // row within sprite (0-15)
let addr_lo: u16 = if self.ctrl & 0x20 == 0 {
// 8x8 sprite: pattern table from ctrl bit 3
let base = ((self.ctrl & 0x08) as u16) << 12;
let tile = s.tile as u16;
if s.attr & 0x80 != 0 {
base | (tile << 4) | (7 - (row & 7)) as u16 // V-flipped
} else {
base | (tile << 4) | (row & 7) as u16
}
} else {
// 8x16 sprite: pattern table from tile id bit 0, two tiles stacked
let table = (s.tile & 0x01) as u16;
let tile_even = (s.tile & 0xFE) as u16;
let row_lo = (row & 7) as u16;
if s.attr & 0x80 != 0 {
// V-flipped: bottom tile first
if row < 8 {
(table << 12) | ((tile_even + 1) << 4) | (7 - row_lo)
} else {
(table << 12) | (tile_even << 4) | (7 - row_lo)
}
} else if row < 8 {
(table << 12) | (tile_even << 4) | row_lo
} else {
(table << 12) | ((tile_even + 1) << 4) | row_lo
}
};
let mut lo = cart.read_chr(addr_lo);
let mut hi = cart.read_chr(addr_lo + 8); // hi plane always +8
if s.attr & 0x40 != 0 { // H-flip
lo = lo.reverse_bits();
hi = hi.reverse_bits();
}
self.sprite_shifter_lo[i] = lo;
self.sprite_shifter_hi[i] = hi;
}
}
fn increment_coarse_x(&mut self) {
if self.vram_addr & 0x001F == 0x001F { // coarse X wraps 31 -> 0
self.vram_addr &= !0x001F;
@@ -306,22 +444,72 @@ impl Ppu {
self.vram_addr = (self.vram_addr & !0x7BE0) | (self.t & 0x7BE0);
}
fn render_bg_pixel(&mut self, _cart: &mut Cartridge) {
let bit_mux = 0x8000u16 >> self.x;
let pattern_lo_bit = (self.bg_pattern_lo & bit_mux) != 0;
let pattern_hi_bit = (self.bg_pattern_hi & bit_mux) != 0;
let attr_lo_bit = (self.bg_attr_lo & bit_mux) != 0;
let attr_hi_bit = (self.bg_attr_hi & bit_mux) != 0;
fn render_pixel(&mut self, _cart: &mut Cartridge) {
// ---- background pixel (mask bit 3) ----
let mut bg_pixel = 0u8;
let mut bg_palette = 0u8;
if self.mask & 0x08 != 0 {
let bit_mux = 0x8000u16 >> self.x;
let lo = (self.bg_pattern_lo & bit_mux) != 0;
let hi = (self.bg_pattern_hi & bit_mux) != 0;
let al = (self.bg_attr_lo & bit_mux) != 0;
let ah = (self.bg_attr_hi & bit_mux) != 0;
bg_pixel = (hi as u8) << 1 | lo as u8;
bg_palette = (ah as u8) << 1 | al as u8;
}
let pixel = (pattern_hi_bit as u8) << 1 | pattern_lo_bit as u8;
let palette_bits = (attr_hi_bit as u8) << 1 | attr_lo_bit as u8;
// ---- foreground (sprite) pixel (mask bit 4) ----
let mut fg_pixel = 0u8;
let mut fg_palette = 0u8;
let mut fg_priority = false;
self.sprite_zero_being_rendered = false;
if self.mask & 0x10 != 0 {
for i in 0..(self.sprite_count.min(8) as usize) {
if self.sprite_scanline[i].x == 0 {
let lo = (self.sprite_shifter_lo[i] & 0x80) != 0;
let hi = (self.sprite_shifter_hi[i] & 0x80) != 0;
let pixel = (hi as u8) << 1 | lo as u8;
if pixel != 0 {
fg_pixel = pixel;
fg_palette = (self.sprite_scanline[i].attr & 0x03) + 0x04; // sprites use palettes 4-7
fg_priority = self.sprite_scanline[i].attr & 0x20 == 0; // bit5=0 -> in front
if i == 0 {
self.sprite_zero_being_rendered = true;
}
break; // first non-transparent sprite wins (priority order)
}
}
}
}
// ---- composite ----
let (pixel, palette) = match (bg_pixel, fg_pixel) {
(0, 0) => (0, 0),
(0, f) => (f, fg_palette),
(b, 0) => (b, bg_palette),
(b, f) if fg_priority => (f, fg_palette),
(b, _) => (b, bg_palette),
};
// ---- sprite zero hit (status bit 6) ----
if self.sprite_zero_hit_possible
&& self.sprite_zero_being_rendered
&& bg_pixel != 0
&& fg_pixel != 0
&& self.mask & 0x08 != 0
&& self.mask & 0x10 != 0
{
if self.cycle >= 9 && self.cycle < 258 { // skip leftmost 8px, both enabled
self.status |= 0x40;
}
}
// ---- write framebuffer ----
let color = if pixel == 0 {
self.palette[0]
} else {
self.palette[(palette_bits * 4 + pixel) as usize]
self.palette[(palette * 4 + pixel) as usize]
};
let idx = (self.scanline as usize) * 256 + (self.cycle as usize - 1);
self.framebuffer[idx] = color;
}