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 320 – BMC-830425C-4391T
//!
//! Specifications:
//! - Primary reference: Mesen2 `Bmc830425C4391T.h`
//! - NesDev: <https://www.nesdev.org/wiki/NES_2.0_Mapper_320>
//!
//! ## Overview
//!
//! Multicart board (e.g., "Super HiK 6-in-1"). Two 16 KB PRG windows with an
//! inner/outer banking scheme supporting UNROM and UOROM modes. CHR is 8 KB RAM.
//! Mirroring is fixed horizontal.
//!
//! ## Register Map ($8000–$FFFF)
//!
//! Any write to $8000–$FFFF:
//! - `inner_reg = value & 0x0F`
//! - If `(addr & 0xFFE0) == 0xF0E0`:
//!   - `outer_reg = addr & 0x0F`
//!   - `prg_mode  = (addr >> 4) & 0x01`
//!
//! ## PRG Banking
//!
//! | Mode   | $8000 bank                          | $C000 bank (fixed)             |
//! |--------|-------------------------------------|-------------------------------|
//! | UOROM (prg_mode=0) | `inner_reg \| (outer_reg << 3)` | `0x0F \| (outer_reg << 3)` |
//! | UNROM (prg_mode=1) | `(inner_reg & 0x07) \| (outer_reg << 3)` | `0x07 \| (outer_reg << 3)` |
//!
//! ## Known Limitations
//!
//! No known gameplay-blocking limitations.

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

const MAPPER_NUMBER: u16 = 320;
const PRG_BANK_SIZE_BYTES: usize = 16 * 1024;
const CHR_BANK_SIZE_BYTES: usize = 8 * 1024;
const OUTER_REG_ADDR_MASK: u16 = 0xFFE0;
const OUTER_REG_ADDR_MATCH: u16 = 0xF0E0;
const REGISTERS_SNAPSHOT_LEN: usize = 3;

pub struct Mapper320 {
    base: BaseMapper,
    inner_reg: u8,
    outer_reg: u8,
    prg_mode: u8,
}

impl Mapper320 {
    pub fn new(ctx: super::mapper::MapperContext) -> Self {
        let capabilities = MapperCapabilities {
            has_dynamic_mirroring: false,
            prg_bank_size_kb: 16,
            chr_bank_size_kb: 8,
            max_prg_ram_kb: 0,
            ..Default::default()
        };

        let mut base = BaseMapper::new(&ctx, capabilities);
        base.configure_prg_banking(PRG_BANK_SIZE_BYTES);
        base.configure_chr_banking(CHR_BANK_SIZE_BYTES);

        let chr_ram = ChrMemory::new_ram(CHR_BANK_SIZE_BYTES);
        base.set_chr_memory(chr_ram);

        let mut mapper = Self {
            base,
            inner_reg: 0,
            outer_reg: 0,
            prg_mode: 0,
        };
        mapper.update_banks();
        mapper
    }

    fn update_banks(&mut self) {
        let outer_shift = (self.outer_reg as i16) << 3;
        let (bank_8000, bank_c000) = if self.prg_mode != 0 {
            // UNROM mode: inner uses 3 bits, fixed is 0x07
            let inner = (self.inner_reg & 0x07) as i16;
            (inner | outer_shift, 0x07 | outer_shift)
        } else {
            // UOROM mode: inner uses 4 bits, fixed is 0x0F
            let inner = (self.inner_reg & 0x0F) as i16;
            (inner | outer_shift, 0x0F | outer_shift)
        };

        self.base.select_prg_page(0, bank_8000);
        self.base.select_prg_page(1, bank_c000);
        self.base.select_chr_page(0, 0);
        self.base.set_mirroring(NametableLayout::Horizontal);
    }

    fn apply_state(&mut self, inner_reg: u8, outer_reg: u8, prg_mode: u8) {
        self.inner_reg = inner_reg;
        self.outer_reg = outer_reg;
        self.prg_mode = prg_mode;
        self.update_banks();
    }
}

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

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

    fn mapper_number(&self) -> u16 {
        MAPPER_NUMBER
    }

    fn write_prg(&mut self, addr: u16, value: u8) {
        if addr < 0x8000 {
            return;
        }

        let new_inner = value & 0x0F;
        let (new_outer, new_mode) = if (addr & OUTER_REG_ADDR_MASK) == OUTER_REG_ADDR_MATCH {
            ((addr & 0x0F) as u8, ((addr >> 4) & 0x01) as u8)
        } else {
            (self.outer_reg, self.prg_mode)
        };

        self.apply_state(new_inner, new_outer, new_mode);
    }

    fn registers_snapshot(&self) -> Vec<u8> {
        vec![self.inner_reg, self.outer_reg, self.prg_mode]
    }

    fn restore_registers(&mut self, data: &[u8]) {
        if data.len() < REGISTERS_SNAPSHOT_LEN {
            return;
        }
        self.apply_state(data[0] & 0x0F, data[1] & 0x0F, data[2] & 0x01);
    }

    fn reset(&mut self) {
        self.apply_state(0, 0, 0);
    }
}

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

    // Use 48 banks (non-power-of-2) to avoid modulo-wrap false passes
    const PRG_BANKS_16K: usize = 48;

    fn make_mapper() -> Mapper320 {
        Mapper320::new(MapperContext::new_for_test(
            MAPPER_NUMBER,
            banked_data(PRG_BANK_SIZE_BYTES, PRG_BANKS_16K),
            vec![],
            NametableLayout::Horizontal,
        ))
    }

    // ── Factory registration ──────────────────────────────────────────────────

    #[test]
    fn mapper_320_is_registered_in_factory() {
        let result = create_mapper(MapperContext::new_for_test(
            MAPPER_NUMBER,
            banked_data(PRG_BANK_SIZE_BYTES, PRG_BANKS_16K),
            vec![],
            NametableLayout::Horizontal,
        ));
        assert!(result.is_ok(), "Mapper 320 must be registered in factory");
    }

    // ── Power-on / reset state ────────────────────────────────────────────────

    #[test]
    fn power_on_selects_uorom_mode_with_inner_0_and_outer_0() {
        let mapper = make_mapper();
        // UOROM mode: bank0 = 0 | (0 << 3) = 0, bank1 = 0x0F | 0 = 15
        assert_eq!(
            mapper.read_prg(0x8000),
            0,
            "$8000 should be bank 0 at power-on"
        );
        assert_eq!(
            mapper.read_prg(0xC000),
            15,
            "$C000 should be bank 15 at power-on"
        );
    }

    #[test]
    fn mirroring_is_fixed_horizontal() {
        let mapper = make_mapper();
        assert_eq!(
            mapper.get_mirroring(),
            NametableLayout::Horizontal,
            "mirroring should always be horizontal"
        );
    }

    #[test]
    fn mirroring_is_forced_horizontal_even_when_header_says_vertical() {
        let mapper = Mapper320::new(MapperContext::new_for_test(
            MAPPER_NUMBER,
            banked_data(PRG_BANK_SIZE_BYTES, PRG_BANKS_16K),
            vec![],
            NametableLayout::Vertical,
        ));
        assert_eq!(
            mapper.get_mirroring(),
            NametableLayout::Horizontal,
            "mapper 320 always enforces horizontal mirroring regardless of ROM header"
        );
    }

    // ── UOROM mode PRG banking (prg_mode = 0) ─────────────────────────────────

    #[test]
    fn uorom_mode_inner_reg_selects_8000_bank_and_0f_is_fixed_at_c000() {
        let mut mapper = make_mapper();
        // Write inner=5 to $8000 (not outer-reg addr), outer=0, mode=0
        mapper.write_prg(0x8000, 0x05);
        // bank_8000 = 5 | (0 << 3) = 5
        // bank_c000 = 0x0F | 0 = 15
        assert_eq!(mapper.read_prg(0x8000), 5, "$8000 should be bank 5");
        assert_eq!(mapper.read_prg(0xC000), 15, "$C000 should be fixed bank 15");
    }

    #[test]
    fn uorom_mode_with_outer_2_shifts_all_banks() {
        let mut mapper = make_mapper();
        // Set outer=2, mode=0 via outer-reg addr (0xF0E0 | (2 & 0x0F) = 0xF0E2, mode=(0xF0E2>>4)&1=0xE&1=0)
        // Wait: (0xF0E0 >> 4) = 0xF0E, &1 = 0. And outer = 0xF0E0 & 0x0F = 0.
        // Let me pick addr 0xF0E2: (0xF0E2 & 0xFFE0) = 0xF0E0 == 0xF0E0 ✓
        //   outer = 0xF0E2 & 0x0F = 2, mode = (0xF0E2 >> 4) & 1 = 0x0F0E & 1 = 0
        // inner = value & 0x0F; write value=3, inner=3
        mapper.write_prg(0xF0E2, 0x03);
        // bank_8000 = 3 | (2 << 3) = 3 | 16 = 19
        // bank_c000 = 0x0F | 16 = 31
        assert_eq!(mapper.read_prg(0x8000), 19, "$8000 should be bank 19");
        assert_eq!(mapper.read_prg(0xC000), 31, "$C000 should be bank 31");
    }

    // ── UNROM mode PRG banking (prg_mode = 1) ─────────────────────────────────

    #[test]
    fn unrom_mode_inner_uses_only_3_bits_and_07_is_fixed_at_c000() {
        let mut mapper = make_mapper();
        // addr 0xF0F2: (0xF0F2 & 0xFFE0) = 0xF0E0 ✓, outer = 2, mode = (0xF0F2>>4)&1 = 0x0F0F & 1 = 1
        // inner = value & 0x0F = 0x0B & 0x0F = 11, but 3-bit = 11 & 7 = 3
        mapper.write_prg(0xF0F2, 0x0B);
        // bank_8000 = (11 & 7) | (2 << 3) = 3 | 16 = 19
        // bank_c000 = 7 | 16 = 23
        assert_eq!(
            mapper.read_prg(0x8000),
            19,
            "$8000 should be bank 19 in UNROM mode"
        );
        assert_eq!(
            mapper.read_prg(0xC000),
            23,
            "$C000 should be fixed bank 23 in UNROM mode"
        );
    }

    #[test]
    fn unrom_mode_distinguishes_from_uorom_mode_fixed_banks() {
        let mut mapper = make_mapper();
        // In UNROM mode (prg_mode=1) with outer=0, fixed bank at $C000 = 7
        // In UOROM mode (prg_mode=0) with outer=0, fixed bank at $C000 = 15
        // Set UNROM mode: addr = 0xF0F0 → outer=0, mode=1
        mapper.write_prg(0xF0F0, 0x00);
        assert_eq!(
            mapper.read_prg(0xC000),
            7,
            "UNROM fixed bank should be 7 with outer=0"
        );

        // Now set UOROM mode: addr = 0xF0E0 → outer=0, mode=0
        mapper.write_prg(0xF0E0, 0x00);
        assert_eq!(
            mapper.read_prg(0xC000),
            15,
            "UOROM fixed bank should be 15 with outer=0"
        );
    }

    // ── Outer reg address decode ───────────────────────────────────────────────

    #[test]
    fn writes_outside_outer_reg_range_do_not_change_outer_or_mode() {
        let mut mapper = make_mapper();
        // First set outer=3, mode=1 via $F0F3
        // addr 0xF0F3: (0xF0F3 & 0xFFE0) = 0xF0E0 ✓, outer = 3, mode = 1
        mapper.write_prg(0xF0F3, 0x00);
        // bank_c000 = 7 | (3 << 3) = 7 | 24 = 31
        assert_eq!(mapper.read_prg(0xC000), 31);

        // Now write to non-outer addr (e.g. $8000): only inner changes
        mapper.write_prg(0x8000, 0x04);
        // outer and mode unchanged: bank_c000 = 7 | 24 = 31
        assert_eq!(
            mapper.read_prg(0xC000),
            31,
            "outer/mode should not change from non-outer write"
        );
        // inner=4, mode=1: bank_8000 = (4 & 7) | 24 = 28
        assert_eq!(
            mapper.read_prg(0x8000),
            28,
            "$8000 should use new inner with existing outer"
        );
    }

    #[test]
    fn writes_below_8000_are_ignored() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x7FFF, 0x05);
        assert_eq!(
            mapper.read_prg(0x8000),
            0,
            "write below $8000 should be ignored"
        );
    }

    // ── CHR RAM ───────────────────────────────────────────────────────────────

    #[test]
    fn chr_ram_is_unbanked_and_writable() {
        let mut mapper = make_mapper();
        mapper.write_chr(0x0100, 0x7E);
        assert_eq!(mapper.read_chr(0x0100), 0x7E, "CHR RAM should be writable");
    }

    // ── Snapshot / restore ────────────────────────────────────────────────────

    #[test]
    fn snapshot_restore_preserves_inner_outer_and_mode() {
        let mut mapper = make_mapper();
        // Set outer=3, mode=1, inner=5 via write: $F0F3, value=5
        mapper.write_prg(0xF0F3, 0x05);

        let snap = mapper.registers_snapshot();
        let mut restored = make_mapper();
        restored.restore_registers(&snap);

        // UNROM mode (mode=1), outer=3, inner=5&7=5
        // bank_8000 = 5 | (3 << 3) = 5 | 24 = 29
        // bank_c000 = 7 | 24 = 31
        assert_eq!(
            restored.read_prg(0x8000),
            29,
            "snapshot should restore bank_8000=29"
        );
        assert_eq!(
            restored.read_prg(0xC000),
            31,
            "snapshot should restore bank_c000=31"
        );
    }

    #[test]
    fn reset_returns_to_power_on_state() {
        let mut mapper = make_mapper();
        mapper.write_prg(0xF0F3, 0x05);
        mapper.reset();

        assert_eq!(
            mapper.read_prg(0x8000),
            0,
            "reset should return $8000 to bank 0"
        );
        assert_eq!(
            mapper.read_prg(0xC000),
            15,
            "reset should return $C000 to bank 15"
        );
    }

    // ── Capabilities ──────────────────────────────────────────────────────────

    #[test]
    fn capabilities_match_bmc830425c4391t_specification() {
        let mapper = make_mapper();
        let caps = mapper.capabilities();
        assert!(!caps.has_irq);
        assert!(!caps.has_expansion_audio);
        assert!(!caps.has_dynamic_mirroring);
        assert!(!caps.has_chr_banking);
        assert_eq!(caps.prg_bank_size_kb, 16);
        assert_eq!(caps.chr_bank_size_kb, 8);
        assert_eq!(caps.max_prg_ram_kb, 0);
    }
}