rustynes_mappers/
m087_jaleco87.rs1#![allow(clippy::cast_possible_truncation, clippy::doc_markdown)]
20
21use crate::cartridge::Mirroring;
22use crate::mapper::{Mapper, MapperCaps, MapperError};
23use alloc::{boxed::Box, vec::Vec};
24use alloc::{format, vec};
25
26const PRG_BANK_16K: usize = 0x4000;
27const PRG_BANK_32K: usize = 0x8000;
28const CHR_BANK_8K: usize = 0x2000;
29const NAMETABLE_SIZE: usize = 0x0400;
30const NAMETABLE_SIZE_U16: u16 = 0x0400;
31
32const SAVE_STATE_VERSION: u8 = 1;
33
34pub struct Jaleco87 {
36 prg_rom: Box<[u8]>,
37 chr_rom: Box<[u8]>,
38 vram: Box<[u8]>,
39 chr_bank: u8,
40 mirroring: Mirroring,
41}
42
43impl Jaleco87 {
44 pub fn new(
51 prg_rom: Box<[u8]>,
52 chr_rom: Box<[u8]>,
53 mirroring: Mirroring,
54 ) -> Result<Self, MapperError> {
55 if prg_rom.len() != PRG_BANK_16K && prg_rom.len() != PRG_BANK_32K {
56 return Err(MapperError::Invalid(format!(
57 "Mapper-87 expects 16 or 32 KiB PRG, got {} bytes",
58 prg_rom.len()
59 )));
60 }
61 if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
62 return Err(MapperError::Invalid(format!(
63 "Mapper-87 expects non-empty CHR-ROM in 8 KiB units, got {} bytes",
64 chr_rom.len()
65 )));
66 }
67 Ok(Self {
68 prg_rom,
69 chr_rom,
70 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
71 chr_bank: 0,
72 mirroring,
73 })
74 }
75
76 const fn nametable_offset(&self, addr: u16) -> usize {
77 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
78 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
79 let physical = self.mirroring.physical_bank(table);
80 physical * NAMETABLE_SIZE + local
81 }
82
83 fn read_prg(&self, addr: u16) -> u8 {
84 let off = (addr - 0x8000) as usize;
85 if self.prg_rom.len() == PRG_BANK_16K {
86 self.prg_rom[off & (PRG_BANK_16K - 1)]
87 } else {
88 self.prg_rom[off]
89 }
90 }
91}
92
93impl Mapper for Jaleco87 {
94 fn has_hardwired_mirroring(&self) -> bool {
97 true
98 }
99
100 fn caps(&self) -> MapperCaps {
103 MapperCaps::NONE
104 }
105
106 fn cpu_read(&mut self, addr: u16) -> u8 {
107 if (0x8000..=0xFFFF).contains(&addr) {
108 self.read_prg(addr)
109 } else {
110 0
111 }
112 }
113
114 fn cpu_write(&mut self, addr: u16, value: u8) {
115 if (0x6000..=0x7FFF).contains(&addr) {
116 self.chr_bank = ((value >> 1) & 0x01) | ((value << 1) & 0x02);
119 }
120 }
121
122 fn ppu_read(&mut self, addr: u16) -> u8 {
123 let addr = addr & 0x3FFF;
124 match addr {
125 0x0000..=0x1FFF => {
126 let bank_count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
127 let bank = (self.chr_bank as usize) % bank_count;
128 self.chr_rom[bank * CHR_BANK_8K + (addr as usize)]
129 }
130 0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
131 _ => 0,
132 }
133 }
134
135 fn ppu_write(&mut self, addr: u16, value: u8) {
136 let addr = addr & 0x3FFF;
137 if let 0x2000..=0x3EFF = addr {
138 let off = self.nametable_offset(addr);
139 self.vram[off] = value;
140 }
141 }
143
144 fn current_mirroring(&self) -> Mirroring {
145 self.mirroring
146 }
147
148 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
149 let mut info = crate::mapper::MapperDebugInfo {
150 mapper_id: 87,
151 name: "Jaleco/Konami (87)".into(),
152 mirroring: crate::mapper::mirroring_name(self.mirroring),
153 ..Default::default()
154 };
155 info.chr_banks
156 .push(("CHR8k".into(), format!("{:#04x}", self.chr_bank)));
157 info
158 }
159
160 fn save_state(&self) -> Vec<u8> {
161 let mut out = Vec::with_capacity(2 + self.vram.len());
162 out.push(SAVE_STATE_VERSION);
163 out.push(self.chr_bank);
164 out.extend_from_slice(&self.vram);
165 out
166 }
167
168 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
169 let expected = 2 + self.vram.len();
170 if data.len() != expected {
171 return Err(MapperError::WrongLength {
172 expected,
173 got: data.len(),
174 });
175 }
176 if data[0] != SAVE_STATE_VERSION {
177 return Err(MapperError::UnsupportedVersion(data[0]));
178 }
179 self.chr_bank = data[1];
180 self.vram.copy_from_slice(&data[2..2 + self.vram.len()]);
181 Ok(())
182 }
183}
184
185#[cfg(test)]
186#[allow(clippy::cast_possible_truncation)]
187mod tests {
188 use super::*;
189
190 fn synth_prg(bytes: usize) -> Box<[u8]> {
191 let mut v = vec![0u8; bytes];
192 for (i, b) in v.iter_mut().enumerate() {
193 *b = (i & 0xFF) as u8;
194 }
195 v.into_boxed_slice()
196 }
197
198 fn synth_chr(banks: usize) -> Box<[u8]> {
199 let mut v = vec![0u8; banks * CHR_BANK_8K];
200 for b in 0..banks {
201 v[b * CHR_BANK_8K] = b as u8;
202 }
203 v.into_boxed_slice()
204 }
205
206 #[test]
207 fn bit_swapped_chr_select() {
208 let mut m =
209 Jaleco87::new(synth_prg(PRG_BANK_32K), synth_chr(4), Mirroring::Vertical).unwrap();
210 m.cpu_write(0x6000, 0b00);
212 assert_eq!(m.ppu_read(0x0000), 0);
213 m.cpu_write(0x6000, 0b01);
215 assert_eq!(m.ppu_read(0x0000), 2);
216 m.cpu_write(0x6000, 0b10);
218 assert_eq!(m.ppu_read(0x0000), 1);
219 m.cpu_write(0x6000, 0b11);
221 assert_eq!(m.ppu_read(0x0000), 3);
222 }
223
224 #[test]
225 fn write_outside_6000_7fff_ignored() {
226 let mut m =
227 Jaleco87::new(synth_prg(PRG_BANK_32K), synth_chr(4), Mirroring::Vertical).unwrap();
228 m.cpu_write(0x6000, 0b01); assert_eq!(m.ppu_read(0x0000), 2);
230 m.cpu_write(0x8000, 0b11); assert_eq!(m.ppu_read(0x0000), 2);
232 }
233
234 #[test]
235 fn prg_16k_mirrors() {
236 let mut m =
237 Jaleco87::new(synth_prg(PRG_BANK_16K), synth_chr(2), Mirroring::Vertical).unwrap();
238 assert_eq!(m.cpu_read(0x8000), m.cpu_read(0xC000));
239 }
240
241 #[test]
242 fn save_state_round_trip() {
243 let mut m =
244 Jaleco87::new(synth_prg(PRG_BANK_32K), synth_chr(4), Mirroring::Vertical).unwrap();
245 m.cpu_write(0x6000, 0b11);
246 let blob = m.save_state();
247 let mut m2 =
248 Jaleco87::new(synth_prg(PRG_BANK_32K), synth_chr(4), Mirroring::Vertical).unwrap();
249 m2.load_state(&blob).unwrap();
250 assert_eq!(m.ppu_read(0x0000), m2.ppu_read(0x0000));
251 }
252}