Support screenshots
This commit is contained in:
		
							parent
							
								
									f1658619ad
								
							
						
					
					
						commit
						b6b0a8c22b
					
				| 
						 | 
				
			
			@ -178,7 +178,7 @@ pub struct Emulator {
 | 
			
		|||
    debuggers: HashMap<SimId, DebugInfo>,
 | 
			
		||||
    stdouts: HashMap<SimId, mpsc::Sender<String>>,
 | 
			
		||||
    watched_regions: HashMap<MemoryRange, Weak<MemoryRegion>>,
 | 
			
		||||
    eye_contents: Vec<u8>,
 | 
			
		||||
    eye_contents: [Vec<u8>; 2],
 | 
			
		||||
    audio_samples: Vec<f32>,
 | 
			
		||||
    buffer: Vec<u8>,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<Vec<u8>>),
 | 
			
		||||
    Exit(oneshot::Sender<()>),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<Option<String>> {
 | 
			
		||||
        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() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue