diff --git a/gb-emu/src/main.rs b/gb-emu/src/main.rs index 759381a..0a1c689 100644 --- a/gb-emu/src/main.rs +++ b/gb-emu/src/main.rs @@ -75,6 +75,9 @@ struct Args { // /// Use webcam as Pocket Camera emulation // #[arg(short, long)] // camera: bool, + /// Record frames to image sequence + #[arg(long)] + record: bool, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -128,8 +131,6 @@ fn run(args: Args) -> ! { let config = config_manager.load_or_create_base_config(); let standalone_config: StandaloneConfig = config_manager.load_or_create_config("standalone"); - let (output, stream) = audio::create_output(args.mute); - let rom_file = RomFile::Path(PathBuf::from(args.rom)); let (rom, camera) = rom_file @@ -139,6 +140,15 @@ fn run(args: Args) -> ! { ) .expect("Error parsing rom"); + let configs = CONFIGS.get_or_init(|| Configs { + standalone_config, + emu_config: config.clone(), + config_dir: config_manager.dir(), + rom_title: rom.get_title().to_owned(), + }); + + let (output, stream) = audio::create_output(args.mute); + let will_be_cgb = rom.rom_type == CgbRomType::CgbOnly || config.prefer_cgb; let shader_path = if will_be_cgb { @@ -163,9 +173,9 @@ fn run(args: Args) -> ! { gb_emu_lib::config::ResolutionOverride::Scale(scale) => Some(scale), gb_emu_lib::config::ResolutionOverride::Default => None, } - .unwrap_or(standalone_config.scale_factor); + .unwrap_or(configs.standalone_config.scale_factor); - let mut window_manager = WindowManager::new(sender, stream); + let mut window_manager = WindowManager::new(sender, stream, args.record); let window = window_manager.add_main( scale_override, @@ -175,24 +185,22 @@ fn run(args: Args) -> ! { ); let tile_window: Option = if args.tile_window { - Some(window_manager.add(standalone_config.scale_factor, None, None, false)) + Some(window_manager.add(configs.standalone_config.scale_factor, None, None, false)) } else { None }; let layer_window: Option = if args.layer_window { - Some(window_manager.add(standalone_config.scale_factor.min(2), None, None, false)) + Some(window_manager.add( + configs.standalone_config.scale_factor.min(2), + None, + None, + false, + )) } else { None }; - CONFIGS.get_or_init(|| Configs { - standalone_config, - emu_config: config.clone(), - config_dir: config_manager.dir(), - rom_title: rom.get_title().to_owned(), - }); - let options = EmulatorOptions::new_with_config(config, config_manager.dir(), window, rom, output) .with_serial_target(if args.ascii { diff --git a/gb-emu/src/window.rs b/gb-emu/src/window.rs index d042f07..f37cfec 100644 --- a/gb-emu/src/window.rs +++ b/gb-emu/src/window.rs @@ -42,6 +42,7 @@ struct WindowData { pub struct WindowManager { event_loop: EventLoop<()>, data: WindowManagerData, + record_main: bool, } struct WindowManagerData { @@ -54,7 +55,7 @@ struct WindowManagerData { } impl WindowManager { - pub(crate) fn new(sender: Sender, _stream: Stream) -> Self { + pub(crate) fn new(sender: Sender, _stream: Stream, record_main: bool) -> Self { let event_loop = EventLoop::new(); #[cfg(feature = "vulkan")] let window_data_manager = @@ -69,8 +70,10 @@ impl WindowManager { window_data_manager, input: Arc::new(Mutex::new(WinitInputHelper::new())), sender, + _stream, }, + record_main, } } @@ -88,6 +91,7 @@ impl WindowManager { resizable, &self.event_loop, true, + self.record_main, ) } @@ -105,6 +109,7 @@ impl WindowManager { resizable, &self.event_loop, false, + false, ) } @@ -122,6 +127,7 @@ impl WindowManager { } impl WindowManagerData { + #[allow(clippy::too_many_arguments)] fn add( &mut self, factor: usize, @@ -130,6 +136,7 @@ impl WindowManagerData { resizable: bool, event_loop: &EventLoop<()>, is_main: bool, + record: bool, ) -> WindowRenderer { let (r, info) = WindowRenderer::new( factor, @@ -139,6 +146,7 @@ impl WindowManagerData { self.window_data_manager.clone(), shader_path, resizable, + record, ); self.windows.insert(info.id, info.data); if is_main { @@ -256,9 +264,16 @@ pub struct WindowRenderer { gamepad_handler: Option, joypad_state: JoypadState, current_rumble: bool, + recording: Option, +} + +struct RecordInfo { + dir: PathBuf, + frame_num: usize, } impl WindowRenderer { + #[allow(clippy::too_many_arguments)] pub fn new( factor: usize, gamepad_handler: Option, @@ -267,6 +282,7 @@ impl WindowRenderer { manager: Arc, shader_path: Option, resizable: bool, + record: bool, ) -> (Self, WindowInfo) { let window = WindowBuilder::new() .with_title("Gameboy") @@ -303,6 +319,22 @@ impl WindowRenderer { data: data.clone(), }; + let recording = if record { + let configs = access_config(); + + let dir = configs.config_dir.join(format!( + "recordings/{} - {}", + chrono::DateTime::::from(std::time::SystemTime::now()).to_rfc3339(), + configs.rom_title, + )); + + std::fs::create_dir_all(&dir).expect("could not create screenshot directory!"); + + Some(RecordInfo { dir, frame_num: 0 }) + } else { + None + }; + ( Self { data, @@ -313,6 +345,7 @@ impl WindowRenderer { gamepad_handler, joypad_state: JoypadState::default(), current_rumble: false, + recording, }, info, ) @@ -359,6 +392,20 @@ impl Renderer<[u8; 4]> for WindowRenderer { last_buf.resize(buffer.len(), [0; 4]); last_buf.copy_from_slice(buffer); } + + if let Some(ref mut info) = self.recording { + let image = ImageBuffer::, _>::from_raw( + self.width as u32, + self.height as u32, + bytemuck::cast_slice(buffer), + ) + .unwrap(); + + let frame_path = info.dir.join(format!("{:0>5}.png", info.frame_num)); + image.save(frame_path).expect("Could not save frame!"); + + info.frame_num += 1; + } } fn set_title(&mut self, title: String) {