Skip to main content

rustynes_mappers/
m091_jy_sf3.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//! Mapper 91: the *Super Fighter III* board and its J.Y. Company clones
3//! (v2.9.6 "Roster").
4//!
5//! Written from `nesdev_wiki/output/INES_Mapper_091.md`.
6//!
7//! | | submapper 0 (JY830623C, YY840238C) | submapper 1 (EJ-006-1) |
8//! |---|---|---|
9//! | register mask | `$F003` | `$F007` |
10//! | IRQ | 64 unfiltered rises of PPU A12 | 16-bit counter, down by 5 every 4th M2 |
11//! | mirroring | hard-wired | `$6004` H / `$6005` V |
12//! | outer bank | `$8000-$9FFF` address bits: A0 = CHR A19, A2-A1 = PRG A18-A17 | none |
13//!
14//! Banks: two 8 KiB PRG banks at `$8000`/`$A000` (`$7000`/`$7001`), the last
15//! 16 KiB fixed at `$C000`, and four 2 KiB CHR banks (`$6000-$6003`).
16//! Submapper 0's outer bank limits the inner PRG bank to 128 KiB (A13-A16),
17//! so "the last bank" is the last 16 KiB of the selected 128 KiB block.
18//!
19//! **Submapper 1's IRQ** is described only as "clocked by the M2 signal with a
20//! factor of 5/4, meaning counting down by five every fourth M2 cycle". The
21//! page does not say when it asserts. This board asserts when a decrement would
22//! take the counter below zero, then stops until `$7007` restarts it. That is
23//! why mapper 91 submapper 1 is `BestEffort` in `tier.rs` while submapper 0 is
24//! Curated.
25
26// Bank arithmetic narrows values already reduced modulo a bank count, and the
27// accessors stay non-`const` to match the other mapper modules; both lints
28// are allowed crate-wide in the sibling modules for the same reasons.
29// `Jy91` mirrors the board's independent flags (CHR-RAM, IRQ enable, IRQ
30// line, A12 level) one to one; folding them would obscure that mapping.
31#![allow(
32    clippy::cast_possible_truncation,
33    clippy::missing_const_for_fn,
34    clippy::struct_excessive_bools
35)]
36
37use crate::cartridge::Mirroring;
38use crate::mapper::{Mapper, MapperCaps, MapperDebugInfo, MapperError};
39use alloc::{boxed::Box, format, vec, vec::Vec};
40
41const PRG_BANK_8K: usize = 0x2000;
42const CHR_BANK_2K: usize = 0x0800;
43const SAVE_STATE_VERSION: u8 = 1;
44
45/// Submapper 0 fires after this many unfiltered PPU A12 rises.
46const A12_RISES_PER_IRQ: u8 = 64;
47
48/// Mapper 91 (JY830623C / EJ-006-1).
49pub struct Jy91 {
50    prg_rom: Box<[u8]>,
51    chr: Box<[u8]>,
52    chr_is_ram: bool,
53    vram: Box<[u8]>,
54    submapper: u8,
55    mirroring: Mirroring,
56    chr_regs: [u8; 4],
57    prg_regs: [u8; 2],
58    /// Submapper 0 outer bank, from the `$8000-$9FFF` write address.
59    outer: u8,
60    irq_enabled: bool,
61    irq_pending: bool,
62    /// Submapper 0: A12 rises counted. Submapper 1: the 16-bit counter.
63    irq_counter: u16,
64    /// Submapper 1: M2 cycles since the last decrement (0-3).
65    irq_prescale: u8,
66    last_a12: bool,
67}
68
69impl Jy91 {
70    /// Construct the board.
71    ///
72    /// # Errors
73    ///
74    /// [`MapperError::Invalid`] when PRG is not a non-zero multiple of 16 KiB
75    /// or CHR is not a multiple of 2 KiB.
76    pub fn new(
77        prg_rom: Box<[u8]>,
78        chr_rom: Box<[u8]>,
79        mirroring: Mirroring,
80        submapper: u8,
81    ) -> Result<Self, MapperError> {
82        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(2 * PRG_BANK_8K) {
83            return Err(MapperError::Invalid(format!(
84                "mapper 91 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
85                prg_rom.len()
86            )));
87        }
88        if !chr_rom.len().is_multiple_of(CHR_BANK_2K) {
89            return Err(MapperError::Invalid(format!(
90                "mapper 91 CHR-ROM size {} is not a multiple of 2 KiB",
91                chr_rom.len()
92            )));
93        }
94        let chr_is_ram = chr_rom.is_empty();
95        let chr = if chr_is_ram {
96            vec![0u8; 4 * CHR_BANK_2K].into_boxed_slice()
97        } else {
98            chr_rom
99        };
100        Ok(Self {
101            prg_rom,
102            chr,
103            chr_is_ram,
104            vram: vec![0u8; 0x800].into_boxed_slice(),
105            submapper: u8::from(submapper == 1),
106            mirroring,
107            chr_regs: [0; 4],
108            prg_regs: [0; 2],
109            outer: 0,
110            irq_enabled: false,
111            irq_pending: false,
112            irq_counter: 0,
113            irq_prescale: 0,
114            last_a12: false,
115        })
116    }
117
118    fn prg_bank(&self, addr: u16) -> usize {
119        let count = self.prg_rom.len() / PRG_BANK_8K;
120        let bank = if self.submapper == 0 {
121            let base = usize::from((self.outer >> 1) & 0x03) << 4;
122            match addr & 0xE000 {
123                0x8000 => base | usize::from(self.prg_regs[0] & 0x0F),
124                0xA000 => base | usize::from(self.prg_regs[1] & 0x0F),
125                0xC000 => base | 0x0E,
126                _ => base | 0x0F,
127            }
128        } else {
129            match addr & 0xE000 {
130                0x8000 => usize::from(self.prg_regs[0]),
131                0xA000 => usize::from(self.prg_regs[1]),
132                0xC000 => count.saturating_sub(2),
133                _ => count - 1,
134            }
135        };
136        bank % count
137    }
138
139    fn chr_offset(&self, addr: u16) -> usize {
140        let slot = usize::from((addr >> 11) & 0x03);
141        let mut bank = usize::from(self.chr_regs[slot]);
142        if self.submapper == 0 {
143            bank |= usize::from(self.outer & 0x01) << 8;
144        }
145        (bank % (self.chr.len() / CHR_BANK_2K)) * CHR_BANK_2K + usize::from(addr & 0x07FF)
146    }
147
148    fn nt_offset(&self, addr: u16) -> usize {
149        let table = ((addr - 0x2000) / 0x400) & 0x03;
150        self.mirroring.physical_bank(table as u8) * 0x400 + usize::from(addr & 0x3FF)
151    }
152}
153
154impl Mapper for Jy91 {
155    fn caps(&self) -> MapperCaps {
156        MapperCaps::CYCLE_IRQ
157    }
158
159    fn has_hardwired_mirroring(&self) -> bool {
160        self.submapper == 0
161    }
162
163    fn cpu_read(&mut self, addr: u16) -> u8 {
164        if addr >= 0x8000 {
165            self.prg_rom[self.prg_bank(addr) * PRG_BANK_8K + usize::from(addr & 0x1FFF)]
166        } else {
167            0
168        }
169    }
170
171    fn cpu_write(&mut self, addr: u16, value: u8) {
172        match addr {
173            0x6000..=0x7FFF => {
174                let mask = if self.submapper == 0 { 0xF003 } else { 0xF007 };
175                match (addr & mask, self.submapper) {
176                    (0x6000..=0x6003, _) => self.chr_regs[usize::from(addr & 0x03)] = value,
177                    (0x6004, 1) => self.mirroring = Mirroring::Horizontal,
178                    (0x6005, 1) => self.mirroring = Mirroring::Vertical,
179                    (0x6006, 1) => {
180                        self.irq_counter = (self.irq_counter & 0xFF00) | u16::from(value);
181                    }
182                    (0x6007, 1) => {
183                        self.irq_counter = (self.irq_counter & 0x00FF) | (u16::from(value) << 8);
184                    }
185                    (0x7000..=0x7001, _) => self.prg_regs[usize::from(addr & 0x01)] = value,
186                    // `$7006` (`$7002` under submapper 0's mask): stop + acknowledge.
187                    (0x7002, 0) | (0x7006, 1) => {
188                        self.irq_enabled = false;
189                        self.irq_pending = false;
190                    }
191                    // `$7007`: start / reset.
192                    (0x7003, 0) | (0x7007, 1) => {
193                        self.irq_enabled = true;
194                        self.irq_pending = false;
195                        self.irq_prescale = 0;
196                        if self.submapper == 0 {
197                            self.irq_counter = 0;
198                        }
199                    }
200                    _ => {}
201                }
202            }
203            0x8000..=0x9FFF if self.submapper == 0 => self.outer = (addr & 0x07) as u8,
204            _ => {}
205        }
206    }
207
208    fn notify_a12(&mut self, level: bool) {
209        let rising = level && !self.last_a12;
210        self.last_a12 = level;
211        if self.submapper != 0 || !rising || !self.irq_enabled {
212            return;
213        }
214        self.irq_counter += 1;
215        if self.irq_counter >= u16::from(A12_RISES_PER_IRQ) {
216            self.irq_pending = true;
217            self.irq_enabled = false;
218        }
219    }
220
221    fn notify_cpu_cycle(&mut self) {
222        if self.submapper != 1 || !self.irq_enabled {
223            return;
224        }
225        self.irq_prescale = (self.irq_prescale + 1) & 0x03;
226        if self.irq_prescale == 0 {
227            if self.irq_counter < 5 {
228                self.irq_counter = 0;
229                self.irq_pending = true;
230                self.irq_enabled = false;
231            } else {
232                self.irq_counter -= 5;
233            }
234        }
235    }
236
237    fn irq_pending(&self) -> bool {
238        self.irq_pending
239    }
240
241    fn chr_phys(&self, addr: u16) -> Option<u32> {
242        if self.chr_is_ram {
243            None
244        } else {
245            u32::try_from(self.chr_offset(addr & 0x1FFF)).ok()
246        }
247    }
248
249    fn ppu_read(&mut self, addr: u16) -> u8 {
250        let addr = addr & 0x3FFF;
251        match addr {
252            0x0000..=0x1FFF => self.chr[self.chr_offset(addr)],
253            0x2000..=0x3EFF => self.vram[self.nt_offset(addr)],
254            _ => 0,
255        }
256    }
257
258    fn ppu_write(&mut self, addr: u16, value: u8) {
259        let addr = addr & 0x3FFF;
260        match addr {
261            0x0000..=0x1FFF if self.chr_is_ram => {
262                let off = self.chr_offset(addr);
263                self.chr[off] = value;
264            }
265            0x2000..=0x3EFF => {
266                let off = self.nt_offset(addr);
267                self.vram[off] = value;
268            }
269            _ => {}
270        }
271    }
272
273    fn nametable_address(&self, addr: u16) -> u16 {
274        u16::try_from(self.nt_offset(addr)).unwrap_or(0)
275    }
276
277    fn current_mirroring(&self) -> Mirroring {
278        self.mirroring
279    }
280
281    fn debug_info(&self) -> MapperDebugInfo {
282        let mut info = MapperDebugInfo {
283            mapper_id: 91,
284            name: format!("J.Y. 91 (submapper {})", self.submapper),
285            mirroring: crate::mapper::mirroring_name(self.mirroring),
286            ..Default::default()
287        };
288        for (i, r) in self.prg_regs.iter().enumerate() {
289            info.prg_banks.push((format!("P{i}"), format!("{r:#04x}")));
290        }
291        for (i, r) in self.chr_regs.iter().enumerate() {
292            info.chr_banks.push((format!("C{i}"), format!("{r:#04x}")));
293        }
294        info.irq_state
295            .push(("counter".into(), format!("{:#06x}", self.irq_counter)));
296        info.irq_state
297            .push(("enabled".into(), format!("{}", self.irq_enabled)));
298        info.extra
299            .push(("outer".into(), format!("{:#04x}", self.outer)));
300        info
301    }
302
303    fn save_state(&self) -> Vec<u8> {
304        let mut out = Vec::with_capacity(20 + self.vram.len() + self.chr.len());
305        out.push(SAVE_STATE_VERSION);
306        out.push(self.submapper);
307        out.push(match self.mirroring {
308            Mirroring::Horizontal => 0,
309            _ => 1,
310        });
311        out.extend_from_slice(&self.chr_regs);
312        out.extend_from_slice(&self.prg_regs);
313        out.push(self.outer);
314        out.push(u8::from(self.irq_enabled));
315        out.push(u8::from(self.irq_pending));
316        out.extend_from_slice(&self.irq_counter.to_le_bytes());
317        out.push(self.irq_prescale);
318        out.push(u8::from(self.last_a12));
319        out.extend_from_slice(&self.vram);
320        if self.chr_is_ram {
321            out.extend_from_slice(&self.chr);
322        }
323        out
324    }
325
326    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
327        const HEAD: usize = 16;
328        let chr = if self.chr_is_ram { self.chr.len() } else { 0 };
329        let expected = HEAD + self.vram.len() + chr;
330        if data.len() != expected {
331            return Err(MapperError::WrongLength {
332                expected,
333                got: data.len(),
334            });
335        }
336        if data[0] != SAVE_STATE_VERSION {
337            return Err(MapperError::UnsupportedVersion(data[0]));
338        }
339        if data[1] != self.submapper {
340            return Err(MapperError::Invalid(format!(
341                "state is for mapper 91 submapper {}, this board is {}",
342                data[1], self.submapper
343            )));
344        }
345        // Submapper 0 counts A12 rises up to `A12_RISES_PER_IRQ` and stops
346        // there, so an enabled counter is below it and a stopped one at most
347        // equal. Anything else loaded cleanly and overflowed `+= 1` on the
348        // next rise. Submapper 1's counter is written whole by `$6006/$6007`,
349        // so every value is one it can hold. Validated before any assignment.
350        let counter = u16::from_le_bytes([data[12], data[13]]);
351        let limit = u16::from(A12_RISES_PER_IRQ);
352        if self.submapper == 0 && (counter > limit || (data[10] != 0 && counter >= limit)) {
353            return Err(MapperError::Invalid(format!(
354                "mapper 91 IRQ counter {counter} is not one submapper 0 reaches \
355                 (enabled {})",
356                data[10] != 0
357            )));
358        }
359        if self.submapper == 1 {
360            self.mirroring = if data[2] == 0 {
361                Mirroring::Horizontal
362            } else {
363                Mirroring::Vertical
364            };
365        }
366        self.chr_regs.copy_from_slice(&data[3..7]);
367        self.prg_regs.copy_from_slice(&data[7..9]);
368        self.outer = data[9] & 0x07;
369        self.irq_enabled = data[10] != 0;
370        self.irq_pending = data[11] != 0;
371        self.irq_counter = counter;
372        self.irq_prescale = data[14] & 0x03;
373        self.last_a12 = data[15] != 0;
374        self.vram
375            .copy_from_slice(&data[HEAD..HEAD + self.vram.len()]);
376        if self.chr_is_ram {
377            self.chr.copy_from_slice(&data[HEAD + self.vram.len()..]);
378        }
379        Ok(())
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    fn board(prg_8k: usize, chr_2k: usize, sub: u8) -> Jy91 {
388        let mut prg = vec![0u8; prg_8k * PRG_BANK_8K];
389        for b in 0..prg_8k {
390            prg[b * PRG_BANK_8K] = b as u8;
391        }
392        let mut chr = vec![0u8; chr_2k * CHR_BANK_2K];
393        for b in 0..chr_2k {
394            chr[b * CHR_BANK_2K] = b as u8;
395            chr[b * CHR_BANK_2K + 1] = (b >> 8) as u8;
396        }
397        Jy91::new(
398            prg.into_boxed_slice(),
399            chr.into_boxed_slice(),
400            Mirroring::Vertical,
401            sub,
402        )
403        .unwrap()
404    }
405
406    fn chr_at(m: &mut Jy91, addr: u16) -> usize {
407        usize::from(m.ppu_read(addr)) | usize::from(m.ppu_read(addr + 1)) << 8
408    }
409
410    #[test]
411    fn banks_follow_the_register_table() {
412        let mut m = board(16, 16, 0);
413        m.cpu_write(0x7000, 3);
414        m.cpu_write(0x7001, 5);
415        m.cpu_write(0x6002, 7);
416        assert_eq!(m.cpu_read(0x8000), 3);
417        assert_eq!(m.cpu_read(0xA000), 5);
418        assert_eq!(m.cpu_read(0xC000), 14, "the last 16 KiB is fixed");
419        assert_eq!(m.cpu_read(0xE000), 15);
420        assert_eq!(chr_at(&mut m, 0x1000), 7);
421        // Submapper 0's mask `$F003` mirrors `$6002` at `$6006`.
422        m.cpu_write(0x6FF6, 9);
423        assert_eq!(chr_at(&mut m, 0x1000), 9);
424    }
425
426    #[test]
427    fn submapper0_outer_bank_comes_from_the_write_address() {
428        let mut m = board(64, 512, 0);
429        m.cpu_write(0x7000, 3);
430        m.cpu_write(0x6000, 0x05);
431        m.cpu_write(0x8007, 0xFF); // PRG A18-A17 = 3, CHR A19 = 1
432        assert_eq!(m.cpu_read(0x8000), 0x30 | 3);
433        assert_eq!(m.cpu_read(0xE000), 0x3F, "last bank of the 128 KiB block");
434        assert_eq!(chr_at(&mut m, 0x0000), 0x105);
435        m.cpu_write(0x9FF2, 0x00); // PRG A18-A17 = 1, CHR A19 = 0
436        assert_eq!(m.cpu_read(0x8000), 0x13);
437        assert_eq!(chr_at(&mut m, 0x0000), 0x05);
438    }
439
440    #[test]
441    fn submapper0_irq_after_64_unfiltered_a12_rises() {
442        let mut m = board(16, 16, 0);
443        m.cpu_write(0x7007, 0);
444        for _ in 0..63 {
445            m.notify_a12(true);
446            m.notify_a12(false);
447        }
448        assert!(!m.irq_pending());
449        m.notify_a12(true);
450        assert!(m.irq_pending(), "the 64th rise asserts");
451        m.cpu_write(0x7006, 0);
452        assert!(!m.irq_pending(), "$7006 acknowledges");
453    }
454
455    #[test]
456    fn submapper1_mirroring_and_m2_irq() {
457        let mut m = board(16, 16, 1);
458        m.cpu_write(0x6004, 0);
459        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
460        m.cpu_write(0x6005, 0);
461        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
462        // 10 counts = two decrements of five, eight M2 cycles, then the
463        // third decrement underflows at M2 cycle twelve.
464        m.cpu_write(0x6006, 10);
465        m.cpu_write(0x6007, 0);
466        m.cpu_write(0x7007, 0);
467        for _ in 0..11 {
468            m.notify_cpu_cycle();
469        }
470        assert!(!m.irq_pending());
471        m.notify_cpu_cycle();
472        assert!(m.irq_pending());
473        // Submapper 1's `$F007` mask: `$6006` is no longer CHR register 2.
474        assert!(!m.has_hardwired_mirroring());
475    }
476
477    #[test]
478    fn state_refuses_a_counter_the_board_cannot_reach() {
479        // Submapper 0 counts A12 rises up to 64 and stops there, so an enabled
480        // counter is below 64 and a stopped one at most 64. 0xFFFF loaded
481        // cleanly and overflowed `+= 1` on the next rise.
482        let good = board(16, 16, 0).save_state();
483        for (enabled, counter) in [(1u8, 0xFFFFu16), (1, 64), (0, 65)] {
484            let mut blob = good.clone();
485            blob[10] = enabled;
486            blob[12..14].copy_from_slice(&counter.to_le_bytes());
487            let mut m = board(16, 16, 0);
488            assert!(
489                matches!(m.load_state(&blob), Err(MapperError::Invalid(_))),
490                "enabled {enabled} counter {counter}"
491            );
492            assert_eq!(m.save_state(), good, "refused before any assignment");
493        }
494        for (enabled, counter) in [(1u8, 63u16), (0, 64)] {
495            let mut blob = good.clone();
496            blob[10] = enabled;
497            blob[12..14].copy_from_slice(&counter.to_le_bytes());
498            board(16, 16, 0).load_state(&blob).unwrap();
499        }
500        // Submapper 1 loads the counter from its registers: any value is real.
501        let mut blob = board(16, 16, 1).save_state();
502        blob[10] = 1;
503        blob[12..14].copy_from_slice(&0xFFFFu16.to_le_bytes());
504        board(16, 16, 1).load_state(&blob).unwrap();
505    }
506
507    #[test]
508    fn state_round_trips() {
509        for sub in [0, 1] {
510            let mut a = board(64, 512, sub);
511            a.cpu_write(0x7000, 7);
512            a.cpu_write(0x6001, 9);
513            a.cpu_write(0x8005, 0);
514            a.cpu_write(0x7007, 0);
515            let blob = a.save_state();
516            let mut b = board(64, 512, sub);
517            b.load_state(&blob).unwrap();
518            assert_eq!(b.save_state(), blob);
519            assert_eq!(b.cpu_read(0x8000), a.cpu_read(0x8000));
520        }
521        assert!(
522            board(16, 16, 1)
523                .load_state(&board(16, 16, 0).save_state())
524                .is_err()
525        );
526    }
527}