neser 0.3.0

NESER - NES Emulator in Rust. Desktop (SDL) and WebAssembly frontends.
Documentation
//! Mapper 014 - SL1632 (MMC3/VRC2 hybrid)
//!
//! Specifications:
//! - Main: <https://www.nesdev.org/wiki/INES_Mapper_014>
//! - Related behavior source: <https://www.nesdev.org/wiki/INES_Mapper_116>
//!
//! Known Limitations:
//! - CHR outer-bit behavior in MMC3 mode follows widely used emulator behavior,
//!   but some hardware edge cases remain unverified.

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

pub struct Mapper14 {
    mmc3: MMC3Mapper,
    mode: u8,
    vrc_prg_regs: [u8; 2],
    vrc_chr_regs: [u8; 8],
    vrc_mirroring: u8,
}

impl Mapper14 {
    const SUPERVISOR_REGISTER: u16 = 0xA131;

    const MODE_MMC3: u8 = 0x02;
    const CHR_OUTER_LOW_HALF: u8 = 0x08;
    const CHR_OUTER_MID_HALF: u8 = 0x20;
    const CHR_OUTER_HIGH_HALF: u8 = 0x80;

    pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
        let mut mapper = Self {
            mmc3: MMC3Mapper::new_with_irq_mode(ctx.prg_rom, ctx.chr_rom, ctx.mirroring, false),
            mode: 0,
            vrc_prg_regs: [0; 2],
            vrc_chr_regs: [0; 8],
            vrc_mirroring: 0,
        };
        mapper.update_vrc_state();
        mapper
    }

    fn in_mmc3_mode(&self) -> bool {
        (self.mode & Self::MODE_MMC3) != 0
    }

    fn update_vrc_state(&mut self) {
        let prg0 = self.vrc_prg_regs[0] as i16;
        let prg1 = self.vrc_prg_regs[1] as i16;
        let chr = self.vrc_chr_regs;
        let mirror_h = (self.vrc_mirroring & 0x01) != 0;

        let base = self.base_mut();
        base.select_prg_page(0, prg0);
        base.select_prg_page(1, prg1);
        base.select_prg_page(2, -2);
        base.select_prg_page(3, -1);

        for (slot, bank) in chr.iter().enumerate() {
            base.select_chr_page(slot, *bank as i16);
        }

        base.set_mirroring_hv(mirror_h);
    }

    fn vrc_chr_outer_bank(&self, addr: u16) -> usize {
        let bit = if addr < 0x1000 {
            Self::CHR_OUTER_LOW_HALF
        } else if addr < 0x1800 {
            Self::CHR_OUTER_MID_HALF
        } else {
            Self::CHR_OUTER_HIGH_HALF
        };

        if (self.mode & bit) != 0 { 0x100 } else { 0 }
    }

    fn vrc_chr_reg_index(addr: u16) -> usize {
        debug_assert!((0xB000..=0xEFFF).contains(&addr));
        let upper = ((addr >> 12) & 0x07) as usize;
        let low = ((addr >> 1) & 0x01) as usize;
        ((upper - 3) << 1) + low
    }
}

impl Mapper for Mapper14 {
    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 write_prg(&mut self, addr: u16, value: u8) {
        if addr == Self::SUPERVISOR_REGISTER {
            self.mode = value;
            return;
        }

        if self.in_mmc3_mode() {
            self.mmc3.write_prg(addr, value);
            return;
        }

        if (0xB000..=0xEFFF).contains(&addr) {
            let reg = Self::vrc_chr_reg_index(addr);
            let low_nibble = (addr & 0x01) == 0;
            if low_nibble {
                self.vrc_chr_regs[reg] = (self.vrc_chr_regs[reg] & 0xF0) | (value & 0x0F);
            } else {
                self.vrc_chr_regs[reg] = (self.vrc_chr_regs[reg] & 0x0F) | ((value & 0x0F) << 4);
            }
        } else {
            match addr & 0xF003 {
                0x8000 => self.vrc_prg_regs[0] = value,
                0x9000 => self.vrc_mirroring = value,
                0xA000 => self.vrc_prg_regs[1] = value,
                _ => {}
            }
        }

        self.update_vrc_state();
    }

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

        if let Some(value) = self.base().try_read_prg_6000(addr) {
            return value;
        }
        if let Some(value) = self.base().try_read_prg_ram(addr) {
            return value;
        }
        match addr {
            0x8000..=0xFFFF => self.base().read_prg_rom(addr),
            _ => 0,
        }
    }

    fn read_prg_open_bus(&self, addr: u16, open_bus: u8) -> u8 {
        if self.in_mmc3_mode() {
            return self.mmc3.read_prg_open_bus(addr, open_bus);
        }
        self.read_prg(addr)
    }

    fn read_chr(&mut self, addr: u16) -> u8 {
        if !self.in_mmc3_mode() {
            return self.base().read_chr(addr);
        }

        let mmc3_bank = self.mmc3.mapped_chr_1k_bank(addr);
        let final_bank = self.vrc_chr_outer_bank(addr) | mmc3_bank;
        let offset = (addr as usize) & 0x03FF;
        self.mmc3.read_chr_1k_at(final_bank, offset)
    }

    fn write_chr(&mut self, addr: u16, value: u8) {
        if !self.in_mmc3_mode() {
            self.base_mut().write_chr(addr, value);
            return;
        }

        let mmc3_bank = self.mmc3.mapped_chr_1k_bank(addr);
        let final_bank = self.vrc_chr_outer_bank(addr) | mmc3_bank;
        let offset = (addr as usize) & 0x03FF;
        self.mmc3.write_chr_1k_at(final_bank, offset, value);
    }

    fn irq_pending(&self) -> bool {
        self.in_mmc3_mode() && self.mmc3.irq_pending()
    }

    fn cpu_cycle(&mut self) {
        if self.in_mmc3_mode() {
            self.mmc3.cpu_cycle();
        }
    }

    fn ppu_address_changed(&mut self, addr: u16) {
        if self.in_mmc3_mode() {
            self.mmc3.ppu_address_changed(addr);
        }
    }

    fn reset(&mut self) {
        self.mode = 0;
        self.vrc_prg_regs = [0; 2];
        self.vrc_chr_regs = [0; 8];
        self.vrc_mirroring = 0;
        self.update_vrc_state();
    }

    fn registers_snapshot(&self) -> Vec<u8> {
        let mut out = vec![self.mode, self.vrc_mirroring];
        out.extend_from_slice(&self.vrc_prg_regs);
        out.extend_from_slice(&self.vrc_chr_regs);
        out.extend(self.mmc3.registers_snapshot());
        out
    }

    fn restore_registers(&mut self, data: &[u8]) {
        if data.len() >= 12 {
            self.mode = data[0];
            self.vrc_mirroring = data[1];
            self.vrc_prg_regs.copy_from_slice(&data[2..4]);
            self.vrc_chr_regs.copy_from_slice(&data[4..12]);
            self.mmc3.restore_registers(&data[12..]);
            if !self.in_mmc3_mode() {
                self.update_vrc_state();
            }
        }
    }

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

    fn wram_size(&self) -> usize {
        8 * 1024
    }

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

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

    const PRG_BANKS_8K: usize = 41;
    const CHR_BANKS_1K: usize = 513;

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

    fn make_mapper_ext() -> Box<dyn Mapper> {
        let prg = banked_data(8 * 1024, PRG_BANKS_8K);
        let chr = banked_data_with_upper_marker(1024, CHR_BANKS_1K);
        create_mapper(MapperContext::new_for_test(
            14,
            prg,
            chr,
            NametableLayout::Vertical,
        ))
        .expect("Mapper 14 should be implemented")
    }

    fn make_mapper_direct() -> Mapper14 {
        Mapper14::new(MapperContext::new_for_test(
            14,
            banked_data(8 * 1024, PRG_BANKS_8K),
            banked_data(1024, CHR_BANKS_1K),
            NametableLayout::Vertical,
        ))
    }

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

    #[test]
    fn supervisor_defaults_to_vrc2_mode() {
        let mapper = make_mapper_direct();
        assert!(
            !mapper.in_mmc3_mode(),
            "power-on mode should default to VRC2"
        );
    }

    #[test]
    fn vrc2_prg_registers_control_8000_and_a000_windows() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 3);
        mapper.write_prg(0xA000, 5);
        assert_eq!(mapper.read_prg(0x8000), 3);
        assert_eq!(mapper.read_prg(0xA000), 5);
    }

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

        mapper.write_prg(0xB000, 0x0A); // low nibble for chr reg 0
        mapper.write_prg(0xB001, 0x03); // high nibble for chr reg 0 => 0x3A

        assert_eq!(mapper.read_chr(0x0000), 0x3A);
    }

    #[test]
    fn supervisor_selects_mmc3_mode() {
        let mut mapper = make_mapper();
        mapper.write_prg(0xA131, 0x02); // mode bit

        mapper.write_prg(0x8000, 0x06); // MMC3 R6
        mapper.write_prg(0x8001, 4);

        assert_eq!(mapper.read_prg(0x8000), 4);
    }

    #[test]
    fn mmc3_mode_uses_outer_chr_bit_for_low_pattern_group() {
        let mut mapper = make_mapper_ext();

        mapper.write_prg(0xA131, 0x0A); // MMC3 mode + CHR outer bit for $0000-$0FFF
        mapper.write_prg(0x8000, 0x00); // MMC3 R0 (2 KB pair)
        mapper.write_prg(0x8001, 4);

        assert_eq!(
            mapper.read_chr(0x0000),
            1,
            "outer CHR bit should select upper marker"
        );
    }

    #[test]
    fn vrc2_mirroring_uses_9000_bit0() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x9000, 0);
        assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);
        mapper.write_prg(0x9000, 1);
        assert_eq!(mapper.get_mirroring(), NametableLayout::Horizontal);
    }

    #[test]
    fn register_snapshot_restore_roundtrip() {
        let mut mapper = make_mapper();
        mapper.write_prg(0xA131, 0x0A);
        mapper.write_prg(0x8000, 0x06);
        mapper.write_prg(0x8001, 3);
        let snap = mapper.registers_snapshot();

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

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