GBA emulator public interface and main loop
3pub mod debug;
5use crate::apu::Apu; 6use crate::bus::{Bus, BusState}; 7use crate::cartridge::Cartridge; 8use crate::dma::DmaState; 9use crate::input::InputState; 10use crate::interrupts::InterruptRegisters; 11use crate::memory::Memory; 12use crate::ppu; 13use crate::ppu::Ppu; 14use crate::prefetch::GamePakPrefetcher; 15use crate::scheduler::{Scheduler, SchedulerEvent}; 16use crate::sio::SerialPort; 17use crate::timers::Timers; 18use arm7tdmi_emu::bus::BusInterface; 19use arm7tdmi_emu::{Arm7Tdmi, Arm7TdmiResetArgs, CpuMode}; 20use bincode::{Decode, Encode}; 21use gba_config::{GbaAspectRatio, GbaAudioInterpolation, GbaButton, GbaInputs, GbaSaveMemory}; 22use jgenesis_common::frontend::{ 23 AudioOutput, Color, ColorCorrection, EmulatorConfigTrait, EmulatorTrait, InputPoller, 24 PartialClone, RenderFrameOptions, Renderer, SaveWriter, TickEffect, TickResult, 25}; 26use jgenesis_proc_macros::ConfigDisplay; 27use std::fmt::{Debug, Display}; 28use thiserror::Error;
Roughly 59.73 fps
34#[derive(Debug, Clone, Copy, Encode, Decode, ConfigDisplay)] 35pub struct GbaAudioConfig { 36 pub audio_interpolation: GbaAudioInterpolation, 37 pub psg_low_pass: bool, 38 pub pulse_1_enabled: bool, 39 pub pulse_2_enabled: bool, 40 pub wavetable_enabled: bool, 41 pub noise_enabled: bool, 42 pub pcm_a_enabled: bool, 43 pub pcm_b_enabled: bool, 44} 45 46impl Default for GbaAudioConfig { 47 fn default() -> Self { 48 Self { 49 audio_interpolation: GbaAudioInterpolation::default(), 50 psg_low_pass: false, 51 pulse_1_enabled: true, 52 pulse_2_enabled: true, 53 wavetable_enabled: true, 54 noise_enabled: true, 55 pcm_a_enabled: true, 56 pcm_b_enabled: true, 57 } 58 } 59} 60 61impl GbaAudioConfig { 62 pub(crate) fn psg_channels_enabled(self) -> [bool; 4] { 63 [self.pulse_1_enabled, self.pulse_2_enabled, self.wavetable_enabled, self.noise_enabled] 64 } 65 66 pub(crate) fn pcm_channels_enabled(self) -> [bool; 2] { 67 [self.pcm_a_enabled, self.pcm_b_enabled] 68 } 69} 70 71#[derive(Debug, Clone, Copy, Default, Encode, Decode, ConfigDisplay)] 72pub struct GbaEmulatorConfig { 73 pub skip_bios_animation: bool, 74 pub aspect_ratio: GbaAspectRatio, 75 pub color_correction: ColorCorrection, 76 pub frame_blending: bool, 77 pub forced_save_memory_type: Option<GbaSaveMemory>, 78 pub allow_opposing_joypad_directions: bool, 79 #[cfg_display(indent_nested)] 80 pub audio: GbaAudioConfig, 81} 82 83impl GbaEmulatorConfig { 84 fn render_options(&self) -> RenderFrameOptions { 85 RenderFrameOptions { 86 pixel_aspect_ratio: self.aspect_ratio.to_pixel_aspect_ratio(), 87 color_correction: self.color_correction, 88 frame_blending: self.frame_blending, 89 ..RenderFrameOptions::default() 90 } 91 } 92} 93 94#[derive(Debug, Clone, Default, Encode, Decode)] 95struct StoppedState { 96 stopped_last_tick: bool, 97 render_counter: u64, 98 audio_output_counter: u64, 99 output_frequency: u64, 100 cycles_remaining: Option<u64>, 101} 102 103#[derive(Debug, Error)] 104pub enum GbaLoadError { 105 #[error("Invalid BIOS ROM; expected length of {expected} bytes, was {actual} bytes")] 106 InvalidBiosLength { expected: usize, actual: usize }, 107} 108 109#[derive(Debug, Error)] 110pub enum GbaError<RErr, AErr, SErr> { 111 #[error("Error rendering video output: {0}")] 112 Render(RErr), 113 #[error("Error outputting audio samples: {0}")] 114 Audio(AErr), 115 #[error("Error persisting save file: {0}")] 116 SaveWrite(SErr), 117} 118 119#[derive(Debug, PartialClone, Encode, Decode)] 120pub struct GameBoyAdvanceEmulator { 121 cpu: Arm7Tdmi<Bus>, 122 #[partial_clone(partial)] 123 bus: Bus, 124 config: GbaEmulatorConfig, 125 last_apu_sync_cycles: u64, 126 frame_count: u64, 127 stop_state: StoppedState, 128} 129 130impl GameBoyAdvanceEmulator {
Errors
Returns an error if emulator initialization fails, e.g. because the BIOS ROM is invalid.
134 pub fn create<S: SaveWriter>( 135 rom: Vec<u8>, 136 bios_rom: Vec<u8>, 137 config: GbaEmulatorConfig, 138 save_writer: &mut S, 139 ) -> Result<Self, GbaLoadError> { 140 let initial_save = save_writer.load_bytes("sav").ok(); 141 let initial_rtc = save_writer.load_serialized("rtc").ok(); 142 143 let memory = Memory::new(bios_rom, config)?; 144 let cartridge = 145 Cartridge::new(rom, initial_save, initial_rtc, config.forced_save_memory_type); 146 147 let mut cpu = Arm7Tdmi::new(); 148 let mut bus = Bus { 149 ppu: Ppu::new(config.skip_bios_animation), 150 apu: Apu::new(config.audio), 151 memory, 152 cartridge, 153 prefetch: GamePakPrefetcher::new(), 154 dma: DmaState::new(), 155 timers: Timers::new(), 156 interrupts: InterruptRegisters::new(), 157 sio: SerialPort::new(), 158 inputs: InputState::new(&config), 159 state: BusState::new(), 160 scheduler: Scheduler::new(), 161 }; 162 163 if !config.skip_bios_animation { 164 cpu.reset(&mut bus); 165 } else { 166 // Based on values that the GBA BIOS sets 167 cpu.manual_reset( 168 Arm7TdmiResetArgs { 169 pc: 0x8000000, 170 sp_usr: 0x3007F00, 171 sp_svc: 0x3007FE0, 172 sp_irq: 0x3007FA0, 173 sp_fiq: 0, 174 mode: CpuMode::System, 175 }, 176 &mut bus, 177 ); 178 } 179 180 // Schedule initial PPU event to guarantee that PPU starts running even if never accessed 181 bus.scheduler.insert_or_update(SchedulerEvent::PpuEvent, ppu::DOTS_PER_LINE.into()); 182 183 Ok(Self { 184 cpu, 185 bus, 186 config, 187 last_apu_sync_cycles: 0, 188 frame_count: 0, 189 stop_state: StoppedState::default(), 190 }) 191 }
193 #[must_use] 194 pub fn has_save_memory(&self) -> bool { 195 self.bus.cartridge.rw_memory().is_some() 196 } 197 198 fn drain_apu<A: AudioOutput>(&mut self, audio_output: &mut A) -> Result<(), A::Err> { 199 self.bus.apu.step_to(self.bus.state.cycles); 200 self.bus.apu.drain_audio_output(audio_output)?; 201 202 self.last_apu_sync_cycles = self.bus.state.cycles; 203 204 Ok(()) 205 } 206 207 fn tick_stopped<R: Renderer, A: AudioOutput, S: SaveWriter>( 208 &mut self, 209 renderer: &mut R, 210 audio_output: &mut A, 211 ) -> TickResult<GbaError<R::Err, A::Err, S::Err>> { 212 // Arbitrarily advance by the equivalent of 2048 clock cycles 213 const INCREMENT: u64 = 2048; 214 215 const CYCLES_PER_FRAME: u64 = (ppu::LINES_PER_FRAME * ppu::DOTS_PER_LINE) as u64; 216 217 // Actual hardware turns off the screen in stop mode; emulate this by displaying solid white 218 const BLANK_FRAME: &[Color] = &[Color::rgb(255, 255, 255); ppu::FRAME_BUFFER_LEN]; 219 220 if !self.stop_state.stopped_last_tick { 221 self.stop_state.stopped_last_tick = true; 222 self.stop_state.render_counter = 0; 223 self.stop_state.audio_output_counter = 0; 224 self.stop_state.cycles_remaining = None; 225 } 226 227 // Output constant 0s for audio 228 self.stop_state.audio_output_counter += INCREMENT * self.stop_state.output_frequency; 229 while self.stop_state.audio_output_counter >= crate::GBA_CLOCK_SPEED { 230 self.stop_state.audio_output_counter -= crate::GBA_CLOCK_SPEED; 231 audio_output.push_sample(0.0, 0.0).map_err(GbaError::Audio)?; 232 } 233 234 let mut tick_effect = TickEffect::None; 235 236 // Repeatedly render a blank frame 237 self.stop_state.render_counter += INCREMENT; 238 while self.stop_state.render_counter >= CYCLES_PER_FRAME { 239 self.stop_state.render_counter -= CYCLES_PER_FRAME; 240 renderer 241 .render_frame( 242 BLANK_FRAME, 243 ppu::FRAME_SIZE, 244 TARGET_FPS, 245 self.config.render_options(), 246 ) 247 .map_err(GbaError::Render)?; 248 tick_effect = TickEffect::FrameRendered; 249 } 250 251 match &mut self.stop_state.cycles_remaining { 252 Some(cycles_remaining) => { 253 *cycles_remaining = cycles_remaining.saturating_sub(INCREMENT); 254 if *cycles_remaining == 0 { 255 // Stop has ended 256 self.stop_state.stopped_last_tick = false; 257 self.bus.interrupts.clear_stop(); 258 } 259 } 260 None => { 261 if self.bus.interrupts.stop_is_ending() { 262 // End stop after roughly 1/8 of a second 263 // Not sure this is accurate, but stop definitely does not end instantly on actual hardware 264 self.stop_state.cycles_remaining = Some(crate::GBA_CLOCK_SPEED / 8); 265 } 266 } 267 } 268 269 Ok(tick_effect) 270 } 271} 272 273impl EmulatorConfigTrait for GbaEmulatorConfig {} 274 275impl EmulatorTrait for GameBoyAdvanceEmulator { 276 type Button = GbaButton; 277 type Inputs = GbaInputs; 278 type Config = GbaEmulatorConfig; 279 type SaveState = Self; 280 281 type Err< 282 RErr: Debug + Display + Send + Sync + 'static, 283 AErr: Debug + Display + Send + Sync + 'static, 284 SErr: Debug + Display + Send + Sync + 'static, 285 > = GbaError<RErr, AErr, SErr>; 286 287 #[inline] 288 fn tick<R, A, I, S>( 289 &mut self, 290 renderer: &mut R, 291 audio_output: &mut A, 292 input_poller: &mut I, 293 save_writer: &mut S, 294 ) -> TickResult<Self::Err<R::Err, A::Err, S::Err>> 295 where 296 R: Renderer, 297 A: AudioOutput, 298 I: InputPoller<Self::Inputs>, 299 S: SaveWriter, 300 { 301 let inputs = *input_poller.poll(); 302 303 if self.bus.interrupts.stopped() { 304 // All hardware is stopped 305 // Stop can only be broken by a keypad interrupt or a Game Pak interrupt 306 self.bus.inputs.update_inputs(inputs, self.bus.state.cycles, &mut self.bus.interrupts); 307 self.bus.cartridge.update_rtc_time(self.bus.state.cycles, &mut self.bus.interrupts); 308 309 return self.tick_stopped::<_, _, S>(renderer, audio_output); 310 } 311 312 self.bus.inputs.update_inputs(inputs, self.bus.state.cycles, &mut self.bus.interrupts); 313 self.bus.cartridge.set_solar_brightness(inputs.solar.brightness); 314 315 // TODO halt should let the CPU execute for 1 more cycle before halting 316 // This is difficult/impossible to implement without being able to suspend CPU execution 317 // mid-instruction 318 if !self.bus.interrupts.cpu_halted() { 319 self.cpu.execute_instruction(&mut self.bus); 320 } else { 321 self.bus.internal_cycles(1); 322 if !self.bus.interrupts.cpu_halted() { 323 // 1-cycle delay when CPU unhalts 324 self.bus.internal_cycles(1); 325 } 326 } 327 328 // Forcibly sync the APU roughly once per line 329 if self.bus.state.cycles - self.last_apu_sync_cycles >= u64::from(ppu::DOTS_PER_LINE) { 330 self.drain_apu(audio_output).map_err(GbaError::Audio)?; 331 } 332 333 self.bus.sio.check_for_interrupt(self.bus.state.cycles, &mut self.bus.interrupts); 334 335 if self.bus.ppu.frame_complete() { 336 self.bus.ppu.clear_frame_complete(); 337 338 self.drain_apu(audio_output).map_err(GbaError::Audio)?; 339 340 renderer 341 .render_frame( 342 self.bus.ppu.frame_buffer(), 343 ppu::FRAME_SIZE, 344 TARGET_FPS, 345 self.config.render_options(), 346 ) 347 .map_err(GbaError::Render)?; 348 349 self.bus.cartridge.update_rtc_time(self.bus.state.cycles, &mut self.bus.interrupts); 350 351 if self.bus.cartridge.take_rw_memory_dirty() 352 && let Some(rw_memory) = self.bus.cartridge.rw_memory() 353 { 354 save_writer.persist_bytes("sav", rw_memory).map_err(GbaError::SaveWrite)?; 355 } 356 357 self.frame_count += 1; 358 359 if let Some(rtc) = self.bus.cartridge.rtc() { 360 // Limit how frequently RTC state is persisted to disk (roughly once per 10 seconds) 361 if self.frame_count.is_multiple_of(600) { 362 save_writer.persist_serialized("rtc", rtc).map_err(GbaError::SaveWrite)?; 363 } 364 } 365 366 return Ok(TickEffect::FrameRendered); 367 } 368 369 Ok(TickEffect::None) 370 } 371 372 fn force_render<R>(&mut self, renderer: &mut R) -> Result<(), R::Err> 373 where 374 R: Renderer, 375 { 376 renderer.render_frame( 377 self.bus.ppu.frame_buffer(), 378 ppu::FRAME_SIZE, 379 TARGET_FPS, 380 self.config.render_options(), 381 ) 382 } 383 384 fn reload_config(&mut self, config: &Self::Config) { 385 self.config = *config; 386 self.bus.apu.reload_config(config.audio); 387 self.bus.inputs.reload_config(config); 388 } 389 390 fn soft_reset(&mut self) { 391 log::warn!("GBA does not support soft reset except in software"); 392 } 393 394 fn hard_reset<S: SaveWriter>(&mut self, save_writer: &mut S) { 395 let rom = self.bus.cartridge.take_rom(); 396 let bios_rom = self.bus.memory.clone_bios_rom(); 397 398 *self = Self::create(rom, bios_rom, self.config, save_writer) 399 .expect("Emulator creation should never fail during hard reset"); 400 } 401 402 fn load_state(&mut self, mut state: Self::SaveState) { 403 state.bus.cartridge.take_rom_from(&mut self.bus.cartridge); 404 *self = state; 405 } 406 407 fn to_save_state(&self) -> Self::SaveState { 408 self.partial_clone() 409 } 410 411 #[inline] 412 fn target_fps(&self) -> f64 { 413 TARGET_FPS 414 } 415 416 #[inline] 417 fn update_audio_output_frequency(&mut self, output_frequency: u64) { 418 self.bus.apu.update_output_frequency(output_frequency); 419 self.stop_state.output_frequency = output_frequency; 420 } 421}