1#![allow(
35 clippy::cast_possible_truncation,
36 clippy::cast_lossless,
37 clippy::missing_const_for_fn,
38 clippy::doc_markdown
39)]
40
41use crate::cartridge::Mirroring;
42use crate::mapper::{Mapper, MapperCaps, MapperError};
43use alloc::{boxed::Box, vec::Vec};
44use alloc::{format, vec};
45
46const PRG_BANK_16K: usize = 0x4000;
47const CHR_BANK_8K: usize = 0x2000;
48const NAMETABLE_SIZE: usize = 0x0400;
49const NAMETABLE_SIZE_U16: u16 = 0x0400;
50
51const SAVE_STATE_VERSION: u8 = 1;
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum M78Variant {
56 HolyDiver,
58 CosmoCarrier,
60}
61
62pub struct M78 {
64 prg_rom: Box<[u8]>,
65 chr: Box<[u8]>,
66 vram: Box<[u8]>,
67 chr_is_ram: bool,
68
69 prg_bank: u8,
70 chr_bank: u8,
71 mirroring: Mirroring,
72 variant: M78Variant,
73}
74
75impl M78 {
76 pub fn new(
86 prg_rom: Box<[u8]>,
87 chr_rom: Box<[u8]>,
88 variant: M78Variant,
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 "Mapper-78 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_8K) {
100 chr_rom
101 } else {
102 return Err(MapperError::Invalid(format!(
103 "Mapper-78 CHR-ROM size {} is not a multiple of 8 KiB",
104 chr_rom.len()
105 )));
106 };
107 let mirroring = match variant {
109 M78Variant::HolyDiver => Mirroring::Horizontal,
110 M78Variant::CosmoCarrier => Mirroring::SingleScreenA,
111 };
112 Ok(Self {
113 prg_rom,
114 chr,
115 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
116 chr_is_ram,
117 prg_bank: 0,
118 chr_bank: 0,
119 mirroring,
120 variant,
121 })
122 }
123
124 fn prg_offset(&self, addr: u16) -> usize {
125 let total = (self.prg_rom.len() / PRG_BANK_16K).max(1);
126 let last = total - 1;
127 let bank = match addr {
128 0x8000..=0xBFFF => (self.prg_bank as usize) % total,
129 _ => last,
130 };
131 bank * PRG_BANK_16K + (addr as usize & 0x3FFF)
132 }
133
134 fn chr_offset(&self, addr: u16) -> usize {
135 let addr = (addr & 0x1FFF) as usize;
136 let total_8k = (self.chr.len() / CHR_BANK_8K).max(1);
137 let bank = (self.chr_bank as usize) % total_8k;
138 bank * CHR_BANK_8K + addr
139 }
140
141 fn nametable_offset(&self, addr: u16) -> usize {
142 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
143 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
144 let physical = self.mirroring.physical_bank(table);
145 physical * NAMETABLE_SIZE + local
146 }
147}
148
149impl Mapper for M78 {
150 fn caps(&self) -> MapperCaps {
153 MapperCaps::NONE
154 }
155
156 fn cpu_read(&mut self, addr: u16) -> u8 {
157 match addr {
158 0x8000..=0xFFFF => {
159 let off = self.prg_offset(addr);
160 self.prg_rom[off % self.prg_rom.len()]
161 }
162 _ => 0,
163 }
164 }
165
166 fn cpu_write(&mut self, addr: u16, value: u8) {
167 if let 0x8000..=0xFFFF = addr {
168 self.prg_bank = value & 0x07;
169 self.chr_bank = (value >> 4) & 0x0F;
170 let mbit = (value & 0x08) != 0;
171 self.mirroring = match self.variant {
172 M78Variant::HolyDiver => {
173 if mbit {
174 Mirroring::Vertical
175 } else {
176 Mirroring::Horizontal
177 }
178 }
179 M78Variant::CosmoCarrier => {
180 if mbit {
181 Mirroring::SingleScreenB
182 } else {
183 Mirroring::SingleScreenA
184 }
185 }
186 };
187 }
188 }
189
190 fn ppu_read(&mut self, addr: u16) -> u8 {
191 let addr = addr & 0x3FFF;
192 match addr {
193 0x0000..=0x1FFF => {
194 let off = self.chr_offset(addr);
195 self.chr[off % self.chr.len()]
196 }
197 0x2000..=0x3EFF => self.vram[self.nametable_offset(addr) % self.vram.len()],
198 _ => 0,
199 }
200 }
201
202 fn ppu_write(&mut self, addr: u16, value: u8) {
203 let addr = addr & 0x3FFF;
204 match addr {
205 0x0000..=0x1FFF => {
206 if self.chr_is_ram {
207 let off = self.chr_offset(addr);
208 let len = self.chr.len();
209 self.chr[off % len] = value;
210 }
211 }
212 0x2000..=0x3EFF => {
213 let off = self.nametable_offset(addr) % self.vram.len();
214 self.vram[off] = value;
215 }
216 _ => {}
217 }
218 }
219
220 fn nametable_address(&self, addr: u16) -> u16 {
221 let off = self.nametable_offset(addr);
222 u16::try_from(off & 0x07FF).unwrap_or(0)
223 }
224
225 fn current_mirroring(&self) -> Mirroring {
226 self.mirroring
227 }
228
229 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
230 let mut info = crate::mapper::MapperDebugInfo {
231 mapper_id: 78,
232 name: match self.variant {
233 M78Variant::HolyDiver => "Mapper 78 (Holy Diver)".into(),
234 M78Variant::CosmoCarrier => "Mapper 78 (Cosmo Carrier)".into(),
235 },
236 mirroring: crate::mapper::mirroring_name(self.mirroring),
237 ..Default::default()
238 };
239 info.prg_banks
240 .push(("PRG".into(), format!("{:#04x}", self.prg_bank)));
241 info.chr_banks
242 .push(("CHR".into(), format!("{:#04x}", self.chr_bank)));
243 info
244 }
245
246 fn save_state(&self) -> Vec<u8> {
247 let mut out = Vec::with_capacity(
248 8 + self.vram.len() + if self.chr_is_ram { self.chr.len() } else { 0 },
249 );
250 out.push(SAVE_STATE_VERSION);
251 out.push(self.prg_bank);
252 out.push(self.chr_bank);
253 out.push(self.mirroring as u8);
254 out.push(match self.variant {
255 M78Variant::HolyDiver => 0,
256 M78Variant::CosmoCarrier => 1,
257 });
258 out.extend_from_slice(&self.vram);
259 if self.chr_is_ram {
260 out.extend_from_slice(&self.chr);
261 }
262 out
263 }
264
265 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
266 let chr_part = if self.chr_is_ram { self.chr.len() } else { 0 };
267 let scalar_len = 5;
269 let expected = scalar_len + self.vram.len() + chr_part;
270 if data.len() != expected {
271 return Err(MapperError::WrongLength {
272 expected,
273 got: data.len(),
274 });
275 }
276 if data[0] != SAVE_STATE_VERSION {
277 return Err(MapperError::UnsupportedVersion(data[0]));
278 }
279 self.prg_bank = data[1];
280 self.chr_bank = data[2];
281 self.mirroring = match data[3] {
282 0 => Mirroring::Horizontal,
283 1 => Mirroring::Vertical,
284 2 => Mirroring::SingleScreenA,
285 3 => Mirroring::SingleScreenB,
286 4 => Mirroring::FourScreen,
287 5 => Mirroring::MapperControlled,
288 other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
289 };
290 self.variant = match data[4] {
291 0 => M78Variant::HolyDiver,
292 1 => M78Variant::CosmoCarrier,
293 other => return Err(MapperError::Invalid(format!("variant {other}"))),
294 };
295 let mut c = 5usize;
296 self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
297 c += self.vram.len();
298 if self.chr_is_ram {
299 self.chr.copy_from_slice(&data[c..c + self.chr.len()]);
300 }
301 Ok(())
302 }
303}
304
305#[cfg(test)]
306#[allow(clippy::cast_possible_truncation)]
307mod tests {
308 use super::*;
309
310 fn synth_prg(banks_16k: usize) -> Box<[u8]> {
311 let mut v = vec![0u8; banks_16k * PRG_BANK_16K];
312 for b in 0..banks_16k {
313 v[b * PRG_BANK_16K] = b as u8;
314 }
315 v.into_boxed_slice()
316 }
317
318 fn synth_chr(banks_8k: usize) -> Box<[u8]> {
319 let mut v = vec![0u8; banks_8k * CHR_BANK_8K];
320 for b in 0..banks_8k {
321 v[b * CHR_BANK_8K] = b as u8;
322 }
323 v.into_boxed_slice()
324 }
325
326 fn fresh_hd() -> M78 {
327 M78::new(synth_prg(8), synth_chr(16), M78Variant::HolyDiver).unwrap()
328 }
329
330 fn fresh_cc() -> M78 {
331 M78::new(synth_prg(8), synth_chr(16), M78Variant::CosmoCarrier).unwrap()
332 }
333
334 #[test]
335 fn prg_low_bank_switch_high_fixed() {
336 let mut m = fresh_hd();
337 assert_eq!(m.cpu_read(0x8000), 0);
338 assert_eq!(m.cpu_read(0xC000), 7); m.cpu_write(0x8000, 0x05); assert_eq!(m.cpu_read(0x8000), 5);
341 assert_eq!(m.cpu_read(0xC000), 7);
342 }
343
344 #[test]
345 fn chr_bank_switch_high_nibble() {
346 let mut m = fresh_hd();
347 m.cpu_write(0x8000, 0x30); assert_eq!(m.ppu_read(0x0000), 3);
349 }
350
351 #[test]
352 fn holy_diver_mirroring_h_v() {
353 let mut m = fresh_hd();
354 assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
355 m.cpu_write(0x8000, 0x08); assert_eq!(m.current_mirroring(), Mirroring::Vertical);
357 m.cpu_write(0x8000, 0x00);
358 assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
359 }
360
361 #[test]
362 fn cosmo_carrier_mirroring_single_screen() {
363 let mut m = fresh_cc();
364 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
365 m.cpu_write(0x8000, 0x08); assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
367 m.cpu_write(0x8000, 0x00);
368 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
369 }
370
371 #[test]
372 fn no_irq() {
373 let mut m = fresh_hd();
374 for _ in 0..1000 {
375 m.notify_cpu_cycle();
376 m.notify_a12(true);
377 m.notify_a12(false);
378 }
379 assert!(!m.irq_pending());
380 }
381
382 #[test]
383 fn save_state_round_trip() {
384 let mut m = fresh_cc();
385 m.cpu_write(0x8000, 0x3D); m.ppu_write(0x2000, 0x66);
387 let blob = m.save_state();
388 let mut m2 = fresh_cc();
389 m2.load_state(&blob).unwrap();
390 assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
391 assert_eq!(m.ppu_read(0x0000), m2.ppu_read(0x0000));
392 assert_eq!(m.current_mirroring(), m2.current_mirroring());
393 assert_eq!(m.ppu_read(0x2000), m2.ppu_read(0x2000));
394 }
395}