use super::state::{apply_color_emphasis, nes_rgb, palette_index}; use super::types::{OAM_SIZE, PALETTE_SIZE, Ppu, ScrollEvent, VISIBLE_SCANLINES, VRAM_SIZE}; use crate::native_core::ines::Mirroring; use crate::native_core::mapper::Mapper; use crate::native_core::state_io as sio; const PPU_STATE_CTX: &str = "ppu state"; impl Ppu { pub fn new() -> Self { Self { vram: [0; VRAM_SIZE], palette_ram: [0; PALETTE_SIZE], oam: [0; OAM_SIZE], ctrl: 0, mask: 0, status: 0, oam_addr: 0, write_latch: false, vram_addr: 0, temp_addr: 0, fine_x: 0, scroll_x: 0, scroll_y: 0, read_buffer: 0, io_latch: 0, scroll_events: vec![ScrollEvent { scanline: 0, x_start: 0, scroll_x: 0, scroll_y: 0, base_nt: 0, }], frame_rgba: vec![0; 256 * 240 * 4], bg_shift_pattern_lo: 0, bg_shift_pattern_hi: 0, bg_shift_attr_lo: 0, bg_shift_attr_hi: 0, next_tile_id: 0, next_tile_attr: 0, next_tile_lsb: 0, next_tile_msb: 0, sprite_indices: [0; 8], sprite_count: 0, next_sprite_indices: [0; 8], next_sprite_count: 0, } } pub fn begin_frame(&mut self) { self.scroll_events.clear(); self.scroll_events.push(self.current_scroll_event(0, 0)); self.bg_shift_pattern_lo = 0; self.bg_shift_pattern_hi = 0; self.bg_shift_attr_lo = 0; self.bg_shift_attr_hi = 0; self.next_tile_id = 0; self.next_tile_attr = 0; self.next_tile_lsb = 0; self.next_tile_msb = 0; self.sprite_count = 0; self.next_sprite_count = 0; } pub fn frame_buffer(&self) -> &[u8] { &self.frame_rgba } pub fn render_dot(&mut self, mapper: &dyn Mapper, scanline: u32, dot: u32) { if dot == 0 || dot > 340 { return; } let rendering_active = self.rendering_enabled() && (scanline < 240 || scanline == 261); let bg_fetch_cycle = rendering_active && ((1..=256).contains(&dot) || (321..=336).contains(&dot)); let show_bg = (self.mask & 0x08) != 0; let show_bg_left = (self.mask & 0x02) != 0; let show_spr = (self.mask & 0x10) != 0; let show_spr_left = (self.mask & 0x04) != 0; if scanline < 240 && (1..=256).contains(&dot) { let x = (dot - 1) as usize; let y = scanline as usize; let bg_layer_enabled = show_bg && (x >= 8 || show_bg_left); let (bg_color_index, bg_opaque) = if bg_layer_enabled { self.background_pixel_from_shifters() } else { (self.read_palette(0), false) }; if !self.sprite0_hit_set() && self.sprite0_hit_at(mapper, y, dot) && bg_opaque { self.set_sprite0_hit(true); } let mut final_color = bg_color_index & 0x3F; let sprite_layer_enabled = show_spr && (x >= 8 || show_spr_left); if sprite_layer_enabled && let Some((spr_color_index, behind_bg)) = self.sprite_pixel(mapper, x, y) && !(behind_bg && bg_opaque) { final_color = spr_color_index & 0x3F; } let (r, g, b) = apply_color_emphasis(nes_rgb(final_color), self.mask); let i = (y * 256 + x) * 4; self.frame_rgba[i] = r; self.frame_rgba[i + 1] = g; self.frame_rgba[i + 2] = b; self.frame_rgba[i + 3] = 255; } if bg_fetch_cycle { self.bg_shift_pattern_lo <<= 1; self.bg_shift_pattern_hi <<= 1; self.bg_shift_attr_lo <<= 1; self.bg_shift_attr_hi <<= 1; let phase = dot & 7; match phase { 1 => { let nt_addr = 0x2000 | (self.vram_addr & 0x0FFF); self.next_tile_id = self.read_nt(nt_addr, mapper); } 3 => { let attr_addr = 0x23C0 | (self.vram_addr & 0x0C00) | ((self.vram_addr >> 4) & 0x38) | ((self.vram_addr >> 2) & 0x07); let attr = self.read_nt(attr_addr, mapper); let shift = (((self.vram_addr >> 4) & 4) | (self.vram_addr & 2)) as u8; self.next_tile_attr = (attr >> shift) & 0x03; } 5 => { let table = if (self.ctrl & 0x10) != 0 { 0x1000 } else { 0x0000 }; let fine_y = (self.vram_addr >> 12) & 0x7; let addr = table + ((self.next_tile_id as u16) << 4) + fine_y; self.next_tile_lsb = mapper.ppu_read(addr); } 7 => { let table = if (self.ctrl & 0x10) != 0 { 0x1000 } else { 0x0000 }; let fine_y = (self.vram_addr >> 12) & 0x7; let addr = table + ((self.next_tile_id as u16) << 4) + fine_y + 8; self.next_tile_msb = mapper.ppu_read(addr); } 0 => { self.reload_bg_shifters(); self.increment_coarse_x(); } _ => {} } } if rendering_active { // Transfer pre-evaluated sprite list at the start of each visible scanline, // so dots 1-256 render with the correct sprites for *this* scanline. if scanline < 240 && dot == 1 && self.sprites_enabled() { self.sprite_count = self.next_sprite_count; self.sprite_indices = self.next_sprite_indices; } if dot == 256 { self.increment_fine_y(); } else if dot == 257 { self.copy_horizontal_bits(); if scanline < 240 { if self.sprites_enabled() { let next_scanline = (scanline as usize + 1) % 240; let (count, indices, overflow) = self.evaluate_sprites_for_scanline(next_scanline); self.next_sprite_count = count; self.next_sprite_indices = indices; if overflow { self.set_sprite_overflow(true); } } } else if scanline == 261 { let (count, indices, _) = self.evaluate_sprites_for_scanline(0); self.next_sprite_count = count; self.next_sprite_indices = indices; } } else if scanline == 261 && (280..=304).contains(&dot) { self.copy_vertical_bits(); } } } pub fn note_scroll_register_write(&mut self, scanline: usize, dot: u32) { self.note_scroll_register_write_legacy(scanline, dot); } pub(super) fn background_pixel_from_shifters(&self) -> (u8, bool) { let bit = 0x8000u16 >> self.fine_x; let p0 = u8::from((self.bg_shift_pattern_lo & bit) != 0); let p1 = u8::from((self.bg_shift_pattern_hi & bit) != 0); let pix = p0 | (p1 << 1); if pix == 0 { return (self.read_palette(0), false); } let a0 = u8::from((self.bg_shift_attr_lo & bit) != 0); let a1 = u8::from((self.bg_shift_attr_hi & bit) != 0); let pal = (a0 | (a1 << 1)) << 2 | pix; (self.read_palette(pal as u16), true) } pub(super) fn reload_bg_shifters(&mut self) { self.bg_shift_pattern_lo = (self.bg_shift_pattern_lo & 0xFF00) | self.next_tile_lsb as u16; self.bg_shift_pattern_hi = (self.bg_shift_pattern_hi & 0xFF00) | self.next_tile_msb as u16; let attr_lo = if (self.next_tile_attr & 0x01) != 0 { 0xFF } else { 0x00 }; let attr_hi = if (self.next_tile_attr & 0x02) != 0 { 0xFF } else { 0x00 }; self.bg_shift_attr_lo = (self.bg_shift_attr_lo & 0xFF00) | attr_lo; self.bg_shift_attr_hi = (self.bg_shift_attr_hi & 0xFF00) | attr_hi; } pub(super) fn increment_coarse_x(&mut self) { if (self.vram_addr & 0x001F) == 31 { self.vram_addr &= !0x001F; self.vram_addr ^= 0x0400; } else { self.vram_addr = self.vram_addr.wrapping_add(1); } } pub(super) fn increment_fine_y(&mut self) { if (self.vram_addr & 0x7000) != 0x7000 { self.vram_addr = self.vram_addr.wrapping_add(0x1000); return; } self.vram_addr &= !0x7000; let mut y = (self.vram_addr & 0x03E0) >> 5; if y == 29 { y = 0; self.vram_addr ^= 0x0800; } else if y == 31 { y = 0; } else { y += 1; } self.vram_addr = (self.vram_addr & !0x03E0) | (y << 5); } pub(super) fn copy_horizontal_bits(&mut self) { self.vram_addr = (self.vram_addr & !0x041F) | (self.temp_addr & 0x041F); } pub(super) fn copy_vertical_bits(&mut self) { self.vram_addr = (self.vram_addr & !0x7BE0) | (self.temp_addr & 0x7BE0); } pub(super) fn evaluate_sprites_for_scanline(&self, y: usize) -> (u8, [u8; 8], bool) { let mut indices = [0u8; 8]; let mut count = 0u8; let sprite_height = if (self.ctrl & 0x20) != 0 { 16i16 } else { 8i16 }; let y = y as i16; for i in 0..64usize { let oam_idx = i * 4; let sprite_y = self.oam[oam_idx] as i16 + 1; if y >= sprite_y && y < sprite_y + sprite_height { if count < 8 { indices[count as usize] = i as u8; count += 1; } else { break; } } } let overflow = self.sprite_overflow_on_scanline(y as usize); (count, indices, overflow) } pub fn note_scroll_register_write_legacy(&mut self, scanline: usize, dot: u32) { let mut target_scanline = scanline; let mut x_start = 0u8; if dot <= 256 { if dot > 0 { x_start = (dot - 1) as u8; } } else { target_scanline = target_scanline.saturating_add(1); } if target_scanline >= VISIBLE_SCANLINES { return; } let clamped = target_scanline as u8; let event = self.current_scroll_event(clamped, x_start); if let Some(last) = self.scroll_events.last_mut() && last.scanline == clamped && last.x_start == x_start { *last = event; return; } self.scroll_events.push(event); } pub fn set_vblank(&mut self, enabled: bool) { if enabled { self.status |= 0x80; } else { self.status &= !0x80; } } pub fn set_sprite0_hit(&mut self, enabled: bool) { if enabled { self.status |= 0x40; } else { self.status &= !0x40; } } pub fn set_sprite_overflow(&mut self, enabled: bool) { if enabled { self.status |= 0x20; } else { self.status &= !0x20; } } #[cfg(test)] pub fn sprite_overflow_set(&self) -> bool { (self.status & 0x20) != 0 } pub fn sprite0_hit_set(&self) -> bool { (self.status & 0x40) != 0 } pub fn rendering_enabled(&self) -> bool { (self.mask & 0x18) != 0 } pub fn sprites_enabled(&self) -> bool { (self.mask & 0x10) != 0 } pub fn nmi_enabled(&self) -> bool { (self.ctrl & 0x80) != 0 } pub fn vblank_flag_set(&self) -> bool { (self.status & 0x80) != 0 } pub fn mmc3_scanline_clock_active(&self) -> bool { let bg_enabled = (self.mask & 0x08) != 0; let spr_enabled = (self.mask & 0x10) != 0; if !bg_enabled && !spr_enabled { return false; } let bg_uses_1000 = bg_enabled && (self.ctrl & 0x10) != 0; let sprite_uses_1000 = if !spr_enabled { false } else if (self.ctrl & 0x20) != 0 { // 8x16 sprites select pattern table per-tile; A12 activity is possible. true } else { (self.ctrl & 0x08) != 0 }; bg_uses_1000 || sprite_uses_1000 } } mod memory; mod persistence; mod registers; mod render;