use crate::{
cart::Cart,
common::{Clocked, NesRegion, Powered},
mapper::Mapped,
memory::{MemRead, MemWrite, Memory, RamState},
ppu::{frame::NTSC_PALETTE, vram::ATTR_OFFSET},
};
use frame::Frame;
use ppu_regs::{PpuRegs, COARSE_X_MASK, COARSE_Y_MASK, NT_X_MASK, NT_Y_MASK};
use serde::{Deserialize, Serialize};
use sprite::Sprite;
use std::{cmp::Ordering, fmt};
use vram::{
Vram, ATTR_START, NT_SIZE, NT_START, PALETTE_END, PALETTE_SIZE, PALETTE_START, SYSTEM_PALETTE,
SYSTEM_PALETTE_SIZE,
};
pub mod frame;
pub mod ppu_regs;
pub mod sprite;
pub mod vram;
pub const RENDER_WIDTH: u32 = 256;
pub const RENDER_HEIGHT: u32 = 240;
pub const RENDER_CHANNELS: usize = 4;
pub const RENDER_PITCH: usize = RENDER_CHANNELS * RENDER_WIDTH as usize;
const RENDER_PIXELS: usize = (RENDER_WIDTH * RENDER_HEIGHT) as usize;
pub const RENDER_SIZE: usize = RENDER_CHANNELS * RENDER_PIXELS;
pub const PATTERN_WIDTH: u32 = RENDER_WIDTH / 2;
pub const PATTERN_PIXELS: usize = (PATTERN_WIDTH * PATTERN_WIDTH) as usize;
pub const PATTERN_SIZE: usize = RENDER_CHANNELS * PATTERN_PIXELS;
const IDLE_CYCLE: u32 = 0; const VISIBLE_CYCLE_START: u32 = 1; const VBLANK_CYCLE: u32 = 1;
const VISIBLE_CYCLE_END: u32 = 256; const OAM_CLEAR_CYCLE_END: u32 = 64;
const SPR_EVAL_CYCLE_START: u32 = 65;
const SPR_EVAL_CYCLE_END: u32 = 256;
const SPR_FETCH_CYCLE_START: u32 = 257; const SPR_FETCH_CYCLE_END: u32 = 320; const COPY_Y_CYCLE_START: u32 = 280; const COPY_Y_CYCLE_END: u32 = 304; const INC_Y_CYCLE: u32 = 256; const COPY_X_CYCLE: u32 = 257; const BG_PREFETCH_CYCLE_START: u32 = 321; const BG_PREFETCH_CYCLE_END: u32 = 336; const BG_DUMMY_CYCLE_START: u32 = 337; const CYCLE_SKIP: u32 = 339; const CYCLE_END: u32 = 340; const POWER_ON_CYCLES: usize = 29658 * 3;
const VISIBLE_SCANLINE_END: u32 = 239;
pub const OAM_SIZE: usize = 64 * 4; pub const SECONDARY_OAM_SIZE: usize = 8 * 4;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[must_use]
pub enum VideoFilter {
Pixellate,
Ntsc,
}
impl VideoFilter {
pub const fn as_slice() -> &'static [Self] {
&[Self::Pixellate, Self::Ntsc]
}
}
impl Default for VideoFilter {
fn default() -> Self {
Self::Ntsc
}
}
impl AsRef<str> for VideoFilter {
fn as_ref(&self) -> &str {
match self {
Self::Pixellate => "Pixellate",
Self::Ntsc => "NTSC",
}
}
}
impl From<usize> for VideoFilter {
fn from(value: usize) -> Self {
if value == 1 {
Self::Ntsc
} else {
Self::Pixellate
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[must_use]
pub enum Mirroring {
Horizontal,
Vertical,
SingleScreenA,
SingleScreenB,
FourScreen,
}
impl Default for Mirroring {
fn default() -> Self {
Mirroring::Horizontal
}
}
#[derive(Debug, Clone)]
#[must_use]
pub struct Viewer {
pub scanline: u32,
pub nametables: Vec<Vec<u8>>,
pub nametable_ids: Vec<u8>,
pub pattern_tables: [Vec<u8>; 2],
pub palette: Vec<u8>,
pub palette_ids: Vec<u8>,
}
impl Default for Viewer {
fn default() -> Self {
Self {
scanline: 0,
nametables: vec![
vec![0; RENDER_SIZE],
vec![0; RENDER_SIZE],
vec![0; RENDER_SIZE],
vec![0; RENDER_SIZE],
],
nametable_ids: vec![0; 4 * NT_SIZE as usize],
pattern_tables: [vec![0; PATTERN_SIZE], vec![0; PATTERN_SIZE]],
palette: vec![0; (PALETTE_SIZE + 4) * 4],
palette_ids: vec![0; (PALETTE_SIZE + 4) * 4],
}
}
}
#[derive(Clone, Serialize, Deserialize)]
#[must_use]
pub struct Ppu {
pub cycle: u32, pub cycle_count: usize, pub scanline: u32, pub nes_region: NesRegion,
pub master_clock: u64,
pub clock_divider: u64,
pub vblank_scanline: u32,
pub prerender_scanline: u32,
pub pal_spr_eval_scanline: u32,
pub nmi_pending: bool, pub prevent_vbl: bool,
pub oam_dma: bool,
pub oam_dma_offset: u8,
pub vram: Vram, pub regs: PpuRegs, pub oamaddr_lo: u8,
pub oamaddr_hi: u8,
pub oamaddr: u8, pub oam: Memory, pub secondary_oamaddr: u8,
pub secondary_oam: Memory, pub oam_fetch: u8,
pub oam_eval_done: bool,
pub overflow_count: u8,
pub sprite_in_range: bool,
pub sprite0_in_range: bool,
pub sprite0_visible: bool,
pub sprite_count: u8,
pub sprites: [Sprite; 8], pub frame: Frame, pub filter: VideoFilter,
#[serde(skip)]
pub viewer: Option<Viewer>,
}
impl Ppu {
pub fn new(nes_region: NesRegion) -> Self {
let mut ppu = Self {
cycle: 0,
cycle_count: 0,
scanline: 0,
nes_region,
master_clock: 0,
clock_divider: 0,
vblank_scanline: 0,
prerender_scanline: 0,
pal_spr_eval_scanline: 0,
nmi_pending: false,
prevent_vbl: false,
oam_dma: false,
oam_dma_offset: 0x00,
regs: PpuRegs::new(nes_region),
oamaddr_lo: 0x00,
oamaddr_hi: 0x00,
oamaddr: 0x00,
oam: Memory::ram(OAM_SIZE, RamState::AllOnes),
secondary_oamaddr: 0x00,
secondary_oam: Memory::ram(SECONDARY_OAM_SIZE, RamState::AllOnes),
oam_fetch: 0xFF,
oam_eval_done: false,
overflow_count: 0,
sprite_in_range: false,
sprite0_in_range: false,
sprite0_visible: false,
sprite_count: 0,
sprites: [Sprite::new(); 8],
vram: Vram::new(),
frame: Frame::new(),
filter: VideoFilter::Ntsc,
viewer: None,
};
ppu.set_nes_region(nes_region);
ppu
}
pub fn set_nes_region(&mut self, nes_region: NesRegion) {
let (clock_divider, vblank_scanline, prerender_scanline) = match nes_region {
NesRegion::Ntsc => (4, 241, 261),
NesRegion::Pal => (5, 241, 311),
NesRegion::Dendy => (5, 291, 311),
};
self.nes_region = nes_region;
self.clock_divider = clock_divider;
self.vblank_scanline = vblank_scanline;
self.prerender_scanline = prerender_scanline;
self.pal_spr_eval_scanline = self.vblank_scanline + 24; self.regs.set_nes_region(nes_region);
}
#[inline]
pub fn load_cart(&mut self, cart: &mut Box<Cart>) {
self.vram.cart = &mut **cart;
}
#[inline]
pub fn open_viewer(&mut self) {
self.viewer = Some(Viewer::default());
self.load_nametables();
self.load_pattern_tables();
self.load_palettes();
}
#[inline]
pub fn close_viewer(&mut self) {
self.viewer = None;
}
#[inline]
pub fn update_viewer(&mut self) {
if let Some(ref viewer) = self.viewer {
if self.cycle == IDLE_CYCLE && self.scanline == viewer.scanline {
self.load_nametables();
self.load_pattern_tables();
self.load_palettes();
}
}
}
#[inline]
pub fn set_viewer_scanline(&mut self, scanline: u32) {
if let Some(ref mut viewer) = self.viewer {
viewer.scanline = scanline;
}
}
#[must_use]
#[inline]
pub fn frame_buffer(&mut self) -> &[u8] {
match self.filter {
VideoFilter::Pixellate => self.decode_buffer(),
VideoFilter::Ntsc => self.apply_ntsc_filter(),
}
}
fn decode_buffer(&mut self) -> &[u8] {
for (idx, color) in self.frame.current_buffer.iter().enumerate() {
let color_idx = ((*color as usize) & (SYSTEM_PALETTE_SIZE - 1)) * 3;
if let [red, green, blue] = SYSTEM_PALETTE[color_idx..=color_idx + 2] {
let idx = idx << 2;
self.frame.output_buffer[idx..=idx + 3].copy_from_slice(&[red, green, blue, 255]);
}
}
&self.frame.output_buffer
}
fn apply_ntsc_filter(&mut self) -> &[u8] {
for (idx, pixel) in self.frame.current_buffer.iter().enumerate() {
let x = idx % 256;
let y = idx / 256;
let even_phase = if self.frame.num & 0x01 == 0x01 { 0 } else { 1 };
let phase = (2 + y * 341 + x + even_phase) % 3;
let color = if x == 0 {
0
} else {
NTSC_PALETTE[phase][(self.frame.prev_pixel & 0x3F) as usize][*pixel as usize]
};
self.frame.prev_pixel = u32::from(*pixel);
let idx = idx << 2;
let red = (color >> 16 & 0xFF) as u8;
let green = (color >> 8 & 0xFF) as u8;
let blue = (color & 0xFF) as u8;
self.frame.output_buffer[idx..=idx + 3].copy_from_slice(&[red, green, blue, 255]);
}
&self.frame.output_buffer
}
fn load_nametables(&mut self) {
if let Some(ref mut viewer) = self.viewer {
for (i, nametable) in viewer.nametables.iter_mut().enumerate() {
let base_addr = NT_START + (i as u16) * NT_SIZE;
for addr in base_addr..(base_addr + NT_SIZE - 64) {
let x_scroll = addr & COARSE_X_MASK;
let y_scroll = (addr & COARSE_Y_MASK) >> 5;
let nt_base_addr = NT_START + (addr & (NT_X_MASK | NT_Y_MASK));
let tile = self.vram.peek(addr);
let tile_addr = self.regs.background_select() + u16::from(tile) * 16;
let supertile = (x_scroll / 4) + (y_scroll / 4) * 8;
let attr = u16::from(self.vram.peek(nt_base_addr + ATTR_OFFSET + supertile));
let corner = ((x_scroll % 4) / 2 + (y_scroll % 4) / 2 * 2) << 1;
let mask = 0x03 << corner;
let palette = (attr & mask) >> corner;
let tile_num = x_scroll + y_scroll * 32;
let tile_x = (tile_num % 32) * 8;
let tile_y = (tile_num / 32) * 8;
viewer.nametable_ids[(addr - NT_START) as usize] = tile;
for y in 0..8 {
let lo = u16::from(self.vram.peek(tile_addr + y));
let hi = u16::from(self.vram.peek(tile_addr + y + 8));
for x in 0..8 {
let pix_type = ((lo >> x) & 1) + (((hi >> x) & 1) << 1);
let palette_idx =
self.vram.peek(PALETTE_START + palette * 4 + pix_type) as usize;
let x = u32::from(tile_x + (7 - x));
let y = u32::from(tile_y + y);
Self::put_pixel(palette_idx, x, y, RENDER_WIDTH, nametable);
}
}
}
}
}
}
fn load_pattern_tables(&mut self) {
if let Some(ref mut viewer) = self.viewer {
let width = RENDER_WIDTH / 2;
for (i, pattern_table) in viewer.pattern_tables.iter_mut().enumerate() {
let start = (i as u16) * 0x1000;
let end = start + 0x1000;
for tile_addr in (start..end).step_by(16) {
let tile_x = ((tile_addr % 0x1000) % 256) / 2;
let tile_y = ((tile_addr % 0x1000) / 256) * 8;
for y in 0..8 {
let lo = u16::from(self.vram.peek(tile_addr + y));
let hi = u16::from(self.vram.peek(tile_addr + y + 8));
for x in 0..8 {
let pix_type = ((lo >> x) & 1) + (((hi >> x) & 1) << 1);
let palette_idx = self.vram.peek(PALETTE_START + pix_type) as usize;
let x = u32::from(tile_x + (7 - x));
let y = u32::from(tile_y + y);
Self::put_pixel(palette_idx, x, y, width, pattern_table);
}
}
}
}
}
}
fn load_palettes(&mut self) {
if let Some(ref mut viewer) = self.viewer {
let width = 16;
for addr in PALETTE_START..PALETTE_END {
let x = u32::from((addr - PALETTE_START) % 16);
let y = u32::from((addr - PALETTE_START) / 16);
let palette_idx = self.vram.peek(addr);
viewer.palette_ids[y as usize * width + x as usize] = palette_idx;
Self::put_pixel(
palette_idx as usize,
x,
y,
width as u32,
&mut viewer.palette,
);
}
}
}
#[inline]
fn run_cycle(&mut self) {
let visible_cycle = matches!(self.cycle, VISIBLE_CYCLE_START..=VISIBLE_CYCLE_END);
let bg_prefetch_cycle =
matches!(self.cycle, BG_PREFETCH_CYCLE_START..=BG_PREFETCH_CYCLE_END);
let bg_dummy_cycle = matches!(self.cycle, BG_DUMMY_CYCLE_START..=CYCLE_END);
let bg_fetch_cycle = bg_prefetch_cycle || visible_cycle;
let spr_eval_cycle = matches!(self.cycle, VISIBLE_CYCLE_START..=SPR_EVAL_CYCLE_END);
let spr_fetch_cycle = matches!(self.cycle, SPR_FETCH_CYCLE_START..=SPR_FETCH_CYCLE_END);
let spr_dummy_cycle = matches!(self.cycle, BG_PREFETCH_CYCLE_START..=CYCLE_END);
let visible_scanline = self.scanline <= VISIBLE_SCANLINE_END;
let prerender_scanline = self.scanline == self.prerender_scanline;
let render_scanline = prerender_scanline || visible_scanline;
if visible_cycle && visible_scanline {
self.render_pixel();
}
if self.rendering_enabled() {
if visible_scanline
|| (self.nes_region == NesRegion::Pal
&& self.scanline >= self.pal_spr_eval_scanline)
{
if spr_eval_cycle {
self.evaluate_sprites();
} else if spr_fetch_cycle {
self.write_oamaddr(0x00);
}
}
if render_scanline {
if bg_fetch_cycle {
self.fetch_background();
if self.cycle & 0x07 == 0x00 {
self.regs.increment_x();
}
} else if bg_dummy_cycle {
self.fetch_bg_nt_byte();
}
match self.cycle {
VISIBLE_CYCLE_START..=8 if prerender_scanline && self.oamaddr >= 0x08 => {
let addr = self.cycle as usize - 1;
self.oam[addr] = self.oam[(self.oamaddr as usize & 0xF8) + addr];
}
INC_Y_CYCLE => self.regs.increment_y(),
COPY_X_CYCLE => self.regs.copy_x(),
COPY_Y_CYCLE_START..=COPY_Y_CYCLE_END if prerender_scanline => {
self.regs.copy_y();
}
_ => (),
}
if prerender_scanline {
self.sprite_count = 0;
}
if spr_fetch_cycle {
self.fetch_sprites();
}
if spr_dummy_cycle {
self.oam_fetch = self.secondary_oam[0];
}
if self.cycle == CYCLE_SKIP
&& prerender_scanline
&& self.frame.num & 0x01 == 0x01
&& self.nes_region == NesRegion::Ntsc
{
log::trace!(
"({}, {}): Skipped odd frame cycle: {}",
self.cycle,
self.scanline,
self.frame.num
);
self.cycle = CYCLE_END;
}
}
}
}
#[inline]
fn fetch_bg_nt_byte(&mut self) {
let nametable_addr_mask = 0x0FFF; let addr = NT_START | (self.regs.v & nametable_addr_mask);
self.frame.nametable = u16::from(self.vram.read(addr));
}
#[inline]
fn fetch_bg_attr_byte(&mut self) {
let v = self.regs.v;
let nametable_select = v & (NT_X_MASK | NT_Y_MASK);
let y_bits = (v >> 4) & 0x38;
let x_bits = (v >> 2) & 0x07;
let addr = ATTR_START | nametable_select | y_bits | x_bits;
self.frame.attribute = self.vram.read(addr);
if self.regs.coarse_y() & 2 > 0 {
self.frame.attribute >>= 4;
}
if self.regs.coarse_x() & 2 > 0 {
self.frame.attribute >>= 2;
}
self.frame.attribute = (self.frame.attribute & 3) << 2;
}
#[inline]
fn fetch_background(&mut self) {
self.frame.tile_data <<= 4;
match self.cycle & 0x07 {
1 => self.fetch_bg_nt_byte(),
3 => self.fetch_bg_attr_byte(),
5 => {
let tile_addr =
self.regs.background_select() + self.frame.nametable * 16 + self.regs.fine_y();
self.frame.tile_lo = self.vram.read(tile_addr);
}
7 => {
let tile_addr =
self.regs.background_select() + self.frame.nametable * 16 + self.regs.fine_y();
self.frame.tile_hi = self.vram.read(tile_addr + 8);
}
0 => {
let mut data = 0u32;
let a = self.frame.attribute;
for _ in 0..8 {
let p1 = (self.frame.tile_lo & 0x80) >> 7;
let p2 = (self.frame.tile_hi & 0x80) >> 6;
self.frame.tile_lo <<= 1;
self.frame.tile_hi <<= 1;
data <<= 4;
data |= u32::from(a | p1 | p2);
}
self.frame.tile_data |= u64::from(data);
}
_ => (),
}
}
#[inline]
fn load_sprites(&mut self) {
let idx = (self.cycle - SPR_FETCH_CYCLE_START) as usize / 8;
let oam_idx = idx << 2;
if let [y, tile_number, attr, x] = self.secondary_oam[oam_idx..=oam_idx + 3] {
let x = u32::from(x);
let y = u32::from(y);
let mut tile_number = u16::from(tile_number);
let palette = ((attr & 0x03) << 2) | 0x10;
let bg_priority = (attr & 0x20) == 0x20;
let flip_horizontal = (attr & 0x40) == 0x40;
let flip_vertical = (attr & 0x80) == 0x80;
let height = self.regs.sprite_height();
let mut line_offset = if (y..y + height).contains(&self.scanline) {
self.scanline - y
} else {
0
};
if flip_vertical {
line_offset = height - 1 - line_offset;
}
if idx >= self.sprite_count.into() {
line_offset = 0;
tile_number = 0xFF;
}
let tile_addr = if height == 16 {
let sprite_select = if tile_number & 0x01 == 0x01 {
0x1000
} else {
0x0000
};
if line_offset >= 8 {
line_offset += 8;
}
sprite_select | ((tile_number & 0xFE) << 4) | line_offset as u16
} else {
self.regs.sprite_select() | (tile_number << 4) | line_offset as u16
};
if idx < self.sprite_count.into() {
let mut sprite = &mut self.sprites[idx];
sprite.x = x;
sprite.y = y;
sprite.tile_lo = self.vram.read(tile_addr);
sprite.tile_hi = self.vram.read(tile_addr + 8);
sprite.palette = palette;
sprite.bg_priority = bg_priority;
sprite.flip_horizontal = flip_horizontal;
sprite.flip_vertical = flip_vertical;
} else {
let _ = self.vram.read(tile_addr);
let _ = self.vram.read(tile_addr + 8);
}
}
}
#[inline]
fn fetch_sprites(&mut self) {
self.write_oamaddr(0x00);
match self.cycle & 0x07 {
1 => self.fetch_bg_nt_byte(), 3 => self.fetch_bg_attr_byte(), 4 => self.load_sprites(),
_ => (),
}
}
#[inline]
fn evaluate_sprites(&mut self) {
match self.cycle {
VISIBLE_CYCLE_START..=OAM_CLEAR_CYCLE_END => {
self.oam_fetch = 0xFF;
self.secondary_oam = Memory::ram(SECONDARY_OAM_SIZE, RamState::AllOnes);
}
SPR_EVAL_CYCLE_START..=SPR_EVAL_CYCLE_END => {
if self.cycle == SPR_EVAL_CYCLE_START {
self.sprite_in_range = false;
self.sprite0_in_range = false;
self.secondary_oamaddr = 0x00;
self.oam_eval_done = false;
self.oamaddr_hi = (self.oamaddr >> 2) & 0x3F;
self.oamaddr_lo = (self.oamaddr) & 0x03;
} else if self.cycle == SPR_EVAL_CYCLE_END {
self.sprite0_visible = self.sprite0_in_range;
self.sprite_count = self.secondary_oamaddr >> 2;
}
if self.cycle & 0x01 == 0x01 {
self.oam_fetch = self.oam[self.oamaddr as usize];
} else {
if self.oam_eval_done {
self.oamaddr_hi = (self.oamaddr_hi + 1) & 0x3F;
if self.secondary_oamaddr >= 0x20 {
self.oam_fetch =
self.secondary_oam[self.secondary_oamaddr as usize & 0x1F];
}
} else {
let y = u32::from(self.oam_fetch);
let height = self.regs.sprite_height();
if !self.sprite_in_range && (y..y + height).contains(&self.scanline) {
self.sprite_in_range = true;
}
if self.secondary_oamaddr < 0x20 {
self.secondary_oam[self.secondary_oamaddr as usize] = self.oam_fetch;
if self.sprite_in_range {
self.oamaddr_lo += 1;
self.secondary_oamaddr += 1;
if self.oamaddr_hi == 0x00 {
self.sprite0_in_range = true;
}
if self.oamaddr_lo == 0x04 {
self.sprite_in_range = false;
self.oamaddr_lo = 0x00;
self.oamaddr_hi = (self.oamaddr_hi + 1) & 0x3F;
if self.oamaddr_hi == 0x00 {
self.oam_eval_done = true;
}
}
} else {
self.oamaddr_hi = (self.oamaddr_hi + 1) & 0x3F;
if self.oamaddr_hi == 0x00 {
self.oam_eval_done = true;
}
}
} else {
self.oam_fetch =
self.secondary_oam[self.secondary_oamaddr as usize & 0x1F];
if self.sprite_in_range {
self.regs.set_sprite_overflow(true);
self.oamaddr_lo += 1;
if self.oamaddr_lo == 0x04 {
self.oamaddr_lo = 0x00;
self.oamaddr_hi = (self.oamaddr_hi + 1) & 0x3F;
}
match self.overflow_count.cmp(&0) {
Ordering::Equal => self.overflow_count = 3,
Ordering::Greater => {
self.overflow_count -= 1;
if self.overflow_count == 0 {
self.oam_eval_done = true;
self.oamaddr_lo = 0x00;
}
}
Ordering::Less => (),
}
} else {
self.oamaddr_hi = (self.oamaddr_hi + 1) & 0x3F;
self.oamaddr_lo = (self.oamaddr_lo + 1) & 0x03;
if self.oamaddr_hi == 0x00 {
self.oam_eval_done = true;
}
}
}
}
self.oamaddr = (self.oamaddr_hi << 2) | (self.oamaddr_lo & 0x03);
}
}
_ => (),
}
}
#[inline]
fn render_pixel(&mut self) {
let x = self.cycle - 1;
let y = self.scanline;
let palette_addr =
if self.rendering_enabled() || (self.read_ppuaddr() & PALETTE_START) != PALETTE_START {
let color = self.pixel_color();
if color & 0x03 > 0 {
u16::from(color)
} else {
0
}
} else {
self.read_ppuaddr() & 0x1F
};
let mut palette = u16::from(self.vram.read(PALETTE_START + palette_addr));
palette &= if self.regs.grayscale() { 0x30 } else { 0x3F };
palette |= u16::from(self.regs.emphasis()) << 1;
self.frame.put_pixel(x, y, palette);
}
#[inline]
fn put_pixel(palette_idx: usize, x: u32, y: u32, width: u32, pixels: &mut [u8]) {
let palette_idx = (palette_idx % SYSTEM_PALETTE_SIZE) * 3;
let red = SYSTEM_PALETTE[palette_idx];
let green = SYSTEM_PALETTE[palette_idx + 1];
let blue = SYSTEM_PALETTE[palette_idx + 2];
let idx = RENDER_CHANNELS * (x + y * width) as usize;
pixels[idx] = red;
pixels[idx + 1] = green;
pixels[idx + 2] = blue;
}
#[inline]
#[must_use]
pub fn pixel_brightness(&self, x: u32, y: u32) -> u32 {
if x >= RENDER_WIDTH || y >= RENDER_HEIGHT {
return 0;
}
let color = self.frame.back_buffer[(x + (y << 8)) as usize] as usize;
let palette_idx = (color & (SYSTEM_PALETTE_SIZE - 1)) * 3;
if let [red, green, blue] = SYSTEM_PALETTE[palette_idx..=palette_idx + 2] {
u32::from(red) + u32::from(green) + u32::from(blue)
} else {
0
}
}
#[inline]
const fn background_color(&self) -> u8 {
let tile_data = (self.frame.tile_data >> 32) as u32;
let data = tile_data >> ((7 - self.regs.fine_x()) * 4);
(data & 0x0F) as u8
}
#[inline]
fn pixel_color(&mut self) -> u8 {
let x = self.cycle - 1;
let left_clip_bg = x < 8 && !self.regs.show_left_background();
let left_clip_spr = x < 8 && !self.regs.show_left_sprites();
let bg_color = if self.regs.show_background() && !left_clip_bg {
self.background_color()
} else {
0
};
let bg_opaque = bg_color & 0x03 != 0;
if self.regs.show_sprites() && !left_clip_spr {
for (i, sprite) in self
.sprites
.iter()
.take(self.sprite_count as usize)
.enumerate()
{
let shift = x as i16 - sprite.x as i16;
if (0..=7).contains(&shift) {
let color = if sprite.flip_horizontal {
(((sprite.tile_hi >> shift) & 0x01) << 1)
| ((sprite.tile_lo >> shift) & 0x01)
} else {
(((sprite.tile_hi << shift) & 0x80) >> 6)
| ((sprite.tile_lo << shift) & 0x80) >> 7
};
if color != 0 {
if i == 0
&& bg_opaque
&& self.sprite0_visible
&& x != 255
&& self.rendering_enabled()
&& !self.regs.sprite0_hit()
{
self.regs.set_sprite0_hit(true);
}
if !bg_opaque || !sprite.bg_priority {
return sprite.palette + color;
}
break;
}
}
}
}
if bg_opaque {
bg_color
} else {
0
}
}
#[must_use]
#[inline]
pub const fn rendering_enabled(&self) -> bool {
self.regs.show_background() || self.regs.show_sprites()
}
#[must_use]
#[inline]
pub const fn nmi_enabled(&self) -> bool {
self.regs.nmi_enabled()
}
#[must_use]
#[inline]
pub const fn nmi_pending(&self) -> bool {
self.nmi_pending
}
#[inline]
fn write_ppuctrl(&mut self, val: u8) {
if self.cycle_count < POWER_ON_CYCLES {
return;
}
self.regs.write_ctrl(val);
log::trace!(
"({}, {}): $2000 NMI Enabled: {}",
self.cycle,
self.scanline,
self.nmi_enabled()
);
if !self.nmi_enabled() {
log::trace!("({}, {}): $2000 NMI Disable", self.cycle, self.scanline);
self.nmi_pending = false;
} else if self.vblank_started() {
log::trace!("({}, {}): $2000 NMI During VBL", self.cycle, self.scanline);
self.nmi_pending = true;
}
}
#[inline]
fn write_ppumask(&mut self, val: u8) {
if self.cycle_count < POWER_ON_CYCLES {
return;
}
self.regs.write_mask(val);
}
#[inline]
pub fn read_ppustatus(&mut self) -> u8 {
let status = self.regs.read_status();
log::trace!("({}, {}): $2002 NMI Ack", self.cycle, self.scanline);
self.nmi_pending = false;
if self.scanline == self.vblank_scanline && self.cycle == VBLANK_CYCLE - 1 {
log::trace!("({}, {}): $2002 Prevent VBL", self.cycle, self.scanline);
self.prevent_vbl = true;
}
self.vram
.cart_mut()
.ppu_write(0x2002, self.regs.peek_status());
(status & 0xE0) | (self.regs.open_bus & 0x1F)
}
#[inline]
const fn peek_ppustatus(&self) -> u8 {
(self.regs.peek_status() & 0xE0) | (self.regs.open_bus & 0x1F)
}
#[inline]
fn start_vblank(&mut self) {
log::trace!("({}, {}): Set VBL flag", self.cycle, self.scanline);
if !self.prevent_vbl {
self.regs.start_vblank();
self.nmi_pending = self.nmi_enabled();
log::trace!(
"({}, {}): VBL NMI: {}",
self.cycle,
self.scanline,
self.nmi_pending
);
}
self.prevent_vbl = false;
self.vram
.cart_mut()
.ppu_write(0x2002, self.regs.peek_status());
}
#[inline]
fn stop_vblank(&mut self) {
log::trace!("({}, {}): Clear VBL flag", self.cycle, self.scanline);
self.regs.stop_vblank();
self.nmi_pending = false;
self.vram
.cart_mut()
.ppu_write(0x2002, self.regs.peek_status());
}
#[must_use]
#[inline]
pub const fn vblank_started(&self) -> bool {
self.regs.vblank_started()
}
#[must_use]
#[inline]
pub const fn read_oamaddr(&self) -> u8 {
self.oamaddr
}
#[inline]
fn write_oamaddr(&mut self, val: u8) {
self.oamaddr = val;
}
#[must_use]
#[inline]
fn read_oamdata(&mut self) -> u8 {
if self.scanline <= VISIBLE_SCANLINE_END
&& self.rendering_enabled()
&& matches!(self.cycle, SPR_FETCH_CYCLE_START..=SPR_FETCH_CYCLE_END)
{
let step = ((self.cycle - SPR_FETCH_CYCLE_START) & 0x07).min(3);
self.secondary_oamaddr = ((self.cycle - SPR_FETCH_CYCLE_START) / 8 * 4 + step) as u8;
self.oam_fetch = self.secondary_oam[self.secondary_oamaddr as usize & 0x1F];
}
self.peek_oamdata()
}
#[inline]
fn peek_oamdata(&self) -> u8 {
if self.scanline <= VISIBLE_SCANLINE_END && self.rendering_enabled() {
self.oam_fetch
} else {
self.oam[self.oamaddr as usize]
}
}
#[inline]
fn write_oamdata(&mut self, mut val: u8) {
if self.rendering_enabled()
&& (self.scanline <= VISIBLE_SCANLINE_END
|| self.scanline == self.prerender_scanline
|| (self.nes_region == NesRegion::Pal
&& self.scanline >= self.pal_spr_eval_scanline))
{
self.write_oamaddr(self.oamaddr.wrapping_add(4));
} else {
if self.oamaddr & 0x03 == 0x02 {
val &= 0xE3;
}
self.oam[self.oamaddr as usize] = val;
self.write_oamaddr(self.oamaddr.wrapping_add(1));
}
}
#[inline]
fn write_ppuscroll(&mut self, val: u8) {
if self.cycle_count < POWER_ON_CYCLES {
return;
}
self.regs.write_scroll(val);
}
#[must_use]
#[inline]
pub const fn read_ppuaddr(&self) -> u16 {
self.regs.read_addr()
}
#[inline]
fn write_ppuaddr(&mut self, val: u8) {
if self.cycle_count < POWER_ON_CYCLES {
return;
}
self.regs.write_addr(val);
self.vram.cart_mut().ppu_addr(self.regs.v);
}
#[inline]
fn update_vram_addr(&mut self) {
if self.rendering_enabled()
&& (self.scanline == self.prerender_scanline || self.scanline <= VISIBLE_SCANLINE_END)
{
self.regs.increment_x();
self.regs.increment_y();
} else {
self.regs.increment_v();
}
}
#[inline]
fn read_ppudata(&mut self) -> u8 {
let val = self.vram.read(self.read_ppuaddr());
let val = if self.read_ppuaddr() <= 0x3EFF {
let buffer = self.vram.buffer;
self.vram.buffer = val;
buffer
} else {
self.vram.buffer = self.vram.read(self.read_ppuaddr() - 0x1000);
val | (self.regs.open_bus & 0xC0)
};
self.update_vram_addr();
self.vram.cart_mut().ppu_addr(self.regs.v);
val
}
#[inline]
fn peek_ppudata(&self) -> u8 {
let val = self.vram.peek(self.read_ppuaddr());
if self.read_ppuaddr() <= 0x3EFF {
self.vram.buffer
} else {
val | (self.regs.open_bus & 0xC0)
}
}
#[inline]
fn write_ppudata(&mut self, val: u8) {
self.vram.write(self.read_ppuaddr(), val);
self.update_vram_addr();
self.vram.cart_mut().ppu_addr(self.regs.v);
}
#[inline]
pub fn run(&mut self, clock: u64) {
while self.master_clock + self.clock_divider <= clock {
self.clock();
self.master_clock += self.clock_divider;
}
}
}
impl Clocked for Ppu {
fn clock(&mut self) -> usize {
if self.scanline == 0 {
self.regs.open_bus = 0x00;
}
self.cycle_count = self.cycle_count.wrapping_add(1);
if self.cycle >= CYCLE_END {
self.cycle = 0;
self.scanline += 1;
if self.scanline == self.vblank_scanline - 1 {
self.frame.increment();
self.frame.swap_buffers();
} else if self.scanline > self.prerender_scanline {
self.scanline = 0;
}
self.update_viewer();
} else {
self.cycle += 1;
self.run_cycle();
if self.cycle == VBLANK_CYCLE {
if self.scanline == self.vblank_scanline {
self.start_vblank();
}
if self.scanline == self.prerender_scanline {
log::trace!(
"({}, {}): Clear Sprite0 Hit, Overflow",
self.cycle,
self.scanline
);
self.regs.set_sprite0_hit(false);
self.regs.set_sprite_overflow(false);
self.stop_vblank();
}
}
}
1
}
}
impl MemRead for Ppu {
#[inline]
fn read(&mut self, addr: u16) -> u8 {
let val = match addr {
0x2002 => self.read_ppustatus(),
0x2004 => self.read_oamdata(),
0x2007 => self.read_ppudata(),
_ => self.regs.open_bus,
};
self.regs.open_bus = val;
val
}
#[inline]
fn peek(&self, addr: u16) -> u8 {
match addr {
0x2002 => self.peek_ppustatus(),
0x2004 => self.peek_oamdata(),
0x2007 => self.peek_ppudata(),
_ => self.regs.open_bus,
}
}
}
impl MemWrite for Ppu {
#[inline]
fn write(&mut self, addr: u16, val: u8) {
self.vram.cart_mut().ppu_write(addr, val);
self.regs.open_bus = val;
match addr {
0x2000 => self.write_ppuctrl(val),
0x2001 => self.write_ppumask(val),
0x2003 => self.write_oamaddr(val),
0x2004 => self.write_oamdata(val),
0x2005 => self.write_ppuscroll(val),
0x2006 => self.write_ppuaddr(val),
0x2007 => self.write_ppudata(val),
_ => (),
}
}
}
impl Powered for Ppu {
fn reset(&mut self) {
self.cycle = 0;
self.cycle_count = 0;
self.scanline = 0;
self.master_clock = 0;
self.prevent_vbl = false;
self.oam_dma = false;
self.oam_dma_offset = 0x00;
self.vram.reset();
self.regs.w = false;
self.regs.set_sprite0_hit(false);
self.regs.set_sprite_overflow(false);
self.secondary_oamaddr = 0x00;
self.oam_fetch = 0xFF;
self.oam_eval_done = false;
self.overflow_count = 0;
self.sprite_in_range = false;
self.sprite0_in_range = false;
self.sprite0_visible = false;
self.sprite_count = 0;
self.sprites = [Sprite::new(); 8];
self.frame.reset();
self.regs.write_ctrl(0);
self.regs.write_mask(0);
self.regs.write_scroll(0);
self.regs.write_addr(0);
}
fn power_cycle(&mut self) {
self.oamaddr_lo = 0x00;
self.oamaddr_hi = 0x00;
self.oamaddr = 0x00;
self.reset();
}
}
impl Default for Ppu {
fn default() -> Self {
Self::new(NesRegion::Ntsc)
}
}
impl fmt::Debug for Ppu {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Ppu")
.field("cycle", &self.cycle)
.field("cycle_count", &self.cycle_count)
.field("scanline", &self.scanline)
.field("nes_region", &self.nes_region)
.field("master_clock", &self.master_clock)
.field("clock_divider", &self.clock_divider)
.field("vblank_scanline", &self.vblank_scanline)
.field("prerender_scanline", &self.prerender_scanline)
.field("nmi_pending", &self.nmi_pending)
.field("prevent_vbl", &self.prevent_vbl)
.field("oam_dma", &self.oam_dma)
.field("dma_offset", &format_args!("${:02X}", &self.oam_dma_offset))
.field("vram", &self.vram)
.field("regs", &self.regs)
.field("oamaddr", &self.oamaddr)
.field("oam", &self.oam)
.field("secondary_oamaddr", &self.secondary_oamaddr)
.field("secondary_oam", &self.secondary_oam)
.field("oam_fetch", &self.oam_fetch)
.field("oam_eval_done", &self.oam_eval_done)
.field("sprite_in_range", &self.sprite_in_range)
.field("sprite0_in_range", &self.sprite0_in_range)
.field("sprite0_visible", &self.sprite0_visible)
.field("sprite_count", &self.sprite_count)
.field("sprites", &self.sprites)
.field("frame", &self.frame)
.field("filter", &self.filter)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{cart::Cart, test_roms};
#[test]
fn scrolling_registers() {
let mut ppu = Ppu::default();
let mut cart = Box::new(Cart::new());
ppu.load_cart(&mut cart);
while ppu.cycle_count < POWER_ON_CYCLES {
ppu.clock();
}
let ppuctrl = 0x2000;
let ppustatus = 0x2002;
let ppuscroll = 0x2005;
let ppuaddr = 0x2006;
let ctrl_write: u8 = 0b11; let t_result: u16 = 0b11 << 10; ppu.write(ppuctrl, ctrl_write);
assert_eq!(ppu.regs.t, t_result);
assert_eq!(ppu.regs.v, 0);
ppu.read(ppustatus);
assert!(!ppu.regs.w);
let scroll_write: u8 = 0b0111_1101;
let t_result: u16 = 0b000_1100_0000_1111;
let x_result: u16 = 0b101;
ppu.write(ppuscroll, scroll_write);
assert_eq!(ppu.regs.t, t_result);
assert_eq!(ppu.regs.x, x_result);
assert!(ppu.regs.w);
let scroll_write: u8 = 0b0101_1110;
let t_result: u16 = 0b110_1101_0110_1111;
ppu.write(ppuscroll, scroll_write);
assert_eq!(ppu.regs.t, t_result);
assert_eq!(ppu.regs.x, x_result);
assert!(!ppu.regs.w);
let addr_write: u8 = 0b0011_1101;
let t_result: u16 = 0b011_1101_0110_1111;
ppu.write(ppuaddr, addr_write);
assert_eq!(ppu.regs.t, t_result);
assert_eq!(ppu.regs.x, x_result);
assert!(ppu.regs.w);
let addr_write: u8 = 0b1111_0000;
let t_result: u16 = 0b011_1101_1111_0000;
ppu.write(ppuaddr, addr_write);
assert_eq!(ppu.regs.t, t_result);
assert_eq!(ppu.regs.v, t_result);
assert_eq!(ppu.regs.x, x_result);
assert!(!ppu.regs.w);
ppu.write(ppuaddr, 0b0000_1000); ppu.write(ppuscroll, 0b0100_0101); ppu.write(ppuscroll, 0b0000_0011); ppu.write(ppuaddr, 0b1001_0110); let t_result: u16 = 0b101_1001_1001_0110;
assert_eq!(ppu.regs.v, t_result);
}
test_roms!(
"test_roms/ppu",
_240pee, color, ntsc_torture,
oam_read,
oam_stress,
open_bus,
palette,
palette_ram,
read_buffer,
scanline,
spr_hit_alignment,
spr_hit_basics,
spr_hit_corners,
spr_hit_double_height,
spr_hit_edge_timing,
spr_hit_flip,
spr_hit_left_clip,
spr_hit_right_edge,
spr_hit_screen_bottom,
spr_hit_timing_basics,
spr_hit_timing_order,
spr_overflow_basics,
spr_overflow_details,
spr_overflow_emulator,
spr_overflow_obscure,
spr_overflow_timing,
sprite_ram,
tv,
vbl_nmi_basics,
vbl_nmi_clear_timing,
vbl_nmi_control,
vbl_nmi_disable,
vbl_nmi_even_odd_frames,
#[ignore = "clock is skipped too late relative to enabling BG Failed #3"]
vbl_nmi_even_odd_timing,
vbl_nmi_frame_basics,
vbl_nmi_off_timing,
vbl_nmi_on_timing,
vbl_nmi_set_time,
vbl_nmi_suppression,
vbl_nmi_timing,
vbl_timing,
vram_access,
);
}