Skip to main content

rustynes_mappers/
m177_hengedianzi.rs

1//! Hengedianzi (mapper 177) -- Chinese unlicensed board.
2//!
3//! A 32 KiB PRG bank select plus a mirroring bit, both in one write-anywhere
4//! register at `$8000-$FFFF`. Its sibling mapper 179
5//! (`m179_hengedianzi.rs`) splits the same two fields across two windows.
6//!
7//! A best-effort (Tier-2) board: register-decode correctness verified against
8//! the reference emulators (`Mesen2`, `GeraNES`) and the nesdev wiki, with no
9//! commercial-oracle ROM in the tree. Banking math is direct slice indexing and
10//! every bank select wraps with `% count`, so a register write can never index
11//! out of bounds -- required for the `#![no_std]` chip stack, which cannot
12//! afford a panic on a register access.
13//!
14//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
15//! and `docs/mappers.md` §Mapper coverage matrix.
16
17#![allow(
18    clippy::bool_to_int_with_if,
19    clippy::cast_lossless,
20    clippy::cast_possible_truncation,
21    clippy::doc_markdown,
22    clippy::match_same_arms,
23    clippy::missing_const_for_fn,
24    clippy::similar_names,
25    clippy::struct_excessive_bools,
26    clippy::too_many_lines,
27    clippy::unreadable_literal
28)]
29
30use crate::cartridge::Mirroring;
31use crate::mapper::{Mapper, MapperCaps, MapperError};
32use alloc::{boxed::Box, vec::Vec};
33use alloc::{format, vec};
34
35const PRG_BANK_32K: usize = 0x8000;
36const CHR_BANK_8K: usize = 0x2000;
37const NAMETABLE_SIZE: usize = 0x0400;
38const NAMETABLE_SIZE_U16: u16 = 0x0400;
39
40/// v2 (v2.7.2) appends the 8 KiB WRAM. Only v2 loads since v2.9.8 (ADR 0042);
41/// a v1 blob used to load with it zeroed.
42const SAVE_STATE_VERSION: u8 = 2;
43const WRAM_SIZE: usize = 0x2000;
44
45// ---------------------------------------------------------------------------
46// Shared nametable helper (mirrors the one in the other simple-mapper modules).
47// ---------------------------------------------------------------------------
48
49const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
50    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
51    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
52    let physical = mirroring.physical_bank(table);
53    physical * NAMETABLE_SIZE + local
54}
55
56/// Mapper 177 (Hengedianzi).
57pub struct Hengedianzi177 {
58    prg_rom: Box<[u8]>,
59    chr_ram: Box<[u8]>,
60    vram: Box<[u8]>,
61    prg_bank: u8,
62    horizontal_mirroring: bool,
63    /// "8 KiB of battery-backed WRAM at CPU $6000-$7FFF"
64    /// (`nesdev_wiki/INES_Mapper_177.xhtml`). Absent before v2.7.2: writes
65    /// vanished and reads returned a made-up `$00`, which the open-bus fix
66    /// turned into a floating bus -- equally wrong for a board that has RAM.
67    wram: Box<[u8]>,
68}
69
70impl Hengedianzi177 {
71    /// Construct a new mapper 177 board.
72    ///
73    /// # Errors
74    ///
75    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
76    /// 32 KiB.
77    pub fn new(prg_rom: Box<[u8]>, _chr_rom: &[u8]) -> Result<Self, MapperError> {
78        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
79            return Err(MapperError::Invalid(format!(
80                "mapper 177 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
81                prg_rom.len()
82            )));
83        }
84        Ok(Self {
85            prg_rom,
86            chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
87            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
88            prg_bank: 0,
89            horizontal_mirroring: false,
90            wram: vec![0u8; WRAM_SIZE].into_boxed_slice(),
91        })
92    }
93}
94
95impl Mapper for Hengedianzi177 {
96    fn caps(&self) -> MapperCaps {
97        MapperCaps::NONE
98    }
99
100    // The WRAM is battery-backed on every board of this type.
101    fn sram(&self) -> &[u8] {
102        &self.wram
103    }
104    fn sram_mut(&mut self) -> &mut [u8] {
105        &mut self.wram
106    }
107
108    fn cpu_read(&mut self, addr: u16) -> u8 {
109        if (0x6000..=0x7FFF).contains(&addr) {
110            return self.wram[usize::from(addr - 0x6000)];
111        }
112        if (0x8000..=0xFFFF).contains(&addr) {
113            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
114            let bank = (self.prg_bank as usize) % count;
115            self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
116        } else {
117            0
118        }
119    }
120
121    fn cpu_write(&mut self, addr: u16, value: u8) {
122        if (0x6000..=0x7FFF).contains(&addr) {
123            self.wram[usize::from(addr - 0x6000)] = value;
124            return;
125        }
126        if (0x8000..=0xFFFF).contains(&addr) {
127            // $8000-$FFFF: `..MP PPPP` — PRG bank is bits 0-4 (5 bits), mirroring
128            // is bit 5. The old code latched all 8 bits as the bank, so a write
129            // that flips the mirroring bit (e.g. $20) selected bank 32 and the
130            // reset vector read garbage → blank boot.
131            self.prg_bank = value & 0x1F;
132            self.horizontal_mirroring = (value & 0x20) != 0;
133        }
134    }
135
136    fn ppu_read(&mut self, addr: u16) -> u8 {
137        let addr = addr & 0x3FFF;
138        match addr {
139            0x0000..=0x1FFF => self.chr_ram[addr as usize],
140            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
141            _ => 0,
142        }
143    }
144
145    fn ppu_write(&mut self, addr: u16, value: u8) {
146        let addr = addr & 0x3FFF;
147        match addr {
148            0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
149            0x2000..=0x3EFF => {
150                let off = nametable_offset(addr, self.current_mirroring());
151                self.vram[off] = value;
152            }
153            _ => {}
154        }
155    }
156
157    fn current_mirroring(&self) -> Mirroring {
158        if self.horizontal_mirroring {
159            Mirroring::Horizontal
160        } else {
161            Mirroring::Vertical
162        }
163    }
164
165    fn save_state(&self) -> Vec<u8> {
166        let mut out = Vec::with_capacity(3 + self.vram.len() + self.chr_ram.len());
167        out.push(SAVE_STATE_VERSION);
168        out.push(self.prg_bank);
169        out.push(u8::from(self.horizontal_mirroring));
170        out.extend_from_slice(&self.vram);
171        out.extend_from_slice(&self.chr_ram);
172        out.extend_from_slice(&self.wram);
173        out
174    }
175
176    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
177        let version = *data.first().ok_or(MapperError::WrongLength {
178            expected: 1,
179            got: 0,
180        })?;
181        if version != SAVE_STATE_VERSION {
182            return Err(MapperError::UnsupportedVersion(version));
183        }
184        let wram_len = self.wram.len();
185        let expected = 3 + self.vram.len() + self.chr_ram.len() + wram_len;
186        if data.len() != expected {
187            return Err(MapperError::WrongLength {
188                expected,
189                got: data.len(),
190            });
191        }
192        self.prg_bank = data[1];
193        self.horizontal_mirroring = data[2] != 0;
194        let mut cursor = 3;
195        self.vram
196            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
197        cursor += self.vram.len();
198        self.chr_ram
199            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
200        cursor += self.chr_ram.len();
201        self.wram.copy_from_slice(&data[cursor..cursor + wram_len]);
202        Ok(())
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    fn synth_prg_32k(banks: usize) -> Box<[u8]> {
211        let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
212        for b in 0..banks {
213            v[b * PRG_BANK_32K] = b as u8;
214        }
215        v.into_boxed_slice()
216    }
217
218    #[test]
219    fn m177_prg_and_mirroring() {
220        let mut m = Hengedianzi177::new(synth_prg_32k(8), &[]).unwrap();
221        // value 0b0010_0011 (0x23): PRG = 0x23 % 8 = 3; bit5 set -> horizontal.
222        m.cpu_write(0x8000, 0b0010_0011);
223        assert_eq!(m.cpu_read(0x8000), 3);
224        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
225        // Clear bit 5 -> vertical.
226        m.cpu_write(0x8000, 0b0000_0010);
227        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
228    }
229
230    #[test]
231    fn m177_save_state_round_trip() {
232        let mut m = Hengedianzi177::new(synth_prg_32k(8), &[]).unwrap();
233        m.cpu_write(0x8000, 0b0010_0101); // PRG 5, horizontal
234        m.ppu_write(0x0004, 0xEE);
235        let blob = m.save_state();
236        let mut m2 = Hengedianzi177::new(synth_prg_32k(8), &[]).unwrap();
237        m2.load_state(&blob).unwrap();
238        assert_eq!(m2.cpu_read(0x8000), 5);
239        assert_eq!(m2.current_mirroring(), Mirroring::Horizontal);
240        assert_eq!(m2.ppu_read(0x0004), 0xEE);
241    }
242}