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
//! APU length counter
//!
//! Specs: https://www.nesdev.org/apu_ref.txt
use crate::trace_apu;

/// Length counter lookup table (32 entries), indexed by bits 7-3 of $4003/$4007/$400B/$400F.
const LENGTH_COUNTER_TABLE: [u8; 32] = [
    10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 12, 16, 24, 18, 48, 20, 96, 22,
    192, 24, 72, 26, 16, 28, 32, 30,
];

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct LengthCounter {
    enabled: bool,
    halt: bool,
    pending_halt: Option<bool>,
    reload_value: u8,
    previous_value: u8,
    value: u8,
}

impl LengthCounter {
    pub fn new() -> Self {
        Self::default()
    }

    /// Reset length counter to initial state
    pub fn reset(&mut self) {
        *self = Self::default();
    }

    pub fn lookup(index: u8) -> u8 {
        LENGTH_COUNTER_TABLE[(index & 0x1F) as usize]
    }

    pub fn set_enabled(&mut self, enabled: bool) {
        trace_apu!(2; "length_counter set_enabled {}", enabled);
        self.enabled = enabled;
        if !enabled {
            // NESDev: when a channel is disabled via $4015, its length counter is cleared.
            self.value = 0;
        }
    }

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

    pub fn set_halt(&mut self, halt: bool) {
        trace_apu!(3; "length_counter set_halt {}", halt);
        self.pending_halt = Some(halt);
    }

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

    pub fn pending_halt(&self) -> Option<bool> {
        self.pending_halt
    }

    pub fn clear(&mut self) {
        trace_apu!(4; "length_counter clear");
        self.value = 0;
    }

    pub fn reload_counter(&mut self) {
        if self.reload_value != 0 {
            if self.value == self.previous_value {
                self.value = self.reload_value;
            }
            self.reload_value = 0;
        }
    }

    pub fn apply_pending_halt(&mut self) {
        if let Some(halt) = self.pending_halt.take() {
            self.halt = halt;
            trace_apu!(4; "length_counter apply_pending_halt {}", halt);
        }
    }

    pub fn set_halt_state(&mut self, halt: bool, pending_halt: Option<bool>) {
        self.halt = halt;
        self.pending_halt = pending_halt;
    }

    pub fn set_reload_state(&mut self, reload_value: u8, previous_value: u8) {
        self.reload_value = reload_value;
        self.previous_value = previous_value;
    }

    pub fn load_from_index(&mut self, index: u8) {
        if self.enabled {
            trace_apu!(3; "length_counter load index={} value={}", index & 0x1F, Self::lookup(index));
            self.reload_value = Self::lookup(index);
            self.previous_value = self.value;
        }
    }

    pub fn clock(&mut self) {
        if !self.halt && self.value > 0 {
            self.value -= 1;
            trace_apu!(5; "length_counter clock value={}", self.value);
        }
    }

    pub fn value(&self) -> u8 {
        self.value
    }

    pub fn reload_value(&self) -> u8 {
        self.reload_value
    }

    pub fn previous_value(&self) -> u8 {
        self.previous_value
    }

    /// Set the length counter value directly (for save-state restore).
    pub fn set_value(&mut self, value: u8) {
        self.value = value;
    }

    /// Enable the length counter (for save-state restore).
    pub fn enable(&mut self) {
        self.enabled = true;
    }

    /// Disable the length counter (for save-state restore).
    pub fn disable(&mut self) {
        self.enabled = false;
    }
}

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

    #[test]
    fn length_counter_table_matches_nesdev() {
        // Values from NESDev APU reference: 32-entry length table.
        let expected: [u8; 32] = [
            10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 12, 16, 24, 18, 48, 20,
            96, 22, 192, 24, 72, 26, 16, 28, 32, 30,
        ];

        for (i, &value) in expected.iter().enumerate() {
            assert_eq!(LengthCounter::lookup(i as u8), value, "index {i}");
        }
    }

    #[test]
    fn load_does_nothing_when_disabled() {
        let mut lc = LengthCounter::new();
        lc.set_enabled(false);

        lc.load_from_index(0);
        lc.reload_counter();
        assert_eq!(lc.value(), 0);
    }

    #[test]
    fn load_sets_value_when_enabled() {
        let mut lc = LengthCounter::new();
        lc.set_enabled(true);

        lc.load_from_index(0);
        lc.reload_counter();
        assert_eq!(lc.value(), 10);

        lc.load_from_index(1);
        lc.reload_counter();
        assert_eq!(lc.value(), 254);
    }

    #[test]
    fn clock_decrements_when_not_halted() {
        let mut lc = LengthCounter::new();
        lc.set_enabled(true);
        lc.set_halt(false);
        lc.apply_pending_halt();

        lc.load_from_index(0); // 10
        lc.reload_counter();
        lc.clock();
        assert_eq!(lc.value(), 9);
    }

    #[test]
    fn clock_does_not_decrement_when_halted() {
        let mut lc = LengthCounter::new();
        lc.set_enabled(true);
        lc.set_halt(true);
        lc.apply_pending_halt();

        lc.load_from_index(0); // 10
        lc.reload_counter();
        lc.clock();
        assert_eq!(lc.value(), 10);
    }

    #[test]
    fn disabling_clears_the_counter_immediately() {
        let mut lc = LengthCounter::new();
        lc.set_enabled(true);
        lc.load_from_index(0);
        lc.reload_counter();
        assert_eq!(lc.value(), 10);

        lc.set_enabled(false);
        assert_eq!(lc.value(), 0);
    }

    #[test]
    fn reset_restores_length_counter_to_initial_state() {
        let mut lc = LengthCounter::new();
        // Modify all fields
        lc.set_enabled(true);
        lc.set_halt(true);
        lc.apply_pending_halt();
        lc.load_from_index(1); // 254
        lc.reload_counter();

        // Verify state changed
        assert!(lc.is_enabled());
        assert!(lc.is_halted());
        assert_eq!(lc.value(), 254);

        // Reset
        lc.reset();

        // Verify all fields back to default
        assert!(!lc.is_enabled());
        assert!(!lc.is_halted());
        assert_eq!(lc.value(), 0);
    }
}