1#![allow(
20 clippy::cast_possible_truncation,
21 clippy::cast_lossless,
22 clippy::match_same_arms,
23 clippy::doc_markdown,
24 clippy::similar_names,
25 clippy::too_many_lines,
26 clippy::missing_const_for_fn,
27 clippy::struct_excessive_bools,
28 clippy::bool_to_int_with_if,
29 clippy::unreadable_literal
30)]
31
32use crate::cartridge::Mirroring;
33use crate::mapper::{Mapper, MapperCaps, MapperError};
34use alloc::{boxed::Box, format, vec, vec::Vec};
35
36const PRG_BANK_8K: usize = 0x2000;
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
54const fn mirroring_to_byte(m: Mirroring) -> u8 {
55 match m {
56 Mirroring::Horizontal => 0,
57 Mirroring::Vertical => 1,
58 Mirroring::SingleScreenA => 2,
59 Mirroring::SingleScreenB => 3,
60 Mirroring::FourScreen => 4,
61 Mirroring::MapperControlled => 5,
62 }
63}
64
65const fn byte_to_mirroring(b: u8, fallback: Mirroring) -> Mirroring {
66 match b {
67 0 => Mirroring::Horizontal,
68 1 => Mirroring::Vertical,
69 2 => Mirroring::SingleScreenA,
70 3 => Mirroring::SingleScreenB,
71 4 => Mirroring::FourScreen,
72 5 => Mirroring::MapperControlled,
73 _ => fallback,
74 }
75}
76
77fn check_prg(prg: &[u8], id: u16) -> Result<(), MapperError> {
79 if prg.is_empty() || !prg.len().is_multiple_of(PRG_BANK_8K) {
80 return Err(MapperError::Invalid(format!(
81 "mapper {id} PRG-ROM size {} is not a non-zero multiple of 8 KiB",
82 prg.len()
83 )));
84 }
85 Ok(())
86}
87
88#[derive(Default, Clone)]
89struct TxcChip {
90 accumulator: u8,
91 inverter: u8,
92 staging: u8,
93 output: u8,
94 increase: bool,
95 invert: bool,
96}
97
98impl TxcChip {
99 const MASK: u8 = 0x07;
100 const SAVE_LEN: usize = 6;
101
102 fn read(&self) -> u8 {
103 (self.accumulator & Self::MASK)
104 | ((self.inverter ^ if self.invert { 0xFF } else { 0 }) & !Self::MASK)
105 }
106
107 fn write(&mut self, addr: u16, value: u8) {
108 if addr < 0x8000 {
109 match addr & 0xE103 {
110 0x4100 => {
111 if self.increase {
112 self.accumulator = self.accumulator.wrapping_add(1);
113 } else {
114 self.accumulator = ((self.accumulator & !Self::MASK)
115 | (self.staging & Self::MASK))
116 ^ if self.invert { 0xFF } else { 0 };
117 }
118 }
119 0x4101 => self.invert = value & 0x01 != 0,
120 0x4102 => {
121 self.staging = value & Self::MASK;
122 self.inverter = value & !Self::MASK;
123 }
124 0x4103 => self.increase = value & 0x01 != 0,
125 _ => {}
126 }
127 } else {
128 self.output = (self.accumulator & 0x0F) | ((self.inverter & 0x08) << 1);
129 }
130 }
131
132 fn save(&self, out: &mut Vec<u8>) {
133 out.push(self.accumulator);
134 out.push(self.inverter);
135 out.push(self.staging);
136 out.push(self.output);
137 out.push(u8::from(self.increase));
138 out.push(u8::from(self.invert));
139 }
140
141 fn load(&mut self, d: &[u8]) {
142 self.accumulator = d[0];
143 self.inverter = d[1];
144 self.staging = d[2];
145 self.output = d[3];
146 self.increase = d[4] != 0;
147 self.invert = d[5] != 0;
148 }
149}
150
151pub struct Sachen3011 {
153 prg_rom: Box<[u8]>,
154 chr: Box<[u8]>,
155 chr_is_ram: bool,
156 vram: Box<[u8]>,
157 mirroring: Mirroring,
158 chr_count_8k: usize,
159 txc: TxcChip,
160}
161
162impl Sachen3011 {
163 fn new(
164 prg_rom: Box<[u8]>,
165 chr_rom: Box<[u8]>,
166 mirroring: Mirroring,
167 ) -> Result<Self, MapperError> {
168 check_prg(&prg_rom, 136)?;
169 let chr_is_ram = chr_rom.is_empty();
170 let chr: Box<[u8]> = if chr_is_ram {
171 vec![0u8; CHR_BANK_8K].into_boxed_slice()
172 } else {
173 chr_rom
174 };
175 let chr_count_8k = (chr.len() / CHR_BANK_8K).max(1);
176 Ok(Self {
177 prg_rom,
178 chr,
179 chr_is_ram,
180 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
181 mirroring,
182 chr_count_8k,
183 txc: TxcChip::default(),
184 })
185 }
186}
187
188impl Mapper for Sachen3011 {
189 fn caps(&self) -> MapperCaps {
190 MapperCaps::NONE
191 }
192
193 fn cpu_read(&mut self, addr: u16) -> u8 {
194 match addr {
195 0x4100..=0x5FFF => {
196 let v = if addr & 0x103 == 0x100 {
198 self.txc.read() & 0x3F
199 } else {
200 0
201 };
202 self.txc.write(addr, 0); v
204 }
205 0x8000..=0xFFFF => {
206 self.prg_rom[(addr as usize & 0x7FFF) % self.prg_rom.len()]
215 }
216 _ => 0,
217 }
218 }
219
220 fn cpu_read_unmapped(&self, addr: u16) -> bool {
221 (matches!(addr, 0x6000..=0x7FFF) && self.sram().is_empty()) || {
224 (0x4020..=0x5FFF).contains(&addr) && (addr & 0x103 != 0x100)
226 }
227 }
228
229 fn cpu_write(&mut self, addr: u16, value: u8) {
230 if (0x4100..=0xFFFF).contains(&addr) {
231 self.txc.write(addr, value & 0x3F);
232 }
233 }
234
235 fn ppu_read(&mut self, addr: u16) -> u8 {
236 let addr = addr & 0x3FFF;
237 match addr {
238 0x0000..=0x1FFF => {
239 let bank = (self.txc.output as usize) % self.chr_count_8k;
240 self.chr[bank * CHR_BANK_8K + (addr as usize & 0x1FFF)]
241 }
242 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
243 _ => 0,
244 }
245 }
246
247 fn ppu_write(&mut self, addr: u16, value: u8) {
248 let addr = addr & 0x3FFF;
249 match addr {
250 0x0000..=0x1FFF if self.chr_is_ram => {
251 self.chr[addr as usize & (CHR_BANK_8K - 1)] = value;
252 }
253 0x2000..=0x3EFF => {
254 let off = nametable_offset(addr, self.mirroring);
255 self.vram[off] = value;
256 }
257 _ => {}
258 }
259 }
260
261 fn current_mirroring(&self) -> Mirroring {
262 self.mirroring
263 }
264
265 fn save_state(&self) -> Vec<u8> {
266 let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
267 let mut out = Vec::with_capacity(1 + TxcChip::SAVE_LEN + 1 + self.vram.len() + chr_ram);
268 out.push(SAVE_STATE_VERSION);
269 self.txc.save(&mut out);
270 out.push(mirroring_to_byte(self.mirroring));
271 out.extend_from_slice(&self.vram);
272 if self.chr_is_ram {
273 out.extend_from_slice(&self.chr);
274 }
275 out
276 }
277
278 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
279 let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
280 let expected = 1 + TxcChip::SAVE_LEN + 1 + self.vram.len() + chr_ram;
281 if data.len() != expected {
282 return Err(MapperError::WrongLength {
283 expected,
284 got: data.len(),
285 });
286 }
287 if data[0] != SAVE_STATE_VERSION {
288 return Err(MapperError::UnsupportedVersion(data[0]));
289 }
290 let mut c = 1;
291 self.txc.load(&data[c..c + TxcChip::SAVE_LEN]);
292 c += TxcChip::SAVE_LEN;
293 self.mirroring = byte_to_mirroring(data[c], self.mirroring);
294 c += 1;
295 self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
296 c += self.vram.len();
297 if self.chr_is_ram {
298 self.chr.copy_from_slice(&data[c..c + self.chr.len()]);
299 }
300 Ok(())
301 }
302}
303
304pub fn new_m136(
309 prg_rom: Box<[u8]>,
310 chr_rom: Box<[u8]>,
311 mirroring: Mirroring,
312) -> Result<Sachen3011, MapperError> {
313 Sachen3011::new(prg_rom, chr_rom, mirroring)
314}
315
316#[cfg(test)]
317#[allow(clippy::cast_possible_truncation)]
318mod tests {
319 use super::*;
320
321 fn synth_prg_8k(banks: usize) -> Box<[u8]> {
322 let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
323 for b in 0..banks {
324 v[b * PRG_BANK_8K] = b as u8;
325 }
326 v.into_boxed_slice()
327 }
328
329 fn synth_chr_8k(banks: usize) -> Box<[u8]> {
330 let mut v = vec![0u8; banks * CHR_BANK_8K];
331 for b in 0..banks {
332 v[b * CHR_BANK_8K] = b as u8;
333 }
334 v.into_boxed_slice()
335 }
336
337 #[test]
338 fn sachen3011_txc_chr_select() {
339 let mut m = new_m136(synth_prg_8k(4), synth_chr_8k(8), Mirroring::Vertical).unwrap();
340 m.cpu_write(0x4102, 0x03); m.cpu_write(0x4103, 0x00); m.cpu_write(0x4100, 0x00); m.cpu_write(0x8000, 0x00); assert_eq!(m.ppu_read(0x0000), 3);
347 }
348
349 #[test]
350 fn sachen3011_save_state_round_trip() {
351 let mut m = new_m136(synth_prg_8k(4), synth_chr_8k(8), Mirroring::Vertical).unwrap();
352 m.cpu_write(0x4102, 0x02);
353 m.cpu_write(0x4100, 0x00);
354 m.cpu_write(0x8000, 0x00);
355 m.ppu_write(0x2003, 0x11);
356 let blob = m.save_state();
357 let mut m2 = new_m136(synth_prg_8k(4), synth_chr_8k(8), Mirroring::Vertical).unwrap();
358 m2.load_state(&blob).unwrap();
359 assert_eq!(m2.ppu_read(0x0000), m.ppu_read(0x0000));
360 assert_eq!(m2.ppu_read(0x2003), 0x11);
361 }
362}