1use crate::archive::{ArchiveEntry, ArchiveError}; 2use crate::config::CommonConfig; 3use crate::input::Joysticks; 4use crate::mainloop::runner::RunnerCommand; 5use crate::mainloop::save::FsSaveWriter; 6use crate::mainloop::{CreatedEmulator, NativeDebugFn, NativeEmulatorArgs, save}; 7use crate::{NativeEmulator, NativeEmulatorError, NativeEmulatorResult, archive, extensions}; 8use jgenesis_common::frontend::{EmulatorTrait, SaveWriter}; 9use jgenesis_native_config::input::mappings::ButtonMappingVec; 10use sdl3::mouse::MouseUtil; 11use sdl3::{AudioSubsystem, EventPump, VideoSubsystem}; 12use std::cell::RefCell; 13use std::fmt::Display; 14use std::fs; 15use std::ops::Deref; 16use std::path::{Path, PathBuf}; 17use std::rc::Rc; 18 19#[derive(Clone)] 20pub struct SdlSubsystems { 21 pub video: Rc<RefCell<VideoSubsystem>>, 22 pub audio: Rc<RefCell<AudioSubsystem>>, 23 pub joysticks: Rc<RefCell<Joysticks>>, 24 pub event_pump: Rc<RefCell<EventPump>>, 25 pub mouse_util: Rc<RefCell<MouseUtil>>, 26} 27 28impl SdlSubsystems {
Initialize SDL3 and all required subsystems.
Errors
Propagates any SDL initialization errors, e.g. if SDL3 is already initialized.
34 pub fn init() -> NativeEmulatorResult<Self> { 35 let sdl_ctx = sdl3::init().map_err(NativeEmulatorError::SdlInit)?; 36 let video = sdl_ctx.video().map_err(NativeEmulatorError::SdlVideoInit)?; 37 let audio = sdl_ctx.audio().map_err(NativeEmulatorError::SdlAudioInit)?; 38 let joystick = sdl_ctx.joystick().map_err(NativeEmulatorError::SdlJoystickInit)?; 39 let event_pump = sdl_ctx.event_pump().map_err(NativeEmulatorError::SdlEventPumpInit)?; 40 let mouse_util = sdl_ctx.mouse(); 41 42 // Allow gamepad inputs while window does not have focus 43 // https://wiki.libsdl.org/SDL3/SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS 44 sdl3::hint::set("SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS", "1"); 45 46 // Register mouse down events for mouse clicks that change window focus 47 // https://wiki.libsdl.org/SDL3/SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH 48 sdl3::hint::set("SDL_MOUSE_FOCUS_CLICKTHROUGH", "1"); 49 50 let joysticks = Joysticks::new(joystick); 51 52 Ok(Self { 53 video: Rc::new(RefCell::new(video)), 54 audio: Rc::new(RefCell::new(audio)), 55 joysticks: Rc::new(RefCell::new(joysticks)), 56 event_pump: Rc::new(RefCell::new(event_pump)), 57 mouse_util: Rc::new(RefCell::new(mouse_util)), 58 }) 59 } 60}
62#[derive(Clone)] 63pub struct ReadInputResult<T> { 64 pub input: T, 65 pub rom_path: PathBuf, 66 pub save_extension: String, 67} 68 69#[derive(Debug, Clone)] 70pub struct WindowTitle(pub String); 71 72impl Deref for WindowTitle { 73 type Target = String; 74 75 fn deref(&self) -> &Self::Target { 76 &self.0 77 } 78} 79 80pub trait CreatableEmulator: EmulatorTrait + Sized { 81 type NativeConfig: Display + Clone + Send + Sync + 'static;
Anything the implementation wishes to pass from [Self::read_create_input] to [Self::create].
Commonly [Vec<u8>] if the input is always a single file loaded entirely into RAM.
86 type CreateInput: Clone + Send + Sync + 'static;
Read inputs and determine the ROM path and extension to use for save files + save states.
Most implementations can simply call [read_rom_file].
Create the emulator.
Reload configuration, if necessary (e.g. to change controller types).
Implementations do not need to explicitly reload common/emulator/input configuration as those are always reloaded before this function gets called.
118 #[allow(unused_variables)] 119 fn initial_inputs(config: &Self::NativeConfig) -> Self::Inputs { 120 Self::Inputs::default() 121 } 122 123 fn input_mappings(config: &Self::NativeConfig) -> ButtonMappingVec<'_, Self::Button>; 124 125 #[allow(unused_variables)] 126 fn turbo_input_mappings(config: &Self::NativeConfig) -> ButtonMappingVec<'_, Self::Button> { 127 vec![] 128 } 129 130 #[allow(unused_variables)] 131 fn change_disc( 132 &mut self, 133 disc_path: &Path, 134 config: &<Self as EmulatorTrait>::Config, 135 ) -> NativeEmulatorResult<Option<WindowTitle>> { 136 log::warn!("This backend does not support disc change"); 137 Ok(None) 138 } 139 140 fn remove_disc(&mut self) -> Option<WindowTitle> { 141 log::warn!("This backend does not support disc change"); 142 None 143 } 144 145 fn debug_fn() -> Option<NativeDebugFn<Self>> { 146 None 147 } 148} 149 150impl<Emulator: CreatableEmulator> NativeEmulator<Emulator> {
Create a new emulator instance.
Errors
Propagates any errors encountered creating or initializing the emulator.
156 #[inline(never)] // Bloats compile times and binary size 157 pub fn create( 158 sdl: SdlSubsystems, 159 config: Box<Emulator::NativeConfig>, 160 ) -> NativeEmulatorResult<Self> { 161 log::info!("Running with config: {config}"); 162 163 let input = Emulator::read_create_input(&config)?; 164 165 let common_config = Emulator::common_config(&config); 166 let determined_paths = save::determine_save_paths( 167 &common_config.save_path, 168 &common_config.state_path, 169 &input.rom_path, 170 &input.save_extension, 171 )?; 172 173 let save_extension = input.save_extension.clone(); 174 175 let create_emulator_fn = { 176 let config = config.clone(); 177 move |save_writer: &mut FsSaveWriter| Emulator::create(input, &config, save_writer) 178 }; 179 180 let mut args = NativeEmulatorArgs::new( 181 Box::new(create_emulator_fn), 182 Emulator::emulator_config(&config).clone(), 183 common_config.clone(), 184 save_extension, 185 determined_paths.save_path, 186 determined_paths.save_state_path, 187 Emulator::input_mappings(&config), 188 ) 189 .with_initial_inputs(Emulator::initial_inputs(&config)) 190 .with_turbo_mappings(Emulator::turbo_input_mappings(&config)) 191 .with_disc_change_fns(Emulator::change_disc, Emulator::remove_disc); 192 193 if let Some(debug_fn) = Emulator::debug_fn() { 194 args = args.with_debug_fn(debug_fn); 195 } 196 197 Self::new(sdl, args) 198 }
Reload configuration.
Errors
Propagates any errors encountered while reloading.
205 pub fn reload_config( 206 &mut self, 207 config: Box<Emulator::NativeConfig>, 208 ) -> NativeEmulatorResult<()> { 209 log::info!("Reloading config: {config}"); 210 211 let common_config = Emulator::common_config(&config); 212 self.update_and_reload_config(common_config, Emulator::emulator_config(&config))?; 213 214 self.input_mapper.update_mappings( 215 common_config.axis_deadzone, 216 &Emulator::input_mappings(&config), 217 &Emulator::turbo_input_mappings(&config), 218 &common_config.hotkey_config.to_mapping_vec(), 219 ); 220 221 Emulator::reload_native_config(self, &config) 222 }
Remove a disc from the emulated system's drive.
Errors
Returns an error if the runner thread has disconnected.
Change the disc in the emulated system's drive.
Errors
Returns an error if the runner thread has disconnected.
243pub type ReadRomResult = ReadInputResult<Vec<u8>>; 244 245pub(crate) fn read_rom_file( 246 path: &Path, 247 supported_extensions: &[&str], 248) -> NativeEmulatorResult<ReadRomResult> { 249 struct NameWithExtension { 250 file_name: String, 251 extension: String, 252 } 253 254 #[derive(Default)] 255 struct ArchiveListCallback { 256 first_supported_file: Option<NameWithExtension>, 257 } 258 259 impl ArchiveListCallback { 260 fn as_fn_mut<'ext>( 261 &mut self, 262 supported_extensions: &'ext [&str], 263 ) -> impl FnMut(ArchiveEntry<'_>) + use<'_, 'ext> { 264 |entry| { 265 if self.first_supported_file.is_some() { 266 return; 267 } 268 269 let Some(extension) = extensions::from_path(entry.file_name) else { return }; 270 if supported_extensions.contains(&extension.as_str()) { 271 self.first_supported_file = 272 Some(NameWithExtension { file_name: entry.file_name.into(), extension }); 273 } 274 } 275 } 276 277 fn open_file( 278 self, 279 archive_path: &Path, 280 read_fn: fn(&Path, &str, usize) -> Result<Vec<u8>, ArchiveError>, 281 ) -> NativeEmulatorResult<(Vec<u8>, String)> { 282 let first_supported_file = self.first_supported_file.ok_or_else(|| { 283 NativeEmulatorError::Archive(ArchiveError::NoSupportedFiles { 284 path: archive_path.display().to_string(), 285 }) 286 })?; 287 288 let contents = read_fn(archive_path, &first_supported_file.file_name, usize::MAX) 289 .map_err(NativeEmulatorError::Archive)?; 290 Ok((contents, first_supported_file.extension)) 291 } 292 } 293 294 let extension = extensions::from_path(path).unwrap_or_default(); 295 let (contents, extension) = match extension.as_str() { 296 "zip" => { 297 let mut callback = ArchiveListCallback::default(); 298 archive::list_files_zip(path, callback.as_fn_mut(supported_extensions)) 299 .map_err(NativeEmulatorError::Archive)?; 300 callback.open_file(path, archive::read_file_zip) 301 } 302 "7z" => { 303 let mut callback = ArchiveListCallback::default(); 304 archive::list_files_7z(path, callback.as_fn_mut(supported_extensions)) 305 .map_err(NativeEmulatorError::Archive)?; 306 callback.open_file(path, archive::read_file_7z) 307 } 308 _ => { 309 let contents = fs::read(path).map_err(|source| NativeEmulatorError::RomRead { 310 path: path.display().to_string(), 311 source, 312 })?; 313 314 Ok((contents, extension)) 315 } 316 }?; 317 318 Ok(ReadRomResult { input: contents, rom_path: path.into(), save_extension: extension }) 319}