1#![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#[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 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 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; let bank = match slot {
136 0 => self.prg_bank[0] as usize,
137 1 => self.prg_bank[1] as usize,
138 2 => bank_count.saturating_sub(2),
143 _ => bank_count - 1, } % 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 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 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 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 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 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 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); 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 m.cpu_write(0xC000, 0xFE);
450 m.cpu_write(0xC001, 0x00); m.cpu_write(0xC002, 0x00); pulse_a12(&mut m);
454 assert!(!m.irq_pending());
455 pulse_a12(&mut m);
457 assert!(m.irq_pending());
458 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); m.cpu_write(0xC001, 0x00);
468 m.cpu_write(0xC002, 0x00);
469 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}