Skip to main content

rustynes_mappers/
m163_nanjing.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//! Mapper 163: the Nanjing FC-001 board (v2.9.6 "Roster").
3//!
4//! Written from `nesdev_wiki/output/INES_Mapper_163.md` and its talk page.
5//!
6//! - `$6000-$7FFF`: 8 KiB of battery-backed PRG-RAM.
7//! - `$8000-$FFFF`: one 32 KiB PRG bank, from `$5000` (A18-A15) and `$5200`
8//!   (A20-A19). While the mode register's A bit is clear, PRG A15/A16 read
9//!   `11`, which is why a reset boots in bank 3.
10//! - CHR: 8 KiB of CHR-RAM. With `$5000` bit 7 set, CHR A12 is PPU A9
11//!   latched on the last rise of PPU A13 instead of PPU A12. That puts the left
12//!   pattern table on the top half of every nametable and the right one on
13//!   the bottom half, whatever the scroll.
14//! - Mirroring is hard-wired.
15//!
16//! **Modelling the A13 rise.** Every PPU bus read reaches the mapper as either
17//! a pattern access (`ppu_read`, A13 = 0) or a nametable access
18//! (`nametable_fetch`, A13 = 1). A13 therefore rises exactly on a nametable
19//! access that follows a pattern access, and that access's A9 is latched. The
20//! attribute fetch that follows a nametable fetch has no rise between them, so
21//! it does not re-latch. That matters: attribute addresses (`$23C0+`) have A9
22//! set.
23//!
24//! The mode register's B bit swaps D0 and D1 of writes to `$5000-$5200`
25//! (the feedback register included). It does not swap writes to itself. The
26//! page notes that 1 MiB boards wire both ASIC PRG A19 and A20 to ROM A19,
27//! "effectively exempting this register from the bit-swap"; the `$5200` swap
28//! is therefore skipped on 1 MiB images.
29
30// Bank arithmetic narrows values already reduced modulo a bank count, and the
31// accessors stay non-`const` to match the other mapper modules; both lints
32// are allowed crate-wide in the sibling modules for the same reasons.
33#![allow(clippy::cast_possible_truncation, clippy::missing_const_for_fn)]
34
35use crate::cartridge::Mirroring;
36use crate::mapper::{Mapper, MapperCaps, MapperDebugInfo, MapperError};
37use alloc::{boxed::Box, format, vec, vec::Vec};
38
39const PRG_BANK_32K: usize = 0x8000;
40const CHR_RAM: usize = 0x2000;
41const WRAM: usize = 0x2000;
42const SAVE_STATE_VERSION: u8 = 1;
43
44/// Mapper 163 (Nanjing FC-001).
45pub struct Nanjing163 {
46    prg_rom: Box<[u8]>,
47    chr_ram: Box<[u8]>,
48    wram: Box<[u8]>,
49    mirroring: Mirroring,
50    /// `$5000`: PRG A18-A15 (bits 0-3), auto-switch enable (bit 7).
51    reg_lo: u8,
52    /// `$5200`: PRG A20-A19 (bits 0-1).
53    reg_hi: u8,
54    /// `$5300`: A (bit 2) and B (bit 0).
55    mode: u8,
56    /// The feedback bit F, read back inverted at `$5500`.
57    feedback: bool,
58    /// PPU A13 as of the last PPU access.
59    a13: bool,
60    /// PPU A9 latched on the last rise of A13.
61    a9_latch: bool,
62}
63
64impl Nanjing163 {
65    /// Construct the board.
66    ///
67    /// # Errors
68    ///
69    /// [`MapperError::Invalid`] when PRG is not a non-zero multiple of 32 KiB.
70    pub fn new(prg_rom: Box<[u8]>, mirroring: Mirroring) -> Result<Self, MapperError> {
71        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
72            return Err(MapperError::Invalid(format!(
73                "mapper 163 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
74                prg_rom.len()
75            )));
76        }
77        Ok(Self {
78            prg_rom,
79            chr_ram: vec![0u8; CHR_RAM].into_boxed_slice(),
80            wram: vec![0u8; WRAM].into_boxed_slice(),
81            mirroring,
82            reg_lo: 0,
83            reg_hi: 0,
84            mode: 0,
85            feedback: false,
86            a13: false,
87            a9_latch: false,
88        })
89    }
90
91    /// D0/D1 swapped when the mode register's B bit is set.
92    fn swap(&self, v: u8) -> u8 {
93        if self.mode & 0x01 != 0 {
94            (v & 0xFC) | ((v & 0x01) << 1) | ((v >> 1) & 0x01)
95        } else {
96            v
97        }
98    }
99
100    fn prg_bank(&self) -> usize {
101        let mut bank = usize::from(self.reg_lo & 0x0F) | (usize::from(self.reg_hi & 0x03) << 4);
102        if self.mode & 0x04 == 0 {
103            bank |= 0x03;
104        }
105        bank % (self.prg_rom.len() / PRG_BANK_32K)
106    }
107
108    fn chr_offset(&self, addr: u16) -> usize {
109        let a = usize::from(addr & 0x0FFF);
110        let a12 = if self.reg_lo & 0x80 != 0 {
111            self.a9_latch
112        } else {
113            addr & 0x1000 != 0
114        };
115        a | (usize::from(a12) << 12)
116    }
117
118    /// A nametable access: A13 rises if the previous access was a pattern one.
119    fn on_nametable_access(&mut self, addr: u16) {
120        if !self.a13 {
121            self.a9_latch = addr & 0x0200 != 0;
122        }
123        self.a13 = true;
124    }
125}
126
127impl Mapper for Nanjing163 {
128    /// v3.1.0: not pure -- it latches PPU A13 from reads, so the PPU's display-only
129    /// "disable sprite limit" reads are not made on this board.
130    fn chr_reads_are_pure(&self) -> bool {
131        false
132    }
133
134    fn sram(&self) -> &[u8] {
135        &self.wram
136    }
137
138    fn sram_mut(&mut self) -> &mut [u8] {
139        &mut self.wram
140    }
141
142    fn caps(&self) -> MapperCaps {
143        MapperCaps::NONE
144    }
145
146    fn has_hardwired_mirroring(&self) -> bool {
147        true
148    }
149
150    /// "All registers are initialized to `$00` on reset."
151    fn reset(&mut self) {
152        self.reg_lo = 0;
153        self.reg_hi = 0;
154        self.mode = 0;
155        self.feedback = false;
156    }
157
158    fn cpu_read_unmapped(&self, addr: u16) -> bool {
159        match addr {
160            0x4020..=0x5FFF => addr & 0xF300 != 0x5100 || addr < 0x5000,
161            _ => false,
162        }
163    }
164
165    fn cpu_read_driven_mask(&self, _addr: u16) -> u8 {
166        0x04
167    }
168
169    fn cpu_read(&mut self, addr: u16) -> u8 {
170        match addr {
171            // `$5500-$5501`, mask `$F300`: the inverted F bit on D2.
172            0x5000..=0x5FFF if addr & 0xF300 == 0x5100 => {
173                if self.feedback {
174                    0
175                } else {
176                    0x04
177                }
178            }
179            0x6000..=0x7FFF => self.wram[usize::from(addr - 0x6000)],
180            0x8000..=0xFFFF => {
181                self.prg_rom[self.prg_bank() * PRG_BANK_32K + usize::from(addr & 0x7FFF)]
182            }
183            _ => 0,
184        }
185    }
186
187    fn cpu_write(&mut self, addr: u16, value: u8) {
188        match addr {
189            0x6000..=0x7FFF => self.wram[usize::from(addr - 0x6000)] = value,
190            0x5000..=0x5FFF => match addr & 0xFF00 {
191                0x5000 => self.reg_lo = self.swap(value),
192                0x5100 => {
193                    let v = self.swap(value);
194                    if addr & 0x01 == 0 {
195                        // A=0: F latched.
196                        self.feedback = v & 0x04 != 0;
197                    } else if v & 0x01 != 0 {
198                        // A=1: flip F when E=1.
199                        self.feedback = !self.feedback;
200                    }
201                }
202                0x5200 => {
203                    self.reg_hi = if self.prg_rom.len() == 1024 * 1024 {
204                        value
205                    } else {
206                        self.swap(value)
207                    };
208                }
209                0x5300 => self.mode = value,
210                _ => {}
211            },
212            _ => {}
213        }
214    }
215
216    fn ppu_read(&mut self, addr: u16) -> u8 {
217        let addr = addr & 0x3FFF;
218        if addr < 0x2000 {
219            self.a13 = false;
220            self.chr_ram[self.chr_offset(addr)]
221        } else {
222            0
223        }
224    }
225
226    fn ppu_write(&mut self, addr: u16, value: u8) {
227        let addr = addr & 0x3FFF;
228        if addr < 0x2000 {
229            self.a13 = false;
230            let off = self.chr_offset(addr);
231            self.chr_ram[off] = value;
232        }
233    }
234
235    fn nametable_fetch(&mut self, addr: u16) -> Option<u8> {
236        self.on_nametable_access(addr);
237        None
238    }
239
240    fn nametable_write(&mut self, addr: u16, _value: u8) -> bool {
241        self.on_nametable_access(addr);
242        false
243    }
244
245    fn current_mirroring(&self) -> Mirroring {
246        self.mirroring
247    }
248
249    fn debug_info(&self) -> MapperDebugInfo {
250        let mut info = MapperDebugInfo {
251            mapper_id: 163,
252            name: "Nanjing FC-001 (163)".into(),
253            mirroring: crate::mapper::mirroring_name(self.mirroring),
254            ..Default::default()
255        };
256        info.prg_banks
257            .push(("32K".into(), format!("{:#04x}", self.prg_bank())));
258        info.chr_banks.push((
259            "auto".into(),
260            format!(
261                "{} (A9 latch {})",
262                self.reg_lo >> 7,
263                u8::from(self.a9_latch)
264            ),
265        ));
266        info.extra
267            .push(("mode".into(), format!("{:#04x}", self.mode)));
268        info
269    }
270
271    fn save_state(&self) -> Vec<u8> {
272        let mut out = Vec::with_capacity(8 + CHR_RAM + WRAM);
273        out.push(SAVE_STATE_VERSION);
274        out.push(self.reg_lo);
275        out.push(self.reg_hi);
276        out.push(self.mode);
277        out.push(u8::from(self.feedback));
278        out.push(u8::from(self.a13));
279        out.push(u8::from(self.a9_latch));
280        out.extend_from_slice(&self.chr_ram);
281        out.extend_from_slice(&self.wram);
282        out
283    }
284
285    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
286        let expected = 7 + CHR_RAM + WRAM;
287        if data.len() != expected {
288            return Err(MapperError::WrongLength {
289                expected,
290                got: data.len(),
291            });
292        }
293        if data[0] != SAVE_STATE_VERSION {
294            return Err(MapperError::UnsupportedVersion(data[0]));
295        }
296        self.reg_lo = data[1];
297        self.reg_hi = data[2];
298        self.mode = data[3];
299        self.feedback = data[4] != 0;
300        self.a13 = data[5] != 0;
301        self.a9_latch = data[6] != 0;
302        self.chr_ram.copy_from_slice(&data[7..7 + CHR_RAM]);
303        self.wram.copy_from_slice(&data[7 + CHR_RAM..]);
304        Ok(())
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    fn board(banks_32k: usize) -> Nanjing163 {
313        let mut prg = vec![0u8; banks_32k * PRG_BANK_32K];
314        for b in 0..banks_32k {
315            prg[b * PRG_BANK_32K] = b as u8;
316        }
317        Nanjing163::new(prg.into_boxed_slice(), Mirroring::Vertical).unwrap()
318    }
319
320    #[test]
321    fn boots_in_bank_3_until_the_a_bit_is_set() {
322        let mut m = board(32);
323        assert_eq!(m.cpu_read(0x8000), 3);
324        m.cpu_write(0x5000, 0x04);
325        assert_eq!(m.cpu_read(0x8000), 0x07, "A clear: A15/A16 forced to 11");
326        m.cpu_write(0x5300, 0x04);
327        assert_eq!(m.cpu_read(0x8000), 0x04);
328        m.cpu_write(0x5200, 0x01);
329        assert_eq!(m.cpu_read(0x8000), 0x14, "$5200 bits are A19-A20");
330        m.reset();
331        assert_eq!(m.cpu_read(0x8000), 3);
332    }
333
334    #[test]
335    fn b_bit_swaps_d0_d1_on_5000_to_5200_but_not_5300() {
336        let mut m = board(64); // 2 MiB: the `$5200` swap applies.
337        m.cpu_write(0x5300, 0x05); // A and B set; its own D0 is not swapped.
338        m.cpu_write(0x5000, 0x01);
339        assert_eq!(m.cpu_read(0x8000), 0x02);
340        m.cpu_write(0x5200, 0x02);
341        assert_eq!(m.cpu_read(0x8000), 0x12);
342    }
343
344    #[test]
345    fn one_mib_boards_do_not_swap_5200() {
346        let mut m = board(32); // 1 MiB.
347        m.cpu_write(0x5300, 0x05);
348        m.cpu_write(0x5200, 0x01);
349        assert_eq!(m.cpu_read(0x8000) >> 4, 1);
350    }
351
352    #[test]
353    fn feedback_is_read_back_inverted_and_flipped_by_5101() {
354        let mut m = board(4);
355        assert_eq!(m.cpu_read(0x5500), 0x04, "F=0 reads back as 1");
356        assert_eq!(m.cpu_read_driven_mask(0x5500), 0x04);
357        assert!(!m.cpu_read_unmapped(0x5501));
358        m.cpu_write(0x5100, 0x04);
359        assert_eq!(m.cpu_read(0x5500), 0x00);
360        m.cpu_write(0x5101, 0x01);
361        assert_eq!(m.cpu_read(0x5500), 0x04, "E=1 at $5101 flips F");
362        m.cpu_write(0x5101, 0x04);
363        assert_eq!(m.cpu_read(0x5500), 0x04, "E=0: F kept, D2 ignored");
364    }
365
366    /// With auto-switch on, a nametable fetch in the top half selects the
367    /// left pattern table and one in the bottom half the right, and the
368    /// attribute fetch (A9 set) that follows does not re-latch.
369    #[test]
370    fn auto_switch_latches_a9_on_the_a13_rise() {
371        let mut m = board(4);
372        m.chr_ram[0x0010] = 0x11;
373        m.chr_ram[0x1010] = 0x22;
374        m.cpu_write(0x5000, 0x80);
375        let _ = m.ppu_read(0x0000);
376        let _ = m.nametable_fetch(0x2020); // top half: A9 = 0
377        let _ = m.nametable_fetch(0x23C1); // attribute: no rise
378        assert_eq!(m.ppu_read(0x1010), 0x11, "left table despite PPU A12=1");
379        let _ = m.nametable_fetch(0x2220); // after a pattern access: rises
380        assert_eq!(m.ppu_read(0x0010), 0x22, "right table despite PPU A12=0");
381        let _ = m.nametable_fetch(0x2020); // rises: latches 0
382        let _ = m.nametable_fetch(0x2220); // A13 already high: no re-latch
383        assert_eq!(m.ppu_read(0x0010), 0x11, "no rise, no re-latch");
384        m.cpu_write(0x5000, 0x00);
385        assert_eq!(m.ppu_read(0x0010), 0x11, "auto-switch off: PPU A12");
386    }
387
388    #[test]
389    fn wram_and_state_round_trip() {
390        let mut a = board(8);
391        a.cpu_write(0x6005, 0x99);
392        a.cpu_write(0x5300, 0x04);
393        a.cpu_write(0x5000, 0x86);
394        a.ppu_write(0x0123, 0x77);
395        let blob = a.save_state();
396        let mut b = board(8);
397        b.load_state(&blob).unwrap();
398        assert_eq!(b.cpu_read(0x6005), 0x99);
399        assert_eq!(b.cpu_read(0x8000), a.cpu_read(0x8000));
400        assert_eq!(b.save_state(), blob);
401    }
402}