Skip to main content

rustynes_mappers/
m179_hengedianzi.rs

1//! Hengedianzi (mapper 179) -- Chinese unlicensed board.
2//!
3//! The same 32 KiB PRG select plus mirroring bit as mapper 177
4//! (`m177_hengedianzi.rs`), but split across two windows: PRG through
5//! `$5000-$5FFF` and mirroring through `$8000-$FFFF`, so a bank switch can
6//! never be confused with an ordinary ROM write.
7//!
8//! A best-effort (Tier-2) board: register-decode correctness verified against
9//! the reference emulators (`Mesen2`, `GeraNES`) and the nesdev wiki, with no
10//! commercial-oracle ROM in the tree. Banking math is direct slice indexing and
11//! every bank select wraps with `% count`, so a register write can never index
12//! out of bounds -- required for the `#![no_std]` chip stack, which cannot
13//! afford a panic on a register access.
14//!
15//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
16//! and `docs/mappers.md` §Mapper coverage matrix.
17
18#![allow(
19    clippy::bool_to_int_with_if,
20    clippy::cast_lossless,
21    clippy::cast_possible_truncation,
22    clippy::doc_markdown,
23    clippy::match_same_arms,
24    clippy::missing_const_for_fn,
25    clippy::similar_names,
26    clippy::struct_excessive_bools,
27    clippy::too_many_lines,
28    clippy::unreadable_literal
29)]
30
31use crate::cartridge::Mirroring;
32use crate::mapper::{Mapper, MapperCaps, MapperError};
33use alloc::{boxed::Box, vec::Vec};
34use alloc::{format, vec};
35
36const PRG_BANK_32K: usize = 0x8000;
37const CHR_BANK_8K: usize = 0x2000;
38const NAMETABLE_SIZE: usize = 0x0400;
39const NAMETABLE_SIZE_U16: u16 = 0x0400;
40
41const SAVE_STATE_VERSION: u8 = 1;
42
43// ---------------------------------------------------------------------------
44// Shared nametable helper (mirrors the one in the other simple-mapper modules).
45// ---------------------------------------------------------------------------
46
47const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
48    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
49    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
50    let physical = mirroring.physical_bank(table);
51    physical * NAMETABLE_SIZE + local
52}
53
54/// Mapper 179 (Hengedianzi variant).
55pub struct Hengedianzi179 {
56    prg_rom: Box<[u8]>,
57    chr_ram: Box<[u8]>,
58    vram: Box<[u8]>,
59    prg_bank: u8,
60    horizontal_mirroring: bool,
61}
62
63impl Hengedianzi179 {
64    /// Construct a new mapper 179 board.
65    ///
66    /// # Errors
67    ///
68    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
69    /// 32 KiB.
70    pub fn new(prg_rom: Box<[u8]>, _chr_rom: &[u8]) -> Result<Self, MapperError> {
71        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
72            return Err(MapperError::Invalid(format!(
73                "mapper 179 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
74                prg_rom.len()
75            )));
76        }
77        Ok(Self {
78            prg_rom,
79            chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
80            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
81            prg_bank: 0,
82            horizontal_mirroring: false,
83        })
84    }
85}
86
87impl Mapper for Hengedianzi179 {
88    fn caps(&self) -> MapperCaps {
89        MapperCaps::NONE
90    }
91
92    // The PRG-bank register answers a write-only window at $5000-$5FFF; reads
93    // there are open bus, so the default `cpu_read_unmapped` is correct.
94
95    fn cpu_read(&mut self, addr: u16) -> u8 {
96        if (0x8000..=0xFFFF).contains(&addr) {
97            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
98            let bank = (self.prg_bank as usize) % count;
99            self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
100        } else {
101            0
102        }
103    }
104
105    fn cpu_write(&mut self, addr: u16, value: u8) {
106        match addr {
107            0x5000..=0x5FFF => self.prg_bank = value >> 1,
108            0x8000..=0xFFFF => self.horizontal_mirroring = (value & 0x01) != 0,
109            _ => {}
110        }
111    }
112
113    fn ppu_read(&mut self, addr: u16) -> u8 {
114        let addr = addr & 0x3FFF;
115        match addr {
116            0x0000..=0x1FFF => self.chr_ram[addr as usize],
117            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
118            _ => 0,
119        }
120    }
121
122    fn ppu_write(&mut self, addr: u16, value: u8) {
123        let addr = addr & 0x3FFF;
124        match addr {
125            0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
126            0x2000..=0x3EFF => {
127                let off = nametable_offset(addr, self.current_mirroring());
128                self.vram[off] = value;
129            }
130            _ => {}
131        }
132    }
133
134    fn current_mirroring(&self) -> Mirroring {
135        if self.horizontal_mirroring {
136            Mirroring::Horizontal
137        } else {
138            Mirroring::Vertical
139        }
140    }
141
142    fn save_state(&self) -> Vec<u8> {
143        let mut out = Vec::with_capacity(3 + self.vram.len() + self.chr_ram.len());
144        out.push(SAVE_STATE_VERSION);
145        out.push(self.prg_bank);
146        out.push(u8::from(self.horizontal_mirroring));
147        out.extend_from_slice(&self.vram);
148        out.extend_from_slice(&self.chr_ram);
149        out
150    }
151
152    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
153        let expected = 3 + self.vram.len() + self.chr_ram.len();
154        if data.len() != expected {
155            return Err(MapperError::WrongLength {
156                expected,
157                got: data.len(),
158            });
159        }
160        if data[0] != SAVE_STATE_VERSION {
161            return Err(MapperError::UnsupportedVersion(data[0]));
162        }
163        self.prg_bank = data[1];
164        self.horizontal_mirroring = data[2] != 0;
165        let mut cursor = 3;
166        self.vram
167            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
168        cursor += self.vram.len();
169        self.chr_ram
170            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
171        Ok(())
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    fn synth_prg_32k(banks: usize) -> Box<[u8]> {
180        let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
181        for b in 0..banks {
182            v[b * PRG_BANK_32K] = b as u8;
183        }
184        v.into_boxed_slice()
185    }
186
187    #[test]
188    fn m179_prg_via_5000_and_mirror_via_8000() {
189        let mut m = Hengedianzi179::new(synth_prg_32k(8), &[]).unwrap();
190        // PRG = value >> 1; write 6 -> bank 3.
191        m.cpu_write(0x5000, 6);
192        assert_eq!(m.cpu_read(0x8000), 3);
193        // Mirroring bit at $8000-$FFFF (bit 0).
194        m.cpu_write(0x8000, 0x01);
195        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
196        m.cpu_write(0x8000, 0x00);
197        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
198    }
199
200    #[test]
201    fn m179_save_state_round_trip() {
202        let mut m = Hengedianzi179::new(synth_prg_32k(8), &[]).unwrap();
203        m.cpu_write(0x5000, 8); // PRG 4
204        m.cpu_write(0x8000, 0x01); // horizontal
205        m.ppu_write(0x0006, 0x12);
206        let blob = m.save_state();
207        let mut m2 = Hengedianzi179::new(synth_prg_32k(8), &[]).unwrap();
208        m2.load_state(&blob).unwrap();
209        assert_eq!(m2.cpu_read(0x8000), 4);
210        assert_eq!(m2.current_mirroring(), Mirroring::Horizontal);
211        assert_eq!(m2.ppu_read(0x0006), 0x12);
212    }
213}