rustynes_mappers/
m185_cnrom185.rs1use crate::cartridge::Mirroring;
23use crate::mapper::{Mapper, MapperCaps, MapperError};
24use alloc::{boxed::Box, vec::Vec};
25use alloc::{format, vec};
26
27const PRG_BANK_16K: usize = 0x4000;
28const PRG_BANK_32K: usize = 0x8000;
29const CHR_BANK_8K: usize = 0x2000;
30const NAMETABLE_SIZE: usize = 0x0400;
31const NAMETABLE_SIZE_U16: u16 = 0x0400;
32
33const SAVE_STATE_VERSION: u8 = 1;
34
35const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
40 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
41 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
42 let physical = mirroring.physical_bank(table);
43 physical * NAMETABLE_SIZE + local
44}
45
46pub struct CnRom185 {
48 prg_rom: Box<[u8]>,
49 chr_rom: Box<[u8]>,
50 vram: Box<[u8]>,
51 chr_reg_raw: u8,
52 chr_bank: u8,
53 chr_enabled: bool,
58 sub_mapper: u8,
59 mirroring: Mirroring,
60}
61
62impl CnRom185 {
63 pub fn new(
73 prg_rom: Box<[u8]>,
74 chr_rom: Box<[u8]>,
75 mirroring: Mirroring,
76 sub_mapper: u8,
77 ) -> Result<Self, MapperError> {
78 if prg_rom.len() != PRG_BANK_16K && prg_rom.len() != PRG_BANK_32K {
79 return Err(MapperError::Invalid(format!(
80 "mapper 185 expects 16 or 32 KiB PRG, got {} bytes",
81 prg_rom.len()
82 )));
83 }
84 if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
85 return Err(MapperError::Invalid(format!(
86 "mapper 185 expects non-empty CHR-ROM in 8 KiB units, got {} bytes",
87 chr_rom.len()
88 )));
89 }
90 Ok(Self {
91 prg_rom,
92 chr_rom,
93 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
94 chr_reg_raw: 0,
95 chr_bank: 0,
96 chr_enabled: true,
97 sub_mapper: sub_mapper & 0x0F,
98 mirroring,
99 })
100 }
101
102 #[allow(clippy::verbose_bit_mask)]
105 const fn chr_enable_for(&self, value: u8) -> bool {
106 match self.sub_mapper {
107 4 => (value & 0x03) == 0,
108 5 => (value & 0x03) == 1,
109 6 => (value & 0x03) == 2,
110 7 => (value & 0x03) == 3,
111 _ => (value & 0x0F) != 0 && value != 0x13,
113 }
114 }
115
116 fn read_prg(&self, addr: u16) -> u8 {
117 let off = (addr - 0x8000) as usize;
118 if self.prg_rom.len() == PRG_BANK_16K {
119 self.prg_rom[off & (PRG_BANK_16K - 1)]
120 } else {
121 self.prg_rom[off]
122 }
123 }
124}
125
126impl Mapper for CnRom185 {
127 fn caps(&self) -> MapperCaps {
128 MapperCaps::NONE
129 }
130
131 fn cpu_read(&mut self, addr: u16) -> u8 {
132 if (0x8000..=0xFFFF).contains(&addr) {
133 self.read_prg(addr)
134 } else {
135 0
136 }
137 }
138
139 fn cpu_write(&mut self, addr: u16, value: u8) {
140 if (0x8000..=0xFFFF).contains(&addr) {
141 let effective = value & self.read_prg(addr);
143 self.chr_reg_raw = effective;
144 self.chr_enabled = self.chr_enable_for(effective);
145 let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
146 let mask = u8::try_from((count - 1) | 0x03).unwrap_or(u8::MAX);
147 self.chr_bank = effective & mask;
148 }
149 }
150
151 fn ppu_read(&mut self, addr: u16) -> u8 {
152 let addr = addr & 0x3FFF;
153 match addr {
154 0x0000..=0x1FFF => {
155 if self.chr_enabled {
156 let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
157 let bank = (self.chr_bank as usize) % count;
158 self.chr_rom[bank * CHR_BANK_8K + addr as usize]
159 } else {
160 0xFF
163 }
164 }
165 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
166 _ => 0,
167 }
168 }
169
170 fn ppu_write(&mut self, addr: u16, value: u8) {
171 let addr = addr & 0x3FFF;
172 if let 0x2000..=0x3EFF = addr {
173 let off = nametable_offset(addr, self.mirroring);
174 self.vram[off] = value;
175 }
176 }
177
178 fn current_mirroring(&self) -> Mirroring {
179 self.mirroring
180 }
181
182 fn save_state(&self) -> Vec<u8> {
183 let mut out = Vec::with_capacity(4 + self.vram.len());
184 out.push(SAVE_STATE_VERSION);
185 out.push(self.chr_reg_raw);
186 out.push(self.chr_bank);
187 out.push(self.sub_mapper);
188 out.extend_from_slice(&self.vram);
189 out
190 }
191
192 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
193 let expected = 4 + self.vram.len();
194 if data.len() != expected {
195 return Err(MapperError::WrongLength {
196 expected,
197 got: data.len(),
198 });
199 }
200 if data[0] != SAVE_STATE_VERSION {
201 return Err(MapperError::UnsupportedVersion(data[0]));
202 }
203 self.chr_reg_raw = data[1];
204 self.chr_bank = data[2];
205 self.sub_mapper = data[3] & 0x0F;
206 self.chr_enabled = self.chr_enable_for(self.chr_reg_raw);
211 self.vram.copy_from_slice(&data[4..4 + self.vram.len()]);
212 Ok(())
213 }
214}
215
216#[cfg(test)]
217#[allow(clippy::cast_possible_truncation)]
218mod tests {
219 use super::*;
220
221 fn synth_chr_8k(banks: usize) -> Box<[u8]> {
222 let mut v = vec![0u8; banks * CHR_BANK_8K];
223 for b in 0..banks {
224 v[b * CHR_BANK_8K] = b as u8;
225 }
226 v.into_boxed_slice()
227 }
228
229 fn synth_prg(bytes: usize, fill: u8) -> Box<[u8]> {
230 vec![fill; bytes].into_boxed_slice()
231 }
232
233 #[test]
234 fn m185_chr_disable_protection_default() {
235 let mut m = CnRom185::new(
236 synth_prg(PRG_BANK_32K, 0xFF),
237 synth_chr_8k(4),
238 Mirroring::Vertical,
239 0,
240 )
241 .unwrap();
242 assert_eq!(m.ppu_read(0x0000), 0);
245 m.cpu_write(0x8000, 1);
248 assert_eq!(m.ppu_read(0x0000), 1);
249 m.cpu_write(0x8000, 0);
251 assert_eq!(m.ppu_read(0x0000), 0xFF);
252 m.cpu_write(0x8000, 0x13);
254 assert_eq!(m.ppu_read(0x0000), 0xFF);
255 }
256
257 #[test]
258 fn m185_submapper_exact_match() {
259 let mut m = CnRom185::new(
260 synth_prg(PRG_BANK_32K, 0xFF),
261 synth_chr_8k(4),
262 Mirroring::Vertical,
263 4, )
265 .unwrap();
266 m.cpu_write(0x8000, 0); assert_eq!(m.ppu_read(0x0000), 0);
268 m.cpu_write(0x8000, 1); assert_eq!(m.ppu_read(0x0000), 0xFF);
270 }
271}