rustynes_mappers/
m180_nichibutsu180.rs1use crate::cartridge::Mirroring;
20use crate::mapper::{Mapper, MapperCaps, MapperError};
21use alloc::{boxed::Box, vec::Vec};
22use alloc::{format, vec};
23
24const PRG_BANK_16K: usize = 0x4000;
25const CHR_BANK_8K: usize = 0x2000;
26const NAMETABLE_SIZE: usize = 0x0400;
27const NAMETABLE_SIZE_U16: u16 = 0x0400;
28
29const SAVE_STATE_VERSION: u8 = 1;
30
31const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
36 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
37 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
38 let physical = mirroring.physical_bank(table);
39 physical * NAMETABLE_SIZE + local
40}
41
42pub struct Nichibutsu180 {
44 prg_rom: Box<[u8]>,
45 chr: Box<[u8]>,
46 vram: Box<[u8]>,
47 chr_is_ram: bool,
48 prg_bank: u8,
49 mirroring: Mirroring,
50}
51
52impl Nichibutsu180 {
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_16K) {
65 return Err(MapperError::Invalid(format!(
66 "mapper 180 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
67 prg_rom.len()
68 )));
69 }
70 let chr_is_ram = chr_rom.is_empty();
71 let chr: Box<[u8]> = if chr_is_ram {
72 vec![0u8; CHR_BANK_8K].into_boxed_slice()
73 } else if chr_rom.len() == CHR_BANK_8K {
74 chr_rom
75 } else {
76 return Err(MapperError::Invalid(format!(
77 "mapper 180 expects 8 KiB CHR (RAM or ROM); got {} bytes",
78 chr_rom.len()
79 )));
80 };
81 Ok(Self {
82 prg_rom,
83 chr,
84 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
85 chr_is_ram,
86 prg_bank: 0,
87 mirroring,
88 })
89 }
90
91 fn read_prg(&self, bank: usize, offset_in_bank: usize) -> u8 {
92 let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
93 let bank = bank % count;
94 self.prg_rom[bank * PRG_BANK_16K + offset_in_bank]
95 }
96}
97
98impl Mapper for Nichibutsu180 {
99 fn has_hardwired_mirroring(&self) -> bool {
102 true
103 }
104
105 fn caps(&self) -> MapperCaps {
106 MapperCaps::NONE
107 }
108
109 fn cpu_read(&mut self, addr: u16) -> u8 {
110 match addr {
111 0x8000..=0xBFFF => self.read_prg(0, addr as usize - 0x8000),
112 0xC000..=0xFFFF => self.read_prg(self.prg_bank as usize, addr as usize - 0xC000),
113 _ => 0,
114 }
115 }
116
117 fn cpu_write(&mut self, addr: u16, value: u8) {
118 if (0x8000..=0xFFFF).contains(&addr) {
119 let prg_byte = self.cpu_read_at_for_conflict(addr);
121 let effective = value & prg_byte;
122 self.prg_bank = effective & 0x07;
123 }
124 }
125
126 fn ppu_read(&mut self, addr: u16) -> u8 {
127 let addr = addr & 0x3FFF;
128 match addr {
129 0x0000..=0x1FFF => self.chr[addr as usize],
130 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
131 _ => 0,
132 }
133 }
134
135 fn ppu_write(&mut self, addr: u16, value: u8) {
136 let addr = addr & 0x3FFF;
137 match addr {
138 0x0000..=0x1FFF => {
139 if self.chr_is_ram {
140 self.chr[addr as usize] = value;
141 }
142 }
143 0x2000..=0x3EFF => {
144 let off = nametable_offset(addr, self.mirroring);
145 self.vram[off] = value;
146 }
147 _ => {}
148 }
149 }
150
151 fn current_mirroring(&self) -> Mirroring {
152 self.mirroring
153 }
154
155 fn save_state(&self) -> Vec<u8> {
156 let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
157 let mut out = Vec::with_capacity(2 + self.vram.len() + chr_extra);
158 out.push(SAVE_STATE_VERSION);
159 out.push(self.prg_bank);
160 out.extend_from_slice(&self.vram);
161 if self.chr_is_ram {
162 out.extend_from_slice(&self.chr);
163 }
164 out
165 }
166
167 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
168 let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
169 let expected = 2 + self.vram.len() + chr_extra;
170 if data.len() != expected {
171 return Err(MapperError::WrongLength {
172 expected,
173 got: data.len(),
174 });
175 }
176 if data[0] != SAVE_STATE_VERSION {
177 return Err(MapperError::UnsupportedVersion(data[0]));
178 }
179 self.prg_bank = data[1];
180 let mut cursor = 2;
181 self.vram
182 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
183 cursor += self.vram.len();
184 if self.chr_is_ram {
185 self.chr
186 .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
187 }
188 Ok(())
189 }
190}
191
192impl Nichibutsu180 {
193 fn cpu_read_at_for_conflict(&self, addr: u16) -> u8 {
196 match addr {
197 0x8000..=0xBFFF => self.read_prg(0, addr as usize - 0x8000),
198 _ => self.read_prg(self.prg_bank as usize, addr as usize - 0xC000),
199 }
200 }
201}
202
203#[cfg(test)]
204#[allow(clippy::cast_possible_truncation)]
205mod tests {
206 use super::*;
207
208 fn synth_prg_16k(banks: usize) -> Box<[u8]> {
209 let mut v = vec![0xFFu8; banks * PRG_BANK_16K];
210 for b in 0..banks {
211 v[b * PRG_BANK_16K] = b as u8;
212 }
213 v.into_boxed_slice()
214 }
215
216 #[test]
217 fn m180_fixes_low_switches_high() {
218 let mut m =
219 Nichibutsu180::new(synth_prg_16k(8), Box::new([]), Mirroring::Vertical).unwrap();
220 assert_eq!(m.cpu_read(0x8000), 0);
222 m.cpu_write(0xC001, 3);
224 assert_eq!(m.cpu_read(0xC000), 3);
225 assert_eq!(m.cpu_read(0x8000), 0);
227 }
228
229 #[test]
230 fn m180_bus_conflict() {
231 let mut m =
234 Nichibutsu180::new(synth_prg_16k(8), Box::new([]), Mirroring::Vertical).unwrap();
235 m.cpu_write(0xC000, 3);
236 assert_eq!(m.cpu_read(0xC000), 0);
237 }
238}