rustynes_mappers/
m094_un1rom.rs1#![allow(
18 clippy::bool_to_int_with_if,
19 clippy::cast_lossless,
20 clippy::cast_possible_truncation,
21 clippy::doc_markdown,
22 clippy::match_same_arms,
23 clippy::missing_const_for_fn,
24 clippy::similar_names,
25 clippy::struct_excessive_bools,
26 clippy::too_many_lines,
27 clippy::unreadable_literal
28)]
29
30use crate::cartridge::Mirroring;
31use crate::mapper::{Mapper, MapperCaps, MapperError};
32use alloc::{boxed::Box, vec::Vec};
33use alloc::{format, vec};
34
35const PRG_BANK_16K: usize = 0x4000;
36const CHR_BANK_8K: usize = 0x2000;
37const NAMETABLE_SIZE: usize = 0x0400;
38const NAMETABLE_SIZE_U16: u16 = 0x0400;
39
40const SAVE_STATE_VERSION: u8 = 1;
41
42const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
43 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
44 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
45 let physical = mirroring.physical_bank(table);
46 physical * NAMETABLE_SIZE + local
47}
48
49pub struct Un1rom94 {
51 prg_rom: Box<[u8]>,
52 chr_ram: Box<[u8]>,
53 vram: Box<[u8]>,
54 prg_bank: u8,
55 mirroring: Mirroring,
56}
57
58impl Un1rom94 {
59 pub fn new(
66 prg_rom: Box<[u8]>,
67 _chr_rom: &[u8],
68 mirroring: Mirroring,
69 ) -> Result<Self, MapperError> {
70 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
71 return Err(MapperError::Invalid(format!(
72 "mapper 94 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
73 prg_rom.len()
74 )));
75 }
76 Ok(Self {
77 prg_rom,
78 chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
79 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
80 prg_bank: 0,
81 mirroring,
82 })
83 }
84
85 fn read_prg(&self, bank: usize, addr: u16) -> u8 {
86 let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
87 let bank = bank % count;
88 self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
89 }
90}
91
92impl Mapper for Un1rom94 {
93 fn has_hardwired_mirroring(&self) -> bool {
96 true
97 }
98
99 fn caps(&self) -> MapperCaps {
100 MapperCaps::NONE
101 }
102
103 fn cpu_read(&mut self, addr: u16) -> u8 {
104 match addr {
105 0x8000..=0xBFFF => self.read_prg(self.prg_bank as usize, addr),
106 0xC000..=0xFFFF => {
107 let last = (self.prg_rom.len() / PRG_BANK_16K).max(1) - 1;
108 self.read_prg(last, addr)
109 }
110 _ => 0,
111 }
112 }
113
114 fn cpu_write(&mut self, addr: u16, value: u8) {
115 if (0x8000..=0xFFFF).contains(&addr) {
116 let conflict = self.cpu_read(addr);
121 let effective = value & conflict;
122 self.prg_bank = (effective >> 2) & 0x07;
125 }
126 }
127
128 fn ppu_read(&mut self, addr: u16) -> u8 {
129 let addr = addr & 0x3FFF;
130 match addr {
131 0x0000..=0x1FFF => self.chr_ram[addr as usize],
132 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
133 _ => 0,
134 }
135 }
136
137 fn ppu_write(&mut self, addr: u16, value: u8) {
138 let addr = addr & 0x3FFF;
139 match addr {
140 0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
141 0x2000..=0x3EFF => {
142 let off = nametable_offset(addr, self.mirroring);
143 self.vram[off] = value;
144 }
145 _ => {}
146 }
147 }
148
149 fn current_mirroring(&self) -> Mirroring {
150 self.mirroring
151 }
152
153 fn save_state(&self) -> Vec<u8> {
154 let mut out = Vec::with_capacity(2 + self.vram.len() + self.chr_ram.len());
155 out.push(SAVE_STATE_VERSION);
156 out.push(self.prg_bank);
157 out.extend_from_slice(&self.vram);
158 out.extend_from_slice(&self.chr_ram);
159 out
160 }
161
162 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
163 let expected = 2 + self.vram.len() + self.chr_ram.len();
164 if data.len() != expected {
165 return Err(MapperError::WrongLength {
166 expected,
167 got: data.len(),
168 });
169 }
170 if data[0] != SAVE_STATE_VERSION {
171 return Err(MapperError::UnsupportedVersion(data[0]));
172 }
173 self.prg_bank = data[1];
174 let mut cursor = 2;
175 self.vram
176 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
177 cursor += self.vram.len();
178 self.chr_ram
179 .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
180 Ok(())
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 fn synth_prg_16k(banks: usize) -> Box<[u8]> {
189 let mut v = vec![0xFFu8; banks * PRG_BANK_16K];
190 for b in 0..banks {
191 v[b * PRG_BANK_16K] = b as u8;
192 }
193 v.into_boxed_slice()
194 }
195
196 #[test]
197 fn m94_prg_bank_from_data_bits() {
198 let mut m = Un1rom94::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
199 m.cpu_write(0x8001, 0b0000_1100);
202 assert_eq!(m.cpu_read(0x8000), 3);
203 assert_eq!(m.cpu_read(0xC000), 7);
205 }
206
207 #[test]
208 fn m94_save_state_round_trip() {
209 let mut m = Un1rom94::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
210 m.cpu_write(0x8001, 0b0001_0000); m.ppu_write(0x0002, 0x9A);
212 let blob = m.save_state();
213 let mut m2 = Un1rom94::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
214 m2.load_state(&blob).unwrap();
215 assert_eq!(m2.cpu_read(0x8000), 4);
216 assert_eq!(m2.ppu_read(0x0002), 0x9A);
217 }
218}