1#![allow(
32 clippy::cast_possible_truncation,
33 clippy::missing_const_for_fn,
34 clippy::struct_excessive_bools
35)]
36
37use crate::cartridge::Mirroring;
38use crate::mapper::{Mapper, MapperCaps, MapperDebugInfo, MapperError};
39use alloc::{boxed::Box, format, vec, vec::Vec};
40
41const PRG_BANK_8K: usize = 0x2000;
42const CHR_BANK_2K: usize = 0x0800;
43const SAVE_STATE_VERSION: u8 = 1;
44
45const A12_RISES_PER_IRQ: u8 = 64;
47
48pub struct Jy91 {
50 prg_rom: Box<[u8]>,
51 chr: Box<[u8]>,
52 chr_is_ram: bool,
53 vram: Box<[u8]>,
54 submapper: u8,
55 mirroring: Mirroring,
56 chr_regs: [u8; 4],
57 prg_regs: [u8; 2],
58 outer: u8,
60 irq_enabled: bool,
61 irq_pending: bool,
62 irq_counter: u16,
64 irq_prescale: u8,
66 last_a12: bool,
67}
68
69impl Jy91 {
70 pub fn new(
77 prg_rom: Box<[u8]>,
78 chr_rom: Box<[u8]>,
79 mirroring: Mirroring,
80 submapper: u8,
81 ) -> Result<Self, MapperError> {
82 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(2 * PRG_BANK_8K) {
83 return Err(MapperError::Invalid(format!(
84 "mapper 91 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
85 prg_rom.len()
86 )));
87 }
88 if !chr_rom.len().is_multiple_of(CHR_BANK_2K) {
89 return Err(MapperError::Invalid(format!(
90 "mapper 91 CHR-ROM size {} is not a multiple of 2 KiB",
91 chr_rom.len()
92 )));
93 }
94 let chr_is_ram = chr_rom.is_empty();
95 let chr = if chr_is_ram {
96 vec![0u8; 4 * CHR_BANK_2K].into_boxed_slice()
97 } else {
98 chr_rom
99 };
100 Ok(Self {
101 prg_rom,
102 chr,
103 chr_is_ram,
104 vram: vec![0u8; 0x800].into_boxed_slice(),
105 submapper: u8::from(submapper == 1),
106 mirroring,
107 chr_regs: [0; 4],
108 prg_regs: [0; 2],
109 outer: 0,
110 irq_enabled: false,
111 irq_pending: false,
112 irq_counter: 0,
113 irq_prescale: 0,
114 last_a12: false,
115 })
116 }
117
118 fn prg_bank(&self, addr: u16) -> usize {
119 let count = self.prg_rom.len() / PRG_BANK_8K;
120 let bank = if self.submapper == 0 {
121 let base = usize::from((self.outer >> 1) & 0x03) << 4;
122 match addr & 0xE000 {
123 0x8000 => base | usize::from(self.prg_regs[0] & 0x0F),
124 0xA000 => base | usize::from(self.prg_regs[1] & 0x0F),
125 0xC000 => base | 0x0E,
126 _ => base | 0x0F,
127 }
128 } else {
129 match addr & 0xE000 {
130 0x8000 => usize::from(self.prg_regs[0]),
131 0xA000 => usize::from(self.prg_regs[1]),
132 0xC000 => count.saturating_sub(2),
133 _ => count - 1,
134 }
135 };
136 bank % count
137 }
138
139 fn chr_offset(&self, addr: u16) -> usize {
140 let slot = usize::from((addr >> 11) & 0x03);
141 let mut bank = usize::from(self.chr_regs[slot]);
142 if self.submapper == 0 {
143 bank |= usize::from(self.outer & 0x01) << 8;
144 }
145 (bank % (self.chr.len() / CHR_BANK_2K)) * CHR_BANK_2K + usize::from(addr & 0x07FF)
146 }
147
148 fn nt_offset(&self, addr: u16) -> usize {
149 let table = ((addr - 0x2000) / 0x400) & 0x03;
150 self.mirroring.physical_bank(table as u8) * 0x400 + usize::from(addr & 0x3FF)
151 }
152}
153
154impl Mapper for Jy91 {
155 fn caps(&self) -> MapperCaps {
156 MapperCaps::CYCLE_IRQ
157 }
158
159 fn has_hardwired_mirroring(&self) -> bool {
160 self.submapper == 0
161 }
162
163 fn cpu_read(&mut self, addr: u16) -> u8 {
164 if addr >= 0x8000 {
165 self.prg_rom[self.prg_bank(addr) * PRG_BANK_8K + usize::from(addr & 0x1FFF)]
166 } else {
167 0
168 }
169 }
170
171 fn cpu_write(&mut self, addr: u16, value: u8) {
172 match addr {
173 0x6000..=0x7FFF => {
174 let mask = if self.submapper == 0 { 0xF003 } else { 0xF007 };
175 match (addr & mask, self.submapper) {
176 (0x6000..=0x6003, _) => self.chr_regs[usize::from(addr & 0x03)] = value,
177 (0x6004, 1) => self.mirroring = Mirroring::Horizontal,
178 (0x6005, 1) => self.mirroring = Mirroring::Vertical,
179 (0x6006, 1) => {
180 self.irq_counter = (self.irq_counter & 0xFF00) | u16::from(value);
181 }
182 (0x6007, 1) => {
183 self.irq_counter = (self.irq_counter & 0x00FF) | (u16::from(value) << 8);
184 }
185 (0x7000..=0x7001, _) => self.prg_regs[usize::from(addr & 0x01)] = value,
186 (0x7002, 0) | (0x7006, 1) => {
188 self.irq_enabled = false;
189 self.irq_pending = false;
190 }
191 (0x7003, 0) | (0x7007, 1) => {
193 self.irq_enabled = true;
194 self.irq_pending = false;
195 self.irq_prescale = 0;
196 if self.submapper == 0 {
197 self.irq_counter = 0;
198 }
199 }
200 _ => {}
201 }
202 }
203 0x8000..=0x9FFF if self.submapper == 0 => self.outer = (addr & 0x07) as u8,
204 _ => {}
205 }
206 }
207
208 fn notify_a12(&mut self, level: bool) {
209 let rising = level && !self.last_a12;
210 self.last_a12 = level;
211 if self.submapper != 0 || !rising || !self.irq_enabled {
212 return;
213 }
214 self.irq_counter += 1;
215 if self.irq_counter >= u16::from(A12_RISES_PER_IRQ) {
216 self.irq_pending = true;
217 self.irq_enabled = false;
218 }
219 }
220
221 fn notify_cpu_cycle(&mut self) {
222 if self.submapper != 1 || !self.irq_enabled {
223 return;
224 }
225 self.irq_prescale = (self.irq_prescale + 1) & 0x03;
226 if self.irq_prescale == 0 {
227 if self.irq_counter < 5 {
228 self.irq_counter = 0;
229 self.irq_pending = true;
230 self.irq_enabled = false;
231 } else {
232 self.irq_counter -= 5;
233 }
234 }
235 }
236
237 fn irq_pending(&self) -> bool {
238 self.irq_pending
239 }
240
241 fn chr_phys(&self, addr: u16) -> Option<u32> {
242 if self.chr_is_ram {
243 None
244 } else {
245 u32::try_from(self.chr_offset(addr & 0x1FFF)).ok()
246 }
247 }
248
249 fn ppu_read(&mut self, addr: u16) -> u8 {
250 let addr = addr & 0x3FFF;
251 match addr {
252 0x0000..=0x1FFF => self.chr[self.chr_offset(addr)],
253 0x2000..=0x3EFF => self.vram[self.nt_offset(addr)],
254 _ => 0,
255 }
256 }
257
258 fn ppu_write(&mut self, addr: u16, value: u8) {
259 let addr = addr & 0x3FFF;
260 match addr {
261 0x0000..=0x1FFF if self.chr_is_ram => {
262 let off = self.chr_offset(addr);
263 self.chr[off] = value;
264 }
265 0x2000..=0x3EFF => {
266 let off = self.nt_offset(addr);
267 self.vram[off] = value;
268 }
269 _ => {}
270 }
271 }
272
273 fn nametable_address(&self, addr: u16) -> u16 {
274 u16::try_from(self.nt_offset(addr)).unwrap_or(0)
275 }
276
277 fn current_mirroring(&self) -> Mirroring {
278 self.mirroring
279 }
280
281 fn debug_info(&self) -> MapperDebugInfo {
282 let mut info = MapperDebugInfo {
283 mapper_id: 91,
284 name: format!("J.Y. 91 (submapper {})", self.submapper),
285 mirroring: crate::mapper::mirroring_name(self.mirroring),
286 ..Default::default()
287 };
288 for (i, r) in self.prg_regs.iter().enumerate() {
289 info.prg_banks.push((format!("P{i}"), format!("{r:#04x}")));
290 }
291 for (i, r) in self.chr_regs.iter().enumerate() {
292 info.chr_banks.push((format!("C{i}"), format!("{r:#04x}")));
293 }
294 info.irq_state
295 .push(("counter".into(), format!("{:#06x}", self.irq_counter)));
296 info.irq_state
297 .push(("enabled".into(), format!("{}", self.irq_enabled)));
298 info.extra
299 .push(("outer".into(), format!("{:#04x}", self.outer)));
300 info
301 }
302
303 fn save_state(&self) -> Vec<u8> {
304 let mut out = Vec::with_capacity(20 + self.vram.len() + self.chr.len());
305 out.push(SAVE_STATE_VERSION);
306 out.push(self.submapper);
307 out.push(match self.mirroring {
308 Mirroring::Horizontal => 0,
309 _ => 1,
310 });
311 out.extend_from_slice(&self.chr_regs);
312 out.extend_from_slice(&self.prg_regs);
313 out.push(self.outer);
314 out.push(u8::from(self.irq_enabled));
315 out.push(u8::from(self.irq_pending));
316 out.extend_from_slice(&self.irq_counter.to_le_bytes());
317 out.push(self.irq_prescale);
318 out.push(u8::from(self.last_a12));
319 out.extend_from_slice(&self.vram);
320 if self.chr_is_ram {
321 out.extend_from_slice(&self.chr);
322 }
323 out
324 }
325
326 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
327 const HEAD: usize = 16;
328 let chr = if self.chr_is_ram { self.chr.len() } else { 0 };
329 let expected = HEAD + self.vram.len() + chr;
330 if data.len() != expected {
331 return Err(MapperError::WrongLength {
332 expected,
333 got: data.len(),
334 });
335 }
336 if data[0] != SAVE_STATE_VERSION {
337 return Err(MapperError::UnsupportedVersion(data[0]));
338 }
339 if data[1] != self.submapper {
340 return Err(MapperError::Invalid(format!(
341 "state is for mapper 91 submapper {}, this board is {}",
342 data[1], self.submapper
343 )));
344 }
345 let counter = u16::from_le_bytes([data[12], data[13]]);
351 let limit = u16::from(A12_RISES_PER_IRQ);
352 if self.submapper == 0 && (counter > limit || (data[10] != 0 && counter >= limit)) {
353 return Err(MapperError::Invalid(format!(
354 "mapper 91 IRQ counter {counter} is not one submapper 0 reaches \
355 (enabled {})",
356 data[10] != 0
357 )));
358 }
359 if self.submapper == 1 {
360 self.mirroring = if data[2] == 0 {
361 Mirroring::Horizontal
362 } else {
363 Mirroring::Vertical
364 };
365 }
366 self.chr_regs.copy_from_slice(&data[3..7]);
367 self.prg_regs.copy_from_slice(&data[7..9]);
368 self.outer = data[9] & 0x07;
369 self.irq_enabled = data[10] != 0;
370 self.irq_pending = data[11] != 0;
371 self.irq_counter = counter;
372 self.irq_prescale = data[14] & 0x03;
373 self.last_a12 = data[15] != 0;
374 self.vram
375 .copy_from_slice(&data[HEAD..HEAD + self.vram.len()]);
376 if self.chr_is_ram {
377 self.chr.copy_from_slice(&data[HEAD + self.vram.len()..]);
378 }
379 Ok(())
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 fn board(prg_8k: usize, chr_2k: usize, sub: u8) -> Jy91 {
388 let mut prg = vec![0u8; prg_8k * PRG_BANK_8K];
389 for b in 0..prg_8k {
390 prg[b * PRG_BANK_8K] = b as u8;
391 }
392 let mut chr = vec![0u8; chr_2k * CHR_BANK_2K];
393 for b in 0..chr_2k {
394 chr[b * CHR_BANK_2K] = b as u8;
395 chr[b * CHR_BANK_2K + 1] = (b >> 8) as u8;
396 }
397 Jy91::new(
398 prg.into_boxed_slice(),
399 chr.into_boxed_slice(),
400 Mirroring::Vertical,
401 sub,
402 )
403 .unwrap()
404 }
405
406 fn chr_at(m: &mut Jy91, addr: u16) -> usize {
407 usize::from(m.ppu_read(addr)) | usize::from(m.ppu_read(addr + 1)) << 8
408 }
409
410 #[test]
411 fn banks_follow_the_register_table() {
412 let mut m = board(16, 16, 0);
413 m.cpu_write(0x7000, 3);
414 m.cpu_write(0x7001, 5);
415 m.cpu_write(0x6002, 7);
416 assert_eq!(m.cpu_read(0x8000), 3);
417 assert_eq!(m.cpu_read(0xA000), 5);
418 assert_eq!(m.cpu_read(0xC000), 14, "the last 16 KiB is fixed");
419 assert_eq!(m.cpu_read(0xE000), 15);
420 assert_eq!(chr_at(&mut m, 0x1000), 7);
421 m.cpu_write(0x6FF6, 9);
423 assert_eq!(chr_at(&mut m, 0x1000), 9);
424 }
425
426 #[test]
427 fn submapper0_outer_bank_comes_from_the_write_address() {
428 let mut m = board(64, 512, 0);
429 m.cpu_write(0x7000, 3);
430 m.cpu_write(0x6000, 0x05);
431 m.cpu_write(0x8007, 0xFF); assert_eq!(m.cpu_read(0x8000), 0x30 | 3);
433 assert_eq!(m.cpu_read(0xE000), 0x3F, "last bank of the 128 KiB block");
434 assert_eq!(chr_at(&mut m, 0x0000), 0x105);
435 m.cpu_write(0x9FF2, 0x00); assert_eq!(m.cpu_read(0x8000), 0x13);
437 assert_eq!(chr_at(&mut m, 0x0000), 0x05);
438 }
439
440 #[test]
441 fn submapper0_irq_after_64_unfiltered_a12_rises() {
442 let mut m = board(16, 16, 0);
443 m.cpu_write(0x7007, 0);
444 for _ in 0..63 {
445 m.notify_a12(true);
446 m.notify_a12(false);
447 }
448 assert!(!m.irq_pending());
449 m.notify_a12(true);
450 assert!(m.irq_pending(), "the 64th rise asserts");
451 m.cpu_write(0x7006, 0);
452 assert!(!m.irq_pending(), "$7006 acknowledges");
453 }
454
455 #[test]
456 fn submapper1_mirroring_and_m2_irq() {
457 let mut m = board(16, 16, 1);
458 m.cpu_write(0x6004, 0);
459 assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
460 m.cpu_write(0x6005, 0);
461 assert_eq!(m.current_mirroring(), Mirroring::Vertical);
462 m.cpu_write(0x6006, 10);
465 m.cpu_write(0x6007, 0);
466 m.cpu_write(0x7007, 0);
467 for _ in 0..11 {
468 m.notify_cpu_cycle();
469 }
470 assert!(!m.irq_pending());
471 m.notify_cpu_cycle();
472 assert!(m.irq_pending());
473 assert!(!m.has_hardwired_mirroring());
475 }
476
477 #[test]
478 fn state_refuses_a_counter_the_board_cannot_reach() {
479 let good = board(16, 16, 0).save_state();
483 for (enabled, counter) in [(1u8, 0xFFFFu16), (1, 64), (0, 65)] {
484 let mut blob = good.clone();
485 blob[10] = enabled;
486 blob[12..14].copy_from_slice(&counter.to_le_bytes());
487 let mut m = board(16, 16, 0);
488 assert!(
489 matches!(m.load_state(&blob), Err(MapperError::Invalid(_))),
490 "enabled {enabled} counter {counter}"
491 );
492 assert_eq!(m.save_state(), good, "refused before any assignment");
493 }
494 for (enabled, counter) in [(1u8, 63u16), (0, 64)] {
495 let mut blob = good.clone();
496 blob[10] = enabled;
497 blob[12..14].copy_from_slice(&counter.to_le_bytes());
498 board(16, 16, 0).load_state(&blob).unwrap();
499 }
500 let mut blob = board(16, 16, 1).save_state();
502 blob[10] = 1;
503 blob[12..14].copy_from_slice(&0xFFFFu16.to_le_bytes());
504 board(16, 16, 1).load_state(&blob).unwrap();
505 }
506
507 #[test]
508 fn state_round_trips() {
509 for sub in [0, 1] {
510 let mut a = board(64, 512, sub);
511 a.cpu_write(0x7000, 7);
512 a.cpu_write(0x6001, 9);
513 a.cpu_write(0x8005, 0);
514 a.cpu_write(0x7007, 0);
515 let blob = a.save_state();
516 let mut b = board(64, 512, sub);
517 b.load_state(&blob).unwrap();
518 assert_eq!(b.save_state(), blob);
519 assert_eq!(b.cpu_read(0x8000), a.cpu_read(0x8000));
520 }
521 assert!(
522 board(16, 16, 1)
523 .load_state(&board(16, 16, 0).save_state())
524 .is_err()
525 );
526 }
527}