use crate::{
nes::{
emulation::Emulation,
event::{NesEvent, NesEventProxy},
input::{Gamepads, InputBindings},
renderer::{FrameRecycle, Renderer, Resources, painter::Painter},
},
platform::Initialize,
};
use anyhow::Context;
use cfg_if::cfg_if;
use config::Config;
use crossbeam::channel::Receiver;
use egui::ahash::HashMap;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use tetanes_core::{time::Instant, video::Frame};
use thingbuf::mpsc::blocking;
use winit::{
event::Modifiers,
event_loop::{ActiveEventLoop, EventLoop},
window::{Window, WindowId},
};
pub mod action;
pub mod audio;
pub mod config;
pub mod emulation;
pub mod event;
pub mod input;
pub mod renderer;
pub mod rom;
pub mod version;
#[derive(Debug)]
#[must_use]
pub struct Nes {
pub(crate) init_state: Option<(Config, NesEventProxy)>,
pub(crate) state: State,
}
#[derive(Debug)]
#[must_use]
pub(crate) enum State {
Suspended {
should_terminate: Arc<AtomicBool>,
},
Pending {
ctx: egui::Context,
window: Arc<Window>,
painter_rx: Receiver<Painter>,
should_terminate: Arc<AtomicBool>,
},
Running(Box<Running>),
Exiting,
}
impl Default for State {
fn default() -> Self {
Self::Suspended {
should_terminate: Default::default(),
}
}
}
impl State {
pub const fn is_exiting(&self) -> bool {
matches!(self, Self::Exiting)
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
#[must_use]
pub enum RunState {
Running,
ManuallyPaused,
AutoPaused,
}
impl RunState {
pub const fn paused(&self) -> bool {
matches!(self, Self::ManuallyPaused | Self::AutoPaused)
}
pub const fn auto_paused(&self) -> bool {
matches!(self, Self::AutoPaused)
}
pub const fn manually_paused(&self) -> bool {
matches!(self, Self::ManuallyPaused)
}
}
#[derive(Debug)]
pub(crate) struct Running {
pub(crate) cfg: Config,
#[cfg_attr(target_arch = "wasm32", allow(unused))]
pub(crate) tx: NesEventProxy,
pub(crate) should_terminate: Arc<AtomicBool>,
pub(crate) emulation: Emulation,
pub(crate) renderer: Renderer,
pub(crate) input_bindings: InputBindings,
pub(crate) gamepads: Gamepads,
pub(crate) modifiers: Modifiers,
pub(crate) replay_recording: bool,
pub(crate) audio_recording: bool,
pub(crate) rewinding: bool,
pub(crate) occluded: bool,
pub(crate) repaint_times: HashMap<WindowId, Instant>,
}
impl Nes {
pub fn run(cfg: Config) -> anyhow::Result<()> {
let event_loop = EventLoop::<NesEvent>::with_user_event().build()?;
let nes = Nes::new(cfg, &event_loop);
cfg_if! {
if #[cfg(target_arch = "wasm32")] {
use winit::platform::web::EventLoopExtWebSys;
event_loop.spawn_app(nes);
} else {
let mut nes = nes;
event_loop.run_app(&mut nes)?;
}
}
Ok(())
}
pub fn should_terminate(&self) -> bool {
match &self.state {
State::Suspended { should_terminate }
| State::Pending {
should_terminate, ..
} => should_terminate.load(Ordering::Relaxed),
State::Running(running) => running.should_terminate.load(Ordering::Relaxed),
State::Exiting => true,
}
}
pub fn new(cfg: Config, event_loop: &EventLoop<NesEvent>) -> Self {
let should_terminate = Arc::new(AtomicBool::new(false));
#[cfg(not(target_arch = "wasm32"))]
let _ = ctrlc::set_handler({
let should_terminate = Arc::clone(&should_terminate);
move || {
should_terminate.store(true, Ordering::Relaxed);
}
});
Self {
init_state: Some((cfg, NesEventProxy::new(event_loop))),
state: State::Suspended { should_terminate },
}
}
pub(crate) fn request_renderer_resources(
&mut self,
event_loop: &ActiveEventLoop,
should_terminate: Arc<AtomicBool>,
) -> anyhow::Result<()> {
let (cfg, tx) = self
.init_state
.as_ref()
.context("config unexpectedly already taken")?;
let (ctx, window, painter_rx) = Renderer::request_resources(event_loop, tx, cfg)?;
self.state = State::Pending {
ctx,
window,
painter_rx,
should_terminate,
};
Ok(())
}
pub(crate) fn init_running(&mut self, event_loop: &ActiveEventLoop) -> anyhow::Result<()> {
match std::mem::take(&mut self.state) {
State::Pending {
ctx,
window,
painter_rx,
should_terminate,
} => {
let resources = Resources {
ctx,
window,
painter: painter_rx.recv()?,
};
let (frame_tx, frame_rx) = blocking::with_recycle::<Frame, _>(10, FrameRecycle);
let (mut cfg, tx) = self
.init_state
.take()
.context("config unexpectedly already taken")?;
let input_bindings = InputBindings::from_input_config(&cfg.input);
let gamepads = Gamepads::new();
cfg.input.update_gamepad_assignments(&gamepads);
let emulation = Emulation::new(tx.clone(), frame_tx.clone(), &cfg)?;
let renderer = Renderer::new(event_loop, tx.clone(), resources, frame_rx, &cfg)?;
let mut running = Running {
cfg,
tx,
should_terminate,
emulation,
renderer,
input_bindings,
gamepads,
modifiers: Modifiers::default(),
replay_recording: false,
audio_recording: false,
rewinding: false,
occluded: false,
repaint_times: HashMap::default(),
};
running.initialize()?;
self.state = State::Running(Box::new(running));
Ok(())
}
State::Running(running) => {
self.state = State::Running(running);
Ok(())
}
State::Suspended { .. } | State::Exiting => anyhow::bail!("not in pending state"),
}
}
}