rustynes_mappers/
m179_hengedianzi.rs1#![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
43const 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
54pub 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 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 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 m.cpu_write(0x5000, 6);
192 assert_eq!(m.cpu_read(0x8000), 3);
193 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); m.cpu_write(0x8000, 0x01); 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}