1#![allow(clippy::cast_possible_truncation, clippy::missing_const_for_fn)]
34
35use crate::cartridge::Mirroring;
36use crate::mapper::{Mapper, MapperCaps, MapperDebugInfo, MapperError};
37use alloc::{boxed::Box, format, vec, vec::Vec};
38
39const PRG_BANK_32K: usize = 0x8000;
40const CHR_RAM: usize = 0x2000;
41const WRAM: usize = 0x2000;
42const SAVE_STATE_VERSION: u8 = 1;
43
44pub struct Nanjing163 {
46 prg_rom: Box<[u8]>,
47 chr_ram: Box<[u8]>,
48 wram: Box<[u8]>,
49 mirroring: Mirroring,
50 reg_lo: u8,
52 reg_hi: u8,
54 mode: u8,
56 feedback: bool,
58 a13: bool,
60 a9_latch: bool,
62}
63
64impl Nanjing163 {
65 pub fn new(prg_rom: Box<[u8]>, mirroring: Mirroring) -> Result<Self, MapperError> {
71 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
72 return Err(MapperError::Invalid(format!(
73 "mapper 163 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
74 prg_rom.len()
75 )));
76 }
77 Ok(Self {
78 prg_rom,
79 chr_ram: vec![0u8; CHR_RAM].into_boxed_slice(),
80 wram: vec![0u8; WRAM].into_boxed_slice(),
81 mirroring,
82 reg_lo: 0,
83 reg_hi: 0,
84 mode: 0,
85 feedback: false,
86 a13: false,
87 a9_latch: false,
88 })
89 }
90
91 fn swap(&self, v: u8) -> u8 {
93 if self.mode & 0x01 != 0 {
94 (v & 0xFC) | ((v & 0x01) << 1) | ((v >> 1) & 0x01)
95 } else {
96 v
97 }
98 }
99
100 fn prg_bank(&self) -> usize {
101 let mut bank = usize::from(self.reg_lo & 0x0F) | (usize::from(self.reg_hi & 0x03) << 4);
102 if self.mode & 0x04 == 0 {
103 bank |= 0x03;
104 }
105 bank % (self.prg_rom.len() / PRG_BANK_32K)
106 }
107
108 fn chr_offset(&self, addr: u16) -> usize {
109 let a = usize::from(addr & 0x0FFF);
110 let a12 = if self.reg_lo & 0x80 != 0 {
111 self.a9_latch
112 } else {
113 addr & 0x1000 != 0
114 };
115 a | (usize::from(a12) << 12)
116 }
117
118 fn on_nametable_access(&mut self, addr: u16) {
120 if !self.a13 {
121 self.a9_latch = addr & 0x0200 != 0;
122 }
123 self.a13 = true;
124 }
125}
126
127impl Mapper for Nanjing163 {
128 fn chr_reads_are_pure(&self) -> bool {
131 false
132 }
133
134 fn sram(&self) -> &[u8] {
135 &self.wram
136 }
137
138 fn sram_mut(&mut self) -> &mut [u8] {
139 &mut self.wram
140 }
141
142 fn caps(&self) -> MapperCaps {
143 MapperCaps::NONE
144 }
145
146 fn has_hardwired_mirroring(&self) -> bool {
147 true
148 }
149
150 fn reset(&mut self) {
152 self.reg_lo = 0;
153 self.reg_hi = 0;
154 self.mode = 0;
155 self.feedback = false;
156 }
157
158 fn cpu_read_unmapped(&self, addr: u16) -> bool {
159 match addr {
160 0x4020..=0x5FFF => addr & 0xF300 != 0x5100 || addr < 0x5000,
161 _ => false,
162 }
163 }
164
165 fn cpu_read_driven_mask(&self, _addr: u16) -> u8 {
166 0x04
167 }
168
169 fn cpu_read(&mut self, addr: u16) -> u8 {
170 match addr {
171 0x5000..=0x5FFF if addr & 0xF300 == 0x5100 => {
173 if self.feedback {
174 0
175 } else {
176 0x04
177 }
178 }
179 0x6000..=0x7FFF => self.wram[usize::from(addr - 0x6000)],
180 0x8000..=0xFFFF => {
181 self.prg_rom[self.prg_bank() * PRG_BANK_32K + usize::from(addr & 0x7FFF)]
182 }
183 _ => 0,
184 }
185 }
186
187 fn cpu_write(&mut self, addr: u16, value: u8) {
188 match addr {
189 0x6000..=0x7FFF => self.wram[usize::from(addr - 0x6000)] = value,
190 0x5000..=0x5FFF => match addr & 0xFF00 {
191 0x5000 => self.reg_lo = self.swap(value),
192 0x5100 => {
193 let v = self.swap(value);
194 if addr & 0x01 == 0 {
195 self.feedback = v & 0x04 != 0;
197 } else if v & 0x01 != 0 {
198 self.feedback = !self.feedback;
200 }
201 }
202 0x5200 => {
203 self.reg_hi = if self.prg_rom.len() == 1024 * 1024 {
204 value
205 } else {
206 self.swap(value)
207 };
208 }
209 0x5300 => self.mode = value,
210 _ => {}
211 },
212 _ => {}
213 }
214 }
215
216 fn ppu_read(&mut self, addr: u16) -> u8 {
217 let addr = addr & 0x3FFF;
218 if addr < 0x2000 {
219 self.a13 = false;
220 self.chr_ram[self.chr_offset(addr)]
221 } else {
222 0
223 }
224 }
225
226 fn ppu_write(&mut self, addr: u16, value: u8) {
227 let addr = addr & 0x3FFF;
228 if addr < 0x2000 {
229 self.a13 = false;
230 let off = self.chr_offset(addr);
231 self.chr_ram[off] = value;
232 }
233 }
234
235 fn nametable_fetch(&mut self, addr: u16) -> Option<u8> {
236 self.on_nametable_access(addr);
237 None
238 }
239
240 fn nametable_write(&mut self, addr: u16, _value: u8) -> bool {
241 self.on_nametable_access(addr);
242 false
243 }
244
245 fn current_mirroring(&self) -> Mirroring {
246 self.mirroring
247 }
248
249 fn debug_info(&self) -> MapperDebugInfo {
250 let mut info = MapperDebugInfo {
251 mapper_id: 163,
252 name: "Nanjing FC-001 (163)".into(),
253 mirroring: crate::mapper::mirroring_name(self.mirroring),
254 ..Default::default()
255 };
256 info.prg_banks
257 .push(("32K".into(), format!("{:#04x}", self.prg_bank())));
258 info.chr_banks.push((
259 "auto".into(),
260 format!(
261 "{} (A9 latch {})",
262 self.reg_lo >> 7,
263 u8::from(self.a9_latch)
264 ),
265 ));
266 info.extra
267 .push(("mode".into(), format!("{:#04x}", self.mode)));
268 info
269 }
270
271 fn save_state(&self) -> Vec<u8> {
272 let mut out = Vec::with_capacity(8 + CHR_RAM + WRAM);
273 out.push(SAVE_STATE_VERSION);
274 out.push(self.reg_lo);
275 out.push(self.reg_hi);
276 out.push(self.mode);
277 out.push(u8::from(self.feedback));
278 out.push(u8::from(self.a13));
279 out.push(u8::from(self.a9_latch));
280 out.extend_from_slice(&self.chr_ram);
281 out.extend_from_slice(&self.wram);
282 out
283 }
284
285 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
286 let expected = 7 + CHR_RAM + WRAM;
287 if data.len() != expected {
288 return Err(MapperError::WrongLength {
289 expected,
290 got: data.len(),
291 });
292 }
293 if data[0] != SAVE_STATE_VERSION {
294 return Err(MapperError::UnsupportedVersion(data[0]));
295 }
296 self.reg_lo = data[1];
297 self.reg_hi = data[2];
298 self.mode = data[3];
299 self.feedback = data[4] != 0;
300 self.a13 = data[5] != 0;
301 self.a9_latch = data[6] != 0;
302 self.chr_ram.copy_from_slice(&data[7..7 + CHR_RAM]);
303 self.wram.copy_from_slice(&data[7 + CHR_RAM..]);
304 Ok(())
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 fn board(banks_32k: usize) -> Nanjing163 {
313 let mut prg = vec![0u8; banks_32k * PRG_BANK_32K];
314 for b in 0..banks_32k {
315 prg[b * PRG_BANK_32K] = b as u8;
316 }
317 Nanjing163::new(prg.into_boxed_slice(), Mirroring::Vertical).unwrap()
318 }
319
320 #[test]
321 fn boots_in_bank_3_until_the_a_bit_is_set() {
322 let mut m = board(32);
323 assert_eq!(m.cpu_read(0x8000), 3);
324 m.cpu_write(0x5000, 0x04);
325 assert_eq!(m.cpu_read(0x8000), 0x07, "A clear: A15/A16 forced to 11");
326 m.cpu_write(0x5300, 0x04);
327 assert_eq!(m.cpu_read(0x8000), 0x04);
328 m.cpu_write(0x5200, 0x01);
329 assert_eq!(m.cpu_read(0x8000), 0x14, "$5200 bits are A19-A20");
330 m.reset();
331 assert_eq!(m.cpu_read(0x8000), 3);
332 }
333
334 #[test]
335 fn b_bit_swaps_d0_d1_on_5000_to_5200_but_not_5300() {
336 let mut m = board(64); m.cpu_write(0x5300, 0x05); m.cpu_write(0x5000, 0x01);
339 assert_eq!(m.cpu_read(0x8000), 0x02);
340 m.cpu_write(0x5200, 0x02);
341 assert_eq!(m.cpu_read(0x8000), 0x12);
342 }
343
344 #[test]
345 fn one_mib_boards_do_not_swap_5200() {
346 let mut m = board(32); m.cpu_write(0x5300, 0x05);
348 m.cpu_write(0x5200, 0x01);
349 assert_eq!(m.cpu_read(0x8000) >> 4, 1);
350 }
351
352 #[test]
353 fn feedback_is_read_back_inverted_and_flipped_by_5101() {
354 let mut m = board(4);
355 assert_eq!(m.cpu_read(0x5500), 0x04, "F=0 reads back as 1");
356 assert_eq!(m.cpu_read_driven_mask(0x5500), 0x04);
357 assert!(!m.cpu_read_unmapped(0x5501));
358 m.cpu_write(0x5100, 0x04);
359 assert_eq!(m.cpu_read(0x5500), 0x00);
360 m.cpu_write(0x5101, 0x01);
361 assert_eq!(m.cpu_read(0x5500), 0x04, "E=1 at $5101 flips F");
362 m.cpu_write(0x5101, 0x04);
363 assert_eq!(m.cpu_read(0x5500), 0x04, "E=0: F kept, D2 ignored");
364 }
365
366 #[test]
370 fn auto_switch_latches_a9_on_the_a13_rise() {
371 let mut m = board(4);
372 m.chr_ram[0x0010] = 0x11;
373 m.chr_ram[0x1010] = 0x22;
374 m.cpu_write(0x5000, 0x80);
375 let _ = m.ppu_read(0x0000);
376 let _ = m.nametable_fetch(0x2020); let _ = m.nametable_fetch(0x23C1); assert_eq!(m.ppu_read(0x1010), 0x11, "left table despite PPU A12=1");
379 let _ = m.nametable_fetch(0x2220); assert_eq!(m.ppu_read(0x0010), 0x22, "right table despite PPU A12=0");
381 let _ = m.nametable_fetch(0x2020); let _ = m.nametable_fetch(0x2220); assert_eq!(m.ppu_read(0x0010), 0x11, "no rise, no re-latch");
384 m.cpu_write(0x5000, 0x00);
385 assert_eq!(m.ppu_read(0x0010), 0x11, "auto-switch off: PPU A12");
386 }
387
388 #[test]
389 fn wram_and_state_round_trip() {
390 let mut a = board(8);
391 a.cpu_write(0x6005, 0x99);
392 a.cpu_write(0x5300, 0x04);
393 a.cpu_write(0x5000, 0x86);
394 a.ppu_write(0x0123, 0x77);
395 let blob = a.save_state();
396 let mut b = board(8);
397 b.load_state(&blob).unwrap();
398 assert_eq!(b.cpu_read(0x6005), 0x99);
399 assert_eq!(b.cpu_read(0x8000), a.cpu_read(0x8000));
400 assert_eq!(b.save_state(), blob);
401 }
402}