1#![allow(
25 clippy::cast_possible_truncation,
26 clippy::cast_lossless,
27 clippy::match_same_arms,
28 clippy::doc_markdown,
29 clippy::similar_names,
30 clippy::too_many_lines,
31 clippy::missing_const_for_fn,
32 clippy::struct_excessive_bools,
33 clippy::bool_to_int_with_if,
34 clippy::unreadable_literal
35)]
36
37use crate::a12_filter::A12RiseFilter;
38use crate::cartridge::Mirroring;
39use crate::mapper::{Mapper, MapperCaps, MapperError};
40use alloc::{boxed::Box, format, vec, vec::Vec};
41
42const PRG_BANK_8K: usize = 0x2000;
43const CHR_BANK_1K: usize = 0x0400;
44const NAMETABLE_SIZE: usize = 0x0400;
45const NAMETABLE_SIZE_U16: u16 = 0x0400;
46
47const SAVE_STATE_VERSION: u8 = 2;
48
49const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
54 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
55 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
56 let physical = mirroring.physical_bank(table);
57 physical * NAMETABLE_SIZE + local
58}
59
60const fn mirroring_to_byte(m: Mirroring) -> u8 {
61 match m {
62 Mirroring::Horizontal => 0,
63 Mirroring::Vertical => 1,
64 Mirroring::SingleScreenA => 2,
65 Mirroring::SingleScreenB => 3,
66 Mirroring::FourScreen => 4,
67 Mirroring::MapperControlled => 5,
68 }
69}
70
71const fn byte_to_mirroring(b: u8, fallback: Mirroring) -> Mirroring {
72 match b {
73 0 => Mirroring::Horizontal,
74 1 => Mirroring::Vertical,
75 2 => Mirroring::SingleScreenA,
76 3 => Mirroring::SingleScreenB,
77 4 => Mirroring::FourScreen,
78 5 => Mirroring::MapperControlled,
79 _ => fallback,
80 }
81}
82
83fn check_prg(prg: &[u8], id: u16) -> Result<(), MapperError> {
85 if prg.is_empty() || !prg.len().is_multiple_of(PRG_BANK_8K) {
86 return Err(MapperError::Invalid(format!(
87 "mapper {id} PRG-ROM size {} is not a non-zero multiple of 8 KiB",
88 prg.len()
89 )));
90 }
91 Ok(())
92}
93
94pub struct Coolboy {
96 prg_rom: Box<[u8]>,
97 chr: Box<[u8]>,
98 chr_is_ram: bool,
99 vram: Box<[u8]>,
100 mirroring: Mirroring,
101 prg_count_8k: usize,
102 chr_count_1k: usize,
103 regs: [u8; 8],
104 bank_select: u8,
105 prg_mode: bool,
106 chr_mode: bool,
107 irq_counter: u8,
108 irq_latch: u8,
109 irq_reload: bool,
110 irq_enabled: bool,
111 irq_pending: bool,
112 a12: A12RiseFilter,
115 ex_regs: [u8; 4],
116}
117
118impl Coolboy {
119 const SAVE_LEN: usize = 8 + 9 + 4 + 1;
120
121 fn new(
122 prg_rom: Box<[u8]>,
123 chr_rom: Box<[u8]>,
124 mirroring: Mirroring,
125 ) -> Result<Self, MapperError> {
126 check_prg(&prg_rom, 268)?;
127 let chr_is_ram = chr_rom.is_empty();
128 let chr: Box<[u8]> = if chr_is_ram {
129 vec![0u8; 0x40000].into_boxed_slice()
130 } else {
131 if !chr_rom.len().is_multiple_of(CHR_BANK_1K) {
132 return Err(MapperError::Invalid(format!(
133 "mapper 268 CHR-ROM size {} is not a multiple of 1 KiB",
134 chr_rom.len()
135 )));
136 }
137 chr_rom
138 };
139 let prg_count_8k = prg_rom.len() / PRG_BANK_8K;
140 let chr_count_1k = (chr.len() / CHR_BANK_1K).max(1);
141 Ok(Self {
142 prg_rom,
143 chr,
144 chr_is_ram,
145 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
146 mirroring,
147 prg_count_8k,
148 chr_count_1k,
149 regs: [0; 8],
150 bank_select: 0,
151 prg_mode: false,
152 chr_mode: false,
153 irq_counter: 0,
154 irq_latch: 0,
155 irq_reload: false,
156 irq_enabled: false,
157 irq_pending: false,
158 a12: A12RiseFilter::new(),
159 ex_regs: [0; 4],
160 })
161 }
162
163 fn prg_bank_mmc3(&self, slot: usize) -> usize {
164 let last = self.prg_count_8k - 1;
165 let second_last = last.saturating_sub(1);
166 let r6 = self.regs[6] as usize;
167 let r7 = self.regs[7] as usize;
168 match (slot, self.prg_mode) {
169 (0, false) => r6,
170 (0, true) => second_last,
171 (1, _) => r7,
172 (2, false) => second_last,
173 (2, true) => r6,
174 (3, _) => last,
175 _ => 0,
176 }
177 }
178
179 fn resolve_prg(&self, slot: usize) -> usize {
180 let page = self.prg_bank_mmc3(slot);
181 let e0 = self.ex_regs[0] as usize;
182 let e1 = self.ex_regs[1] as usize;
183 let e3 = self.ex_regs[3] as usize;
184 let mut mask =
185 ((0x3F | (e1 & 0x40) | ((e1 & 0x20) << 2)) ^ ((e0 & 0x40) >> 2)) ^ ((e1 & 0x80) >> 2);
186 let base = (e0 & 0x07) | ((e1 & 0x10) >> 1) | ((e1 & 0x0C) << 2) | ((e0 & 0x30) << 2);
187 let bank = if e3 & 0x10 == 0 {
188 ((base << 4) & !mask) | (page & mask)
189 } else {
190 mask &= 0xF0;
191 let emask = if e1 & 0x02 != 0 {
192 (e3 & 0x0C) | (slot & 0x01)
193 } else {
194 e3 & 0x0E
195 };
196 ((base << 4) & !mask) | (page & mask) | emask | (slot & 0x01)
197 };
198 bank % self.prg_count_8k
199 }
200
201 fn chr_bank_mmc3(&self, slot: usize) -> usize {
202 let banks: [usize; 8] = if self.chr_mode {
203 [
204 self.regs[2] as usize,
205 self.regs[3] as usize,
206 self.regs[4] as usize,
207 self.regs[5] as usize,
208 self.regs[0] as usize & !1,
209 (self.regs[0] as usize & !1) | 1,
210 self.regs[1] as usize & !1,
211 (self.regs[1] as usize & !1) | 1,
212 ]
213 } else {
214 [
215 self.regs[0] as usize & !1,
216 (self.regs[0] as usize & !1) | 1,
217 self.regs[1] as usize & !1,
218 (self.regs[1] as usize & !1) | 1,
219 self.regs[2] as usize,
220 self.regs[3] as usize,
221 self.regs[4] as usize,
222 self.regs[5] as usize,
223 ]
224 };
225 banks[slot & 0x07]
226 }
227
228 fn resolve_chr(&self, slot: usize) -> usize {
229 let page = self.chr_bank_mmc3(slot);
230 let e0 = self.ex_regs[0] as usize;
231 let e2 = self.ex_regs[2] as usize;
232 let e3 = self.ex_regs[3] as usize;
233 let mask = 0xFF ^ (e0 & 0x80);
234 let bank = if e3 & 0x10 != 0 {
235 (page & 0x80 & mask) | (((e0 & 0x08) << 4) & !mask) | ((e2 & 0x0F) << 3) | slot
236 } else {
237 (page & mask) | (((e0 & 0x08) << 4) & !mask)
238 };
239 bank % self.chr_count_1k
240 }
241
242 fn write_mmc3(&mut self, addr: u16, value: u8) {
243 match addr & 0xE001 {
244 0x8000 => {
245 self.bank_select = value & 0x07;
246 self.prg_mode = value & 0x40 != 0;
247 self.chr_mode = value & 0x80 != 0;
248 }
249 0x8001 => {
250 let idx = (self.bank_select & 0x07) as usize;
251 self.regs[idx] = value;
252 }
253 0xA000 => {
254 self.mirroring = if value & 0x01 == 0 {
255 Mirroring::Vertical
256 } else {
257 Mirroring::Horizontal
258 };
259 }
260 0xC000 => self.irq_latch = value,
261 0xC001 => {
262 self.irq_counter = 0;
263 self.irq_reload = true;
264 }
265 0xE000 => {
266 self.irq_enabled = false;
267 self.irq_pending = false;
268 }
269 0xE001 => self.irq_enabled = true,
270 _ => {}
271 }
272 }
273}
274
275impl Mapper for Coolboy {
276 fn caps(&self) -> MapperCaps {
277 MapperCaps {
278 cpu_cycle_hook: true,
281 audio: false,
282 frame_event_hook: false,
283 irq_source: true,
284 }
285 }
286
287 fn cpu_read(&mut self, addr: u16) -> u8 {
288 match addr {
289 0x8000..=0x9FFF => {
290 let b = self.resolve_prg(0);
291 self.prg_rom[b * PRG_BANK_8K + (addr as usize & 0x1FFF)]
292 }
293 0xA000..=0xBFFF => {
294 let b = self.resolve_prg(1);
295 self.prg_rom[b * PRG_BANK_8K + (addr as usize & 0x1FFF)]
296 }
297 0xC000..=0xDFFF => {
298 let b = self.resolve_prg(2);
299 self.prg_rom[b * PRG_BANK_8K + (addr as usize & 0x1FFF)]
300 }
301 0xE000..=0xFFFF => {
302 let b = self.resolve_prg(3);
303 self.prg_rom[b * PRG_BANK_8K + (addr as usize & 0x1FFF)]
304 }
305 _ => 0,
306 }
307 }
308
309 fn cpu_write(&mut self, addr: u16, value: u8) {
310 match addr {
311 0x6000..=0x7FFF => {
312 if (self.ex_regs[3] & 0x90) != 0x80 {
314 self.ex_regs[(addr & 0x03) as usize] = value;
315 }
316 }
317 0x8000..=0xFFFF => self.write_mmc3(addr, value),
318 _ => {}
319 }
320 }
321
322 fn ppu_read(&mut self, addr: u16) -> u8 {
323 let addr = addr & 0x3FFF;
324 match addr {
325 0x0000..=0x1FFF => {
326 if self.chr_is_ram {
327 return self.chr[addr as usize & (self.chr.len() - 1)];
328 }
329 let slot = (addr as usize) / CHR_BANK_1K;
330 let b = self.resolve_chr(slot);
331 self.chr[b * CHR_BANK_1K + (addr as usize & 0x3FF)]
332 }
333 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
334 _ => 0,
335 }
336 }
337
338 fn ppu_write(&mut self, addr: u16, value: u8) {
339 let addr = addr & 0x3FFF;
340 match addr {
341 0x0000..=0x1FFF if self.chr_is_ram => {
342 self.chr[addr as usize & (self.chr.len() - 1)] = value;
343 }
344 0x2000..=0x3EFF => {
345 let off = nametable_offset(addr, self.mirroring);
346 self.vram[off] = value;
347 }
348 _ => {}
349 }
350 }
351
352 fn notify_cpu_cycle(&mut self) {
353 self.a12.tick();
354 }
355
356 fn notify_a12(&mut self, level: bool) {
357 if !self.a12.edge(level) {
360 return;
361 }
362 if self.irq_counter == 0 || self.irq_reload {
363 self.irq_counter = self.irq_latch;
364 self.irq_reload = false;
365 } else {
366 self.irq_counter = self.irq_counter.wrapping_sub(1);
367 }
368 if self.irq_counter == 0 && self.irq_enabled {
369 self.irq_pending = true;
370 }
371 }
372
373 fn irq_pending(&self) -> bool {
374 self.irq_pending
375 }
376
377 fn irq_acknowledge(&mut self) {
378 self.irq_pending = false;
379 }
380
381 fn current_mirroring(&self) -> Mirroring {
382 self.mirroring
383 }
384
385 fn save_state(&self) -> Vec<u8> {
386 let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
387 let mut out = Vec::with_capacity(1 + Self::SAVE_LEN + self.vram.len() + chr_ram);
388 out.push(SAVE_STATE_VERSION);
389 out.extend_from_slice(&self.regs);
390 out.push(self.bank_select);
391 out.push(u8::from(self.prg_mode));
392 out.push(u8::from(self.chr_mode));
393 out.push(self.irq_counter);
394 out.push(self.irq_latch);
395 out.push(u8::from(self.irq_reload));
396 out.push(u8::from(self.irq_enabled));
397 out.push(u8::from(self.irq_pending));
398 out.push(self.a12.to_byte());
399 out.extend_from_slice(&self.ex_regs);
400 out.push(mirroring_to_byte(self.mirroring));
401 out.extend_from_slice(&self.vram);
402 if self.chr_is_ram {
403 out.extend_from_slice(&self.chr);
404 }
405 out
406 }
407
408 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
409 let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
410 let expected = 1 + Self::SAVE_LEN + self.vram.len() + chr_ram;
411 if data.len() != expected {
412 return Err(MapperError::WrongLength {
413 expected,
414 got: data.len(),
415 });
416 }
417 let version = data[0];
421 if version != SAVE_STATE_VERSION {
422 return Err(MapperError::UnsupportedVersion(version));
423 }
424 let mut c = 1;
425 self.regs.copy_from_slice(&data[c..c + 8]);
426 c += 8;
427 self.bank_select = data[c];
428 self.prg_mode = data[c + 1] != 0;
429 self.chr_mode = data[c + 2] != 0;
430 self.irq_counter = data[c + 3];
431 self.irq_latch = data[c + 4];
432 self.irq_reload = data[c + 5] != 0;
433 self.irq_enabled = data[c + 6] != 0;
434 self.irq_pending = data[c + 7] != 0;
435 self.a12 = A12RiseFilter::from_byte(data[c + 8]);
436 c += 9;
437 self.ex_regs.copy_from_slice(&data[c..c + 4]);
438 c += 4;
439 self.mirroring = byte_to_mirroring(data[c], self.mirroring);
440 c += 1;
441 self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
442 c += self.vram.len();
443 if self.chr_is_ram {
444 self.chr.copy_from_slice(&data[c..c + self.chr.len()]);
445 }
446 Ok(())
447 }
448}
449
450pub fn new_m268(
455 prg_rom: Box<[u8]>,
456 chr_rom: Box<[u8]>,
457 mirroring: Mirroring,
458) -> Result<Coolboy, MapperError> {
459 Coolboy::new(prg_rom, chr_rom, mirroring)
460}
461
462#[cfg(test)]
474#[allow(clippy::cast_possible_truncation)]
475mod tests {
476
477 #[test]
481 fn irq_counts_scanlines_not_raw_a12_pulses() {
482 let mut m = new_m268(synth_prg_8k(64), synth_chr_1k(128), Mirroring::Vertical).unwrap();
483 assert_eq!(crate::a12_filter::irqs_over_scanlines(&mut m, 16), 2);
484 }
485
486 use super::*;
487
488 fn synth_prg_8k(banks: usize) -> Box<[u8]> {
489 let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
490 for b in 0..banks {
491 v[b * PRG_BANK_8K] = b as u8;
492 }
493 v.into_boxed_slice()
494 }
495
496 fn synth_chr_1k(banks: usize) -> Box<[u8]> {
497 let mut v = vec![0u8; banks * CHR_BANK_1K];
498 for b in 0..banks {
499 v[b * CHR_BANK_1K] = b as u8;
500 }
501 v.into_boxed_slice()
502 }
503
504 #[test]
505 fn coolboy_outer_regs_and_irq() {
506 let mut m = new_m268(synth_prg_8k(64), synth_chr_1k(128), Mirroring::Vertical).unwrap();
507 m.cpu_write(0x6000, 0x01); m.cpu_write(0x8000, 0x06); m.cpu_write(0x8001, 3);
510 let v = m.cpu_read(0x8000);
511 assert!((v as usize) < 64); m.cpu_write(0xC000, 1);
514 m.cpu_write(0xC001, 0);
515 m.cpu_write(0xE001, 0);
516 m.notify_a12(false);
517 for _ in 0..3 {
519 m.notify_cpu_cycle();
520 }
521 m.notify_a12(true);
522 m.notify_a12(false);
523 for _ in 0..3 {
525 m.notify_cpu_cycle();
526 }
527 m.notify_a12(true);
528 assert!(m.irq_pending());
529 }
530
531 #[test]
532 fn coolboy_save_state_round_trip() {
533 let mut m = new_m268(synth_prg_8k(64), synth_chr_1k(128), Mirroring::Horizontal).unwrap();
534 m.cpu_write(0x6000, 0x05);
535 m.cpu_write(0x6001, 0x02);
536 m.cpu_write(0x8000, 0x06);
537 m.cpu_write(0x8001, 4);
538 m.ppu_write(0x2005, 0x3C);
539 let blob = m.save_state();
540 let mut m2 = new_m268(synth_prg_8k(64), synth_chr_1k(128), Mirroring::Horizontal).unwrap();
541 m2.load_state(&blob).unwrap();
542 assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
543 assert_eq!(m2.ppu_read(0x2005), 0x3C);
544 }
545}