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