1#![allow(
18 clippy::cast_possible_truncation,
19 clippy::cast_lossless,
20 clippy::missing_const_for_fn,
21 clippy::needless_pass_by_ref_mut,
22 clippy::manual_range_patterns,
23 clippy::match_same_arms,
24 clippy::too_many_arguments
25)]
26
27use crate::cartridge::Mirroring;
28use crate::mapper::{Mapper, MapperCaps, MapperError};
29use alloc::{boxed::Box, vec::Vec};
30use alloc::{format, vec};
31
32const PRG_BANK_8K: usize = 0x2000;
33const CHR_BANK_4K: usize = 0x1000;
34const CHR_BANK_8K: usize = 0x2000;
35const NAMETABLE_SIZE: usize = 0x0400;
36const NAMETABLE_SIZE_U16: u16 = 0x0400;
37
38const MMC2_SECTION_VERSION: u8 = 2;
53
54fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
55 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
56 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
57 let physical = mirroring.physical_bank(table);
58 physical * NAMETABLE_SIZE + local
59}
60
61pub struct Mmc2 {
63 prg_rom: Box<[u8]>,
64 chr_rom: Box<[u8]>,
65 vram: Box<[u8]>,
66 chr_is_ram: bool,
67 prg_bank: u8,
68 chr_lo_fd: u8,
69 chr_lo_fe: u8,
70 chr_hi_fd: u8,
71 chr_hi_fe: u8,
72 latch_lo_fe: bool,
74 latch_hi_fe: bool,
76 mirroring: Mirroring,
77}
78
79impl Mmc2 {
80 pub fn new(
89 prg_rom: Box<[u8]>,
90 chr_rom: Box<[u8]>,
91 mirroring: Mirroring,
92 ) -> Result<Self, MapperError> {
93 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
94 return Err(MapperError::Invalid(format!(
95 "MMC2 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
96 prg_rom.len()
97 )));
98 }
99 let chr_is_ram = chr_rom.is_empty();
100 let chr: Box<[u8]> = if chr_is_ram {
101 vec![0u8; CHR_BANK_8K].into_boxed_slice()
102 } else if chr_rom.len().is_multiple_of(CHR_BANK_4K) {
103 chr_rom
104 } else {
105 return Err(MapperError::Invalid(format!(
106 "MMC2 CHR-ROM size {} is not a multiple of 4 KiB",
107 chr_rom.len()
108 )));
109 };
110 Ok(Self {
111 prg_rom,
112 chr_rom: chr,
113 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
114 chr_is_ram,
115 prg_bank: 0,
116 chr_lo_fd: 0,
117 chr_lo_fe: 0,
118 chr_hi_fd: 0,
119 chr_hi_fe: 0,
120 latch_lo_fe: false,
121 latch_hi_fe: false,
122 mirroring,
123 })
124 }
125
126 fn prg_offset(&self, addr: u16) -> usize {
127 let total_8k = self.prg_rom.len() / PRG_BANK_8K;
128 let last3 = total_8k.saturating_sub(3);
129 let last2 = total_8k.saturating_sub(2);
130 let last1 = total_8k.saturating_sub(1);
131 let bank = match addr & 0xE000 {
132 0x8000 => (self.prg_bank as usize) % total_8k.max(1),
133 0xA000 => last3,
134 0xC000 => last2,
135 _ => last1, };
137 bank * PRG_BANK_8K + ((addr as usize) & 0x1FFF)
138 }
139
140 fn chr_offset(&mut self, addr: u16) -> usize {
141 let addr = (addr & 0x1FFF) as usize;
142 let total_4k = (self.chr_rom.len() / CHR_BANK_4K).max(1);
143 let bank = if addr < CHR_BANK_4K {
144 let b = if self.latch_lo_fe {
145 self.chr_lo_fe
146 } else {
147 self.chr_lo_fd
148 };
149 (b as usize) % total_4k
150 } else {
151 let b = if self.latch_hi_fe {
152 self.chr_hi_fe
153 } else {
154 self.chr_hi_fd
155 };
156 (b as usize) % total_4k
157 };
158 bank * CHR_BANK_4K + (addr & (CHR_BANK_4K - 1))
159 }
160
161 fn update_latch(&mut self, addr: u16) {
165 match addr & 0x3FF8 {
166 0x0FD8 => self.latch_lo_fe = false,
167 0x0FE8 => self.latch_lo_fe = true,
168 0x1FD8 => self.latch_hi_fe = false,
169 0x1FE8 => self.latch_hi_fe = true,
170 _ => {}
171 }
172 }
173}
174
175impl Mapper for Mmc2 {
176 fn chr_reads_are_pure(&self) -> bool {
179 false
180 }
181
182 fn caps(&self) -> MapperCaps {
185 MapperCaps::NONE
186 }
187
188 fn cpu_read(&mut self, addr: u16) -> u8 {
189 match addr {
190 0x8000..=0xFFFF => {
191 let off = self.prg_offset(addr);
192 self.prg_rom[off % self.prg_rom.len()]
193 }
194 _ => 0,
195 }
196 }
197
198 fn cpu_write(&mut self, addr: u16, value: u8) {
199 match addr & 0xF000 {
200 0xA000 => self.prg_bank = value & 0x0F,
201 0xB000 => self.chr_lo_fd = value & 0x1F,
202 0xC000 => self.chr_lo_fe = value & 0x1F,
203 0xD000 => self.chr_hi_fd = value & 0x1F,
204 0xE000 => self.chr_hi_fe = value & 0x1F,
205 0xF000 => {
206 self.mirroring = if value & 1 == 0 {
207 Mirroring::Vertical
208 } else {
209 Mirroring::Horizontal
210 };
211 }
212 _ => {}
213 }
214 }
215
216 fn ppu_read(&mut self, addr: u16) -> u8 {
217 let addr = addr & 0x3FFF;
218 match addr {
219 0x0000..=0x1FFF => {
220 let off = self.chr_offset(addr);
221 let v = self.chr_rom[off % self.chr_rom.len()];
222 self.update_latch(addr);
223 v
224 }
225 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring) % self.vram.len()],
226 _ => 0,
227 }
228 }
229
230 fn ppu_write(&mut self, addr: u16, value: u8) {
231 let addr = addr & 0x3FFF;
232 match addr {
233 0x0000..=0x1FFF => {
234 if self.chr_is_ram {
235 let off = self.chr_offset(addr);
236 let len = self.chr_rom.len();
237 self.chr_rom[off % len] = value;
238 }
239 }
240 0x2000..=0x3EFF => {
241 let off = nametable_offset(addr, self.mirroring) % self.vram.len();
242 self.vram[off] = value;
243 }
244 _ => {}
245 }
246 }
247
248 fn current_mirroring(&self) -> Mirroring {
249 self.mirroring
250 }
251
252 fn save_state(&self) -> Vec<u8> {
253 let mut out = Vec::with_capacity(16 + self.vram.len() + self.ram_block_len());
254 out.push(MMC2_SECTION_VERSION);
255 out.push(self.prg_bank);
256 out.push(self.chr_lo_fd);
257 out.push(self.chr_lo_fe);
258 out.push(self.chr_hi_fd);
259 out.push(self.chr_hi_fe);
260 out.push(u8::from(self.latch_lo_fe));
261 out.push(u8::from(self.latch_hi_fe));
262 out.push(self.mirroring as u8);
263 out.extend_from_slice(&self.vram);
264 if self.chr_is_ram {
266 out.extend_from_slice(&self.chr_rom);
267 }
268 out
269 }
270
271 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
272 let version = data.first().copied().unwrap_or(0);
273 if version != MMC2_SECTION_VERSION {
277 return Err(MapperError::UnsupportedVersion(version));
278 }
279 let ram_len = self.ram_block_len();
280 let core_len = 9 + self.vram.len();
282 let expected = core_len + ram_len;
283 if data.len() != expected {
284 return Err(MapperError::WrongLength {
285 expected,
286 got: data.len(),
287 });
288 }
289 self.prg_bank = data[1];
290 self.chr_lo_fd = data[2];
291 self.chr_lo_fe = data[3];
292 self.chr_hi_fd = data[4];
293 self.chr_hi_fe = data[5];
294 self.latch_lo_fe = data[6] != 0;
295 self.latch_hi_fe = data[7] != 0;
296 self.mirroring = match data[8] {
297 0 => Mirroring::Horizontal,
298 1 => Mirroring::Vertical,
299 2 => Mirroring::SingleScreenA,
300 3 => Mirroring::SingleScreenB,
301 4 => Mirroring::FourScreen,
302 5 => Mirroring::MapperControlled,
303 other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
304 };
305 self.vram.copy_from_slice(&data[9..core_len]);
306 if self.chr_is_ram {
307 self.chr_rom.copy_from_slice(&data[core_len..]);
308 }
309 Ok(())
310 }
311}
312
313impl Mmc2 {
314 fn ram_block_len(&self) -> usize {
318 if self.chr_is_ram {
319 self.chr_rom.len()
320 } else {
321 0
322 }
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 fn synth(banks_8k: usize) -> Box<[u8]> {
331 let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
332 for b in 0..banks_8k {
333 v[b * PRG_BANK_8K] = b as u8;
334 }
335 v.into_boxed_slice()
336 }
337
338 fn synth_chr_4k(banks: usize) -> Box<[u8]> {
339 let mut v = vec![0u8; banks * CHR_BANK_4K];
340 for b in 0..banks {
341 v[b * CHR_BANK_4K] = b as u8;
342 }
343 v.into_boxed_slice()
344 }
345
346 #[test]
347 fn mmc2_swap_window_via_latch() {
348 let mut m = Mmc2::new(synth(8), synth_chr_4k(4), Mirroring::Vertical).unwrap();
349 m.chr_lo_fd = 0;
350 m.chr_lo_fe = 1;
351 assert_eq!(m.ppu_read(0x0000), 0);
353 let _ = m.ppu_read(0x0FE8);
355 assert_eq!(m.ppu_read(0x0000), 1);
356 }
357
358 #[test]
362 fn mmc2_save_state_carries_chr_ram() {
363 let mut m = Mmc2::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
364 m.chr_rom[0x0000] = 0x11;
365 m.chr_rom[0x1FFF] = 0x22;
366 let blob = m.save_state();
367 let mut m2 = Mmc2::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
368 m2.load_state(&blob).expect("round-trip");
369 assert_eq!(m2.chr_rom[0x0000], 0x11);
370 assert_eq!(m2.chr_rom[0x1FFF], 0x22);
371 }
372
373 #[test]
376 fn mmc2_v1_blob_is_refused() {
377 let mut m = Mmc2::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
378 m.cpu_write(0xA000, 3);
379 let core_len = 9 + m.vram.len();
380 let mut v1 = m.save_state()[..core_len].to_vec();
381 v1[0] = 1;
382 let mut m2 = Mmc2::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
383 assert!(matches!(
384 m2.load_state(&v1),
385 Err(MapperError::UnsupportedVersion(1))
386 ));
387 }
388
389 #[test]
391 fn mmc2_truncated_chr_ram_tail_is_rejected() {
392 let m = Mmc2::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
393 let blob = m.save_state();
394 let mut m2 = Mmc2::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
395 let err = m2
396 .load_state(&blob[..blob.len() - 1])
397 .expect_err("a truncated v2 blob must be rejected");
398 assert!(matches!(err, MapperError::WrongLength { .. }), "{err:?}");
399 }
400}