Skip to main content

rustynes_mappers/
m016_bandai_fcg.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//
3// Provenance: the Bandai FCG serial-EEPROM handling (`Eeprom24C01` / `Eeprom24C02`) is derived from Mesen2 (GPL-3.0-or-later), `Core/NES/Mappers/Bandai/`. See docs/originality-and-provenance.md (Section 1)
4// and NOTICE for the complete, audited derivation record.
5//! Bandai FCG (iNES mappers 16 and 159) implementation.
6//!
7//! Covers the Bandai FCG-1/FCG-2 and LZ93D50 ASICs. Banking: a 16 KiB
8//! switchable PRG bank at `$8000-$BFFF` (last bank fixed at `$C000`), eight
9//! independent 1 KiB CHR banks, software mirroring control, and a 16-bit
10//! down-counting CPU-cycle (M2) IRQ. Some boards add a serial I²C EEPROM
11//! (24C02 on mapper 16 submapper 5, X24C01 on mapper 159).
12//!
13//! # Register window (`nesdev_wiki/INES_Mapper_016.xhtml`)
14//!
15//! The register block is the same set of offsets `$0-$D`, but the decode
16//! window differs by submapper:
17//!
18//! - Submapper 4 (FCG-1/2): registers respond at `$6000-$7FFF` (mask
19//!   `$E00F`); writes to `$600B/$600C` modify the counter directly.
20//! - Submapper 5 (LZ93D50): registers respond at `$8000-$FFFF` (mask
21//!   `$800F`); `$800B/$800C` modify a *latch* copied to the counter on a
22//!   `$800A` write; an EEPROM read appears in bit 4 of `$6000-$7FFF`.
23//! - Submapper 0 (unspecified): respond in both ranges (the union).
24//!
25//! Mapper 159 is mapper 16 submapper 5 with a 128-byte X24C01 EEPROM
26//! (instead of the 256-byte 24C02 on mapper 16).
27//!
28//! Mapper 153 (v2.9.6, written from `nesdev_wiki/output/INES_Mapper_153.md`)
29//! is an LZ93D50 with 8 KiB of battery-backed WRAM and no EEPROM. Its CHR
30//! registers `$8000-$8003` become the outer 256 KiB PRG bank (bit 0, "the same
31//! value must be written to all four"); CHR is 8 KiB of unbanked CHR-RAM; and
32//! `$800D` bit 5 enables the WRAM chip. The page notes that *Famicom Jump II*
33//! freezes when it boots with zero-filled WRAM and runs after a soft reset;
34//! that is the game, not the board.
35//!
36//! ## Offsets (relative to the window base, masked to `$x..F`)
37//!
38//! | Offset | Function                                              |
39//! |--------|-------------------------------------------------------|
40//! | `$0-7` | 1 KiB CHR bank N at PPU `$N*0x400`                     |
41//! | `$8`   | 16 KiB PRG bank at `$8000` (low 4 bits)               |
42//! | `$9`   | Mirroring (0 V, 1 H, 2 1scA, 3 1scB)                  |
43//! | `$A`   | IRQ control: bit 0 enable; LZ93D50 also latch->counter |
44//! | `$B`   | IRQ counter/latch low byte                            |
45//! | `$C`   | IRQ counter/latch high byte                           |
46//! | `$D`   | EEPROM control (LZ93D50): bit 5 SCL, bit 6 SDA, bit 7 dir |
47//!
48//! # EEPROM
49//!
50//! An I²C state machine ([`Eeprom`]) for the X24C01 (159) / 24C02 (16) is
51//! implemented below. Provenance: it is **derived from Mesen2's `Eeprom24C01` /
52//! `Eeprom24C02`** (`Core/NES/Mappers/Bandai/`, GPL-3.0-or-later); the I2C
53//! protocol it models is the published Xicor/Intersil X24C01 / 24C02 datasheet.
54//! See NOTICE and docs/originality-and-provenance.md (Section 1). It clocks bits on the SCL
55//! **rising** edge and
56//! advances the mode/ACK handshake on the **falling** edge, detects
57//! START/STOP as SDA transitions while SCL is held high, and honors the two
58//! chips' differing bit order (X24C01 LSB-first, 24C02 MSB-first) and
59//! addressing (X24C01 combined word-address+R/W byte vs. 24C02
60//! device-select + word-address). The X24C01 bit order and rise/fall
61//! handshake were the boot-blocker for the mapper-159 games (a blank screen
62//! while the game busy-waited on the EEPROM probe). It is **not**
63//! datasheet-timing-verified — there are no redistributable behavioral test
64//! fixtures for these boards — so it is verified at the unit-test +
65//! boot-smoke level against the reference emulators.
66
67#![allow(
68    clippy::cast_possible_truncation,
69    clippy::cast_lossless,
70    clippy::struct_excessive_bools,
71    clippy::missing_const_for_fn,
72    clippy::doc_markdown,
73    clippy::option_if_let_else
74)]
75
76use crate::cartridge::Mirroring;
77use crate::mapper::{Mapper, MapperCaps, MapperError};
78use alloc::{boxed::Box, vec::Vec};
79use alloc::{format, vec};
80
81const PRG_BANK_16K: usize = 0x4000;
82const CHR_BANK_1K: usize = 0x0400;
83const NAMETABLE_SIZE: usize = 0x0400;
84const NAMETABLE_SIZE_U16: u16 = 0x0400;
85
86/// v2 (v2.9.6) appends mapper 153's outer bank, WRAM enable and WRAM. Since
87/// v2.9.8 (ADR 0042) a v1 blob is refused; it used to load on the variants
88/// that have neither.
89const SAVE_STATE_VERSION: u8 = 2;
90
91/// Mapper 153's WRAM.
92const WRAM_153: usize = 0x2000;
93
94/// FCG board / EEPROM variant.
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum FcgVariant {
97    /// Mapper 16 submapper 0: respond in both `$6000-$7FFF` and
98    /// `$8000-$FFFF`; behaves as LZ93D50 (latched counter) with a 24C02.
99    Both,
100    /// Mapper 16 submapper 4: FCG-1/2, register window `$6000-$7FFF`,
101    /// counter written directly, no EEPROM.
102    Fcg,
103    /// Mapper 16 submapper 5: LZ93D50, register window `$8000-$FFFF`,
104    /// latched counter, optional 256-byte 24C02 EEPROM.
105    Lz93d50_24c02,
106    /// Mapper 159: LZ93D50 with a 128-byte X24C01 EEPROM.
107    Lz93d50_24c01,
108    /// Mapper 153: LZ93D50 with 8 KiB of battery-backed WRAM and an outer
109    /// 256 KiB PRG bank in place of the CHR registers.
110    Lz93d50Wram,
111}
112
113impl FcgVariant {
114    const fn responds_low(self) -> bool {
115        matches!(self, Self::Both | Self::Fcg)
116    }
117    const fn responds_high(self) -> bool {
118        matches!(
119            self,
120            Self::Both | Self::Lz93d50_24c02 | Self::Lz93d50_24c01 | Self::Lz93d50Wram
121        )
122    }
123    /// LZ93D50 latches the IRQ counter (`$x0B/$x0C` write a latch); FCG-1/2
124    /// writes the counter directly.
125    const fn latched_counter(self) -> bool {
126        !matches!(self, Self::Fcg)
127    }
128    const fn eeprom_bytes(self) -> usize {
129        match self {
130            Self::Lz93d50_24c01 => 128,
131            Self::Both | Self::Lz93d50_24c02 => 256,
132            Self::Fcg | Self::Lz93d50Wram => 0,
133        }
134    }
135    /// X24C01 uses a combined 7-bit word-address byte; the 24C02 uses a
136    /// device-select byte followed by a separate word-address byte.
137    const fn is_x24c01(self) -> bool {
138        matches!(self, Self::Lz93d50_24c01)
139    }
140}
141
142/// Serial I²C EEPROM (X24C01 / 24C02) state machine.
143///
144/// An independent I2C state machine for the X24C01 / 24C02 serial EEPROMs,
145/// implemented from the published I2C serial-EEPROM datasheet protocol. The
146/// protocol is driven on
147/// **both** SCL edges: bits are clocked on the rising edge, and the
148/// mode/ACK handshake advances on the falling edge — exactly how the boards
149/// drive the line. START / STOP are detected as SDA transitions while SCL is
150/// held high.
151///
152/// The two chips differ in two ways that matter for boot:
153///
154/// - **Bit order.** The X24C01 (mapper 159) shifts addr/data **LSB-first**
155///   (`bit << counter`); the 24C02 (mapper 16) shifts **MSB-first**
156///   (`bit << (7 - counter)`).
157/// - **Addressing.** The X24C01 takes a single combined byte (7-bit word
158///   address + R/W bit); the 24C02 takes a device-select byte (`0xA0 | …`)
159///   followed by a separate word-address byte.
160///
161/// It is not a cycle/timing-accurate datasheet model, but it matches the
162/// reference emulators' boot-relevant behavior.
163#[derive(Debug, Clone)]
164struct Eeprom {
165    mem: Box<[u8]>,
166    is_x24c01: bool,
167
168    last_scl: u8,
169    last_sda: u8,
170
171    mode: I2cMode,
172    next_mode: I2cMode,
173    /// Device-select byte (24C02 only; includes the R/W bit).
174    chip_addr: u8,
175    /// Word address into `mem`.
176    addr: u8,
177    /// Byte being shifted in (write) or out (read).
178    data: u8,
179    /// Bits shifted so far in the current byte (0..=8).
180    counter: u8,
181    /// The bit currently driven back onto SDA toward the CPU (high = 1).
182    output: u8,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186enum I2cMode {
187    Idle,
188    ChipAddress,
189    Address,
190    Read,
191    Write,
192    SendAck,
193    WaitAck,
194}
195
196impl Eeprom {
197    fn new(bytes: usize, is_x24c01: bool) -> Self {
198        Self {
199            mem: vec![0xFFu8; bytes.max(1)].into_boxed_slice(),
200            is_x24c01,
201            last_scl: 0,
202            last_sda: 0,
203            mode: I2cMode::Idle,
204            next_mode: I2cMode::Idle,
205            chip_addr: 0,
206            addr: 0,
207            data: 0,
208            counter: 0,
209            output: 1,
210        }
211    }
212
213    fn addr_mask(&self) -> u8 {
214        if self.is_x24c01 { 0x7F } else { 0xFF }
215    }
216
217    /// Shift one bit into `dest` at the current `counter` position, honoring
218    /// the chip's bit order, and advance the counter.
219    fn write_bit(&mut self, dest: &mut u8, value: u8) {
220        if self.counter < 8 {
221            let shift = if self.is_x24c01 {
222                self.counter
223            } else {
224                7 - self.counter
225            };
226            let mask = !(1u8 << shift);
227            *dest = (*dest & mask) | (value << shift);
228            self.counter += 1;
229        }
230    }
231
232    /// Drive `output` from the current `data` bit at `counter`, honoring the
233    /// chip's bit order, and advance the counter.
234    fn read_bit(&mut self) {
235        if self.counter < 8 {
236            let shift = if self.is_x24c01 {
237                self.counter
238            } else {
239                7 - self.counter
240            };
241            self.output = u8::from((self.data & (1u8 << shift)) != 0);
242            self.counter += 1;
243        }
244    }
245
246    /// Drive the EEPROM lines from the `$x00D` register. `scl`/`sda` are the
247    /// host-driven clock/data levels; the device's response appears on
248    /// [`Self::read_sda`].
249    fn write_lines(&mut self, scl_b: bool, sda_b: bool) {
250        let scl = u8::from(scl_b);
251        let sda = u8::from(sda_b);
252
253        if self.last_scl != 0 && scl != 0 && sda < self.last_sda {
254            // START: SDA high->low while SCL stable high.
255            self.mode = if self.is_x24c01 {
256                I2cMode::Address
257            } else {
258                I2cMode::ChipAddress
259            };
260            self.addr = 0;
261            self.counter = 0;
262            self.output = 1;
263        } else if self.last_scl != 0 && scl != 0 && sda > self.last_sda {
264            // STOP: SDA low->high while SCL stable high.
265            self.mode = I2cMode::Idle;
266            self.output = 1;
267        } else if scl > self.last_scl {
268            self.clock_rise(sda);
269        } else if scl < self.last_scl {
270            self.clock_fall(sda);
271        }
272
273        self.last_scl = scl;
274        self.last_sda = sda;
275    }
276
277    fn read_sda(&self) -> bool {
278        self.output != 0
279    }
280
281    fn clock_rise(&mut self, sda: u8) {
282        match self.mode {
283            I2cMode::ChipAddress => {
284                let mut chip = self.chip_addr;
285                self.write_bit(&mut chip, sda);
286                self.chip_addr = chip;
287            }
288            I2cMode::Address => {
289                if self.is_x24c01 {
290                    // X24C01: 7 address bits, then the 8th bit selects R/W.
291                    if self.counter < 7 {
292                        let mut addr = self.addr;
293                        self.write_bit(&mut addr, sda);
294                        self.addr = addr;
295                    } else if self.counter == 7 {
296                        self.counter = 8;
297                        if sda != 0 {
298                            self.next_mode = I2cMode::Read;
299                            self.data = self.mem[(self.addr & 0x7F) as usize];
300                        } else {
301                            self.next_mode = I2cMode::Write;
302                        }
303                    }
304                } else {
305                    let mut addr = self.addr;
306                    self.write_bit(&mut addr, sda);
307                    self.addr = addr;
308                }
309            }
310            I2cMode::Read => self.read_bit(),
311            I2cMode::Write => {
312                let mut data = self.data;
313                self.write_bit(&mut data, sda);
314                self.data = data;
315            }
316            I2cMode::SendAck => self.output = 0,
317            I2cMode::WaitAck => {
318                if sda == 0 {
319                    if self.is_x24c01 {
320                        // X24C01: the master ack ends the read; STOP follows.
321                        self.next_mode = I2cMode::Idle;
322                    } else {
323                        // 24C02: sequential read continues to the next byte.
324                        self.next_mode = I2cMode::Read;
325                        self.data = self.mem[self.addr as usize];
326                    }
327                }
328            }
329            I2cMode::Idle => {}
330        }
331    }
332
333    fn clock_fall(&mut self, _sda: u8) {
334        match self.mode {
335            I2cMode::ChipAddress => {
336                if self.counter == 8 {
337                    if (self.chip_addr & 0xA0) == 0xA0 {
338                        self.mode = I2cMode::SendAck;
339                        self.counter = 0;
340                        self.output = 1;
341                        if self.chip_addr & 0x01 != 0 {
342                            self.next_mode = I2cMode::Read;
343                            self.data = self.mem[self.addr as usize];
344                        } else {
345                            self.next_mode = I2cMode::Address;
346                        }
347                    } else {
348                        self.mode = I2cMode::Idle;
349                        self.counter = 0;
350                        self.output = 1;
351                    }
352                }
353            }
354            I2cMode::Address => {
355                if self.is_x24c01 {
356                    if self.counter == 8 {
357                        // Ack the address, then run the queued read/write.
358                        self.mode = I2cMode::SendAck;
359                        self.output = 1;
360                    }
361                } else if self.counter == 8 {
362                    self.counter = 0;
363                    self.mode = I2cMode::SendAck;
364                    self.next_mode = I2cMode::Write;
365                    self.output = 1;
366                }
367            }
368            I2cMode::SendAck => {
369                self.mode = self.next_mode;
370                self.counter = 0;
371                self.output = 1;
372            }
373            I2cMode::Read => {
374                if self.counter == 8 {
375                    self.mode = I2cMode::WaitAck;
376                    // The EEPROM byte-address counter is a hardware ring: it advances
377                    // within the device and rolls over at the top rather than
378                    // faulting. `addr` is a `u8` and `addr_mask()` is 0xFF on every
379                    // chip but the X24C01, so the mask cannot express that rollover
380                    // on its own -- the add traps in a debug build first. Wrap
381                    // explicitly; the mask still confines the X24C01 to 7 bits.
382                    self.addr = self.addr.wrapping_add(1) & self.addr_mask();
383                }
384            }
385            I2cMode::Write => {
386                if self.counter == 8 {
387                    self.mode = I2cMode::SendAck;
388                    self.next_mode = if self.is_x24c01 {
389                        I2cMode::Idle
390                    } else {
391                        I2cMode::Write
392                    };
393                    self.mem[(self.addr & self.addr_mask()) as usize] = self.data;
394                    // The EEPROM byte-address counter is a hardware ring: it advances
395                    // within the device and rolls over at the top rather than
396                    // faulting. `addr` is a `u8` and `addr_mask()` is 0xFF on every
397                    // chip but the X24C01, so the mask cannot express that rollover
398                    // on its own -- the add traps in a debug build first. Wrap
399                    // explicitly; the mask still confines the X24C01 to 7 bits.
400                    self.addr = self.addr.wrapping_add(1) & self.addr_mask();
401                    self.counter = 0;
402                }
403            }
404            I2cMode::WaitAck => {
405                if !self.is_x24c01 {
406                    self.mode = self.next_mode;
407                    self.counter = 0;
408                    self.output = 1;
409                }
410            }
411            I2cMode::Idle => {}
412        }
413    }
414}
415
416/// Bandai FCG mapper (iNES mappers 16 + 159).
417pub struct BandaiFcg {
418    prg_rom: Box<[u8]>,
419    chr: Box<[u8]>,
420    vram: Box<[u8]>,
421    chr_is_ram: bool,
422
423    variant: FcgVariant,
424
425    prg_bank: u8,
426    chr_banks: [u8; 8],
427    mirroring: Mirroring,
428
429    // 16-bit down-counting IRQ.
430    irq_latch: u16,
431    irq_counter: u16,
432    irq_enabled: bool,
433    irq_pending: bool,
434
435    eeprom: Option<Eeprom>,
436    // Last value written to the EEPROM control register (for save-state).
437    eeprom_ctrl: u8,
438
439    /// Mapper 153: the outer 256 KiB PRG bank (`$8000-$8003` bit 0).
440    outer: u8,
441    /// Mapper 153: `$800D` bit 5, the WRAM chip enable.
442    wram_enabled: bool,
443    /// Mapper 153: 8 KiB of battery-backed WRAM; empty on the other variants.
444    wram: Box<[u8]>,
445}
446
447impl BandaiFcg {
448    /// Construct a new Bandai FCG mapper.
449    ///
450    /// `prg_rom` must be a non-zero multiple of 16 KiB; CHR-ROM must be a
451    /// multiple of 1 KiB (CHR-RAM allocated as 8 KiB when empty).
452    ///
453    /// # Errors
454    ///
455    /// Returns [`MapperError::Invalid`] on size mismatch.
456    pub fn new(
457        prg_rom: Box<[u8]>,
458        chr_rom: Box<[u8]>,
459        mirroring: Mirroring,
460        variant: FcgVariant,
461    ) -> Result<Self, MapperError> {
462        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
463            return Err(MapperError::Invalid(format!(
464                "Bandai-FCG PRG-ROM size {} is not a non-zero multiple of 16 KiB",
465                prg_rom.len()
466            )));
467        }
468        let chr_is_ram = chr_rom.is_empty();
469        let chr: Box<[u8]> = if chr_is_ram {
470            vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
471        } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
472            chr_rom
473        } else {
474            return Err(MapperError::Invalid(format!(
475                "Bandai-FCG CHR-ROM size {} is not a multiple of 1 KiB",
476                chr_rom.len()
477            )));
478        };
479        let eeprom = if variant.eeprom_bytes() > 0 {
480            Some(Eeprom::new(variant.eeprom_bytes(), variant.is_x24c01()))
481        } else {
482            None
483        };
484        Ok(Self {
485            prg_rom,
486            chr,
487            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
488            chr_is_ram,
489            variant,
490            prg_bank: 0,
491            chr_banks: [0; 8],
492            mirroring,
493            irq_latch: 0,
494            irq_counter: 0,
495            irq_enabled: false,
496            irq_pending: false,
497            eeprom,
498            eeprom_ctrl: 0,
499            outer: 0,
500            wram_enabled: false,
501            wram: if variant == FcgVariant::Lz93d50Wram {
502                vec![0u8; WRAM_153].into_boxed_slice()
503            } else {
504                Box::new([])
505            },
506        })
507    }
508
509    /// Mapper 153's outer bank as a 16 KiB bank offset (256 KiB = 16 banks).
510    const fn outer_base(&self) -> usize {
511        (self.outer as usize & 0x01) << 4
512    }
513
514    const fn nametable_offset(&self, addr: u16) -> usize {
515        let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
516        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
517        let physical = self.mirroring.physical_bank(table);
518        physical * NAMETABLE_SIZE + local
519    }
520
521    fn chr_offset(&self, addr: u16) -> usize {
522        // Mapper 153: "8 KiB unbanked CHR-RAM" and "No CHR banking is
523        // available" (`INES_Mapper_153`). Its `$8000-$8003` registers drive
524        // the outer PRG bank instead, so `chr_banks` is never written there
525        // and must not be consulted: routing through it mapped all eight
526        // 1 KiB windows onto the first 1 KiB.
527        if self.variant == FcgVariant::Lz93d50Wram {
528            return addr as usize & 0x1FFF;
529        }
530        let slot = (addr as usize / CHR_BANK_1K) & 0x07;
531        let total = (self.chr.len() / CHR_BANK_1K).max(1);
532        let bank = (self.chr_banks[slot] as usize) % total;
533        bank * CHR_BANK_1K + (addr as usize & (CHR_BANK_1K - 1))
534    }
535
536    /// Apply a register write decoded to offset `$0-$F`.
537    fn write_reg(&mut self, off: u8, value: u8) {
538        match off & 0x0F {
539            // Mapper 153: PA12/PA13 are grounded, so `$0-$3` are the outer
540            // PRG bank and `$4-$7` do nothing.
541            0x0..=0x3 if self.variant == FcgVariant::Lz93d50Wram => self.outer = value & 0x01,
542            0x4..=0x7 if self.variant == FcgVariant::Lz93d50Wram => {}
543            0x0..=0x7 => self.chr_banks[(off & 0x07) as usize] = value,
544            0x8 => self.prg_bank = value & 0x0F,
545            0x9 => {
546                self.mirroring = match value & 0x03 {
547                    0 => Mirroring::Vertical,
548                    1 => Mirroring::Horizontal,
549                    2 => Mirroring::SingleScreenA,
550                    _ => Mirroring::SingleScreenB,
551                };
552            }
553            0xA => {
554                // IRQ control. Bit 0 = enable. Writing acknowledges.
555                self.irq_pending = false;
556                self.irq_enabled = (value & 0x01) != 0;
557                if self.variant.latched_counter() {
558                    // LZ93D50: copy latch to counter.
559                    self.irq_counter = self.irq_latch;
560                }
561            }
562            0xB => {
563                if self.variant.latched_counter() {
564                    self.irq_latch = (self.irq_latch & 0xFF00) | value as u16;
565                } else {
566                    self.irq_counter = (self.irq_counter & 0xFF00) | value as u16;
567                }
568            }
569            0xC => {
570                if self.variant.latched_counter() {
571                    self.irq_latch = (self.irq_latch & 0x00FF) | ((value as u16) << 8);
572                } else {
573                    self.irq_counter = (self.irq_counter & 0x00FF) | ((value as u16) << 8);
574                }
575            }
576            0xD if self.variant == FcgVariant::Lz93d50Wram => {
577                self.eeprom_ctrl = value;
578                self.wram_enabled = value & 0x20 != 0;
579            }
580            0xD => {
581                self.eeprom_ctrl = value;
582                if let Some(ee) = self.eeprom.as_mut() {
583                    // Bit 5 = SCL, bit 6 = SDA (host-driven), bit 7 = dir.
584                    let scl = (value & 0x20) != 0;
585                    let sda = (value & 0x40) != 0;
586                    ee.write_lines(scl, sda);
587                }
588            }
589            _ => {}
590        }
591    }
592}
593
594impl Mapper for BandaiFcg {
595    // Battery save: the serial EEPROM's contents -- 128 bytes (24C01) or 256
596    // (24C02) -- when this variant carries one, and nothing otherwise (core
597    // audit IMP-08). The EEPROM is reached through an I2C protocol, not a CPU
598    // window, so this accessor is the ONLY way the frontend can persist it.
599    fn sram(&self) -> &[u8] {
600        if !self.wram.is_empty() {
601            return &self.wram;
602        }
603        self.eeprom.as_ref().map_or(&[], |e| &e.mem)
604    }
605    fn sram_mut(&mut self) -> &mut [u8] {
606        if !self.wram.is_empty() {
607            return &mut self.wram;
608        }
609        match self.eeprom.as_mut() {
610            Some(e) => &mut e.mem,
611            None => &mut [],
612        }
613    }
614
615    // v2.8.0 Phase 4 — CPU-cycle hook + IRQ source; no on-cart audio.
616    fn caps(&self) -> MapperCaps {
617        MapperCaps::CYCLE_IRQ
618    }
619
620    fn cpu_read(&mut self, addr: u16) -> u8 {
621        match addr {
622            0x6000..=0x7FFF if !self.wram.is_empty() => {
623                if self.wram_enabled {
624                    self.wram[usize::from(addr - 0x6000)]
625                } else {
626                    0
627                }
628            }
629            0x6000..=0x7FFF => {
630                // EEPROM read appears in bit 4 (LZ93D50). Otherwise open bus
631                // (the bus's open-bus latch handles unmapped reads, but the
632                // FCG drives bit 4 here).
633                if let Some(ee) = self.eeprom.as_ref() {
634                    let bit = u8::from(ee.read_sda());
635                    return bit << 4;
636                }
637                0
638            }
639            0x8000..=0xBFFF => {
640                let total = (self.prg_rom.len() / PRG_BANK_16K).max(1);
641                let bank = (self.prg_bank as usize | self.outer_base()) % total;
642                self.prg_rom[bank * PRG_BANK_16K + (addr - 0x8000) as usize]
643            }
644            0xC000..=0xFFFF => {
645                let total = (self.prg_rom.len() / PRG_BANK_16K).max(1);
646                // Mapper 153: the last bank of the selected 256 KiB.
647                let last = if self.wram.is_empty() {
648                    total - 1
649                } else {
650                    (0x0F | self.outer_base()) % total
651                };
652                self.prg_rom[last * PRG_BANK_16K + (addr - 0xC000) as usize]
653            }
654            _ => 0,
655        }
656    }
657
658    fn cpu_write(&mut self, addr: u16, value: u8) {
659        if !self.wram.is_empty() && self.wram_enabled && (0x6000..=0x7FFF).contains(&addr) {
660            self.wram[usize::from(addr - 0x6000)] = value;
661        }
662        if self.variant.responds_low() && (0x6000..=0x7FFF).contains(&addr) {
663            self.write_reg((addr & 0x0F) as u8, value);
664        }
665        if self.variant.responds_high() && (0x8000..=0xFFFF).contains(&addr) {
666            self.write_reg((addr & 0x0F) as u8, value);
667        }
668    }
669
670    fn cpu_read_unmapped(&self, addr: u16) -> bool {
671        // The FCG drives bit 4 of $6000-$7FFF (EEPROM) when an EEPROM is
672        // present, so that window is mapped; otherwise default behavior.
673        if self.eeprom.is_some() && (0x6000..=0x7FFF).contains(&addr) {
674            return false;
675        }
676        if !self.wram.is_empty() && (0x6000..=0x7FFF).contains(&addr) {
677            return !self.wram_enabled;
678        }
679        (0x4020..=0x5FFF).contains(&addr)
680    }
681
682    fn ppu_read(&mut self, addr: u16) -> u8 {
683        let addr = addr & 0x3FFF;
684        match addr {
685            0x0000..=0x1FFF => {
686                let off = self.chr_offset(addr);
687                self.chr[off % self.chr.len()]
688            }
689            0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
690            _ => 0,
691        }
692    }
693
694    fn ppu_write(&mut self, addr: u16, value: u8) {
695        let addr = addr & 0x3FFF;
696        match addr {
697            0x0000..=0x1FFF => {
698                if self.chr_is_ram {
699                    let off = self.chr_offset(addr);
700                    let len = self.chr.len();
701                    self.chr[off % len] = value;
702                }
703            }
704            0x2000..=0x3EFF => {
705                let off = self.nametable_offset(addr);
706                self.vram[off] = value;
707            }
708            _ => {}
709        }
710    }
711
712    fn notify_cpu_cycle(&mut self) {
713        if !self.irq_enabled {
714            return;
715        }
716        // Down-counter: IRQ asserts when the counter holds zero, then it
717        // wraps to $FFFF and keeps counting (per the wiki: "When it holds a
718        // value of zero, an IRQ is generated").
719        if self.irq_counter == 0 {
720            self.irq_pending = true;
721        }
722        self.irq_counter = self.irq_counter.wrapping_sub(1);
723    }
724
725    fn irq_pending(&self) -> bool {
726        self.irq_pending
727    }
728
729    fn current_mirroring(&self) -> Mirroring {
730        self.mirroring
731    }
732
733    fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
734        let id = match self.variant {
735            FcgVariant::Lz93d50_24c01 => 159,
736            FcgVariant::Lz93d50Wram => 153,
737            _ => 16,
738        };
739        let mut info = crate::mapper::MapperDebugInfo {
740            mapper_id: id,
741            name: format!("Bandai FCG ({id})"),
742            mirroring: crate::mapper::mirroring_name(self.mirroring),
743            ..Default::default()
744        };
745        info.prg_banks
746            .push(("PRG".into(), format!("{:#04x}", self.prg_bank)));
747        for (i, b) in self.chr_banks.iter().enumerate() {
748            info.chr_banks
749                .push((format!("CHR{i}"), format!("{b:#04x}")));
750        }
751        info.irq_state
752            .push(("latch".into(), format!("{:#06x}", self.irq_latch)));
753        info.irq_state
754            .push(("counter".into(), format!("{:#06x}", self.irq_counter)));
755        info.irq_state
756            .push(("enabled".into(), format!("{}", self.irq_enabled)));
757        info.irq_state
758            .push(("pending".into(), format!("{}", self.irq_pending)));
759        info.extra.push((
760            "eeprom".into(),
761            match self.eeprom.as_ref() {
762                Some(ee) => format!("{} bytes", ee.mem.len()),
763                None => "none".into(),
764            },
765        ));
766        info
767    }
768
769    fn save_state(&self) -> Vec<u8> {
770        let ee_len = self.eeprom.as_ref().map_or(0, |e| e.mem.len());
771        let mut out = Vec::with_capacity(
772            18 + self.vram.len() + ee_len + if self.chr_is_ram { self.chr.len() } else { 0 },
773        );
774        out.push(SAVE_STATE_VERSION);
775        out.push(self.prg_bank);
776        out.extend_from_slice(&self.chr_banks);
777        out.push(self.mirroring as u8);
778        out.extend_from_slice(&self.irq_latch.to_le_bytes());
779        out.extend_from_slice(&self.irq_counter.to_le_bytes());
780        out.push(u8::from(self.irq_enabled));
781        out.push(u8::from(self.irq_pending));
782        out.push(self.eeprom_ctrl);
783        // EEPROM contents (if any).
784        if let Some(ee) = self.eeprom.as_ref() {
785            out.extend_from_slice(&ee.mem);
786        }
787        out.extend_from_slice(&self.vram);
788        if self.chr_is_ram {
789            out.extend_from_slice(&self.chr);
790        }
791        out.push(self.outer);
792        out.push(u8::from(self.wram_enabled));
793        out.extend_from_slice(&self.wram);
794        out
795    }
796
797    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
798        let ee_len = self.eeprom.as_ref().map_or(0, |e| e.mem.len());
799        let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
800        // Only the current version is read (v2.9.8, ADR 0042); a v1 blob,
801        // which carried no mapper-153 tail, used to load on boards without one.
802        let core_len = 18 + self.vram.len() + ee_len + need_chr;
803        let expected = match data.first() {
804            Some(&SAVE_STATE_VERSION) => core_len + 2 + self.wram.len(),
805            Some(&v) => return Err(MapperError::UnsupportedVersion(v)),
806            None => core_len,
807        };
808        if data.len() != expected {
809            return Err(MapperError::WrongLength {
810                expected,
811                got: data.len(),
812            });
813        }
814        self.prg_bank = data[1];
815        self.chr_banks.copy_from_slice(&data[2..10]);
816        self.mirroring = match data[10] {
817            0 => Mirroring::Horizontal,
818            1 => Mirroring::Vertical,
819            2 => Mirroring::SingleScreenA,
820            3 => Mirroring::SingleScreenB,
821            4 => Mirroring::FourScreen,
822            other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
823        };
824        self.irq_latch = u16::from_le_bytes([data[11], data[12]]);
825        self.irq_counter = u16::from_le_bytes([data[13], data[14]]);
826        self.irq_enabled = data[15] != 0;
827        // Bytes 16+ : pending, eeprom_ctrl, eeprom mem, vram, chr.
828        // We packed pending + ctrl after the fixed block; recompute cursor.
829        // To keep the layout simple, re-derive: indices 16 = pending,
830        // 17 = ctrl. (with_capacity sizing already accounts for the +16
831        // header containing version+prg+8 chr+mir+2 latch+2 counter+enabled.)
832        let mut cursor = 16;
833        let pending = data[cursor];
834        cursor += 1;
835        let ctrl = data[cursor];
836        cursor += 1;
837        self.irq_pending = pending != 0;
838        self.eeprom_ctrl = ctrl;
839        if let Some(ee) = self.eeprom.as_mut() {
840            ee.mem.copy_from_slice(&data[cursor..cursor + ee.mem.len()]);
841            cursor += ee.mem.len();
842        }
843        self.vram
844            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
845        cursor += self.vram.len();
846        if self.chr_is_ram {
847            self.chr
848                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
849            cursor += self.chr.len();
850        }
851        self.outer = data[cursor] & 0x01;
852        self.wram_enabled = data[cursor + 1] != 0;
853        cursor += 2;
854        self.wram
855            .copy_from_slice(&data[cursor..cursor + self.wram.len()]);
856        Ok(())
857    }
858}
859
860#[cfg(test)]
861#[allow(clippy::cast_possible_truncation)]
862mod tests {
863    use super::*;
864
865    fn synth_prg(banks_16k: usize) -> Box<[u8]> {
866        let mut v = vec![0u8; banks_16k * PRG_BANK_16K];
867        for b in 0..banks_16k {
868            v[b * PRG_BANK_16K] = b as u8;
869        }
870        v.into_boxed_slice()
871    }
872
873    fn synth_chr(banks_1k: usize) -> Box<[u8]> {
874        let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
875        for b in 0..banks_1k {
876            v[b * CHR_BANK_1K] = b as u8;
877        }
878        v.into_boxed_slice()
879    }
880
881    #[test]
882    fn lz93d50_prg_bank_and_fixed_last() {
883        let mut m = BandaiFcg::new(
884            synth_prg(8),
885            synth_chr(16),
886            Mirroring::Vertical,
887            FcgVariant::Lz93d50_24c02,
888        )
889        .unwrap();
890        assert_eq!(m.cpu_read(0x8000), 0);
891        assert_eq!(m.cpu_read(0xC000), 7);
892        m.cpu_write(0x8008, 3);
893        assert_eq!(m.cpu_read(0x8000), 3);
894        assert_eq!(m.cpu_read(0xC000), 7);
895    }
896
897    #[test]
898    fn chr_bank_select_per_1k_slot() {
899        let mut m = BandaiFcg::new(
900            synth_prg(8),
901            synth_chr(16),
902            Mirroring::Vertical,
903            FcgVariant::Lz93d50_24c02,
904        )
905        .unwrap();
906        m.cpu_write(0x8000, 4); // CHR slot 0 -> bank 4
907        m.cpu_write(0x8004, 9); // CHR slot 4 ($1000) -> bank 9
908        assert_eq!(m.ppu_read(0x0000), 4);
909        assert_eq!(m.ppu_read(0x1000), 9);
910    }
911
912    #[test]
913    fn mirroring_control() {
914        let mut m = BandaiFcg::new(
915            synth_prg(8),
916            synth_chr(16),
917            Mirroring::Vertical,
918            FcgVariant::Lz93d50_24c02,
919        )
920        .unwrap();
921        m.cpu_write(0x8009, 1);
922        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
923        m.cpu_write(0x8009, 2);
924        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
925    }
926
927    #[test]
928    fn lz93d50_latched_irq_counts_down_to_zero() {
929        let mut m = BandaiFcg::new(
930            synth_prg(8),
931            synth_chr(16),
932            Mirroring::Vertical,
933            FcgVariant::Lz93d50_24c02,
934        )
935        .unwrap();
936        // Latch = 3.
937        m.cpu_write(0x800B, 0x03);
938        m.cpu_write(0x800C, 0x00);
939        // Enable + copy latch->counter.
940        m.cpu_write(0x800A, 0x01);
941        assert_eq!(m.irq_counter, 3);
942        m.notify_cpu_cycle(); // 3 -> 2
943        m.notify_cpu_cycle(); // 2 -> 1
944        m.notify_cpu_cycle(); // 1 -> 0
945        assert!(!m.irq_pending());
946        m.notify_cpu_cycle(); // counter holds 0 -> IRQ
947        assert!(m.irq_pending());
948    }
949
950    #[test]
951    fn fcg_writes_counter_directly() {
952        let mut m = BandaiFcg::new(
953            synth_prg(8),
954            synth_chr(16),
955            Mirroring::Vertical,
956            FcgVariant::Fcg,
957        )
958        .unwrap();
959        // FCG-1/2 responds in $6000-$7FFF and writes the counter directly.
960        m.cpu_write(0x600B, 0x02);
961        m.cpu_write(0x600C, 0x00);
962        assert_eq!(m.irq_counter, 2);
963        m.cpu_write(0x600A, 0x01); // enable (no latch copy)
964        assert_eq!(m.irq_counter, 2);
965    }
966
967    #[test]
968    fn irq_acknowledge_on_control_write() {
969        let mut m = BandaiFcg::new(
970            synth_prg(8),
971            synth_chr(16),
972            Mirroring::Vertical,
973            FcgVariant::Lz93d50_24c02,
974        )
975        .unwrap();
976        m.irq_pending = true;
977        m.cpu_write(0x800A, 0x00); // disable + ack
978        assert!(!m.irq_pending());
979        assert!(!m.irq_enabled);
980    }
981
982    #[test]
983    fn eeprom_present_for_159_absent_for_fcg() {
984        let m159 = BandaiFcg::new(
985            synth_prg(8),
986            synth_chr(16),
987            Mirroring::Vertical,
988            FcgVariant::Lz93d50_24c01,
989        )
990        .unwrap();
991        assert!(m159.eeprom.is_some());
992        assert_eq!(m159.eeprom.as_ref().unwrap().mem.len(), 128);
993        let mfcg = BandaiFcg::new(
994            synth_prg(8),
995            synth_chr(16),
996            Mirroring::Vertical,
997            FcgVariant::Fcg,
998        )
999        .unwrap();
1000        assert!(mfcg.eeprom.is_none());
1001    }
1002
1003    #[test]
1004    fn eeprom_idle_reads_high() {
1005        let mut m = BandaiFcg::new(
1006            synth_prg(8),
1007            synth_chr(16),
1008            Mirroring::Vertical,
1009            FcgVariant::Lz93d50_24c01,
1010        )
1011        .unwrap();
1012        // With no I2C transaction, the device releases SDA (out_bit high) ->
1013        // bit 4 set.
1014        assert_eq!(m.cpu_read(0x6000) & 0x10, 0x10);
1015    }
1016
1017    /// Drive the X24C01 I²C lines directly through the `$800D` register,
1018    /// mirroring how a game bit-bangs the bus: SDA is set up while SCL is
1019    /// low, then SCL is pulsed high and back low to clock the bit.
1020    struct I2cDriver<'a> {
1021        m: &'a mut BandaiFcg,
1022    }
1023
1024    impl I2cDriver<'_> {
1025        const SCL: u8 = 0x20;
1026        const SDA: u8 = 0x40;
1027
1028        fn lines(&mut self, scl: bool, sda: bool) {
1029            let v = (u8::from(scl) * Self::SCL) | (u8::from(sda) * Self::SDA);
1030            self.m.cpu_write(0x800D, v);
1031        }
1032
1033        fn start(&mut self) {
1034            // SDA high, SCL high, then SDA falls while SCL stays high.
1035            self.lines(true, true);
1036            self.lines(true, false);
1037        }
1038
1039        fn stop(&mut self) {
1040            // SDA low while SCL high, then SDA rises while SCL stays high.
1041            self.lines(true, false);
1042            self.lines(true, true);
1043        }
1044
1045        /// Clock one bit out from the master to the device (write direction).
1046        fn send_bit(&mut self, bit: bool) {
1047            self.lines(false, bit); // set SDA while SCL low
1048            self.lines(true, bit); // clock rise (device samples)
1049            self.lines(false, bit); // clock fall (device advances)
1050        }
1051
1052        /// Clock one bit while releasing SDA, returning the device's output.
1053        fn recv_bit(&mut self) -> bool {
1054            self.lines(false, true); // release SDA, SCL low
1055            self.lines(true, true); // clock rise (device drives output)
1056            let out = self.m.eeprom.as_ref().unwrap().read_sda();
1057            self.lines(false, true); // clock fall
1058            out
1059        }
1060
1061        /// The X24C01 word-address byte is LSB-first: 7 address bits then R/W.
1062        fn send_addr_rw(&mut self, addr: u8, read: bool) {
1063            for i in 0..7 {
1064                self.send_bit((addr >> i) & 1 != 0);
1065            }
1066            self.send_bit(read);
1067        }
1068
1069        /// Read the device-driven ack bit (low = ack).
1070        fn read_ack(&mut self) -> bool {
1071            !self.recv_bit()
1072        }
1073
1074        fn send_data_lsb_first(&mut self, byte: u8) {
1075            for i in 0..8 {
1076                self.send_bit((byte >> i) & 1 != 0);
1077            }
1078        }
1079
1080        fn recv_data_lsb_first(&mut self) -> u8 {
1081            let mut byte = 0u8;
1082            for i in 0..8 {
1083                if self.recv_bit() {
1084                    byte |= 1 << i;
1085                }
1086            }
1087            byte
1088        }
1089    }
1090
1091    #[test]
1092    fn x24c01_write_then_read_round_trips() {
1093        let mut m = BandaiFcg::new(
1094            synth_prg(8),
1095            synth_chr(16),
1096            Mirroring::Vertical,
1097            FcgVariant::Lz93d50_24c01,
1098        )
1099        .unwrap();
1100        {
1101            let mut d = I2cDriver { m: &mut m };
1102            // Write 0x5A to word address 0x12.
1103            d.start();
1104            d.send_addr_rw(0x12, false); // write
1105            assert!(d.read_ack(), "device must ack the address byte");
1106            d.send_data_lsb_first(0x5A);
1107            assert!(d.read_ack(), "device must ack the data byte");
1108            d.stop();
1109
1110            // Read it back from word address 0x12.
1111            d.start();
1112            d.send_addr_rw(0x12, true); // read
1113            assert!(d.read_ack(), "device must ack the read address");
1114            let got = d.recv_data_lsb_first();
1115            assert_eq!(got, 0x5A, "read-back must match the written byte");
1116            d.stop();
1117        }
1118        assert_eq!(m.eeprom.as_ref().unwrap().mem[0x12], 0x5A);
1119    }
1120
1121    #[test]
1122    fn save_state_round_trip_with_eeprom() {
1123        let mut m = BandaiFcg::new(
1124            synth_prg(8),
1125            synth_chr(16),
1126            Mirroring::Horizontal,
1127            FcgVariant::Lz93d50_24c02,
1128        )
1129        .unwrap();
1130        m.cpu_write(0x8008, 2);
1131        m.cpu_write(0x8000, 5);
1132        m.cpu_write(0x800B, 0x10);
1133        m.cpu_write(0x800A, 0x01);
1134        if let Some(ee) = m.eeprom.as_mut() {
1135            ee.mem[0] = 0x42;
1136        }
1137        let blob = m.save_state();
1138        let mut m2 = BandaiFcg::new(
1139            synth_prg(8),
1140            synth_chr(16),
1141            Mirroring::Horizontal,
1142            FcgVariant::Lz93d50_24c02,
1143        )
1144        .unwrap();
1145        m2.load_state(&blob).unwrap();
1146        assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
1147        assert_eq!(m.ppu_read(0x0000), m2.ppu_read(0x0000));
1148        assert_eq!(m.irq_counter, m2.irq_counter);
1149        assert_eq!(m2.eeprom.as_ref().unwrap().mem[0], 0x42);
1150    }
1151
1152    // ---- EEPROM address-counter rollover ------------------------------------
1153    //
1154    // The counter is a `u8` masked to 0xFF (0x7F on the X24C01), so the mask
1155    // cannot express the wrap on its own and the add has to do it. These pin
1156    // both chips at the boundary, for the read path and the write path, because
1157    // the bug was a debug-build panic rather than a wrong value -- a test that
1158    // only checked the returned byte would have passed before the fix.
1159
1160    #[test]
1161    fn eeprom_address_wraps_at_the_top_of_each_chip() {
1162        for (bytes, is_x24c01, top) in [(256usize, false, 0xFFu8), (128, true, 0x7F)] {
1163            let mut e = Eeprom::new(bytes, is_x24c01);
1164            e.addr = top;
1165            e.data = 0xA5;
1166            e.counter = 8;
1167            e.mode = I2cMode::Write;
1168            // Completing the byte must advance past the top without trapping.
1169            e.clock_fall(0);
1170            assert_eq!(e.addr, 0, "x24c01={is_x24c01}: ${top:02X} + 1 wraps to $00");
1171        }
1172    }
1173
1174    #[test]
1175    fn eeprom_sequential_read_wraps_at_the_top_of_each_chip() {
1176        for (bytes, is_x24c01, top) in [(256usize, false, 0xFFu8), (128, true, 0x7F)] {
1177            let mut e = Eeprom::new(bytes, is_x24c01);
1178            e.addr = top;
1179            e.counter = 8;
1180            e.mode = I2cMode::Read;
1181            e.clock_fall(0);
1182            assert_eq!(e.addr, 0, "x24c01={is_x24c01}: read rollover reaches $00");
1183        }
1184    }
1185
1186    // ---- Mapper 153 (`INES_Mapper_153.md`) ------------------------------
1187
1188    fn m153(banks_16k: usize) -> BandaiFcg {
1189        BandaiFcg::new(
1190            synth_prg(banks_16k),
1191            Box::new([]),
1192            Mirroring::Vertical,
1193            FcgVariant::Lz93d50Wram,
1194        )
1195        .unwrap()
1196    }
1197
1198    #[test]
1199    fn m153_outer_bank_from_8000_to_8003_and_fixed_last_of_the_block() {
1200        let mut m = m153(32); // 512 KiB
1201        m.cpu_write(0x8008, 3);
1202        assert_eq!(m.cpu_read(0x8000), 3);
1203        assert_eq!(m.cpu_read(0xC000), 15, "last bank of the first 256 KiB");
1204        for a in 0x8000..=0x8003u16 {
1205            m.cpu_write(a, 1);
1206        }
1207        assert_eq!(m.cpu_read(0x8000), 16 + 3);
1208        assert_eq!(m.cpu_read(0xC000), 31);
1209        // `$8004-$8007` do nothing on this board.
1210        m.cpu_write(0x8004, 0);
1211        assert_eq!(m.cpu_read(0x8000), 16 + 3);
1212    }
1213
1214    #[test]
1215    fn m153_wram_follows_the_800d_chip_enable() {
1216        let mut m = m153(32);
1217        assert_eq!(m.sram().len(), WRAM_153, "the battery save is the WRAM");
1218        assert!(m.cpu_read_unmapped(0x6000), "disabled at power-on");
1219        m.cpu_write(0x6000, 0x11);
1220        m.cpu_write(0x800D, 0x20);
1221        assert!(!m.cpu_read_unmapped(0x6000));
1222        assert_eq!(m.cpu_read(0x6000), 0, "the disabled write was dropped");
1223        m.cpu_write(0x7FFF, 0x22);
1224        assert_eq!(m.cpu_read(0x7FFF), 0x22);
1225        m.cpu_write(0x800D, 0x00);
1226        assert!(m.cpu_read_unmapped(0x7FFF));
1227    }
1228
1229    #[test]
1230    fn m153_chr_is_unbanked_ram_and_irq_is_the_lz93d50s() {
1231        let mut m = m153(32);
1232        m.cpu_write(0x8000, 1); // outer bank, not CHR bank 0
1233        m.ppu_write(0x0005, 0x9A);
1234        assert_eq!(m.ppu_read(0x0005), 0x9A);
1235        m.cpu_write(0x800B, 2);
1236        m.cpu_write(0x800C, 0);
1237        m.cpu_write(0x800A, 1);
1238        for _ in 0..3 {
1239            m.notify_cpu_cycle();
1240        }
1241        assert!(m.irq_pending(), "latched counter 2 reaches zero");
1242    }
1243
1244    /// `INES_Mapper_153`: "PPU $0000-$1FFF: 8 KiB unbanked CHR-RAM" and "No
1245    /// CHR banking is available". Every byte of the 8 KiB is its own cell, so
1246    /// the eight 1 KiB windows must not alias one another. The v2.9.6 model
1247    /// routed CHR-RAM through the CHR bank registers, which this board never
1248    /// writes, so all eight windows landed on the first 1 KiB and *Famicom
1249    /// Jump II*'s pattern tables overwrote each other (the striped title).
1250    #[test]
1251    fn m153_chr_ram_is_8k_with_no_aliasing_between_1k_windows() {
1252        let mut m = m153(32);
1253        // Writes to the CHR-register offsets are the outer bank on this board
1254        // and must not reach the CHR mapping either.
1255        for a in 0x8000..=0x8007u16 {
1256            m.cpu_write(a, 0x05);
1257        }
1258        for slot in 0..8u16 {
1259            m.ppu_write(slot * 0x400 + 0x123, 0xA0 | slot as u8);
1260        }
1261        for slot in 0..8u16 {
1262            assert_eq!(
1263                m.ppu_read(slot * 0x400 + 0x123),
1264                0xA0 | slot as u8,
1265                "1 KiB window {slot} aliased another window"
1266            );
1267        }
1268        m.ppu_write(0x1FFF, 0x5A);
1269        assert_eq!(m.ppu_read(0x1FFF), 0x5A);
1270        assert_eq!(m.ppu_read(0x03FF), 0, "$1FFF must not alias $03FF");
1271    }
1272
1273    #[test]
1274    fn m153_state_round_trips_and_v1_is_refused() {
1275        let mut a = m153(32);
1276        a.cpu_write(0x8001, 1);
1277        a.cpu_write(0x800D, 0x20);
1278        a.cpu_write(0x6123, 0x44);
1279        let blob = a.save_state();
1280        let mut b = m153(32);
1281        b.load_state(&blob).unwrap();
1282        assert_eq!(b.cpu_read(0x6123), 0x44);
1283        assert_eq!(b.cpu_read(0xC000), 31);
1284        assert_eq!(b.save_state(), blob);
1285        let mut v1 = blob.clone();
1286        v1[0] = 1;
1287        v1.truncate(v1.len() - 2 - WRAM_153);
1288        assert!(
1289            b.load_state(&v1).is_err(),
1290            "a v1 blob has no WRAM to restore"
1291        );
1292        // Since v2.9.8 (ADR 0042) a v1 blob is refused on a board with no 153
1293        // tail too; it used to load there.
1294        let mut fcg = BandaiFcg::new(
1295            synth_prg(8),
1296            synth_chr(128),
1297            Mirroring::Vertical,
1298            FcgVariant::Fcg,
1299        )
1300        .unwrap();
1301        let mut v1 = fcg.save_state();
1302        v1[0] = 1;
1303        v1.truncate(v1.len() - 2);
1304        assert!(matches!(
1305            fcg.load_state(&v1),
1306            Err(MapperError::UnsupportedVersion(1))
1307        ));
1308    }
1309}