From 5df5f910c2da2772ca09f2c5b89129dc9550b60a Mon Sep 17 00:00:00 2001 From: Imbris Date: Sat, 20 Mar 2021 04:03:42 -0400 Subject: [PATCH] Implement screenshots --- assets/voxygen/shaders/blit-frag.glsl | 16 ++ assets/voxygen/shaders/blit-vert.glsl | 15 ++ voxygen/src/render/pipelines/blit.rs | 129 +++++++++++++++ voxygen/src/render/pipelines/mod.rs | 1 + voxygen/src/render/renderer.rs | 91 ++++------- voxygen/src/render/renderer/drawer.rs | 59 ++++++- voxygen/src/render/renderer/screenshot.rs | 185 ++++++++++++++++++++++ voxygen/src/render/renderer/shaders.rs | 2 + voxygen/src/window.rs | 66 ++++---- 9 files changed, 461 insertions(+), 103 deletions(-) create mode 100644 assets/voxygen/shaders/blit-frag.glsl create mode 100644 assets/voxygen/shaders/blit-vert.glsl create mode 100644 voxygen/src/render/pipelines/blit.rs create mode 100644 voxygen/src/render/renderer/screenshot.rs diff --git a/assets/voxygen/shaders/blit-frag.glsl b/assets/voxygen/shaders/blit-frag.glsl new file mode 100644 index 0000000000..d0818b374a --- /dev/null +++ b/assets/voxygen/shaders/blit-frag.glsl @@ -0,0 +1,16 @@ +#version 420 core + +layout(set = 0, binding = 0) +uniform texture2D t_src_color; +layout(set = 0, binding = 1) +uniform sampler s_src_color; + +layout(location = 0) in vec2 uv; + +layout(location = 0) out vec4 tgt_color; + +void main() { + vec4 color = texture(sampler2D(t_src_color, s_src_color), uv); + + tgt_color = vec4(color.rgb, 1); +} diff --git a/assets/voxygen/shaders/blit-vert.glsl b/assets/voxygen/shaders/blit-vert.glsl new file mode 100644 index 0000000000..f6538bd743 --- /dev/null +++ b/assets/voxygen/shaders/blit-vert.glsl @@ -0,0 +1,15 @@ +#version 420 core + +layout(location = 0) out vec2 uv; + +void main() { + // Generate fullscreen triangle + vec2 v_pos = vec2( + float(gl_VertexIndex / 2) * 4.0 - 1.0, + float(gl_VertexIndex % 2) * 4.0 - 1.0 + ); + + uv = (v_pos * vec2(1.0, -1.0) + 1.0) * 0.5; + + gl_Position = vec4(v_pos, 0.0, 1.0); +} diff --git a/voxygen/src/render/pipelines/blit.rs b/voxygen/src/render/pipelines/blit.rs new file mode 100644 index 0000000000..0a0cc38de7 --- /dev/null +++ b/voxygen/src/render/pipelines/blit.rs @@ -0,0 +1,129 @@ +use super::{ + super::{AaMode, Consts}, + GlobalsLayouts, +}; +use bytemuck::{Pod, Zeroable}; +use vek::*; + +pub struct BindGroup { + pub(in super::super) bind_group: wgpu::BindGroup, +} + +pub struct BlitLayout { + pub layout: wgpu::BindGroupLayout, +} + +impl BlitLayout { + pub fn new(device: &wgpu::Device) -> Self { + Self { + layout: device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: None, + entries: &[ + // Color source + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::Sampler { + filtering: true, + comparison: false, + }, + count: None, + }, + ], + }), + } + } + + pub fn bind( + &self, + device: &wgpu::Device, + src_color: &wgpu::TextureView, + sampler: &wgpu::Sampler, + ) -> BindGroup { + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &self.layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(src_color), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(sampler), + }, + ], + }); + + BindGroup { bind_group } + } +} + +pub struct BlitPipeline { + pub pipeline: wgpu::RenderPipeline, +} + +impl BlitPipeline { + pub fn new( + device: &wgpu::Device, + vs_module: &wgpu::ShaderModule, + fs_module: &wgpu::ShaderModule, + sc_desc: &wgpu::SwapChainDescriptor, + layout: &BlitLayout, + ) -> Self { + common_base::span!(_guard, "BlitPipeline::new"); + let render_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Blit pipeline layout"), + push_constant_ranges: &[], + bind_group_layouts: &[&layout.layout], + }); + + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Blit pipeline"), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: vs_module, + entry_point: "main", + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(wgpu::FragmentState { + module: fs_module, + entry_point: "main", + targets: &[wgpu::ColorTargetState { + format: sc_desc.format, + blend: None, + write_mask: wgpu::ColorWrite::ALL, + }], + }), + }); + + Self { + pipeline: render_pipeline, + } + } +} diff --git a/voxygen/src/render/pipelines/mod.rs b/voxygen/src/render/pipelines/mod.rs index 152bd417e2..51e70de035 100644 --- a/voxygen/src/render/pipelines/mod.rs +++ b/voxygen/src/render/pipelines/mod.rs @@ -1,3 +1,4 @@ +pub mod blit; pub mod clouds; pub mod figure; pub mod fluid; diff --git a/voxygen/src/render/renderer.rs b/voxygen/src/render/renderer.rs index a7eb4dc9c8..af8e31e921 100644 --- a/voxygen/src/render/renderer.rs +++ b/voxygen/src/render/renderer.rs @@ -2,6 +2,7 @@ mod binding; pub(super) mod drawer; // Consts and bind groups for post-process and clouds mod locals; +mod screenshot; mod shaders; mod shadow_map; @@ -16,8 +17,8 @@ use super::{ mesh::Mesh, model::{DynamicModel, Model}, pipelines::{ - clouds, figure, fluid, lod_terrain, particle, postprocess, shadow, skybox, sprite, terrain, - ui, GlobalsBindGroup, GlobalsLayouts, ShadowTexturesBindGroup, + blit, clouds, figure, fluid, lod_terrain, particle, postprocess, shadow, skybox, sprite, + terrain, ui, GlobalsBindGroup, GlobalsLayouts, ShadowTexturesBindGroup, }, texture::Texture, AaMode, AddressMode, CloudMode, FilterMode, FluidMode, LightingMode, RenderError, RenderMode, @@ -50,6 +51,7 @@ struct Layouts { sprite: sprite::SpriteLayout, terrain: terrain::TerrainLayout, ui: ui::UiLayout, + blit: blit::BlitLayout, } /// A type that stores all the pipelines associated with this renderer. @@ -66,6 +68,7 @@ struct Pipelines { sprite: sprite::SpritePipeline, terrain: terrain::TerrainPipeline, ui: ui::UiPipeline, + blit: blit::BlitPipeline, } /// Render target views @@ -117,6 +120,9 @@ pub struct Renderer { mode: RenderMode, resolution: Vec2, + // If this is Some then a screenshot will be taken and passed to the handler here + take_screenshot: Option, + profiler: wgpu_profiler::GpuProfiler, profile_times: Vec, } @@ -217,6 +223,7 @@ impl Renderer { let sprite = sprite::SpriteLayout::new(&device); let terrain = terrain::TerrainLayout::new(&device); let ui = ui::UiLayout::new(&device); + let blit = blit::BlitLayout::new(&device); Layouts { global, @@ -229,6 +236,7 @@ impl Renderer { sprite, terrain, ui, + blit, } }; @@ -371,6 +379,8 @@ impl Renderer { mode, resolution: Vec2::new(dims.width, dims.height), + take_screenshot: None, + profiler, profile_times: Vec::new(), }) @@ -882,7 +892,7 @@ impl Renderer { Err(wgpu::SwapChainError::Timeout) => { // This will probably be resolved on the next frame // NOTE: we don't log this because it happens very frequently with - // PresentMode::Fifo on certain machines + // PresentMode::Fifo and unlimited FPS on certain machines return Ok(None); }, Err(err @ wgpu::SwapChainError::Outdated) => { @@ -1133,12 +1143,13 @@ impl Renderer { ) } - /// Creates a download buffer, downloads the win_color_view, and converts to - /// a image::DynamicImage. - //pub fn create_screenshot(&mut self) -> Result { - pub fn create_screenshot(&mut self) { - // TODO: save alongside a screenshot + /// Queue to obtain a screenshot on the next frame render + pub fn create_screenshot( + &mut self, + screenshot_handler: impl FnOnce(image::DynamicImage) + Send + 'static, + ) { + // Queue screenshot + self.take_screenshot = Some(Box::new(screenshot_handler)); // Take profiler snapshot if self.mode.profiler_enabled { let file_name = format!( @@ -1158,58 +1169,6 @@ impl Renderer { info!("Saved GPU timing snapshot as: {}", file_name); } } - //todo!() - // let (width, height) = self.get_resolution().into_tuple(); - - // let download_buf = self - // .device - // .create_buffer(&wgpu::BufferDescriptor { - // label: None, - // size: width * height * 4, - // usage : wgpu::BufferUsage::COPY_DST, - // mapped_at_creation: true - // }); - - // let encoder = - // self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor - // {label: None}); - - // encoder.copy_texture_to_buffer(&wgpu::TextureCopyViewBase { - // origin: &self.wi - // }, destination, copy_size) - - // self.encoder.copy_texture_to_buffer_raw( - // self.win_color_view.raw().get_texture(), - // None, - // gfx::texture::RawImageInfo { - // xoffset: 0, - // yoffset: 0, - // zoffset: 0, - // width, - // height, - // depth: 0, - // format: WinColorFmt::get_format(), - // mipmap: 0, - // }, - // download.raw(), - // 0, - // )?; - // self.flush(); - - // // Assumes that the format is Rgba8. - // let raw_data = self - // .factory - // .read_mapping(&download)? - // .chunks_exact(width as usize) - // .rev() - // .flatten() - // .flatten() - // .map(|&e| e) - // .collect::>(); - // Ok(image::DynamicImage::ImageRgba8( - // // Should not fail if the dimensions are correct. - // image::ImageBuffer::from_raw(width as u32, height as u32, - // raw_data).unwrap(), )) } // /// Queue the rendering of the provided skybox model in the upcoming frame. @@ -2167,6 +2126,15 @@ fn create_pipelines( &layouts.postprocess, ); + // Construct a pipeline for blitting, used during screenshotting + let blit_pipeline = blit::BlitPipeline::new( + device, + &create_shader("blit-vert", ShaderKind::Vertex)?, + &create_shader("blit-frag", ShaderKind::Fragment)?, + sc_desc, + &layouts.blit, + ); + // Consider reenabling at some time in the future // // // Construct a pipeline for rendering the player silhouette @@ -2233,6 +2201,7 @@ fn create_pipelines( lod_terrain: lod_terrain_pipeline, clouds: clouds_pipeline, postprocess: postprocess_pipeline, + blit: blit_pipeline, }, // player_shadow_pipeline, Some(point_shadow_pipeline), diff --git a/voxygen/src/render/renderer/drawer.rs b/voxygen/src/render/renderer/drawer.rs index d71ff59c8d..2a9f3a83be 100644 --- a/voxygen/src/render/renderer/drawer.rs +++ b/voxygen/src/render/renderer/drawer.rs @@ -5,8 +5,8 @@ use super::{ instances::Instances, model::{DynamicModel, Model, SubModel}, pipelines::{ - clouds, figure, fluid, lod_terrain, particle, postprocess, shadow, skybox, sprite, - terrain, ui, ColLights, GlobalsBindGroup, Light, Shadow, + blit, clouds, figure, fluid, lod_terrain, particle, postprocess, shadow, skybox, + sprite, terrain, ui, ColLights, GlobalsBindGroup, Light, Shadow, }, }, Renderer, ShadowMap, ShadowMapRenderer, @@ -35,6 +35,9 @@ pub struct Drawer<'frame> { borrow: RendererBorrow<'frame>, swap_tex: wgpu::SwapChainTexture, globals: &'frame GlobalsBindGroup, + // Texture and other info for taking a screenshot + // Writes to this instead in the third pass if it is present + taking_screenshot: Option, } impl<'frame> Drawer<'frame> { @@ -44,6 +47,16 @@ impl<'frame> Drawer<'frame> { swap_tex: wgpu::SwapChainTexture, globals: &'frame GlobalsBindGroup, ) -> Self { + let taking_screenshot = renderer.take_screenshot.take().map(|screenshot_fn| { + super::screenshot::TakeScreenshot::new( + &renderer.device, + &renderer.layouts.blit, + &renderer.sampler, + &renderer.sc_desc, + screenshot_fn, + ) + }); + let borrow = RendererBorrow { queue: &renderer.queue, device: &renderer.device, @@ -64,6 +77,7 @@ impl<'frame> Drawer<'frame> { borrow, swap_tex, globals, + taking_screenshot, } } @@ -169,7 +183,12 @@ impl<'frame> Drawer<'frame> { encoder.scoped_render_pass("third_pass", device, &wgpu::RenderPassDescriptor { label: Some("third pass (postprocess + ui)"), color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { - attachment: &self.swap_tex.view, + // If a screenshot was requested render to that as an intermediate texture + // instead + attachment: self + .taking_screenshot + .as_ref() + .map_or(&self.swap_tex.view, |s| s.texture_view()), resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), @@ -329,9 +348,41 @@ impl<'frame> Drop for Drawer<'frame> { fn drop(&mut self) { // TODO: submitting things to the queue can let the gpu start on them sooner // maybe we should submit each render pass to the queue as they are produced? - let (mut encoder, profiler) = self.encoder.take().unwrap().end_scope(); + let mut encoder = self.encoder.take().unwrap(); + + // If taking a screenshot + if let Some(screenshot) = self.taking_screenshot.take() { + // Image needs to be copied from the screenshot texture to the swapchain texture + let mut render_pass = encoder.scoped_render_pass( + "screenshot blit", + self.borrow.device, + &wgpu::RenderPassDescriptor { + label: Some("Blit screenshot pass"), + color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { + attachment: &self.swap_tex.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: true, + }, + }], + depth_stencil_attachment: None, + }, + ); + render_pass.set_pipeline(&self.borrow.pipelines.blit.pipeline); + render_pass.set_bind_group(0, &screenshot.bind_group(), &[]); + render_pass.draw(0..3, 0..1); + drop(render_pass); + // Issues a command to copy from the texture to a buffer and then sends the + // buffer off to another thread to be mapped and processed + screenshot.download_and_handle(&mut encoder); + } + + let (mut encoder, profiler) = encoder.end_scope(); profiler.resolve_queries(&mut encoder); + self.borrow.queue.submit(std::iter::once(encoder.finish())); + profiler .end_frame() .expect("Gpu profiler error! Maybe there was an unclosed scope?"); diff --git a/voxygen/src/render/renderer/screenshot.rs b/voxygen/src/render/renderer/screenshot.rs new file mode 100644 index 0000000000..cd52438c5e --- /dev/null +++ b/voxygen/src/render/renderer/screenshot.rs @@ -0,0 +1,185 @@ +use super::super::pipelines::blit; +use tracing::error; + +pub type ScreenshotFn = Box; + +pub struct TakeScreenshot { + bind_group: blit::BindGroup, + view: wgpu::TextureView, + texture: wgpu::Texture, + buffer: wgpu::Buffer, + screenshot_fn: ScreenshotFn, + // Dimensions used for copying from the screenshot texture to a buffer + width: u32, + height: u32, + bytes_per_pixel: u8, +} + +impl TakeScreenshot { + pub fn new( + device: &wgpu::Device, + blit_layout: &blit::BlitLayout, + sampler: &wgpu::Sampler, + // Used to determine the resolution and texture format + sc_desc: &wgpu::SwapChainDescriptor, + // Function that is given the image after downloading it from the GPU + // This is executed in a background thread + screenshot_fn: ScreenshotFn, + ) -> Self { + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("screenshot tex"), + size: wgpu::Extent3d { + width: sc_desc.width, + height: sc_desc.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: sc_desc.format, + usage: wgpu::TextureUsage::COPY_SRC + | wgpu::TextureUsage::SAMPLED + | wgpu::TextureUsage::RENDER_ATTACHMENT, + }); + + let view = texture.create_view(&wgpu::TextureViewDescriptor { + label: Some("screenshot tex view"), + format: Some(sc_desc.format), + dimension: Some(wgpu::TextureViewDimension::D2), + aspect: wgpu::TextureAspect::All, + base_mip_level: 0, + level_count: None, + base_array_layer: 0, + array_layer_count: None, + }); + + let bind_group = blit_layout.bind(device, &view, sampler); + + let bytes_per_pixel = sc_desc.format.describe().block_size; + let padded_bytes_per_row = padded_bytes_per_row(sc_desc.width, bytes_per_pixel); + + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("screenshot download buffer"), + size: (padded_bytes_per_row * sc_desc.height) as u64, + usage: wgpu::BufferUsage::COPY_DST | wgpu::BufferUsage::MAP_READ, + mapped_at_creation: false, + }); + + Self { + bind_group, + texture, + view, + buffer, + screenshot_fn, + width: sc_desc.width, + height: sc_desc.height, + bytes_per_pixel, + } + } + + /// Get the texture view for the screenshot + /// This can then be used as a render attachment + pub fn texture_view(&self) -> &wgpu::TextureView { &self.view } + + /// Get the bind group used for blitting the screenshot to the current + /// swapchain image + pub fn bind_group(&self) -> &wgpu::BindGroup { &self.bind_group.bind_group } + + /// NOTE: spawns thread + /// Call this after rendering to the screenshot texture + pub fn download_and_handle(self, encoder: &mut wgpu::CommandEncoder) { + // Calculate padded bytes per row + let padded_bytes_per_row = padded_bytes_per_row(self.width, self.bytes_per_pixel); + // Copy image to a buffer + encoder.copy_texture_to_buffer( + wgpu::TextureCopyView { + texture: &self.texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + }, + wgpu::BufferCopyView { + buffer: &self.buffer, + layout: wgpu::TextureDataLayout { + offset: 0, + bytes_per_row: padded_bytes_per_row, + rows_per_image: 0, + }, + }, + wgpu::Extent3d { + width: self.width, + height: self.height, + depth_or_array_layers: 1, + }, + ); + // Send buffer to another thread for async mapping, downloading, and passing to + // the given handler function (which probably saves it to the disk) + std::thread::Builder::new() + .name("screenshot".into()) + .spawn(move || { + self.download_and_handle_internal(); + }) + .expect("Failed to spawn screenshot thread"); + } + + fn download_and_handle_internal(self) { + // Calculate padded bytes per row + let padded_bytes_per_row = padded_bytes_per_row(self.width, self.bytes_per_pixel); + let singlethread_rt = match tokio::runtime::Builder::new_current_thread().build() { + Ok(rt) => rt, + Err(err) => { + error!(?err, "Could not create tokio runtime"); + return; + }, + }; + + // Map buffer + let buffer_slice = self.buffer.slice(..); + let buffer_map_future = buffer_slice.map_async(wgpu::MapMode::Read); + + // Wait on buffer mapping + let pixel_bytes = match singlethread_rt.block_on(buffer_map_future) { + // Buffer is mapped and we can read it + Ok(()) => { + // Copy to a Vec + let padded_buffer = buffer_slice.get_mapped_range(); + let mut pixel_bytes = Vec::new(); + padded_buffer + .chunks(padded_bytes_per_row as usize) + .map(|padded_chunk| { + &padded_chunk[..self.width as usize * self.bytes_per_pixel as usize] + }) + .for_each(|row| pixel_bytes.extend_from_slice(row)); + pixel_bytes + }, + // Error + Err(err) => { + error!( + ?err, + "Failed to map buffer for downloading a screenshot from the GPU" + ); + return; + }, + }; + + // Construct image + // TODO: support other formats + let image = image::ImageBuffer::, Vec>::from_vec( + self.width, + self.height, + pixel_bytes, + ) + .expect("Failed to create ImageBuffer! Buffer was not large enough. This should not occur"); + let image = image::DynamicImage::ImageBgra8(image); + + // Call supplied handler + (self.screenshot_fn)(image); + } +} + +// Graphics API requires a specific alignment for buffer copies +fn padded_bytes_per_row(width: u32, bytes_per_pixel: u8) -> u32 { + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; + let unpadded_bytes_per_row = width * bytes_per_pixel as u32; + let padding = (align - unpadded_bytes_per_row % align) % align; + unpadded_bytes_per_row + padding +} diff --git a/voxygen/src/render/renderer/shaders.rs b/voxygen/src/render/renderer/shaders.rs index 3373dbac83..8e21c8b12c 100644 --- a/voxygen/src/render/renderer/shaders.rs +++ b/voxygen/src/render/renderer/shaders.rs @@ -66,6 +66,8 @@ impl assets::Compound for Shaders { "clouds-frag", "postprocess-vert", "postprocess-frag", + "blit-vert", + "blit-frag", "player-shadow-frag", "light-shadows-geom", "light-shadows-frag", diff --git a/voxygen/src/window.rs b/voxygen/src/window.rs index 552a2fdd43..8083f7cc50 100644 --- a/voxygen/src/window.rs +++ b/voxygen/src/window.rs @@ -1341,44 +1341,34 @@ impl Window { pub fn send_event(&mut self, event: Event) { self.events.push(event) } pub fn take_screenshot(&mut self, settings: &Settings) { - let _ = self.renderer.create_screenshot(); - // TODO - /*match self.renderer.create_screenshot() { - Ok(img) => { - let mut path = settings.screenshots_path.clone(); - let sender = self.message_sender.clone(); - - let builder = std::thread::Builder::new().name("screenshot".into()); - builder - .spawn(move || { - use std::time::SystemTime; - // Check if folder exists and create it if it does not - if !path.exists() { - if let Err(e) = std::fs::create_dir_all(&path) { - warn!(?e, "Couldn't create folder for screenshot"); - let _result = sender - .send(String::from("Couldn't create folder for screenshot")); - } - } - path.push(format!( - "screenshot_{}.png", - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .map(|d| d.as_millis()) - .unwrap_or(0) - )); - if let Err(e) = img.save(&path) { - warn!(?e, "Couldn't save screenshot"); - let _result = sender.send(String::from("Couldn't save screenshot")); - } else { - let _result = sender - .send(format!("Screenshot saved to {}", path.to_string_lossy())); - } - }) - .unwrap(); - }, - Err(e) => error!(?e, "Couldn't create screenshot due to renderer error"), - }*/ + let sender = self.message_sender.clone(); + let mut path = settings.screenshots_path.clone(); + self.renderer.create_screenshot(move |image| { + use std::time::SystemTime; + // Check if folder exists and create it if it does not + if !path.exists() { + if let Err(e) = std::fs::create_dir_all(&path) { + warn!(?e, "Couldn't create folder for screenshot"); + let _result = + sender.send(String::from("Couldn't create folder for screenshot")); + } + } + path.push(format!( + "screenshot_{}.png", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0) + )); + // Try to save the image + if let Err(e) = image.into_rgba8().save(&path) { + warn!(?e, "Couldn't save screenshot"); + let _result = sender.send(String::from("Couldn't save screenshot")); + } else { + let _result = + sender.send(format!("Screenshot saved to {}", path.to_string_lossy())); + } + }); } fn is_pressed(