1#![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
43pub 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 chr_2k: [u8; 2],
52 chr_1k: [u8; 4],
53 mirroring: Mirroring,
54}
55
56impl TaitoTc0190 {
57 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; let bank = match slot {
110 0 => self.prg_bank[0] as usize,
111 1 => self.prg_bank[1] as usize,
112 2 => bank_count.saturating_sub(2), _ => bank_count - 1, } % 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 let idx = ((addr >> 10) & 0x03) as usize; 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 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 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 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 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 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 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); assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
329 m.cpu_write(0x8000, 0x00); assert_eq!(m.current_mirroring(), Mirroring::Vertical);
331 }
332
333 #[test]
334 fn chr_2k_and_1k_banks() {
335 let mut m = TaitoTc0190::new(synth_prg(4), synth_chr_1k(16), Mirroring::Vertical).unwrap();
337 m.cpu_write(0x8002, 0x02);
339 assert_eq!(m.ppu_read(0x0000), 4);
340 m.cpu_write(0xA000, 0x09);
342 assert_eq!(m.ppu_read(0x1000), 9);
343 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}