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 { 29 /// Initialize SDL3 and all required subsystems. 30 /// 31 /// # Errors 32 /// 33 /// 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} 61 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; 82 83 /// Anything the implementation wishes to pass from [`Self::read_create_input`] to [`Self::create`]. 84 /// 85 /// Commonly [`Vec<u8>`] if the input is always a single file loaded entirely into RAM. 86 type CreateInput: Clone + Send + Sync + 'static; 87 88 /// Read inputs and determine the ROM path and extension to use for save files + save states. 89 /// 90 /// Most implementations can simply call [`read_rom_file`]. 91 fn read_create_input( 92 config: &Self::NativeConfig, 93 ) -> NativeEmulatorResult<ReadInputResult<Self::CreateInput>>; 94 95 /// Create the emulator. 96 fn create( 97 input: ReadInputResult<Self::CreateInput>, 98 config: &Self::NativeConfig, 99 save_writer: &mut impl SaveWriter, 100 ) -> NativeEmulatorResult<CreatedEmulator<Self>>; 101 102 fn common_config(config: &Self::NativeConfig) -> &CommonConfig; 103 104 fn emulator_config(config: &Self::NativeConfig) -> &Self::Config; 105 106 /// Reload configuration, if necessary (e.g. to change controller types). 107 /// 108 /// Implementations do not need to explicitly reload common/emulator/input configuration as 109 /// those are always reloaded before this function gets called. 110 #[allow(unused_variables)] 111 fn reload_native_config( 112 emulator: &mut NativeEmulator<Self>, 113 config: &Self::NativeConfig, 114 ) -> NativeEmulatorResult<()> { 115 Ok(()) 116 } 117 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> { 151 /// Create a new emulator instance. 152 /// 153 /// # Errors 154 /// 155 /// 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 } 199 200 /// Reload configuration. 201 /// 202 /// # Errors 203 /// 204 /// 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 } 223 224 /// Remove a disc from the emulated system's drive. 225 /// 226 /// # Errors 227 /// 228 /// Returns an error if the runner thread has disconnected. 229 pub fn remove_disc(&mut self) -> NativeEmulatorResult<()> { 230 self.runner.send_command(RunnerCommand::RemoveDisc) 231 } 232 233 /// Change the disc in the emulated system's drive. 234 /// 235 /// # Errors 236 /// 237 /// Returns an error if the runner thread has disconnected. 238 pub fn change_disc(&mut self, disc_path: PathBuf) -> NativeEmulatorResult<()> { 239 self.runner.send_command(RunnerCommand::ChangeDisc(disc_path)) 240 } 241} 242 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}