neser 1.1.0

NESER - Nintendo Emulation Systems Engine (Rust). Desktop and WebAssembly frontends.
Documentation
//! Mapper 290 – BMC-NTD-03 (Asder 20-in-1 multicart)
//!
//! Specifications:
//! - Primary source: NesDev wiki
//!   <https://www.nesdev.org/wiki/NES_2.0_Mapper_290>
//!
//! # Hardware overview
//!
//! Used by the Asder 20-in-1 multicart. All banking is controlled by a single
//! address latch: any write to `$8000–$FFFF` latches the write address (data is
//! ignored), and all bank/mirroring configuration is encoded in that address.
//!
//! # Address latch (`$8000`, write — address bits only)
//!
//! ```text
//! A~[1PPP PMCC Sp.. .CCC]
//!     ||| |||| ||    |||
//!     ||| ||++-------+++- Select 8 KiB CHR-ROM bank at PPU $0000–$1FFF
//!     ||| ||   |+-------- Select 16 KiB PRG-ROM bank at CPU $8000/$C000 (when S=1)
//!     ||| ||   +--------- PRG-ROM bank size: 0=32 KiB, 1=16 KiB
//!     ||| |+------------- Nametable mirroring: 0=Vertical, 1=Horizontal
//!     +++-+-------------- Select 32 KiB PRG-ROM bank at CPU $8000
//! ```
//!
//! # PRG banking
//!
//! The 4-bit outer bank field spans address bits 14:11 (`1PPP P` minus the leading 1).
//! Combined bit layout:
//! - bits 14:12 (`PPP`) and bit 11 (`P`) form a 4-bit outer PRG selector.
//! - bit 10 (`C`) = mirroring.
//! - bit 9 (`C`) = nothing (part of chip select?), not directly used.
//! - bit 8 (`S`) = PRG bank size.
//! - bit 7 (`p`) = 16 KiB inner bank bit (when S=1).
//! - bits 2:0 (`CCC`) = CHR bank bits 2:0.
//! - bits 9:8 (`CC` before `Sp`) also contribute CHR bits 4:3.
//!
//! **32 KiB mode** (`S`=0, addr bit 8 clear):
//! - The outer 4-bit field selects a 32 KiB PRG block.
//! - `$8000–$BFFF` → 16 KiB page `outer × 2`
//! - `$C000$FFFF` → 16 KiB page `outer × 2 + 1`
//!
//! **16 KiB mode** (`S`=1, addr bit 8 set):
//! - The outer field selects a 16 KiB base; bit 7 selects inner bank.
//! - `$8000–$BFFF` → page `outer × 2 | inner`
//! - `$C000$FFFF` → page `outer × 2 | inner`  (mirrored)
//!
//! # CHR banking
//!
//! 8 KiB CHR-ROM bank selected by:
//! - bits 9:8 of address → CHR bits 4:3
//! - bits 2:0 of address → CHR bits 2:0
//!
//! # Mirroring
//!
//! Bit 10 of the write address: `0` = Vertical, `1` = Horizontal.
//!
//! # Power-on / reset state
//!
//! Equivalent to writing address `$8000`: all banks 0, CHR 0, vertical mirroring.

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

const MAPPER_NUMBER: u16 = 290;
const PRG_BANK_SIZE: usize = 16 * 1024;
const CHR_BANK_SIZE: usize = 8 * 1024;

/// Mapper 290 – BMC-NTD-03 multicart.
///
/// All banking is determined by the write **address** (not data) written to `$8000`.
pub struct Mapper290 {
    base: BaseMapper,
    /// The last write address latched.
    latch: u16,
}

impl Mapper290 {
    pub fn new(ctx: MapperContext) -> Self {
        let capabilities = MapperCapabilities {
            has_chr_banking: true,
            has_dynamic_mirroring: true,
            prg_bank_size_kb: PRG_BANK_SIZE / 1024,
            chr_bank_size_kb: CHR_BANK_SIZE / 1024,
            max_prg_ram_kb: 0,
            ..Default::default()
        };
        let mut base = BaseMapper::new(&ctx, capabilities);
        base.configure_prg_banking(PRG_BANK_SIZE);
        base.configure_chr_banking(CHR_BANK_SIZE);

        let mut mapper = Self {
            base,
            latch: 0x8000,
        };
        mapper.apply_latch();
        mapper
    }

    fn apply_latch(&mut self) {
        let addr = self.latch;
        // Outer PRG bank: bits 14:11 → 4-bit selector for 32 KiB outer block
        let outer = ((addr >> 11) & 0x0F) as i16;
        // PRG bank size mode: bit 8 (S)
        let size_16k = addr & 0x0100 != 0;
        // Inner 16 KiB bank bit: bit 7 (p)
        let inner = ((addr >> 7) & 0x01) as i16;
        // Mirroring: bit 10 (C)
        let horiz = addr & 0x0400 != 0;
        // CHR bank: bits 9:8 → CHR bits 4:3; bits 2:0 → CHR bits 2:0
        let chr = ((((addr >> 8) & 0x03) << 3) | (addr & 0x07)) as i16;

        if size_16k {
            // 16 KiB mode: both windows map to same inner bank
            let page = (outer << 1) | inner;
            self.base.select_prg_page(0, page);
            self.base.select_prg_page(1, page);
        } else {
            // 32 KiB mode: consecutive pair
            self.base.select_prg_page(0, outer << 1);
            self.base.select_prg_page(1, (outer << 1) + 1);
        }

        self.base.select_chr_page(0, chr);
        self.base.set_mirroring_hv(horiz);
    }
}

impl Mapper for Mapper290 {
    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 {
            self.latch = addr;
            self.apply_latch();
        }
    }

    fn reset(&mut self) {
        self.latch = 0x8000;
        self.apply_latch();
    }

    fn registers_snapshot(&self) -> Vec<u8> {
        self.latch.to_le_bytes().to_vec()
    }

    fn restore_registers(&mut self, data: &[u8]) {
        if data.len() < 2 {
            return;
        }
        self.latch = u16::from_le_bytes([data[0], data[1]]);
        self.apply_latch();
    }
}

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

    // 32 × 16 KiB = 512 KiB PRG (covers all outer bank combinations)
    const PRG_BANKS: usize = 32;
    // 32 × 8 KiB = 256 KiB CHR
    const CHR_BANKS: usize = 32;

    fn make_mapper() -> Mapper290 {
        Mapper290::new(MapperContext::new_for_test(
            MAPPER_NUMBER,
            banked_data(PRG_BANK_SIZE, PRG_BANKS),
            banked_data(CHR_BANK_SIZE, CHR_BANKS),
            NametableLayout::Vertical,
        ))
    }

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

    #[test]
    fn mapper_290_is_registered_in_factory() {
        let result = create_mapper(MapperContext::new_for_test(
            MAPPER_NUMBER,
            banked_data(PRG_BANK_SIZE, PRG_BANKS),
            banked_data(CHR_BANK_SIZE, CHR_BANKS),
            NametableLayout::Vertical,
        ));
        assert!(
            result.is_ok(),
            "Mapper 290 must be registered in the factory"
        );
    }

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

    #[test]
    fn power_on_both_prg_windows_map_to_bank_0_in_32k_mode() {
        let mapper = make_mapper();
        // latch = $8000: outer=0, S=0 (32KiB mode) → pages 0, 1
        assert_eq!(mapper.read_prg(0x8000), 0, "$8000 → bank 0");
        assert_eq!(mapper.read_prg(0xC000), 1, "$C000 → bank 1");
    }

    #[test]
    fn power_on_chr_is_bank_0() {
        let mut mapper = make_mapper();
        assert_eq!(mapper.read_chr(0x0000), 0);
    }

    #[test]
    fn power_on_mirroring_is_vertical() {
        let mapper = make_mapper();
        assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);
    }

    // ── 32 KiB mode ───────────────────────────────────────────────────────────

    #[test]
    fn nrom256_outer_bank_selects_prg_pair() {
        let mut mapper = make_mapper();
        // bits 14:11 = 0001 → outer=1 → pages 2,3; addr = $8000 | (1<<11) = $8800
        mapper.write_prg(0x8800, 0x00);
        assert_eq!(mapper.read_prg(0x8000), 2, "outer=1 → bank 2");
        assert_eq!(mapper.read_prg(0xC000), 3, "outer=1 → bank 3");
    }

    #[test]
    fn nrom256_outer_bank_3_maps_pages_6_7() {
        let mut mapper = make_mapper();
        // bits 14:11 = 0011 → outer=3; addr = $8000 | (3<<11) = $9800
        mapper.write_prg(0x9800, 0x00);
        assert_eq!(mapper.read_prg(0x8000), 6, "outer=3 → bank 6");
        assert_eq!(mapper.read_prg(0xC000), 7, "outer=3 → bank 7");
    }

    // ── 16 KiB mode ───────────────────────────────────────────────────────────

    #[test]
    fn nrom128_mode_mirrors_both_windows() {
        let mut mapper = make_mapper();
        // S=1 (bit 8) → 16 KiB mode, inner=0 → both windows at page 0
        mapper.write_prg(0x8100, 0x00);
        assert_eq!(mapper.read_prg(0x8000), 0, "16 KiB mode, inner=0 → bank 0");
        assert_eq!(mapper.read_prg(0xC000), 0, "upper mirrors lower");
    }

    #[test]
    fn nrom128_inner_bit_selects_bank_within_pair() {
        let mut mapper = make_mapper();
        // outer=1 (bit 11), S=1 (bit 8), inner=1 (bit 7)
        // addr = $8000 | (1<<11) | 0x0100 | 0x0080 = $8980
        // outer=1 → base=2; inner=1 → page = 2 | 1 = 3
        mapper.write_prg(0x8980, 0x00);
        assert_eq!(mapper.read_prg(0x8000), 3, "outer=1, inner=1 → bank 3");
        assert_eq!(mapper.read_prg(0xC000), 3, "both windows map to bank 3");
    }

    // ── CHR banking ───────────────────────────────────────────────────────────

    #[test]
    fn chr_bank_from_addr_bits_2_0() {
        let mut mapper = make_mapper();
        // CHR bank uses address bits 9:8 as the high bits (→ CHR bits 4:3) and
        // bits 2:0 as the low bits. For addr = $8005: bits 9:8 = 0, bits 2:0 = 5,
        // so CHR bank = (0 << 3) | 5 = 5.
        mapper.write_prg(0x8005, 0x00);
        assert_eq!(mapper.read_chr(0x0000), 5, "CHR bank 5");
    }

    #[test]
    fn chr_bank_from_addr_bits_9_8() {
        let mut mapper = make_mapper();
        // CHR bank uses address bits 9:8 as the high bits and bits 2:0 as the low bits.
        // For addr = $8200: bits 9:8 = 2 (bit 9 set, bit 8 clear), bits 2:0 = 0,
        // so CHR bank = (2 << 3) | 0 = 16.
        mapper.write_prg(0x8200, 0x00);
        assert_eq!(mapper.read_chr(0x0000), 16, "CHR bank from addr bits 9:8");
    }

    // ── Mirroring ─────────────────────────────────────────────────────────────

    #[test]
    fn addr_bit_10_selects_horizontal_mirroring() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8400, 0x00); // bit 10 set
        assert_eq!(mapper.get_mirroring(), NametableLayout::Horizontal);
    }

    #[test]
    fn addr_bit_10_clear_gives_vertical_mirroring() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8400, 0x00); // set Horizontal
        mapper.write_prg(0x8000, 0x00); // clear bit 10 → back to Vertical
        assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);
    }

    // ── Reset ─────────────────────────────────────────────────────────────────

    #[test]
    fn reset_restores_power_on_state() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8800, 0x00); // outer=1 → banks 2,3
        mapper.reset();
        assert_eq!(mapper.read_prg(0x8000), 0, "PRG bank 0 after reset");
        assert_eq!(
            mapper.get_mirroring(),
            NametableLayout::Vertical,
            "V mirroring after reset"
        );
    }

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

    #[test]
    fn snapshot_restore_roundtrip() {
        let mut mapper = make_mapper();
        // outer=2 (bits 14:11 = 0010 → addr bit 12), CHR bits 2:0 = 5
        // addr = $8000 | (2<<11) | 0x0005 = $9005
        mapper.write_prg(0x9005, 0x00);
        let snap = mapper.registers_snapshot();

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

        assert_eq!(restored.read_prg(0x8000), mapper.read_prg(0x8000));
        assert_eq!(restored.read_prg(0xC000), mapper.read_prg(0xC000));
        assert_eq!(restored.get_mirroring(), mapper.get_mirroring());
    }

    #[test]
    fn restore_with_short_data_is_noop() {
        let mut mapper = make_mapper();
        mapper.restore_registers(&[0x80]); // only 1 byte, need 2
        assert_eq!(mapper.read_prg(0x8000), 0, "state unchanged");
    }

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

    #[test]
    fn capabilities_match_specification() {
        let mapper = make_mapper();
        let caps = mapper.capabilities();
        assert!(caps.has_chr_banking, "CHR banking");
        assert!(caps.has_dynamic_mirroring, "dynamic mirroring");
        assert!(!caps.has_irq, "no IRQ");
        assert_eq!(caps.prg_bank_size_kb, 16);
        assert_eq!(caps.chr_bank_size_kb, 8);
        assert_eq!(caps.max_prg_ram_kb, 0);
    }
}