Skip to main content

rustynes_mappers/
m118_txsrom.rs

1//! TxSROM / MMC3-TLSROM (iNES mapper 118) implementation.
2//!
3//! TxSROM (TKSROM + TLSROM boards) is a standard Nintendo MMC3 used in a
4//! nonstandard way: the CHR A17 line is wired directly to CIRAM A10 instead of
5//! the MMC3's own CIRAM A10 output. The practical effect is that **bit 7 of
6//! each CHR bank register selects which physical nametable backs the
7//! corresponding nametable slot** — programs choose per-slot mirroring exactly
8//! the way they choose CHR banks (`nesdev_wiki/INES_Mapper_118.xhtml`).
9//!
10//! Everything else — PRG/CHR banking, the A12-edge scanline IRQ, the `$C000`
11//! reload / `$E000` enable protocol — is byte-for-byte the MMC3, so this
12//! mapper **embeds an [`Mmc3`]** and delegates all of its behaviour, snooping
13//! only the bank-select / bank-data writes to maintain its own copy of the six
14//! CHR registers (and the CHR A12-inversion mode bit) so it can derive the
15//! per-slot nametable mapping. The `$A000` mirroring register is a no-op on
16//! these boards (its effect is bypassed by the CHR-A17 wiring).
17//!
18//! # Per-slot nametable mapping
19//!
20//! With CHR mode 0 (`$8000` bit 7 = 0), the two 2 KiB CHR banks (R0/R1) cover
21//! pattern `$0000-$0FFF`; bit 7 of R0 selects the nametable for slots 0/1
22//! (`$2000`/`$2400`), bit 7 of R1 selects slots 2/3 (`$2800`/`$2C00`). With
23//! CHR mode 1 (bit 7 = 1) the four 1 KiB banks (R2-R5) cover `$0000-$0FFF`,
24//! and bit 7 of R2/R3/R4/R5 selects the nametable for slots 0/1/2/3
25//! individually (enabling all four mirroring layouts).
26
27#![allow(
28    clippy::cast_possible_truncation,
29    clippy::cast_lossless,
30    clippy::missing_const_for_fn,
31    clippy::doc_markdown
32)]
33
34use crate::cartridge::Mirroring;
35use crate::m004_mmc3::Mmc3;
36use crate::mapper::{Mapper, MapperCaps, MapperDebugInfo, MapperError};
37use alloc::format;
38use alloc::{boxed::Box, vec::Vec};
39
40const NAMETABLE_SIZE_U16: u16 = 0x0400;
41
42const SAVE_STATE_VERSION: u8 = 1;
43
44/// TxSROM / TLSROM mapper (iNES mapper 118).
45pub struct TxSrom {
46    inner: Mmc3,
47    // Snooped copies of the six CHR bank registers + the selected register
48    // index + the CHR mode bit, used only to derive per-slot nametable
49    // mirroring. (The MMC3 owns the authoritative copies for banking/IRQ.)
50    chr_regs: [u8; 6],
51    bank_select: u8,
52    chr_mode: bool,
53}
54
55impl TxSrom {
56    /// Construct a new TxSROM mapper. Arguments mirror [`Mmc3::new`] minus the
57    /// revision (TxSROM is always a stock MMC3).
58    ///
59    /// # Errors
60    ///
61    /// Returns [`MapperError::Invalid`] on size mismatch (propagated from the
62    /// embedded MMC3).
63    pub fn new(
64        prg_rom: Box<[u8]>,
65        chr_rom: Box<[u8]>,
66        initial_mirroring: Mirroring,
67        prg_ram_bytes: usize,
68    ) -> Result<Self, MapperError> {
69        let inner = Mmc3::new(
70            prg_rom,
71            chr_rom,
72            initial_mirroring,
73            prg_ram_bytes,
74            crate::m004_mmc3::Mmc3Revision::Sharp,
75        )?;
76        Ok(Self {
77            inner,
78            chr_regs: [0; 6],
79            bank_select: 0,
80            chr_mode: false,
81        })
82    }
83
84    /// Resolve a nametable slot (0..=3) to a physical CIRAM bank (0 or 1)
85    /// using bit 7 of the CHR bank register that maps to that slot.
86    fn nt_bank(&self, slot: u8) -> usize {
87        // The register whose bit 7 controls this slot depends on the CHR mode.
88        let reg = if self.chr_mode {
89            // Mode 1: four 1 KiB banks R2-R5 cover $0000-$0FFF; one per slot.
90            match slot {
91                0 => self.chr_regs[2],
92                1 => self.chr_regs[3],
93                2 => self.chr_regs[4],
94                _ => self.chr_regs[5],
95            }
96        } else {
97            // Mode 0: two 2 KiB banks R0/R1 cover $0000-$0FFF; R0 -> slots
98            // 0/1, R1 -> slots 2/3.
99            if slot < 2 {
100                self.chr_regs[0]
101            } else {
102                self.chr_regs[1]
103            }
104        };
105        usize::from((reg & 0x80) != 0)
106    }
107}
108
109impl Mapper for TxSrom {
110    // Battery save lives in the wrapped MMC3's PRG-RAM (core audit IMP-10):
111    // without forwarding, TxSROM carts such as Ys III wrote an empty `.sav`.
112    fn sram(&self) -> &[u8] {
113        self.inner.sram()
114    }
115    fn sram_mut(&mut self) -> &mut [u8] {
116        self.inner.sram_mut()
117    }
118
119    // v2.8.0 Phase 4 — CPU-cycle hook + IRQ source; no on-cart audio.
120    fn caps(&self) -> MapperCaps {
121        MapperCaps::CYCLE_IRQ
122    }
123
124    fn cpu_read(&mut self, addr: u16) -> u8 {
125        self.inner.cpu_read(addr)
126    }
127
128    fn cpu_write(&mut self, addr: u16, value: u8) {
129        // Snoop the bank-select / bank-data registers for the nametable bits.
130        if let 0x8000..=0x9FFF = addr {
131            if addr & 1 == 0 {
132                self.bank_select = value & 0x07;
133                self.chr_mode = (value & 0x80) != 0;
134            } else {
135                let idx = (self.bank_select & 0x07) as usize;
136                if idx < 6 {
137                    self.chr_regs[idx] = value;
138                }
139            }
140        }
141        self.inner.cpu_write(addr, value);
142    }
143
144    fn cpu_read_unmapped(&self, addr: u16) -> bool {
145        self.inner.cpu_read_unmapped(addr)
146    }
147
148    fn ppu_read(&mut self, addr: u16) -> u8 {
149        self.inner.ppu_read(addr)
150    }
151
152    fn ppu_write(&mut self, addr: u16, value: u8) {
153        self.inner.ppu_write(addr, value);
154    }
155
156    fn nametable_address(&self, addr: u16) -> u16 {
157        // Per-slot nametable selection via CHR bank bit 7 (the CHR-A17 ->
158        // CIRAM-A10 wiring). Overrides the MMC3's H/V mirroring entirely.
159        let table = (((addr.wrapping_sub(0x2000)) / NAMETABLE_SIZE_U16) & 0x03) as u8;
160        let local = addr & (NAMETABLE_SIZE_U16 - 1);
161        let bank = self.nt_bank(table) as u16;
162        bank * NAMETABLE_SIZE_U16 + local
163    }
164
165    fn current_mirroring(&self) -> Mirroring {
166        // The effective mirroring is per-slot and dynamic; report
167        // MapperControlled so the bus uses our `nametable_address` override.
168        Mirroring::MapperControlled
169    }
170
171    fn notify_a12(&mut self, level: bool) {
172        self.inner.notify_a12(level);
173    }
174
175    fn notify_a12_at_sub_dot(&mut self, level: bool, sub_dot: u8) {
176        self.inner.notify_a12_at_sub_dot(level, sub_dot);
177    }
178
179    fn notify_cpu_cycle(&mut self) {
180        self.inner.notify_cpu_cycle();
181    }
182
183    fn irq_pending(&self) -> bool {
184        self.inner.irq_pending()
185    }
186
187    fn irq_acknowledge(&mut self) {
188        self.inner.irq_acknowledge();
189    }
190
191    fn debug_info(&self) -> MapperDebugInfo {
192        let mut info = self.inner.debug_info();
193        info.mapper_id = 118;
194        info.name = "TxSROM / TLSROM (118)".into();
195        info.mirroring = "MapperControlled";
196        for slot in 0u8..4 {
197            info.extra
198                .push((format!("NT{slot}"), format!("{}", self.nt_bank(slot))));
199        }
200        info
201    }
202
203    fn save_state(&self) -> Vec<u8> {
204        // Our own small header (version + snooped state) followed by the
205        // embedded MMC3's full save state.
206        let inner = self.inner.save_state();
207        let mut out = Vec::with_capacity(1 + 6 + 1 + 1 + inner.len());
208        out.push(SAVE_STATE_VERSION);
209        out.extend_from_slice(&self.chr_regs);
210        out.push(self.bank_select);
211        out.push(u8::from(self.chr_mode));
212        out.extend_from_slice(&inner);
213        out
214    }
215
216    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
217        const HEADER: usize = 1 + 6 + 1 + 1;
218        if data.len() < HEADER {
219            return Err(MapperError::WrongLength {
220                expected: HEADER,
221                got: data.len(),
222            });
223        }
224        if data[0] != SAVE_STATE_VERSION {
225            return Err(MapperError::UnsupportedVersion(data[0]));
226        }
227        self.chr_regs.copy_from_slice(&data[1..7]);
228        self.bank_select = data[7];
229        self.chr_mode = data[8] != 0;
230        self.inner.load_state(&data[HEADER..])
231    }
232}
233
234#[cfg(test)]
235#[allow(clippy::cast_possible_truncation)]
236mod tests {
237    use super::*;
238    use alloc::vec;
239
240    fn synth_prg(banks_8k: usize) -> Box<[u8]> {
241        let mut v = vec![0u8; banks_8k * 0x2000];
242        for b in 0..banks_8k {
243            v[b * 0x2000] = b as u8;
244        }
245        v.into_boxed_slice()
246    }
247
248    fn synth_chr(banks_1k: usize) -> Box<[u8]> {
249        let mut v = vec![0u8; banks_1k * 0x0400];
250        for b in 0..banks_1k {
251            v[b * 0x0400] = b as u8;
252        }
253        v.into_boxed_slice()
254    }
255
256    fn fresh() -> TxSrom {
257        TxSrom::new(synth_prg(8), synth_chr(64), Mirroring::Vertical, 0).unwrap()
258    }
259
260    fn select_write(m: &mut TxSrom, reg: u8, value: u8) {
261        m.cpu_write(0x8000, reg);
262        m.cpu_write(0x8001, value);
263    }
264
265    #[test]
266    fn delegates_prg_banking_to_mmc3() {
267        let mut m = fresh();
268        // Last bank fixed at $E000 just like MMC3.
269        assert_eq!(m.cpu_read(0xE000), 7);
270        select_write(&mut m, 6, 3); // R6 = 3
271        assert_eq!(m.cpu_read(0x8000), 3);
272    }
273
274    #[test]
275    fn reports_mapper_controlled_mirroring() {
276        let m = fresh();
277        assert_eq!(m.current_mirroring(), Mirroring::MapperControlled);
278    }
279
280    #[test]
281    fn mode0_nametable_bits_from_r0_r1() {
282        let mut m = fresh();
283        // CHR mode 0 (bit 7 clear). R0 bit 7 set -> slots 0/1 use bank 1.
284        select_write(&mut m, 0, 0x80); // R0 = $80
285        select_write(&mut m, 1, 0x00); // R1 = $00
286        // Slot 0 ($2000) and slot 1 ($2400) -> bank 1.
287        assert_eq!(m.nametable_address(0x2000) >> 10, 1);
288        assert_eq!(m.nametable_address(0x2400) >> 10, 1);
289        // Slot 2 ($2800) and slot 3 ($2C00) -> bank 0 (R1 bit 7 clear).
290        assert_eq!(m.nametable_address(0x2800) >> 10, 0);
291        assert_eq!(m.nametable_address(0x2C00) >> 10, 0);
292    }
293
294    #[test]
295    fn mode1_nametable_bits_from_r2_r5() {
296        let mut m = fresh();
297        // CHR mode 1: set bit 7 of $8000 to enable. Then R2-R5 each control
298        // one slot.
299        m.cpu_write(0x8000, 0x80 | 2); // mode 1, select R2
300        m.cpu_write(0x8001, 0x80); // R2 bit 7 -> slot 0 bank 1
301        m.cpu_write(0x8000, 0x80 | 3); // select R3
302        m.cpu_write(0x8001, 0x00); // R3 -> slot 1 bank 0
303        m.cpu_write(0x8000, 0x80 | 4);
304        m.cpu_write(0x8001, 0x80); // R4 -> slot 2 bank 1
305        m.cpu_write(0x8000, 0x80 | 5);
306        m.cpu_write(0x8001, 0x00); // R5 -> slot 3 bank 0
307        assert_eq!(m.nametable_address(0x2000) >> 10, 1);
308        assert_eq!(m.nametable_address(0x2400) >> 10, 0);
309        assert_eq!(m.nametable_address(0x2800) >> 10, 1);
310        assert_eq!(m.nametable_address(0x2C00) >> 10, 0);
311    }
312
313    #[test]
314    fn irq_delegates_to_mmc3() {
315        let mut m = fresh();
316        m.cpu_write(0xC000, 3);
317        m.cpu_write(0xC001, 0);
318        m.cpu_write(0xE001, 0);
319        for _ in 0..5 {
320            m.notify_a12(false);
321            for _ in 0..4 {
322                m.notify_cpu_cycle();
323            }
324            m.notify_a12(true);
325        }
326        assert!(m.irq_pending());
327    }
328
329    #[test]
330    fn save_state_round_trip() {
331        let mut m = fresh();
332        select_write(&mut m, 0, 0x80);
333        select_write(&mut m, 6, 3);
334        m.cpu_write(0xC000, 0x10);
335        let blob = m.save_state();
336        let mut m2 = fresh();
337        m2.load_state(&blob).unwrap();
338        assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
339        assert_eq!(m.nametable_address(0x2000), m2.nametable_address(0x2000));
340    }
341}