Skip to main content

rustynes_mappers/
m064_rambo1.rs

1//! Tengen RAMBO-1 (iNES mapper 64) implementation.
2//!
3//! The RAMBO-1 is Tengen's MMC3 variant with three extra features:
4//!
5//! - A **third** switchable 8 KiB PRG bank (register `RF`) at the slot the
6//!   MMC3 fixes to the second-to-last bank, so three of the four 8 KiB PRG
7//!   windows are switchable (`$8000`/`$A000`/`$C000`), only `$E000` is fixed
8//!   to the last bank.
9//! - **Finer CHR banking**: a "full 1 KiB" mode bit (`$8000` bit 5) replaces
10//!   the two 2 KiB CHR banks (R0/R1) with four 1 KiB banks (R0/R8 + R1/R9),
11//!   so the whole `$0000-$0FFF` half can be eight 1 KiB banks.
12//! - A **dual-mode IRQ**: a scanline (PPU A12) mode like MMC3 *and* a
13//!   CPU-cycle mode (clocked every 4 CPU cycles), selected by `$C001` bit 0.
14//!
15//! See `nesdev_wiki/RAMBO_1.xhtml`, `nesdev_wiki/INES_Mapper_064.xhtml`, and
16//! `docs/mappers.md`.
17//!
18//! # Banking registers
19//!
20//! `$8000` (even) bank-select: bits 0-3 pick the register, bit 5 = full-1KiB
21//! CHR mode (K), bit 6 = PRG mode (P), bit 7 = CHR A12 inversion (C).
22//! `$8001` (odd) writes the selected register:
23//!
24//! | Reg | Purpose                                                       |
25//! |-----|---------------------------------------------------------------|
26//! | R0  | 2 KiB CHR @ `$0000` (or 1 KiB when K=1)                        |
27//! | R1  | 2 KiB CHR @ `$0800` (or 1 KiB when K=1)                        |
28//! | R2  | 1 KiB CHR @ `$1000`                                            |
29//! | R3  | 1 KiB CHR @ `$1400`                                            |
30//! | R4  | 1 KiB CHR @ `$1800`                                            |
31//! | R5  | 1 KiB CHR @ `$1C00`                                            |
32//! | R6  | 8 KiB PRG @ `$8000` (P=0) / `$C000` (P=1)                      |
33//! | R7  | 8 KiB PRG @ `$A000`                                            |
34//! | R8  | 1 KiB CHR @ `$0400` (only when K=1)                            |
35//! | R9  | 1 KiB CHR @ `$0C00` (only when K=1)                            |
36//! | RF  | 8 KiB PRG @ `$C000` (P=0) / `$8000` (P=1)                      |
37//!
38//! Bit 7 (CHR A12 inversion) swaps the `$0xxx` and `$1xxx` halves exactly like
39//! the MMC3 CHR mode bit.
40//!
41//! # IRQ
42//!
43//! `$C000` (even): IRQ latch / reload value.
44//! `$C001` (odd): bit 0 = mode select (0 = scanline / PPU A12, 1 = CPU cycle).
45//! Writing `$C001` also clears the counter so it reloads on the next clock.
46//! `$E000` (even): disable + acknowledge. `$E001` (odd): enable.
47//!
48//! The IRQ counter, on each clock (scanline A12 rise or every 4 CPU cycles):
49//! if a `$C001` write happened since the last clock, reload from the latch;
50//! else if the counter is 0, reload; else decrement. Then, if the counter is
51//! 0 and IRQs are enabled, assert -- after a reload as well as after a
52//! decrement, so a latch of 0 asserts on every clock. The +1 reload-kick quirk (`if non-zero, value | 1`) and the
53//! one-cycle assertion delay are documented but not separately modelled here —
54//! we treat the assert as immediate, which is sufficient for the boot-smoke +
55//! register-level verification we can perform (no behavioural fixtures exist).
56
57#![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
79/// Tengen RAMBO-1 mapper (iNES mapper 64).
80pub struct Rambo1 {
81    prg_rom: Box<[u8]>,
82    chr: Box<[u8]>,
83    vram: Box<[u8]>,
84    chr_is_ram: bool,
85
86    // R0..R9 + RF (index 10) — 11 bank registers.
87    regs: [u8; 16],
88    bank_select: u8,
89    prg_mode: bool,    // $8000 bit 6
90    chr_mode: bool,    // $8000 bit 7 (A12 inversion)
91    chr_1k_mode: bool, // $8000 bit 5 (K)
92
93    mirroring: Mirroring,
94
95    // IRQ.
96    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, // false = scanline (A12), true = CPU cycle
102    irq_prescaler: u8,  // /4 prescaler for CPU-cycle mode
103
104    // A12 filter state (scanline mode).
105    last_a12: bool,
106    a12_low_cycle: u64,
107    cpu_cycle: u64,
108}
109
110impl Rambo1 {
111    /// Construct a new RAMBO-1 mapper.
112    ///
113    /// `prg_rom` must be a non-zero multiple of 8 KiB; CHR-ROM (when present)
114    /// must be a multiple of 1 KiB. CHR-RAM (8 KiB) is allocated when no
115    /// CHR-ROM is supplied.
116    ///
117    /// # Errors
118    ///
119    /// Returns [`MapperError::Invalid`] on size mismatch.
120    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    /// RF lives at register index 15.
167    const RF: usize = 15;
168
169    /// PRG: R6/RF swap between `$8000` and `$C000` per `prg_mode`; R7 fixed at
170    /// `$A000`; `$E000` always the last bank.
171    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    /// CHR slot -> 1 KiB bank index, accounting for the K (full-1KiB) mode,
190    /// the C (A12 inversion) mode, and the 2 KiB even-bank forcing.
191    fn chr_bank_1k(&self, slot: usize) -> usize {
192        // Resolve the slot to the "low-half" semantic slot (0..8) before
193        // applying A12 inversion: when chr_mode is set, the $0xxx and $1xxx
194        // halves are swapped.
195        let s = if self.chr_mode { slot ^ 0x4 } else { slot };
196        // s now refers to the canonical layout where slots 0-3 are the
197        // R0/R1 (2 KiB) region and slots 4-7 are R2-R5.
198        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    /// Clock the 8-bit IRQ counter. Returns `true` if the IRQ line should
251    /// assert. Shared by scanline and CPU-cycle modes.
252    ///
253    /// The zero test follows whichever step ran (NESdev "RAMBO-1", IRQ
254    /// counter operation: "If IRQ counter is now 0 AND IRQs are enabled"),
255    /// so a reload that lands on 0 asserts as well as a decrement that
256    /// reaches 0. A latch of 0 therefore raises the IRQ on every clock,
257    /// which *Skull & Crossbones* relies on. Until v2.9.8 only the
258    /// decrement path asserted.
259    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    // v2.8.0 Phase 4 — CPU-cycle hook + IRQ source; no on-cart audio.
274    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                    // Registers 0-9 + RF (15) are valid; 10-14 unused.
299                    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                // $A001 odd: unimplemented on RAMBO-1 (no PRG-RAM).
313            }
314            0xC000..=0xDFFF => {
315                if addr & 1 == 0 {
316                    self.irq_latch = value;
317                } else {
318                    self.irq_cpu_mode = (value & 1) != 0;
319                    // Clear counter so it reloads on next clock; reset the
320                    // CPU-cycle prescaler.
321                    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        // Scanline (PPU A12) mode only; CPU-cycle mode ignores A12.
383        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        // CPU-cycle mode: clock the counter every 4 CPU cycles.
404        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        // 1 (ver) + 16 (regs) + 1+1+1+1+1+1+1+1+1+1+1+1+1 (13 scalars) + 8 + 8
494        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        // mode 0: R6@$8000, R7@$A000, RF@$C000, last@$E000.
596        select_write(&mut m, 6, 3); // R6 = 3
597        select_write(&mut m, 7, 5); // R7 = 5
598        select_write(&mut m, 0x0F, 9); // RF = 9
599        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); // last of 32 banks
603    }
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        // mode 1: RF@$8000, R6@$C000.
611        m.cpu_write(0x8000, 0x40 | 0); // select R0 but set prg_mode bit
612        assert_eq!(m.cpu_read(0x8000), 9); // RF
613        assert_eq!(m.cpu_read(0xC000), 3); // R6
614    }
615
616    #[test]
617    fn chr_2k_mode_forces_even_banks() {
618        let mut m = fresh();
619        select_write(&mut m, 0, 4); // R0 = 4 -> 2 KiB @ $0000
620        assert_eq!(m.ppu_read(0x0000), 4);
621        assert_eq!(m.ppu_read(0x0400), 5); // even-forced second 1 KiB
622        select_write(&mut m, 2, 9); // R2 = 1 KiB @ $1000
623        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        // Enable K (bit 5) and set R0/R8/R1/R9.
630        m.cpu_write(0x8000, 0x20 | 0); // select R0, K=1
631        m.cpu_write(0x8001, 10);
632        m.cpu_write(0x8000, 0x20 | 8); // select R8, K=1
633        m.cpu_write(0x8001, 11);
634        assert_eq!(m.ppu_read(0x0000), 10); // R0 1 KiB
635        assert_eq!(m.ppu_read(0x0400), 11); // R8 1 KiB
636    }
637
638    #[test]
639    fn chr_a12_inversion_swaps_halves() {
640        let mut m = fresh();
641        select_write(&mut m, 0, 4); // R0 (2 KiB)
642        select_write(&mut m, 2, 9); // R2 (1 KiB)
643        // Without inversion: R0 @ $0000, R2 @ $1000.
644        assert_eq!(m.ppu_read(0x0000), 4);
645        assert_eq!(m.ppu_read(0x1000), 9);
646        // With inversion (bit 7): R2 region moves to $0000, R0 to $1000.
647        m.cpu_write(0x8000, 0x80 | 0);
648        assert_eq!(m.ppu_read(0x0000), 9); // R2 now @ $0000
649        assert_eq!(m.ppu_read(0x1000), 4); // R0 now @ $1000
650    }
651
652    #[test]
653    fn scanline_irq_decrements_and_asserts() {
654        let mut m = fresh();
655        m.cpu_write(0xC000, 3); // latch = 3
656        m.cpu_write(0xC001, 0); // scanline mode (bit 0 = 0), reload pending
657        m.cpu_write(0xE001, 0); // enable
658        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        // Edge 1 reload 3; edges 2-4 -> 2,1,0 (assert).
666        assert!(m.irq_pending());
667    }
668
669    #[test]
670    fn zero_latch_reload_asserts_on_every_clock() {
671        // NESdev "RAMBO-1", IRQ counter operation: after the reload-or-
672        // decrement step, "If IRQ counter is now 0 AND IRQs are enabled:
673        // trigger IRQ" -- the check follows a reload too, not only a
674        // decrement. With a latch of 0 every clock reloads 0 and asserts.
675        // Skull & Crossbones writes $C000=0, $C001=0, $E001 every vblank and
676        // waits on the resulting scanline IRQ; without it the game never
677        // leaves a mostly black screen.
678        let mut m = fresh();
679        m.cpu_write(0xC000, 0); // latch = 0
680        m.cpu_write(0xC001, 0); // scanline mode, reload pending
681        m.cpu_write(0xE001, 0); // enable
682        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); // reload path (C001 written): 0 -> assert
690        assert!(m.irq_pending(), "reload to 0 asserts");
691        m.cpu_write(0xE000, 0); // ack + disable
692        m.cpu_write(0xE001, 0); // enable
693        edge(&mut m); // counter-is-0 path: reload 0 -> assert
694        assert!(m.irq_pending(), "zero counter reloading 0 asserts again");
695        // Disabled: the same clock does not assert.
696        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); // latch = 2
705        m.cpu_write(0xC001, 1); // CPU-cycle mode (bit 0 = 1), reload pending
706        m.cpu_write(0xE001, 0); // enable
707        // A12 must be ignored in CPU-cycle mode.
708        m.notify_a12(false);
709        m.notify_a12(true);
710        assert!(!m.irq_pending());
711        // First /4 clock: reload 2. Next two /4 clocks: 1, 0 (assert).
712        // That's 3 clocks = 12 cpu cycles.
713        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); // cpu mode
735        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}