diff --git a/src/emulator.rs b/src/emulator.rs index b4d0ba8..6e8dd0d 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -178,7 +178,7 @@ pub struct Emulator { debuggers: HashMap, stdouts: HashMap>, watched_regions: HashMap>, - eye_contents: Vec, + eye_contents: [Vec; 2], audio_samples: Vec, buffer: Vec, } @@ -205,7 +205,7 @@ impl Emulator { debuggers: HashMap::new(), stdouts: HashMap::new(), watched_regions: HashMap::new(), - eye_contents: vec![0u8; 384 * 224 * 2], + eye_contents: [vec![0u8; 384 * 224 * 2], vec![0u8; 384 * 224 * 2]], audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE), buffer: vec![], }) @@ -528,9 +528,12 @@ impl Emulator { let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { continue; }; - if sim.read_pixels(&mut self.eye_contents) { + if sim.read_pixels(&mut self.eye_contents[sim_id.to_index()]) { idle = false; - if renderer.queue_render(&self.eye_contents).is_err() { + if renderer + .queue_render(&self.eye_contents[sim_id.to_index()]) + .is_err() + { self.renderers.remove(&sim_id); } } @@ -702,6 +705,10 @@ impl Emulator { sim.set_keys(keys); } } + EmulatorCommand::Screenshot(sim_id, sender) => { + let contents = self.eye_contents[sim_id.to_index()].clone(); + let _ = sender.send(contents); + } EmulatorCommand::Exit(done) => { for sim_id in SimId::values() { if let Err(error) = self.save_sram(sim_id) { @@ -761,6 +768,7 @@ pub enum EmulatorCommand { Unlink, Reset(SimId), SetKeys(SimId, VBKey), + Screenshot(SimId, oneshot::Sender>), Exit(oneshot::Sender<()>), } diff --git a/src/input.rs b/src/input.rs index ce7a06f..4b0ce3a 100644 --- a/src/input.rs +++ b/src/input.rs @@ -471,11 +471,12 @@ pub enum Command { FastForward(u32), Reset, PauseResume, + Screenshot, // if you update this, update Command::all and add a default } impl Command { - pub fn all() -> [Self; 6] { + pub fn all() -> [Self; 7] { [ Self::OpenRom, Self::Quit, @@ -483,6 +484,7 @@ impl Command { Self::Reset, Self::FrameAdvance, Self::FastForward(0), + Self::Screenshot, ] } @@ -494,6 +496,7 @@ impl Command { Self::Reset => "Reset", Self::FrameAdvance => "Frame Advance", Self::FastForward(_) => "Fast Forward", + Self::Screenshot => "Screenshot", } } } @@ -539,6 +542,10 @@ impl Default for Shortcuts { Command::FastForward(0), KeyboardShortcut::new(Modifiers::NONE, Key::Space), ); + shortcuts.set( + Command::Screenshot, + KeyboardShortcut::new(Modifiers::NONE, Key::F12), + ); shortcuts } } diff --git a/src/window/game.rs b/src/window/game.rs index f2d4631..c987e3e 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -6,11 +6,12 @@ use crate::{ input::{Command, ShortcutProvider}, persistence::Persistence, }; +use anyhow::Context as _; use egui::{ Align2, Button, CentralPanel, Color32, Context, Direction, Frame, TopBottomPanel, Ui, Vec2, ViewportBuilder, ViewportCommand, ViewportId, Window, menu, }; -use egui_toast::{Toast, Toasts}; +use egui_toast::{Toast, ToastKind, ToastOptions, Toasts}; use serde::{Deserialize, Serialize}; use winit::event_loop::EventLoopProxy; @@ -69,7 +70,7 @@ impl GameWindow { } } - fn show_menu(&mut self, ctx: &Context, ui: &mut Ui) { + fn show_menu(&mut self, ctx: &Context, ui: &mut Ui, toasts: &mut Toasts) { let state = self.client.emulator_state(); let is_ready = self.client.sim_state(self.sim_id) == SimState::Ready; let can_pause = is_ready && state == EmulatorState::Running; @@ -113,6 +114,16 @@ impl GameWindow { self.client .send_command(EmulatorCommand::SetSpeed(speed as f64)); } + Command::Screenshot => { + let autopause = state == EmulatorState::Running && can_pause; + if autopause { + self.client.send_command(EmulatorCommand::Pause); + } + pollster::block_on(self.take_screenshot(toasts)); + if autopause { + self.client.send_command(EmulatorCommand::Resume); + } + } } } @@ -178,6 +189,17 @@ impl GameWindow { self.client.send_command(EmulatorCommand::FrameAdvance); ui.close_menu(); } + ui.separator(); + if ui + .add_enabled( + is_ready, + self.button_for(ui.ctx(), "Screenshot", Command::Screenshot), + ) + .clicked() + { + pollster::block_on(self.take_screenshot(toasts)); + ui.close_menu(); + } }); ui.menu_button("Options", |ui| self.show_options_menu(ctx, ui)); ui.menu_button("Multiplayer", |ui| { @@ -262,6 +284,56 @@ impl GameWindow { }); } + async fn take_screenshot(&self, toasts: &mut Toasts) { + match self.try_take_screenshot().await { + Ok(Some(path)) => { + toasts.add( + Toast::new() + .kind(ToastKind::Info) + .options(ToastOptions::default().duration_in_seconds(5.0)) + .text(format!("Saved to {path}")), + ); + } + Ok(None) => {} + Err(error) => { + toasts.add( + Toast::new() + .kind(ToastKind::Error) + .options(ToastOptions::default().duration_in_seconds(5.0)) + .text(format!("{error:#}")), + ); + } + } + } + + async fn try_take_screenshot(&self) -> anyhow::Result> { + let (tx, rx) = oneshot::channel(); + self.client + .send_command(EmulatorCommand::Screenshot(self.sim_id, tx)); + let bytes = rx.await.context("Could not take screenshot")?; + let file = rfd::AsyncFileDialog::new() + .add_filter("PNG images", &["png"]) + .set_file_name("screenshot.png") + .save_file() + .await; + let Some(file) = file else { + return Ok(None); + }; + if bytes.len() != 384 * 224 * 2 { + anyhow::bail!("Unexpected screenshot size"); + } + let mut screencap = image::GrayImage::new(384 * 2, 224); + for (index, pixel) in bytes.into_iter().enumerate() { + let x = (index / 2) % 384 + ((index % 2) * 384); + let y = (index / 2) / 384; + screencap.put_pixel(x as u32, y as u32, image::Luma([pixel])); + } + screencap + .save(&file.path()) + .context("Could not save screenshot")?; + Ok(Some(file.path().display().to_string())) + } + fn show_options_menu(&mut self, ctx: &Context, ui: &mut Ui) { ui.menu_button("Video", |ui| { ui.menu_button("Screen Size", |ui| { @@ -478,7 +550,7 @@ impl AppWindow for GameWindow { .exact_height(22.0) .show(ctx, |ui| { menu::bar(ui, |ui| { - self.show_menu(ctx, ui); + self.show_menu(ctx, ui, &mut toasts); }); }); if self.color_picker.is_some() {