1#![allow(clippy::cast_possible_truncation, clippy::missing_const_for_fn)]
35
36use crate::cartridge::Mirroring;
37use crate::mapper::{Mapper, MapperCaps, MapperDebugInfo, MapperError};
38use alloc::{boxed::Box, format, vec, vec::Vec};
39
40const PRG_BANK_16K: usize = 0x4000;
41const CHR_BANK_8K: usize = 0x2000;
42const CHIP: usize = 512 * 1024;
43const SAVE_STATE_VERSION: u8 = 1;
44
45pub struct Action52M228 {
47 prg_rom: Box<[u8]>,
48 chr: Box<[u8]>,
49 chr_is_ram: bool,
50 vram: Box<[u8]>,
51 addr: u16,
53 data: u8,
55}
56
57impl Action52M228 {
58 pub fn new(prg_rom: Box<[u8]>, chr_rom: Box<[u8]>) -> Result<Self, MapperError> {
65 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
66 return Err(MapperError::Invalid(format!(
67 "mapper 228 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
68 prg_rom.len()
69 )));
70 }
71 if !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
72 return Err(MapperError::Invalid(format!(
73 "mapper 228 CHR-ROM size {} is not a multiple of 8 KiB",
74 chr_rom.len()
75 )));
76 }
77 let chr_is_ram = chr_rom.is_empty();
78 let chr = if chr_is_ram {
79 vec![0u8; CHR_BANK_8K].into_boxed_slice()
80 } else {
81 chr_rom
82 };
83 Ok(Self {
84 prg_rom,
85 chr,
86 chr_is_ram,
87 vram: vec![0u8; 0x800].into_boxed_slice(),
88 addr: 0x8000,
89 data: 0,
90 })
91 }
92
93 fn chip(&self) -> usize {
94 usize::from((self.addr >> 11) & 0x03)
95 }
96
97 fn chip_absent(&self) -> bool {
99 self.prg_rom.len() == 3 * CHIP && self.chip() == 2
100 }
101
102 fn prg_offset(&self, cpu: u16) -> usize {
104 let page = usize::from((self.addr >> 6) & 0x1F);
105 let page = if self.addr & 0x0020 != 0 {
106 page
107 } else if cpu < 0xC000 {
108 page & !1
109 } else {
110 page | 1
111 };
112 let chip = match (self.prg_rom.len() == 3 * CHIP, self.chip()) {
113 (true, 3) => 2,
115 (_, c) => c,
116 };
117 let bank = (chip * 32 + page) % (self.prg_rom.len() / PRG_BANK_16K);
118 bank * PRG_BANK_16K + (usize::from(cpu) & 0x3FFF)
119 }
120
121 fn chr_bank(&self) -> usize {
122 let bank = (usize::from(self.addr & 0x0F) << 2) | usize::from(self.data & 0x03);
123 bank % (self.chr.len() / CHR_BANK_8K)
124 }
125
126 fn mirroring(&self) -> Mirroring {
127 if self.addr & 0x2000 != 0 {
128 Mirroring::Horizontal
129 } else {
130 Mirroring::Vertical
131 }
132 }
133
134 fn nt_offset(&self, addr: u16) -> usize {
135 let table = ((addr - 0x2000) / 0x400) & 0x03;
136 self.mirroring().physical_bank(table as u8) * 0x400 + (usize::from(addr) & 0x3FF)
137 }
138}
139
140impl Mapper for Action52M228 {
141 fn caps(&self) -> MapperCaps {
142 MapperCaps::NONE
143 }
144
145 fn reset(&mut self) {
146 self.addr = 0x8000;
147 self.data = 0;
148 }
149
150 fn cpu_read_unmapped(&self, addr: u16) -> bool {
151 match addr {
152 0x4020..=0x7FFF => true,
153 _ => self.chip_absent(),
154 }
155 }
156
157 fn cpu_read(&mut self, addr: u16) -> u8 {
158 if addr >= 0x8000 && !self.chip_absent() {
159 self.prg_rom[self.prg_offset(addr)]
160 } else {
161 0
162 }
163 }
164
165 fn cpu_write(&mut self, addr: u16, value: u8) {
166 if addr >= 0x8000 {
167 self.addr = addr;
168 self.data = value;
169 }
170 }
171
172 fn chr_phys(&self, addr: u16) -> Option<u32> {
173 if self.chr_is_ram {
174 None
175 } else {
176 u32::try_from(self.chr_bank() * CHR_BANK_8K + usize::from(addr & 0x1FFF)).ok()
177 }
178 }
179
180 fn ppu_read(&mut self, addr: u16) -> u8 {
181 let addr = addr & 0x3FFF;
182 match addr {
183 0x0000..=0x1FFF => self.chr[self.chr_bank() * CHR_BANK_8K + usize::from(addr)],
184 0x2000..=0x3EFF => self.vram[self.nt_offset(addr)],
185 _ => 0,
186 }
187 }
188
189 fn ppu_write(&mut self, addr: u16, value: u8) {
190 let addr = addr & 0x3FFF;
191 match addr {
192 0x0000..=0x1FFF if self.chr_is_ram => {
193 let off = self.chr_bank() * CHR_BANK_8K + usize::from(addr);
194 self.chr[off] = value;
195 }
196 0x2000..=0x3EFF => {
197 let off = self.nt_offset(addr);
198 self.vram[off] = value;
199 }
200 _ => {}
201 }
202 }
203
204 fn nametable_address(&self, addr: u16) -> u16 {
205 u16::try_from(self.nt_offset(addr)).unwrap_or(0)
206 }
207
208 fn current_mirroring(&self) -> Mirroring {
209 self.mirroring()
210 }
211
212 fn debug_info(&self) -> MapperDebugInfo {
213 let mut info = MapperDebugInfo {
214 mapper_id: 228,
215 name: "Action 52 (228)".into(),
216 mirroring: crate::mapper::mirroring_name(self.mirroring()),
217 ..Default::default()
218 };
219 info.prg_banks
220 .push(("chip".into(), format!("{}", self.chip())));
221 info.prg_banks
222 .push(("page".into(), format!("{:#04x}", (self.addr >> 6) & 0x1F)));
223 info.chr_banks
224 .push(("bank".into(), format!("{:#04x}", self.chr_bank())));
225 info
226 }
227
228 fn save_state(&self) -> Vec<u8> {
229 let mut out = Vec::with_capacity(4 + self.vram.len() + CHR_BANK_8K);
230 out.push(SAVE_STATE_VERSION);
231 out.extend_from_slice(&self.addr.to_le_bytes());
232 out.push(self.data);
233 out.extend_from_slice(&self.vram);
234 if self.chr_is_ram {
235 out.extend_from_slice(&self.chr);
236 }
237 out
238 }
239
240 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
241 let chr = if self.chr_is_ram { self.chr.len() } else { 0 };
242 let expected = 4 + self.vram.len() + chr;
243 if data.len() != expected {
244 return Err(MapperError::WrongLength {
245 expected,
246 got: data.len(),
247 });
248 }
249 if data[0] != SAVE_STATE_VERSION {
250 return Err(MapperError::UnsupportedVersion(data[0]));
251 }
252 self.addr = u16::from_le_bytes([data[1], data[2]]) | 0x8000;
253 self.data = data[3];
254 self.vram.copy_from_slice(&data[4..4 + self.vram.len()]);
255 if self.chr_is_ram {
256 self.chr.copy_from_slice(&data[4 + self.vram.len()..]);
257 }
258 Ok(())
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 fn board(prg_16k: usize, chr_8k: usize) -> Action52M228 {
268 let mut prg = vec![0u8; prg_16k * PRG_BANK_16K];
269 for b in 0..prg_16k {
270 prg[b * PRG_BANK_16K] = b as u8;
271 }
272 let mut chr = vec![0u8; chr_8k * CHR_BANK_8K];
273 for b in 0..chr_8k {
274 chr[b * CHR_BANK_8K] = b as u8;
275 }
276 Action52M228::new(prg.into_boxed_slice(), chr.into_boxed_slice()).unwrap()
277 }
278
279 fn reg(horizontal: bool, chip: u16, page: u16, mirrored_16k: bool, chr_hi: u16) -> u16 {
282 0x8000
283 | (u16::from(horizontal) << 13)
284 | (chip << 11)
285 | (page << 6)
286 | (u16::from(mirrored_16k) << 5)
287 | chr_hi
288 }
289
290 #[test]
291 fn action52_chips_0_1_3_and_chip_2_is_open_bus() {
292 let mut m = board(96, 64); m.cpu_write(reg(false, 0, 5, true, 0), 0);
294 assert_eq!(m.cpu_read(0x8000), 5);
295 m.cpu_write(reg(false, 1, 5, true, 0), 0);
296 assert_eq!(m.cpu_read(0x8000), 32 + 5);
297 m.cpu_write(reg(false, 3, 5, true, 0), 0);
298 assert_eq!(
299 m.cpu_read(0x8000),
300 64 + 5,
301 "chip 3 is the image's third chip"
302 );
303 m.cpu_write(reg(false, 2, 5, true, 0), 0);
304 assert!(m.cpu_read_unmapped(0x8000), "chip 2 does not exist");
305 assert!(m.cpu_read_unmapped(0xC000));
306 }
307
308 #[test]
309 fn prg_size_bit_selects_32k_or_mirrored_16k() {
310 let mut m = board(96, 64);
311 m.cpu_write(reg(false, 0, 7, false, 0), 0);
312 assert_eq!(m.cpu_read(0x8000), 6, "32 KiB: P & ~1 at $8000");
313 assert_eq!(m.cpu_read(0xC000), 7, "32 KiB: P | 1 at $C000");
314 m.cpu_write(reg(false, 0, 6, true, 0), 0);
315 assert_eq!(m.cpu_read(0x8000), 6);
316 assert_eq!(m.cpu_read(0xC000), 6, "16 KiB mirrored");
317 }
318
319 #[test]
320 fn chr_bank_takes_four_address_bits_and_two_data_bits() {
321 let mut m = board(96, 64);
322 m.cpu_write(reg(false, 0, 0, true, 0x0B), 0x02);
323 assert_eq!(m.ppu_read(0x0000), (0x0B << 2) | 2);
324 m.cpu_write(reg(false, 0, 0, true, 0x0B), 0xFD); assert_eq!(m.ppu_read(0x0000), (0x0B << 2) | 1);
326 }
327
328 #[test]
329 fn mirroring_bit_and_reset() {
330 let mut m = board(96, 64);
331 m.cpu_write(reg(true, 1, 3, true, 1), 3);
332 assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
333 m.reset();
334 assert_eq!(m.current_mirroring(), Mirroring::Vertical);
335 assert_eq!(m.cpu_read(0x8000), 0, "reset = a write of $00 to $8000");
336 assert_eq!(m.ppu_read(0x0000), 0);
337 }
338
339 #[test]
340 fn a_single_chip_image_is_indexed_directly() {
341 let mut m = board(16, 8);
343 m.cpu_write(reg(false, 2, 3, true, 0), 0);
344 assert!(!m.cpu_read_unmapped(0x8000));
345 assert_eq!(m.cpu_read(0x8000), 3, "(2*32 + 3) wraps to bank 3");
346 }
347
348 #[test]
349 fn state_round_trips() {
350 let mut a = board(96, 64);
351 a.cpu_write(reg(true, 3, 9, false, 5), 1);
352 a.ppu_write(0x2001, 0x44);
353 let blob = a.save_state();
354 let mut b = board(96, 64);
355 b.load_state(&blob).unwrap();
356 assert_eq!(b.cpu_read(0x8000), a.cpu_read(0x8000));
357 assert_eq!(b.ppu_read(0x0000), a.ppu_read(0x0000));
358 assert_eq!(b.ppu_read(0x2001), 0x44);
359 assert_eq!(b.save_state(), blob);
360 }
361}