Skip to main content

rustynes_mappers/
m033_taito_tc0190.rs

1//! Taito TC0190 / TC0350 (iNES mapper 33) implementation.
2//!
3//! A Taito ASIC board with two switchable 8 KiB PRG banks (the upper two 8 KiB
4//! PRG slots are hardwired to the last two banks), two switchable 2 KiB CHR
5//! banks, four switchable 1 KiB CHR banks, and a software mirroring control.
6//! There is **no IRQ** — that is the very-similar mapper 48 (TC0690), which
7//! adds the MMC3-style A12 IRQ counter and inverts the mirroring bit.
8//!
9//! Register map (nesdev `INES_Mapper_033.xhtml`), `$8000-$BFFF` mask `$A003`:
10//!
11//! ```text
12//!   $8000 [.MPP PPPP]  M = mirroring (0 = Vertical, 1 = Horizontal)
13//!                      P = PRG reg 0 (8 KiB @ $8000)
14//!   $8001 [..PP PPPP]  PRG reg 1 (8 KiB @ $A000)
15//!   $8002 [CCCC CCCC]  CHR reg 0 (2 KiB @ $0000)
16//!   $8003 [CCCC CCCC]  CHR reg 1 (2 KiB @ $0800)
17//!   $A000 [CCCC CCCC]  CHR reg 2 (1 KiB @ $1000)
18//!   $A001 [CCCC CCCC]  CHR reg 3 (1 KiB @ $1400)
19//!   $A002 [CCCC CCCC]  CHR reg 4 (1 KiB @ $1800)
20//!   $A003 [CCCC CCCC]  CHR reg 5 (1 KiB @ $1C00)
21//! ```
22//!
23//! The 2 KiB CHR registers do NOT drop their LSB (unlike MMC3): the written
24//! value is the bank offset in 2 KiB units.
25//!
26//! See `docs/mappers.md` §Mapper coverage matrix.
27
28#![allow(clippy::cast_possible_truncation, clippy::doc_markdown)]
29
30use crate::cartridge::Mirroring;
31use crate::mapper::{Mapper, MapperCaps, MapperError};
32use alloc::{boxed::Box, vec::Vec};
33use alloc::{format, vec};
34
35const PRG_BANK_8K: usize = 0x2000;
36const CHR_BANK_1K: usize = 0x0400;
37const CHR_BANK_2K: usize = 0x0800;
38const NAMETABLE_SIZE: usize = 0x0400;
39const NAMETABLE_SIZE_U16: u16 = 0x0400;
40
41const SAVE_STATE_VERSION: u8 = 1;
42
43/// Taito TC0190 / TC0350 mapper (iNES mapper 33).
44pub struct TaitoTc0190 {
45    prg_rom: Box<[u8]>,
46    chr: Box<[u8]>,
47    vram: Box<[u8]>,
48    chr_is_ram: bool,
49    prg_bank: [u8; 2],
50    /// Two 2 KiB CHR banks ($8002/$8003) then four 1 KiB CHR banks ($A000-3).
51    chr_2k: [u8; 2],
52    chr_1k: [u8; 4],
53    mirroring: Mirroring,
54}
55
56impl TaitoTc0190 {
57    /// Construct a new Taito-33 mapper.
58    ///
59    /// `prg_rom` must be a non-zero multiple of 8 KiB. CHR-RAM is selected when
60    /// `chr_rom` is empty; otherwise CHR-ROM length must be a multiple of 8 KiB.
61    ///
62    /// # Errors
63    ///
64    /// Returns [`MapperError::Invalid`] when sizes don't match the constraints.
65    pub fn new(
66        prg_rom: Box<[u8]>,
67        chr_rom: Box<[u8]>,
68        mirroring: Mirroring,
69    ) -> Result<Self, MapperError> {
70        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
71            return Err(MapperError::Invalid(format!(
72                "Taito-33 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
73                prg_rom.len()
74            )));
75        }
76        let chr_is_ram = chr_rom.is_empty();
77        let chr: Box<[u8]> = if chr_is_ram {
78            vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
79        } else if chr_rom.len().is_multiple_of(CHR_BANK_2K) {
80            chr_rom
81        } else {
82            return Err(MapperError::Invalid(format!(
83                "Taito-33 expects a 2 KiB multiple of CHR; got {} bytes",
84                chr_rom.len()
85            )));
86        };
87        Ok(Self {
88            prg_rom,
89            chr,
90            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
91            chr_is_ram,
92            prg_bank: [0, 0],
93            chr_2k: [0, 0],
94            chr_1k: [0, 0, 0, 0],
95            mirroring,
96        })
97    }
98
99    const fn nametable_offset(&self, addr: u16) -> usize {
100        let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
101        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
102        let physical = self.mirroring.physical_bank(table);
103        physical * NAMETABLE_SIZE + local
104    }
105
106    fn read_prg(&self, addr: u16) -> u8 {
107        let bank_count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
108        let slot = (addr >> 13) & 0x03; // 0=$8000,1=$A000,2=$C000,3=$E000
109        let bank = match slot {
110            0 => self.prg_bank[0] as usize,
111            1 => self.prg_bank[1] as usize,
112            // `saturating_sub`: a single-bank (8 KiB) PRG image is accepted by
113            // the constructor, and a bare `- 2` underflows on that untrusted-ROM
114            // path (panic under overflow checks; only the later `% bank_count`
115            // saves release builds).
116            2 => bank_count.saturating_sub(2), // fixed second-last
117            _ => bank_count - 1,               // fixed last: `.max(1)` makes this safe
118        } % bank_count;
119        let off = (addr as usize) & (PRG_BANK_8K - 1);
120        self.prg_rom[bank * PRG_BANK_8K + off]
121    }
122
123    fn chr_offset(&self, addr: u16) -> usize {
124        let len = self.chr.len().max(1);
125        match addr {
126            0x0000..=0x07FF => {
127                let base = (self.chr_2k[0] as usize) * CHR_BANK_2K;
128                (base + (addr as usize & (CHR_BANK_2K - 1))) % len
129            }
130            0x0800..=0x0FFF => {
131                let base = (self.chr_2k[1] as usize) * CHR_BANK_2K;
132                (base + (addr as usize & (CHR_BANK_2K - 1))) % len
133            }
134            _ => {
135                // $1000-$1FFF: four 1 KiB banks.
136                let idx = ((addr >> 10) & 0x03) as usize; // 0..=3 over $1000-$1FFF
137                let base = (self.chr_1k[idx] as usize) * CHR_BANK_1K;
138                (base + (addr as usize & (CHR_BANK_1K - 1))) % len
139            }
140        }
141    }
142}
143
144impl Mapper for TaitoTc0190 {
145    // v2.8.0 Phase 4 — no per-cycle hooks (no IRQ, no audio): the bus
146    // skips all four per-CPU-cycle dispatches for this board.
147    fn caps(&self) -> MapperCaps {
148        MapperCaps::NONE
149    }
150
151    fn cpu_read(&mut self, addr: u16) -> u8 {
152        if (0x8000..=0xFFFF).contains(&addr) {
153            self.read_prg(addr)
154        } else {
155            0
156        }
157    }
158
159    fn cpu_write(&mut self, addr: u16, value: u8) {
160        if !(0x8000..=0xFFFF).contains(&addr) {
161            return;
162        }
163        // Mask $A003: registers repeat across the $8000-$FFFF window.
164        match addr & 0xA003 {
165            0x8000 => {
166                self.mirroring = if (value & 0x40) != 0 {
167                    Mirroring::Horizontal
168                } else {
169                    Mirroring::Vertical
170                };
171                self.prg_bank[0] = value & 0x3F;
172            }
173            0x8001 => self.prg_bank[1] = value & 0x3F,
174            0x8002 => self.chr_2k[0] = value,
175            0x8003 => self.chr_2k[1] = value,
176            0xA000 => self.chr_1k[0] = value,
177            0xA001 => self.chr_1k[1] = value,
178            0xA002 => self.chr_1k[2] = value,
179            0xA003 => self.chr_1k[3] = value,
180            _ => {}
181        }
182    }
183
184    fn ppu_read(&mut self, addr: u16) -> u8 {
185        let addr = addr & 0x3FFF;
186        match addr {
187            0x0000..=0x1FFF => self.chr[self.chr_offset(addr)],
188            0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
189            _ => 0,
190        }
191    }
192
193    fn ppu_write(&mut self, addr: u16, value: u8) {
194        let addr = addr & 0x3FFF;
195        match addr {
196            0x0000..=0x1FFF => {
197                if self.chr_is_ram {
198                    let off = self.chr_offset(addr);
199                    self.chr[off] = value;
200                }
201            }
202            0x2000..=0x3EFF => {
203                let off = self.nametable_offset(addr);
204                self.vram[off] = value;
205            }
206            _ => {}
207        }
208    }
209
210    fn current_mirroring(&self) -> Mirroring {
211        self.mirroring
212    }
213
214    fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
215        let mut info = crate::mapper::MapperDebugInfo {
216            mapper_id: 33,
217            name: "Taito TC0190 (33)".into(),
218            mirroring: crate::mapper::mirroring_name(self.mirroring),
219            ..Default::default()
220        };
221        for (i, b) in self.prg_bank.iter().enumerate() {
222            info.prg_banks
223                .push((format!("PRG{i}"), format!("{b:#04x}")));
224        }
225        for (i, b) in self.chr_2k.iter().enumerate() {
226            info.chr_banks
227                .push((format!("CHR2k{i}"), format!("{b:#04x}")));
228        }
229        for (i, b) in self.chr_1k.iter().enumerate() {
230            info.chr_banks
231                .push((format!("CHR1k{i}"), format!("{b:#04x}")));
232        }
233        info
234    }
235
236    fn save_state(&self) -> Vec<u8> {
237        let mut out = Vec::with_capacity(
238            10 + self.vram.len() + if self.chr_is_ram { self.chr.len() } else { 0 },
239        );
240        out.push(SAVE_STATE_VERSION);
241        out.extend_from_slice(&self.prg_bank);
242        out.extend_from_slice(&self.chr_2k);
243        out.extend_from_slice(&self.chr_1k);
244        // Persist the software mirroring select ($8000 bit 6).
245        out.push(u8::from(self.mirroring == Mirroring::Horizontal));
246        out.extend_from_slice(&self.vram);
247        if self.chr_is_ram {
248            out.extend_from_slice(&self.chr);
249        }
250        out
251    }
252
253    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
254        let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
255        let expected = 10 + self.vram.len() + need_chr;
256        if data.len() != expected {
257            return Err(MapperError::WrongLength {
258                expected,
259                got: data.len(),
260            });
261        }
262        if data[0] != SAVE_STATE_VERSION {
263            return Err(MapperError::UnsupportedVersion(data[0]));
264        }
265        self.prg_bank.copy_from_slice(&data[1..3]);
266        self.chr_2k.copy_from_slice(&data[3..5]);
267        self.chr_1k.copy_from_slice(&data[5..9]);
268        self.mirroring = if data[9] != 0 {
269            Mirroring::Horizontal
270        } else {
271            Mirroring::Vertical
272        };
273        let mut cursor = 10;
274        self.vram
275            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
276        cursor += self.vram.len();
277        if self.chr_is_ram {
278            self.chr
279                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
280        }
281        Ok(())
282    }
283}
284
285#[cfg(test)]
286#[allow(clippy::cast_possible_truncation)]
287mod tests {
288    use super::*;
289
290    fn synth_prg(banks_8k: usize) -> Box<[u8]> {
291        let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
292        for b in 0..banks_8k {
293            v[b * PRG_BANK_8K] = b as u8;
294        }
295        v.into_boxed_slice()
296    }
297
298    fn synth_chr_1k(banks: usize) -> Box<[u8]> {
299        let mut v = vec![0u8; banks * CHR_BANK_1K];
300        for b in 0..banks {
301            v[b * CHR_BANK_1K] = b as u8;
302        }
303        v.into_boxed_slice()
304    }
305
306    #[test]
307    fn prg_banks_and_fixed_tail() {
308        let mut m = TaitoTc0190::new(synth_prg(8), synth_chr_1k(8), Mirroring::Vertical).unwrap();
309        // Defaults: PRG0=0, PRG1=0, $C000={-2}=6, $E000={-1}=7.
310        assert_eq!(m.cpu_read(0x8000), 0);
311        assert_eq!(m.cpu_read(0xA000), 0);
312        assert_eq!(m.cpu_read(0xC000), 6);
313        assert_eq!(m.cpu_read(0xE000), 7);
314        // Select PRG0=3, PRG1=5.
315        m.cpu_write(0x8000, 0x03);
316        m.cpu_write(0x8001, 0x05);
317        assert_eq!(m.cpu_read(0x8000), 3);
318        assert_eq!(m.cpu_read(0xA000), 5);
319        // Fixed tail unchanged.
320        assert_eq!(m.cpu_read(0xC000), 6);
321        assert_eq!(m.cpu_read(0xE000), 7);
322    }
323
324    #[test]
325    fn mirroring_bit_6() {
326        let mut m = TaitoTc0190::new(synth_prg(4), synth_chr_1k(8), Mirroring::Vertical).unwrap();
327        m.cpu_write(0x8000, 0x40); // bit 6 set -> Horizontal
328        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
329        m.cpu_write(0x8000, 0x00); // bit 6 clear -> Vertical
330        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
331    }
332
333    #[test]
334    fn chr_2k_and_1k_banks() {
335        // 2 KiB regs do not drop the LSB: written value is the 2 KiB offset.
336        let mut m = TaitoTc0190::new(synth_prg(4), synth_chr_1k(16), Mirroring::Vertical).unwrap();
337        // CHR reg 0 (2 KiB @ $0000) -> bank 2 means 2 KiB offset 2 = 1 KiB idx 4.
338        m.cpu_write(0x8002, 0x02);
339        assert_eq!(m.ppu_read(0x0000), 4);
340        // CHR reg 2 (1 KiB @ $1000) -> 1 KiB bank 9.
341        m.cpu_write(0xA000, 0x09);
342        assert_eq!(m.ppu_read(0x1000), 9);
343        // CHR reg 5 (1 KiB @ $1C00) -> 1 KiB bank 11.
344        m.cpu_write(0xA003, 0x0B);
345        assert_eq!(m.ppu_read(0x1C00), 11);
346    }
347
348    #[test]
349    fn save_state_round_trip() {
350        let mut m = TaitoTc0190::new(synth_prg(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
351        m.cpu_write(0x8000, 0x42);
352        m.cpu_write(0x8001, 0x05);
353        m.cpu_write(0x8002, 0x03);
354        m.cpu_write(0xA001, 0x07);
355        let blob = m.save_state();
356        let mut m2 = TaitoTc0190::new(synth_prg(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
357        m2.load_state(&blob).unwrap();
358        assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
359        assert_eq!(m.cpu_read(0xA000), m2.cpu_read(0xA000));
360        assert_eq!(m.ppu_read(0x0000), m2.ppu_read(0x0000));
361        assert_eq!(m.ppu_read(0x1400), m2.ppu_read(0x1400));
362        assert_eq!(m.current_mirroring(), m2.current_mirroring());
363    }
364}