diff --git a/README.md b/README.md index 03a73b7d..5281e020 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,7 @@ Cross-platform multi-console Sega emulator that supports the Sega Genesis / Mega * Some simple horizontal blur and naive anti-dither shaders for blending dithered pixel patterns, which were extremely common on these consoles due to limited color palettes and lack of hardware-supported transparency * Optional 2x CPU overclocking for Sega Master System and Game Gear emulation -Major TODOs: -* Investigate and fix why _Silpheed_ randomly freezes during gameplay - -Minor TODOs: +TODOs: * Support multiple Sega CD BIOS versions in GUI and automatically use the correct one based on disc region * Support CHD files for Sega CD in addition to BIN/CUE * Investigate and fix a few minor issues, like the EA logo flickering for a single frame in _Galahad_ diff --git a/genesis-core/src/api.rs b/genesis-core/src/api.rs index f7193fbd..fceb2b77 100644 --- a/genesis-core/src/api.rs +++ b/genesis-core/src/api.rs @@ -2,7 +2,7 @@ use crate::audio::GenesisAudioDownsampler; use crate::input::{GenesisInputs, InputState}; -use crate::memory::{Cartridge, MainBus, MainBusSignals, Memory}; +use crate::memory::{Cartridge, MainBus, MainBusSignals, MainBusWrites, Memory}; use crate::vdp::{Vdp, VdpConfig, VdpTickEffect}; use crate::ym2612::{Ym2612, YmTickEffect}; use bincode::{Decode, Encode}; @@ -155,12 +155,29 @@ pub struct GenesisEmulator { ym2612: Ym2612, input: InputState, timing_mode: TimingMode, + main_bus_writes: MainBusWrites, aspect_ratio: GenesisAspectRatio, adjust_aspect_ratio_in_2x_resolution: bool, audio_downsampler: GenesisAudioDownsampler, master_clock_cycles: u64, } +// This is a macro instead of a function so that it only mutably borrows the needed fields +macro_rules! new_main_bus { + ($self:expr, m68k_reset: $m68k_reset:expr) => { + MainBus::new( + &mut $self.memory, + &mut $self.vdp, + &mut $self.psg, + &mut $self.ym2612, + &mut $self.input, + $self.timing_mode, + MainBusSignals { z80_busack: $self.z80.stalled(), m68k_reset: $m68k_reset }, + std::mem::take(&mut $self.main_bus_writes), + ) + }; +} + impl GenesisEmulator { /// Initialize the emulator from the given ROM. /// @@ -200,6 +217,7 @@ impl GenesisEmulator { &mut input, timing_mode, MainBusSignals { z80_busack: false, m68k_reset: true }, + MainBusWrites::new(), )); Self { @@ -210,11 +228,12 @@ impl GenesisEmulator { psg, ym2612, input, + timing_mode, + main_bus_writes: MainBusWrites::new(), aspect_ratio: config.aspect_ratio, adjust_aspect_ratio_in_2x_resolution: config.adjust_aspect_ratio_in_2x_resolution, audio_downsampler: GenesisAudioDownsampler::new(timing_mode), master_clock_cycles: 0, - timing_mode, } } @@ -302,15 +321,7 @@ impl TickableEmulator for GenesisEmulator { S: SaveWriter, S::Err: Debug + Display + Send + Sync + 'static, { - let mut bus = MainBus::new( - &mut self.memory, - &mut self.vdp, - &mut self.psg, - &mut self.ym2612, - &mut self.input, - self.timing_mode, - MainBusSignals { z80_busack: self.z80.stalled(), m68k_reset: false }, - ); + let mut bus = new_main_bus!(self, m68k_reset: false); let m68k_cycles = self.m68k.execute_instruction(&mut bus); let elapsed_mclk_cycles = u64::from(m68k_cycles) * M68K_MCLK_DIVIDER; @@ -322,6 +333,8 @@ impl TickableEmulator for GenesisEmulator { self.z80.tick(&mut bus); } + self.main_bus_writes = bus.apply_writes(); + self.memory.medium_mut().tick(m68k_cycles); self.input.tick(m68k_cycles); @@ -377,15 +390,7 @@ impl Resettable for GenesisEmulator { fn soft_reset(&mut self) { log::info!("Soft resetting console"); - self.m68k.execute_instruction(&mut MainBus::new( - &mut self.memory, - &mut self.vdp, - &mut self.psg, - &mut self.ym2612, - &mut self.input, - self.timing_mode, - MainBusSignals { z80_busack: false, m68k_reset: true }, - )); + self.m68k.execute_instruction(&mut new_main_bus!(self, m68k_reset: true)); self.memory.reset_z80_signals(); self.ym2612.reset(); } diff --git a/genesis-core/src/memory.rs b/genesis-core/src/memory.rs index 9ef2492f..2e95adec 100644 --- a/genesis-core/src/memory.rs +++ b/genesis-core/src/memory.rs @@ -119,6 +119,7 @@ impl Cartridge { Self { rom: Rom(rom_bytes), external_memory, ram_mapped, mapper, svp, region } } + #[inline] pub fn tick(&mut self, m68k_cycles: u32) { if let Some(svp) = &mut self.svp { svp.tick(&self.rom.0, m68k_cycles); @@ -197,6 +198,7 @@ pub trait PhysicalMedium { } impl PhysicalMedium for Cartridge { + #[inline] fn read_byte(&mut self, address: u32) -> u8 { if let Some(svp) = &mut self.svp { let word = svp.m68k_read(address & !1, &self.rom.0); @@ -213,6 +215,7 @@ impl PhysicalMedium for Cartridge { self.rom.get(rom_addr as usize).unwrap_or(0xFF) } + #[inline] fn read_word(&mut self, address: u32) -> u16 { if let Some(svp) = &mut self.svp { return svp.m68k_read(address, &self.rom.0); @@ -230,6 +233,7 @@ impl PhysicalMedium for Cartridge { u16::from_be_bytes([msb, lsb]) } + #[inline] fn read_word_for_dma(&mut self, address: u32) -> u16 { if self.svp.is_some() { // SVP cartridge memory has the same delay issue as Sega CD word RAM; Virtua Racing sets @@ -240,6 +244,7 @@ impl PhysicalMedium for Cartridge { } } + #[inline] fn write_byte(&mut self, address: u32, value: u8) { if let Some(svp) = &mut self.svp { svp.m68k_write_byte(address, value); @@ -259,6 +264,7 @@ impl PhysicalMedium for Cartridge { } } + #[inline] fn write_word(&mut self, address: u32, value: u16) { if let Some(svp) = &mut self.svp { svp.m68k_write_word(address, value); @@ -278,6 +284,7 @@ impl PhysicalMedium for Cartridge { } } + #[inline] fn region(&self) -> GenesisRegion { self.region } @@ -354,21 +361,25 @@ impl Memory { } } + #[inline] #[must_use] pub fn hardware_region(&self) -> GenesisRegion { self.physical_medium.region() } + #[inline] #[must_use] pub fn medium(&self) -> &Medium { &self.physical_medium } + #[inline] #[must_use] pub fn medium_mut(&mut self) -> &mut Medium { &mut self.physical_medium } + #[inline] pub fn reset_z80_signals(&mut self) { self.signals = Signals::default(); } @@ -394,16 +405,19 @@ impl Memory { self.physical_medium.program_title() } + #[inline] #[must_use] pub fn external_ram(&self) -> &[u8] { self.physical_medium.external_ram() } + #[inline] #[must_use] pub fn is_external_ram_persistent(&self) -> bool { self.physical_medium.is_ram_persistent() } + #[inline] #[must_use] pub fn get_and_clear_external_ram_dirty(&mut self) -> bool { self.physical_medium.get_and_clear_ram_dirty() @@ -416,6 +430,25 @@ pub struct MainBusSignals { pub m68k_reset: bool, } +#[derive(Debug, Clone, Default, Encode, Decode)] +pub struct MainBusWrites { + byte: Vec<(u32, u8)>, + word: Vec<(u32, u16)>, +} + +impl MainBusWrites { + #[inline] + #[must_use] + pub fn new() -> Self { + Self { byte: Vec::with_capacity(20), word: Vec::with_capacity(20) } + } + + fn clear(&mut self) { + self.byte.clear(); + self.word.clear(); + } +} + pub struct MainBus<'a, Medium> { memory: &'a mut Memory, vdp: &'a mut Vdp, @@ -424,9 +457,12 @@ pub struct MainBus<'a, Medium> { input: &'a mut InputState, timing_mode: TimingMode, signals: MainBusSignals, + pending_writes: MainBusWrites, } impl<'a, Medium: PhysicalMedium> MainBus<'a, Medium> { + #[allow(clippy::too_many_arguments)] + #[inline] pub fn new( memory: &'a mut Memory, vdp: &'a mut Vdp, @@ -435,12 +471,11 @@ impl<'a, Medium: PhysicalMedium> MainBus<'a, Medium> { input: &'a mut InputState, timing_mode: TimingMode, signals: MainBusSignals, + pending_writes: MainBusWrites, ) -> Self { - Self { memory, vdp, psg, ym2612, input, timing_mode, signals } + Self { memory, vdp, psg, ym2612, input, timing_mode, signals, pending_writes } } - // TODO remove - #[allow(clippy::match_same_arms)] fn read_io_register(&self, address: u32) -> u8 { match address { // Version register @@ -510,70 +545,33 @@ impl<'a, Medium: PhysicalMedium> MainBus<'a, Medium> { _ => unreachable!("address & 0x1F is always <= 0x1F"), } } -} -// The Genesis has a 24-bit bus, not 32-bit -const ADDRESS_MASK: u32 = 0xFFFFFF; - -impl<'a, Medium: PhysicalMedium> m68000_emu::BusInterface for MainBus<'a, Medium> { + /// Take the pending writes Vecs without applying them #[inline] - fn read_byte(&mut self, address: u32) -> u8 { - let address = address & ADDRESS_MASK; - log::trace!("Main bus byte read, address={address:06X}"); - match address { - 0x000000..=0x7FFFFF | 0xA12000..=0xA1500F => { - self.memory.physical_medium.read_byte(address) - } - 0xA00000..=0xA0FFFF => { - // Z80 memory map - // For 68k access, $8000-$FFFF mirrors $0000-$7FFF - ::read_memory(self, (address & 0x7FFF) as u16) - } - 0xA10000..=0xA1001F => self.read_io_register(address), - 0xA11100..=0xA11101 => (!self.signals.z80_busack).into(), - 0xC00000..=0xC0001F => self.read_vdp_byte(address), - 0xE00000..=0xFFFFFF => self.memory.main_ram[(address & 0xFFFF) as usize], - _ => 0xFF, - } + #[must_use] + pub fn take_writes(self) -> MainBusWrites { + self.pending_writes } + /// Apply all pending writes, then clear and return the pending writes Vecs #[inline] - fn read_word(&mut self, address: u32) -> u16 { - let address = address & ADDRESS_MASK; - log::trace!("Main bus word read, address={address:06X}"); - match address { - 0x000000..=0x7FFFFF | 0xA12000..=0xA1500F => { - self.memory.physical_medium.read_word(address) - } - 0xA00000..=0xA0FFFF => { - // All Z80 access is byte-size; word reads mirror the byte in both MSB and LSB - let byte = self.read_byte(address); - u16::from_le_bytes([byte, byte]) - } - 0xA10000..=0xA1001F => self.read_io_register(address).into(), - 0xA11100..=0xA11101 => { - // Word reads of Z80 BUSREQ signal mirror the byte in both MSB and LSB - let byte: u8 = (!self.signals.z80_busack).into(); - u16::from_le_bytes([byte, byte]) - } - 0xC00000..=0xC00003 => self.vdp.read_data(), - 0xC00004..=0xC00007 => self.vdp.read_status(), - 0xC00008..=0xC0000F => self.vdp.hv_counter(), - 0xE00000..=0xFFFFFF => { - let ram_addr = (address & 0xFFFF) as usize; - u16::from_be_bytes([ - self.memory.main_ram[ram_addr], - self.memory.main_ram[(ram_addr + 1) & 0xFFFF], - ]) - } - _ => 0xFFFF, + #[must_use] + pub fn apply_writes(mut self) -> MainBusWrites { + let mut pending_writes = mem::take(&mut self.pending_writes); + + for &(address, value) in &pending_writes.byte { + self.apply_byte_write(address, value); } + + for &(address, value) in &pending_writes.word { + self.apply_word_write(address, value); + } + + pending_writes.clear(); + pending_writes } - #[inline] - // TODO remove - #[allow(clippy::match_same_arms)] - fn write_byte(&mut self, address: u32, value: u8) { + fn apply_byte_write(&mut self, address: u32, value: u8) { let address = address & ADDRESS_MASK; log::trace!("Main bus byte write: address={address:06X}, value={value:02X}"); match address { @@ -610,10 +608,7 @@ impl<'a, Medium: PhysicalMedium> m68000_emu::BusInterface for MainBus<'a, Medium } } - #[inline] - // TODO remove - #[allow(clippy::match_same_arms)] - fn write_word(&mut self, address: u32, value: u16) { + fn apply_word_write(&mut self, address: u32, value: u16) { let address = address & ADDRESS_MASK; log::trace!("Main bus word write: address={address:06X}, value={value:02X}"); match address { @@ -622,7 +617,7 @@ impl<'a, Medium: PhysicalMedium> m68000_emu::BusInterface for MainBus<'a, Medium } 0xA00000..=0xA0FFFF => { // Z80 memory map; word-size writes write the MSB as a byte-size write - self.write_byte(address, (value >> 8) as u8); + self.apply_byte_write(address, (value >> 8) as u8); } 0xA10000..=0xA1001F => { self.write_io_register(address, value as u8); @@ -649,6 +644,75 @@ impl<'a, Medium: PhysicalMedium> m68000_emu::BusInterface for MainBus<'a, Medium _ => {} } } +} + +// The Genesis has a 24-bit bus, not 32-bit +const ADDRESS_MASK: u32 = 0xFFFFFF; + +impl<'a, Medium: PhysicalMedium> m68000_emu::BusInterface for MainBus<'a, Medium> { + #[inline] + fn read_byte(&mut self, address: u32) -> u8 { + let address = address & ADDRESS_MASK; + log::trace!("Main bus byte read, address={address:06X}"); + match address { + 0x000000..=0x7FFFFF | 0xA12000..=0xA1500F => { + self.memory.physical_medium.read_byte(address) + } + 0xA00000..=0xA0FFFF => { + // Z80 memory map + // For 68k access, $8000-$FFFF mirrors $0000-$7FFF + ::read_memory(self, (address & 0x7FFF) as u16) + } + 0xA10000..=0xA1001F => self.read_io_register(address), + 0xA11100..=0xA11101 => (!self.signals.z80_busack).into(), + 0xC00000..=0xC0001F => self.read_vdp_byte(address), + 0xE00000..=0xFFFFFF => self.memory.main_ram[(address & 0xFFFF) as usize], + _ => 0xFF, + } + } + + #[inline] + fn read_word(&mut self, address: u32) -> u16 { + let address = address & ADDRESS_MASK; + log::trace!("Main bus word read, address={address:06X}"); + match address { + 0x000000..=0x7FFFFF | 0xA12000..=0xA1500F => { + self.memory.physical_medium.read_word(address) + } + 0xA00000..=0xA0FFFF => { + // All Z80 access is byte-size; word reads mirror the byte in both MSB and LSB + let byte = self.read_byte(address); + u16::from_le_bytes([byte, byte]) + } + 0xA10000..=0xA1001F => self.read_io_register(address).into(), + 0xA11100..=0xA11101 => { + // Word reads of Z80 BUSREQ signal mirror the byte in both MSB and LSB + let byte: u8 = (!self.signals.z80_busack).into(); + u16::from_le_bytes([byte, byte]) + } + 0xC00000..=0xC00003 => self.vdp.read_data(), + 0xC00004..=0xC00007 => self.vdp.read_status(), + 0xC00008..=0xC0000F => self.vdp.hv_counter(), + 0xE00000..=0xFFFFFF => { + let ram_addr = (address & 0xFFFF) as usize; + u16::from_be_bytes([ + self.memory.main_ram[ram_addr], + self.memory.main_ram[(ram_addr + 1) & 0xFFFF], + ]) + } + _ => 0xFFFF, + } + } + + #[inline] + fn write_byte(&mut self, address: u32, value: u8) { + self.pending_writes.byte.push((address, value)); + } + + #[inline] + fn write_word(&mut self, address: u32, value: u16) { + self.pending_writes.word.push((address, value)); + } #[inline] fn interrupt_level(&self) -> u8 { @@ -660,10 +724,12 @@ impl<'a, Medium: PhysicalMedium> m68000_emu::BusInterface for MainBus<'a, Medium self.vdp.acknowledge_m68k_interrupt(); } + #[inline] fn halt(&self) -> bool { self.vdp.should_halt_cpu() } + #[inline] fn reset(&self) -> bool { self.signals.m68k_reset } @@ -760,7 +826,7 @@ impl<'a, Medium: PhysicalMedium> z80_emu::BusInterface for MainBus<'a, Medium> { 0x8000..=0xFFFF => { let m68k_addr = self.memory.z80_bank_register.map_to_68k_address(address); if !(0xA00000..=0xA0FFFF).contains(&m68k_addr) { - ::write_byte(self, m68k_addr, value); + self.apply_byte_write(m68k_addr, value); } else { // TODO this should lock up the system panic!( diff --git a/segacd-core/src/api.rs b/segacd-core/src/api.rs index fd519dd3..5abf41c3 100644 --- a/segacd-core/src/api.rs +++ b/segacd-core/src/api.rs @@ -10,7 +10,7 @@ use crate::memory::{SegaCd, SubBus}; use crate::rf5c164::{PcmTickEffect, Rf5c164}; use bincode::{Decode, Encode}; use genesis_core::input::InputState; -use genesis_core::memory::{MainBus, MainBusSignals, Memory}; +use genesis_core::memory::{MainBus, MainBusSignals, MainBusWrites, Memory}; use genesis_core::vdp::{Vdp, VdpTickEffect}; use genesis_core::ym2612::{Ym2612, YmTickEffect}; use genesis_core::{ @@ -122,6 +122,7 @@ pub struct SegaCdEmulator { input: InputState, audio_downsampler: AudioDownsampler, timing_mode: TimingMode, + main_bus_writes: MainBusWrites, aspect_ratio: GenesisAspectRatio, adjust_aspect_ratio_in_2x_resolution: bool, disc_title: String, @@ -131,6 +132,22 @@ pub struct SegaCdEmulator { sub_cpu_wait_cycles: u64, } +// This is a macro instead of a function so that it only mutably borrows the needed fields +macro_rules! new_main_bus { + ($self:expr, m68k_reset: $m68k_reset:expr) => { + MainBus::new( + &mut $self.memory, + &mut $self.vdp, + &mut $self.psg, + &mut $self.ym2612, + &mut $self.input, + $self.timing_mode, + MainBusSignals { z80_busack: $self.z80.stalled(), m68k_reset: $m68k_reset }, + std::mem::take(&mut $self.main_bus_writes), + ) + }; +} + impl SegaCdEmulator { /// # Errors /// @@ -210,6 +227,7 @@ impl SegaCdEmulator { &mut input, timing_mode, MainBusSignals { z80_busack: false, m68k_reset: true }, + MainBusWrites::new(), )); let audio_downsampler = AudioDownsampler::new(timing_mode); @@ -226,6 +244,7 @@ impl SegaCdEmulator { input, audio_downsampler, timing_mode, + main_bus_writes: MainBusWrites::new(), aspect_ratio: emulator_config.genesis.aspect_ratio, adjust_aspect_ratio_in_2x_resolution: emulator_config .genesis @@ -305,15 +324,7 @@ impl TickableEmulator for SegaCdEmulator { S: SaveWriter, S::Err: Debug + Display + Send + Sync + 'static, { - let mut main_bus = MainBus::new( - &mut self.memory, - &mut self.vdp, - &mut self.psg, - &mut self.ym2612, - &mut self.input, - self.timing_mode, - MainBusSignals { z80_busack: self.z80.stalled(), m68k_reset: false }, - ); + let mut main_bus = new_main_bus!(self, m68k_reset: false); // Main 68000 let main_cpu_cycles = self.main_cpu.execute_instruction(&mut main_bus); @@ -328,6 +339,8 @@ impl TickableEmulator for SegaCdEmulator { self.z80.tick(&mut main_bus); } + self.main_bus_writes = main_bus.take_writes(); + let genesis_master_clock_rate = match self.timing_mode { TimingMode::Ntsc => NTSC_GENESIS_MASTER_CLOCK_RATE, TimingMode::Pal => PAL_GENESIS_MASTER_CLOCK_RATE, @@ -367,6 +380,9 @@ impl TickableEmulator for SegaCdEmulator { // Sub 68000 self.tick_sub_cpu(sub_cpu_cycles); + // Apply main CPU writes after ticking the sub CPU; this fixes random freezing in Silpheed + self.main_bus_writes = new_main_bus!(self, m68k_reset: false).apply_writes(); + // Input state (for 6-button controller reset) self.input.tick(main_cpu_cycles); @@ -427,15 +443,7 @@ impl TickableEmulator for SegaCdEmulator { impl Resettable for SegaCdEmulator { fn soft_reset(&mut self) { // Reset main CPU - self.main_cpu.execute_instruction(&mut MainBus::new( - &mut self.memory, - &mut self.vdp, - &mut self.psg, - &mut self.ym2612, - &mut self.input, - self.timing_mode, - MainBusSignals { z80_busack: false, m68k_reset: true }, - )); + self.main_cpu.execute_instruction(&mut new_main_bus!(self, m68k_reset: true)); self.memory.reset_z80_signals(); self.ym2612.reset(); diff --git a/segacd-core/src/memory.rs b/segacd-core/src/memory.rs index d0e8e5c8..b16a8514 100644 --- a/segacd-core/src/memory.rs +++ b/segacd-core/src/memory.rs @@ -760,6 +760,7 @@ pub struct SubBus<'a> { } impl<'a> SubBus<'a> { + #[inline] pub fn new( memory: &'a mut Memory, graphics_coprocessor: &'a mut GraphicsCoprocessor,