rustynes_mappers/
m177_hengedianzi.rs1#![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
40const SAVE_STATE_VERSION: u8 = 2;
43const WRAM_SIZE: usize = 0x2000;
44
45const 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
56pub 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 wram: Box<[u8]>,
68}
69
70impl Hengedianzi177 {
71 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 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 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 m.cpu_write(0x8000, 0b0010_0011);
223 assert_eq!(m.cpu_read(0x8000), 3);
224 assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
225 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); 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}