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 348 - BMC-830118-C (MMC3 variant)
//!
//! Specifications:
//! - Mesen reference: `Bmc830118C`
//! - Mapper family: MMC3 variant
//!
//! Known Limitations:
//! - No known gameplay-blocking functional limitations are currently documented.

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

/// Mapper 348 - BMC-830118-C (MMC3 variant)
///
/// Additional register:
/// - `$6800-$68FF` write-only outer register `reg`
///   - `reg[3:2]` extends PRG/CHR bank selection.
///   - When `reg[3:2] == 0b11`, upper PRG windows are forced:
///     - `$C000-$DFFF`: `0x32 | (low_window0 & 0x0F)`
///     - `$E000-$FFFF`: `0x32 | (low_window1 & 0x0F)`
pub struct Mapper348 {
    mmc3: MMC3Mapper,
    outer_reg: u8,
}

impl Mapper348 {
    const PRG_BANK_SIZE: usize = 0x2000;
    const PRG_BANK_MASK: usize = Self::PRG_BANK_SIZE - 1;
    const CHR_1K_BANK_SIZE: usize = 0x0400;
    const CHR_BANK_MASK: usize = Self::CHR_1K_BANK_SIZE - 1;
    const SPECIAL_MODE_FIXED_PRG_BASE: usize = 0x32;

    pub fn new(ctx: super::mapper::MapperContext) -> Self {
        let prg_rom = ctx.prg_rom;
        let chr_rom = ctx.chr_rom;
        let mirroring = ctx.mirroring;
        Self {
            mmc3: MMC3Mapper::new_with_irq_mode(prg_rom, chr_rom, mirroring, false),
            outer_reg: 0,
        }
    }

    fn outer_bank_high_bits(&self) -> usize {
        (self.outer_reg & 0x0C) as usize
    }

    fn special_mode(&self) -> bool {
        self.outer_bank_high_bits() == 0x0C
    }

    fn apply_chr_outer_bits(&self, raw_1k_bank: usize) -> usize {
        (self.outer_bank_high_bits() << 5) | (raw_1k_bank & 0x7F)
    }

    fn apply_prg_outer_bits(&self, raw_8k_page: u8) -> usize {
        (self.outer_bank_high_bits() << 2) | (usize::from(raw_8k_page) & 0x0F)
    }

    fn forced_special_mode_prg_bank(&self, source_window_addr: u16) -> usize {
        let low_nibble = usize::from(self.mmc3.raw_prg_8k_page_number(source_window_addr)) & 0x0F;
        Self::SPECIAL_MODE_FIXED_PRG_BASE | low_nibble
    }

    fn mapped_chr_bank_and_offset(&self, addr: u16) -> (usize, usize) {
        let raw_bank = self.mmc3.raw_chr_1k_bank(addr);
        let bank = self.apply_chr_outer_bits(raw_bank);
        let offset = (addr as usize) & Self::CHR_BANK_MASK;
        (bank, offset)
    }

    fn mapped_prg_bank_for_addr(&self, addr: u16) -> usize {
        if self.special_mode() {
            match addr {
                0x8000..=0x9FFF => {
                    self.apply_prg_outer_bits(self.mmc3.raw_prg_8k_page_number(0x8000))
                }
                0xA000..=0xBFFF => {
                    self.apply_prg_outer_bits(self.mmc3.raw_prg_8k_page_number(0xA000))
                }
                0xC000..=0xDFFF => self.forced_special_mode_prg_bank(0x8000),
                0xE000..=0xFFFF => self.forced_special_mode_prg_bank(0xA000),
                _ => 0,
            }
        } else {
            let raw = self.mmc3.raw_prg_8k_page_number(addr);
            self.apply_prg_outer_bits(raw)
        }
    }
}

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

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

    fn mmc3_delegate(&self) -> Option<&MMC3Mapper> {
        Some(&self.mmc3)
    }

    fn mmc3_delegate_mut(&mut self) -> Option<&mut MMC3Mapper> {
        Some(&mut self.mmc3)
    }

    fn read_prg(&self, addr: u16) -> u8 {
        if !(0x8000..=0xFFFF).contains(&addr) {
            return 0;
        }

        let bank = self.mapped_prg_bank_for_addr(addr);
        let offset = (addr as usize) & Self::PRG_BANK_MASK;
        self.mmc3.read_prg_at_bank(bank, offset)
    }

    fn write_prg(&mut self, addr: u16, value: u8) {
        if (0x6800..=0x68FF).contains(&addr) {
            self.outer_reg = value;
        } else if (0x8000..=0xFFFF).contains(&addr) {
            self.mmc3.write_prg(addr, value);
        }
    }

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

    fn read_chr(&mut self, addr: u16) -> u8 {
        let (bank, offset) = self.mapped_chr_bank_and_offset(addr);
        self.mmc3.read_chr_1k_at(bank, offset)
    }

    fn write_chr(&mut self, addr: u16, value: u8) {
        let (bank, offset) = self.mapped_chr_bank_and_offset(addr);
        self.mmc3.write_chr_1k_at(bank, offset, value);
    }

    fn registers_snapshot(&self) -> Vec<u8> {
        let mut snap = self.mmc3.registers_snapshot();
        snap.push(self.outer_reg);
        snap
    }

    fn restore_registers(&mut self, data: &[u8]) {
        if let Some((&outer, mmc3_data)) = data.split_last() {
            self.outer_reg = outer;
            self.mmc3.restore_registers(mmc3_data);
        }
    }

    fn reset(&mut self) {
        self.outer_reg = 0;
        self.mmc3.reset();
    }

    fn capabilities(&self) -> MapperCapabilities {
        MapperCapabilities {
            has_irq: true,
            has_chr_banking: true,
            has_dynamic_mirroring: true,
            has_expansion_audio: false,
            max_prg_ram_kb: 0,
            prg_bank_size_kb: 8,
            chr_bank_size_kb: 1,
            ..Default::default()
        }
    }
}

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

    const PRG_BANKS_8K: usize = 64;
    const CHR_BANKS_1K: usize = 256;

    fn make_mapper() -> Box<dyn Mapper> {
        create_mapper(MapperContext::new_for_test(
            348,
            banked_data(8 * 1024, PRG_BANKS_8K),
            banked_data(1024, CHR_BANKS_1K),
            NametableLayout::Vertical,
        ))
        .expect("Mapper 348 should be implemented")
    }

    #[test]
    fn mapper_348_is_registered_in_factory() {
        let result = create_mapper(MapperContext::new_for_test(
            348,
            banked_data(8 * 1024, PRG_BANKS_8K),
            banked_data(1024, CHR_BANKS_1K),
            NametableLayout::Vertical,
        ));
        assert!(
            result.is_ok(),
            "Mapper 348 must be registered in the mapper factory"
        );
    }

    #[test]
    fn outer_register_bits_2_3_extend_chr_bank_selection() {
        let mut mapper = make_mapper();

        mapper.write_prg(0x8000, 0x00);
        mapper.write_prg(0x8001, 0x01);
        let chr_before = mapper.read_chr(0x0000);

        mapper.write_prg(0x6800, 0x04);
        let chr_after = mapper.read_chr(0x0000);

        assert_ne!(
            chr_before, chr_after,
            "Writing $6800 with bit2 set should change CHR bank high bits"
        );
    }

    #[test]
    fn special_mode_reg_0x0c_forces_upper_prg_windows_from_0x32_or_low_nibble() {
        let mut mapper = make_mapper();

        mapper.write_prg(0x6800, 0x0C);

        mapper.write_prg(0x8000, 0x06);
        mapper.write_prg(0x8001, 0x09);
        mapper.write_prg(0x8000, 0x07);
        mapper.write_prg(0x8001, 0x03);

        let bank_low_0 = mapper.read_prg(0x8000);
        let bank_low_1 = mapper.read_prg(0xA000);
        let bank_high_0 = mapper.read_prg(0xC000);
        let bank_high_1 = mapper.read_prg(0xE000);

        assert_eq!(bank_high_0, 0x32 | (bank_low_0 & 0x0F));
        assert_eq!(bank_high_1, 0x32 | (bank_low_1 & 0x0F));
    }

    #[test]
    fn mirroring_and_capabilities_follow_mmc3_expectations() {
        let mut mapper = make_mapper();

        mapper.write_prg(0xA000, 0x00);
        assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);

        mapper.write_prg(0xA000, 0x01);
        assert_eq!(mapper.get_mirroring(), NametableLayout::Horizontal);

        let caps = mapper.capabilities();
        assert!(caps.has_irq);
        assert!(caps.has_chr_banking);
        assert!(caps.has_dynamic_mirroring);
        assert!(!caps.has_expansion_audio);
    }

    #[test]
    fn writes_to_6000_region_except_6800_68ff_do_not_change_outer_register() {
        let mut mapper = make_mapper();

        mapper.write_prg(0x6800, 0x00);
        mapper.write_prg(0x8000, 0x00);
        mapper.write_prg(0x8001, 0x01);
        let baseline = mapper.read_chr(0x0000);

        mapper.write_prg(0x6000, 0x04);
        mapper.write_prg(0x67FF, 0x04);
        mapper.write_prg(0x6900, 0x04);
        mapper.write_prg(0x7FFF, 0x04);

        assert_eq!(mapper.read_chr(0x0000), baseline);

        mapper.write_prg(0x6800, 0x04);
        assert_ne!(mapper.read_chr(0x0000), baseline);
    }
}