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
//! DMA Controller for cycle-accurate OAM DMA and DMC DMA handling.
//!
//! This module implements the DMA state machines that allow:
//! - OAM DMA to be executed cycle-by-cycle
//! - DMC DMA to interrupt OAM DMA mid-transfer
//! - Proper priority handling (DMC has higher priority than OAM)

/// OAM DMA state machine states
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OamDmaState {
    /// No OAM DMA in progress
    Idle,
    /// Waiting for CPU read cycle to start
    Pending { page: u8 },
    /// Alignment cycle (dummy read, odd-cycle alignment)
    Aligning { page: u8 },
    /// Reading byte from source page
    Reading { page: u8, offset: u8 },
    /// Writing byte to $2004
    Writing { page: u8, offset: u8, value: u8 },
}

/// DMC DMA state machine states
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DmcDmaState {
    /// No DMC DMA in progress
    Idle,
    /// Halt cycle - waiting on CPU read, halted_addr is the address CPU was trying to read
    Halt { halted_addr: u16 },
    /// Dummy cycle - repeat the halted read
    Dummy { halted_addr: u16 },
    /// Alignment cycle (if needed)
    Aligning { halted_addr: u16 },
    /// Get cycle - read the sample byte
    Reading,
}

/// DMA Controller manages both OAM DMA and DMC DMA state
#[derive(Debug, Clone)]
pub struct DmaController {
    /// OAM DMA state machine
    pub oam_state: OamDmaState,
    /// DMC DMA state machine
    pub dmc_state: DmcDmaState,
    /// Total cycles consumed by current DMA operation(s)
    pub cycles_consumed: u16,
    /// Whether an OAM DMA alignment cycle is needed
    oam_needs_align: bool,
    /// Tracks if we're on a "get" cycle (even total cycles) for DMC DMA alignment
    is_get_cycle: bool,
}

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

impl DmaController {
    pub fn new() -> Self {
        Self {
            oam_state: OamDmaState::Idle,
            dmc_state: DmcDmaState::Idle,
            cycles_consumed: 0,
            oam_needs_align: false,
            is_get_cycle: true,
        }
    }

    /// Check if any DMA is currently active (OAM or DMC)
    pub fn is_active(&self) -> bool {
        self.oam_state != OamDmaState::Idle || self.dmc_state != DmcDmaState::Idle
    }

    /// Check if OAM DMA is active (pending or in progress)
    pub fn oam_dma_active(&self) -> bool {
        self.oam_state != OamDmaState::Idle
    }

    /// Check if DMC DMA is active
    pub fn dmc_dma_active(&self) -> bool {
        self.dmc_state != DmcDmaState::Idle
    }

    /// Start an OAM DMA transfer from the given page
    /// Called when CPU writes to $4014
    pub fn start_oam_dma(&mut self, page: u8, is_odd_cycle: bool) {
        self.oam_state = OamDmaState::Pending { page };
        self.oam_needs_align = is_odd_cycle;
        self.cycles_consumed = 0;
    }

    /// Start a DMC DMA request
    /// Called when DMC needs a sample byte
    pub fn start_dmc_dma(&mut self, halted_addr: u16) {
        if self.dmc_state == DmcDmaState::Idle {
            self.dmc_state = DmcDmaState::Halt { halted_addr };
        }
    }

    /// Check if we need to halt the CPU for DMC DMA
    /// Returns true if DMC DMA should start on the current read cycle
    pub fn should_start_dmc_dma(&self) -> bool {
        matches!(self.dmc_state, DmcDmaState::Halt { .. })
    }

    /// Step the DMA state machine by one CPU cycle
    /// Returns a DmaAction describing what memory access to perform this cycle
    pub fn step(&mut self, total_cpu_cycles: u64) -> DmaAction {
        self.is_get_cycle = total_cpu_cycles.is_multiple_of(2);

        // DMC DMA has priority over OAM DMA
        if self.dmc_state != DmcDmaState::Idle {
            return self.step_dmc_dma();
        }

        // OAM DMA
        if self.oam_state != OamDmaState::Idle {
            return self.step_oam_dma();
        }

        DmaAction::None
    }

    fn step_dmc_dma(&mut self) -> DmaAction {
        self.cycles_consumed += 1;

        match self.dmc_state {
            DmcDmaState::Idle => DmaAction::None,

            DmcDmaState::Halt { halted_addr } => {
                // Halt cycle: repeat the CPU's halted read
                self.dmc_state = DmcDmaState::Dummy { halted_addr };
                DmaAction::DummyRead(halted_addr)
            }

            DmcDmaState::Dummy { halted_addr } => {
                // Dummy cycle complete, check if we need alignment
                if !self.is_get_cycle {
                    self.dmc_state = DmcDmaState::Aligning { halted_addr };
                    DmaAction::DummyRead(halted_addr)
                } else {
                    self.dmc_state = DmcDmaState::Reading;
                    DmaAction::DmcRead
                }
            }

            DmcDmaState::Aligning { .. } => {
                // Alignment cycle complete, do the actual read
                self.dmc_state = DmcDmaState::Reading;
                DmaAction::DmcRead
            }

            DmcDmaState::Reading => {
                // DMC read complete
                self.dmc_state = DmcDmaState::Idle;
                DmaAction::None
            }
        }
    }

    fn step_oam_dma(&mut self) -> DmaAction {
        self.cycles_consumed += 1;

        match self.oam_state {
            OamDmaState::Idle => DmaAction::None,

            OamDmaState::Pending { page } => {
                // First cycle: halt
                if self.oam_needs_align {
                    self.oam_state = OamDmaState::Aligning { page };
                    DmaAction::DummyRead(0) // Dummy read for halt
                } else {
                    self.oam_state = OamDmaState::Reading { page, offset: 0 };
                    DmaAction::DummyRead(0) // Dummy read for halt
                }
            }

            OamDmaState::Aligning { page } => {
                // Alignment cycle complete, start reading
                self.oam_state = OamDmaState::Reading { page, offset: 0 };
                DmaAction::DummyRead(0) // Dummy cycle
            }

            OamDmaState::Reading { page, offset } => {
                // Read from source
                let addr = ((page as u16) << 8) | (offset as u16);
                self.oam_state = OamDmaState::Writing {
                    page,
                    offset,
                    value: 0, // Will be filled after read
                };
                DmaAction::OamRead(addr)
            }

            OamDmaState::Writing {
                page,
                offset,
                value,
            } => {
                // Write to $2004
                let next_offset = offset.wrapping_add(1);
                if next_offset == 0 {
                    // Transfer complete
                    self.oam_state = OamDmaState::Idle;
                } else {
                    self.oam_state = OamDmaState::Reading {
                        page,
                        offset: next_offset,
                    };
                }
                DmaAction::OamWrite(value)
            }
        }
    }

    /// Update OAM DMA state with the value read from memory
    pub fn set_oam_read_value(&mut self, value: u8) {
        if let OamDmaState::Writing { page, offset, .. } = self.oam_state {
            self.oam_state = OamDmaState::Writing {
                page,
                offset,
                value,
            };
        }
    }

    /// Get the cycles consumed and reset the counter
    pub fn take_cycles_consumed(&mut self) -> u16 {
        let cycles = self.cycles_consumed;
        self.cycles_consumed = 0;
        cycles
    }

    /// Reset all DMA state
    pub fn reset(&mut self) {
        self.oam_state = OamDmaState::Idle;
        self.dmc_state = DmcDmaState::Idle;
        self.cycles_consumed = 0;
        self.oam_needs_align = false;
        self.is_get_cycle = true;
    }
}

/// Actions that can be returned by the DMA controller
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DmaAction {
    /// No DMA action this cycle
    None,
    /// Dummy read from address (discard value)
    DummyRead(u16),
    /// Read for OAM DMA from the given address
    OamRead(u16),
    /// Write to $2004 (OAM data) with the given value
    OamWrite(u8),
    /// DMC sample byte read (address comes from APU)
    DmcRead,
}

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

    #[test]
    fn test_new_controller_is_idle() {
        let dma = DmaController::new();
        assert!(!dma.is_active());
        assert_eq!(dma.oam_state, OamDmaState::Idle);
        assert_eq!(dma.dmc_state, DmcDmaState::Idle);
    }

    #[test]
    fn test_oam_dma_start_on_even_cycle() {
        let mut dma = DmaController::new();
        dma.start_oam_dma(0x02, false); // Not odd cycle

        assert!(dma.is_active());
        assert!(dma.oam_dma_active());
        assert!(!dma.oam_needs_align);
    }

    #[test]
    fn test_oam_dma_start_on_odd_cycle() {
        let mut dma = DmaController::new();
        dma.start_oam_dma(0x02, true); // Odd cycle

        assert!(dma.is_active());
        assert!(dma.oam_dma_active());
        assert!(dma.oam_needs_align);
    }

    #[test]
    fn test_dmc_dma_start() {
        let mut dma = DmaController::new();
        dma.start_dmc_dma(0x8000);

        assert!(dma.is_active());
        assert!(dma.dmc_dma_active());
        assert!(dma.should_start_dmc_dma());
    }

    #[test]
    fn test_dmc_has_priority_over_oam() {
        let mut dma = DmaController::new();

        // Start OAM DMA first
        dma.start_oam_dma(0x02, false);

        // Then DMC DMA request comes in
        dma.start_dmc_dma(0x8000);

        // Step should process DMC first
        let action = dma.step(0);
        assert!(
            matches!(action, DmaAction::DummyRead(_)),
            "DMC should take priority"
        );
        assert!(dma.dmc_dma_active());
    }

    #[test]
    fn test_set_oam_read_value_updates_writing_state() {
        let mut dma = DmaController::new();

        dma.start_oam_dma(0x02, false);
        let action = dma.step(0);
        assert!(matches!(action, DmaAction::DummyRead(_)));

        let action = dma.step(1);
        assert!(matches!(action, DmaAction::OamRead(0x0200)));

        dma.set_oam_read_value(0xAB);
        match dma.oam_state {
            OamDmaState::Writing { value, .. } => assert_eq!(value, 0xAB),
            _ => panic!("expected OAM DMA to be in Writing state"),
        }
    }

    #[test]
    fn test_take_cycles_consumed_returns_and_resets_counter() {
        let mut dma = DmaController::new();

        dma.start_oam_dma(0x02, false);
        let _ = dma.step(0);
        let _ = dma.step(1);

        let cycles = dma.take_cycles_consumed();
        assert!(cycles >= 2);
        assert_eq!(dma.take_cycles_consumed(), 0);
    }

    #[test]
    fn test_reset_clears_state_and_counters() {
        let mut dma = DmaController::new();
        dma.start_oam_dma(0x02, true);
        dma.start_dmc_dma(0x8000);
        let _ = dma.step(0);

        dma.reset();
        assert_eq!(dma.oam_state, OamDmaState::Idle);
        assert_eq!(dma.dmc_state, DmcDmaState::Idle);
        assert_eq!(dma.cycles_consumed, 0);
        assert!(!dma.is_active());
    }
}