#![no_std]
#[cfg(test)]
mod tests;
use bytemuck::{Pod, Zeroable};
pub type Color = u8;
#[derive(Debug, Copy, Clone, Default)]
pub struct Palette {
pub colors: [Color; 3],
}
const SPRITE_SIZE: u8 = 4;
#[derive(Debug, Copy, Clone, Default, Pod, Zeroable)]
#[repr(C)]
pub struct Sprite {
pub y: u8,
pub pattern_index: u8,
pub attributes: u8,
pub x: u8,
}
#[derive(Debug)]
pub struct Ppu {
oam: [Sprite; 64],
sprite_palettes: [Palette; 4],
tile_palettes: [Palette; 4],
zero_colors: [Color; 4],
ctrl: u8,
mask: u8,
status: u8,
t_reg: u16,
v_reg: u16,
fine_x_scroll: u8, w_latch: bool,
read_buffer: u8,
sprite_0_cur_line: bool,
sprite_0_next_line: bool,
secondary_oam: [Sprite; 8],
sprite_render_states: [SpriteRenderState; 8],
oam_mdr: u8,
oam_evaluation_index: u8,
secondary_oam_evaluation_index: u8,
sprite_evaluation_state: SpriteEvaluationState,
oam_evaluation_index_overflow: bool,
temp_sprite_pattern_lo: u8,
tile_pattern_shift_reg: u32, tile_attribute_shift_reg: u16, tile_attribute_latch: u8,
temp_tile_pattern_index: u8,
temp_tile_attribute: u8,
temp_tile_pattern_lo: u8,
temp_tile_pattern_hi: u8,
cur_scanline: u16,
cur_dot: u16,
is_even_frame: bool,
}
#[derive(Debug, Copy, Clone, Default)]
struct SpriteRenderState {
pattern_shift_reg: u16, x_counter: u8,
attributes: u8,
}
#[derive(Eq, PartialEq)]
enum RenderMode {
Normal,
PreRender,
}
#[derive(Debug, Eq, PartialEq)]
enum SpriteEvaluationState {
CheckingNormal,
CheckingOverflow,
CopyingNormal(u8),
CopyingOverflow(u8),
Complete,
}
pub trait Mapper {
fn read(&mut self, addr: u16) -> u8;
fn write(&mut self, addr: u16, value: u8);
}
pub trait PixelBuffer {
fn set_color(&mut self, x: u8, y: u8, color: Color, emphasis: ColorEmphasis);
}
pub const PPUCTRL_ADDR_INC: u8 = 1 << 2;
pub const PPUCTRL_SPRITE_PATTERN_TABLE: u8 = 1 << 3;
pub const PPUCTRL_TILE_PATTERN_TABLE: u8 = 1 << 4;
pub const PPUCTRL_SPRITE_SIZE: u8 = 1 << 5;
pub const PPUCTRL_MSS: u8 = 1 << 6;
pub const PPUCTRL_NMI_ENABLE: u8 = 1 << 7;
pub const PPUMASK_GREYSCALE: u8 = 1 << 0;
pub const PPUMASK_SHOW_COLUMN_0_TILES: u8 = 1 << 1;
pub const PPUMASK_SHOW_COLUMN_0_SPRITES: u8 = 1 << 2;
pub const PPUMASK_SHOW_TILES: u8 = 1 << 3;
pub const PPUMASK_SHOW_SPRITES: u8 = 1 << 4;
pub const PPUMASK_EMPH_RED: u8 = 1 << 5;
pub const PPUMASK_EMPH_GREEN: u8 = 1 << 6;
pub const PPUMASK_EMPH_BLUE: u8 = 1 << 7;
pub const PPUSTATUS_OVERFLOW: u8 = 1 << 5;
pub const PPUSTATUS_SPRITE_0_HIT: u8 = 1 << 6;
pub const PPUSTATUS_VBLANK: u8 = 1 << 7;
pub const SPRITE_PALETTE_MASK: u8 = 0b00000011;
pub const SPRITE_PRIORITY: u8 = 0b00100000;
pub const SPRITE_FLIP_X: u8 = 0b01000000;
pub const SPRITE_FLIP_Y: u8 = 0b10000000;
const X_SCROLL_MASK: u16 = 0b000_01_00000_11111;
const Y_SCROLL_MASK: u16 = 0b111_10_11111_00000;
const COARSE_X_SCROLL_MASK: u16 = 0b000_00_00000_11111;
const FINE_Y_SCROLL_MASK: u16 = 0b111_00_00000_00000;
const COARSE_Y_SCROLL_MASK: u16 = 0b000_00_11111_00000;
const COARSE_X_OFFSET: u16 = 0;
const FINE_Y_OFFSET: u16 = 12;
const COARSE_Y_OFFSET: u16 = 5;
const NAMETABLE_SELECT_MASK: u16 = 0b000_11_00000_00000;
const NAMETABLE_Y_SELECT_MASK: u16 = 0b000_10_00000_00000;
const NAMETABLE_SELECT_OFFSET: u16 = 10;
const NAMETABLE_BASE_ADDRESS: u16 = 0b10_00_0000_000000;
const ATTRIBUTE_TABLE_OFFSET: u16 = 0b00_00_1111_000000;
#[derive(Debug, Copy, Clone)]
pub struct ColorEmphasis {
pub bits: u8,
}
#[repr(u16)]
enum BitPlane {
Lo = 0,
Hi = 8,
}
#[derive(Debug, Copy, Clone)]
struct PixelInfo {
color: Color,
sprite_0_hit: bool,
}
fn pattern_table_base(b: u8) -> u16 {
match b {
0 => 0x0000,
_ => 0x1000,
}
}
fn morton_encode_16(lo: u8, hi: u8) -> u16 {
let mut x = u16::from(lo);
x = (x | (x << 4)) & 0x0F0F;
x = (x | (x << 2)) & 0x3333;
x = (x | (x << 1)) & 0x5555;
let mut y = u16::from(hi);
y = (y | (y << 4)) & 0x0F0F;
y = (y | (y << 2)) & 0x3333;
y = (y | (y << 1)) & 0x5555;
x | (y << 1)
}
fn palette_address_color_index(addr: u16) -> usize {
(addr & 0b11) as usize
}
fn palette_address_palette_index(addr: u16) -> usize {
((addr >> 2) & 0b11) as usize
}
fn is_palette_address_tile_color(addr: u16) -> bool {
(addr & 0b10000) == 0
}
impl Ppu {
pub fn new() -> Self {
Ppu {
oam: [Sprite::default(); 64],
sprite_palettes: [Palette::default(); 4],
tile_palettes: [Palette::default(); 4],
zero_colors: [0; 4],
ctrl: 0,
mask: 0,
status: 0,
t_reg: 0,
v_reg: 0,
fine_x_scroll: 0,
w_latch: false,
read_buffer: 0,
sprite_0_cur_line: false,
sprite_0_next_line: false,
secondary_oam: [Sprite::default(); 8],
sprite_render_states: [SpriteRenderState::default(); 8],
oam_mdr: 0,
oam_evaluation_index: 0,
secondary_oam_evaluation_index: 0,
sprite_evaluation_state: SpriteEvaluationState::CheckingNormal,
oam_evaluation_index_overflow: false,
temp_sprite_pattern_lo: 0,
tile_pattern_shift_reg: 0,
tile_attribute_shift_reg: 0,
tile_attribute_latch: 0,
temp_tile_pattern_index: 0,
temp_tile_attribute: 0,
temp_tile_pattern_lo: 0,
temp_tile_pattern_hi: 0,
cur_scanline: 261,
cur_dot: 0,
is_even_frame: false,
}
}
pub fn tick<M: Mapper, B: PixelBuffer>(&mut self, mapper: &mut M, buffer: &mut B) {
match self.cur_scanline {
0..=239 => self.tick_render(mapper, buffer, RenderMode::Normal), 240..=260 => self.tick_vblank(), 261 => self.tick_render(mapper, buffer, RenderMode::PreRender), _ => unreachable!(),
}
match self.cur_dot {
339 if self.cur_scanline == 261 => {
if !self.is_even_frame && self.is_rendering_enabled() {
self.cur_dot = 0;
self.cur_scanline = 0;
} else {
self.cur_dot += 1;
}
self.is_even_frame = !self.is_even_frame;
}
0..=339 => {
self.cur_dot += 1;
}
340 => {
self.cur_dot = 0;
self.cur_scanline = (self.cur_scanline + 1) % 262;
}
_ => unreachable!(),
}
}
pub fn tick_to_next_vblank<M: Mapper, B: PixelBuffer>(
&mut self,
mapper: &mut M,
buffer: &mut B,
) {
while self.status & PPUSTATUS_VBLANK != 0 {
self.tick(mapper, buffer);
}
while self.status & PPUSTATUS_VBLANK == 0 {
self.tick(mapper, buffer);
}
}
pub fn tick_to_next_sprite_0_hit<M: Mapper, B: PixelBuffer>(
&mut self,
mapper: &mut M,
buffer: &mut B,
) {
while self.status & PPUSTATUS_SPRITE_0_HIT != 0 {
self.tick(mapper, buffer);
}
while self.status & PPUSTATUS_SPRITE_0_HIT == 0 {
self.tick(mapper, buffer);
}
}
pub fn set_oam(&mut self, sprites: [Sprite; 64]) {
self.oam = sprites;
}
pub fn set_oam_bytes(&mut self, bytes: [u8; 256]) {
self.oam_bytes_mut().copy_from_slice(&bytes);
}
pub fn write_addr(&mut self, b: u8) {
let b = u16::from(b);
if !self.w_latch {
self.t_reg &= 0x00FF;
self.t_reg |= (b & 0x3F) << 8;
} else {
self.t_reg &= 0xFF00;
self.t_reg |= b;
self.v_reg = self.t_reg;
}
self.w_latch = !self.w_latch;
}
pub fn write_scroll(&mut self, b: u8) {
let fine = b & 0b00000111;
let coarse = b >> 3;
if !self.w_latch {
self.fine_x_scroll = fine;
self.t_reg &= !COARSE_X_SCROLL_MASK | NAMETABLE_SELECT_MASK;
self.t_reg |= u16::from(coarse) << COARSE_X_OFFSET;
} else {
self.t_reg &= !Y_SCROLL_MASK | NAMETABLE_SELECT_MASK;
self.t_reg |= u16::from(fine) << FINE_Y_OFFSET;
self.t_reg |= u16::from(coarse) << COARSE_Y_OFFSET;
}
self.w_latch = !self.w_latch;
}
pub fn write_ctrl(&mut self, b: u8) {
assert_eq!((b & PPUCTRL_MSS), 0, "PPUCTRL bit 6 set; system short");
self.ctrl = b;
let nametable_bits = u16::from(b & 0b00000011);
self.t_reg &= !NAMETABLE_SELECT_MASK;
self.t_reg |= nametable_bits << NAMETABLE_SELECT_OFFSET;
}
pub fn write_mask(&mut self, b: u8) {
self.mask = b;
}
pub fn read_status(&mut self) -> u8 {
let ret = self.status;
self.status &= !PPUSTATUS_VBLANK;
self.w_latch = false;
ret
}
pub fn read_data<M: Mapper>(&mut self, mapper: &mut M) -> u8 {
let effective_addr = self.v_reg & 0x3FFF;
self.increment_address();
let data = core::mem::replace(&mut self.read_buffer, mapper.read(effective_addr));
self.access_palette_ram(effective_addr).unwrap_or(data)
}
pub fn write_data<M: Mapper>(&mut self, mapper: &mut M, value: u8) {
let effective_addr = self.v_reg & 0x3FFF;
self.increment_address();
if let Some(color) = self.access_palette_ram_mut(effective_addr) {
*color = value & 0x3F;
} else {
mapper.write(effective_addr, value);
}
}
pub fn write_data_iter<M: Mapper, I>(&mut self, mapper: &mut M, values: I)
where
I: IntoIterator<Item = u8>,
{
for value in values {
self.write_data(mapper, value);
}
}
pub fn write_oam_addr(&mut self, addr: u8) {
for i in 0..8 {
let src_index = (0x20 + i) as usize;
let dest_index = self.oam_evaluation_index.wrapping_add(i) as usize;
let val = self.oam_bytes()[src_index];
self.oam_bytes_mut()[dest_index] = val;
}
self.oam_evaluation_index = addr;
}
pub fn read_oam_data(&mut self) -> u8 {
if self.is_rendering() {
self.oam_mdr
} else {
self.cur_oam_byte()
}
}
pub fn write_oam_data(&mut self, value: u8) {
if self.is_rendering() {
self.increment_oam_evaluation_index(SPRITE_SIZE);
} else {
let index = self.oam_evaluation_index as usize;
self.oam_bytes_mut()[index] = value;
self.increment_oam_evaluation_index(1);
}
}
fn access_palette_ram(&mut self, addr: u16) -> Option<u8> {
if addr < 0x3F00 {
return None;
}
let color_index = palette_address_color_index(addr);
let palette_index = palette_address_palette_index(addr);
Some(if color_index == 0 {
self.zero_colors[palette_index]
} else if is_palette_address_tile_color(addr) {
self.tile_palettes[palette_index].colors[color_index - 1]
} else {
self.sprite_palettes[palette_index].colors[color_index - 1]
})
}
fn access_palette_ram_mut(&mut self, addr: u16) -> Option<&mut u8> {
if addr < 0x3F00 {
return None;
}
let color_index = palette_address_color_index(addr);
let palette_index = palette_address_palette_index(addr);
Some(if color_index == 0 {
&mut self.zero_colors[palette_index]
} else if is_palette_address_tile_color(addr) {
&mut self.tile_palettes[palette_index].colors[color_index - 1]
} else {
&mut self.sprite_palettes[palette_index].colors[color_index - 1]
})
}
fn increment_address(&mut self) {
let new_addr = self.v_reg
+ match self.ctrl & PPUCTRL_ADDR_INC {
0 => 1,
_ => 32,
};
self.v_reg ^= new_addr;
self.v_reg &= 0xC000;
self.v_reg ^= new_addr;
}
fn sprite_height(&self) -> u8 {
match self.ctrl & PPUCTRL_SPRITE_SIZE {
0 => 8,
_ => 16,
}
}
fn oam_bytes(&self) -> &[u8; 256] {
<&[u8; 256]>::try_from(bytemuck::bytes_of(&self.oam)).unwrap()
}
fn oam_bytes_mut(&mut self) -> &mut [u8; 256] {
<&mut [u8; 256]>::try_from(bytemuck::bytes_of_mut(&mut self.oam)).unwrap()
}
fn secondary_oam_bytes(&self) -> &[u8; 32] {
<&[u8; 32]>::try_from(bytemuck::bytes_of(&self.secondary_oam)).unwrap()
}
fn secondary_oam_bytes_mut(&mut self) -> &mut [u8; 32] {
<&mut [u8; 32]>::try_from(bytemuck::bytes_of_mut(&mut self.secondary_oam)).unwrap()
}
fn sprite_pattern_table_base(&self) -> u16 {
pattern_table_base(self.ctrl & PPUCTRL_SPRITE_PATTERN_TABLE)
}
fn tile_pattern_table_base(&self) -> u16 {
pattern_table_base(self.ctrl & PPUCTRL_TILE_PATTERN_TABLE)
}
fn increment_coarse_x(&mut self) {
let coarse_x = (self.v_reg & X_SCROLL_MASK).wrapping_add(!X_SCROLL_MASK + 1);
self.v_reg ^= coarse_x;
self.v_reg &= !X_SCROLL_MASK;
self.v_reg ^= coarse_x;
}
fn increment_y(&mut self) {
if self.v_reg & FINE_Y_SCROLL_MASK != FINE_Y_SCROLL_MASK {
self.v_reg += 1 << FINE_Y_OFFSET;
} else {
self.v_reg &= !FINE_Y_SCROLL_MASK;
let coarse_y = (self.v_reg & COARSE_Y_SCROLL_MASK) >> COARSE_Y_OFFSET;
self.v_reg = match coarse_y {
29 => (self.v_reg & !COARSE_Y_SCROLL_MASK) ^ NAMETABLE_Y_SELECT_MASK,
31 => self.v_reg & !COARSE_Y_SCROLL_MASK,
_ => self.v_reg + (1 << COARSE_Y_OFFSET),
}
}
}
fn nametable_address(&self) -> u16 {
NAMETABLE_BASE_ADDRESS | (self.v_reg & 0b000_11_11111_11111)
}
fn attribute_table_address(&self) -> u16 {
let attr_x_component = (self.v_reg >> 2) & 0b000_00_0000_000111;
let attr_y_component = (self.v_reg >> 4) & 0b000_00_0000_111000;
NAMETABLE_BASE_ADDRESS
| (self.v_reg & NAMETABLE_SELECT_MASK)
| ATTRIBUTE_TABLE_OFFSET
| attr_x_component
| attr_y_component
}
fn fine_y_scroll(&self) -> u8 {
(self.v_reg >> FINE_Y_OFFSET) as u8
}
fn tile_pattern_address(&self, index: u8, plane: BitPlane) -> u16 {
let tile_offset = u16::from(index) * 16;
let row_offset = u16::from(self.fine_y_scroll());
self.tile_pattern_table_base() + tile_offset + row_offset + plane as u16
}
fn sprite_pattern_address(&self, sprite: Sprite, plane: BitPlane) -> u16 {
let distance_from_sprite_top = if sprite.attributes & SPRITE_FLIP_Y == 0 {
self.cur_scanline.wrapping_sub(u16::from(sprite.y))
} else {
(u16::from(sprite.y) + u16::from(self.sprite_height() - 1))
.wrapping_sub(self.cur_scanline)
};
if self.ctrl & PPUCTRL_SPRITE_SIZE == 0 {
let tile_offset = u16::from(sprite.pattern_index) * 16;
let row_offset = distance_from_sprite_top & 0b111;
self.sprite_pattern_table_base() + tile_offset + row_offset + plane as u16
} else {
let table_base = pattern_table_base(sprite.pattern_index & 1);
let tile_pair_offset = u16::from(sprite.pattern_index & 0b11111110) * 16;
let tile_select = (distance_from_sprite_top & 0b1000) << 1;
let row_offset = distance_from_sprite_top & 0b111;
table_base + tile_pair_offset + tile_select + row_offset + plane as u16
}
}
fn flush_horizontal_scroll(&mut self) {
self.v_reg ^= self.t_reg;
self.v_reg &= Y_SCROLL_MASK;
self.v_reg ^= self.t_reg;
}
fn flush_vertical_scroll(&mut self) {
self.v_reg ^= self.t_reg;
self.v_reg &= X_SCROLL_MASK;
self.v_reg ^= self.t_reg;
}
fn tile_attribute_bits_from_temp(&self) -> u8 {
let x_bit = (self.v_reg >> 4) & 0b100;
let y_bit = self.v_reg & 0b010;
self.temp_tile_attribute >> (x_bit | y_bit) as u8
}
fn are_sprites_visible(&self) -> bool {
let in_column_0 = self.cur_dot < 8;
(self.mask & PPUMASK_SHOW_SPRITES != 0)
&& !(in_column_0 && self.mask & PPUMASK_SHOW_COLUMN_0_SPRITES == 0)
}
fn are_tiles_visible(&self) -> bool {
let in_column_0 = self.cur_dot < 8;
(self.mask & PPUMASK_SHOW_TILES != 0)
&& !(in_column_0 && self.mask & PPUMASK_SHOW_COLUMN_0_TILES == 0)
}
fn is_rendering_enabled(&self) -> bool {
self.mask & PPUMASK_SHOW_TILES != 0 || self.mask & PPUMASK_SHOW_SPRITES != 0
}
fn is_rendering(&self) -> bool {
self.is_rendering_enabled() && (self.cur_scanline < 240 || self.cur_scanline == 261)
}
fn greyscale_mask(&self) -> u8 {
if self.mask & PPUMASK_GREYSCALE == 0 {
0x3F
} else {
0x30
}
}
fn background_color(&self) -> Color {
self.zero_colors[0]
}
fn calculate_cur_pixel(&self) -> PixelInfo {
let tile_attribute = (self.tile_attribute_shift_reg >> (2 * self.fine_x_scroll)) & 0b11;
let tile_color_index = if self.are_tiles_visible() {
(self.tile_pattern_shift_reg >> (2 * self.fine_x_scroll)) & 0b11
} else {
0
};
let tile_attribute = tile_attribute as usize;
let tile_color_index = tile_color_index as usize;
let (visible_sprite_index, visible_sprite) = self
.sprite_render_states
.iter()
.enumerate()
.filter(|&(_, s)| {
self.are_sprites_visible() && s.x_counter == 0 && s.pattern_shift_reg & 0b11 != 0
})
.next()
.unzip();
let color = match (visible_sprite, tile_color_index) {
(Some(s), _) if tile_color_index == 0 || s.attributes & SPRITE_PRIORITY == 0 => {
let sprite_palette_index = (s.attributes & SPRITE_PALETTE_MASK) as usize;
let sprite_color_index = (s.pattern_shift_reg & 0b11) as usize;
self.sprite_palettes[sprite_palette_index].colors[sprite_color_index - 1]
}
(_, 0) => self.background_color(),
(_, _) => self.tile_palettes[tile_attribute].colors[tile_color_index - 1],
};
let sprite_0_hit = self.sprite_0_cur_line
&& visible_sprite_index == Some(0)
&& tile_color_index != 0
&& self.cur_dot < 255;
PixelInfo {
color,
sprite_0_hit,
}
}
fn output_pixel<B: PixelBuffer>(&self, buffer: &mut B, color: Color) {
let color = color & self.greyscale_mask();
let emphasis = ColorEmphasis {
bits: self.mask >> 5,
};
buffer.set_color(self.cur_dot as u8, self.cur_scanline as u8, color, emphasis);
}
fn tick_tile_pipeline<M: Mapper>(&mut self, mapper: &mut M) {
match (self.cur_dot - 1) & 0b111 {
0b000 => {
self.temp_tile_pattern_index = mapper.read(self.nametable_address());
}
0b010 => {
self.temp_tile_attribute = mapper.read(self.attribute_table_address());
}
0b100 => {
let tile_pattern = self.temp_tile_pattern_index;
self.temp_tile_pattern_lo = mapper
.read(self.tile_pattern_address(tile_pattern, BitPlane::Lo))
.reverse_bits();
}
0b110 => {
let tile_pattern = self.temp_tile_pattern_index;
self.temp_tile_pattern_hi = mapper
.read(self.tile_pattern_address(tile_pattern, BitPlane::Hi))
.reverse_bits();
}
0b111 => {
let packed_pattern =
morton_encode_16(self.temp_tile_pattern_lo, self.temp_tile_pattern_hi);
self.tile_pattern_shift_reg |= u32::from(packed_pattern) << 16;
self.tile_attribute_latch = self.tile_attribute_bits_from_temp();
self.increment_coarse_x();
}
_ => {}
}
}
fn cur_oam_byte(&self) -> u8 {
self.oam_bytes()[self.oam_evaluation_index as usize]
}
fn cur_secondary_oam_byte_mut(&mut self) -> &mut u8 {
let i = self.secondary_oam_evaluation_index as usize;
&mut self.secondary_oam_bytes_mut()[i]
}
fn is_secondary_oam_full(&self) -> bool {
self.secondary_oam_evaluation_index as usize == self.secondary_oam_bytes().len()
}
fn increment_oam_evaluation_index(&mut self, n: u8) {
(
self.oam_evaluation_index,
self.oam_evaluation_index_overflow,
) = self.oam_evaluation_index.overflowing_add(n);
}
fn is_sprite_y_in_range(&self, y: u8) -> bool {
let sprite_height = self.sprite_height();
let cur_y = self.cur_scanline as u8;
y <= cur_y && (cur_y - y) < sprite_height
}
fn tick_clear_secondary_oam(&mut self) {
if self.cur_dot & 1 == 0 {
let i = ((self.cur_dot - 1) >> 1) as usize;
self.secondary_oam_bytes_mut()[i] = self.oam_mdr;
} else {
self.oam_mdr = 0xFF;
}
}
fn complete_sprite_evaluation(&mut self) -> SpriteEvaluationState {
self.oam_evaluation_index &= !0b11;
SpriteEvaluationState::Complete
}
fn tick_sprite_evaluation_check_normal(&mut self) {
let sprite_y = self.oam_mdr;
let sprite_in_range = self.is_sprite_y_in_range(sprite_y);
*self.cur_secondary_oam_byte_mut() = sprite_y;
if sprite_in_range {
self.sprite_0_next_line |= self.oam_evaluation_index == 0;
self.sprite_evaluation_state = SpriteEvaluationState::CopyingNormal(2);
self.secondary_oam_evaluation_index += 1;
self.increment_oam_evaluation_index(1);
} else {
self.increment_oam_evaluation_index(SPRITE_SIZE);
if self.oam_evaluation_index_overflow {
self.sprite_evaluation_state = self.complete_sprite_evaluation();
}
}
}
fn tick_sprite_evaluation_check_overflow(&mut self) {
let sprite_y = self.oam_mdr;
let sprite_in_range = self.is_sprite_y_in_range(sprite_y);
if sprite_in_range {
self.status |= PPUSTATUS_OVERFLOW;
self.sprite_evaluation_state = SpriteEvaluationState::CopyingOverflow(2);
self.increment_oam_evaluation_index(1);
} else {
let inc = 0b101 - (((self.oam_evaluation_index & 0b11) + 1) & 0b100);
self.increment_oam_evaluation_index(inc);
if self.oam_evaluation_index_overflow {
self.sprite_evaluation_state = self.complete_sprite_evaluation();
}
}
}
fn tick_sprite_evaluation_copy_normal(&mut self, remaining: u8) {
if !self.is_secondary_oam_full() {
*self.cur_secondary_oam_byte_mut() = self.oam_mdr;
self.secondary_oam_evaluation_index += 1;
}
self.increment_oam_evaluation_index(1);
self.sprite_evaluation_state = if remaining > 0 {
SpriteEvaluationState::CopyingNormal(remaining - 1)
} else if self.oam_evaluation_index_overflow {
self.complete_sprite_evaluation()
} else if self.is_secondary_oam_full() {
SpriteEvaluationState::CheckingOverflow
} else {
SpriteEvaluationState::CheckingNormal
};
}
fn tick_sprite_evaluation_copy_overflow(&mut self, remaining: u8) {
self.increment_oam_evaluation_index(1);
self.sprite_evaluation_state = if remaining > 0 {
SpriteEvaluationState::CopyingOverflow(remaining - 1)
} else {
self.complete_sprite_evaluation()
};
}
fn tick_sprite_evaluation_complete(&mut self) {
self.increment_oam_evaluation_index(SPRITE_SIZE);
}
fn tick_sprite_evaluation(&mut self) {
use SpriteEvaluationState::*;
if self.cur_dot & 1 == 0 {
match self.sprite_evaluation_state {
CheckingNormal => self.tick_sprite_evaluation_check_normal(),
CheckingOverflow => self.tick_sprite_evaluation_check_overflow(),
CopyingNormal(remaining) => self.tick_sprite_evaluation_copy_normal(remaining),
CopyingOverflow(remaining) => self.tick_sprite_evaluation_copy_overflow(remaining),
Complete => self.tick_sprite_evaluation_complete(),
}
} else {
self.oam_mdr = self.cur_oam_byte();
}
}
fn fetch_sprite_pattern<M: Mapper>(
&self,
mapper: &mut M,
sprite: Sprite,
plane: BitPlane,
) -> u8 {
let addr = self.sprite_pattern_address(sprite, plane);
let pattern = mapper.read(addr);
if !self.is_sprite_y_in_range(sprite.y) {
0x00
} else if sprite.attributes & SPRITE_FLIP_X == 0 {
pattern.reverse_bits()
} else {
pattern
}
}
fn tick_sprite_fetches<M: Mapper>(&mut self, mapper: &mut M) {
let sprite_index = (((self.cur_dot - 1) >> 3) & 0b111) as usize;
let sprite = self.secondary_oam[sprite_index];
self.oam_mdr = sprite.x;
match (self.cur_dot - 1) & 0b111 {
0b000 => {
mapper.read(self.nametable_address());
self.oam_mdr = sprite.y;
}
0b001 => {
self.oam_mdr = sprite.pattern_index;
}
0b010 => {
mapper.read(self.nametable_address());
self.oam_mdr = sprite.attributes;
self.sprite_render_states[sprite_index].attributes = sprite.attributes;
}
0b011 => {
self.sprite_render_states[sprite_index].x_counter = sprite.x;
}
0b100 => {
self.temp_sprite_pattern_lo =
self.fetch_sprite_pattern(mapper, sprite, BitPlane::Lo);
}
0b110 => {
let pattern_hi = self.fetch_sprite_pattern(mapper, sprite, BitPlane::Hi);
let pattern = morton_encode_16(self.temp_sprite_pattern_lo, pattern_hi);
self.sprite_render_states[sprite_index].pattern_shift_reg = pattern;
}
_ => {}
}
}
fn tick_sprites<M: Mapper>(&mut self, mapper: &mut M) {
match self.cur_dot {
0 => {
self.secondary_oam_evaluation_index = 0;
self.sprite_evaluation_state = SpriteEvaluationState::CheckingNormal;
self.oam_evaluation_index_overflow = false;
self.sprite_0_cur_line = self.sprite_0_next_line;
self.sprite_0_next_line = false;
}
1..=64 => {
self.tick_clear_secondary_oam();
}
65..=256 => {
self.tick_sprite_evaluation();
}
257..=320 => {
self.oam_evaluation_index = 0;
self.tick_sprite_fetches(mapper);
}
321..=340 => {
self.oam_mdr = self.secondary_oam_bytes()[0];
}
_ => {}
}
}
fn update_tile_shift_regs(&mut self) {
self.tile_pattern_shift_reg >>= 2;
self.tile_attribute_shift_reg >>= 2;
self.tile_attribute_shift_reg |= u16::from(self.tile_attribute_latch) << 14;
}
fn update_sprite_counters_and_shift_regs(&mut self) {
for render_state in &mut self.sprite_render_states {
if render_state.x_counter > 0 {
render_state.x_counter -= 1;
} else {
render_state.pattern_shift_reg >>= 2;
}
}
}
fn tick_render<M: Mapper, B: PixelBuffer>(
&mut self,
mapper: &mut M,
buffer: &mut B,
mode: RenderMode,
) {
if !self.is_rendering_enabled() {
if self.cur_dot < 256 && mode == RenderMode::Normal {
let color = self
.access_palette_ram(self.v_reg & 0x3FFF)
.unwrap_or(self.background_color());
self.output_pixel(buffer, color);
}
return;
}
if matches!(self.cur_dot, 1..=256 | 321..=336) {
self.update_tile_shift_regs();
self.tick_tile_pipeline(mapper);
}
if mode == RenderMode::Normal {
self.tick_sprites(mapper);
if self.cur_dot < 256 {
let pixel_info = self.calculate_cur_pixel();
if pixel_info.sprite_0_hit {
self.status |= PPUSTATUS_SPRITE_0_HIT;
}
self.output_pixel(buffer, pixel_info.color);
self.update_sprite_counters_and_shift_regs();
}
} else if self.cur_dot > 256 {
self.tick_sprites(mapper);
}
match self.cur_dot {
1 if mode == RenderMode::PreRender => {
self.status &= !(PPUSTATUS_SPRITE_0_HIT | PPUSTATUS_OVERFLOW | PPUSTATUS_VBLANK);
}
256 => {
self.increment_y();
}
257 => {
self.flush_horizontal_scroll();
}
280..=304 if mode == RenderMode::PreRender => {
self.flush_vertical_scroll();
}
337 | 339 => {
mapper.read(self.nametable_address());
}
_ => {}
}
}
fn tick_vblank(&mut self) {
if self.cur_scanline == 241 && self.cur_dot == 1 {
self.status |= PPUSTATUS_VBLANK;
}
}
}