Skip to main content

rustynes_mappers/
waixing.rs

1//! Waixing boards: mappers 162, 178, 242 and 253.
2//!
3//! Waixing produced a long line of unlicensed Chinese cartridges. Most are
4//! address-decoded 32 KiB PRG banking with a mirroring bit and optional
5//! PRG-RAM -- broad rather than deep, with the variation being which address
6//! bits carry the bank and where the mirroring control sits.
7//!
8//! Mapper 253 is the exception and the reason this module is not trivial: it
9//! carries a *scaled* IRQ counter (the prescaler divides the CPU clock before
10//! the counter sees it) and a CHR-RAM escape, where two specific CHR bank
11//! values redirect the fetch to RAM instead of ROM. Both are modelled here
12//! rather than approximated.
13//!
14//! A best-effort (Tier-2) board: register-decode correctness verified against
15//! the reference emulators (`Mesen2`, `GeraNES`) and the nesdev wiki, with no
16//! commercial-oracle ROM in the tree. Banking math is direct slice indexing and
17//! every bank select wraps with `% count`, so a register write can never index
18//! out of bounds -- required for the `#![no_std]` chip stack, which cannot
19//! afford a panic on a register access.
20//!
21//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
22//! and `docs/mappers.md` §Mapper coverage matrix.
23
24#![allow(
25    clippy::bool_to_int_with_if,
26    clippy::cast_lossless,
27    clippy::cast_possible_truncation,
28    clippy::doc_markdown,
29    clippy::match_same_arms,
30    clippy::missing_const_for_fn,
31    clippy::similar_names,
32    clippy::struct_excessive_bools,
33    clippy::too_many_lines,
34    clippy::unreadable_literal
35)]
36
37use crate::cartridge::Mirroring;
38use crate::mapper::{Mapper, MapperCaps, MapperError};
39use alloc::{boxed::Box, vec::Vec};
40use alloc::{format, vec};
41
42const PRG_BANK_8K: usize = 0x2000;
43const PRG_BANK_16K: usize = 0x4000;
44const PRG_BANK_32K: usize = 0x8000;
45const CHR_BANK_2K: usize = 0x0800;
46const CHR_BANK_8K: usize = 0x2000;
47const NAMETABLE_SIZE: usize = 0x0400;
48const NAMETABLE_SIZE_U16: u16 = 0x0400;
49
50const SAVE_STATE_VERSION: u8 = 1;
51
52// ---------------------------------------------------------------------------
53// Shared nametable helper (mirrors the one in the other simple-mapper modules).
54// ---------------------------------------------------------------------------
55
56const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
57    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
58    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
59    let physical = mirroring.physical_bank(table);
60    physical * NAMETABLE_SIZE + local
61}
62
63const CHR_BANK_1K: usize = 0x0400;
64
65const fn byte_to_mirroring(b: u8, fallback: Mirroring) -> Mirroring {
66    match b {
67        0 => Mirroring::Horizontal,
68        1 => Mirroring::Vertical,
69        2 => Mirroring::SingleScreenA,
70        3 => Mirroring::SingleScreenB,
71        4 => Mirroring::FourScreen,
72        5 => Mirroring::MapperControlled,
73        _ => fallback,
74    }
75}
76
77/// Validate a PRG-ROM image is a non-zero multiple of 8 KiB.
78fn check_prg(prg: &[u8], id: u16) -> Result<(), MapperError> {
79    if prg.is_empty() || !prg.len().is_multiple_of(PRG_BANK_8K) {
80        return Err(MapperError::Invalid(format!(
81            "mapper {id} PRG-ROM size {} is not a non-zero multiple of 8 KiB",
82            prg.len()
83        )));
84    }
85    Ok(())
86}
87
88const fn mirroring_to_byte(m: Mirroring) -> u8 {
89    match m {
90        Mirroring::Horizontal => 0,
91        Mirroring::Vertical => 1,
92        Mirroring::SingleScreenA => 2,
93        Mirroring::SingleScreenB => 3,
94        Mirroring::FourScreen => 4,
95        Mirroring::MapperControlled => 5,
96    }
97}
98
99/// Mapper 242 (Waixing `43-in-1`).
100pub struct Waixing242 {
101    prg_rom: Box<[u8]>,
102    chr_ram: Box<[u8]>,
103    vram: Box<[u8]>,
104    /// 8 KiB work-RAM at $6000-$7FFF.
105    prg_ram: Box<[u8]>,
106    prg_bank: u8,
107    horizontal_mirroring: bool,
108}
109
110impl Waixing242 {
111    /// Construct a new mapper 242 board.
112    ///
113    /// # Errors
114    ///
115    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
116    /// 32 KiB.
117    pub fn new(
118        prg_rom: Box<[u8]>,
119        _chr_rom: &[u8],
120        _mirroring: Mirroring,
121    ) -> Result<Self, MapperError> {
122        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
123            return Err(MapperError::Invalid(format!(
124                "mapper 242 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
125                prg_rom.len()
126            )));
127        }
128        Ok(Self {
129            prg_rom,
130            chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
131            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
132            prg_ram: vec![0u8; PRG_BANK_8K].into_boxed_slice(),
133            prg_bank: 0,
134            horizontal_mirroring: false,
135        })
136    }
137}
138
139impl Mapper for Waixing242 {
140    fn sram(&self) -> &[u8] {
141        &self.prg_ram
142    }
143    fn sram_mut(&mut self) -> &mut [u8] {
144        &mut self.prg_ram
145    }
146    fn caps(&self) -> MapperCaps {
147        MapperCaps::NONE
148    }
149
150    // The $6000-$7FFF work-RAM is mapped (the trait default already treats
151    // $6000-$FFFF as mapped, so no override is needed).
152
153    fn cpu_read(&mut self, addr: u16) -> u8 {
154        if (0x6000..=0x7FFF).contains(&addr) {
155            self.prg_ram[(addr - 0x6000) as usize]
156        } else if (0x8000..=0xFFFF).contains(&addr) {
157            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
158            let bank = (self.prg_bank as usize) % count;
159            self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
160        } else {
161            0
162        }
163    }
164
165    fn cpu_write(&mut self, addr: u16, value: u8) {
166        if (0x6000..=0x7FFF).contains(&addr) {
167            self.prg_ram[(addr - 0x6000) as usize] = value;
168        } else if (0x8000..=0xFFFF).contains(&addr) {
169            // nesdev iNES 242 decode (address-latched): bit 1 (M) = mirroring
170            // (0 = vertical, 1 = horizontal); the 32 KiB PRG bank = the inner
171            // bank (PRG A16..A14 = address bits 2..4) OR'd with the outer bank
172            // (PRG A18..A17 = address bits 5..6) shifted into place. The whole
173            // $8000-$FFFF window is one switchable 32 KiB page.
174            let inner = ((addr >> 2) & 0x07) as u8;
175            let outer = ((addr >> 5) & 0x03) as u8;
176            // inner is A16..A14 (a 16 KiB granularity); the 32 KiB bank takes the
177            // upper bits: A18..A15 = outer<<2 | inner>>1.
178            self.prg_bank = (outer << 2) | (inner >> 1);
179            self.horizontal_mirroring = (addr & 0x02) != 0;
180        }
181    }
182
183    fn ppu_read(&mut self, addr: u16) -> u8 {
184        let addr = addr & 0x3FFF;
185        match addr {
186            0x0000..=0x1FFF => self.chr_ram[addr as usize],
187            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
188            _ => 0,
189        }
190    }
191
192    fn ppu_write(&mut self, addr: u16, value: u8) {
193        let addr = addr & 0x3FFF;
194        match addr {
195            0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
196            0x2000..=0x3EFF => {
197                let off = nametable_offset(addr, self.current_mirroring());
198                self.vram[off] = value;
199            }
200            _ => {}
201        }
202    }
203
204    fn current_mirroring(&self) -> Mirroring {
205        if self.horizontal_mirroring {
206            Mirroring::Horizontal
207        } else {
208            Mirroring::Vertical
209        }
210    }
211
212    fn save_state(&self) -> Vec<u8> {
213        let mut out =
214            Vec::with_capacity(3 + self.prg_ram.len() + self.vram.len() + self.chr_ram.len());
215        out.push(SAVE_STATE_VERSION);
216        out.push(self.prg_bank);
217        out.push(u8::from(self.horizontal_mirroring));
218        out.extend_from_slice(&self.prg_ram);
219        out.extend_from_slice(&self.vram);
220        out.extend_from_slice(&self.chr_ram);
221        out
222    }
223
224    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
225        let expected = 3 + self.prg_ram.len() + self.vram.len() + self.chr_ram.len();
226        if data.len() != expected {
227            return Err(MapperError::WrongLength {
228                expected,
229                got: data.len(),
230            });
231        }
232        if data[0] != SAVE_STATE_VERSION {
233            return Err(MapperError::UnsupportedVersion(data[0]));
234        }
235        self.prg_bank = data[1];
236        self.horizontal_mirroring = data[2] != 0;
237        let mut cursor = 3;
238        self.prg_ram
239            .copy_from_slice(&data[cursor..cursor + self.prg_ram.len()]);
240        cursor += self.prg_ram.len();
241        self.vram
242            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
243        cursor += self.vram.len();
244        self.chr_ram
245            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
246        Ok(())
247    }
248}
249
250/// Mapper 162 (Waixing FS304).
251pub struct WaixingFs304M162 {
252    prg_rom: Box<[u8]>,
253    chr_ram: Box<[u8]>,
254    vram: Box<[u8]>,
255    /// 8 KiB battery-backed PRG-RAM at CPU $6000-$7FFF. The Waixing RPGs read
256    /// it during boot; without it they hang on a blank frame.
257    prg_ram: Box<[u8]>,
258    regs: [u8; 4],
259    mirroring: Mirroring,
260}
261
262impl WaixingFs304M162 {
263    /// Construct a new mapper 162 board.
264    ///
265    /// # Errors
266    ///
267    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
268    /// 32 KiB.
269    pub fn new(
270        prg_rom: Box<[u8]>,
271        _chr_rom: &[u8],
272        mirroring: Mirroring,
273    ) -> Result<Self, MapperError> {
274        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
275            return Err(MapperError::Invalid(format!(
276                "mapper 162 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
277                prg_rom.len()
278            )));
279        }
280        Ok(Self {
281            prg_rom,
282            chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
283            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
284            prg_ram: vec![0u8; PRG_BANK_8K].into_boxed_slice(),
285            regs: [0; 4],
286            mirroring,
287        })
288    }
289
290    const fn prg_bank(&self) -> usize {
291        let r0 = self.regs[0] as usize;
292        let r1 = self.regs[1] as usize;
293        let r2 = self.regs[2] as usize;
294        let r3 = self.regs[3] as usize;
295        let a = (r3 >> 2) & 1; // $5300.2 — A16 mode
296        let b = r3 & 1; // $5300.0 — A15 mode
297        let a16 = if a == 0 { 1 } else { (r0 >> 1) & 1 };
298        let a15 = if b == 0 {
299            (r1 >> 1) & 1
300        } else if a == 0 {
301            1
302        } else {
303            r0 & 1
304        };
305        let a17 = (r0 >> 2) & 1;
306        let a18 = (r0 >> 3) & 1;
307        let a19 = r2 & 1;
308        let a20 = (r2 >> 1) & 1;
309        a15 | (a16 << 1) | (a17 << 2) | (a18 << 3) | (a19 << 4) | (a20 << 5)
310    }
311}
312
313impl Mapper for WaixingFs304M162 {
314    fn sram(&self) -> &[u8] {
315        &self.prg_ram
316    }
317    fn sram_mut(&mut self) -> &mut [u8] {
318        &mut self.prg_ram
319    }
320    fn caps(&self) -> MapperCaps {
321        MapperCaps::NONE
322    }
323
324    fn cpu_read(&mut self, addr: u16) -> u8 {
325        match addr {
326            0x6000..=0x7FFF => self.prg_ram[(addr - 0x6000) as usize],
327            0x8000..=0xFFFF => {
328                let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
329                let bank = self.prg_bank() % count;
330                self.prg_rom[bank * PRG_BANK_32K + (addr as usize & 0x7FFF)]
331            }
332            _ => 0,
333        }
334    }
335
336    fn cpu_read_unmapped(&self, addr: u16) -> bool {
337        // v2.7.2 (core audit §5.5): with no save RAM, nothing drives
338        // `$6000-$7FFF` and it floats; see `Mapper::cpu_read_unmapped`.
339        (matches!(addr, 0x6000..=0x7FFF) && self.sram().is_empty()) || {
340            // $5000-$5FFF carries the write-only register block; the rest of
341            // $4020-$5FFF is open bus. $6000-$7FFF is PRG-RAM and $8000-$FFFF is
342            // mapped PRG.
343            (0x4020..=0x5FFF).contains(&addr)
344        }
345    }
346
347    fn cpu_write(&mut self, addr: u16, value: u8) {
348        if (0x6000..=0x7FFF).contains(&addr) {
349            self.prg_ram[(addr - 0x6000) as usize] = value;
350        } else if (0x5000..=0x5FFF).contains(&addr) {
351            let idx = ((addr >> 8) & 0x03) as usize;
352            self.regs[idx] = value;
353        }
354    }
355
356    fn ppu_read(&mut self, addr: u16) -> u8 {
357        let addr = addr & 0x3FFF;
358        match addr {
359            0x0000..=0x1FFF => self.chr_ram[addr as usize],
360            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
361            _ => 0,
362        }
363    }
364
365    fn ppu_write(&mut self, addr: u16, value: u8) {
366        let addr = addr & 0x3FFF;
367        match addr {
368            0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
369            0x2000..=0x3EFF => {
370                let off = nametable_offset(addr, self.mirroring);
371                self.vram[off] = value;
372            }
373            _ => {}
374        }
375    }
376
377    fn current_mirroring(&self) -> Mirroring {
378        self.mirroring
379    }
380
381    fn save_state(&self) -> Vec<u8> {
382        let mut out =
383            Vec::with_capacity(5 + self.vram.len() + self.chr_ram.len() + self.prg_ram.len());
384        out.push(SAVE_STATE_VERSION);
385        out.extend_from_slice(&self.regs);
386        out.extend_from_slice(&self.vram);
387        out.extend_from_slice(&self.chr_ram);
388        out.extend_from_slice(&self.prg_ram);
389        out
390    }
391
392    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
393        let expected = 5 + self.vram.len() + self.chr_ram.len() + self.prg_ram.len();
394        if data.len() != expected {
395            return Err(MapperError::WrongLength {
396                expected,
397                got: data.len(),
398            });
399        }
400        if data[0] != SAVE_STATE_VERSION {
401            return Err(MapperError::UnsupportedVersion(data[0]));
402        }
403        self.regs.copy_from_slice(&data[1..5]);
404        let mut cursor = 5;
405        self.vram
406            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
407        cursor += self.vram.len();
408        self.chr_ram
409            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
410        cursor += self.chr_ram.len();
411        self.prg_ram
412            .copy_from_slice(&data[cursor..cursor + self.prg_ram.len()]);
413        Ok(())
414    }
415}
416
417// ===========================================================================
418// Mapper 178 — Waixing / San Guo Zhong Chen (educational series).
419//
420// A $4800-$4803 register block plus 8 KiB work-RAM at $6000 (NESdev
421// INES_Mapper_178 / Waixing FS305):
422//   $4800 : bit 0 = mirroring (0 = vertical, 1 = horizontal),
423//           bits 1-2 = PRG banking mode
424//             0 = NROM-256 / BNROM (32 KiB switchable)
425//             1 = UNROM (16 KiB switchable at $8000, fixed-111b at $C000)
426//             2 = NROM-128 (16 KiB mirrored)
427//             3 = UNROM variant ($C000 = inner|1 instead of all-ones).
428//   $4801 : bits 0-2 = inner PRG bank (PRG A16..A14, i.e. 16 KiB units).
429//   $4802 : outer PRG bank (PRG A17+).
430//   $4803 : PRG-RAM bank (stored only; the staged games use a single 8 KiB).
431// 16 KiB bank = (reg2 << 3) | (reg1 & 0x07). The OLD code read bit 0 of $4800
432// as the PRG mode (it is the MIRRORING bit) and bit 1 as mirroring (it is a
433// PRG-mode bit) — the two were swapped, and the bank composition masked wrong,
434// so educational titles booted the wrong bank and blanked. CHR is 8 KiB RAM.
435// ===========================================================================
436
437/// Mapper 178 (Waixing educational series).
438pub struct Waixing178 {
439    prg_rom: Box<[u8]>,
440    chr_ram: Box<[u8]>,
441    vram: Box<[u8]>,
442    prg_ram: Box<[u8]>,
443    regs: [u8; 4],
444}
445
446impl Waixing178 {
447    /// Construct a new mapper 178 board.
448    ///
449    /// # Errors
450    ///
451    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
452    /// 16 KiB.
453    pub fn new(
454        prg_rom: Box<[u8]>,
455        _chr_rom: &[u8],
456        _mirroring: Mirroring,
457    ) -> Result<Self, MapperError> {
458        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
459            return Err(MapperError::Invalid(format!(
460                "mapper 178 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
461                prg_rom.len()
462            )));
463        }
464        Ok(Self {
465            prg_rom,
466            chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
467            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
468            prg_ram: vec![0u8; PRG_BANK_8K].into_boxed_slice(),
469            regs: [0; 4],
470        })
471    }
472
473    /// Composed inner+outer 16 KiB bank: outer ($4802) shifted past the 3-bit
474    /// inner ($4801 bits 0-2).
475    const fn prg_base16(&self) -> usize {
476        ((self.regs[2] as usize) << 3) | (self.regs[1] as usize & 0x07)
477    }
478
479    /// PRG banking mode from $4800 bits 1-2 (0..=3).
480    const fn prg_mode(&self) -> u8 {
481        (self.regs[0] >> 1) & 0x03
482    }
483
484    fn read_prg(&self, bank16: usize, addr: u16) -> u8 {
485        let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
486        let bank = bank16 % count;
487        self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
488    }
489}
490
491impl Mapper for Waixing178 {
492    fn sram(&self) -> &[u8] {
493        &self.prg_ram
494    }
495    fn sram_mut(&mut self) -> &mut [u8] {
496        &mut self.prg_ram
497    }
498    fn caps(&self) -> MapperCaps {
499        MapperCaps::NONE
500    }
501
502    fn cpu_read(&mut self, addr: u16) -> u8 {
503        match addr {
504            0x6000..=0x7FFF => self.prg_ram[(addr - 0x6000) as usize],
505            0x8000..=0xBFFF => {
506                let base = self.prg_base16();
507                // $8000: NROM-256/BNROM (mode 0) presents the even bank of the
508                // 32 KiB pair; every other mode switches a 16 KiB bank directly.
509                let bank = if self.prg_mode() == 0 {
510                    base & !1
511                } else {
512                    base
513                };
514                self.read_prg(bank, addr)
515            }
516            0xC000..=0xFFFF => {
517                let base = self.prg_base16();
518                let bank = match self.prg_mode() {
519                    // NROM-256 / BNROM: high half of the 32 KiB pair.
520                    0 => (base & !1) | 1,
521                    // UNROM: $C000 fixed to the last bank of the outer block
522                    // (inner bits = 111b).
523                    1 => (base & !0x07) | 0x07,
524                    // NROM-128: 16 KiB mirrored.
525                    2 => base,
526                    // UNROM variant: $C000 = inner | 1.
527                    _ => base | 1,
528                };
529                self.read_prg(bank, addr)
530            }
531            _ => 0,
532        }
533    }
534
535    fn cpu_read_unmapped(&self, addr: u16) -> bool {
536        // v2.7.2 (core audit §5.5): with no save RAM, nothing drives
537        // `$6000-$7FFF` and it floats; see `Mapper::cpu_read_unmapped`.
538        (matches!(addr, 0x6000..=0x7FFF) && self.sram().is_empty()) || {
539            // $4800-$4803 are write-only registers; $6000-$FFFF is mapped
540            // (work-RAM + PRG).  The remaining $4020-$5FFF window is open bus.
541            (0x4020..=0x5FFF).contains(&addr)
542        }
543    }
544
545    fn cpu_write(&mut self, addr: u16, value: u8) {
546        match addr {
547            0x4800..=0x4803 => self.regs[(addr - 0x4800) as usize] = value,
548            0x6000..=0x7FFF => self.prg_ram[(addr - 0x6000) as usize] = value,
549            _ => {}
550        }
551    }
552
553    fn ppu_read(&mut self, addr: u16) -> u8 {
554        let addr = addr & 0x3FFF;
555        match addr {
556            0x0000..=0x1FFF => self.chr_ram[addr as usize],
557            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
558            _ => 0,
559        }
560    }
561
562    fn ppu_write(&mut self, addr: u16, value: u8) {
563        let addr = addr & 0x3FFF;
564        match addr {
565            0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
566            0x2000..=0x3EFF => {
567                let off = nametable_offset(addr, self.current_mirroring());
568                self.vram[off] = value;
569            }
570            _ => {}
571        }
572    }
573
574    fn current_mirroring(&self) -> Mirroring {
575        // $4800 bit 0: 0 = vertical, 1 = horizontal.
576        if (self.regs[0] & 0x01) != 0 {
577            Mirroring::Horizontal
578        } else {
579            Mirroring::Vertical
580        }
581    }
582
583    fn save_state(&self) -> Vec<u8> {
584        let mut out =
585            Vec::with_capacity(5 + self.prg_ram.len() + self.vram.len() + self.chr_ram.len());
586        out.push(SAVE_STATE_VERSION);
587        out.extend_from_slice(&self.regs);
588        out.extend_from_slice(&self.prg_ram);
589        out.extend_from_slice(&self.vram);
590        out.extend_from_slice(&self.chr_ram);
591        out
592    }
593
594    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
595        let expected = 5 + self.prg_ram.len() + self.vram.len() + self.chr_ram.len();
596        if data.len() != expected {
597            return Err(MapperError::WrongLength {
598                expected,
599                got: data.len(),
600            });
601        }
602        if data[0] != SAVE_STATE_VERSION {
603            return Err(MapperError::UnsupportedVersion(data[0]));
604        }
605        self.regs.copy_from_slice(&data[1..5]);
606        let mut cursor = 5;
607        self.prg_ram
608            .copy_from_slice(&data[cursor..cursor + self.prg_ram.len()]);
609        cursor += self.prg_ram.len();
610        self.vram
611            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
612        cursor += self.vram.len();
613        self.chr_ram
614            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
615        Ok(())
616    }
617}
618
619/// Waixing VRC4-clone (mapper 253, *Dragon Ball Z*).
620pub struct Waixing253 {
621    prg_rom: Box<[u8]>,
622    chr: Box<[u8]>,
623    /// `true` when the cart supplied no CHR-ROM, so `self.chr` is the cart's
624    /// (writable) CHR-RAM and must be serialized in the save-state. Distinct
625    /// from the 2 KiB `chr_ram` escape (the Mesen2 `lo == 4|5` window), which
626    /// always exists.
627    chr_is_ram: bool,
628    chr_ram: Box<[u8]>,
629    vram: Box<[u8]>,
630    mirroring: Mirroring,
631    prg_count_8k: usize,
632    chr_count_1k: usize,
633    prg: [u8; 2],
634    chr_low: [u8; 8],
635    chr_high: [u8; 8],
636    force_chr_rom: bool,
637    irq_reload: u8,
638    irq_counter: u8,
639    irq_enabled: bool,
640    irq_scaler: u16,
641    irq_pending: bool,
642}
643
644impl Waixing253 {
645    const SAVE_LEN: usize = 2 + 8 + 8 + 1 + 1 + 1 + 1 + 2 + 1 + 1;
646
647    fn new(
648        prg_rom: Box<[u8]>,
649        chr_rom: Box<[u8]>,
650        mirroring: Mirroring,
651    ) -> Result<Self, MapperError> {
652        check_prg(&prg_rom, 253)?;
653        let chr_is_ram = chr_rom.is_empty();
654        let chr: Box<[u8]> = if chr_is_ram {
655            // CHR-RAM variant: allocate the conventional 8 KiB (matching the
656            // MMC3 `8 * CHR_BANK_1K` convention) so the banked CHR path has a
657            // real, writable backing store instead of a stub 1 KiB.
658            vec![0u8; CHR_BANK_8K].into_boxed_slice()
659        } else {
660            if !chr_rom.len().is_multiple_of(CHR_BANK_1K) {
661                return Err(MapperError::Invalid(format!(
662                    "mapper 253 CHR-ROM size {} is not a multiple of 1 KiB",
663                    chr_rom.len()
664                )));
665            }
666            chr_rom
667        };
668        let prg_count_8k = prg_rom.len() / PRG_BANK_8K;
669        let chr_count_1k = (chr.len() / CHR_BANK_1K).max(1);
670        Ok(Self {
671            prg_rom,
672            chr,
673            chr_is_ram,
674            chr_ram: vec![0u8; CHR_BANK_2K].into_boxed_slice(), // 2 KiB CHR-RAM escape.
675            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
676            mirroring,
677            prg_count_8k,
678            chr_count_1k,
679            prg: [0; 2],
680            chr_low: [0; 8],
681            chr_high: [0; 8],
682            force_chr_rom: false,
683            irq_reload: 0,
684            irq_counter: 0,
685            irq_enabled: false,
686            irq_scaler: 0,
687            irq_pending: false,
688        })
689    }
690
691    fn prg_bank(&self, slot: usize) -> usize {
692        let count = self.prg_count_8k;
693        match slot {
694            0 => self.prg[0] as usize % count,
695            1 => self.prg[1] as usize % count,
696            2 => count.saturating_sub(2),
697            _ => count - 1,
698        }
699    }
700}
701
702impl Mapper for Waixing253 {
703    fn caps(&self) -> MapperCaps {
704        MapperCaps::CYCLE_IRQ
705    }
706
707    fn cpu_read(&mut self, addr: u16) -> u8 {
708        match addr {
709            0x8000..=0xFFFF => {
710                let slot = (addr as usize - 0x8000) / PRG_BANK_8K;
711                let bank = self.prg_bank(slot);
712                self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
713            }
714            _ => 0,
715        }
716    }
717
718    fn cpu_write(&mut self, addr: u16, value: u8) {
719        if (0xB000..=0xE00C).contains(&addr) {
720            let slot = ((((addr & 0x08) | (addr >> 8)) >> 3) as usize).wrapping_add(2) & 0x07;
721            let shift = (addr & 0x04) as u8;
722            let lo = (self.chr_low[slot] & (0xF0u8 >> shift)) | (value << shift);
723            self.chr_low[slot] = lo;
724            if slot == 0 {
725                if lo == 0xC8 {
726                    self.force_chr_rom = false;
727                } else if lo == 0x88 {
728                    self.force_chr_rom = true;
729                }
730            }
731            if shift != 0 {
732                self.chr_high[slot] = value >> 4;
733            }
734        } else {
735            match addr {
736                0x8010 => self.prg[0] = value,
737                0xA010 => self.prg[1] = value,
738                0x9400 => {
739                    self.mirroring = match value & 0x03 {
740                        0 => Mirroring::Vertical,
741                        1 => Mirroring::Horizontal,
742                        2 => Mirroring::SingleScreenA,
743                        _ => Mirroring::SingleScreenB,
744                    };
745                }
746                0xF000 => {
747                    self.irq_reload = (self.irq_reload & 0xF0) | (value & 0x0F);
748                    self.irq_pending = false;
749                }
750                0xF004 => {
751                    self.irq_reload = (self.irq_reload & 0x0F) | (value << 4);
752                    self.irq_pending = false;
753                }
754                0xF008 => {
755                    self.irq_counter = self.irq_reload;
756                    self.irq_enabled = value & 0x02 != 0;
757                    self.irq_scaler = 0;
758                    self.irq_pending = false;
759                }
760                _ => {}
761            }
762        }
763    }
764
765    fn ppu_read(&mut self, addr: u16) -> u8 {
766        let addr = addr & 0x3FFF;
767        match addr {
768            0x0000..=0x1FFF => {
769                let slot = (addr as usize) / CHR_BANK_1K;
770                let lo = self.chr_low[slot];
771                if (lo == 4 || lo == 5) && !self.force_chr_rom {
772                    let page = (lo as usize & 0x01) * CHR_BANK_1K;
773                    return self.chr_ram
774                        [(page + (addr as usize & 0x3FF)) & (self.chr_ram.len() - 1)];
775                }
776                let page =
777                    (lo as usize | ((self.chr_high[slot] as usize) << 8)) % self.chr_count_1k;
778                self.chr[page * CHR_BANK_1K + (addr as usize & 0x3FF)]
779            }
780            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
781            _ => 0,
782        }
783    }
784
785    fn ppu_write(&mut self, addr: u16, value: u8) {
786        let addr = addr & 0x3FFF;
787        match addr {
788            0x0000..=0x1FFF => {
789                let slot = (addr as usize) / CHR_BANK_1K;
790                let lo = self.chr_low[slot];
791                if (lo == 4 || lo == 5) && !self.force_chr_rom {
792                    let page = (lo as usize & 0x01) * CHR_BANK_1K;
793                    let off = (page + (addr as usize & 0x3FF)) & (self.chr_ram.len() - 1);
794                    self.chr_ram[off] = value;
795                } else if self.chr_is_ram {
796                    // CHR-RAM variant: writes land in the banked CHR store
797                    // (mirrors the `ppu_read` banked path). For a CHR-ROM cart
798                    // this is a no-op (ROM is not writable).
799                    let page =
800                        (lo as usize | ((self.chr_high[slot] as usize) << 8)) % self.chr_count_1k;
801                    self.chr[page * CHR_BANK_1K + (addr as usize & 0x3FF)] = value;
802                }
803            }
804            0x2000..=0x3EFF => {
805                let off = nametable_offset(addr, self.mirroring);
806                self.vram[off] = value;
807            }
808            _ => {}
809        }
810    }
811
812    fn notify_cpu_cycle(&mut self) {
813        if self.irq_enabled {
814            self.irq_scaler += 1;
815            if self.irq_scaler >= 114 {
816                self.irq_scaler = 0;
817                self.irq_counter = self.irq_counter.wrapping_add(1);
818                if self.irq_counter == 0 {
819                    self.irq_counter = self.irq_reload;
820                    self.irq_pending = true;
821                }
822            }
823        }
824    }
825
826    fn irq_pending(&self) -> bool {
827        self.irq_pending
828    }
829
830    fn irq_acknowledge(&mut self) {
831        self.irq_pending = false;
832    }
833
834    fn current_mirroring(&self) -> Mirroring {
835        self.mirroring
836    }
837
838    fn save_state(&self) -> Vec<u8> {
839        // CHR-RAM variant: the banked `self.chr` is mutable, so serialize it.
840        let chr_ram_main = if self.chr_is_ram { self.chr.len() } else { 0 };
841        let mut out = Vec::with_capacity(
842            1 + Self::SAVE_LEN + self.vram.len() + self.chr_ram.len() + chr_ram_main,
843        );
844        out.push(SAVE_STATE_VERSION);
845        out.extend_from_slice(&self.prg);
846        out.extend_from_slice(&self.chr_low);
847        out.extend_from_slice(&self.chr_high);
848        out.push(u8::from(self.force_chr_rom));
849        out.push(self.irq_reload);
850        out.push(self.irq_counter);
851        out.push(u8::from(self.irq_enabled));
852        out.extend_from_slice(&self.irq_scaler.to_le_bytes());
853        out.push(u8::from(self.irq_pending));
854        out.push(mirroring_to_byte(self.mirroring));
855        out.extend_from_slice(&self.vram);
856        out.extend_from_slice(&self.chr_ram);
857        if self.chr_is_ram {
858            out.extend_from_slice(&self.chr);
859        }
860        out
861    }
862
863    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
864        let chr_ram_main = if self.chr_is_ram { self.chr.len() } else { 0 };
865        let expected = 1 + Self::SAVE_LEN + self.vram.len() + self.chr_ram.len() + chr_ram_main;
866        if data.len() != expected {
867            return Err(MapperError::WrongLength {
868                expected,
869                got: data.len(),
870            });
871        }
872        if data[0] != SAVE_STATE_VERSION {
873            return Err(MapperError::UnsupportedVersion(data[0]));
874        }
875        let mut c = 1;
876        self.prg.copy_from_slice(&data[c..c + 2]);
877        c += 2;
878        self.chr_low.copy_from_slice(&data[c..c + 8]);
879        c += 8;
880        self.chr_high.copy_from_slice(&data[c..c + 8]);
881        c += 8;
882        self.force_chr_rom = data[c] != 0;
883        self.irq_reload = data[c + 1];
884        self.irq_counter = data[c + 2];
885        self.irq_enabled = data[c + 3] != 0;
886        self.irq_scaler = u16::from_le_bytes([data[c + 4], data[c + 5]]);
887        self.irq_pending = data[c + 6] != 0;
888        self.mirroring = byte_to_mirroring(data[c + 7], self.mirroring);
889        c += 8;
890        self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
891        c += self.vram.len();
892        self.chr_ram
893            .copy_from_slice(&data[c..c + self.chr_ram.len()]);
894        c += self.chr_ram.len();
895        if self.chr_is_ram {
896            self.chr.copy_from_slice(&data[c..c + self.chr.len()]);
897        }
898        Ok(())
899    }
900}
901
902/// Mapper 253 (Waixing VRC4-clone, *Dragon Ball Z*).
903///
904/// # Errors
905/// [`MapperError::Invalid`] on a bad PRG/CHR size.
906pub fn new_m253(
907    prg_rom: Box<[u8]>,
908    chr_rom: Box<[u8]>,
909    mirroring: Mirroring,
910) -> Result<Waixing253, MapperError> {
911    Waixing253::new(prg_rom, chr_rom, mirroring)
912}
913
914#[cfg(test)]
915#[allow(clippy::cast_possible_truncation)]
916mod tests {
917    use super::*;
918    /// 1 KiB-banked CHR: byte 0 of each 1 KiB bank holds the bank index.
919    fn synth_chr_1k(banks: usize) -> Box<[u8]> {
920        let mut v = vec![0u8; banks * CHR_BANK_1K];
921        for b in 0..banks {
922            v[b * CHR_BANK_1K] = b as u8;
923        }
924        v.into_boxed_slice()
925    }
926
927    fn synth_prg_32k(banks: usize) -> Box<[u8]> {
928        let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
929        for b in 0..banks {
930            v[b * PRG_BANK_32K] = b as u8;
931        }
932        v.into_boxed_slice()
933    }
934
935    fn synth_prg_16k(banks: usize) -> Box<[u8]> {
936        let mut v = vec![0xFFu8; banks * PRG_BANK_16K];
937        for b in 0..banks {
938            v[b * PRG_BANK_16K] = b as u8;
939        }
940        v.into_boxed_slice()
941    }
942
943    fn synth_prg_8k(banks: usize) -> Box<[u8]> {
944        let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
945        for b in 0..banks {
946            v[b * PRG_BANK_8K] = b as u8;
947        }
948        v.into_boxed_slice()
949    }
950
951    #[test]
952    fn m242_address_decoded_32k_and_mirror() {
953        let mut m = Waixing242::new(synth_prg_32k(16), &[], Mirroring::Vertical).unwrap();
954        // A = 0x8018: bank = (0x18>>3)&0x0F = 3; mirror = (A&2)==0 -> V.
955        m.cpu_write(0x8018, 0);
956        assert_eq!(m.cpu_read(0x8000), 3);
957        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
958        // A = 0x801A: mirror bit set -> horizontal.
959        m.cpu_write(0x801A, 0);
960        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
961    }
962
963    #[test]
964    fn m242_save_state_round_trip() {
965        let mut m = Waixing242::new(synth_prg_32k(16), &[], Mirroring::Vertical).unwrap();
966        m.cpu_write(0x8028, 0); // bank 5
967        m.ppu_write(0x0006, 0x44);
968        let blob = m.save_state();
969        let mut m2 = Waixing242::new(synth_prg_32k(16), &[], Mirroring::Vertical).unwrap();
970        m2.load_state(&blob).unwrap();
971        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
972        assert_eq!(m2.ppu_read(0x0006), 0x44);
973    }
974
975    #[test]
976    fn m162_regs_compose_prg() {
977        let mut m = WaixingFs304M162::new(synth_prg_32k(64), &[], Mirroring::Vertical).unwrap();
978        // Reset: all regs 0 -> A=$5300.2=0 -> A16=1, A15=$5100.1=0 -> bank #2.
979        assert_eq!(m.cpu_read(0x8000), 2);
980        // Waixing mode $5300=$04 ($5300.2=1): A16=$5000.1, A15=$5100.1.
981        m.cpu_write(0x5300, 0x04);
982        m.cpu_write(0x5000, 0x02); // $5000.1 = 1 -> A16 = 1 -> bank still has A16 set
983        assert_eq!(m.cpu_read(0x8000), 2); // A16=1 -> bank 2
984        m.cpu_write(0x5100, 0x02); // $5100.1 = 1 -> A15 = 1 -> bank 3
985        assert_eq!(m.cpu_read(0x8000), 3);
986        m.cpu_write(0x5000, 0x00); // $5000.1 = 0 -> A16 = 0; A15 still 1 -> bank 1
987        assert_eq!(m.cpu_read(0x8000), 1);
988        // A17/A18 from $5000 bits 2/3; A19/A20 from $5200 bits 0/1.
989        m.cpu_write(0x5000, 0x0C); // bits 3,2 -> A18,A17 = 1,1 -> +12; A16=0,A15(=$5100.1)=1 -> 1
990        m.cpu_write(0x5200, 0x03); // A20,A19 = 1,1 -> +48
991        assert_eq!(m.cpu_read(0x8000), 1 + 12 + 48);
992    }
993
994    #[test]
995    fn m162_save_state_round_trip() {
996        let mut m = WaixingFs304M162::new(synth_prg_32k(8), &[], Mirroring::Vertical).unwrap();
997        m.cpu_write(0x5000, 6);
998        m.ppu_write(0x0012, 0x44);
999        let blob = m.save_state();
1000        let mut m2 = WaixingFs304M162::new(synth_prg_32k(8), &[], Mirroring::Vertical).unwrap();
1001        m2.load_state(&blob).unwrap();
1002        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
1003        assert_eq!(m2.ppu_read(0x0012), 0x44);
1004    }
1005
1006    #[test]
1007    fn m178_prg_mode_and_work_ram() {
1008        let mut m = Waixing178::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
1009        // UNROM mode (bits 1-2 = 01 -> $4800 = 0x02); inner reg1 = 3 -> base 3.
1010        m.cpu_write(0x4800, 0x02);
1011        m.cpu_write(0x4801, 0x03);
1012        m.cpu_write(0x4802, 0x00);
1013        assert_eq!(m.cpu_read(0x8000), 3); // switchable 16 KiB at $8000
1014        assert_eq!(m.cpu_read(0xC000), 7); // UNROM: $C000 fixed to inner 111b
1015        // NROM-256 / BNROM (mode 0): 32 KiB pair from the even bank.
1016        m.cpu_write(0x4800, 0x00);
1017        m.cpu_write(0x4801, 0x02); // base 2 -> 32 KiB pair (2,3)
1018        assert_eq!(m.cpu_read(0x8000), 2);
1019        assert_eq!(m.cpu_read(0xC000), 3);
1020        // Work-RAM round trips.
1021        m.cpu_write(0x6000, 0x77);
1022        assert_eq!(m.cpu_read(0x6000), 0x77);
1023        // Mirroring: $4800 bit 0 (1 = horizontal).
1024        m.cpu_write(0x4800, 0x01);
1025        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
1026        m.cpu_write(0x4800, 0x00);
1027        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
1028    }
1029
1030    #[test]
1031    fn m178_save_state_round_trip() {
1032        let mut m = Waixing178::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
1033        m.cpu_write(0x4800, 0x02);
1034        m.cpu_write(0x4801, 0x02);
1035        m.cpu_write(0x6010, 0x55);
1036        let blob = m.save_state();
1037        let mut m2 = Waixing178::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
1038        m2.load_state(&blob).unwrap();
1039        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
1040        assert_eq!(m2.cpu_read(0x6010), 0x55);
1041    }
1042
1043    #[test]
1044    fn waixing253_prg_and_scaled_irq() {
1045        let mut m = new_m253(synth_prg_8k(16), synth_chr_1k(64), Mirroring::Vertical).unwrap();
1046        m.cpu_write(0x8010, 4); // prg[0] = 4
1047        m.cpu_write(0xA010, 6); // prg[1] = 6
1048        assert_eq!(m.cpu_read(0x8000), 4);
1049        assert_eq!(m.cpu_read(0xA000), 6);
1050        assert_eq!(m.cpu_read(0xE000), 15); // fixed last.
1051
1052        m.cpu_write(0xF000, 0x0E); // reload low
1053        m.cpu_write(0xF008, 0x02); // load + enable
1054        // counter loaded with 0x0E; needs (0x100-0x0E) ticks * 114.
1055        let mut fired = false;
1056        for _ in 0..(256 * 115) {
1057            m.notify_cpu_cycle();
1058            if m.irq_pending() {
1059                fired = true;
1060                break;
1061            }
1062        }
1063        assert!(fired);
1064    }
1065
1066    #[test]
1067    fn waixing253_chr_ram_escape_and_round_trip() {
1068        let mut m = new_m253(synth_prg_8k(16), synth_chr_1k(64), Mirroring::Vertical).unwrap();
1069        // CHR low reg value 4 on slot 0 + not force-rom -> CHR-RAM.
1070        m.cpu_write(0xB000, 0x04); // slot 0 low nibble = 4
1071        m.ppu_write(0x0000, 0x5E);
1072        assert_eq!(m.ppu_read(0x0000), 0x5E);
1073        let blob = m.save_state();
1074        let mut m2 = new_m253(synth_prg_8k(16), synth_chr_1k(64), Mirroring::Vertical).unwrap();
1075        m2.load_state(&blob).unwrap();
1076        assert_eq!(m2.ppu_read(0x0000), 0x5E);
1077    }
1078
1079    #[test]
1080    fn waixing253_chr_ram_variant_writable_and_round_trips() {
1081        // No CHR-ROM => the banked `self.chr` is 8 KiB CHR-RAM and must be
1082        // writable through the normal banked path (regression: it was a
1083        // read-only 1 KiB stub that `ppu_write` never touched).
1084        let mut m = new_m253(synth_prg_8k(16), Box::new([]), Mirroring::Vertical).unwrap();
1085        // Default chr_low[0] == 0 -> banked CHR path (not the 4/5 escape).
1086        m.ppu_write(0x0000, 0xA5);
1087        m.ppu_write(0x0123, 0x3C);
1088        assert_eq!(m.ppu_read(0x0000), 0xA5);
1089        assert_eq!(m.ppu_read(0x0123), 0x3C);
1090        // The 8 KiB CHR-RAM must survive a save-state round trip.
1091        let blob = m.save_state();
1092        let mut m2 = new_m253(synth_prg_8k(16), Box::new([]), Mirroring::Vertical).unwrap();
1093        m2.load_state(&blob).unwrap();
1094        assert_eq!(m2.ppu_read(0x0000), 0xA5);
1095        assert_eq!(m2.ppu_read(0x0123), 0x3C);
1096    }
1097
1098    #[test]
1099    fn waixing253_chr_rom_not_writable() {
1100        // With CHR-ROM provided, `ppu_write` on the banked path is a no-op.
1101        let mut m = new_m253(synth_prg_8k(16), synth_chr_1k(64), Mirroring::Vertical).unwrap();
1102        let before = m.ppu_read(0x0010);
1103        m.ppu_write(0x0010, before.wrapping_add(1));
1104        assert_eq!(m.ppu_read(0x0010), before, "CHR-ROM must not be mutable");
1105    }
1106}