UNROM 512 (iNES mapper 30)
This is a homebrew mapper that extends UNROM to support up to 512KB of PRG ROM and up to 32KB of bankable CHR RAM.
There is a variant of this board that supports "four-screen VRAM" by mapping the nametable addresses to CHR RAM, and there is another variant with flashable battery-backed PRG ROM.
iNES headers don't always specify the CHR RAM size, so default to 32KB if header is not NES 2.0
23pub const INES_CHR_RAM_LEN: u32 = 32 * 1024;
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 26enum HardwiredNametableMirroring { 27 Horizontal, 28 Vertical, 29 SingleScreen, 30 FourScreen, 31} 32 33impl HardwiredNametableMirroring { 34 fn from_header(header: &INesHeader) -> Self { 35 // Bits 3 (4-screen VRAM) and 0 (horizontal / vertical mirroring) determine mirroring: 36 // 00 = Horizontal 37 // 01 = Vertical 38 // 10 = Switchable single-screen 39 // 11 = Four-screen 40 match (header.has_four_screen_vram, header.nametable_mirroring) { 41 (false, NametableMirroring::Horizontal) => HardwiredNametableMirroring::Horizontal, 42 (false, NametableMirroring::Vertical) => HardwiredNametableMirroring::Vertical, 43 (true, NametableMirroring::Horizontal) => HardwiredNametableMirroring::SingleScreen, 44 (true, NametableMirroring::Vertical) => HardwiredNametableMirroring::FourScreen, 45 _ => panic!( 46 "iNES header nametable mirroring should always be vertical or horizontal, was {:?}", 47 header.nametable_mirroring 48 ), 49 } 50 } 51 52 fn to_effective(self, single_screen_select: bool) -> Option<NametableMirroring> { 53 match (self, single_screen_select) { 54 (Self::Horizontal, _) => Some(NametableMirroring::Horizontal), 55 (Self::Vertical, _) => Some(NametableMirroring::Vertical), 56 (Self::SingleScreen, false) => Some(NametableMirroring::SingleScreenBank0), 57 (Self::SingleScreen, true) => Some(NametableMirroring::SingleScreenBank1), 58 (Self::FourScreen, _) => None, 59 } 60 } 61}
Prelude for both flash write commands
Clear command bytes that occur after the prelude
Write command byte that occurs after the prelude
72const FLASH_WRITE: (u16, u8) = (0x9555, 0xA0);
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 75enum FlashWriteState { 76 Prelude { idx: u8 }, 77 ClearOrWrite, 78 Clear { idx: u8 }, 79 ClearBank, 80 ClearLastByte { bank: u8 }, 81 WriteBank, 82 WriteLastByte { bank: u8 }, 83} 84 85impl Default for FlashWriteState { 86 fn default() -> Self { 87 Self::Prelude { idx: 0 } 88 } 89} 90 91#[derive(Debug, Clone, Copy, Encode, Decode)] 92struct FlashState { 93 state: FlashWriteState, 94 dirty: bool, 95} 96 97impl FlashState { 98 fn new() -> Self { 99 Self { state: FlashWriteState::default(), dirty: false } 100 } 101} 102 103#[derive(Debug, Clone, Encode, Decode)] 104pub struct Unrom512 { 105 prg_bank: u8, 106 chr_bank: u8, 107 nametable_mirroring: HardwiredNametableMirroring, 108 effective_nametable_mirroring: Option<NametableMirroring>, 109 single_screen_select: bool, 110 flashable: bool, 111 flash_state: FlashState, 112} 113 114impl Unrom512 { 115 pub fn new(original_prg_rom: &[u8], header: &INesHeader) -> Self { 116 let mut nametable_mirroring = HardwiredNametableMirroring::from_header(header); 117 118 if nametable_mirroring == HardwiredNametableMirroring::SingleScreen 119 && CRC.checksum(original_prg_rom) == BLACK_BOX_CHALLENGE_CHECKSUM 120 { 121 // Black Box Challenge expects 4-screen mirroring, but the version of the ROM for download 122 // on the developer's website specifies 1-screen in the iNES header; override that so 123 // the game renders correctly 124 log::info!("Overriding UNROM 512 nametable mirroring from 1-screen to 4-screen"); 125 nametable_mirroring = HardwiredNametableMirroring::FourScreen; 126 } 127 128 log::info!("UNROM 512 nametable mirroring: {nametable_mirroring:?}"); 129 130 Self { 131 prg_bank: 0, 132 chr_bank: 0, 133 nametable_mirroring, 134 effective_nametable_mirroring: nametable_mirroring.to_effective(false), 135 single_screen_select: false, 136 flashable: header.has_battery, 137 flash_state: FlashState::new(), 138 } 139 } 140} 141 142enum PpuMapResult { 143 PpuVram(u16), 144 ChrRam(u32), 145} 146 147impl MapperImpl<Unrom512> { 148 pub fn is_flashable(&self) -> bool { 149 self.data.flashable 150 } 151 152 pub fn get_and_clear_dirty_bit(&mut self) -> bool { 153 let dirty = self.data.flash_state.dirty; 154 self.data.flash_state.dirty = false; 155 dirty 156 } 157 158 pub fn read_cpu_address(&mut self, address: u16, cpu_open_bus: u8) -> u8 { 159 match address { 160 0x8000..=0xBFFF => { 161 // Mappable 16KB PRG ROM bank 162 let rom_addr = BankSizeKb::Sixteen.to_absolute_address(self.data.prg_bank, address); 163 self.cartridge.get_prg_rom(rom_addr) 164 } 165 0xC000..=0xFFFF => { 166 // Fixed to last 16KB of PRG ROM 167 let rom_addr = BankSizeKb::Sixteen 168 .to_absolute_address_last_bank(self.cartridge.prg_rom.len() as u32, address); 169 self.cartridge.get_prg_rom(rom_addr) 170 } 171 0x0000..=0x401F => panic!("Invalid CPU map address: {address:04X}"), 172 0x4020..=0x7FFF => cpu_open_bus, 173 } 174 } 175 176 pub fn write_cpu_address(&mut self, address: u16, value: u8) { 177 // This mapper only has one register located at $8000-$FFFF 178 if !address.bit(15) { 179 return; 180 } 181 182 let single_screen_select = value.bit(7); 183 self.data.single_screen_select = single_screen_select; 184 self.data.effective_nametable_mirroring = 185 self.data.nametable_mirroring.to_effective(single_screen_select); 186 187 self.data.chr_bank = (value >> 5) & 0x03; 188 self.data.prg_bank = value & 0x1F; 189 190 if self.data.flashable { 191 self.try_flash_write(address, value); 192 } 193 } 194 195 fn try_flash_write(&mut self, address: u16, value: u8) { 196 self.data.flash_state.state = match self.data.flash_state.state { 197 FlashWriteState::Prelude { idx } => { 198 if (address, value) == FLASH_PRELUDE[idx as usize] { 199 let new_idx = idx + 1; 200 if new_idx == FLASH_PRELUDE.len() as u8 { 201 FlashWriteState::ClearOrWrite 202 } else { 203 FlashWriteState::Prelude { idx: new_idx } 204 } 205 } else { 206 FlashWriteState::default() 207 } 208 } 209 FlashWriteState::ClearOrWrite => { 210 if (address, value) == FLASH_CLEAR[0] { 211 FlashWriteState::Clear { idx: 1 } 212 } else if (address, value) == FLASH_WRITE { 213 FlashWriteState::WriteBank 214 } else { 215 FlashWriteState::default() 216 } 217 } 218 FlashWriteState::Clear { idx } => { 219 if (address, value) == FLASH_CLEAR[idx as usize] { 220 let new_idx = idx + 1; 221 if new_idx == FLASH_CLEAR.len() as u8 { 222 FlashWriteState::ClearBank 223 } else { 224 FlashWriteState::Clear { idx: new_idx } 225 } 226 } else { 227 FlashWriteState::default() 228 } 229 } 230 FlashWriteState::ClearBank => { 231 // Bank is always written to $C000 232 if address == 0xC000 { 233 FlashWriteState::ClearLastByte { bank: value } 234 } else { 235 FlashWriteState::default() 236 } 237 } 238 FlashWriteState::ClearLastByte { bank } => { 239 // Last byte written should always be $30 240 if value == 0x30 { 241 // Bits 12-13 of address specify which 4KB bank to clear within the 16KB bank 242 let base_addr = (u32::from(bank) << 14) 243 | u32::from(address & 0x3000) & (self.cartridge.prg_rom.len() as u32 - 1); 244 let end_addr = base_addr + 0x1000; 245 self.cartridge.prg_rom[base_addr as usize..end_addr as usize].fill(0); 246 self.data.flash_state.dirty = true; 247 248 log::trace!("Cleared PRG ROM [{base_addr:X} {end_addr:X})"); 249 } 250 251 FlashWriteState::default() 252 } 253 FlashWriteState::WriteBank => { 254 // Bank is always written to $C000 255 if address == 0xC000 { 256 FlashWriteState::WriteLastByte { bank: value } 257 } else { 258 FlashWriteState::default() 259 } 260 } 261 FlashWriteState::WriteLastByte { bank } => { 262 // Bits 0-13 of address specify the address within the 16KB bank to write 263 let address = (u32::from(bank) << 14) 264 | u32::from(address & 0x3FFF) & (self.cartridge.prg_rom.len() as u32 - 1); 265 self.cartridge.prg_rom[address as usize] = value; 266 self.data.flash_state.dirty = true; 267 268 log::trace!("PRG ROM flash write: {address:X} {value:02X}"); 269 270 FlashWriteState::default() 271 } 272 }; 273 } 274 275 fn map_ppu_address(&self, address: u16) -> PpuMapResult { 276 match (address, self.data.effective_nametable_mirroring) { 277 (0x0000..=0x1FFF, _) => { 278 // Mappable 8KB CHR RAM bank 279 let ram_addr = BankSizeKb::Eight.to_absolute_address(self.data.chr_bank, address); 280 PpuMapResult::ChrRam(ram_addr) 281 } 282 (0x2000..=0x3FFF, None) => { 283 // Uses 4-screen VRAM; fixed to last 8KB of CHR RAM 284 let ram_addr = BankSizeKb::Eight 285 .to_absolute_address_last_bank(self.cartridge.chr_ram.len() as u32, address); 286 PpuMapResult::ChrRam(ram_addr) 287 } 288 (0x2000..=0x3FFF, Some(nametable_mirroring)) => { 289 // Maps to PPU VRAM using specified nametable mirroring 290 let ram_addr = nametable_mirroring.map_to_vram(address); 291 PpuMapResult::PpuVram(ram_addr) 292 } 293 (0x4000..=0xFFFF, _) => panic!("Invalid PPU map address: {address:04X}"), 294 } 295 } 296 297 pub fn read_ppu_address(&mut self, address: u16, vram: &[u8; 2048]) -> u8 { 298 match self.map_ppu_address(address) { 299 PpuMapResult::PpuVram(vram_addr) => vram[vram_addr as usize], 300 PpuMapResult::ChrRam(chr_ram_addr) => self.cartridge.get_chr_ram(chr_ram_addr), 301 } 302 } 303 304 pub fn write_ppu_address(&mut self, address: u16, value: u8, vram: &mut [u8; 2048]) { 305 match self.map_ppu_address(address) { 306 PpuMapResult::PpuVram(vram_addr) => { 307 vram[vram_addr as usize] = value; 308 } 309 PpuMapResult::ChrRam(chr_ram_addr) => { 310 self.cartridge.set_chr_ram(chr_ram_addr, value); 311 } 312 } 313 } 314}