api.rsannotatedapi.rssource524 lines · 16.0 KB · raw
1//! Genesis public interface and main loop
2
3pub mod debug;
4
5use crate::api::debug::GenesisDebugger;
6use crate::audio::GenesisAudioResampler;
7use crate::bus::GenesisBus;
8use crate::bus::debug::{Debug68000Bus, DebugZ80Bus};
9use bincode::{Decode, Encode};
10use cdrom::reader::CdRom;
11use genesis_components::cartridge::Cartridge;
12use genesis_components::vdp::{Vdp, VdpTickEffect};
13use genesis_config::{GenesisButton, GenesisEmulatorConfig, GenesisInputs, GenesisRegion};
14use jgenesis_common::frontend::{
15    AudioOutput, EmulatorTrait, InputPoller, Modal, PartialClone, RenderFrameOptions, Renderer,
16    SaveWriter, TickEffect, TickResult, TimingMode,
17};
18use m68000_emu::M68000;
19use s32x_core::api::Sega32X;
20use segacd_core::api::{SegaCd, SegaCdLoadError, SegaCdLoadResult};
21use std::fmt::{Debug, Display, Formatter};
22use thiserror::Error;
23use z80_emu::Z80;
24
25const SRAM_EXTENSION: &str = "sav";
26const BACKUP_RAM_EXTENSION: &str = "bram";
27const RAM_CARTRIDGE_EXTENSION: &str = "ramc";
28
29#[derive(Debug, Error)]
30pub enum GenesisError<RErr, AErr, SErr> {
31    #[error("Sega CD disc error: {0}")]
32    SegaCd(#[from] SegaCdLoadError),
33    #[error("Rendering error: {0}")]
34    Render(RErr),
35    #[error("Audio output error: {0}")]
36    Audio(AErr),
37    #[error("Save write error: {0}")]
38    Save(SErr),
39}
40
41pub type GenesisResult<RErr, AErr, SErr> = Result<TickEffect, GenesisError<RErr, AErr, SErr>>;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
44pub enum GenesisHardware {
45    Standalone,
46    SegaCd,
47    Sega32X,
48    SegaCd32X,
49}
50
51impl Display for GenesisHardware {
52    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
53        match self {
54            Self::Standalone => write!(f, "Genesis"),
55            Self::SegaCd => write!(f, "Sega CD"),
56            Self::Sega32X => write!(f, "32X"),
57            Self::SegaCd32X => write!(f, "Sega CD 32X"),
58        }
59    }
60}
61
62impl GenesisHardware {
63    #[must_use]
64    #[inline]
65    pub fn has_sega_cd(self) -> bool {
66        matches!(self, Self::SegaCd | Self::SegaCd32X)
67    }
68
69    #[must_use]
70    #[inline]
71    pub fn has_32x(self) -> bool {
72        matches!(self, Self::Sega32X | Self::SegaCd32X)
73    }
74}
75
76#[derive(Debug, Encode, Decode, PartialClone)]
77pub struct GenesisEmulator {
78    m68k: M68000,
79    z80: Z80,
80    #[partial_clone(partial)]
81    bus: GenesisBus,
82    timing_mode: TimingMode,
83    audio_resampler: GenesisAudioResampler,
84    hardware: GenesisHardware,
85    config: GenesisEmulatorConfig,
86}
87
88impl GenesisEmulator {
89    /// # Errors
90    ///
91    /// Propagates any errors encountered while initializing Sega CD (if Sega CD is enabled).
92    pub fn create<S: SaveWriter>(
93        hardware: GenesisHardware,
94        mut cartridge_rom: Option<Vec<u8>>,
95        sega_cd_bios_rom: Option<Vec<u8>>,
96        mut disc: Option<CdRom>,
97        config: GenesisEmulatorConfig,
98        save_writer: &mut S,
99    ) -> SegaCdLoadResult<Self> {
100        log::info!("Running with hardware {hardware}");
101
102        if cartridge_rom.as_ref().is_some_and(Vec::is_empty) {
103            // Later code assumes cartridge ROM is non-empty if Some
104            cartridge_rom = None;
105        }
106
107        let sega_cd_present = hardware.has_sega_cd();
108        let s32x_present = hardware.has_32x();
109
110        let initial_ram = save_writer.load_bytes(SRAM_EXTENSION).ok();
111
112        let mut cartridge = cartridge_rom
113            .map(|rom| Cartridge::new(rom, initial_ram, config.forced_region, &config.cheat_codes));
114
115        let region = cartridge
116            .as_ref()
117            .map(Cartridge::region)
118            .or_else(|| {
119                let disc = disc.as_mut()?;
120                segacd_core::api::parse_disc_region(disc).ok()
121            })
122            .unwrap_or(GenesisRegion::Americas);
123
124        let timing_mode = config.forced_timing_mode.unwrap_or(match region {
125            GenesisRegion::Europe => TimingMode::Pal,
126            GenesisRegion::Americas | GenesisRegion::Japan => TimingMode::Ntsc,
127        });
128
129        log::info!("Using timing / display mode {timing_mode}");
130
131        let sega_cd = if sega_cd_present {
132            let Some(bios_rom) = sega_cd_bios_rom else { return Err(SegaCdLoadError::MissingBios) };
133
134            let backup_ram_extension = match &cartridge {
135                Some(_) => BACKUP_RAM_EXTENSION,
136                None => SRAM_EXTENSION,
137            };
138            let initial_backup_ram = save_writer.load_bytes(backup_ram_extension).ok();
139
140            let initial_ram_cartridge = save_writer.load_bytes(RAM_CARTRIDGE_EXTENSION).ok();
141
142            let sega_cd = SegaCd::new(
143                bios_rom,
144                disc,
145                timing_mode,
146                initial_backup_ram,
147                initial_ram_cartridge,
148                &config,
149            )?;
150            Some(sega_cd)
151        } else {
152            None
153        };
154
155        // If 32X is present, let 32X own the cartridge instead of GenesisBus
156        let sega_32x = s32x_present.then(|| Sega32X::new(timing_mode, cartridge.take(), &config));
157
158        let bus = GenesisBus::new(timing_mode, cartridge, sega_cd, sega_32x, &config);
159
160        // The Genesis does not allow TAS to lock the bus, so don't allow TAS writes
161        let m68k = M68000::builder().allow_tas_writes(false).name("Main".into()).build();
162        let z80 = Z80::new();
163
164        let audio_resampler = GenesisAudioResampler::new(hardware, timing_mode, &config);
165
166        let mut emulator = Self { m68k, z80, bus, timing_mode, audio_resampler, hardware, config };
167
168        // Reset CPU so that execution will start from the right place
169        emulator.bus.m68k_reset = true;
170        emulator.m68k.execute_instruction(&mut emulator.bus);
171        emulator.bus.m68k_reset = false;
172
173        Ok(emulator)
174    }
175
176    #[must_use]
177    pub fn game_title(&mut self) -> Option<String> {
178        self.bus.game_title()
179    }
180
181    #[inline]
182    #[must_use]
183    pub fn has_sram(&self) -> bool {
184        self.bus.has_persistent_ram()
185    }
186
187    #[inline]
188    #[must_use]
189    pub fn timing_mode(&self) -> TimingMode {
190        self.timing_mode
191    }
192
193    #[inline]
194    #[must_use]
195    pub fn hardware(&self) -> GenesisHardware {
196        self.hardware
197    }
198
199    pub fn remove_disc(&mut self) {
200        if let Some(sega_cd) = &mut self.bus.sega_cd {
201            sega_cd.remove_disc();
202        }
203    }
204
205    /// # Errors
206    ///
207    /// Propagates any CD-ROM read errors.
208    pub fn change_disc(&mut self, disc: CdRom) -> SegaCdLoadResult<()> {
209        if let Some(sega_cd) = &mut self.bus.sega_cd {
210            sega_cd.change_disc(disc)?;
211        }
212
213        Ok(())
214    }
215
216    fn render_frame<R: Renderer>(&mut self, renderer: &mut R) -> Result<(), R::Err> {
217        match &mut self.bus.sega_32x {
218            Some(sega_32x) if sega_32x.adapter_enabled() => {
219                sega_32x.render_frame(&self.bus.vdp, renderer)
220            }
221            _ => render_frame(self.timing_mode, &self.bus.vdp, &self.config, renderer),
222        }
223    }
224
225    #[inline]
226    fn tick_inner<const DEBUG: bool, R, A, I, S>(
227        &mut self,
228        renderer: &mut R,
229        audio_output: &mut A,
230        input_poller: &mut I,
231        save_writer: &mut S,
232        mut debugger: Option<&mut GenesisDebugger>,
233    ) -> TickResult<GenesisError<R::Err, A::Err, S::Err>>
234    where
235        R: Renderer,
236        A: AudioOutput,
237        I: InputPoller<GenesisInputs>,
238        S: SaveWriter,
239    {
240        self.bus.input.set_inputs(*input_poller.poll());
241        self.bus.m68k_opcode = self.m68k.next_opcode();
242
243        let m68k_pc = self.m68k.pc();
244        let m68k_wait = self.bus.cycles.m68k_wait_cpu_cycles != 0;
245        let m68k_cycles = if m68k_wait {
246            self.bus.cycles.take_m68k_wait_cpu_cycles()
247        } else if DEBUG && let Some(debugger) = &mut debugger {
248            let mut debug_bus = Debug68000Bus::new(&mut self.bus, &mut self.z80, debugger);
249            self.m68k.execute_instruction(&mut debug_bus)
250        } else {
251            self.m68k.execute_instruction(&mut self.bus)
252        };
253
254        let elapsed_mclk_cycles = self.bus.cycles.record_68k_instruction(
255            m68k_pc,
256            m68k_cycles,
257            m68k_wait,
258            self.bus.vdp.should_halt_cpu(),
259        );
260
261        while self.bus.cycles.should_tick_z80() {
262            if !self.bus.cycles.z80_halt {
263                if DEBUG && let Some(debugger) = &mut debugger {
264                    let mut debug_bus = DebugZ80Bus::new(&mut self.bus, &mut self.m68k, debugger);
265                    self.z80.tick(&mut debug_bus);
266                } else {
267                    self.z80.tick(&mut self.bus);
268                }
269            }
270            self.bus.cycles.z80_cycle();
271        }
272
273        let vdp_tick_effect = self.bus.tick_components::<DEBUG>(
274            m68k_cycles,
275            elapsed_mclk_cycles,
276            m68k_wait,
277            &mut self.audio_resampler,
278            debugger.as_mut().map(|debugger| debugger.with_cpus(&mut self.m68k, &mut self.z80)),
279        )?;
280
281        self.audio_resampler.output_samples(audio_output).map_err(GenesisError::Audio)?;
282
283        if vdp_tick_effect == VdpTickEffect::FrameComplete {
284            if let Some(sega_32x) = &mut self.bus.sega_32x
285                && sega_32x.adapter_enabled()
286            {
287                sega_32x.composite_frame(&mut self.bus.vdp);
288            }
289
290            self.render_frame(renderer).map_err(GenesisError::Render)?;
291
292            self.persist_save_files(save_writer).map_err(GenesisError::Save)?;
293        }
294
295        Ok(match vdp_tick_effect {
296            VdpTickEffect::FrameComplete => TickEffect::FrameRendered,
297            VdpTickEffect::None => TickEffect::None,
298        })
299    }
300
301    fn persist_save_files<S: SaveWriter>(&mut self, save_writer: &mut S) -> Result<(), S::Err> {
302        if !self.bus.has_persistent_ram() || !self.bus.get_and_clear_persistent_ram_dirty() {
303            return Ok(());
304        }
305
306        if let Some(ram) = self.bus.cartridge().map(Cartridge::external_ram)
307            && !ram.is_empty()
308        {
309            save_writer.persist_bytes(SRAM_EXTENSION, ram)?;
310        }
311
312        if let Some(sega_cd) = &self.bus.sega_cd {
313            // Backup RAM is the primary save location for Sega CD games, so prefer .sav over .bram
314            // if there's no game cartridge
315            let backup_ram_extension = match self.bus.cartridge() {
316                Some(_) => BACKUP_RAM_EXTENSION,
317                None => SRAM_EXTENSION,
318            };
319            save_writer.persist_bytes(backup_ram_extension, sega_cd.backup_ram())?;
320
321            // RAM cartridge is only present when there's no game cartridge (there's only 1 slot)
322            if self.bus.cartridge().is_none() {
323                save_writer.persist_bytes(RAM_CARTRIDGE_EXTENSION, sega_cd.ram_cartridge())?;
324            }
325        }
326
327        Ok(())
328    }
329
330    /// Similar to [`<Self as EmulatorTrait>::tick`] but runs with debugger hooks enabled.
331    ///
332    /// # Errors
333    ///
334    /// This method will propagate any errors encountered while rendering frames or pushing audio
335    /// samples.
336    #[inline]
337    pub fn debug_tick<R, A, I, S>(
338        &mut self,
339        renderer: &mut R,
340        audio_output: &mut A,
341        input_poller: &mut I,
342        save_writer: &mut S,
343        debugger: &mut GenesisDebugger,
344    ) -> GenesisResult<R::Err, A::Err, S::Err>
345    where
346        R: Renderer,
347        A: AudioOutput,
348        I: InputPoller<GenesisInputs>,
349        S: SaveWriter,
350    {
351        self.tick_inner::<true, _, _, _, _>(
352            renderer,
353            audio_output,
354            input_poller,
355            save_writer,
356            Some(debugger),
357        )
358    }
359}
360
361fn render_frame<R: Renderer>(
362    timing_mode: TimingMode,
363    vdp: &Vdp,
364    config: &GenesisEmulatorConfig,
365    renderer: &mut R,
366) -> Result<(), R::Err> {
367    let frame_size = vdp.frame_size();
368    let pixel_aspect_ratio = config.aspect_ratio.to_pixel_aspect_ratio(
369        timing_mode,
370        frame_size,
371        config.to_gen_par_params(),
372    );
373    let target_fps = genesis_components::target_framerate(vdp, timing_mode);
374
375    renderer.render_frame(
376        vdp.frame_buffer(),
377        frame_size,
378        target_fps,
379        RenderFrameOptions {
380            pixel_aspect_ratio,
381            composite_params: Some(vdp.composite_params()),
382            ..RenderFrameOptions::default()
383        },
384    )
385}
386
387impl EmulatorTrait for GenesisEmulator {
388    type Button = GenesisButton;
389    type Inputs = GenesisInputs;
390    type Config = GenesisEmulatorConfig;
391    type SaveState = Self;
392
393    type Err<
394        RErr: Debug + Display + Send + Sync + 'static,
395        AErr: Debug + Display + Send + Sync + 'static,
396        SErr: Debug + Display + Send + Sync + 'static,
397    > = GenesisError<RErr, AErr, SErr>;
398
399    /// Execute one 68000 CPU instruction and run the rest of the components for the appropriate
400    /// number of cycles.
401    ///
402    /// # Errors
403    ///
404    /// This method will propagate any errors encountered while rendering frames or pushing audio
405    /// samples.
406    #[inline]
407    fn tick<R, A, I, S>(
408        &mut self,
409        renderer: &mut R,
410        audio_output: &mut A,
411        input_poller: &mut I,
412        save_writer: &mut S,
413    ) -> GenesisResult<R::Err, A::Err, S::Err>
414    where
415        R: Renderer,
416        A: AudioOutput,
417        I: InputPoller<Self::Inputs>,
418        S: SaveWriter,
419    {
420        self.tick_inner::<false, _, _, _, _>(
421            renderer,
422            audio_output,
423            input_poller,
424            save_writer,
425            None,
426        )
427    }
428
429    fn force_render<R>(&mut self, renderer: &mut R) -> Result<(), R::Err>
430    where
431        R: Renderer,
432    {
433        self.render_frame(renderer)
434    }
435
436    fn reload_config(&mut self, config: &Self::Config) {
437        self.bus.reload_config(config);
438        self.audio_resampler.reload_config(self.timing_mode, config);
439
440        self.config = config.clone();
441    }
442
443    fn soft_reset(&mut self) {
444        log::info!("Soft resetting console");
445
446        self.bus.reset();
447
448        self.bus.m68k_reset = true;
449        self.m68k.execute_instruction(&mut self.bus);
450        self.bus.m68k_reset = false;
451    }
452
453    fn hard_reset<S: SaveWriter>(&mut self, save_writer: &mut S) {
454        log::info!("Hard resetting console");
455
456        let cartridge_rom = self.bus.cartridge_mut().map(Cartridge::take_rom);
457
458        let (sega_cd_bios, disc) = match self.bus.sega_cd.take().map(SegaCd::take_bios_and_disc) {
459            Some((bios_rom, disc)) => (Some(bios_rom), disc),
460            None => (None, None),
461        };
462
463        let sega_cd_bios =
464            sega_cd_bios.map(|bios| bios.into_iter().flat_map(u16::to_be_bytes).collect());
465
466        *self = GenesisEmulator::create(
467            self.hardware,
468            cartridge_rom,
469            sega_cd_bios,
470            disc,
471            self.config.clone(),
472            save_writer,
473        )
474        .expect("Hard reset should not error");
475    }
476
477    fn load_state(&mut self, mut state: Self::SaveState) {
478        if let Some(state_cartridge) = state.bus.cartridge_mut()
479            && let Some(self_cartridge) = self.bus.cartridge_mut()
480        {
481            state_cartridge.take_rom_from(self_cartridge);
482        }
483
484        if let Some(state_sega_cd) = &mut state.bus.sega_cd
485            && let Some(self_sega_cd) = &mut self.bus.sega_cd
486        {
487            state_sega_cd.take_bios_and_disc_from(self_sega_cd);
488        }
489
490        *self = state;
491    }
492
493    fn to_save_state(&self) -> Self::SaveState {
494        self.partial_clone()
495    }
496
497    fn target_fps(&self) -> f64 {
498        genesis_components::target_framerate(&self.bus.vdp, self.timing_mode)
499    }
500
501    fn update_audio_output_frequency(&mut self, output_frequency: u64) {
502        self.audio_resampler.update_output_frequency(output_frequency);
503    }
504
505    fn startup_modals(&self) -> Vec<Modal> {
506        let mut modals = Vec::new();
507
508        if self.config.remove_sprite_limits
509            && (self
510                .bus
511                .cartridge
512                .as_ref()
513                .is_some_and(|cartridge| cartridge.metadata().sprite_limit_compatibility_issues)
514                || self.bus.sega_cd.as_ref().is_some_and(SegaCd::has_six_button_incompatible_game))
515        {
516            modals.push(Modal {
517                id: None,
518                text: genesis_components::SPRITE_LIMITS_MODAL_MESSAGE.into(),
519            });
520        }
521
522        modals
523    }
524}