cge_nes 0.1.2

Cycle-accurate NES (Nintendo Entertainment System) emulator library: CPU, PPU, cartridge, input, and iNES ROM loading.
Documentation
//! Pixel rendering and color calculation for the PPU.
//!
//! This module handles the final stage of PPU rendering, where background and sprite
//! colors are determined for each pixel. It implements:
//! * Background tile color calculation
//! * Sprite color calculation
//! * Sprite priority handling
//! * Color palette lookup
//! * Sprite zero hit detection

use crate::ppu::palette::{Color, PaletteColorIndex};
use crate::ppu::sprite::SpriteData;
use crate::ppu::{Ppu, TILE_DIM};

/// Represents a background tile to be rendered at the current pixel.
///
/// Contains the color pattern data and palette index needed to render
/// one row of a background tile.
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub struct BgTileToShow {
    /// Two bytes containing the color pattern for one row (high and low bits)
    row_color_index_bytes: (u8, u8),
    /// Index into the background palette (0-3)
    palette_index: u8,
}

impl BgTileToShow {
    /// Creates a new background tile with the given pattern data and palette index.
    ///
    /// # Parameters
    /// * `row_color_index_bytes` - Tuple of (high byte, low byte) for the tile pattern
    /// * `palette_index` - Index (0-3) selecting which background palette to use
    pub fn new(row_color_index_bytes: (u8, u8), palette_index: u8) -> Self {
        Self {
            row_color_index_bytes,
            palette_index,
        }
    }
}

impl Ppu {
    /// Extracts a 2-bit color index from a pair of pattern table bytes.
    ///
    /// # Parameters
    /// * `row_bytes` - Tuple of (high byte, low byte) from pattern table
    /// * `local_x` - X position within the 8-pixel tile (0-7)
    ///
    /// # Returns
    /// A 2-bit color index (0-3) for the specified pixel position
    #[inline]
    fn get_color_index_from_row_bytes(row_bytes: (u8, u8), local_x: u8) -> u8 {
        let local_x = 7 - (local_x & 0x7);

        let high_bit = (row_bytes.0 >> local_x) & 1;
        let low_bit = (row_bytes.1 >> local_x) & 1;

        (high_bit << 1) | low_bit
    }

    /// Calculates the background color for a pixel, if any.
    ///
    /// # Parameters
    /// * `x` - X coordinate on screen
    /// * `tile` - Background tile data for the current pixel
    ///
    /// # Returns
    /// `Some(Color)` if a non-transparent background pixel should be drawn,
    /// `None` if the background is transparent at this position
    #[inline]
    fn calculate_bg_color(&self, x: u8, tile: BgTileToShow) -> Option<Color> {
        let local_x = (x.wrapping_add(self.regs.scroll().0)) % TILE_DIM;

        let color_index = Ppu::get_color_index_from_row_bytes(tile.row_color_index_bytes, local_x);

        if color_index != 0 {
            let color = self.palette.read_background_palette_color(
                PaletteColorIndex::new(tile.palette_index, color_index),
                self.regs.ppu_mask().into(),
                self.regs.greyscale_flag(),
            );
            return Some(color);
        }

        None
    }

    /// Calculates the sprite color for a pixel, if any.
    ///
    /// # Parameters
    /// * `x` - X coordinate on screen
    /// * `sprite` - Sprite data for potential rendering
    ///
    /// # Returns
    /// `Some(Color)` if a non-transparent sprite pixel should be drawn,
    /// `None` if the sprite is transparent at this position
    #[inline]
    fn calculate_sprite_color(&self, x: u8, sprite: &SpriteData) -> Option<Color> {
        // Get the pattern table data for this sprite's current row
        let row_bytes = sprite
            .row_color_index_bytes()
            //.expect("SpriteData has not been initialised with pixel color indices");
            .unwrap_or((0xF, 0xF));

        // Calculate the x position within the sprite (0-7)
        let mut local_x = x.saturating_sub(sprite.obj_attributes().x()) % TILE_DIM;
        // If the sprite is horizontally flipped, invert the x position
        if sprite.obj_attributes().horizontal_flip_flag() {
            local_x = !local_x;
        }
        // Extract the 2-bit color index for this pixel
        let color_index = Ppu::get_color_index_from_row_bytes(row_bytes, local_x);

        // If the color is not transparent (0)
        if color_index != 0 {
            // Look up the actual color from the sprite's palette
            let color = self.palette.read_sprite_palette_color(
                PaletteColorIndex::new(sprite.obj_attributes().palette_index(), color_index),
                self.regs.ppu_mask().into(),
                self.regs.greyscale_flag(),
            );
            return Some(color);
        }

        None
    }

    /// Calculates the final color for a pixel, considering both background and sprites.
    ///
    /// This function implements the NES PPU's sprite priority system and returns
    /// the appropriate color based on background tiles, sprite priorities, and
    /// transparency.
    ///
    /// # Parameters
    /// * `coords` - Tuple of (x,y) coordinates on screen
    /// * `tile` - Optional background tile data for this pixel
    /// * `show_sprites` - Whether sprite rendering is enabled for this pixel
    ///
    /// # Returns
    /// The final `Color` to be displayed for this pixel
    #[inline]
    pub(super) fn calculate_pixel_color_index(
        &mut self,
        coords: (u8, u8),
        tile: Option<BgTileToShow>,
        show_sprites: bool,
    ) -> Color {
        // Get the list of sprites to process for this pixel
        let sprites = if show_sprites {
            self.sprites_at_current_pixel.as_slice()
        } else {
            [].as_slice()
        };

        // Scan sprites in OAM order and remember the first one with an opaque
        // pixel. We cache the color so we don't recompute it later.
        let mut first_opaque: Option<(&SpriteData, Color)> = None;
        for sprite in sprites.iter() {
            if let Some(sprite_color) = self.calculate_sprite_color(coords.0, sprite) {
                first_opaque = Some((sprite, sprite_color));
                break;
            }
        }

        // Sprite zero hit fires when sprite 0 has an opaque pixel and the BG
        // also has an opaque pixel at this coordinate, regardless of sprite 0's
        // priority bit. The hit is suppressed on the leftmost 8 pixels (handled
        // by `show_sprites` already returning false there).
        if let Some((sprite, _)) = first_opaque {
            if sprite.oam_index() == 0 {
                if let Some(tile) = tile {
                    if self.calculate_bg_color(coords.0, tile).is_some() {
                        self.regs.set_sprite_zero_hit();
                    }
                }
            }
        }

        // Resolve the final color based on the first opaque sprite and BG.
        if let Some((sprite, sprite_color)) = first_opaque {
            // PRIORITY bit set means "draw behind opaque BG".
            let sprite_behind_bg = !sprite.obj_attributes().draw_in_front_flag();
            if sprite_behind_bg {
                if let Some(tile) = tile {
                    if let Some(bg_color) = self.calculate_bg_color(coords.0, tile) {
                        return bg_color;
                    }
                }
            }
            return sprite_color;
        }

        // No opaque sprite at this pixel: render the background, falling back
        // to the universal backdrop color if there is no BG tile.
        if let Some(tile) = tile {
            if let Some(bg_color) = self.calculate_bg_color(coords.0, tile) {
                return bg_color;
            }
        }

        self.palette
            .read_background_color(self.regs.ppu_mask().into(), self.regs.greyscale_flag())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn color_index_from_row() {
        for i in 0..8 {
            let high_byte = 1 << (7 - i);
            let low_byte = !high_byte;

            for x in 0..8 {
                let color_index = Ppu::get_color_index_from_row_bytes((high_byte, low_byte), x);
                if i == x {
                    assert_eq!(color_index, 2);
                } else {
                    assert_eq!(color_index, 1);
                }
            }
        }
    }
}