Skip to main content

rustynes_mappers/
m065_irem_h3001.rs

1//! Irem H3001 (iNES mapper 65) implementation.
2//!
3//! Used by Daiku no Gen-san 2, Kaiketsu Yanchamaru 3 (Spartan X 2), etc.
4//! It has a VRC4/MMC3-like banking surface and a 16-bit CPU-cycle IRQ that
5//! reloads from a write-high/write-low latch and counts **down** each M2.
6//!
7//! # Registers (`nesdev_wiki/INES_Mapper_065.xhtml`)
8//!
9//! | Addr        | Purpose                                                  |
10//! |-------------|----------------------------------------------------------|
11//! | `$8000`     | PRG reg 0 (8 KiB @ `$8000`, or @ `$C000` per `$9000`)    |
12//! | `$9000`     | bit 7 = PRG bank layout                                  |
13//! | `$9001`     | bits 6-7 = mirroring (00=V, 10=H, 01/11=1scA)            |
14//! | `$9003`     | bit 7 = IRQ enable                                       |
15//! | `$9004`     | (any write) reload IRQ counter from the 16-bit value     |
16//! | `$9005`     | IRQ reload value HIGH 8 bits                              |
17//! | `$9006`     | IRQ reload value LOW 8 bits                              |
18//! | `$A000`     | PRG reg 1 (8 KiB @ `$A000`)                               |
19//! | `$B000-7`   | eight 1 KiB CHR bank regs (`$0000`-`$1C00`)               |
20//!
21//! `$E000` is always fixed to bank `$3F`; the second-fixed bank (`$8000` or
22//! `$C000`, depending on `$9000` bit 7) is `$3E`.
23//!
24//! Powerup quirk: `$8000` reads as `$00` and `$A000` as `$01` (games rely on
25//! it). We initialise both registers accordingly.
26//!
27//! # IRQ
28//!
29//! A 16-bit down-counter decrements by 1 each CPU cycle while enabled; when it
30//! reaches 0 it asserts an IRQ and **stops** (no wrap, no auto-reload). Any
31//! write to `$9003` or `$9004` acknowledges a pending IRQ; `$9004` also copies
32//! the 16-bit reload value into the counter. `$9005`/`$9006` set the reload
33//! value only (note `$9005` is the HIGH byte).
34//!
35//! Reuses the CPU-cycle IRQ family pattern (`m021_vrc4.rs`, `m073_vrc3.rs`).
36
37#![allow(
38    clippy::cast_possible_truncation,
39    clippy::cast_lossless,
40    clippy::missing_const_for_fn,
41    clippy::struct_excessive_bools,
42    clippy::match_same_arms,
43    clippy::doc_markdown
44)]
45
46use crate::cartridge::Mirroring;
47use crate::mapper::{Mapper, MapperCaps, MapperError};
48use alloc::{boxed::Box, vec::Vec};
49use alloc::{format, vec};
50
51const PRG_BANK_8K: usize = 0x2000;
52const CHR_BANK_1K: usize = 0x0400;
53const NAMETABLE_SIZE: usize = 0x0400;
54const NAMETABLE_SIZE_U16: u16 = 0x0400;
55
56const SAVE_STATE_VERSION: u8 = 1;
57
58/// Irem H3001 mapper (iNES mapper 65).
59pub struct IremH3001 {
60    prg_rom: Box<[u8]>,
61    chr_rom: Box<[u8]>,
62    vram: Box<[u8]>,
63    chr_is_ram: bool,
64
65    prg_reg0: u8,
66    prg_reg1: u8,
67    prg_layout: bool,  // $9000 bit 7
68    chr_regs: [u8; 8], // eight 1 KiB CHR bank selects
69
70    mirroring: Mirroring,
71
72    irq_reload: u16,
73    irq_counter: u16,
74    irq_enabled: bool,
75    irq_pending: bool,
76}
77
78impl IremH3001 {
79    /// Construct a new H3001 mapper.
80    ///
81    /// `prg_rom` must be a non-zero multiple of 8 KiB; CHR-ROM (when present)
82    /// must be a multiple of 1 KiB. CHR-RAM (8 KiB) is allocated when no
83    /// CHR-ROM is supplied.
84    ///
85    /// # Errors
86    ///
87    /// Returns [`MapperError::Invalid`] on size mismatch.
88    pub fn new(
89        prg_rom: Box<[u8]>,
90        chr_rom: Box<[u8]>,
91        mirroring: Mirroring,
92    ) -> Result<Self, MapperError> {
93        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
94            return Err(MapperError::Invalid(format!(
95                "H3001 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
96                prg_rom.len()
97            )));
98        }
99        let chr_is_ram = chr_rom.is_empty();
100        let chr_data: Box<[u8]> = if chr_is_ram {
101            vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
102        } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
103            chr_rom
104        } else {
105            return Err(MapperError::Invalid(format!(
106                "H3001 CHR-ROM size {} is not a multiple of 1 KiB",
107                chr_rom.len()
108            )));
109        };
110        Ok(Self {
111            prg_rom,
112            chr_rom: chr_data,
113            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
114            chr_is_ram,
115            // Powerup values games rely on.
116            prg_reg0: 0x00,
117            prg_reg1: 0x01,
118            prg_layout: false,
119            chr_regs: [0; 8],
120            mirroring,
121            irq_reload: 0,
122            irq_counter: 0,
123            irq_enabled: false,
124            irq_pending: false,
125        })
126    }
127
128    fn prg_offset(&self, addr: u16) -> usize {
129        let total = (self.prg_rom.len() / PRG_BANK_8K).max(1);
130        // $3E and $3F are the canonical second-last / last fixed banks but a
131        // small synthetic ROM may have fewer banks — mask into range.
132        let fixed_3e = 0x3E % total;
133        let fixed_3f = 0x3F % total;
134        let bank = match (addr & 0xE000, self.prg_layout) {
135            (0x8000, false) => self.prg_reg0 as usize,
136            (0x8000, true) => fixed_3e,
137            (0xA000, _) => self.prg_reg1 as usize,
138            (0xC000, false) => fixed_3e,
139            (0xC000, true) => self.prg_reg0 as usize,
140            (0xE000, _) => fixed_3f,
141            _ => 0,
142        };
143        (bank % total) * PRG_BANK_8K + (addr as usize & 0x1FFF)
144    }
145
146    fn chr_offset(&self, addr: u16) -> usize {
147        let addr = (addr & 0x1FFF) as usize;
148        let total_1k = (self.chr_rom.len() / CHR_BANK_1K).max(1);
149        let slot = addr / CHR_BANK_1K;
150        let bank = (self.chr_regs[slot] as usize) % total_1k;
151        bank * CHR_BANK_1K + (addr & (CHR_BANK_1K - 1))
152    }
153
154    fn nametable_offset(&self, addr: u16) -> usize {
155        let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
156        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
157        let physical = self.mirroring.physical_bank(table);
158        physical * NAMETABLE_SIZE + local
159    }
160}
161
162impl Mapper for IremH3001 {
163    // v2.8.0 Phase 4 — CPU-cycle hook + IRQ source; no on-cart audio.
164    fn caps(&self) -> MapperCaps {
165        MapperCaps::CYCLE_IRQ
166    }
167
168    fn cpu_read(&mut self, addr: u16) -> u8 {
169        match addr {
170            0x8000..=0xFFFF => {
171                let off = self.prg_offset(addr);
172                self.prg_rom[off % self.prg_rom.len()]
173            }
174            _ => 0,
175        }
176    }
177
178    fn cpu_write(&mut self, addr: u16, value: u8) {
179        match addr {
180            0x8000..=0x8FFF => self.prg_reg0 = value,
181            0x9000..=0x9FFF => match addr & 0x0007 {
182                0 => self.prg_layout = (value & 0x80) != 0,
183                1 => {
184                    self.mirroring = match (value >> 6) & 0x03 {
185                        0 => Mirroring::Vertical,
186                        2 => Mirroring::Horizontal,
187                        // 1 and 3 -> one-screen A.
188                        _ => Mirroring::SingleScreenA,
189                    };
190                }
191                3 => {
192                    self.irq_enabled = (value & 0x80) != 0;
193                    self.irq_pending = false;
194                }
195                4 => {
196                    self.irq_counter = self.irq_reload;
197                    self.irq_pending = false;
198                }
199                5 => self.irq_reload = (self.irq_reload & 0x00FF) | ((value as u16) << 8),
200                6 => self.irq_reload = (self.irq_reload & 0xFF00) | (value as u16),
201                _ => {}
202            },
203            0xA000..=0xAFFF => self.prg_reg1 = value,
204            0xB000..=0xBFFF => {
205                let slot = (addr & 0x0007) as usize;
206                self.chr_regs[slot] = value;
207            }
208            _ => {}
209        }
210    }
211
212    fn ppu_read(&mut self, addr: u16) -> u8 {
213        let addr = addr & 0x3FFF;
214        match addr {
215            0x0000..=0x1FFF => {
216                let off = self.chr_offset(addr);
217                self.chr_rom[off % self.chr_rom.len()]
218            }
219            0x2000..=0x3EFF => self.vram[self.nametable_offset(addr) % self.vram.len()],
220            _ => 0,
221        }
222    }
223
224    fn ppu_write(&mut self, addr: u16, value: u8) {
225        let addr = addr & 0x3FFF;
226        match addr {
227            0x0000..=0x1FFF => {
228                if self.chr_is_ram {
229                    let off = self.chr_offset(addr);
230                    let len = self.chr_rom.len();
231                    self.chr_rom[off % len] = value;
232                }
233            }
234            0x2000..=0x3EFF => {
235                let off = self.nametable_offset(addr) % self.vram.len();
236                self.vram[off] = value;
237            }
238            _ => {}
239        }
240    }
241
242    fn notify_cpu_cycle(&mut self) {
243        if !self.irq_enabled || self.irq_counter == 0 {
244            return;
245        }
246        self.irq_counter -= 1;
247        if self.irq_counter == 0 {
248            self.irq_pending = true;
249        }
250    }
251
252    fn irq_pending(&self) -> bool {
253        self.irq_pending
254    }
255
256    fn current_mirroring(&self) -> Mirroring {
257        self.mirroring
258    }
259
260    fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
261        let mut info = crate::mapper::MapperDebugInfo {
262            mapper_id: 65,
263            name: "Irem H3001 (65)".into(),
264            mirroring: crate::mapper::mirroring_name(self.mirroring),
265            ..Default::default()
266        };
267        info.prg_banks
268            .push(("layout".into(), format!("{}", u8::from(self.prg_layout))));
269        info.prg_banks
270            .push(("reg0".into(), format!("{:#04x}", self.prg_reg0)));
271        info.prg_banks
272            .push(("reg1".into(), format!("{:#04x}", self.prg_reg1)));
273        for (i, b) in self.chr_regs.iter().enumerate() {
274            info.chr_banks.push((format!("C{i}"), format!("{b:#04x}")));
275        }
276        info.irq_state
277            .push(("reload".into(), format!("{:#06x}", self.irq_reload)));
278        info.irq_state
279            .push(("counter".into(), format!("{:#06x}", self.irq_counter)));
280        info.irq_state
281            .push(("enabled".into(), format!("{}", self.irq_enabled)));
282        info.irq_state
283            .push(("pending".into(), format!("{}", self.irq_pending)));
284        info
285    }
286
287    fn save_state(&self) -> Vec<u8> {
288        let mut out = Vec::with_capacity(
289            24 + self.vram.len()
290                + if self.chr_is_ram {
291                    self.chr_rom.len()
292                } else {
293                    0
294                },
295        );
296        out.push(SAVE_STATE_VERSION);
297        out.push(self.prg_reg0);
298        out.push(self.prg_reg1);
299        out.push(u8::from(self.prg_layout));
300        out.extend_from_slice(&self.chr_regs);
301        out.push(self.mirroring as u8);
302        out.extend_from_slice(&self.irq_reload.to_le_bytes());
303        out.extend_from_slice(&self.irq_counter.to_le_bytes());
304        out.push(u8::from(self.irq_enabled));
305        out.push(u8::from(self.irq_pending));
306        out.extend_from_slice(&self.vram);
307        if self.chr_is_ram {
308            out.extend_from_slice(&self.chr_rom);
309        }
310        out
311    }
312
313    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
314        let chr_part = if self.chr_is_ram {
315            self.chr_rom.len()
316        } else {
317            0
318        };
319        // 1 + 1 + 1 + 1 + 8 (chr regs) + 1 (mir) + 2 + 2 + 1 + 1
320        let scalar_len = 1 + 1 + 1 + 1 + 8 + 1 + 2 + 2 + 1 + 1;
321        let expected = scalar_len + self.vram.len() + chr_part;
322        if data.len() != expected {
323            return Err(MapperError::WrongLength {
324                expected,
325                got: data.len(),
326            });
327        }
328        if data[0] != SAVE_STATE_VERSION {
329            return Err(MapperError::UnsupportedVersion(data[0]));
330        }
331        let mut c = 1usize;
332        self.prg_reg0 = data[c];
333        c += 1;
334        self.prg_reg1 = data[c];
335        c += 1;
336        self.prg_layout = data[c] != 0;
337        c += 1;
338        self.chr_regs.copy_from_slice(&data[c..c + 8]);
339        c += 8;
340        self.mirroring = match data[c] {
341            0 => Mirroring::Horizontal,
342            1 => Mirroring::Vertical,
343            2 => Mirroring::SingleScreenA,
344            3 => Mirroring::SingleScreenB,
345            4 => Mirroring::FourScreen,
346            5 => Mirroring::MapperControlled,
347            other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
348        };
349        c += 1;
350        self.irq_reload = u16::from_le_bytes([data[c], data[c + 1]]);
351        c += 2;
352        self.irq_counter = u16::from_le_bytes([data[c], data[c + 1]]);
353        c += 2;
354        self.irq_enabled = data[c] != 0;
355        c += 1;
356        self.irq_pending = data[c] != 0;
357        c += 1;
358        self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
359        c += self.vram.len();
360        if self.chr_is_ram {
361            self.chr_rom
362                .copy_from_slice(&data[c..c + self.chr_rom.len()]);
363        }
364        Ok(())
365    }
366}
367
368#[cfg(test)]
369#[allow(clippy::cast_possible_truncation)]
370mod tests {
371    use super::*;
372
373    fn synth_prg(banks_8k: usize) -> Box<[u8]> {
374        let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
375        for b in 0..banks_8k {
376            v[b * PRG_BANK_8K] = b as u8;
377        }
378        v.into_boxed_slice()
379    }
380
381    fn synth_chr(banks_1k: usize) -> Box<[u8]> {
382        let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
383        for b in 0..banks_1k {
384            v[b * CHR_BANK_1K] = b as u8;
385        }
386        v.into_boxed_slice()
387    }
388
389    fn fresh() -> IremH3001 {
390        // 64 banks of 8 KiB = 512 KiB so $3E/$3F resolve to distinct banks.
391        IremH3001::new(synth_prg(64), synth_chr(64), Mirroring::Vertical).unwrap()
392    }
393
394    #[test]
395    fn powerup_register_values() {
396        let mut m = fresh();
397        // $8000 reg0 = 0, $A000 reg1 = 1 at power-on.
398        assert_eq!(m.cpu_read(0x8000), 0);
399        assert_eq!(m.cpu_read(0xA000), 1);
400    }
401
402    #[test]
403    fn prg_layout_0_fixes_c000_to_3e_and_e000_to_3f() {
404        let mut m = fresh();
405        m.cpu_write(0x8000, 5); // reg0 -> bank 5 @ $8000
406        assert_eq!(m.cpu_read(0x8000), 5);
407        assert_eq!(m.cpu_read(0xC000), 0x3E);
408        assert_eq!(m.cpu_read(0xE000), 0x3F);
409    }
410
411    #[test]
412    fn prg_layout_1_swaps_to_c000() {
413        let mut m = fresh();
414        m.cpu_write(0x8000, 5); // reg0
415        m.cpu_write(0x9000, 0x80); // layout bit
416        assert_eq!(m.cpu_read(0x8000), 0x3E); // fixed
417        assert_eq!(m.cpu_read(0xC000), 5); // reg0 here now
418        assert_eq!(m.cpu_read(0xE000), 0x3F);
419    }
420
421    #[test]
422    fn chr_eight_1k_banks() {
423        let mut m = fresh();
424        m.cpu_write(0xB000, 10);
425        m.cpu_write(0xB004, 20);
426        assert_eq!(m.ppu_read(0x0000), 10);
427        assert_eq!(m.ppu_read(0x1000), 20);
428    }
429
430    #[test]
431    fn mirroring_select() {
432        let mut m = fresh();
433        m.cpu_write(0x9001, 0x00);
434        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
435        m.cpu_write(0x9001, 0x80); // %10
436        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
437        m.cpu_write(0x9001, 0x40); // %01
438        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
439    }
440
441    #[test]
442    fn irq_counts_down_and_asserts() {
443        let mut m = fresh();
444        m.cpu_write(0x9005, 0x00); // reload high
445        m.cpu_write(0x9006, 0x03); // reload low -> 3
446        m.cpu_write(0x9004, 0x00); // load counter from reload
447        m.cpu_write(0x9003, 0x80); // enable
448        m.notify_cpu_cycle(); // 3 -> 2
449        m.notify_cpu_cycle(); // 2 -> 1
450        assert!(!m.irq_pending());
451        m.notify_cpu_cycle(); // 1 -> 0 -> assert
452        assert!(m.irq_pending());
453    }
454
455    #[test]
456    fn irq_stops_at_zero_no_wrap() {
457        let mut m = fresh();
458        m.cpu_write(0x9005, 0x00);
459        m.cpu_write(0x9006, 0x01);
460        m.cpu_write(0x9004, 0x00);
461        m.cpu_write(0x9003, 0x80);
462        m.notify_cpu_cycle(); // -> 0, assert
463        assert!(m.irq_pending());
464        assert_eq!(m.irq_counter, 0);
465        // Many more cycles: counter must NOT wrap below 0.
466        for _ in 0..1000 {
467            m.notify_cpu_cycle();
468        }
469        assert_eq!(m.irq_counter, 0);
470    }
471
472    #[test]
473    fn write_9003_or_9004_acks() {
474        let mut m = fresh();
475        m.irq_pending = true;
476        m.cpu_write(0x9003, 0x00); // ack (and disable)
477        assert!(!m.irq_pending());
478        m.irq_pending = true;
479        m.cpu_write(0x9004, 0x00); // ack + reload
480        assert!(!m.irq_pending());
481    }
482
483    #[test]
484    fn save_state_round_trip() {
485        let mut m = fresh();
486        m.cpu_write(0x8000, 5);
487        m.cpu_write(0xB002, 7);
488        m.cpu_write(0x9005, 0x12);
489        m.cpu_write(0x9006, 0x34);
490        m.cpu_write(0x9004, 0x00);
491        m.cpu_write(0x9003, 0x80);
492        m.ppu_write(0x2000, 0x55);
493        let blob = m.save_state();
494        let mut m2 = fresh();
495        m2.load_state(&blob).unwrap();
496        assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
497        assert_eq!(m.ppu_read(0x0800), m2.ppu_read(0x0800));
498        assert_eq!(m.irq_counter, m2.irq_counter);
499        assert_eq!(m.ppu_read(0x2000), m2.ppu_read(0x2000));
500    }
501}