api.rsannotatedapi.rssource524 lines · 16.0 KB · raw

Genesis public interface and main loop

3pub mod debug;
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 {

Errors

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    }
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    }

Errors

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    }
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    }

Similar to [<Self as EmulatorTrait>::tick] but runs with debugger hooks enabled.

Errors

This method will propagate any errors encountered while rendering frames or pushing audio 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}
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>;

Execute one 68000 CPU instruction and run the rest of the components for the appropriate number of cycles.

Errors

This method will propagate any errors encountered while rendering frames or pushing audio 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    }
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}