1#![allow(clippy::cast_possible_truncation, clippy::doc_markdown)]
39
40use crate::cartridge::Mirroring;
41use crate::mapper::{Mapper, MapperCaps, MapperError};
42use alloc::{boxed::Box, vec::Vec};
43use alloc::{format, vec};
44
45const PRG_BANK_8K: usize = 0x2000;
46const CHR_BANK_1K: usize = 0x0400;
47const NAMETABLE_SIZE: usize = 0x0400;
48const NAMETABLE_SIZE_U16: u16 = 0x0400;
49const RAM_LEN: usize = 0x80; const RAM_MAGIC: u8 = 0xA3;
51
52const SAVE_STATE_VERSION: u8 = 2;
57
58pub struct TaitoX1005 {
60 prg_rom: Box<[u8]>,
61 chr: Box<[u8]>,
62 vram: Box<[u8]>,
63 ram: [u8; RAM_LEN],
64 chr_is_ram: bool,
65 chr_1k: [u8; 8],
67 prg_bank: [u8; 3],
69 mirroring: Mirroring,
70 ram_enable: [u8; 2],
73}
74
75impl TaitoX1005 {
76 pub fn new(
85 prg_rom: Box<[u8]>,
86 chr_rom: Box<[u8]>,
87 mirroring: Mirroring,
88 ) -> Result<Self, MapperError> {
89 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
90 return Err(MapperError::Invalid(format!(
91 "Taito-X1-005 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
92 prg_rom.len()
93 )));
94 }
95 let chr_is_ram = chr_rom.is_empty();
96 let chr: Box<[u8]> = if chr_is_ram {
97 vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
98 } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
99 chr_rom
100 } else {
101 return Err(MapperError::Invalid(format!(
102 "Taito-X1-005 expects a 1 KiB multiple of CHR; got {} bytes",
103 chr_rom.len()
104 )));
105 };
106 Ok(Self {
107 prg_rom,
108 chr,
109 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
110 ram: [0u8; RAM_LEN],
111 chr_is_ram,
112 chr_1k: [0, 1, 2, 3, 4, 5, 6, 7],
113 prg_bank: [0, 1, 2],
114 mirroring,
115 ram_enable: [0, 0],
116 })
117 }
118
119 const fn ram_enabled(&self) -> bool {
120 self.ram_enable[0] == RAM_MAGIC && self.ram_enable[1] == RAM_MAGIC
121 }
122
123 const fn nametable_offset(&self, addr: u16) -> usize {
124 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
125 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
126 let physical = self.mirroring.physical_bank(table);
127 physical * NAMETABLE_SIZE + local
128 }
129
130 fn read_prg(&self, addr: u16) -> u8 {
131 let bank_count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
132 let slot = (addr >> 13) & 0x03; let bank = match slot {
134 0 => self.prg_bank[0] as usize,
135 1 => self.prg_bank[1] as usize,
136 2 => self.prg_bank[2] as usize,
137 _ => bank_count - 1,
140 } % bank_count;
141 let off = (addr as usize) & (PRG_BANK_8K - 1);
142 self.prg_rom[bank * PRG_BANK_8K + off]
143 }
144
145 fn chr_offset(&self, addr: u16) -> usize {
146 let len = self.chr.len().max(1);
147 let idx = ((addr >> 10) & 0x07) as usize; let base = (self.chr_1k[idx] as usize) * CHR_BANK_1K;
149 (base + (addr as usize & (CHR_BANK_1K - 1))) % len
150 }
151}
152
153impl Mapper for TaitoX1005 {
154 fn sram(&self) -> &[u8] {
158 &self.ram
159 }
160 fn sram_mut(&mut self) -> &mut [u8] {
161 &mut self.ram
162 }
163
164 fn caps(&self) -> MapperCaps {
167 MapperCaps::NONE
168 }
169
170 fn cpu_read(&mut self, addr: u16) -> u8 {
171 match addr {
172 0x7F00..=0x7FFF => {
173 if self.ram_enabled() {
174 self.ram[(addr as usize) & (RAM_LEN - 1)]
175 } else {
176 0
177 }
178 }
179 0x8000..=0xFFFF => self.read_prg(addr),
180 _ => 0,
181 }
182 }
183
184 fn cpu_write(&mut self, addr: u16, value: u8) {
185 match addr {
186 0x7EF0 => {
191 let base = value & 0xFE;
192 self.chr_1k[0] = base;
193 self.chr_1k[1] = base | 1;
194 }
195 0x7EF1 => {
196 let base = value & 0xFE;
197 self.chr_1k[2] = base;
198 self.chr_1k[3] = base | 1;
199 }
200 0x7EF2 => self.chr_1k[4] = value,
201 0x7EF3 => self.chr_1k[5] = value,
202 0x7EF4 => self.chr_1k[6] = value,
203 0x7EF5 => self.chr_1k[7] = value,
204 0x7EF6 | 0x7EF7 => {
205 self.mirroring = if (value & 0x01) != 0 {
208 Mirroring::Vertical
209 } else {
210 Mirroring::Horizontal
211 };
212 }
213 0x7EF8 => self.ram_enable[0] = value,
214 0x7EF9 => self.ram_enable[1] = value,
215 0x7EFA | 0x7EFB => self.prg_bank[0] = value,
216 0x7EFC | 0x7EFD => self.prg_bank[1] = value,
217 0x7EFE | 0x7EFF => self.prg_bank[2] = value,
218 0x7F00..=0x7FFF if self.ram_enabled() => {
219 self.ram[(addr as usize) & (RAM_LEN - 1)] = value;
220 }
221 _ => {}
222 }
223 }
224
225 fn ppu_read(&mut self, addr: u16) -> u8 {
226 let addr = addr & 0x3FFF;
227 match addr {
228 0x0000..=0x1FFF => self.chr[self.chr_offset(addr)],
229 0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
230 _ => 0,
231 }
232 }
233
234 fn ppu_write(&mut self, addr: u16, value: u8) {
235 let addr = addr & 0x3FFF;
236 match addr {
237 0x0000..=0x1FFF => {
238 if self.chr_is_ram {
239 let off = self.chr_offset(addr);
240 self.chr[off] = value;
241 }
242 }
243 0x2000..=0x3EFF => {
244 let off = self.nametable_offset(addr);
245 self.vram[off] = value;
246 }
247 _ => {}
248 }
249 }
250
251 fn current_mirroring(&self) -> Mirroring {
252 self.mirroring
253 }
254
255 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
256 let mut info = crate::mapper::MapperDebugInfo {
257 mapper_id: 80,
258 name: "Taito X1-005 (80)".into(),
259 mirroring: crate::mapper::mirroring_name(self.mirroring),
260 ..Default::default()
261 };
262 for (i, b) in self.prg_bank.iter().enumerate() {
263 info.prg_banks
264 .push((format!("PRG{i}"), format!("{b:#04x}")));
265 }
266 for (i, b) in self.chr_1k.iter().enumerate() {
267 info.chr_banks
268 .push((format!("CHR{i}"), format!("{b:#04x}")));
269 }
270 info
271 }
272
273 fn save_state(&self) -> Vec<u8> {
274 let mut out = Vec::with_capacity(
277 15 + RAM_LEN + self.vram.len() + if self.chr_is_ram { self.chr.len() } else { 0 },
278 );
279 out.push(SAVE_STATE_VERSION);
280 out.extend_from_slice(&self.chr_1k);
281 out.extend_from_slice(&self.prg_bank);
282 out.push(match self.mirroring {
283 Mirroring::Horizontal => 1,
284 _ => 0,
285 });
286 out.extend_from_slice(&self.ram_enable);
287 out.extend_from_slice(&self.ram);
288 out.extend_from_slice(&self.vram);
289 if self.chr_is_ram {
290 out.extend_from_slice(&self.chr);
291 }
292 out
293 }
294
295 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
296 if let Some(&ver) = data.first()
301 && ver != SAVE_STATE_VERSION
302 {
303 return Err(MapperError::UnsupportedVersion(ver));
304 }
305 let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
306 let expected = 15 + RAM_LEN + self.vram.len() + need_chr;
307 if data.len() != expected {
308 return Err(MapperError::WrongLength {
309 expected,
310 got: data.len(),
311 });
312 }
313 self.chr_1k.copy_from_slice(&data[1..9]);
314 self.prg_bank.copy_from_slice(&data[9..12]);
315 self.mirroring = if data[12] == 1 {
316 Mirroring::Horizontal
317 } else {
318 Mirroring::Vertical
319 };
320 self.ram_enable.copy_from_slice(&data[13..15]);
321 let mut cursor = 15;
322 self.ram.copy_from_slice(&data[cursor..cursor + RAM_LEN]);
323 cursor += RAM_LEN;
324 self.vram
325 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
326 cursor += self.vram.len();
327 if self.chr_is_ram {
328 self.chr
329 .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
330 }
331 Ok(())
332 }
333}
334
335#[cfg(test)]
336#[allow(clippy::cast_possible_truncation)]
337mod tests {
338 use super::*;
339
340 fn synth_prg(banks_8k: usize) -> Box<[u8]> {
341 let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
342 for b in 0..banks_8k {
343 v[b * PRG_BANK_8K] = b as u8;
344 }
345 v.into_boxed_slice()
346 }
347
348 fn synth_chr_1k(banks: usize) -> Box<[u8]> {
349 let mut v = vec![0u8; banks * CHR_BANK_1K];
350 for b in 0..banks {
351 v[b * CHR_BANK_1K] = b as u8;
352 }
353 v.into_boxed_slice()
354 }
355
356 #[test]
357 fn prg_banks_and_fixed_tail() {
358 let mut m = TaitoX1005::new(synth_prg(8), synth_chr_1k(8), Mirroring::Vertical).unwrap();
359 assert_eq!(m.cpu_read(0x8000), 0);
361 assert_eq!(m.cpu_read(0xA000), 1);
362 assert_eq!(m.cpu_read(0xC000), 2);
363 assert_eq!(m.cpu_read(0xE000), 7);
364 m.cpu_write(0x7EFA, 3);
367 m.cpu_write(0x7EFC, 5);
368 m.cpu_write(0x7EFE, 4);
369 assert_eq!(m.cpu_read(0x8000), 3);
370 assert_eq!(m.cpu_read(0xA000), 5);
371 assert_eq!(m.cpu_read(0xC000), 4);
372 assert_eq!(m.cpu_read(0xE000), 7);
373 m.cpu_write(0x7EFB, 1);
375 m.cpu_write(0x7EFD, 2);
376 m.cpu_write(0x7EFF, 6);
377 assert_eq!(m.cpu_read(0x8000), 1);
378 assert_eq!(m.cpu_read(0xA000), 2);
379 assert_eq!(m.cpu_read(0xC000), 6);
380 }
381
382 #[test]
383 fn chr_2k_and_1k_banks() {
384 let mut m = TaitoX1005::new(synth_prg(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
385 m.cpu_write(0x7EF0, 4);
387 assert_eq!(m.ppu_read(0x0000), 4);
388 assert_eq!(m.ppu_read(0x0400), 5); m.cpu_write(0x7EF1, 8);
391 assert_eq!(m.ppu_read(0x0800), 8);
392 assert_eq!(m.ppu_read(0x0C00), 9);
393 m.cpu_write(0x7EF2, 11);
395 assert_eq!(m.ppu_read(0x1000), 11);
396 m.cpu_write(0x7EF5, 13);
397 assert_eq!(m.ppu_read(0x1C00), 13);
398 }
399
400 #[test]
401 fn mirroring_register() {
402 let mut m = TaitoX1005::new(synth_prg(4), synth_chr_1k(8), Mirroring::Vertical).unwrap();
403 m.cpu_write(0x7EF6, 0x01);
405 assert_eq!(m.current_mirroring(), Mirroring::Vertical);
406 m.cpu_write(0x7EF6, 0x00);
407 assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
408 }
409
410 #[test]
411 fn battery_ram_needs_both_magic_writes() {
412 let mut m = TaitoX1005::new(synth_prg(4), synth_chr_1k(8), Mirroring::Vertical).unwrap();
413 m.cpu_write(0x7F00, 0x55);
415 assert_eq!(m.cpu_read(0x7F00), 0);
416 m.cpu_write(0x7EF8, RAM_MAGIC);
418 m.cpu_write(0x7F00, 0x55);
419 assert_eq!(m.cpu_read(0x7F00), 0);
420 m.cpu_write(0x7EF9, RAM_MAGIC);
422 m.cpu_write(0x7F00, 0x55);
423 assert_eq!(m.cpu_read(0x7F00), 0x55);
424 assert_eq!(m.cpu_read(0x7F80), 0x55);
426 }
427
428 #[test]
429 fn load_state_rejects_old_version_cleanly() {
430 let mut m = TaitoX1005::new(synth_prg(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
435 let mut blob = m.save_state();
436 blob[0] = 1; match m.load_state(&blob) {
438 Err(MapperError::UnsupportedVersion(1)) => {}
439 other => panic!("expected UnsupportedVersion(1), got {other:?}"),
440 }
441 }
442
443 #[test]
444 fn save_state_round_trip() {
445 let mut m = TaitoX1005::new(synth_prg(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
446 m.cpu_write(0x7EFA, 5);
447 m.cpu_write(0x7EFC, 6);
448 m.cpu_write(0x7EF0, 4);
449 m.cpu_write(0x7EF6, 0x01);
450 m.cpu_write(0x7EF8, RAM_MAGIC);
451 m.cpu_write(0x7EF9, RAM_MAGIC);
452 m.cpu_write(0x7F10, 0xAB);
453 let blob = m.save_state();
454 let mut m2 = TaitoX1005::new(synth_prg(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
455 m2.load_state(&blob).unwrap();
456 assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
457 assert_eq!(m.cpu_read(0xA000), m2.cpu_read(0xA000));
458 assert_eq!(m.ppu_read(0x0000), m2.ppu_read(0x0000));
459 assert_eq!(m.current_mirroring(), m2.current_mirroring());
460 assert_eq!(m2.cpu_read(0x7F10), 0xAB);
461 }
462}