1//! Sega CD public interface and main loop 2 3pub mod debug; 4 5use crate::api::debug::SegaCdDebugger; 6use crate::bus::SegaCdBus; 7use crate::bus::debug::DebugSegaCdBus; 8use crate::memory; 9use bincode::{Decode, Encode}; 10use cdrom::CdRomError; 11use cdrom::cdtime::CdTime; 12use cdrom::reader::CdRom; 13use genesis_components::cartridge::GenesisRegionExt; 14use genesis_config::GenesisEmulatorConfig; 15use genesis_config::GenesisRegion; 16use jgenesis_common::frontend::{PartialClone, TimingMode}; 17use jgenesis_common::num::U16Ext; 18use m68000_emu::M68000; 19use std::fmt::Debug; 20use std::mem; 21use thiserror::Error; 22 23pub const DEFAULT_SUB_CPU_DIVIDER: u64 = genesis_config::NATIVE_SUB_CPU_DIVIDER; 24 25const NTSC_GENESIS_MASTER_CLOCK_RATE: u64 = genesis_components::NTSC_GENESIS_MCLK_FREQUENCY as u64; 26const PAL_GENESIS_MASTER_CLOCK_RATE: u64 = genesis_components::PAL_GENESIS_MCLK_FREQUENCY as u64; 27pub const SEGA_CD_MASTER_CLOCK_RATE: u64 = 50_000_000; 28 29const BIOS_LEN: usize = memory::BIOS_LEN; 30 31#[derive(Debug, Error)] 32pub enum SegaCdLoadError { 33 #[error("BIOS is required for Sega CD emulation")] 34 MissingBios, 35 #[error("BIOS must be {BIOS_LEN} bytes, was {bios_len} bytes")] 36 InvalidBios { bios_len: usize }, 37 #[error("CD-ROM-related error: {0}")] 38 CdRom(#[from] CdRomError), 39} 40 41pub type SegaCdLoadResult<T> = Result<T, SegaCdLoadError>; 42 43pub trait SegaCdAudioOutput { 44 fn collect_pcm(&mut self, sample: (f64, f64)); 45 46 fn collect_cd(&mut self, sample: (f64, f64)); 47} 48 49#[derive(Debug, Encode, Decode, PartialClone)] 50pub struct SegaCd { 51 sub_cpu: M68000, 52 #[partial_clone(partial)] 53 bus: SegaCdBus, 54 disc_title: Option<String>, 55 six_button_incompatible_game: bool, 56 timing_mode: TimingMode, 57 sega_cd_mclk_cycles: u64, 58 sega_cd_mclk_cycle_product: u64, 59 sub_cpu_divider: u64, 60 sub_cpu_wait_cycles: u64, 61 config: GenesisEmulatorConfig, 62} 63 64impl SegaCd { 65 /// # Errors 66 /// 67 /// Returns an error if the BIOS ROM is not the correct size or if there is an error reading 68 /// the CD-ROM image. 69 pub fn new( 70 bios: Vec<u8>, 71 disc: Option<CdRom>, 72 timing_mode: TimingMode, 73 initial_backup_ram: Option<Vec<u8>>, 74 initial_ram_cartridge: Option<Vec<u8>>, 75 config: &GenesisEmulatorConfig, 76 ) -> SegaCdLoadResult<Self> { 77 if bios.len() != memory::BIOS_LEN { 78 return Err(SegaCdLoadError::InvalidBios { bios_len: bios.len() }); 79 } 80 81 let mut bus = 82 SegaCdBus::new(bios, disc, initial_backup_ram, initial_ram_cartridge, config)?; 83 84 let disc_title = bus.disc_title()?; 85 let six_button_incompatible_game = bus.has_six_button_incompatible_game()?; 86 87 let sub_cpu = M68000::builder().allow_tas_writes(true).name("Sub".into()).build(); 88 let sub_cpu_divider = config.sega_cd.sub_cpu_divider.get(); 89 90 Ok(Self { 91 sub_cpu, 92 bus, 93 timing_mode, 94 disc_title, 95 six_button_incompatible_game, 96 sega_cd_mclk_cycles: 0, 97 sega_cd_mclk_cycle_product: 0, 98 sub_cpu_divider, 99 sub_cpu_wait_cycles: 0, 100 config: config.clone(), 101 }) 102 } 103 104 /// # Errors 105 /// 106 /// Propagates any CD-ROM disc read errors. 107 #[inline] 108 pub fn tick<const DEBUG: bool>( 109 &mut self, 110 genesis_mclk_elapsed: u64, 111 audio_output: &mut impl SegaCdAudioOutput, 112 debugger: &mut impl SegaCdDebugger, 113 ) -> SegaCdLoadResult<()> { 114 self.sega_cd_mclk_cycle_product += genesis_mclk_elapsed * SEGA_CD_MASTER_CLOCK_RATE; 115 let scd_mclk_elapsed = match self.timing_mode { 116 TimingMode::Ntsc => { 117 let elapsed = self.sega_cd_mclk_cycle_product / NTSC_GENESIS_MASTER_CLOCK_RATE; 118 self.sega_cd_mclk_cycle_product -= elapsed * NTSC_GENESIS_MASTER_CLOCK_RATE; 119 elapsed 120 } 121 TimingMode::Pal => { 122 let elapsed = self.sega_cd_mclk_cycle_product / PAL_GENESIS_MASTER_CLOCK_RATE; 123 self.sega_cd_mclk_cycle_product -= elapsed * PAL_GENESIS_MASTER_CLOCK_RATE; 124 elapsed 125 } 126 }; 127 128 let prev_scd_mclk_cycles = self.sega_cd_mclk_cycles; 129 self.sega_cd_mclk_cycles += scd_mclk_elapsed; 130 131 let pcm_cycles = self.sega_cd_mclk_cycles / DEFAULT_SUB_CPU_DIVIDER 132 - prev_scd_mclk_cycles / DEFAULT_SUB_CPU_DIVIDER; 133 let elapsed_scd_mclk_cycles = self.sega_cd_mclk_cycles - prev_scd_mclk_cycles; 134 135 // This match seems silly, but it avoids doing an integer division for the common dividers 136 // of 1-4. Dividers higher than 4 can only be set via the CLI or by manually editing config 137 // (and underclocking probably won't work well anyway) 138 let sub_cpu_cycles = match self.sub_cpu_divider { 139 DEFAULT_SUB_CPU_DIVIDER => pcm_cycles, 140 3 => self.sega_cd_mclk_cycles / 3 - prev_scd_mclk_cycles / 3, 141 2 => (self.sega_cd_mclk_cycles >> 1) - (prev_scd_mclk_cycles >> 1), 142 1 => elapsed_scd_mclk_cycles, 143 _ => { 144 self.sega_cd_mclk_cycles / self.sub_cpu_divider 145 - prev_scd_mclk_cycles / self.sub_cpu_divider 146 } 147 }; 148 149 self.bus.tick_components(scd_mclk_elapsed, pcm_cycles, audio_output)?; 150 151 self.tick_sub_cpu::<DEBUG>(sub_cpu_cycles, debugger); 152 153 Ok(()) 154 } 155 156 fn tick_sub_cpu<const DEBUG: bool>( 157 &mut self, 158 mut sub_cpu_cycles: u64, 159 debugger: &mut impl SegaCdDebugger, 160 ) { 161 if self.bus.word_ram().sub_performed_blocked_access() { 162 // If the sub CPU accesses word RAM while it's in 2M mode and owned by the main CPU, it 163 // should halt until the main CPU writes DMNA=1 to transfer ownership to the sub CPU. 164 // Marko's Magic Football depends on this or it will have glitched map graphics 165 log::trace!("Not running sub CPU because word RAM writes are buffered"); 166 return; 167 } 168 169 while sub_cpu_cycles >= self.sub_cpu_wait_cycles { 170 self.bus.flush_buffered_sub_writes(); 171 172 sub_cpu_cycles -= self.sub_cpu_wait_cycles; 173 174 self.sub_cpu_wait_cycles = if DEBUG { 175 let mut debug_bus = DebugSegaCdBus::new(&mut self.bus, debugger); 176 self.sub_cpu.execute_instruction(&mut debug_bus).into() 177 } else { 178 self.sub_cpu.execute_instruction(&mut self.bus).into() 179 }; 180 181 if self.bus.word_ram().sub_performed_blocked_access() { 182 return; 183 } 184 } 185 186 self.sub_cpu_wait_cycles -= sub_cpu_cycles; 187 } 188 189 #[must_use] 190 #[inline] 191 pub fn main_read_memory<const WORD: bool>(&self, address: u32) -> u16 { 192 if address & 0x200000 == 0 { 193 // BIOS ROM / PRG RAM 194 if WORD { 195 self.bus.main_read_bios_prg_ram(address) 196 } else { 197 self.bus.main_read_bios_prg_ram(address & !1).be_byte(address & 1).into() 198 } 199 } else { 200 // Word RAM 201 if WORD { 202 let msb = self.bus.main_read_word_ram(address); 203 let lsb = self.bus.main_read_word_ram(address + 1); 204 u16::from_be_bytes([msb, lsb]) 205 } else { 206 self.bus.main_read_word_ram(address).into() 207 } 208 } 209 } 210 211 #[inline] 212 pub fn main_write_memory<const WORD: bool>(&mut self, address: u32, value: u16) { 213 if address & 0x200000 == 0 { 214 // BIOS ROM / PRG RAM 215 self.bus.main_write_bios_prg_ram::<WORD>(address, value); 216 } else { 217 // Word RAM 218 if WORD { 219 self.bus.main_write_word_ram(address, value.msb()); 220 self.bus.main_write_word_ram(address + 1, value.lsb()); 221 } else { 222 self.bus.main_write_word_ram(address, value as u8); 223 } 224 } 225 } 226 227 #[must_use] 228 #[inline] 229 pub fn read_word_for_dma(&mut self, address: u32, open_bus: &mut u16) -> u16 { 230 if address & 0x200000 == 0 { 231 *open_bus = self.main_read_memory::<true>(address); 232 *open_bus 233 } else { 234 // Word RAM reads are delayed 235 mem::replace(open_bus, self.main_read_memory::<true>(address)) 236 } 237 } 238 239 #[must_use] 240 #[inline] 241 pub fn read_ram_cartridge<const WORD: bool>(&self, mut address: u32) -> u16 { 242 if WORD { 243 address |= 1; 244 } 245 self.bus.read_ram_cartridge(address).into() 246 } 247 248 #[inline] 249 pub fn write_ram_cartridge<const WORD: bool>(&mut self, mut address: u32, value: u16) { 250 if WORD { 251 address |= 1; 252 } 253 self.bus.write_ram_cartridge(address, value as u8); 254 } 255 256 #[must_use] 257 #[inline] 258 pub fn main_read_register<const WORD: bool>(&mut self, address: u32) -> u16 { 259 self.bus.main_read_register::<WORD>(address) 260 } 261 262 #[inline] 263 pub fn main_write_register<const WORD: bool>(&mut self, address: u32, value: u16) { 264 self.bus.main_write_register::<WORD>(address, value); 265 } 266 267 pub fn reload_config(&mut self, config: &GenesisEmulatorConfig) { 268 self.sub_cpu_divider = config.sega_cd.sub_cpu_divider.get(); 269 self.bus.reload_config(config); 270 271 self.config = config.clone(); 272 } 273 274 pub fn reset(&mut self) { 275 self.bus.reset(); 276 } 277 278 #[must_use] 279 pub fn disc_title(&self) -> Option<String> { 280 self.disc_title.clone() 281 } 282 283 #[must_use] 284 pub fn region(&self) -> GenesisRegion { 285 self.bus.region() 286 } 287 288 #[must_use] 289 pub fn has_six_button_incompatible_game(&self) -> bool { 290 self.six_button_incompatible_game 291 } 292 293 pub fn take_backup_ram_dirty(&mut self) -> bool { 294 self.bus.take_backup_ram_dirty() 295 } 296 297 #[must_use] 298 pub fn backup_ram(&self) -> &[u8] { 299 self.bus.backup_ram() 300 } 301 302 #[must_use] 303 pub fn ram_cartridge(&self) -> &[u8] { 304 self.bus.ram_cartridge() 305 } 306 307 #[must_use] 308 pub fn take_bios_and_disc(self) -> (Vec<u16>, Option<CdRom>) { 309 self.bus.take_bios_and_disc() 310 } 311 312 pub fn take_bios_and_disc_from(&mut self, other: &mut Self) { 313 self.bus.take_bios_and_disc_from(&mut other.bus); 314 } 315 316 /// # Errors 317 /// 318 /// Propagates any CD-ROM read errors. 319 pub fn change_disc(&mut self, disc: CdRom) -> SegaCdLoadResult<()> { 320 self.bus.change_disc(disc); 321 self.disc_title = self.bus.disc_title()?; 322 323 Ok(()) 324 } 325 326 pub fn remove_disc(&mut self) { 327 self.bus.remove_disc(); 328 self.disc_title = None; 329 } 330} 331 332/// Attempt to parse a console region out of the CD-ROM's data track. 333/// 334/// Returns None if unable to confidently determine region. 335/// 336/// # Errors 337/// 338/// Propagates any errors encountered while reading the CD-ROM files from disk. 339pub fn parse_disc_region(disc: &mut CdRom) -> SegaCdLoadResult<GenesisRegion> { 340 // ROM header is always located at track 1 sector 0 341 let mut rom_header = vec![0; cdrom::BYTES_PER_SECTOR as usize]; 342 disc.read_sector(1, CdTime::SECTOR_0_START, &mut rom_header)?; 343 344 // Sega CD ROM header starts at $010 because the first 16 bytes are sync + CD-ROM data track header 345 let region = GenesisRegion::from_rom(&rom_header[0x010..]).unwrap_or_else(|| { 346 log::warn!("Unable to determine region from disc header; defaulting to US"); 347 GenesisRegion::Americas 348 }); 349 350 let serial_number = &rom_header[0x190..0x1A0]; 351 352 // The Smurfs (EU) has a US header but only works properly with PAL timings 353 if region == GenesisRegion::Americas && serial_number == b"GM T-151015-00 " { 354 return Ok(GenesisRegion::Europe); 355 } 356 357 // Hack to fix Snatcher (US/EU), which incorrectly reports its region as J in the header 358 if region == GenesisRegion::Japan && serial_number == b"GM T-95035 -00 " { 359 let console_name = &rom_header[0x110..0x120]; 360 if console_name == "SEGA GENESIS ".as_bytes() { 361 return Ok(GenesisRegion::Americas); 362 } else if console_name == "SEGA MEGA DRIVE ".as_bytes() { 363 return Ok(GenesisRegion::Europe); 364 } 365 // Any other console name is unexpected, leave region as-is 366 } 367 368 Ok(region) 369}