use std::{ collections::HashMap, fs::{self, File}, io::{Read, Seek, SeekFrom, Write}, path::{Path, PathBuf}, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, mpsc::{self, RecvError, TryRecvError}, Arc, }, }; use anyhow::Result; use egui_toast::{Toast, ToastKind, ToastOptions}; use crate::{audio::Audio, graphics::TextureSink}; pub use shrooms_vb_core::VBKey; use shrooms_vb_core::{Sim, EXPECTED_FRAME_SIZE}; mod shrooms_vb_core; pub struct EmulatorBuilder { rom: Option, commands: mpsc::Receiver, sim_count: Arc, running: Arc<[AtomicBool; 2]>, has_game: Arc<[AtomicBool; 2]>, audio_on: Arc<[AtomicBool; 2]>, linked: Arc, } #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] pub enum SimId { Player1, Player2, } impl SimId { pub const fn values() -> [Self; 2] { [Self::Player1, Self::Player2] } pub const fn to_index(self) -> usize { match self { Self::Player1 => 0, Self::Player2 => 1, } } } struct Cart { rom_path: PathBuf, rom: Vec, sram_file: File, sram: Vec, } impl Cart { fn load(rom_path: &Path, sim_id: SimId) -> Result { let rom = fs::read(rom_path)?; let mut sram_file = File::options() .read(true) .write(true) .create(true) .truncate(false) .open(sram_path(rom_path, sim_id))?; sram_file.set_len(8 * 1024)?; let mut sram = vec![]; sram_file.read_to_end(&mut sram)?; Ok(Cart { rom_path: rom_path.to_path_buf(), rom, sram_file, sram, }) } } fn sram_path(rom_path: &Path, sim_id: SimId) -> PathBuf { match sim_id { SimId::Player1 => rom_path.with_extension("p1.sram"), SimId::Player2 => rom_path.with_extension("p2.sram"), } } impl EmulatorBuilder { pub fn new() -> (Self, EmulatorClient) { let (queue, commands) = mpsc::channel(); let builder = Self { rom: None, commands, sim_count: Arc::new(AtomicUsize::new(0)), running: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]), has_game: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]), audio_on: Arc::new([AtomicBool::new(true), AtomicBool::new(true)]), linked: Arc::new(AtomicBool::new(false)), }; let client = EmulatorClient { queue, sim_count: builder.sim_count.clone(), running: builder.running.clone(), has_game: builder.has_game.clone(), audio_on: builder.audio_on.clone(), linked: builder.linked.clone(), }; (builder, client) } pub fn with_rom(self, path: &Path) -> Self { Self { rom: Some(path.into()), ..self } } pub fn build(self) -> Result { let mut emulator = Emulator::new( self.commands, self.sim_count, self.running, self.has_game, self.audio_on, self.linked, )?; if let Some(path) = self.rom { emulator.load_cart(SimId::Player1, &path)?; } Ok(emulator) } } pub struct Emulator { sims: Vec, carts: [Option; 2], audio: Audio, commands: mpsc::Receiver, sim_count: Arc, running: Arc<[AtomicBool; 2]>, has_game: Arc<[AtomicBool; 2]>, audio_on: Arc<[AtomicBool; 2]>, linked: Arc, renderers: HashMap, messages: HashMap>, } impl Emulator { fn new( commands: mpsc::Receiver, sim_count: Arc, running: Arc<[AtomicBool; 2]>, has_game: Arc<[AtomicBool; 2]>, audio_on: Arc<[AtomicBool; 2]>, linked: Arc, ) -> Result { Ok(Self { sims: vec![], carts: [None, None], audio: Audio::init()?, commands, sim_count, running, has_game, audio_on, linked, renderers: HashMap::new(), messages: HashMap::new(), }) } pub fn load_cart(&mut self, sim_id: SimId, path: &Path) -> Result<()> { let cart = Cart::load(path, sim_id)?; self.reset_sim(sim_id, Some(cart))?; Ok(()) } pub fn start_second_sim(&mut self, rom: Option) -> Result<()> { let rom_path = if let Some(path) = rom { Some(path) } else { self.carts[0].as_ref().map(|c| c.rom_path.clone()) }; let cart = match rom_path { Some(rom_path) => Some(Cart::load(&rom_path, SimId::Player2)?), None => None, }; self.reset_sim(SimId::Player2, cart)?; self.link_sims(); Ok(()) } fn reset_sim(&mut self, sim_id: SimId, new_cart: Option) -> Result<()> { self.save_sram(sim_id)?; let index = sim_id.to_index(); while self.sims.len() <= index { self.sims.push(Sim::new()); } self.sim_count.store(self.sims.len(), Ordering::Relaxed); let sim = &mut self.sims[index]; sim.reset(); if let Some(cart) = new_cart { sim.load_cart(cart.rom.clone(), cart.sram.clone())?; self.carts[index] = Some(cart); self.has_game[index].store(true, Ordering::Release); } if self.has_game[index].load(Ordering::Acquire) { self.running[index].store(true, Ordering::Release); } Ok(()) } fn link_sims(&mut self) { let (first, second) = self.sims.split_at_mut(1); let Some(first) = first.first_mut() else { return; }; let Some(second) = second.first_mut() else { return; }; first.link(second); self.linked.store(true, Ordering::Release); } fn unlink_sims(&mut self) { let Some(first) = self.sims.first_mut() else { return; }; first.unlink(); self.linked.store(false, Ordering::Release); } pub fn pause_sim(&mut self, sim_id: SimId) -> Result<()> { self.running[sim_id.to_index()].store(false, Ordering::Release); self.save_sram(sim_id) } fn save_sram(&mut self, sim_id: SimId) -> Result<()> { let sim = self.sims.get_mut(sim_id.to_index()); let cart = self.carts[sim_id.to_index()].as_mut(); if let (Some(sim), Some(cart)) = (sim, cart) { sim.read_sram(&mut cart.sram); cart.sram_file.seek(SeekFrom::Start(0))?; cart.sram_file.write_all(&cart.sram)?; } Ok(()) } pub fn stop_second_sim(&mut self) -> Result<()> { self.save_sram(SimId::Player2)?; self.renderers.remove(&SimId::Player2); self.sims.truncate(1); self.sim_count.store(self.sims.len(), Ordering::Relaxed); self.running[SimId::Player2.to_index()].store(false, Ordering::Release); self.has_game[SimId::Player2.to_index()].store(false, Ordering::Release); self.linked.store(false, Ordering::Release); Ok(()) } pub fn run(&mut self) { let mut eye_contents = vec![0u8; 384 * 224 * 2]; let mut audio_samples = vec![]; loop { let p1_running = self.running[SimId::Player1.to_index()].load(Ordering::Acquire); let p2_running = self.running[SimId::Player2.to_index()].load(Ordering::Acquire); let mut idle = p1_running || p2_running; if p1_running && p2_running { Sim::emulate_many(&mut self.sims); } else if p1_running { self.sims[SimId::Player1.to_index()].emulate(); } else if p2_running { self.sims[SimId::Player2.to_index()].emulate(); } for sim_id in SimId::values() { let Some(renderer) = self.renderers.get_mut(&sim_id) else { continue; }; let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { continue; }; if sim.read_pixels(&mut eye_contents) { idle = false; if renderer.queue_render(&eye_contents).is_err() { self.renderers.remove(&sim_id); } } } let p1_audio = p1_running && self.audio_on[SimId::Player1.to_index()].load(Ordering::Acquire); let p2_audio = p2_running && self.audio_on[SimId::Player2.to_index()].load(Ordering::Acquire); let weight = if p1_audio && p2_audio { 0.5 } else { 1.0 }; if p1_audio { if let Some(sim) = self.sims.get_mut(SimId::Player1.to_index()) { sim.read_samples(&mut audio_samples, weight); } } if p2_audio { if let Some(sim) = self.sims.get_mut(SimId::Player2.to_index()) { sim.read_samples(&mut audio_samples, weight); } } if audio_samples.is_empty() { audio_samples.resize(EXPECTED_FRAME_SIZE, 0.0); } else { idle = false; } self.audio.update(&audio_samples); audio_samples.clear(); if idle { // The game is paused, and we have output all the video/audio we have. // Block the thread until a new command comes in. match self.commands.recv() { Ok(command) => self.handle_command(command), Err(RecvError) => { return; } } } loop { match self.commands.try_recv() { Ok(command) => self.handle_command(command), Err(TryRecvError::Empty) => { break; } Err(TryRecvError::Disconnected) => { return; } } } } } fn handle_command(&mut self, command: EmulatorCommand) { match command { EmulatorCommand::ConnectToSim(sim_id, renderer, messages) => { self.renderers.insert(sim_id, renderer); self.messages.insert(sim_id, messages); } EmulatorCommand::LoadGame(sim_id, path) => { if let Err(error) = self.load_cart(sim_id, &path) { self.report_error(sim_id, format!("Error loading rom: {error}")); } } EmulatorCommand::StartSecondSim(path) => { if let Err(error) = self.start_second_sim(path) { self.report_error( SimId::Player2, format!("Error starting second sim: {error}"), ); } } EmulatorCommand::StopSecondSim => { if let Err(error) = self.stop_second_sim() { self.report_error( SimId::Player2, format!("Error stopping second sim: {error}"), ); } } EmulatorCommand::Pause => { for sim_id in SimId::values() { if let Err(error) = self.pause_sim(sim_id) { self.report_error(sim_id, format!("Error pausing: {error}")); } } } EmulatorCommand::Resume => { for sim_id in SimId::values() { let index = sim_id.to_index(); if self.has_game[index].load(Ordering::Acquire) { self.running[index].store(true, Ordering::Relaxed); } } } EmulatorCommand::SetAudioEnabled(p1, p2) => { self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release); self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release); } EmulatorCommand::Link => { self.link_sims(); } EmulatorCommand::Unlink => { self.unlink_sims(); } EmulatorCommand::Reset(sim_id) => { if let Err(error) = self.reset_sim(sim_id, None) { self.report_error(sim_id, format!("Error resetting sim: {error}")); } } EmulatorCommand::SetKeys(sim_id, keys) => { if let Some(sim) = self.sims.get_mut(sim_id.to_index()) { sim.set_keys(keys); } } EmulatorCommand::Exit(done) => { for sim_id in SimId::values() { if let Err(error) = self.save_sram(sim_id) { self.report_error(sim_id, format!("Error saving sram on exit: {error}")); } } let _ = done.send(()); } } } fn report_error(&self, sim_id: SimId, message: String) { let messages = self .messages .get(&sim_id) .or_else(|| self.messages.get(&SimId::Player1)); if let Some(msg) = messages { let toast = Toast::new() .kind(ToastKind::Error) .options(ToastOptions::default().duration_in_seconds(5.0)) .text(&message); if msg.send(toast).is_ok() { return; } } eprintln!("{}", message); } } #[derive(Debug)] pub enum EmulatorCommand { ConnectToSim(SimId, TextureSink, mpsc::Sender), LoadGame(SimId, PathBuf), StartSecondSim(Option), StopSecondSim, Pause, Resume, SetAudioEnabled(bool, bool), Link, Unlink, Reset(SimId), SetKeys(SimId, VBKey), Exit(oneshot::Sender<()>), } #[derive(Clone)] pub struct EmulatorClient { queue: mpsc::Sender, sim_count: Arc, running: Arc<[AtomicBool; 2]>, has_game: Arc<[AtomicBool; 2]>, audio_on: Arc<[AtomicBool; 2]>, linked: Arc, } impl EmulatorClient { pub fn has_player_2(&self) -> bool { self.sim_count.load(Ordering::Acquire) == 2 } pub fn is_running(&self, sim_id: SimId) -> bool { self.running[sim_id.to_index()].load(Ordering::Acquire) } pub fn has_game(&self, sim_id: SimId) -> bool { self.has_game[sim_id.to_index()].load(Ordering::Acquire) } pub fn are_sims_linked(&self) -> bool { self.linked.load(Ordering::Acquire) } pub fn is_audio_enabled(&self, sim_id: SimId) -> bool { self.audio_on[sim_id.to_index()].load(Ordering::Acquire) } pub fn send_command(&self, command: EmulatorCommand) -> bool { match self.queue.send(command) { Ok(()) => true, Err(err) => { eprintln!( "could not send command {:?} as emulator is shut down", err.0 ); false } } } }