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