Skip to main content

rustynes_mappers/
m105_nes_event.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//! Mapper 105: NES-EVENT, the *Nintendo World Championships 1990* board
3//! (v2.9.6 "Roster").
4//!
5//! Written from `nesdev_wiki/output/NES_EVENT.md` (the page, Disch's notes on
6//! it, and its hardware notes). The board is an MMC1 with its CHR lines
7//! rewired, two 128 KiB PRG EPROMs, 8 KiB of CHR-RAM, 8 KiB of PRG-RAM, and a
8//! 30-bit M2 counter.
9//!
10//! The MMC1 is the project's own [`Mmc1`]: its serial port (the five-write
11//! protocol, the reset bit, the consecutive-write filter) is exactly the
12//! chip's. This board reads the four resulting registers and wires them:
13//!
14//! - **`$A000` (CHR bank 0)** is `...I OAA.`. `I` controls the timer and the
15//!   lock, `O` picks the PRG chip, and `AA` is a 32 KiB bank in the first chip.
16//! - **PRG.** Until unlocked, the first 32 KiB of the first chip, "no matter
17//!   what". Once unlocked, `O=0` takes the 32 KiB bank `AA` of the first chip.
18//!   `O=1` banks the second chip the ordinary MMC1 way, from `$E000` and the
19//!   `$8000` PRG mode.
20//! - **Unlock.** Writing `I=0` then `I=1` unlocks PRG banking. Power-on and
21//!   reset lock it again.
22//! - **Timer.** While `I=0` the counter counts up every M2 cycle. `I=1` resets
23//!   it to 0, holds it, and acknowledges the IRQ. It fires on reaching
24//!   `$20000000 | DIP << 25`. The page's tournament setting has switch C
25//!   closed (DIP `%0100`, about 6.25 minutes on NTSC), and that is the
26//!   default here; [`NesEvent105::set_dip`] changes it.
27//! - **CHR** is 8 KiB of RAM, never banked: the MMC1's CHR registers are
28//!   rewired to the lines above.
29//! - **WRAM** at `$6000-$7FFF` obeys `$E000` bit 4, as on any MMC1.
30
31// Bank arithmetic narrows values already reduced modulo a bank count, and the
32// accessors stay non-`const` to match the other mapper modules; both lints
33// are allowed crate-wide in the sibling modules for the same reasons.
34#![allow(clippy::cast_possible_truncation, clippy::missing_const_for_fn)]
35
36use crate::cartridge::Mirroring;
37use crate::m001_mmc1::Mmc1;
38use crate::mapper::{Mapper, MapperCaps, MapperDebugInfo, MapperError};
39use alloc::{boxed::Box, format, vec, vec::Vec};
40
41const PRG_BANK_16K: usize = 0x4000;
42const CHIP: usize = 0x2_0000;
43const CHR_RAM: usize = 0x2000;
44const WRAM: usize = 0x2000;
45const SAVE_STATE_VERSION: u8 = 1;
46/// The largest `target()`: DIP 15, `0x2000_0000 | 15 << 25`.
47const MAX_TARGET: u32 = 0x3E00_0000;
48
49/// The tournament DIP setting: switch C closed.
50pub const NWC_TOURNAMENT_DIP: u8 = 0b0100;
51
52/// Mapper 105 (NES-EVENT).
53pub struct NesEvent105 {
54    mmc1: Mmc1,
55    prg_rom: Box<[u8]>,
56    chr_ram: Box<[u8]>,
57    wram: Box<[u8]>,
58    unlocked: bool,
59    /// `I=0` has been seen since power-on or reset (the first half of the
60    /// unlock sequence).
61    seen_i_low: bool,
62    counter: u32,
63    irq_pending: bool,
64    dip: u8,
65}
66
67impl NesEvent105 {
68    /// Construct the board.
69    ///
70    /// # Errors
71    ///
72    /// [`MapperError::Invalid`] when PRG is not a non-zero multiple of 32 KiB.
73    pub fn new(prg_rom: Box<[u8]>, mirroring: Mirroring) -> Result<Self, MapperError> {
74        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(2 * PRG_BANK_16K) {
75            return Err(MapperError::Invalid(format!(
76                "mapper 105 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
77                prg_rom.len()
78            )));
79        }
80        // The embedded MMC1 only runs the serial port and the nametables; it
81        // is given a minimal ROM it never reads and no PRG-RAM of its own.
82        let mmc1 = Mmc1::new(
83            vec![0u8; 2 * PRG_BANK_16K].into_boxed_slice(),
84            Box::new([]),
85            mirroring,
86            0,
87        )?;
88        Ok(Self {
89            mmc1,
90            prg_rom,
91            chr_ram: vec![0u8; CHR_RAM].into_boxed_slice(),
92            wram: vec![0u8; WRAM].into_boxed_slice(),
93            unlocked: false,
94            seen_i_low: false,
95            counter: 0,
96            irq_pending: false,
97            dip: NWC_TOURNAMENT_DIP,
98        })
99    }
100
101    /// Set the four timer DIP switches (bits 28-25 of the target).
102    pub fn set_dip(&mut self, dip: u8) {
103        self.dip = dip & 0x0F;
104    }
105
106    fn target(&self) -> u32 {
107        0x2000_0000 | (u32::from(self.dip) << 25)
108    }
109
110    fn i_bit(&self) -> bool {
111        self.mmc1.registers().1 & 0x10 != 0
112    }
113
114    fn prg_offset(&self, addr: u16) -> usize {
115        let (control, chr0, _, prg) = self.mmc1.registers();
116        let within = usize::from(addr & 0x3FFF);
117        let high = addr & 0x4000 != 0;
118        let bank16 = if !self.unlocked {
119            usize::from(high)
120        } else if chr0 & 0x08 == 0 {
121            usize::from((chr0 >> 1) & 0x03) * 2 + usize::from(high)
122        } else {
123            let chip = CHIP / PRG_BANK_16K;
124            let p = usize::from(prg & 0x07);
125            let inner = match (control >> 2) & 0x03 {
126                0 | 1 => (p & !1) + usize::from(high),
127                2 => {
128                    if high {
129                        p
130                    } else {
131                        0
132                    }
133                }
134                _ => {
135                    if high {
136                        chip - 1
137                    } else {
138                        p
139                    }
140                }
141            };
142            chip + inner
143        };
144        (bank16 * PRG_BANK_16K + within) % self.prg_rom.len()
145    }
146
147    fn wram_disabled(&self) -> bool {
148        self.mmc1.registers().3 & 0x10 != 0
149    }
150}
151
152impl Mapper for NesEvent105 {
153    fn sram(&self) -> &[u8] {
154        &self.wram
155    }
156
157    fn sram_mut(&mut self) -> &mut [u8] {
158        &mut self.wram
159    }
160
161    fn caps(&self) -> MapperCaps {
162        MapperCaps::CYCLE_IRQ
163    }
164
165    fn reset(&mut self) {
166        self.unlocked = false;
167        self.seen_i_low = false;
168    }
169
170    fn cpu_read_unmapped(&self, addr: u16) -> bool {
171        match addr {
172            0x6000..=0x7FFF => self.wram_disabled(),
173            _ => addr < 0x6000,
174        }
175    }
176
177    fn cpu_read(&mut self, addr: u16) -> u8 {
178        match addr {
179            0x6000..=0x7FFF => self.wram[usize::from(addr - 0x6000)],
180            0x8000..=0xFFFF => self.prg_rom[self.prg_offset(addr)],
181            _ => 0,
182        }
183    }
184
185    fn cpu_write(&mut self, addr: u16, value: u8) {
186        match addr {
187            0x6000..=0x7FFF if !self.wram_disabled() => {
188                self.wram[usize::from(addr - 0x6000)] = value;
189            }
190            0x8000..=0xFFFF => {
191                let before = self.mmc1.shift_count();
192                self.mmc1.cpu_write(addr, value);
193                let committed = before == 4 && self.mmc1.shift_count() == 0 && value & 0x80 == 0;
194                if !(committed && addr & 0xE000 == 0xA000) {
195                    return;
196                }
197                // A committed `$A000` write: the I bit's two jobs.
198                if self.i_bit() {
199                    self.counter = 0;
200                    self.irq_pending = false;
201                    if self.seen_i_low {
202                        self.unlocked = true;
203                    }
204                } else {
205                    self.seen_i_low = true;
206                }
207            }
208            _ => {}
209        }
210    }
211
212    fn notify_cpu_cycle(&mut self) {
213        self.mmc1.notify_cpu_cycle();
214        if self.i_bit() || self.irq_pending {
215            return;
216        }
217        self.counter += 1;
218        if self.counter >= self.target() {
219            self.irq_pending = true;
220        }
221    }
222
223    fn irq_pending(&self) -> bool {
224        self.irq_pending
225    }
226
227    fn ppu_read(&mut self, addr: u16) -> u8 {
228        let addr = addr & 0x3FFF;
229        if addr < 0x2000 {
230            self.chr_ram[usize::from(addr)]
231        } else {
232            self.mmc1.ppu_read(addr)
233        }
234    }
235
236    fn ppu_write(&mut self, addr: u16, value: u8) {
237        let addr = addr & 0x3FFF;
238        if addr < 0x2000 {
239            self.chr_ram[usize::from(addr)] = value;
240        } else {
241            self.mmc1.ppu_write(addr, value);
242        }
243    }
244
245    fn nametable_address(&self, addr: u16) -> u16 {
246        self.mmc1.nametable_address(addr)
247    }
248
249    fn current_mirroring(&self) -> Mirroring {
250        self.mmc1.current_mirroring()
251    }
252
253    fn debug_info(&self) -> MapperDebugInfo {
254        let mut info = self.mmc1.debug_info();
255        info.mapper_id = 105;
256        info.name = "NES-EVENT (105)".into();
257        info.irq_state
258            .push(("timer".into(), format!("{:#010x}", self.counter)));
259        info.irq_state
260            .push(("target".into(), format!("{:#010x}", self.target())));
261        info.extra
262            .push(("unlocked".into(), format!("{}", self.unlocked)));
263        info
264    }
265
266    fn save_state(&self) -> Vec<u8> {
267        let inner = self.mmc1.save_state();
268        let mut out = Vec::with_capacity(16 + inner.len() + CHR_RAM + WRAM);
269        out.push(SAVE_STATE_VERSION);
270        out.push(u8::from(self.unlocked));
271        out.push(u8::from(self.seen_i_low));
272        out.extend_from_slice(&self.counter.to_le_bytes());
273        out.push(u8::from(self.irq_pending));
274        out.push(self.dip);
275        out.extend_from_slice(&(inner.len() as u32).to_le_bytes());
276        out.extend_from_slice(&inner);
277        out.extend_from_slice(&self.chr_ram);
278        out.extend_from_slice(&self.wram);
279        out
280    }
281
282    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
283        const HEAD: usize = 13;
284        if data.len() < HEAD {
285            return Err(MapperError::WrongLength {
286                expected: HEAD,
287                got: data.len(),
288            });
289        }
290        if data[0] != SAVE_STATE_VERSION {
291            return Err(MapperError::UnsupportedVersion(data[0]));
292        }
293        let inner_len = u32::from_le_bytes([data[9], data[10], data[11], data[12]]) as usize;
294        // Checked: `inner_len` comes from the blob, and on a 32-bit target
295        // (`usize` = u32) the plain sum wraps, passing the length check and
296        // panicking at the slice below.
297        let expected = HEAD
298            .checked_add(inner_len)
299            .and_then(|n| n.checked_add(CHR_RAM + WRAM));
300        if expected != Some(data.len()) {
301            return Err(MapperError::WrongLength {
302                expected: expected.unwrap_or(usize::MAX),
303                got: data.len(),
304            });
305        }
306        // The timer stops at `target()`, whose largest value (DIP 15) is
307        // `MAX_TARGET`; a DIP change mid-run only lowers it. A higher counter
308        // is one the board cannot produce, validated before any assignment.
309        let counter = u32::from_le_bytes([data[3], data[4], data[5], data[6]]);
310        if counter > MAX_TARGET {
311            return Err(MapperError::Invalid(format!(
312                "mapper 105 timer {counter:#010x} is past the largest target {MAX_TARGET:#010x}"
313            )));
314        }
315        self.mmc1.load_state(&data[HEAD..HEAD + inner_len])?;
316        self.unlocked = data[1] != 0;
317        self.seen_i_low = data[2] != 0;
318        self.counter = counter;
319        self.irq_pending = data[7] != 0;
320        self.dip = data[8] & 0x0F;
321        let cur = HEAD + inner_len;
322        self.chr_ram.copy_from_slice(&data[cur..cur + CHR_RAM]);
323        self.wram.copy_from_slice(&data[cur + CHR_RAM..]);
324        Ok(())
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    fn board() -> NesEvent105 {
333        let mut prg = vec![0u8; 2 * CHIP];
334        for b in 0..(2 * CHIP / PRG_BANK_16K) {
335            prg[b * PRG_BANK_16K] = b as u8;
336        }
337        NesEvent105::new(prg.into_boxed_slice(), Mirroring::Horizontal).unwrap()
338    }
339
340    /// One serial-port register write, five bits LSB first.
341    fn mmc1(m: &mut NesEvent105, addr: u16, value: u8) {
342        for i in 0..5 {
343            m.cpu_write(addr, (value >> i) & 1);
344            // The consecutive-write filter needs a gap between writes.
345            m.notify_cpu_cycle();
346            m.notify_cpu_cycle();
347        }
348    }
349
350    #[test]
351    fn locked_to_the_first_32k_until_i_goes_low_then_high() {
352        let mut m = board();
353        mmc1(&mut m, 0xA000, 0x16); // I=1, O=0, AA=3: still locked
354        assert_eq!(m.cpu_read(0x8000), 0);
355        assert_eq!(m.cpu_read(0xC000), 1);
356        mmc1(&mut m, 0xA000, 0x06); // I=0
357        assert_eq!(m.cpu_read(0x8000), 0, "I=0 alone does not unlock");
358        mmc1(&mut m, 0xA000, 0x16); // I=1: unlocked
359        assert_eq!(m.cpu_read(0x8000), 6, "32 KiB bank 3 of chip 0");
360        assert_eq!(m.cpu_read(0xC000), 7);
361        m.reset();
362        assert_eq!(m.cpu_read(0x8000), 0, "reset locks it again");
363    }
364
365    #[test]
366    fn o_bit_selects_chip_1_with_mmc1_banking() {
367        let mut m = board();
368        mmc1(&mut m, 0xA000, 0x00);
369        mmc1(&mut m, 0xA000, 0x18); // I=1, O=1: unlocked, chip 1
370        mmc1(&mut m, 0x8000, 0x0C); // PRG mode 3
371        mmc1(&mut m, 0xE000, 0x02);
372        assert_eq!(m.cpu_read(0x8000), 8 + 2);
373        assert_eq!(m.cpu_read(0xC000), 15, "fixed last bank of chip 1");
374        mmc1(&mut m, 0x8000, 0x08); // PRG mode 2
375        assert_eq!(m.cpu_read(0x8000), 8);
376        assert_eq!(m.cpu_read(0xC000), 10);
377        mmc1(&mut m, 0x8000, 0x00); // 32 KiB mode
378        mmc1(&mut m, 0xE000, 0x05);
379        assert_eq!(m.cpu_read(0x8000), 8 + 4);
380        assert_eq!(m.cpu_read(0xC000), 8 + 5);
381    }
382
383    #[test]
384    fn timer_counts_while_i_is_low_and_fires_at_the_dip_target() {
385        let mut m = board();
386        m.set_dip(0);
387        mmc1(&mut m, 0xA000, 0x00); // I=0: counting from here
388        m.counter = 0x2000_0000 - 3;
389        m.notify_cpu_cycle();
390        m.notify_cpu_cycle();
391        assert!(!m.irq_pending());
392        m.notify_cpu_cycle();
393        assert!(m.irq_pending(), "$20000000 with every switch open");
394        mmc1(&mut m, 0xA000, 0x10); // I=1: acknowledge + reset + hold
395        assert!(!m.irq_pending());
396        assert_eq!(m.counter, 0);
397        m.notify_cpu_cycle();
398        assert_eq!(m.counter, 0, "held while I=1");
399    }
400
401    #[test]
402    fn the_tournament_dip_is_the_default() {
403        let m = board();
404        assert_eq!(m.target(), 0x2800_0000);
405    }
406
407    #[test]
408    fn chr_is_unbanked_ram_and_wram_obeys_e000_bit4() {
409        let mut m = board();
410        mmc1(&mut m, 0x8000, 0x10); // 4 KiB CHR mode: must not bank anything
411        mmc1(&mut m, 0xC000, 0x01);
412        m.ppu_write(0x1234, 0x5A);
413        assert_eq!(m.ppu_read(0x1234), 0x5A);
414        m.cpu_write(0x6000, 0x77);
415        assert_eq!(m.cpu_read(0x6000), 0x77);
416        mmc1(&mut m, 0xE000, 0x10);
417        assert!(m.cpu_read_unmapped(0x6000));
418    }
419
420    /// The timer stops once it reaches `target()`, and the largest target is
421    /// `0x3E00_0000` (DIP 15). A higher counter is one the board cannot
422    /// produce. It loaded cleanly and could reach `u32::MAX` and overflow.
423    #[test]
424    fn state_refuses_a_counter_past_the_largest_target() {
425        let good = board().save_state();
426        let mut blob = good.clone();
427        blob[3..7].copy_from_slice(&0x3E00_0001u32.to_le_bytes());
428        let mut m = board();
429        assert!(matches!(m.load_state(&blob), Err(MapperError::Invalid(_))));
430        assert_eq!(m.save_state(), good, "refused before any assignment");
431        blob[3..7].copy_from_slice(&0x3E00_0000u32.to_le_bytes());
432        board().load_state(&blob).unwrap();
433    }
434
435    /// The embedded MMC1 length is read from the blob. On a 32-bit target,
436    /// adding it to the other section sizes wrapped `usize` (a panic in
437    /// debug builds). It must be a clean refusal on every target.
438    #[test]
439    fn state_refuses_an_embedded_length_that_overflows() {
440        let mut blob = board().save_state();
441        blob[9..13].copy_from_slice(&u32::MAX.to_le_bytes());
442        assert!(board().load_state(&blob).is_err());
443    }
444
445    #[test]
446    fn state_round_trips() {
447        let mut a = board();
448        mmc1(&mut a, 0xA000, 0x00);
449        mmc1(&mut a, 0xA000, 0x1A);
450        a.ppu_write(0x0001, 3);
451        a.cpu_write(0x6001, 4);
452        let blob = a.save_state();
453        let mut b = board();
454        b.load_state(&blob).unwrap();
455        assert_eq!(b.cpu_read(0x8000), a.cpu_read(0x8000));
456        assert_eq!(b.save_state(), blob);
457    }
458}