Skip to main content

rustynes_mappers/
m036_txc_policeman.rs

1//! TXC 01-22000 / Policeman (mapper 36).
2//!
3//! A bank-select register reached through the `$4100-$5FFF` expansion window
4//! on address line A8. Unlike the mapper 132 board in
5//! `m132_txc_22211.rs`, it drives its banking registers directly rather than
6//! through the TXC accumulator chip.
7//!
8//! A best-effort (Tier-2) board: register-decode correctness verified against
9//! the reference emulators (`Mesen2`, `GeraNES`) and the nesdev wiki, with no
10//! commercial-oracle ROM in the tree. Banking math is direct slice indexing and
11//! every bank select wraps with `% count`, so a register write can never index
12//! out of bounds -- required for the `#![no_std]` chip stack, which cannot
13//! afford a panic on a register access.
14//!
15//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
16//! and `docs/mappers.md` §Mapper coverage matrix.
17
18#![allow(
19    clippy::bool_to_int_with_if,
20    clippy::cast_lossless,
21    clippy::cast_possible_truncation,
22    clippy::doc_markdown,
23    clippy::match_same_arms,
24    clippy::missing_const_for_fn,
25    clippy::similar_names,
26    clippy::struct_excessive_bools,
27    clippy::too_many_lines,
28    clippy::unreadable_literal
29)]
30
31use crate::cartridge::Mirroring;
32use crate::mapper::{Mapper, MapperCaps, MapperError};
33use alloc::{boxed::Box, vec::Vec};
34use alloc::{format, vec};
35
36const PRG_BANK_32K: usize = 0x8000;
37const CHR_BANK_8K: usize = 0x2000;
38const NAMETABLE_SIZE: usize = 0x0400;
39const NAMETABLE_SIZE_U16: u16 = 0x0400;
40
41const SAVE_STATE_VERSION: u8 = 1;
42
43// ---------------------------------------------------------------------------
44// Shared nametable helper (mirrors the one in the other simple-mapper modules).
45// ---------------------------------------------------------------------------
46
47const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
48    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
49    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
50    let physical = mirroring.physical_bank(table);
51    physical * NAMETABLE_SIZE + local
52}
53
54/// Mapper 36 (TXC 01-22000 / Policeman).
55pub struct Txc36 {
56    prg_rom: Box<[u8]>,
57    chr_rom: Box<[u8]>,
58    vram: Box<[u8]>,
59    prg_bank: u8,
60    chr_bank: u8,
61    mirroring: Mirroring,
62}
63
64impl Txc36 {
65    /// Construct a new mapper 36 board.
66    ///
67    /// # Errors
68    ///
69    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
70    /// 32 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
71    pub fn new(
72        prg_rom: Box<[u8]>,
73        chr_rom: Box<[u8]>,
74        mirroring: Mirroring,
75    ) -> Result<Self, MapperError> {
76        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
77            return Err(MapperError::Invalid(format!(
78                "mapper 36 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
79                prg_rom.len()
80            )));
81        }
82        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
83            return Err(MapperError::Invalid(format!(
84                "mapper 36 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
85                chr_rom.len()
86            )));
87        }
88        Ok(Self {
89            prg_rom,
90            chr_rom,
91            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
92            prg_bank: 0,
93            chr_bank: 0,
94            mirroring,
95        })
96    }
97}
98
99impl Mapper for Txc36 {
100    fn caps(&self) -> MapperCaps {
101        MapperCaps::NONE
102    }
103
104    // Register window $4100-$5FFF is write-only; reads there fall through to
105    // open bus, so the default `cpu_read_unmapped` is correct.
106
107    fn cpu_read(&mut self, addr: u16) -> u8 {
108        if (0x8000..=0xFFFF).contains(&addr) {
109            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
110            let bank = (self.prg_bank as usize) % count;
111            self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
112        } else {
113            0
114        }
115    }
116
117    fn cpu_write(&mut self, addr: u16, value: u8) {
118        if (0x4100..=0x5FFF).contains(&addr) && (addr & 0x0100) != 0 {
119            self.prg_bank = (value >> 4) & 0x0F;
120            self.chr_bank = value & 0x0F;
121        }
122    }
123
124    fn ppu_read(&mut self, addr: u16) -> u8 {
125        let addr = addr & 0x3FFF;
126        match addr {
127            0x0000..=0x1FFF => {
128                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
129                let bank = (self.chr_bank as usize) % count;
130                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
131            }
132            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
133            _ => 0,
134        }
135    }
136
137    fn ppu_write(&mut self, addr: u16, value: u8) {
138        let addr = addr & 0x3FFF;
139        if let 0x2000..=0x3EFF = addr {
140            let off = nametable_offset(addr, self.mirroring);
141            self.vram[off] = value;
142        }
143    }
144
145    fn current_mirroring(&self) -> Mirroring {
146        self.mirroring
147    }
148
149    fn save_state(&self) -> Vec<u8> {
150        let mut out = Vec::with_capacity(3 + self.vram.len());
151        out.push(SAVE_STATE_VERSION);
152        out.push(self.prg_bank);
153        out.push(self.chr_bank);
154        out.extend_from_slice(&self.vram);
155        out
156    }
157
158    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
159        let expected = 3 + self.vram.len();
160        if data.len() != expected {
161            return Err(MapperError::WrongLength {
162                expected,
163                got: data.len(),
164            });
165        }
166        if data[0] != SAVE_STATE_VERSION {
167            return Err(MapperError::UnsupportedVersion(data[0]));
168        }
169        self.prg_bank = data[1];
170        self.chr_bank = data[2];
171        self.vram.copy_from_slice(&data[3..3 + self.vram.len()]);
172        Ok(())
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    fn synth_prg_32k(banks: usize) -> Box<[u8]> {
181        let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
182        for b in 0..banks {
183            v[b * PRG_BANK_32K] = b as u8;
184        }
185        v.into_boxed_slice()
186    }
187
188    fn synth_chr_8k(banks: usize) -> Box<[u8]> {
189        let mut v = vec![0u8; banks * CHR_BANK_8K];
190        for b in 0..banks {
191            v[b * CHR_BANK_8K] = b as u8;
192        }
193        v.into_boxed_slice()
194    }
195
196    #[test]
197    fn m36_register_decodes_on_a8() {
198        let mut m = Txc36::new(synth_prg_32k(4), synth_chr_8k(16), Mirroring::Vertical).unwrap();
199        // $4100 has A8 set: value PPPP_CCCC. 0b0011_1010 -> PRG 3, CHR 10.
200        m.cpu_write(0x4100, 0b0011_1010);
201        assert_eq!(m.cpu_read(0x8000), 3);
202        assert_eq!(m.ppu_read(0x0000), 10);
203        // In-window address with A8 clear ($4200) must not latch.
204        m.cpu_write(0x4200, 0b0000_0001);
205        assert_eq!(m.cpu_read(0x8000), 3);
206    }
207}