From ce138f21d4f5164a84cced6279483989d41fc58b Mon Sep 17 00:00:00 2001 From: Jujstme Date: Thu, 21 Sep 2023 16:38:00 +0200 Subject: [PATCH 1/3] Added support for Sega Master System / Game Gear (SMS) emulators --- Cargo.toml | 1 + src/emulator/mod.rs | 2 + src/emulator/sms/blastem.rs | 33 +++++++++ src/emulator/sms/fusion.rs | 37 ++++++++++ src/emulator/sms/mod.rs | 129 +++++++++++++++++++++++++++++++++ src/emulator/sms/retroarch.rs | 133 ++++++++++++++++++++++++++++++++++ 6 files changed, 335 insertions(+) create mode 100644 src/emulator/sms/blastem.rs create mode 100644 src/emulator/sms/fusion.rs create mode 100644 src/emulator/sms/mod.rs create mode 100644 src/emulator/sms/retroarch.rs diff --git a/Cargo.toml b/Cargo.toml index f26dade..0866b3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,4 +38,5 @@ gcn = ["flags"] genesis = ["flags", "signature"] ps1 = ["flags", "signature"] ps2 = ["flags", "signature"] +sms = ["flags", "signature"] wii = ["flags"] diff --git a/src/emulator/mod.rs b/src/emulator/mod.rs index adb03e8..c41b186 100644 --- a/src/emulator/mod.rs +++ b/src/emulator/mod.rs @@ -10,5 +10,7 @@ pub mod genesis; pub mod ps1; #[cfg(feature = "ps2")] pub mod ps2; +#[cfg(feature = "sms")] +pub mod sms; #[cfg(feature = "wii")] pub mod wii; diff --git a/src/emulator/sms/blastem.rs b/src/emulator/sms/blastem.rs new file mode 100644 index 0000000..f390e5c --- /dev/null +++ b/src/emulator/sms/blastem.rs @@ -0,0 +1,33 @@ +use crate::{runtime::MemoryRangeFlags, signature::Signature, Address, Address32, Process}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct State; + +impl State { + pub fn find_ram(&mut self, game: &Process) -> Option
{ + const SIG: Signature<15> = Signature::new("66 81 E1 FF 1F 0F B7 C9 8A 89 ?? ?? ?? ?? C3"); + + let scanned_address = game + .memory_ranges() + .filter(|m| { + m.flags() + .unwrap_or_default() + .contains(MemoryRangeFlags::WRITE) + && m.size().unwrap_or_default() == 0x101000 + }) + .find_map(|m| SIG.scan_process_range(game, m.range().ok()?))? + + 10; + + let wram: Address = game.read::(scanned_address).ok()?.into(); + + if wram.is_null() { + None + } else { + Some(wram) + } + } + + pub const fn keep_alive(&self) -> bool { + true + } +} diff --git a/src/emulator/sms/fusion.rs b/src/emulator/sms/fusion.rs new file mode 100644 index 0000000..da4b7c0 --- /dev/null +++ b/src/emulator/sms/fusion.rs @@ -0,0 +1,37 @@ +use crate::{signature::Signature, Address, Address32, Process}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct State { + addr: Address, +} + +impl State { + pub fn find_ram(&mut self, game: &Process) -> Option
{ + const SIG: Signature<4> = Signature::new("74 C8 83 3D"); + + let main_module = super::PROCESS_NAMES + .iter() + .filter(|(_, state)| matches!(state, super::State::Fusion(_))) + .find_map(|(name, _)| game.get_module_range(name).ok())?; + + let ptr = SIG.scan_process_range(game, main_module)? + 4; + self.addr = game.read::(ptr).ok()?.into(); + + Some(game.read::(self.addr).ok()?.add(0xC000).into()) + } + + pub fn keep_alive(&self, game: &Process, wram_base: &mut Option
) -> bool { + *wram_base = Some(match game.read::(self.addr) { + Ok(Address32::NULL) => Address::NULL, + Ok(x) => x.add(0xC000).into(), + _ => return false, + }); + true + } + + pub const fn new() -> Self { + Self { + addr: Address::NULL, + } + } +} diff --git a/src/emulator/sms/mod.rs b/src/emulator/sms/mod.rs new file mode 100644 index 0000000..a2f28bb --- /dev/null +++ b/src/emulator/sms/mod.rs @@ -0,0 +1,129 @@ +//! Support for attaching to SEGA Master System / SEGA GameGear emulators. + +use crate::{Address, Error, Process}; +use bytemuck::CheckedBitPattern; + +mod blastem; +mod fusion; +mod retroarch; + +/// A SEGA Master System / GameGear emulator that the auto splitter is attached to. +pub struct Emulator { + /// The attached emulator process + process: Process, + /// An enum stating which emulator is currently attached + state: State, + /// The memory address of the emulated RAM + ram_base: Option
, +} + +impl Emulator { + /// Attaches to the emulator process + /// + /// Returns `Option` if successful, `None` otherwise. + /// + /// Supported emulators are: + /// - Retroarch, with one of the following cores: `genesis_plus_gx_libretro.dll`, + /// `genesis_plus_gx_wide_libretro.dll`, `picodrive_libretro.dll`, `smsplus_libretro.dll`, `gearsystem_libretro.dll` + /// - Fusion + /// - BlastEm + pub fn attach() -> Option { + let (&state, process) = PROCESS_NAMES + .iter() + .find_map(|(name, state)| Some((state, Process::attach(name)?)))?; + + Some(Self { + process, + state, + ram_base: None, + }) + } + + /// Checks whether the emulator is still open. If it is not open anymore, + /// you should drop the emulator. + pub fn is_open(&self) -> bool { + self.process.is_open() + } + + /// Calls the internal routines needed in order to find (and update, if + /// needed) the address of the emulated RAM. + /// + /// Returns true if successful, false otherwise. + pub fn update(&mut self) -> bool { + if self.ram_base.is_none() { + self.ram_base = match match &mut self.state { + State::Retroarch(x) => x.find_ram(&self.process), + State::Fusion(x) => x.find_ram(&self.process), + State::BlastEm(x) => x.find_ram(&self.process), + } { + None => return false, + something => something, + }; + } + + let success = match &self.state { + State::Retroarch(x) => x.keep_alive(&self.process), + State::Fusion(x) => x.keep_alive(&self.process, &mut self.ram_base), + State::BlastEm(x) => x.keep_alive(), + }; + + if success { + true + } else { + self.ram_base = None; + false + } + } + + /// Reads raw data from the emulated RAM ignoring all endianness settings + /// The same call, performed on two different emulators, can be different + /// due to the endianness used by the emulator. + /// + /// The offset provided must not be higher than `0xFFFF`, otherwise this + /// method will immediately return `Err()`. + /// + /// This call is meant to be used by experienced users. + pub fn read_ignoring_endianness(&self, offset: u32) -> Result { + if offset > 0xFFFF { + return Err(Error {}); + } + + let wram = self.ram_base.ok_or(Error {})?; + + self.process.read(wram + offset) + } + + /// Reads any value from the emulated RAM. + /// + /// The offset provided is meant to be the same used on the original, + /// big-endian system. + /// + /// The SEGA Master System has 8KB of RAM, mapped from address + /// `0xC000` to `0xDFFF`. + /// + /// Providing any offset outside this range will return `Err()`. + pub fn read(&self, offset: u32) -> Result { + if (offset > 0x1FFF && offset < 0xC000) || offset > 0xDFFF { + return Err(Error {}); + } + + let wram = self.ram_base.ok_or(Error {})?; + let end_offset = offset.checked_sub(0xC000).unwrap_or(offset); + + self.process.read(wram + end_offset) + } +} + +#[doc(hidden)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum State { + Retroarch(retroarch::State), + Fusion(fusion::State), + BlastEm(blastem::State), +} + +static PROCESS_NAMES: &[(&str, State)] = &[ + ("retroarch.exe", State::Retroarch(retroarch::State::new())), + ("Fusion.exe", State::Fusion(fusion::State::new())), + ("blastem.exe", State::BlastEm(blastem::State)), +]; diff --git a/src/emulator/sms/retroarch.rs b/src/emulator/sms/retroarch.rs new file mode 100644 index 0000000..1ad53d7 --- /dev/null +++ b/src/emulator/sms/retroarch.rs @@ -0,0 +1,133 @@ +use crate::{file_format::pe, signature::Signature, Address, Address32, Address64, Process}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct State { + core_base: Address, +} + +impl State { + pub fn find_ram(&mut self, game: &Process) -> Option
{ + const SUPPORTED_CORES: &[&str] = &[ + "genesis_plus_gx_libretro.dll", + "genesis_plus_gx_wide_libretro.dll", + "picodrive_libretro.dll", + "smsplus_libretro.dll", + "gearsystem_libretro.dll", + ]; + + let main_module_address = super::PROCESS_NAMES + .iter() + .filter(|(_, state)| matches!(state, super::State::Retroarch(_))) + .find_map(|(name, _)| game.get_module_address(name).ok())?; + + let is_64_bit = + pe::MachineType::read(game, main_module_address) == Some(pe::MachineType::X86_64); + + let (core_name, core_address) = SUPPORTED_CORES + .iter() + .find_map(|&m| Some((m, game.get_module_address(m).ok()?)))?; + + self.core_base = core_address; + + match core_name { + "genesis_plus_gx_libretro.dll" | "genesis_plus_gx_wide_libretro.dll" => { + self.genesis_plus(game, is_64_bit, core_name) + } + "picodrive_libretro.dll" => self.picodrive(game, is_64_bit, core_name), + "smsplus_libretro.dll" => self.sms_plus(game, is_64_bit, core_name), + "gearsystem_libretro.dll" => self.gearsystem(game, is_64_bit, core_name), + _ => None, + } + } + + fn picodrive(&self, game: &Process, is_64_bit: bool, core_name: &str) -> Option
{ + let module_size = game.get_module_size(core_name).ok()?; + + Some( + if is_64_bit { + const SIG: Signature<9> = Signature::new("48 8D 0D ?? ?? ?? ?? 41 B8"); + let ptr = SIG.scan_process_range(game, (self.core_base, module_size))? + 3; + ptr + 0x4 + game.read::(ptr).ok()? + } else { + const SIG: Signature<8> = Signature::new("B9 ?? ?? ?? ?? C1 EF 10"); + let ptr = SIG.scan_process_range(game, (self.core_base, module_size))? + 1; + game.read::(ptr).ok()?.into() + } + 0x20000, + ) + } + + fn genesis_plus(&self, game: &Process, is_64_bit: bool, core_name: &str) -> Option
{ + let module_size = game.get_module_size(core_name).ok()?; + + Some(if is_64_bit { + const SIG: Signature<10> = Signature::new("48 8D 0D ?? ?? ?? ?? 4C 8B 2D"); + let ptr = SIG.scan_process_range(game, (self.core_base, module_size))? + 3; + ptr + 0x4 + game.read::(ptr).ok()? + } else { + const SIG: Signature<7> = Signature::new("A3 ?? ?? ?? ?? 29 F9"); + let ptr = SIG.scan_process_range(game, (self.core_base, module_size))? + 1; + game.read::(ptr).ok()?.into() + }) + } + + fn sms_plus(&self, game: &Process, is_64_bit: bool, core_name: &str) -> Option
{ + let module_size = game.get_module_size(core_name).ok()?; + + Some(if is_64_bit { + const SIG: Signature<5> = Signature::new("31 F6 48 C7 05"); + let ptr = SIG.scan_process_range(game, (self.core_base, module_size))? + 5; + ptr + 0x8 + game.read::(ptr).ok()? + } else { + const SIG: Signature<4> = Signature::new("83 FA 02 B8"); + let ptr = SIG.scan_process_range(game, (self.core_base, module_size))? + 4; + game.read::(ptr).ok()?.into() + }) + } + + fn gearsystem(&self, game: &Process, is_64_bit: bool, core_name: &str) -> Option
{ + let module_size = game.get_module_size(core_name).ok()?; + + Some(if is_64_bit { + const SIG: Signature<13> = Signature::new("83 ?? 02 75 ?? 48 8B 0D ?? ?? ?? ?? E8"); + let ptr = SIG.scan_process_range(game, (self.core_base, module_size))? + 8; + let offset = game + .read::(ptr + 13 + 0x4 + game.read::(ptr + 13).ok()? + 3) + .ok()?; + let addr = game + .read_pointer_path64::( + ptr + 0x4 + game.read::(ptr).ok()?, + &[0x0, 0x0, offset as _], + ) + .ok()?; + if addr.is_null() { + return None; + } else { + addr.add(0xC000).into() + } + } else { + const SIG: Signature<12> = Signature::new("83 ?? 02 75 ?? 8B ?? ?? ?? ?? ?? E8"); + let ptr = SIG.scan_process_range(game, (self.core_base, module_size))? + 7; + let offset = game + .read::(ptr + 12 + 0x4 + game.read::(ptr + 12).ok()? + 2) + .ok()?; + let addr = game + .read_pointer_path32::(ptr, &[0x0, 0x0, 0x0, offset as _]) + .ok()?; + if addr.is_null() { + return None; + } else { + addr.add(0xC000).into() + } + }) + } + + pub fn keep_alive(&self, game: &Process) -> bool { + game.read::(self.core_base).is_ok() + } + + pub const fn new() -> Self { + Self { + core_base: Address::NULL, + } + } +} From d6e941503183d5ffa626cbcb921922aec3cdd7f3 Mon Sep 17 00:00:00 2001 From: Jujstme Date: Fri, 22 Sep 2023 13:25:49 +0200 Subject: [PATCH 2/3] No need for a mutable reference here --- src/emulator/sms/blastem.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emulator/sms/blastem.rs b/src/emulator/sms/blastem.rs index f390e5c..5a00aef 100644 --- a/src/emulator/sms/blastem.rs +++ b/src/emulator/sms/blastem.rs @@ -4,7 +4,7 @@ use crate::{runtime::MemoryRangeFlags, signature::Signature, Address, Address32, pub struct State; impl State { - pub fn find_ram(&mut self, game: &Process) -> Option
{ + pub fn find_ram(&self, game: &Process) -> Option
{ const SIG: Signature<15> = Signature::new("66 81 E1 FF 1F 0F B7 C9 8A 89 ?? ?? ?? ?? C3"); let scanned_address = game From 35b295daa2d20bc99b61bc2189e566d512642780 Mon Sep 17 00:00:00 2001 From: Jujstme Date: Sat, 23 Sep 2023 09:45:54 +0200 Subject: [PATCH 3/3] Added support for MEdnafen (SMS) and removed an unused leftover function --- src/emulator/sms/mednafen.rs | 30 ++++++++++++++++++++++++++++++ src/emulator/sms/mod.rs | 26 ++++++-------------------- 2 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 src/emulator/sms/mednafen.rs diff --git a/src/emulator/sms/mednafen.rs b/src/emulator/sms/mednafen.rs new file mode 100644 index 0000000..569359d --- /dev/null +++ b/src/emulator/sms/mednafen.rs @@ -0,0 +1,30 @@ +use crate::{file_format::pe, signature::Signature, Address, Address32, Process}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct State; + +impl State { + pub fn find_ram(&self, game: &Process) -> Option
{ + const SIG_32: Signature<8> = Signature::new("25 FF 1F 00 00 0F B6 80"); + const SIG_64: Signature<7> = Signature::new("25 FF 1F 00 00 88 90"); + + let main_module_range = super::PROCESS_NAMES + .iter() + .filter(|(_, state)| matches!(state, super::State::Mednafen(_))) + .find_map(|(name, _)| game.get_module_range(name).ok())?; + + let is_64_bit = + pe::MachineType::read(game, main_module_range.0) == Some(pe::MachineType::X86_64); + + let ptr = match is_64_bit { + true => SIG_64.scan_process_range(game, main_module_range)? + 8, + false => SIG_32.scan_process_range(game, main_module_range)? + 7, + }; + + Some(game.read::(ptr).ok()?.into()) + } + + pub const fn keep_alive(&self) -> bool { + true + } +} diff --git a/src/emulator/sms/mod.rs b/src/emulator/sms/mod.rs index a2f28bb..99206ee 100644 --- a/src/emulator/sms/mod.rs +++ b/src/emulator/sms/mod.rs @@ -5,6 +5,7 @@ use bytemuck::CheckedBitPattern; mod blastem; mod fusion; +mod mednafen; mod retroarch; /// A SEGA Master System / GameGear emulator that the auto splitter is attached to. @@ -55,6 +56,7 @@ impl Emulator { State::Retroarch(x) => x.find_ram(&self.process), State::Fusion(x) => x.find_ram(&self.process), State::BlastEm(x) => x.find_ram(&self.process), + State::Mednafen(x) => x.find_ram(&self.process), } { None => return false, something => something, @@ -65,6 +67,7 @@ impl Emulator { State::Retroarch(x) => x.keep_alive(&self.process), State::Fusion(x) => x.keep_alive(&self.process, &mut self.ram_base), State::BlastEm(x) => x.keep_alive(), + State::Mednafen(x) => x.keep_alive(), }; if success { @@ -75,28 +78,9 @@ impl Emulator { } } - /// Reads raw data from the emulated RAM ignoring all endianness settings - /// The same call, performed on two different emulators, can be different - /// due to the endianness used by the emulator. - /// - /// The offset provided must not be higher than `0xFFFF`, otherwise this - /// method will immediately return `Err()`. - /// - /// This call is meant to be used by experienced users. - pub fn read_ignoring_endianness(&self, offset: u32) -> Result { - if offset > 0xFFFF { - return Err(Error {}); - } - - let wram = self.ram_base.ok_or(Error {})?; - - self.process.read(wram + offset) - } - /// Reads any value from the emulated RAM. /// - /// The offset provided is meant to be the same used on the original, - /// big-endian system. + /// The offset provided is meant to be the same used on the original hardware. /// /// The SEGA Master System has 8KB of RAM, mapped from address /// `0xC000` to `0xDFFF`. @@ -120,10 +104,12 @@ pub enum State { Retroarch(retroarch::State), Fusion(fusion::State), BlastEm(blastem::State), + Mednafen(mednafen::State), } static PROCESS_NAMES: &[(&str, State)] = &[ ("retroarch.exe", State::Retroarch(retroarch::State::new())), ("Fusion.exe", State::Fusion(fusion::State::new())), ("blastem.exe", State::BlastEm(blastem::State)), + ("mednafen.exe", State::Mednafen(mednafen::State)), ];