neser 0.3.1

NESER - NES Emulator in Rust. Desktop (SDL) and WebAssembly frontends.
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);
    }
}