api.rsannotatedapi.rssource338 lines · 10.7 KB · raw
1//! Game Boy emulator public interface and main loop
2
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;
26
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 {
123    /// # Errors
124    ///
125    /// 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    }
185
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}