rustynes_mappers/m001_mmc1.rs
1//! MMC1 (iNES mapper 1) implementation.
2//!
3//! See `docs/mappers.md` §Mapper coverage matrix and §MMC1; see
4//! `ref-docs/research-report.md` §MMC1 for the source material.
5//!
6//! MMC1 is a serial mapper: writes to `$8000-$FFFF` are accumulated in a 5-bit
7//! shift register over five consecutive CPU writes. The fifth write commits
8//! the assembled value to one of four internal registers selected by bits
9//! 14-13 of the destination address (`$8000-$9FFF` -> control, `$A000-$BFFF`
10//! -> CHR0, `$C000-$DFFF` -> CHR1, `$E000-$FFFF` -> PRG). A write whose data
11//! has bit 7 set resets the shift register and ORs the control register
12//! with `$0C` (forcing PRG mode 3 = "fix last bank @ $C000").
13//!
14//! Consecutive-write bug: on real hardware the serial port ignores the DATA
15//! bit of a write that lands on the CPU cycle immediately after another
16//! serial-port write, whether that earlier write was accepted or itself
17//! ignored (nesdev: it "ignores every write after the first"). A write with
18//! bit 7 set is never ignored: the reset takes effect on any cycle. A
19//! read-modify-write instruction's two back-to-back writes are the case that
20//! matters: Bill & Ted's Excellent Adventure relies on the second data write
21//! being dropped, and Shinsenden on a reset in that second write landing. We
22//! track the cycle counter via [`Mapper::notify_cpu_cycle`]. Until v2.8.2 the
23//! reset was filtered too (RTL audit R-3.5a, which found the `MiSTer` core right
24//! and this implementation wrong).
25//!
26//! The default revision when the cartridge header lacks an NES 2.0 submapper
27//! byte is **Sharp** (project policy: Star Trek: 25th Anniversary requires
28//! the Sharp variant). NES 2.0 submapper 5 selects SEROM/SHROM/SH1ROM (no
29//! PRG-RAM), but at the level of MMC1 register semantics those are
30//! observationally equivalent.
31
32use crate::cartridge::Mirroring;
33use crate::mapper::{Mapper, MapperCaps, MapperError};
34use alloc::{boxed::Box, vec::Vec};
35use alloc::{format, vec};
36
37const PRG_BANK_16K: usize = 0x4000;
38const CHR_BANK_4K: usize = 0x1000;
39const CHR_BANK_8K: usize = 0x2000;
40const PRG_RAM_DEFAULT: usize = 0x2000;
41const NAMETABLE_SIZE: usize = 0x0400;
42const NAMETABLE_SIZE_U16: u16 = 0x0400;
43
44/// v2 appends one byte: the latched PPU A12 of the last CHR fetch, which picks
45/// the CHR register that drives SUROM / SOROM / SXROM's outer lines in 4 KiB
46/// CHR mode. Since v2.9.8 (ADR 0042) a v1 blob is refused; it used to load
47/// with that latch cleared.
48const SAVE_STATE_VERSION: u8 = 2;
49/// Size of one outer PRG-ROM half on SUROM / SXROM.
50const PRG_OUTER_256K: usize = 0x4_0000;
51/// One PRG-RAM bank.
52const PRG_RAM_BANK_8K: usize = 0x2000;
53
54/// MMC1 mapper.
55pub struct Mmc1 {
56 prg_rom: Box<[u8]>,
57 chr: Box<[u8]>,
58 prg_ram: Box<[u8]>,
59 /// Internal nametable VRAM (2 KiB) — owned by the console, but we keep it
60 /// here for now to mirror the NROM stop-gap until the PPU integration in
61 /// Sprint 2-1 moves CIRAM into the PPU.
62 vram: Box<[u8]>,
63 chr_is_ram: bool,
64
65 // MMC1 internal registers (5 bits each).
66 control: u8, // mirror, prg-mode, chr-mode
67 chr0: u8, // CHR bank 0 ($A000-$BFFF write target)
68 chr1: u8, // CHR bank 1 ($C000-$DFFF write target)
69 prg: u8, // PRG bank ($E000-$FFFF write target)
70
71 // 5-write protocol shift register + count.
72 shift: u8,
73 shift_count: u8,
74
75 // Cycle of the most recent serial-port write, accepted or ignored (for the
76 // consecutive-write bug; since v2.8.2 an ignored write counts too, per
77 // "ignores every write after the first"). `u64::MAX` means "no prior
78 // write to inhibit on".
79 last_write_cycle: u64,
80 cpu_cycle: u64,
81
82 /// The PPU A12 level, as the PPU last reported it through `notify_a12`.
83 /// In 4 KiB CHR mode it selects which CHR register drives the outer PRG /
84 /// PRG-RAM lines on SUROM / SOROM / SXROM (see [`Self::outer_reg`]).
85 /// Latched from the A12 notification, not from `ppu_read`: a debugger
86 /// peek of CHR (`Bus::debug_peek_ppu`) goes through `ppu_read` and must
87 /// not change CPU-side banking (PR #550 review).
88 chr_a12_high: bool,
89}
90
91impl Mmc1 {
92 /// Construct a new MMC1 mapper.
93 ///
94 /// `prg_rom` must be a multiple of 16 KiB (typical sizes: 32 KiB up to
95 /// 512 KiB for SXROM). CHR-RAM is selected when `chr_rom` is empty;
96 /// otherwise CHR-ROM length must be a non-zero multiple of 4 KiB.
97 /// `prg_ram_bytes` of 0 selects the default 8 KiB.
98 ///
99 /// # Errors
100 ///
101 /// Returns [`MapperError::Invalid`] when sizes don't match.
102 pub fn new(
103 prg_rom: Box<[u8]>,
104 chr_rom: Box<[u8]>,
105 initial_mirroring: Mirroring,
106 prg_ram_bytes: usize,
107 ) -> Result<Self, MapperError> {
108 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
109 return Err(MapperError::Invalid(format!(
110 "MMC1 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
111 prg_rom.len()
112 )));
113 }
114 let chr_is_ram = chr_rom.is_empty();
115 let chr: Box<[u8]> = if chr_is_ram {
116 vec![0u8; CHR_BANK_8K].into_boxed_slice()
117 } else if chr_rom.len().is_multiple_of(CHR_BANK_4K) {
118 chr_rom
119 } else {
120 return Err(MapperError::Invalid(format!(
121 "MMC1 CHR-ROM size {} is not a multiple of 4 KiB",
122 chr_rom.len()
123 )));
124 };
125
126 let prg_ram_size = if prg_ram_bytes == 0 {
127 PRG_RAM_DEFAULT
128 } else {
129 prg_ram_bytes
130 };
131
132 // Initial control: PRG mode 3 (fix last bank at $C000-$FFFF), CHR
133 // mode 0 (8 KiB), single-screen-A. Per MMC1 power-on per nesdev wiki:
134 // "Common sense suggests $0C as a likely starting value because it
135 // forces $C000-$FFFF to be the last 16 KiB of PRG and the
136 // initial mirroring is undefined; software should set it itself."
137 // We start with mirroring derived from the iNES header byte (rather
138 // than letting the mapper pick), matching most test ROMs that don't
139 // set the control register before issuing reads.
140 let initial_control = match initial_mirroring {
141 Mirroring::SingleScreenA => 0x0C,
142 Mirroring::SingleScreenB => 0x0D,
143 Mirroring::Horizontal => 0x0F,
144 // Vertical / FourScreen / MapperControlled: default to vertical
145 // layout (a sensible neutral pick for headers that don't strictly
146 // belong to MMC1 like four-screen).
147 _ => 0x0E,
148 };
149
150 Ok(Self {
151 prg_rom,
152 chr,
153 prg_ram: vec![0u8; prg_ram_size].into_boxed_slice(),
154 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
155 chr_is_ram,
156 control: initial_control,
157 chr0: 0,
158 chr1: 0,
159 prg: 0,
160 shift: 0x10, // bit 4 set marks "5 writes still needed"
161 shift_count: 0,
162 last_write_cycle: u64::MAX,
163 cpu_cycle: 0,
164 chr_a12_high: false,
165 })
166 }
167
168 /// The four internal registers, `(control, chr0, chr1, prg)`, for boards
169 /// that embed an MMC1 and resolve banking themselves (mapper 105,
170 /// NES-EVENT, v2.9.6). Read-only: the serial port stays the only writer.
171 pub(crate) const fn registers(&self) -> (u8, u8, u8, u8) {
172 (self.control, self.chr0, self.chr1, self.prg)
173 }
174
175 /// Bits collected by the serial port so far (0-4). A write that finds 4
176 /// here and leaves 0 without the reset bit committed a register; that is
177 /// how mapper 105 sees a write to `$A000` even when its value is unchanged.
178 pub(crate) const fn shift_count(&self) -> u8 {
179 self.shift_count
180 }
181
182 /// Map a PPU address in `$2000-$3EFF` to a 2 KiB-VRAM offset using the
183 /// current mirroring mode (control bits 1-0).
184 fn nametable_offset(&self, addr: u16) -> usize {
185 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
186 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
187 let physical = self.current_mirroring().physical_bank(table);
188 physical * NAMETABLE_SIZE + local
189 }
190
191 /// Does the board repurpose the CHR register's upper bits?
192 ///
193 /// SOROM, SUROM and SXROM "address only 8 KiB of CHR-ROM/-RAM" and route
194 /// the spare CHR lines to PRG A18 and PRG-RAM A13/A14
195 /// (`nesdev_wiki/MMC1.xhtml`). A board with more CHR uses those bits as
196 /// real CHR address lines, so nothing here applies to it.
197 fn chr_lines_repurposed(&self) -> bool {
198 self.chr.len() <= CHR_BANK_8K
199 }
200
201 /// The CHR register currently driving the outer lines: CHR bank 0 in 8 KiB
202 /// CHR mode; in 4 KiB mode, whichever register the last CHR fetch selected.
203 /// The wiki warns that mismatched registers make PRG "bankswitched ... as
204 /// the PPU renders", which is exactly this behaviour.
205 const fn outer_reg(&self) -> u8 {
206 if self.control & 0x10 != 0 && self.chr_a12_high {
207 self.chr1
208 } else {
209 self.chr0
210 }
211 }
212
213 /// Base offset of the 256 KiB PRG-ROM half selected by `P` (bit 4), or 0.
214 fn prg_outer_base(&self) -> usize {
215 if self.chr_lines_repurposed() && self.prg_rom.len() > PRG_OUTER_256K {
216 usize::from((self.outer_reg() >> 4) & 1) * PRG_OUTER_256K
217 } else {
218 0
219 }
220 }
221
222 /// Byte offset of the selected 8 KiB PRG-RAM bank: SXROM (32 KiB) uses
223 /// bits 3-2, SOROM (16 KiB) bit 3 only ("only implements the upper S").
224 fn prg_ram_bank_base(&self) -> usize {
225 if !self.chr_lines_repurposed() {
226 return 0;
227 }
228 let reg = usize::from(self.outer_reg());
229 match self.prg_ram.len() {
230 0x8000 => ((reg >> 2) & 0x03) * PRG_RAM_BANK_8K,
231 0x4000 => ((reg >> 3) & 0x01) * PRG_RAM_BANK_8K,
232 _ => 0,
233 }
234 }
235
236 /// Number of 16 KiB PRG banks the cartridge holds.
237 const fn prg_bank_count(&self) -> usize {
238 self.prg_rom.len() / PRG_BANK_16K
239 }
240
241 /// Resolve a CPU read at `$8000-$FFFF` into a PRG-ROM byte.
242 fn map_prg(&self, addr: u16) -> u8 {
243 let prg_mode = (self.control >> 2) & 0x03;
244 // The PRG register's 4 low bits address 16 x 16 KiB = 256 KiB; its bit
245 // 4 is the PRG-RAM disable, never a bank bit. SUROM / SXROM reach
246 // 512 KiB through the CHR register's `P` bit instead, which picks the
247 // 256 KiB half for the WHOLE window, fixed bank included (core audit
248 // §5.4; this used to claim all five PRG bits and read only four).
249 let outer = self.prg_outer_base();
250 // Never zero: `outer` is non-zero only on a ROM larger than 256 KiB, and
251 // the floor keeps mode 3's `bank_count - 1` and the modulo below safe
252 // even if that invariant is ever broken.
253 let bank_count = self
254 .prg_bank_count()
255 .saturating_sub(outer / PRG_BANK_16K)
256 .clamp(1, PRG_OUTER_256K / PRG_BANK_16K);
257 let prg_bank = self.prg & 0x0F;
258
259 let (bank_low, bank_high): (usize, usize) = match prg_mode {
260 0 | 1 => {
261 // 32 KiB switch: bank-low = prg & 0xE, bank-high = bank-low + 1
262 let bl = (prg_bank & 0x0E) as usize;
263 (bl, bl + 1)
264 }
265 2 => {
266 // First bank fixed to bank 0; second selectable
267 (0, prg_bank as usize)
268 }
269 _ => {
270 // Mode 3: first selectable; second fixed to last
271 (prg_bank as usize, bank_count - 1)
272 }
273 };
274 let bank_low = bank_low % bank_count;
275 let bank_high = bank_high % bank_count;
276
277 let offset_in_bank = (addr - 0x8000) as usize & (PRG_BANK_16K - 1);
278 let bank = if (addr & 0x4000) == 0 {
279 bank_low
280 } else {
281 bank_high
282 };
283 self.prg_rom[outer + bank * PRG_BANK_16K + offset_in_bank]
284 }
285
286 /// Resolve a PPU read at `$0000-$1FFF` into a CHR byte.
287 fn map_chr(&self, addr: u16) -> usize {
288 let chr_mode_8k = (self.control & 0x10) == 0;
289 if chr_mode_8k {
290 // 8 KiB CHR bank: CHR0 selects, low bit forced to 0.
291 let bank_count = (self.chr.len() / CHR_BANK_8K).max(1);
292 let bank = ((self.chr0 as usize) >> 1) % bank_count;
293 bank * CHR_BANK_8K + (addr as usize & (CHR_BANK_8K - 1))
294 } else {
295 // 4 KiB banks
296 let bank_count = (self.chr.len() / CHR_BANK_4K).max(1);
297 let bank = if addr < 0x1000 {
298 self.chr0 as usize
299 } else {
300 self.chr1 as usize
301 };
302 let bank = bank % bank_count;
303 bank * CHR_BANK_4K + (addr as usize & (CHR_BANK_4K - 1))
304 }
305 }
306
307 /// Is the `$6000-$7FFF` PRG-RAM window currently disabled?
308 ///
309 /// A2 (v2.2.3). MMC1 has **two** software write-protect layers and `RustyNES`
310 /// previously modelled neither, reading and writing `prg_ram`
311 /// unconditionally:
312 ///
313 /// * **`$E000` bit 4** — the PRG-RAM disable common to every MMC1 board.
314 /// Set = RAM deselected (`/CE` deasserted).
315 /// * **SNROM's second layer** — on a board whose CHR is 8 KiB of RAM, the
316 /// CHR bank register doubles as a PRG-RAM enable: `$A000` bit 4 is wired
317 /// to the RAM's second enable. Only meaningful when `chr_is_ram`, which
318 /// is how SNROM is distinguished from the CHR-ROM boards (SJROM etc.)
319 /// that route those bits to real CHR banking instead.
320 ///
321 /// Holy Mapperel distinguishes the two: `M1_P128K_C32K` (SJROM, CHR-ROM)
322 /// reported `1000` — the `$E000` layer alone — while `M1_P128K_CR8K`
323 /// (SNROM, CHR-RAM) reported `5000`, both layers
324 /// (`MAPTEST_WRAMEN2 $40 | MAPTEST_WRAMEN $10`).
325 ///
326 /// The second layer is SNROM's alone. On SUROM / SXROM (> 256 KiB PRG) the
327 /// same CHR bit 4 is PRG A18, and on SOROM / SXROM (> 8 KiB PRG-RAM) the
328 /// board wires the S bits instead, so neither is a RAM enable there.
329 fn prg_ram_disabled(&self) -> bool {
330 if self.prg & 0x10 != 0 {
331 return true;
332 }
333 let snrom = self.chr_is_ram
334 && self.prg_rom.len() <= PRG_OUTER_256K
335 && self.prg_ram.len() <= PRG_RAM_BANK_8K;
336 snrom && (self.outer_reg() & 0x10) != 0
337 }
338
339 /// Index into `prg_ram` for a `$6000-$7FFF` access, after banking.
340 fn prg_ram_index(&self, addr: u16) -> usize {
341 (self.prg_ram_bank_base() + usize::from(addr - 0x6000)) % self.prg_ram.len().max(1)
342 }
343
344 /// Apply a completed 5-bit write to the appropriate internal register.
345 const fn commit(&mut self, addr: u16, value: u8) {
346 match addr & 0xE000 {
347 0x8000 => self.control = value,
348 0xA000 => self.chr0 = value,
349 0xC000 => self.chr1 = value,
350 // 0xE000
351 _ => self.prg = value,
352 }
353 }
354}
355
356impl Mapper for Mmc1 {
357 fn sram(&self) -> &[u8] {
358 &self.prg_ram
359 }
360 fn sram_mut(&mut self) -> &mut [u8] {
361 &mut self.prg_ram
362 }
363 // v2.8.0 Phase 4 — MMC1 overrides ONLY notify_cpu_cycle (the
364 // consecutive-write throttle); it has no IRQ and no audio.
365 fn caps(&self) -> MapperCaps {
366 MapperCaps {
367 cpu_cycle_hook: true,
368 audio: false,
369 frame_event_hook: false,
370 irq_source: false,
371 }
372 }
373
374 /// A2: a disabled PRG-RAM window is not driven, so the databus floats.
375 ///
376 /// Same contract as the FME-7 fix: report the window unmapped and let the
377 /// bus preserve its open-bus latch, rather than inventing a byte here.
378 fn cpu_read_unmapped(&self, addr: u16) -> bool {
379 if matches!(addr, 0x6000..=0x7FFF) {
380 return self.prg_ram.is_empty() || self.prg_ram_disabled();
381 }
382 addr < 0x6000
383 }
384
385 fn cpu_read(&mut self, addr: u16) -> u8 {
386 match addr {
387 0x6000..=0x7FFF => {
388 if self.prg_ram.is_empty() {
389 0
390 } else {
391 self.prg_ram[self.prg_ram_index(addr)]
392 }
393 }
394 0x8000..=0xFFFF => self.map_prg(addr),
395 _ => 0,
396 }
397 }
398
399 fn cpu_write(&mut self, addr: u16, value: u8) {
400 match addr {
401 0x6000..=0x7FFF => {
402 // A2: a disabled window is write-protected (both layers).
403 if self.prg_ram_disabled() {
404 return;
405 }
406 if !self.prg_ram.is_empty() {
407 let idx = self.prg_ram_index(addr);
408 self.prg_ram[idx] = value;
409 }
410 }
411 0x8000..=0xFFFF => {
412 // Consecutive-cycle writes (nesdev MMC1): the serial port
413 // "ignores every write after the first", but "this restriction
414 // only applies to the data being written on bit 0; the bit 7
415 // reset is never ignored". So the filter is checked AFTER the
416 // reset, and every serial-port write -- ignored ones included
417 // -- counts as the last write. Until v2.8.2 the reset was
418 // filtered too, which *Shinsenden* (a reset on an `RRA abs,X`'s
419 // second write) needs not to be; the MiSTer RTL was right and
420 // this was wrong (RTL audit R-3.5a, inverted).
421 let consecutive = self.last_write_cycle != u64::MAX
422 && self.cpu_cycle == self.last_write_cycle.wrapping_add(1);
423 self.last_write_cycle = self.cpu_cycle;
424
425 if value & 0x80 != 0 {
426 // Reset: clear shift; OR control with $0C (force PRG mode 3).
427 self.shift = 0x10;
428 self.shift_count = 0;
429 self.control |= 0x0C;
430 return;
431 }
432 if consecutive {
433 return;
434 }
435 // Shift bit 0 of value into bit 4 of shift, sliding right.
436 // After 5 writes, bit 0 of (original) shift is the LSB of
437 // the latched value; equivalently: low 5 bits, LSB first.
438 let new_lsb = value & 0x01;
439 self.shift = (self.shift >> 1) | (new_lsb << 4);
440 self.shift_count += 1;
441 if self.shift_count == 5 {
442 let latched = self.shift & 0x1F;
443 self.commit(addr, latched);
444 self.shift = 0x10;
445 self.shift_count = 0;
446 }
447 }
448 _ => {}
449 }
450 }
451
452 fn chr_phys(&self, addr: u16) -> Option<u32> {
453 if self.chr_is_ram {
454 None
455 } else {
456 u32::try_from(self.map_chr(addr & 0x1FFF)).ok()
457 }
458 }
459
460 // The only writer of `chr_a12_high`. The PPU reports every A12 transition
461 // here (not gated on capabilities), including the ones its `$2007`
462 // accesses make, which is the line the MMC1 actually watches.
463 fn notify_a12(&mut self, level: bool) {
464 self.chr_a12_high = level;
465 }
466
467 fn ppu_read(&mut self, addr: u16) -> u8 {
468 let addr = addr & 0x3FFF;
469 match addr {
470 0x0000..=0x1FFF => {
471 let off = self.map_chr(addr);
472 self.chr[off]
473 }
474 0x2000..=0x3EFF => {
475 let off = self.nametable_offset(addr);
476 self.vram[off]
477 }
478 _ => 0,
479 }
480 }
481
482 fn ppu_write(&mut self, addr: u16, value: u8) {
483 let addr = addr & 0x3FFF;
484 match addr {
485 0x0000..=0x1FFF => {
486 if self.chr_is_ram {
487 let off = self.map_chr(addr);
488 self.chr[off] = value;
489 }
490 }
491 0x2000..=0x3EFF => {
492 let off = self.nametable_offset(addr);
493 self.vram[off] = value;
494 }
495 _ => {}
496 }
497 }
498
499 fn notify_cpu_cycle(&mut self) {
500 self.cpu_cycle = self.cpu_cycle.wrapping_add(1);
501 }
502
503 fn current_mirroring(&self) -> Mirroring {
504 match self.control & 0x03 {
505 0 => Mirroring::SingleScreenA,
506 1 => Mirroring::SingleScreenB,
507 2 => Mirroring::Vertical,
508 _ => Mirroring::Horizontal,
509 }
510 }
511
512 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
513 let mut info = crate::mapper::MapperDebugInfo {
514 mapper_id: 1,
515 name: "MMC1".into(),
516 mirroring: crate::mapper::mirroring_name(self.current_mirroring()),
517 ..Default::default()
518 };
519 info.prg_banks
520 .push(("PRG".into(), format!("{:#04x}", self.prg)));
521 info.chr_banks
522 .push(("CHR0".into(), format!("{:#04x}", self.chr0)));
523 info.chr_banks
524 .push(("CHR1".into(), format!("{:#04x}", self.chr1)));
525 info.extra
526 .push(("control".into(), format!("{:#04x}", self.control)));
527 info.extra.push((
528 "shift".into(),
529 format!("{:#04x} (count {})", self.shift, self.shift_count),
530 ));
531 info
532 }
533
534 fn save_state(&self) -> Vec<u8> {
535 // Tagged blob: [version, control, chr0, chr1, prg, shift, count,
536 // prg_ram..., vram..., chr_if_ram..., chr_a12_high (v2)]
537 let mut out = Vec::with_capacity(
538 8 + self.prg_ram.len()
539 + self.vram.len()
540 + if self.chr_is_ram { self.chr.len() } else { 0 },
541 );
542 out.push(SAVE_STATE_VERSION);
543 out.push(self.control);
544 out.push(self.chr0);
545 out.push(self.chr1);
546 out.push(self.prg);
547 out.push(self.shift);
548 out.push(self.shift_count);
549 out.extend_from_slice(&self.prg_ram);
550 out.extend_from_slice(&self.vram);
551 if self.chr_is_ram {
552 out.extend_from_slice(&self.chr);
553 }
554 out.push(u8::from(self.chr_a12_high));
555 out
556 }
557
558 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
559 let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
560 let version = *data.first().ok_or(MapperError::WrongLength {
561 expected: 1,
562 got: 0,
563 })?;
564 if version != SAVE_STATE_VERSION {
565 return Err(MapperError::UnsupportedVersion(version));
566 }
567 let expected = 7 + self.prg_ram.len() + self.vram.len() + need_chr + 1;
568 if data.len() != expected {
569 return Err(MapperError::WrongLength {
570 expected,
571 got: data.len(),
572 });
573 }
574 // The serial port holds at most four bits between commits (the fifth
575 // write commits and resets), in a 5-bit register. A restored count
576 // above 4 would wrap or overflow at the next write (NC-12, v2.9.9).
577 if data[6] > 4 || data[5] > 0x1F {
578 return Err(MapperError::Invalid(format!(
579 "MMC1 serial port out of range: shift {:#04x}, count {}",
580 data[5], data[6]
581 )));
582 }
583 self.control = data[1];
584 self.chr0 = data[2];
585 self.chr1 = data[3];
586 self.prg = data[4];
587 self.shift = data[5];
588 self.shift_count = data[6];
589 let mut cursor = 7;
590 self.prg_ram
591 .copy_from_slice(&data[cursor..cursor + self.prg_ram.len()]);
592 cursor += self.prg_ram.len();
593 self.vram
594 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
595 cursor += self.vram.len();
596 if self.chr_is_ram {
597 self.chr
598 .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
599 cursor += self.chr.len();
600 }
601 self.chr_a12_high = data[cursor] != 0;
602 Ok(())
603 }
604}
605
606#[cfg(test)]
607#[allow(clippy::cast_possible_truncation)]
608mod tests {
609 use super::*;
610
611 fn synth_prg(banks: usize) -> Box<[u8]> {
612 // Each bank starts with a marker byte equal to the bank index, then
613 // address-low rolls. Lets us check which bank an address resolves to.
614 let mut v = vec![0u8; banks * PRG_BANK_16K];
615 for b in 0..banks {
616 for o in 0..PRG_BANK_16K {
617 v[b * PRG_BANK_16K + o] = if o == 0 { b as u8 } else { (o & 0xFF) as u8 };
618 }
619 }
620 v.into_boxed_slice()
621 }
622
623 fn synth_chr(banks_4k: usize) -> Box<[u8]> {
624 let mut v = vec![0u8; banks_4k * CHR_BANK_4K];
625 for b in 0..banks_4k {
626 for o in 0..CHR_BANK_4K {
627 v[b * CHR_BANK_4K + o] = if o == 0 { b as u8 } else { (o ^ 0x55) as u8 };
628 }
629 }
630 v.into_boxed_slice()
631 }
632
633 /// Issue the 5 bit-writes that latch `value` into the register bank
634 /// selected by the high two address bits.
635 fn write5(m: &mut Mmc1, addr: u16, value: u8) {
636 for i in 0..5 {
637 let bit = (value >> i) & 1;
638 // Drive notify_cpu_cycle far enough between each write so the
639 // consecutive-write bug never fires in tests.
640 for _ in 0..3 {
641 m.notify_cpu_cycle();
642 }
643 m.cpu_write(addr, bit);
644 }
645 }
646
647 /// nesdev MMC1, "Consecutive-cycle writes": the serial port ignores a
648 /// write on the cycle after another, but "this restriction only applies to
649 /// the data being written on bit 0; the bit 7 reset is never ignored".
650 /// *Shinsenden* sets bit 7 on a read-modify-write's SECOND write (`RRA
651 /// abs,X`) "and will crash ... if this reset is ignored". Before v2.8.2 the
652 /// oracle filtered the reset too; the `MiSTer` RTL never did (RTL audit
653 /// R-3.5a had the two the wrong way round).
654 #[test]
655 fn a_reset_on_the_cycle_after_a_write_is_never_ignored() {
656 let mut m = Mmc1::new(synth_prg(4), synth_chr(2), Mirroring::Vertical, 0).unwrap();
657 write5(&mut m, 0x8000, 0b0_1000); // PRG mode 2: $C000 switchable
658 write5(&mut m, 0xE000, 1);
659 assert_eq!(m.cpu_read(0xC000), 1, "mode 2 set up");
660 for _ in 0..3 {
661 m.notify_cpu_cycle();
662 }
663 m.cpu_write(0x8000, 0x7F); // RMW's first write: data, accepted
664 m.notify_cpu_cycle();
665 m.cpu_write(0x8000, 0x80); // the next cycle: the reset
666 assert_eq!(
667 m.cpu_read(0xC000),
668 3,
669 "the reset ORs $0C into Control, so the last bank is fixed at $C000"
670 );
671 // The shift register was cleared too: a fresh 5-write sequence latches.
672 write5(&mut m, 0xE000, 2);
673 assert_eq!(m.cpu_read(0x8000), 2);
674 }
675
676 /// The other half of the same rule, as *Bill & Ted's Excellent Adventure*
677 /// needs it: `INC` on a `$FF` byte writes `$FF` (a reset) and then `$00`
678 /// on the next cycle, and that `$00` data write must be ignored.
679 #[test]
680 fn a_data_write_on_the_cycle_after_a_reset_is_ignored() {
681 let mut m = Mmc1::new(synth_prg(4), synth_chr(2), Mirroring::Vertical, 0).unwrap();
682 for _ in 0..3 {
683 m.notify_cpu_cycle();
684 }
685 m.cpu_write(0x8000, 0xFF);
686 m.notify_cpu_cycle();
687 m.cpu_write(0x8000, 0x00); // ignored: would misalign the next sequence
688 write5(&mut m, 0xE000, 2);
689 assert_eq!(m.cpu_read(0x8000), 2, "the ignored write left no stray bit");
690 }
691
692 #[test]
693 fn mmc1_default_is_prg_mode_3_last_bank_at_c000() {
694 // Build a 4-bank PRG (64 KiB). Default control sets PRG mode 3:
695 // bank 0 selectable @ $8000, last bank fixed @ $C000.
696 let mut m = Mmc1::new(synth_prg(4), synth_chr(2), Mirroring::Vertical, 0).unwrap();
697 // $C000 should map to bank 3 (last); marker byte = 3.
698 assert_eq!(m.cpu_read(0xC000), 3);
699 // $8000 starts at bank 0 (prg register defaults to 0).
700 assert_eq!(m.cpu_read(0x8000), 0);
701 }
702
703 #[test]
704 fn mmc1_prg_register_switches_first_bank_in_mode_3() {
705 let mut m = Mmc1::new(synth_prg(4), synth_chr(2), Mirroring::Vertical, 0).unwrap();
706 // Set PRG bank 2.
707 write5(&mut m, 0xE000, 2);
708 assert_eq!(m.cpu_read(0x8000), 2);
709 assert_eq!(m.cpu_read(0xC000), 3); // last bank still fixed
710 }
711
712 #[test]
713 fn mmc1_prg_mode_2_fixes_first_bank_at_8000() {
714 let mut m = Mmc1::new(synth_prg(4), synth_chr(2), Mirroring::Vertical, 0).unwrap();
715 // Control: prg-mode = 2 (bits 3-2 = 10), chr-mode = 0, mirror = horiz (3).
716 write5(&mut m, 0x8000, 0b0_1011);
717 // PRG register selects bank for $C000. Set to 1.
718 write5(&mut m, 0xE000, 1);
719 assert_eq!(m.cpu_read(0x8000), 0); // fixed bank 0
720 assert_eq!(m.cpu_read(0xC000), 1); // selectable
721 }
722
723 #[test]
724 fn mmc1_prg_mode_0_or_1_switches_32k() {
725 let mut m = Mmc1::new(synth_prg(4), synth_chr(2), Mirroring::Vertical, 0).unwrap();
726 // Control: prg-mode = 0 (bits 3-2 = 00), chr-mode = 0, mirror = horiz.
727 write5(&mut m, 0x8000, 0b0_0011);
728 // PRG register = 2 -> 32 KiB switch picks (bank 2, bank 3).
729 write5(&mut m, 0xE000, 2);
730 assert_eq!(m.cpu_read(0x8000), 2);
731 assert_eq!(m.cpu_read(0xC000), 3);
732 }
733
734 #[test]
735 fn mmc1_chr_mode_4k_switches_independent_banks() {
736 let mut m = Mmc1::new(synth_prg(2), synth_chr(4), Mirroring::Vertical, 0).unwrap();
737 // Control: chr-mode = 1 (bit 4 set), prg-mode = 3, mirror = horiz.
738 write5(&mut m, 0x8000, 0b1_1111);
739 // CHR0 = bank 1, CHR1 = bank 2.
740 write5(&mut m, 0xA000, 1);
741 write5(&mut m, 0xC000, 2);
742 assert_eq!(m.ppu_read(0x0000), 1);
743 assert_eq!(m.ppu_read(0x1000), 2);
744 }
745
746 #[test]
747 fn mmc1_chr_mode_8k_uses_chr0_only() {
748 let mut m = Mmc1::new(synth_prg(2), synth_chr(4), Mirroring::Vertical, 0).unwrap();
749 // chr-mode = 0 (bit 4 clear), prg-mode = 3, mirror = horiz.
750 write5(&mut m, 0x8000, 0b0_1111);
751 // CHR0 = 2 -> 8 KiB bank starts at CHR4K-bank (2 >> 1) * 8 KiB = bank index 1.
752 write5(&mut m, 0xA000, 2);
753 assert_eq!(m.ppu_read(0x0000), 2); // 4K-bank index 2 -> first byte
754 assert_eq!(m.ppu_read(0x1000), 3); // next 4K-bank
755 }
756
757 #[test]
758 fn mmc1_reset_bit_forces_prg_mode_3() {
759 let mut m = Mmc1::new(synth_prg(4), synth_chr(2), Mirroring::Vertical, 0).unwrap();
760 // Set prg-mode = 0 first.
761 write5(&mut m, 0x8000, 0b0_0011);
762 // Now write reset bit.
763 m.cpu_write(0x8000, 0x80);
764 // Control should have been ORed with $0C. With our prior control of
765 // 0b0_0011 (= 0x03), after | 0x0C = 0x0F (mode 3, mirror horiz).
766 // Last bank should be fixed at $C000.
767 assert_eq!(m.cpu_read(0xC000), 3);
768 }
769
770 #[test]
771 fn mmc1_consecutive_write_bug_drops_second_write() {
772 let mut m = Mmc1::new(synth_prg(4), synth_chr(2), Mirroring::Vertical, 0).unwrap();
773 // Write bit 0 of shift. Then on the very next cycle, write bit 1 —
774 // hardware drops it.
775 m.notify_cpu_cycle();
776 m.cpu_write(0xE000, 1); // accepted; shift = 0x10 -> 0x18 (bit 4 from new_lsb)
777 m.notify_cpu_cycle();
778 m.cpu_write(0xE000, 1); // dropped (cycle = last_write+1)
779 // Continue with 4 more accepted writes.
780 for _ in 0..4 {
781 for _ in 0..3 {
782 m.notify_cpu_cycle();
783 }
784 m.cpu_write(0xE000, 0);
785 }
786 // We accepted bits: 1, 0, 0, 0, 0 -> latched value = 0b00001 = 1.
787 // PRG register = 1, mode 3 default, so $8000 -> bank 1.
788 assert_eq!(m.cpu_read(0x8000), 1);
789 }
790
791 #[test]
792 fn mmc1_mirroring_switches() {
793 let mut m = Mmc1::new(synth_prg(2), synth_chr(2), Mirroring::Vertical, 0).unwrap();
794 // single-screen-A
795 write5(&mut m, 0x8000, 0b0_1100);
796 m.ppu_write(0x2000, 0xAA);
797 assert_eq!(m.ppu_read(0x2400), 0xAA);
798 assert_eq!(m.ppu_read(0x2800), 0xAA);
799 assert_eq!(m.ppu_read(0x2C00), 0xAA);
800 // single-screen-B
801 write5(&mut m, 0x8000, 0b0_1101);
802 m.ppu_write(0x2000, 0xBB); // writes physical bank 1
803 // All four nametables now alias to bank 1.
804 assert_eq!(m.ppu_read(0x2400), 0xBB);
805 // vertical
806 write5(&mut m, 0x8000, 0b0_1110);
807 m.ppu_write(0x2000, 0x11); // bank 0
808 m.ppu_write(0x2400, 0x22); // bank 1
809 assert_eq!(m.ppu_read(0x2800), 0x11); // mirror of $2000
810 assert_eq!(m.ppu_read(0x2C00), 0x22); // mirror of $2400
811 // horizontal
812 write5(&mut m, 0x8000, 0b0_1111);
813 m.ppu_write(0x2000, 0x33); // bank 0
814 m.ppu_write(0x2800, 0x44); // bank 1
815 assert_eq!(m.ppu_read(0x2400), 0x33); // mirror of $2000
816 assert_eq!(m.ppu_read(0x2C00), 0x44); // mirror of $2800
817 }
818
819 #[test]
820 fn mmc1_save_state_round_trip() {
821 let mut m = Mmc1::new(synth_prg(2), synth_chr(2), Mirroring::Vertical, 0).unwrap();
822 write5(&mut m, 0xE000, 1);
823 m.cpu_write(0x6010, 0xCC);
824 m.ppu_write(0x2000, 0xDD);
825 let blob = m.save_state();
826 let mut m2 = Mmc1::new(synth_prg(2), synth_chr(2), Mirroring::Vertical, 0).unwrap();
827 m2.load_state(&blob).unwrap();
828 assert_eq!(m2.cpu_read(0x6010), 0xCC);
829 assert_eq!(m2.ppu_read(0x2000), 0xDD);
830 // PRG register should have round-tripped.
831 assert_eq!(m2.cpu_read(0x8000), 1);
832 }
833
834 // ---------------------------------------------------------------
835 // A2 (v2.2.3): the two PRG-RAM write-protect layers.
836 // ---------------------------------------------------------------
837
838 /// `$E000` bit 4 is the disable common to every MMC1 board. Write-protect
839 /// AND read-float, on a CHR-ROM board where the SNROM layer is inert.
840 #[test]
841 fn mmc1_e000_bit4_write_protects_and_floats_prg_ram() {
842 let mut m = Mmc1::new(synth_prg(8), synth_chr(8), Mirroring::Vertical, 0).unwrap();
843 // Enabled by default: a write sticks and the window is mapped.
844 m.cpu_write(0x6000, 0xA5);
845 assert_eq!(m.cpu_read(0x6000), 0xA5);
846 assert!(
847 !m.cpu_read_unmapped(0x6000),
848 "enabled window must be mapped"
849 );
850
851 // $E000 bit 4 set -> RAM deselected.
852 write5(&mut m, 0xE000, 0x10);
853 assert!(m.cpu_read_unmapped(0x6000), "disabled window must float");
854 m.cpu_write(0x6000, 0x5A); // must be discarded
855
856 // Re-enable: the pre-disable byte survives, proving the write above
857 // was dropped rather than merely hidden by the float.
858 write5(&mut m, 0xE000, 0x00);
859 assert!(!m.cpu_read_unmapped(0x6000));
860 assert_eq!(
861 m.cpu_read(0x6000),
862 0xA5,
863 "write while disabled must not land"
864 );
865 }
866
867 /// SNROM's second layer: on a CHR-**RAM** board the CHR register's bit 4 is
868 /// wired to the RAM's other enable, so it disables PRG-RAM independently of
869 /// `$E000`. This is what separated Holy Mapperel's `5000` (SNROM) from
870 /// `1000` (SJROM).
871 #[test]
872 fn mmc1_snrom_chr_bit4_is_a_second_prg_ram_enable() {
873 // Empty CHR => chr_is_ram, i.e. an SNROM-class board.
874 let mut m = Mmc1::new(synth_prg(8), Box::new([]), Mirroring::Vertical, 0).unwrap();
875 m.cpu_write(0x6000, 0x3C);
876 assert_eq!(m.cpu_read(0x6000), 0x3C);
877
878 // $E000 stays ENABLED; only the CHR-register layer disables.
879 write5(&mut m, 0xA000, 0x10);
880 assert!(
881 m.cpu_read_unmapped(0x6000),
882 "SNROM: $A000 bit 4 alone must disable PRG-RAM"
883 );
884 m.cpu_write(0x6000, 0x99); // discarded
885
886 write5(&mut m, 0xA000, 0x00);
887 assert_eq!(m.cpu_read(0x6000), 0x3C);
888 }
889
890 /// The same CHR-register bit must NOT touch PRG-RAM on a CHR-ROM board —
891 /// there it is a real CHR bank select. This is the regression that would
892 /// break every SJROM/SUROM title if the layer were applied unconditionally.
893 #[test]
894 fn mmc1_chr_rom_board_ignores_the_snrom_layer() {
895 let mut m = Mmc1::new(synth_prg(8), synth_chr(8), Mirroring::Vertical, 0).unwrap();
896 m.cpu_write(0x6000, 0x77);
897 write5(&mut m, 0xA000, 0x10); // a CHR bank select, not a RAM enable
898 assert!(
899 !m.cpu_read_unmapped(0x6000),
900 "CHR-ROM board: $A000 bit 4 is CHR banking, must not gate PRG-RAM"
901 );
902 assert_eq!(m.cpu_read(0x6000), 0x77);
903 }
904
905 // ---- v2.7.2: SUROM / SOROM / SXROM (core audit §5.4) -----------------
906 //
907 // Written from nesdev_wiki/MMC1.xhtml §"SOROM, SUROM and SXROM": on boards
908 // that address only 8 KiB of CHR, the CHR bank register reads `PSSxC`.
909 // P (bit 4) selects the 256 KiB PRG-ROM half and "applies to all the PRG
910 // area, including the normally fixed bank"; SS (bits 3-2) select the 8 KiB
911 // PRG-RAM bank (SOROM wires only the upper S, SXROM both: bit 3 = A14,
912 // bit 2 = A13).
913
914 #[test]
915 fn surom_bit_4_selects_the_upper_256k_half_including_the_fixed_bank() {
916 // 512 KiB PRG (32 x 16 KiB), CHR-RAM: SUROM.
917 let mut m = Mmc1::new(synth_prg(32), Box::new([]), Mirroring::Vertical, 0).unwrap();
918 // Power-on: PRG mode 3, outer half 0 -> the fixed bank is bank 15.
919 assert_eq!(
920 m.cpu_read(0xC000),
921 15,
922 "fixed bank = last of the LOWER half"
923 );
924 write5(&mut m, 0xA000, 0x10); // P = 1
925 assert_eq!(m.cpu_read(0xC000), 31, "fixed bank follows the outer half");
926 write5(&mut m, 0xE000, 2);
927 assert_eq!(
928 m.cpu_read(0x8000),
929 18,
930 "switchable bank 2 of the upper half"
931 );
932 write5(&mut m, 0xA000, 0x00);
933 assert_eq!(m.cpu_read(0x8000), 2);
934 }
935
936 #[test]
937 fn surom_bit_4_is_not_snrom_prg_ram_disable() {
938 // On SNROM (<= 256 KiB, CHR-RAM) CHR bit 4 disables PRG-RAM; on SUROM
939 // the same line is PRG A18, so selecting the upper half must leave the
940 // RAM enabled.
941 let mut m = Mmc1::new(synth_prg(32), Box::new([]), Mirroring::Vertical, 0).unwrap();
942 write5(&mut m, 0xA000, 0x10);
943 m.cpu_write(0x6000, 0x5A);
944 assert_eq!(m.cpu_read(0x6000), 0x5A);
945 }
946
947 #[test]
948 fn sxrom_bits_3_and_2_bank_32k_of_prg_ram() {
949 let mut m = Mmc1::new(synth_prg(32), Box::new([]), Mirroring::Vertical, 0x8000).unwrap();
950 for bank in 0..4u8 {
951 write5(&mut m, 0xA000, bank << 2);
952 m.cpu_write(0x6000, 0xA0 | bank);
953 }
954 for bank in 0..4u8 {
955 write5(&mut m, 0xA000, bank << 2);
956 assert_eq!(
957 m.cpu_read(0x6000),
958 0xA0 | bank,
959 "bank {bank} kept its own byte"
960 );
961 assert_eq!(
962 m.sram()[usize::from(bank) * 0x2000],
963 0xA0 | bank,
964 "sram() is the whole 32 KiB, in bank order"
965 );
966 }
967 }
968
969 #[test]
970 fn sorom_banks_16k_of_prg_ram_with_bit_3_only() {
971 // 256 KiB PRG, CHR-RAM, 16 KiB PRG-RAM: SOROM. Bit 2 is unconnected.
972 let mut m = Mmc1::new(synth_prg(16), Box::new([]), Mirroring::Vertical, 0x4000).unwrap();
973 write5(&mut m, 0xA000, 0x00);
974 m.cpu_write(0x6000, 0x11);
975 write5(&mut m, 0xA000, 0x08);
976 m.cpu_write(0x6000, 0x22);
977 write5(&mut m, 0xA000, 0x04); // bit 2 alone: still bank 0
978 assert_eq!(m.cpu_read(0x6000), 0x11);
979 write5(&mut m, 0xA000, 0x0C);
980 assert_eq!(m.cpu_read(0x6000), 0x22);
981 }
982
983 #[test]
984 fn in_4k_chr_mode_the_outer_bits_follow_the_register_the_ppu_last_used() {
985 // "In 4KB CHR bank mode ... P and S bits in both CHR bank registers
986 // must be set to the same values, or the PRG-ROM and/or RAM will be
987 // bankswitched as the PPU renders" -- so the live register is the one
988 // selected by the last CHR fetch's A12.
989 let mut m = Mmc1::new(synth_prg(32), Box::new([]), Mirroring::Vertical, 0).unwrap();
990 write5(&mut m, 0x8000, 0b1_1110); // CHR 4 KiB mode, PRG mode 3
991 write5(&mut m, 0xA000, 0x00);
992 write5(&mut m, 0xC000, 0x10);
993 m.notify_a12(true); // PPU A12 high -> CHR bank 1 drives
994 assert_eq!(m.cpu_read(0xC000), 31);
995 m.notify_a12(false); // A12 low -> CHR bank 0
996 assert_eq!(m.cpu_read(0xC000), 15);
997 }
998
999 #[test]
1000 fn a_chr_peek_does_not_change_cpu_banking() {
1001 // `Bus::debug_peek_ppu` documents a side-effect-free sample and routes
1002 // CHR through `ppu_read`/`ppu_write`. Only the PPU's A12 notification
1003 // may move the live register (PR #550 review).
1004 let mut m = Mmc1::new(synth_prg(32), Box::new([]), Mirroring::Vertical, 0).unwrap();
1005 write5(&mut m, 0x8000, 0b1_1110);
1006 write5(&mut m, 0xA000, 0x00);
1007 write5(&mut m, 0xC000, 0x10);
1008 let _ = m.ppu_read(0x1000);
1009 m.ppu_write(0x1000, 0);
1010 assert_eq!(m.cpu_read(0xC000), 15, "still CHR bank 0's half");
1011 assert!(!m.chr_a12_high);
1012 }
1013
1014 #[test]
1015 fn chr_rom_boards_keep_bits_4_to_2_as_chr_banking() {
1016 // SKROM-style: 128 KiB CHR-ROM means the CHR register's upper bits are
1017 // real CHR address lines, never PRG / PRG-RAM selects.
1018 let mut m = Mmc1::new(synth_prg(16), synth_chr(32), Mirroring::Vertical, 0).unwrap();
1019 write5(&mut m, 0xA000, 0x1C);
1020 assert_eq!(
1021 m.cpu_read(0xC000),
1022 15,
1023 "no outer PRG bank on a CHR-ROM board"
1024 );
1025 m.cpu_write(0x6000, 0x33);
1026 write5(&mut m, 0xA000, 0x00);
1027 assert_eq!(
1028 m.cpu_read(0x6000),
1029 0x33,
1030 "no PRG-RAM banking on a CHR-ROM board"
1031 );
1032 }
1033
1034 #[test]
1035 fn a_pre_v2_7_2_save_state_is_refused() {
1036 // v1 blobs lack the trailing CHR-A12 latch byte. They loaded with the
1037 // latch cleared until v2.9.8, which reads the current layout only
1038 // (ADR 0042).
1039 let mut m = Mmc1::new(synth_prg(32), Box::new([]), Mirroring::Vertical, 0).unwrap();
1040 m.notify_a12(true);
1041 let mut blob = m.save_state();
1042 assert_eq!(blob[0], 2);
1043 assert_eq!(*blob.last().unwrap(), 1, "the latch is saved");
1044 let mut fresh = Mmc1::new(synth_prg(32), Box::new([]), Mirroring::Vertical, 0).unwrap();
1045 fresh.load_state(&blob).expect("a v2 blob loads");
1046 assert!(fresh.chr_a12_high, "the latch is restored");
1047 blob.pop();
1048 blob[0] = 1;
1049 assert!(matches!(
1050 m.load_state(&blob),
1051 Err(MapperError::UnsupportedVersion(1))
1052 ));
1053 assert!(m.load_state(&[9]).is_err(), "an unknown version is refused");
1054 }
1055
1056 /// NC-12 (v2.9.9 re-audit): a restored serial count above 4, or a shift
1057 /// register with bits above 4, is refused rather than wrapping (release)
1058 /// or overflowing (debug) at the next serial write.
1059 #[test]
1060 fn an_out_of_range_serial_port_is_refused_on_restore() {
1061 let m = Mmc1::new(synth_prg(4), synth_chr(2), Mirroring::Vertical, 0).unwrap();
1062 let good = m.save_state();
1063 let mut probe = Mmc1::new(synth_prg(4), synth_chr(2), Mirroring::Vertical, 0).unwrap();
1064 assert!(probe.load_state(&good).is_ok());
1065 for (idx, value) in [(6usize, 5u8), (6, 0xFF), (5, 0x20), (5, 0xFF)] {
1066 let mut bad = good.clone();
1067 bad[idx] = value;
1068 assert!(
1069 matches!(probe.load_state(&bad), Err(MapperError::Invalid(_))),
1070 "byte {idx} = {value:#04x} must be refused"
1071 );
1072 }
1073 let mut edge = good;
1074 edge[6] = 4;
1075 edge[5] = 0x1F;
1076 assert!(
1077 probe.load_state(&edge).is_ok(),
1078 "count 4, shift $1F is a real state"
1079 );
1080 }
1081}