SNES cartridge loading and mapping code

3use crate::api::{CoprocessorRoms, SnesLoadError, SnesLoadResult};
4use bincode::{Decode, Encode};
5use crc::Crc;
6use jgenesis_common::frontend::{PartialClone, SaveWriter, TimingMode};
7use jgenesis_proc_macros::{FakeDecode, FakeEncode};
8use snes_coprocessors::cx4::Cx4;
9use snes_coprocessors::obc1::Obc1;
10use snes_coprocessors::sa1::Sa1;
11use snes_coprocessors::sdd1::Sdd1;
12use snes_coprocessors::spc7110::Spc7110;
13use snes_coprocessors::srtc::SRtc;
14use snes_coprocessors::st018::St018;
15use snes_coprocessors::superfx::SuperFx;
16use snes_coprocessors::upd77c25::{Upd77c25, Upd77c25Variant};
17use snes_coprocessors::{superfx, upd77c25};
18use std::cmp::Ordering;
19use std::fmt::{Display, Formatter};
20use std::mem;
21use std::num::NonZeroU64;
22use std::ops::{Deref, Range};
24const CRC: Crc<u32> = Crc::<u32>::new(&crc::CRC_32_ISO_HDLC);
25
26#[derive(Debug, Clone, FakeEncode, FakeDecode)]
27pub struct Rom(pub Box<[u8]>);
28
29impl Default for Rom {
30    fn default() -> Self {
31        Rom(vec![].into_boxed_slice())
32    }
33}
34
35impl Deref for Rom {
36    type Target = Box<[u8]>;
37
38    fn deref(&self) -> &Self::Target {
39        &self.0
40    }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44enum CartridgeType {
45    LoRom,
46    HiRom,
47    ExHiRom,
48    Cx4,
49    Obc1,
50    Sa1,
51    Sdd1,
52    Spc7110,
53    SuperFx,
54}
55
56impl Display for CartridgeType {
57    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
58        match self {
59            Self::LoRom => write!(f, "LoROM"),
60            Self::HiRom => write!(f, "HiROM"),
61            Self::ExHiRom => write!(f, "ExHiROM"),
62            Self::Cx4 => write!(f, "CX4"),
63            Self::Obc1 => write!(f, "OBC1"),
64            Self::Sa1 => write!(f, "SA-1"),
65            Self::Sdd1 => write!(f, "S-DD1"),
66            Self::Spc7110 => write!(f, "SPC7110"),
67            Self::SuperFx => write!(f, "Super FX"),
68        }
69    }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73enum DspVariant {
74    Dsp1,
75    Dsp2,
76    Dsp3,
77    Dsp4,
78}
79
80impl Display for DspVariant {
81    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
82        match self {
83            Self::Dsp1 => write!(f, "DSP-1"),
84            Self::Dsp2 => write!(f, "DSP-2"),
85            Self::Dsp3 => write!(f, "DSP-3"),
86            Self::Dsp4 => write!(f, "DSP-4"),
87        }
88    }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
92pub struct DspLoromPortAddresses {
93    banks: Range<u32>,
94    offset_mask: u32,
95}
96
97impl DspLoromPortAddresses {
98    // PCB SHVC-1B0N-01
99    // DSP-1 or DSP-4 with no SRAM and at most 1MB of ROM
100    const DSP_1_4: Self = Self { banks: 0x30..0x40, offset_mask: 0x8000 };
101
102    // PCB SHVC-2B3B-01
103    // DSP-1 with 8KB SRAM and up to 2MB of ROM
104    const DSP_1_LARGE: Self = Self { banks: 0x60..0x70, offset_mask: 0x0000 };
105
106    // PCB SHVC-1B5B-01 (DSP-2 w/ 32KB SRAM) or SHVC-1B3B-01 (DSP-3 w/ 8KB SRAM)
107    const DSP_2_3: Self = Self { banks: 0x20..0x40, offset_mask: 0x8000 };
108
109    fn from_metadata(rom_len: usize, sram_len: usize) -> Self {
110        if rom_len > 1024 * 1024 {
111            // More than 1MB of ROM; assume DSP-1 large variant
112            Self::DSP_1_LARGE
113        } else if sram_len != 0 {
114            // At most 1MB of ROM but has SRAM; assume DSP-2 or DSP-3
115            Self::DSP_2_3
116        } else {
117            // At most 1MB of ROM and no SRAM; assume DSP-1 normal variant or DSP-4
118            Self::DSP_1_4
119        }
120    }
121
122    fn is_dsp_port(&self, bank: u32, offset: u32) -> bool {
123        self.banks.contains(&(bank & 0x7F)) && offset & 0x8000 == self.offset_mask
124    }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128enum St01xVariant {
129    St010,
130    St011,
131}
132
133impl From<St01xVariant> for Upd77c25Variant {
134    fn from(value: St01xVariant) -> Self {
135        match value {
136            St01xVariant::St010 => Self::St010,
137            St01xVariant::St011 => Self::St011,
138        }
139    }
140}
141
142impl Display for St01xVariant {
143    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
144        match self {
145            Self::St010 => write!(f, "ST010"),
146            Self::St011 => write!(f, "ST011"),
147        }
148    }
149}
150
151const LOROM_HEADER_ADDR: usize = 0x007FC0;
152const HIROM_HEADER_ADDR: usize = 0x00FFC0;
153const EXHIROM_HEADER_ADDR: usize = 0x40FFC0;
154
155const HEADER_MAP_OFFSET: usize = 0x15;
156
157const LOROM_RESET_VECTOR: usize = 0x7FFC;
158const HIROM_RESET_VECTOR: usize = 0xFFFC;
159
160#[derive(Debug, Clone, Encode, Decode, PartialClone)]
161pub enum Cartridge {
162    LoRom {
163        #[partial_clone(default)]
164        rom: Rom,
165        sram: Box<[u8]>,
166    },
167    HiRom {
168        #[partial_clone(default)]
169        rom: Rom,
170        sram: Box<[u8]>,
171    },
172    ExHiRom {
173        #[partial_clone(default)]
174        rom: Rom,
175        sram: Box<[u8]>,
176        srtc: Option<SRtc>,
177    },
178    Cx4(#[partial_clone(partial)] Cx4),
179    DspLoRom {
180        #[partial_clone(default)]
181        rom: Rom,
182        sram: Box<[u8]>,
183        upd77c25: Upd77c25,
184        port_addresses: DspLoromPortAddresses,
185    },
186    DspHiRom {
187        #[partial_clone(default)]
188        rom: Rom,
189        sram: Box<[u8]>,
190        upd77c25: Upd77c25,
191    },
192    Obc1(#[partial_clone(partial)] Obc1),
193    Sa1(#[partial_clone(partial)] Sa1),
194    Sdd1(#[partial_clone(partial)] Sdd1),
195    Spc7110(#[partial_clone(partial)] Spc7110),
196    SuperFx(#[partial_clone(partial)] SuperFx),
197    St01x {
198        #[partial_clone(default)]
199        rom: Rom,
200        upd77c25: Upd77c25,
201    },
202    St018 {
203        #[partial_clone(default)]
204        rom: Rom,
205        sram: Box<[u8]>,
206        st018: St018,
207    },
208}
209
210impl Cartridge {
211    pub fn create<S: SaveWriter>(
212        mut rom: Vec<u8>,
213        initial_sram: Option<Vec<u8>>,
214        coprocessor_roms: &CoprocessorRoms,
215        forced_timing_mode: Option<TimingMode>,
216        gsu_overclock_factor: NonZeroU64,
217        save_writer: &mut S,
218    ) -> SnesLoadResult<(Self, TimingMode)> {
219        // Older SNES ROM images have an extra 512-byte header; check for that and strip it off
220        if rom.len() & 0x7FFF == 0x0200 {
221            rom = rom[0x200..].to_vec();
222        }
223
224        if rom.len() < 32 * 1024 {
225            return Err(SnesLoadError::RomTooSmall { size: rom.len() });
226        }
227
228        let cartridge_type = guess_cartridge_type(&rom).unwrap_or_else(|| {
229            log::error!("Unable to confidently determine ROM type; defaulting to LoROM");
230            CartridgeType::LoRom
231        });
232
233        if cartridge_type != CartridgeType::Spc7110 {
234            jgenesis_common::rom::mirror_to_next_power_of_two(&mut rom);
235            log::debug!("ROM length after mirroring is {} bytes", rom.len());
236
237            debug_assert_eq!(rom.len().count_ones(), 1);
238        }
239
240        let rom = rom.into_boxed_slice();
241
242        let rom_header_addr = match cartridge_type {
243            CartridgeType::LoRom
244            | CartridgeType::Cx4
245            | CartridgeType::Obc1
246            | CartridgeType::Sa1
247            | CartridgeType::Sdd1
248            | CartridgeType::SuperFx => LOROM_HEADER_ADDR,
249            CartridgeType::HiRom | CartridgeType::Spc7110 => HIROM_HEADER_ADDR,
250            CartridgeType::ExHiRom => EXHIROM_HEADER_ADDR,
251        };
252
253        // Determine NTSC/PAL
254        let region_byte = rom[rom_header_addr + 0x19];
255        let timing_mode =
256            forced_timing_mode.unwrap_or_else(|| region_to_timing_mode(region_byte, &rom));
257
258        // $FFD8 contains SRAM size as a kilobytes power of 2
259        let sram_header_byte = rom[rom_header_addr | 0x0018];
260
261        // Check if cartridge is ST010/ST011; these don't report RAM size in the header (always 4KB)
262        let chipset_byte = rom[rom_header_addr + 0x16];
263        let is_st01x = chipset_byte == 0xF6
264            && rom[rom_header_addr + 0x14] == 0x00
265            && rom[rom_header_addr - 1] == 0x01;
266
267        let sram_len = if is_st01x {
268            upd77c25::ST01X_RAM_LEN_BYTES
269        } else if cartridge_type == CartridgeType::SuperFx {
270            superfx::guess_ram_len(&rom)
271        } else if sram_header_byte == 0 {
272            0
273        } else if sram_header_byte > 21 {
274            log::warn!(
275                "Invalid SRAM header byte ${sram_header_byte:02X}, giving 256KB of SRAM; game may not work properly"
276            );
277            256 * 1024
278        } else {
279            1 << (10 + sram_header_byte)
280        };
281
282        let sram = match initial_sram {
283            Some(sram) if sram.len() == sram_len => sram.into_boxed_slice(),
284            _ => vec![0xFF; sram_len].into_boxed_slice(),
285        };
286
287        log::info!("Using mapper {cartridge_type} with SRAM size {sram_len}");
288
289        if is_st01x {
290            let st01x_variant = guess_st01x_variant(&rom);
291
292            log::info!("Detected {st01x_variant} coprocessor");
293
294            let st01x_rom_fn = match st01x_variant {
295                St01xVariant::St010 => {
296                    coprocessor_roms.st010.as_ref().ok_or(SnesLoadError::MissingSt010Rom)?
297                }
298                St01xVariant::St011 => {
299                    coprocessor_roms.st011.as_ref().ok_or(SnesLoadError::MissingSt011Rom)?
300                }
301            };
302
303            let st01x_rom = st01x_rom_fn()
304                .map_err(|(source, path)| SnesLoadError::CoprocessorRomLoad { source, path })?;
305            let upd77c25 = Upd77c25::new(&st01x_rom, st01x_variant.into(), &sram, timing_mode);
306
307            return Ok((Self::St01x { rom: Rom(rom), upd77c25 }, timing_mode));
308        }
309
310        let is_st018 = chipset_byte == 0xF5 && rom[rom_header_addr - 1] == 0x02;
311        if is_st018 {
312            log::info!("Detected ST018 coprocessor");
313
314            let st018_rom_fn =
315                coprocessor_roms.st018.as_ref().ok_or(SnesLoadError::MissingSt018Rom)?;
316            let st018_rom = st018_rom_fn()
317                .map_err(|(source, path)| SnesLoadError::CoprocessorRomLoad { source, path })?;
318            let st018 = St018::new(&st018_rom).map_err(SnesLoadError::St018RomLoad)?;
319
320            return Ok((Self::St018 { rom: Rom(rom), sram, st018 }, timing_mode));
321        }
322
323        let rom_checksum = CRC.checksum(&rom);
324        log::info!("ROM CRC32: {rom_checksum:08X}");
325
326        // Check for DSP-1/2/3/4 coprocessor (identified by chipset $03-$05, can be LoROM or HiROM)
327        let force_dsp1 = should_force_dsp1(rom_checksum);
328        if force_dsp1
329            || ((0x03..0x06).contains(&chipset_byte)
330                && matches!(cartridge_type, CartridgeType::LoRom | CartridgeType::HiRom))
331        {
332            let dsp_variant = guess_dsp_variant(rom_checksum);
333
334            log::info!("Detected DSP coprocessor of type {dsp_variant}");
335
336            let dsp_rom_fn = match dsp_variant {
337                DspVariant::Dsp1 => {
338                    coprocessor_roms.dsp1.as_ref().ok_or(SnesLoadError::MissingDsp1Rom)?
339                }
340                DspVariant::Dsp2 => {
341                    coprocessor_roms.dsp2.as_ref().ok_or(SnesLoadError::MissingDsp2Rom)?
342                }
343                DspVariant::Dsp3 => {
344                    coprocessor_roms.dsp3.as_ref().ok_or(SnesLoadError::MissingDsp3Rom)?
345                }
346                DspVariant::Dsp4 => {
347                    coprocessor_roms.dsp4.as_ref().ok_or(SnesLoadError::MissingDsp4Rom)?
348                }
349            };
350
351            let dsp_rom = dsp_rom_fn()
352                .map_err(|(source, path)| SnesLoadError::CoprocessorRomLoad { source, path })?;
353            let upd77c25 = Upd77c25::new(&dsp_rom, Upd77c25Variant::Dsp, &sram, timing_mode);
354
355            return match cartridge_type {
356                CartridgeType::LoRom => {
357                    let port_addresses = DspLoromPortAddresses::from_metadata(rom.len(), sram_len);
358
359                    log::debug!(
360                        "DSP-n port banks: {:02X?}, offset mask: {:04X}",
361                        port_addresses.banks,
362                        port_addresses.offset_mask
363                    );
364
365                    Ok((
366                        Self::DspLoRom { rom: Rom(rom), sram, upd77c25, port_addresses },
367                        timing_mode,
368                    ))
369                }
370                CartridgeType::HiRom => {
371                    Ok((Self::DspHiRom { rom: Rom(rom), sram, upd77c25 }, timing_mode))
372                }
373                _ => unreachable!("nested match expressions"),
374            };
375        }
376
377        let cartridge = match cartridge_type {
378            CartridgeType::LoRom => Self::LoRom { rom: Rom(rom), sram },
379            CartridgeType::HiRom => Self::HiRom { rom: Rom(rom), sram },
380            CartridgeType::ExHiRom => new_exhirom_cartridge(rom, sram, save_writer),
381            CartridgeType::Cx4 => Self::Cx4(Cx4::new(rom)),
382            CartridgeType::Obc1 => Self::Obc1(Obc1::new(rom, sram)),
383            CartridgeType::Sa1 => Self::Sa1(Sa1::new(rom, sram, timing_mode)),
384            CartridgeType::Sdd1 => Self::Sdd1(Sdd1::new(rom, sram)),
385            CartridgeType::Spc7110 => Self::Spc7110(Spc7110::new(rom, sram, save_writer)),
386            CartridgeType::SuperFx => Self::SuperFx(SuperFx::new(rom, sram, gsu_overclock_factor)),
387        };
388
389        Ok((cartridge, timing_mode))
390    }
391
392    pub fn read(&mut self, address: u32) -> Option<u8> {
393        let bank = (address >> 16) & 0xFF;
394        let offset = address & 0xFFFF;
395        let (mapped_address, rom, sram) = match self {
396            Self::LoRom { rom, sram } => {
397                (lorom_map_address(address, rom.len() as u32, sram.len() as u32), rom, sram)
398            }
399            Self::DspLoRom { rom, sram, upd77c25, port_addresses } => {
400                if port_addresses.is_dsp_port(bank, offset) {
401                    return Some(if offset & 0x4000 == 0 {
402                        upd77c25.read_data()
403                    } else {
404                        upd77c25.read_status()
405                    });
406                }
407
408                (lorom_map_address(address, rom.len() as u32, sram.len() as u32), rom, sram)
409            }
410            Self::HiRom { rom, sram } => {
411                (hirom_map_address(address, rom.len() as u32, sram.len() as u32), rom, sram)
412            }
413            Self::DspHiRom { rom, sram, upd77c25 } => match (bank, offset) {
414                (0x00..=0x0F | 0x80..=0x8F, 0x6000..=0x6FFF) => return Some(upd77c25.read_data()),
415                (0x00..=0x0F | 0x80..=0x8F, 0x7000..=0x7FFF) => {
416                    return Some(upd77c25.read_status());
417                }
418                _ => (hirom_map_address(address, rom.len() as u32, sram.len() as u32), rom, sram),
419            },
420            Self::ExHiRom { rom, sram, srtc, .. } => match (bank, offset, srtc) {
421                (0x00..=0x3F | 0x80..=0xBF, 0x2800, Some(srtc)) => return Some(srtc.read()),
422                _ => (exhirom_map_address(address, rom.len() as u32, sram.len() as u32), rom, sram),
423            },
424            Self::Cx4(cx4) => return cx4.read(address),
425            Self::Obc1(obc1) => return obc1.read(address),
426            Self::Sa1(sa1) => return sa1.snes_read(address),
427            Self::Sdd1(sdd1) => return sdd1.read(address),
428            Self::Spc7110(spc7110) => return spc7110.read(address),
429            Self::SuperFx(sfx) => return sfx.read(address),
430            Self::St01x { rom, upd77c25 } => {
431                return match (bank, offset) {
432                    (0x60..=0x67, 0x0000) => Some(upd77c25.read_data()),
433                    (0x60..=0x67, 0x0001) => Some(upd77c25.read_status()),
434                    (0x68..=0x6F, 0x0000..=0x0FFF) => {
435                        let sram_addr = ((bank & 0x7) << 12) | (offset & 0xFFF);
436                        Some(upd77c25.read_ram(sram_addr))
437                    }
438                    _ => match lorom_map_address(address, rom.len() as u32, 0) {
439                        CartridgeAddress::Rom(rom_addr) => Some(rom[rom_addr as usize]),
440                        _ => None,
441                    },
442                };
443            }
444            Self::St018 { rom, sram, st018 } => {
445                return match (bank, offset) {
446                    (0x00..=0x3F | 0x80..=0xBF, 0x3800..=0x3804) => st018.snes_read(address),
447                    (0x68..=0x6F, 0x0000..=0x7FFF) => {
448                        let sram_addr = (offset as usize) & (sram.len() - 1);
449                        Some(sram[sram_addr])
450                    }
451                    _ => match lorom_map_address(address, rom.len() as u32, 0) {
452                        CartridgeAddress::Rom(rom_addr) => Some(rom[rom_addr as usize]),
453                        _ => None,
454                    },
455                };
456            }
457        };
458
459        match mapped_address {
460            CartridgeAddress::None => None,
461            CartridgeAddress::Rom(rom_addr) => Some(rom[rom_addr as usize]),
462            CartridgeAddress::Sram(sram_addr) => Some(sram[sram_addr as usize]),
463        }
464    }
465
466    pub fn write(&mut self, address: u32, value: u8) {
467        let bank = (address >> 16) & 0xFF;
468        let offset = address & 0xFFFF;
469        match self {
470            Self::LoRom { rom, sram } => {
471                if let CartridgeAddress::Sram(sram_addr) =
472                    lorom_map_address(address, rom.len() as u32, sram.len() as u32)
473                {
474                    sram[sram_addr as usize] = value;
475                }
476            }
477            Self::DspLoRom { rom, sram, upd77c25, port_addresses } => {
478                if port_addresses.is_dsp_port(bank, offset) {
479                    if offset & 0x4000 == 0 {
480                        upd77c25.write_data(value);
481                    }
482                    // Status port is not writable
483                    return;
484                }
485
486                if let CartridgeAddress::Sram(sram_addr) =
487                    lorom_map_address(address, rom.len() as u32, sram.len() as u32)
488                {
489                    sram[sram_addr as usize] = value;
490                }
491            }
492            Self::HiRom { rom, sram, .. } => {
493                if let CartridgeAddress::Sram(sram_addr) =
494                    hirom_map_address(address, rom.len() as u32, sram.len() as u32)
495                {
496                    sram[sram_addr as usize] = value;
497                }
498            }
499            Self::DspHiRom { rom, sram, upd77c25 } => match (bank, offset) {
500                (0x00..=0x0F | 0x80..=0x8F, 0x6000..=0x6FFF) => {
501                    upd77c25.write_data(value);
502                }
503                _ => {
504                    if let CartridgeAddress::Sram(sram_addr) =
505                        hirom_map_address(address, rom.len() as u32, sram.len() as u32)
506                    {
507                        sram[sram_addr as usize] = value;
508                    }
509                }
510            },
511            Self::ExHiRom { rom, sram, srtc, .. } => match (bank, offset, srtc) {
512                (0x00..=0x3F | 0x80..=0xBF, 0x2801, Some(srtc)) => srtc.write(value),
513                _ => {
514                    if let CartridgeAddress::Sram(sram_addr) =
515                        exhirom_map_address(address, rom.len() as u32, sram.len() as u32)
516                    {
517                        sram[sram_addr as usize] = value;
518                    }
519                }
520            },
521            Self::Cx4(cx4) => {
522                cx4.write(address, value);
523            }
524            Self::Obc1(obc1) => {
525                obc1.write(address, value);
526            }
527            Self::Sa1(sa1) => {
528                sa1.snes_write(address, value);
529            }
530            Self::Sdd1(sdd1) => {
531                sdd1.write(address, value);
532            }
533            Self::Spc7110(spc7110) => {
534                spc7110.write(address, value);
535            }
536            Self::SuperFx(sfx) => {
537                sfx.write(address, value);
538            }
539            Self::St01x { upd77c25, .. } => match (bank, offset) {
540                (0x60..=0x67, 0x0000) => upd77c25.write_data(value),
541                (0x68..=0x6F, 0x0000..=0x0FFF) => {
542                    let sram_addr = ((bank & 0x7) << 12) | (offset & 0xFFF);
543                    upd77c25.write_ram(sram_addr, value);
544                }
545                _ => {}
546            },
547            Self::St018 { sram, st018, .. } => match (bank, offset) {
548                (0x00..=0x3F | 0x80..=0xBF, 0x3800..=0x3804) => st018.snes_write(address, value),
549                (0x68..=0x6F, 0x0000..=0x7FFF) => {
550                    let sram_addr = (address as usize) & (sram.len() - 1);
551                    sram[sram_addr] = value;
552                }
553                _ => {}
554            },
555        }
556    }
557
558    pub fn irq(&self) -> bool {
559        match self {
560            Self::Sa1(sa1) => sa1.snes_irq(),
561            Self::SuperFx(sfx) => sfx.irq(),
562            _ => false,
563        }
564    }
565
566    pub fn take_rom(&mut self) -> Vec<u8> {
567        match self {
568            Self::LoRom { rom, .. }
569            | Self::HiRom { rom, .. }
570            | Self::ExHiRom { rom, .. }
571            | Self::DspLoRom { rom, .. }
572            | Self::DspHiRom { rom, .. }
573            | Self::St01x { rom, .. }
574            | Self::St018 { rom, .. } => mem::take(&mut rom.0).into_vec(),
575            Self::Cx4(cx4) => cx4.take_rom(),
576            Self::Obc1(obc1) => obc1.take_rom(),
577            Self::Sa1(sa1) => sa1.take_rom(),
578            Self::Sdd1(sdd1) => sdd1.take_rom(),
579            Self::Spc7110(spc7110) => spc7110.take_rom(),
580            Self::SuperFx(sfx) => sfx.take_rom(),
581        }
582    }
583
584    pub fn take_rom_from(&mut self, other: &mut Self) {
585        let other_rom = other.take_rom();
586
587        match self {
588            Self::LoRom { rom, .. }
589            | Self::HiRom { rom, .. }
590            | Self::ExHiRom { rom, .. }
591            | Self::DspLoRom { rom, .. }
592            | Self::DspHiRom { rom, .. }
593            | Self::St01x { rom, .. }
594            | Self::St018 { rom, .. } => {
595                *rom = Rom(other_rom.into_boxed_slice());
596            }
597            Self::Cx4(cx4) => {
598                cx4.set_rom(other_rom);
599            }
600            Self::Obc1(obc1) => {
601                obc1.set_rom(other_rom);
602            }
603            Self::Sa1(sa1) => {
604                sa1.set_rom(other_rom);
605            }
606            Self::Sdd1(sdd1) => {
607                sdd1.set_rom(other_rom);
608            }
609            Self::Spc7110(spc7110) => {
610                spc7110.set_rom(other_rom);
611            }
612            Self::SuperFx(sfx) => {
613                sfx.set_rom(other_rom);
614            }
615        }
616    }
617
618    pub fn has_battery(&self) -> bool {
619        match self {
620            Self::Cx4(..) => false,
621            Self::ExHiRom { .. }
622            | Self::Obc1(..)
623            | Self::Spc7110(..)
624            | Self::St01x { .. }
625            | Self::St018 { .. } => true,
626            Self::LoRom { sram, .. }
627            | Self::HiRom { sram, .. }
628            | Self::DspLoRom { sram, .. }
629            | Self::DspHiRom { sram, .. } => !sram.is_empty(),
630            Self::Sa1(sa1) => sa1.has_battery(),
631            Self::Sdd1(sdd1) => sdd1.has_battery(),
632            Self::SuperFx(sfx) => sfx.has_battery(),
633        }
634    }
635
636    pub fn sram(&self) -> Option<&[u8]> {
637        match self {
638            Self::LoRom { sram, .. }
639            | Self::HiRom { sram, .. }
640            | Self::ExHiRom { sram, .. }
641            | Self::DspLoRom { sram, .. }
642            | Self::DspHiRom { sram, .. }
643                if !sram.is_empty() =>
644            {
645                Some(sram)
646            }
647            Self::LoRom { .. }
648            | Self::HiRom { .. }
649            | Self::ExHiRom { .. }
650            | Self::DspLoRom { .. }
651            | Self::DspHiRom { .. }
652            | Self::Cx4 { .. } => None,
653            Self::Obc1(obc1) => Some(obc1.sram()),
654            Self::Sa1(sa1) => sa1.sram(),
655            Self::Sdd1(sdd1) => sdd1.sram(),
656            Self::Spc7110(spc7110) => Some(spc7110.sram()),
657            Self::SuperFx(sfx) => Some(sfx.sram()),
658            Self::St01x { upd77c25, .. } => Some(upd77c25.sram()),
659            Self::St018 { sram, .. } => Some(sram),
660        }
661    }
662
663    pub fn write_auxiliary_save_files<S: SaveWriter>(
664        &self,
665        save_writer: &mut S,
666    ) -> Result<(), S::Err> {
667        match self {
668            Self::ExHiRom { srtc: Some(srtc), .. } => {
669                save_writer.persist_serialized("rtc", srtc)?;
670            }
671            Self::Spc7110(spc7110) => {
672                if let Some(rtc) = spc7110.rtc() {
673                    save_writer.persist_serialized("rtc", rtc)?;
674                }
675            }
676            _ => {}
677        }
678
679        Ok(())
680    }
681
682    pub fn tick(&mut self, master_cycles_elapsed: u64) {
683        match self {
684            Self::DspLoRom { upd77c25, .. }
685            | Self::DspHiRom { upd77c25, .. }
686            | Self::St01x { upd77c25, .. } => {
687                upd77c25.tick(master_cycles_elapsed);
688            }
689            Self::Sa1(sa1) => {
690                sa1.tick(master_cycles_elapsed);
691            }
692            Self::SuperFx(sfx) => {
693                sfx.tick(master_cycles_elapsed);
694            }
695            Self::St018 { st018, .. } => {
696                st018.tick(master_cycles_elapsed);
697            }
698            _ => {}
699        }
700    }
701
702    pub fn reset(&mut self) {
703        match self {
704            Self::ExHiRom { srtc: Some(srtc), .. } => {
705                srtc.reset_state();
706            }
707            Self::DspLoRom { upd77c25, .. }
708            | Self::DspHiRom { upd77c25, .. }
709            | Self::St01x { upd77c25, .. } => {
710                upd77c25.reset();
711            }
712            Self::Sa1(sa1) => {
713                sa1.reset();
714            }
715            Self::Sdd1(sdd1) => {
716                sdd1.notify_dma_end();
717            }
718            Self::SuperFx(sfx) => {
719                sfx.reset();
720            }
721            _ => {}
722        }
723    }
724
725    pub fn notify_dma_start(&mut self, channel: u8, source_address: u32) {
726        match self {
727            Self::Sa1(sa1) => sa1.notify_dma_start(source_address),
728            Self::Sdd1(sdd1) => sdd1.notify_dma_start(channel, source_address),
729            _ => {}
730        }
731    }
732
733    pub fn notify_dma_end(&mut self) {
734        match self {
735            Self::Sa1(sa1) => sa1.notify_dma_end(),
736            Self::Sdd1(sdd1) => sdd1.notify_dma_end(),
737            _ => {}
738        }
739    }
740
741    pub fn update_gsu_overclock_factor(&mut self, overclock_factor: NonZeroU64) {
742        if let Self::SuperFx(sfx) = self {
743            sfx.update_gsu_overclock_factor(overclock_factor);
744        }
745    }
746}
747
748fn new_exhirom_cartridge<S: SaveWriter>(
749    rom: Box<[u8]>,
750    initial_sram: Box<[u8]>,
751    save_writer: &mut S,
752) -> Cartridge {
753    // Chipset byte of $55 indicates S-RTC present (only used by Daikaijuu Monogatari II)
754    let has_srtc = rom[EXHIROM_HEADER_ADDR + 0x16] == 0x55;
755    let srtc = has_srtc.then(|| {
756        let mut srtc = save_writer.load_serialized("rtc").ok().unwrap_or_else(SRtc::new);
757        srtc.reset_state();
758        srtc
759    });
760
761    log::info!("ExHiROM cartridge has S-RTC: {}", srtc.is_some());
762
763    Cartridge::ExHiRom { rom: Rom(rom), sram: initial_sram, srtc }
764}
765
766fn region_to_timing_mode(region_byte: u8, rom: &[u8]) -> TimingMode {
767    // Games with invalid region bytes that require PAL timings
768    const FORCE_PAL_CHECKSUMS: &[u32] = &[
769        0x39E5E4A3, // Tintin in Tibet (Europe) (En,Es,Sv)
770        0x9F279963, // Total Football (Europe) (Proto)
771    ];
772
773    match region_byte {
774        // Japan / USA / South Korea / Canada / Brazil
775        0x00 | 0x01 | 0x0D | 0x0F | 0x10 => TimingMode::Ntsc,
776        // various European and Asian countries (other than Japan/Korea) + Australia
777        0x02..=0x0C | 0x11 => TimingMode::Pal,
778        _ => {
779            let checksum = CRC.checksum(rom);
780            if FORCE_PAL_CHECKSUMS.contains(&checksum) {
781                TimingMode::Pal
782            } else {
783                log::warn!(
784                    "Unrecognized region byte in ROM header, defaulting to NTSC: {region_byte:02X}"
785                );
786                TimingMode::Ntsc
787            }
788        }
789    }
790}
791
792fn guess_cartridge_type(rom: &[u8]) -> Option<CartridgeType> {
793    if rom.len() < 0x8000 {
794        log::error!("ROM is too small; all ROMs should be at least 32KB, was {} bytes", rom.len());
795        return None;
796    }
797
798    if rom.len() < 0x10000 {
799        // Any ROM less than 64KB must be LoROM; HiROM <64KB wouldn't have anywhere to store
800        // the 65816 interrupt vectors
801        if let Some(coprocessor) = check_for_lorom_coprocessor(rom) {
802            return Some(coprocessor);
803        }
804
805        return Some(CartridgeType::LoRom);
806    }
807
808    if rom.len() >= 0x410000 {
809        // $25 = ExHiROM, $35 = ExHiROM + FastROM
810        // A ROM >4MB with $25/$35 in the header is almost certainly ExHiROM; only 2 (?) non-ExHiROM
811        // games are larger than 4MB
812        let exhirom_map_byte = rom[EXHIROM_HEADER_ADDR + HEADER_MAP_OFFSET];
813        if exhirom_map_byte == 0x25 || exhirom_map_byte == 0x35 {
814            return Some(CartridgeType::ExHiRom);
815        }
816    }
817
818    let mut lorom_points = 0;
819    let mut hirom_points = 0;
820
821    let lorom_map_byte = rom[LOROM_HEADER_ADDR + HEADER_MAP_OFFSET];
822    if lorom_map_byte == 0x20 || lorom_map_byte == 0x30 {
823        // $20 == LoROM, $30 == LoROM + FastROM
824        lorom_points += 1;
825    }
826
827    let hirom_map_byte = rom[HIROM_HEADER_ADDR + HEADER_MAP_OFFSET];
828    if hirom_map_byte == 0x21 || hirom_map_byte == 0x31 {
829        // $21 == HiROM, $31 == HiROM + FastROM
830        hirom_points += 1;
831    }
832
833    // All LoROM vectors should be in the range $8000-$FFFF, and A15 is ignored for mapping to ROM
834    let lorom_vector = u16::from_le_bytes([rom[LOROM_RESET_VECTOR], rom[LOROM_RESET_VECTOR + 1]]);
835    if lorom_vector >= 0x8000 && seems_like_valid_reset_vector(rom, lorom_vector & 0x7FFF) {
836        lorom_points += 1;
837    }
838
839    let hirom_vector = u16::from_le_bytes([rom[HIROM_RESET_VECTOR], rom[HIROM_RESET_VECTOR + 1]]);
840    if seems_like_valid_reset_vector(rom, hirom_vector) {
841        hirom_points += 1;
842    }
843
844    // Check for any coprocessors that put the header in the LoROM header location
845    if lorom_points >= hirom_points
846        && let Some(coprocessor) = check_for_lorom_coprocessor(rom)
847    {
848        return Some(coprocessor);
849    }
850
851    // Check for SPC7110
852    // Identified by chipset == $F5/$F9 and sub-chipset == $00 in HiROM header area
853    let hirom_chipset_byte = rom[HIROM_HEADER_ADDR + 0x16];
854    if hirom_points >= lorom_points
855        && rom[HIROM_HEADER_ADDR + 0x1A] == 0x33
856        && (hirom_chipset_byte == 0xF5 || hirom_chipset_byte == 0xF9)
857        && rom[HIROM_HEADER_ADDR - 1] == 0x00
858    {
859        return Some(CartridgeType::Spc7110);
860    }
861
862    match lorom_points.cmp(&hirom_points) {
863        Ordering::Less => Some(CartridgeType::HiRom),
864        Ordering::Greater => Some(CartridgeType::LoRom),
865        Ordering::Equal => None,
866    }
867}
868
869fn check_for_lorom_coprocessor(rom: &[u8]) -> Option<CartridgeType> {
870    const FORCE_COPROCESSOR_CHECKSUMS: &[(u32, CartridgeType)] = &[
871        (0x239D2862, CartridgeType::Sa1), // SA1 Demonstration Program (Unknown) (Tech Demo)
872    ];
873
874    let checksum = CRC.checksum(rom);
875    for &(other_checksum, cartridge_type) in FORCE_COPROCESSOR_CHECKSUMS {
876        if checksum == other_checksum {
877            return Some(cartridge_type);
878        }
879    }
880
881    let lorom_map_byte = rom[LOROM_HEADER_ADDR + HEADER_MAP_OFFSET];
882
883    // Check for CX4
884    // Identified by chipset == $Fx and subtype == $10
885    if rom[LOROM_HEADER_ADDR + 0x1A] == 0x33
886        && rom[LOROM_HEADER_ADDR + 0x16] & 0xF0 == 0xF0
887        && rom[LOROM_HEADER_ADDR - 1] == 0x10
888    {
889        return Some(CartridgeType::Cx4);
890    }
891
892    // Check for S-DD1
893    // Identified by map == $22/$32 and chipset == $4x in the LoROM header area
894    if (lorom_map_byte == 0x22 || lorom_map_byte == 0x32)
895        && (0x43..0x46).contains(&rom[LOROM_HEADER_ADDR + 0x16])
896    {
897        return Some(CartridgeType::Sdd1);
898    }
899
900    // Check for SA-1
901    // Identified by map == $23 and chipset == $3x in the LoROM header area
902    if lorom_map_byte == 0x23 && (0x33..0x36).contains(&rom[LOROM_HEADER_ADDR + 0x16]) {
903        return Some(CartridgeType::Sa1);
904    }
905
906    // Check for Super FX
907    // Identified by map == $20 and chipset $13-$1A in the LoROM header area
908    if lorom_map_byte == 0x20 && (0x13..0x1B).contains(&rom[LOROM_HEADER_ADDR + 0x16]) {
909        return Some(CartridgeType::SuperFx);
910    }
911
912    // Check for OBC1
913    // Identified by chipset $25 in the LoROM header area
914    if rom[LOROM_HEADER_ADDR + 0x16] == 0x25 {
915        return Some(CartridgeType::Obc1);
916    }
917
918    None
919}
920
921fn should_force_dsp1(checksum: u32) -> bool {
922    // DSP1 Tech Demo (World) (Tech Demo)
923    checksum == 0xD18A00CD
924}
925
926fn guess_dsp_variant(checksum: u32) -> DspVariant {
927    match checksum {
928        // Dungeon Master (U/J/J Rev 1/E)
929        0x0DFD9CEB | 0x7A1BA194 | 0xAA79FA33 | 0x89A67ADF => DspVariant::Dsp2,
930        // SD Gundam GX (J)
931        0x4DC3D903 => DspVariant::Dsp3,
932        // Top Gear 3000 (U/E) / The Planet's Champ TG 3000 (J)
933        0xA20BE998 | 0x493FDB13 | 0xB9B9DF06 => DspVariant::Dsp4,
934        _ => DspVariant::Dsp1,
935    }
936}
937
938fn guess_st01x_variant(rom: &[u8]) -> St01xVariant {
939    let checksum = CRC.checksum(rom);
940
941    // Hayazashi Nidan Morita Shougi (J)
942    if checksum == 0x81E822AD { St01xVariant::St011 } else { St01xVariant::St010 }
943}
944
945const CLC_OPCODE: u8 = 0x18;
946const SEI_OPCODE: u8 = 0x78;
947
948fn seems_like_valid_reset_vector(rom: &[u8], vector: u16) -> bool {
949    // Nearly all games execute either SEI or CLC as the first instruction at the RESET vector
950    let vector = vector as usize;
951    vector < rom.len() && (rom[vector] == CLC_OPCODE || rom[vector] == SEI_OPCODE)
952}
953
954pub(crate) enum CartridgeAddress {
955    None,
956    Rom(u32),
957    Sram(u32),
958}
959
960pub(crate) fn lorom_map_address(address: u32, rom_len: u32, sram_len: u32) -> CartridgeAddress {
961    let bank = address >> 16;
962    let offset = address & 0xFFFF;
963    match (bank, offset) {
964        (0x00..=0x3F | 0x80..=0xBF | 0x70..=0x7D | 0xF0..=0xFF, 0x8000..=0xFFFF)
965        | (0x40..=0x6F | 0xC0..=0xEF, _) => {
966            // ROM; typically at $8000-$FFFF and sometimes mirrored into $0000-$7FFF
967            let rom_addr = lorom_map_rom_address(address, rom_len);
968            CartridgeAddress::Rom(rom_addr)
969        }
970        (0x70..=0x7D | 0xF0..=0xFF, 0x0000..=0x7FFF) => {
971            // SRAM, if mapped
972            if sram_len != 0 {
973                // Ignore A15 and shift A16-19 right by one
974                // Games with more than 32KB of SRAM depend on this, e.g. Dezaemon (128KB SRAM)
975                let sram_addr = (address & 0x7FFF) | ((address & 0xF0000) >> 1);
976
977                // SRAM size is always a power of 2; use that to mask address
978                let sram_addr = sram_addr & (sram_len - 1);
979                CartridgeAddress::Sram(sram_addr)
980            } else {
981                // Treat as ROM mirror
982                let rom_addr = lorom_map_rom_address(address, rom_len);
983                CartridgeAddress::Rom(rom_addr)
984            }
985        }
986        _ => CartridgeAddress::None,
987    }
988}
989
990pub(crate) fn lorom_map_rom_address(address: u32, rom_len: u32) -> u32 {
991    // LoROM mapping ignores A23 and A15, and A16-22 are shifted right 1
992    let rom_addr = ((address & 0x7F0000) >> 1) | (address & 0x007FFF);
993    rom_addr & (rom_len - 1)
994}
995
996fn hirom_map_address(address: u32, rom_len: u32, sram_len: u32) -> CartridgeAddress {
997    let bank = address >> 16;
998    let offset = address & 0xFFFF;
999    match (bank, offset) {
1000        (0x40..=0x7D | 0xC0..=0xFF, _) | (0x00..=0x3F | 0x80..=0xBF, 0x8000..=0xFFFF) => {
1001            // ROM
1002            let rom_addr = hirom_map_rom_address(address, rom_len);
1003            CartridgeAddress::Rom(rom_addr)
1004        }
1005        (0x20..=0x3F | 0xA0..=0xBF, 0x6000..=0x7FFF) if sram_len != 0 => {
1006            // SRAM, if mapped
1007            let sram_bank = bank & 0x1F;
1008            let sram_addr = (sram_bank << 13) | (offset & 0x1FFF);
1009            CartridgeAddress::Sram(sram_addr & (sram_len - 1))
1010        }
1011        _ => CartridgeAddress::None,
1012    }
1013}
1014
1015fn hirom_map_rom_address(address: u32, rom_len: u32) -> u32 {
1016    // HiROM mapping simply ignores A23 and A22
1017    let rom_addr = address & 0x3FFFFF;
1018    rom_addr & (rom_len - 1)
1019}
1020
1021fn exhirom_map_address(address: u32, rom_len: u32, sram_len: u32) -> CartridgeAddress {
1022    let bank = address >> 16;
1023    let offset = address & 0xFFFF;
1024    match (bank, offset) {
1025        (0x40..=0x7D | 0xC0..=0xFF, _) | (0x00..=0x3F | 0x80..=0xBF, 0x8000..=0xFFFF) => {
1026            // ROM
1027            let rom_addr = exhirom_map_rom_address(address, rom_len);
1028            CartridgeAddress::Rom(rom_addr)
1029        }
1030        (0x00..=0x3F | 0x80..=0xBF, 0x6000..=0x7FFF) if sram_len != 0 => {
1031            // SRAM, if mapped (note bank range is different from regular HiROM)
1032            let sram_bank = bank & 0x1F;
1033            let sram_addr = ((sram_bank << 13) | (offset & 0x1FFF)) & (sram_len - 1);
1034            CartridgeAddress::Sram(sram_addr)
1035        }
1036        _ => CartridgeAddress::None,
1037    }
1038}
1039
1040fn exhirom_map_rom_address(address: u32, rom_len: u32) -> u32 {
1041    // ExHiROM mapping ignores A22, and A23 is inverted and shifted right 1
1042    let rom_addr = (address & 0x3FFFFF) | (((address >> 1) & 0x400000) ^ 0x400000);
1043    rom_addr & (rom_len - 1)
1044}