rustynes_mappers/
m097_irem_tam_s1.rs1#![allow(
19 clippy::bool_to_int_with_if,
20 clippy::cast_lossless,
21 clippy::cast_possible_truncation,
22 clippy::doc_markdown,
23 clippy::match_same_arms,
24 clippy::missing_const_for_fn,
25 clippy::similar_names,
26 clippy::struct_excessive_bools,
27 clippy::too_many_lines,
28 clippy::unreadable_literal
29)]
30
31use crate::cartridge::Mirroring;
32use crate::mapper::{Mapper, MapperCaps, MapperError};
33use alloc::{boxed::Box, vec::Vec};
34use alloc::{format, vec};
35
36const PRG_BANK_16K: usize = 0x4000;
37const CHR_BANK_8K: usize = 0x2000;
38const NAMETABLE_SIZE: usize = 0x0400;
39const NAMETABLE_SIZE_U16: u16 = 0x0400;
40
41const SAVE_STATE_VERSION: u8 = 1;
42
43const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
48 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
49 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
50 let physical = mirroring.physical_bank(table);
51 physical * NAMETABLE_SIZE + local
52}
53
54pub struct Irem97 {
56 prg_rom: Box<[u8]>,
57 chr: Box<[u8]>,
58 vram: Box<[u8]>,
59 chr_is_ram: bool,
60 prg_bank: u8,
61 vertical_mirroring: bool,
62}
63
64impl Irem97 {
65 pub fn new(
72 prg_rom: Box<[u8]>,
73 chr_rom: Box<[u8]>,
74 mirroring: Mirroring,
75 ) -> Result<Self, MapperError> {
76 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
77 return Err(MapperError::Invalid(format!(
78 "mapper 97 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
79 prg_rom.len()
80 )));
81 }
82 let chr_is_ram = chr_rom.is_empty();
83 let chr: Box<[u8]> = if chr_is_ram {
84 vec![0u8; CHR_BANK_8K].into_boxed_slice()
85 } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
86 chr_rom
87 } else {
88 return Err(MapperError::Invalid(format!(
89 "mapper 97 CHR-ROM size {} is not a multiple of 8 KiB",
90 chr_rom.len()
91 )));
92 };
93 Ok(Self {
94 prg_rom,
95 chr,
96 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
97 chr_is_ram,
98 prg_bank: 0,
99 vertical_mirroring: mirroring == Mirroring::Vertical,
100 })
101 }
102
103 fn prg_count_16k(&self) -> usize {
104 (self.prg_rom.len() / PRG_BANK_16K).max(1)
105 }
106}
107
108impl Mapper for Irem97 {
109 fn caps(&self) -> MapperCaps {
110 MapperCaps::NONE
111 }
112
113 fn cpu_read(&mut self, addr: u16) -> u8 {
114 match addr {
115 0x8000..=0xBFFF => {
117 let last = self.prg_count_16k() - 1;
118 self.prg_rom[last * PRG_BANK_16K + (addr as usize - 0x8000)]
119 }
120 0xC000..=0xFFFF => {
122 let bank = (self.prg_bank as usize) % self.prg_count_16k();
123 self.prg_rom[bank * PRG_BANK_16K + (addr as usize - 0xC000)]
124 }
125 _ => 0,
126 }
127 }
128
129 fn cpu_write(&mut self, addr: u16, value: u8) {
130 if (0x8000..=0xFFFF).contains(&addr) {
131 self.prg_bank = value & 0x1F;
132 self.vertical_mirroring = (value & 0x80) != 0;
133 }
134 }
135
136 fn ppu_read(&mut self, addr: u16) -> u8 {
137 let addr = addr & 0x3FFF;
138 match addr {
139 0x0000..=0x1FFF => self.chr[addr as usize],
140 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
141 _ => 0,
142 }
143 }
144
145 fn ppu_write(&mut self, addr: u16, value: u8) {
146 let addr = addr & 0x3FFF;
147 match addr {
148 0x0000..=0x1FFF => {
149 if self.chr_is_ram {
150 self.chr[addr as usize] = value;
151 }
152 }
153 0x2000..=0x3EFF => {
154 let off = nametable_offset(addr, self.current_mirroring());
155 self.vram[off] = value;
156 }
157 _ => {}
158 }
159 }
160
161 fn current_mirroring(&self) -> Mirroring {
162 if self.vertical_mirroring {
163 Mirroring::Vertical
164 } else {
165 Mirroring::Horizontal
166 }
167 }
168
169 fn save_state(&self) -> Vec<u8> {
170 let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
171 let mut out = Vec::with_capacity(3 + self.vram.len() + chr_extra);
172 out.push(SAVE_STATE_VERSION);
173 out.push(self.prg_bank);
174 out.push(u8::from(self.vertical_mirroring));
175 out.extend_from_slice(&self.vram);
176 if self.chr_is_ram {
177 out.extend_from_slice(&self.chr);
178 }
179 out
180 }
181
182 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
183 let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
184 let expected = 3 + self.vram.len() + chr_extra;
185 if data.len() != expected {
186 return Err(MapperError::WrongLength {
187 expected,
188 got: data.len(),
189 });
190 }
191 if data[0] != SAVE_STATE_VERSION {
192 return Err(MapperError::UnsupportedVersion(data[0]));
193 }
194 self.prg_bank = data[1];
195 self.vertical_mirroring = data[2] != 0;
196 let mut cursor = 3;
197 self.vram
198 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
199 cursor += self.vram.len();
200 if self.chr_is_ram {
201 self.chr
202 .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
203 }
204 Ok(())
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 fn synth_prg_16k(banks: usize) -> Box<[u8]> {
213 let mut v = vec![0xFFu8; banks * PRG_BANK_16K];
214 for b in 0..banks {
215 v[b * PRG_BANK_16K] = b as u8;
216 }
217 v.into_boxed_slice()
218 }
219
220 fn synth_chr_8k(banks: usize) -> Box<[u8]> {
221 let mut v = vec![0u8; banks * CHR_BANK_8K];
222 for b in 0..banks {
223 v[b * CHR_BANK_8K] = b as u8;
224 }
225 v.into_boxed_slice()
226 }
227
228 #[test]
229 fn m97_fixed_first_switchable_second() {
230 let mut m = Irem97::new(synth_prg_16k(8), synth_chr_8k(1), Mirroring::Horizontal).unwrap();
231 assert_eq!(m.cpu_read(0x8000), 7);
233 m.cpu_write(0x8000, 0b1000_0011);
235 assert_eq!(m.cpu_read(0xC000), 3);
236 assert_eq!(m.current_mirroring(), Mirroring::Vertical);
237 assert_eq!(m.cpu_read(0x8000), 7);
239 }
240}