1//! Genesis public interface and main loop 2 3pub mod debug; 4 5use crate::api::debug::GenesisDebugger; 6use crate::audio::GenesisAudioResampler; 7use crate::bus::GenesisBus; 8use crate::bus::debug::{Debug68000Bus, DebugZ80Bus}; 9use bincode::{Decode, Encode}; 10use cdrom::reader::CdRom; 11use genesis_components::cartridge::Cartridge; 12use genesis_components::vdp::{Vdp, VdpTickEffect}; 13use genesis_config::{GenesisButton, GenesisEmulatorConfig, GenesisInputs, GenesisRegion}; 14use jgenesis_common::frontend::{ 15 AudioOutput, EmulatorTrait, InputPoller, Modal, PartialClone, RenderFrameOptions, Renderer, 16 SaveWriter, TickEffect, TickResult, TimingMode, 17}; 18use m68000_emu::M68000; 19use s32x_core::api::Sega32X; 20use segacd_core::api::{SegaCd, SegaCdLoadError, SegaCdLoadResult}; 21use std::fmt::{Debug, Display, Formatter}; 22use thiserror::Error; 23use z80_emu::Z80; 24 25const SRAM_EXTENSION: &str = "sav"; 26const BACKUP_RAM_EXTENSION: &str = "bram"; 27const RAM_CARTRIDGE_EXTENSION: &str = "ramc"; 28 29#[derive(Debug, Error)] 30pub enum GenesisError<RErr, AErr, SErr> { 31 #[error("Sega CD disc error: {0}")] 32 SegaCd(#[from] SegaCdLoadError), 33 #[error("Rendering error: {0}")] 34 Render(RErr), 35 #[error("Audio output error: {0}")] 36 Audio(AErr), 37 #[error("Save write error: {0}")] 38 Save(SErr), 39} 40 41pub type GenesisResult<RErr, AErr, SErr> = Result<TickEffect, GenesisError<RErr, AErr, SErr>>; 42 43#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 44pub enum GenesisHardware { 45 Standalone, 46 SegaCd, 47 Sega32X, 48 SegaCd32X, 49} 50 51impl Display for GenesisHardware { 52 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 53 match self { 54 Self::Standalone => write!(f, "Genesis"), 55 Self::SegaCd => write!(f, "Sega CD"), 56 Self::Sega32X => write!(f, "32X"), 57 Self::SegaCd32X => write!(f, "Sega CD 32X"), 58 } 59 } 60} 61 62impl GenesisHardware { 63 #[must_use] 64 #[inline] 65 pub fn has_sega_cd(self) -> bool { 66 matches!(self, Self::SegaCd | Self::SegaCd32X) 67 } 68 69 #[must_use] 70 #[inline] 71 pub fn has_32x(self) -> bool { 72 matches!(self, Self::Sega32X | Self::SegaCd32X) 73 } 74} 75 76#[derive(Debug, Encode, Decode, PartialClone)] 77pub struct GenesisEmulator { 78 m68k: M68000, 79 z80: Z80, 80 #[partial_clone(partial)] 81 bus: GenesisBus, 82 timing_mode: TimingMode, 83 audio_resampler: GenesisAudioResampler, 84 hardware: GenesisHardware, 85 config: GenesisEmulatorConfig, 86} 87 88impl GenesisEmulator { 89 /// # Errors 90 /// 91 /// Propagates any errors encountered while initializing Sega CD (if Sega CD is enabled). 92 pub fn create<S: SaveWriter>( 93 hardware: GenesisHardware, 94 mut cartridge_rom: Option<Vec<u8>>, 95 sega_cd_bios_rom: Option<Vec<u8>>, 96 mut disc: Option<CdRom>, 97 config: GenesisEmulatorConfig, 98 save_writer: &mut S, 99 ) -> SegaCdLoadResult<Self> { 100 log::info!("Running with hardware {hardware}"); 101 102 if cartridge_rom.as_ref().is_some_and(Vec::is_empty) { 103 // Later code assumes cartridge ROM is non-empty if Some 104 cartridge_rom = None; 105 } 106 107 let sega_cd_present = hardware.has_sega_cd(); 108 let s32x_present = hardware.has_32x(); 109 110 let initial_ram = save_writer.load_bytes(SRAM_EXTENSION).ok(); 111 112 let mut cartridge = cartridge_rom 113 .map(|rom| Cartridge::new(rom, initial_ram, config.forced_region, &config.cheat_codes)); 114 115 let region = cartridge 116 .as_ref() 117 .map(Cartridge::region) 118 .or_else(|| { 119 let disc = disc.as_mut()?; 120 segacd_core::api::parse_disc_region(disc).ok() 121 }) 122 .unwrap_or(GenesisRegion::Americas); 123 124 let timing_mode = config.forced_timing_mode.unwrap_or(match region { 125 GenesisRegion::Europe => TimingMode::Pal, 126 GenesisRegion::Americas | GenesisRegion::Japan => TimingMode::Ntsc, 127 }); 128 129 log::info!("Using timing / display mode {timing_mode}"); 130 131 let sega_cd = if sega_cd_present { 132 let Some(bios_rom) = sega_cd_bios_rom else { return Err(SegaCdLoadError::MissingBios) }; 133 134 let backup_ram_extension = match &cartridge { 135 Some(_) => BACKUP_RAM_EXTENSION, 136 None => SRAM_EXTENSION, 137 }; 138 let initial_backup_ram = save_writer.load_bytes(backup_ram_extension).ok(); 139 140 let initial_ram_cartridge = save_writer.load_bytes(RAM_CARTRIDGE_EXTENSION).ok(); 141 142 let sega_cd = SegaCd::new( 143 bios_rom, 144 disc, 145 timing_mode, 146 initial_backup_ram, 147 initial_ram_cartridge, 148 &config, 149 )?; 150 Some(sega_cd) 151 } else { 152 None 153 }; 154 155 // If 32X is present, let 32X own the cartridge instead of GenesisBus 156 let sega_32x = s32x_present.then(|| Sega32X::new(timing_mode, cartridge.take(), &config)); 157 158 let bus = GenesisBus::new(timing_mode, cartridge, sega_cd, sega_32x, &config); 159 160 // The Genesis does not allow TAS to lock the bus, so don't allow TAS writes 161 let m68k = M68000::builder().allow_tas_writes(false).name("Main".into()).build(); 162 let z80 = Z80::new(); 163 164 let audio_resampler = GenesisAudioResampler::new(hardware, timing_mode, &config); 165 166 let mut emulator = Self { m68k, z80, bus, timing_mode, audio_resampler, hardware, config }; 167 168 // Reset CPU so that execution will start from the right place 169 emulator.bus.m68k_reset = true; 170 emulator.m68k.execute_instruction(&mut emulator.bus); 171 emulator.bus.m68k_reset = false; 172 173 Ok(emulator) 174 } 175 176 #[must_use] 177 pub fn game_title(&mut self) -> Option<String> { 178 self.bus.game_title() 179 } 180 181 #[inline] 182 #[must_use] 183 pub fn has_sram(&self) -> bool { 184 self.bus.has_persistent_ram() 185 } 186 187 #[inline] 188 #[must_use] 189 pub fn timing_mode(&self) -> TimingMode { 190 self.timing_mode 191 } 192 193 #[inline] 194 #[must_use] 195 pub fn hardware(&self) -> GenesisHardware { 196 self.hardware 197 } 198 199 pub fn remove_disc(&mut self) { 200 if let Some(sega_cd) = &mut self.bus.sega_cd { 201 sega_cd.remove_disc(); 202 } 203 } 204 205 /// # Errors 206 /// 207 /// Propagates any CD-ROM read errors. 208 pub fn change_disc(&mut self, disc: CdRom) -> SegaCdLoadResult<()> { 209 if let Some(sega_cd) = &mut self.bus.sega_cd { 210 sega_cd.change_disc(disc)?; 211 } 212 213 Ok(()) 214 } 215 216 fn render_frame<R: Renderer>(&mut self, renderer: &mut R) -> Result<(), R::Err> { 217 match &mut self.bus.sega_32x { 218 Some(sega_32x) if sega_32x.adapter_enabled() => { 219 sega_32x.render_frame(&self.bus.vdp, renderer) 220 } 221 _ => render_frame(self.timing_mode, &self.bus.vdp, &self.config, renderer), 222 } 223 } 224 225 #[inline] 226 fn tick_inner<const DEBUG: bool, R, A, I, S>( 227 &mut self, 228 renderer: &mut R, 229 audio_output: &mut A, 230 input_poller: &mut I, 231 save_writer: &mut S, 232 mut debugger: Option<&mut GenesisDebugger>, 233 ) -> TickResult<GenesisError<R::Err, A::Err, S::Err>> 234 where 235 R: Renderer, 236 A: AudioOutput, 237 I: InputPoller<GenesisInputs>, 238 S: SaveWriter, 239 { 240 self.bus.input.set_inputs(*input_poller.poll()); 241 self.bus.m68k_opcode = self.m68k.next_opcode(); 242 243 let m68k_pc = self.m68k.pc(); 244 let m68k_wait = self.bus.cycles.m68k_wait_cpu_cycles != 0; 245 let m68k_cycles = if m68k_wait { 246 self.bus.cycles.take_m68k_wait_cpu_cycles() 247 } else if DEBUG && let Some(debugger) = &mut debugger { 248 let mut debug_bus = Debug68000Bus::new(&mut self.bus, &mut self.z80, debugger); 249 self.m68k.execute_instruction(&mut debug_bus) 250 } else { 251 self.m68k.execute_instruction(&mut self.bus) 252 }; 253 254 let elapsed_mclk_cycles = self.bus.cycles.record_68k_instruction( 255 m68k_pc, 256 m68k_cycles, 257 m68k_wait, 258 self.bus.vdp.should_halt_cpu(), 259 ); 260 261 while self.bus.cycles.should_tick_z80() { 262 if !self.bus.cycles.z80_halt { 263 if DEBUG && let Some(debugger) = &mut debugger { 264 let mut debug_bus = DebugZ80Bus::new(&mut self.bus, &mut self.m68k, debugger); 265 self.z80.tick(&mut debug_bus); 266 } else { 267 self.z80.tick(&mut self.bus); 268 } 269 } 270 self.bus.cycles.z80_cycle(); 271 } 272 273 let vdp_tick_effect = self.bus.tick_components::<DEBUG>( 274 m68k_cycles, 275 elapsed_mclk_cycles, 276 m68k_wait, 277 &mut self.audio_resampler, 278 debugger.as_mut().map(|debugger| debugger.with_cpus(&mut self.m68k, &mut self.z80)), 279 )?; 280 281 self.audio_resampler.output_samples(audio_output).map_err(GenesisError::Audio)?; 282 283 if vdp_tick_effect == VdpTickEffect::FrameComplete { 284 if let Some(sega_32x) = &mut self.bus.sega_32x 285 && sega_32x.adapter_enabled() 286 { 287 sega_32x.composite_frame(&mut self.bus.vdp); 288 } 289 290 self.render_frame(renderer).map_err(GenesisError::Render)?; 291 292 self.persist_save_files(save_writer).map_err(GenesisError::Save)?; 293 } 294 295 Ok(match vdp_tick_effect { 296 VdpTickEffect::FrameComplete => TickEffect::FrameRendered, 297 VdpTickEffect::None => TickEffect::None, 298 }) 299 } 300 301 fn persist_save_files<S: SaveWriter>(&mut self, save_writer: &mut S) -> Result<(), S::Err> { 302 if !self.bus.has_persistent_ram() || !self.bus.get_and_clear_persistent_ram_dirty() { 303 return Ok(()); 304 } 305 306 if let Some(ram) = self.bus.cartridge().map(Cartridge::external_ram) 307 && !ram.is_empty() 308 { 309 save_writer.persist_bytes(SRAM_EXTENSION, ram)?; 310 } 311 312 if let Some(sega_cd) = &self.bus.sega_cd { 313 // Backup RAM is the primary save location for Sega CD games, so prefer .sav over .bram 314 // if there's no game cartridge 315 let backup_ram_extension = match self.bus.cartridge() { 316 Some(_) => BACKUP_RAM_EXTENSION, 317 None => SRAM_EXTENSION, 318 }; 319 save_writer.persist_bytes(backup_ram_extension, sega_cd.backup_ram())?; 320 321 // RAM cartridge is only present when there's no game cartridge (there's only 1 slot) 322 if self.bus.cartridge().is_none() { 323 save_writer.persist_bytes(RAM_CARTRIDGE_EXTENSION, sega_cd.ram_cartridge())?; 324 } 325 } 326 327 Ok(()) 328 } 329 330 /// Similar to [`<Self as EmulatorTrait>::tick`] but runs with debugger hooks enabled. 331 /// 332 /// # Errors 333 /// 334 /// This method will propagate any errors encountered while rendering frames or pushing audio 335 /// samples. 336 #[inline] 337 pub fn debug_tick<R, A, I, S>( 338 &mut self, 339 renderer: &mut R, 340 audio_output: &mut A, 341 input_poller: &mut I, 342 save_writer: &mut S, 343 debugger: &mut GenesisDebugger, 344 ) -> GenesisResult<R::Err, A::Err, S::Err> 345 where 346 R: Renderer, 347 A: AudioOutput, 348 I: InputPoller<GenesisInputs>, 349 S: SaveWriter, 350 { 351 self.tick_inner::<true, _, _, _, _>( 352 renderer, 353 audio_output, 354 input_poller, 355 save_writer, 356 Some(debugger), 357 ) 358 } 359} 360 361fn render_frame<R: Renderer>( 362 timing_mode: TimingMode, 363 vdp: &Vdp, 364 config: &GenesisEmulatorConfig, 365 renderer: &mut R, 366) -> Result<(), R::Err> { 367 let frame_size = vdp.frame_size(); 368 let pixel_aspect_ratio = config.aspect_ratio.to_pixel_aspect_ratio( 369 timing_mode, 370 frame_size, 371 config.to_gen_par_params(), 372 ); 373 let target_fps = genesis_components::target_framerate(vdp, timing_mode); 374 375 renderer.render_frame( 376 vdp.frame_buffer(), 377 frame_size, 378 target_fps, 379 RenderFrameOptions { 380 pixel_aspect_ratio, 381 composite_params: Some(vdp.composite_params()), 382 ..RenderFrameOptions::default() 383 }, 384 ) 385} 386 387impl EmulatorTrait for GenesisEmulator { 388 type Button = GenesisButton; 389 type Inputs = GenesisInputs; 390 type Config = GenesisEmulatorConfig; 391 type SaveState = Self; 392 393 type Err< 394 RErr: Debug + Display + Send + Sync + 'static, 395 AErr: Debug + Display + Send + Sync + 'static, 396 SErr: Debug + Display + Send + Sync + 'static, 397 > = GenesisError<RErr, AErr, SErr>; 398 399 /// Execute one 68000 CPU instruction and run the rest of the components for the appropriate 400 /// number of cycles. 401 /// 402 /// # Errors 403 /// 404 /// This method will propagate any errors encountered while rendering frames or pushing audio 405 /// samples. 406 #[inline] 407 fn tick<R, A, I, S>( 408 &mut self, 409 renderer: &mut R, 410 audio_output: &mut A, 411 input_poller: &mut I, 412 save_writer: &mut S, 413 ) -> GenesisResult<R::Err, A::Err, S::Err> 414 where 415 R: Renderer, 416 A: AudioOutput, 417 I: InputPoller<Self::Inputs>, 418 S: SaveWriter, 419 { 420 self.tick_inner::<false, _, _, _, _>( 421 renderer, 422 audio_output, 423 input_poller, 424 save_writer, 425 None, 426 ) 427 } 428 429 fn force_render<R>(&mut self, renderer: &mut R) -> Result<(), R::Err> 430 where 431 R: Renderer, 432 { 433 self.render_frame(renderer) 434 } 435 436 fn reload_config(&mut self, config: &Self::Config) { 437 self.bus.reload_config(config); 438 self.audio_resampler.reload_config(self.timing_mode, config); 439 440 self.config = config.clone(); 441 } 442 443 fn soft_reset(&mut self) { 444 log::info!("Soft resetting console"); 445 446 self.bus.reset(); 447 448 self.bus.m68k_reset = true; 449 self.m68k.execute_instruction(&mut self.bus); 450 self.bus.m68k_reset = false; 451 } 452 453 fn hard_reset<S: SaveWriter>(&mut self, save_writer: &mut S) { 454 log::info!("Hard resetting console"); 455 456 let cartridge_rom = self.bus.cartridge_mut().map(Cartridge::take_rom); 457 458 let (sega_cd_bios, disc) = match self.bus.sega_cd.take().map(SegaCd::take_bios_and_disc) { 459 Some((bios_rom, disc)) => (Some(bios_rom), disc), 460 None => (None, None), 461 }; 462 463 let sega_cd_bios = 464 sega_cd_bios.map(|bios| bios.into_iter().flat_map(u16::to_be_bytes).collect()); 465 466 *self = GenesisEmulator::create( 467 self.hardware, 468 cartridge_rom, 469 sega_cd_bios, 470 disc, 471 self.config.clone(), 472 save_writer, 473 ) 474 .expect("Hard reset should not error"); 475 } 476 477 fn load_state(&mut self, mut state: Self::SaveState) { 478 if let Some(state_cartridge) = state.bus.cartridge_mut() 479 && let Some(self_cartridge) = self.bus.cartridge_mut() 480 { 481 state_cartridge.take_rom_from(self_cartridge); 482 } 483 484 if let Some(state_sega_cd) = &mut state.bus.sega_cd 485 && let Some(self_sega_cd) = &mut self.bus.sega_cd 486 { 487 state_sega_cd.take_bios_and_disc_from(self_sega_cd); 488 } 489 490 *self = state; 491 } 492 493 fn to_save_state(&self) -> Self::SaveState { 494 self.partial_clone() 495 } 496 497 fn target_fps(&self) -> f64 { 498 genesis_components::target_framerate(&self.bus.vdp, self.timing_mode) 499 } 500 501 fn update_audio_output_frequency(&mut self, output_frequency: u64) { 502 self.audio_resampler.update_output_frequency(output_frequency); 503 } 504 505 fn startup_modals(&self) -> Vec<Modal> { 506 let mut modals = Vec::new(); 507 508 if self.config.remove_sprite_limits 509 && (self 510 .bus 511 .cartridge 512 .as_ref() 513 .is_some_and(|cartridge| cartridge.metadata().sprite_limit_compatibility_issues) 514 || self.bus.sega_cd.as_ref().is_some_and(SegaCd::has_six_button_incompatible_game)) 515 { 516 modals.push(Modal { 517 id: None, 518 text: genesis_components::SPRITE_LIMITS_MODAL_MESSAGE.into(), 519 }); 520 } 521 522 modals 523 } 524}