rustynes_mappers/
m038_bitcorp38.rs1use crate::cartridge::Mirroring;
16use crate::mapper::{Mapper, MapperCaps, MapperError};
17use alloc::{boxed::Box, vec::Vec};
18use alloc::{format, vec};
19
20const PRG_BANK_32K: usize = 0x8000;
21const CHR_BANK_8K: usize = 0x2000;
22const NAMETABLE_SIZE: usize = 0x0400;
23const NAMETABLE_SIZE_U16: u16 = 0x0400;
24
25const SAVE_STATE_VERSION: u8 = 1;
26
27const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
28 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
29 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
30 let physical = mirroring.physical_bank(table);
31 physical * NAMETABLE_SIZE + local
32}
33
34pub struct Bitcorp38 {
44 prg_rom: Box<[u8]>,
45 chr_rom: Box<[u8]>,
46 vram: Box<[u8]>,
47 prg_bank: u8,
48 chr_bank: u8,
49 mirroring: Mirroring,
50}
51
52impl Bitcorp38 {
53 pub fn new(
60 prg_rom: Box<[u8]>,
61 chr_rom: Box<[u8]>,
62 mirroring: Mirroring,
63 ) -> Result<Self, MapperError> {
64 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
65 return Err(MapperError::Invalid(format!(
66 "mapper 38 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
67 prg_rom.len()
68 )));
69 }
70 if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
71 return Err(MapperError::Invalid(format!(
72 "mapper 38 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
73 chr_rom.len()
74 )));
75 }
76 Ok(Self {
77 prg_rom,
78 chr_rom,
79 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
80 prg_bank: 0,
81 chr_bank: 0,
82 mirroring,
83 })
84 }
85}
86
87impl Mapper for Bitcorp38 {
88 fn has_hardwired_mirroring(&self) -> bool {
91 true
92 }
93
94 fn caps(&self) -> MapperCaps {
95 MapperCaps::NONE
96 }
97
98 fn cpu_read(&mut self, addr: u16) -> u8 {
99 if (0x8000..=0xFFFF).contains(&addr) {
100 let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
101 let bank = (self.prg_bank as usize) % count;
102 self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
103 } else {
104 0
105 }
106 }
107
108 fn cpu_write(&mut self, addr: u16, value: u8) {
109 if (0x7000..=0x7FFF).contains(&addr) {
110 self.prg_bank = value & 0x03;
111 self.chr_bank = (value >> 2) & 0x03;
112 }
113 }
114
115 fn ppu_read(&mut self, addr: u16) -> u8 {
116 let addr = addr & 0x3FFF;
117 match addr {
118 0x0000..=0x1FFF => {
119 let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
120 let bank = (self.chr_bank as usize) % count;
121 self.chr_rom[bank * CHR_BANK_8K + addr as usize]
122 }
123 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
124 _ => 0,
125 }
126 }
127
128 fn ppu_write(&mut self, addr: u16, value: u8) {
129 let addr = addr & 0x3FFF;
130 if let 0x2000..=0x3EFF = addr {
131 let off = nametable_offset(addr, self.mirroring);
132 self.vram[off] = value;
133 }
134 }
135
136 fn current_mirroring(&self) -> Mirroring {
137 self.mirroring
138 }
139
140 fn save_state(&self) -> Vec<u8> {
141 let mut out = Vec::with_capacity(3 + self.vram.len());
142 out.push(SAVE_STATE_VERSION);
143 out.push(self.prg_bank);
144 out.push(self.chr_bank);
145 out.extend_from_slice(&self.vram);
146 out
147 }
148
149 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
150 let expected = 3 + self.vram.len();
151 if data.len() != expected {
152 return Err(MapperError::WrongLength {
153 expected,
154 got: data.len(),
155 });
156 }
157 if data[0] != SAVE_STATE_VERSION {
158 return Err(MapperError::UnsupportedVersion(data[0]));
159 }
160 self.prg_bank = data[1];
161 self.chr_bank = data[2];
162 self.vram.copy_from_slice(&data[3..3 + self.vram.len()]);
163 Ok(())
164 }
165}
166
167#[cfg(test)]
168#[allow(clippy::cast_possible_truncation)]
169mod tests {
170 use super::*;
171
172 fn synth_prg_32k(banks: usize) -> Box<[u8]> {
173 let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
174 for b in 0..banks {
175 v[b * PRG_BANK_32K] = b as u8;
176 }
177 v.into_boxed_slice()
178 }
179
180 fn synth_chr_8k(banks: usize) -> Box<[u8]> {
181 let mut v = vec![0u8; banks * CHR_BANK_8K];
182 for b in 0..banks {
183 v[b * CHR_BANK_8K] = b as u8;
184 }
185 v.into_boxed_slice()
186 }
187
188 #[test]
189 fn m38_latch_selects_prg_and_chr() {
190 let mut m = Bitcorp38::new(synth_prg_32k(4), synth_chr_8k(4), Mirroring::Vertical).unwrap();
191 m.cpu_write(0x7123, 0b0000_1110);
193 assert_eq!(m.cpu_read(0x8000), 2);
194 assert_eq!(m.ppu_read(0x0000), 3);
195 m.cpu_write(0x6000, 0xFF);
197 assert_eq!(m.cpu_read(0x8000), 2);
198 }
199
200 #[test]
201 fn m38_save_state_round_trip() {
202 let mut m = Bitcorp38::new(synth_prg_32k(4), synth_chr_8k(4), Mirroring::Vertical).unwrap();
203 m.cpu_write(0x7000, 0b0000_1101); let blob = m.save_state();
205 let mut m2 =
206 Bitcorp38::new(synth_prg_32k(4), synth_chr_8k(4), Mirroring::Vertical).unwrap();
207 m2.load_state(&blob).unwrap();
208 assert_eq!(m2.cpu_read(0x8000), 1);
209 assert_eq!(m2.ppu_read(0x0000), 3);
210 }
211}