1#![allow(
21 clippy::bool_to_int_with_if,
22 clippy::cast_lossless,
23 clippy::cast_possible_truncation,
24 clippy::doc_markdown,
25 clippy::match_same_arms,
26 clippy::missing_const_for_fn,
27 clippy::similar_names,
28 clippy::struct_excessive_bools,
29 clippy::too_many_lines,
30 clippy::unreadable_literal
31)]
32
33use crate::cartridge::Mirroring;
34use crate::mapper::{Mapper, MapperCaps, MapperError};
35use alloc::{boxed::Box, vec::Vec};
36use alloc::{format, vec};
37
38const PRG_BANK_8K: usize = 0x2000;
39const CHR_BANK_2K: usize = 0x0800;
40const NAMETABLE_SIZE: usize = 0x0400;
41const NAMETABLE_SIZE_U16: u16 = 0x0400;
42
43const SAVE_STATE_VERSION: u8 = 1;
44
45const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
50 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
51 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
52 let physical = mirroring.physical_bank(table);
53 physical * NAMETABLE_SIZE + local
54}
55
56pub struct Namcot3446M76 {
58 prg_rom: Box<[u8]>,
59 chr_rom: Box<[u8]>,
60 vram: Box<[u8]>,
61 reg_index: u8,
62 prg_banks: [u8; 2],
63 chr_banks: [u8; 4],
64 mirroring: Mirroring,
65}
66
67impl Namcot3446M76 {
68 pub fn new(
75 prg_rom: Box<[u8]>,
76 chr_rom: Box<[u8]>,
77 mirroring: Mirroring,
78 ) -> Result<Self, MapperError> {
79 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
80 return Err(MapperError::Invalid(format!(
81 "mapper 76 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
82 prg_rom.len()
83 )));
84 }
85 if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_2K) {
86 return Err(MapperError::Invalid(format!(
87 "mapper 76 CHR-ROM size {} is not a non-zero multiple of 2 KiB",
88 chr_rom.len()
89 )));
90 }
91 Ok(Self {
92 prg_rom,
93 chr_rom,
94 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
95 reg_index: 0,
96 prg_banks: [0, 1],
97 chr_banks: [0, 1, 2, 3],
98 mirroring,
99 })
100 }
101
102 fn prg_offset(&self, bank: usize, addr: u16) -> u8 {
103 let count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
104 let bank = bank % count;
105 self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
106 }
107}
108
109impl Mapper for Namcot3446M76 {
110 fn caps(&self) -> MapperCaps {
111 MapperCaps::NONE
112 }
113
114 fn cpu_read(&mut self, addr: u16) -> u8 {
115 let last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1;
116 match addr {
117 0x8000..=0x9FFF => self.prg_offset(self.prg_banks[0] as usize, addr),
118 0xA000..=0xBFFF => self.prg_offset(self.prg_banks[1] as usize, addr),
119 0xC000..=0xDFFF => self.prg_offset(last.saturating_sub(1), addr),
122 0xE000..=0xFFFF => self.prg_offset(last, addr),
123 _ => 0,
124 }
125 }
126
127 fn cpu_write(&mut self, addr: u16, value: u8) {
128 match addr {
129 0x8000..=0x9FFF if (addr & 0x01) == 0 => self.reg_index = value & 0x07,
130 0x8000..=0x9FFF => match self.reg_index {
131 2 => self.chr_banks[0] = value,
132 3 => self.chr_banks[1] = value,
133 4 => self.chr_banks[2] = value,
134 5 => self.chr_banks[3] = value,
135 6 => self.prg_banks[0] = value,
136 7 => self.prg_banks[1] = value,
137 _ => {}
138 },
139 _ => {}
140 }
141 }
142
143 fn ppu_read(&mut self, addr: u16) -> u8 {
144 let addr = addr & 0x3FFF;
145 match addr {
146 0x0000..=0x1FFF => {
147 let slot = (addr >> 11) as usize & 0x03;
148 let count = (self.chr_rom.len() / CHR_BANK_2K).max(1);
149 let bank = (self.chr_banks[slot] as usize) % count;
150 self.chr_rom[bank * CHR_BANK_2K + (addr as usize & 0x07FF)]
151 }
152 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
153 _ => 0,
154 }
155 }
156
157 fn ppu_write(&mut self, addr: u16, value: u8) {
158 let addr = addr & 0x3FFF;
159 if let 0x2000..=0x3EFF = addr {
160 let off = nametable_offset(addr, self.mirroring);
161 self.vram[off] = value;
162 }
163 }
164
165 fn current_mirroring(&self) -> Mirroring {
166 self.mirroring
167 }
168
169 fn save_state(&self) -> Vec<u8> {
170 let mut out = Vec::with_capacity(1 + 1 + 2 + 4 + self.vram.len());
171 out.push(SAVE_STATE_VERSION);
172 out.push(self.reg_index);
173 out.extend_from_slice(&self.prg_banks);
174 out.extend_from_slice(&self.chr_banks);
175 out.extend_from_slice(&self.vram);
176 out
177 }
178
179 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
180 let expected = 1 + 1 + 2 + 4 + self.vram.len();
181 if data.len() != expected {
182 return Err(MapperError::WrongLength {
183 expected,
184 got: data.len(),
185 });
186 }
187 if data[0] != SAVE_STATE_VERSION {
188 return Err(MapperError::UnsupportedVersion(data[0]));
189 }
190 self.reg_index = data[1];
191 self.prg_banks.copy_from_slice(&data[2..4]);
192 self.chr_banks.copy_from_slice(&data[4..8]);
193 self.vram.copy_from_slice(&data[8..8 + self.vram.len()]);
194 Ok(())
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 fn synth_prg_8k(banks: usize) -> Box<[u8]> {
203 let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
204 for b in 0..banks {
205 v[b * PRG_BANK_8K] = b as u8;
206 }
207 v.into_boxed_slice()
208 }
209
210 fn synth_chr_2k(banks: usize) -> Box<[u8]> {
211 let mut v = vec![0u8; banks * CHR_BANK_2K];
212 for b in 0..banks {
213 v[b * CHR_BANK_2K] = b as u8;
214 }
215 v.into_boxed_slice()
216 }
217
218 #[test]
219 fn m76_register_pairs_select_banks() {
220 let mut m =
221 Namcot3446M76::new(synth_prg_8k(8), synth_chr_2k(8), Mirroring::Vertical).unwrap();
222 m.cpu_write(0x8000, 6);
224 m.cpu_write(0x8001, 3);
225 assert_eq!(m.cpu_read(0x8000), 3);
226 m.cpu_write(0x8000, 2);
228 m.cpu_write(0x8001, 5);
229 assert_eq!(m.ppu_read(0x0000), 5);
230 assert_eq!(m.cpu_read(0xC000), 6);
232 assert_eq!(m.cpu_read(0xE000), 7);
233 }
234
235 #[test]
236 fn m76_save_state_round_trip() {
237 let mut m =
238 Namcot3446M76::new(synth_prg_8k(8), synth_chr_2k(8), Mirroring::Vertical).unwrap();
239 m.cpu_write(0x8000, 7);
240 m.cpu_write(0x8001, 4); let blob = m.save_state();
242 let mut m2 =
243 Namcot3446M76::new(synth_prg_8k(8), synth_chr_2k(8), Mirroring::Vertical).unwrap();
244 m2.load_state(&blob).unwrap();
245 assert_eq!(m2.cpu_read(0xA000), 4);
246 }
247}