1#![allow(
58 clippy::cast_possible_truncation,
59 clippy::cast_lossless,
60 clippy::missing_const_for_fn,
61 clippy::struct_excessive_bools,
62 clippy::match_same_arms,
63 clippy::doc_markdown,
64 clippy::doc_lazy_continuation
65)]
66
67use crate::cartridge::Mirroring;
68use crate::mapper::{Mapper, MapperCaps, MapperError};
69use alloc::{boxed::Box, vec::Vec};
70use alloc::{format, vec};
71
72const PRG_BANK_8K: usize = 0x2000;
73const CHR_BANK_1K: usize = 0x0400;
74const NAMETABLE_SIZE: usize = 0x0400;
75const NAMETABLE_SIZE_U16: u16 = 0x0400;
76
77const SAVE_STATE_VERSION: u8 = 1;
78
79pub struct Rambo1 {
81 prg_rom: Box<[u8]>,
82 chr: Box<[u8]>,
83 vram: Box<[u8]>,
84 chr_is_ram: bool,
85
86 regs: [u8; 16],
88 bank_select: u8,
89 prg_mode: bool, chr_mode: bool, chr_1k_mode: bool, mirroring: Mirroring,
94
95 irq_latch: u8,
97 irq_counter: u8,
98 irq_reload_pending: bool,
99 irq_enabled: bool,
100 irq_pending: bool,
101 irq_cpu_mode: bool, irq_prescaler: u8, last_a12: bool,
106 a12_low_cycle: u64,
107 cpu_cycle: u64,
108}
109
110impl Rambo1 {
111 pub fn new(
121 prg_rom: Box<[u8]>,
122 chr_rom: Box<[u8]>,
123 mirroring: Mirroring,
124 ) -> Result<Self, MapperError> {
125 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
126 return Err(MapperError::Invalid(format!(
127 "RAMBO-1 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
128 prg_rom.len()
129 )));
130 }
131 let chr_is_ram = chr_rom.is_empty();
132 let chr: Box<[u8]> = if chr_is_ram {
133 vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
134 } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
135 chr_rom
136 } else {
137 return Err(MapperError::Invalid(format!(
138 "RAMBO-1 CHR-ROM size {} is not a multiple of 1 KiB",
139 chr_rom.len()
140 )));
141 };
142 Ok(Self {
143 prg_rom,
144 chr,
145 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
146 chr_is_ram,
147 regs: [0; 16],
148 bank_select: 0,
149 prg_mode: false,
150 chr_mode: false,
151 chr_1k_mode: false,
152 mirroring,
153 irq_latch: 0,
154 irq_counter: 0,
155 irq_reload_pending: false,
156 irq_enabled: false,
157 irq_pending: false,
158 irq_cpu_mode: false,
159 irq_prescaler: 0,
160 last_a12: false,
161 a12_low_cycle: 0,
162 cpu_cycle: 0,
163 })
164 }
165
166 const RF: usize = 15;
168
169 fn prg_offset(&self, addr: u16) -> usize {
172 let total = (self.prg_rom.len() / PRG_BANK_8K).max(1);
173 let last = total - 1;
174 let r6 = self.regs[6] as usize;
175 let r7 = self.regs[7] as usize;
176 let rf = self.regs[Self::RF] as usize;
177 let bank = match (addr & 0xE000, self.prg_mode) {
178 (0x8000, false) => r6,
179 (0x8000, true) => rf,
180 (0xA000, _) => r7,
181 (0xC000, false) => rf,
182 (0xC000, true) => r6,
183 (0xE000, _) => last,
184 _ => 0,
185 };
186 (bank % total) * PRG_BANK_8K + (addr as usize & 0x1FFF)
187 }
188
189 fn chr_bank_1k(&self, slot: usize) -> usize {
192 let s = if self.chr_mode { slot ^ 0x4 } else { slot };
196 match s {
199 0 => {
200 if self.chr_1k_mode {
201 self.regs[0] as usize
202 } else {
203 (self.regs[0] as usize) & !1
204 }
205 }
206 1 => {
207 if self.chr_1k_mode {
208 self.regs[8] as usize
209 } else {
210 ((self.regs[0] as usize) & !1) | 1
211 }
212 }
213 2 => {
214 if self.chr_1k_mode {
215 self.regs[1] as usize
216 } else {
217 (self.regs[1] as usize) & !1
218 }
219 }
220 3 => {
221 if self.chr_1k_mode {
222 self.regs[9] as usize
223 } else {
224 ((self.regs[1] as usize) & !1) | 1
225 }
226 }
227 4 => self.regs[2] as usize,
228 5 => self.regs[3] as usize,
229 6 => self.regs[4] as usize,
230 7 => self.regs[5] as usize,
231 _ => 0,
232 }
233 }
234
235 fn chr_offset(&self, addr: u16) -> usize {
236 let addr = (addr & 0x1FFF) as usize;
237 let total_1k = (self.chr.len() / CHR_BANK_1K).max(1);
238 let slot = addr / CHR_BANK_1K;
239 let bank = self.chr_bank_1k(slot) % total_1k;
240 bank * CHR_BANK_1K + (addr & (CHR_BANK_1K - 1))
241 }
242
243 fn nametable_offset(&self, addr: u16) -> usize {
244 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
245 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
246 let physical = self.mirroring.physical_bank(table);
247 physical * NAMETABLE_SIZE + local
248 }
249
250 fn clock_irq(&mut self) -> bool {
260 if self.irq_reload_pending {
261 self.irq_counter = self.irq_latch;
262 self.irq_reload_pending = false;
263 } else if self.irq_counter == 0 {
264 self.irq_counter = self.irq_latch;
265 } else {
266 self.irq_counter = self.irq_counter.wrapping_sub(1);
267 }
268 self.irq_counter == 0 && self.irq_enabled
269 }
270}
271
272impl Mapper for Rambo1 {
273 fn caps(&self) -> MapperCaps {
275 MapperCaps::CYCLE_IRQ
276 }
277
278 fn cpu_read(&mut self, addr: u16) -> u8 {
279 match addr {
280 0x8000..=0xFFFF => {
281 let off = self.prg_offset(addr);
282 self.prg_rom[off % self.prg_rom.len()]
283 }
284 _ => 0,
285 }
286 }
287
288 fn cpu_write(&mut self, addr: u16, value: u8) {
289 match addr {
290 0x8000..=0x9FFF => {
291 if addr & 1 == 0 {
292 self.bank_select = value & 0x0F;
293 self.chr_1k_mode = (value & 0x20) != 0;
294 self.prg_mode = (value & 0x40) != 0;
295 self.chr_mode = (value & 0x80) != 0;
296 } else {
297 let idx = self.bank_select as usize;
298 if idx <= 9 || idx == Self::RF {
300 self.regs[idx] = value;
301 }
302 }
303 }
304 0xA000..=0xBFFF => {
305 if addr & 1 == 0 {
306 self.mirroring = if value & 1 == 0 {
307 Mirroring::Vertical
308 } else {
309 Mirroring::Horizontal
310 };
311 }
312 }
314 0xC000..=0xDFFF => {
315 if addr & 1 == 0 {
316 self.irq_latch = value;
317 } else {
318 self.irq_cpu_mode = (value & 1) != 0;
319 self.irq_reload_pending = true;
322 self.irq_counter = 0;
323 self.irq_prescaler = 0;
324 }
325 }
326 0xE000..=0xFFFF => {
327 if addr & 1 == 0 {
328 self.irq_enabled = false;
329 self.irq_pending = false;
330 } else {
331 self.irq_enabled = true;
332 }
333 }
334 _ => {}
335 }
336 }
337
338 fn ppu_read(&mut self, addr: u16) -> u8 {
339 let addr = addr & 0x3FFF;
340 match addr {
341 0x0000..=0x1FFF => {
342 let off = self.chr_offset(addr);
343 self.chr[off % self.chr.len()]
344 }
345 0x2000..=0x3EFF => self.vram[self.nametable_offset(addr) % self.vram.len()],
346 _ => 0,
347 }
348 }
349
350 fn ppu_write(&mut self, addr: u16, value: u8) {
351 let addr = addr & 0x3FFF;
352 match addr {
353 0x0000..=0x1FFF => {
354 if self.chr_is_ram {
355 let off = self.chr_offset(addr);
356 let len = self.chr.len();
357 self.chr[off % len] = value;
358 }
359 }
360 0x2000..=0x3EFF => {
361 let off = self.nametable_offset(addr) % self.vram.len();
362 self.vram[off] = value;
363 }
364 _ => {}
365 }
366 }
367
368 fn nametable_address(&self, addr: u16) -> u16 {
369 let off = self.nametable_offset(addr);
370 u16::try_from(off & 0x07FF).unwrap_or(0)
371 }
372
373 fn current_mirroring(&self) -> Mirroring {
374 self.mirroring
375 }
376
377 fn notify_a12(&mut self, level: bool) {
378 self.notify_a12_at_sub_dot(level, 1);
379 }
380
381 fn notify_a12_at_sub_dot(&mut self, level: bool, _sub_dot: u8) {
382 if self.irq_cpu_mode {
384 self.last_a12 = level;
385 return;
386 }
387 if !self.last_a12 && level {
388 let gap = self.cpu_cycle.saturating_sub(self.a12_low_cycle);
389 if gap >= 3 && self.clock_irq() {
390 self.irq_pending = true;
391 }
392 } else if self.last_a12 && !level {
393 self.a12_low_cycle = self.cpu_cycle;
394 }
395 self.last_a12 = level;
396 }
397
398 fn notify_cpu_cycle(&mut self) {
399 self.cpu_cycle = self.cpu_cycle.wrapping_add(1);
400 if !self.irq_cpu_mode {
401 return;
402 }
403 self.irq_prescaler = self.irq_prescaler.wrapping_add(1);
405 if self.irq_prescaler >= 4 {
406 self.irq_prescaler = 0;
407 if self.clock_irq() {
408 self.irq_pending = true;
409 }
410 }
411 }
412
413 fn irq_pending(&self) -> bool {
414 self.irq_pending
415 }
416
417 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
418 let mut info = crate::mapper::MapperDebugInfo {
419 mapper_id: 64,
420 name: "Tengen RAMBO-1 (64)".into(),
421 mirroring: crate::mapper::mirroring_name(self.mirroring),
422 ..Default::default()
423 };
424 info.prg_banks
425 .push(("mode".into(), format!("{}", u8::from(self.prg_mode))));
426 info.prg_banks
427 .push(("R6".into(), format!("{:#04x}", self.regs[6])));
428 info.prg_banks
429 .push(("R7".into(), format!("{:#04x}", self.regs[7])));
430 info.prg_banks
431 .push(("RF".into(), format!("{:#04x}", self.regs[Self::RF])));
432 info.chr_banks
433 .push(("K".into(), format!("{}", u8::from(self.chr_1k_mode))));
434 info.chr_banks
435 .push(("C".into(), format!("{}", u8::from(self.chr_mode))));
436 for i in 0..6 {
437 info.chr_banks
438 .push((format!("R{i}"), format!("{:#04x}", self.regs[i])));
439 }
440 info.chr_banks
441 .push(("R8".into(), format!("{:#04x}", self.regs[8])));
442 info.chr_banks
443 .push(("R9".into(), format!("{:#04x}", self.regs[9])));
444 info.irq_state.push((
445 "mode".into(),
446 if self.irq_cpu_mode {
447 "cpu".into()
448 } else {
449 "scanline".into()
450 },
451 ));
452 info.irq_state
453 .push(("counter".into(), format!("{:#04x}", self.irq_counter)));
454 info.irq_state
455 .push(("latch".into(), format!("{:#04x}", self.irq_latch)));
456 info.irq_state
457 .push(("enabled".into(), format!("{}", self.irq_enabled)));
458 info.irq_state
459 .push(("pending".into(), format!("{}", self.irq_pending)));
460 info
461 }
462
463 fn save_state(&self) -> Vec<u8> {
464 let mut out = Vec::with_capacity(
465 48 + self.vram.len() + if self.chr_is_ram { self.chr.len() } else { 0 },
466 );
467 out.push(SAVE_STATE_VERSION);
468 out.extend_from_slice(&self.regs);
469 out.push(self.bank_select);
470 out.push(u8::from(self.prg_mode));
471 out.push(u8::from(self.chr_mode));
472 out.push(u8::from(self.chr_1k_mode));
473 out.push(self.mirroring as u8);
474 out.push(self.irq_latch);
475 out.push(self.irq_counter);
476 out.push(u8::from(self.irq_reload_pending));
477 out.push(u8::from(self.irq_enabled));
478 out.push(u8::from(self.irq_pending));
479 out.push(u8::from(self.irq_cpu_mode));
480 out.push(self.irq_prescaler);
481 out.push(u8::from(self.last_a12));
482 out.extend_from_slice(&self.a12_low_cycle.to_le_bytes());
483 out.extend_from_slice(&self.cpu_cycle.to_le_bytes());
484 out.extend_from_slice(&self.vram);
485 if self.chr_is_ram {
486 out.extend_from_slice(&self.chr);
487 }
488 out
489 }
490
491 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
492 let chr_part = if self.chr_is_ram { self.chr.len() } else { 0 };
493 let scalar_len = 1 + 16 + 13 + 8 + 8;
495 let expected = scalar_len + self.vram.len() + chr_part;
496 if data.len() != expected {
497 return Err(MapperError::WrongLength {
498 expected,
499 got: data.len(),
500 });
501 }
502 if data[0] != SAVE_STATE_VERSION {
503 return Err(MapperError::UnsupportedVersion(data[0]));
504 }
505 self.regs.copy_from_slice(&data[1..17]);
506 let mut c = 17usize;
507 self.bank_select = data[c];
508 c += 1;
509 self.prg_mode = data[c] != 0;
510 c += 1;
511 self.chr_mode = data[c] != 0;
512 c += 1;
513 self.chr_1k_mode = data[c] != 0;
514 c += 1;
515 self.mirroring = match data[c] {
516 0 => Mirroring::Horizontal,
517 1 => Mirroring::Vertical,
518 2 => Mirroring::SingleScreenA,
519 3 => Mirroring::SingleScreenB,
520 4 => Mirroring::FourScreen,
521 5 => Mirroring::MapperControlled,
522 other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
523 };
524 c += 1;
525 self.irq_latch = data[c];
526 c += 1;
527 self.irq_counter = data[c];
528 c += 1;
529 self.irq_reload_pending = data[c] != 0;
530 c += 1;
531 self.irq_enabled = data[c] != 0;
532 c += 1;
533 self.irq_pending = data[c] != 0;
534 c += 1;
535 self.irq_cpu_mode = data[c] != 0;
536 c += 1;
537 self.irq_prescaler = data[c];
538 c += 1;
539 self.last_a12 = data[c] != 0;
540 c += 1;
541 self.a12_low_cycle = u64::from_le_bytes(
542 data[c..c + 8]
543 .try_into()
544 .map_err(|_| MapperError::Invalid("a12_low_cycle truncated".into()))?,
545 );
546 c += 8;
547 self.cpu_cycle = u64::from_le_bytes(
548 data[c..c + 8]
549 .try_into()
550 .map_err(|_| MapperError::Invalid("cpu_cycle truncated".into()))?,
551 );
552 c += 8;
553 self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
554 c += self.vram.len();
555 if self.chr_is_ram {
556 self.chr.copy_from_slice(&data[c..c + self.chr.len()]);
557 }
558 Ok(())
559 }
560}
561
562#[cfg(test)]
563#[allow(clippy::cast_possible_truncation, clippy::identity_op)]
564mod tests {
565 use super::*;
566
567 fn synth_prg(banks_8k: usize) -> Box<[u8]> {
568 let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
569 for b in 0..banks_8k {
570 v[b * PRG_BANK_8K] = b as u8;
571 }
572 v.into_boxed_slice()
573 }
574
575 fn synth_chr(banks_1k: usize) -> Box<[u8]> {
576 let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
577 for b in 0..banks_1k {
578 v[b * CHR_BANK_1K] = b as u8;
579 }
580 v.into_boxed_slice()
581 }
582
583 fn fresh() -> Rambo1 {
584 Rambo1::new(synth_prg(32), synth_chr(64), Mirroring::Vertical).unwrap()
585 }
586
587 fn select_write(m: &mut Rambo1, reg: u8, value: u8) {
588 m.cpu_write(0x8000, reg);
589 m.cpu_write(0x8001, value);
590 }
591
592 #[test]
593 fn prg_three_switchable_plus_fixed_last() {
594 let mut m = fresh();
595 select_write(&mut m, 6, 3); select_write(&mut m, 7, 5); select_write(&mut m, 0x0F, 9); assert_eq!(m.cpu_read(0x8000), 3);
600 assert_eq!(m.cpu_read(0xA000), 5);
601 assert_eq!(m.cpu_read(0xC000), 9);
602 assert_eq!(m.cpu_read(0xE000), 31); }
604
605 #[test]
606 fn prg_mode_swaps_r6_and_rf() {
607 let mut m = fresh();
608 select_write(&mut m, 6, 3);
609 select_write(&mut m, 0x0F, 9);
610 m.cpu_write(0x8000, 0x40 | 0); assert_eq!(m.cpu_read(0x8000), 9); assert_eq!(m.cpu_read(0xC000), 3); }
615
616 #[test]
617 fn chr_2k_mode_forces_even_banks() {
618 let mut m = fresh();
619 select_write(&mut m, 0, 4); assert_eq!(m.ppu_read(0x0000), 4);
621 assert_eq!(m.ppu_read(0x0400), 5); select_write(&mut m, 2, 9); assert_eq!(m.ppu_read(0x1000), 9);
624 }
625
626 #[test]
627 fn chr_1k_mode_uses_r8_r9() {
628 let mut m = fresh();
629 m.cpu_write(0x8000, 0x20 | 0); m.cpu_write(0x8001, 10);
632 m.cpu_write(0x8000, 0x20 | 8); m.cpu_write(0x8001, 11);
634 assert_eq!(m.ppu_read(0x0000), 10); assert_eq!(m.ppu_read(0x0400), 11); }
637
638 #[test]
639 fn chr_a12_inversion_swaps_halves() {
640 let mut m = fresh();
641 select_write(&mut m, 0, 4); select_write(&mut m, 2, 9); assert_eq!(m.ppu_read(0x0000), 4);
645 assert_eq!(m.ppu_read(0x1000), 9);
646 m.cpu_write(0x8000, 0x80 | 0);
648 assert_eq!(m.ppu_read(0x0000), 9); assert_eq!(m.ppu_read(0x1000), 4); }
651
652 #[test]
653 fn scanline_irq_decrements_and_asserts() {
654 let mut m = fresh();
655 m.cpu_write(0xC000, 3); m.cpu_write(0xC001, 0); m.cpu_write(0xE001, 0); for _ in 0..5 {
659 m.notify_a12(false);
660 for _ in 0..4 {
661 m.notify_cpu_cycle();
662 }
663 m.notify_a12(true);
664 }
665 assert!(m.irq_pending());
667 }
668
669 #[test]
670 fn zero_latch_reload_asserts_on_every_clock() {
671 let mut m = fresh();
679 m.cpu_write(0xC000, 0); m.cpu_write(0xC001, 0); m.cpu_write(0xE001, 0); let edge = |m: &mut Rambo1| {
683 m.notify_a12(false);
684 for _ in 0..4 {
685 m.notify_cpu_cycle();
686 }
687 m.notify_a12(true);
688 };
689 edge(&mut m); assert!(m.irq_pending(), "reload to 0 asserts");
691 m.cpu_write(0xE000, 0); m.cpu_write(0xE001, 0); edge(&mut m); assert!(m.irq_pending(), "zero counter reloading 0 asserts again");
695 m.cpu_write(0xE000, 0);
697 edge(&mut m);
698 assert!(!m.irq_pending());
699 }
700
701 #[test]
702 fn cpu_cycle_irq_clocks_every_four() {
703 let mut m = fresh();
704 m.cpu_write(0xC000, 2); m.cpu_write(0xC001, 1); m.cpu_write(0xE001, 0); m.notify_a12(false);
709 m.notify_a12(true);
710 assert!(!m.irq_pending());
711 for _ in 0..12 {
714 m.notify_cpu_cycle();
715 }
716 assert!(m.irq_pending());
717 }
718
719 #[test]
720 fn e000_acks_and_disables() {
721 let mut m = fresh();
722 m.irq_pending = true;
723 m.cpu_write(0xE000, 0);
724 assert!(!m.irq_pending());
725 assert!(!m.irq_enabled);
726 }
727
728 #[test]
729 fn save_state_round_trip() {
730 let mut m = fresh();
731 select_write(&mut m, 6, 3);
732 select_write(&mut m, 0x0F, 9);
733 m.cpu_write(0xC000, 0x42);
734 m.cpu_write(0xC001, 1); m.cpu_write(0xE001, 0);
736 m.ppu_write(0x2000, 0x77);
737 let blob = m.save_state();
738 let mut m2 = fresh();
739 m2.load_state(&blob).unwrap();
740 assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
741 assert_eq!(m.cpu_read(0xC000), m2.cpu_read(0xC000));
742 assert_eq!(m.irq_cpu_mode, m2.irq_cpu_mode);
743 assert_eq!(m.ppu_read(0x2000), m2.ppu_read(0x2000));
744 }
745}