rustynes_mappers/
m132_txc_22211.rs1#![allow(
24 clippy::bool_to_int_with_if,
25 clippy::cast_lossless,
26 clippy::cast_possible_truncation,
27 clippy::doc_markdown,
28 clippy::match_same_arms,
29 clippy::missing_const_for_fn,
30 clippy::similar_names,
31 clippy::struct_excessive_bools,
32 clippy::too_many_lines,
33 clippy::unreadable_literal
34)]
35
36use crate::cartridge::Mirroring;
37use crate::mapper::{Mapper, MapperCaps, MapperError};
38use alloc::{boxed::Box, vec::Vec};
39use alloc::{format, vec};
40
41const PRG_BANK_32K: usize = 0x8000;
42const CHR_BANK_8K: usize = 0x2000;
43const NAMETABLE_SIZE: usize = 0x0400;
44const NAMETABLE_SIZE_U16: u16 = 0x0400;
45
46const SAVE_STATE_VERSION: u8 = 1;
47
48const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
53 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
54 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
55 let physical = mirroring.physical_bank(table);
56 physical * NAMETABLE_SIZE + local
57}
58
59#[derive(Clone, Copy, Default)]
62struct TxcChip {
63 accumulator: u8,
64 inverter: u8,
65 staging: u8,
66 output: u8,
67 increase: bool,
68 invert: bool,
69}
70
71impl TxcChip {
72 const MASK: u8 = 0x07;
73
74 const fn output(self) -> u8 {
75 self.output
76 }
77
78 const fn read(self) -> u8 {
79 let invert_xor = if self.invert { 0xFF } else { 0x00 };
80 (self.accumulator & Self::MASK) | ((self.inverter ^ invert_xor) & !Self::MASK)
81 }
82
83 const fn write(&mut self, absolute: u16, value: u8) {
87 if absolute < 0x8000 {
88 match absolute & 0xE103 {
89 0x4100 => {
90 if self.increase {
91 self.accumulator = self.accumulator.wrapping_add(1);
92 } else {
93 let invert_xor = if self.invert { 0xFF } else { 0x00 };
94 self.accumulator = ((self.accumulator & !Self::MASK)
95 | (self.staging & Self::MASK))
96 ^ invert_xor;
97 }
98 }
99 0x4101 => self.invert = (value & 0x01) != 0,
100 0x4102 => {
101 self.staging = value & Self::MASK;
102 self.inverter = value & !Self::MASK;
103 }
104 0x4103 => self.increase = (value & 0x01) != 0,
105 _ => {}
106 }
107 } else {
108 self.output = (self.accumulator & 0x0F) | ((self.inverter & 0x08) << 1);
110 }
111 }
112}
113
114pub struct Txc132 {
116 prg_rom: Box<[u8]>,
117 chr_rom: Box<[u8]>,
118 vram: Box<[u8]>,
119 txc: TxcChip,
120 mirroring: Mirroring,
121}
122
123impl Txc132 {
124 pub fn new(
131 prg_rom: Box<[u8]>,
132 chr_rom: Box<[u8]>,
133 mirroring: Mirroring,
134 ) -> Result<Self, MapperError> {
135 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
136 return Err(MapperError::Invalid(format!(
137 "mapper 132 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
138 prg_rom.len()
139 )));
140 }
141 if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
142 return Err(MapperError::Invalid(format!(
143 "mapper 132 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
144 chr_rom.len()
145 )));
146 }
147 Ok(Self {
148 prg_rom,
149 chr_rom,
150 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
151 txc: TxcChip::default(),
152 mirroring,
153 })
154 }
155}
156
157impl Mapper for Txc132 {
158 fn caps(&self) -> MapperCaps {
159 MapperCaps::NONE
160 }
161
162 fn cpu_read_unmapped(&self, addr: u16) -> bool {
167 (matches!(addr, 0x6000..=0x7FFF) && self.sram().is_empty()) || {
170 (0x4020..=0x40FF).contains(&addr)
171 }
172 }
173
174 fn cpu_read(&mut self, addr: u16) -> u8 {
175 match addr {
176 0x4100..=0x5FFF => {
177 if (addr & 0x0103) == 0x0100 {
180 self.txc.read() & 0x0F
181 } else {
182 0
183 }
184 }
185 0x8000..=0xFFFF => {
186 let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
187 let bank = (((self.txc.output() >> 2) & 0x01) as usize) % count;
188 self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
189 }
190 _ => 0,
191 }
192 }
193
194 fn cpu_write(&mut self, addr: u16, value: u8) {
195 if (0x4100..=0x5FFF).contains(&addr) || (0x8000..=0xFFFF).contains(&addr) {
196 self.txc.write(addr, value & 0x0F);
197 }
198 }
199
200 fn ppu_read(&mut self, addr: u16) -> u8 {
201 let addr = addr & 0x3FFF;
202 match addr {
203 0x0000..=0x1FFF => {
204 let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
205 let bank = ((self.txc.output() & 0x03) as usize) % count;
206 self.chr_rom[bank * CHR_BANK_8K + addr as usize]
207 }
208 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
209 _ => 0,
210 }
211 }
212
213 fn ppu_write(&mut self, addr: u16, value: u8) {
214 let addr = addr & 0x3FFF;
215 if let 0x2000..=0x3EFF = addr {
216 let off = nametable_offset(addr, self.mirroring);
217 self.vram[off] = value;
218 }
219 }
220
221 fn current_mirroring(&self) -> Mirroring {
222 self.mirroring
223 }
224
225 fn save_state(&self) -> Vec<u8> {
226 let mut out = Vec::with_capacity(7 + self.vram.len());
227 out.push(SAVE_STATE_VERSION);
228 out.push(self.txc.accumulator);
229 out.push(self.txc.inverter);
230 out.push(self.txc.staging);
231 out.push(self.txc.output);
232 out.push(u8::from(self.txc.increase));
233 out.push(u8::from(self.txc.invert));
234 out.extend_from_slice(&self.vram);
235 out
236 }
237
238 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
239 let expected = 7 + self.vram.len();
240 if data.len() != expected {
241 return Err(MapperError::WrongLength {
242 expected,
243 got: data.len(),
244 });
245 }
246 if data[0] != SAVE_STATE_VERSION {
247 return Err(MapperError::UnsupportedVersion(data[0]));
248 }
249 self.txc.accumulator = data[1];
250 self.txc.inverter = data[2];
251 self.txc.staging = data[3];
252 self.txc.output = data[4];
253 self.txc.increase = data[5] != 0;
254 self.txc.invert = data[6] != 0;
255 self.vram.copy_from_slice(&data[7..7 + self.vram.len()]);
256 Ok(())
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 fn synth_prg_32k(banks: usize) -> Box<[u8]> {
265 let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
266 for b in 0..banks {
267 v[b * PRG_BANK_32K] = b as u8;
268 }
269 v.into_boxed_slice()
270 }
271
272 fn synth_chr_8k(banks: usize) -> Box<[u8]> {
273 let mut v = vec![0u8; banks * CHR_BANK_8K];
274 for b in 0..banks {
275 v[b * CHR_BANK_8K] = b as u8;
276 }
277 v.into_boxed_slice()
278 }
279
280 #[test]
281 fn m132_txc_chip_drives_banks() {
282 let mut m = Txc132::new(synth_prg_32k(2), synth_chr_8k(4), Mirroring::Vertical).unwrap();
283 m.cpu_write(0x4103, 0x00); m.cpu_write(0x4102, 0b0000_1011 & 0x0F); m.cpu_write(0x4100, 0x00); m.cpu_write(0x8000, 0x00); assert_eq!(m.cpu_read(0x8000), 0); assert_eq!(m.ppu_read(0x0000), 3); assert!(!m.cpu_read_unmapped(0x4100));
297 }
298
299 #[test]
300 fn m132_save_state_round_trips_txc_chip_state() {
301 let mut t = Txc132::new(synth_prg_32k(2), synth_chr_8k(4), Mirroring::Vertical).unwrap();
302 t.cpu_write(0x4103, 0x00);
303 t.cpu_write(0x4102, 0x03);
304 t.cpu_write(0x4100, 0x00);
305 t.cpu_write(0x8000, 0x00);
306 let blob = t.save_state();
307 let mut t2 = Txc132::new(synth_prg_32k(2), synth_chr_8k(4), Mirroring::Vertical).unwrap();
308 t2.load_state(&blob).unwrap();
309 assert_eq!(t2.ppu_read(0x0000), t.ppu_read(0x0000));
310 }
311}