create.rsannotatedcreate.rssource319 lines · 10.8 KB · raw
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].

91    fn read_create_input(
92        config: &Self::NativeConfig,
93    ) -> NativeEmulatorResult<ReadInputResult<Self::CreateInput>>;

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>>;
102    fn common_config(config: &Self::NativeConfig) -> &CommonConfig;
103
104    fn emulator_config(config: &Self::NativeConfig) -> &Self::Config;

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.

110    #[allow(unused_variables)]
111    fn reload_native_config(
112        emulator: &mut NativeEmulator<Self>,
113        config: &Self::NativeConfig,
114    ) -> NativeEmulatorResult<()> {
115        Ok(())
116    }
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.

229    pub fn remove_disc(&mut self) -> NativeEmulatorResult<()> {
230        self.runner.send_command(RunnerCommand::RemoveDisc)
231    }

Change the disc in the emulated system's drive.

Errors

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}
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}