Cx4 coprocessor, a programmable Hitachi HG51B169 CPU clocked at 20 MHz
Used by Mega Man X2 and Mega Man X3
5mod functions;
7use crate::common; 8use crate::common::{Rom, impl_take_set_rom}; 9use bincode::{Decode, Encode}; 10use jgenesis_common::num::{U16Ext, U24Ext}; 11use jgenesis_proc_macros::PartialClone; 12use std::cmp; 13 14const RAM_LEN: usize = 3 * 1024; 15 16const LOROM_NMI_VECTOR: usize = 0x7FEA; 17const LOROM_IRQ_VECTOR: usize = 0x7FEE; 18 19type Cx4Ram = [u8; RAM_LEN]; 20 21#[derive(Debug, Clone, Encode, Decode)] 22struct Cx4Registers { 23 // Cx4's general-purpose registers (16x 24-bit) 24 gpr: [u32; 16], 25 dma_source_address: u32, 26 dma_destination_address: u16, 27 dma_length: u16, 28 program_rom_base: u32, 29 instruction_page: u16, 30 instruction_pointer: u8, 31 nmi_vector: u16, 32 irq_vector: u16, 33 // R/W registers with unknown functionality (MMX2/MMX3 always set them to fixed values) 34 unknown_register_7f50: u8, 35 unknown_register_7f51: u8, 36 unknown_register_7f52: u8, 37} 38 39impl Cx4Registers { 40 fn new(rom: &[u8]) -> Self { 41 // Pre-populate NMI and IRQ vectors with the values from ROM 42 let nmi_vector = u16::from_le_bytes([rom[LOROM_NMI_VECTOR], rom[LOROM_NMI_VECTOR + 1]]); 43 let irq_vector = u16::from_le_bytes([rom[LOROM_IRQ_VECTOR], rom[LOROM_IRQ_VECTOR + 1]]); 44 Self { 45 gpr: [0; 16], 46 dma_source_address: 0, 47 dma_destination_address: 0, 48 dma_length: 0, 49 program_rom_base: 0, 50 instruction_page: 0, 51 instruction_pointer: 0, 52 nmi_vector, 53 irq_vector, 54 unknown_register_7f50: 0, 55 unknown_register_7f51: 0, 56 unknown_register_7f52: 0, 57 } 58 } 59 60 fn read(&self, offset: u16) -> Option<u8> { 61 let value = match offset { 62 // Program ROM base, low byte 63 0x7F49 => self.program_rom_base.low_byte(), 64 // Program ROM base, middle byte 65 0x7F4A => self.program_rom_base.mid_byte(), 66 // Program ROM base, high byte 67 0x7F4B => self.program_rom_base.high_byte(), 68 // R/W registers with unknown functionality 69 0x7F50 => self.unknown_register_7f50, 70 0x7F51 => self.unknown_register_7f51, 71 0x7F52 => self.unknown_register_7f52, 72 // TODO should return busy bit in bit 6 after writing to $7F47/$7F48/$7F4F 73 0x7F5E => 0x00, 74 // NMI vector 75 0x7F6A => self.nmi_vector.lsb(), 76 0x7F6B => self.nmi_vector.msb(), 77 // IRQ vector 78 0x7F6E => self.irq_vector.lsb(), 79 0x7F6F => self.irq_vector.msb(), 80 // CX4 24-bit registers 81 0x7F80..=0x7FAF => self.read_24_bit_register(offset), 82 _ => { 83 log::info!("CX4 register read: {offset:04X}"); 84 return None; 85 } 86 }; 87 88 Some(value) 89 } 90 91 fn write(&mut self, offset: u16, value: u8, rom: &[u8], ram: &mut Cx4Ram) { 92 match offset { 93 0x7F40 => { 94 // DMA source address, low byte 95 self.dma_source_address = 96 (self.dma_source_address & 0xFFFF_FF00) | u32::from(value); 97 } 98 0x7F41 => { 99 // DMA source address, middle byte 100 self.dma_source_address = 101 (self.dma_source_address & 0xFFFF_00FF) | (u32::from(value) << 8); 102 } 103 0x7F42 => { 104 // DMA source address, high byte 105 self.dma_source_address = 106 (self.dma_source_address & 0x0000_FFFF) | (u32::from(value) << 16); 107 } 108 0x7F43 => { 109 // DMA length, low byte 110 self.dma_length.set_lsb(value); 111 } 112 0x7F44 => { 113 // DMA length, high byte 114 self.dma_length.set_msb(value); 115 } 116 0x7F45 => { 117 // DMA destination, low byte 118 self.dma_destination_address.set_lsb(value); 119 } 120 0x7F46 => { 121 // DMA destination, high byte 122 self.dma_destination_address.set_msb(value); 123 } 124 0x7F47 => { 125 // Start ROM-to-CX4 DMA 126 if value == 0 { 127 self.run_dma(rom, ram); 128 } 129 } 130 0x7F48 | 0x7F4C => { 131 // Write-only register with unknown functionality 132 // TODO set busy bit for $7F48 133 } 134 0x7F49 => { 135 // Program ROM base, low byte 136 self.program_rom_base = (self.program_rom_base & 0xFFFF_FF00) | u32::from(value); 137 } 138 0x7F4A => { 139 // Program ROM base, middle byte 140 self.program_rom_base = 141 (self.program_rom_base & 0xFFFF_00FF) | (u32::from(value) << 8); 142 } 143 0x7F4B => { 144 // Program ROM base, high byte 145 self.program_rom_base = 146 (self.program_rom_base & 0x0000_FFFF) | (u32::from(value) << 16); 147 } 148 0x7F4D => { 149 // Program ROM instruction page, low byte 150 self.instruction_page.set_lsb(value); 151 } 152 0x7F4E => { 153 // Program ROM instruction page, high byte 154 self.instruction_page.set_msb(value); 155 } 156 0x7F4F => { 157 // Program ROM instruction pointer + execute instruction 158 // TODO set busy bit? 159 self.instruction_pointer = value; 160 161 // The DSP obviously takes some time to complete the requested function, but 162 // MMX2 and MMX3 seem to work fine if all function calls complete instantly from the 163 // game's perspective 164 functions::execute(self, rom, ram); 165 } 166 0x7F50 => { 167 // R/W register with unknown functionality 168 self.unknown_register_7f50 = value; 169 } 170 0x7F51 => { 171 // R/W register with unknown functionality 172 self.unknown_register_7f51 = value; 173 } 174 0x7F52 => { 175 // R/W register with unknown functionality 176 self.unknown_register_7f52 = value; 177 } 178 0x7F6A => { 179 // NMI vector, low byte 180 self.nmi_vector.set_lsb(value); 181 } 182 0x7F6B => { 183 // NMI vector, high byte 184 self.nmi_vector.set_msb(value); 185 } 186 0x7F6E => { 187 // IRQ vector, low byte 188 self.irq_vector.set_lsb(value); 189 } 190 0x7F6F => { 191 // IRQ vector, high byte 192 self.irq_vector.set_msb(value); 193 } 194 0x7F80..=0x7FAF => { 195 // CX4 24-bit registers 196 self.write_24_bit_register(offset, value); 197 } 198 _ => { 199 log::info!("CX4 register write: {offset:04X} {value:02X}"); 200 } 201 } 202 } 203 204 fn read_24_bit_register(&self, offset: u16) -> u8 { 205 let idx = (offset & 0x3F) / 3; 206 let shift = (offset % 3) * 8; 207 (self.gpr[idx as usize] >> shift) as u8 208 } 209 210 fn write_24_bit_register(&mut self, offset: u16, value: u8) { 211 let idx = (offset & 0x3F) / 3; 212 let (mask, shift) = match offset % 3 { 213 0 => (0xFFFF_FF00, 0), 214 1 => (0xFFFF_00FF, 8), 215 2 => (0x0000_FFFF, 16), 216 _ => panic!("value % 3 is always 0/1/2"), 217 }; 218 219 let existing_value = self.gpr[idx as usize]; 220 self.gpr[idx as usize] = (existing_value & mask) | (u32::from(value) << shift); 221 } 222 223 fn run_dma(&self, rom: &[u8], ram: &mut Cx4Ram) { 224 if !(0x6000..0x6C00).contains(&self.dma_destination_address) { 225 return; 226 } 227 228 log::trace!( 229 "Running DMA with src={:06X}, dst={:04X}, len={:04X}", 230 self.dma_source_address, 231 self.dma_destination_address, 232 self.dma_length 233 ); 234 235 let mut src_addr = self.dma_source_address; 236 let dest_addr: usize = (self.dma_destination_address - 0x6000).into(); 237 let dma_len: usize = cmp::min(0x0C00 - dest_addr, self.dma_length.into()); 238 239 for ram_value in &mut ram[dest_addr..dest_addr + dma_len] { 240 let rom_addr = common::lorom_map_rom_address(src_addr, rom.len() as u32); 241 *ram_value = rom[rom_addr as usize]; 242 243 src_addr = (src_addr + 1) & 0xFFFFFF; 244 } 245 } 246 247 fn risc_pc(&self) -> u32 { 248 self.program_rom_base 249 + (u32::from(self.instruction_page) << 9) 250 + (u32::from(self.instruction_pointer) << 1) 251 } 252 253 fn increment_instruction_pointer(&mut self) { 254 self.instruction_pointer = self.instruction_pointer.wrapping_add(1); 255 } 256} 257 258#[derive(Debug, Clone, Encode, Decode, PartialClone)] 259pub struct Cx4 { 260 #[partial_clone(default)] 261 rom: Rom, 262 ram: Box<Cx4Ram>, 263 registers: Cx4Registers, 264} 265 266impl Cx4 { 267 #[allow(clippy::missing_panics_doc)] 268 #[must_use] 269 pub fn new(rom: Box<[u8]>) -> Self { 270 let registers = Cx4Registers::new(&rom); 271 Self { 272 rom: Rom(rom), 273 ram: vec![0; RAM_LEN].into_boxed_slice().try_into().unwrap(), 274 registers, 275 } 276 } 277 278 #[inline] 279 #[must_use] 280 pub fn read(&self, address: u32) -> Option<u8> { 281 let bank = (address >> 16) as u8; 282 let offset = address as u16; 283 match (bank, offset) { 284 // NMI vector 285 (0x00, 0xFFEA) => Some(self.registers.nmi_vector.lsb()), 286 (0x00, 0xFFEB) => Some(self.registers.nmi_vector.msb()), 287 // IRQ vector 288 (0x00, 0xFFEE) => Some(self.registers.irq_vector.lsb()), 289 (0x00, 0xFFEF) => Some(self.registers.irq_vector.msb()), 290 // CX4 RAM (3KB) 291 (0x00..=0x3F | 0x80..=0xBF, 0x6000..=0x6BFF) => { 292 Some(self.ram[(address & 0xFFF) as usize]) 293 } 294 // CX4 registers 295 (0x00..=0x3F | 0x80..=0xBF, 0x7F40..=0x7FAF) => self.registers.read(offset), 296 // SRAM range, which is unmapped in all CX4 games and always reads $00 297 (0x70..=0x77, _) => Some(0x00), 298 // Treat other addresses as LoROM 299 _ => map_rom_address(address, self.rom.len() as u32) 300 .map(|rom_addr| self.rom[rom_addr as usize]), 301 } 302 } 303 304 #[inline] 305 pub fn write(&mut self, address: u32, value: u8) { 306 let bank = (address >> 16) as u8; 307 let offset = address as u16; 308 match (bank, offset) { 309 // CX4 RAM (3KB) 310 (0x00..=0x3F | 0x80..=0xBF, 0x6000..=0x6BFF) => { 311 self.ram[(address & 0xFFF) as usize] = value; 312 } 313 // CX4 registers 314 (0x00..=0x3F | 0x80..=0xBF, 0x7F40..=0x7FAF) => { 315 self.registers.write(offset, value, &self.rom, &mut self.ram); 316 } 317 _ => { 318 log::info!("CX4 write: {address:06X} {value:02X}"); 319 } 320 } 321 } 322 323 impl_take_set_rom!(rom); 324} 325 326fn map_rom_address(address: u32, rom_len: u32) -> Option<u32> { 327 let bank = (address >> 16) & 0xFF; 328 let offset = address & 0xFFFF; 329 match (bank, offset) { 330 (0x00..=0x3F | 0x80..=0xBF, 0x8000..=0xFFFF) | (0x40..=0x7D | 0xC0..=0xFF, _) => { 331 // The only games that use this coprocessor are Mega Man X2 and Mega Man X3, 332 // which respectively have ROM sizes of 1.5MB and 2MB 333 // Both use LoROM address mapping 334 let rom_addr = ((address & 0x7F0000) >> 1) | (address & 0x007FFF); 335 Some(if rom_len == 1 << 21 { 336 // 2MB ROM 337 rom_addr & ((1 << 21) - 1) 338 } else { 339 // 1.5MB ROM; mirror the last 0.5MB to the 1.5-2.0MB range 340 if rom_addr & (1 << 20) == 0 { 341 rom_addr & ((1 << 20) - 1) 342 } else { 343 (1 << 20) | (rom_addr & ((1 << 19) - 1)) 344 } 345 }) 346 } 347 _ => None, 348 } 349}