rustynes_mappers/
m096_bandai96.rs1use crate::cartridge::Mirroring;
22use crate::mapper::{Mapper, MapperCaps, MapperError};
23use alloc::{boxed::Box, vec::Vec};
24use alloc::{format, vec};
25
26const PRG_BANK_32K: usize = 0x8000;
27const CHR_BANK_4K: usize = 0x1000;
28const NAMETABLE_SIZE: usize = 0x0400;
29const NAMETABLE_SIZE_U16: u16 = 0x0400;
30
31const SAVE_STATE_VERSION: u8 = 1;
32
33const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
38 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
39 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
40 let physical = mirroring.physical_bank(table);
41 physical * NAMETABLE_SIZE + local
42}
43
44pub struct Bandai96 {
46 prg_rom: Box<[u8]>,
47 chr: Box<[u8]>,
48 vram: Box<[u8]>,
49 chr_is_ram: bool,
50 prg_bank: u8,
51 outer_chr: u8,
52 inner_chr: u8,
53 last_ppu_addr: u16,
54 mirroring: Mirroring,
55}
56
57impl Bandai96 {
58 pub fn new(
65 prg_rom: Box<[u8]>,
66 chr_rom: Box<[u8]>,
67 mirroring: Mirroring,
68 ) -> Result<Self, MapperError> {
69 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
70 return Err(MapperError::Invalid(format!(
71 "mapper 96 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
72 prg_rom.len()
73 )));
74 }
75 let chr_is_ram = chr_rom.is_empty();
76 let chr: Box<[u8]> = if chr_is_ram {
77 vec![0u8; 2 * CHR_BANK_4K].into_boxed_slice()
79 } else if chr_rom.len().is_multiple_of(CHR_BANK_4K) {
80 chr_rom
81 } else {
82 return Err(MapperError::Invalid(format!(
83 "mapper 96 CHR-ROM size {} is not a multiple of 4 KiB",
84 chr_rom.len()
85 )));
86 };
87 Ok(Self {
88 prg_rom,
89 chr,
90 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
91 chr_is_ram,
92 prg_bank: 0,
93 outer_chr: 0,
94 inner_chr: 0,
95 last_ppu_addr: 0,
96 mirroring,
97 })
98 }
99
100 fn chr_count_4k(&self) -> usize {
101 (self.chr.len() / CHR_BANK_4K).max(1)
102 }
103
104 fn chr_offset(&self, addr: u16) -> usize {
105 let slot = (addr >> 12) & 0x01;
107 let bank = if slot == 0 {
108 self.outer_chr | self.inner_chr
109 } else {
110 self.outer_chr | 0x03
111 };
112 let bank = (bank as usize) % self.chr_count_4k();
113 bank * CHR_BANK_4K + (addr as usize & (CHR_BANK_4K - 1))
114 }
115}
116
117impl Mapper for Bandai96 {
118 fn chr_reads_are_pure(&self) -> bool {
121 false
122 }
123
124 fn caps(&self) -> MapperCaps {
125 MapperCaps::NONE
126 }
127
128 fn cpu_read(&mut self, addr: u16) -> u8 {
129 if (0x8000..=0xFFFF).contains(&addr) {
130 let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
131 let bank = (self.prg_bank as usize) % count;
132 self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
133 } else {
134 0
135 }
136 }
137
138 fn cpu_write(&mut self, addr: u16, value: u8) {
139 if (0x8000..=0xFFFF).contains(&addr) {
140 self.prg_bank = value & 0x03;
141 self.outer_chr = value & 0x04;
142 }
143 }
144
145 fn ppu_read(&mut self, addr: u16) -> u8 {
146 let masked = addr & 0x3FFF;
147 if (self.last_ppu_addr & 0x3000) != 0x2000 && (masked & 0x3000) == 0x2000 {
150 self.inner_chr = ((masked >> 8) & 0x03) as u8;
151 }
152 self.last_ppu_addr = masked;
153 match masked {
154 0x0000..=0x1FFF => self.chr[self.chr_offset(masked)],
155 0x2000..=0x3EFF => self.vram[nametable_offset(masked, self.mirroring)],
156 _ => 0,
157 }
158 }
159
160 fn ppu_write(&mut self, addr: u16, value: u8) {
161 let masked = addr & 0x3FFF;
162 if (self.last_ppu_addr & 0x3000) != 0x2000 && (masked & 0x3000) == 0x2000 {
163 self.inner_chr = ((masked >> 8) & 0x03) as u8;
164 }
165 self.last_ppu_addr = masked;
166 match masked {
167 0x0000..=0x1FFF => {
168 if self.chr_is_ram {
169 let off = self.chr_offset(masked);
170 self.chr[off] = value;
171 }
172 }
173 0x2000..=0x3EFF => {
174 let off = nametable_offset(masked, self.mirroring);
175 self.vram[off] = value;
176 }
177 _ => {}
178 }
179 }
180
181 fn current_mirroring(&self) -> Mirroring {
182 self.mirroring
183 }
184
185 fn save_state(&self) -> Vec<u8> {
186 let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
187 let mut out = Vec::with_capacity(6 + self.vram.len() + chr_extra);
188 out.push(SAVE_STATE_VERSION);
189 out.push(self.prg_bank);
190 out.push(self.outer_chr);
191 out.push(self.inner_chr);
192 out.push((self.last_ppu_addr & 0xFF) as u8);
193 out.push((self.last_ppu_addr >> 8) as u8);
194 out.extend_from_slice(&self.vram);
195 if self.chr_is_ram {
196 out.extend_from_slice(&self.chr);
197 }
198 out
199 }
200
201 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
202 let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
203 let expected = 6 + self.vram.len() + chr_extra;
204 if data.len() != expected {
205 return Err(MapperError::WrongLength {
206 expected,
207 got: data.len(),
208 });
209 }
210 if data[0] != SAVE_STATE_VERSION {
211 return Err(MapperError::UnsupportedVersion(data[0]));
212 }
213 self.prg_bank = data[1];
214 self.outer_chr = data[2];
215 self.inner_chr = data[3];
216 self.last_ppu_addr = u16::from(data[4]) | (u16::from(data[5]) << 8);
217 let mut cursor = 6;
218 self.vram
219 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
220 cursor += self.vram.len();
221 if self.chr_is_ram {
222 self.chr
223 .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
224 }
225 Ok(())
226 }
227}
228
229#[cfg(test)]
230#[allow(clippy::cast_possible_truncation)]
231mod tests {
232 use super::*;
233
234 fn synth_prg_32k(banks: usize) -> Box<[u8]> {
235 let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
236 for b in 0..banks {
237 v[b * PRG_BANK_32K] = b as u8;
238 }
239 v.into_boxed_slice()
240 }
241
242 fn synth_chr_4k(banks: usize) -> Box<[u8]> {
243 let mut v = vec![0u8; banks * CHR_BANK_4K];
244 for b in 0..banks {
245 v[b * CHR_BANK_4K] = b as u8;
246 }
247 v.into_boxed_slice()
248 }
249
250 #[test]
251 fn m96_prg_and_outer_chr() {
252 let mut m =
253 Bandai96::new(synth_prg_32k(4), synth_chr_4k(8), Mirroring::Horizontal).unwrap();
254 m.cpu_write(0x8000, 0b0000_0011); assert_eq!(m.cpu_read(0x8000), 3);
257 assert_eq!(m.ppu_read(0x1000), 3);
259 }
260
261 #[test]
262 fn m96_inner_chr_latched_from_ppu_bus() {
263 let mut m =
264 Bandai96::new(synth_prg_32k(4), synth_chr_4k(8), Mirroring::Horizontal).unwrap();
265 m.cpu_write(0x8000, 0);
267 let _ = m.ppu_read(0x0000);
270 let _ = m.ppu_read(0x2100);
271 assert_eq!(m.ppu_read(0x0000), 1);
273 let _ = m.ppu_read(0x0000);
275 let _ = m.ppu_read(0x2300);
276 assert_eq!(m.ppu_read(0x0000), 3);
277 }
278
279 #[test]
280 fn m96_save_state_round_trips_ppu_bus_latch() {
281 let mut b =
282 Bandai96::new(synth_prg_32k(4), synth_chr_4k(8), Mirroring::Horizontal).unwrap();
283 b.cpu_write(0x8000, 0);
284 let _ = b.ppu_read(0x0000);
285 let _ = b.ppu_read(0x2200);
286 let blob = b.save_state();
287 let mut b2 =
288 Bandai96::new(synth_prg_32k(4), synth_chr_4k(8), Mirroring::Horizontal).unwrap();
289 b2.load_state(&blob).unwrap();
290 assert_eq!(b2.ppu_read(0x0000), b.ppu_read(0x0000));
291 }
292}