Skip to main content

rustynes_mappers/
m048_taito_tc0690.rs

1//! Taito TC0690 (iNES mapper 48) implementation.
2//!
3//! The IRQ-bearing sibling of the TC0190 (mapper 33). Same PRG/CHR banking
4//! shape — two switchable 8 KiB PRG banks (the upper two slots fixed to the
5//! last two banks), two switchable 2 KiB CHR banks, four switchable 1 KiB CHR
6//! banks — plus an **MMC3-style A12 scanline IRQ counter** and a mirroring
7//! register at `$E000` bit 6. Used by Don Doko Don 2, Flintstones 2, Jetsons,
8//! Bakushou!! Jinsei Gekijou 3.
9//!
10//! Register map (nesdev `INES_Mapper_048.xhtml`):
11//!
12//! ```text
13//!   $8000 [M... PPPP]  M = (subm) mirroring on some boards; 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//!   $C000 [IIII IIII]  IRQ latch (reload value, inverted on this board)
22//!   $C001 [....  ...]  IRQ reload (clear counter, reload on next A12 rise)
23//!   $C002 [....  ...]  IRQ enable (acknowledge + enable)
24//!   $C003 [....  ...]  IRQ disable (acknowledge + disable)
25//!   $E000 [.M.. ....]  M = mirroring (bit 6: 0 = Vertical, 1 = Horizontal)
26//! ```
27//!
28//! The IRQ counter is the MMC3 A12-edge model. The TC0690 latch byte is the
29//! reload value XOR-decremented by hardware (`value ^ 0xFF`, then the counter
30//! reloads at `latch + 1`); we model the latch as `value ^ 0xFF` so the
31//! counter behaves like an MMC3 reload of that period. The TC0690 has a
32//! 1-CPU-cycle IRQ-assert delay relative to MMC3 that we do not model exactly
33//! (close enough for every licensed game; documented in `docs/mappers.md`).
34//!
35//! See `docs/mappers.md` §Mapper coverage matrix.
36
37#![allow(clippy::cast_possible_truncation, clippy::doc_markdown)]
38
39use crate::cartridge::Mirroring;
40use crate::mapper::{Mapper, MapperCaps, MapperError};
41use alloc::{boxed::Box, vec::Vec};
42use alloc::{format, vec};
43
44const PRG_BANK_8K: usize = 0x2000;
45const CHR_BANK_1K: usize = 0x0400;
46const CHR_BANK_2K: usize = 0x0800;
47const NAMETABLE_SIZE: usize = 0x0400;
48const NAMETABLE_SIZE_U16: u16 = 0x0400;
49
50const SAVE_STATE_VERSION: u8 = 1;
51
52/// Taito TC0690 mapper (iNES mapper 48).
53#[allow(clippy::struct_excessive_bools)]
54pub struct TaitoTc0690 {
55    prg_rom: Box<[u8]>,
56    chr: Box<[u8]>,
57    vram: Box<[u8]>,
58    chr_is_ram: bool,
59    prg_bank: [u8; 2],
60    chr_2k: [u8; 2],
61    chr_1k: [u8; 4],
62    mirroring: Mirroring,
63    // MMC3-style A12 IRQ counter state.
64    irq_latch: u8,
65    irq_counter: u8,
66    irq_reload_pending: bool,
67    irq_enabled: bool,
68    irq_pending_line: bool,
69    last_a12: bool,
70    cpu_cycle: u64,
71    a12_low_cycle: u64,
72}
73
74impl TaitoTc0690 {
75    /// Construct a new Taito TC0690 mapper.
76    ///
77    /// `prg_rom` must be a non-zero multiple of 8 KiB. CHR-RAM is selected when
78    /// `chr_rom` is empty; otherwise CHR-ROM length must be a multiple of 2 KiB.
79    ///
80    /// # Errors
81    ///
82    /// Returns [`MapperError::Invalid`] when sizes don't match the constraints.
83    pub fn new(
84        prg_rom: Box<[u8]>,
85        chr_rom: Box<[u8]>,
86        mirroring: Mirroring,
87    ) -> Result<Self, MapperError> {
88        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
89            return Err(MapperError::Invalid(format!(
90                "Taito-48 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
91                prg_rom.len()
92            )));
93        }
94        let chr_is_ram = chr_rom.is_empty();
95        let chr: Box<[u8]> = if chr_is_ram {
96            vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
97        } else if chr_rom.len().is_multiple_of(CHR_BANK_2K) {
98            chr_rom
99        } else {
100            return Err(MapperError::Invalid(format!(
101                "Taito-48 expects a 2 KiB multiple of CHR; got {} bytes",
102                chr_rom.len()
103            )));
104        };
105        Ok(Self {
106            prg_rom,
107            chr,
108            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
109            chr_is_ram,
110            prg_bank: [0, 0],
111            chr_2k: [0, 0],
112            chr_1k: [0, 0, 0, 0],
113            mirroring,
114            irq_latch: 0,
115            irq_counter: 0,
116            irq_reload_pending: false,
117            irq_enabled: false,
118            irq_pending_line: false,
119            last_a12: false,
120            cpu_cycle: 0,
121            a12_low_cycle: 0,
122        })
123    }
124
125    const fn nametable_offset(&self, addr: u16) -> usize {
126        let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
127        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
128        let physical = self.mirroring.physical_bank(table);
129        physical * NAMETABLE_SIZE + local
130    }
131
132    fn read_prg(&self, addr: u16) -> u8 {
133        let bank_count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
134        let slot = (addr >> 13) & 0x03; // 0=$8000,1=$A000,2=$C000,3=$E000
135        let bank = match slot {
136            0 => self.prg_bank[0] as usize,
137            1 => self.prg_bank[1] as usize,
138            // `saturating_sub`: a single-bank (8 KiB) PRG image is accepted by
139            // the constructor, and a bare `- 2` underflows on that untrusted-ROM
140            // path (panic under overflow checks; only the later `% bank_count`
141            // saves release builds).
142            2 => bank_count.saturating_sub(2),
143            _ => bank_count - 1, // `.max(1)` makes this safe
144        } % bank_count;
145        let off = (addr as usize) & (PRG_BANK_8K - 1);
146        self.prg_rom[bank * PRG_BANK_8K + off]
147    }
148
149    fn chr_offset(&self, addr: u16) -> usize {
150        let len = self.chr.len().max(1);
151        match addr {
152            0x0000..=0x07FF => {
153                let base = (self.chr_2k[0] as usize) * CHR_BANK_2K;
154                (base + (addr as usize & (CHR_BANK_2K - 1))) % len
155            }
156            0x0800..=0x0FFF => {
157                let base = (self.chr_2k[1] as usize) * CHR_BANK_2K;
158                (base + (addr as usize & (CHR_BANK_2K - 1))) % len
159            }
160            _ => {
161                let idx = ((addr >> 10) & 0x03) as usize;
162                let base = (self.chr_1k[idx] as usize) * CHR_BANK_1K;
163                (base + (addr as usize & (CHR_BANK_1K - 1))) % len
164            }
165        }
166    }
167
168    /// Clock the MMC3-style IRQ counter on a filtered A12 rising edge.
169    /// Returns `true` if the counter transitioned to zero (assert).
170    const fn clock_irq(&mut self) -> bool {
171        if self.irq_counter == 0 || self.irq_reload_pending {
172            self.irq_counter = self.irq_latch;
173            self.irq_reload_pending = false;
174        } else {
175            self.irq_counter = self.irq_counter.wrapping_sub(1);
176        }
177        self.irq_counter == 0 && self.irq_enabled
178    }
179}
180
181impl Mapper for TaitoTc0690 {
182    // v2.8.0 Phase 4 — CPU-cycle hook + IRQ source; no on-cart audio.
183    fn caps(&self) -> MapperCaps {
184        MapperCaps::CYCLE_IRQ
185    }
186
187    fn cpu_read(&mut self, addr: u16) -> u8 {
188        if (0x8000..=0xFFFF).contains(&addr) {
189            self.read_prg(addr)
190        } else {
191            0
192        }
193    }
194
195    fn cpu_write(&mut self, addr: u16, value: u8) {
196        if !(0x8000..=0xFFFF).contains(&addr) {
197            return;
198        }
199        match addr & 0xE003 {
200            0x8000 => self.prg_bank[0] = value & 0x3F,
201            0x8001 => self.prg_bank[1] = value & 0x3F,
202            0x8002 => self.chr_2k[0] = value,
203            0x8003 => self.chr_2k[1] = value,
204            0xA000 => self.chr_1k[0] = value,
205            0xA001 => self.chr_1k[1] = value,
206            0xA002 => self.chr_1k[2] = value,
207            0xA003 => self.chr_1k[3] = value,
208            0xC000 => {
209                // The TC0690 latch is the one's-complement of the reload value.
210                self.irq_latch = value ^ 0xFF;
211            }
212            0xC001 => {
213                self.irq_counter = 0;
214                self.irq_reload_pending = true;
215            }
216            0xC002 => {
217                self.irq_enabled = true;
218            }
219            0xC003 => {
220                self.irq_enabled = false;
221                self.irq_pending_line = false;
222            }
223            0xE000 => {
224                self.mirroring = if (value & 0x40) != 0 {
225                    Mirroring::Horizontal
226                } else {
227                    Mirroring::Vertical
228                };
229            }
230            _ => {}
231        }
232    }
233
234    fn ppu_read(&mut self, addr: u16) -> u8 {
235        let addr = addr & 0x3FFF;
236        match addr {
237            0x0000..=0x1FFF => self.chr[self.chr_offset(addr)],
238            0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
239            _ => 0,
240        }
241    }
242
243    fn ppu_write(&mut self, addr: u16, value: u8) {
244        let addr = addr & 0x3FFF;
245        match addr {
246            0x0000..=0x1FFF => {
247                if self.chr_is_ram {
248                    let off = self.chr_offset(addr);
249                    self.chr[off] = value;
250                }
251            }
252            0x2000..=0x3EFF => {
253                let off = self.nametable_offset(addr);
254                self.vram[off] = value;
255            }
256            _ => {}
257        }
258    }
259
260    fn current_mirroring(&self) -> Mirroring {
261        self.mirroring
262    }
263
264    fn notify_a12(&mut self, level: bool) {
265        // MMC3 A12 filter: a rising edge < 3 CPU cycles after the prior fall
266        // is filtered. The TC0690 uses the same A12-edge counter mechanism.
267        if !self.last_a12 && level {
268            let gap = self.cpu_cycle.saturating_sub(self.a12_low_cycle);
269            if gap >= 3 && self.clock_irq() {
270                self.irq_pending_line = true;
271            }
272        } else if self.last_a12 && !level {
273            self.a12_low_cycle = self.cpu_cycle;
274        }
275        self.last_a12 = level;
276    }
277
278    fn notify_cpu_cycle(&mut self) {
279        self.cpu_cycle = self.cpu_cycle.wrapping_add(1);
280    }
281
282    fn irq_pending(&self) -> bool {
283        self.irq_pending_line
284    }
285
286    fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
287        let mut info = crate::mapper::MapperDebugInfo {
288            mapper_id: 48,
289            name: "Taito TC0690 (48)".into(),
290            mirroring: crate::mapper::mirroring_name(self.mirroring),
291            ..Default::default()
292        };
293        for (i, b) in self.prg_bank.iter().enumerate() {
294            info.prg_banks
295                .push((format!("PRG{i}"), format!("{b:#04x}")));
296        }
297        for (i, b) in self.chr_2k.iter().enumerate() {
298            info.chr_banks
299                .push((format!("CHR2k{i}"), format!("{b:#04x}")));
300        }
301        for (i, b) in self.chr_1k.iter().enumerate() {
302            info.chr_banks
303                .push((format!("CHR1k{i}"), format!("{b:#04x}")));
304        }
305        info.irq_state
306            .push(("counter".into(), format!("{:#04x}", self.irq_counter)));
307        info.irq_state
308            .push(("latch".into(), format!("{:#04x}", self.irq_latch)));
309        info.irq_state
310            .push(("enabled".into(), format!("{}", u8::from(self.irq_enabled))));
311        info
312    }
313
314    fn save_state(&self) -> Vec<u8> {
315        let mut out = Vec::with_capacity(
316            64 + self.vram.len() + if self.chr_is_ram { self.chr.len() } else { 0 },
317        );
318        out.push(SAVE_STATE_VERSION);
319        out.extend_from_slice(&self.prg_bank);
320        out.extend_from_slice(&self.chr_2k);
321        out.extend_from_slice(&self.chr_1k);
322        out.push(u8::from(self.mirroring == Mirroring::Horizontal));
323        out.push(self.irq_latch);
324        out.push(self.irq_counter);
325        out.push(u8::from(self.irq_reload_pending));
326        out.push(u8::from(self.irq_enabled));
327        out.push(u8::from(self.irq_pending_line));
328        out.push(u8::from(self.last_a12));
329        out.extend_from_slice(&self.cpu_cycle.to_le_bytes());
330        out.extend_from_slice(&self.a12_low_cycle.to_le_bytes());
331        out.extend_from_slice(&self.vram);
332        if self.chr_is_ram {
333            out.extend_from_slice(&self.chr);
334        }
335        out
336    }
337
338    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
339        let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
340        // 1 ver + 2 prg + 2 chr2k + 4 chr1k + 1 mir + 1 latch + 1 counter
341        //   + 1 reload + 1 enabled + 1 pending + 1 last_a12 + 8 cpu + 8 low
342        //   = 32 header bytes.
343        let header = 32;
344        let expected = header + self.vram.len() + need_chr;
345        if data.len() != expected {
346            return Err(MapperError::WrongLength {
347                expected,
348                got: data.len(),
349            });
350        }
351        if data[0] != SAVE_STATE_VERSION {
352            return Err(MapperError::UnsupportedVersion(data[0]));
353        }
354        self.prg_bank.copy_from_slice(&data[1..3]);
355        self.chr_2k.copy_from_slice(&data[3..5]);
356        self.chr_1k.copy_from_slice(&data[5..9]);
357        self.mirroring = if data[9] != 0 {
358            Mirroring::Horizontal
359        } else {
360            Mirroring::Vertical
361        };
362        self.irq_latch = data[10];
363        self.irq_counter = data[11];
364        self.irq_reload_pending = data[12] != 0;
365        self.irq_enabled = data[13] != 0;
366        self.irq_pending_line = data[14] != 0;
367        self.last_a12 = data[15] != 0;
368        self.cpu_cycle = u64::from_le_bytes(data[16..24].try_into().unwrap());
369        self.a12_low_cycle = u64::from_le_bytes(data[24..32].try_into().unwrap());
370        let mut cursor = header;
371        self.vram
372            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
373        cursor += self.vram.len();
374        if self.chr_is_ram {
375            self.chr
376                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
377        }
378        Ok(())
379    }
380}
381
382#[cfg(test)]
383#[allow(clippy::cast_possible_truncation)]
384mod tests {
385    use super::*;
386
387    fn synth_prg(banks_8k: usize) -> Box<[u8]> {
388        let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
389        for b in 0..banks_8k {
390            v[b * PRG_BANK_8K] = b as u8;
391        }
392        v.into_boxed_slice()
393    }
394
395    fn synth_chr_1k(banks: usize) -> Box<[u8]> {
396        let mut v = vec![0u8; banks * CHR_BANK_1K];
397        for b in 0..banks {
398            v[b * CHR_BANK_1K] = b as u8;
399        }
400        v.into_boxed_slice()
401    }
402
403    fn pulse_a12(m: &mut TaitoTc0690) {
404        // Advance CPU cycles so the A12 filter gap is satisfied, then rise.
405        m.notify_a12(false);
406        for _ in 0..4 {
407            m.notify_cpu_cycle();
408        }
409        m.notify_a12(true);
410    }
411
412    #[test]
413    fn prg_banks_and_fixed_tail() {
414        let mut m = TaitoTc0690::new(synth_prg(8), synth_chr_1k(8), Mirroring::Vertical).unwrap();
415        assert_eq!(m.cpu_read(0x8000), 0);
416        assert_eq!(m.cpu_read(0xA000), 0);
417        assert_eq!(m.cpu_read(0xC000), 6);
418        assert_eq!(m.cpu_read(0xE000), 7);
419        m.cpu_write(0x8000, 0x03);
420        m.cpu_write(0x8001, 0x05);
421        assert_eq!(m.cpu_read(0x8000), 3);
422        assert_eq!(m.cpu_read(0xA000), 5);
423    }
424
425    #[test]
426    fn mirroring_at_e000_bit6() {
427        let mut m = TaitoTc0690::new(synth_prg(4), synth_chr_1k(8), Mirroring::Vertical).unwrap();
428        m.cpu_write(0xE000, 0x40);
429        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
430        m.cpu_write(0xE000, 0x00);
431        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
432    }
433
434    #[test]
435    fn chr_2k_and_1k_banks() {
436        let mut m = TaitoTc0690::new(synth_prg(4), synth_chr_1k(16), Mirroring::Vertical).unwrap();
437        m.cpu_write(0x8002, 0x02); // 2 KiB bank 2 = 1 KiB idx 4
438        assert_eq!(m.ppu_read(0x0000), 4);
439        m.cpu_write(0xA000, 0x09);
440        assert_eq!(m.ppu_read(0x1000), 9);
441        m.cpu_write(0xA003, 0x0B);
442        assert_eq!(m.ppu_read(0x1C00), 11);
443    }
444
445    #[test]
446    fn irq_counts_down_and_asserts() {
447        let mut m = TaitoTc0690::new(synth_prg(4), synth_chr_1k(8), Mirroring::Vertical).unwrap();
448        // Latch value 0xFE -> latch byte = 0xFE ^ 0xFF = 0x01 (period 1).
449        m.cpu_write(0xC000, 0xFE);
450        m.cpu_write(0xC001, 0x00); // reload pending
451        m.cpu_write(0xC002, 0x00); // enable
452        // First filtered rise: reload counter to latch (1).
453        pulse_a12(&mut m);
454        assert!(!m.irq_pending());
455        // Second filtered rise: decrement 1 -> 0, assert.
456        pulse_a12(&mut m);
457        assert!(m.irq_pending());
458        // Disable acknowledges the line.
459        m.cpu_write(0xC003, 0x00);
460        assert!(!m.irq_pending());
461    }
462
463    #[test]
464    fn irq_filter_rejects_close_rise() {
465        let mut m = TaitoTc0690::new(synth_prg(4), synth_chr_1k(8), Mirroring::Vertical).unwrap();
466        m.cpu_write(0xC000, 0xFF); // latch = 0x00 (period 0)
467        m.cpu_write(0xC001, 0x00);
468        m.cpu_write(0xC002, 0x00);
469        // Rise too soon after fall (gap < 3): filtered, no clock.
470        m.notify_a12(false);
471        m.notify_cpu_cycle();
472        m.notify_a12(true);
473        assert!(!m.irq_pending());
474    }
475
476    #[test]
477    fn save_state_round_trip() {
478        let mut m = TaitoTc0690::new(synth_prg(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
479        m.cpu_write(0x8000, 0x04);
480        m.cpu_write(0x8002, 0x03);
481        m.cpu_write(0xC000, 0xF0);
482        m.cpu_write(0xC002, 0x00);
483        m.cpu_write(0xE000, 0x40);
484        pulse_a12(&mut m);
485        let blob = m.save_state();
486        let mut m2 = TaitoTc0690::new(synth_prg(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
487        m2.load_state(&blob).unwrap();
488        assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
489        assert_eq!(m.ppu_read(0x0000), m2.ppu_read(0x0000));
490        assert_eq!(m.current_mirroring(), m2.current_mirroring());
491        assert_eq!(m.irq_pending(), m2.irq_pending());
492    }
493}