1#![allow(
32 clippy::cast_possible_truncation,
33 clippy::cast_lossless,
34 clippy::missing_const_for_fn,
35 clippy::doc_markdown
36)]
37
38use crate::cartridge::Mirroring;
39use crate::mapper::{Mapper, MapperCaps, MapperError};
40use alloc::{boxed::Box, vec::Vec};
41use alloc::{format, vec};
42
43const PRG_BANK_8K: usize = 0x2000;
44const CHR_BANK_1K: usize = 0x0400;
45const NAMETABLE_SIZE: usize = 0x0400;
46const NAMETABLE_SIZE_U16: u16 = 0x0400;
47
48const SAVE_STATE_VERSION: u8 = 1;
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum Namco175Board {
53 N175,
56 N340,
59}
60
61pub struct Namco175 {
63 prg_rom: Box<[u8]>,
64 chr_rom: Box<[u8]>,
65 prg_ram: Box<[u8]>,
66 vram: Box<[u8]>,
67 chr_is_ram: bool,
68
69 prg: [u8; 3], chr_regs: [u8; 8], prg_ram_enabled: bool,
72 mirroring: Mirroring,
73 board: Namco175Board,
74}
75
76impl Namco175 {
77 pub fn new(
87 prg_rom: Box<[u8]>,
88 chr_rom: Box<[u8]>,
89 mirroring: Mirroring,
90 board: Namco175Board,
91 ) -> Result<Self, MapperError> {
92 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
93 return Err(MapperError::Invalid(format!(
94 "Namco-175/340 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
95 prg_rom.len()
96 )));
97 }
98 let chr_is_ram = chr_rom.is_empty();
99 let chr_data: Box<[u8]> = if chr_is_ram {
100 vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
101 } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
102 chr_rom
103 } else {
104 return Err(MapperError::Invalid(format!(
105 "Namco-175/340 CHR-ROM size {} is not a multiple of 1 KiB",
106 chr_rom.len()
107 )));
108 };
109 Ok(Self {
110 prg_rom,
111 chr_rom: chr_data,
112 prg_ram: vec![0u8; 8 * 1024].into_boxed_slice(),
113 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
114 chr_is_ram,
115 prg: [0; 3],
116 chr_regs: [0; 8],
117 prg_ram_enabled: false,
118 mirroring,
119 board,
120 })
121 }
122
123 fn prg_offset(&self, addr: u16) -> usize {
124 let total = (self.prg_rom.len() / PRG_BANK_8K).max(1);
125 let last = total - 1;
126 let bank = match addr & 0xE000 {
127 0x8000 => (self.prg[0] as usize) % total,
128 0xA000 => (self.prg[1] as usize) % total,
129 0xC000 => (self.prg[2] as usize) % total,
130 0xE000 => last,
131 _ => 0,
132 };
133 bank * PRG_BANK_8K + (addr as usize & 0x1FFF)
134 }
135
136 fn chr_offset(&self, addr: u16) -> usize {
137 let addr = (addr & 0x1FFF) as usize;
138 let total_1k = (self.chr_rom.len() / CHR_BANK_1K).max(1);
139 let slot = addr / CHR_BANK_1K;
140 let bank = (self.chr_regs[slot] as usize) % total_1k;
141 bank * CHR_BANK_1K + (addr & (CHR_BANK_1K - 1))
142 }
143
144 fn nametable_offset(&self, addr: u16) -> usize {
145 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
146 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
147 let physical = self.mirroring.physical_bank(table);
148 physical * NAMETABLE_SIZE + local
149 }
150}
151
152impl Mapper for Namco175 {
153 fn sram(&self) -> &[u8] {
154 &self.prg_ram
155 }
156 fn sram_mut(&mut self) -> &mut [u8] {
157 &mut self.prg_ram
158 }
159 fn caps(&self) -> MapperCaps {
162 MapperCaps::NONE
163 }
164
165 fn cpu_read(&mut self, addr: u16) -> u8 {
166 match addr {
167 0x6000..=0x7FFF => {
168 if matches!(self.board, Namco175Board::N175) && self.prg_ram_enabled {
169 self.prg_ram[(addr - 0x6000) as usize % self.prg_ram.len()]
170 } else {
171 0
172 }
173 }
174 0x8000..=0xFFFF => {
175 let off = self.prg_offset(addr);
176 self.prg_rom[off % self.prg_rom.len()]
177 }
178 _ => 0,
179 }
180 }
181
182 fn cpu_write(&mut self, addr: u16, value: u8) {
183 match addr {
184 0x6000..=0x7FFF => {
185 if matches!(self.board, Namco175Board::N175) && self.prg_ram_enabled {
186 let off = (addr - 0x6000) as usize % self.prg_ram.len();
187 self.prg_ram[off] = value;
188 }
189 }
190 0x8000..=0xBFFF => {
192 let slot = ((addr - 0x8000) >> 11) as usize;
193 if slot < 8 {
194 self.chr_regs[slot] = value;
195 }
196 }
197 0xC000..=0xC7FF => {
199 if matches!(self.board, Namco175Board::N175) {
200 self.prg_ram_enabled = (value & 0x01) != 0;
201 }
202 }
203 0xE000..=0xE7FF => {
205 self.prg[0] = value & 0x3F;
206 if matches!(self.board, Namco175Board::N340) {
207 self.mirroring = match (value >> 6) & 0x03 {
208 0 => Mirroring::SingleScreenA,
209 1 => Mirroring::Vertical,
210 2 => Mirroring::SingleScreenB,
211 _ => Mirroring::Horizontal,
212 };
213 }
214 }
215 0xE800..=0xEFFF => self.prg[1] = value & 0x3F,
217 0xF000..=0xF7FF => self.prg[2] = value & 0x3F,
219 _ => {}
220 }
221 }
222
223 fn ppu_read(&mut self, addr: u16) -> u8 {
224 let addr = addr & 0x3FFF;
225 match addr {
226 0x0000..=0x1FFF => {
227 let off = self.chr_offset(addr);
228 self.chr_rom[off % self.chr_rom.len()]
229 }
230 0x2000..=0x3EFF => self.vram[self.nametable_offset(addr) % self.vram.len()],
231 _ => 0,
232 }
233 }
234
235 fn ppu_write(&mut self, addr: u16, value: u8) {
236 let addr = addr & 0x3FFF;
237 match addr {
238 0x0000..=0x1FFF => {
239 if self.chr_is_ram {
240 let off = self.chr_offset(addr);
241 let len = self.chr_rom.len();
242 self.chr_rom[off % len] = value;
243 }
244 }
245 0x2000..=0x3EFF => {
246 let off = self.nametable_offset(addr) % self.vram.len();
247 self.vram[off] = value;
248 }
249 _ => {}
250 }
251 }
252
253 fn nametable_address(&self, addr: u16) -> u16 {
254 let off = self.nametable_offset(addr);
255 u16::try_from(off & 0x07FF).unwrap_or(0)
256 }
257
258 fn current_mirroring(&self) -> Mirroring {
259 self.mirroring
260 }
261
262 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
263 let mut info = crate::mapper::MapperDebugInfo {
264 mapper_id: 210,
265 name: match self.board {
266 Namco175Board::N175 => "Namco 175 (210)".into(),
267 Namco175Board::N340 => "Namco 340 (210)".into(),
268 },
269 mirroring: crate::mapper::mirroring_name(self.mirroring),
270 ..Default::default()
271 };
272 for (i, b) in self.prg.iter().enumerate() {
273 info.prg_banks
274 .push((format!("PRG{i}"), format!("{b:#04x}")));
275 }
276 for (i, b) in self.chr_regs.iter().enumerate() {
277 info.chr_banks.push((format!("C{i}"), format!("{b:#04x}")));
278 }
279 info
280 }
281
282 fn save_state(&self) -> Vec<u8> {
283 let mut out = Vec::with_capacity(
284 16 + self.prg_ram.len()
285 + self.vram.len()
286 + if self.chr_is_ram {
287 self.chr_rom.len()
288 } else {
289 0
290 },
291 );
292 out.push(SAVE_STATE_VERSION);
293 out.extend_from_slice(&self.prg);
294 out.extend_from_slice(&self.chr_regs);
295 out.push(u8::from(self.prg_ram_enabled));
296 out.push(self.mirroring as u8);
297 out.push(match self.board {
298 Namco175Board::N175 => 0,
299 Namco175Board::N340 => 1,
300 });
301 out.extend_from_slice(&self.prg_ram);
302 out.extend_from_slice(&self.vram);
303 if self.chr_is_ram {
304 out.extend_from_slice(&self.chr_rom);
305 }
306 out
307 }
308
309 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
310 let chr_part = if self.chr_is_ram {
311 self.chr_rom.len()
312 } else {
313 0
314 };
315 let scalar_len = 1 + 3 + 8 + 1 + 1 + 1;
317 let expected = scalar_len + self.prg_ram.len() + self.vram.len() + chr_part;
318 if data.len() != expected {
319 return Err(MapperError::WrongLength {
320 expected,
321 got: data.len(),
322 });
323 }
324 if data[0] != SAVE_STATE_VERSION {
325 return Err(MapperError::UnsupportedVersion(data[0]));
326 }
327 let mut c = 1usize;
328 self.prg.copy_from_slice(&data[c..c + 3]);
329 c += 3;
330 self.chr_regs.copy_from_slice(&data[c..c + 8]);
331 c += 8;
332 self.prg_ram_enabled = data[c] != 0;
333 c += 1;
334 self.mirroring = match data[c] {
335 0 => Mirroring::Horizontal,
336 1 => Mirroring::Vertical,
337 2 => Mirroring::SingleScreenA,
338 3 => Mirroring::SingleScreenB,
339 4 => Mirroring::FourScreen,
340 5 => Mirroring::MapperControlled,
341 other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
342 };
343 c += 1;
344 self.board = match data[c] {
345 0 => Namco175Board::N175,
346 1 => Namco175Board::N340,
347 other => return Err(MapperError::Invalid(format!("board {other}"))),
348 };
349 c += 1;
350 self.prg_ram
351 .copy_from_slice(&data[c..c + self.prg_ram.len()]);
352 c += self.prg_ram.len();
353 self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
354 c += self.vram.len();
355 if self.chr_is_ram {
356 self.chr_rom
357 .copy_from_slice(&data[c..c + self.chr_rom.len()]);
358 }
359 Ok(())
360 }
361}
362
363#[cfg(test)]
364#[allow(clippy::cast_possible_truncation)]
365mod tests {
366 use super::*;
367
368 fn synth_prg(banks_8k: usize) -> Box<[u8]> {
369 let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
370 for b in 0..banks_8k {
371 v[b * PRG_BANK_8K] = b as u8;
372 }
373 v.into_boxed_slice()
374 }
375
376 fn synth_chr(banks_1k: usize) -> Box<[u8]> {
377 let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
378 for b in 0..banks_1k {
379 v[b * CHR_BANK_1K] = b as u8;
380 }
381 v.into_boxed_slice()
382 }
383
384 fn fresh175() -> Namco175 {
385 Namco175::new(
386 synth_prg(16),
387 synth_chr(64),
388 Mirroring::Vertical,
389 Namco175Board::N175,
390 )
391 .unwrap()
392 }
393
394 fn fresh340() -> Namco175 {
395 Namco175::new(
396 synth_prg(16),
397 synth_chr(64),
398 Mirroring::Vertical,
399 Namco175Board::N340,
400 )
401 .unwrap()
402 }
403
404 #[test]
405 fn prg_three_switchable_plus_fixed() {
406 let mut m = fresh175();
407 assert_eq!(m.cpu_read(0xE000), 15); m.cpu_write(0xE000, 3); m.cpu_write(0xE800, 5); m.cpu_write(0xF000, 9); assert_eq!(m.cpu_read(0x8000), 3);
412 assert_eq!(m.cpu_read(0xA000), 5);
413 assert_eq!(m.cpu_read(0xC000), 9);
414 assert_eq!(m.cpu_read(0xE000), 15);
415 }
416
417 #[test]
418 fn chr_eight_1k_banks() {
419 let mut m = fresh175();
420 m.cpu_write(0x8000, 10); m.cpu_write(0xA000, 20); assert_eq!(m.ppu_read(0x0000), 10);
423 assert_eq!(m.ppu_read(0x1000), 20);
424 }
425
426 #[test]
427 fn n175_no_irq() {
428 let mut m = fresh175();
429 for _ in 0..2000 {
430 m.notify_cpu_cycle();
431 }
432 assert!(!m.irq_pending());
433 }
434
435 #[test]
436 fn n340_no_irq() {
437 let mut m = fresh340();
438 for _ in 0..2000 {
439 m.notify_cpu_cycle();
440 }
441 assert!(!m.irq_pending());
442 }
443
444 #[test]
445 fn n175_prg_ram_enable_gated() {
446 let mut m = fresh175();
447 m.cpu_write(0x6000, 0xAB);
449 assert_eq!(m.cpu_read(0x6000), 0);
450 m.cpu_write(0xC000, 0x01); m.cpu_write(0x6000, 0xCD);
452 assert_eq!(m.cpu_read(0x6000), 0xCD);
453 }
454
455 #[test]
456 fn n175_mirroring_hardwired_ignores_e000_upper_bits() {
457 let mut m = fresh175();
458 assert_eq!(m.current_mirroring(), Mirroring::Vertical);
459 m.cpu_write(0xE000, 0xC0);
461 assert_eq!(m.current_mirroring(), Mirroring::Vertical);
462 }
463
464 #[test]
465 fn n340_mirroring_select() {
466 let mut m = fresh340();
467 m.cpu_write(0xE000, 0x00); assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
469 m.cpu_write(0xE000, 0x40); assert_eq!(m.current_mirroring(), Mirroring::Vertical);
471 m.cpu_write(0xE000, 0x80); assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
473 m.cpu_write(0xE000, 0xC0); assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
475 }
476
477 #[test]
478 fn save_state_round_trip() {
479 let mut m = fresh340();
480 m.cpu_write(0xE000, 0x40 | 3);
481 m.cpu_write(0x9000, 7);
482 m.ppu_write(0x2000, 0x33);
483 let blob = m.save_state();
484 let mut m2 = fresh340();
485 m2.load_state(&blob).unwrap();
486 assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
487 assert_eq!(m.ppu_read(0x0800), m2.ppu_read(0x0800));
488 assert_eq!(m.current_mirroring(), m2.current_mirroring());
489 }
490}