rustynes_mappers/
m011_color_dreams.rs1#![allow(
12 clippy::cast_possible_truncation,
13 clippy::cast_lossless,
14 clippy::missing_const_for_fn,
15 clippy::needless_pass_by_ref_mut,
16 clippy::manual_range_patterns,
17 clippy::match_same_arms,
18 clippy::too_many_arguments
19)]
20
21use crate::cartridge::Mirroring;
22use crate::mapper::{Mapper, MapperCaps, MapperError};
23use alloc::{boxed::Box, vec::Vec};
24use alloc::{format, vec};
25
26const CHR_BANK_8K: usize = 0x2000;
27const NAMETABLE_SIZE: usize = 0x0400;
28const NAMETABLE_SIZE_U16: u16 = 0x0400;
29
30const COLOR_DREAMS_SECTION_VERSION: u8 = 2;
43
44fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
45 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
46 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
47 let physical = mirroring.physical_bank(table);
48 physical * NAMETABLE_SIZE + local
49}
50
51pub struct ColorDreams {
53 prg_rom: Box<[u8]>,
54 chr_rom: Box<[u8]>,
55 vram: Box<[u8]>,
56 chr_is_ram: bool,
57 prg_bank: u8,
58 chr_bank: u8,
59 mirroring: Mirroring,
60}
61
62impl ColorDreams {
63 pub fn new(
69 prg_rom: Box<[u8]>,
70 chr_rom: Box<[u8]>,
71 mirroring: Mirroring,
72 ) -> Result<Self, MapperError> {
73 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(32 * 1024) {
74 return Err(MapperError::Invalid(format!(
75 "Color Dreams PRG-ROM size {} is not a non-zero multiple of 32 KiB",
76 prg_rom.len()
77 )));
78 }
79 let chr_is_ram = chr_rom.is_empty();
80 let chr: Box<[u8]> = if chr_is_ram {
81 vec![0u8; CHR_BANK_8K].into_boxed_slice()
82 } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
83 chr_rom
84 } else {
85 return Err(MapperError::Invalid(format!(
86 "Color Dreams CHR-ROM size {} is not a multiple of 8 KiB",
87 chr_rom.len()
88 )));
89 };
90 Ok(Self {
91 prg_rom,
92 chr_rom: chr,
93 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
94 chr_is_ram,
95 prg_bank: 0,
96 chr_bank: 0,
97 mirroring,
98 })
99 }
100}
101
102impl Mapper for ColorDreams {
103 fn has_hardwired_mirroring(&self) -> bool {
106 true
107 }
108
109 fn caps(&self) -> MapperCaps {
112 MapperCaps::NONE
113 }
114
115 fn cpu_read(&mut self, addr: u16) -> u8 {
116 if addr < 0x8000 {
117 return 0;
118 }
119 let total_32k = (self.prg_rom.len() / (32 * 1024)).max(1);
120 let bank = (self.prg_bank as usize) % total_32k;
121 let off = bank * 32 * 1024 + (addr as usize - 0x8000);
122 self.prg_rom[off % self.prg_rom.len()]
123 }
124
125 fn cpu_write(&mut self, addr: u16, value: u8) {
126 if addr >= 0x8000 {
127 let conflict = self.cpu_read(addr);
129 let v = value & conflict;
130 self.prg_bank = v & 0x03;
131 self.chr_bank = (v >> 4) & 0x0F;
132 }
133 }
134
135 fn ppu_read(&mut self, addr: u16) -> u8 {
136 let addr = addr & 0x3FFF;
137 match addr {
138 0x0000..=0x1FFF => {
139 let total_8k = (self.chr_rom.len() / CHR_BANK_8K).max(1);
140 let bank = (self.chr_bank as usize) % total_8k;
141 self.chr_rom[(bank * CHR_BANK_8K + addr as usize) % self.chr_rom.len()]
142 }
143 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring) % self.vram.len()],
144 _ => 0,
145 }
146 }
147
148 fn ppu_write(&mut self, addr: u16, value: u8) {
149 let addr = addr & 0x3FFF;
150 if let 0x2000..=0x3EFF = addr {
151 let off = nametable_offset(addr, self.mirroring) % self.vram.len();
152 self.vram[off] = value;
153 } else if (0x0000..=0x1FFF).contains(&addr) && self.chr_is_ram {
154 let total_8k = (self.chr_rom.len() / CHR_BANK_8K).max(1);
155 let bank = (self.chr_bank as usize) % total_8k;
156 let off = (bank * CHR_BANK_8K + addr as usize) % self.chr_rom.len();
157 self.chr_rom[off] = value;
158 }
159 }
160
161 fn current_mirroring(&self) -> Mirroring {
162 self.mirroring
163 }
164
165 fn save_state(&self) -> Vec<u8> {
166 let mut out = Vec::with_capacity(4 + self.vram.len() + self.ram_block_len());
167 out.push(COLOR_DREAMS_SECTION_VERSION);
168 out.push(self.prg_bank);
169 out.push(self.chr_bank);
170 out.extend_from_slice(&self.vram);
171 if self.chr_is_ram {
173 out.extend_from_slice(&self.chr_rom);
174 }
175 out
176 }
177
178 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
179 let version = data.first().copied().unwrap_or(0);
180 if version != COLOR_DREAMS_SECTION_VERSION {
184 return Err(MapperError::UnsupportedVersion(version));
185 }
186 let ram_len = self.ram_block_len();
187 let core_len = 3 + self.vram.len();
189 let expected = core_len + ram_len;
190 if data.len() != expected {
191 return Err(MapperError::WrongLength {
192 expected,
193 got: data.len(),
194 });
195 }
196 self.prg_bank = data[1];
197 self.chr_bank = data[2];
198 self.vram.copy_from_slice(&data[3..core_len]);
199 if self.chr_is_ram {
200 self.chr_rom.copy_from_slice(&data[core_len..]);
201 }
202 Ok(())
203 }
204}
205
206impl ColorDreams {
207 fn ram_block_len(&self) -> usize {
211 if self.chr_is_ram {
212 self.chr_rom.len()
213 } else {
214 0
215 }
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn color_dreams_bus_conflict() {
225 let mut prg = vec![0u8; 32 * 1024];
226 prg[0] = 0x55;
228 let m_prg: Box<[u8]> = prg.into_boxed_slice();
229 let chr = vec![0u8; 8 * 1024].into_boxed_slice();
230 let mut m = ColorDreams::new(m_prg, chr, Mirroring::Vertical).unwrap();
231 m.cpu_write(0x8000, 0xFF);
232 assert_eq!(m.prg_bank, 1);
234 }
235
236 #[test]
240 fn color_dreams_save_state_carries_chr_ram() {
241 let mut m = ColorDreams::new(
242 vec![0u8; 32 * 1024].into_boxed_slice(),
243 Box::new([]),
244 Mirroring::Vertical,
245 )
246 .unwrap();
247 m.chr_rom[0x0000] = 0x11;
248 m.chr_rom[0x1FFF] = 0x22;
249 let blob = m.save_state();
250 let mut m2 = ColorDreams::new(
251 vec![0u8; 32 * 1024].into_boxed_slice(),
252 Box::new([]),
253 Mirroring::Vertical,
254 )
255 .unwrap();
256 m2.load_state(&blob).expect("round-trip");
257 assert_eq!(m2.chr_rom[0x0000], 0x11);
258 assert_eq!(m2.chr_rom[0x1FFF], 0x22);
259 }
260
261 #[test]
264 fn color_dreams_v1_blob_is_refused() {
265 let mut m = ColorDreams::new(
266 vec![0u8; 32 * 1024].into_boxed_slice(),
267 Box::new([]),
268 Mirroring::Vertical,
269 )
270 .unwrap();
271 m.chr_bank = 5;
272 let core_len = 3 + m.vram.len();
273 let mut v1 = m.save_state()[..core_len].to_vec();
274 v1[0] = 1;
275 let mut m2 = ColorDreams::new(
276 vec![0u8; 32 * 1024].into_boxed_slice(),
277 Box::new([]),
278 Mirroring::Vertical,
279 )
280 .unwrap();
281 assert!(matches!(
282 m2.load_state(&v1),
283 Err(MapperError::UnsupportedVersion(1))
284 ));
285 }
286
287 #[test]
289 fn color_dreams_truncated_chr_ram_tail_is_rejected() {
290 let m = ColorDreams::new(
291 vec![0u8; 32 * 1024].into_boxed_slice(),
292 Box::new([]),
293 Mirroring::Vertical,
294 )
295 .unwrap();
296 let blob = m.save_state();
297 let mut m2 = ColorDreams::new(
298 vec![0u8; 32 * 1024].into_boxed_slice(),
299 Box::new([]),
300 Mirroring::Vertical,
301 )
302 .unwrap();
303 let err = m2
304 .load_state(&blob[..blob.len() - 1])
305 .expect_err("a truncated v2 blob must be rejected");
306 assert!(matches!(err, MapperError::WrongLength { .. }), "{err:?}");
307 }
308}