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 118 - TxSROM (MMC3 variant with CHR-bit7 CIRAM selection)
//!
//! Specifications:
//! - Main: <https://www.nesdev.org/wiki/INES_Mapper_118>
//! - Base behavior: <https://www.nesdev.org/wiki/MMC3>
//!
//! 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};

pub struct Mapper118 {
    mmc3: MMC3Mapper,
    ciram: [u8; Self::CIRAM_SIZE],
}

impl Mapper118 {
    const MAPPER_NUMBER: u16 = 118;
    const CIRAM_SIZE: usize = 0x0800;
    const NAMETABLE_PAGE_SIZE: usize = 0x0400;

    pub fn new(ctx: super::mapper::MapperContext) -> Self {
        Self {
            mmc3: MMC3Mapper::new_with_irq_mode(ctx.prg_rom, ctx.chr_rom, ctx.mirroring, false),
            ciram: [0; Self::CIRAM_SIZE],
        }
    }

    fn ciram_page_for_addr(&self, addr: u16) -> usize {
        let raw_chr_bank = self.mmc3.raw_chr_1k_bank(addr & 0x1FFF);
        (raw_chr_bank >> 7) & 0x01
    }
}

impl Mapper for Mapper118 {
    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 mapper_number(&self) -> u16 {
        Self::MAPPER_NUMBER
    }

    fn read_prg(&self, addr: u16) -> u8 {
        self.mmc3.read_prg(addr)
    }

    fn write_prg(&mut self, addr: u16, value: u8) {
        self.mmc3.write_prg(addr, value);
    }

    fn read_prg_open_bus(&self, addr: u16, open_bus: u8) -> u8 {
        self.mmc3.read_prg_open_bus(addr, open_bus)
    }

    fn read_chr(&mut self, addr: u16) -> u8 {
        self.mmc3.read_chr(addr)
    }

    fn write_chr(&mut self, addr: u16, value: u8) {
        self.mmc3.write_chr(addr, value);
    }

    fn read_nametable(&mut self, addr: u16) -> Option<u8> {
        let addr = addr & 0x2FFF;
        if !(0x2000..=0x2FFF).contains(&addr) {
            return None;
        }

        let page = self.ciram_page_for_addr(addr);
        let offset = (addr as usize) & (Self::NAMETABLE_PAGE_SIZE - 1);
        Some(self.ciram[page * Self::NAMETABLE_PAGE_SIZE + offset])
    }

    fn write_nametable(&mut self, addr: u16, value: u8) -> bool {
        let addr = addr & 0x2FFF;
        if !(0x2000..=0x2FFF).contains(&addr) {
            return false;
        }

        let page = self.ciram_page_for_addr(addr);
        let offset = (addr as usize) & (Self::NAMETABLE_PAGE_SIZE - 1);
        self.ciram[page * Self::NAMETABLE_PAGE_SIZE + offset] = value;
        true
    }

    fn wram_size(&self) -> usize {
        self.mmc3.wram_size()
    }

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

    fn load_wram_snapshot(&mut self, data: &[u8]) {
        self.mmc3.load_wram_snapshot(data);
    }

    fn initialize_ram(&mut self, mode: crate::console::RamInitMode) {
        self.mmc3.initialize_ram(mode);
        crate::console::initialize_ram(&mut self.ciram, mode);
    }

    fn registers_snapshot(&self) -> Vec<u8> {
        let mut snapshot = self.mmc3.registers_snapshot();
        snapshot.extend_from_slice(&self.ciram);
        snapshot
    }

    fn restore_registers(&mut self, data: &[u8]) {
        let min_len = self.mmc3.registers_snapshot().len() + Self::CIRAM_SIZE;
        if data.len() < min_len {
            return;
        }
        let (mmc3_data, ciram_data) = data.split_at(data.len() - Self::CIRAM_SIZE);
        self.mmc3.restore_registers(mmc3_data);
        self.ciram.copy_from_slice(ciram_data);
    }

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

    fn capabilities(&self) -> MapperCapabilities {
        self.mmc3.capabilities()
    }
}

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

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

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

    #[test]
    fn mapper_118_is_registered_in_factory() {
        let _mapper = make_mapper(118);
    }

    #[test]
    fn mapper_118_prg_chr_and_irq_match_mmc3() {
        let mut txsrom = make_mapper(118);
        let mut mmc3 = make_mapper(4);

        for mapper in [&mut txsrom, &mut mmc3] {
            mapper.write_prg(0x8000, 0x06);
            mapper.write_prg(0x8001, 0x09);
            mapper.write_prg(0x8000, 0x07);
            mapper.write_prg(0x8001, 0x0B);
            mapper.write_prg(0x8000, 0x80);
            mapper.write_prg(0x8001, 0x14);
            mapper.write_prg(0x8000, 0x02);
            mapper.write_prg(0x8001, 0x21);
        }

        for addr in [0x8000, 0xA000, 0xC000, 0xE000] {
            assert_eq!(txsrom.read_prg(addr), mmc3.read_prg(addr));
        }
        for addr in [0x0000, 0x0400, 0x0800, 0x1000, 0x1400, 0x1C00] {
            assert_eq!(txsrom.read_chr(addr), mmc3.read_chr(addr));
        }

        for mapper in [&mut txsrom, &mut mmc3] {
            mapper.write_prg(0xC000, 0x01);
            mapper.write_prg(0xC001, 0x00);
            mapper.write_prg(0xE001, 0x00);
        }
        for _ in 0..2 {
            txsrom.ppu_address_changed(0x0FFF);
            mmc3.ppu_address_changed(0x0FFF);
            for _ in 0..3 {
                txsrom.cpu_cycle();
                mmc3.cpu_cycle();
            }
            txsrom.ppu_address_changed(0x1000);
            mmc3.ppu_address_changed(0x1000);
        }
        assert_eq!(txsrom.irq_pending(), mmc3.irq_pending());
    }

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

        mapper.write_prg(0x8000, 0x00);
        mapper.write_prg(0x8001, 0x00);
        assert!(mapper.write_nametable(0x2000, 0x11));

        mapper.write_prg(0x8000, 0x00);
        mapper.write_prg(0x8001, 0x80);
        assert!(mapper.write_nametable(0x2000, 0xAA));
        assert_eq!(mapper.read_nametable(0x2000), Some(0xAA));

        mapper.write_prg(0xA000, 0x01);
        assert_eq!(mapper.read_nametable(0x2000), Some(0xAA));

        mapper.write_prg(0x8000, 0x00);
        mapper.write_prg(0x8001, 0x00);
        assert_eq!(mapper.read_nametable(0x2000), Some(0x11));
    }

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

        // CHR mode 0: $2000/$2400 use R0/R0+1, $2800/$2C00 use R1/R1+1
        mapper.write_prg(0x8000, 0x00); // R0, CHR mode 0
        mapper.write_prg(0x8001, 0x00); // bit7=0 -> NT0
        mapper.write_prg(0x8000, 0x01); // R1
        mapper.write_prg(0x8001, 0x80); // bit7=1 -> NT1

        assert!(mapper.write_nametable(0x2000, 0x12)); // NT0
        assert!(mapper.write_nametable(0x2800, 0x34)); // NT1
        assert_eq!(mapper.read_nametable(0x2000), Some(0x12));
        assert_eq!(mapper.read_nametable(0x2400), Some(0x12));
        assert_eq!(mapper.read_nametable(0x2800), Some(0x34));
        assert_eq!(mapper.read_nametable(0x2C00), Some(0x34));

        // CHR mode 1: $2000/$2400/$2800/$2C00 use R2/R3/R4/R5 respectively.
        mapper.write_prg(0x8000, 0x82); // R2, CHR mode 1
        mapper.write_prg(0x8001, 0x80); // NT1
        mapper.write_prg(0x8000, 0x83); // R3
        mapper.write_prg(0x8001, 0x00); // NT0
        mapper.write_prg(0x8000, 0x84); // R4
        mapper.write_prg(0x8001, 0x80); // NT1
        mapper.write_prg(0x8000, 0x85); // R5
        mapper.write_prg(0x8001, 0x00); // NT0

        assert!(mapper.write_nametable(0x2000, 0x56)); // NT1
        assert!(mapper.write_nametable(0x2400, 0x78)); // NT0
        assert_eq!(mapper.read_nametable(0x2000), Some(0x56));
        assert_eq!(mapper.read_nametable(0x2800), Some(0x56));
        assert_eq!(mapper.read_nametable(0x2400), Some(0x78));
        assert_eq!(mapper.read_nametable(0x2C00), Some(0x78));
    }

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

        mapper.initialize_ram(RamInitMode::SeededRandom(0x118));

        let snapshot = mapper.registers_snapshot();
        let ciram_start = snapshot.len() - 0x800;
        let ciram = &snapshot[ciram_start..];
        assert!(
            ciram.iter().any(|&b| b != 0),
            "initialize_ram should initialize mapper-owned CIRAM as well"
        );
    }

    #[test]
    fn mapper_118_snapshot_restore_preserves_bank_and_mirroring_state() {
        let mut mapper = make_mapper(118);
        mapper.write_prg(0x8000, 0x06);
        mapper.write_prg(0x8001, 0x19);
        mapper.write_prg(0x8000, 0x00);
        mapper.write_prg(0x8001, 0x80);
        assert!(mapper.write_nametable(0x2000, 0xCC));
        mapper.write_prg(0x8001, 0x00);
        assert!(mapper.write_nametable(0x2000, 0x33));
        mapper.write_prg(0x8001, 0x80);

        let snapshot = mapper.registers_snapshot();

        let mut restored = make_mapper(118);
        restored.restore_registers(&snapshot);

        assert_eq!(restored.read_prg(0x8000), mapper.read_prg(0x8000));
        assert_eq!(restored.read_chr(0x0000), mapper.read_chr(0x0000));
        assert_eq!(restored.read_nametable(0x2000), Some(0xCC));
        restored.write_prg(0x8000, 0x00);
        restored.write_prg(0x8001, 0x00);
        assert_eq!(restored.read_nametable(0x2000), Some(0x33));
    }
}