Skip to main content

rustynes_mappers/
jaleco_discrete.rs

1//! Jaleco `JF-13` (mapper 86) and `JF-11` / `JF-14` (mapper 140) -- discrete
2//! PRG/CHR latch boards.
3//!
4//! Both put their single bank-select latch in the PRG-RAM window rather than
5//! at `$8000`: `$6000-$6FFF` for mapper 86, the full `$6000-$7FFF` for
6//! mapper 140.
7//!
8//! Jaleco's larger ASICs live in `m087_jaleco87.rs` and `m018_jaleco_ss88006.rs`.
9//!
10//! A discrete-logic board in the shape of the stock mappers (`NROM`, `CNROM`,
11//! `UxROM`, `GxROM`, `AxROM`): bank-select latch registers, no IRQ, no on-cart
12//! audio. Banking / mirroring semantics are cross-checked against the
13//! `GeraNES` reference emulator (cross-referenced, not copied)
14//! and the nesdev wiki, and validated by register-decode + save-state unit
15//! tests.
16//!
17//! See `docs/mappers.md` §Mapper coverage matrix.
18
19#![allow(
20    clippy::bool_to_int_with_if,
21    clippy::cast_lossless,
22    clippy::cast_possible_truncation,
23    clippy::doc_markdown,
24    clippy::match_same_arms,
25    clippy::missing_const_for_fn,
26    clippy::similar_names,
27    clippy::struct_excessive_bools,
28    clippy::too_many_lines,
29    clippy::unreadable_literal
30)]
31
32use crate::cartridge::Mirroring;
33use crate::mapper::{Mapper, MapperCaps, MapperError};
34use alloc::{boxed::Box, vec::Vec};
35use alloc::{format, vec};
36
37const PRG_BANK_16K: usize = 0x4000;
38const PRG_BANK_32K: usize = 0x8000;
39const CHR_BANK_8K: usize = 0x2000;
40const NAMETABLE_SIZE: usize = 0x0400;
41const NAMETABLE_SIZE_U16: u16 = 0x0400;
42
43const SAVE_STATE_VERSION: u8 = 1;
44
45const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
46    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
47    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
48    let physical = mirroring.physical_bank(table);
49    physical * NAMETABLE_SIZE + local
50}
51
52/// Mapper 86 (Jaleco `JF-13`).
53pub struct Jaleco86 {
54    prg_rom: Box<[u8]>,
55    chr_rom: Box<[u8]>,
56    vram: Box<[u8]>,
57    prg_bank: u8,
58    chr_bank: u8,
59    mirroring: Mirroring,
60}
61
62impl Jaleco86 {
63    /// Construct a new mapper 86 board.
64    ///
65    /// # Errors
66    ///
67    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
68    /// 32 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
69    pub fn new(
70        prg_rom: Box<[u8]>,
71        chr_rom: Box<[u8]>,
72        mirroring: Mirroring,
73    ) -> Result<Self, MapperError> {
74        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
75            return Err(MapperError::Invalid(format!(
76                "mapper 86 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
77                prg_rom.len()
78            )));
79        }
80        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
81            return Err(MapperError::Invalid(format!(
82                "mapper 86 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
83                chr_rom.len()
84            )));
85        }
86        Ok(Self {
87            prg_rom,
88            chr_rom,
89            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
90            prg_bank: 0,
91            chr_bank: 0,
92            mirroring,
93        })
94    }
95}
96
97impl Mapper for Jaleco86 {
98    fn caps(&self) -> MapperCaps {
99        MapperCaps::NONE
100    }
101
102    fn cpu_read(&mut self, addr: u16) -> u8 {
103        if (0x8000..=0xFFFF).contains(&addr) {
104            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
105            let bank = (self.prg_bank as usize) % count;
106            self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
107        } else {
108            0
109        }
110    }
111
112    fn cpu_write(&mut self, addr: u16, value: u8) {
113        // Bank register at $6000-$6FFF only; $7000-$7FFF is the ADPCM port.
114        if (0x6000..=0x6FFF).contains(&addr) {
115            self.prg_bank = (value >> 4) & 0x03;
116            self.chr_bank = (value & 0x03) | ((value >> 4) & 0x04);
117        }
118    }
119
120    fn ppu_read(&mut self, addr: u16) -> u8 {
121        let addr = addr & 0x3FFF;
122        match addr {
123            0x0000..=0x1FFF => {
124                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
125                let bank = (self.chr_bank as usize) % count;
126                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
127            }
128            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
129            _ => 0,
130        }
131    }
132
133    fn ppu_write(&mut self, addr: u16, value: u8) {
134        let addr = addr & 0x3FFF;
135        if let 0x2000..=0x3EFF = addr {
136            let off = nametable_offset(addr, self.mirroring);
137            self.vram[off] = value;
138        }
139    }
140
141    fn current_mirroring(&self) -> Mirroring {
142        self.mirroring
143    }
144
145    fn save_state(&self) -> Vec<u8> {
146        let mut out = Vec::with_capacity(3 + self.vram.len());
147        out.push(SAVE_STATE_VERSION);
148        out.push(self.prg_bank);
149        out.push(self.chr_bank);
150        out.extend_from_slice(&self.vram);
151        out
152    }
153
154    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
155        let expected = 3 + self.vram.len();
156        if data.len() != expected {
157            return Err(MapperError::WrongLength {
158                expected,
159                got: data.len(),
160            });
161        }
162        if data[0] != SAVE_STATE_VERSION {
163            return Err(MapperError::UnsupportedVersion(data[0]));
164        }
165        self.prg_bank = data[1];
166        self.chr_bank = data[2];
167        self.vram.copy_from_slice(&data[3..3 + self.vram.len()]);
168        Ok(())
169    }
170}
171
172// ===========================================================================
173// Mapper 140 — Jaleco JF-11 / JF-14.
174//
175// Single latch across the whole $6000-$7FFF window. The byte PPPP_CCCC selects:
176//   PRG = (value >> 4) & 0x03   (32 KiB)
177//   CHR = value & 0x0F          (8 KiB, 4-bit)
178// Mirroring is header-fixed; no IRQ.
179// ===========================================================================
180
181/// Mapper 140 (Jaleco `JF-11`/`JF-14`).
182pub struct Jaleco140 {
183    prg_rom: Box<[u8]>,
184    chr_rom: Box<[u8]>,
185    vram: Box<[u8]>,
186    prg_bank: u8,
187    chr_bank: u8,
188    mirroring: Mirroring,
189}
190
191impl Jaleco140 {
192    /// Construct a new mapper 140 board.
193    ///
194    /// # Errors
195    ///
196    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
197    /// 32 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
198    pub fn new(
199        prg_rom: Box<[u8]>,
200        chr_rom: Box<[u8]>,
201        mirroring: Mirroring,
202    ) -> Result<Self, MapperError> {
203        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
204            return Err(MapperError::Invalid(format!(
205                "mapper 140 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
206                prg_rom.len()
207            )));
208        }
209        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
210            return Err(MapperError::Invalid(format!(
211                "mapper 140 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
212                chr_rom.len()
213            )));
214        }
215        Ok(Self {
216            prg_rom,
217            chr_rom,
218            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
219            prg_bank: 0,
220            chr_bank: 0,
221            mirroring,
222        })
223    }
224}
225
226impl Mapper for Jaleco140 {
227    fn caps(&self) -> MapperCaps {
228        MapperCaps::NONE
229    }
230
231    fn cpu_read(&mut self, addr: u16) -> u8 {
232        if (0x8000..=0xFFFF).contains(&addr) {
233            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
234            let bank = (self.prg_bank as usize) % count;
235            self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
236        } else {
237            0
238        }
239    }
240
241    fn cpu_write(&mut self, addr: u16, value: u8) {
242        if (0x6000..=0x7FFF).contains(&addr) {
243            self.prg_bank = (value >> 4) & 0x03;
244            self.chr_bank = value & 0x0F;
245        }
246    }
247
248    fn ppu_read(&mut self, addr: u16) -> u8 {
249        let addr = addr & 0x3FFF;
250        match addr {
251            0x0000..=0x1FFF => {
252                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
253                let bank = (self.chr_bank as usize) % count;
254                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
255            }
256            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
257            _ => 0,
258        }
259    }
260
261    fn ppu_write(&mut self, addr: u16, value: u8) {
262        let addr = addr & 0x3FFF;
263        if let 0x2000..=0x3EFF = addr {
264            let off = nametable_offset(addr, self.mirroring);
265            self.vram[off] = value;
266        }
267    }
268
269    fn current_mirroring(&self) -> Mirroring {
270        self.mirroring
271    }
272
273    fn save_state(&self) -> Vec<u8> {
274        let mut out = Vec::with_capacity(3 + self.vram.len());
275        out.push(SAVE_STATE_VERSION);
276        out.push(self.prg_bank);
277        out.push(self.chr_bank);
278        out.extend_from_slice(&self.vram);
279        out
280    }
281
282    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
283        let expected = 3 + self.vram.len();
284        if data.len() != expected {
285            return Err(MapperError::WrongLength {
286                expected,
287                got: data.len(),
288            });
289        }
290        if data[0] != SAVE_STATE_VERSION {
291            return Err(MapperError::UnsupportedVersion(data[0]));
292        }
293        self.prg_bank = data[1];
294        self.chr_bank = data[2];
295        self.vram.copy_from_slice(&data[3..3 + self.vram.len()]);
296        Ok(())
297    }
298}
299
300/// Shared register/strobe state for the Jaleco JF-17/19 family (mappers 72/92).
301// The four flags each model a distinct hardware signal (CHR-RAM presence, the
302// two edge-triggered latch strobes, and the JF-17-vs-JF-19 PRG layout); they
303// are not a bitfield-able state and reading them as named bools is clearest.
304#[allow(clippy::struct_excessive_bools)]
305struct JalecoLatch {
306    prg_rom: Box<[u8]>,
307    chr: Box<[u8]>,
308    vram: Box<[u8]>,
309    chr_is_ram: bool,
310    prg_bank: u8,
311    chr_bank: u8,
312    prev_prg_strobe: bool,
313    prev_chr_strobe: bool,
314    mirroring: Mirroring,
315    /// Mask applied to the PRG-bank nibble (0x0F for 72, 0x1F for 92).
316    prg_field_mask: u8,
317    /// PRG window layout. `false` (mapper 72, JF-17): switchable bank at
318    /// `$8000-$BFFF`, fixed LAST bank at `$C000-$FFFF`. `true` (mapper 92,
319    /// JF-19): fixed FIRST bank at `$8000-$BFFF`, switchable bank at
320    /// `$C000-$FFFF` — the reset vector lives in the fixed half, so this layout
321    /// is load-bearing for boot.
322    switchable_high: bool,
323}
324
325impl JalecoLatch {
326    fn new(
327        prg_rom: Box<[u8]>,
328        chr_rom: Box<[u8]>,
329        mirroring: Mirroring,
330        prg_field_mask: u8,
331        switchable_high: bool,
332        id: u16,
333    ) -> Result<Self, MapperError> {
334        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
335            return Err(MapperError::Invalid(format!(
336                "mapper {id} PRG-ROM size {} is not a non-zero multiple of 16 KiB",
337                prg_rom.len()
338            )));
339        }
340        let chr_is_ram = chr_rom.is_empty();
341        let chr: Box<[u8]> = if chr_is_ram {
342            vec![0u8; CHR_BANK_8K].into_boxed_slice()
343        } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
344            chr_rom
345        } else {
346            return Err(MapperError::Invalid(format!(
347                "mapper {id} CHR-ROM size {} is not a multiple of 8 KiB",
348                chr_rom.len()
349            )));
350        };
351        Ok(Self {
352            prg_rom,
353            chr,
354            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
355            chr_is_ram,
356            prg_bank: 0,
357            chr_bank: 0,
358            prev_prg_strobe: false,
359            prev_chr_strobe: false,
360            mirroring,
361            prg_field_mask,
362            switchable_high,
363        })
364    }
365
366    fn prg_count_16k(&self) -> usize {
367        (self.prg_rom.len() / PRG_BANK_16K).max(1)
368    }
369
370    fn read_prg_bank(&self, bank: usize, off: usize) -> u8 {
371        let bank = bank % self.prg_count_16k();
372        self.prg_rom[bank * PRG_BANK_16K + off]
373    }
374
375    fn chr_offset(&self, addr: u16) -> usize {
376        let count = (self.chr.len() / CHR_BANK_8K).max(1);
377        let bank = (self.chr_bank as usize) % count;
378        bank * CHR_BANK_8K + addr as usize
379    }
380
381    fn cpu_read(&self, addr: u16) -> u8 {
382        let last = self.prg_count_16k() - 1;
383        match addr {
384            0x8000..=0xBFFF => {
385                // JF-19 (mapper 92): fixed FIRST bank here. JF-17 (mapper 72):
386                // switchable bank here.
387                let bank = if self.switchable_high {
388                    0
389                } else {
390                    self.prg_bank as usize
391                };
392                self.read_prg_bank(bank, addr as usize - 0x8000)
393            }
394            0xC000..=0xFFFF => {
395                // JF-19: switchable bank here. JF-17: fixed LAST bank here.
396                let bank = if self.switchable_high {
397                    self.prg_bank as usize
398                } else {
399                    last
400                };
401                self.read_prg_bank(bank, addr as usize - 0xC000)
402            }
403            _ => 0,
404        }
405    }
406
407    fn cpu_write(&mut self, addr: u16, value: u8) {
408        if (0x8000..=0xFFFF).contains(&addr) {
409            // Bus conflict: AND the written byte with the underlying PRG byte.
410            let effective = value & self.cpu_read(addr);
411            let prg_strobe = (effective & 0x80) != 0;
412            let chr_strobe = (effective & 0x40) != 0;
413            if prg_strobe && !self.prev_prg_strobe {
414                self.prg_bank = effective & self.prg_field_mask;
415            }
416            if chr_strobe && !self.prev_chr_strobe {
417                self.chr_bank = effective & 0x0F;
418            }
419            self.prev_prg_strobe = prg_strobe;
420            self.prev_chr_strobe = chr_strobe;
421        }
422    }
423
424    fn ppu_read(&self, addr: u16) -> u8 {
425        let addr = addr & 0x3FFF;
426        match addr {
427            0x0000..=0x1FFF => self.chr[self.chr_offset(addr)],
428            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
429            _ => 0,
430        }
431    }
432
433    fn ppu_write(&mut self, addr: u16, value: u8) {
434        let addr = addr & 0x3FFF;
435        match addr {
436            0x0000..=0x1FFF => {
437                if self.chr_is_ram {
438                    let off = self.chr_offset(addr);
439                    self.chr[off] = value;
440                }
441            }
442            0x2000..=0x3EFF => {
443                let off = nametable_offset(addr, self.mirroring);
444                self.vram[off] = value;
445            }
446            _ => {}
447        }
448    }
449
450    fn save_state(&self) -> Vec<u8> {
451        let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
452        let mut out = Vec::with_capacity(5 + self.vram.len() + chr_extra);
453        out.push(SAVE_STATE_VERSION);
454        out.push(self.prg_bank);
455        out.push(self.chr_bank);
456        out.push(u8::from(self.prev_prg_strobe));
457        out.push(u8::from(self.prev_chr_strobe));
458        out.extend_from_slice(&self.vram);
459        if self.chr_is_ram {
460            out.extend_from_slice(&self.chr);
461        }
462        out
463    }
464
465    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
466        let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
467        let expected = 5 + self.vram.len() + chr_extra;
468        if data.len() != expected {
469            return Err(MapperError::WrongLength {
470                expected,
471                got: data.len(),
472            });
473        }
474        if data[0] != SAVE_STATE_VERSION {
475            return Err(MapperError::UnsupportedVersion(data[0]));
476        }
477        self.prg_bank = data[1];
478        self.chr_bank = data[2];
479        self.prev_prg_strobe = data[3] != 0;
480        self.prev_chr_strobe = data[4] != 0;
481        let mut cursor = 5;
482        self.vram
483            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
484        cursor += self.vram.len();
485        if self.chr_is_ram {
486            self.chr
487                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
488        }
489        Ok(())
490    }
491}
492
493/// Mapper 72 (Jaleco `JF-17`/`JF-19`).
494pub struct Jaleco72 {
495    inner: JalecoLatch,
496}
497
498impl Jaleco72 {
499    /// Construct a new mapper 72 board.
500    ///
501    /// # Errors
502    ///
503    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
504    /// 16 KiB, or CHR-ROM (when present) is not a multiple of 8 KiB.
505    pub fn new(
506        prg_rom: Box<[u8]>,
507        chr_rom: Box<[u8]>,
508        mirroring: Mirroring,
509    ) -> Result<Self, MapperError> {
510        Ok(Self {
511            inner: JalecoLatch::new(prg_rom, chr_rom, mirroring, 0x0F, false, 72)?,
512        })
513    }
514}
515
516impl Mapper for Jaleco72 {
517    fn caps(&self) -> MapperCaps {
518        MapperCaps::NONE
519    }
520
521    fn cpu_read(&mut self, addr: u16) -> u8 {
522        self.inner.cpu_read(addr)
523    }
524
525    fn cpu_write(&mut self, addr: u16, value: u8) {
526        self.inner.cpu_write(addr, value);
527    }
528
529    fn ppu_read(&mut self, addr: u16) -> u8 {
530        self.inner.ppu_read(addr)
531    }
532
533    fn ppu_write(&mut self, addr: u16, value: u8) {
534        self.inner.ppu_write(addr, value);
535    }
536
537    fn current_mirroring(&self) -> Mirroring {
538        self.inner.mirroring
539    }
540
541    fn save_state(&self) -> Vec<u8> {
542        self.inner.save_state()
543    }
544
545    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
546        self.inner.load_state(data)
547    }
548}
549
550/// Mapper 92 (Jaleco `JF-19`-variant — like 72 with a 5-bit PRG field).
551pub struct Jaleco92 {
552    inner: JalecoLatch,
553}
554
555impl Jaleco92 {
556    /// Construct a new mapper 92 board.
557    ///
558    /// # Errors
559    ///
560    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
561    /// 16 KiB, or CHR-ROM (when present) is not a multiple of 8 KiB.
562    pub fn new(
563        prg_rom: Box<[u8]>,
564        chr_rom: Box<[u8]>,
565        mirroring: Mirroring,
566    ) -> Result<Self, MapperError> {
567        Ok(Self {
568            inner: JalecoLatch::new(prg_rom, chr_rom, mirroring, 0x1F, true, 92)?,
569        })
570    }
571}
572
573impl Mapper for Jaleco92 {
574    fn caps(&self) -> MapperCaps {
575        MapperCaps::NONE
576    }
577
578    fn cpu_read(&mut self, addr: u16) -> u8 {
579        self.inner.cpu_read(addr)
580    }
581
582    fn cpu_write(&mut self, addr: u16, value: u8) {
583        self.inner.cpu_write(addr, value);
584    }
585
586    fn ppu_read(&mut self, addr: u16) -> u8 {
587        self.inner.ppu_read(addr)
588    }
589
590    fn ppu_write(&mut self, addr: u16, value: u8) {
591        self.inner.ppu_write(addr, value);
592    }
593
594    fn current_mirroring(&self) -> Mirroring {
595        self.inner.mirroring
596    }
597
598    fn save_state(&self) -> Vec<u8> {
599        self.inner.save_state()
600    }
601
602    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
603        self.inner.load_state(data)
604    }
605}
606
607/// Mapper 101 (Jaleco `JF-10` CHR latch).
608pub struct Jaleco101 {
609    prg_rom: Box<[u8]>,
610    chr_rom: Box<[u8]>,
611    vram: Box<[u8]>,
612    chr_bank: u8,
613    mirroring: Mirroring,
614}
615
616impl Jaleco101 {
617    /// Construct a new mapper 101 board.
618    ///
619    /// # Errors
620    ///
621    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
622    /// 32 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
623    pub fn new(
624        prg_rom: Box<[u8]>,
625        chr_rom: Box<[u8]>,
626        mirroring: Mirroring,
627    ) -> Result<Self, MapperError> {
628        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
629            return Err(MapperError::Invalid(format!(
630                "mapper 101 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
631                prg_rom.len()
632            )));
633        }
634        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
635            return Err(MapperError::Invalid(format!(
636                "mapper 101 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
637                chr_rom.len()
638            )));
639        }
640        Ok(Self {
641            prg_rom,
642            chr_rom,
643            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
644            chr_bank: 0,
645            mirroring,
646        })
647    }
648}
649
650impl Mapper for Jaleco101 {
651    fn caps(&self) -> MapperCaps {
652        MapperCaps::NONE
653    }
654
655    fn cpu_read(&mut self, addr: u16) -> u8 {
656        if (0x8000..=0xFFFF).contains(&addr) {
657            self.prg_rom[addr as usize - 0x8000]
658        } else {
659            0
660        }
661    }
662
663    fn cpu_write(&mut self, addr: u16, value: u8) {
664        // The CHR latch is in the PRG-RAM ($6000-$7FFF) window.
665        if (0x6000..=0x7FFF).contains(&addr) {
666            self.chr_bank = value;
667        }
668    }
669
670    fn ppu_read(&mut self, addr: u16) -> u8 {
671        let addr = addr & 0x3FFF;
672        match addr {
673            0x0000..=0x1FFF => {
674                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
675                let bank = (self.chr_bank as usize) % count;
676                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
677            }
678            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
679            _ => 0,
680        }
681    }
682
683    fn ppu_write(&mut self, addr: u16, value: u8) {
684        let addr = addr & 0x3FFF;
685        if let 0x2000..=0x3EFF = addr {
686            let off = nametable_offset(addr, self.mirroring);
687            self.vram[off] = value;
688        }
689    }
690
691    fn current_mirroring(&self) -> Mirroring {
692        self.mirroring
693    }
694
695    fn save_state(&self) -> Vec<u8> {
696        let mut out = Vec::with_capacity(2 + self.vram.len());
697        out.push(SAVE_STATE_VERSION);
698        out.push(self.chr_bank);
699        out.extend_from_slice(&self.vram);
700        out
701    }
702
703    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
704        let expected = 2 + self.vram.len();
705        if data.len() != expected {
706            return Err(MapperError::WrongLength {
707                expected,
708                got: data.len(),
709            });
710        }
711        if data[0] != SAVE_STATE_VERSION {
712            return Err(MapperError::UnsupportedVersion(data[0]));
713        }
714        self.chr_bank = data[1];
715        self.vram.copy_from_slice(&data[2..2 + self.vram.len()]);
716        Ok(())
717    }
718}
719
720#[cfg(test)]
721#[allow(clippy::cast_possible_truncation)]
722mod tests {
723    use super::*;
724
725    fn synth_prg_32k(banks: usize) -> Box<[u8]> {
726        let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
727        for b in 0..banks {
728            v[b * PRG_BANK_32K] = b as u8;
729        }
730        v.into_boxed_slice()
731    }
732
733    /// 16 KiB-banked PRG: byte 0 of each 16 KiB bank holds the bank index.
734    fn synth_prg_16k(banks: usize) -> Box<[u8]> {
735        let mut v = vec![0xFFu8; banks * PRG_BANK_16K];
736        for b in 0..banks {
737            v[b * PRG_BANK_16K] = b as u8;
738        }
739        v.into_boxed_slice()
740    }
741
742    fn synth_chr_8k(banks: usize) -> Box<[u8]> {
743        let mut v = vec![0u8; banks * CHR_BANK_8K];
744        for b in 0..banks {
745            v[b * CHR_BANK_8K] = b as u8;
746        }
747        v.into_boxed_slice()
748    }
749
750    #[test]
751    fn m86_latch_selects_prg_and_chr() {
752        let mut m = Jaleco86::new(synth_prg_32k(4), synth_chr_8k(8), Mirroring::Vertical).unwrap();
753        // value VVdd_pPcc; PRG = (v>>4)&3, CHR = (v&3) | ((v>>4)&4).
754        // 0b0011_0010: PRG = (0b0011_0010>>4)&3 = 0b11&3 = 3.
755        //              CHR = (0b10) | ((0b0011)&4 -> 0) = 2.
756        m.cpu_write(0x6000, 0b0011_0010);
757        assert_eq!(m.cpu_read(0x8000), 3);
758        assert_eq!(m.ppu_read(0x0000), 2);
759        // High CHR bit: value 0b0100_0001 -> CHR = 1 | ((0b0100)&4 = 4) = 5.
760        m.cpu_write(0x6000, 0b0100_0001);
761        assert_eq!(m.ppu_read(0x0000), 5);
762        // $7000 (ADPCM port) must NOT change banking.
763        m.cpu_write(0x7000, 0xFF);
764        assert_eq!(m.ppu_read(0x0000), 5);
765    }
766
767    #[test]
768    fn m140_latch_selects_prg_and_chr() {
769        let mut m =
770            Jaleco140::new(synth_prg_32k(4), synth_chr_8k(16), Mirroring::Vertical).unwrap();
771        // value PPPP_CCCC -> PRG = (v>>4)&3, CHR = v&0x0F.
772        m.cpu_write(0x6FFF, 0b0010_1010); // PRG 2, CHR 10
773        assert_eq!(m.cpu_read(0x8000), 2);
774        assert_eq!(m.ppu_read(0x0000), 10);
775        // $7FFF is still in the $6000-$7FFF window.
776        m.cpu_write(0x7FFF, 0b0001_0011); // PRG 1, CHR 3
777        assert_eq!(m.cpu_read(0x8000), 1);
778        assert_eq!(m.ppu_read(0x0000), 3);
779    }
780
781    #[test]
782    fn m72_strobe_latches_on_rising_edge() {
783        let mut m = Jaleco72::new(synth_prg_16k(8), synth_chr_8k(16), Mirroring::Vertical).unwrap();
784        // PRG window offsets >0 hold 0xFF, so bus conflict is transparent there.
785        // Write to $8001 (byte 0xFF). PRG strobe (bit7) rising + bank 3.
786        m.cpu_write(0x8001, 0b1000_0011);
787        assert_eq!(m.cpu_read(0x8000), 3);
788        // Last 16 KiB bank fixed at $C000: bank 7.
789        assert_eq!(m.cpu_read(0xC000), 7);
790        // CHR strobe (bit6) rising + bank 5.
791        m.cpu_write(0x8001, 0b0100_0101);
792        assert_eq!(m.ppu_read(0x0000), 5);
793    }
794
795    #[test]
796    fn m72_no_relatch_without_falling_edge() {
797        let mut m = Jaleco72::new(synth_prg_16k(8), synth_chr_8k(16), Mirroring::Vertical).unwrap();
798        m.cpu_write(0x8001, 0b1000_0011); // latch PRG 3
799        assert_eq!(m.cpu_read(0x8000), 3);
800        // Strobe still high, new bank value -> must NOT re-latch.
801        m.cpu_write(0x8001, 0b1000_0101);
802        assert_eq!(m.cpu_read(0x8000), 3);
803        // Drop strobe, then raise again -> re-latches.
804        m.cpu_write(0x8001, 0b0000_0000);
805        m.cpu_write(0x8001, 0b1000_0101);
806        assert_eq!(m.cpu_read(0x8000), 5);
807    }
808
809    #[test]
810    fn m92_uses_5bit_prg_field() {
811        let mut m =
812            Jaleco92::new(synth_prg_16k(32), synth_chr_8k(16), Mirroring::Vertical).unwrap();
813        // 5-bit PRG field: value 0b1001_0001 -> strobe + bank 0x11 = 17.
814        m.cpu_write(0x8001, 0b1001_0001);
815        // JF-19 layout: $8000 is the FIXED first bank (0); the switchable bank
816        // appears at $C000.
817        assert_eq!(m.cpu_read(0x8000), 0);
818        assert_eq!(m.cpu_read(0xC000), 17);
819    }
820
821    #[test]
822    fn m72_save_state_round_trips_strobe_state() {
823        let mut j = Jaleco72::new(synth_prg_16k(8), synth_chr_8k(16), Mirroring::Vertical).unwrap();
824        j.cpu_write(0x8001, 0b1100_0011); // PRG 3 + CHR 3, both strobes high
825        let blob = j.save_state();
826        let mut j2 =
827            Jaleco72::new(synth_prg_16k(8), synth_chr_8k(16), Mirroring::Vertical).unwrap();
828        j2.load_state(&blob).unwrap();
829        assert_eq!(j2.cpu_read(0x8000), 3);
830        assert_eq!(j2.ppu_read(0x0000), 3);
831        // Strobe still high after restore -> a same-value write must not relatch
832        // from a fresh edge.
833        j2.cpu_write(0x8001, 0b1100_0101);
834        assert_eq!(j2.cpu_read(0x8000), 3);
835    }
836
837    #[test]
838    fn m101_chr_latch_in_6000_window() {
839        let mut m =
840            Jaleco101::new(synth_prg_32k(1), synth_chr_8k(8), Mirroring::Horizontal).unwrap();
841        m.cpu_write(0x6000, 5);
842        assert_eq!(m.ppu_read(0x0000), 5);
843        // PRG is fixed.
844        assert_eq!(m.cpu_read(0x8000), 0);
845    }
846
847    #[test]
848    fn m101_save_state_round_trip() {
849        let mut m =
850            Jaleco101::new(synth_prg_32k(1), synth_chr_8k(8), Mirroring::Horizontal).unwrap();
851        m.cpu_write(0x7FFF, 6);
852        let blob = m.save_state();
853        let mut m2 =
854            Jaleco101::new(synth_prg_32k(1), synth_chr_8k(8), Mirroring::Horizontal).unwrap();
855        m2.load_state(&blob).unwrap();
856        assert_eq!(m2.ppu_read(0x0000), 6);
857    }
858}