neser 0.1.1

NESER - NES Emulator in Rust - is a NES emulator written in Rust. It aims to be a high-quality, hardware-accurate emulator that is also easy to use and extend. It supports a wide range of NES games and features, including various mappers, audio processing, and input handling. NESER is designed to be modular and extensible, allowing developers to easily add new features or support for additional hardware. It can be run using one of two frontends: a native desktop application using SDL2, or a web application using WebAssembly. The desktop application provides a high-performance, feature-rich experience with support for various input devices and display options, while the web application allows users to play NES games directly in their browsers without needing to install any software in a BYOR manner (Bring Your Own Roms).
Documentation
use super::screen_buffer::ScreenBuffer;

/// Manages final pixel composition, color effects, and screen output
pub struct Rendering {
    /// Screen buffer for rendered pixels
    screen_buffer: ScreenBuffer,
    /// Whether Famicom emphasis bit swap is active (green/blue swapped)
    pub(crate) famicom_emphasis: bool,
}

impl Default for Rendering {
    fn default() -> Self {
        Self::new()
    }
}

impl Rendering {
    /// Create a new Rendering instance
    pub fn new() -> Self {
        Self {
            screen_buffer: ScreenBuffer::new(),
            famicom_emphasis: false,
        }
    }

    /// Get reference to screen buffer
    pub fn screen_buffer(&self) -> &ScreenBuffer {
        &self.screen_buffer
    }

    /// Get mutable reference to screen buffer
    pub fn screen_buffer_mut(&mut self) -> &mut ScreenBuffer {
        &mut self.screen_buffer
    }

    pub fn screen_buffer_snapshot(&self) -> Vec<u8> {
        self.screen_buffer.snapshot()
    }

    pub fn screen_buffer_crc32(&self) -> u32 {
        self.screen_buffer.crc32()
    }

    pub fn restore_screen_buffer(&mut self, data: &[u8]) {
        self.screen_buffer.restore_from_snapshot(data);
    }

    /// Compose and render a pixel to the screen buffer
    #[cfg(test)]
    #[allow(clippy::too_many_arguments)]
    pub fn render_pixel(
        &mut self,
        screen_x: u32,
        screen_y: u32,
        bg_pixel: u8,
        sprite_pixel: Option<(u8, usize, bool)>,
        grayscale: bool,
        color_emphasis: u8,
        palette_lookup: impl Fn(u8) -> u8,
        system_palette_lookup: impl Fn(u8) -> (u8, u8, u8),
    ) -> bool {
        let (palette_index, sprite_0_hit) = select_palette_index(bg_pixel, sprite_pixel);

        // Look up the color in the palette RAM
        let color_value =
            crate::ppu::color_effects::apply_grayscale(palette_lookup(palette_index), grayscale);

        // Convert to RGB using the system palette
        let (mut r, mut g, mut b) = system_palette_lookup(color_value);

        // Apply color emphasis/tint
        (r, g, b) = crate::ppu::color_effects::apply_color_emphasis(
            r,
            g,
            b,
            color_emphasis,
            self.famicom_emphasis,
        );

        // Write to the screen buffer
        self.screen_buffer.set_pixel(screen_x, screen_y, r, g, b);

        sprite_0_hit
    }
}

#[cfg(test)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderingDebugState {
    pub screen_buffer: super::screen_buffer::ScreenBufferDebugState,
}

#[cfg(test)]
impl Rendering {
    pub fn debug_state(&self) -> RenderingDebugState {
        RenderingDebugState {
            screen_buffer: self.screen_buffer.debug_state(),
        }
    }

    pub fn set_debug_state(&mut self, state: RenderingDebugState) {
        self.screen_buffer.set_debug_state(state.screen_buffer);
    }
}

#[cfg(test)]
#[inline(always)]
fn select_palette_index(bg_pixel: u8, sprite_pixel: Option<(u8, usize, bool)>) -> (u8, bool) {
    let mut palette_index = bg_pixel;
    let mut sprite_0_hit = false;

    if let Some((sprite_palette_idx, sprite_idx, is_foreground)) = sprite_pixel {
        if bg_pixel == 0 || is_foreground {
            palette_index = sprite_palette_idx;
        }
        sprite_0_hit = sprite_idx == 0 && bg_pixel != 0;
    }

    (palette_index, sprite_0_hit)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ppu::color_effects::{apply_color_emphasis, apply_grayscale};

    #[test]
    fn test_rendering_new() {
        let rendering = Rendering::new();
        assert!(rendering.screen_buffer().get_pixel(0, 0) == (0, 0, 0));
    }

    #[test]
    fn test_render_pixel_background() {
        let mut rendering = Rendering::new();
        rendering.render_pixel(
            10,
            10,
            1,               // bg_pixel
            None,            // sprite_pixel
            false,           // grayscale
            0,               // color_emphasis
            |idx| idx,       // palette_lookup
            |_| (255, 0, 0), // system_palette_lookup (red)
        );
        assert_eq!(rendering.screen_buffer().get_pixel(10, 10), (255, 0, 0));
    }

    #[test]
    fn test_render_pixel_sprite_foreground() {
        let mut rendering = Rendering::new();
        rendering.render_pixel(
            10,
            10,
            1,                   // bg_pixel (opaque)
            Some((16, 0, true)), // sprite_pixel (foreground)
            false,
            0,
            |idx| idx,
            |idx| if idx == 16 { (0, 255, 0) } else { (255, 0, 0) },
        );
        // Sprite should be shown (foreground)
        assert_eq!(rendering.screen_buffer().get_pixel(10, 10), (0, 255, 0));
    }

    #[test]
    fn test_render_pixel_sprite_background_priority() {
        let mut rendering = Rendering::new();
        rendering.render_pixel(
            10,
            10,
            1,                    // bg_pixel (opaque)
            Some((16, 0, false)), // sprite_pixel (background priority)
            false,
            0,
            |idx| idx,
            |idx| if idx == 1 { (255, 0, 0) } else { (0, 255, 0) },
        );
        // Background should be shown (sprite has background priority)
        assert_eq!(rendering.screen_buffer().get_pixel(10, 10), (255, 0, 0));
    }

    #[test]
    fn test_render_pixel_grayscale() {
        let mut rendering = Rendering::new();
        rendering.render_pixel(
            10,
            10,
            1,
            None,
            true, // grayscale
            0,
            // Distinct palette values ensure masking happens on the palette value (0 -> 0x00, 1 -> 0x2F)
            |palette_index| {
                if palette_index == 0 { 0x00 } else { 0x2F }
            },
            |color_value| (color_value, color_value, color_value),
        );
        assert_eq!(
            rendering.screen_buffer().get_pixel(10, 10),
            (0x20, 0x20, 0x20)
        );
    }

    #[test]
    fn test_render_pixel_sprite_0_hit() {
        let mut rendering = Rendering::new();
        let hit = rendering.render_pixel(
            10,
            10,
            1,                   // bg_pixel (opaque)
            Some((16, 0, true)), // sprite 0 (index 0)
            false,
            0,
            |idx| idx,
            |_| (255, 255, 255),
        );
        assert!(hit); // Should detect sprite 0 hit
    }

    #[test]
    fn test_render_pixel_no_sprite_0_hit_transparent_bg() {
        let mut rendering = Rendering::new();
        let hit = rendering.render_pixel(
            10,
            10,
            0,                   // bg_pixel (transparent)
            Some((16, 0, true)), // sprite 0
            false,
            0,
            |idx| idx,
            |_| (255, 255, 255),
        );
        assert!(!hit); // No hit when bg is transparent
    }

    #[test]
    fn test_select_palette_index_sprite_0_hit_foreground() {
        let (palette_index, sprite_0_hit) = select_palette_index(1, Some((16, 0, true)));
        assert_eq!(palette_index, 16);
        assert!(sprite_0_hit);
    }

    #[test]
    fn test_apply_color_emphasis_red_only() {
        let (r, g, b) = apply_color_emphasis(100, 100, 100, 0x01, false);
        assert_eq!(r, 110);
        assert_eq!(g, 75);
        assert_eq!(b, 75);
    }

    #[test]
    fn test_apply_grayscale_enabled() {
        assert_eq!(apply_grayscale(0x2f, true), 0x20);
        assert_eq!(apply_grayscale(0x2f, false), 0x2f);
    }
}