cge_nes 0.1.2

Cycle-accurate NES (Nintendo Entertainment System) emulator library: CPU, PPU, cartridge, input, and iNES ROM loading.
Documentation
//! Palette RAM implementation and palette helpers.
//!
//! Models the PPU palette RAM (0x3F00..=0x3F1F), including mirroring behavior
//! and helpers for palette/color indexing and lookups.
use crate::ppu::palette::color::EmphasisFlags;
use crate::ppu::palette::Color;
use devices6502;

#[derive(Debug)]
/// Compact index into one of the four-color palettes (background or sprite).
///
/// The low two bits select the color within a palette (0..=3). The next two
/// bits select which of the four palettes to use (0..=3).
pub struct PaletteColorIndex(u8);

/// Size in bytes of the PPU's palette RAM ($3F00..=$3F1F), including mirrors.
const PALETTE_RAM_SIZE: usize = 32;

#[derive(Default)]
/// PPU palette RAM device, handling mirroring behavior and color lookups.
pub struct Ram {
    /// Raw palette bytes; mirrored addresses write-through as on real hardware.
    data: [u8; PALETTE_RAM_SIZE],
}

impl PaletteColorIndex {
    /// Construct a combined palette/color index.
    #[inline]
    pub fn new(palette_index: u8, color_index: u8) -> Self {
        debug_assert!(color_index < 4);
        debug_assert!(palette_index < 4);
        let high = (palette_index & 0x03) << 2;
        let low = color_index & 0x3;
        Self(high | low)
    }

    /// Return the 2-bit palette index (0..=3).
    #[inline]
    pub fn palette_index(&self) -> u8 {
        (self.0 & 0x0C) >> 2
    }

    /// Return the 2-bit color index within the palette (0..=3).
    #[inline]
    pub fn color_index(&self) -> u8 {
        self.0 & 0x03
    }

    /// Decompose into (palette_index, color_index).
    pub fn destruct(&self) -> (u8, u8) {
        (self.palette_index(), self.color_index())
    }
}

impl From<PaletteColorIndex> for usize {
    fn from(value: PaletteColorIndex) -> Self {
        value.0 as Self
    }
}

impl From<u8> for PaletteColorIndex {
    fn from(value: u8) -> Self {
        Self(value)
    }
}

impl Ram {
    /// Create a new palette RAM with all bytes initialized to zero.
    pub fn new() -> Self {
        Self::default()
    }

    /// Read raw palette byte at index, applying palette RAM mirroring.
    pub fn read(&self, index: usize) -> u8 {
        self.data[index % PALETTE_RAM_SIZE]
    }

    /// Write raw palette byte at index, performing mirror write when required.
    ///
    /// On the NES, addresses that are multiples of 4 mirror to the corresponding
    /// entry with bit 4 toggled (background color mirrors into sprite background, etc.).
    pub fn write(&mut self, index: usize, data: u8) {
        self.data[index % PALETTE_RAM_SIZE] = data;

        if (index % 4) == 0 {
            // Writing to a mirrored location
            // Flip te 5th bit and write again
            let addr = index ^ 0x10;
            self.data[addr % PALETTE_RAM_SIZE] = data;
        }
    }

    /// Read the universal background color ($3F00).
    #[inline]
    pub fn read_background_color(&self, emphasis: EmphasisFlags, greyscale: bool) -> Color {
        let v = if greyscale {
            self.data[0] & 0x30
        } else {
            self.data[0]
        };
        Color::new(v, emphasis)
    }

    /// Read a color from either a background or sprite palette based on index.
    #[inline]
    pub fn read_palette_color(
        &self,
        index: PaletteColorIndex,
        is_sprite: bool,
        emphasis: EmphasisFlags,
        greyscale: bool,
    ) -> Color {
        const BACKGROUND_PALETTES_START_INDEX: u8 = 1;
        const SPRITE_PALETTES_START_INDEX: u8 = 17;

        let addr = if index.color_index() == 0 {
            0
        } else {
            let start_index = if is_sprite {
                SPRITE_PALETTES_START_INDEX
            } else {
                BACKGROUND_PALETTES_START_INDEX
            };

            (index.palette_index() * 4) + (index.color_index() - 1) + start_index
        } as usize;

        let v = if greyscale {
            self.data[addr] & 0x30
        } else {
            self.data[addr]
        };
        Color::new(v, emphasis)
    }

    /// Convenience: read a color from a background palette.
    #[inline]
    pub fn read_background_palette_color(
        &self,
        index: PaletteColorIndex,
        emphasis: EmphasisFlags,
        greyscale: bool,
    ) -> Color {
        self.read_palette_color(index, false, emphasis, greyscale)
    }

    /// Convenience: read a color from a sprite palette.
    #[inline]
    pub fn read_sprite_palette_color(
        &self,
        index: PaletteColorIndex,
        emphasis: EmphasisFlags,
        greyscale: bool,
    ) -> Color {
        self.read_palette_color(index, true, emphasis, greyscale)
    }
}

impl devices6502::Device for Ram {
    fn with_data(data: &[u8]) -> Self {
        let mut new_ram = Ram::default();
        new_ram.init_data(data);
        new_ram
    }

    fn init_data(&mut self, data: &[u8]) {
        self.data.copy_from_slice(data);
    }

    fn cache_current_read_data(&self, destination: &mut [u8]) {
        destination.copy_from_slice(&self.data);
    }

    fn read(&self, addr: u16) -> u8 {
        self.read(addr as usize)
    }

    fn write(&mut self, data: u8, addr: u16) {
        self.write(addr as usize, data);
    }

    fn addr_space_size() -> u32 {
        PALETTE_RAM_SIZE as u32
    }

    fn addr_bits_count() -> u8 {
        PALETTE_RAM_SIZE.ilog2() as u8
    }

    fn addr_space_size_dyn(&self) -> u32 {
        PALETTE_RAM_SIZE as u32
    }

    fn addr_bits_count_dyn(&self) -> u8 {
        PALETTE_RAM_SIZE.ilog2() as u8
    }
}

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

    fn multiple_of_four(v: usize) -> bool {
        (v % 4) == 0
    }

    #[test]
    fn createpaletteram_writevalues_colorscorrect() {
        let mut palette_ram = Ram::new();

        let colors: Vec<Color> = (0u8..(PALETTE_RAM_SIZE as u8))
            .map(|x| Color::new(x, EmphasisFlags::empty()))
            .collect();

        for (i, color) in colors.iter().enumerate() {
            if !multiple_of_four(i) || (i < 16) {
                // Don't write mirrored values
                palette_ram.write(i, color.color_index());
            }
        }

        assert_eq!(
            palette_ram.read_background_color(EmphasisFlags::empty(), false),
            colors[0]
        );

        for bg_palette_index in 0..4 {
            for color_index in 0..4 {
                let original_color = if color_index == 0 {
                    colors[0]
                } else {
                    colors[bg_palette_index * 4 + color_index]
                };
                let read_ram_color = palette_ram.read_background_palette_color(
                    PaletteColorIndex::new(bg_palette_index as u8, color_index as u8),
                    EmphasisFlags::empty(),
                    false,
                );
                assert_eq!(original_color, read_ram_color);
            }
        }

        for sprite_palette_index in 0..4 {
            for color_index in 0..4 {
                let original_color = if color_index == 0 {
                    colors[0]
                } else {
                    colors[sprite_palette_index * 4 + color_index + 16]
                };
                let read_ram_color = palette_ram.read_sprite_palette_color(
                    PaletteColorIndex::new(sprite_palette_index as u8, color_index as u8),
                    EmphasisFlags::empty(),
                    false,
                );
                assert_eq!(original_color, read_ram_color);
            }
        }
    }

    #[test]
    fn createpaletteram_writemirroredvalues_colorscorrect() {
        let mut palette_ram = Ram::new();

        let mirrored_pairs: Vec<(usize, usize)> = (0..4).map(|i| (i * 4, i * 4 + 16)).collect();

        for (i, (first, second)) in mirrored_pairs.iter().enumerate() {
            palette_ram.write(*first, 1 + i as u8);
            assert_eq!(palette_ram.read(*second), 1 + i as u8);

            palette_ram.write(*second, 10 + i as u8);
            assert_eq!(palette_ram.read(*first), 10 + i as u8);
        }
    }
}