Implement screenshots

This commit is contained in:
Imbris 2021-03-20 04:03:42 -04:00
parent af9505b084
commit c7473fd843
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 figure;
pub mod fluid;

View File

@ -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<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,
profile_times: Vec<wgpu_profiler::GpuTimerScopeResult>,
}
@ -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<image::DynamicImage,
// RenderError> {
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::<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.
@ -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),

View File

@ -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<super::screenshot::TakeScreenshot>,
}
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?");

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",
"postprocess-vert",
"postprocess-frag",
"blit-vert",
"blit-frag",
"player-shadow-frag",
"light-shadows-geom",
"light-shadows-frag",

View File

@ -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(