api.rsannotatedapi.rssource369 lines · 12.0 KB · raw

Sega CD public interface and main loop

3pub mod debug;
5use crate::api::debug::SegaCdDebugger;
6use crate::bus::SegaCdBus;
7use crate::bus::debug::DebugSegaCdBus;
8use crate::memory;
9use bincode::{Decode, Encode};
10use cdrom::CdRomError;
11use cdrom::cdtime::CdTime;
12use cdrom::reader::CdRom;
13use genesis_components::cartridge::GenesisRegionExt;
14use genesis_config::GenesisEmulatorConfig;
15use genesis_config::GenesisRegion;
16use jgenesis_common::frontend::{PartialClone, TimingMode};
17use jgenesis_common::num::U16Ext;
18use m68000_emu::M68000;
19use std::fmt::Debug;
20use std::mem;
21use thiserror::Error;
22
23pub const DEFAULT_SUB_CPU_DIVIDER: u64 = genesis_config::NATIVE_SUB_CPU_DIVIDER;
24
25const NTSC_GENESIS_MASTER_CLOCK_RATE: u64 = genesis_components::NTSC_GENESIS_MCLK_FREQUENCY as u64;
26const PAL_GENESIS_MASTER_CLOCK_RATE: u64 = genesis_components::PAL_GENESIS_MCLK_FREQUENCY as u64;
27pub const SEGA_CD_MASTER_CLOCK_RATE: u64 = 50_000_000;
28
29const BIOS_LEN: usize = memory::BIOS_LEN;
30
31#[derive(Debug, Error)]
32pub enum SegaCdLoadError {
33    #[error("BIOS is required for Sega CD emulation")]
34    MissingBios,
35    #[error("BIOS must be {BIOS_LEN} bytes, was {bios_len} bytes")]
36    InvalidBios { bios_len: usize },
37    #[error("CD-ROM-related error: {0}")]
38    CdRom(#[from] CdRomError),
39}
40
41pub type SegaCdLoadResult<T> = Result<T, SegaCdLoadError>;
42
43pub trait SegaCdAudioOutput {
44    fn collect_pcm(&mut self, sample: (f64, f64));
45
46    fn collect_cd(&mut self, sample: (f64, f64));
47}
48
49#[derive(Debug, Encode, Decode, PartialClone)]
50pub struct SegaCd {
51    sub_cpu: M68000,
52    #[partial_clone(partial)]
53    bus: SegaCdBus,
54    disc_title: Option<String>,
55    six_button_incompatible_game: bool,
56    timing_mode: TimingMode,
57    sega_cd_mclk_cycles: u64,
58    sega_cd_mclk_cycle_product: u64,
59    sub_cpu_divider: u64,
60    sub_cpu_wait_cycles: u64,
61    config: GenesisEmulatorConfig,
62}
63
64impl SegaCd {

Errors

Returns an error if the BIOS ROM is not the correct size or if there is an error reading the CD-ROM image.

69    pub fn new(
70        bios: Vec<u8>,
71        disc: Option<CdRom>,
72        timing_mode: TimingMode,
73        initial_backup_ram: Option<Vec<u8>>,
74        initial_ram_cartridge: Option<Vec<u8>>,
75        config: &GenesisEmulatorConfig,
76    ) -> SegaCdLoadResult<Self> {
77        if bios.len() != memory::BIOS_LEN {
78            return Err(SegaCdLoadError::InvalidBios { bios_len: bios.len() });
79        }
80
81        let mut bus =
82            SegaCdBus::new(bios, disc, initial_backup_ram, initial_ram_cartridge, config)?;
83
84        let disc_title = bus.disc_title()?;
85        let six_button_incompatible_game = bus.has_six_button_incompatible_game()?;
86
87        let sub_cpu = M68000::builder().allow_tas_writes(true).name("Sub".into()).build();
88        let sub_cpu_divider = config.sega_cd.sub_cpu_divider.get();
89
90        Ok(Self {
91            sub_cpu,
92            bus,
93            timing_mode,
94            disc_title,
95            six_button_incompatible_game,
96            sega_cd_mclk_cycles: 0,
97            sega_cd_mclk_cycle_product: 0,
98            sub_cpu_divider,
99            sub_cpu_wait_cycles: 0,
100            config: config.clone(),
101        })
102    }

Errors

Propagates any CD-ROM disc read errors.

107    #[inline]
108    pub fn tick<const DEBUG: bool>(
109        &mut self,
110        genesis_mclk_elapsed: u64,
111        audio_output: &mut impl SegaCdAudioOutput,
112        debugger: &mut impl SegaCdDebugger,
113    ) -> SegaCdLoadResult<()> {
114        self.sega_cd_mclk_cycle_product += genesis_mclk_elapsed * SEGA_CD_MASTER_CLOCK_RATE;
115        let scd_mclk_elapsed = match self.timing_mode {
116            TimingMode::Ntsc => {
117                let elapsed = self.sega_cd_mclk_cycle_product / NTSC_GENESIS_MASTER_CLOCK_RATE;
118                self.sega_cd_mclk_cycle_product -= elapsed * NTSC_GENESIS_MASTER_CLOCK_RATE;
119                elapsed
120            }
121            TimingMode::Pal => {
122                let elapsed = self.sega_cd_mclk_cycle_product / PAL_GENESIS_MASTER_CLOCK_RATE;
123                self.sega_cd_mclk_cycle_product -= elapsed * PAL_GENESIS_MASTER_CLOCK_RATE;
124                elapsed
125            }
126        };
127
128        let prev_scd_mclk_cycles = self.sega_cd_mclk_cycles;
129        self.sega_cd_mclk_cycles += scd_mclk_elapsed;
130
131        let pcm_cycles = self.sega_cd_mclk_cycles / DEFAULT_SUB_CPU_DIVIDER
132            - prev_scd_mclk_cycles / DEFAULT_SUB_CPU_DIVIDER;
133        let elapsed_scd_mclk_cycles = self.sega_cd_mclk_cycles - prev_scd_mclk_cycles;
134
135        // This match seems silly, but it avoids doing an integer division for the common dividers
136        // of 1-4. Dividers higher than 4 can only be set via the CLI or by manually editing config
137        // (and underclocking probably won't work well anyway)
138        let sub_cpu_cycles = match self.sub_cpu_divider {
139            DEFAULT_SUB_CPU_DIVIDER => pcm_cycles,
140            3 => self.sega_cd_mclk_cycles / 3 - prev_scd_mclk_cycles / 3,
141            2 => (self.sega_cd_mclk_cycles >> 1) - (prev_scd_mclk_cycles >> 1),
142            1 => elapsed_scd_mclk_cycles,
143            _ => {
144                self.sega_cd_mclk_cycles / self.sub_cpu_divider
145                    - prev_scd_mclk_cycles / self.sub_cpu_divider
146            }
147        };
148
149        self.bus.tick_components(scd_mclk_elapsed, pcm_cycles, audio_output)?;
150
151        self.tick_sub_cpu::<DEBUG>(sub_cpu_cycles, debugger);
152
153        Ok(())
154    }
156    fn tick_sub_cpu<const DEBUG: bool>(
157        &mut self,
158        mut sub_cpu_cycles: u64,
159        debugger: &mut impl SegaCdDebugger,
160    ) {
161        if self.bus.word_ram().sub_performed_blocked_access() {
162            // If the sub CPU accesses word RAM while it's in 2M mode and owned by the main CPU, it
163            // should halt until the main CPU writes DMNA=1 to transfer ownership to the sub CPU.
164            // Marko's Magic Football depends on this or it will have glitched map graphics
165            log::trace!("Not running sub CPU because word RAM writes are buffered");
166            return;
167        }
168
169        while sub_cpu_cycles >= self.sub_cpu_wait_cycles {
170            self.bus.flush_buffered_sub_writes();
171
172            sub_cpu_cycles -= self.sub_cpu_wait_cycles;
173
174            self.sub_cpu_wait_cycles = if DEBUG {
175                let mut debug_bus = DebugSegaCdBus::new(&mut self.bus, debugger);
176                self.sub_cpu.execute_instruction(&mut debug_bus).into()
177            } else {
178                self.sub_cpu.execute_instruction(&mut self.bus).into()
179            };
180
181            if self.bus.word_ram().sub_performed_blocked_access() {
182                return;
183            }
184        }
185
186        self.sub_cpu_wait_cycles -= sub_cpu_cycles;
187    }
188
189    #[must_use]
190    #[inline]
191    pub fn main_read_memory<const WORD: bool>(&self, address: u32) -> u16 {
192        if address & 0x200000 == 0 {
193            // BIOS ROM / PRG RAM
194            if WORD {
195                self.bus.main_read_bios_prg_ram(address)
196            } else {
197                self.bus.main_read_bios_prg_ram(address & !1).be_byte(address & 1).into()
198            }
199        } else {
200            // Word RAM
201            if WORD {
202                let msb = self.bus.main_read_word_ram(address);
203                let lsb = self.bus.main_read_word_ram(address + 1);
204                u16::from_be_bytes([msb, lsb])
205            } else {
206                self.bus.main_read_word_ram(address).into()
207            }
208        }
209    }
210
211    #[inline]
212    pub fn main_write_memory<const WORD: bool>(&mut self, address: u32, value: u16) {
213        if address & 0x200000 == 0 {
214            // BIOS ROM / PRG RAM
215            self.bus.main_write_bios_prg_ram::<WORD>(address, value);
216        } else {
217            // Word RAM
218            if WORD {
219                self.bus.main_write_word_ram(address, value.msb());
220                self.bus.main_write_word_ram(address + 1, value.lsb());
221            } else {
222                self.bus.main_write_word_ram(address, value as u8);
223            }
224        }
225    }
226
227    #[must_use]
228    #[inline]
229    pub fn read_word_for_dma(&mut self, address: u32, open_bus: &mut u16) -> u16 {
230        if address & 0x200000 == 0 {
231            *open_bus = self.main_read_memory::<true>(address);
232            *open_bus
233        } else {
234            // Word RAM reads are delayed
235            mem::replace(open_bus, self.main_read_memory::<true>(address))
236        }
237    }
238
239    #[must_use]
240    #[inline]
241    pub fn read_ram_cartridge<const WORD: bool>(&self, mut address: u32) -> u16 {
242        if WORD {
243            address |= 1;
244        }
245        self.bus.read_ram_cartridge(address).into()
246    }
247
248    #[inline]
249    pub fn write_ram_cartridge<const WORD: bool>(&mut self, mut address: u32, value: u16) {
250        if WORD {
251            address |= 1;
252        }
253        self.bus.write_ram_cartridge(address, value as u8);
254    }
255
256    #[must_use]
257    #[inline]
258    pub fn main_read_register<const WORD: bool>(&mut self, address: u32) -> u16 {
259        self.bus.main_read_register::<WORD>(address)
260    }
261
262    #[inline]
263    pub fn main_write_register<const WORD: bool>(&mut self, address: u32, value: u16) {
264        self.bus.main_write_register::<WORD>(address, value);
265    }
266
267    pub fn reload_config(&mut self, config: &GenesisEmulatorConfig) {
268        self.sub_cpu_divider = config.sega_cd.sub_cpu_divider.get();
269        self.bus.reload_config(config);
270
271        self.config = config.clone();
272    }
273
274    pub fn reset(&mut self) {
275        self.bus.reset();
276    }
277
278    #[must_use]
279    pub fn disc_title(&self) -> Option<String> {
280        self.disc_title.clone()
281    }
282
283    #[must_use]
284    pub fn region(&self) -> GenesisRegion {
285        self.bus.region()
286    }
287
288    #[must_use]
289    pub fn has_six_button_incompatible_game(&self) -> bool {
290        self.six_button_incompatible_game
291    }
292
293    pub fn take_backup_ram_dirty(&mut self) -> bool {
294        self.bus.take_backup_ram_dirty()
295    }
296
297    #[must_use]
298    pub fn backup_ram(&self) -> &[u8] {
299        self.bus.backup_ram()
300    }
301
302    #[must_use]
303    pub fn ram_cartridge(&self) -> &[u8] {
304        self.bus.ram_cartridge()
305    }
306
307    #[must_use]
308    pub fn take_bios_and_disc(self) -> (Vec<u16>, Option<CdRom>) {
309        self.bus.take_bios_and_disc()
310    }
311
312    pub fn take_bios_and_disc_from(&mut self, other: &mut Self) {
313        self.bus.take_bios_and_disc_from(&mut other.bus);
314    }

Errors

Propagates any CD-ROM read errors.

319    pub fn change_disc(&mut self, disc: CdRom) -> SegaCdLoadResult<()> {
320        self.bus.change_disc(disc);
321        self.disc_title = self.bus.disc_title()?;
322
323        Ok(())
324    }
326    pub fn remove_disc(&mut self) {
327        self.bus.remove_disc();
328        self.disc_title = None;
329    }
330}

Attempt to parse a console region out of the CD-ROM's data track.

Returns None if unable to confidently determine region.

Errors

Propagates any errors encountered while reading the CD-ROM files from disk.

339pub fn parse_disc_region(disc: &mut CdRom) -> SegaCdLoadResult<GenesisRegion> {
340    // ROM header is always located at track 1 sector 0
341    let mut rom_header = vec![0; cdrom::BYTES_PER_SECTOR as usize];
342    disc.read_sector(1, CdTime::SECTOR_0_START, &mut rom_header)?;
343
344    // Sega CD ROM header starts at $010 because the first 16 bytes are sync + CD-ROM data track header
345    let region = GenesisRegion::from_rom(&rom_header[0x010..]).unwrap_or_else(|| {
346        log::warn!("Unable to determine region from disc header; defaulting to US");
347        GenesisRegion::Americas
348    });
349
350    let serial_number = &rom_header[0x190..0x1A0];
351
352    // The Smurfs (EU) has a US header but only works properly with PAL timings
353    if region == GenesisRegion::Americas && serial_number == b"GM T-151015-00  " {
354        return Ok(GenesisRegion::Europe);
355    }
356
357    // Hack to fix Snatcher (US/EU), which incorrectly reports its region as J in the header
358    if region == GenesisRegion::Japan && serial_number == b"GM T-95035 -00  " {
359        let console_name = &rom_header[0x110..0x120];
360        if console_name == "SEGA GENESIS    ".as_bytes() {
361            return Ok(GenesisRegion::Americas);
362        } else if console_name == "SEGA MEGA DRIVE ".as_bytes() {
363            return Ok(GenesisRegion::Europe);
364        }
365        // Any other console name is unexpected, leave region as-is
366    }
367
368    Ok(region)
369}