use crate::cartridge::Cartridge; use crate::mapper::Mirroring; pub struct Ppu { ctrl: u8, // $2000 mask: u8, // $2001 status: u8, // $2002 oamaddr: u8, // $2003 oam: [u8; 256], // $2004 scanline: u32, cycle: u32, framebuffer: [u8; 256 * 240], nmi_pending: bool, frame_complete: bool, odd_frame: bool, t: u16, x: u8, pattern_lo_latch: u8, pattern_hi_latch: u8, bg_pattern_lo: u16, bg_pattern_hi: u16, bg_attr_lo: u16, bg_attr_hi: u16, 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: [u8; 0x1000], // 4KB nametables (FourScreen support) palette: [u8; 32], } impl Ppu { pub fn tick(&mut self, cart: &mut Cartridge, cycles: u64) { for _ in 0..(cycles * 3) { self.dot(cart); } } fn dot(&mut self, cart: &mut Cartridge) { // vblank flag + NMI if self.cycle == 0 && self.scanline == 241 { self.status |= 0x80; if self.ctrl & 0x80 != 0 { self.nmi_pending = true; } } // clear vblank if self.cycle == 0 && self.scanline == 261 { self.status &= !0x80; } let render_bg = self.mask & 0x08 != 0; if render_bg && self.scanline < 240 && self.cycle == 0 { // fine-X scroll: consume the first `x` pixels of this scanline self.bg_pattern_lo <<= self.x; self.bg_pattern_hi <<= self.x; self.bg_attr_lo <<= self.x; self.bg_attr_hi <<= self.x; } if render_bg && self.scanline < 240 && self.cycle < 256 { self.render_bg_pixel(cart); } if render_bg && (self.scanline < 240 || self.scanline == 261) { self.bg_fetch(cart); } // advance timing self.cycle += 1; let rendering = self.mask & 0x18 != 0; // bg or sprite render enabled let line_len = if self.scanline == 261 && self.odd_frame && rendering { 340 } else { 341 }; if self.cycle >= line_len { self.cycle = 0; self.scanline += 1; if self.scanline >= 262 { self.scanline = 0; self.frame_complete = true; self.odd_frame = !self.odd_frame; } } } pub fn new() -> Self { Self { ctrl: 0, mask: 0, status: 0, oamaddr: 0, oam: [0; 256], scanline: 0, cycle: 0, framebuffer: [0; 256 * 240], nmi_pending: false, frame_complete: false, odd_frame: false, t: 0, x: 0, pattern_lo_latch: 0, pattern_hi_latch: 0, bg_pattern_lo: 0, bg_pattern_hi: 0, bg_attr_lo: 0, bg_attr_hi: 0, tile_latch: 0, attr_latch: 0, write_latch: false, vram_addr: 0, data_buffer: 0, vram: [0; 0x1000], palette: [0; 32], } } pub fn read(&mut self, cart: &mut Cartridge, addr: u16, open_bus: u8) -> u8 { match addr & 0x2007 { 0x2000 => open_bus, // write-only: returns bus contents 0x2001 => open_bus, // write-only 0x2002 => { let s = (self.status & 0xE0) | (open_bus & 0x1F); self.status &= !0x80; // reading clears the vblank flag self.write_latch = false; s } 0x2003 => open_bus, // write-only 0x2004 => self.oam[self.oamaddr as usize], 0x2005 => open_bus, // write-only 0x2006 => open_bus, // write-only 0x2007 => { let data = self.vram_read(cart, self.vram_addr); let result = if self.vram_addr >= 0x3F00 { data } else { self.data_buffer }; self.data_buffer = data; self.vram_addr = self.increment_addr(); result } _ => open_bus, } } pub fn write(&mut self, cart: &mut Cartridge, addr: u16, val: u8) { match addr & 0x2007 { 0x2000 => self.ctrl = val, 0x2001 => self.mask = val, 0x2002 => {} // write ignored 0x2003 => self.oamaddr = val, 0x2004 => { self.oam[self.oamaddr as usize] = val; self.oamaddr = self.oamaddr.wrapping_add(1); } 0x2005 => { if !self.write_latch { self.t = (self.t & 0x7FE0) | ((val >> 3) as u16); // coarse X (bits 0-4) self.x = val & 0x07; // fine X } else { self.t = (self.t & 0x0C1F) // keep fine Y + nametable Y + coarse X | (((val & 0xF8) as u16) << 2) // coarse Y -> bits 5-9 | ((val & 0x07) as u16) << 12; // fine Y -> bits 12-14 } self.write_latch = !self.write_latch; } 0x2006 => { if !self.write_latch { self.t = (self.t & 0x00FF) | (((val & 0x3F) as u16) << 8); // high 6 bits -> bits 8-13 } else { self.t = (self.t & 0x7F00) | (val as u16); // low 8 bits self.vram_addr = self.t; // v = t } self.write_latch = !self.write_latch; } 0x2007 => { self.vram_write(cart, self.vram_addr, val); self.vram_addr = self.increment_addr(); } _ => {} } } fn increment_addr(&self) -> u16 { let step = if self.ctrl & 0x04 != 0 { 32 } else { 1 }; self.vram_addr.wrapping_add(step) & 0x3FFF } fn vram_read(&mut self, cart: &mut Cartridge, addr: u16) -> u8 { match addr { 0x0000..=0x1FFF => cart.read_chr(addr), // pattern tables -> CHR 0x2000..=0x3EFF => self.nametable_read(cart, addr), 0x3F00..=0x3FFF => self.palette_read(addr), _ => 0, } } fn vram_write(&mut self, cart: &mut Cartridge, addr: u16, val: u8) { match addr { 0x0000..=0x1FFF => cart.write_chr(addr, val), // CHR-RAM writes through 0x2000..=0x3EFF => { let off = self.nametable_offset(cart, addr); self.vram[off] = val; } 0x3F00..=0x3FFF => { let i = palette_index(addr); self.palette[i] = val; } _ => {} } } fn nametable_offset(&self, cart: &Cartridge, addr: u16) -> usize { let a = addr & 0x2FFF; // fold the $3000-$3EFF mirror into $2000-$2FFF let table = match cart.mirroring() { Mirroring::Horizontal => (a >> 11) & 1, Mirroring::Vertical => (a >> 10) & 1, Mirroring::OneScreenLow => 0, Mirroring::OneScreenHigh => 1, Mirroring::FourScreen => (a >> 10) & 3, }; table as usize * 0x400 + (a & 0x3FF) as usize } fn nametable_read(&self, cart: &Cartridge, addr: u16) -> u8 { self.vram[self.nametable_offset(cart, addr)] } fn palette_read(&self, addr: u16) -> u8 { self.palette[palette_index(addr)] } fn bg_fetch(&mut self, cart: &mut Cartridge) { // shift registers move left every dot self.bg_pattern_lo <<= 1; self.bg_pattern_hi <<= 1; self.bg_attr_lo <<= 1; self.bg_attr_hi <<= 1; if self.cycle == 255 && self.scanline < 240 { self.copy_horizontal(); // dot 255: reload coarse X from t } if self.cycle == 256 && (self.scanline < 240 || self.scanline == 261) { self.increment_vertical(); // dot 256: advance to next scanline's tiles } if self.cycle == 280 && self.scanline == 261 { self.copy_vertical(); // dot 280 (pre-render): reload vertical bits from t } match self.cycle % 8 { 0 => self.tile_latch = self.nametable_read(cart, 0x2000 | (self.vram_addr & 0x0FFF)), 2 => { // attribute byte address (coarseY>>2, coarseX>>2, nametable select) let addr = 0x23C0 | (self.vram_addr & 0x0C00) | ((self.vram_addr >> 4) & 0x38) | ((self.vram_addr >> 2) & 0x07); let byte = self.nametable_read(cart, addr); // select the 2 bits for this tile's 2x2 block let shift = ((self.vram_addr & 0x40) >> 4) | (self.vram_addr & 0x02); self.attr_latch = (byte >> shift) & 0x03; } 4 => { let base = ((self.ctrl & 0x10) as u16) << 8; // BG pattern table select let fine_y = (self.vram_addr >> 12) & 0x07; self.pattern_lo_latch = cart.read_chr(base | ((self.tile_latch as u16) << 4) | fine_y); } 6 => { let base = ((self.ctrl & 0x10) as u16) << 8; let fine_y = (self.vram_addr >> 12) & 0x07; self.pattern_hi_latch = cart.read_chr(base | ((self.tile_latch as u16) << 4) | fine_y + 8); } 7 => { // reload shift registers with the fetched tile self.bg_pattern_lo = (self.bg_pattern_lo & 0xFF00) | self.pattern_lo_latch as u16; self.bg_pattern_hi = (self.bg_pattern_hi & 0xFF00) | self.pattern_hi_latch as u16; // 0xFF/0x00 per attribute bit -> constant top bit during the tile's 8 pixels self.bg_attr_lo = (self.bg_attr_lo & 0xFF00) | if self.attr_latch & 0x01 != 0 { 0xFF } else { 0x00 }; self.bg_attr_hi = (self.bg_attr_hi & 0xFF00) | if self.attr_latch & 0x02 != 0 { 0xFF } else { 0x00 }; self.increment_coarse_x(); } _ => {} } } fn increment_coarse_x(&mut self) { if self.vram_addr & 0x001F == 0x001F { // coarse X wraps 31 -> 0 self.vram_addr &= !0x001F; self.vram_addr ^= 0x0400; // flip nametable X bit } else { self.vram_addr += 1; } } fn increment_vertical(&mut self) { if self.vram_addr & 0x7000 != 0x7000 { self.vram_addr += 0x1000; // fine Y++ } else { self.vram_addr &= !0x7000; // fine Y = 0 let mut y = (self.vram_addr >> 5) & 0x1F; // coarse Y if y == 29 { y = 0; self.vram_addr ^= 0x0800; // flip nametable Y } else if y == 31 { y = 0; } else { y += 1; } self.vram_addr = (self.vram_addr & !0x03E0) | (y << 5); } } fn copy_horizontal(&mut self) { // t's coarse X + nametable X -> v self.vram_addr = (self.vram_addr & !0x041F) | (self.t & 0x041F); } fn copy_vertical(&mut self) { // t's fine Y + coarse Y + nametable Y -> v self.vram_addr = (self.vram_addr & !0x7BE0) | (self.t & 0x7BE0); } fn render_bg_pixel(&mut self, _cart: &mut Cartridge) { // fine-X offset: the first `x` dots render from the pre-fetched tile let pattern_lo_bit = (self.bg_pattern_lo >> 15) & 1; let pattern_hi_bit = (self.bg_pattern_hi >> 15) & 1; let attr_lo_bit = (self.bg_attr_lo >> 15) & 1; let attr_hi_bit = (self.bg_attr_hi >> 15) & 1; let pixel = (pattern_hi_bit << 1) | pattern_lo_bit; let palette_bits = (attr_hi_bit << 1) | attr_lo_bit; let color = if pixel == 0 { self.palette[0] // "universal background" transparent color } else { self.palette[(palette_bits * 4 + pixel) as usize] // background palette }; let idx = (self.scanline as usize) * 256 + (self.cycle as usize); self.framebuffer[idx] = color; } pub fn frame_done(&self) -> bool { self.frame_complete } pub fn take_nmi(&mut self) -> bool { let n = self.nmi_pending; self.nmi_pending = false; n } pub fn framebuffer(&self) -> &[u8] { &self.framebuffer } pub fn begin_frame(&mut self) { self.frame_complete = false; self.scanline = 0; self.cycle = 0; } // read-only accessors for the headless state dump pub fn ctrl(&self) -> u8 { self.ctrl } pub fn mask(&self) -> u8 { self.mask } pub fn status(&self) -> u8 { self.status } pub fn scanline(&self) -> u32 { self.scanline } pub fn cycle(&self) -> u32 { self.cycle } pub fn vram_addr(&self) -> u16 { self.vram_addr } pub fn t(&self) -> u16 { self.t } pub fn palette(&self) -> &[u8; 32] { &self.palette } pub fn vram(&self) -> &[u8; 0x1000] { &self.vram } pub fn framebuffer_nonzero(&self) -> usize { self.framebuffer.iter().filter(|&&b| b != 0).count() } } fn palette_index(addr: u16) -> usize { let mut i = (addr & 0x1F) as usize; // $3F20-$3FFF folds into $3F00-$3F1F if i >= 0x10 && i % 4 == 0 { i -= 0x10; } // 10/14/18/1C mirror to 00/04/08/0C i }