Skip to main content

rustynes_mappers/
m156_daou156.rs

1//! Daou Infosys (mapper 156) -- Korean licensed boards, e.g. Metal Force.
2//!
3//! Separate register windows for PRG and for each of the eight 1 KiB CHR
4//! slots, plus a runtime single-screen mirroring control -- unusually
5//! fine-grained for a board with no IRQ. The CHR registers are 16 bits wide,
6//! split across a low and a high write, which is why each slot occupies two
7//! addresses.
8//!
9//! A best-effort (Tier-2) board: register-decode correctness verified against
10//! the `GeraNES` reference emulator (cross-referenced, not copied)
11//! and the nesdev wiki, with no commercial-oracle ROM in the tree. Banking math
12//! is direct slice indexing and every bank select wraps with `% count`, so a
13//! register write can never index out of bounds -- required for the `#![no_std]`
14//! chip stack, which cannot afford a panic on a register access.
15//!
16//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
17//! and `docs/mappers.md` §Mapper coverage matrix.
18
19use crate::cartridge::Mirroring;
20use crate::mapper::{Mapper, MapperCaps, MapperError};
21use alloc::{boxed::Box, vec::Vec};
22use alloc::{format, vec};
23
24const PRG_BANK_16K: usize = 0x4000;
25const CHR_BANK_1K: usize = 0x0400;
26const NAMETABLE_SIZE: usize = 0x0400;
27const NAMETABLE_SIZE_U16: u16 = 0x0400;
28
29/// v2 (v2.7.2) appends the 8 KiB RAM. Only v2 loads since v2.9.8 (ADR 0042);
30/// a v1 blob used to load with it zeroed.
31const SAVE_STATE_VERSION: u8 = 2;
32const WRAM_SIZE: usize = 0x2000;
33
34// ---------------------------------------------------------------------------
35// Shared nametable helper (mirrors the one in the other simple-mapper modules).
36// ---------------------------------------------------------------------------
37
38const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
39    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
40    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
41    let physical = mirroring.physical_bank(table);
42    physical * NAMETABLE_SIZE + local
43}
44
45/// Mapper 156 (DIS23C01 DAOU).
46pub struct Daou156 {
47    prg_rom: Box<[u8]>,
48    chr_rom: Box<[u8]>,
49    vram: Box<[u8]>,
50    prg_bank: u8,
51    // 8 low nibbles + 8 high nibbles, composed into a 1 KiB bank per slot.
52    chr_lo: [u8; 8],
53    chr_hi: [u8; 8],
54    mirroring: Mirroring,
55    /// "CPU $6000-$7FFF: 8 KiB RAM" (`nesdev_wiki/INES_Mapper_156.xhtml`).
56    /// Absent before v2.7.2.
57    wram: Box<[u8]>,
58    /// Whether the header marks it battery-backed (then it is the save).
59    battery: bool,
60}
61
62impl Daou156 {
63    /// Construct a new mapper 156 board.
64    ///
65    /// # Errors
66    ///
67    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
68    /// 16 KiB or CHR-ROM is empty / not a multiple of 1 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_16K) {
75            return Err(MapperError::Invalid(format!(
76                "mapper 156 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
77                prg_rom.len()
78            )));
79        }
80        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_1K) {
81            return Err(MapperError::Invalid(format!(
82                "mapper 156 CHR-ROM size {} is not a non-zero multiple of 1 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_lo: [0; 8],
92            chr_hi: [0; 8],
93            // DAOU/DIS23C01 powers on single-screen (nametable A) per Mesen2
94            // InitMapper; the $C014 register flips it to H/V at runtime.
95            mirroring: Mirroring::SingleScreenA,
96            wram: vec![0u8; WRAM_SIZE].into_boxed_slice(),
97            battery: false,
98        })
99    }
100
101    /// Mark the RAM battery-backed (from the header), making it the save.
102    #[must_use]
103    pub const fn with_battery(mut self, battery: bool) -> Self {
104        self.battery = battery;
105        self
106    }
107
108    fn read_chr(&self, addr: u16) -> u8 {
109        let count1k = (self.chr_rom.len() / CHR_BANK_1K).max(1);
110        let slot = (addr as usize >> 10) & 0x07;
111        let bank = ((self.chr_lo[slot] as usize) | ((self.chr_hi[slot] as usize) << 8)) % count1k;
112        self.chr_rom[bank * CHR_BANK_1K + (addr as usize & 0x03FF)]
113    }
114}
115
116impl Mapper for Daou156 {
117    fn caps(&self) -> MapperCaps {
118        MapperCaps::NONE
119    }
120
121    fn sram(&self) -> &[u8] {
122        if self.battery { &self.wram } else { &[] }
123    }
124    fn sram_mut(&mut self) -> &mut [u8] {
125        if self.battery {
126            &mut self.wram
127        } else {
128            &mut []
129        }
130    }
131
132    /// The RAM is always present, so `$6000-$7FFF` stays mapped.
133    fn cpu_read_unmapped(&self, addr: u16) -> bool {
134        (0x4020..=0x5FFF).contains(&addr)
135    }
136
137    fn cpu_read(&mut self, addr: u16) -> u8 {
138        let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
139        match addr {
140            0x6000..=0x7FFF => self.wram[usize::from(addr - 0x6000)],
141            0x8000..=0xBFFF => {
142                let bank = (self.prg_bank as usize) % count;
143                self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
144            }
145            0xC000..=0xFFFF => {
146                let last = count - 1;
147                self.prg_rom[last * PRG_BANK_16K + (addr as usize & 0x3FFF)]
148            }
149            _ => 0,
150        }
151    }
152
153    fn cpu_write(&mut self, addr: u16, value: u8) {
154        match addr {
155            0x6000..=0x7FFF => self.wram[usize::from(addr - 0x6000)] = value,
156            // $C000-$C00F: 16 CHR-bank-nibble registers. Mesen2 decodes the
157            // 1 KiB slot as (addr & 0x03) + (addr >= 0xC008 ? 4 : 0) and selects
158            // the low/high nibble array by bit 2 (0x04) — NOT a flat lo[0..8] /
159            // hi[0..8] split. The old flat decode wrote the wrong slot's nibble,
160            // so CHR banks resolved to garbage → blank/garbled boot.
161            0xC000..=0xC00F => {
162                let slot = ((addr & 0x03) + if addr >= 0xC008 { 4 } else { 0 }) as usize;
163                if addr & 0x04 != 0 {
164                    self.chr_hi[slot] = value;
165                } else {
166                    self.chr_lo[slot] = value;
167                }
168            }
169            0xC010 => self.prg_bank = value,
170            // $C014: 0 = vertical, 1 = horizontal (Mesen2). The old code mapped
171            // this to a single-screen A/B toggle, which never matched the game's
172            // expected nametable layout.
173            0xC014 => {
174                self.mirroring = if value & 0x01 != 0 {
175                    Mirroring::Horizontal
176                } else {
177                    Mirroring::Vertical
178                };
179            }
180            _ => {}
181        }
182    }
183
184    fn ppu_read(&mut self, addr: u16) -> u8 {
185        let addr = addr & 0x3FFF;
186        match addr {
187            0x0000..=0x1FFF => self.read_chr(addr),
188            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
189            _ => 0,
190        }
191    }
192
193    fn ppu_write(&mut self, addr: u16, value: u8) {
194        let addr = addr & 0x3FFF;
195        if (0x2000..=0x3EFF).contains(&addr) {
196            let off = nametable_offset(addr, self.current_mirroring());
197            self.vram[off] = value;
198        }
199    }
200
201    fn current_mirroring(&self) -> Mirroring {
202        self.mirroring
203    }
204
205    fn save_state(&self) -> Vec<u8> {
206        let mut out = Vec::with_capacity(19 + self.vram.len());
207        out.push(SAVE_STATE_VERSION);
208        out.push(self.prg_bank);
209        out.extend_from_slice(&self.chr_lo);
210        out.extend_from_slice(&self.chr_hi);
211        out.push(match self.mirroring {
212            Mirroring::Horizontal => 0,
213            Mirroring::Vertical => 1,
214            Mirroring::SingleScreenB => 2,
215            _ => 3, // SingleScreenA (power-on default) + any other
216        });
217        out.extend_from_slice(&self.vram);
218        out.extend_from_slice(&self.wram);
219        out
220    }
221
222    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
223        let version = *data.first().ok_or(MapperError::WrongLength {
224            expected: 1,
225            got: 0,
226        })?;
227        if version != SAVE_STATE_VERSION {
228            return Err(MapperError::UnsupportedVersion(version));
229        }
230        let wram_len = self.wram.len();
231        let expected = 19 + self.vram.len() + wram_len;
232        if data.len() != expected {
233            return Err(MapperError::WrongLength {
234                expected,
235                got: data.len(),
236            });
237        }
238        self.prg_bank = data[1];
239        self.chr_lo.copy_from_slice(&data[2..10]);
240        self.chr_hi.copy_from_slice(&data[10..18]);
241        self.mirroring = match data[18] {
242            0 => Mirroring::Horizontal,
243            1 => Mirroring::Vertical,
244            2 => Mirroring::SingleScreenB,
245            _ => Mirroring::SingleScreenA,
246        };
247        self.vram.copy_from_slice(&data[19..19 + self.vram.len()]);
248        let cur = 19 + self.vram.len();
249        self.wram.copy_from_slice(&data[cur..cur + wram_len]);
250        Ok(())
251    }
252}
253
254#[cfg(test)]
255#[allow(clippy::cast_possible_truncation)]
256mod tests {
257    use super::*;
258
259    fn synth_prg_16k(banks: usize) -> Box<[u8]> {
260        let mut v = vec![0xFFu8; banks * PRG_BANK_16K];
261        for b in 0..banks {
262            v[b * PRG_BANK_16K] = b as u8;
263        }
264        v.into_boxed_slice()
265    }
266
267    fn synth_chr_1k(banks: usize) -> Box<[u8]> {
268        let mut v = vec![0u8; banks * CHR_BANK_1K];
269        for b in 0..banks {
270            v[b * CHR_BANK_1K] = b as u8;
271        }
272        v.into_boxed_slice()
273    }
274
275    #[test]
276    fn m156_chr_compose_prg_and_mirroring() {
277        let mut m = Daou156::new(synth_prg_16k(8), synth_chr_1k(32), Mirroring::Vertical).unwrap();
278        // Power-on mirroring is single-screen A (Mesen2 InitMapper).
279        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
280        // PRG $C010 -> bank 3.
281        m.cpu_write(0xC010, 3);
282        assert_eq!(m.cpu_read(0x8000), 3);
283        assert_eq!(m.cpu_read(0xC000), 7); // fixed last
284        // CHR slot 0: low = 5 ($C000), high = 0 -> bank 5.
285        m.cpu_write(0xC000, 5);
286        assert_eq!(m.ppu_read(0x0000), 5);
287        // High nibble of slot 0 lives at $C004 (bit 2 selects the high array):
288        // low 5 | (high 1 << 8) = 0x105, wraps mod 32 -> 5.
289        m.cpu_write(0xC004, 1);
290        assert_eq!(m.ppu_read(0x0000), (0x105usize % 32) as u8);
291        // Mirroring $C014: 1 = horizontal, 0 = vertical.
292        m.cpu_write(0xC014, 1);
293        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
294        m.cpu_write(0xC014, 0);
295        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
296    }
297
298    #[test]
299    fn m156_save_state_round_trip() {
300        let mut m = Daou156::new(synth_prg_16k(8), synth_chr_1k(32), Mirroring::Vertical).unwrap();
301        m.cpu_write(0xC010, 2);
302        m.cpu_write(0xC001, 4);
303        m.cpu_write(0xC014, 1);
304        let blob = m.save_state();
305        let mut m2 = Daou156::new(synth_prg_16k(8), synth_chr_1k(32), Mirroring::Vertical).unwrap();
306        m2.load_state(&blob).unwrap();
307        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
308        assert_eq!(m2.ppu_read(0x0400), m.ppu_read(0x0400));
309        assert_eq!(m2.current_mirroring(), Mirroring::Horizontal);
310    }
311}