api.rsannotatedapi.rssource421 lines · 13.8 KB · raw

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

31const TARGET_FPS: f64 =
32    (crate::GBA_CLOCK_SPEED as f64) / (ppu::LINES_PER_FRAME as f64) / (ppu::DOTS_PER_LINE as f64);
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}