1#![allow(
29 clippy::cast_possible_truncation,
30 clippy::cast_lossless,
31 clippy::missing_const_for_fn,
32 clippy::doc_markdown
33)]
34
35use crate::cartridge::Mirroring;
36use crate::m004_mmc3::Mmc3;
37use crate::mapper::{Mapper, MapperCaps, MapperDebugInfo, MapperError};
38use alloc::format;
39use alloc::string::ToString;
40use alloc::{boxed::Box, vec, vec::Vec};
41
42const CHR_BANK_1K: usize = 0x0400;
43const CHR_RAM_SIZE: usize = 8 * CHR_BANK_1K; const CHR_RAM_SELECT: usize = 0x40;
47
48const SAVE_STATE_VERSION: u8 = 1;
49
50pub struct Tqrom {
53 inner: Mmc3,
54 chr_ram: Box<[u8]>,
55}
56
57impl Tqrom {
58 pub fn new(
70 prg_rom: Box<[u8]>,
71 chr_rom: Box<[u8]>,
72 initial_mirroring: Mirroring,
73 prg_ram_bytes: usize,
74 ) -> Result<Self, MapperError> {
75 let inner = Mmc3::new(
76 prg_rom,
77 chr_rom,
78 initial_mirroring,
79 prg_ram_bytes,
80 crate::m004_mmc3::Mmc3Revision::Sharp,
81 )?;
82 Ok(Self {
83 inner,
84 chr_ram: vec![0u8; CHR_RAM_SIZE].into_boxed_slice(),
85 })
86 }
87
88 fn resolve_chr(&self, addr: u16) -> (bool, usize) {
91 let bank = self.inner.chr_bank_1k(addr);
92 let offset_in_bank = (addr as usize) & (CHR_BANK_1K - 1);
93 if bank & CHR_RAM_SELECT != 0 {
94 let ram_bank = bank & 0x07;
96 (true, ram_bank * CHR_BANK_1K + offset_in_bank)
97 } else {
98 let rom_bank = bank & 0x3F;
100 (false, rom_bank * CHR_BANK_1K + offset_in_bank)
101 }
102 }
103}
104
105impl Mapper for Tqrom {
106 fn sram(&self) -> &[u8] {
108 self.inner.sram()
109 }
110 fn sram_mut(&mut self) -> &mut [u8] {
111 self.inner.sram_mut()
112 }
113
114 fn caps(&self) -> MapperCaps {
116 MapperCaps::CYCLE_IRQ
117 }
118
119 fn cpu_read(&mut self, addr: u16) -> u8 {
120 self.inner.cpu_read(addr)
121 }
122
123 fn cpu_write(&mut self, addr: u16, value: u8) {
124 self.inner.cpu_write(addr, value);
125 }
126
127 fn cpu_read_unmapped(&self, addr: u16) -> bool {
128 self.inner.cpu_read_unmapped(addr)
129 }
130
131 fn ppu_read(&mut self, addr: u16) -> u8 {
132 let a = addr & 0x3FFF;
133 if a < 0x2000 {
134 let (is_ram, off) = self.resolve_chr(a);
135 if is_ram {
136 self.chr_ram[off % self.chr_ram.len()]
137 } else {
138 self.inner.ppu_read(a)
143 }
144 } else {
145 self.inner.ppu_read(a)
147 }
148 }
149
150 fn ppu_write(&mut self, addr: u16, value: u8) {
151 let a = addr & 0x3FFF;
152 if a < 0x2000 {
153 let (is_ram, off) = self.resolve_chr(a);
154 if is_ram {
155 let len = self.chr_ram.len();
156 self.chr_ram[off % len] = value;
157 }
158 } else {
160 self.inner.ppu_write(a, value);
161 }
162 }
163
164 fn nametable_address(&self, addr: u16) -> u16 {
165 self.inner.nametable_address(addr)
166 }
167
168 fn current_mirroring(&self) -> Mirroring {
169 self.inner.current_mirroring()
170 }
171
172 fn notify_a12(&mut self, level: bool) {
173 self.inner.notify_a12(level);
174 }
175
176 fn notify_a12_at_sub_dot(&mut self, level: bool, sub_dot: u8) {
177 self.inner.notify_a12_at_sub_dot(level, sub_dot);
178 }
179
180 fn notify_cpu_cycle(&mut self) {
181 self.inner.notify_cpu_cycle();
182 }
183
184 fn irq_pending(&self) -> bool {
185 self.inner.irq_pending()
186 }
187
188 fn irq_acknowledge(&mut self) {
189 self.inner.irq_acknowledge();
190 }
191
192 fn debug_info(&self) -> MapperDebugInfo {
193 let mut info = self.inner.debug_info();
194 info.mapper_id = 119;
195 info.name = "TQROM (119)".into();
196 for slot in 0u16..8 {
197 let (is_ram, _) = self.resolve_chr(slot * CHR_BANK_1K as u16);
198 info.chr_banks.push((
199 format!("slot{slot}"),
200 if is_ram { "RAM" } else { "ROM" }.to_string(),
201 ));
202 }
203 info
204 }
205
206 fn save_state(&self) -> Vec<u8> {
207 let inner = self.inner.save_state();
210 let mut out = Vec::with_capacity(1 + self.chr_ram.len() + inner.len());
211 out.push(SAVE_STATE_VERSION);
212 out.extend_from_slice(&self.chr_ram);
213 out.extend_from_slice(&inner);
214 out
215 }
216
217 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
218 let header = 1 + self.chr_ram.len();
219 if data.len() < header {
220 return Err(MapperError::WrongLength {
221 expected: header,
222 got: data.len(),
223 });
224 }
225 if data[0] != SAVE_STATE_VERSION {
226 return Err(MapperError::UnsupportedVersion(data[0]));
227 }
228 self.chr_ram.copy_from_slice(&data[1..header]);
229 self.inner.load_state(&data[header..])
230 }
231}
232
233#[cfg(test)]
234#[allow(clippy::cast_possible_truncation)]
235mod tests {
236 use super::*;
237 use alloc::vec;
238
239 const PRG_BANK_8K: usize = 0x2000;
240
241 fn synth_prg(banks_8k: usize) -> Box<[u8]> {
242 let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
243 for b in 0..banks_8k {
244 v[b * PRG_BANK_8K] = b as u8;
245 }
246 v.into_boxed_slice()
247 }
248
249 fn synth_chr_rom() -> Box<[u8]> {
251 let mut v = vec![0u8; 64 * CHR_BANK_1K];
252 for b in 0..64 {
253 v[b * CHR_BANK_1K] = b as u8;
254 }
255 v.into_boxed_slice()
256 }
257
258 fn fresh() -> Tqrom {
259 Tqrom::new(synth_prg(8), synth_chr_rom(), Mirroring::Vertical, 0).unwrap()
260 }
261
262 fn select_write(m: &mut Tqrom, reg: u8, value: u8) {
263 m.cpu_write(0x8000, reg);
264 m.cpu_write(0x8001, value);
265 }
266
267 #[test]
268 fn delegates_prg_banking_to_mmc3() {
269 let mut m = fresh();
270 assert_eq!(m.cpu_read(0xE000), 7);
272 select_write(&mut m, 6, 3); assert_eq!(m.cpu_read(0x8000), 3);
274 }
275
276 #[test]
277 fn bit6_clear_reads_chr_rom() {
278 let mut m = fresh();
279 select_write(&mut m, 2, 5);
282 assert_eq!(m.ppu_read(0x1000), 5, "bit-6-clear bank reads CHR-ROM");
283 }
284
285 #[test]
286 fn bit6_set_reads_and_writes_chr_ram() {
287 let mut m = fresh();
288 select_write(&mut m, 2, 0x40);
290 assert_eq!(m.ppu_read(0x1000), 0, "fresh CHR-RAM bank reads zero");
291 m.ppu_write(0x1000, 0xAB);
293 assert_eq!(m.ppu_read(0x1000), 0xAB, "CHR-RAM is writable");
294 select_write(&mut m, 2, 0x41);
296 assert_eq!(m.ppu_read(0x1000), 0, "CHR-RAM bank 1 distinct from bank 0");
297 m.ppu_write(0x1000, 0xCD);
298 assert_eq!(m.ppu_read(0x1000), 0xCD);
299 select_write(&mut m, 2, 0x40);
301 assert_eq!(m.ppu_read(0x1000), 0xAB);
302 }
303
304 #[test]
305 fn chr_rom_writes_are_ignored() {
306 let mut m = fresh();
307 select_write(&mut m, 2, 5);
309 m.ppu_write(0x1000, 0xFF);
310 assert_eq!(m.ppu_read(0x1000), 5, "write to a CHR-ROM bank is ignored");
311 }
312
313 #[test]
314 fn chr_ram_bank_index_uses_low_3_bits() {
315 let mut m = fresh();
316 select_write(&mut m, 2, 0x47);
318 m.ppu_write(0x1000, 0x77);
319 assert_eq!(m.ppu_read(0x1000), 0x77);
320 select_write(&mut m, 2, 0x4F);
322 assert_eq!(
323 m.ppu_read(0x1000),
324 0x77,
325 "CHR-RAM bank index is the low 3 bits of the register"
326 );
327 }
328
329 #[test]
330 fn irq_delegates_to_mmc3() {
331 let mut m = fresh();
332 m.cpu_write(0xC000, 3);
333 m.cpu_write(0xC001, 0);
334 m.cpu_write(0xE001, 0);
335 for _ in 0..5 {
336 m.notify_a12(false);
337 for _ in 0..4 {
338 m.notify_cpu_cycle();
339 }
340 m.notify_a12(true);
341 }
342 assert!(m.irq_pending(), "IRQ counter behaves like MMC3");
343 }
344
345 #[test]
346 fn mirroring_register_toggles_h_v() {
347 let mut m = fresh();
348 m.cpu_write(0xA000, 0);
349 assert_eq!(m.current_mirroring(), Mirroring::Vertical);
350 m.cpu_write(0xA000, 1);
351 assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
352 }
353
354 #[test]
355 fn save_load_round_trip_preserves_chr_ram() {
356 let mut m = fresh();
357 select_write(&mut m, 6, 3);
358 select_write(&mut m, 2, 0x40); m.ppu_write(0x1000, 0x5A);
360 m.cpu_write(0xC000, 0x10);
361 let blob = m.save_state();
362 let mut other = fresh();
363 other.load_state(&blob).unwrap();
364 assert_eq!(other.cpu_read(0x8000), m.cpu_read(0x8000));
365 other.cpu_write(0x8000, 2);
367 other.cpu_write(0x8001, 0x40);
368 assert_eq!(other.ppu_read(0x1000), 0x5A);
369 }
370}