Skip to main content

rustynes_mappers/
m250_nitra250.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//
3// Provenance: the Nitra address decode (register data from A0-A7, the MMC3 even/odd line from A10) is derived from Mesen2 (GPL-3.0-or-later), `MMC3_250`, alongside the NESdev "INES Mapper 250" documentation. See docs/originality-and-provenance.md (Section 1)
4// and NOTICE for the complete, audited derivation record.
5//! Nitra (mapper 250) -- Time Diver Avenger.
6//!
7//! An MMC3 work-alike that moves the register interface into the *address*:
8//! the value written is ignored, and the low byte of the address supplies
9//! the data instead. The underlying bank/IRQ behaviour is MMC3's, which is
10//! why this board carries an A12-driven scanline IRQ counter where the rest
11//! of its size class carries none.
12//!
13//! A best-effort (Tier-2) board: register-decode correctness verified against
14//! the `GeraNES` reference emulator (cross-referenced, not copied)
15//! and the nesdev wiki, with no commercial-oracle ROM in the tree. Banking math
16//! is direct slice indexing and every bank select wraps with `% count`, so a
17//! register write can never index out of bounds -- required for the `#![no_std]`
18//! chip stack, which cannot afford a panic on a register access.
19//!
20//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
21//! and `docs/mappers.md` §Mapper coverage matrix.
22
23use crate::a12_filter::A12RiseFilter;
24use crate::cartridge::Mirroring;
25use crate::mapper::{Mapper, MapperCaps, MapperError};
26use alloc::{boxed::Box, vec::Vec};
27use alloc::{format, vec};
28
29const PRG_BANK_8K: usize = 0x2000;
30const CHR_BANK_1K: usize = 0x0400;
31const NAMETABLE_SIZE: usize = 0x0400;
32const NAMETABLE_SIZE_U16: u16 = 0x0400;
33
34/// v2 (v2.9.7): the IRQ counter counts scanlines (MMC3), not CPU cycles, and
35/// the A12 filter byte follows the IRQ flags. A v1 counter meant cycles, so a
36/// v1 state is refused (as `UnsupportedVersion`) rather than reinterpreted.
37const SAVE_STATE_VERSION: u8 = 2;
38
39// ---------------------------------------------------------------------------
40// Shared nametable helper (mirrors the one in the other simple-mapper modules).
41// ---------------------------------------------------------------------------
42
43const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
44    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
45    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
46    let physical = mirroring.physical_bank(table);
47    physical * NAMETABLE_SIZE + local
48}
49
50/// Mapper 250 (Nitra, *Time Diver Avenger*).
51// Independent banking / mode / IRQ flags; grouping them would obscure the
52// MMC3-equivalent register decode for no gain (mirrors `MapperCaps`).
53#[allow(clippy::struct_excessive_bools)]
54pub struct Nitra250 {
55    prg_rom: Box<[u8]>,
56    chr_rom: Box<[u8]>,
57    vram: Box<[u8]>,
58    reg_index: u8,
59    bank_regs: [u8; 8],
60    prg_mode: bool,
61    chr_mode: bool,
62    horizontal_mirroring: bool,
63    irq_latch: u8,
64    irq_counter: u8,
65    irq_reload: bool,
66    irq_enabled: bool,
67    irq_pending: bool,
68    /// v2.9.7 — MMC3's A12 rise filter (see `a12_filter`).
69    a12: A12RiseFilter,
70}
71
72impl Nitra250 {
73    /// Construct a new mapper 250 board.
74    ///
75    /// # Errors
76    ///
77    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
78    /// 8 KiB or CHR-ROM is empty / not a multiple of 1 KiB.
79    pub fn new(
80        prg_rom: Box<[u8]>,
81        chr_rom: Box<[u8]>,
82        mirroring: Mirroring,
83    ) -> Result<Self, MapperError> {
84        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
85            return Err(MapperError::Invalid(format!(
86                "mapper 250 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
87                prg_rom.len()
88            )));
89        }
90        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_1K) {
91            return Err(MapperError::Invalid(format!(
92                "mapper 250 CHR-ROM size {} is not a non-zero multiple of 1 KiB",
93                chr_rom.len()
94            )));
95        }
96        Ok(Self {
97            prg_rom,
98            chr_rom,
99            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
100            reg_index: 0,
101            bank_regs: [0; 8],
102            prg_mode: false,
103            chr_mode: false,
104            horizontal_mirroring: mirroring == Mirroring::Horizontal,
105            irq_latch: 0,
106            irq_counter: 0,
107            irq_reload: false,
108            irq_enabled: false,
109            irq_pending: false,
110            a12: A12RiseFilter::new(),
111        })
112    }
113
114    fn read_prg(&self, bank: usize, addr: u16) -> u8 {
115        let count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
116        let bank = bank % count;
117        self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
118    }
119
120    fn prg_bank_for(&self, addr: u16) -> usize {
121        let last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1;
122        // `saturating_sub`, not `last - 1`: `new()` accepts any non-zero 8 KiB
123        // multiple, so an 8 KiB PRG gives `last == 0` and the fixed second-last
124        // slot underflows on an ordinary `$C000` read — a panic under overflow
125        // checks, contradicting this module's own "cannot afford a panic on a
126        // register access" invariant. `m004_mmc3` uses the same guard.
127        let second_last = last.saturating_sub(1);
128        let r6 = self.bank_regs[6] as usize;
129        let r7 = self.bank_regs[7] as usize;
130        match (self.prg_mode, addr) {
131            (false, 0x8000..=0x9FFF) | (true, 0xC000..=0xDFFF) => r6,
132            (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => second_last,
133            (_, 0xA000..=0xBFFF) => r7,
134            _ => last,
135        }
136    }
137
138    fn read_chr(&self, addr: u16) -> u8 {
139        let count1k = (self.chr_rom.len() / CHR_BANK_1K).max(1);
140        // MMC3-style: chr_mode swaps the two 2 KiB and four 1 KiB regions.
141        let region = (addr >> 10) & 0x07;
142        let region = if self.chr_mode { region ^ 0x04 } else { region };
143        let bank1k = match region {
144            0 => self.bank_regs[0] as usize & !1,
145            1 => (self.bank_regs[0] as usize & !1) + 1,
146            2 => self.bank_regs[1] as usize & !1,
147            3 => (self.bank_regs[1] as usize & !1) + 1,
148            4 => self.bank_regs[2] as usize,
149            5 => self.bank_regs[3] as usize,
150            6 => self.bank_regs[4] as usize,
151            _ => self.bank_regs[5] as usize,
152        };
153        let bank = bank1k % count1k;
154        self.chr_rom[bank * CHR_BANK_1K + (addr as usize & 0x03FF)]
155    }
156}
157
158impl Mapper for Nitra250 {
159    fn caps(&self) -> MapperCaps {
160        MapperCaps::CYCLE_IRQ
161    }
162
163    fn cpu_read(&mut self, addr: u16) -> u8 {
164        if (0x8000..=0xFFFF).contains(&addr) {
165            let bank = self.prg_bank_for(addr);
166            self.read_prg(bank, addr)
167        } else {
168            0
169        }
170    }
171
172    fn cpu_write(&mut self, addr: u16, _value: u8) {
173        // The MMC3-equivalent "data" is the low byte of the address (A0-A7); the
174        // MMC3 even/odd register line is carried by A10 (bit 10 of the address),
175        // not A8 — Mesen2 MMC3_250 decodes `(addr & 0xE000) | ((addr & 0x0400)
176        // >> 10)`. A8 left the bank-select / mirroring writes mis-routed, so the
177        // reset vector landed in the wrong PRG bank → blank boot.
178        let value = (addr & 0x00FF) as u8;
179        let odd = (addr & 0x0400) != 0;
180        match addr & 0xE000 {
181            0x8000 => {
182                if odd {
183                    self.bank_regs[self.reg_index as usize] = value;
184                } else {
185                    self.reg_index = value & 0x07;
186                    self.prg_mode = (value & 0x40) != 0;
187                    self.chr_mode = (value & 0x80) != 0;
188                }
189            }
190            0xA000 => {
191                if !odd {
192                    self.horizontal_mirroring = (value & 0x01) != 0;
193                }
194            }
195            0xC000 => {
196                if odd {
197                    self.irq_reload = true;
198                } else {
199                    self.irq_latch = value;
200                }
201            }
202            0xE000 => {
203                if odd {
204                    self.irq_enabled = true;
205                } else {
206                    self.irq_enabled = false;
207                    self.irq_pending = false;
208                }
209            }
210            _ => {}
211        }
212    }
213
214    fn ppu_read(&mut self, addr: u16) -> u8 {
215        let addr = addr & 0x3FFF;
216        match addr {
217            0x0000..=0x1FFF => self.read_chr(addr),
218            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
219            _ => 0,
220        }
221    }
222
223    fn ppu_write(&mut self, addr: u16, value: u8) {
224        let addr = addr & 0x3FFF;
225        if (0x2000..=0x3EFF).contains(&addr) {
226            let off = nametable_offset(addr, self.current_mirroring());
227            self.vram[off] = value;
228        }
229    }
230
231    fn notify_cpu_cycle(&mut self) {
232        self.a12.tick();
233    }
234
235    /// The MMC3 scanline counter (v2.9.7). `INES_Mapper_250.md`: "regular MMC3
236    /// chip connected in \[a\] different way", where the difference is only in
237    /// how the registers are addressed. Until v2.9.7 this was an 8-bit counter
238    /// decremented every CPU cycle, which nothing on the page supports; the
239    /// splits of *Time Diver Avenger* landed at arbitrary points and its
240    /// playfield drew from the wrong CHR banks (`T-COMMERCIAL-GARBLE`).
241    /// Reload on zero or a pending reload, else decrement; assert at zero when
242    /// enabled. A rise counts only through MMC3's filter.
243    fn notify_a12(&mut self, level: bool) {
244        if !self.a12.edge(level) {
245            return;
246        }
247        if self.irq_counter == 0 || self.irq_reload {
248            self.irq_counter = self.irq_latch;
249            self.irq_reload = false;
250        } else {
251            self.irq_counter -= 1;
252        }
253        if self.irq_counter == 0 && self.irq_enabled {
254            self.irq_pending = true;
255        }
256    }
257
258    fn irq_pending(&self) -> bool {
259        self.irq_pending
260    }
261
262    fn irq_acknowledge(&mut self) {
263        self.irq_pending = false;
264    }
265
266    fn current_mirroring(&self) -> Mirroring {
267        if self.horizontal_mirroring {
268            Mirroring::Horizontal
269        } else {
270            Mirroring::Vertical
271        }
272    }
273
274    fn save_state(&self) -> Vec<u8> {
275        let mut out = Vec::with_capacity(19 + self.vram.len());
276        out.push(SAVE_STATE_VERSION);
277        out.push(self.reg_index);
278        out.extend_from_slice(&self.bank_regs);
279        out.push(u8::from(self.prg_mode));
280        out.push(u8::from(self.chr_mode));
281        out.push(u8::from(self.horizontal_mirroring));
282        out.push(self.irq_latch);
283        out.push(self.irq_counter);
284        out.push(u8::from(self.irq_reload));
285        out.push(u8::from(self.irq_enabled));
286        out.push(u8::from(self.irq_pending));
287        out.push(self.a12.to_byte());
288        out.extend_from_slice(&self.vram);
289        out
290    }
291
292    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
293        // The version is checked BEFORE the length: a v1 state is one byte
294        // shorter, and must be reported as the version it is rather than as a
295        // truncated v2 (agy on #577).
296        match data.first() {
297            None => {
298                return Err(MapperError::WrongLength {
299                    expected: 1,
300                    got: 0,
301                });
302            }
303            Some(&v) if v != SAVE_STATE_VERSION => {
304                return Err(MapperError::UnsupportedVersion(v));
305            }
306            Some(_) => {}
307        }
308        let expected = 19 + self.vram.len();
309        if data.len() != expected {
310            return Err(MapperError::WrongLength {
311                expected,
312                got: data.len(),
313            });
314        }
315        self.reg_index = data[1] & 0x07;
316        self.bank_regs.copy_from_slice(&data[2..10]);
317        self.prg_mode = data[10] != 0;
318        self.chr_mode = data[11] != 0;
319        self.horizontal_mirroring = data[12] != 0;
320        self.irq_latch = data[13];
321        self.irq_counter = data[14];
322        self.irq_reload = data[15] != 0;
323        self.irq_enabled = data[16] != 0;
324        self.irq_pending = data[17] != 0;
325        self.a12 = A12RiseFilter::from_byte(data[18]);
326        self.vram.copy_from_slice(&data[19..19 + self.vram.len()]);
327        Ok(())
328    }
329}
330
331#[cfg(test)]
332#[allow(clippy::cast_possible_truncation)]
333mod tests {
334    use super::*;
335
336    fn synth_prg_8k(banks: usize) -> Box<[u8]> {
337        let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
338        for b in 0..banks {
339            v[b * PRG_BANK_8K] = b as u8;
340        }
341        v.into_boxed_slice()
342    }
343
344    fn synth_chr_1k(banks: usize) -> Box<[u8]> {
345        let mut v = vec![0u8; banks * CHR_BANK_1K];
346        for b in 0..banks {
347            v[b * CHR_BANK_1K] = b as u8;
348        }
349        v.into_boxed_slice()
350    }
351
352    #[test]
353    fn m250_address_encoded_mmc3_banking() {
354        let mut m = Nitra250::new(synth_prg_8k(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
355        // A10 (0x0400) carries the MMC3 even/odd line; A0-A7 carry the data.
356        // Even $8000 (A10=0), data 0x06 -> reg select index 6.
357        m.cpu_write(0x8000 | 0x06, 0);
358        // Odd $8000 (A10=1), data 0x03 -> bank_regs[6] = 3.
359        m.cpu_write(0x8000 | 0x400 | 0x03, 0);
360        assert_eq!(m.cpu_read(0x8000), 3);
361        // Mirroring via even $A000 (A10=0), data bit0 = 1.
362        m.cpu_write(0xA000 | 0x01, 0);
363        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
364    }
365
366    /// v2.9.7 (`T-COMMERCIAL-GARBLE`): `INES_Mapper_250.md` says the board is
367    /// a "regular MMC3 chip connected in \[a\] different way" (A10 selects the
368    /// register, A7-A0 carry the data), so its IRQ is the MMC3 scanline
369    /// counter clocked by filtered A12 rises. It was an 8-bit M2 cycle counter,
370    /// which nothing on the page supports: splits landed at arbitrary points,
371    /// and *Time Diver Avenger* drew its playfield from the wrong CHR banks.
372    /// Latch 7 through the address-encoded registers (`$C007` = latch 7,
373    /// `$C400` reload, `$E400` enable): once per eight scanlines.
374    #[test]
375    fn m250_irq_is_the_mmc3_scanline_counter() {
376        let mut m = Nitra250::new(synth_prg_8k(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
377        m.cpu_write(0xC000 | 0x07, 0);
378        m.cpu_write(0xC000 | 0x400, 0);
379        m.cpu_write(0xE000 | 0x400, 0);
380        let mut irqs = 0;
381        for _ in 0..16 {
382            for _ in 0..85 {
383                m.notify_cpu_cycle();
384            }
385            for _ in 0..8 {
386                m.notify_a12(true);
387                m.notify_cpu_cycle();
388                m.notify_a12(false);
389                m.notify_cpu_cycle();
390            }
391            if m.irq_pending() {
392                irqs += 1;
393                m.irq_acknowledge();
394            }
395        }
396        assert_eq!(irqs, 2, "once per eight scanlines, as an MMC3");
397    }
398
399    #[test]
400    fn m250_save_state_round_trip() {
401        let mut m = Nitra250::new(synth_prg_8k(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
402        m.cpu_write(0x8000 | 0x06, 0);
403        m.cpu_write(0x8000 | 0x400 | 0x02, 0);
404        m.cpu_write(0xC000 | 0x05, 0);
405        m.cpu_write(0xC000 | 0x400, 0);
406        m.cpu_write(0xE000 | 0x400, 0);
407        m.notify_a12(true);
408        m.notify_cpu_cycle();
409        let blob = m.save_state();
410        let mut m2 = Nitra250::new(synth_prg_8k(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
411        m2.load_state(&blob).unwrap();
412        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
413    }
414
415    /// A v1 state (v2.9.6 and earlier: 18 bytes plus the nametables, its IRQ
416    /// counter in CPU cycles) is refused by VERSION, not reported as a
417    /// truncated v2.
418    #[test]
419    fn m250_v1_state_is_refused_by_version() {
420        let mut m = Nitra250::new(synth_prg_8k(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
421        let mut v1 = m.save_state();
422        v1[0] = 1;
423        v1.remove(18);
424        assert!(matches!(
425            m.load_state(&v1),
426            Err(MapperError::UnsupportedVersion(1))
427        ));
428        assert!(matches!(
429            m.load_state(&[]),
430            Err(MapperError::WrongLength { .. })
431        ));
432    }
433}