rustynes_mappers/
m041_caltron41.rs1use crate::cartridge::Mirroring;
19use crate::mapper::{Mapper, MapperCaps, MapperError};
20use alloc::{boxed::Box, vec::Vec};
21use alloc::{format, vec};
22
23const PRG_BANK_32K: usize = 0x8000;
24const CHR_BANK_8K: usize = 0x2000;
25const NAMETABLE_SIZE: usize = 0x0400;
26const NAMETABLE_SIZE_U16: u16 = 0x0400;
27
28const SAVE_STATE_VERSION: u8 = 1;
29
30const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
31 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
32 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
33 let physical = mirroring.physical_bank(table);
34 physical * NAMETABLE_SIZE + local
35}
36
37pub struct Caltron41 {
39 prg_rom: Box<[u8]>,
40 chr_rom: Box<[u8]>,
41 vram: Box<[u8]>,
42 prg_bank: u8,
43 outer_chr: u8,
44 inner_chr: u8,
45 inner_enable: bool,
46 horizontal_mirroring: bool,
47}
48
49impl Caltron41 {
50 pub fn new(
57 prg_rom: Box<[u8]>,
58 chr_rom: Box<[u8]>,
59 mirroring: Mirroring,
60 ) -> Result<Self, MapperError> {
61 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
62 return Err(MapperError::Invalid(format!(
63 "mapper 41 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
64 prg_rom.len()
65 )));
66 }
67 if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
68 return Err(MapperError::Invalid(format!(
69 "mapper 41 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
70 chr_rom.len()
71 )));
72 }
73 let horizontal_mirroring = mirroring == Mirroring::Horizontal;
76 Ok(Self {
77 prg_rom,
78 chr_rom,
79 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
80 prg_bank: 0,
81 outer_chr: 0,
82 inner_chr: 0,
83 inner_enable: false,
84 horizontal_mirroring,
85 })
86 }
87
88 const fn chr_bank(&self) -> u8 {
89 (self.outer_chr << 2) | self.inner_chr
90 }
91
92 fn chr_offset(&self, addr: u16) -> usize {
93 let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
94 let bank = (self.chr_bank() as usize) % count;
95 bank * CHR_BANK_8K + addr as usize
96 }
97
98 fn read_prg(&self, addr: u16) -> u8 {
99 let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
100 let bank = (self.prg_bank as usize) % count;
101 self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
102 }
103}
104
105impl Mapper for Caltron41 {
106 fn caps(&self) -> MapperCaps {
107 MapperCaps::NONE
108 }
109
110 fn cpu_read(&mut self, addr: u16) -> u8 {
114 if (0x8000..=0xFFFF).contains(&addr) {
115 self.read_prg(addr)
116 } else {
117 0
118 }
119 }
120
121 fn cpu_write(&mut self, addr: u16, value: u8) {
122 match addr {
123 0x6000..=0x67FF => {
124 let e = ((addr >> 2) & 0x01) as u8;
126 let pp = (addr & 0x03) as u8;
127 self.prg_bank = (e << 2) | pp;
128 self.inner_enable = e != 0;
129 self.outer_chr = ((addr >> 3) & 0x03) as u8;
130 self.horizontal_mirroring = ((addr >> 5) & 0x01) != 0;
131 }
132 0x8000..=0xFFFF if self.inner_enable => {
133 let effective = value & self.read_prg(addr);
135 self.inner_chr = effective & 0x03;
136 }
137 _ => {}
138 }
139 }
140
141 fn ppu_read(&mut self, addr: u16) -> u8 {
142 let addr = addr & 0x3FFF;
143 match addr {
144 0x0000..=0x1FFF => self.chr_rom[self.chr_offset(addr)],
145 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
146 _ => 0,
147 }
148 }
149
150 fn ppu_write(&mut self, addr: u16, value: u8) {
151 let addr = addr & 0x3FFF;
152 if let 0x2000..=0x3EFF = addr {
153 let off = nametable_offset(addr, self.current_mirroring());
154 self.vram[off] = value;
155 }
156 }
157
158 fn current_mirroring(&self) -> Mirroring {
159 if self.horizontal_mirroring {
160 Mirroring::Horizontal
161 } else {
162 Mirroring::Vertical
163 }
164 }
165
166 fn save_state(&self) -> Vec<u8> {
167 let mut out = Vec::with_capacity(6 + self.vram.len());
168 out.push(SAVE_STATE_VERSION);
169 out.push(self.prg_bank);
170 out.push(self.outer_chr);
171 out.push(self.inner_chr);
172 out.push(u8::from(self.inner_enable));
173 out.push(u8::from(self.horizontal_mirroring));
174 out.extend_from_slice(&self.vram);
175 out
176 }
177
178 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
179 let expected = 6 + self.vram.len();
180 if data.len() != expected {
181 return Err(MapperError::WrongLength {
182 expected,
183 got: data.len(),
184 });
185 }
186 if data[0] != SAVE_STATE_VERSION {
187 return Err(MapperError::UnsupportedVersion(data[0]));
188 }
189 self.prg_bank = data[1];
190 self.outer_chr = data[2];
191 self.inner_chr = data[3];
192 self.inner_enable = data[4] != 0;
193 self.horizontal_mirroring = data[5] != 0;
194 self.vram.copy_from_slice(&data[6..6 + self.vram.len()]);
195 Ok(())
196 }
197}
198
199#[cfg(test)]
200#[allow(clippy::cast_possible_truncation)]
201mod tests {
202 use super::*;
203
204 fn synth_prg_32k(banks: usize) -> Box<[u8]> {
205 let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
206 for b in 0..banks {
207 v[b * PRG_BANK_32K] = b as u8;
208 }
209 v.into_boxed_slice()
210 }
211
212 fn synth_chr_8k(banks: usize) -> Box<[u8]> {
213 let mut v = vec![0u8; banks * CHR_BANK_8K];
214 for b in 0..banks {
215 v[b * CHR_BANK_8K] = b as u8;
216 }
217 v.into_boxed_slice()
218 }
219
220 #[test]
221 fn m41_outer_register_decodes_from_address() {
222 let mut m =
223 Caltron41::new(synth_prg_32k(8), synth_chr_8k(16), Mirroring::Vertical).unwrap();
224 let addr = 0x6000 | (1 << 5) | (0b11 << 3) | (1 << 2) | 0b10;
229 m.cpu_write(addr, 0x00);
230 assert_eq!(m.cpu_read(0x8000), 6);
231 assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
232 m.cpu_write(0x8001, 0b01); assert_eq!(m.ppu_read(0x0000), 13);
237 }
238
239 #[test]
240 fn m41_inner_write_gated_by_enable() {
241 let mut m =
242 Caltron41::new(synth_prg_32k(8), synth_chr_8k(16), Mirroring::Vertical).unwrap();
243 let addr = 0x6000 | (0b01 << 3); m.cpu_write(addr, 0x00);
246 m.cpu_write(0x8001, 0b11);
248 assert_eq!(m.ppu_read(0x0000), 4);
250 }
251
252 #[test]
253 fn m41_inner_chr_has_bus_conflict() {
254 let mut m =
257 Caltron41::new(synth_prg_32k(8), synth_chr_8k(16), Mirroring::Vertical).unwrap();
258 let addr = 0x6000 | (1 << 2); m.cpu_write(addr, 0x00);
262 m.cpu_write(0x8000, 0b11);
265 assert_eq!(m.ppu_read(0x0000), 0); }
267}