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.
Errors
This method will return an error if unable to send the command to the emulator runner thread.
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.
Errors
Returns an error if runner thread has disconnected.
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;