1#![allow(clippy::cast_possible_truncation, clippy::doc_markdown)]
41
42use crate::cartridge::Mirroring;
43use crate::mapper::{Mapper, MapperCaps, MapperError};
44use alloc::{boxed::Box, vec::Vec};
45use alloc::{format, vec};
46
47const PRG_BANK_8K: usize = 0x2000;
48const CHR_BANK_1K: usize = 0x0400;
49const NAMETABLE_SIZE: usize = 0x0400;
50const NAMETABLE_SIZE_U16: u16 = 0x0400;
51const PRG_RAM_LEN: usize = 0x2000; const RAM_MAGIC0: u8 = 0xCA;
54const RAM_MAGIC1: u8 = 0x69;
55const RAM_MAGIC2: u8 = 0x84;
56
57const SAVE_STATE_VERSION: u8 = 1;
58
59pub struct TaitoX1017 {
61 prg_rom: Box<[u8]>,
62 chr: Box<[u8]>,
63 vram: Box<[u8]>,
64 prg_ram: Box<[u8]>,
65 chr_is_ram: bool,
66 chr_2k: [u8; 2],
69 chr_1k: [u8; 4],
71 chr_invert: bool,
73 prg_bank: [u8; 3],
75 mirroring: Mirroring,
76 ram_enable: [u8; 3],
78 irq_regs: [u8; 2],
81}
82
83impl TaitoX1017 {
84 pub fn new(
90 prg_rom: Box<[u8]>,
91 chr_rom: Box<[u8]>,
92 mirroring: Mirroring,
93 ) -> Result<Self, MapperError> {
94 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
95 return Err(MapperError::Invalid(format!(
96 "Taito-X1-017 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
97 prg_rom.len()
98 )));
99 }
100 let chr_is_ram = chr_rom.is_empty();
101 let chr: Box<[u8]> = if chr_is_ram {
102 vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
103 } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
104 chr_rom
105 } else {
106 return Err(MapperError::Invalid(format!(
107 "Taito-X1-017 expects a 1 KiB multiple of CHR; got {} bytes",
108 chr_rom.len()
109 )));
110 };
111 Ok(Self {
112 prg_rom,
113 chr,
114 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
115 prg_ram: vec![0u8; PRG_RAM_LEN].into_boxed_slice(),
116 chr_is_ram,
117 chr_2k: [0, 0],
118 chr_1k: [0, 0, 0, 0],
119 chr_invert: false,
120 prg_bank: [0, 1, 2],
121 mirroring,
122 ram_enable: [0, 0, 0],
123 irq_regs: [0, 0],
124 })
125 }
126
127 const fn ram_enabled_for(&self, addr: u16) -> bool {
133 match addr {
134 0x6000..=0x67FF => self.ram_enable[0] == RAM_MAGIC0,
135 0x6800..=0x6FFF => self.ram_enable[1] == RAM_MAGIC1,
136 0x7000..=0x73FF => self.ram_enable[2] == RAM_MAGIC2,
137 _ => false,
138 }
139 }
140
141 const fn nametable_offset(&self, addr: u16) -> usize {
142 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
143 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
144 let physical = self.mirroring.physical_bank(table);
145 physical * NAMETABLE_SIZE + local
146 }
147
148 fn read_prg(&self, addr: u16) -> u8 {
149 let bank_count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
150 let slot = (addr >> 13) & 0x03; let bank = match slot {
152 0 => self.prg_bank[0] as usize,
153 1 => self.prg_bank[1] as usize,
154 2 => self.prg_bank[2] as usize,
155 _ => bank_count - 1,
156 } % bank_count;
157 let off = (addr as usize) & (PRG_BANK_8K - 1);
158 self.prg_rom[bank * PRG_BANK_8K + off]
159 }
160
161 const fn chr_bank_for_window(&self, window: usize) -> usize {
164 let low_half = window < 4;
168 let use_2k_for_low = !self.chr_invert; let in_2k_region = low_half == use_2k_for_low;
170 let local = window & 0x03; if in_2k_region {
172 let base = (self.chr_2k[(local >> 1) & 1] as usize) << 1; base + (local & 1)
175 } else {
176 self.chr_1k[local] as usize
177 }
178 }
179
180 fn chr_offset(&self, addr: u16) -> usize {
181 let len = self.chr.len().max(1);
182 let window = ((addr >> 10) & 0x07) as usize;
183 let base = self.chr_bank_for_window(window) * CHR_BANK_1K;
184 (base + (addr as usize & (CHR_BANK_1K - 1))) % len
185 }
186}
187
188impl Mapper for TaitoX1017 {
189 fn sram(&self) -> &[u8] {
190 &self.prg_ram
191 }
192 fn sram_mut(&mut self) -> &mut [u8] {
193 &mut self.prg_ram
194 }
195 fn caps(&self) -> MapperCaps {
198 MapperCaps::NONE
199 }
200
201 fn cpu_read(&mut self, addr: u16) -> u8 {
202 match addr {
203 0x6000..=0x73FF => {
204 if self.ram_enabled_for(addr) {
205 self.prg_ram[(addr as usize) - 0x6000]
206 } else {
207 0
208 }
209 }
210 0x8000..=0xFFFF => self.read_prg(addr),
211 _ => 0,
212 }
213 }
214
215 fn cpu_write(&mut self, addr: u16, value: u8) {
216 match addr {
217 0x6000..=0x73FF => {
218 if self.ram_enabled_for(addr) {
219 self.prg_ram[(addr as usize) - 0x6000] = value;
220 }
221 }
222 0x7EF0 => self.chr_2k[0] = value >> 1,
223 0x7EF1 => self.chr_2k[1] = value >> 1,
224 0x7EF2 => self.chr_1k[0] = value,
225 0x7EF3 => self.chr_1k[1] = value,
226 0x7EF4 => self.chr_1k[2] = value,
227 0x7EF5 => self.chr_1k[3] = value,
228 0x7EF6 => {
229 self.chr_invert = (value & 0x02) != 0;
230 self.mirroring = if (value & 0x01) != 0 {
231 Mirroring::Vertical
232 } else {
233 Mirroring::Horizontal
234 };
235 }
236 0x7EF7 => self.ram_enable[0] = value,
237 0x7EF8 => self.ram_enable[1] = value,
238 0x7EF9 => self.ram_enable[2] = value,
239 0x7EFA => self.prg_bank[0] = value >> 2,
240 0x7EFB => self.prg_bank[1] = value >> 2,
241 0x7EFC => self.prg_bank[2] = value >> 2,
242 0x7EFD => self.irq_regs[0] = value,
243 0x7EFE => self.irq_regs[1] = value,
244 _ => {}
245 }
246 }
247
248 fn ppu_read(&mut self, addr: u16) -> u8 {
249 let addr = addr & 0x3FFF;
250 match addr {
251 0x0000..=0x1FFF => self.chr[self.chr_offset(addr)],
252 0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
253 _ => 0,
254 }
255 }
256
257 fn ppu_write(&mut self, addr: u16, value: u8) {
258 let addr = addr & 0x3FFF;
259 match addr {
260 0x0000..=0x1FFF => {
261 if self.chr_is_ram {
262 let off = self.chr_offset(addr);
263 self.chr[off] = value;
264 }
265 }
266 0x2000..=0x3EFF => {
267 let off = self.nametable_offset(addr);
268 self.vram[off] = value;
269 }
270 _ => {}
271 }
272 }
273
274 fn current_mirroring(&self) -> Mirroring {
275 self.mirroring
276 }
277
278 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
279 let mut info = crate::mapper::MapperDebugInfo {
280 mapper_id: 82,
281 name: "Taito X1-017 (82)".into(),
282 mirroring: crate::mapper::mirroring_name(self.mirroring),
283 ..Default::default()
284 };
285 info.prg_banks
286 .push(("chr_inv".into(), format!("{}", u8::from(self.chr_invert))));
287 for (i, b) in self.prg_bank.iter().enumerate() {
288 info.prg_banks
289 .push((format!("PRG{i}"), format!("{b:#04x}")));
290 }
291 for (i, b) in self.chr_2k.iter().enumerate() {
292 info.chr_banks
293 .push((format!("CHR2K{i}"), format!("{b:#04x}")));
294 }
295 for (i, b) in self.chr_1k.iter().enumerate() {
296 info.chr_banks
297 .push((format!("CHR1K{i}"), format!("{b:#04x}")));
298 }
299 info
300 }
301
302 fn save_state(&self) -> Vec<u8> {
303 let mut out = Vec::with_capacity(
304 17 + PRG_RAM_LEN + self.vram.len() + if self.chr_is_ram { self.chr.len() } else { 0 },
305 );
306 out.push(SAVE_STATE_VERSION);
307 out.extend_from_slice(&self.chr_2k);
308 out.extend_from_slice(&self.chr_1k);
309 out.push(u8::from(self.chr_invert));
310 out.extend_from_slice(&self.prg_bank);
311 out.push(match self.mirroring {
312 Mirroring::Vertical => 1,
313 _ => 0,
314 });
315 out.extend_from_slice(&self.ram_enable);
316 out.extend_from_slice(&self.irq_regs);
317 out.extend_from_slice(&self.prg_ram);
318 out.extend_from_slice(&self.vram);
319 if self.chr_is_ram {
320 out.extend_from_slice(&self.chr);
321 }
322 out
323 }
324
325 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
326 let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
327 let expected = 17 + PRG_RAM_LEN + self.vram.len() + need_chr;
328 if data.len() != expected {
329 return Err(MapperError::WrongLength {
330 expected,
331 got: data.len(),
332 });
333 }
334 if data[0] != SAVE_STATE_VERSION {
335 return Err(MapperError::UnsupportedVersion(data[0]));
336 }
337 self.chr_2k.copy_from_slice(&data[1..3]);
338 self.chr_1k.copy_from_slice(&data[3..7]);
339 self.chr_invert = data[7] != 0;
340 self.prg_bank.copy_from_slice(&data[8..11]);
341 self.mirroring = if data[11] == 1 {
342 Mirroring::Vertical
343 } else {
344 Mirroring::Horizontal
345 };
346 self.ram_enable.copy_from_slice(&data[12..15]);
347 self.irq_regs.copy_from_slice(&data[15..17]);
348 let mut cursor = 17;
349 self.prg_ram
350 .copy_from_slice(&data[cursor..cursor + PRG_RAM_LEN]);
351 cursor += PRG_RAM_LEN;
352 self.vram
353 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
354 cursor += self.vram.len();
355 if self.chr_is_ram {
356 self.chr
357 .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
358 }
359 Ok(())
360 }
361}
362
363#[cfg(test)]
364#[allow(clippy::cast_possible_truncation)]
365mod tests {
366 use super::*;
367
368 fn synth_prg(banks_8k: usize) -> Box<[u8]> {
369 let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
370 for b in 0..banks_8k {
371 v[b * PRG_BANK_8K] = b as u8;
372 }
373 v.into_boxed_slice()
374 }
375
376 fn synth_chr_1k(banks: usize) -> Box<[u8]> {
377 let mut v = vec![0u8; banks * CHR_BANK_1K];
378 for b in 0..banks {
379 v[b * CHR_BANK_1K] = b as u8;
380 }
381 v.into_boxed_slice()
382 }
383
384 #[test]
385 fn prg_banks_value_shifted_and_fixed_tail() {
386 let mut m = TaitoX1017::new(synth_prg(16), synth_chr_1k(8), Mirroring::Horizontal).unwrap();
387 m.cpu_write(0x7EFA, 0x0C);
389 assert_eq!(m.cpu_read(0x8000), 3);
390 m.cpu_write(0x7EFB, 0x10); assert_eq!(m.cpu_read(0xA000), 4);
392 m.cpu_write(0x7EFC, 0x14); assert_eq!(m.cpu_read(0xC000), 5);
394 assert_eq!(m.cpu_read(0xE000), 15);
396 }
397
398 #[test]
399 fn chr_2k_value_shifted_mode0() {
400 let mut m = TaitoX1017::new(synth_prg(8), synth_chr_1k(16), Mirroring::Horizontal).unwrap();
401 m.cpu_write(0x7EF0, 8);
404 assert_eq!(m.ppu_read(0x0000), 8);
405 assert_eq!(m.ppu_read(0x0400), 9); m.cpu_write(0x7EF1, 10); assert_eq!(m.ppu_read(0x0800), 10);
408 assert_eq!(m.ppu_read(0x0C00), 11);
409 m.cpu_write(0x7EF2, 12);
411 assert_eq!(m.ppu_read(0x1000), 12);
412 m.cpu_write(0x7EF5, 15);
413 assert_eq!(m.ppu_read(0x1C00), 15);
414 }
415
416 #[test]
417 fn chr_a12_inversion_swaps_halves() {
418 let mut m = TaitoX1017::new(synth_prg(8), synth_chr_1k(16), Mirroring::Horizontal).unwrap();
419 m.cpu_write(0x7EF0, 8); m.cpu_write(0x7EF2, 12); m.cpu_write(0x7EF6, 0x02);
423 assert_eq!(m.ppu_read(0x0000), 12); assert_eq!(m.ppu_read(0x1000), 8); assert_eq!(m.ppu_read(0x1400), 9);
426 }
427
428 #[test]
429 fn mirroring_register_bit0() {
430 let mut m = TaitoX1017::new(synth_prg(4), synth_chr_1k(8), Mirroring::Horizontal).unwrap();
431 m.cpu_write(0x7EF6, 0x01); assert_eq!(m.current_mirroring(), Mirroring::Vertical);
433 m.cpu_write(0x7EF6, 0x00); assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
435 }
436
437 #[test]
438 fn prg_ram_per_region_unlock() {
439 let mut m = TaitoX1017::new(synth_prg(4), synth_chr_1k(8), Mirroring::Horizontal).unwrap();
440 m.cpu_write(0x6000, 0x55);
442 assert_eq!(m.cpu_read(0x6000), 0);
443 m.cpu_write(0x7EF7, RAM_MAGIC0);
444 m.cpu_write(0x6000, 0x55);
445 assert_eq!(m.cpu_read(0x6000), 0x55);
446 m.cpu_write(0x7EF7, RAM_MAGIC2);
448 m.cpu_write(0x7000, 0x66);
449 assert_eq!(m.cpu_read(0x7000), 0);
450 m.cpu_write(0x7EF9, RAM_MAGIC2);
451 m.cpu_write(0x7000, 0x66);
452 assert_eq!(m.cpu_read(0x7000), 0x66);
453 }
454
455 #[test]
456 fn save_state_round_trip() {
457 let mut m =
458 TaitoX1017::new(synth_prg(16), synth_chr_1k(16), Mirroring::Horizontal).unwrap();
459 m.cpu_write(0x7EFA, 0x10);
460 m.cpu_write(0x7EF0, 8);
461 m.cpu_write(0x7EF6, 0x03); m.cpu_write(0x7EF7, RAM_MAGIC0);
463 m.cpu_write(0x6010, 0x99);
464 let blob = m.save_state();
465 let mut m2 =
466 TaitoX1017::new(synth_prg(16), synth_chr_1k(16), Mirroring::Horizontal).unwrap();
467 m2.load_state(&blob).unwrap();
468 assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
469 assert_eq!(m.ppu_read(0x1000), m2.ppu_read(0x1000));
470 assert_eq!(m.current_mirroring(), m2.current_mirroring());
471 assert_eq!(m2.cpu_read(0x6010), 0x99);
472 }
473}