Sega Master System / Game Gear public interface and main loop
3use crate::audio::{AudioResampler, TimingModeExt}; 4use crate::bus::Bus; 5use crate::input::InputState; 6use crate::memory::Memory; 7use crate::vdp::{Vdp, VdpBuffer, VdpTickEffect, ViewportSize}; 8use crate::{VdpVersion, vdp}; 9use bincode::{Decode, Encode}; 10use jgenesis_common::boxedarray::BoxedColorArray; 11use jgenesis_common::cheats::ByteCheatCodeU16Address; 12use jgenesis_common::frontend::{ 13 AudioOutput, Color, CompositeParams, EmulatorConfigTrait, EmulatorTrait, FrameSize, 14 InputPoller, PartialClone, RenderFrameOptions, Renderer, SamplesPerColorCycle, SaveWriter, 15 TickEffect, TimingMode, 16}; 17use jgenesis_proc_macros::ConfigDisplay; 18use smsgg_config::{ 19 GgAspectRatio, SmsAspectRatio, SmsGgButton, SmsGgInputs, SmsGgRegion, SmsModel, 20}; 21use std::fmt::{Debug, Display}; 22use std::num::NonZeroU32; 23use thiserror::Error; 24use ti_sn76489::{Sn76489, Sn76489TickEffect, Sn76489Version}; 25use ym_opll::Ym2413; 26use z80_emu::{InterruptMode, Z80};
28#[derive(Debug, Error)] 29pub enum SmsGgError<RErr, AErr, SErr> { 30 #[error("Rendering error: {0}")] 31 Render(RErr), 32 #[error("Audio output error: {0}")] 33 Audio(AErr), 34 #[error("Save write error: {0}")] 35 SaveWrite(SErr), 36} 37 38pub type SmsGgResult<RErr, AErr, SErr> = Result<TickEffect, SmsGgError<RErr, AErr, SErr>>; 39 40#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 41pub enum SmsGgHardware { 42 MasterSystem, 43 GameGear, 44 Sg1000, 45} 46 47#[derive(Debug, Clone, Encode, Decode, ConfigDisplay)] 48pub struct SmsGgEmulatorConfig { 49 pub sms_timing_mode: TimingMode, 50 pub sms_model: SmsModel, 51 pub forced_psg_version: Option<Sn76489Version>, 52 pub sms_aspect_ratio: SmsAspectRatio, 53 pub gg_aspect_ratio: GgAspectRatio, 54 pub remove_sprite_limit: bool, 55 pub forced_region: Option<SmsGgRegion>, 56 pub sms_crop_vertical_border: bool, 57 pub sms_crop_left_border: bool, 58 pub gg_frame_blending: bool, 59 pub gg_use_sms_resolution: bool, 60 pub fm_sound_unit_enabled: bool, 61 pub z80_divider: NonZeroU32, 62 pub allow_opposing_joypad_directions: bool, 63 #[cfg_display(skip)] 64 pub cheat_codes: Vec<ByteCheatCodeU16Address>, 65} 66 67impl EmulatorConfigTrait for SmsGgEmulatorConfig { 68 fn with_overclocking_disabled(&self) -> Self { 69 Self { z80_divider: NonZeroU32::new(crate::NATIVE_Z80_DIVIDER).unwrap(), ..self.clone() } 70 } 71} 72 73impl SmsGgEmulatorConfig { 74 pub(crate) fn region(&self, memory: &Memory) -> SmsGgRegion { 75 self.forced_region.unwrap_or_else(|| memory.guess_cartridge_region()) 76 } 77 78 pub(crate) fn render_options(&self, hardware: SmsGgHardware) -> RenderFrameOptions { 79 match hardware { 80 SmsGgHardware::MasterSystem | SmsGgHardware::Sg1000 => self.sms_render_options(), 81 SmsGgHardware::GameGear => self.gg_render_options(), 82 } 83 } 84 85 pub(crate) fn sms_render_options(&self) -> RenderFrameOptions { 86 RenderFrameOptions { 87 pixel_aspect_ratio: self.sms_aspect_ratio.to_pixel_aspect_ratio(), 88 composite_params: Some(CompositeParams { 89 upscale_factor: 10, 90 samples_per_color_cycle: SamplesPerColorCycle::Fifteen, 91 }), 92 ..RenderFrameOptions::default() 93 } 94 } 95 96 pub(crate) fn gg_render_options(&self) -> RenderFrameOptions { 97 RenderFrameOptions { 98 pixel_aspect_ratio: self.gg_aspect_ratio.to_pixel_aspect_ratio(), 99 frame_blending: self.gg_frame_blending, 100 ..RenderFrameOptions::default() 101 } 102 } 103} 104 105#[derive(Debug, Clone, Encode, Decode, PartialClone)] 106pub struct SmsGgEmulator { 107 #[partial_clone(partial)] 108 memory: Memory, 109 z80: Z80, 110 vdp: Vdp, 111 vdp_version: VdpVersion, 112 psg: Sn76489, 113 ym2413: Option<Ym2413>, 114 input: InputState, 115 audio_resampler: AudioResampler, 116 frame_buffer: BoxedColorArray<{ vdp::FRAME_BUFFER_LEN }>, 117 config: SmsGgEmulatorConfig, 118 vdp_mclk_counter: u32, 119 psg_mclk_counter: u32, 120 frame_count: u64, 121 reset_frames_remaining: u32, 122} 123 124const VDP_DIVIDER: u32 = 10; 125const PSG_DIVIDER: u32 = 15; 126 127const YM2413_CLOCK_INTERVAL: u8 = 72; 128 129impl SmsGgEmulator { 130 #[must_use] 131 pub fn create<S: SaveWriter>( 132 rom: Option<Vec<u8>>, 133 bios_rom: Option<Vec<u8>>, 134 hardware: SmsGgHardware, 135 config: SmsGgEmulatorConfig, 136 save_writer: &mut S, 137 ) -> Self { 138 let cartridge_ram = save_writer.load_bytes("sav").ok(); 139 140 let vdp_version = determine_vdp_version(hardware, &config); 141 let psg_version = determine_psg_version(hardware, &config); 142 143 log::info!("VDP version: {vdp_version:?}"); 144 log::info!("PSG version: {psg_version:?}"); 145 146 let rom = match rom { 147 Some(rom) if !rom.is_empty() => rom, 148 _ => vec![0xFF; 0x8000], 149 }; 150 let memory = Memory::new(rom, bios_rom, cartridge_ram, hardware, &config); 151 let vdp = Vdp::new(vdp_version, &config); 152 let psg = Sn76489::new(psg_version); 153 let input = InputState::new(config.region(&memory), &config); 154 155 log::info!("Region in cartridge header: {:?}", memory.guess_cartridge_region()); 156 157 let mut z80 = Z80::new(); 158 init_z80(&mut z80); 159 160 let ym2413 = 161 config.fm_sound_unit_enabled.then(|| ym_opll::new_ym2413(YM2413_CLOCK_INTERVAL)); 162 163 let timing_mode = vdp.timing_mode(); 164 Self { 165 memory, 166 z80, 167 vdp, 168 vdp_version, 169 psg, 170 ym2413, 171 input, 172 audio_resampler: AudioResampler::new(timing_mode), 173 frame_buffer: BoxedColorArray::new(), 174 config, 175 vdp_mclk_counter: 0, 176 psg_mclk_counter: 0, 177 frame_count: 0, 178 reset_frames_remaining: 0, 179 } 180 } 181 182 #[must_use] 183 pub fn hardware(&self) -> SmsGgHardware { 184 self.vdp_version.hardware() 185 } 186 187 #[must_use] 188 pub fn vdp_version(&self) -> VdpVersion { 189 self.vdp_version 190 } 191 192 #[inline] 193 #[must_use] 194 pub fn has_sram(&self) -> bool { 195 self.memory.cartridge_has_battery() 196 } 197 198 fn render_frame<R: Renderer>(&mut self, renderer: &mut R) -> Result<(), R::Err> { 199 populate_frame_buffer( 200 self.vdp.frame_buffer(), 201 self.vdp.viewport(), 202 self.vdp_version, 203 self.config.sms_crop_vertical_border, 204 self.config.sms_crop_left_border, 205 self.frame_buffer.as_mut_slice(), 206 ); 207 208 let viewport = self.vdp.viewport(); 209 let frame_width = if self.config.sms_crop_left_border { 210 viewport.width_without_border().into() 211 } else { 212 viewport.width.into() 213 }; 214 let frame_height = if self.config.sms_crop_vertical_border { 215 viewport.height_without_border().into() 216 } else { 217 viewport.height.into() 218 }; 219 220 let frame_size = FrameSize { width: frame_width, height: frame_height }; 221 renderer.render_frame( 222 self.frame_buffer.as_slice(), 223 frame_size, 224 self.target_fps(), 225 self.config.render_options(self.vdp_version.hardware()), 226 ) 227 } 228 229 pub fn copy_cram(&self, out: &mut [Color]) { 230 self.vdp.copy_cram(out); 231 } 232 233 pub fn copy_vram(&self, out: &mut [Color], palette: u8, row_len: usize) { 234 self.vdp.copy_vram(out, palette, row_len); 235 } 236 237 pub fn dump_vdp_registers(&self, callback: impl FnMut(u32, &[(&str, &str)])) { 238 self.vdp.dump_registers(callback); 239 } 240} 241 242fn init_z80(z80: &mut Z80) { 243 z80.set_pc(0x0000); 244 z80.set_sp(0xDFFF); 245 z80.set_interrupt_mode(InterruptMode::Mode1); 246} 247 248fn determine_vdp_version(hardware: SmsGgHardware, config: &SmsGgEmulatorConfig) -> VdpVersion { 249 match (hardware, config.sms_timing_mode, config.sms_model) { 250 (SmsGgHardware::MasterSystem, TimingMode::Ntsc, SmsModel::Sms1) => { 251 VdpVersion::NtscMasterSystem1 252 } 253 (SmsGgHardware::MasterSystem, TimingMode::Ntsc, SmsModel::Sms2) => { 254 VdpVersion::NtscMasterSystem2 255 } 256 (SmsGgHardware::MasterSystem, TimingMode::Pal, SmsModel::Sms1) => { 257 VdpVersion::PalMasterSystem1 258 } 259 (SmsGgHardware::MasterSystem, TimingMode::Pal, SmsModel::Sms2) => { 260 VdpVersion::PalMasterSystem2 261 } 262 (SmsGgHardware::GameGear, _, _) => VdpVersion::GameGear, 263 (SmsGgHardware::Sg1000, TimingMode::Ntsc, _) => VdpVersion::NtscSg1000, 264 (SmsGgHardware::Sg1000, TimingMode::Pal, _) => VdpVersion::PalSg1000, 265 } 266} 267 268fn determine_psg_version(hardware: SmsGgHardware, config: &SmsGgEmulatorConfig) -> Sn76489Version { 269 config.forced_psg_version.unwrap_or(match hardware { 270 SmsGgHardware::MasterSystem => Sn76489Version::MasterSystem2, 271 SmsGgHardware::GameGear => Sn76489Version::Standard, 272 SmsGgHardware::Sg1000 => Sn76489Version::Discrete, 273 }) 274} 275 276impl EmulatorTrait for SmsGgEmulator { 277 type Button = SmsGgButton; 278 type Inputs = SmsGgInputs; 279 type Config = SmsGgEmulatorConfig; 280 type SaveState = Self; 281 282 type Err< 283 RErr: Debug + Display + Send + Sync + 'static, 284 AErr: Debug + Display + Send + Sync + 'static, 285 SErr: Debug + Display + Send + Sync + 'static, 286 > = SmsGgError<RErr, AErr, SErr>;
Execute a single CPU instruction and run the rest of the components for the corresponding number of cycles.
Errors
This method will propagate any errors encountered while rendering frames, pushing audio samples, or persisting cartridge SRAM.
295 #[inline] 296 fn tick<R, A, I, S>( 297 &mut self, 298 renderer: &mut R, 299 audio_output: &mut A, 300 input_poller: &mut I, 301 save_writer: &mut S, 302 ) -> SmsGgResult<R::Err, A::Err, S::Err> 303 where 304 R: Renderer, 305 A: AudioOutput, 306 I: InputPoller<Self::Inputs>, 307 S: SaveWriter, 308 { 309 self.input.set_inputs(*input_poller.poll()); 310 311 let z80_t_cycles = self.z80.execute_instruction(&mut Bus::new( 312 self.vdp_version, 313 &mut self.memory, 314 &mut self.vdp, 315 &mut self.psg, 316 self.ym2413.as_mut(), 317 &mut self.input, 318 )); 319 320 let mclk_cycles = z80_t_cycles * self.config.z80_divider.get(); 321 self.vdp_mclk_counter += mclk_cycles; 322 self.psg_mclk_counter += mclk_cycles; 323 324 while self.psg_mclk_counter >= PSG_DIVIDER { 325 self.psg_mclk_counter -= PSG_DIVIDER; 326 327 if let Some(ym2413) = &mut self.ym2413 { 328 ym2413.tick(); 329 } 330 if self.psg.tick() == Sn76489TickEffect::Clocked { 331 let (psg_sample_l, psg_sample_r) = 332 if self.memory.psg_enabled() { self.psg.sample() } else { (0.0, 0.0) }; 333 let ym_sample = if self.memory.fm_enabled() { 334 self.ym2413.as_ref().map_or(0.0, Ym2413::sample) 335 } else { 336 0.0 337 }; 338 339 let sample_l = psg_sample_l + ym_sample; 340 let sample_r = psg_sample_r + ym_sample; 341 self.audio_resampler.collect_sample(sample_l, sample_r); 342 } 343 } 344 345 self.audio_resampler.output_samples(audio_output).map_err(SmsGgError::Audio)?; 346 347 let mut frame_rendered = false; 348 while self.vdp_mclk_counter >= VDP_DIVIDER { 349 self.vdp_mclk_counter -= VDP_DIVIDER; 350 351 if self.vdp.tick() == VdpTickEffect::FrameComplete { 352 self.render_frame(renderer).map_err(SmsGgError::Render)?; 353 frame_rendered = true; 354 355 self.input.set_reset(self.reset_frames_remaining != 0); 356 self.reset_frames_remaining = self.reset_frames_remaining.saturating_sub(1); 357 358 self.frame_count += 1; 359 if self.frame_count.is_multiple_of(60) 360 && self.memory.cartridge_has_battery() 361 && self.memory.cartridge_ram_dirty() 362 { 363 self.memory.clear_cartridge_ram_dirty(); 364 save_writer 365 .persist_bytes("sav", self.memory.cartridge_ram()) 366 .map_err(SmsGgError::SaveWrite)?; 367 } 368 } 369 } 370 371 Ok(if frame_rendered { TickEffect::FrameRendered } else { TickEffect::None }) 372 }
374 fn force_render<R>(&mut self, renderer: &mut R) -> Result<(), R::Err> 375 where 376 R: Renderer, 377 { 378 self.render_frame(renderer) 379 } 380 381 fn reload_config(&mut self, config: &Self::Config) { 382 self.config = config.clone(); 383 384 let hardware = self.hardware(); 385 self.vdp_version = determine_vdp_version(hardware, config); 386 self.vdp.update_config(self.vdp_version, config); 387 388 self.psg.set_version(determine_psg_version(hardware, config)); 389 390 self.memory.update_config(config); 391 392 self.input.reload_config(config.region(&self.memory), config); 393 self.audio_resampler.update_timing_mode(self.vdp.timing_mode()); 394 } 395 396 fn soft_reset(&mut self) { 397 log::info!("Soft resetting console"); 398 399 // The SMS RESET button only sets a bit in a register; emulate "soft reset" by keeping the 400 // button virtually held down for 5 frames 401 self.reset_frames_remaining = 5; 402 } 403 404 fn hard_reset<S: SaveWriter>(&mut self, _save_writer: &mut S) { 405 log::info!("Hard resetting console"); 406 407 self.memory.reset(&self.config); 408 409 self.z80 = Z80::new(); 410 init_z80(&mut self.z80); 411 412 self.vdp = Vdp::new(self.vdp_version, &self.config); 413 self.psg = Sn76489::new(self.psg.version()); 414 self.input = InputState::new(self.input.region(), &self.config); 415 416 self.ym2413 = 417 self.config.fm_sound_unit_enabled.then(|| ym_opll::new_ym2413(YM2413_CLOCK_INTERVAL)); 418 419 self.frame_buffer = BoxedColorArray::new(); 420 421 self.vdp_mclk_counter = 0; 422 self.psg_mclk_counter = 0; 423 self.frame_count = 0; 424 self.reset_frames_remaining = 0; 425 } 426 427 fn load_state(&mut self, mut state: Self::SaveState) { 428 state.memory.take_rom_from(&mut self.memory); 429 *self = state; 430 } 431 432 fn to_save_state(&self) -> Self::SaveState { 433 self.partial_clone() 434 } 435 436 fn target_fps(&self) -> f64 { 437 let timing_mode = self.vdp.timing_mode(); 438 let mclk_frequency = timing_mode.mclk_frequency(); 439 let scanlines_per_frame = match timing_mode { 440 TimingMode::Ntsc => vdp::NTSC_SCANLINES_PER_FRAME, 441 TimingMode::Pal => vdp::PAL_SCANLINES_PER_FRAME, 442 }; 443 444 mclk_frequency / f64::from(vdp::MCLK_CYCLES_PER_SCANLINE) / f64::from(scanlines_per_frame) 445 } 446 447 fn update_audio_output_frequency(&mut self, output_frequency: u64) { 448 self.audio_resampler.update_output_frequency(output_frequency); 449 } 450} 451 452fn populate_frame_buffer( 453 vdp_buffer: &VdpBuffer, 454 viewport: ViewportSize, 455 vdp_version: VdpVersion, 456 crop_vertical_border: bool, 457 crop_left_border: bool, 458 frame_buffer: &mut [Color], 459) { 460 let (row_skip, row_take) = if crop_vertical_border { 461 (viewport.top_border_height as usize, viewport.height_without_border() as usize) 462 } else { 463 (0, viewport.height as usize) 464 }; 465 let (col_skip, screen_width) = if crop_left_border { 466 (viewport.left_border_width as usize, viewport.width_without_border() as usize) 467 } else { 468 (0, viewport.width as usize) 469 }; 470 471 for (i, row) in vdp_buffer.iter().skip(row_skip).take(row_take).enumerate() { 472 for (j, color) in row.iter().copied().skip(col_skip).enumerate() { 473 let color = match vdp_version.hardware() { 474 SmsGgHardware::MasterSystem => Color::rgb( 475 vdp::convert_sms_color(color & 0x03), 476 vdp::convert_sms_color((color >> 2) & 0x03), 477 vdp::convert_sms_color((color >> 4) & 0x03), 478 ), 479 SmsGgHardware::GameGear => Color::rgb( 480 vdp::convert_gg_color(color & 0x0F), 481 vdp::convert_gg_color((color >> 4) & 0x0F), 482 vdp::convert_gg_color((color >> 8) & 0x0F), 483 ), 484 SmsGgHardware::Sg1000 => vdp::convert_sg_color(color), 485 }; 486 487 frame_buffer[i * screen_width + j] = color; 488 } 489 } 490}