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.

https://www.nesdev.org/wiki/UNROM_512

11use crate::bus::cartridge::mappers::{BankSizeKb, NametableMirroring};
12use crate::bus::cartridge::{INesHeader, MapperImpl};
13use bincode::{Decode, Encode};
14use crc::Crc;
15use jgenesis_common::num::GetBit;
17const CRC: Crc<u32> = Crc::<u32>::new(&crc::CRC_32_ISO_HDLC);
18const BLACK_BOX_CHALLENGE_CHECKSUM: u32 = 0xBFB003C9;
19
20pub const MAPPER_NUMBER: u16 = 30;

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

64const FLASH_PRELUDE: &[(u16, u8)] =
65    &[(0xC000, 0x01), (0x9555, 0xAA), (0xC000, 0x00), (0xAAAA, 0x55), (0xC000, 0x01)];

Clear command bytes that occur after the prelude

68const FLASH_CLEAR: &[(u16, u8)] =
69    &[(0x9555, 0x80), (0xC000, 0x01), (0x9555, 0xAA), (0xC000, 0x00), (0xAAAA, 0x55)];

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}