cx4.rsannotatedcx4.rssource349 lines · 12.0 KB · raw

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}