api.rsannotatedapi.rssource490 lines · 16.1 KB · raw

Sega Master System / Game Gear public interface and main loop

3use crate::audio::{AudioResampler, TimingModeExt};
4use crate::bus::Bus;
5use crate::input::InputState;
6use crate::memory::Memory;
7use crate::vdp::{Vdp, VdpBuffer, VdpTickEffect, ViewportSize};
8use crate::{VdpVersion, vdp};
9use bincode::{Decode, Encode};
10use jgenesis_common::boxedarray::BoxedColorArray;
11use jgenesis_common::cheats::ByteCheatCodeU16Address;
12use jgenesis_common::frontend::{
13    AudioOutput, Color, CompositeParams, EmulatorConfigTrait, EmulatorTrait, FrameSize,
14    InputPoller, PartialClone, RenderFrameOptions, Renderer, SamplesPerColorCycle, SaveWriter,
15    TickEffect, TimingMode,
16};
17use jgenesis_proc_macros::ConfigDisplay;
18use smsgg_config::{
19    GgAspectRatio, SmsAspectRatio, SmsGgButton, SmsGgInputs, SmsGgRegion, SmsModel,
20};
21use std::fmt::{Debug, Display};
22use std::num::NonZeroU32;
23use thiserror::Error;
24use ti_sn76489::{Sn76489, Sn76489TickEffect, Sn76489Version};
25use ym_opll::Ym2413;
26use z80_emu::{InterruptMode, Z80};
28#[derive(Debug, Error)]
29pub enum SmsGgError<RErr, AErr, SErr> {
30    #[error("Rendering error: {0}")]
31    Render(RErr),
32    #[error("Audio output error: {0}")]
33    Audio(AErr),
34    #[error("Save write error: {0}")]
35    SaveWrite(SErr),
36}
37
38pub type SmsGgResult<RErr, AErr, SErr> = Result<TickEffect, SmsGgError<RErr, AErr, SErr>>;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
41pub enum SmsGgHardware {
42    MasterSystem,
43    GameGear,
44    Sg1000,
45}
46
47#[derive(Debug, Clone, Encode, Decode, ConfigDisplay)]
48pub struct SmsGgEmulatorConfig {
49    pub sms_timing_mode: TimingMode,
50    pub sms_model: SmsModel,
51    pub forced_psg_version: Option<Sn76489Version>,
52    pub sms_aspect_ratio: SmsAspectRatio,
53    pub gg_aspect_ratio: GgAspectRatio,
54    pub remove_sprite_limit: bool,
55    pub forced_region: Option<SmsGgRegion>,
56    pub sms_crop_vertical_border: bool,
57    pub sms_crop_left_border: bool,
58    pub gg_frame_blending: bool,
59    pub gg_use_sms_resolution: bool,
60    pub fm_sound_unit_enabled: bool,
61    pub z80_divider: NonZeroU32,
62    pub allow_opposing_joypad_directions: bool,
63    #[cfg_display(skip)]
64    pub cheat_codes: Vec<ByteCheatCodeU16Address>,
65}
66
67impl EmulatorConfigTrait for SmsGgEmulatorConfig {
68    fn with_overclocking_disabled(&self) -> Self {
69        Self { z80_divider: NonZeroU32::new(crate::NATIVE_Z80_DIVIDER).unwrap(), ..self.clone() }
70    }
71}
72
73impl SmsGgEmulatorConfig {
74    pub(crate) fn region(&self, memory: &Memory) -> SmsGgRegion {
75        self.forced_region.unwrap_or_else(|| memory.guess_cartridge_region())
76    }
77
78    pub(crate) fn render_options(&self, hardware: SmsGgHardware) -> RenderFrameOptions {
79        match hardware {
80            SmsGgHardware::MasterSystem | SmsGgHardware::Sg1000 => self.sms_render_options(),
81            SmsGgHardware::GameGear => self.gg_render_options(),
82        }
83    }
84
85    pub(crate) fn sms_render_options(&self) -> RenderFrameOptions {
86        RenderFrameOptions {
87            pixel_aspect_ratio: self.sms_aspect_ratio.to_pixel_aspect_ratio(),
88            composite_params: Some(CompositeParams {
89                upscale_factor: 10,
90                samples_per_color_cycle: SamplesPerColorCycle::Fifteen,
91            }),
92            ..RenderFrameOptions::default()
93        }
94    }
95
96    pub(crate) fn gg_render_options(&self) -> RenderFrameOptions {
97        RenderFrameOptions {
98            pixel_aspect_ratio: self.gg_aspect_ratio.to_pixel_aspect_ratio(),
99            frame_blending: self.gg_frame_blending,
100            ..RenderFrameOptions::default()
101        }
102    }
103}
104
105#[derive(Debug, Clone, Encode, Decode, PartialClone)]
106pub struct SmsGgEmulator {
107    #[partial_clone(partial)]
108    memory: Memory,
109    z80: Z80,
110    vdp: Vdp,
111    vdp_version: VdpVersion,
112    psg: Sn76489,
113    ym2413: Option<Ym2413>,
114    input: InputState,
115    audio_resampler: AudioResampler,
116    frame_buffer: BoxedColorArray<{ vdp::FRAME_BUFFER_LEN }>,
117    config: SmsGgEmulatorConfig,
118    vdp_mclk_counter: u32,
119    psg_mclk_counter: u32,
120    frame_count: u64,
121    reset_frames_remaining: u32,
122}
123
124const VDP_DIVIDER: u32 = 10;
125const PSG_DIVIDER: u32 = 15;
126
127const YM2413_CLOCK_INTERVAL: u8 = 72;
128
129impl SmsGgEmulator {
130    #[must_use]
131    pub fn create<S: SaveWriter>(
132        rom: Option<Vec<u8>>,
133        bios_rom: Option<Vec<u8>>,
134        hardware: SmsGgHardware,
135        config: SmsGgEmulatorConfig,
136        save_writer: &mut S,
137    ) -> Self {
138        let cartridge_ram = save_writer.load_bytes("sav").ok();
139
140        let vdp_version = determine_vdp_version(hardware, &config);
141        let psg_version = determine_psg_version(hardware, &config);
142
143        log::info!("VDP version: {vdp_version:?}");
144        log::info!("PSG version: {psg_version:?}");
145
146        let rom = match rom {
147            Some(rom) if !rom.is_empty() => rom,
148            _ => vec![0xFF; 0x8000],
149        };
150        let memory = Memory::new(rom, bios_rom, cartridge_ram, hardware, &config);
151        let vdp = Vdp::new(vdp_version, &config);
152        let psg = Sn76489::new(psg_version);
153        let input = InputState::new(config.region(&memory), &config);
154
155        log::info!("Region in cartridge header: {:?}", memory.guess_cartridge_region());
156
157        let mut z80 = Z80::new();
158        init_z80(&mut z80);
159
160        let ym2413 =
161            config.fm_sound_unit_enabled.then(|| ym_opll::new_ym2413(YM2413_CLOCK_INTERVAL));
162
163        let timing_mode = vdp.timing_mode();
164        Self {
165            memory,
166            z80,
167            vdp,
168            vdp_version,
169            psg,
170            ym2413,
171            input,
172            audio_resampler: AudioResampler::new(timing_mode),
173            frame_buffer: BoxedColorArray::new(),
174            config,
175            vdp_mclk_counter: 0,
176            psg_mclk_counter: 0,
177            frame_count: 0,
178            reset_frames_remaining: 0,
179        }
180    }
181
182    #[must_use]
183    pub fn hardware(&self) -> SmsGgHardware {
184        self.vdp_version.hardware()
185    }
186
187    #[must_use]
188    pub fn vdp_version(&self) -> VdpVersion {
189        self.vdp_version
190    }
191
192    #[inline]
193    #[must_use]
194    pub fn has_sram(&self) -> bool {
195        self.memory.cartridge_has_battery()
196    }
197
198    fn render_frame<R: Renderer>(&mut self, renderer: &mut R) -> Result<(), R::Err> {
199        populate_frame_buffer(
200            self.vdp.frame_buffer(),
201            self.vdp.viewport(),
202            self.vdp_version,
203            self.config.sms_crop_vertical_border,
204            self.config.sms_crop_left_border,
205            self.frame_buffer.as_mut_slice(),
206        );
207
208        let viewport = self.vdp.viewport();
209        let frame_width = if self.config.sms_crop_left_border {
210            viewport.width_without_border().into()
211        } else {
212            viewport.width.into()
213        };
214        let frame_height = if self.config.sms_crop_vertical_border {
215            viewport.height_without_border().into()
216        } else {
217            viewport.height.into()
218        };
219
220        let frame_size = FrameSize { width: frame_width, height: frame_height };
221        renderer.render_frame(
222            self.frame_buffer.as_slice(),
223            frame_size,
224            self.target_fps(),
225            self.config.render_options(self.vdp_version.hardware()),
226        )
227    }
228
229    pub fn copy_cram(&self, out: &mut [Color]) {
230        self.vdp.copy_cram(out);
231    }
232
233    pub fn copy_vram(&self, out: &mut [Color], palette: u8, row_len: usize) {
234        self.vdp.copy_vram(out, palette, row_len);
235    }
236
237    pub fn dump_vdp_registers(&self, callback: impl FnMut(u32, &[(&str, &str)])) {
238        self.vdp.dump_registers(callback);
239    }
240}
241
242fn init_z80(z80: &mut Z80) {
243    z80.set_pc(0x0000);
244    z80.set_sp(0xDFFF);
245    z80.set_interrupt_mode(InterruptMode::Mode1);
246}
247
248fn determine_vdp_version(hardware: SmsGgHardware, config: &SmsGgEmulatorConfig) -> VdpVersion {
249    match (hardware, config.sms_timing_mode, config.sms_model) {
250        (SmsGgHardware::MasterSystem, TimingMode::Ntsc, SmsModel::Sms1) => {
251            VdpVersion::NtscMasterSystem1
252        }
253        (SmsGgHardware::MasterSystem, TimingMode::Ntsc, SmsModel::Sms2) => {
254            VdpVersion::NtscMasterSystem2
255        }
256        (SmsGgHardware::MasterSystem, TimingMode::Pal, SmsModel::Sms1) => {
257            VdpVersion::PalMasterSystem1
258        }
259        (SmsGgHardware::MasterSystem, TimingMode::Pal, SmsModel::Sms2) => {
260            VdpVersion::PalMasterSystem2
261        }
262        (SmsGgHardware::GameGear, _, _) => VdpVersion::GameGear,
263        (SmsGgHardware::Sg1000, TimingMode::Ntsc, _) => VdpVersion::NtscSg1000,
264        (SmsGgHardware::Sg1000, TimingMode::Pal, _) => VdpVersion::PalSg1000,
265    }
266}
267
268fn determine_psg_version(hardware: SmsGgHardware, config: &SmsGgEmulatorConfig) -> Sn76489Version {
269    config.forced_psg_version.unwrap_or(match hardware {
270        SmsGgHardware::MasterSystem => Sn76489Version::MasterSystem2,
271        SmsGgHardware::GameGear => Sn76489Version::Standard,
272        SmsGgHardware::Sg1000 => Sn76489Version::Discrete,
273    })
274}
275
276impl EmulatorTrait for SmsGgEmulator {
277    type Button = SmsGgButton;
278    type Inputs = SmsGgInputs;
279    type Config = SmsGgEmulatorConfig;
280    type SaveState = Self;
281
282    type Err<
283        RErr: Debug + Display + Send + Sync + 'static,
284        AErr: Debug + Display + Send + Sync + 'static,
285        SErr: Debug + Display + Send + Sync + 'static,
286    > = SmsGgError<RErr, AErr, SErr>;

Execute a single CPU instruction and run the rest of the components for the corresponding number of cycles.

Errors

This method will propagate any errors encountered while rendering frames, pushing audio samples, or persisting cartridge SRAM.

295    #[inline]
296    fn tick<R, A, I, S>(
297        &mut self,
298        renderer: &mut R,
299        audio_output: &mut A,
300        input_poller: &mut I,
301        save_writer: &mut S,
302    ) -> SmsGgResult<R::Err, A::Err, S::Err>
303    where
304        R: Renderer,
305        A: AudioOutput,
306        I: InputPoller<Self::Inputs>,
307        S: SaveWriter,
308    {
309        self.input.set_inputs(*input_poller.poll());
310
311        let z80_t_cycles = self.z80.execute_instruction(&mut Bus::new(
312            self.vdp_version,
313            &mut self.memory,
314            &mut self.vdp,
315            &mut self.psg,
316            self.ym2413.as_mut(),
317            &mut self.input,
318        ));
319
320        let mclk_cycles = z80_t_cycles * self.config.z80_divider.get();
321        self.vdp_mclk_counter += mclk_cycles;
322        self.psg_mclk_counter += mclk_cycles;
323
324        while self.psg_mclk_counter >= PSG_DIVIDER {
325            self.psg_mclk_counter -= PSG_DIVIDER;
326
327            if let Some(ym2413) = &mut self.ym2413 {
328                ym2413.tick();
329            }
330            if self.psg.tick() == Sn76489TickEffect::Clocked {
331                let (psg_sample_l, psg_sample_r) =
332                    if self.memory.psg_enabled() { self.psg.sample() } else { (0.0, 0.0) };
333                let ym_sample = if self.memory.fm_enabled() {
334                    self.ym2413.as_ref().map_or(0.0, Ym2413::sample)
335                } else {
336                    0.0
337                };
338
339                let sample_l = psg_sample_l + ym_sample;
340                let sample_r = psg_sample_r + ym_sample;
341                self.audio_resampler.collect_sample(sample_l, sample_r);
342            }
343        }
344
345        self.audio_resampler.output_samples(audio_output).map_err(SmsGgError::Audio)?;
346
347        let mut frame_rendered = false;
348        while self.vdp_mclk_counter >= VDP_DIVIDER {
349            self.vdp_mclk_counter -= VDP_DIVIDER;
350
351            if self.vdp.tick() == VdpTickEffect::FrameComplete {
352                self.render_frame(renderer).map_err(SmsGgError::Render)?;
353                frame_rendered = true;
354
355                self.input.set_reset(self.reset_frames_remaining != 0);
356                self.reset_frames_remaining = self.reset_frames_remaining.saturating_sub(1);
357
358                self.frame_count += 1;
359                if self.frame_count.is_multiple_of(60)
360                    && self.memory.cartridge_has_battery()
361                    && self.memory.cartridge_ram_dirty()
362                {
363                    self.memory.clear_cartridge_ram_dirty();
364                    save_writer
365                        .persist_bytes("sav", self.memory.cartridge_ram())
366                        .map_err(SmsGgError::SaveWrite)?;
367                }
368            }
369        }
370
371        Ok(if frame_rendered { TickEffect::FrameRendered } else { TickEffect::None })
372    }
374    fn force_render<R>(&mut self, renderer: &mut R) -> Result<(), R::Err>
375    where
376        R: Renderer,
377    {
378        self.render_frame(renderer)
379    }
380
381    fn reload_config(&mut self, config: &Self::Config) {
382        self.config = config.clone();
383
384        let hardware = self.hardware();
385        self.vdp_version = determine_vdp_version(hardware, config);
386        self.vdp.update_config(self.vdp_version, config);
387
388        self.psg.set_version(determine_psg_version(hardware, config));
389
390        self.memory.update_config(config);
391
392        self.input.reload_config(config.region(&self.memory), config);
393        self.audio_resampler.update_timing_mode(self.vdp.timing_mode());
394    }
395
396    fn soft_reset(&mut self) {
397        log::info!("Soft resetting console");
398
399        // The SMS RESET button only sets a bit in a register; emulate "soft reset" by keeping the
400        // button virtually held down for 5 frames
401        self.reset_frames_remaining = 5;
402    }
403
404    fn hard_reset<S: SaveWriter>(&mut self, _save_writer: &mut S) {
405        log::info!("Hard resetting console");
406
407        self.memory.reset(&self.config);
408
409        self.z80 = Z80::new();
410        init_z80(&mut self.z80);
411
412        self.vdp = Vdp::new(self.vdp_version, &self.config);
413        self.psg = Sn76489::new(self.psg.version());
414        self.input = InputState::new(self.input.region(), &self.config);
415
416        self.ym2413 =
417            self.config.fm_sound_unit_enabled.then(|| ym_opll::new_ym2413(YM2413_CLOCK_INTERVAL));
418
419        self.frame_buffer = BoxedColorArray::new();
420
421        self.vdp_mclk_counter = 0;
422        self.psg_mclk_counter = 0;
423        self.frame_count = 0;
424        self.reset_frames_remaining = 0;
425    }
426
427    fn load_state(&mut self, mut state: Self::SaveState) {
428        state.memory.take_rom_from(&mut self.memory);
429        *self = state;
430    }
431
432    fn to_save_state(&self) -> Self::SaveState {
433        self.partial_clone()
434    }
435
436    fn target_fps(&self) -> f64 {
437        let timing_mode = self.vdp.timing_mode();
438        let mclk_frequency = timing_mode.mclk_frequency();
439        let scanlines_per_frame = match timing_mode {
440            TimingMode::Ntsc => vdp::NTSC_SCANLINES_PER_FRAME,
441            TimingMode::Pal => vdp::PAL_SCANLINES_PER_FRAME,
442        };
443
444        mclk_frequency / f64::from(vdp::MCLK_CYCLES_PER_SCANLINE) / f64::from(scanlines_per_frame)
445    }
446
447    fn update_audio_output_frequency(&mut self, output_frequency: u64) {
448        self.audio_resampler.update_output_frequency(output_frequency);
449    }
450}
451
452fn populate_frame_buffer(
453    vdp_buffer: &VdpBuffer,
454    viewport: ViewportSize,
455    vdp_version: VdpVersion,
456    crop_vertical_border: bool,
457    crop_left_border: bool,
458    frame_buffer: &mut [Color],
459) {
460    let (row_skip, row_take) = if crop_vertical_border {
461        (viewport.top_border_height as usize, viewport.height_without_border() as usize)
462    } else {
463        (0, viewport.height as usize)
464    };
465    let (col_skip, screen_width) = if crop_left_border {
466        (viewport.left_border_width as usize, viewport.width_without_border() as usize)
467    } else {
468        (0, viewport.width as usize)
469    };
470
471    for (i, row) in vdp_buffer.iter().skip(row_skip).take(row_take).enumerate() {
472        for (j, color) in row.iter().copied().skip(col_skip).enumerate() {
473            let color = match vdp_version.hardware() {
474                SmsGgHardware::MasterSystem => Color::rgb(
475                    vdp::convert_sms_color(color & 0x03),
476                    vdp::convert_sms_color((color >> 2) & 0x03),
477                    vdp::convert_sms_color((color >> 4) & 0x03),
478                ),
479                SmsGgHardware::GameGear => Color::rgb(
480                    vdp::convert_gg_color(color & 0x0F),
481                    vdp::convert_gg_color((color >> 4) & 0x0F),
482                    vdp::convert_gg_color((color >> 8) & 0x0F),
483                ),
484                SmsGgHardware::Sg1000 => vdp::convert_sg_color(color),
485            };
486
487            frame_buffer[i * screen_width + j] = color;
488        }
489    }
490}