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 084 – PC-SMB2J (Super Mario Bros. 2 Japan FDS conversion)
//!
//! Specifications:
//! - Primary: NesDev wiki (<https://www.nesdev.org/wiki/INES_Mapper_084>) — details unknown.
//!   The wiki notes this mapper is "reportedly PC-SMB2J" and may be "same FDS port as Mapper 40 or 50".
//! - Fallback: No Mesen2 implementation exists (absent from MapperFactory.cpp).
//!
//! This mapper is implemented as an alias for [Mapper 040 (NTDEC 2722)][`super::ntdec_2722::Ntdec2722Mapper`],
//! which is the closest documented mapper for SMB2J FDS conversions per the NesDev note.
//! FCEUX historically supported this mapper but dropped it; no authoritative source is available.

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

    // 11 banks × 8 KiB — non-power-of-two to prevent false-pass wrapping.
    const PRG_BANKS: usize = 11;
    const CHR_BANKS: usize = 1;

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

    // --- Factory ---

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

    // --- PRG fixed banks ---

    #[test]
    fn prg_6000_is_fixed_bank_6() {
        let mapper = make_mapper();
        assert_eq!(
            mapper.read_prg(0x6000),
            6,
            "$6000 must read from fixed bank 6"
        );
    }

    #[test]
    fn prg_8000_is_fixed_bank_4() {
        let mapper = make_mapper();
        assert_eq!(
            mapper.read_prg(0x8000),
            4,
            "$8000 must read from fixed bank 4"
        );
    }

    #[test]
    fn prg_a000_is_fixed_bank_5() {
        let mapper = make_mapper();
        assert_eq!(
            mapper.read_prg(0xA000),
            5,
            "$A000 must read from fixed bank 5"
        );
    }

    #[test]
    fn prg_e000_is_fixed_bank_7() {
        let mapper = make_mapper();
        assert_eq!(
            mapper.read_prg(0xE000),
            7,
            "$E000 must read from fixed bank 7"
        );
    }

    // --- PRG switchable window ---

    #[test]
    fn prg_c000_defaults_to_bank_0() {
        let mapper = make_mapper();
        assert_eq!(
            mapper.read_prg(0xC000),
            0,
            "$C000 defaults to bank 0 on power-on"
        );
    }

    #[test]
    fn prg_c000_selects_bank_via_e000_register() {
        let mut mapper = make_mapper();
        mapper.write_prg(0xE000, 3);
        assert_eq!(
            mapper.read_prg(0xC000),
            3,
            "$E000 write must select bank 3 at $C000"
        );
    }

    #[test]
    fn prg_bank_selection_wraps_at_total_bank_count() {
        let mut mapper = make_mapper();
        mapper.write_prg(0xE000, 11); // 11 % 11 = 0
        assert_eq!(
            mapper.read_prg(0xC000),
            0,
            "bank index must wrap mod total banks"
        );
    }

    // --- Mirroring ---

    #[test]
    fn mirroring_is_preserved_from_header() {
        let mapper = create_mapper(MapperContext::new_for_test(
            84,
            banked_data(8 * 1024, PRG_BANKS),
            banked_data(8 * 1024, CHR_BANKS),
            NametableLayout::Horizontal,
        ))
        .expect("Mapper 84 should be implemented");
        assert_eq!(mapper.get_mirroring(), NametableLayout::Horizontal);
    }

    // --- IRQ ---

    #[test]
    fn irq_not_pending_initially() {
        let mapper = make_mapper();
        assert!(!mapper.irq_pending(), "IRQ must not be pending on power-on");
    }

    #[test]
    fn irq_does_not_fire_while_disabled() {
        let mut mapper = make_mapper();
        for _ in 0..8192 {
            mapper.cpu_cycle();
        }
        assert!(!mapper.irq_pending(), "IRQ must not fire when disabled");
    }

    #[test]
    fn irq_fires_after_4096_cycles_when_enabled() {
        let mut mapper = make_mapper();
        mapper.write_prg(0xA000, 0); // enable
        for _ in 0..4096 {
            mapper.cpu_cycle();
        }
        assert!(mapper.irq_pending(), "IRQ must fire after 4096 CPU cycles");
    }

    #[test]
    fn irq_does_not_fire_before_4096_cycles() {
        let mut mapper = make_mapper();
        mapper.write_prg(0xA000, 0); // enable
        for _ in 0..4095 {
            mapper.cpu_cycle();
        }
        assert!(
            !mapper.irq_pending(),
            "IRQ must not fire before 4096 cycles"
        );
    }

    #[test]
    fn irq_self_acknowledges_at_8192_cycles() {
        let mut mapper = make_mapper();
        mapper.write_prg(0xA000, 0); // enable
        for _ in 0..8192 {
            mapper.cpu_cycle();
        }
        assert!(
            !mapper.irq_pending(),
            "IRQ must self-acknowledge after 8192 cycles"
        );
    }

    #[test]
    fn irq_acknowledged_and_disabled_by_8000_write() {
        let mut mapper = make_mapper();
        mapper.write_prg(0xA000, 0); // enable
        for _ in 0..4096 {
            mapper.cpu_cycle();
        }
        assert!(mapper.irq_pending());
        mapper.write_prg(0x8000, 0); // disable + ack
        assert!(!mapper.irq_pending(), "IRQ must clear after $8000 write");
    }

    #[test]
    fn irq_counter_resets_when_re_enabled() {
        let mut mapper = make_mapper();
        mapper.write_prg(0xA000, 0);
        for _ in 0..4096 {
            mapper.cpu_cycle();
        }
        mapper.write_prg(0x8000, 0); // ack
        mapper.write_prg(0xA000, 0); // re-enable
        for _ in 0..4095 {
            mapper.cpu_cycle();
        }
        assert!(
            !mapper.irq_pending(),
            "IRQ must not fire until 4096 cycles after re-enable"
        );
    }

    // --- Snapshot ---

    #[test]
    fn registers_snapshot_and_restore() {
        let mut mapper = make_mapper();
        mapper.write_prg(0xE000, 2);
        mapper.write_prg(0xA000, 0); // enable IRQ
        for _ in 0..100 {
            mapper.cpu_cycle();
        }
        let snap = mapper.registers_snapshot();
        let mut restored = make_mapper();
        restored.restore_registers(&snap);
        assert_eq!(
            restored.read_prg(0xC000),
            mapper.read_prg(0xC000),
            "Restored $C000 bank must match"
        );
        assert_eq!(
            restored.irq_pending(),
            mapper.irq_pending(),
            "Restored IRQ pending state must match"
        );
    }
}