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
//! Mapper 329 – EDU2000 board
//!
//! Specifications:
//! - NesDev wiki: unavailable due to network restriction (Cloudflare 403).
//! - Fallback: Mesen2 `Edu2000.h` (primary reference used).
//!
//! # Hardware overview
//!
//! Used for EDU2000 educational cartridges.
//!
//! - PRG-ROM: 32 KiB pages; bank selected by bits 4:0 of the control register
//! - CHR-ROM: 8 KiB fixed (bank 0 always)
//! - PRG-RAM (Work RAM): 32 KiB total; 8 KiB window at $6000–$7FFF;
//!   page selected by bits 7:6 of the control register (4 pages of 8 KiB)
//! - Control register: any write to $8000–$FFFF latches the written byte
//! - Mirroring: fixed from header (not programmable)
//! - IRQ: none
//! - Bus conflicts: none
//!
//! # Register format (written to $8000–$FFFF)
//!
//! | Bits | Effect                                         |
//! |------|------------------------------------------------|
//! | 4:0  | PRG-ROM 32 KiB bank select                     |
//! | 7:6  | PRG-RAM 8 KiB page select (from 32 KiB pool)  |
//!
//! # Power-on state
//!
//! Register = 0: PRG-ROM bank 0 at $8000–$FFFF; PRG-RAM page 0 at $6000–$7FFF.

use crate::cartridge::base_mapper::BaseMapper;
use crate::cartridge::mapper::{Mapper, MapperCapabilities};

/// Total PRG-RAM size: 32 KiB (4 pages × 8 KiB).
const WRAM_SIZE: usize = 32 * 1024;
/// PRG-RAM page size: 8 KiB.
const WRAM_PAGE_SIZE: usize = 8 * 1024;

/// Mapper 329 – EDU2000 board
///
/// Control register layout:
/// - Bits 4:0 → PRG-ROM 32 KiB bank
/// - Bits 7:6 → PRG-RAM 8 KiB page (from 32 KiB pool)
pub struct Mapper329 {
    base: BaseMapper,
    reg: u8,
    wram: [u8; WRAM_SIZE],
}

impl Mapper329 {
    pub fn new(ctx: super::mapper::MapperContext) -> Self {
        let capabilities = MapperCapabilities {
            prg_bank_size_kb: 32,
            chr_bank_size_kb: 8,
            max_prg_ram_kb: 32,
            ..Default::default()
        };
        let mut base = BaseMapper::new(&ctx, capabilities);
        base.configure_prg_banking(32 * 1024);
        base.configure_chr_banking(8 * 1024);
        let mut mapper = Self {
            base,
            reg: 0,
            wram: [0; WRAM_SIZE],
        };
        mapper.update_banks();
        mapper
    }

    fn update_banks(&mut self) {
        let prg_bank = (self.reg & 0x1F) as i16;
        self.base.select_prg_page(0, prg_bank);
        self.base.select_chr_page(0, 0);
    }

    fn wram_page(&self) -> usize {
        ((self.reg >> 6) & 0x03) as usize
    }

    fn wram_offset(&self, addr: u16) -> usize {
        let page_base = self.wram_page() * WRAM_PAGE_SIZE;
        page_base + (addr - 0x6000) as usize
    }
}

impl Mapper for Mapper329 {
    fn base(&self) -> &BaseMapper {
        &self.base
    }

    fn base_mut(&mut self) -> &mut BaseMapper {
        &mut self.base
    }

    fn read_prg(&self, addr: u16) -> u8 {
        match addr {
            0x6000..=0x7FFF => self.wram[self.wram_offset(addr)],
            0x8000..=0xFFFF => self.base.read_prg_rom(addr),
            _ => 0,
        }
    }

    fn write_prg(&mut self, addr: u16, value: u8) {
        match addr {
            0x6000..=0x7FFF => {
                let offset = self.wram_offset(addr);
                self.wram[offset] = value;
            }
            0x8000..=0xFFFF => {
                self.reg = value;
                self.update_banks();
            }
            _ => {}
        }
    }

    fn wram_size(&self) -> usize {
        WRAM_SIZE
    }

    fn wram_snapshot(&self) -> Vec<u8> {
        self.wram.to_vec()
    }

    fn load_wram_snapshot(&mut self, data: &[u8]) {
        let len = data.len().min(WRAM_SIZE);
        self.wram[..len].copy_from_slice(&data[..len]);
    }

    fn registers_snapshot(&self) -> Vec<u8> {
        vec![self.reg]
    }

    fn restore_registers(&mut self, data: &[u8]) {
        if let Some(&r) = data.first() {
            self.reg = r;
            self.update_banks();
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cartridge::NametableLayout;
    use crate::cartridge::mapper::{MapperContext, create_mapper};
    use crate::cartridge::test_helpers::banked_data;

    // Non-power-of-two bank counts to prevent false-pass modulo wrapping.
    const PRG_BANKS: usize = 11; // 11 × 32 KB = 352 KB
    const CHR_BANKS: usize = 7; // 7 × 8 KB = 56 KB

    fn make_mapper() -> Mapper329 {
        let prg = banked_data(32 * 1024, PRG_BANKS);
        let chr = banked_data(8 * 1024, CHR_BANKS);
        Mapper329::new(MapperContext::new_for_test(
            329,
            prg,
            chr,
            NametableLayout::Horizontal,
        ))
    }

    // --- Registration ---

    #[test]
    fn mapper_329_is_registered() {
        let prg = banked_data(32 * 1024, PRG_BANKS);
        let chr = banked_data(8 * 1024, CHR_BANKS);
        let result = create_mapper(MapperContext::new_for_test(
            329,
            prg,
            chr,
            NametableLayout::Horizontal,
        ));
        assert!(
            result.is_ok(),
            "Mapper 329 must be registered in the factory"
        );
    }

    // --- PRG-ROM banking ---

    #[test]
    fn initial_prg_bank_is_zero() {
        let mapper = make_mapper();
        // Bank 0 is filled with 0x00 by banked_data
        assert_eq!(
            mapper.read_prg(0x8000),
            0x00,
            "initial PRG bank should be 0"
        );
    }

    #[test]
    fn prg_banking_selects_32kb_bank_via_low_5_bits() {
        let mut mapper = make_mapper();
        // Bank 3 is filled with 0x03
        mapper.write_prg(0x8000, 0x03);
        assert_eq!(
            mapper.read_prg(0x8000),
            0x03,
            "PRG bank 3 should be mapped at $8000"
        );
    }

    #[test]
    fn prg_bank_register_ignores_upper_bits() {
        let mut mapper = make_mapper();
        // bits 7:6 are WRAM page bits, bit 5 is unused; PRG bank = value & 0x1F
        // Write 0xC3: bits 7:6 = 0b11 (WRAM page 3), bits 4:0 = 0x03 (PRG bank 3)
        mapper.write_prg(0x8000, 0xC3);
        // PRG should be bank 3 (0x03)
        assert_eq!(
            mapper.read_prg(0x8000),
            0x03,
            "PRG bank should use only bits 4:0"
        );
    }

    #[test]
    fn prg_register_write_at_any_address_in_8000_ffff() {
        let mut mapper = make_mapper();
        mapper.write_prg(0xFFFF, 0x05);
        assert_eq!(
            mapper.read_prg(0x8000),
            0x05,
            "write to $FFFF should set PRG bank 5"
        );
    }

    // --- CHR banking ---

    #[test]
    fn chr_is_fixed_at_bank_0_initially() {
        let mapper = make_mapper();
        // CHR bank 0 is filled with 0x00
        assert_eq!(
            mapper.base.read_chr(0x0000),
            0x00,
            "CHR should be at bank 0"
        );
    }

    #[test]
    fn chr_remains_at_bank_0_after_register_write() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 0x06);
        // CHR is always fixed at bank 0
        assert_eq!(
            mapper.base.read_chr(0x0000),
            0x00,
            "CHR must remain at bank 0"
        );
    }

    // --- PRG-RAM (Work RAM) banking ---

    #[test]
    fn initial_wram_page_is_zero() {
        let mut mapper = make_mapper();
        // Write a marker to page 0 addr $6000
        mapper.write_prg(0x6000, 0xAB);
        assert_eq!(
            mapper.read_prg(0x6000),
            0xAB,
            "initial WRAM page 0 should be accessible at $6000"
        );
    }

    #[test]
    fn wram_page_selected_by_bits_7_6() {
        let mut mapper = make_mapper();
        // Write distinct values to pages 0..3 via register switching
        // Page 0 (bits 7:6 = 00)
        mapper.write_prg(0x8000, 0x00); // page 0
        mapper.write_prg(0x6000, 0xAA);
        // Page 1 (bits 7:6 = 01)
        mapper.write_prg(0x8000, 0x40); // bits 7:6=01, PRG bank 0
        mapper.write_prg(0x6000, 0xBB);
        // Page 2 (bits 7:6 = 10)
        mapper.write_prg(0x8000, 0x80); // bits 7:6=10, PRG bank 0
        mapper.write_prg(0x6000, 0xCC);
        // Page 3 (bits 7:6 = 11)
        mapper.write_prg(0x8000, 0xC0); // bits 7:6=11, PRG bank 0
        mapper.write_prg(0x6000, 0xDD);

        // Verify each page holds its own value
        mapper.write_prg(0x8000, 0x00); // back to page 0
        assert_eq!(mapper.read_prg(0x6000), 0xAA, "page 0 should hold 0xAA");
        mapper.write_prg(0x8000, 0x40);
        assert_eq!(mapper.read_prg(0x6000), 0xBB, "page 1 should hold 0xBB");
        mapper.write_prg(0x8000, 0x80);
        assert_eq!(mapper.read_prg(0x6000), 0xCC, "page 2 should hold 0xCC");
        mapper.write_prg(0x8000, 0xC0);
        assert_eq!(mapper.read_prg(0x6000), 0xDD, "page 3 should hold 0xDD");
    }

    #[test]
    fn wram_pages_are_independent_8kb_windows() {
        let mut mapper = make_mapper();
        // Write the full 8 KB window in page 0
        mapper.write_prg(0x8000, 0x00);
        mapper.write_prg(0x6000, 0x11);
        mapper.write_prg(0x7FFF, 0x22);

        // Switch to page 1 – values in page 0 should not be visible
        mapper.write_prg(0x8000, 0x40);
        assert_ne!(
            mapper.read_prg(0x6000),
            0x11,
            "page 1 $6000 should differ from page 0"
        );
        assert_ne!(
            mapper.read_prg(0x7FFF),
            0x22,
            "page 1 $7FFF should differ from page 0"
        );

        // Switch back to page 0 and verify values are preserved
        mapper.write_prg(0x8000, 0x00);
        assert_eq!(
            mapper.read_prg(0x6000),
            0x11,
            "page 0 $6000 should be preserved"
        );
        assert_eq!(
            mapper.read_prg(0x7FFF),
            0x22,
            "page 0 $7FFF should be preserved"
        );
    }

    // --- WRAM persistence / save-state ---

    #[test]
    fn wram_size_is_32kb() {
        let mapper = make_mapper();
        assert_eq!(mapper.wram_size(), 32 * 1024, "WRAM must be 32 KiB");
    }

    #[test]
    fn wram_snapshot_and_restore_roundtrip() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 0x00);
        mapper.write_prg(0x6000, 0x42);
        mapper.write_prg(0x8000, 0x40);
        mapper.write_prg(0x6000, 0x99);

        let snapshot = mapper.wram_snapshot();
        let mut mapper2 = make_mapper();
        mapper2.load_wram_snapshot(&snapshot);

        mapper2.write_prg(0x8000, 0x00);
        assert_eq!(
            mapper2.read_prg(0x6000),
            0x42,
            "page 0 value should survive snapshot"
        );
        mapper2.write_prg(0x8000, 0x40);
        assert_eq!(
            mapper2.read_prg(0x6000),
            0x99,
            "page 1 value should survive snapshot"
        );
    }

    #[test]
    fn registers_snapshot_and_restore_roundtrip() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 0xC5); // PRG bank 5, WRAM page 3
        let snap = mapper.registers_snapshot();
        let mut mapper2 = make_mapper();
        mapper2.restore_registers(&snap);
        // After restore, PRG bank should be 5
        assert_eq!(
            mapper2.read_prg(0x8000),
            0x05,
            "PRG bank should be restored to 5"
        );
    }
}