neser 0.1.1

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 105 - NES-EVENT (Nintendo World Championships)
//!
//! Specifications:
//! - Main: <https://www.nesdev.org/wiki/NES-EVENT>
//! - Mapper family: MMC1-derived serial register interface
//!
//! Known Limitations:
//! - IRQ timing is modeled as a configurable CPU-cycle countdown driven by CHR bank 0 register writes.
//!   Real hardware uses an event-board timer circuit with DIP-switch-selected duration.
//! - This implementation focuses on mapper-level correctness needed for bank/mirroring control and IRQ
//!   signaling, not full discrete-logic cycle accuracy.

use crate::cartridge::BaseMapper;
use crate::cartridge::mapper::{Mapper, MapperCapabilities};
use crate::cartridge::mmc1::MMC1Mapper;

/// Mapper 105 (NES-EVENT).
///
/// This mapper reuses MMC1 banking/mirroring behavior and adds a CPU-cycle IRQ timer.
/// The timer is controlled through CHR bank 0 register writes:
/// - bit 4 set: timer reset/disabled, pending IRQ cleared
/// - bit 4 clear: timer armed
/// - bits 0-3: countdown value (in CPU cycles, with 0 treated as 1)
pub struct Mapper105 {
    inner: MMC1Mapper,
    irq_counter: u8,
    irq_reload: u8,
    irq_enabled: bool,
    irq_pending: bool,
    last_chr_bank_0: u8,
}

impl Mapper105 {
    const SNAPSHOT_SIZE: usize = 5;
    const CHR_BANK_MASK: u8 = 0x1F;
    const MMC1_WRITE_COMPLETE_COUNT: u8 = 4;
    const MMC1_CHR_BANK0_REGISTER_ADDR: u16 = 0xA000;
    const MMC1_WRITE_COUNT_IDX: usize = 1;
    const MMC1_CHR_BANK0_IDX: usize = 3;
    const MMC1_LAST_CHR_REG_ADDR_LO_IDX: usize = 23;
    const MMC1_LAST_CHR_REG_ADDR_HI_IDX: usize = 24;
    const MMC1_MIN_REG_SNAPSHOT_SIZE: usize = Self::MMC1_LAST_CHR_REG_ADDR_HI_IDX + 1;

    pub fn new(ctx: super::mapper::MapperContext) -> Self {
        let mut inner = MMC1Mapper::new(ctx);
        Self::force_timer_disable_bit_on_powerup(&mut inner);
        let chr_bank_0 = Self::chr_bank_0_from_mapper(&inner);

        let mut mapper = Self {
            inner,
            irq_counter: 0,
            irq_reload: 0,
            irq_enabled: false,
            irq_pending: false,
            last_chr_bank_0: chr_bank_0,
        };
        mapper.apply_timer_control(chr_bank_0);
        mapper
    }

    fn force_timer_disable_bit_on_powerup(inner: &mut MMC1Mapper) {
        let mut regs = inner.registers_snapshot();
        if regs.len() >= 4 {
            regs[3] |= 0x10;
            inner.restore_registers(&regs);
        }
    }

    fn chr_bank_0_from_mapper(inner: &MMC1Mapper) -> u8 {
        inner
            .registers_snapshot()
            .get(Self::MMC1_CHR_BANK0_IDX)
            .copied()
            .unwrap_or(0)
            & Self::CHR_BANK_MASK
    }

    fn apply_timer_control(&mut self, chr_bank_0: u8) {
        self.irq_reload = chr_bank_0 & 0x0F;
        if (chr_bank_0 & 0x10) != 0 {
            self.irq_enabled = false;
            self.irq_pending = false;
            self.irq_counter = 0;
        } else {
            self.irq_enabled = true;
            self.irq_pending = false;
            // Treat reload 0 as a 1-cycle timer so arming always progresses.
            self.irq_counter = self.irq_reload.max(1);
        }
    }

    fn maybe_update_timer_from_chr_register(&mut self) {
        let chr_bank_0 = Self::chr_bank_0_from_mapper(&self.inner);
        if chr_bank_0 != self.last_chr_bank_0 {
            self.last_chr_bank_0 = chr_bank_0;
            self.apply_timer_control(chr_bank_0);
        }
    }

    fn apply_timer_on_chr_bank0_commit(&mut self, before: &[u8], after: &[u8]) {
        if before.len() < Self::MMC1_MIN_REG_SNAPSHOT_SIZE
            || after.len() < Self::MMC1_MIN_REG_SNAPSHOT_SIZE
        {
            return;
        }

        let committed = before[Self::MMC1_WRITE_COUNT_IDX] == Self::MMC1_WRITE_COMPLETE_COUNT
            && after[Self::MMC1_WRITE_COUNT_IDX] == 0;
        let committed_reg = u16::from_le_bytes([
            after[Self::MMC1_LAST_CHR_REG_ADDR_LO_IDX],
            after[Self::MMC1_LAST_CHR_REG_ADDR_HI_IDX],
        ]);

        if committed && committed_reg == Self::MMC1_CHR_BANK0_REGISTER_ADDR {
            self.last_chr_bank_0 = after[Self::MMC1_CHR_BANK0_IDX] & Self::CHR_BANK_MASK;
            self.apply_timer_control(self.last_chr_bank_0);
        }
    }

    fn tick_irq_timer(&mut self) {
        if !self.irq_enabled {
            return;
        }
        if self.irq_counter > 0 {
            self.irq_counter -= 1;
        }
        if self.irq_counter == 0 {
            self.irq_pending = true;
            self.irq_enabled = false;
        }
    }
}

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

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

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

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

    fn write_prg(&mut self, addr: u16, value: u8) {
        let before = self.inner.registers_snapshot();
        self.inner.write_prg(addr, value);
        let after = self.inner.registers_snapshot();
        self.apply_timer_on_chr_bank0_commit(&before, &after);
    }

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

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

    fn cpu_cycle(&mut self) {
        self.inner.cpu_cycle();
        self.tick_irq_timer();
    }

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

    fn get_mirroring(&self) -> crate::cartridge::NametableLayout {
        self.inner.get_mirroring()
    }

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

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

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

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

    fn registers_snapshot(&self) -> Vec<u8> {
        let mut snap =
            Vec::with_capacity(Self::SNAPSHOT_SIZE + self.inner.registers_snapshot().len());
        snap.push(self.irq_enabled as u8);
        snap.push(self.irq_pending as u8);
        snap.push(self.last_chr_bank_0);
        snap.push(self.irq_counter);
        snap.push(self.irq_reload);
        snap.extend(self.inner.registers_snapshot());
        snap
    }

    fn restore_registers(&mut self, data: &[u8]) {
        if data.len() >= Self::SNAPSHOT_SIZE {
            self.irq_enabled = data[0] != 0;
            self.irq_pending = data[1] != 0;
            // Snapshot stores only CHR bank 0 lower 5 bits.
            self.last_chr_bank_0 = data[2] & Self::CHR_BANK_MASK;
            self.irq_counter = data[3];
            self.irq_reload = data[4];
            self.inner.restore_registers(&data[Self::SNAPSHOT_SIZE..]);
            self.maybe_update_timer_from_chr_register();
        }
    }

    fn reset(&mut self) {
        self.inner.reset();
        Self::force_timer_disable_bit_on_powerup(&mut self.inner);
        self.last_chr_bank_0 = Self::chr_bank_0_from_mapper(&self.inner);
        self.apply_timer_control(self.last_chr_bank_0);
    }

    fn capabilities(&self) -> MapperCapabilities {
        let mut caps = self.inner.capabilities();
        caps.has_irq = true;
        caps
    }
}

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

    const PRG_BANKS_16K: usize = 11;
    const CHR_BANKS_4K: usize = 9;

    fn write_mmc1_register<M: Mapper + ?Sized>(mapper: &mut M, addr: u16, value: u8) {
        // MMC1 serial protocol in this codebase requires two cpu_cycle() ticks
        // between writes so consecutive-write filtering does not drop the bit.
        for bit in 0..5 {
            mapper.cpu_cycle();
            mapper.cpu_cycle();
            mapper.write_prg(addr, (value >> bit) & 0x01);
        }
    }

    fn make_mapper() -> Box<dyn Mapper> {
        let prg_rom = banked_data(16 * 1024, PRG_BANKS_16K);
        let chr_rom = banked_data(4 * 1024, CHR_BANKS_4K);
        create_mapper(MapperContext::new_for_test(
            105,
            prg_rom,
            chr_rom,
            NametableLayout::Horizontal,
        ))
        .expect("Mapper 105 should be implemented")
    }

    #[test]
    fn mapper_105_is_registered() {
        let prg_rom = banked_data(16 * 1024, PRG_BANKS_16K);
        let chr_rom = banked_data(4 * 1024, CHR_BANKS_4K);
        let result = create_mapper(MapperContext::new_for_test(
            105,
            prg_rom,
            chr_rom,
            NametableLayout::Horizontal,
        ));
        assert!(result.is_ok(), "Mapper 105 must be registered");
    }

    #[test]
    fn mapper_105_prg_bank_switching_matches_mmc1_mode_3_at_8000_bfff() {
        let mut mapper = make_mapper();
        write_mmc1_register(mapper.as_mut(), 0xE000, 5);
        assert_eq!(mapper.read_prg(0x8000), 5);
    }

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

        write_mmc1_register(mapper.as_mut(), 0x8000, 0b00000);
        assert_eq!(mapper.get_mirroring(), NametableLayout::SingleScreenLower);

        write_mmc1_register(mapper.as_mut(), 0x8000, 0b00001);
        assert_eq!(mapper.get_mirroring(), NametableLayout::SingleScreenUpper);

        write_mmc1_register(mapper.as_mut(), 0x8000, 0b00010);
        assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);

        write_mmc1_register(mapper.as_mut(), 0x8000, 0b00011);
        assert_eq!(mapper.get_mirroring(), NametableLayout::Horizontal);
    }

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

        // Timer is armed on the final serial commit write; pre-commit cpu cycles
        // in write_mmc1_register() do not consume the new countdown value.
        write_mmc1_register(mapper.as_mut(), 0xA000, 0b00011);
        assert!(!mapper.irq_pending());

        mapper.cpu_cycle();
        mapper.cpu_cycle();
        assert!(
            !mapper.irq_pending(),
            "IRQ should not fire before countdown"
        );

        mapper.cpu_cycle();
        assert!(
            mapper.irq_pending(),
            "IRQ should fire when countdown reaches zero"
        );

        write_mmc1_register(mapper.as_mut(), 0xA000, 0b10011);
        assert!(
            !mapper.irq_pending(),
            "setting timer control bit must clear pending IRQ"
        );
    }

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

        // Reload value 0 must still produce a 1-cycle timer when bit4 is clear.
        write_mmc1_register(mapper.as_mut(), 0xA000, 0b00000);
        assert!(!mapper.irq_pending());
        mapper.cpu_cycle();
        assert!(
            mapper.irq_pending(),
            "reload 0 should be treated as 1 cycle"
        );

        // Bit4 set should disable timer and clear pending IRQ.
        write_mmc1_register(mapper.as_mut(), 0xA000, 0b10000);
        assert!(!mapper.irq_pending());
        mapper.cpu_cycle();
        assert!(
            !mapper.irq_pending(),
            "timer should remain disabled while bit4 is set"
        );
    }

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

        write_mmc1_register(mapper.as_mut(), 0xA000, 0b00001);
        mapper.cpu_cycle();
        assert!(mapper.irq_pending(), "initial timer arm should fire IRQ");

        // Rewriting the same committed value should re-apply timer control,
        // clearing pending IRQ and re-arming the countdown.
        write_mmc1_register(mapper.as_mut(), 0xA000, 0b00001);
        assert!(
            !mapper.irq_pending(),
            "same-value rewrite must clear pending IRQ and restart timer"
        );
        mapper.cpu_cycle();
        assert!(
            mapper.irq_pending(),
            "re-armed timer should fire again after configured countdown"
        );
    }
}