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} 167 168/// Spawn a runner thread and return a handle to it. 169/// 170/// # Errors 171/// 172/// Propagates any errors encountered while initializing the emulator in the runner thread. 173/// 174/// 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} 275 276impl<Emulator: EmulatorTrait> RunnerThreadHandle<Emulator> { 277 /// Receive and render a pending frame if one is available. Blocks for up to `timeout`. 278 /// 279 /// # Errors 280 /// 281 /// Returns an error on timeout, if the runner thread has disconnected, or if `renderer` 282 /// returns an error rendering the received frame. 283 pub fn try_recv_frame<R: Renderer>( 284 &self, 285 renderer: &mut R, 286 timeout: Duration, 287 ) -> Result<(), RecvFrameError<R::Err>> { 288 self.renderer_handle.try_recv_frame(renderer, timeout) 289 } 290 291 /// Receive and propagate an error from the runner thread if one is available. Does not block. 292 /// 293 /// # Errors 294 /// 295 /// 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 } 303 304 /// Receive a command response from the runner thread if one is available. Does not block. 305 /// 306 /// The main thread should *not* wait for a response after sending a command; this is very 307 /// likely to cause a deadlock since the runner may be waiting for the main thread to receive 308 /// a frame. 309 pub fn try_recv_command_response(&self) -> Option<RunnerCommandResponse> { 310 self.response_receiver.try_recv().ok() 311 } 312 313 /// Tell the runner thread whether it should pause emulation. 314 pub fn set_paused(&self, paused: bool) { 315 self.paused.store(paused, Ordering::Relaxed); 316 } 317 318 /// Send updated inputs to the runner thread. 319 pub fn update_inputs(&mut self, inputs: &Emulator::Inputs) { 320 self.input_poller_handle.update_inputs(inputs); 321 } 322 323 /// Send a command to the runner thread. 324 /// 325 /// # Errors 326 /// 327 /// Returns an error if the runner thread has disconnected. 328 pub fn send_command(&self, command: RunnerCommand<Emulator>) -> NativeEmulatorResult<()> { 329 self.command_sender.send(command).map_err(|_| NativeEmulatorError::LostRunnerConnection) 330 } 331 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}