Game Boy emulator public interface and main loop
3use crate::apu::Apu; 4use crate::bus::Bus; 5use crate::cartridge::{Cartridge, SoftwareType}; 6use crate::cgb::CgbRegisters; 7use crate::dma::DmaUnit; 8use crate::graphics::RgbaFrameBuffer; 9use crate::inputs::InputState; 10use crate::interrupts::InterruptRegisters; 11use crate::memory::Memory; 12use crate::ppu::Ppu; 13use crate::serial::SerialPort; 14use crate::sm83::Sm83; 15use crate::timer::GbTimer; 16use crate::{HardwareMode, audio, ppu}; 17use bincode::{Decode, Encode}; 18use gb_config::{GameBoyButton, GameBoyInputs, GbAspectRatio, GbAudioResampler, GbPalette}; 19use jgenesis_common::frontend::{ 20 AudioOutput, Color, ColorCorrection, EmulatorConfigTrait, EmulatorTrait, InputPoller, 21 PartialClone, RenderFrameOptions, Renderer, SaveWriter, TickEffect, TickResult, 22}; 23use jgenesis_proc_macros::ConfigDisplay; 24use std::fmt::{Debug, Display}; 25use thiserror::Error;
27#[derive(Debug, Error)] 28pub enum GameBoyLoadError { 29 #[error("ROM header contains invalid SRAM size byte: ${0:02X}")] 30 InvalidSramByte(u8), 31 #[error("ROM header contains unsupported mapper byte: ${0:02X}")] 32 UnsupportedMapperByte(u8), 33 #[error("Incorrect boot ROM size; expected {expected} bytes, was {actual} bytes")] 34 InvalidBootRomSize { actual: usize, expected: usize }, 35 #[error("ROM image too small; expected at least {expected} bytes, was {size} bytes")] 36 RomTooSmall { size: usize, expected: usize }, 37} 38 39#[derive(Debug, Error)] 40pub enum GameBoyError<RErr, AErr, SErr> { 41 #[error("Error rendering a frame: {0}")] 42 Rendering(RErr), 43 #[error("Error outputting audio samples: {0}")] 44 Audio(AErr), 45 #[error("Error writing save file: {0}")] 46 SaveWrite(SErr), 47} 48 49#[derive(Debug, Clone, Copy, Encode, Decode, ConfigDisplay)] 50pub struct GameBoyEmulatorConfig { 51 pub force_dmg_mode: bool, 52 pub force_cgb_mode: bool, 53 pub pretend_to_be_gba: bool, 54 pub aspect_ratio: GbAspectRatio, 55 pub gb_palette: GbPalette, 56 #[cfg_display(debug_fmt)] 57 pub gb_custom_palette: [(u8, u8, u8); 4], 58 pub gbc_color_correction: ColorCorrection, 59 pub frame_blending: bool, 60 pub audio_resampler: GbAudioResampler, 61 pub audio_60hz_hack: bool, 62 pub allow_opposing_joypad_directions: bool, 63} 64 65impl EmulatorConfigTrait for GameBoyEmulatorConfig {} 66 67impl GameBoyEmulatorConfig { 68 fn render_options(&self, hardware_mode: HardwareMode) -> RenderFrameOptions { 69 RenderFrameOptions { 70 pixel_aspect_ratio: self.aspect_ratio.to_pixel_aspect_ratio(), 71 color_correction: match hardware_mode { 72 HardwareMode::Dmg => ColorCorrection::None, 73 HardwareMode::Cgb => self.gbc_color_correction, 74 }, 75 frame_blending: self.frame_blending, 76 ..RenderFrameOptions::default() 77 } 78 } 79} 80 81#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 82pub enum BackgroundTileMap { 83 #[default] 84 Zero, 85 One, 86} 87 88#[derive(Debug, Clone, Encode, Decode)] 89pub struct BootRoms { 90 pub dmg: Option<Vec<u8>>, 91 pub cgb: Option<Vec<u8>>, 92} 93 94impl BootRoms { 95 fn take(&mut self) -> Self { 96 Self { dmg: self.dmg.take(), cgb: self.cgb.take() } 97 } 98} 99 100#[derive(Debug, Clone, Encode, Decode, PartialClone)] 101pub struct GameBoyEmulator { 102 hardware_mode: HardwareMode, 103 cpu: Sm83, 104 ppu: Ppu, 105 apu: Apu, 106 memory: Memory, 107 serial_port: SerialPort, 108 interrupt_registers: InterruptRegisters, 109 cgb_registers: CgbRegisters, 110 #[partial_clone(partial)] 111 cartridge: Cartridge, 112 timer: GbTimer, 113 dma_unit: DmaUnit, 114 input_state: InputState, 115 rgba_buffer: RgbaFrameBuffer, 116 config: GameBoyEmulatorConfig, 117 frame_count: u64, 118 // Kept around to enable hard reset 119 boot_roms: BootRoms, 120} 121 122impl GameBoyEmulator {
Errors
This function will return an error if it cannot load the ROM (e.g. unsupported mapper).
126 pub fn create<S: SaveWriter>( 127 mut rom: Vec<u8>, 128 boot_roms: BootRoms, 129 config: GameBoyEmulatorConfig, 130 save_writer: &mut S, 131 ) -> Result<Self, GameBoyLoadError> { 132 let software_type = SoftwareType::from_rom(&rom); 133 let hardware_mode = match software_type { 134 SoftwareType::DmgOnly => { 135 if config.force_cgb_mode { 136 HardwareMode::Cgb 137 } else { 138 HardwareMode::Dmg 139 } 140 } 141 SoftwareType::CgbEnhanced | SoftwareType::CgbOnly => { 142 if config.force_dmg_mode { 143 HardwareMode::Dmg 144 } else { 145 HardwareMode::Cgb 146 } 147 } 148 }; 149 150 log::info!("Running with hardware mode {hardware_mode}, software type {software_type}"); 151 152 let boot_rom = match hardware_mode { 153 HardwareMode::Dmg => boot_roms.dmg.clone(), 154 HardwareMode::Cgb => boot_roms.cgb.clone(), 155 }; 156 let boot_rom_present = boot_rom.is_some(); 157 158 let ppu = Ppu::new(hardware_mode, &rom, boot_rom_present); 159 let memory = Memory::new(boot_rom, hardware_mode)?; 160 161 let initial_sram = save_writer.load_bytes("sav").ok(); 162 163 jgenesis_common::rom::mirror_to_next_power_of_two(&mut rom); 164 let cartridge = Cartridge::create(rom.into_boxed_slice(), initial_sram, save_writer)?; 165 166 Ok(Self { 167 hardware_mode, 168 cpu: Sm83::new(hardware_mode, config.pretend_to_be_gba, boot_rom_present), 169 ppu, 170 apu: Apu::new(config, hardware_mode), 171 memory, 172 serial_port: SerialPort::new(hardware_mode), 173 interrupt_registers: InterruptRegisters::default(), 174 cgb_registers: CgbRegisters::new(), 175 cartridge, 176 timer: GbTimer::new(), 177 dma_unit: DmaUnit::new(hardware_mode), 178 input_state: InputState::new(&config), 179 rgba_buffer: RgbaFrameBuffer::default(), 180 config, 181 frame_count: 0, 182 boot_roms, 183 }) 184 }
186 pub fn copy_background(&self, tile_map: BackgroundTileMap, out: &mut [Color]) { 187 self.ppu.copy_background(tile_map, self.cgb_registers.dmg_compatibility, out); 188 } 189 190 pub fn copy_sprites(&self, out: &mut [Color]) { 191 self.ppu.copy_sprites(self.cgb_registers.dmg_compatibility, out); 192 } 193 194 pub fn copy_palettes(&self, out: &mut [Color]) { 195 self.ppu.copy_palettes(out); 196 } 197 198 #[inline] 199 #[must_use] 200 pub fn is_using_double_height_sprites(&self) -> bool { 201 self.ppu.is_using_double_height_sprites() 202 } 203 204 #[inline] 205 #[must_use] 206 pub fn is_cgb_mode(&self) -> bool { 207 self.hardware_mode == HardwareMode::Cgb 208 } 209} 210 211impl EmulatorTrait for GameBoyEmulator { 212 type Button = GameBoyButton; 213 type Inputs = GameBoyInputs; 214 type Config = GameBoyEmulatorConfig; 215 type SaveState = Self; 216 217 type Err< 218 RErr: Debug + Display + Send + Sync + 'static, 219 AErr: Debug + Display + Send + Sync + 'static, 220 SErr: Debug + Display + Send + Sync + 'static, 221 > = GameBoyError<RErr, AErr, SErr>; 222 223 fn tick<R, A, I, S>( 224 &mut self, 225 renderer: &mut R, 226 audio_output: &mut A, 227 input_poller: &mut I, 228 save_writer: &mut S, 229 ) -> TickResult<Self::Err<R::Err, A::Err, S::Err>> 230 where 231 R: Renderer, 232 A: AudioOutput, 233 I: InputPoller<Self::Inputs>, 234 S: SaveWriter, 235 { 236 self.input_state.set_inputs(*input_poller.poll(), &mut self.interrupt_registers); 237 238 self.cpu.execute_instruction(&mut Bus { 239 hardware_mode: self.hardware_mode, 240 ppu: &mut self.ppu, 241 apu: &mut self.apu, 242 memory: &mut self.memory, 243 serial_port: &mut self.serial_port, 244 cartridge: &mut self.cartridge, 245 interrupt_registers: &mut self.interrupt_registers, 246 cgb_registers: &mut self.cgb_registers, 247 timer: &mut self.timer, 248 dma_unit: &mut self.dma_unit, 249 input_state: &mut self.input_state, 250 }); 251 252 self.apu.drain_samples_into(audio_output).map_err(GameBoyError::Audio)?; 253 254 if self.ppu.frame_complete() { 255 self.ppu.clear_frame_complete(); 256 self.rgba_buffer.copy_from(self.ppu.frame_buffer(), self.hardware_mode, &self.config); 257 renderer 258 .render_frame( 259 self.rgba_buffer.as_ref(), 260 ppu::FRAME_SIZE, 261 self.target_fps(), 262 self.config.render_options(self.hardware_mode), 263 ) 264 .map_err(GameBoyError::Rendering)?; 265 266 self.cartridge.update_rtc_time(); 267 268 if self.cartridge.has_battery() 269 && self.frame_count % 60 == 30 270 && self.cartridge.get_and_clear_sram_dirty() 271 { 272 let sram = self.cartridge.sram(); 273 save_writer.persist_bytes("sav", sram).map_err(GameBoyError::SaveWrite)?; 274 275 self.cartridge.save_rtc_state(save_writer).map_err(GameBoyError::SaveWrite)?; 276 } 277 278 self.frame_count += 1; 279 280 Ok(TickEffect::FrameRendered) 281 } else { 282 Ok(TickEffect::None) 283 } 284 } 285 286 fn force_render<R>(&mut self, renderer: &mut R) -> Result<(), R::Err> 287 where 288 R: Renderer, 289 { 290 self.rgba_buffer.copy_from(self.ppu.frame_buffer(), self.hardware_mode, &self.config); 291 renderer.render_frame( 292 self.rgba_buffer.as_ref(), 293 ppu::FRAME_SIZE, 294 self.target_fps(), 295 self.config.render_options(self.hardware_mode), 296 ) 297 } 298 299 fn reload_config(&mut self, config: &Self::Config) { 300 self.config = *config; 301 self.apu.reload_config(*config); 302 self.input_state.reload_config(config); 303 } 304 305 fn soft_reset(&mut self) { 306 log::warn!("The Game Boy does not support soft reset except in software"); 307 } 308 309 fn hard_reset<S: SaveWriter>(&mut self, save_writer: &mut S) { 310 let rom = self.cartridge.take_rom(); 311 312 *self = Self::create(rom, self.boot_roms.take(), self.config, save_writer) 313 .expect("Hard reset should never fail to load cartridge"); 314 } 315 316 fn load_state(&mut self, mut state: Self::SaveState) { 317 state.cartridge.take_rom_from(&mut self.cartridge); 318 *self = state; 319 } 320 321 fn to_save_state(&self) -> Self::SaveState { 322 self.partial_clone() 323 } 324 325 fn target_fps(&self) -> f64 { 326 if self.config.audio_60hz_hack { 327 60.0 328 } else { 329 // Approximately 59.73 Hz 330 let dots_per_frame = f64::from(ppu::DOTS_PER_LINE) * f64::from(ppu::LINES_PER_FRAME); 331 2.0 * audio::GB_APU_FREQUENCY / dots_per_frame 332 } 333 } 334 335 fn update_audio_output_frequency(&mut self, output_frequency: u64) { 336 self.apu.update_output_frequency(output_frequency); 337 } 338}