1use bincode::{Decode, Encode}; 2use jgenesis_common::num::GetBit; 3use jgenesis_proc_macros::MatchEachVariantMacro; 4use std::cmp; 5use std::ops::RangeInclusive; 6 7const CODEMASTERS_CHECKSUM_ADDR: usize = 0x7FE6; 8const SEGA_HEADER_ADDR_RANGE: RangeInclusive<usize> = 0x7FF0..=0x7FFF; 9 10macro_rules! invalid_cartridge_address { 11 ($address:expr) => { 12 panic!("Invalid cartridge address {:04X}, must be $0000-$BFFF", $address) 13 }; 14} 15 16fn read_16kb_banked(bytes: &[u8], address: u16, bank: u32) -> u8 { 17 let rom_addr = (bank << 14) | u32::from(address & 0x3FFF); 18 read_wrapped(bytes, rom_addr) 19} 20 21fn read_wrapped(bytes: &[u8], address: u32) -> u8 { 22 let wrapped_addr = (address as usize) & (bytes.len() - 1); 23 bytes[wrapped_addr] 24} 25 26fn write_16kb_banked(bytes: &mut [u8], address: u16, bank: u32, value: u8) { 27 let rom_addr = (bank << 14) | u32::from(address & 0x3FFF); 28 write_wrapped(bytes, rom_addr, value); 29} 30 31fn write_wrapped(bytes: &mut [u8], address: u32, value: u8) { 32 let wrapped_addr = (address as usize) & (bytes.len() - 1); 33 bytes[wrapped_addr] = value; 34} 35 36#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] 37pub struct SegaMapper { 38 rom_banks: [u32; 3], 39 ram_bank: u32, 40 ram_enabled: bool, 41} 42 43impl SegaMapper { 44 pub fn new() -> Self { 45 Self { rom_banks: [0, 1, 2], ram_bank: 0, ram_enabled: false } 46 } 47 48 pub fn read(&self, address: u16, rom: &[u8], ram: &[u8]) -> u8 { 49 match address { 50 0x0000..=0x03FF => { 51 // First 1KB of ROM 52 read_wrapped(rom, address.into()) 53 } 54 0x0400..=0x7FFF => { 55 // Mappable ROM banks 0/1 56 let bank = self.rom_banks[(address / 0x4000) as usize]; 57 read_16kb_banked(rom, address, bank) 58 } 59 0x8000..=0xBFFF => { 60 // Mappable ROM bank 2 or RAM bank 61 if self.ram_enabled { 62 read_16kb_banked(ram, address, self.ram_bank) 63 } else { 64 read_16kb_banked(rom, address, self.rom_banks[2]) 65 } 66 } 67 0xC000..=0xFFFF => invalid_cartridge_address!(address), 68 } 69 } 70 71 pub fn write(&mut self, address: u16, value: u8, ram: &mut [u8], ram_dirty: &mut bool) { 72 match address { 73 0x8000..=0xBFFF if self.ram_enabled => { 74 // RAM bank (if mapped) 75 write_16kb_banked(ram, address, self.ram_bank, value); 76 *ram_dirty = true; 77 } 78 0xFFFC => { 79 // Control / RAM banking 80 self.ram_bank = value.bit(2).into(); 81 self.ram_enabled = value.bit(3); 82 // TODO Bank shifting and RAM enabled for $C000-$FFFF; not used by any known software 83 log::debug!( 84 "$FFFC write: {value:02X} (RAM bank = {}, RAM enabled = {})", 85 self.ram_bank, 86 self.ram_enabled 87 ); 88 } 89 0xFFFD..=0xFFFF => { 90 // ROM banking 91 let bank_idx = address - 0xFFFD; 92 self.rom_banks[bank_idx as usize] = value.into(); 93 log::debug!("${address:02X} write: {value:02X} (ROM bank {bank_idx})"); 94 } 95 _ => {} 96 } 97 } 98} 99 100#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] 101pub struct CodemastersMapper { 102 rom_banks: [u32; 3], 103 ram_enabled: bool, 104} 105 106impl CodemastersMapper { 107 pub fn new() -> Self { 108 Self { rom_banks: [0, 1, 2], ram_enabled: false } 109 } 110 111 pub fn read(&self, address: u16, rom: &[u8], ram: &[u8]) -> u8 { 112 match address { 113 0x0000..=0x9FFF => { 114 // ROM banks (16KB) 115 let bank = self.rom_banks[(address / 0x4000) as usize]; 116 read_16kb_banked(rom, address, bank) 117 } 118 0xA000..=0xBFFF => { 119 // RAM or upper half of ROM bank 2 120 if self.ram_enabled { 121 // No Codemasters game has more than 8KB of RAM 122 read_wrapped(ram, (address & 0x1FFF).into()) 123 } else { 124 read_16kb_banked(rom, address, self.rom_banks[2]) 125 } 126 } 127 0xC000..=0xFFFF => invalid_cartridge_address!(address), 128 } 129 } 130 131 pub fn write(&mut self, address: u16, value: u8, ram: &mut [u8], ram_dirty: &mut bool) { 132 match address { 133 0x0000..=0x3FFF => { 134 // ROM bank 0 135 self.rom_banks[0] = value.into(); 136 log::trace!("${address:04X} write: {value:02X} (ROM bank 0)"); 137 } 138 0x4000..=0x7FFF => { 139 // ROM bank 1 and RAM enabled 140 self.rom_banks[1] = value.into(); 141 self.ram_enabled = value.bit(7); 142 log::trace!( 143 "${address:04X} write: {value:02X} (ROM bank 1, RAM enabled = {})", 144 self.ram_enabled 145 ); 146 } 147 0x8000..=0xBFFF => { 148 // ROM bank 2 and RAM (in upper half if enabled) 149 if self.ram_enabled && address >= 0xA000 { 150 write_wrapped(ram, (address & 0x1FFF).into(), value); 151 *ram_dirty = true; 152 } else { 153 self.rom_banks[2] = value.into(); 154 log::trace!("${address:04X} write: {value:02X} (ROM bank 2)"); 155 } 156 } 157 0xC000..=0xFFFF => {} 158 } 159 } 160} 161 162#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] 163pub struct Sg1000Mapper { 164 ram_start_addr: usize, 165} 166 167impl Sg1000Mapper { 168 pub fn new(unmirrored_rom: &[u8]) -> Self { 169 // Some documentation says RAM starts at $A000, but some games have more than 8KB of RAM 170 // and expect it to start at $8000 (e.g. The Castle) 171 Self { ram_start_addr: cmp::max(0x8000, unmirrored_rom.len()) } 172 } 173} 174 175#[allow(clippy::unused_self)] 176impl Sg1000Mapper { 177 pub fn read(&self, address: u16, rom: &[u8], ram: &[u8]) -> u8 { 178 match address { 179 0x0000..=0x7FFF => rom[(address as usize) & (rom.len() - 1)], 180 0x8000..=0xFFFF => { 181 let address = address as usize; 182 if address < self.ram_start_addr { 183 rom[address & (rom.len() - 1)] 184 } else { 185 ram[(address - self.ram_start_addr) & (ram.len() - 1)] 186 } 187 } 188 } 189 } 190 191 pub fn write(&mut self, address: u16, value: u8, ram: &mut [u8], ram_dirty: &mut bool) { 192 let address = address as usize; 193 if address >= self.ram_start_addr { 194 ram[(address - self.ram_start_addr) & (ram.len() - 1)] = value; 195 *ram_dirty = true; 196 } 197 } 198} 199 200#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MatchEachVariantMacro)] 201pub enum Mapper { 202 Sega(SegaMapper), 203 Codemasters(CodemastersMapper), 204 Sg1000(Sg1000Mapper), 205} 206 207impl Mapper { 208 // Codemasters ROMs have a 16-bit checksum at $7FE6 which is the sum of all 16-bit words in the ROM 209 // except for the words in the Sega header. If summing all of the words matches the word at 210 // $7FE6, assume this is a Codemasters ROM 211 pub fn detect_from_rom(rom: &[u8]) -> Self { 212 if rom.len() < 32 * 1024 { 213 // No real ROMs should be less than 32KB, but regardless this isn't a Codemasters ROM 214 return Self::Sega(SegaMapper::new()); 215 } 216 217 let expected_checksum = u16::from_le_bytes([ 218 rom[CODEMASTERS_CHECKSUM_ADDR], 219 rom[CODEMASTERS_CHECKSUM_ADDR + 1], 220 ]); 221 let mut checksum = 0_u16; 222 for address in (0..rom.len() & !1).step_by(2) { 223 if !SEGA_HEADER_ADDR_RANGE.contains(&address) { 224 let word = u16::from_le_bytes([rom[address], rom[address + 1]]); 225 checksum = checksum.wrapping_add(word); 226 } 227 } 228 229 if checksum == expected_checksum { 230 log::info!( 231 "Codemasters-style ROM checksum is {checksum:04X}, matches word at {CODEMASTERS_CHECKSUM_ADDR:04X}; assuming this is a Codemasters ROM" 232 ); 233 Self::Codemasters(CodemastersMapper::new()) 234 } else { 235 Self::Sega(SegaMapper::new()) 236 } 237 } 238 239 pub fn read(&self, address: u16, rom: &[u8], ram: &[u8]) -> u8 { 240 match_each_variant!(self, mapper => mapper.read(address, rom, ram)) 241 } 242 243 pub fn write(&mut self, address: u16, value: u8, ram: &mut [u8], ram_dirty: &mut bool) { 244 match_each_variant!(self, mapper => mapper.write(address, value, ram, ram_dirty)); 245 } 246 247 pub fn name(&self) -> &'static str { 248 match self { 249 Self::Sega(_) => "Sega", 250 Self::Codemasters(_) => "Codemasters", 251 Self::Sg1000(_) => "SG-1000", 252 } 253 } 254}