rustynes_mappers/
m152_bandai152.rs1#![allow(clippy::cast_possible_truncation, clippy::doc_markdown)]
27
28use crate::cartridge::Mirroring;
29use crate::mapper::{Mapper, MapperCaps, MapperError};
30use alloc::{boxed::Box, vec::Vec};
31use alloc::{format, vec};
32
33const PRG_BANK_16K: usize = 0x4000;
34const CHR_BANK_8K: usize = 0x2000;
35const NAMETABLE_SIZE: usize = 0x0400;
36const NAMETABLE_SIZE_U16: u16 = 0x0400;
37
38const SAVE_STATE_VERSION: u8 = 1;
39
40pub struct Bandai152 {
42 prg_rom: Box<[u8]>,
43 chr: Box<[u8]>,
44 vram: Box<[u8]>,
45 chr_is_ram: bool,
46 prg_bank: u8,
47 chr_bank: u8,
48 mirroring: Mirroring,
49}
50
51impl Bandai152 {
52 pub fn new(prg_rom: Box<[u8]>, chr_rom: Box<[u8]>) -> Result<Self, MapperError> {
63 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
64 return Err(MapperError::Invalid(format!(
65 "Bandai-152 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
66 prg_rom.len()
67 )));
68 }
69 let chr_is_ram = chr_rom.is_empty();
70 let chr: Box<[u8]> = if chr_is_ram {
71 vec![0u8; CHR_BANK_8K].into_boxed_slice()
72 } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
73 chr_rom
74 } else {
75 return Err(MapperError::Invalid(format!(
76 "Bandai-152 expects an 8 KiB multiple of CHR; got {} bytes",
77 chr_rom.len()
78 )));
79 };
80 Ok(Self {
81 prg_rom,
82 chr,
83 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
84 chr_is_ram,
85 prg_bank: 0,
86 chr_bank: 0,
87 mirroring: Mirroring::SingleScreenA,
89 })
90 }
91
92 const fn nametable_offset(&self, addr: u16) -> usize {
93 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
94 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
95 let physical = self.mirroring.physical_bank(table);
96 physical * NAMETABLE_SIZE + local
97 }
98
99 fn chr_offset(&self, addr: u16) -> usize {
100 let bank_count = (self.chr.len() / CHR_BANK_8K).max(1);
101 let bank = (self.chr_bank as usize) % bank_count;
102 bank * CHR_BANK_8K + (addr as usize & (CHR_BANK_8K - 1))
103 }
104}
105
106impl Mapper for Bandai152 {
107 fn caps(&self) -> MapperCaps {
110 MapperCaps::NONE
111 }
112
113 fn cpu_read(&mut self, addr: u16) -> u8 {
114 let bank_count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
115 match addr {
116 0x8000..=0xBFFF => {
117 let bank = (self.prg_bank as usize) % bank_count;
118 let off = (addr - 0x8000) as usize;
119 self.prg_rom[bank * PRG_BANK_16K + off]
120 }
121 0xC000..=0xFFFF => {
122 let last = bank_count - 1;
123 let off = (addr - 0xC000) as usize;
124 self.prg_rom[last * PRG_BANK_16K + off]
125 }
126 _ => 0,
127 }
128 }
129
130 fn cpu_write(&mut self, addr: u16, value: u8) {
131 if let 0x8000..=0xFFFF = addr {
132 self.mirroring = if (value & 0x80) != 0 {
135 Mirroring::SingleScreenB
136 } else {
137 Mirroring::SingleScreenA
138 };
139 self.prg_bank = (value >> 4) & 0x07;
140 self.chr_bank = value & 0x0F;
141 }
142 }
143
144 fn ppu_read(&mut self, addr: u16) -> u8 {
145 let addr = addr & 0x3FFF;
146 match addr {
147 0x0000..=0x1FFF => self.chr[self.chr_offset(addr)],
148 0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
149 _ => 0,
150 }
151 }
152
153 fn ppu_write(&mut self, addr: u16, value: u8) {
154 let addr = addr & 0x3FFF;
155 match addr {
156 0x0000..=0x1FFF => {
157 if self.chr_is_ram {
158 let off = self.chr_offset(addr);
159 self.chr[off] = value;
160 }
161 }
162 0x2000..=0x3EFF => {
163 let off = self.nametable_offset(addr);
164 self.vram[off] = value;
165 }
166 _ => {}
167 }
168 }
169
170 fn current_mirroring(&self) -> Mirroring {
171 self.mirroring
172 }
173
174 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
175 let mut info = crate::mapper::MapperDebugInfo {
176 mapper_id: 152,
177 name: "Bandai 74161/161 (152)".into(),
178 mirroring: crate::mapper::mirroring_name(self.mirroring),
179 ..Default::default()
180 };
181 info.prg_banks
182 .push(("PRG".into(), format!("{:#04x}", self.prg_bank)));
183 info.chr_banks
184 .push(("CHR".into(), format!("{:#04x}", self.chr_bank)));
185 info
186 }
187
188 fn save_state(&self) -> Vec<u8> {
189 let mut out = Vec::with_capacity(
190 4 + self.vram.len() + if self.chr_is_ram { self.chr.len() } else { 0 },
191 );
192 out.push(SAVE_STATE_VERSION);
193 out.push(self.prg_bank);
194 out.push(self.chr_bank);
195 out.push(u8::from(self.mirroring == Mirroring::SingleScreenB));
197 out.extend_from_slice(&self.vram);
198 if self.chr_is_ram {
199 out.extend_from_slice(&self.chr);
200 }
201 out
202 }
203
204 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
205 let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
206 let expected = 4 + self.vram.len() + need_chr;
207 if data.len() != expected {
208 return Err(MapperError::WrongLength {
209 expected,
210 got: data.len(),
211 });
212 }
213 if data[0] != SAVE_STATE_VERSION {
214 return Err(MapperError::UnsupportedVersion(data[0]));
215 }
216 self.prg_bank = data[1];
217 self.chr_bank = data[2];
218 self.mirroring = if data[3] != 0 {
219 Mirroring::SingleScreenB
220 } else {
221 Mirroring::SingleScreenA
222 };
223 let mut cursor = 4;
224 self.vram
225 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
226 cursor += self.vram.len();
227 if self.chr_is_ram {
228 self.chr
229 .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
230 }
231 Ok(())
232 }
233}
234
235#[cfg(test)]
236#[allow(clippy::cast_possible_truncation)]
237mod tests {
238 use super::*;
239
240 fn synth_prg(banks: usize) -> Box<[u8]> {
241 let mut v = vec![0u8; banks * PRG_BANK_16K];
242 for b in 0..banks {
243 v[b * PRG_BANK_16K] = b as u8;
244 }
245 v.into_boxed_slice()
246 }
247
248 fn synth_chr(banks_8k: usize) -> Box<[u8]> {
249 let mut v = vec![0u8; banks_8k * CHR_BANK_8K];
250 for b in 0..banks_8k {
251 v[b * CHR_BANK_8K] = b as u8;
252 }
253 v.into_boxed_slice()
254 }
255
256 #[test]
257 fn defaults_first_prg_last_fixed_and_screen_a() {
258 let mut m = Bandai152::new(synth_prg(8), synth_chr(4)).unwrap();
259 assert_eq!(m.cpu_read(0x8000), 0);
260 assert_eq!(m.cpu_read(0xC000), 7); assert_eq!(m.ppu_read(0x0000), 0);
262 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
263 }
264
265 #[test]
266 fn prg_chr_and_mirror_select() {
267 let mut m = Bandai152::new(synth_prg(8), synth_chr(8)).unwrap();
268 m.cpu_write(0x8000, 0xD3);
271 assert_eq!(m.cpu_read(0x8000), 5);
272 assert_eq!(m.cpu_read(0xC000), 7); assert_eq!(m.ppu_read(0x0000), 3);
274 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
275 m.cpu_write(0x8000, 0x53);
277 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
278 }
279
280 #[test]
281 fn prg_bank_masks_to_three_bits() {
282 let mut m = Bandai152::new(synth_prg(8), synth_chr(4)).unwrap();
284 m.cpu_write(0x8000, 0xF0); assert_eq!(m.cpu_read(0x8000), 7);
286 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
287 }
288
289 #[test]
290 fn chr_ram_round_trip() {
291 let mut m = Bandai152::new(synth_prg(2), Box::new([])).unwrap();
292 m.ppu_write(0x0010, 0xCD);
293 assert_eq!(m.ppu_read(0x0010), 0xCD);
294 }
295
296 #[test]
297 fn save_state_round_trip() {
298 let mut m = Bandai152::new(synth_prg(4), synth_chr(2)).unwrap();
299 m.cpu_write(0x8000, 0x91); let blob = m.save_state();
301 let mut m2 = Bandai152::new(synth_prg(4), synth_chr(2)).unwrap();
302 m2.load_state(&blob).unwrap();
303 assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
304 assert_eq!(m.ppu_read(0x0000), m2.ppu_read(0x0000));
305 assert_eq!(m.current_mirroring(), m2.current_mirroring());
306 }
307}