Implement screenshots

This commit is contained in:
Imbris 2021-03-20 04:03:42 -04:00 committed by Avi Weinstock
parent 0ad51204ec
commit d7b651451b
9 changed files with 461 additions and 103 deletions

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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,
}
}
}

View File

@ -1,3 +1,4 @@
pub mod blit;
pub mod clouds; pub mod clouds;
pub mod figure; pub mod figure;
pub mod fluid; pub mod fluid;

View File

@ -2,6 +2,7 @@ mod binding;
pub(super) mod drawer; pub(super) mod drawer;
// Consts and bind groups for post-process and clouds // Consts and bind groups for post-process and clouds
mod locals; mod locals;
mod screenshot;
mod shaders; mod shaders;
mod shadow_map; mod shadow_map;
@ -16,8 +17,8 @@ use super::{
mesh::Mesh, mesh::Mesh,
model::{DynamicModel, Model}, model::{DynamicModel, Model},
pipelines::{ pipelines::{
clouds, figure, fluid, lod_terrain, particle, postprocess, shadow, skybox, sprite, terrain, blit, clouds, figure, fluid, lod_terrain, particle, postprocess, shadow, skybox, sprite,
ui, GlobalsBindGroup, GlobalsLayouts, ShadowTexturesBindGroup, terrain, ui, GlobalsBindGroup, GlobalsLayouts, ShadowTexturesBindGroup,
}, },
texture::Texture, texture::Texture,
AaMode, AddressMode, CloudMode, FilterMode, FluidMode, LightingMode, RenderError, RenderMode, AaMode, AddressMode, CloudMode, FilterMode, FluidMode, LightingMode, RenderError, RenderMode,
@ -50,6 +51,7 @@ struct Layouts {
sprite: sprite::SpriteLayout, sprite: sprite::SpriteLayout,
terrain: terrain::TerrainLayout, terrain: terrain::TerrainLayout,
ui: ui::UiLayout, ui: ui::UiLayout,
blit: blit::BlitLayout,
} }
/// A type that stores all the pipelines associated with this renderer. /// A type that stores all the pipelines associated with this renderer.
@ -66,6 +68,7 @@ struct Pipelines {
sprite: sprite::SpritePipeline, sprite: sprite::SpritePipeline,
terrain: terrain::TerrainPipeline, terrain: terrain::TerrainPipeline,
ui: ui::UiPipeline, ui: ui::UiPipeline,
blit: blit::BlitPipeline,
} }
/// Render target views /// Render target views
@ -117,6 +120,9 @@ pub struct Renderer {
mode: RenderMode, mode: RenderMode,
resolution: Vec2<u32>, resolution: Vec2<u32>,
// If this is Some then a screenshot will be taken and passed to the handler here
take_screenshot: Option<screenshot::ScreenshotFn>,
profiler: wgpu_profiler::GpuProfiler, profiler: wgpu_profiler::GpuProfiler,
profile_times: Vec<wgpu_profiler::GpuTimerScopeResult>, profile_times: Vec<wgpu_profiler::GpuTimerScopeResult>,
} }
@ -217,6 +223,7 @@ impl Renderer {
let sprite = sprite::SpriteLayout::new(&device); let sprite = sprite::SpriteLayout::new(&device);
let terrain = terrain::TerrainLayout::new(&device); let terrain = terrain::TerrainLayout::new(&device);
let ui = ui::UiLayout::new(&device); let ui = ui::UiLayout::new(&device);
let blit = blit::BlitLayout::new(&device);
Layouts { Layouts {
global, global,
@ -229,6 +236,7 @@ impl Renderer {
sprite, sprite,
terrain, terrain,
ui, ui,
blit,
} }
}; };
@ -371,6 +379,8 @@ impl Renderer {
mode, mode,
resolution: Vec2::new(dims.width, dims.height), resolution: Vec2::new(dims.width, dims.height),
take_screenshot: None,
profiler, profiler,
profile_times: Vec::new(), profile_times: Vec::new(),
}) })
@ -882,7 +892,7 @@ impl Renderer {
Err(wgpu::SwapChainError::Timeout) => { Err(wgpu::SwapChainError::Timeout) => {
// This will probably be resolved on the next frame // This will probably be resolved on the next frame
// NOTE: we don't log this because it happens very frequently with // 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); return Ok(None);
}, },
Err(err @ wgpu::SwapChainError::Outdated) => { Err(err @ wgpu::SwapChainError::Outdated) => {
@ -1133,12 +1143,13 @@ impl Renderer {
) )
} }
/// Creates a download buffer, downloads the win_color_view, and converts to /// Queue to obtain a screenshot on the next frame render
/// a image::DynamicImage. pub fn create_screenshot(
//pub fn create_screenshot(&mut self) -> Result<image::DynamicImage, &mut self,
// RenderError> { screenshot_handler: impl FnOnce(image::DynamicImage) + Send + 'static,
pub fn create_screenshot(&mut self) { ) {
// TODO: save alongside a screenshot // Queue screenshot
self.take_screenshot = Some(Box::new(screenshot_handler));
// Take profiler snapshot // Take profiler snapshot
if self.mode.profiler_enabled { if self.mode.profiler_enabled {
let file_name = format!( let file_name = format!(
@ -1158,58 +1169,6 @@ impl Renderer {
info!("Saved GPU timing snapshot as: {}", file_name); 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::<Vec<_>>();
// 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. // /// Queue the rendering of the provided skybox model in the upcoming frame.
@ -2167,6 +2126,15 @@ fn create_pipelines(
&layouts.postprocess, &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 // Consider reenabling at some time in the future
// //
// // Construct a pipeline for rendering the player silhouette // // Construct a pipeline for rendering the player silhouette
@ -2233,6 +2201,7 @@ fn create_pipelines(
lod_terrain: lod_terrain_pipeline, lod_terrain: lod_terrain_pipeline,
clouds: clouds_pipeline, clouds: clouds_pipeline,
postprocess: postprocess_pipeline, postprocess: postprocess_pipeline,
blit: blit_pipeline,
}, },
// player_shadow_pipeline, // player_shadow_pipeline,
Some(point_shadow_pipeline), Some(point_shadow_pipeline),

View File

@ -5,8 +5,8 @@ use super::{
instances::Instances, instances::Instances,
model::{DynamicModel, Model, SubModel}, model::{DynamicModel, Model, SubModel},
pipelines::{ pipelines::{
clouds, figure, fluid, lod_terrain, particle, postprocess, shadow, skybox, sprite, blit, clouds, figure, fluid, lod_terrain, particle, postprocess, shadow, skybox,
terrain, ui, ColLights, GlobalsBindGroup, Light, Shadow, sprite, terrain, ui, ColLights, GlobalsBindGroup, Light, Shadow,
}, },
}, },
Renderer, ShadowMap, ShadowMapRenderer, Renderer, ShadowMap, ShadowMapRenderer,
@ -35,6 +35,9 @@ pub struct Drawer<'frame> {
borrow: RendererBorrow<'frame>, borrow: RendererBorrow<'frame>,
swap_tex: wgpu::SwapChainTexture, swap_tex: wgpu::SwapChainTexture,
globals: &'frame GlobalsBindGroup, 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<super::screenshot::TakeScreenshot>,
} }
impl<'frame> Drawer<'frame> { impl<'frame> Drawer<'frame> {
@ -44,6 +47,16 @@ impl<'frame> Drawer<'frame> {
swap_tex: wgpu::SwapChainTexture, swap_tex: wgpu::SwapChainTexture,
globals: &'frame GlobalsBindGroup, globals: &'frame GlobalsBindGroup,
) -> Self { ) -> 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 { let borrow = RendererBorrow {
queue: &renderer.queue, queue: &renderer.queue,
device: &renderer.device, device: &renderer.device,
@ -64,6 +77,7 @@ impl<'frame> Drawer<'frame> {
borrow, borrow,
swap_tex, swap_tex,
globals, globals,
taking_screenshot,
} }
} }
@ -169,7 +183,12 @@ impl<'frame> Drawer<'frame> {
encoder.scoped_render_pass("third_pass", device, &wgpu::RenderPassDescriptor { encoder.scoped_render_pass("third_pass", device, &wgpu::RenderPassDescriptor {
label: Some("third pass (postprocess + ui)"), label: Some("third pass (postprocess + ui)"),
color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { 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, resolve_target: None,
ops: wgpu::Operations { ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
@ -329,9 +348,41 @@ impl<'frame> Drop for Drawer<'frame> {
fn drop(&mut self) { fn drop(&mut self) {
// TODO: submitting things to the queue can let the gpu start on them sooner // 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? // 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); profiler.resolve_queries(&mut encoder);
self.borrow.queue.submit(std::iter::once(encoder.finish())); self.borrow.queue.submit(std::iter::once(encoder.finish()));
profiler profiler
.end_frame() .end_frame()
.expect("Gpu profiler error! Maybe there was an unclosed scope?"); .expect("Gpu profiler error! Maybe there was an unclosed scope?");

View File

@ -0,0 +1,185 @@
use super::super::pipelines::blit;
use tracing::error;
pub type ScreenshotFn = Box<dyn FnOnce(image::DynamicImage) + Send>;
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::<image::Bgra<u8>, Vec<u8>>::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
}

View File

@ -66,6 +66,8 @@ impl assets::Compound for Shaders {
"clouds-frag", "clouds-frag",
"postprocess-vert", "postprocess-vert",
"postprocess-frag", "postprocess-frag",
"blit-vert",
"blit-frag",
"player-shadow-frag", "player-shadow-frag",
"light-shadows-geom", "light-shadows-geom",
"light-shadows-frag", "light-shadows-frag",

View File

@ -1344,44 +1344,34 @@ impl Window {
pub fn send_event(&mut self, event: Event) { self.events.push(event) } pub fn send_event(&mut self, event: Event) { self.events.push(event) }
pub fn take_screenshot(&mut self, settings: &Settings) { pub fn take_screenshot(&mut self, settings: &Settings) {
let _ = self.renderer.create_screenshot(); let sender = self.message_sender.clone();
// TODO let mut path = settings.screenshots_path.clone();
/*match self.renderer.create_screenshot() { self.renderer.create_screenshot(move |image| {
Ok(img) => { use std::time::SystemTime;
let mut path = settings.screenshots_path.clone(); // Check if folder exists and create it if it does not
let sender = self.message_sender.clone(); if !path.exists() {
if let Err(e) = std::fs::create_dir_all(&path) {
let builder = std::thread::Builder::new().name("screenshot".into()); warn!(?e, "Couldn't create folder for screenshot");
builder let _result =
.spawn(move || { sender.send(String::from("Couldn't create folder for screenshot"));
use std::time::SystemTime; }
// Check if folder exists and create it if it does not }
if !path.exists() { path.push(format!(
if let Err(e) = std::fs::create_dir_all(&path) { "screenshot_{}.png",
warn!(?e, "Couldn't create folder for screenshot"); SystemTime::now()
let _result = sender .duration_since(SystemTime::UNIX_EPOCH)
.send(String::from("Couldn't create folder for screenshot")); .map(|d| d.as_millis())
} .unwrap_or(0)
} ));
path.push(format!( // Try to save the image
"screenshot_{}.png", if let Err(e) = image.into_rgba8().save(&path) {
SystemTime::now() warn!(?e, "Couldn't save screenshot");
.duration_since(SystemTime::UNIX_EPOCH) let _result = sender.send(String::from("Couldn't save screenshot"));
.map(|d| d.as_millis()) } else {
.unwrap_or(0) let _result =
)); sender.send(format!("Screenshot saved to {}", path.to_string_lossy()));
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"),
}*/
} }
fn is_pressed( fn is_pressed(