Skip to main content

rustynes_mappers/
m018_jaleco_ss88006.rs

1//! Jaleco SS88006 (iNES mapper 18) implementation.
2//!
3//! Used by Ganbare Goemon Gaiden, Magical Kids Doropie (The Krion
4//! Conquest), Pizza Pop, etc. The mapper is wired only to A12-A14, A0-A1,
5//! and D0-D3, so every bank number is written as two 4-bit nibbles across a
6//! pair of sequential addresses (low nibble at the even/first address, high
7//! nibble at the next).
8//!
9//! # Banking (`nesdev_wiki/INES_Mapper_018.xhtml`)
10//!
11//! - PRG: three 8 KiB switchable banks at `$8000` / `$A000` / `$C000`; the
12//!   last 8 KiB is fixed at `$E000`. Bank 0 is 6 bits ($8000 low 4 + $8001
13//!   low 2); banks 1/2 are similar pairs ($8002/$8003, $9000/$9001).
14//! - CHR: eight 1 KiB banks; each is an 8-bit value split low4/high4 across
15//!   `$A000-$DFFF` address pairs.
16//! - Mirroring control at `$F002` (0 H, 1 V, 2 1scA, 3 1scB).
17//!
18//! # IRQ
19//!
20//! A 16-bit reload value is written as four nibbles to `$E000-$E003`. A
21//! write to `$F000` reloads the counter from the reload value and
22//! acknowledges. `$F001` selects the counter width via bits F/E/T (don't
23//! propagate the borrow past bit 12 / 8 / 4 respectively; F overrides E
24//! overrides T; none set = full 16-bit) and bit 0 enables counting. When
25//! enabled the counter counts down each M2 cycle; when the selected window
26//! borrows (underflows past its low bits) the IRQ asserts.
27
28#![allow(
29    clippy::cast_possible_truncation,
30    clippy::cast_lossless,
31    clippy::missing_const_for_fn,
32    clippy::doc_markdown,
33    clippy::struct_excessive_bools
34)]
35
36use crate::cartridge::Mirroring;
37use crate::mapper::{Mapper, MapperCaps, MapperError};
38use alloc::{boxed::Box, vec::Vec};
39use alloc::{format, vec};
40
41const PRG_BANK_8K: usize = 0x2000;
42const CHR_BANK_1K: usize = 0x0400;
43const NAMETABLE_SIZE: usize = 0x0400;
44const NAMETABLE_SIZE_U16: u16 = 0x0400;
45
46const SAVE_STATE_VERSION: u8 = 1;
47
48/// Jaleco SS88006 mapper (iNES mapper 18).
49pub struct JalecoSs88006 {
50    prg_rom: Box<[u8]>,
51    chr: Box<[u8]>,
52    vram: Box<[u8]>,
53    prg_ram: Box<[u8]>,
54    chr_is_ram: bool,
55
56    // Three switchable 8 KiB PRG banks.
57    prg_banks: [u8; 3],
58    // Eight 1 KiB CHR banks.
59    chr_banks: [u8; 8],
60    mirroring: Mirroring,
61
62    prg_ram_enable: bool,
63    prg_ram_write: bool,
64
65    // 16-bit IRQ reload value + counter.
66    irq_reload: u16,
67    irq_counter: u16,
68    irq_enabled: bool,
69    // Counter-width mask selected by the F/E/T bits ($F001 bits 3-1).
70    irq_width: IrqWidth,
71    irq_pending: bool,
72}
73
74/// Effective IRQ counter width selected by `$F001` bits F/E/T.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76enum IrqWidth {
77    Bits16,
78    Bits12,
79    Bits8,
80    Bits4,
81}
82
83impl IrqWidth {
84    const fn from_ctrl(value: u8) -> Self {
85        // F (bit 3) overrides E (bit 2) overrides T (bit 1).
86        if value & 0x08 != 0 {
87            Self::Bits4
88        } else if value & 0x04 != 0 {
89            Self::Bits8
90        } else if value & 0x02 != 0 {
91            Self::Bits12
92        } else {
93            Self::Bits16
94        }
95    }
96
97    /// Mask of the active counter bits.
98    const fn mask(self) -> u16 {
99        match self {
100            Self::Bits16 => 0xFFFF,
101            Self::Bits12 => 0x0FFF,
102            Self::Bits8 => 0x00FF,
103            Self::Bits4 => 0x000F,
104        }
105    }
106
107    fn to_byte(self) -> u8 {
108        match self {
109            Self::Bits16 => 0,
110            Self::Bits12 => 1,
111            Self::Bits8 => 2,
112            Self::Bits4 => 3,
113        }
114    }
115
116    const fn from_byte(b: u8) -> Self {
117        match b {
118            1 => Self::Bits12,
119            2 => Self::Bits8,
120            3 => Self::Bits4,
121            _ => Self::Bits16,
122        }
123    }
124}
125
126impl JalecoSs88006 {
127    /// Construct a new SS88006 mapper.
128    ///
129    /// `prg_rom` must be a non-zero multiple of 8 KiB; CHR-ROM must be a
130    /// multiple of 1 KiB (CHR-RAM allocated as 8 KiB when empty).
131    ///
132    /// # Errors
133    ///
134    /// Returns [`MapperError::Invalid`] on size mismatch.
135    pub fn new(
136        prg_rom: Box<[u8]>,
137        chr_rom: Box<[u8]>,
138        mirroring: Mirroring,
139    ) -> Result<Self, MapperError> {
140        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
141            return Err(MapperError::Invalid(format!(
142                "SS88006 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
143                prg_rom.len()
144            )));
145        }
146        let chr_is_ram = chr_rom.is_empty();
147        let chr: Box<[u8]> = if chr_is_ram {
148            vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
149        } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
150            chr_rom
151        } else {
152            return Err(MapperError::Invalid(format!(
153                "SS88006 CHR-ROM size {} is not a multiple of 1 KiB",
154                chr_rom.len()
155            )));
156        };
157        Ok(Self {
158            prg_rom,
159            chr,
160            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
161            prg_ram: vec![0u8; 8 * 1024].into_boxed_slice(),
162            chr_is_ram,
163            prg_banks: [0, 1, 2],
164            chr_banks: [0; 8],
165            mirroring,
166            prg_ram_enable: true,
167            prg_ram_write: true,
168            irq_reload: 0,
169            irq_counter: 0,
170            irq_enabled: false,
171            irq_width: IrqWidth::Bits16,
172            irq_pending: false,
173        })
174    }
175
176    const fn nametable_offset(&self, addr: u16) -> usize {
177        let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
178        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
179        let physical = self.mirroring.physical_bank(table);
180        physical * NAMETABLE_SIZE + local
181    }
182
183    fn prg_offset(&self, addr: u16) -> usize {
184        let total = (self.prg_rom.len() / PRG_BANK_8K).max(1);
185        let last = total - 1;
186        let bank = match addr & 0xE000 {
187            0x8000 => (self.prg_banks[0] as usize) % total,
188            0xA000 => (self.prg_banks[1] as usize) % total,
189            0xC000 => (self.prg_banks[2] as usize) % total,
190            0xE000 => last,
191            _ => 0,
192        };
193        bank * PRG_BANK_8K + (addr as usize & 0x1FFF)
194    }
195
196    fn chr_offset(&self, addr: u16) -> usize {
197        let slot = (addr as usize / CHR_BANK_1K) & 0x07;
198        let total = (self.chr.len() / CHR_BANK_1K).max(1);
199        let bank = (self.chr_banks[slot] as usize) % total;
200        bank * CHR_BANK_1K + (addr as usize & (CHR_BANK_1K - 1))
201    }
202
203    fn set_nibble(reg: &mut u8, high: bool, value: u8) {
204        let v = value & 0x0F;
205        if high {
206            *reg = (*reg & 0x0F) | (v << 4);
207        } else {
208            *reg = (*reg & 0xF0) | v;
209        }
210    }
211
212    /// Set one nibble of a 16-bit register at `nibble` index (0 = bits 0-3,
213    /// 1 = bits 4-7, ...).
214    fn set_reload_nibble(&mut self, nibble: u8, value: u8) {
215        let shift = (nibble & 0x03) * 4;
216        let v = (value as u16) & 0x0F;
217        self.irq_reload = (self.irq_reload & !(0x000F << shift)) | (v << shift);
218    }
219}
220
221impl Mapper for JalecoSs88006 {
222    fn sram(&self) -> &[u8] {
223        &self.prg_ram
224    }
225    fn sram_mut(&mut self) -> &mut [u8] {
226        &mut self.prg_ram
227    }
228    // v2.8.0 Phase 4 — CPU-cycle hook + IRQ source; no on-cart audio.
229    fn caps(&self) -> MapperCaps {
230        MapperCaps::CYCLE_IRQ
231    }
232
233    fn cpu_read(&mut self, addr: u16) -> u8 {
234        match addr {
235            0x6000..=0x7FFF => {
236                if self.prg_ram_enable {
237                    self.prg_ram[(addr - 0x6000) as usize % self.prg_ram.len()]
238                } else {
239                    0
240                }
241            }
242            0x8000..=0xFFFF => {
243                let off = self.prg_offset(addr);
244                self.prg_rom[off % self.prg_rom.len()]
245            }
246            _ => 0,
247        }
248    }
249
250    fn cpu_write(&mut self, addr: u16, value: u8) {
251        if (0x6000..=0x7FFF).contains(&addr) {
252            if self.prg_ram_enable && self.prg_ram_write {
253                let len = self.prg_ram.len();
254                self.prg_ram[(addr - 0x6000) as usize % len] = value;
255            }
256            return;
257        }
258        // Address decoded as A14-A12 (register group) + A1-A0 (register).
259        let reg = addr & 0xF003;
260        match reg {
261            // PRG bank 0 low / high (high only has 2 bits).
262            0x8000 => Self::set_nibble(&mut self.prg_banks[0], false, value),
263            0x8001 => {
264                let cur = self.prg_banks[0];
265                self.prg_banks[0] = (cur & 0x0F) | ((value & 0x03) << 4);
266            }
267            // PRG bank 1.
268            0x8002 => Self::set_nibble(&mut self.prg_banks[1], false, value),
269            0x8003 => {
270                let cur = self.prg_banks[1];
271                self.prg_banks[1] = (cur & 0x0F) | ((value & 0x03) << 4);
272            }
273            // PRG bank 2.
274            0x9000 => Self::set_nibble(&mut self.prg_banks[2], false, value),
275            0x9001 => {
276                let cur = self.prg_banks[2];
277                self.prg_banks[2] = (cur & 0x0F) | ((value & 0x03) << 4);
278            }
279            // PRG-RAM protect.
280            0x9002 => {
281                self.prg_ram_enable = (value & 0x01) != 0;
282                self.prg_ram_write = (value & 0x02) != 0;
283            }
284            // CHR banks 0-7, each low/high nibble pair.
285            0xA000 => Self::set_nibble(&mut self.chr_banks[0], false, value),
286            0xA001 => Self::set_nibble(&mut self.chr_banks[0], true, value),
287            0xA002 => Self::set_nibble(&mut self.chr_banks[1], false, value),
288            0xA003 => Self::set_nibble(&mut self.chr_banks[1], true, value),
289            0xB000 => Self::set_nibble(&mut self.chr_banks[2], false, value),
290            0xB001 => Self::set_nibble(&mut self.chr_banks[2], true, value),
291            0xB002 => Self::set_nibble(&mut self.chr_banks[3], false, value),
292            0xB003 => Self::set_nibble(&mut self.chr_banks[3], true, value),
293            0xC000 => Self::set_nibble(&mut self.chr_banks[4], false, value),
294            0xC001 => Self::set_nibble(&mut self.chr_banks[4], true, value),
295            0xC002 => Self::set_nibble(&mut self.chr_banks[5], false, value),
296            0xC003 => Self::set_nibble(&mut self.chr_banks[5], true, value),
297            0xD000 => Self::set_nibble(&mut self.chr_banks[6], false, value),
298            0xD001 => Self::set_nibble(&mut self.chr_banks[6], true, value),
299            0xD002 => Self::set_nibble(&mut self.chr_banks[7], false, value),
300            0xD003 => Self::set_nibble(&mut self.chr_banks[7], true, value),
301            // IRQ reload value nibbles.
302            0xE000 => self.set_reload_nibble(0, value),
303            0xE001 => self.set_reload_nibble(1, value),
304            0xE002 => self.set_reload_nibble(2, value),
305            0xE003 => self.set_reload_nibble(3, value),
306            // IRQ reload (copy reload value into counter + ack).
307            0xF000 => {
308                self.irq_counter = self.irq_reload;
309                self.irq_pending = false;
310            }
311            // IRQ counter size + enable + ack.
312            0xF001 => {
313                self.irq_enabled = (value & 0x01) != 0;
314                self.irq_width = IrqWidth::from_ctrl(value);
315                self.irq_pending = false;
316            }
317            // Mirroring.
318            0xF002 => {
319                self.mirroring = match value & 0x03 {
320                    0 => Mirroring::Horizontal,
321                    1 => Mirroring::Vertical,
322                    2 => Mirroring::SingleScreenA,
323                    _ => Mirroring::SingleScreenB,
324                };
325            }
326            // $F003: expansion sound (uPD7755C ADPCM) — not emulated.
327            _ => {}
328        }
329    }
330
331    fn ppu_read(&mut self, addr: u16) -> u8 {
332        let addr = addr & 0x3FFF;
333        match addr {
334            0x0000..=0x1FFF => {
335                let off = self.chr_offset(addr);
336                self.chr[off % self.chr.len()]
337            }
338            0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
339            _ => 0,
340        }
341    }
342
343    fn ppu_write(&mut self, addr: u16, value: u8) {
344        let addr = addr & 0x3FFF;
345        match addr {
346            0x0000..=0x1FFF => {
347                if self.chr_is_ram {
348                    let off = self.chr_offset(addr);
349                    let len = self.chr.len();
350                    self.chr[off % len] = value;
351                }
352            }
353            0x2000..=0x3EFF => {
354                let off = self.nametable_offset(addr);
355                self.vram[off] = value;
356            }
357            _ => {}
358        }
359    }
360
361    fn notify_cpu_cycle(&mut self) {
362        if !self.irq_enabled {
363            return;
364        }
365        // Count down only the active-width low bits; an IRQ fires when those
366        // bits borrow (i.e. were zero before the decrement). The unused high
367        // bits are preserved.
368        let mask = self.irq_width.mask();
369        let active = self.irq_counter & mask;
370        if active == 0 {
371            self.irq_pending = true;
372        }
373        let new_active = active.wrapping_sub(1) & mask;
374        self.irq_counter = (self.irq_counter & !mask) | new_active;
375    }
376
377    fn irq_pending(&self) -> bool {
378        self.irq_pending
379    }
380
381    fn current_mirroring(&self) -> Mirroring {
382        self.mirroring
383    }
384
385    fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
386        let mut info = crate::mapper::MapperDebugInfo {
387            mapper_id: 18,
388            name: "Jaleco SS88006 (18)".into(),
389            mirroring: crate::mapper::mirroring_name(self.mirroring),
390            ..Default::default()
391        };
392        for (i, b) in self.prg_banks.iter().enumerate() {
393            info.prg_banks
394                .push((format!("PRG{i}"), format!("{b:#04x}")));
395        }
396        for (i, b) in self.chr_banks.iter().enumerate() {
397            info.chr_banks
398                .push((format!("CHR{i}"), format!("{b:#04x}")));
399        }
400        info.irq_state
401            .push(("reload".into(), format!("{:#06x}", self.irq_reload)));
402        info.irq_state
403            .push(("counter".into(), format!("{:#06x}", self.irq_counter)));
404        info.irq_state
405            .push(("enabled".into(), format!("{}", self.irq_enabled)));
406        info.irq_state
407            .push(("pending".into(), format!("{}", self.irq_pending)));
408        info
409    }
410
411    fn save_state(&self) -> Vec<u8> {
412        let mut out = Vec::with_capacity(22 + self.vram.len() + self.prg_ram.len());
413        out.push(SAVE_STATE_VERSION);
414        out.extend_from_slice(&self.prg_banks);
415        out.extend_from_slice(&self.chr_banks);
416        out.push(self.mirroring as u8);
417        out.push(u8::from(self.prg_ram_enable));
418        out.push(u8::from(self.prg_ram_write));
419        out.extend_from_slice(&self.irq_reload.to_le_bytes());
420        out.extend_from_slice(&self.irq_counter.to_le_bytes());
421        out.push(u8::from(self.irq_enabled));
422        out.push(self.irq_width.to_byte());
423        out.push(u8::from(self.irq_pending));
424        out.extend_from_slice(&self.vram);
425        out.extend_from_slice(&self.prg_ram);
426        if self.chr_is_ram {
427            out.extend_from_slice(&self.chr);
428        }
429        out
430    }
431
432    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
433        let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
434        let expected = 22 + self.vram.len() + self.prg_ram.len() + need_chr;
435        if data.len() != expected {
436            return Err(MapperError::WrongLength {
437                expected,
438                got: data.len(),
439            });
440        }
441        if data[0] != SAVE_STATE_VERSION {
442            return Err(MapperError::UnsupportedVersion(data[0]));
443        }
444        self.prg_banks.copy_from_slice(&data[1..4]);
445        self.chr_banks.copy_from_slice(&data[4..12]);
446        self.mirroring = match data[12] {
447            0 => Mirroring::Horizontal,
448            1 => Mirroring::Vertical,
449            2 => Mirroring::SingleScreenA,
450            3 => Mirroring::SingleScreenB,
451            4 => Mirroring::FourScreen,
452            other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
453        };
454        self.prg_ram_enable = data[13] != 0;
455        self.prg_ram_write = data[14] != 0;
456        self.irq_reload = u16::from_le_bytes([data[15], data[16]]);
457        self.irq_counter = u16::from_le_bytes([data[17], data[18]]);
458        // Bytes 19/20/21 = enabled / width / pending.
459        let mut cursor = 19;
460        self.irq_enabled = data[cursor] != 0;
461        cursor += 1;
462        self.irq_width = IrqWidth::from_byte(data[cursor]);
463        cursor += 1;
464        self.irq_pending = data[cursor] != 0;
465        cursor += 1;
466        self.vram
467            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
468        cursor += self.vram.len();
469        self.prg_ram
470            .copy_from_slice(&data[cursor..cursor + self.prg_ram.len()]);
471        cursor += self.prg_ram.len();
472        if self.chr_is_ram {
473            self.chr
474                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
475        }
476        Ok(())
477    }
478}
479
480#[cfg(test)]
481#[allow(clippy::cast_possible_truncation)]
482mod tests {
483    use super::*;
484
485    fn synth_prg(banks_8k: usize) -> Box<[u8]> {
486        let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
487        for b in 0..banks_8k {
488            v[b * PRG_BANK_8K] = b as u8;
489        }
490        v.into_boxed_slice()
491    }
492
493    fn synth_chr(banks_1k: usize) -> Box<[u8]> {
494        let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
495        for b in 0..banks_1k {
496            v[b * CHR_BANK_1K] = b as u8;
497        }
498        v.into_boxed_slice()
499    }
500
501    #[test]
502    fn prg_defaults_and_fixed_last() {
503        let mut m = JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Vertical).unwrap();
504        assert_eq!(m.cpu_read(0x8000), 0);
505        assert_eq!(m.cpu_read(0xA000), 1);
506        assert_eq!(m.cpu_read(0xC000), 2);
507        assert_eq!(m.cpu_read(0xE000), 15); // last fixed
508    }
509
510    #[test]
511    fn prg_bank_nibble_pair() {
512        let mut m = JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Vertical).unwrap();
513        // PRG bank 0 = 0x0A: low nibble 0xA at $8000, high 2 bits 0 at $8001.
514        m.cpu_write(0x8000, 0x0A);
515        assert_eq!(m.cpu_read(0x8000), 0x0A);
516        // Set high 2 bits -> bank 0x1A masked to 16 banks = 0x0A still in range?
517        m.cpu_write(0x8001, 0x01); // high bits -> 0x1A = 26 % 16 = 10
518        assert_eq!(m.cpu_read(0x8000), 26 % 16);
519    }
520
521    #[test]
522    fn chr_bank_nibble_pair() {
523        let mut m = JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Vertical).unwrap();
524        // CHR bank 0 = 0x1B: low nibble 0xB at $A000, high nibble 0x1 at $A001.
525        m.cpu_write(0xA000, 0x0B);
526        m.cpu_write(0xA001, 0x01);
527        assert_eq!(m.ppu_read(0x0000), 0x1B);
528        // CHR bank 4 ($1000) via $C000/$C001.
529        m.cpu_write(0xC000, 0x05);
530        assert_eq!(m.ppu_read(0x1000), 5);
531    }
532
533    #[test]
534    fn mirroring_control() {
535        let mut m = JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Vertical).unwrap();
536        m.cpu_write(0xF002, 0); // Horizontal
537        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
538        m.cpu_write(0xF002, 1); // Vertical
539        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
540        m.cpu_write(0xF002, 3); // 1scB
541        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
542    }
543
544    #[test]
545    fn irq_reload_value_nibbles() {
546        let mut m = JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Vertical).unwrap();
547        m.cpu_write(0xE000, 0x0A);
548        m.cpu_write(0xE001, 0x0B);
549        m.cpu_write(0xE002, 0x0C);
550        m.cpu_write(0xE003, 0x0D);
551        assert_eq!(m.irq_reload, 0xDCBA);
552    }
553
554    #[test]
555    fn irq_16bit_counts_down_to_borrow() {
556        let mut m = JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Vertical).unwrap();
557        // Reload = 2.
558        m.cpu_write(0xE000, 0x02);
559        m.cpu_write(0xF000, 0x00); // reload counter
560        assert_eq!(m.irq_counter, 2);
561        m.cpu_write(0xF001, 0x01); // enable, 16-bit width
562        m.notify_cpu_cycle(); // 2 -> 1
563        m.notify_cpu_cycle(); // 1 -> 0
564        assert!(!m.irq_pending());
565        m.notify_cpu_cycle(); // 0 -> borrow -> IRQ
566        assert!(m.irq_pending());
567    }
568
569    #[test]
570    fn irq_width_8bit_ignores_high_bits() {
571        let mut m = JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Vertical).unwrap();
572        // Reload = 0x0102; in 8-bit width only low byte (0x02) counts.
573        m.cpu_write(0xE000, 0x02);
574        m.cpu_write(0xE001, 0x00);
575        m.cpu_write(0xE002, 0x01);
576        m.cpu_write(0xF000, 0x00);
577        // Enable + E bit (bit 2) = 8-bit width.
578        m.cpu_write(0xF001, 0x01 | 0x04);
579        m.notify_cpu_cycle(); // low byte 2 -> 1
580        m.notify_cpu_cycle(); // 1 -> 0
581        assert!(!m.irq_pending());
582        m.notify_cpu_cycle(); // borrow on low byte -> IRQ
583        assert!(m.irq_pending());
584        // High byte preserved.
585        assert_eq!(m.irq_counter & 0xFF00, 0x0100);
586    }
587
588    #[test]
589    fn irq_reload_acknowledges() {
590        let mut m = JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Vertical).unwrap();
591        m.irq_pending = true;
592        m.cpu_write(0xF000, 0x00);
593        assert!(!m.irq_pending());
594    }
595
596    #[test]
597    fn save_state_round_trip() {
598        let mut m =
599            JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Horizontal).unwrap();
600        m.cpu_write(0x8000, 0x05);
601        m.cpu_write(0xA000, 0x03);
602        m.cpu_write(0xE000, 0x07);
603        m.cpu_write(0xF000, 0x00);
604        m.cpu_write(0xF001, 0x05);
605        let blob = m.save_state();
606        let mut m2 =
607            JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Horizontal).unwrap();
608        m2.load_state(&blob).unwrap();
609        assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
610        assert_eq!(m.ppu_read(0x0000), m2.ppu_read(0x0000));
611        assert_eq!(m.irq_counter, m2.irq_counter);
612        assert_eq!(m.irq_width, m2.irq_width);
613    }
614}