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}

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.

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    }

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.

309    pub fn try_recv_command_response(&self) -> Option<RunnerCommandResponse> {
310        self.response_receiver.try_recv().ok()
311    }

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    }

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    }

Send a command to the runner thread.

Errors

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    }
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}