Skip to main content

rustynes_mappers/
m210_namco175.rs

1//! Namco 175 / 340 (iNES mapper 210) implementation.
2//!
3//! Both are cost-reduced Namco 163 board variants **without** the expansion
4//! audio, ASIC-internal RAM, ROM nametables, or (importantly) the IRQ counter
5//! (`nesdev_wiki/INES_Mapper_210.xhtml`). They differ only in mirroring and
6//! PRG-RAM:
7//!
8//! | Function   | N163 | N175 (submapper 1) | N340 (submapper 2)        |
9//! |------------|------|--------------------|---------------------------|
10//! | IRQ        | yes  | no                 | no                        |
11//! | WRAM       | opt  | optional, enable-gated | none                  |
12//! | Mirroring  | ext  | hardwired H/V      | selectable H/V/1scA/1scB  |
13//!
14//! NOTE: contrary to a common assumption, the **340 has no IRQ** — only the
15//! full Namco 163 does. This implementation therefore never asserts an IRQ for
16//! either submapper.
17//!
18//! # Banking
19//!
20//! - `$6000-$7FFF`: 8 KiB PRG-RAM (Namco 175 only; gated by `$C000` bit 0).
21//! - `$8000-$9FFF` / `$A000-$BFFF` / `$C000-$DFFF`: three switchable 8 KiB PRG
22//!   banks (`$E000`/`$E800`/`$F000` low bits).
23//! - `$E000-$FFFF`: fixed to the last 8 KiB bank.
24//! - PPU `$0000-$1FFF`: eight switchable 1 KiB CHR banks (`$8000-$BFFF`,
25//!   one register per `$0800` window).
26//!
27//! On the Namco 340, the upper two bits of `$E000` select mirroring
28//! (0=1scA, 1=Vertical, 2=1scB, 3=Horizontal). The Namco 175 ignores those
29//! bits and uses the hardwired iNES-header mirroring.
30
31#![allow(
32    clippy::cast_possible_truncation,
33    clippy::cast_lossless,
34    clippy::missing_const_for_fn,
35    clippy::doc_markdown
36)]
37
38use crate::cartridge::Mirroring;
39use crate::mapper::{Mapper, MapperCaps, MapperError};
40use alloc::{boxed::Box, vec::Vec};
41use alloc::{format, vec};
42
43const PRG_BANK_8K: usize = 0x2000;
44const CHR_BANK_1K: usize = 0x0400;
45const NAMETABLE_SIZE: usize = 0x0400;
46const NAMETABLE_SIZE_U16: u16 = 0x0400;
47
48const SAVE_STATE_VERSION: u8 = 1;
49
50/// Board variant for mapper 210.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum Namco175Board {
53    /// Namco 175 (submapper 1): hardwired H/V mirroring, optional enable-gated
54    /// PRG-RAM at `$6000-$7FFF`.
55    N175,
56    /// Namco 340 (submapper 2): selectable H/V/1scA/1scB mirroring via the
57    /// upper bits of `$E000`; no PRG-RAM.
58    N340,
59}
60
61/// Namco 175 / 340 mapper (iNES mapper 210).
62pub struct Namco175 {
63    prg_rom: Box<[u8]>,
64    chr_rom: Box<[u8]>,
65    prg_ram: Box<[u8]>,
66    vram: Box<[u8]>,
67    chr_is_ram: bool,
68
69    prg: [u8; 3],      // switchable banks @ $8000/$A000/$C000
70    chr_regs: [u8; 8], // eight 1 KiB CHR bank selects
71    prg_ram_enabled: bool,
72    mirroring: Mirroring,
73    board: Namco175Board,
74}
75
76impl Namco175 {
77    /// Construct a new Namco 175/340 mapper.
78    ///
79    /// `prg_rom` must be a non-zero multiple of 8 KiB; CHR-ROM (when present)
80    /// must be a multiple of 1 KiB. CHR-RAM (8 KiB) is allocated when no
81    /// CHR-ROM is supplied.
82    ///
83    /// # Errors
84    ///
85    /// Returns [`MapperError::Invalid`] on size mismatch.
86    pub fn new(
87        prg_rom: Box<[u8]>,
88        chr_rom: Box<[u8]>,
89        mirroring: Mirroring,
90        board: Namco175Board,
91    ) -> Result<Self, MapperError> {
92        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
93            return Err(MapperError::Invalid(format!(
94                "Namco-175/340 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
95                prg_rom.len()
96            )));
97        }
98        let chr_is_ram = chr_rom.is_empty();
99        let chr_data: Box<[u8]> = if chr_is_ram {
100            vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
101        } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
102            chr_rom
103        } else {
104            return Err(MapperError::Invalid(format!(
105                "Namco-175/340 CHR-ROM size {} is not a multiple of 1 KiB",
106                chr_rom.len()
107            )));
108        };
109        Ok(Self {
110            prg_rom,
111            chr_rom: chr_data,
112            prg_ram: vec![0u8; 8 * 1024].into_boxed_slice(),
113            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
114            chr_is_ram,
115            prg: [0; 3],
116            chr_regs: [0; 8],
117            prg_ram_enabled: false,
118            mirroring,
119            board,
120        })
121    }
122
123    fn prg_offset(&self, addr: u16) -> usize {
124        let total = (self.prg_rom.len() / PRG_BANK_8K).max(1);
125        let last = total - 1;
126        let bank = match addr & 0xE000 {
127            0x8000 => (self.prg[0] as usize) % total,
128            0xA000 => (self.prg[1] as usize) % total,
129            0xC000 => (self.prg[2] as usize) % total,
130            0xE000 => last,
131            _ => 0,
132        };
133        bank * PRG_BANK_8K + (addr as usize & 0x1FFF)
134    }
135
136    fn chr_offset(&self, addr: u16) -> usize {
137        let addr = (addr & 0x1FFF) as usize;
138        let total_1k = (self.chr_rom.len() / CHR_BANK_1K).max(1);
139        let slot = addr / CHR_BANK_1K;
140        let bank = (self.chr_regs[slot] as usize) % total_1k;
141        bank * CHR_BANK_1K + (addr & (CHR_BANK_1K - 1))
142    }
143
144    fn nametable_offset(&self, addr: u16) -> usize {
145        let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
146        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
147        let physical = self.mirroring.physical_bank(table);
148        physical * NAMETABLE_SIZE + local
149    }
150}
151
152impl Mapper for Namco175 {
153    fn sram(&self) -> &[u8] {
154        &self.prg_ram
155    }
156    fn sram_mut(&mut self) -> &mut [u8] {
157        &mut self.prg_ram
158    }
159    // v2.8.0 Phase 4 — no per-cycle hooks (no IRQ, no audio): the bus
160    // skips all four per-CPU-cycle dispatches for this board.
161    fn caps(&self) -> MapperCaps {
162        MapperCaps::NONE
163    }
164
165    fn cpu_read(&mut self, addr: u16) -> u8 {
166        match addr {
167            0x6000..=0x7FFF => {
168                if matches!(self.board, Namco175Board::N175) && self.prg_ram_enabled {
169                    self.prg_ram[(addr - 0x6000) as usize % self.prg_ram.len()]
170                } else {
171                    0
172                }
173            }
174            0x8000..=0xFFFF => {
175                let off = self.prg_offset(addr);
176                self.prg_rom[off % self.prg_rom.len()]
177            }
178            _ => 0,
179        }
180    }
181
182    fn cpu_write(&mut self, addr: u16, value: u8) {
183        match addr {
184            0x6000..=0x7FFF => {
185                if matches!(self.board, Namco175Board::N175) && self.prg_ram_enabled {
186                    let off = (addr - 0x6000) as usize % self.prg_ram.len();
187                    self.prg_ram[off] = value;
188                }
189            }
190            // CHR select: one register per $0800 window across $8000-$BFFF.
191            0x8000..=0xBFFF => {
192                let slot = ((addr - 0x8000) >> 11) as usize;
193                if slot < 8 {
194                    self.chr_regs[slot] = value;
195                }
196            }
197            // External PRG RAM enable (Namco 175 only).
198            0xC000..=0xC7FF => {
199                if matches!(self.board, Namco175Board::N175) {
200                    self.prg_ram_enabled = (value & 0x01) != 0;
201                }
202            }
203            // PRG select 1 (+ Namco 340 mirroring in the upper two bits).
204            0xE000..=0xE7FF => {
205                self.prg[0] = value & 0x3F;
206                if matches!(self.board, Namco175Board::N340) {
207                    self.mirroring = match (value >> 6) & 0x03 {
208                        0 => Mirroring::SingleScreenA,
209                        1 => Mirroring::Vertical,
210                        2 => Mirroring::SingleScreenB,
211                        _ => Mirroring::Horizontal,
212                    };
213                }
214            }
215            // PRG select 2.
216            0xE800..=0xEFFF => self.prg[1] = value & 0x3F,
217            // PRG select 3.
218            0xF000..=0xF7FF => self.prg[2] = value & 0x3F,
219            _ => {}
220        }
221    }
222
223    fn ppu_read(&mut self, addr: u16) -> u8 {
224        let addr = addr & 0x3FFF;
225        match addr {
226            0x0000..=0x1FFF => {
227                let off = self.chr_offset(addr);
228                self.chr_rom[off % self.chr_rom.len()]
229            }
230            0x2000..=0x3EFF => self.vram[self.nametable_offset(addr) % self.vram.len()],
231            _ => 0,
232        }
233    }
234
235    fn ppu_write(&mut self, addr: u16, value: u8) {
236        let addr = addr & 0x3FFF;
237        match addr {
238            0x0000..=0x1FFF => {
239                if self.chr_is_ram {
240                    let off = self.chr_offset(addr);
241                    let len = self.chr_rom.len();
242                    self.chr_rom[off % len] = value;
243                }
244            }
245            0x2000..=0x3EFF => {
246                let off = self.nametable_offset(addr) % self.vram.len();
247                self.vram[off] = value;
248            }
249            _ => {}
250        }
251    }
252
253    fn nametable_address(&self, addr: u16) -> u16 {
254        let off = self.nametable_offset(addr);
255        u16::try_from(off & 0x07FF).unwrap_or(0)
256    }
257
258    fn current_mirroring(&self) -> Mirroring {
259        self.mirroring
260    }
261
262    fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
263        let mut info = crate::mapper::MapperDebugInfo {
264            mapper_id: 210,
265            name: match self.board {
266                Namco175Board::N175 => "Namco 175 (210)".into(),
267                Namco175Board::N340 => "Namco 340 (210)".into(),
268            },
269            mirroring: crate::mapper::mirroring_name(self.mirroring),
270            ..Default::default()
271        };
272        for (i, b) in self.prg.iter().enumerate() {
273            info.prg_banks
274                .push((format!("PRG{i}"), format!("{b:#04x}")));
275        }
276        for (i, b) in self.chr_regs.iter().enumerate() {
277            info.chr_banks.push((format!("C{i}"), format!("{b:#04x}")));
278        }
279        info
280    }
281
282    fn save_state(&self) -> Vec<u8> {
283        let mut out = Vec::with_capacity(
284            16 + self.prg_ram.len()
285                + self.vram.len()
286                + if self.chr_is_ram {
287                    self.chr_rom.len()
288                } else {
289                    0
290                },
291        );
292        out.push(SAVE_STATE_VERSION);
293        out.extend_from_slice(&self.prg);
294        out.extend_from_slice(&self.chr_regs);
295        out.push(u8::from(self.prg_ram_enabled));
296        out.push(self.mirroring as u8);
297        out.push(match self.board {
298            Namco175Board::N175 => 0,
299            Namco175Board::N340 => 1,
300        });
301        out.extend_from_slice(&self.prg_ram);
302        out.extend_from_slice(&self.vram);
303        if self.chr_is_ram {
304            out.extend_from_slice(&self.chr_rom);
305        }
306        out
307    }
308
309    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
310        let chr_part = if self.chr_is_ram {
311            self.chr_rom.len()
312        } else {
313            0
314        };
315        // 1 + 3 + 8 + 1 + 1 + 1
316        let scalar_len = 1 + 3 + 8 + 1 + 1 + 1;
317        let expected = scalar_len + self.prg_ram.len() + self.vram.len() + chr_part;
318        if data.len() != expected {
319            return Err(MapperError::WrongLength {
320                expected,
321                got: data.len(),
322            });
323        }
324        if data[0] != SAVE_STATE_VERSION {
325            return Err(MapperError::UnsupportedVersion(data[0]));
326        }
327        let mut c = 1usize;
328        self.prg.copy_from_slice(&data[c..c + 3]);
329        c += 3;
330        self.chr_regs.copy_from_slice(&data[c..c + 8]);
331        c += 8;
332        self.prg_ram_enabled = data[c] != 0;
333        c += 1;
334        self.mirroring = match data[c] {
335            0 => Mirroring::Horizontal,
336            1 => Mirroring::Vertical,
337            2 => Mirroring::SingleScreenA,
338            3 => Mirroring::SingleScreenB,
339            4 => Mirroring::FourScreen,
340            5 => Mirroring::MapperControlled,
341            other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
342        };
343        c += 1;
344        self.board = match data[c] {
345            0 => Namco175Board::N175,
346            1 => Namco175Board::N340,
347            other => return Err(MapperError::Invalid(format!("board {other}"))),
348        };
349        c += 1;
350        self.prg_ram
351            .copy_from_slice(&data[c..c + self.prg_ram.len()]);
352        c += self.prg_ram.len();
353        self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
354        c += self.vram.len();
355        if self.chr_is_ram {
356            self.chr_rom
357                .copy_from_slice(&data[c..c + self.chr_rom.len()]);
358        }
359        Ok(())
360    }
361}
362
363#[cfg(test)]
364#[allow(clippy::cast_possible_truncation)]
365mod tests {
366    use super::*;
367
368    fn synth_prg(banks_8k: usize) -> Box<[u8]> {
369        let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
370        for b in 0..banks_8k {
371            v[b * PRG_BANK_8K] = b as u8;
372        }
373        v.into_boxed_slice()
374    }
375
376    fn synth_chr(banks_1k: usize) -> Box<[u8]> {
377        let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
378        for b in 0..banks_1k {
379            v[b * CHR_BANK_1K] = b as u8;
380        }
381        v.into_boxed_slice()
382    }
383
384    fn fresh175() -> Namco175 {
385        Namco175::new(
386            synth_prg(16),
387            synth_chr(64),
388            Mirroring::Vertical,
389            Namco175Board::N175,
390        )
391        .unwrap()
392    }
393
394    fn fresh340() -> Namco175 {
395        Namco175::new(
396            synth_prg(16),
397            synth_chr(64),
398            Mirroring::Vertical,
399            Namco175Board::N340,
400        )
401        .unwrap()
402    }
403
404    #[test]
405    fn prg_three_switchable_plus_fixed() {
406        let mut m = fresh175();
407        assert_eq!(m.cpu_read(0xE000), 15); // last bank fixed
408        m.cpu_write(0xE000, 3); // PRG0 = 3
409        m.cpu_write(0xE800, 5); // PRG1 = 5
410        m.cpu_write(0xF000, 9); // PRG2 = 9
411        assert_eq!(m.cpu_read(0x8000), 3);
412        assert_eq!(m.cpu_read(0xA000), 5);
413        assert_eq!(m.cpu_read(0xC000), 9);
414        assert_eq!(m.cpu_read(0xE000), 15);
415    }
416
417    #[test]
418    fn chr_eight_1k_banks() {
419        let mut m = fresh175();
420        m.cpu_write(0x8000, 10); // C0
421        m.cpu_write(0xA000, 20); // C4 @ $1000
422        assert_eq!(m.ppu_read(0x0000), 10);
423        assert_eq!(m.ppu_read(0x1000), 20);
424    }
425
426    #[test]
427    fn n175_no_irq() {
428        let mut m = fresh175();
429        for _ in 0..2000 {
430            m.notify_cpu_cycle();
431        }
432        assert!(!m.irq_pending());
433    }
434
435    #[test]
436    fn n340_no_irq() {
437        let mut m = fresh340();
438        for _ in 0..2000 {
439            m.notify_cpu_cycle();
440        }
441        assert!(!m.irq_pending());
442    }
443
444    #[test]
445    fn n175_prg_ram_enable_gated() {
446        let mut m = fresh175();
447        // Disabled -> writes ignored.
448        m.cpu_write(0x6000, 0xAB);
449        assert_eq!(m.cpu_read(0x6000), 0);
450        m.cpu_write(0xC000, 0x01); // enable
451        m.cpu_write(0x6000, 0xCD);
452        assert_eq!(m.cpu_read(0x6000), 0xCD);
453    }
454
455    #[test]
456    fn n175_mirroring_hardwired_ignores_e000_upper_bits() {
457        let mut m = fresh175();
458        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
459        // Upper bits of $E000 must NOT change mirroring on the 175.
460        m.cpu_write(0xE000, 0xC0);
461        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
462    }
463
464    #[test]
465    fn n340_mirroring_select() {
466        let mut m = fresh340();
467        m.cpu_write(0xE000, 0x00); // %00 -> 1scA
468        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
469        m.cpu_write(0xE000, 0x40); // %01 -> Vertical
470        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
471        m.cpu_write(0xE000, 0x80); // %10 -> 1scB
472        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
473        m.cpu_write(0xE000, 0xC0); // %11 -> Horizontal
474        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
475    }
476
477    #[test]
478    fn save_state_round_trip() {
479        let mut m = fresh340();
480        m.cpu_write(0xE000, 0x40 | 3);
481        m.cpu_write(0x9000, 7);
482        m.ppu_write(0x2000, 0x33);
483        let blob = m.save_state();
484        let mut m2 = fresh340();
485        m2.load_state(&blob).unwrap();
486        assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
487        assert_eq!(m.ppu_read(0x0800), m2.ppu_read(0x0800));
488        assert_eq!(m.current_mirroring(), m2.current_mirroring());
489    }
490}