runner.rsannotatedrunner.rssource633 lines · 21.2 KB · raw
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}