Skip to main content

rustynes_mappers/
m083_cony.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//! Mapper 83: Cony / Yoko (v2.9.6 "Roster").
3//!
4//! Written from `nesdev_wiki/output/INES_Mapper_083.md`. Three subtypes,
5//! told apart by NES 2.0 submapper or, on an iNES header, by CHR-ROM size as
6//! the page describes:
7//!
8//! | submapper | CHR-ROM | CHR banking | `$6000-$7FFF` |
9//! |---|---|---|---|
10//! | 0 | 256 KiB | 1 KiB x 8 | 8 KiB PRG-ROM (reg 3) when mode bit 5 is set, else open bus |
11//! | 1 | 512 KiB | 2 KiB x 4 (`$8310/1/6/7`) | as submapper 0 |
12//! | 2 | 1 MiB | 1 KiB x 8 inside a 256 KiB outer bank | 32 KiB WRAM, 8 KiB bank from `$8000` bits 6-7 |
13//!
14//! Registers (masks from the page): `$8000` PRG register 4 / outer bank
15//! (`$8300`), `$8100` mode (`$8300`), `$8200` IRQ low + acknowledge and `$8201`
16//! IRQ high + enable (`$8301`), `$8300-$8303` PRG registers (`$8313`), and
17//! `$8310-$8317` CHR registers (`$831F`).
18//!
19//! PRG modes (mode bits 3-4): 0 = 16 KiB at `$8000` from register 4 plus the
20//! last 16 KiB; 1 = 32 KiB from register 4 >> 1; 2 and 3 = three 8 KiB banks
21//! from registers 0-2 plus the last 8 KiB. On submapper 2, all of it is inside
22//! the 256 KiB outer bank from register 4 bits 4-5.
23//!
24//! IRQ: a 16-bit counter that, while enabled and non-zero, counts up (mode
25//! bit 6 clear) or down (set) every M2 cycle. On reaching zero it asserts and
26//! disables itself. `$8201` copies mode bit 7 to the (otherwise inaccessible)
27//! enable.
28//!
29//! The page gives the DIP switch (`$5000`) and scratch-RAM (`$5100-$5FFF`)
30//! masks only as "probably `$DF00`" / "probably `$DF03`". Taken literally,
31//! `$DF00` would also decode at `$7000`, which submappers 0 and 1 use for
32//! PRG-ROM, so both are decoded inside `$5000-$5FFF` only.
33
34// Bank arithmetic narrows values already reduced modulo a bank count, and the
35// accessors stay non-`const` to match the other mapper modules; both lints
36// are allowed crate-wide in the sibling modules for the same reasons.
37#![allow(clippy::cast_possible_truncation, clippy::missing_const_for_fn)]
38
39use crate::cartridge::Mirroring;
40use crate::mapper::{Mapper, MapperCaps, MapperDebugInfo, MapperError};
41use alloc::{boxed::Box, format, vec, vec::Vec};
42
43const PRG_BANK_8K: usize = 0x2000;
44const CHR_BANK_1K: usize = 0x0400;
45const WRAM_SUB2: usize = 0x8000;
46const SAVE_STATE_VERSION: u8 = 1;
47
48/// Mapper 83 (Cony / Yoko).
49pub struct Cony83 {
50    prg_rom: Box<[u8]>,
51    chr: Box<[u8]>,
52    chr_is_ram: bool,
53    vram: Box<[u8]>,
54    wram: Box<[u8]>,
55    submapper: u8,
56    mode: u8,
57    /// `$8000`: PRG register 4 / outer bank / WRAM bank.
58    reg4: u8,
59    prg_regs: [u8; 4],
60    chr_regs: [u8; 8],
61    scratch: [u8; 4],
62    dip: u8,
63    irq_counter: u16,
64    irq_enabled: bool,
65    irq_pending: bool,
66}
67
68impl Cony83 {
69    /// Construct the board. `submapper` is the NES 2.0 submapper, or `None`
70    /// on an iNES header, in which case the page's CHR-size heuristic picks it.
71    ///
72    /// # Errors
73    ///
74    /// [`MapperError::Invalid`] when PRG is not a non-zero multiple of 8 KiB
75    /// or CHR is not a multiple of 1 KiB.
76    pub fn new(
77        prg_rom: Box<[u8]>,
78        chr_rom: Box<[u8]>,
79        submapper: Option<u8>,
80    ) -> Result<Self, MapperError> {
81        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
82            return Err(MapperError::Invalid(format!(
83                "mapper 83 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
84                prg_rom.len()
85            )));
86        }
87        if !chr_rom.len().is_multiple_of(CHR_BANK_1K) {
88            return Err(MapperError::Invalid(format!(
89                "mapper 83 CHR-ROM size {} is not a multiple of 1 KiB",
90                chr_rom.len()
91            )));
92        }
93        let submapper = match submapper {
94            Some(s @ 0..=2) => s,
95            Some(_) => 0,
96            None => match chr_rom.len() {
97                0x8_0000 => 1,
98                0x10_0000 => 2,
99                _ => 0,
100            },
101        };
102        let chr_is_ram = chr_rom.is_empty();
103        let chr = if chr_is_ram {
104            vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
105        } else {
106            chr_rom
107        };
108        Ok(Self {
109            prg_rom,
110            chr,
111            chr_is_ram,
112            vram: vec![0u8; 0x800].into_boxed_slice(),
113            wram: vec![0u8; if submapper == 2 { WRAM_SUB2 } else { 0 }].into_boxed_slice(),
114            submapper,
115            mode: 0,
116            reg4: 0,
117            prg_regs: [0; 4],
118            chr_regs: [0; 8],
119            scratch: [0; 4],
120            dip: 0,
121            irq_counter: 0,
122            irq_enabled: false,
123            irq_pending: false,
124        })
125    }
126
127    /// Set the DIP switch (0-3) read at `$5000`.
128    pub fn set_dip(&mut self, dip: u8) {
129        self.dip = dip & 0x03;
130    }
131
132    /// The 8 KiB PRG bank for `addr` (`$8000-$FFFF`).
133    fn prg_bank(&self, addr: u16) -> usize {
134        let total = self.prg_rom.len() / PRG_BANK_8K;
135        let (base, window) = if self.submapper == 2 {
136            (usize::from((self.reg4 >> 4) & 0x03) * 32, 32.min(total))
137        } else {
138            (0, total)
139        };
140        let last = window - 1;
141        let slot = usize::from((addr >> 13) & 0x03);
142        let r4 = usize::from(self.reg4 & 0x0F);
143        let inner = match (self.mode >> 3) & 0x03 {
144            0 => {
145                if slot < 2 {
146                    r4 * 2 + slot
147                } else {
148                    // The last two banks of the window. `+ window` keeps the
149                    // sum non-negative on an 8 or 16 KiB image, where
150                    // `last - 3 + slot` underflowed; the modulo is a no-op on
151                    // any window of four banks or more.
152                    (last + window + slot - 3) % window
153                }
154            }
155            1 => (r4 >> 1) * 4 + slot,
156            _ => {
157                if slot < 3 {
158                    usize::from(self.prg_regs[slot])
159                } else {
160                    last
161                }
162            }
163        };
164        let inner = if self.submapper == 2 {
165            inner % window
166        } else {
167            inner
168        };
169        (base + inner) % total
170    }
171
172    fn chr_offset(&self, addr: u16) -> usize {
173        let within = usize::from(addr & 0x03FF);
174        let slot = usize::from((addr >> 10) & 0x07);
175        let bank = match self.submapper {
176            1 => {
177                // 2 KiB banks from `$8310`, `$8311`, `$8316`, `$8317`.
178                let reg = [0, 0, 1, 1, 6, 6, 7, 7][slot];
179                usize::from(self.chr_regs[reg]) * 2 + (slot & 1)
180            }
181            2 => usize::from(self.chr_regs[slot]) | (usize::from((self.reg4 >> 4) & 0x03) << 8),
182            _ => usize::from(self.chr_regs[slot]),
183        };
184        (bank % (self.chr.len() / CHR_BANK_1K)) * CHR_BANK_1K + within
185    }
186
187    fn mirroring(&self) -> Mirroring {
188        match self.mode & 0x03 {
189            0 => Mirroring::Vertical,
190            1 => Mirroring::Horizontal,
191            2 => Mirroring::SingleScreenA,
192            _ => Mirroring::SingleScreenB,
193        }
194    }
195
196    fn nt_offset(&self, addr: u16) -> usize {
197        let table = ((addr - 0x2000) / 0x400) & 0x03;
198        self.mirroring().physical_bank(table as u8) * 0x400 + usize::from(addr & 0x3FF)
199    }
200
201    /// Submappers 0/1: `$6000-$7FFF` is ROM when mode bit 5 is set.
202    fn low_rom_enabled(&self) -> bool {
203        self.submapper != 2 && self.mode & 0x20 != 0
204    }
205
206    fn wram_offset(&self, addr: u16) -> usize {
207        (usize::from(self.reg4 >> 6) * 0x2000 + usize::from(addr & 0x1FFF)) % self.wram.len()
208    }
209}
210
211impl Mapper for Cony83 {
212    fn sram(&self) -> &[u8] {
213        &self.wram
214    }
215
216    fn sram_mut(&mut self) -> &mut [u8] {
217        &mut self.wram
218    }
219
220    fn caps(&self) -> MapperCaps {
221        MapperCaps::CYCLE_IRQ
222    }
223
224    fn cpu_read_unmapped(&self, addr: u16) -> bool {
225        match addr {
226            0x4020..=0x4FFF => true,
227            0x6000..=0x7FFF => !self.low_rom_enabled() && self.wram.is_empty(),
228            // `$5000-$5FFF` (DIP switch, scratch RAM) and PRG-ROM.
229            _ => false,
230        }
231    }
232
233    fn cpu_read_driven_mask(&self, addr: u16) -> u8 {
234        if addr & 0xFF00 == 0x5000 { 0x03 } else { 0xFF }
235    }
236
237    fn cpu_read(&mut self, addr: u16) -> u8 {
238        match addr {
239            0x5000..=0x50FF => self.dip,
240            0x5100..=0x5FFF => self.scratch[usize::from(addr & 0x03)],
241            0x6000..=0x7FFF if self.low_rom_enabled() => {
242                let bank = usize::from(self.prg_regs[3]) % (self.prg_rom.len() / PRG_BANK_8K);
243                self.prg_rom[bank * PRG_BANK_8K + usize::from(addr & 0x1FFF)]
244            }
245            0x6000..=0x7FFF if !self.wram.is_empty() => self.wram[self.wram_offset(addr)],
246            0x8000..=0xFFFF => {
247                self.prg_rom[self.prg_bank(addr) * PRG_BANK_8K + usize::from(addr & 0x1FFF)]
248            }
249            _ => 0,
250        }
251    }
252
253    fn cpu_write(&mut self, addr: u16, value: u8) {
254        match addr {
255            0x5100..=0x5FFF => self.scratch[usize::from(addr & 0x03)] = value,
256            0x6000..=0x7FFF if !self.wram.is_empty() => {
257                let off = self.wram_offset(addr);
258                self.wram[off] = value;
259            }
260            0x8000..=0xFFFF => match addr & 0x8300 {
261                0x8000 => self.reg4 = value,
262                0x8100 => self.mode = value,
263                0x8200 => {
264                    if addr & 0x01 == 0 {
265                        self.irq_counter = (self.irq_counter & 0xFF00) | u16::from(value);
266                        self.irq_pending = false;
267                    } else {
268                        self.irq_counter = (self.irq_counter & 0x00FF) | (u16::from(value) << 8);
269                        self.irq_enabled = self.mode & 0x80 != 0;
270                    }
271                }
272                _ => match addr & 0x831F {
273                    r @ 0x8300..=0x8303 => self.prg_regs[usize::from(r & 0x03)] = value,
274                    r @ 0x8310..=0x8317 => self.chr_regs[usize::from(r & 0x07)] = value,
275                    _ => {}
276                },
277            },
278            _ => {}
279        }
280    }
281
282    fn notify_cpu_cycle(&mut self) {
283        if !self.irq_enabled || self.irq_counter == 0 {
284            return;
285        }
286        self.irq_counter = if self.mode & 0x40 != 0 {
287            self.irq_counter.wrapping_sub(1)
288        } else {
289            self.irq_counter.wrapping_add(1)
290        };
291        if self.irq_counter == 0 {
292            self.irq_pending = true;
293            self.irq_enabled = false;
294        }
295    }
296
297    fn irq_pending(&self) -> bool {
298        self.irq_pending
299    }
300
301    fn chr_phys(&self, addr: u16) -> Option<u32> {
302        if self.chr_is_ram {
303            None
304        } else {
305            u32::try_from(self.chr_offset(addr & 0x1FFF)).ok()
306        }
307    }
308
309    fn ppu_read(&mut self, addr: u16) -> u8 {
310        let addr = addr & 0x3FFF;
311        match addr {
312            0x0000..=0x1FFF => self.chr[self.chr_offset(addr)],
313            0x2000..=0x3EFF => self.vram[self.nt_offset(addr)],
314            _ => 0,
315        }
316    }
317
318    fn ppu_write(&mut self, addr: u16, value: u8) {
319        let addr = addr & 0x3FFF;
320        match addr {
321            0x0000..=0x1FFF if self.chr_is_ram => {
322                let off = self.chr_offset(addr);
323                self.chr[off] = value;
324            }
325            0x2000..=0x3EFF => {
326                let off = self.nt_offset(addr);
327                self.vram[off] = value;
328            }
329            _ => {}
330        }
331    }
332
333    fn nametable_address(&self, addr: u16) -> u16 {
334        u16::try_from(self.nt_offset(addr)).unwrap_or(0)
335    }
336
337    fn current_mirroring(&self) -> Mirroring {
338        self.mirroring()
339    }
340
341    fn debug_info(&self) -> MapperDebugInfo {
342        let mut info = MapperDebugInfo {
343            mapper_id: 83,
344            name: format!("Cony/Yoko (submapper {})", self.submapper),
345            mirroring: crate::mapper::mirroring_name(self.mirroring()),
346            ..Default::default()
347        };
348        info.prg_banks
349            .push(("mode".into(), format!("{}", (self.mode >> 3) & 3)));
350        info.prg_banks
351            .push(("R4".into(), format!("{:#04x}", self.reg4)));
352        for (i, r) in self.prg_regs.iter().enumerate() {
353            info.prg_banks.push((format!("R{i}"), format!("{r:#04x}")));
354        }
355        for (i, r) in self.chr_regs.iter().enumerate() {
356            info.chr_banks.push((format!("C{i}"), format!("{r:#04x}")));
357        }
358        info.irq_state
359            .push(("counter".into(), format!("{:#06x}", self.irq_counter)));
360        info.irq_state
361            .push(("enabled".into(), format!("{}", self.irq_enabled)));
362        info
363    }
364
365    fn save_state(&self) -> Vec<u8> {
366        let mut out = Vec::with_capacity(32 + self.vram.len() + self.wram.len());
367        out.push(SAVE_STATE_VERSION);
368        out.push(self.submapper);
369        out.push(self.mode);
370        out.push(self.reg4);
371        out.extend_from_slice(&self.prg_regs);
372        out.extend_from_slice(&self.chr_regs);
373        out.extend_from_slice(&self.scratch);
374        out.push(self.dip);
375        out.extend_from_slice(&self.irq_counter.to_le_bytes());
376        out.push(u8::from(self.irq_enabled));
377        out.push(u8::from(self.irq_pending));
378        out.extend_from_slice(&self.vram);
379        out.extend_from_slice(&self.wram);
380        if self.chr_is_ram {
381            out.extend_from_slice(&self.chr);
382        }
383        out
384    }
385
386    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
387        const HEAD: usize = 25;
388        let chr = if self.chr_is_ram { self.chr.len() } else { 0 };
389        let expected = HEAD + self.vram.len() + self.wram.len() + chr;
390        if data.len() != expected {
391            return Err(MapperError::WrongLength {
392                expected,
393                got: data.len(),
394            });
395        }
396        if data[0] != SAVE_STATE_VERSION {
397            return Err(MapperError::UnsupportedVersion(data[0]));
398        }
399        if data[1] != self.submapper {
400            return Err(MapperError::Invalid(format!(
401                "state is for mapper 83 submapper {}, this board is {}",
402                data[1], self.submapper
403            )));
404        }
405        self.mode = data[2];
406        self.reg4 = data[3];
407        self.prg_regs.copy_from_slice(&data[4..8]);
408        self.chr_regs.copy_from_slice(&data[8..16]);
409        self.scratch.copy_from_slice(&data[16..20]);
410        self.dip = data[20] & 0x03;
411        self.irq_counter = u16::from_le_bytes([data[21], data[22]]);
412        self.irq_enabled = data[23] != 0;
413        self.irq_pending = data[24] != 0;
414        let mut cur = HEAD;
415        self.vram.copy_from_slice(&data[cur..cur + self.vram.len()]);
416        cur += self.vram.len();
417        let n = self.wram.len();
418        self.wram.copy_from_slice(&data[cur..cur + n]);
419        cur += n;
420        if self.chr_is_ram {
421            self.chr.copy_from_slice(&data[cur..]);
422        }
423        Ok(())
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    fn board(prg_8k: usize, chr_1k: usize, sub: Option<u8>) -> Cony83 {
432        let mut prg = vec![0u8; prg_8k * PRG_BANK_8K];
433        for b in 0..prg_8k {
434            prg[b * PRG_BANK_8K] = b as u8;
435        }
436        let mut chr = vec![0u8; chr_1k * CHR_BANK_1K];
437        for b in 0..chr_1k {
438            chr[b * CHR_BANK_1K] = b as u8;
439            chr[b * CHR_BANK_1K + 1] = (b >> 8) as u8;
440        }
441        Cony83::new(prg.into_boxed_slice(), chr.into_boxed_slice(), sub).unwrap()
442    }
443
444    fn chr_at(m: &mut Cony83, addr: u16) -> usize {
445        usize::from(m.ppu_read(addr)) | usize::from(m.ppu_read(addr + 1)) << 8
446    }
447
448    #[test]
449    fn submapper_follows_chr_size_on_ines() {
450        assert_eq!(board(32, 256, None).submapper, 0);
451        assert_eq!(board(32, 512, None).submapper, 1);
452        assert_eq!(board(128, 1024, None).submapper, 2);
453        assert_eq!(board(32, 256, Some(2)).submapper, 2, "NES 2.0 wins");
454    }
455
456    #[test]
457    fn prg_modes_follow_the_page() {
458        let mut m = board(32, 256, None);
459        m.cpu_write(0x8000, 0x05);
460        m.cpu_write(0x8100, 0x00);
461        assert_eq!(m.cpu_read(0x8000), 10, "mode 0: 16 KiB bank 5");
462        assert_eq!(m.cpu_read(0xA000), 11);
463        assert_eq!(m.cpu_read(0xC000), 30, "the last 16 KiB");
464        assert_eq!(m.cpu_read(0xE000), 31);
465        m.cpu_write(0x8100, 0x08);
466        assert_eq!(m.cpu_read(0x8000), 8, "mode 1: 32 KiB bank 5>>1 = 2");
467        assert_eq!(m.cpu_read(0xE000), 11);
468        m.cpu_write(0x8100, 0x10);
469        m.cpu_write(0x8300, 3);
470        m.cpu_write(0x8301, 4);
471        m.cpu_write(0x8302, 5);
472        assert_eq!(m.cpu_read(0x8000), 3, "mode 2: registers 0-2");
473        assert_eq!(m.cpu_read(0xA000), 4);
474        assert_eq!(m.cpu_read(0xC000), 5);
475        assert_eq!(m.cpu_read(0xE000), 31);
476        m.cpu_write(0x8100, 0x18);
477        assert_eq!(m.cpu_read(0xA000), 4, "mode 3 = mode 2");
478    }
479
480    #[test]
481    fn prg_rom_at_6000_on_submappers_0_and_1() {
482        let mut m = board(32, 256, None);
483        m.cpu_write(0x8303, 7);
484        assert!(m.cpu_read_unmapped(0x6000), "bit 5 clear: open bus");
485        m.cpu_write(0x8100, 0x20);
486        assert!(!m.cpu_read_unmapped(0x6000));
487        assert_eq!(m.cpu_read(0x6000), 7);
488    }
489
490    #[test]
491    fn register_masks_mirror_the_decodes() {
492        let mut m = board(32, 256, None);
493        m.cpu_write(0x8100, 0x20);
494        m.cpu_write(0xB3E3, 6); // & $8313 = $8303: PRG register 3
495        assert_eq!(m.cpu_read(0x6000), 6);
496        m.cpu_write(0x8318, 0x44); // $8318-$831F: no function
497        m.cpu_write(0xBF14, 0x21); // & $831F = $8314: CHR register 4
498        assert_eq!(chr_at(&mut m, 0x1000), 0x21);
499    }
500
501    #[test]
502    fn submapper1_uses_2k_chr_registers() {
503        let mut m = board(32, 512, None);
504        m.cpu_write(0x8310, 3);
505        m.cpu_write(0x8311, 4);
506        m.cpu_write(0x8316, 5);
507        m.cpu_write(0x8317, 6);
508        m.cpu_write(0x8312, 0x7F); // no function on submapper 1
509        assert_eq!(chr_at(&mut m, 0x0000), 6);
510        assert_eq!(chr_at(&mut m, 0x0400), 7);
511        assert_eq!(chr_at(&mut m, 0x0800), 8);
512        assert_eq!(chr_at(&mut m, 0x1000), 10);
513        assert_eq!(chr_at(&mut m, 0x1C00), 13);
514    }
515
516    #[test]
517    fn submapper2_outer_bank_and_wram() {
518        let mut m = board(128, 1024, None);
519        m.cpu_write(0x8100, 0x10);
520        m.cpu_write(0x8300, 3);
521        m.cpu_write(0x8314, 5);
522        m.cpu_write(0x8000, 0x20); // outer bank 2
523        assert_eq!(m.cpu_read(0x8000), 64 + 3);
524        assert_eq!(
525            m.cpu_read(0xE000),
526            64 + 31,
527            "last bank of the outer 256 KiB"
528        );
529        assert_eq!(chr_at(&mut m, 0x1000), 0x205);
530        // WRAM banks from bits 6-7.
531        m.cpu_write(0x6000, 0xAA);
532        m.cpu_write(0x8000, 0x60);
533        m.cpu_write(0x6000, 0xBB);
534        m.cpu_write(0x8000, 0x20);
535        assert_eq!(m.cpu_read(0x6000), 0xAA);
536        assert_eq!(m.sram().len(), WRAM_SUB2);
537    }
538
539    #[test]
540    fn irq_counts_up_or_down_and_disables_at_zero() {
541        let mut m = board(32, 256, None);
542        m.cpu_write(0x8100, 0x80); // enable latch, count up
543        m.cpu_write(0x8200, 0xFE);
544        m.cpu_write(0x8201, 0xFF);
545        m.notify_cpu_cycle();
546        assert!(!m.irq_pending());
547        m.notify_cpu_cycle();
548        assert!(m.irq_pending(), "$FFFE + 2 = 0");
549        m.cpu_write(0x8200, 0x02);
550        assert!(!m.irq_pending(), "$8200 acknowledges");
551        m.cpu_write(0x8100, 0xC0); // count down
552        m.cpu_write(0x8201, 0x00);
553        m.notify_cpu_cycle();
554        m.notify_cpu_cycle();
555        assert!(m.irq_pending(), "2 - 2 = 0");
556        m.cpu_write(0x8200, 0x05);
557        m.notify_cpu_cycle();
558        assert_eq!(m.irq_counter, 5, "disabled after firing");
559        // The enable copies mode bit 7 at the $8201 write, not later.
560        m.cpu_write(0x8100, 0x40);
561        m.cpu_write(0x8201, 0x00);
562        m.cpu_write(0x8100, 0xC0);
563        m.notify_cpu_cycle();
564        assert_eq!(m.irq_counter, 5);
565    }
566
567    #[test]
568    fn dip_and_scratch_ram() {
569        let mut m = board(32, 256, None);
570        m.set_dip(2);
571        assert_eq!(m.cpu_read(0x5000) & 0x03, 2);
572        assert_eq!(m.cpu_read_driven_mask(0x5000), 0x03);
573        m.cpu_write(0x5102, 0x5A);
574        assert_eq!(m.cpu_read(0x5F06), 0x5A, "scratch byte 2, mask $DF03");
575    }
576
577    /// `Cony83::new` accepts any non-zero multiple of 8 KiB. On a 16 KiB image
578    /// mode 0's fixed half is the last two banks, which is the whole image, and
579    /// on an 8 KiB one it is bank 0 twice. `last - 3 + slot` underflowed there
580    /// (a panic with overflow checks on), so the fixed slots are computed in
581    /// the window.
582    #[test]
583    fn mode_0_fixed_half_on_a_small_image() {
584        let mut m = board(2, 256, None);
585        m.cpu_write(0x8100, 0x00);
586        assert_eq!(m.cpu_read(0xC000), 0, "second-to-last of two banks");
587        assert_eq!(m.cpu_read(0xE000), 1, "the last bank");
588        let mut m = board(1, 256, None);
589        m.cpu_write(0x8100, 0x00);
590        assert_eq!(m.cpu_read(0xC000), 0);
591        assert_eq!(m.cpu_read(0xE000), 0);
592    }
593
594    #[test]
595    fn state_round_trips() {
596        for chr in [256, 512, 1024] {
597            let mut a = board(128, chr, None);
598            a.cpu_write(0x8100, 0x93);
599            a.cpu_write(0x8000, 0x65);
600            a.cpu_write(0x8313, 0x22);
601            a.cpu_write(0x5101, 7);
602            a.cpu_write(0x6001, 3);
603            let blob = a.save_state();
604            let mut b = board(128, chr, None);
605            b.load_state(&blob).unwrap();
606            assert_eq!(b.save_state(), blob);
607        }
608    }
609}