neser 0.1.0

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
/// Manages PPU status flags including VBlank, sprite 0 hit, and NMI
pub struct Status {
    /// VBlank flag (bit 7 of status register)
    vblank_flag: bool,
    /// Sprite 0 Hit flag (bit 6 of status register)
    sprite_0_hit: bool,
    /// Pending sprite 0 hit (becomes readable next cycle)
    pending_sprite_0_hit: bool,
    /// Sprite Overflow flag (bit 5 of status register)
    sprite_overflow: bool,
    /// NMI enabled flag
    nmi_enabled: bool,
    /// Frame complete flag - set when VBlank starts, regardless of NMI generation
    frame_complete: bool,
}

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

impl Status {
    /// Create a new Status instance
    pub fn new() -> Self {
        Self {
            vblank_flag: false,
            sprite_0_hit: false,
            pending_sprite_0_hit: false,
            sprite_overflow: false,
            nmi_enabled: false,
            frame_complete: false,
        }
    }

    /// Reset status to initial state
    pub fn reset(&mut self) {
        self.vblank_flag = false;
        self.sprite_0_hit = false;
        self.pending_sprite_0_hit = false;
        self.sprite_overflow = false;
        self.nmi_enabled = false;
        self.frame_complete = false;
    }

    /// Enter VBlank period
    pub fn enter_vblank(&mut self) {
        self.vblank_flag = true;
        self.frame_complete = true;
    }

    /// Exit VBlank period (clear VBL flag, but NOT sprite flags)
    pub fn exit_vblank(&mut self) {
        self.vblank_flag = false;
        // Note: Sprite 0 hit and sprite overflow are cleared separately at scanline 261, pixel 0
    }

    /// Clear sprite 0 hit and sprite overflow flags (happens at scanline 261, pixel 0)
    pub fn clear_sprite_flags(&mut self) {
        self.sprite_0_hit = false;
        self.pending_sprite_0_hit = false;
        self.sprite_overflow = false;
    }

    /// Trigger NMI edge (used when NMI is enabled mid-VBlank)
    pub fn trigger_nmi(&mut self) {
        self.nmi_enabled = true;
    }

    /// Clear any latched/pending NMI edge.
    pub fn clear_nmi(&mut self) {
        self.nmi_enabled = false;
    }

    /// Read the status register (clears VBlank flag and write toggle)
    /// Returns the status byte
    pub fn read_status(&mut self) -> u8 {
        let status = self.peek_status();

        // Reading status clears VBlank flag.
        self.vblank_flag = false;

        status
    }

    /// Read the status register without side effects.
    /// Returns the status byte without clearing flags.
    pub fn peek_status(&self) -> u8 {
        let mut status = 0u8;

        if self.vblank_flag {
            status |= 0b1000_0000; // Bit 7: VBlank
        }
        if self.sprite_0_hit {
            status |= 0b0100_0000; // Bit 6: Sprite 0 hit
        }
        if self.sprite_overflow {
            status |= 0b0010_0000; // Bit 5: Sprite overflow
        }

        status
    }

    /// Poll NMI status and clear it
    pub fn poll_nmi(&mut self) -> bool {
        let result = self.nmi_enabled;
        self.nmi_enabled = false;
        result
    }

    /// Poll frame complete status and clear it
    pub fn poll_frame_complete(&mut self) -> bool {
        let result = self.frame_complete;
        self.frame_complete = false;
        result
    }

    /// Check if we're in VBlank period
    pub fn is_in_vblank(&self) -> bool {
        self.vblank_flag
    }

    /// Set sprite 0 hit flag immediately
    pub fn set_sprite_0_hit(&mut self) {
        self.sprite_0_hit = true;
    }

    /// Set sprite overflow flag
    pub fn set_sprite_overflow(&mut self) {
        self.sprite_overflow = true;
    }

    /// Check if sprite 0 hit flag is set
    pub fn is_sprite_0_hit(&self) -> bool {
        self.sprite_0_hit
    }

    /// Get sprite overflow flag for save-state.
    pub fn is_sprite_overflow(&self) -> bool {
        self.sprite_overflow
    }

    /// Get NMI enabled flag for save-state.
    pub fn is_nmi_enabled(&self) -> bool {
        self.nmi_enabled
    }

    pub fn pending_sprite_0_hit(&self) -> bool {
        self.pending_sprite_0_hit
    }

    pub fn frame_complete(&self) -> bool {
        self.frame_complete
    }

    /// Restore status state from a save-state.
    pub fn restore_state(
        &mut self,
        vblank: bool,
        sprite_0_hit: bool,
        sprite_overflow: bool,
        nmi_enabled: bool,
    ) {
        self.vblank_flag = vblank;
        self.sprite_0_hit = sprite_0_hit;
        self.pending_sprite_0_hit = false;
        self.sprite_overflow = sprite_overflow;
        self.nmi_enabled = nmi_enabled;
        // Note: frame_complete is derived state, will be set during next vblank
        self.frame_complete = false;
    }

    pub fn set_pending_sprite_0_hit(&mut self, pending: bool) {
        self.pending_sprite_0_hit = pending;
    }

    pub fn set_frame_complete(&mut self, complete: bool) {
        self.frame_complete = complete;
    }
}

#[cfg(test)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatusDebugState {
    pub vblank_flag: bool,
    pub sprite_0_hit: bool,
    pub pending_sprite_0_hit: bool,
    pub sprite_overflow: bool,
    pub nmi_enabled: bool,
    pub frame_complete: bool,
}

#[cfg(test)]
impl Status {
    pub fn debug_state(&self) -> StatusDebugState {
        StatusDebugState {
            vblank_flag: self.vblank_flag,
            sprite_0_hit: self.sprite_0_hit,
            pending_sprite_0_hit: self.pending_sprite_0_hit,
            sprite_overflow: self.sprite_overflow,
            nmi_enabled: self.nmi_enabled,
            frame_complete: self.frame_complete,
        }
    }

    pub fn set_debug_state(&mut self, state: StatusDebugState) {
        self.vblank_flag = state.vblank_flag;
        self.sprite_0_hit = state.sprite_0_hit;
        self.pending_sprite_0_hit = state.pending_sprite_0_hit;
        self.sprite_overflow = state.sprite_overflow;
        self.nmi_enabled = state.nmi_enabled;
        self.frame_complete = state.frame_complete;
    }
}

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

    #[test]
    fn test_status_new() {
        let status = Status::new();
        assert!(!status.is_in_vblank());
        assert!(!status.is_sprite_0_hit());
    }

    #[test]
    fn test_status_reset() {
        let mut status = Status::new();
        status.enter_vblank();
        status.reset();
        assert!(!status.is_in_vblank());
    }

    #[test]
    fn test_enter_vblank() {
        let mut status = Status::new();
        status.enter_vblank();
        assert!(status.is_in_vblank());
    }

    #[test]
    fn test_exit_vblank() {
        let mut status = Status::new();
        status.enter_vblank();
        status.exit_vblank();
        assert!(!status.is_in_vblank());
    }

    #[test]
    fn test_read_status_clears_vblank() {
        let mut status = Status::new();
        status.enter_vblank();

        let status_byte = status.read_status();
        assert_eq!(status_byte & 0b1000_0000, 0b1000_0000);
        assert!(!status.is_in_vblank());
    }

    #[test]
    fn test_peek_status_does_not_clear_vblank() {
        let mut status = Status::new();
        status.enter_vblank();

        let status_byte = status.peek_status();
        assert_eq!(status_byte & 0b1000_0000, 0b1000_0000);
        assert!(status.is_in_vblank());
    }

    #[test]
    fn test_read_status_clears_vblank_even_on_vblank_start() {
        let mut status = Status::new();
        status.enter_vblank();

        // Reading at the VBlank start dot should still clear the flag.
        let status_byte = status.read_status();
        assert_eq!(status_byte & 0b1000_0000, 0b1000_0000);
        assert!(!status.is_in_vblank());
    }

    #[test]
    fn test_sprite_0_hit() {
        let mut status = Status::new();
        status.set_sprite_0_hit();
        assert!(status.is_sprite_0_hit());

        let status_byte = status.read_status();
        assert_eq!(status_byte & 0b0100_0000, 0b0100_0000);
    }

    #[test]
    fn test_sprite_overflow() {
        let mut status = Status::new();
        status.set_sprite_overflow();

        let status_byte = status.read_status();
        assert_eq!(status_byte & 0b0010_0000, 0b0010_0000);
    }

    #[test]
    fn test_poll_nmi() {
        let mut status = Status::new();
        status.enter_vblank();

        // NMI edge must be explicitly latched.
        status.trigger_nmi();

        assert!(status.poll_nmi());
        assert!(!status.poll_nmi()); // Should be cleared after first poll
    }

    #[test]
    fn test_poll_frame_complete() {
        let mut status = Status::new();
        status.enter_vblank();

        assert!(status.poll_frame_complete());
        assert!(!status.poll_frame_complete()); // Should be cleared after first poll
    }
}