Skip to main content

rustynes_mappers/
kaiser.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//
3// Provenance: the Kaiser boards are derived from Mesen2 (GPL-3.0-or-later), `Waixing/Mapper253.h` (the mapper 253 Waixing VRC4 clone). See docs/originality-and-provenance.md (Section 1)
4// and NOTICE for the complete, audited derivation record.
5//! Kaiser boards: `KS202` (mapper 56), `KS7017` (142), `KS7031` (303),
6//! `KS7016` (305), `KS7013B` (306) and relatives.
7//!
8//! Kaiser's pirate boards are unusual for their size class in carrying real
9//! IRQ counters, and in disagreeing about direction: `KS202` counts *up* to a
10//! target while `KS7017` counts *down* to zero. `KS7031` is stranger still --
11//! it maps four independently-selected 2 KiB PRG windows, a granularity no
12//! licensed board uses.
13//!
14//! One [`KaiserMapper`] with a [`KaiserBoard`] discriminant rather than five
15//! types, because the boards share their register-file and save-state shape
16//! and differ only in decode and IRQ direction.
17//!
18//! A best-effort (Tier-2) board: register-decode correctness verified against
19//! the reference emulators (`Mesen2`, `GeraNES`) and the nesdev wiki, with no
20//! commercial-oracle ROM in the tree. Banking math is direct slice indexing and
21//! every bank select wraps with `% count`, so a register write can never index
22//! out of bounds -- required for the `#![no_std]` chip stack, which cannot
23//! afford a panic on a register access.
24//!
25//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
26//! and `docs/mappers.md` §Mapper coverage matrix.
27
28#![allow(
29    clippy::cast_possible_truncation,
30    clippy::cast_lossless,
31    clippy::match_same_arms,
32    clippy::doc_markdown,
33    clippy::similar_names,
34    clippy::too_many_lines,
35    clippy::missing_const_for_fn,
36    clippy::struct_excessive_bools,
37    clippy::bool_to_int_with_if,
38    clippy::unreadable_literal
39)]
40
41use crate::cartridge::Mirroring;
42use crate::mapper::{Mapper, MapperCaps, MapperError};
43use alloc::{boxed::Box, format, vec, vec::Vec};
44
45const PRG_BANK_8K: usize = 0x2000;
46const PRG_BANK_16K: usize = 0x4000;
47const PRG_BANK_2K: usize = 0x0800;
48const CHR_BANK_1K: usize = 0x0400;
49const CHR_BANK_8K: usize = 0x2000;
50const NAMETABLE_SIZE: usize = 0x0400;
51const NAMETABLE_SIZE_U16: u16 = 0x0400;
52
53const SAVE_STATE_VERSION: u8 = 1;
54
55// ---------------------------------------------------------------------------
56// Shared nametable + mirroring helpers (mirror the other simple-mapper modules).
57// ---------------------------------------------------------------------------
58
59const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
60    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
61    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
62    let physical = mirroring.physical_bank(table);
63    physical * NAMETABLE_SIZE + local
64}
65
66const fn mirroring_to_byte(m: Mirroring) -> u8 {
67    match m {
68        Mirroring::Horizontal => 0,
69        Mirroring::Vertical => 1,
70        Mirroring::SingleScreenA => 2,
71        Mirroring::SingleScreenB => 3,
72        Mirroring::FourScreen => 4,
73        Mirroring::MapperControlled => 5,
74    }
75}
76
77const fn byte_to_mirroring(b: u8, fallback: Mirroring) -> Mirroring {
78    match b {
79        0 => Mirroring::Horizontal,
80        1 => Mirroring::Vertical,
81        2 => Mirroring::SingleScreenA,
82        3 => Mirroring::SingleScreenB,
83        4 => Mirroring::FourScreen,
84        5 => Mirroring::MapperControlled,
85        _ => fallback,
86    }
87}
88
89/// Validate a PRG-ROM image is a non-zero multiple of 8 KiB.
90fn check_prg(prg: &[u8], id: u16) -> Result<(), MapperError> {
91    if prg.is_empty() || !prg.len().is_multiple_of(PRG_BANK_8K) {
92        return Err(MapperError::Invalid(format!(
93            "mapper {id} PRG-ROM size {} is not a non-zero multiple of 8 KiB",
94            prg.len()
95        )));
96    }
97    Ok(())
98}
99
100/// Which Kaiser variant a [`KaiserMapper`] models.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum KaiserBoard {
103    /// Mapper 56 (KS202) — extra CHR + mirror writes; up-counting M2 IRQ.
104    M56,
105    /// Mapper 142 (KS7032) — like 56 without the extra CHR/mirror writes.
106    M142,
107    /// Mapper 303 (KS7017) — address-decoded PRG + down-counting M2 IRQ.
108    M303,
109    /// Mapper 305 (KS7031) — four 2 KiB $6000 PRG-ROM windows (no IRQ).
110    M305,
111    /// Mapper 306 (KS7016) — address-decoded $6000 PRG window (no IRQ).
112    M306,
113    /// Mapper 312 (KS7013B) — $6000 PRG select + $8000 mirroring (no IRQ).
114    M312,
115}
116
117/// A Kaiser FDS-conversion / window board.
118pub struct KaiserMapper {
119    board: KaiserBoard,
120    prg_rom: Box<[u8]>,
121    chr: Box<[u8]>,
122    chr_is_ram: bool,
123    vram: Box<[u8]>,
124    wram: Box<[u8]>,
125    mirroring: Mirroring,
126    // KS202/KS7032 (56/142).
127    prg_regs: [u8; 4],
128    selected_reg: u8,
129    use_rom: bool,
130    chr_banks: [u8; 8],
131    // KS7016 (306) PRG-ROM $6000 window.
132    win_6000: u8,
133    // KS7031 (305) four 2 KiB windows.
134    regs4: [u8; 4],
135    // 312 PRG select (16 KiB).
136    prg16: u8,
137    // IRQ (56/142 up-count, 303 down-count).
138    irq_counter: u16,
139    irq_reload: u16,
140    irq_enabled: bool,
141    irq_control: u8,
142    irq_pending: bool,
143}
144
145impl KaiserMapper {
146    const SAVE_LEN: usize = 4 + 1 + 1 + 8 + 1 + 4 + 1 + 2 + 2 + 1 + 1 + 1 + 1;
147
148    fn new(
149        board: KaiserBoard,
150        prg_rom: Box<[u8]>,
151        chr_rom: Box<[u8]>,
152        mirroring: Mirroring,
153        id: u16,
154    ) -> Result<Self, MapperError> {
155        check_prg(&prg_rom, id)?;
156        let chr_is_ram = chr_rom.is_empty();
157        let chr: Box<[u8]> = if chr_is_ram {
158            vec![0u8; CHR_BANK_8K].into_boxed_slice()
159        } else {
160            chr_rom
161        };
162        Ok(Self {
163            board,
164            prg_rom,
165            chr,
166            chr_is_ram,
167            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
168            wram: vec![0u8; PRG_BANK_8K].into_boxed_slice(),
169            mirroring,
170            prg_regs: [0; 4],
171            selected_reg: 0,
172            use_rom: false,
173            chr_banks: [0; 8],
174            win_6000: 8,
175            regs4: [0; 4],
176            prg16: 0,
177            irq_counter: 0,
178            irq_reload: 0,
179            irq_enabled: false,
180            irq_control: 0,
181            irq_pending: false,
182        })
183    }
184
185    fn prg_count_8k(&self) -> usize {
186        (self.prg_rom.len() / PRG_BANK_8K).max(1)
187    }
188
189    fn prg_count_16k(&self) -> usize {
190        (self.prg_rom.len() / PRG_BANK_16K).max(1)
191    }
192
193    fn prg_count_2k(&self) -> usize {
194        (self.prg_rom.len() / PRG_BANK_2K).max(1)
195    }
196
197    fn chr_count_1k(&self) -> usize {
198        (self.chr.len() / CHR_BANK_1K).max(1)
199    }
200}
201
202impl Mapper for KaiserMapper {
203    fn caps(&self) -> MapperCaps {
204        match self.board {
205            KaiserBoard::M56 | KaiserBoard::M142 | KaiserBoard::M303 => MapperCaps::CYCLE_IRQ,
206            _ => MapperCaps::NONE,
207        }
208    }
209
210    fn cpu_read(&mut self, addr: u16) -> u8 {
211        match self.board {
212            KaiserBoard::M56 | KaiserBoard::M142 => match addr {
213                0x6000..=0x7FFF => {
214                    // KS7032 (142) has no RAM here: the window is always the
215                    // PRG-ROM bank that bank-select value 4 picks
216                    // (`nesdev_wiki/INES_Mapper_142`; core ledger F-09,
217                    // v2.9.5). KS202 (56) keeps RAM unless `use_rom` is set.
218                    if self.use_rom || self.board == KaiserBoard::M142 {
219                        let count = self.prg_count_8k();
220                        let bank = (self.prg_regs[3] as usize) % count;
221                        self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
222                    } else {
223                        self.wram[addr as usize & 0x1FFF]
224                    }
225                }
226                0x8000..=0xFFFF => {
227                    let slot = (addr as usize - 0x8000) / PRG_BANK_8K;
228                    let count = self.prg_count_8k();
229                    let bank = if slot == 3 {
230                        count - 1
231                    } else {
232                        self.prg_regs[slot] as usize % count
233                    };
234                    self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
235                }
236                _ => 0,
237            },
238            KaiserBoard::M303 => match addr {
239                0x4030 => {
240                    let p = self.irq_pending;
241                    self.irq_pending = false;
242                    u8::from(p)
243                }
244                0x8000..=0xBFFF => {
245                    let count = self.prg_count_16k();
246                    let bank = (self.prg16 as usize) % count;
247                    self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
248                }
249                0xC000..=0xFFFF => {
250                    let count = self.prg_count_16k();
251                    let bank = 2 % count;
252                    self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
253                }
254                _ => 0,
255            },
256            KaiserBoard::M305 => match addr {
257                0x6000..=0x7FFF => {
258                    let win = (addr as usize - 0x6000) / PRG_BANK_2K;
259                    let count = self.prg_count_2k();
260                    let bank = (self.regs4[win] as usize) % count;
261                    self.prg_rom[bank * PRG_BANK_2K + (addr as usize & 0x7FF)]
262                }
263                0x8000..=0xFFFF => {
264                    // Fixed last 32 KiB (16 x 2 KiB windows = banks count-16..count-1).
265                    let count = self.prg_count_2k();
266                    let win = (addr as usize - 0x8000) / PRG_BANK_2K;
267                    let bank = count.saturating_sub(16 - win) % count;
268                    self.prg_rom[bank * PRG_BANK_2K + (addr as usize & 0x7FF)]
269                }
270                _ => 0,
271            },
272            KaiserBoard::M306 => match addr {
273                0x6000..=0x7FFF => {
274                    let count = self.prg_count_8k();
275                    let bank = (self.win_6000 as usize) % count;
276                    self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
277                }
278                0x8000..=0xFFFF => {
279                    // Fixed last 32 KiB.
280                    let count = self.prg_count_8k();
281                    let slot = (addr as usize - 0x8000) / PRG_BANK_8K;
282                    let bank = count.saturating_sub(4 - slot) % count;
283                    self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
284                }
285                _ => 0,
286            },
287            KaiserBoard::M312 => match addr {
288                0x8000..=0xBFFF => {
289                    let count = self.prg_count_16k();
290                    let bank = (self.prg16 as usize) % count;
291                    self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
292                }
293                0xC000..=0xFFFF => {
294                    let count = self.prg_count_16k();
295                    let bank = count - 1;
296                    self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
297                }
298                _ => 0,
299            },
300        }
301    }
302
303    fn cpu_read_unmapped(&self, addr: u16) -> bool {
304        // v2.7.2 (core audit §5.5): M303 and M312 drive nothing at
305        // `$6000-$7FFF`, so it floats there. M305 / M306 map PRG-ROM into that
306        // window, M56 RAM or ROM, and M142 ROM, so theirs stays mapped.
307        let floats_6000 = matches!(self.board, KaiserBoard::M303 | KaiserBoard::M312)
308            && matches!(addr, 0x6000..=0x7FFF);
309        floats_6000
310            || match self.board {
311                KaiserBoard::M303 => addr != 0x4030 && (0x4020..=0x5FFF).contains(&addr),
312                _ => (0x4020..=0x5FFF).contains(&addr),
313            }
314    }
315
316    fn cpu_write(&mut self, addr: u16, value: u8) {
317        match self.board {
318            KaiserBoard::M56 | KaiserBoard::M142 => match addr & 0xF000 {
319                // KS202 (M56) carries "8 KB PRG RAM bank (not battery
320                // backed!)" at `$6000-$7FFF` (nesdev_wiki/INES_Mapper_056).
321                // The read path served it but no write path existed, so the
322                // game's work RAM silently stayed zero (v2.7.2, found by
323                // `tests/prg_ram_window_open_bus.rs`). M142 has no RAM there
324                // per the wiki: its window is PRG-ROM (core ledger F-09).
325                0x6000 | 0x7000 => {
326                    if self.board == KaiserBoard::M56 && !self.use_rom {
327                        self.wram[addr as usize & 0x1FFF] = value;
328                    }
329                }
330                0x8000 => self.irq_reload = (self.irq_reload & 0xFFF0) | (value as u16 & 0x0F),
331                0x9000 => {
332                    self.irq_reload = (self.irq_reload & 0xFF0F) | ((value as u16 & 0x0F) << 4);
333                }
334                0xA000 => {
335                    self.irq_reload = (self.irq_reload & 0xF0FF) | ((value as u16 & 0x0F) << 8);
336                }
337                0xB000 => {
338                    self.irq_reload = (self.irq_reload & 0x0FFF) | ((value as u16 & 0x0F) << 12);
339                }
340                0xC000 => {
341                    self.irq_control = value;
342                    if value & 0x02 != 0 {
343                        self.irq_counter = self.irq_reload;
344                    }
345                    self.irq_enabled = value & 0x02 != 0;
346                    self.irq_pending = false;
347                }
348                0xD000 => self.irq_pending = false,
349                0xE000 => self.selected_reg = (value & 0x07).wrapping_sub(1),
350                0xF000 => {
351                    match self.selected_reg {
352                        0..=3 => {
353                            let i = self.selected_reg as usize;
354                            self.prg_regs[i] = (self.prg_regs[i] & 0x10) | (value & 0x0F);
355                        }
356                        4 => self.use_rom = value & 0x04 != 0,
357                        _ => {}
358                    }
359                    if self.board == KaiserBoard::M56 {
360                        match addr & 0xFC00 {
361                            0xF000 => {
362                                let bank = (addr & 0x03) as usize;
363                                self.prg_regs[bank] = (value & 0x10) | (self.prg_regs[bank] & 0x0F);
364                            }
365                            0xF800 => {
366                                self.mirroring = if value & 0x01 != 0 {
367                                    Mirroring::Vertical
368                                } else {
369                                    Mirroring::Horizontal
370                                };
371                            }
372                            0xFC00 => self.chr_banks[(addr & 0x07) as usize] = value,
373                            _ => {}
374                        }
375                    }
376                }
377                _ => {}
378            },
379            KaiserBoard::M303 => {
380                if addr & 0xFF00 == 0x4A00 {
381                    self.prg16 = (((addr >> 2) & 0x03) | ((addr >> 4) & 0x04)) as u8;
382                } else if addr == 0x4020 {
383                    self.irq_pending = false;
384                    self.irq_counter = (self.irq_counter & 0xFF00) | value as u16;
385                } else if addr == 0x4021 {
386                    self.irq_pending = false;
387                    self.irq_counter = (self.irq_counter & 0x00FF) | ((value as u16) << 8);
388                    self.irq_enabled = true;
389                } else if addr == 0x4025 {
390                    self.mirroring = if (value >> 3) & 0x01 != 0 {
391                        Mirroring::Horizontal
392                    } else {
393                        Mirroring::Vertical
394                    };
395                }
396            }
397            KaiserBoard::M305 => {
398                if (0x8000..=0xFFFF).contains(&addr) {
399                    self.regs4[((addr >> 11) & 0x03) as usize] = value;
400                }
401            }
402            KaiserBoard::M306 => {
403                if addr >= 0x8000 {
404                    let mode = (addr & 0x30) == 0x30;
405                    match addr & 0xD943 {
406                        0xD943 => {
407                            self.win_6000 = if mode {
408                                0x0B
409                            } else {
410                                ((addr >> 2) & 0x0F) as u8
411                            };
412                        }
413                        0xD903 => {
414                            self.win_6000 = if mode {
415                                0x08 | ((addr >> 2) & 0x03) as u8
416                            } else {
417                                0x0B
418                            };
419                        }
420                        _ => {}
421                    }
422                }
423            }
424            KaiserBoard::M312 => {
425                if addr < 0x8000 {
426                    if (0x6000..=0x7FFF).contains(&addr) {
427                        self.prg16 = value;
428                    }
429                } else {
430                    self.mirroring = if value & 0x01 != 0 {
431                        Mirroring::Horizontal
432                    } else {
433                        Mirroring::Vertical
434                    };
435                }
436            }
437        }
438    }
439
440    fn ppu_read(&mut self, addr: u16) -> u8 {
441        let addr = addr & 0x3FFF;
442        match addr {
443            0x0000..=0x1FFF => {
444                if self.chr_is_ram {
445                    return self.chr[addr as usize & (self.chr.len() - 1)];
446                }
447                if self.board == KaiserBoard::M56 {
448                    let slot = (addr as usize) / CHR_BANK_1K;
449                    let count = self.chr_count_1k();
450                    let bank = (self.chr_banks[slot] as usize) % count;
451                    return self.chr[bank * CHR_BANK_1K + (addr as usize & 0x3FF)];
452                }
453                self.chr[addr as usize & (self.chr.len() - 1)]
454            }
455            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
456            _ => 0,
457        }
458    }
459
460    fn ppu_write(&mut self, addr: u16, value: u8) {
461        let addr = addr & 0x3FFF;
462        match addr {
463            0x0000..=0x1FFF if self.chr_is_ram => {
464                self.chr[addr as usize & (self.chr.len() - 1)] = value;
465            }
466            0x2000..=0x3EFF => {
467                let off = nametable_offset(addr, self.mirroring);
468                self.vram[off] = value;
469            }
470            _ => {}
471        }
472    }
473
474    fn notify_cpu_cycle(&mut self) {
475        match self.board {
476            KaiserBoard::M56 | KaiserBoard::M142 => {
477                if self.irq_control & 0x02 != 0 {
478                    self.irq_counter = self.irq_counter.wrapping_add(1);
479                    if self.irq_counter == 0xFFFF {
480                        self.irq_counter = self.irq_reload;
481                        self.irq_control &= !0x02;
482                        self.irq_pending = true;
483                    }
484                }
485            }
486            KaiserBoard::M303 if self.irq_enabled && self.irq_counter != 0 => {
487                self.irq_counter -= 1;
488                if self.irq_counter == 0 {
489                    self.irq_enabled = false;
490                    self.irq_pending = true;
491                }
492            }
493            _ => {}
494        }
495    }
496
497    fn irq_pending(&self) -> bool {
498        self.irq_pending
499    }
500
501    fn irq_acknowledge(&mut self) {
502        self.irq_pending = false;
503    }
504
505    fn current_mirroring(&self) -> Mirroring {
506        self.mirroring
507    }
508
509    fn save_state(&self) -> Vec<u8> {
510        let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
511        let mut out =
512            Vec::with_capacity(1 + Self::SAVE_LEN + self.vram.len() + self.wram.len() + chr_ram);
513        out.push(SAVE_STATE_VERSION);
514        out.extend_from_slice(&self.prg_regs);
515        out.push(self.selected_reg);
516        out.push(u8::from(self.use_rom));
517        out.extend_from_slice(&self.chr_banks);
518        out.push(self.win_6000);
519        out.extend_from_slice(&self.regs4);
520        out.push(self.prg16);
521        out.extend_from_slice(&self.irq_counter.to_le_bytes());
522        out.extend_from_slice(&self.irq_reload.to_le_bytes());
523        out.push(u8::from(self.irq_enabled));
524        out.push(self.irq_control);
525        out.push(u8::from(self.irq_pending));
526        out.push(mirroring_to_byte(self.mirroring));
527        out.extend_from_slice(&self.vram);
528        out.extend_from_slice(&self.wram);
529        if self.chr_is_ram {
530            out.extend_from_slice(&self.chr);
531        }
532        out
533    }
534
535    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
536        let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
537        let expected = 1 + Self::SAVE_LEN + self.vram.len() + self.wram.len() + chr_ram;
538        if data.len() != expected {
539            return Err(MapperError::WrongLength {
540                expected,
541                got: data.len(),
542            });
543        }
544        if data[0] != SAVE_STATE_VERSION {
545            return Err(MapperError::UnsupportedVersion(data[0]));
546        }
547        let mut c = 1;
548        self.prg_regs.copy_from_slice(&data[c..c + 4]);
549        c += 4;
550        self.selected_reg = data[c];
551        self.use_rom = data[c + 1] != 0;
552        c += 2;
553        self.chr_banks.copy_from_slice(&data[c..c + 8]);
554        c += 8;
555        self.win_6000 = data[c];
556        c += 1;
557        self.regs4.copy_from_slice(&data[c..c + 4]);
558        c += 4;
559        self.prg16 = data[c];
560        c += 1;
561        self.irq_counter = u16::from_le_bytes([data[c], data[c + 1]]);
562        self.irq_reload = u16::from_le_bytes([data[c + 2], data[c + 3]]);
563        c += 4;
564        self.irq_enabled = data[c] != 0;
565        self.irq_control = data[c + 1];
566        self.irq_pending = data[c + 2] != 0;
567        self.mirroring = byte_to_mirroring(data[c + 3], self.mirroring);
568        c += 4;
569        self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
570        c += self.vram.len();
571        self.wram.copy_from_slice(&data[c..c + self.wram.len()]);
572        c += self.wram.len();
573        if self.chr_is_ram {
574            self.chr.copy_from_slice(&data[c..c + self.chr.len()]);
575        }
576        Ok(())
577    }
578}
579
580macro_rules! kaiser_ctor {
581    ($fn_name:ident, $board:expr, $id:expr, $doc:expr) => {
582        #[doc = $doc]
583        ///
584        /// # Errors
585        /// [`MapperError::Invalid`] on a bad PRG/CHR size.
586        pub fn $fn_name(
587            prg_rom: Box<[u8]>,
588            chr_rom: Box<[u8]>,
589            mirroring: Mirroring,
590        ) -> Result<KaiserMapper, MapperError> {
591            KaiserMapper::new($board, prg_rom, chr_rom, mirroring, $id)
592        }
593    };
594}
595
596kaiser_ctor!(new_m56, KaiserBoard::M56, 56, "Mapper 56 (Kaiser KS202).");
597kaiser_ctor!(
598    new_m142,
599    KaiserBoard::M142,
600    142,
601    "Mapper 142 (Kaiser KS7032)."
602);
603kaiser_ctor!(
604    new_m303,
605    KaiserBoard::M303,
606    303,
607    "Mapper 303 (Kaiser KS7017)."
608);
609kaiser_ctor!(
610    new_m305,
611    KaiserBoard::M305,
612    305,
613    "Mapper 305 (Kaiser KS7031)."
614);
615kaiser_ctor!(
616    new_m306,
617    KaiserBoard::M306,
618    306,
619    "Mapper 306 (Kaiser KS7016)."
620);
621kaiser_ctor!(
622    new_m312,
623    KaiserBoard::M312,
624    312,
625    "Mapper 312 (Kaiser KS7013B)."
626);
627
628// ===========================================================================
629// Waixing253 (mapper 253) — Waixing VRC4-clone, Dragon Ball Z.
630//
631// Per-1 KiB CHR low/high registers ($B000-$E00C), a CHR-RAM escape (CHR reg
632// value 4/5 + a force-ROM toggle on slot 0 via $88/$C8), two 8 KiB PRG selects
633// ($8010/$A010), $9400 mirroring, and a /114-scaled CPU-cycle IRQ ($F000 etc.).
634// Register map per the NESdev wiki mapper-253 documentation; the implementation
635// is derived from Mesen2's `Waixing/Mapper253.h` (GPL-3.0-or-later).
636// See NOTICE + docs/originality-and-provenance.md §1.
637// ===========================================================================
638
639#[cfg(test)]
640#[allow(clippy::cast_possible_truncation)]
641mod tests {
642    use super::*;
643
644    fn synth_prg_8k(banks: usize) -> Box<[u8]> {
645        let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
646        for b in 0..banks {
647            v[b * PRG_BANK_8K] = b as u8;
648        }
649        v.into_boxed_slice()
650    }
651
652    fn synth_prg_16k(banks: usize) -> Box<[u8]> {
653        let mut v = vec![0xFFu8; banks * PRG_BANK_16K];
654        for b in 0..banks {
655            v[b * PRG_BANK_16K] = b as u8;
656        }
657        v.into_boxed_slice()
658    }
659
660    fn synth_chr_1k(banks: usize) -> Box<[u8]> {
661        let mut v = vec![0u8; banks * CHR_BANK_1K];
662        for b in 0..banks {
663            v[b * CHR_BANK_1K] = b as u8;
664        }
665        v.into_boxed_slice()
666    }
667
668    fn synth_chr_8k(banks: usize) -> Box<[u8]> {
669        let mut v = vec![0u8; banks * CHR_BANK_8K];
670        for b in 0..banks {
671            v[b * CHR_BANK_8K] = b as u8;
672        }
673        v.into_boxed_slice()
674    }
675
676    fn synth_prg_2k_tagged(banks: usize) -> Box<[u8]> {
677        let mut v = vec![0xFFu8; banks * PRG_BANK_2K];
678        for b in 0..banks {
679            v[b * PRG_BANK_2K] = b as u8;
680        }
681        v.into_boxed_slice()
682    }
683
684    #[test]
685    fn kaiser202_prg_regs_and_up_count_irq() {
686        let mut m = new_m142(synth_prg_8k(16), synth_chr_8k(1), Mirroring::Vertical).unwrap();
687        m.cpu_write(0xE000, 0x01); // select reg (1-1=0)
688        m.cpu_write(0xF000, 0x03); // prg_regs[0] low = 3
689        assert_eq!(m.cpu_read(0x8000), 3);
690
691        // IRQ: reload, enable, count up to 0xFFFF.
692        m.cpu_write(0x8000, 0x0E); // reload low nibble
693        m.cpu_write(0xC000, 0x02); // enable + load
694        // Counter loads 0x...E; count up until 0xFFFF wraps.
695        let mut fired = false;
696        for _ in 0..0x20000 {
697            m.notify_cpu_cycle();
698            if m.irq_pending() {
699                fired = true;
700                break;
701            }
702        }
703        assert!(fired);
704    }
705
706    /// KS7032's `$6000-$7FFF` is "8 KB switchable PRG ROM bank" selected by
707    /// bank-select value 4 (`nesdev_wiki/INES_Mapper_142`), with no RAM. It
708    /// used to read a zero work RAM there until the undocumented value 5 set
709    /// a ROM flag (core ledger F-09, closed v2.9.5).
710    #[test]
711    fn ks7032_6000_window_is_prg_rom_from_bank_select_4() {
712        let mut m = new_m142(synth_prg_8k(16), synth_chr_8k(1), Mirroring::Vertical).unwrap();
713        // Power-on: the register is 0, so the window shows PRG bank 0.
714        assert_eq!(m.cpu_read(0x6000), 0);
715        m.cpu_write(0xE000, 0x04); // select the $6000 bank register
716        m.cpu_write(0xF000, 0x05);
717        assert_eq!(m.cpu_read(0x6000), 5);
718        // ROM, not RAM: a write does not stick.
719        m.cpu_write(0x6000, 0xA5);
720        assert_eq!(m.cpu_read(0x6000), 5);
721        assert!(!m.cpu_read_unmapped(0x6000));
722    }
723
724    #[test]
725    fn kaiser202_save_state_round_trip() {
726        let mut m = new_m56(synth_prg_8k(16), synth_chr_1k(8), Mirroring::Vertical).unwrap();
727        m.cpu_write(0xE000, 0x01);
728        m.cpu_write(0xF000, 0x05);
729        m.cpu_write(0xFC00, 0x02); // m56 CHR write
730        m.ppu_write(0x2002, 0x44);
731        let blob = m.save_state();
732        let mut m2 = new_m56(synth_prg_8k(16), synth_chr_1k(8), Mirroring::Vertical).unwrap();
733        m2.load_state(&blob).unwrap();
734        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
735        assert_eq!(m2.ppu_read(0x2002), 0x44);
736    }
737
738    #[test]
739    fn kaiser7017_prg_and_down_count_irq() {
740        let mut m = new_m303(synth_prg_16k(8), synth_chr_8k(1), Mirroring::Vertical).unwrap();
741        // $4Axx address-decoded PRG select.
742        m.cpu_write(0x4A00 | (1 << 2), 0); // prg16 = ((1<<2)>>2)&3 = 1
743        assert_eq!(m.cpu_read(0x8000), 1);
744
745        m.cpu_write(0x4020, 0x03); // counter low
746        m.cpu_write(0x4021, 0x00); // counter high + enable -> counter = 3
747        for _ in 0..3 {
748            m.notify_cpu_cycle();
749        }
750        assert!(m.irq_pending());
751        assert_eq!(m.cpu_read(0x4030), 0x01); // read-ack returns pending then clears.
752        assert!(!m.irq_pending());
753    }
754
755    #[test]
756    fn kaiser7031_windowed_prg() {
757        // 8 KiB == 4 x 2 KiB pages; use a 2 KiB-tagged 16 KiB image (8 pages).
758        let mut m = new_m305(synth_prg_2k_tagged(8), synth_chr_8k(1), Mirroring::Vertical).unwrap();
759        // $8000-$FFFF: window = (addr>>11)&3, value = 2 KiB page index.
760        m.cpu_write(0x8000, 5); // regs4[0] = 5
761        assert_eq!(m.cpu_read(0x6000), 5); // first 2 KiB $6000 window -> page 5.
762    }
763
764    #[test]
765    fn kaiser7031_save_state_round_trip() {
766        let mut m = new_m305(synth_prg_8k(8), Box::new([]), Mirroring::Vertical).unwrap();
767        m.cpu_write(0x8000, 3);
768        m.cpu_write(0x8800, 4);
769        m.ppu_write(0x0005, 0x21);
770        let blob = m.save_state();
771        let mut m2 = new_m305(synth_prg_8k(8), Box::new([]), Mirroring::Vertical).unwrap();
772        m2.load_state(&blob).unwrap();
773        assert_eq!(m2.cpu_read(0x6000), m.cpu_read(0x6000));
774        assert_eq!(m2.ppu_read(0x0005), 0x21);
775    }
776
777    #[test]
778    fn kaiser7016_window_decode() {
779        let mut m = new_m306(synth_prg_8k(16), synth_chr_8k(1), Mirroring::Vertical).unwrap();
780        // $D943 with mode bits (addr&0x30 != 0x30) -> _prgReg = (addr>>2)&0x0F.
781        let addr = 0xD943; // addr&0x30 = 0x00 -> not mode -> reg = (0xD943>>2)&0x0F
782        m.cpu_write(addr, 0);
783        let v = m.cpu_read(0x6000);
784        assert!((v as usize) < 16);
785    }
786
787    #[test]
788    fn kaiser7013b_prg_and_mirror() {
789        let mut m = new_m312(synth_prg_16k(8), synth_chr_8k(1), Mirroring::Vertical).unwrap();
790        m.cpu_write(0x6000, 3); // prg16 = 3
791        assert_eq!(m.cpu_read(0x8000), 3);
792        assert_eq!(m.cpu_read(0xC000), 7); // fixed last bank.
793        m.cpu_write(0x8000, 0x01); // horizontal
794        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
795    }
796}