Skip to main content

rustynes_mappers/
m082_taito_x1_017.rs

1//! Taito X1-017 (iNES mapper 82) implementation.
2//!
3//! A Taito ASIC board (Kyuukyoku Harikiri Koushien, Kyuukyoku Harikiri Stadium
4//! III, SD Keiji - Blader). Like the X1-005 it exposes a register window high
5//! in `$6000-$7FFF`, but with three protectable 8 KiB PRG-RAM regions, a CHR
6//! A12-inversion mode bit, and a distinct CHR-bank-to-PPU mapping. The chip
7//! has an IRQ surface ($7EFD-$7EFF) that the licensed games do not use; it is
8//! modelled as register storage only (no counter clock), which is sufficient
9//! for those titles.
10//!
11//! Register map (nesdev `INES_Mapper_082.xhtml`):
12//!
13//! ```text
14//!   $7EF0 [CCCC CCC.]  2 KiB CHR bank 0 (value >> 1)
15//!   $7EF1 [CCCC CCC.]  2 KiB CHR bank 1 (value >> 1)
16//!   $7EF2 [CCCC CCCC]  1 KiB CHR bank 2
17//!   $7EF3 [CCCC CCCC]  1 KiB CHR bank 3
18//!   $7EF4 [CCCC CCCC]  1 KiB CHR bank 4
19//!   $7EF5 [CCCC CCCC]  1 KiB CHR bank 5
20//!   $7EF6 [.... ..IM]  I = CHR A12 inversion (bit 1), M = mirroring (bit 0;
21//!                      0 = Horizontal, 1 = Vertical)
22//!   $7EF7 [VVVV VVVV]  PRG-RAM enable $6000-$67FF (write $CA)
23//!   $7EF8 [VVVV VVVV]  PRG-RAM enable $6800-$6FFF (write $69)
24//!   $7EF9 [VVVV VVVV]  PRG-RAM enable $7000-$73FF (write $84)
25//!   $7EFA [..DC BA..]  8 KiB PRG bank -> $8000 (value >> 2)
26//!   $7EFB [..DC BA..]  8 KiB PRG bank -> $A000 (value >> 2)
27//!   $7EFC [..DC BA..]  8 KiB PRG bank -> $C000 (value >> 2)
28//! ```
29//!
30//! **CHR mapping (the X1-017 quirk).** When `$7EF6` bit 1 is clear (mode 0) the
31//! two 2 KiB banks occupy PPU `$0000-$0FFF` and the four 1 KiB banks
32//! `$1000-$1FFF`; when set (mode 1) the layout inverts so the 1 KiB banks land
33//! at `$0000-$0FFF` and the 2 KiB banks at `$1000-$1FFF`. The 2 KiB selects are
34//! 7-bit and shift right by one during address calculation.
35//!
36//! The last 8 KiB PRG bank ($E000) is fixed to the final bank.
37//!
38//! See `docs/mappers.md` §Mapper coverage matrix.
39
40#![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; // one contiguous 8 KiB WRAM window $6000-$7FFF
52
53const RAM_MAGIC0: u8 = 0xCA;
54const RAM_MAGIC1: u8 = 0x69;
55const RAM_MAGIC2: u8 = 0x84;
56
57const SAVE_STATE_VERSION: u8 = 1;
58
59/// Taito X1-017 mapper (iNES mapper 82).
60pub 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    /// 2 KiB CHR bank registers ($7EF0/$7EF1), stored as the 1 KiB-resolution
67    /// base (`value >> 1` shifted to 1 KiB units = `(value >> 1) << 1`).
68    chr_2k: [u8; 2],
69    /// 1 KiB CHR bank registers ($7EF2-$7EF5).
70    chr_1k: [u8; 4],
71    /// CHR A12 inversion ($7EF6 bit 1).
72    chr_invert: bool,
73    /// 8 KiB PRG banks for $8000 / $A000 / $C000.
74    prg_bank: [u8; 3],
75    mirroring: Mirroring,
76    /// Per-region PRG-RAM enable latches ($7EF7/$7EF8/$7EF9).
77    ram_enable: [u8; 3],
78    /// IRQ register surface (latch / control / acknowledge) — stored but the
79    /// counter is not clocked (the licensed games never enable it).
80    irq_regs: [u8; 2],
81}
82
83impl TaitoX1017 {
84    /// Construct a new Taito X1-017 mapper.
85    ///
86    /// # Errors
87    ///
88    /// Returns [`MapperError::Invalid`] when sizes don't match the constraints.
89    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    /// Whether the PRG-RAM byte at `addr` ($6000-$73FF) is enabled. The chip
128    /// gates three sub-regions independently; the upper $7400-$7DFF is not
129    /// RAM-mapped (and $7EF0+ is the register window). We gate the whole
130    /// $6000-$7FFF window on the union for read/write simplicity, but honor the
131    /// per-region unlock value for each access.
132    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; // 0=$8000,1=$A000,2=$C000,3=$E000
151        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    /// Resolve the 1 KiB CHR bank index for one of the eight PPU `$0000-$1FFF`
162    /// 1 KiB windows, honoring the A12-inversion mode.
163    const fn chr_bank_for_window(&self, window: usize) -> usize {
164        // `window` is 0..=7 (PPU addr >> 10). In mode 0 the 2 KiB banks occupy
165        // the low half ($0000-$0FFF = windows 0-3) and the 1 KiB banks the high
166        // half ($1000-$1FFF = windows 4-7). Mode 1 swaps the halves.
167        let low_half = window < 4;
168        let use_2k_for_low = !self.chr_invert; // mode 0 -> 2K low; mode 1 -> 1K low
169        let in_2k_region = low_half == use_2k_for_low;
170        let local = window & 0x03; // 0..=3 within the 4 KiB half
171        if in_2k_region {
172            // Two 2 KiB banks span this 4 KiB half (windows pair up).
173            let base = (self.chr_2k[(local >> 1) & 1] as usize) << 1; // 1K base
174            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    // v2.8.0 Phase 4 — no per-cycle hooks (no IRQ, no audio): the bus
196    // skips all four per-CPU-cycle dispatches for this board.
197    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        // PRG bank value is value >> 2. Write 0x0C -> bank 3.
388        m.cpu_write(0x7EFA, 0x0C);
389        assert_eq!(m.cpu_read(0x8000), 3);
390        m.cpu_write(0x7EFB, 0x10); // bank 4
391        assert_eq!(m.cpu_read(0xA000), 4);
392        m.cpu_write(0x7EFC, 0x14); // bank 5
393        assert_eq!(m.cpu_read(0xC000), 5);
394        // $E000 fixed to last bank (16 banks -> bank 15).
395        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        // Mode 0 (default): 2K banks at $0000-$0FFF, 1K banks at $1000-$1FFF.
402        // $7EF0 value 8 -> chr_2k base = 8>>1 = 4 -> 1K base 8; window 0 = bank 8.
403        m.cpu_write(0x7EF0, 8);
404        assert_eq!(m.ppu_read(0x0000), 8);
405        assert_eq!(m.ppu_read(0x0400), 9); // second 1K of the 2K bank
406        m.cpu_write(0x7EF1, 10); // base = 5 -> 1K base 10
407        assert_eq!(m.ppu_read(0x0800), 10);
408        assert_eq!(m.ppu_read(0x0C00), 11);
409        // 1K banks in the high half.
410        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); // 2K bank -> base 8
420        m.cpu_write(0x7EF2, 12); // 1K bank
421        // Mode 1: 1K banks at low half, 2K banks at high half.
422        m.cpu_write(0x7EF6, 0x02);
423        assert_eq!(m.ppu_read(0x0000), 12); // 1K bank now at $0000
424        assert_eq!(m.ppu_read(0x1000), 8); // 2K bank now at $1000
425        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); // bit 0 set -> Vertical
432        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
433        m.cpu_write(0x7EF6, 0x00); // -> Horizontal
434        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        // Region 0 ($6000-$67FF) needs $CA.
441        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        // Region 2 ($7000-$73FF) needs $84, not $CA.
447        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); // invert + vertical
462        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}