1use crate::cartridge::external::ExternalMemory; 2use crate::svp::Svp; 3use bincode::{Decode, Encode}; 4use crc::Crc; 5use genesis_config::{GenesisEmulatorConfig, GenesisRegion}; 6use jgenesis_common::cheats::CheatWordOverrides; 7use jgenesis_common::num::{GetBit, U16Ext}; 8use jgenesis_proc_macros::{FakeDecode, FakeEncode, PartialClone}; 9use regex::Regex; 10use std::sync::LazyLock; 11use std::{array, iter, mem}; 12 13pub mod eeprom; 14pub mod external; 15 16const CRC: Crc<u32> = Crc::<u32>::new(&crc::CRC_32_ISO_HDLC); 17 18#[derive(Debug, Clone, Default, FakeEncode, FakeDecode)] 19pub struct Rom(pub Box<[u16]>); 20 21impl Rom { 22 #[must_use] 23 pub fn new(bytes: Vec<u8>) -> Self { 24 Self(bytes_to_words(bytes)) 25 } 26 27 #[must_use] 28 pub fn read_byte(&self, address: u32) -> Option<u8> { 29 let word_addr = (address >> 1) as usize; 30 31 if word_addr < self.0.len() { 32 let word = self.0[word_addr]; 33 let byte = word >> (8 * ((address & 1) ^ 1)); 34 Some(byte as u8) 35 } else { 36 log::debug!("Out of bounds ROM byte read {address:06X}"); 37 None 38 } 39 } 40 41 #[must_use] 42 pub fn read_word(&self, address: u32) -> Option<u16> { 43 let word_addr = (address >> 1) as usize; 44 45 if word_addr < self.0.len() { 46 Some(self.0[word_addr]) 47 } else { 48 log::debug!("Out of bounds ROM word read {address:06X}"); 49 None 50 } 51 } 52} 53 54fn bytes_to_words(bytes: Vec<u8>) -> Box<[u16]> { 55 let words: Vec<_> = bytes 56 .chunks(2) 57 .map(|chunk| { 58 let msb = chunk[0]; 59 let lsb = chunk.get(1).copied().unwrap_or(0); 60 u16::from_be_bytes([msb, lsb]) 61 }) 62 .collect(); 63 words.into_boxed_slice() 64} 65 66fn words_to_bytes(words: Box<[u16]>) -> Vec<u8> { 67 words.into_iter().flat_map(u16::to_be_bytes).collect() 68} 69 70#[derive(Debug, Clone, Copy, Encode, Decode)] 71pub struct BasicMapper { 72 ram_mapped: bool, 73} 74 75impl BasicMapper { 76 #[must_use] 77 pub fn new(initial_ram_mapped: bool) -> Self { 78 Self { ram_mapped: initial_ram_mapped } 79 } 80 81 #[must_use] 82 pub fn read_byte(self, address: u32, rom: &Rom, external: &ExternalMemory) -> Option<u8> { 83 if self.ram_mapped 84 && let Some(byte) = external.read_byte(address) 85 { 86 Some(byte) 87 } else { 88 rom.read_byte(address) 89 } 90 } 91 92 #[must_use] 93 pub fn read_word(self, address: u32, rom: &Rom, external: &ExternalMemory) -> Option<u16> { 94 if self.ram_mapped 95 && let Some(word) = external.read_word(address) 96 { 97 Some(word) 98 } else { 99 rom.read_word(address) 100 } 101 } 102 103 pub fn write_byte(self, address: u32, value: u8, external: &mut ExternalMemory) { 104 if self.ram_mapped { 105 external.write_byte(address, value); 106 } 107 } 108 109 pub fn write_word(self, address: u32, value: u16, external: &mut ExternalMemory) { 110 if self.ram_mapped { 111 external.write_word(address, value); 112 } 113 } 114 115 pub fn write_register(&mut self, address: u32, value: u8) { 116 if address == 0xA130F1 { 117 write_ram_mapped_register(&mut self.ram_mapped, value); 118 } 119 } 120} 121 122fn write_ram_mapped_register(ram_mapped: &mut bool, value: u8) { 123 *ram_mapped = value.bit(0); 124 125 log::debug!("RAM map register write: {value:02X} (mapped = {ram_mapped})"); 126} 127 128#[derive(Debug, Clone, Copy, Encode, Decode)] 129pub struct SsfMapper { 130 rom_banks: [u8; 8], 131 ram_mapped: bool, 132} 133 134impl SsfMapper { 135 #[must_use] 136 pub fn new(initial_ram_mapped: bool) -> Self { 137 Self { rom_banks: array::from_fn(|i| i as u8), ram_mapped: initial_ram_mapped } 138 } 139 140 #[must_use] 141 pub fn map_address(self, address: u32) -> u32 { 142 let idx = (address >> 19) & 0x07; 143 let bank_number: u32 = self.rom_banks[idx as usize].into(); 144 (bank_number << 19) | (address & 0x07FFFF) 145 } 146 147 #[must_use] 148 pub fn read_byte(self, address: u32, rom: &Rom, external: &ExternalMemory) -> Option<u8> { 149 if self.ram_mapped 150 && let Some(byte) = external.read_byte(address) 151 { 152 Some(byte) 153 } else { 154 let rom_addr = self.map_address(address); 155 rom.read_byte(rom_addr) 156 } 157 } 158 159 #[must_use] 160 pub fn read_word(self, address: u32, rom: &Rom, external: &ExternalMemory) -> Option<u16> { 161 if self.ram_mapped 162 && let Some(word) = external.read_word(address) 163 { 164 Some(word) 165 } else { 166 let rom_addr = self.map_address(address); 167 rom.read_word(rom_addr) 168 } 169 } 170 171 pub fn write_byte(self, address: u32, value: u8, external: &mut ExternalMemory) { 172 if self.ram_mapped { 173 external.write_byte(address, value); 174 } 175 } 176 177 pub fn write_word(self, address: u32, value: u16, external: &mut ExternalMemory) { 178 if self.ram_mapped { 179 external.write_word(address, value); 180 } 181 } 182 183 pub fn write_register(&mut self, address: u32, value: u8) { 184 match address { 185 0xA130F1 => write_ram_mapped_register(&mut self.ram_mapped, value), 186 0xA130F3..=0xA130FF if address.bit(0) => { 187 // ROM bank registers 188 let idx = (address >> 1) & 7; 189 self.rom_banks[idx as usize] = value; 190 191 log::trace!("ROM bank {idx} set to {value:02X}"); 192 } 193 _ => {} 194 } 195 } 196 197 #[must_use] 198 #[allow(clippy::missing_panics_doc)] 199 pub fn should_use(rom: &[u8]) -> bool { 200 // Enable the bank switching mapper for any cartridge that declares its system type as "SEGA SSF" 201 if &rom[0x100..0x110] == b"SEGA SSF " { 202 return true; 203 } 204 205 // If ROM is larger than 4 MB but address range is specified as $000000-$3FFFFF, assume it 206 // needs the SSF mapper (Super Street Fighter II, Demons of Asteborg, various homebrew) 207 if rom.len() > 0x400000 { 208 let rom_range_start = u32::from_be_bytes(rom[0x1A0..0x1A4].try_into().unwrap()); 209 let rom_range_end = u32::from_be_bytes(rom[0x1A4..0x1A8].try_into().unwrap()); 210 if rom_range_start == 0x000000 && rom_range_end == 0x3FFFFF { 211 return true; 212 } 213 } 214 215 false 216 } 217} 218 219#[derive(Debug, Clone, Encode, Decode)] 220enum Mapper { 221 Basic(BasicMapper), 222 Ssf(SsfMapper), 223 Svp(Box<Svp>), 224 // TODO this mapper is probably not implemented correctly - seems to have ROM banking 225 UnlRockmanX3, 226} 227 228impl Mapper { 229 fn read_byte(&mut self, address: u32, rom: &Rom, external: &ExternalMemory) -> Option<u8> { 230 match self { 231 Self::Basic(mapper) => mapper.read_byte(address, rom, external), 232 Self::Ssf(mapper) => mapper.read_byte(address, rom, external), 233 Self::Svp(svp) => { 234 let word = svp.m68k_read(address, &rom.0); 235 let byte = if !address.bit(0) { word.msb() } else { word.lsb() }; 236 Some(byte) 237 } 238 Self::UnlRockmanX3 => rom.read_byte(address), 239 } 240 } 241 242 fn read_word(&mut self, address: u32, rom: &Rom, external: &ExternalMemory) -> Option<u16> { 243 match self { 244 Self::Basic(mapper) => mapper.read_word(address, rom, external), 245 Self::Ssf(mapper) => mapper.read_word(address, rom, external), 246 Self::Svp(svp) => Some(svp.m68k_read(address, &rom.0)), 247 Self::UnlRockmanX3 => { 248 match address { 249 // The unlicensed Rockman X3 port depends on $A13000 reads returning a value where the lower 250 // 4 bits are $C or else it will immediately crash and display "decode error" 251 0xA13000 => Some(0xC), 252 _ => rom.read_word(address), 253 } 254 } 255 } 256 } 257 258 fn peek_word(&self, address: u32, rom: &Rom, external: &ExternalMemory) -> Option<u16> { 259 match self { 260 Self::Basic(mapper) => mapper.read_word(address, rom, external), 261 Self::Ssf(mapper) => mapper.read_word(address, rom, external), 262 Self::Svp(svp) => Some(svp.m68k_peek(address, &rom.0)), 263 Self::UnlRockmanX3 => rom.read_word(address), 264 } 265 } 266 267 fn write_byte(&mut self, address: u32, value: u8, external: &mut ExternalMemory) { 268 match self { 269 Self::Basic(mapper) => mapper.write_byte(address, value, external), 270 Self::Ssf(mapper) => mapper.write_byte(address, value, external), 271 Self::Svp(svp) => svp.m68k_write_byte(address, value), 272 Self::UnlRockmanX3 => {} 273 } 274 } 275 276 fn write_word(&mut self, address: u32, value: u16, external: &mut ExternalMemory) { 277 match self { 278 Self::Basic(mapper) => mapper.write_word(address, value, external), 279 Self::Ssf(mapper) => mapper.write_word(address, value, external), 280 Self::Svp(svp) => svp.m68k_write_word(address, value), 281 Self::UnlRockmanX3 => {} 282 } 283 } 284 285 fn write_register_byte(&mut self, address: u32, value: u8) { 286 match self { 287 Self::Basic(mapper) => mapper.write_register(address, value), 288 Self::Ssf(mapper) => mapper.write_register(address, value), 289 Self::Svp(svp) => svp.m68k_write_byte(address, value), 290 Self::UnlRockmanX3 => {} 291 } 292 } 293 294 fn write_register_word(&mut self, address: u32, value: u16) { 295 match self { 296 Self::Basic(mapper) => mapper.write_register(address | 1, value as u8), 297 Self::Ssf(mapper) => mapper.write_register(address | 1, value as u8), 298 Self::Svp(svp) => svp.m68k_write_word(address, value), 299 Self::UnlRockmanX3 => {} 300 } 301 } 302 303 fn name(&self) -> &'static str { 304 match self { 305 Self::Basic(..) => "Basic", 306 Self::Ssf(..) => "SSF", 307 Self::Svp(..) => "SVP", 308 Self::UnlRockmanX3 => "Unlicensed Rockman X3", 309 } 310 } 311} 312 313pub trait GenesisRegionExt: Sized + Copy { 314 #[must_use] 315 fn from_rom(rom: &[u8]) -> Option<Self>; 316 317 #[must_use] 318 fn version_bit(self) -> bool; 319} 320 321impl GenesisRegionExt for GenesisRegion { 322 fn from_rom(rom: &[u8]) -> Option<Self> { 323 // European games with incorrect region headers that indicate US or JP support 324 const DEFAULT_EUROPE_CHECKSUMS: &[u32] = &[ 325 0x28165BD1, // Alisia Dragoon (Europe) 326 0x224256C7, // Andre Agassi Tennis (Europe) 327 0x90F5C2B7, // Brian Lara Cricket (Europe) 328 0xEB8F4374, // Indiana Jones and the Last Crusade (Europe) 329 0xD97D1699, // International Rugby (Europe) 330 0xC0DCE0E5, // Midway Presents Arcade's Greatest Hits (Europe) 331 0x924E57D3, // Olympic Gold (Europe) 332 0xFA537A45, // Winter Olympics (Europe) 333 0xDACA01C3, // World Class Leader Board (Europe) 334 0x4C926BF6, // Nuance Xmas-Intro 2024 335 0x0F51DD6A, // Chaekopon by Limp Ninja 336 ]; 337 338 if DEFAULT_EUROPE_CHECKSUMS.contains(&CRC.checksum(rom)) { 339 return Some(GenesisRegion::Europe); 340 } 341 342 // $100 contains the system name 343 // If it's "SEGA GENESIS" instead of "SEGA MEGA DRIVE", ignore region string and assume US 344 if &rom[0x100..0x100 + b"SEGA GENESIS".len()] == b"SEGA GENESIS" { 345 return Some(GenesisRegion::Americas); 346 } 347 348 // Shadow Squadron / Stellar Assault (UE) reports its region as E in the header, 349 // but it's NTSC-compatible; prefer Americas if region is not forced so it will run at 350 // 60Hz instead of 50Hz 351 if &rom[0x180..0x18E] == "GM MK-84509-00".as_bytes() { 352 return Some(GenesisRegion::Americas); 353 } 354 355 if &rom[0x1F0..0x1F6] == b"EUROPE" { 356 // Another World (E) has the string "EUROPE" in the region section; special case this 357 // so that it's not detected as U (this game does not work with NTSC timings) 358 return Some(GenesisRegion::Europe); 359 } 360 361 let region_bytes = &rom[0x1F0..0x1F3]; 362 363 // Prefer Americas if region code contains a 'U' 364 if region_bytes.contains(&b'U') { 365 return Some(GenesisRegion::Americas); 366 } 367 368 // Otherwise, prefer Japan if it contains a 'J' 369 if region_bytes.contains(&b'J') { 370 return Some(GenesisRegion::Japan); 371 } 372 373 // Finally, prefer Europe if it contains an 'E' 374 if region_bytes.contains(&b'E') { 375 return Some(GenesisRegion::Europe); 376 } 377 378 // If region code contains neither a 'U' nor a 'J', treat it as a hex char 379 let c = region_bytes[0] as char; 380 let value = u8::from_str_radix(&c.to_string(), 16).ok()?; 381 if value.bit(2) { 382 // Bit 2 = Americas 383 Some(GenesisRegion::Americas) 384 } else if value.bit(0) { 385 // Bit 0 = Asia 386 Some(GenesisRegion::Japan) 387 } else if value.bit(3) { 388 // Bit 3 = Europe 389 Some(GenesisRegion::Europe) 390 } else { 391 // Invalid 392 None 393 } 394 } 395 396 #[inline] 397 fn version_bit(self) -> bool { 398 self != Self::Japan 399 } 400} 401 402#[derive(Debug, Clone, Encode, Decode)] 403pub struct CartridgeMetadata { 404 pub program_title: String, 405 pub region: GenesisRegion, 406 // If the game has known issues when sprite limits are removed (e.g. Sonic 1) 407 pub sprite_limit_compatibility_issues: bool, 408 pub six_button_incompatible: bool, 409} 410 411type CartridgeCheatOverrides = CheatWordOverrides<0x000000, 0x3FFFFF>; 412 413#[derive(Debug, Clone, Encode, Decode, PartialClone)] 414pub struct Cartridge { 415 #[partial_clone(default)] 416 rom: Rom, 417 external: ExternalMemory, 418 mapper: Mapper, 419 forced_region: Option<GenesisRegion>, 420 metadata: CartridgeMetadata, 421 cheat_overrides: CartridgeCheatOverrides, 422} 423 424const TRIPLE_PLAY_GOLD_SERIAL: &[u8] = b"T-172116"; 425const TRIPLE_PLAY_96_SERIAL: &[u8] = b"T-172026"; 426 427const QUACKSHOT_REV_A_SERIAL: &[u8] = b"GM 00004054-01"; 428 429const ROCKMAN_X3_CHECKSUM: u32 = 0x3EE639F0; 430 431impl Cartridge { 432 #[must_use] 433 pub fn new( 434 rom_bytes: Vec<u8>, 435 initial_ram_bytes: Option<Vec<u8>>, 436 forced_region: Option<GenesisRegion>, 437 cheat_codes: &[(u32, u16)], 438 ) -> Self { 439 // Take checksum before potentially byteswapping the ROM 440 let checksum = CRC.checksum(&rom_bytes); 441 log::info!("ROM CRC32: {checksum:08X}"); 442 443 let mut rom_bytes = ensure_rom_in_expected_format(rom_bytes); 444 445 let region = GenesisRegion::from_rom(&rom_bytes).unwrap_or_else(|| { 446 log::warn!("Unable to determine cartridge region from ROM header; using Americas"); 447 GenesisRegion::Americas 448 }); 449 450 log::info!("Cartridge region: {region:?}"); 451 452 let external_memory = ExternalMemory::from_rom(&rom_bytes, checksum, initial_ram_bytes); 453 454 let serial_number = &rom_bytes[0x183..0x18B]; 455 let is_triple_play = rom_bytes.len() >= 0x300000 456 && (serial_number == TRIPLE_PLAY_GOLD_SERIAL || serial_number == TRIPLE_PLAY_96_SERIAL); 457 if is_triple_play { 458 fix_triple_play_rom(&mut rom_bytes); 459 } 460 461 // Initialize ram_mapped to true if external memory is present and the address range is past 462 // the end of ROM, _or_ if the cartridge has EEPROM (some >2MB EEPROM games depend on this). 463 // Some games with cartridge RAM will never write to $A130F1, assuming RAM is always mapped. 464 // 465 // Special case Triple Play because its RAM is mapped to $200000-$20FFFF while ROM is mapped 466 // to $000000-$1FFFFF + $300000-$3FFFFF, and it never writes to $A130F1 467 let external_present = !matches!(external_memory, ExternalMemory::None); 468 let is_eeprom = matches!(external_memory, ExternalMemory::Eeprom { .. }); 469 let ram_start_past_rom = external_memory.address_range().start >= rom_bytes.len() as u32; 470 let initial_ram_mapped = 471 external_present && (ram_start_past_rom || is_eeprom || is_triple_play); 472 473 let serial_number = &rom_bytes[0x183..0x18B]; 474 let mapper = if is_virtua_racing(serial_number) { 475 // Only one game uses the SVP, Virtua Racing 476 Mapper::Svp(Box::new(Svp::new())) 477 } else if SsfMapper::should_use(&rom_bytes) { 478 Mapper::Ssf(SsfMapper::new(initial_ram_mapped)) 479 } else if checksum == ROCKMAN_X3_CHECKSUM { 480 Mapper::UnlRockmanX3 481 } else { 482 Mapper::Basic(BasicMapper::new(initial_ram_mapped)) 483 }; 484 485 log::info!("Using mapper {}", mapper.name()); 486 487 if rom_bytes.len() == 0x80000 && &rom_bytes[0x180..0x18E] == QUACKSHOT_REV_A_SERIAL { 488 rom_bytes = fix_quackshot_rev_a_rom(rom_bytes); 489 } 490 491 let program_title = parse_title_from_header(&rom_bytes, region); 492 let sprite_limit_compatibility_issues = has_sprite_limit_compatibility_issues(&rom_bytes); 493 let six_button_incompatible_game = is_six_button_incompatible(&rom_bytes); 494 495 let metadata = CartridgeMetadata { 496 program_title, 497 region, 498 sprite_limit_compatibility_issues, 499 six_button_incompatible: six_button_incompatible_game, 500 }; 501 502 let cheat_overrides = CartridgeCheatOverrides::new(cheat_codes); 503 504 Self { 505 rom: Rom::new(rom_bytes), 506 external: external_memory, 507 mapper, 508 forced_region, 509 metadata, 510 cheat_overrides, 511 } 512 } 513 514 #[inline] 515 pub fn read<const WORD: bool>(&mut self, address: u32, open_bus: u16) -> u16 { 516 if WORD { 517 self.read_word(address, open_bus) 518 } else { 519 self.read_byte(address, open_bus).into() 520 } 521 } 522 523 #[inline] 524 pub fn read_byte(&mut self, address: u32, open_bus: u16) -> u8 { 525 if let Some(cheat) = self.cheat_overrides.get(address) { 526 return cheat.to_be_bytes()[(address & 1) as usize]; 527 } 528 529 self.mapper 530 .read_byte(address, &self.rom, &self.external) 531 .unwrap_or_else(|| open_bus.to_be_bytes()[(address & 1) as usize]) 532 } 533 534 #[inline] 535 pub fn read_word(&mut self, address: u32, open_bus: u16) -> u16 { 536 if let Some(cheat) = self.cheat_overrides.get(address) { 537 return cheat; 538 } 539 540 self.mapper.read_word(address, &self.rom, &self.external).unwrap_or(open_bus) 541 } 542 543 #[inline] 544 pub fn read_word_for_dma(&mut self, address: u32, open_bus: &mut u16) -> u16 { 545 // SVP cartridge memory has the same delay issue as Sega CD word RAM; Virtua Racing sets 546 // DMA source address 2 higher than the "correct" address 547 match &mut self.mapper { 548 Mapper::Svp(svp) => mem::replace(open_bus, svp.m68k_read(address, &self.rom.0)), 549 _ => { 550 *open_bus = self.read_word(address, *open_bus); 551 *open_bus 552 } 553 } 554 } 555 556 #[inline] 557 pub fn write<const WORD: bool>(&mut self, address: u32, value: u16) { 558 if WORD { 559 self.write_word(address, value); 560 } else { 561 self.write_byte(address, value as u8); 562 } 563 } 564 565 #[inline] 566 pub fn write_byte(&mut self, address: u32, value: u8) { 567 match address { 568 0x000000..=0x7FFFFF => { 569 self.mapper.write_byte(address, value, &mut self.external); 570 } 571 0xA13000..=0xA15FFF => { 572 self.mapper.write_register_byte(address, value); 573 } 574 _ => { 575 log::debug!("Write to invalid cartridge address: {address:06X} {value:02X}"); 576 } 577 } 578 } 579 580 #[inline] 581 pub fn write_word(&mut self, address: u32, value: u16) { 582 match address { 583 0x000000..=0x7FFFFF => { 584 self.mapper.write_word(address, value, &mut self.external); 585 } 586 0xA13000..=0xA15FFF => { 587 self.mapper.write_register_word(address, value); 588 } 589 _ => { 590 log::debug!("Write to invalid cartridge address: {address:06X} {value:04X}"); 591 } 592 } 593 } 594 595 #[must_use] 596 #[inline] 597 pub fn region(&self) -> GenesisRegion { 598 self.forced_region.unwrap_or(self.metadata.region) 599 } 600 601 #[inline] 602 pub fn tick(&mut self, m68k_cycles: u32) { 603 if let Mapper::Svp(svp) = &mut self.mapper { 604 svp.tick(&self.rom.0, m68k_cycles); 605 } 606 } 607 608 #[must_use] 609 pub fn take_rom(&mut self) -> Vec<u8> { 610 let words = mem::take(&mut self.rom.0); 611 words_to_bytes(words) 612 } 613 614 pub fn take_rom_from(&mut self, other: &mut Self) { 615 self.rom = mem::take(&mut other.rom); 616 } 617 618 #[inline] 619 #[must_use] 620 pub fn external_ram(&self) -> &[u8] { 621 self.external.get_memory() 622 } 623 624 #[inline] 625 #[must_use] 626 pub fn is_ram_persistent(&self) -> bool { 627 self.external.is_persistent() 628 } 629 630 #[inline] 631 #[must_use] 632 pub fn get_and_clear_ram_dirty(&mut self) -> bool { 633 self.external.get_and_clear_dirty_bit() 634 } 635 636 #[inline] 637 #[must_use] 638 pub fn metadata(&self) -> &CartridgeMetadata { 639 &self.metadata 640 } 641 642 #[inline] 643 #[must_use] 644 pub fn program_title(&self) -> &str { 645 &self.metadata.program_title 646 } 647 648 pub fn reload_config(&mut self, config: &GenesisEmulatorConfig) { 649 self.forced_region = config.forced_region; 650 self.cheat_overrides.update_cheat_codes(&config.cheat_codes); 651 } 652 653 #[must_use] 654 pub fn peek_word(&self, address: u32) -> u16 { 655 self.mapper.peek_word(address, &self.rom, &self.external).unwrap_or(0xFFFF) 656 } 657 658 #[must_use] 659 pub fn debug_rom_view(&mut self) -> &mut [u16] { 660 self.rom.0.as_mut() 661 } 662 663 #[must_use] 664 pub fn debug_rom_view_shared(&self) -> &[u16] { 665 self.rom.0.as_ref() 666 } 667} 668 669fn ensure_rom_in_expected_format(mut rom: Vec<u8>) -> Vec<u8> { 670 // For very tiny ROMs, pad to 1KB before doing anything else 671 // e.g. "Mona in 344 bytes" demo 672 const MIN_ROM_LEN: usize = 1024; 673 674 if rom.len() < MIN_ROM_LEN { 675 jgenesis_common::rom::mirror_to_next_power_of_two(&mut rom); 676 677 while rom.len() < MIN_ROM_LEN { 678 for i in 0..rom.len() { 679 rom.push(rom[i]); 680 } 681 } 682 } 683 684 rom = remove_copier_header(rom); 685 rom = deinterleave_rom(rom); 686 ensure_big_endian(rom) 687} 688 689fn remove_copier_header(rom: Vec<u8>) -> Vec<u8> { 690 // Some older ROMs contain a useless 512-byte copier header; remove it if present 691 if rom.len() & 0x3FF != 0x200 { 692 // ROM length is not off by 512 from a reasonable number 693 return rom; 694 } 695 696 // TMSS header is normally at $100-$103, would be at $303-$304 with the 512-byte header 697 let tmss_header = &rom[0x300..0x304]; 698 699 // Interleaved header bytes are normally at $80-$81 for even and $2080-$2081 for odd 700 let interleaved_tmss_even = &rom[0x0280..0x0282]; 701 let interleaved_tmss_odd = &rom[0x2280..0x2282]; 702 703 if tmss_header != b"SEGA" 704 && tmss_header != b"ESAG" 705 && !(interleaved_tmss_even == b"EA" && interleaved_tmss_odd == b"SG") 706 { 707 // Removing the copier header would not produce a valid TMSS header 708 return rom; 709 } 710 711 log::info!("ROM image appears to have a 512-byte copier header; removing it"); 712 713 rom.into_iter().skip(512).collect() 714} 715 716fn ensure_big_endian(mut rom: Vec<u8>) -> Vec<u8> { 717 // Every licensed game contains the ASCII string "SEGA" at $100-$104 in ROM 718 // If the string "ESAG" is detected there, byteswap the ROM 719 if &rom[0x100..0x104] == "ESAG".as_bytes() { 720 log::info!("Byteswapping ROM because it appears to be little-endian"); 721 722 for chunk in rom.as_chunks_mut::<2>().0 { 723 chunk.swap(0, 1); 724 } 725 } 726 727 rom 728} 729 730fn deinterleave_rom(rom: Vec<u8>) -> Vec<u8> { 731 // Some older ROM images, usually with the .smd file extension, are interleaved. 732 // This format consists of 16KB blocks where each block contains 8KB of even bytes followed by 733 // 8KB of odd bytes. 734 if !rom.len().is_multiple_of(16 * 1024) { 735 // Interleaved ROM sizes should always be a multiple of 16KB 736 return rom; 737 } 738 739 if &rom[0x100..0x104] == b"SEGA" || &rom[0x100..0x104] == b"ESAG" { 740 // ROM image already contains valid TMSS text; don't try to deinterleave 741 return rom; 742 } 743 744 if &rom[0x0080..0x0082] != b"EA" || &rom[0x2080..0x2082] != b"SG" { 745 // Deinterleaving would not produce valid TMSS text; don't try to deinterleave 746 return rom; 747 } 748 749 log::info!("ROM image appears to be interleaved; deinterleaving it"); 750 751 let mut deinterleaved = vec![0; rom.len()]; 752 for block_addr in (0..rom.len()).step_by(0x4000) { 753 for i in 0..0x2000 { 754 deinterleaved[block_addr + 2 * i] = rom[block_addr + 0x2000 + i]; 755 deinterleaved[block_addr + 2 * i + 1] = rom[block_addr + i]; 756 } 757 } 758 759 deinterleaved 760} 761 762fn fix_triple_play_rom(rom: &mut Vec<u8>) { 763 // Triple Play expects the third MB of the ROM to be mapped to $300000-$3FFFFF instead 764 // of $200000-$2FFFFF; accomplish this by duplicating the data 765 if rom.len() < 0x400000 { 766 rom.extend(iter::repeat_n(0xFF, 0x400000 - rom.len())); 767 } 768 769 let (first, second) = rom.split_at_mut(0x300000); 770 second[..0x100000].copy_from_slice(&first[0x200000..0x300000]); 771} 772 773fn fix_quackshot_rev_a_rom(rom: Vec<u8>) -> Vec<u8> { 774 // QuackShot (Rev A) is a 512KB ROM with an unusual ROM address mapping: 775 // $000000-$0FFFFF: First 256KB of ROM mirrored 4x 776 // $100000-$1FFFFF: Second 256KB of ROM mirrored 4x 777 // Rather than implement custom mapping logic, just remap the ROM while loading it 778 let mut remapped_rom = vec![0; 0x200000]; 779 for i in (0x000000..0x100000).step_by(0x40000) { 780 remapped_rom[i..i + 0x40000].copy_from_slice(&rom[..0x40000]); 781 remapped_rom[i + 0x100000..i + 0x140000].copy_from_slice(&rom[0x40000..]); 782 } 783 784 remapped_rom 785} 786 787#[must_use] 788#[allow(clippy::missing_panics_doc, clippy::items_after_statements)] 789pub fn parse_title_from_header(rom: &[u8], region: GenesisRegion) -> String { 790 let addr = match region { 791 GenesisRegion::Americas | GenesisRegion::Europe => 0x0150, 792 GenesisRegion::Japan => 0x0120, 793 }; 794 let bytes = &rom[addr..addr + 48]; 795 let title = bytes.iter().copied().map(|b| b as char).collect::<String>(); 796 797 static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r" +").unwrap()); 798 RE.replace_all(title.trim(), " ").into() 799} 800 801fn is_virtua_racing(serial_number: &[u8]) -> bool { 802 serial_number == b"MK-1229 " || serial_number == b"G-7001 " 803} 804 805fn has_sprite_limit_compatibility_issues(rom: &[u8]) -> bool { 806 const SERIAL_NUMBERS: &[&[u8]] = &[ 807 b"GM 00001009", // Sonic the Hedgehog (USA, Europe) 808 b"GM 00004049", // Sonic the Hedgehog (Japan, Europe, Korea) 809 ]; 810 811 let serial_number = &rom[0x180..0x18B]; 812 SERIAL_NUMBERS.contains(&serial_number) 813} 814 815#[must_use] 816pub fn is_six_button_incompatible(rom: &[u8]) -> bool { 817 // List of games from https://segaretro.org/Six_Button_Control_Pad_(Mega_Drive) 818 const SERIAL_NUMBERS: &[&[u8]] = &[ 819 b"GM_T-081056", // Arch Rivals - The Arcade Game (USA, Europe) 820 b"GM T-49116 ", // Beast Wrestler (USA) 821 b"GM T-74023-", // Double Dragon II - The Revenge (Japan) 822 b"GM T-50156 ", // The Faery Tale Adventure (USA, Europe) 823 b"GM 00004016", // Forgotten Worlds (World) 824 b"GM 00001122", // Golden Axe II (World) 825 b"T-88056-50 ", // International Rugby (Europe) 826 b"GM T-50016 ", // John Madden Football (USA, Europe) 827 b"GM T-103026", // King of the Monsters (USA) 828 b"GM G-4104 0", // King of the Monsters (Japan) 829 b"GM MK-1086-", // King of the Monsters (Europe) 830 b"GM MK-1210 ", // Mario Lemieux Hockey (USA, Europe) 831 b"GM T-48036 ", // Ms. Pac-Man (USA, Europe) 832 b"GM T-79016", // Olympic Gold (Japan, USA, Europe) 833 b"GM T-113106", // Second Samurai (Europe) 834 b"GM T-50216 ", // Starflight (USA, Europe) 835 b"GM T-95026-", // Sunset Riders (USA, Europe) 836 b"GM T-70015-", // The Terminator (USA, Europe) (Sega CD) 837 ]; 838 839 let serial_number = &rom[0x180..0x18B]; 840 SERIAL_NUMBERS.contains(&serial_number) 841}