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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
//! Mapper 093 – Sunsoft-2 IC (Sunsoft-3R board, 74S161/32)
//!
//! Specifications:
//! - Primary: NesDev wiki <https://www.nesdev.org/wiki/INES_Mapper_093>
//! - Fallback: Mesen2 `Sunsoft93.h`
//!
//! Known Limitations:
//! - No known gameplay-blocking functional limitations are currently documented.

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

/// Mapper 093 – Sunsoft-2 IC (Sunsoft-3R board)
///
/// Hardware: Sunsoft-2 IC (74S161 + 7432 discrete logic)
///
/// Specifications:
/// - Primary: <https://www.nesdev.org/wiki/INES_Mapper_093>
/// - PRG-ROM: Up to 128 KiB — 16 KiB switchable at $8000–$BFFF + fixed last bank at $C000$FFFF
/// - PRG-RAM: None
/// - CHR: 8 KiB RAM, enable-gated (E=0: reads return 0/open-bus, writes ignored; E=1: normal)
/// - Mirroring: Fixed from header (not programmable)
/// - Bus conflicts: Yes (register at $8000–$FFFF)
/// - IRQ: None
///
/// Register ($8000–$FFFF, write-only, **bus conflicts**):
/// ```text
/// [.PPP ...E]
///   P = bits 6:4 – 16 KiB PRG bank mapped at $8000–$BFFF
///   E = bit 0    – CHR-RAM enable (0 = disabled, 1 = normal)
/// ```
///
/// Power-on state: PRG bank 0, CHR-RAM disabled.
pub struct Mapper93 {
    base: BaseMapper,
    prg_bank: u8,
    chr_enabled: bool,
}

impl Mapper93 {
    pub fn new(ctx: super::mapper::MapperContext) -> Self {
        let capabilities = MapperCapabilities {
            max_prg_ram_kb: 0,
            prg_bank_size_kb: 16,
            ..Default::default()
        };
        let mut base = BaseMapper::new(&ctx, capabilities);
        base.configure_prg_banking(16 * 1024);
        // Upper 16 KB window ($C000$FFFF) is fixed to the last bank.
        base.select_prg_page(1, -1);
        base.set_bus_conflicts(true);
        let mut mapper = Self {
            base,
            prg_bank: 0,
            chr_enabled: false,
        };
        mapper.update_prg();
        mapper
    }

    fn update_prg(&mut self) {
        self.base.select_prg_page(0, self.prg_bank as i16);
    }
}

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

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

    fn write_prg(&mut self, addr: u16, value: u8) {
        if !(0x8000..=0xFFFF).contains(&addr) {
            return;
        }
        let effective = self.base.apply_bus_conflict(addr, value);
        self.prg_bank = (effective >> 4) & 0x07;
        self.chr_enabled = (effective & 0x01) != 0;
        self.update_prg();
    }

    fn read_chr(&mut self, addr: u16) -> u8 {
        if !self.chr_enabled {
            return 0; // open bus when CHR-RAM disabled
        }
        self.base.read_chr(addr)
    }

    fn write_chr(&mut self, addr: u16, value: u8) {
        if !self.chr_enabled {
            return; // writes ignored when CHR-RAM disabled
        }
        self.base.write_chr(addr, value);
    }

    fn registers_snapshot(&self) -> Vec<u8> {
        vec![self.prg_bank, u8::from(self.chr_enabled)]
    }

    fn restore_registers(&mut self, data: &[u8]) {
        if let Some(&bank) = data.first() {
            self.prg_bank = bank;
            self.update_prg();
        }
        if let Some(&enabled) = data.get(1) {
            self.chr_enabled = enabled != 0;
        }
    }

    fn reset(&mut self) {
        self.prg_bank = 0;
        self.chr_enabled = false;
        self.update_prg();
    }
}

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

    // Non-power-of-two PRG bank count prevents false-pass modulo wrapping.
    // Mapper 93 supports up to 8 banks (3 bits); use 5 banks here.
    const PRG_BANKS: usize = 5; // 5 × 16 KiB = 80 KiB

    /// Build a 5-bank × 16 KiB PRG ROM where:
    /// - All bytes are 0xFF (so bus conflicts pass write values through unchanged)
    /// - Offset 0x100 within each bank stores the bank index (for bank identification)
    fn make_prg_rom() -> Vec<u8> {
        let bank_size = 16 * 1024;
        let mut rom = vec![0xFF_u8; bank_size * PRG_BANKS];
        for bank in 0..PRG_BANKS {
            rom[bank * bank_size + 0x100] = bank as u8;
        }
        rom
    }

    fn make_mapper() -> Mapper93 {
        Mapper93::new(MapperContext::new_for_test(
            93,
            make_prg_rom(),
            vec![],
            NametableLayout::Horizontal,
        ))
    }

    /// Read bank index from a 16 KiB window by sampling offset 0x100.
    fn read_prg_bank(mapper: &Mapper93, window_base: u16) -> u8 {
        mapper.read_prg(window_base + 0x100)
    }

    // --- Registration ---

    #[test]
    fn mapper_93_is_registered() {
        let result = create_mapper(MapperContext::new_for_test(
            93,
            make_prg_rom(),
            vec![],
            NametableLayout::Horizontal,
        ));
        assert!(
            result.is_ok(),
            "Mapper 93 must be registered in the factory"
        );
    }

    // --- Power-on state ---

    #[test]
    fn power_on_lower_window_is_bank0() {
        let mapper = make_mapper();
        assert_eq!(
            read_prg_bank(&mapper, 0x8000),
            0,
            "$8000–$BFFF must map to PRG bank 0 at power-on"
        );
    }

    #[test]
    fn power_on_upper_window_is_last_bank() {
        let mapper = make_mapper();
        let last = (PRG_BANKS - 1) as u8;
        assert_eq!(
            read_prg_bank(&mapper, 0xC000),
            last,
            "$C000–$FFFF must be fixed to the last PRG bank at power-on"
        );
    }

    #[test]
    fn power_on_chr_ram_is_disabled() {
        // Before any register write, CHR-RAM E=0: reads return 0.
        let mut mapper = make_mapper();
        mapper.write_chr(0x0000, 0xAB); // write should be ignored
        assert_eq!(
            mapper.read_chr(0x0000),
            0,
            "CHR-RAM reads must return 0 (open bus) at power-on (E=0)"
        );
    }

    // --- PRG bank switching (bits 6:4) ---

    #[test]
    fn prg_bank_1_selected_by_bits_6_4() {
        // bits[6:4] = 0b001 → bank 1; bit0=1 (chr enable)
        // Write 0x11 = 0001_0001 → effective = 0x11 & 0xFF = 0x11 → prg_bank=1
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 0x11);
        assert_eq!(
            read_prg_bank(&mapper, 0x8000),
            1,
            "Writing 0x11 must select PRG bank 1 at $8000–$BFFF"
        );
    }

    #[test]
    fn prg_bank_3_selected_by_bits_6_4() {
        // bits[6:4] = 0b011 → bank 3; bit0=1
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 0x31);
        assert_eq!(
            read_prg_bank(&mapper, 0x8000),
            3,
            "Writing 0x31 must select PRG bank 3"
        );
    }

    #[test]
    fn prg_upper_window_stays_fixed_after_bank_switch() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 0x31); // PRG bank 3
        let last = (PRG_BANKS - 1) as u8;
        assert_eq!(
            read_prg_bank(&mapper, 0xC000),
            last,
            "$C000–$FFFF must remain fixed to the last bank after a bank switch"
        );
    }

    #[test]
    fn prg_bank_covers_full_16kb_window() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 0x31); // select bank 3
        // Both ends of the lower window map to bank 3
        assert_eq!(mapper.read_prg(0x8000 + 0x100), 3);
        assert_eq!(mapper.read_prg(0xBFFF), 0xFF); // non-index byte = 0xFF fill
    }

    #[test]
    fn prg_register_only_responds_to_8000_ffff() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x7FFF, 0x31); // outside register range — must be ignored
        assert_eq!(
            read_prg_bank(&mapper, 0x8000),
            0,
            "Write below $8000 must not affect PRG bank"
        );
    }

    // --- CHR-RAM enable/disable (bit 0) ---

    #[test]
    fn chr_ram_enabled_when_bit0_is_1() {
        let mut mapper = make_mapper();
        // Enable CHR-RAM: write 0x01 (prg_bank=0, chr_enable=1)
        mapper.write_prg(0x8000, 0x01);
        mapper.write_chr(0x0000, 0xAB);
        assert_eq!(
            mapper.read_chr(0x0000),
            0xAB,
            "CHR-RAM must be readable and writable when E=1"
        );
    }

    #[test]
    fn chr_ram_disabled_when_bit0_is_0() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 0x01); // enable
        mapper.write_chr(0x0000, 0xAB);
        mapper.write_prg(0x8000, 0x00); // disable (bit0=0, bus: 0x00&0xFF=0x00)
        assert_eq!(
            mapper.read_chr(0x0000),
            0,
            "CHR-RAM reads must return 0 after disabling E bit"
        );
    }

    #[test]
    fn chr_writes_ignored_when_disabled() {
        let mut mapper = make_mapper();
        // Write to CHR while disabled
        mapper.write_chr(0x0100, 0x55);
        // Enable and verify write was not stored
        mapper.write_prg(0x8000, 0x01);
        assert_eq!(
            mapper.read_chr(0x0100),
            0,
            "CHR-RAM write while disabled must be discarded"
        );
    }

    #[test]
    fn chr_ram_covers_full_8kb_window() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 0x01); // enable
        mapper.write_chr(0x0000, 0x11);
        mapper.write_chr(0x1FFF, 0x22);
        assert_eq!(mapper.read_chr(0x0000), 0x11);
        assert_eq!(mapper.read_chr(0x1FFF), 0x22);
    }

    // --- Bus conflicts ---

    #[test]
    fn bus_conflicts_apply_to_register_writes() {
        // Use a PRG ROM with bank 0 bytes = 0x10 at write address.
        // Writing 0x50 (prg_bank=5 without conflict) → effective = 0x50 & 0x10 = 0x10 → bank 1.
        let bank_size = 16 * 1024;
        let mut prg_rom = vec![0xFF_u8; bank_size * PRG_BANKS];
        // Set ROM[$8000] (offset 0 in bank 0) to 0x10
        prg_rom[0] = 0x10;
        // Embed bank markers at offset 0x100
        for bank in 0..PRG_BANKS {
            prg_rom[bank * bank_size + 0x100] = bank as u8;
        }
        let mut mapper = Mapper93::new(MapperContext::new_for_test(
            93,
            prg_rom,
            vec![],
            NametableLayout::Horizontal,
        ));
        // Without bus conflict: 0x50 >> 4 & 7 = 5 → bank 5.
        // With bus conflict: (0x50 & 0x10) >> 4 & 7 = 0x10 >> 4 & 7 = 1 → bank 1.
        mapper.write_prg(0x8000, 0x50);
        assert_eq!(
            read_prg_bank(&mapper, 0x8000),
            1,
            "Bus conflict must AND written value with PRG-ROM byte"
        );
    }

    // --- Mirroring ---

    #[test]
    fn mirroring_fixed_from_header() {
        let mapper = make_mapper();
        assert_eq!(
            mapper.get_mirroring(),
            NametableLayout::Horizontal,
            "Mirroring must be fixed from header"
        );
    }

    #[test]
    fn mirroring_not_changed_by_register_writes() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 0xFF);
        assert_eq!(
            mapper.get_mirroring(),
            NametableLayout::Horizontal,
            "Mirroring must not change after register write"
        );
    }

    // --- No IRQ ---

    #[test]
    fn irq_never_pending() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 0xFF);
        assert!(!mapper.irq_pending(), "Mapper 93 must never assert IRQ");
    }

    // --- Snapshot / restore ---

    #[test]
    fn registers_snapshot_round_trips() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 0x31); // PRG bank 3, CHR enabled

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

        assert_eq!(
            read_prg_bank(&restored, 0x8000),
            3,
            "Restored mapper must map PRG bank 3"
        );
        // Write to CHR-RAM to confirm it is enabled after restore
        restored.write_chr(0x0000, 0xDE);
        assert_eq!(
            restored.read_chr(0x0000),
            0xDE,
            "Restored mapper must have CHR-RAM enabled"
        );
    }

    #[test]
    fn registers_snapshot_preserves_chr_disabled_state() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 0x20); // PRG bank 2, CHR disabled

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

        assert_eq!(
            restored.read_chr(0x0000),
            0,
            "Restored mapper must have CHR-RAM disabled when E=0 was snapshotted"
        );
    }

    // --- Reset ---

    #[test]
    fn reset_returns_to_power_on_state() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 0x31); // PRG bank 3, CHR enabled
        mapper.write_chr(0x0000, 0x42);

        mapper.reset();

        assert_eq!(
            read_prg_bank(&mapper, 0x8000),
            0,
            "PRG bank must be 0 after reset"
        );
        assert_eq!(
            mapper.read_chr(0x0000),
            0,
            "CHR-RAM must return 0 (disabled) after reset"
        );
    }
}