Skip to main content

rustynes_mappers/
m088_namco118.rs

1//! Namco 118 / DxROM (iNES mappers 206 and 88) implementation.
2//!
3//! Mapper 206 is the simpler predecessor of the MMC3: a `$8000` bank-select
4//! + `$8001` bank-data register pair drives six bank registers, but there is
5//! **no IRQ, no A12 clocking, no PRG-RAM, and no runtime mirroring control**
6//! (the nametable arrangement is hardwired from the iNES header).
7//!
8//! | Reg | Purpose                                         |
9//! |-----|-------------------------------------------------|
10//! | R0  | 2 KiB CHR bank @ PPU `$0000-$07FF`              |
11//! | R1  | 2 KiB CHR bank @ PPU `$0800-$0FFF`              |
12//! | R2  | 1 KiB CHR bank @ PPU `$1000-$13FF`              |
13//! | R3  | 1 KiB CHR bank @ PPU `$1400-$17FF`              |
14//! | R4  | 1 KiB CHR bank @ PPU `$1800-$1BFF`              |
15//! | R5  | 1 KiB CHR bank @ PPU `$1C00-$1FFF`              |
16//! | R6  | 8 KiB PRG bank @ CPU `$8000-$9FFF`              |
17//! | R7  | 8 KiB PRG bank @ CPU `$A000-$BFFF`              |
18//!
19//! The CHR layout is fixed (the left pattern table always gets the two 2 KiB
20//! banks, the right table the four 1 KiB banks — no MMC3-style CHR mode bit),
21//! and the last two 8 KiB PRG banks are always fixed at `$C000`/`$E000`.
22//!
23//! Mapper 206 limits PRG to 128 KiB and CHR to 64 KiB (bank-register width:
24//! only bits 0-3 exist for the 8 KiB PRG regs and bits 1-5 / 0-5 for the CHR
25//! regs). We mask the register values to the ROM size at use time, which is
26//! equivalent in practice.
27//!
28//! Mapper 88 is identical to 206 except CHR is increased to 128 KiB by wiring
29//! PPU A12 to CHR A16: tiles in `$0xxx` come from the first 64 KiB and tiles
30//! in `$1xxx` from the second 64 KiB. We model this with the canonical
31//! implementation note from the wiki: mask the 1 KiB bank index to `$3F` and
32//! OR `$40` for the four 1 KiB registers (R2-R5). An undersize ROM on a
33//! mapper-88 board behaves identically to mapper 206.
34//!
35//! See `docs/mappers.md`, `nesdev_wiki/INES_Mapper_206.xhtml`, and
36//! `nesdev_wiki/INES_Mapper_088.xhtml`.
37
38#![allow(
39    clippy::cast_possible_truncation,
40    clippy::cast_lossless,
41    clippy::missing_const_for_fn,
42    clippy::doc_markdown,
43    clippy::doc_lazy_continuation
44)]
45
46use crate::cartridge::Mirroring;
47use crate::mapper::{Mapper, MapperCaps, MapperError};
48use alloc::{boxed::Box, vec::Vec};
49use alloc::{format, vec};
50
51const PRG_BANK_8K: usize = 0x2000;
52const CHR_BANK_1K: usize = 0x0400;
53const NAMETABLE_SIZE: usize = 0x0400;
54const NAMETABLE_SIZE_U16: u16 = 0x0400;
55
56// v2 appends the mirroring byte. Mapper 154 makes mirroring MUTABLE state (the
57// $8000-$FFFF bit-6 nametable select), so a v1 blob -- which never carried it --
58// would silently restore the wrong CIRAM page. 88/206 keep a constant mirroring
59// and are unaffected in behaviour, but share the layout. v1 is refused since
60// v2.9.8 (ADR 0042).
61const SAVE_STATE_VERSION: u8 = 2;
62
63/// Board variant for the Namco 118 family.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum Namco118Board {
66    /// Mapper 206 / DxROM base: 64 KiB CHR maximum, plain bank registers.
67    Dxrom,
68    /// Mapper 88: PPU A12 wired to CHR A16, splitting CHR into two disjoint
69    /// 64 KiB halves between the left and right pattern tables.
70    M88,
71    /// Mapper 154 / NAMCOT-3453: mapper 88 plus one-screen nametable control.
72    ///
73    /// Identical to [`Namco118Board::M88`] -- including the A12/A16 CHR split --
74    /// with a single added bit that selects between the two CIRAM pages. The
75    /// board exists for exactly one game, *Devil Man*.
76    M154,
77}
78
79/// Namco 118 / DxROM mapper (iNES 206 + 88).
80pub struct Namco118 {
81    prg_rom: Box<[u8]>,
82    chr: Box<[u8]>,
83    vram: Box<[u8]>,
84    chr_is_ram: bool,
85
86    // R0..R5 (CHR) and R6/R7 (PRG) bank registers.
87    regs: [u8; 8],
88    bank_select: u8,
89
90    mirroring: Mirroring,
91    fixed_4screen: bool,
92    board: Namco118Board,
93}
94
95impl Namco118 {
96    /// Construct a new Namco 118 / DxROM mapper.
97    ///
98    /// `prg_rom` must be a non-zero multiple of 8 KiB; CHR-ROM must be a
99    /// multiple of 1 KiB (CHR-RAM is allocated as 8 KiB when `chr_rom` is
100    /// empty).
101    ///
102    /// # Errors
103    ///
104    /// Returns [`MapperError::Invalid`] on size mismatch.
105    pub fn new(
106        prg_rom: Box<[u8]>,
107        chr_rom: Box<[u8]>,
108        mirroring: Mirroring,
109        board: Namco118Board,
110    ) -> Result<Self, MapperError> {
111        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
112            return Err(MapperError::Invalid(format!(
113                "Namco-118 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
114                prg_rom.len()
115            )));
116        }
117        let chr_is_ram = chr_rom.is_empty();
118        let chr: Box<[u8]> = if chr_is_ram {
119            vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
120        } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
121            chr_rom
122        } else {
123            return Err(MapperError::Invalid(format!(
124                "Namco-118 CHR-ROM size {} is not a multiple of 1 KiB",
125                chr_rom.len()
126            )));
127        };
128        let fixed_4screen = matches!(mirroring, Mirroring::FourScreen);
129        let vram_size = if fixed_4screen {
130            4 * NAMETABLE_SIZE
131        } else {
132            2 * NAMETABLE_SIZE
133        };
134        Ok(Self {
135            prg_rom,
136            chr,
137            vram: vec![0u8; vram_size].into_boxed_slice(),
138            chr_is_ram,
139            regs: [0; 8],
140            bank_select: 0,
141            mirroring,
142            fixed_4screen,
143            board,
144        })
145    }
146
147    /// PRG: R6 @ $8000, R7 @ $A000, then the last two 8 KiB banks fixed.
148    fn prg_offset(&self, addr: u16) -> usize {
149        let total = (self.prg_rom.len() / PRG_BANK_8K).max(1);
150        let last = total - 1;
151        let second_last = total.saturating_sub(2);
152        let bank = match addr & 0xE000 {
153            0x8000 => (self.regs[6] as usize) % total,
154            0xA000 => (self.regs[7] as usize) % total,
155            0xC000 => second_last,
156            0xE000 => last,
157            _ => 0,
158        };
159        bank * PRG_BANK_8K + (addr as usize & 0x1FFF)
160    }
161
162    /// CHR: fixed layout (no mode bit). R0/R1 supply the two 2 KiB banks
163    /// covering `$0000-$0FFF`; R2-R5 supply the four 1 KiB banks covering
164    /// `$1000-$1FFF`. On mapper 88 the four 1 KiB regs are OR'd with `$40`
165    /// (after masking to `$3F`) so they index the second CHR half.
166    fn chr_offset(&self, addr: u16) -> usize {
167        let addr = (addr & 0x1FFF) as usize;
168        let total_1k = (self.chr.len() / CHR_BANK_1K).max(1);
169        let slot = addr / CHR_BANK_1K;
170        let bank_1k: usize = match slot {
171            // R0 = 2 KiB @ $0000 (low bit forced even by hardware).
172            0 => (self.regs[0] as usize) & !1,
173            1 => ((self.regs[0] as usize) & !1) | 1,
174            // R1 = 2 KiB @ $0800.
175            2 => (self.regs[1] as usize) & !1,
176            3 => ((self.regs[1] as usize) & !1) | 1,
177            // R2-R5 = 1 KiB each @ $1000-$1FFF.
178            4 => self.m88_high(self.regs[2] as usize),
179            5 => self.m88_high(self.regs[3] as usize),
180            6 => self.m88_high(self.regs[4] as usize),
181            7 => self.m88_high(self.regs[5] as usize),
182            _ => 0,
183        };
184        let bank = bank_1k % total_1k;
185        bank * CHR_BANK_1K + (addr & (CHR_BANK_1K - 1))
186    }
187
188    /// For mapper 88, the right-pattern-table 1 KiB registers select from the
189    /// second 64 KiB CHR half (`bank & $3F | $40`). For DxROM, identity.
190    fn m88_high(&self, reg: usize) -> usize {
191        match self.board {
192            // 154 is 88 in every respect but the nametable bit, including this.
193            Namco118Board::M88 | Namco118Board::M154 => (reg & 0x3F) | 0x40,
194            Namco118Board::Dxrom => reg,
195        }
196    }
197
198    fn nametable_offset(&self, addr: u16) -> usize {
199        let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
200        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
201        if self.fixed_4screen {
202            (table as usize) * NAMETABLE_SIZE + local
203        } else {
204            let physical = self.mirroring.physical_bank(table);
205            physical * NAMETABLE_SIZE + local
206        }
207    }
208}
209
210impl Mapper for Namco118 {
211    // v2.8.0 Phase 4 — no per-cycle hooks (no IRQ, no audio): the bus
212    // skips all four per-CPU-cycle dispatches for this board.
213    fn caps(&self) -> MapperCaps {
214        MapperCaps::NONE
215    }
216
217    fn cpu_read(&mut self, addr: u16) -> u8 {
218        match addr {
219            0x8000..=0xFFFF => {
220                let off = self.prg_offset(addr);
221                self.prg_rom[off % self.prg_rom.len()]
222            }
223            _ => 0,
224        }
225    }
226
227    fn cpu_write(&mut self, addr: u16, value: u8) {
228        if let 0x8000..=0xFFFF = addr {
229            // Mapper 154 (NAMCOT-3453) carries a one-screen nametable select in
230            // bit 6, and the wiki is explicit that it is decoded across the WHOLE
231            // $8000-$FFFF range -- unlike the bank-select register it shares a
232            // byte with, which the associated Namco 108 only decodes at
233            // $8000-$9FFF. So this is deliberately outside the even/odd split
234            // below and sees every write, including bank-data writes to odd
235            // addresses and writes above $9FFF.
236            if self.board == Namco118Board::M154 {
237                self.mirroring = if value & 0x40 == 0 {
238                    Mirroring::SingleScreenA
239                } else {
240                    Mirroring::SingleScreenB
241                };
242            }
243            // Register mask $E001: even = bank-select, odd = bank-data.
244            // There are no control registers in $A000-$FFFF (unlike MMC3).
245            if addr & 1 == 0 {
246                self.bank_select = value & 0x07;
247            } else {
248                self.regs[(self.bank_select & 0x07) as usize] = value;
249            }
250        }
251    }
252
253    fn ppu_read(&mut self, addr: u16) -> u8 {
254        let addr = addr & 0x3FFF;
255        match addr {
256            0x0000..=0x1FFF => {
257                let off = self.chr_offset(addr);
258                self.chr[off % self.chr.len()]
259            }
260            0x2000..=0x3EFF => self.vram[self.nametable_offset(addr) % self.vram.len()],
261            _ => 0,
262        }
263    }
264
265    fn ppu_write(&mut self, addr: u16, value: u8) {
266        let addr = addr & 0x3FFF;
267        match addr {
268            0x0000..=0x1FFF => {
269                if self.chr_is_ram {
270                    let off = self.chr_offset(addr);
271                    let len = self.chr.len();
272                    self.chr[off % len] = value;
273                }
274            }
275            0x2000..=0x3EFF => {
276                let off = self.nametable_offset(addr) % self.vram.len();
277                self.vram[off] = value;
278            }
279            _ => {}
280        }
281    }
282
283    fn nametable_address(&self, addr: u16) -> u16 {
284        let off = self.nametable_offset(addr);
285        u16::try_from(off & 0x07FF).unwrap_or(0)
286    }
287
288    fn current_mirroring(&self) -> Mirroring {
289        self.mirroring
290    }
291
292    fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
293        let id = match self.board {
294            Namco118Board::M88 => 88,
295            Namco118Board::M154 => 154,
296            Namco118Board::Dxrom => 206,
297        };
298        let mut info = crate::mapper::MapperDebugInfo {
299            mapper_id: id,
300            name: match self.board {
301                Namco118Board::M88 => "Namco 118 (88)".into(),
302                Namco118Board::M154 => "NAMCOT-3453 (154)".into(),
303                Namco118Board::Dxrom => "DxROM / Namco 118 (206)".into(),
304            },
305            mirroring: crate::mapper::mirroring_name(self.mirroring),
306            ..Default::default()
307        };
308        info.prg_banks
309            .push(("R6".into(), format!("{:#04x}", self.regs[6])));
310        info.prg_banks
311            .push(("R7".into(), format!("{:#04x}", self.regs[7])));
312        for i in 0..6 {
313            info.chr_banks
314                .push((format!("R{i}"), format!("{:#04x}", self.regs[i])));
315        }
316        info
317    }
318
319    fn save_state(&self) -> Vec<u8> {
320        let mut out = Vec::with_capacity(
321            11 + self.vram.len() + if self.chr_is_ram { self.chr.len() } else { 0 },
322        );
323        out.push(SAVE_STATE_VERSION);
324        out.extend_from_slice(&self.regs);
325        out.push(self.bank_select);
326        out.push(match self.mirroring {
327            Mirroring::Horizontal => 0,
328            Mirroring::Vertical => 1,
329            Mirroring::SingleScreenA => 2,
330            Mirroring::SingleScreenB => 3,
331            Mirroring::FourScreen => 4,
332            Mirroring::MapperControlled => 5,
333        });
334        out.extend_from_slice(&self.vram);
335        if self.chr_is_ram {
336            out.extend_from_slice(&self.chr);
337        }
338        out
339    }
340
341    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
342        let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
343        // Only the current version is read (v2.9.8, ADR 0042). A v1 blob, one
344        // byte shorter for want of the mirroring byte, used to load with the
345        // constructed mirroring kept. The version is checked before the
346        // length, so an old blob reports the version it is.
347        match data.first() {
348            None => {
349                return Err(MapperError::WrongLength {
350                    expected: 1,
351                    got: 0,
352                });
353            }
354            Some(&v) if v != SAVE_STATE_VERSION => {
355                return Err(MapperError::UnsupportedVersion(v));
356            }
357            Some(_) => {}
358        }
359        let expected = 11 + self.vram.len() + need_chr;
360        if data.len() != expected {
361            return Err(MapperError::WrongLength {
362                expected,
363                got: data.len(),
364            });
365        }
366        self.regs.copy_from_slice(&data[1..9]);
367        self.bank_select = data[9];
368        self.mirroring = match data[10] {
369            0 => Mirroring::Horizontal,
370            1 => Mirroring::Vertical,
371            2 => Mirroring::SingleScreenA,
372            3 => Mirroring::SingleScreenB,
373            4 => Mirroring::FourScreen,
374            5 => Mirroring::MapperControlled,
375            _ => self.mirroring,
376        };
377        let mut cursor = 11;
378        self.vram
379            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
380        cursor += self.vram.len();
381        if self.chr_is_ram {
382            self.chr
383                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
384        }
385        Ok(())
386    }
387}
388
389#[cfg(test)]
390#[allow(clippy::cast_possible_truncation)]
391mod tests {
392    use super::*;
393
394    fn synth_prg(banks_8k: usize) -> Box<[u8]> {
395        let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
396        for b in 0..banks_8k {
397            v[b * PRG_BANK_8K] = b as u8;
398        }
399        v.into_boxed_slice()
400    }
401
402    fn synth_chr(banks_1k: usize) -> Box<[u8]> {
403        let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
404        for b in 0..banks_1k {
405            v[b * CHR_BANK_1K] = b as u8;
406        }
407        v.into_boxed_slice()
408    }
409
410    #[test]
411    fn prg_last_two_banks_fixed() {
412        // 8 banks of 8 KiB = 64 KiB PRG.
413        let mut m = Namco118::new(
414            synth_prg(8),
415            synth_chr(64),
416            Mirroring::Vertical,
417            Namco118Board::Dxrom,
418        )
419        .unwrap();
420        assert_eq!(m.cpu_read(0xC000), 6); // second-to-last fixed
421        assert_eq!(m.cpu_read(0xE000), 7); // last fixed
422    }
423
424    #[test]
425    fn prg_bank_select_r6_r7() {
426        let mut m = Namco118::new(
427            synth_prg(8),
428            synth_chr(64),
429            Mirroring::Vertical,
430            Namco118Board::Dxrom,
431        )
432        .unwrap();
433        // Select R6 = 3.
434        m.cpu_write(0x8000, 6);
435        m.cpu_write(0x8001, 3);
436        assert_eq!(m.cpu_read(0x8000), 3);
437        // Select R7 = 5.
438        m.cpu_write(0x8000, 7);
439        m.cpu_write(0x8001, 5);
440        assert_eq!(m.cpu_read(0xA000), 5);
441        // Fixed banks unchanged.
442        assert_eq!(m.cpu_read(0xE000), 7);
443    }
444
445    #[test]
446    fn chr_fixed_layout_2k_then_1k() {
447        let mut m = Namco118::new(
448            synth_prg(8),
449            synth_chr(64),
450            Mirroring::Vertical,
451            Namco118Board::Dxrom,
452        )
453        .unwrap();
454        // R0 (2 KiB @ $0000): write 4 -> even-forced bank 4 at $0000.
455        m.cpu_write(0x8000, 0);
456        m.cpu_write(0x8001, 4);
457        assert_eq!(m.ppu_read(0x0000), 4);
458        assert_eq!(m.ppu_read(0x0400), 5); // 2 KiB block, second 1 KiB
459        // R2 (1 KiB @ $1000): write 9.
460        m.cpu_write(0x8000, 2);
461        m.cpu_write(0x8001, 9);
462        assert_eq!(m.ppu_read(0x1000), 9);
463    }
464
465    #[test]
466    fn m88_right_table_uses_second_chr_half() {
467        // 128 KiB CHR = 128 1 KiB banks.
468        let mut m = Namco118::new(
469            synth_prg(8),
470            synth_chr(128),
471            Mirroring::Vertical,
472            Namco118Board::M88,
473        )
474        .unwrap();
475        // R2 (1 KiB @ $1000): write 5 -> (5 & 0x3F) | 0x40 = 0x45 = 69.
476        m.cpu_write(0x8000, 2);
477        m.cpu_write(0x8001, 5);
478        assert_eq!(m.ppu_read(0x1000), 69);
479        // R0 (left table) is unaffected by the A16 wiring.
480        m.cpu_write(0x8000, 0);
481        m.cpu_write(0x8001, 2);
482        assert_eq!(m.ppu_read(0x0000), 2);
483    }
484
485    #[test]
486    fn no_irq() {
487        let mut m = Namco118::new(
488            synth_prg(8),
489            synth_chr(64),
490            Mirroring::Vertical,
491            Namco118Board::Dxrom,
492        )
493        .unwrap();
494        // A12 / cpu-cycle notifications must never raise an IRQ.
495        for _ in 0..1000 {
496            m.notify_a12(true);
497            m.notify_a12(false);
498            m.notify_cpu_cycle();
499        }
500        assert!(!m.irq_pending());
501    }
502
503    #[test]
504    fn save_state_round_trip() {
505        let mut m = Namco118::new(
506            synth_prg(8),
507            synth_chr(64),
508            Mirroring::Horizontal,
509            Namco118Board::Dxrom,
510        )
511        .unwrap();
512        m.cpu_write(0x8000, 6);
513        m.cpu_write(0x8001, 2);
514        m.cpu_write(0x8000, 0);
515        m.cpu_write(0x8001, 8);
516        let blob = m.save_state();
517        let mut m2 = Namco118::new(
518            synth_prg(8),
519            synth_chr(64),
520            Mirroring::Horizontal,
521            Namco118Board::Dxrom,
522        )
523        .unwrap();
524        m2.load_state(&blob).unwrap();
525        assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
526        assert_eq!(m.ppu_read(0x0000), m2.ppu_read(0x0000));
527    }
528
529    // ---- mapper 154 (NAMCOT-3453) ------------------------------------------
530
531    fn m154() -> Namco118 {
532        Namco118::new(
533            synth_prg(8),
534            synth_chr(128),
535            Mirroring::Vertical,
536            Namco118Board::M154,
537        )
538        .unwrap()
539    }
540
541    #[test]
542    fn m154_bit6_selects_the_one_screen_page() {
543        let mut m = m154();
544        m.cpu_write(0x8000, 0x00);
545        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
546        m.cpu_write(0x8000, 0x40);
547        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
548    }
549
550    #[test]
551    fn m154_decodes_the_nametable_bit_across_the_whole_8000_ffff_range() {
552        // The wiki is explicit that this bit is present over the entire 32 KiB
553        // range, unlike the bank-select register it shares a byte with. An
554        // implementation that folded it into the even-address arm would miss
555        // every one of these.
556        for addr in [0x8001u16, 0x9FFF, 0xA000, 0xC001, 0xFFFF] {
557            let mut m = m154();
558            m.cpu_write(0x8000, 0x00);
559            assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
560            m.cpu_write(addr, 0x40);
561            assert_eq!(
562                m.current_mirroring(),
563                Mirroring::SingleScreenB,
564                "bit 6 must be decoded at ${addr:04X}"
565            );
566        }
567    }
568
569    #[test]
570    fn m154_keeps_the_mapper_88_chr_split() {
571        // 154 is 88 in every respect but the nametable bit, so the right pattern
572        // table must still come from the second 64 KiB half.
573        let mut a = m154();
574        let mut b = Namco118::new(
575            synth_prg(8),
576            synth_chr(128),
577            Mirroring::Vertical,
578            Namco118Board::M88,
579        )
580        .unwrap();
581        for m in [&mut a, &mut b] {
582            m.cpu_write(0x8000, 0x02); // select R2
583            m.cpu_write(0x8001, 0x05);
584        }
585        assert_eq!(a.ppu_read(0x1000), b.ppu_read(0x1000));
586        assert_eq!(a.ppu_read(0x1000), (0x05u8 & 0x3F) | 0x40);
587    }
588
589    #[test]
590    fn m154_bank_writes_still_work_while_setting_mirroring() {
591        // The nametable bit rides along with bank-data writes, so a value with
592        // bit 6 set must still land in the selected register.
593        let mut m = m154();
594        m.cpu_write(0x8000, 0x06); // select R6 (PRG @ $8000)
595        m.cpu_write(0x8001, 0x43); // bank 3, and bit 6 set
596        assert_eq!(
597            m.cpu_read(0x8000),
598            3,
599            "the bank register still took the value"
600        );
601        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
602    }
603
604    #[test]
605    fn m154_mirroring_survives_a_save_state() {
606        // Mirroring is mutable on this board, so it has to be serialized -- a v1
607        // blob never carried it and would restore the wrong CIRAM page.
608        let mut m = m154();
609        m.cpu_write(0x8000, 0x40);
610        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
611        let blob = m.save_state();
612
613        let mut m2 = m154();
614        m2.cpu_write(0x8000, 0x00);
615        assert_eq!(m2.current_mirroring(), Mirroring::SingleScreenA);
616        m2.load_state(&blob).unwrap();
617        assert_eq!(m2.current_mirroring(), Mirroring::SingleScreenB);
618    }
619
620    #[test]
621    fn m88_ignores_bit6_so_it_is_unchanged() {
622        let mut m = Namco118::new(
623            synth_prg(8),
624            synth_chr(128),
625            Mirroring::Vertical,
626            Namco118Board::M88,
627        )
628        .unwrap();
629        m.cpu_write(0x8000, 0x40);
630        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
631    }
632
633    #[test]
634    fn m88_refuses_a_v1_state_that_predates_the_mirroring_byte() {
635        // v1 carried no mirroring byte. It loaded, keeping the constructed
636        // mirroring, until v2.9.8 made every legacy layout an error (ADR 0042).
637        let mut m = Namco118::new(
638            synth_prg(8),
639            synth_chr(64),
640            Mirroring::Vertical,
641            Namco118Board::M88,
642        )
643        .unwrap();
644        m.cpu_write(0x8000, 0x06);
645        m.cpu_write(0x8001, 3);
646        let v2 = m.save_state();
647
648        // Legacy form: v1 byte, and the mirroring byte (index 10) removed.
649        let mut v1 = Vec::with_capacity(v2.len() - 1);
650        v1.extend_from_slice(&v2[..10]);
651        v1.extend_from_slice(&v2[11..]);
652        v1[0] = 1;
653
654        let mut m2 = Namco118::new(
655            synth_prg(8),
656            synth_chr(64),
657            Mirroring::Vertical,
658            Namco118Board::M88,
659        )
660        .unwrap();
661        assert!(matches!(
662            m2.load_state(&v1),
663            Err(MapperError::UnsupportedVersion(1))
664        ));
665        // A v2-length blob declaring v1 is refused the same way.
666        let mut mismatched = v2.clone();
667        mismatched[0] = 1;
668        assert!(m2.load_state(&mismatched).is_err());
669        // ... and a v2 blob one byte short is a truncation, not a v1 state.
670        assert!(matches!(
671            m2.load_state(&v2[..v2.len() - 1]),
672            Err(MapperError::WrongLength { .. })
673        ));
674        m2.load_state(&v2).expect("the current blob loads");
675        assert_eq!(m2.cpu_read(0x8000), 3, "bank state round-trips");
676    }
677}