Genesis public interface and main loop
3pub mod debug;
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 {
Errors
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 }
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 }
Errors
Propagates any CD-ROM read errors.
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 }
Similar to [<Self as EmulatorTrait>::tick] but runs with debugger hooks enabled.
Errors
This method will propagate any errors encountered while rendering frames or pushing audio 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}
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>;
Execute one 68000 CPU instruction and run the rest of the components for the appropriate number of cycles.
Errors
This method will propagate any errors encountered while rendering frames or pushing audio 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 }
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}