Skip to main content

rustynes_mappers/
m244_cne_decathlon.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//
3// Provenance: the two data-byte scramble tables (`LUT_PRG`, `LUT_CHR`) match Mesen2 (GPL-3.0-or-later) `Mapper244` and puNES (GPL-2.0-or-later) `mapper_244`, per the in-file comment, and are treated as derived from them. Classified as derived in v2.7.1 (core audit section 6.2). See docs/originality-and-provenance.md (Section 1)
4// and NOTICE for the complete, audited derivation record.
5
6//! C&E Decathlon (mapper 244).
7//!
8//! A value-decoded sibling of the C&E multicart in `m240_cne_multicart.rs`:
9//! the *written byte* selects the PRG and CHR banks through lookup tables
10//! rather than supplying the bank number directly.
11//!
12//! A discrete-logic board in the shape of the stock mappers (`NROM`, `CNROM`,
13//! `UxROM`, `GxROM`, `AxROM`): bank-select latch registers, no IRQ, no on-cart
14//! audio. Banking / mirroring semantics are cross-checked against the
15//! `GeraNES` reference emulator (cross-referenced, not copied)
16//! and the nesdev wiki, and validated by register-decode + save-state unit
17//! tests.
18//!
19//! See `docs/mappers.md` §Mapper coverage matrix.
20
21#![allow(
22    clippy::bool_to_int_with_if,
23    clippy::cast_lossless,
24    clippy::cast_possible_truncation,
25    clippy::doc_markdown,
26    clippy::match_same_arms,
27    clippy::missing_const_for_fn,
28    clippy::similar_names,
29    clippy::struct_excessive_bools,
30    clippy::too_many_lines,
31    clippy::unreadable_literal
32)]
33
34use crate::cartridge::Mirroring;
35use crate::mapper::{Mapper, MapperCaps, MapperError};
36use alloc::{boxed::Box, vec::Vec};
37use alloc::{format, vec};
38
39const PRG_BANK_32K: usize = 0x8000;
40const CHR_BANK_8K: usize = 0x2000;
41const NAMETABLE_SIZE: usize = 0x0400;
42const NAMETABLE_SIZE_U16: u16 = 0x0400;
43
44const SAVE_STATE_VERSION: u8 = 1;
45
46const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
47    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
48    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
49    let physical = mirroring.physical_bank(table);
50    physical * NAMETABLE_SIZE + local
51}
52
53/// Mapper 244 (Decathlon).
54pub struct Decathlon244 {
55    prg_rom: Box<[u8]>,
56    chr_rom: Box<[u8]>,
57    vram: Box<[u8]>,
58    prg_bank: u8,
59    chr_bank: u8,
60    mirroring: Mirroring,
61}
62
63impl Decathlon244 {
64    /// Construct a new mapper 244 board.
65    ///
66    /// # Errors
67    ///
68    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
69    /// 32 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
70    pub fn new(
71        prg_rom: Box<[u8]>,
72        chr_rom: Box<[u8]>,
73        mirroring: Mirroring,
74    ) -> Result<Self, MapperError> {
75        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
76            return Err(MapperError::Invalid(format!(
77                "mapper 244 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
78                prg_rom.len()
79            )));
80        }
81        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
82            return Err(MapperError::Invalid(format!(
83                "mapper 244 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
84                chr_rom.len()
85            )));
86        }
87        Ok(Self {
88            prg_rom,
89            chr_rom,
90            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
91            prg_bank: 0,
92            chr_bank: 0,
93            mirroring,
94        })
95    }
96}
97
98impl Mapper for Decathlon244 {
99    fn caps(&self) -> MapperCaps {
100        MapperCaps::NONE
101    }
102
103    fn cpu_read(&mut self, addr: u16) -> u8 {
104        if (0x8000..=0xFFFF).contains(&addr) {
105            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
106            let bank = (self.prg_bank as usize) % count;
107            self.prg_rom[bank * PRG_BANK_32K + (addr as usize & 0x7FFF)]
108        } else {
109            0
110        }
111    }
112
113    fn cpu_write(&mut self, addr: u16, value: u8) {
114        // Mapper 244 decodes the written DATA byte (not the address) through two
115        // scramble LUTs, selecting CHR vs PRG by bit 3:
116        //   value & 0x08 != 0 -> CHR 8 KiB = LUT_CHR[(value>>4)&7][value&7]
117        //   else              -> PRG 32 KiB = LUT_PRG[(value>>4)&3][value&3]
118        // The old code ignored the data byte and decoded address bits with no
119        // scramble, so it banked to the wrong PRG/CHR and the menu never drew.
120        // (Mesen2 Mapper244 / puNES mapper_244 carry the identical tables.)
121        const LUT_PRG: [[u8; 4]; 4] = [[0, 1, 2, 3], [3, 2, 1, 0], [0, 2, 1, 3], [3, 1, 2, 0]];
122        const LUT_CHR: [[u8; 8]; 8] = [
123            [0, 1, 2, 3, 4, 5, 6, 7],
124            [0, 2, 1, 3, 4, 6, 5, 7],
125            [0, 1, 4, 5, 2, 3, 6, 7],
126            [0, 4, 1, 5, 2, 6, 3, 7],
127            [0, 4, 2, 6, 1, 5, 3, 7],
128            [0, 2, 4, 6, 1, 3, 5, 7],
129            [7, 6, 5, 4, 3, 2, 1, 0],
130            [7, 6, 5, 4, 3, 2, 1, 0],
131        ];
132        if (0x8000..=0xFFFF).contains(&addr) {
133            if value & 0x08 != 0 {
134                self.chr_bank = LUT_CHR[((value >> 4) & 0x07) as usize][(value & 0x07) as usize];
135            } else {
136                self.prg_bank = LUT_PRG[((value >> 4) & 0x03) as usize][(value & 0x03) as usize];
137            }
138        }
139    }
140
141    fn ppu_read(&mut self, addr: u16) -> u8 {
142        let addr = addr & 0x3FFF;
143        match addr {
144            0x0000..=0x1FFF => {
145                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
146                let bank = (self.chr_bank as usize) % count;
147                self.chr_rom[bank * CHR_BANK_8K + (addr as usize & 0x1FFF)]
148            }
149            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
150            _ => 0,
151        }
152    }
153
154    fn ppu_write(&mut self, addr: u16, value: u8) {
155        let addr = addr & 0x3FFF;
156        if (0x2000..=0x3EFF).contains(&addr) {
157            let off = nametable_offset(addr, self.mirroring);
158            self.vram[off] = value;
159        }
160    }
161
162    fn current_mirroring(&self) -> Mirroring {
163        self.mirroring
164    }
165
166    fn save_state(&self) -> Vec<u8> {
167        let mut out = Vec::with_capacity(3 + self.vram.len());
168        out.push(SAVE_STATE_VERSION);
169        out.push(self.prg_bank);
170        out.push(self.chr_bank);
171        out.extend_from_slice(&self.vram);
172        out
173    }
174
175    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
176        let expected = 3 + self.vram.len();
177        if data.len() != expected {
178            return Err(MapperError::WrongLength {
179                expected,
180                got: data.len(),
181            });
182        }
183        if data[0] != SAVE_STATE_VERSION {
184            return Err(MapperError::UnsupportedVersion(data[0]));
185        }
186        self.prg_bank = data[1];
187        self.chr_bank = data[2];
188        self.vram.copy_from_slice(&data[3..3 + self.vram.len()]);
189        Ok(())
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    fn synth_prg_32k(banks: usize) -> Box<[u8]> {
198        let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
199        for b in 0..banks {
200            v[b * PRG_BANK_32K] = b as u8;
201        }
202        v.into_boxed_slice()
203    }
204
205    fn synth_chr_8k(banks: usize) -> Box<[u8]> {
206        let mut v = vec![0u8; banks * CHR_BANK_8K];
207        for b in 0..banks {
208            v[b * CHR_BANK_8K] = b as u8;
209        }
210        v.into_boxed_slice()
211    }
212
213    #[test]
214    fn m244_value_decoded_banks() {
215        let mut m =
216            Decathlon244::new(synth_prg_32k(4), synth_chr_8k(8), Mirroring::Vertical).unwrap();
217        // PRG select (value & 0x08 == 0): value 0x11 -> LUT_PRG[(1)][1] = 2.
218        m.cpu_write(0x8000, 0x11);
219        assert_eq!(m.cpu_read(0x8000), 2);
220        // value 0x30 -> LUT_PRG[3][0] = 3.
221        m.cpu_write(0x8000, 0x30);
222        assert_eq!(m.cpu_read(0x8000), 3);
223        // CHR select (value & 0x08 != 0): value 0x09 -> LUT_CHR[0][1] = 1.
224        m.cpu_write(0x8000, 0x09);
225        assert_eq!(m.ppu_read(0x0000), 1);
226        // value 0x6E -> LUT_CHR[6][6] = 1 (table row 6 reversed).
227        m.cpu_write(0x8000, 0x6E);
228        assert_eq!(m.ppu_read(0x0000), 1);
229    }
230
231    #[test]
232    fn m244_save_state_round_trip() {
233        let mut m =
234            Decathlon244::new(synth_prg_32k(4), synth_chr_8k(8), Mirroring::Vertical).unwrap();
235        m.cpu_write(0x8000, 0x11); // PRG = LUT_PRG[1][1] = 2
236        let blob = m.save_state();
237        let mut m2 =
238            Decathlon244::new(synth_prg_32k(4), synth_chr_8k(8), Mirroring::Vertical).unwrap();
239        m2.load_state(&blob).unwrap();
240        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
241        assert_eq!(m2.ppu_read(0x0000), m.ppu_read(0x0000));
242    }
243}