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    }
517
518    /// Run the emulator for some amount of time.
519    ///
520    /// # Errors
521    ///
522    /// This method will propagate any errors encountered when rendering frames, pushing audio
523    /// 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    }
633
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    }
773
774    /// # Errors
775    ///
776    /// 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    }
780
781    /// # Errors
782    ///
783    /// 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    }
787
788    /// # Errors
789    ///
790    /// 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    }
814
815    /// # Errors
816    ///
817    /// 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    }
821
822    /// # Errors
823    ///
824    /// 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    }
828
829    /// # Errors
830    ///
831    /// Returns an error if runner thread has disconnected or if there is an error rendering the
832    /// 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    }
846
847    /// Try to load the most recent save state.
848    ///
849    /// If there are no save states or the most recent save state is invalid, this method will log
850    /// 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    }
873
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;