1use crate::config::CommonConfig; 2use crate::mainloop::audio::{SdlAudioOutput, SdlAudioOutputHandle}; 3use crate::mainloop::create::WindowTitle; 4use crate::mainloop::input::{ThreadedInputPoller, ThreadedInputPollerHandle}; 5use crate::mainloop::render::{ 6 RecvFrameError, ThreadedRenderer, ThreadedRendererError, ThreadedRendererHandle, 7}; 8use crate::mainloop::rewind::Rewinder; 9use crate::mainloop::save::{DeterminedPaths, FsSaveWriter}; 10use crate::mainloop::state::{SaveStatePaths, StateSaverThreadHandle}; 11use crate::mainloop::{CreateEmulatorFn, CreatedEmulator, save, state}; 12use crate::{NativeEmulatorError, NativeEmulatorResult, SaveStateMetadata}; 13use jgenesis_common::frontend::{ 14 AudioOutput, EmulatorTrait, Modal, Renderer, SaveWriter, TickEffect, 15}; 16use jgenesis_debugger_frontend::DebuggerRunnerProcess; 17use jgenesis_native_config::common::WindowSize; 18use std::error::Error; 19use std::path::{Path, PathBuf}; 20use std::sync::atomic::{AtomicBool, Ordering}; 21use std::sync::mpsc::{Receiver, Sender, TryRecvError}; 22use std::sync::{Arc, Mutex, mpsc}; 23use std::thread; 24use std::time::Duration; 25use thiserror::Error; 26 27pub type ChangeDiscFn<Emulator> = fn( 28 &mut Emulator, 29 &Path, 30 &<Emulator as EmulatorTrait>::Config, 31) -> NativeEmulatorResult<Option<WindowTitle>>; 32 33pub type RemoveDiscFn<Emulator> = fn(&mut Emulator) -> Option<WindowTitle>; 34 35pub enum RunnerCommand<Emulator: EmulatorTrait> { 36 Terminate, 37 SoftReset, 38 HardReset, 39 ChangeDisc(PathBuf), 40 RemoveDisc, 41 StepFrame, 42 FastForward { enabled: bool }, 43 Rewind { enabled: bool }, 44 SaveState { slot: usize }, 45 LoadState { slot: usize }, 46 ForceRender, 47 ReloadConfig(Box<(CommonConfig, Emulator::Config)>), 48 StartDebugger(Box<NativeDebuggerRunnerProcess<Emulator>>), 49 StopDebugger, 50} 51 52#[derive(Debug)] 53pub enum RunnerCommandResponse { 54 SaveStateSucceeded { slot: usize }, 55 LoadStateSucceeded { slot: usize }, 56 SaveStateFailed { slot: usize, err: NativeEmulatorError }, 57 LoadStateFailed { slot: usize, err: NativeEmulatorError }, 58 ChangeDiscSucceeded { window_title: Option<WindowTitle> }, 59 ChangeDiscFailed(NativeEmulatorError), 60} 61 62pub type NativeDebuggerRunnerProcess<Emulator> = dyn DebuggerRunnerProcess< 63 Emulator, 64 ThreadedRenderer, 65 SdlAudioOutput, 66 ThreadedInputPoller<<Emulator as EmulatorTrait>::Inputs>, 67 FsSaveWriter, 68 >; 69 70struct RunnerThreadState<Emulator: EmulatorTrait> { 71 emulator: Emulator, 72 renderer: ThreadedRenderer, 73 audio_output: SdlAudioOutput, 74 input_poller: ThreadedInputPoller<Emulator::Inputs>, 75 save_writer: FsSaveWriter, 76 common_config: CommonConfig, 77 emulator_config: Emulator::Config, 78 command_receiver: Receiver<RunnerCommand<Emulator>>, 79 response_sender: Sender<RunnerCommandResponse>, 80 error_sender: Sender<Box<dyn Error + Send + Sync + 'static>>, 81 rom_path: PathBuf, 82 rom_extension: String, 83 base_save_state_path: PathBuf, 84 save_state_paths: SaveStatePaths, 85 save_state_metadata: Arc<Mutex<SaveStateMetadata>>, 86 state_saver_thread: StateSaverThreadHandle<Emulator>, 87 paused: Arc<AtomicBool>, 88 step_frame: bool, 89 rewinder: Rewinder<Emulator>, 90 change_disc_fn: ChangeDiscFn<Emulator>, 91 remove_disc_fn: RemoveDiscFn<Emulator>, 92 debugger_process: Option<Box<NativeDebuggerRunnerProcess<Emulator>>>, 93} 94 95impl<Emulator: EmulatorTrait> RunnerThreadState<Emulator> { 96 fn update_save_paths(&mut self) -> NativeEmulatorResult<()> { 97 let DeterminedPaths { save_path, save_state_path } = save::determine_save_paths( 98 &self.common_config.save_path, 99 &self.common_config.state_path, 100 &self.rom_path, 101 &self.rom_extension, 102 )?; 103 104 self.save_writer.update_path(save_path); 105 106 if save_state_path != self.base_save_state_path { 107 self.save_state_paths = state::init_paths(&save_state_path)?; 108 *self.save_state_metadata.lock().unwrap() = 109 SaveStateMetadata::load(&self.save_state_paths, Emulator::save_state_version()); 110 self.base_save_state_path = save_state_path; 111 } 112 113 Ok(()) 114 } 115 116 fn reload_configs( 117 &mut self, 118 common_config: CommonConfig, 119 emulator_config: Emulator::Config, 120 ) -> NativeEmulatorResult<()> { 121 self.common_config = common_config; 122 self.emulator_config = emulator_config; 123 124 self.emulator.reload_config(&self.emulator_config); 125 self.audio_output.reload_config(&self.common_config); 126 self.emulator.update_audio_output_frequency(self.audio_output.output_frequency()); 127 128 // In case fast forward hotkey changed 129 self.audio_output.set_speed_multiplier(1); 130 131 self.update_save_paths()?; 132 133 self.rewinder.set_buffer_duration(Duration::from_secs( 134 self.common_config.rewind_buffer_length_seconds, 135 )); 136 137 Ok(()) 138 } 139} 140 141pub struct RunnerSpawnArgs<'a, Emulator: EmulatorTrait> { 142 pub create_emulator_fn: Box<CreateEmulatorFn<Emulator>>, 143 pub change_disc_fn: ChangeDiscFn<Emulator>, 144 pub remove_disc_fn: RemoveDiscFn<Emulator>, 145 pub common_config: CommonConfig, 146 pub emulator_config: Emulator::Config, 147 pub rom_extension: String, 148 pub save_state_path: PathBuf, 149 pub initial_inputs: Emulator::Inputs, 150 pub audio_output_handle: &'a mut SdlAudioOutputHandle, 151 pub audio_output: SdlAudioOutput, 152 pub save_writer: FsSaveWriter, 153} 154 155pub struct RunnerThreadHandle<Emulator: EmulatorTrait> { 156 initial_window_title: String, 157 default_window_size: WindowSize, 158 startup_modals: Vec<Modal>, 159 renderer_handle: ThreadedRendererHandle, 160 input_poller_handle: ThreadedInputPollerHandle<Emulator::Inputs>, 161 command_sender: Sender<RunnerCommand<Emulator>>, 162 response_receiver: Receiver<RunnerCommandResponse>, 163 error_receiver: Receiver<Box<dyn Error + Send + Sync + 'static>>, 164 save_state_metadata: Arc<Mutex<SaveStateMetadata>>, 165 paused: Arc<AtomicBool>, 166}
Spawn a runner thread and return a handle to it.
Errors
Propagates any errors encountered while initializing the emulator in the runner thread.
If this function returns an error, the runner thread has terminated (or was not started).
175pub fn spawn<Emulator: EmulatorTrait>( 176 RunnerSpawnArgs { 177 create_emulator_fn, 178 change_disc_fn, 179 remove_disc_fn, 180 common_config, 181 emulator_config, 182 rom_extension, 183 save_state_path, 184 initial_inputs, 185 audio_output_handle, 186 audio_output, 187 mut save_writer, 188 }: RunnerSpawnArgs<'_, Emulator>, 189) -> NativeEmulatorResult<RunnerThreadHandle<Emulator>> { 190 let (init_sender, init_receiver) = mpsc::sync_channel(0); 191 let (command_sender, command_receiver) = mpsc::channel(); 192 let (response_sender, response_receiver) = mpsc::channel(); 193 let (error_sender, error_receiver) = mpsc::channel(); 194 195 let paused = Arc::new(AtomicBool::new(false)); 196 197 let (renderer, renderer_handle) = ThreadedRenderer::new(); 198 let input_poller = ThreadedInputPoller::new(initial_inputs); 199 let input_poller_handle = input_poller.handle(); 200 201 let save_state_paths = state::init_paths(&save_state_path)?; 202 let save_state_metadata = Arc::new(Mutex::new(SaveStateMetadata::load( 203 &save_state_paths, 204 Emulator::save_state_version(), 205 ))); 206 207 let runner_handle = { 208 let paused = Arc::clone(&paused); 209 let save_state_metadata = Arc::clone(&save_state_metadata); 210 211 let common_config = common_config.clone(); 212 213 thread::spawn(move || match create_emulator_fn(&mut save_writer) { 214 Ok(CreatedEmulator { emulator, window_title, default_window_size }) => { 215 let startup_modals = emulator.startup_modals(); 216 init_sender.send(Ok((window_title, default_window_size, startup_modals))).unwrap(); 217 218 let state_saver_thread = state::spawn_state_saver_thread(); 219 220 let rewinder = 221 Rewinder::new(Duration::from_secs(common_config.rewind_buffer_length_seconds)); 222 223 let rom_path = common_config.rom_file_path.clone(); 224 run_thread(RunnerThreadState { 225 emulator, 226 renderer, 227 audio_output, 228 input_poller, 229 save_writer, 230 common_config, 231 emulator_config, 232 command_receiver, 233 response_sender, 234 error_sender, 235 rom_path, 236 rom_extension, 237 base_save_state_path: save_state_path, 238 save_state_paths, 239 save_state_metadata, 240 state_saver_thread, 241 paused, 242 step_frame: false, 243 rewinder, 244 change_disc_fn, 245 remove_disc_fn, 246 debugger_process: None, 247 }); 248 249 log::info!("Runner thread has terminated"); 250 } 251 Err(err) => { 252 init_sender.send(Err(err)).unwrap(); 253 } 254 }) 255 }; 256 257 audio_output_handle.set_emulator_thread(runner_handle.thread().clone()); 258 259 let (initial_window_title, default_window_size, startup_modals) = 260 init_receiver.recv().unwrap()?; 261 262 Ok(RunnerThreadHandle { 263 initial_window_title, 264 default_window_size, 265 startup_modals, 266 renderer_handle, 267 input_poller_handle, 268 command_sender, 269 response_receiver, 270 error_receiver, 271 save_state_metadata, 272 paused, 273 }) 274}
276impl<Emulator: EmulatorTrait> RunnerThreadHandle<Emulator> {
Receive and render a pending frame if one is available. Blocks for up to timeout.
Errors
Returns an error on timeout, if the runner thread has disconnected, or if renderer
returns an error rendering the received frame.
Receive and propagate an error from the runner thread if one is available. Does not block.
Errors
Returns an error if one is received or if the runner thread has disconnected.
296 pub fn try_recv_error(&self) -> NativeEmulatorResult<()> { 297 match self.error_receiver.try_recv() { 298 Ok(err) => Err(NativeEmulatorError::Emulator(err)), 299 Err(TryRecvError::Empty) => Ok(()), 300 Err(TryRecvError::Disconnected) => Err(NativeEmulatorError::LostRunnerConnection), 301 } 302 }
Receive a command response from the runner thread if one is available. Does not block.
The main thread should not wait for a response after sending a command; this is very likely to cause a deadlock since the runner may be waiting for the main thread to receive a frame.
Tell the runner thread whether it should pause emulation.
Send updated inputs to the runner thread.
Send a command to the runner thread.
Errors
Returns an error if the runner thread has disconnected.
332 pub fn save_state_metadata(&self) -> &Arc<Mutex<SaveStateMetadata>> { 333 &self.save_state_metadata 334 } 335 336 pub fn initial_window_title(&self) -> &str { 337 &self.initial_window_title 338 } 339 340 pub fn default_window_size(&self) -> WindowSize { 341 self.default_window_size 342 } 343 344 pub fn startup_modals(&self) -> &[Modal] { 345 &self.startup_modals 346 } 347} 348 349fn run_thread<Emulator: EmulatorTrait>(mut state: RunnerThreadState<Emulator>) { 350 loop { 351 match handle_commands(&mut state) { 352 Ok(CommandEffect::None) => {} 353 Ok(CommandEffect::Terminate) => return, 354 Err(CommandError::ReloadConfig(err)) => { 355 log::error!("{}", CommandError::ReloadConfig(err)); 356 } 357 Err(CommandError::Render(err)) => { 358 log::error!("Error rendering frame: {err}"); 359 let _ = state.error_sender.send(err.into()); 360 return; 361 } 362 Err(CommandError::LostConnection) => { 363 log::error!("{}", CommandError::LostConnection); 364 return; 365 } 366 } 367 368 let paused = state.paused.load(Ordering::Relaxed); 369 let rewinding = state.rewinder.is_rewinding(); 370 371 let should_run_emulator = !rewinding && (!paused || state.step_frame); 372 373 if should_run_emulator { 374 if let Err(err) = run_till_next_frame(&mut state) { 375 let _ = state.error_sender.send(err.into()); 376 return; 377 } 378 379 state.rewinder.record_frame(&state.emulator); 380 381 state.audio_output.adjust_dynamic_resampling_ratio(); 382 state.emulator.update_audio_output_frequency(state.audio_output.output_frequency()); 383 } 384 385 state.step_frame = false; 386 387 if rewinding 388 && let Err(err) = state.rewinder.tick( 389 &mut state.emulator, 390 &mut state.renderer, 391 &state.emulator_config, 392 ) 393 { 394 let _ = state.error_sender.send(err.into()); 395 return; 396 } 397 398 if let Some(debugger_process) = &mut state.debugger_process 399 && let Err(err) = debugger_process.run(&mut state.emulator) 400 { 401 log::error!("Error updating debugger in runner thread: {err}"); 402 } 403 404 if !should_run_emulator { 405 // Don't spin loop when the emulator is paused or rewinding 406 thread::sleep(Duration::from_millis(1)); 407 } 408 } 409} 410 411#[derive(Debug, Clone, Copy, PartialEq, Eq)] 412enum CommandEffect { 413 None, 414 Terminate, 415} 416 417#[derive(Debug, Error)] 418enum CommandError { 419 #[error("Error reloading config: {0}")] 420 ReloadConfig(#[source] NativeEmulatorError), 421 #[error("Error rendering frame: {0}")] 422 Render(#[from] ThreadedRendererError), 423 #[error("Lost connection to main thread")] 424 LostConnection, 425} 426 427fn handle_commands<Emulator: EmulatorTrait>( 428 state: &mut RunnerThreadState<Emulator>, 429) -> Result<CommandEffect, CommandError> { 430 loop { 431 match state.command_receiver.try_recv() { 432 Ok(command) => match handle_command(state, command) { 433 Ok(CommandEffect::None) => {} 434 other => return other, 435 }, 436 Err(TryRecvError::Empty) => break, 437 Err(TryRecvError::Disconnected) => return Err(CommandError::LostConnection), 438 } 439 } 440 441 try_recv_save_state_responses(state)?; 442 443 Ok(CommandEffect::None) 444} 445 446fn handle_command<Emulator: EmulatorTrait>( 447 state: &mut RunnerThreadState<Emulator>, 448 command: RunnerCommand<Emulator>, 449) -> Result<CommandEffect, CommandError> { 450 match command { 451 RunnerCommand::Terminate => { 452 return Ok(CommandEffect::Terminate); 453 } 454 RunnerCommand::SoftReset => { 455 state.emulator.soft_reset(); 456 } 457 RunnerCommand::HardReset => { 458 state.emulator.hard_reset(&mut state.save_writer); 459 } 460 RunnerCommand::ChangeDisc(path) => { 461 change_disc(state, path)?; 462 } 463 RunnerCommand::RemoveDisc => { 464 remove_disc(state)?; 465 } 466 RunnerCommand::StepFrame => { 467 state.step_frame = true; 468 } 469 RunnerCommand::FastForward { enabled: true } => { 470 state.audio_output.set_speed_multiplier(state.common_config.fast_forward_multiplier); 471 state.rewinder.set_speed_multiplier(state.common_config.fast_forward_multiplier); 472 } 473 RunnerCommand::FastForward { enabled: false } => { 474 state.audio_output.set_speed_multiplier(1); 475 state.rewinder.set_speed_multiplier(1); 476 } 477 RunnerCommand::Rewind { enabled: true } => { 478 state.rewinder.start_rewinding(); 479 } 480 RunnerCommand::Rewind { enabled: false } => { 481 state.rewinder.stop_rewinding(); 482 } 483 RunnerCommand::SaveState { slot } => { 484 save_state(state, slot)?; 485 } 486 RunnerCommand::LoadState { slot } => { 487 load_state(state, slot)?; 488 } 489 RunnerCommand::ForceRender => { 490 state.emulator.force_render(&mut state.renderer)?; 491 } 492 RunnerCommand::ReloadConfig(configs) => { 493 state.reload_configs(configs.0, configs.1).map_err(CommandError::ReloadConfig)?; 494 } 495 RunnerCommand::StartDebugger(debugger_process) => { 496 state.debugger_process = Some(debugger_process); 497 } 498 RunnerCommand::StopDebugger => { 499 state.debugger_process = None; 500 } 501 } 502 503 Ok(CommandEffect::None) 504} 505 506pub(crate) type RunTillNextErr<Emulator> = <Emulator as EmulatorTrait>::Err< 507 <ThreadedRenderer as Renderer>::Err, 508 <SdlAudioOutput as AudioOutput>::Err, 509 <FsSaveWriter as SaveWriter>::Err, 510>; 511 512fn run_till_next_frame<Emulator: EmulatorTrait>( 513 state: &mut RunnerThreadState<Emulator>, 514) -> Result<(), RunTillNextErr<Emulator>> { 515 match &mut state.debugger_process { 516 Some(debugger_process) => debugger_process.run_emulator_till_next_frame( 517 &mut state.emulator, 518 &mut state.renderer, 519 &mut state.audio_output, 520 &mut state.input_poller, 521 &mut state.save_writer, 522 ), 523 None => { 524 while state.emulator.tick( 525 &mut state.renderer, 526 &mut state.audio_output, 527 &mut state.input_poller, 528 &mut state.save_writer, 529 )? != TickEffect::FrameRendered 530 {} 531 532 Ok(()) 533 } 534 } 535} 536 537fn save_state<Emulator: EmulatorTrait>( 538 state: &mut RunnerThreadState<Emulator>, 539 slot: usize, 540) -> Result<(), CommandError> { 541 if state 542 .state_saver_thread 543 .send_save_request( 544 &state.emulator, 545 &state.save_state_paths, 546 slot, 547 Arc::clone(&state.save_state_metadata), 548 ) 549 .is_err() 550 { 551 log::error!("Lost connection to state saver thread; this is probably a bug"); 552 return state 553 .response_sender 554 .send(RunnerCommandResponse::SaveStateFailed { 555 slot, 556 err: NativeEmulatorError::LostRunnerConnection, 557 }) 558 .map_err(|_| CommandError::LostConnection); 559 } 560 561 Ok(()) 562} 563 564fn try_recv_save_state_responses<Emulator: EmulatorTrait>( 565 state: &mut RunnerThreadState<Emulator>, 566) -> Result<(), CommandError> { 567 while let Some(result) = state.state_saver_thread.try_recv_save_response() { 568 match result { 569 Ok(response) => { 570 state 571 .response_sender 572 .send(RunnerCommandResponse::SaveStateSucceeded { slot: response.slot }) 573 .map_err(|_| CommandError::LostConnection)?; 574 } 575 Err((err, response)) => { 576 state 577 .response_sender 578 .send(RunnerCommandResponse::SaveStateFailed { slot: response.slot, err }) 579 .map_err(|_| CommandError::LostConnection)?; 580 } 581 } 582 } 583 584 Ok(()) 585} 586 587fn load_state<Emulator: EmulatorTrait>( 588 state: &mut RunnerThreadState<Emulator>, 589 slot: usize, 590) -> Result<(), CommandError> { 591 let result = 592 state::load(&mut state.emulator, &state.emulator_config, &state.save_state_paths, slot); 593 594 let message = match result { 595 Ok(()) => RunnerCommandResponse::LoadStateSucceeded { slot }, 596 Err(err) => RunnerCommandResponse::LoadStateFailed { slot, err }, 597 }; 598 599 state.response_sender.send(message).map_err(|_| CommandError::LostConnection) 600} 601 602fn change_disc<Emulator: EmulatorTrait>( 603 state: &mut RunnerThreadState<Emulator>, 604 path: PathBuf, 605) -> Result<(), CommandError> { 606 let result = (state.change_disc_fn)(&mut state.emulator, &path, &state.emulator_config); 607 608 if result.is_ok() { 609 state.rom_path.clone_from(&path); 610 611 if let Err(err) = state.update_save_paths() { 612 log::error!("Error updating save paths after disc change: {err}"); 613 } 614 } 615 616 let message = match result { 617 Ok(window_title) => RunnerCommandResponse::ChangeDiscSucceeded { window_title }, 618 Err(err) => RunnerCommandResponse::ChangeDiscFailed(err), 619 }; 620 621 state.response_sender.send(message).map_err(|_| CommandError::LostConnection) 622} 623 624fn remove_disc<Emulator: EmulatorTrait>( 625 state: &mut RunnerThreadState<Emulator>, 626) -> Result<(), CommandError> { 627 let window_title = (state.remove_disc_fn)(&mut state.emulator); 628 629 state 630 .response_sender 631 .send(RunnerCommandResponse::ChangeDiscSucceeded { window_title }) 632 .map_err(|_| CommandError::LostConnection) 633}