1mod audio;
2mod create;
3mod gb;
4mod gba;
5mod genesis;
6mod input;
7mod nes;
8mod pce;
9mod render;
10mod rewind;
11mod runner;
12mod save;
13mod smsgg;
14mod snes;
15mod state;
16mod window;
17
18pub use audio::AudioError;
19pub use create::SdlSubsystems;
20pub use gb::NativeGameBoyEmulator;
21pub use gba::NativeGbaEmulator;
22pub use genesis::NativeGenesisEmulator;
23pub use nes::NativeNesEmulator;
24pub use pce::NativePcEngineEmulator;
25pub use save::SaveWriteError;
26pub use smsgg::NativeSmsGgEmulator;
27pub use snes::NativeSnesEmulator;
28pub use state::{SAVE_STATE_SLOTS, SaveStateMetadata};
29
30use crate::archive::ArchiveError;
31use crate::config::CommonConfig;
32use crate::fpstracker::FpsTracker;
33use crate::input::{InputEvent, InputMapper, Joysticks};
34use crate::mainloop::audio::{SdlAudioOutput, SdlAudioOutputHandle};
35use crate::mainloop::create::WindowTitle;
36use crate::mainloop::input::ThreadedInputPoller;
37use crate::mainloop::render::{RecvFrameError, ThreadedRenderer};
38use crate::mainloop::runner::{
39    ChangeDiscFn, RemoveDiscFn, RunnerCommand, RunnerCommandResponse, RunnerSpawnArgs,
40    RunnerThreadHandle,
41};
42use crate::mainloop::save::FsSaveWriter;
43use crate::mainloop::window::{AtomicWindowSize, CreateWindowArgs, RendererExt};
44use bincode::error::{DecodeError, EncodeError};
45use egui_sdl3_wgpu::FrameRunEffect;
46use gb_core::api::GameBoyLoadError;
47use gba_core::api::GbaLoadError;
48use genesis_config::GenesisRegion;
49use jgenesis_common::frontend::{EmulatorConfigTrait, EmulatorTrait, MappableInputs};
50use jgenesis_debugger_frontend::{DebugFn, DebuggerWindow};
51use jgenesis_native_config::EguiTheme;
52use jgenesis_native_config::common::{HideMouseCursor, WindowSize};
53use jgenesis_native_config::input::mappings::ButtonMappingVec;
54use jgenesis_native_config::input::{CompactHotkey, Hotkey};
55use jgenesis_renderer::renderer::{RendererError, WgpuRenderer};
56use nes_core::api::NesInitializationError;
57use sdl3::event::{Event, WindowEvent};
58use sdl3::mouse::MouseUtil;
59use sdl3::video::{Window, WindowBuildError};
60use sdl3::{EventPump, IntegerOrSdlError, VideoSubsystem};
61use segacd_core::api::SegaCdLoadError;
62use snes_core::api::SnesLoadError;
63use std::cell::RefCell;
64use std::error::Error;
65use std::ffi::NulError;
66use std::fmt::Debug;
67use std::io;
68use std::path::{Path, PathBuf};
69use std::rc::Rc;
70use std::sync::Arc;
71use std::sync::mpsc::RecvTimeoutError;
72use std::time::Duration;
73use thiserror::Error;
74
75const MODAL_DURATION: Duration = Duration::from_secs(3);
76
77type NativeDebugFn<Emulator> = DebugFn<
78    Emulator,
79    ThreadedRenderer,
80    SdlAudioOutput,
81    ThreadedInputPoller<<Emulator as EmulatorTrait>::Inputs>,
82    FsSaveWriter,
83>;
84
85struct HotkeyState<Emulator: EmulatorTrait> {
86    hide_mouse_cursor: HideMouseCursor,
87    mouse_over_emulator_window: bool,
88    save_state_slot: usize,
89    paused: bool,
90    fast_forwarding: bool,
91    rewinding: bool,
92    overclocking_enabled: bool,
93    debugger_window: Option<DebuggerWindow>,
94    egui_theme: EguiTheme,
95    debug_fn: NativeDebugFn<Emulator>,
96}
97
98impl<Emulator: EmulatorTrait> HotkeyState<Emulator> {
99    fn new(common_config: &CommonConfig, debug_fn: NativeDebugFn<Emulator>) -> Self {
100        Self {
101            hide_mouse_cursor: common_config.hide_mouse_cursor,
102            mouse_over_emulator_window: common_config.launch_in_fullscreen,
103            save_state_slot: 0,
104            paused: false,
105            fast_forwarding: false,
106            rewinding: false,
107            overclocking_enabled: true,
108            debugger_window: None,
109            egui_theme: common_config.egui_theme,
110            debug_fn,
111        }
112    }
113
114    fn is_debugger_window_id(&self, window_id: u32) -> bool {
115        self.debugger_window.as_ref().is_some_and(|debugger| window_id == debugger.window_id())
116    }
117}
118
119#[derive(Debug, Clone, Copy)]
120struct WindowState {
121    gui_focused: bool,
122    emulator_focused: bool,
123    debugger_focused: bool,
124}
125
126impl WindowState {
127    fn new() -> Self {
128        Self {
129            gui_focused: false,
130            emulator_focused: true, // Assume emulator window has focus at launch
131            debugger_focused: false,
132        }
133    }
134
135    fn any_focused(self) -> bool {
136        self.gui_focused || self.emulator_focused || self.debugger_focused
137    }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum NativeTickEffect {
142    PowerOff,
143    Exit,
144}
145
146pub struct NativeEmulator<Emulator: EmulatorTrait> {
147    runner: RunnerThreadHandle<Emulator>,
148    // Config sent from the frontend
149    raw_config: Emulator::Config,
150    // Config with overclocking maybe forcibly disabled due to hotkey state
151    config: Emulator::Config,
152    common_config: CommonConfig,
153    renderer: WgpuRenderer<Window>,
154    window_size: Arc<AtomicWindowSize>,
155    audio_output_handle: SdlAudioOutputHandle,
156    input_mapper: InputMapper<Emulator::Button>,
157    inputs: Emulator::Inputs,
158    hotkey_state: HotkeyState<Emulator>,
159    window_state: WindowState,
160    fps_tracker: FpsTracker,
161    video: Rc<RefCell<VideoSubsystem>>,
162    event_pump: Rc<RefCell<EventPump>>,
163    mouse_util: Rc<RefCell<MouseUtil>>,
164}
165
166impl<Emulator: EmulatorTrait> NativeEmulator<Emulator> {
167    fn reload_common_config(&mut self, config: &CommonConfig) -> Result<(), AudioError> {
168        let borderless_changed = config.borderless_window != self.common_config.borderless_window;
169        self.common_config = config.clone();
170
171        self.renderer.reload_config(config.renderer_config);
172        if borderless_changed {
173            self.renderer.update_borderless(config.borderless_window, &self.window_size);
174        }
175
176        self.audio_output_handle.reload_config(config)?;
177
178        self.hotkey_state.hide_mouse_cursor = config.hide_mouse_cursor;
179
180        // Reset speed multiplier in case the fast forward hotkey changed
181        self.renderer.set_speed_multiplier(1);
182
183        self.update_show_cursor();
184
185        self.hotkey_state.egui_theme = config.egui_theme;
186        if let Some(debugger) = &mut self.hotkey_state.debugger_window {
187            debugger.update_egui_theme(config.egui_theme);
188        }
189
190        Ok(())
191    }
192
193    pub fn focus(&mut self) {
194        self.renderer.focus();
195    }
196
197    pub fn joysticks(&self) -> Rc<RefCell<Joysticks>> {
198        self.input_mapper.joysticks()
199    }
200
201    pub fn event_pump(&self) -> Rc<RefCell<EventPump>> {
202        Rc::clone(&self.event_pump)
203    }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207enum HotkeyEffect {
208    PowerOff,
209    Exit,
210}
211
212#[derive(Debug, Error)]
213pub enum NativeEmulatorError {
214    #[error("{0}")]
215    Render(#[from] RendererError),
216    #[error("{0}")]
217    Audio(#[from] AudioError),
218    #[error("{0}")]
219    SaveWrite(#[from] SaveWriteError),
220    #[error("Error initializing SDL3: {0}")]
221    SdlInit(sdl3::Error),
222    #[error("Error initializing SDL3 video subsystem: {0}")]
223    SdlVideoInit(sdl3::Error),
224    #[error("Error initializing SDL3 audio subsystem: {0}")]
225    SdlAudioInit(sdl3::Error),
226    #[error("Error initializing SDL3 joystick subsystem: {0}")]
227    SdlJoystickInit(sdl3::Error),
228    #[error("Error initializing SDL3 event pump: {0}")]
229    SdlEventPumpInit(sdl3::Error),
230    #[error("Error creating SDL3 window: {0}")]
231    SdlCreateWindow(#[from] WindowBuildError),
232    #[error("Error changing window title to '{title}': {source}")]
233    SdlSetWindowTitle {
234        title: String,
235        #[source]
236        source: NulError,
237    },
238    #[error("Error toggling window fullscreen: {0}")]
239    SdlSetFullscreen(sdl3::Error),
240    #[error("Error opening joystick {device_id}: {source}")]
241    SdlJoystickOpen {
242        device_id: u32,
243        #[source]
244        source: IntegerOrSdlError,
245    },
246    #[error("Unable to determine file name for path: '{0}'")]
247    ParseFileName(String),
248    #[error("Unable to determine file extension for path: '{0}'")]
249    ParseFileExtension(String),
250    #[error("Failed to create save directory at '{path}': {source}")]
251    CreateSaveDir {
252        path: String,
253        #[source]
254        source: io::Error,
255    },
256    #[error("Failed to read ROM file at '{path}': {source}")]
257    RomRead {
258        path: String,
259        #[source]
260        source: io::Error,
261    },
262    #[error("{0}")]
263    Archive(#[from] ArchiveError),
264    #[error("No SMS BIOS provided")]
265    SmsNoBios,
266    #[error("No Game Gear BIOS provided")]
267    GgNoBios,
268    #[error("Error opening BIOS file at '{path}': {source}")]
269    SmsGgBiosRead {
270        path: PathBuf,
271        #[source]
272        source: io::Error,
273    },
274    #[error("{0} BIOS is required for Sega CD emulation")]
275    SegaCdNoBios(GenesisRegion),
276    #[error("Error opening BIOS file at '{path}': {source}")]
277    SegaCdBiosRead {
278        path: PathBuf,
279        #[source]
280        source: io::Error,
281    },
282    #[error("{0}")]
283    SegaCdDisc(#[from] SegaCdLoadError),
284    #[error("{0}")]
285    NesLoad(#[from] NesInitializationError),
286    #[error("{0}")]
287    SnesLoad(#[from] SnesLoadError),
288    #[error("No Game Boy boot ROM provided")]
289    GbNoDmgBootRom,
290    #[error("No Game Boy Color boot ROM provided")]
291    GbNoCgbBootRom,
292    #[error("Failed to load boot ROM: {0}")]
293    GbBootRomLoad(io::Error),
294    #[error("{0}")]
295    GameBoyLoad(#[from] GameBoyLoadError),
296    #[error("No Game Boy Advance BIOS provided")]
297    GbaNoBios,
298    #[error("Failed to load GBA BIOS: {0}")]
299    GbaBiosLoad(io::Error),
300    #[error("Failed to initialize GBA emulator: {0}")]
301    GbaLoad(#[from] GbaLoadError),
302    #[error("I/O error opening save state file '{path}': {source}")]
303    StateFileOpen {
304        path: String,
305        #[source]
306        source: io::Error,
307    },
308    #[error("Error saving state: {0}")]
309    SaveState(#[from] EncodeError),
310    #[error("Error saving state: {0}")]
311    SaveStateIo(io::Error),
312    #[error("Error loading state: {0}")]
313    LoadState(#[from] DecodeError),
314    #[error("Error loading state: {0}")]
315    LoadStateIo(io::Error),
316    #[error("Save state begins with invalid prefix")]
317    LoadStatePrefixMismatch,
318    #[error("Save state version mismatch; expected '{expected}', got '{actual}'")]
319    LoadStateVersionMismatch { expected: String, actual: String },
320    #[error("Lost connection to runner thread")]
321    LostRunnerConnection,
322    #[error("Error changing/removing disc: {0}")]
323    ChangeDisc(#[source] Box<dyn Error + Send + Sync + 'static>),
324    #[error("Error in emulation core: {0}")]
325    Emulator(#[source] Box<dyn Error + Send + Sync + 'static>),
326}
327
328pub type NativeEmulatorResult<T> = Result<T, NativeEmulatorError>;
329
330pub struct CreatedEmulator<Emulator: EmulatorTrait> {
331    pub emulator: Emulator,
332    pub window_title: String,
333    pub default_window_size: WindowSize,
334}
335
336pub(crate) type CreateEmulatorFn<Emulator> = dyn FnOnce(&mut FsSaveWriter) -> Result<CreatedEmulator<Emulator>, NativeEmulatorError>
337    + Send
338    + Sync
339    + 'static;
340
341pub(crate) struct NativeEmulatorArgs<'input, 'turbo, Emulator: EmulatorTrait> {
342    pub create_emulator_fn: Box<CreateEmulatorFn<Emulator>>,
343    pub change_disc_fn: ChangeDiscFn<Emulator>,
344    pub remove_disc_fn: RemoveDiscFn<Emulator>,
345    pub emulator_config: Emulator::Config,
346    pub common_config: CommonConfig,
347    pub rom_extension: String,
348    pub save_path: PathBuf,
349    pub save_state_path: PathBuf,
350    pub button_mappings: ButtonMappingVec<'input, Emulator::Button>,
351    pub turbo_mappings: ButtonMappingVec<'turbo, Emulator::Button>,
352    pub initial_inputs: Emulator::Inputs,
353    pub debug_fn: NativeDebugFn<Emulator>,
354}
355
356impl<'input, 'turbo, Emulator> NativeEmulatorArgs<'input, 'turbo, Emulator>
357where
358    Emulator: EmulatorTrait,
359{
360    #[allow(clippy::too_many_arguments)]
361    pub fn new(
362        create_emulator_fn: Box<CreateEmulatorFn<Emulator>>,
363        emulator_config: Emulator::Config,
364        common_config: CommonConfig,
365        rom_extension: String,
366        save_path: PathBuf,
367        save_state_path: PathBuf,
368        button_mappings: ButtonMappingVec<'input, Emulator::Button>,
369    ) -> Self {
370        NativeEmulatorArgs {
371            create_emulator_fn,
372            change_disc_fn: |_emulator, _path, _config| Ok(None),
373            remove_disc_fn: |_emulator| None,
374            emulator_config,
375            common_config,
376            rom_extension,
377            save_path,
378            save_state_path,
379            button_mappings,
380            turbo_mappings: vec![],
381            initial_inputs: Emulator::Inputs::default(),
382            debug_fn: jgenesis_debugger_frontend::null_debug_fn,
383        }
384    }
385
386    pub fn with_turbo_mappings(
387        mut self,
388        turbo_mappings: ButtonMappingVec<'turbo, Emulator::Button>,
389    ) -> Self {
390        self.turbo_mappings = turbo_mappings;
391        self
392    }
393
394    pub fn with_initial_inputs(mut self, inputs: Emulator::Inputs) -> Self {
395        self.initial_inputs = inputs;
396        self
397    }
398
399    pub fn with_debug_fn(mut self, debug_fn: NativeDebugFn<Emulator>) -> Self {
400        self.debug_fn = debug_fn;
401        self
402    }
403
404    pub fn with_disc_change_fns(
405        mut self,
406        change_disc_fn: ChangeDiscFn<Emulator>,
407        remove_disc_fn: RemoveDiscFn<Emulator>,
408    ) -> Self {
409        self.change_disc_fn = change_disc_fn;
410        self.remove_disc_fn = remove_disc_fn;
411        self
412    }
413}
414
415impl<Emulator> NativeEmulator<Emulator>
416where
417    Emulator: EmulatorTrait,
418{
419    fn new(
420        sdl: SdlSubsystems,
421        NativeEmulatorArgs {
422            create_emulator_fn,
423            change_disc_fn,
424            remove_disc_fn,
425            emulator_config,
426            common_config,
427            rom_extension,
428            save_path,
429            save_state_path,
430            button_mappings,
431            turbo_mappings,
432            initial_inputs,
433            debug_fn,
434        }: NativeEmulatorArgs<'_, '_, Emulator>,
435    ) -> NativeEmulatorResult<Self> {
436        let (audio_output, mut audio_output_handle) =
437            SdlAudioOutput::create_and_init(sdl.audio, &common_config)?;
438
439        let input_mapper = InputMapper::new(
440            sdl.joysticks,
441            common_config.axis_deadzone,
442            &button_mappings,
443            &turbo_mappings,
444            &common_config.hotkey_config.to_mapping_vec(),
445        );
446
447        let hotkey_state = HotkeyState::new(&common_config, debug_fn);
448
449        let save_writer = FsSaveWriter::new(save_path);
450
451        let runner = runner::spawn(RunnerSpawnArgs {
452            create_emulator_fn,
453            change_disc_fn,
454            remove_disc_fn,
455            common_config: common_config.clone(),
456            emulator_config: emulator_config.clone(),
457            rom_extension: rom_extension.clone(),
458            save_state_path,
459            initial_inputs: initial_inputs.clone(),
460            audio_output_handle: &mut audio_output_handle,
461            audio_output,
462            save_writer,
463        })?;
464
465        let initial_window_size = common_config.window_size.unwrap_or(runner.default_window_size());
466
467        let (window, atomic_window_size) = window::create(
468            &sdl.video.borrow(),
469            CreateWindowArgs {
470                title: runner.initial_window_title(),
471                width: initial_window_size.width,
472                height: initial_window_size.height,
473                fullscreen: common_config.launch_in_fullscreen,
474                borderless: common_config.borderless_window,
475            },
476        )?;
477
478        let window_size = window::size(&window);
479        let renderer = pollster::block_on(WgpuRenderer::new(
480            window,
481            window_size,
482            common_config.renderer_config,
483        ))?;
484
485        let mut emulator = Self {
486            runner,
487            raw_config: emulator_config.clone(),
488            config: emulator_config,
489            common_config: common_config.clone(),
490            renderer,
491            window_size: atomic_window_size,
492            audio_output_handle,
493            input_mapper,
494            inputs: initial_inputs,
495            hotkey_state,
496            window_state: WindowState::new(),
497            fps_tracker: FpsTracker::new(),
498            video: sdl.video,
499            event_pump: sdl.event_pump,
500            mouse_util: sdl.mouse_util,
501        };
502
503        for modal in emulator.runner.startup_modals() {
504            let modal = modal.clone();
505            emulator.renderer.add_or_update_modal(modal.id, modal.text, MODAL_DURATION);
506        }
507
508        if common_config.load_recent_state_at_launch {
509            emulator.try_load_most_recent_state();
510        }
511
512        // Make a best effort to focus the newly-created emulator window
513        emulator.renderer.focus();
514
515        Ok(emulator)
516    }

Run the emulator for some amount of time.

Errors

This method will propagate any errors encountered when rendering frames, pushing audio samples, or writing save files.

524    #[inline(never)] // Bloats compile times and binary size
525    pub fn run(
526        &mut self,
527        sdl_event_handler: &mut dyn FnMut(&Event),
528    ) -> NativeEmulatorResult<Option<NativeTickEffect>> {
529        self.runner.try_recv_error()?;
530
531        let paused = self.hotkey_state.paused
532            || self
533                .common_config
534                .pause_emulator
535                .should_pause(self.window_state.emulator_focused, self.window_state.any_focused());
536
537        self.runner.set_paused(paused);
538
539        for event in Rc::clone(&self.event_pump).borrow_mut().poll_iter() {
540            sdl_event_handler(&event);
541
542            self.input_mapper.handle_event(
543                &event,
544                self.renderer.window_id(),
545                self.renderer.current_display_info(),
546            );
547
548            if let Some(debugger_window) = &mut self.hotkey_state.debugger_window {
549                debugger_window.handle_sdl_event(&event);
550            }
551
552            match event {
553                Event::Quit { .. } => {
554                    return Ok(Some(NativeTickEffect::PowerOff));
555                }
556                Event::Window { win_event, window_id, .. } => {
557                    if let Some(effect) = self.handle_window_event(&win_event, window_id)? {
558                        return Ok(Some(effect));
559                    }
560                }
561                Event::MouseButtonDown { window_id, .. }
562                    if window_id == self.renderer.window_id()
563                        && self.inputs.needs_relative_mouse_mode()
564                        && !self.hotkey_state.paused =>
565                {
566                    let mouse_util = self.mouse_util.borrow();
567                    let window = self.renderer.window();
568
569                    if !mouse_util.relative_mouse_mode(window) {
570                        mouse_util.set_relative_mouse_mode(window, true);
571
572                        self.renderer.add_or_update_modal(
573                            Some("relative_mouse".into()),
574                            "Mouse input enabled".into(),
575                            MODAL_DURATION,
576                        );
577                    }
578                }
579                Event::MouseMotion { window_id, .. } => {
580                    self.hotkey_state.mouse_over_emulator_window =
581                        window_id == self.renderer.window_id();
582                    self.update_show_cursor();
583                }
584                _ => {}
585            }
586        }
587
588        if let Some(effect) = self.process_input_events()? {
589            return Ok(Some(effect));
590        }
591
592        if self.hotkey_state.rewinding {
593            self.renderer.reset_interframe_state();
594        }
595
596        while let Some(response) = self.runner.try_recv_command_response() {
597            self.handle_runner_cmd_response(response);
598        }
599
600        match self.runner.try_recv_frame(&mut self.renderer, Duration::from_millis(5)) {
601            Ok(()) => {
602                self.input_mapper.frame_complete();
603                if let Some(effect) = self.process_input_events()? {
604                    return Ok(Some(effect));
605                }
606
607                self.fps_tracker.record_frame();
608            }
609            Err(RecvFrameError::Render(err)) => return Err(NativeEmulatorError::Render(err)),
610            Err(
611                RecvFrameError::Recv(RecvTimeoutError::Disconnected)
612                | RecvFrameError::LostConnection,
613            ) => {
614                return Err(NativeEmulatorError::LostRunnerConnection);
615            }
616            Err(RecvFrameError::Recv(RecvTimeoutError::Timeout)) => {}
617        }
618
619        if let Some(debugger_window) = &mut self.hotkey_state.debugger_window {
620            match debugger_window.update() {
621                Ok(FrameRunEffect::None) => {}
622                Ok(FrameRunEffect::Closed) => {
623                    self.stop_debugger()?;
624                }
625                Err(err) => {
626                    log::error!("Error updating debugger window: {err}");
627                }
628            }
629        }
630
631        Ok(None)
632    }
634    fn update_show_cursor(&self) {
635        let mouse_util = self.mouse_util.borrow();
636
637        if self.hotkey_state.mouse_over_emulator_window {
638            mouse_util.show_cursor(
639                !self.hotkey_state.hide_mouse_cursor.should_hide(self.renderer.is_fullscreen()),
640            );
641        } else {
642            mouse_util.show_cursor(true);
643        }
644    }
645
646    fn stop_debugger(&mut self) -> NativeEmulatorResult<()> {
647        self.hotkey_state.debugger_window = None;
648        self.window_state.debugger_focused = false;
649        self.runner.send_command(RunnerCommand::StopDebugger)
650    }
651
652    #[allow(clippy::missing_errors_doc)]
653    pub fn handle_window_event(
654        &mut self,
655        win_event: &WindowEvent,
656        window_id: u32,
657    ) -> NativeEmulatorResult<Option<NativeTickEffect>> {
658        match win_event {
659            WindowEvent::CloseRequested => {
660                if window_id == self.renderer.window_id() {
661                    return Ok(Some(NativeTickEffect::PowerOff));
662                }
663
664                if self.hotkey_state.is_debugger_window_id(window_id) {
665                    self.stop_debugger()?;
666                }
667            }
668            WindowEvent::FocusGained => {
669                if window_id == self.renderer.window_id() {
670                    self.window_state.emulator_focused = true;
671                } else if self.hotkey_state.is_debugger_window_id(window_id) {
672                    self.window_state.debugger_focused = true;
673                }
674            }
675            WindowEvent::FocusLost => {
676                if window_id == self.renderer.window_id() {
677                    self.window_state.emulator_focused = false;
678                } else if self.hotkey_state.is_debugger_window_id(window_id) {
679                    self.window_state.debugger_focused = false;
680                }
681            }
682            WindowEvent::Resized(..)
683            | WindowEvent::PixelSizeChanged(..)
684            | WindowEvent::Maximized
685                if window_id == self.renderer.window_id() =>
686            {
687                let window_size = window::size(self.renderer.window());
688                self.renderer.handle_resize(window_size);
689                self.window_size.update(window_size);
690            }
691            _ => {}
692        }
693
694        Ok(None)
695    }
696
697    fn process_input_events(&mut self) -> NativeEmulatorResult<Option<NativeTickEffect>> {
698        let input_events = self.input_mapper.input_events();
699        for event in input_events.borrow_mut().drain(..) {
700            match event {
701                InputEvent::Button { button, player, pressed } => {
702                    self.inputs.set_field(button, player, pressed);
703                    if let Some(modal) = self.inputs.modal_for_input(button, player, pressed) {
704                        self.renderer.add_or_update_modal(modal.id, modal.text, MODAL_DURATION);
705                    }
706                }
707                InputEvent::AnalogValueChange { button, player, value } => {
708                    self.inputs.set_analog(button, player, value);
709                }
710                InputEvent::MouseMotion { position, delta, display_info } => {
711                    self.inputs.handle_mouse_motion(position, delta, display_info);
712                }
713                InputEvent::MouseLeave => {
714                    self.inputs.handle_mouse_leave();
715                }
716                InputEvent::Hotkey { hotkey, pressed } => {
717                    let effect = self.handle_hotkey_event(hotkey, pressed)?;
718                    match effect {
719                        Some(HotkeyEffect::PowerOff) => {
720                            return Ok(Some(NativeTickEffect::PowerOff));
721                        }
722                        Some(HotkeyEffect::Exit) => {
723                            return Ok(Some(NativeTickEffect::Exit));
724                        }
725                        None => {}
726                    }
727                }
728            }
729        }
730
731        self.runner.update_inputs(&self.inputs);
732
733        Ok(None)
734    }
735
736    fn handle_runner_cmd_response(&mut self, response: RunnerCommandResponse) {
737        match response {
738            RunnerCommandResponse::SaveStateSucceeded { slot } => {
739                self.renderer.add_modal(format!("Saved state to slot {slot}"), MODAL_DURATION);
740                self.hotkey_state.save_state_slot = slot;
741            }
742            RunnerCommandResponse::LoadStateSucceeded { slot } => {
743                // Force renderer to clear any interframe state
744                self.renderer.reload();
745
746                self.renderer.add_modal(format!("Loaded state from slot {slot}"), MODAL_DURATION);
747                self.hotkey_state.save_state_slot = slot;
748            }
749            RunnerCommandResponse::SaveStateFailed { slot, err } => {
750                self.renderer
751                    .add_modal(format!("Failed to save state to slot {slot}"), MODAL_DURATION);
752                log::error!("Failed to save state to slot {slot}: {err}");
753            }
754            RunnerCommandResponse::LoadStateFailed { slot, err } => {
755                self.renderer
756                    .add_modal(format!("Failed to load state from slot {slot}"), MODAL_DURATION);
757                log::error!("Failed to load state from slot {slot}: {err}");
758            }
759            RunnerCommandResponse::ChangeDiscSucceeded { window_title } => {
760                if let Some(WindowTitle(mut window_title)) = window_title {
761                    window_title.retain(|c| (c as u8) != 0);
762
763                    self.renderer
764                        .set_window_title(&window_title)
765                        .expect("Window title does not have any null characters");
766                }
767            }
768            RunnerCommandResponse::ChangeDiscFailed(err) => {
769                log::error!("Failed to change disc: {err}");
770            }
771        }
772    }

Errors

This method will return an error if unable to send the command to the emulator runner thread.

777    pub fn soft_reset(&mut self) -> NativeEmulatorResult<()> {
778        self.runner.send_command(RunnerCommand::SoftReset)
779    }

Errors

This method will return an error if unable to send the command to the emulator runner thread.

784    pub fn hard_reset(&mut self) -> NativeEmulatorResult<()> {
785        self.runner.send_command(RunnerCommand::HardReset)
786    }

Errors

This method will return an error if unable to send the command to the emulator runner thread.

791    pub fn open_memory_viewer(&mut self) -> NativeEmulatorResult<()> {
792        if self.hotkey_state.debugger_window.is_some() {
793            return Ok(());
794        }
795
796        let (runner_process, main_process) = (self.hotkey_state.debug_fn)();
797
798        let debugger_window = match DebuggerWindow::new(
799            &self.video.borrow(),
800            self.hotkey_state.egui_theme,
801            self.renderer.config(),
802            main_process,
803        ) {
804            Ok(window) => window,
805            Err(err) => {
806                log::error!("Error creating debugger window: {err}");
807                return Ok(());
808            }
809        };
810
811        self.hotkey_state.debugger_window = Some(debugger_window);
812        self.runner.send_command(RunnerCommand::StartDebugger(runner_process))
813    }

Errors

Returns an error if runner thread has disconnected.

818    pub fn save_state(&mut self, slot: usize) -> NativeEmulatorResult<()> {
819        self.runner.send_command(RunnerCommand::SaveState { slot })
820    }

Errors

Returns an error if runner thread has disconnected.

825    pub fn load_state(&mut self, slot: usize) -> NativeEmulatorResult<()> {
826        self.runner.send_command(RunnerCommand::LoadState { slot })
827    }

Errors

Returns an error if runner thread has disconnected or if there is an error rendering the frame to the viewport.

833    pub fn force_render(&mut self) -> NativeEmulatorResult<()> {
834        self.runner.send_command(RunnerCommand::ForceRender)?;
835
836        match self.runner.try_recv_frame(&mut self.renderer, Duration::from_millis(10)) {
837            Ok(()) | Err(RecvFrameError::Recv(_)) => {}
838            Err(RecvFrameError::LostConnection) => {
839                return Err(NativeEmulatorError::LostRunnerConnection);
840            }
841            Err(RecvFrameError::Render(err)) => return Err(NativeEmulatorError::Render(err)),
842        }
843
844        Ok(())
845    }

Try to load the most recent save state.

If there are no save states or the most recent save state is invalid, this method will log an error and not modify any emulator state.

851    #[allow(clippy::missing_panics_doc)]
852    pub fn try_load_most_recent_state(&mut self) {
853        let max_time = self
854            .runner
855            .save_state_metadata()
856            .lock()
857            .unwrap()
858            .times_nanos
859            .into_iter()
860            .enumerate()
861            .filter_map(|(i, option)| option.map(|time| (i, time)))
862            .max_by_key(|&(_, time)| time);
863
864        let Some((slot, _)) = max_time else {
865            log::error!("No save states found; not loading a save state at launch");
866            return;
867        };
868
869        if let Err(err) = self.load_state(slot) {
870            log::error!("Error loading save state slot {slot} at launch: {err}");
871        }
872    }
874    #[allow(clippy::missing_panics_doc)]
875    pub fn save_state_metadata(&self) -> SaveStateMetadata {
876        self.runner.save_state_metadata().lock().unwrap().clone()
877    }
878
879    pub fn update_gui_focused(&mut self, gui_focused: bool) {
880        if gui_focused != self.window_state.gui_focused {
881            log::debug!("GUI window focus changed to: {gui_focused}");
882        }
883
884        self.window_state.gui_focused = gui_focused;
885    }
886
887    fn handle_hotkey_event(
888        &mut self,
889        hotkey: Hotkey,
890        pressed: bool,
891    ) -> NativeEmulatorResult<Option<HotkeyEffect>> {
892        if pressed {
893            let effect = self.handle_hotkey_pressed(hotkey)?;
894            return Ok(effect);
895        }
896        // else, hotkey released
897
898        match hotkey {
899            Hotkey::FastForward => self.set_fast_forwarding(false)?,
900            Hotkey::Rewind => self.set_rewinding(false)?,
901            _ => {}
902        }
903
904        Ok(None)
905    }
906
907    fn handle_hotkey_pressed(
908        &mut self,
909        hotkey: Hotkey,
910    ) -> NativeEmulatorResult<Option<HotkeyEffect>> {
911        let hotkey = hotkey.to_compact();
912        match hotkey {
913            CompactHotkey::PowerOff => return Ok(Some(HotkeyEffect::PowerOff)),
914            CompactHotkey::Exit => return Ok(Some(HotkeyEffect::Exit)),
915            CompactHotkey::ToggleFullscreen => self.toggle_fullscreen()?,
916            CompactHotkey::SaveState => self.save_state(self.hotkey_state.save_state_slot)?,
917            CompactHotkey::SaveStateSlot(slot) => self.save_state(slot)?,
918            CompactHotkey::LoadState => self.hotkey_load_state(None),
919            CompactHotkey::LoadStateSlot(slot) => self.hotkey_load_state(Some(slot)),
920            CompactHotkey::SoftReset => self.soft_reset()?,
921            CompactHotkey::HardReset => self.hard_reset()?,
922            CompactHotkey::NextSaveStateSlot => self.next_save_state_slot(),
923            CompactHotkey::PrevSaveStateSlot => self.prev_save_state_slot(),
924            CompactHotkey::Pause => {
925                self.hotkey_state.paused = !self.hotkey_state.paused;
926                if self.hotkey_state.paused {
927                    self.mouse_util.borrow().set_relative_mouse_mode(self.renderer.window(), false);
928                }
929            }
930            CompactHotkey::StepFrame => {
931                self.runner.send_command(RunnerCommand::StepFrame)?;
932            }
933            CompactHotkey::FastForward => self.set_fast_forwarding(true)?,
934            CompactHotkey::Rewind => self.set_rewinding(true)?,
935            CompactHotkey::CancelMouseInput => {
936                self.mouse_util.borrow().set_relative_mouse_mode(self.renderer.window(), false);
937            }
938            CompactHotkey::ToggleOverclocking => self.toggle_overclocking()?,
939            CompactHotkey::OpenDebugger => self.open_memory_viewer()?,
940        }
941
942        Ok(None)
943    }
944
945    fn toggle_fullscreen(&mut self) -> NativeEmulatorResult<()> {
946        let fullscreen = self
947            .renderer
948            .toggle_fullscreen(self.common_config.borderless_window, &self.window_size)
949            .map_err(NativeEmulatorError::SdlSetFullscreen)?;
950
951        // Don't wait for mouse motion to hide cursor when entering fullscreen
952        if fullscreen {
953            self.mouse_util
954                .borrow()
955                .show_cursor(!self.hotkey_state.hide_mouse_cursor.should_hide(fullscreen));
956        }
957
958        Ok(())
959    }
960
961    fn hotkey_load_state(&mut self, slot: Option<usize>) {
962        let slot = slot.unwrap_or(self.hotkey_state.save_state_slot);
963
964        if let Err(err) = self.load_state(slot) {
965            log::error!("Error loading save state from slot {slot}: {err}");
966        }
967    }
968
969    fn next_save_state_slot(&mut self) {
970        self.hotkey_state.save_state_slot =
971            (self.hotkey_state.save_state_slot + 1) % SAVE_STATE_SLOTS;
972        self.render_selected_slot_modal();
973    }
974
975    fn prev_save_state_slot(&mut self) {
976        self.hotkey_state.save_state_slot = if self.hotkey_state.save_state_slot == 0 {
977            SAVE_STATE_SLOTS - 1
978        } else {
979            self.hotkey_state.save_state_slot - 1
980        };
981        self.render_selected_slot_modal();
982    }
983
984    fn render_selected_slot_modal(&mut self) {
985        self.renderer.add_or_update_modal(
986            Some("selected_state_slot".into()),
987            format!("Selected save state slot {}", self.hotkey_state.save_state_slot),
988            MODAL_DURATION,
989        );
990    }
991
992    fn set_fast_forwarding(&mut self, enabled: bool) -> NativeEmulatorResult<()> {
993        self.runner.send_command(RunnerCommand::FastForward { enabled })?;
994        self.hotkey_state.fast_forwarding = enabled;
995        self.update_renderer_fast_forward_config();
996
997        Ok(())
998    }
999
1000    fn set_rewinding(&mut self, enabled: bool) -> NativeEmulatorResult<()> {
1001        self.runner.send_command(RunnerCommand::Rewind { enabled })?;
1002        self.hotkey_state.rewinding = enabled;
1003        self.update_renderer_fast_forward_config();
1004
1005        Ok(())
1006    }
1007
1008    fn update_renderer_fast_forward_config(&mut self) {
1009        let fast_forwarding = self.hotkey_state.fast_forwarding;
1010        let rewinding = self.hotkey_state.rewinding;
1011
1012        let speed_multiplier = if fast_forwarding && !rewinding {
1013            self.common_config.fast_forward_multiplier
1014        } else {
1015            1
1016        };
1017        self.renderer.set_speed_multiplier(speed_multiplier);
1018
1019        self.renderer.set_force_sync_off(rewinding);
1020    }
1021
1022    fn toggle_overclocking(&mut self) -> NativeEmulatorResult<()> {
1023        self.hotkey_state.overclocking_enabled = !self.hotkey_state.overclocking_enabled;
1024        self.update_and_reload_config(&self.common_config.clone(), &self.raw_config.clone())?;
1025
1026        let modal_text = if self.hotkey_state.overclocking_enabled {
1027            "Overclocking settings enabled"
1028        } else {
1029            "Overclocking settings disabled"
1030        };
1031        self.renderer.add_or_update_modal(
1032            Some("overclocking_settings".into()),
1033            modal_text.into(),
1034            MODAL_DURATION,
1035        );
1036
1037        Ok(())
1038    }
1039
1040    fn update_and_reload_config(
1041        &mut self,
1042        common_config: &CommonConfig,
1043        emulator_config: &Emulator::Config,
1044    ) -> NativeEmulatorResult<()> {
1045        self.reload_common_config(common_config)?;
1046
1047        self.raw_config = emulator_config.clone();
1048        self.config = if self.hotkey_state.overclocking_enabled {
1049            self.raw_config.clone()
1050        } else {
1051            self.raw_config.with_overclocking_disabled()
1052        };
1053
1054        self.runner.send_command(RunnerCommand::ReloadConfig(Box::new((
1055            self.common_config.clone(),
1056            self.config.clone(),
1057        ))))
1058    }
1059}
1060
1061impl<Emulator: EmulatorTrait> Drop for NativeEmulator<Emulator> {
1062    fn drop(&mut self) {
1063        let _ = self.runner.send_command(RunnerCommand::Terminate);
1064        self.mouse_util.borrow().show_cursor(true);
1065    }
1066}
1067
1068fn file_name_no_ext<P: AsRef<Path>>(path: P) -> NativeEmulatorResult<String> {
1069    path.as_ref()
1070        .with_extension("")
1071        .file_name()
1072        .map(|file_name| file_name.to_string_lossy().into_owned())
1073        .ok_or_else(|| NativeEmulatorError::ParseFileName(path.as_ref().display().to_string()))
1074}
1075
1076macro_rules! bincode_config {
1077    () => {
1078        bincode::config::standard()
1079            .with_little_endian()
1080            .with_fixed_int_encoding()
1081            .with_limit::<{ 100 * 1024 * 1024 }>()
1082    };
1083}
1084
1085use bincode_config;