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;