diff --git a/voxygen/src/render/mod.rs b/voxygen/src/render/mod.rs index d7c0f9edf9..2c9d6ab9fd 100644 --- a/voxygen/src/render/mod.rs +++ b/voxygen/src/render/mod.rs @@ -8,6 +8,7 @@ pub mod mesh; pub mod model; pub mod pipelines; pub mod renderer; +mod screenshot; pub mod texture; // Reexports diff --git a/voxygen/src/render/pipelines/blit.rs b/voxygen/src/render/pipelines/blit.rs new file mode 100644 index 0000000000..48b1aa12d2 --- /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::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: wgpu::CullMode::None, + polygon_mode: wgpu::PolygonMode::Fill, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: samples, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(wgpu::FragmentState { + module: fs_module, + entry_point: "main", + targets: &[wgpu::ColorTargetState { + format: sc_desc.format, + alpha_blend: wgpu::BlendState::REPLACE, + color_blend: wgpu::BlendState::REPLACE, + 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 3c597ff190..88895c2d9c 100644 --- a/voxygen/src/render/renderer.rs +++ b/voxygen/src/render/renderer.rs @@ -1038,7 +1038,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 and unlimited FPS + // PresentMode::Fifo and unlimited FPS on certain machines return Ok(None); }, Err(err @ wgpu::SwapChainError::Outdated) => { diff --git a/voxygen/src/render/renderer/drawer.rs b/voxygen/src/render/renderer/drawer.rs index 6af5f66905..c722a68e31 100644 --- a/voxygen/src/render/renderer/drawer.rs +++ b/voxygen/src/render/renderer/drawer.rs @@ -19,6 +19,8 @@ pub struct Drawer<'a> { pub renderer: &'a mut Renderer, tex: wgpu::SwapChainTexture, globals: &'a GlobalsBindGroup, + // Texture to write in screenshot data if that was requested this frame + taking_screenshot: Option, } impl<'a> Drawer<'a> { @@ -141,7 +143,9 @@ impl<'a> Drawer<'a> { .begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("third pass (postprocess + ui)"), color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { - attachment: &self.tex.view, + // If a screenshot was requested render to that as an intermediate texture + // instead + attachment: self.taking_screenshot.map_or(&self.tex.view, |s| &s.tex), resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), @@ -295,6 +299,31 @@ impl<'a> Drawer<'a> { impl<'a> Drop for Drawer<'a> { fn drop(&mut self) { + // If taking a screenshot + if let Some(screenshot) = self.taking_screenshot { + // Image needs to be copied from the screenshot texture to the swapchain texture + let mut render_pass = + self.encoder + .as_mut() + .unwrap() + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&label), + color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { + attachment: &self.tex.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: true, + }, + }], + }); + self.render_pass.set_pipeline(&self.blit.pipeline); + self.render_pass + .set_bind_group(0, &screenshot.bind_group.bind_group, &[]); + self.render_pass.draw(0..3, 0..1); + drop(render_pass); + // TODO: Send screenshot off to another thread here + } // 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? self.renderer diff --git a/voxygen/src/render/renderer/screenshot.rs b/voxygen/src/render/renderer/screenshot.rs new file mode 100644 index 0000000000..8883a9a452 --- /dev/null +++ b/voxygen/src/render/renderer/screenshot.rs @@ -0,0 +1,124 @@ +use super::super::pipelines::blit; + +pub type ScreenshotFn = Box; + +pub struct ScreenshotDownloader { + buffer: wgpu::Buffer, + screenshot_fn: ScreenshotFn, +} + +pub struct TakeScreenshot { + bind_group: blit::BindGroup, + view: wgpu::TextureView, + texture: wgpu::Texture, + // /// Option so that we can pass ownership of the contents of this field around (and eventually to a new thread) without + // /// taking ownership of this whole struct + downloader: ScreenshotDownloader, + // Dimensions used for copying from the screenshot texture to a buffer + width: u32, + height: u32, + bytes_per_pixel: u8, +} + +//pub struct TakingScreenshot<'a> { +// pub bind_group: &'a blit::BindGroup, +// pub tex: &'a wgpu::TextureView, +// downloader: Option +//} + +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: 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_ATTACHEMENT, + }); + + 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, &taking_tex, sampler); + + let bytes_per_pixel = sc_desc.format.describe().block_size; + + let buffer = device.create_buffer(&wgpu::BufferDescriptor{ + label: Some("screenshot download buffer"), + size: padded_bytes_per_row * + } + ) + let downloader = ScreenshotDownloader { screenshot_fn, buffer }; + + Self { + bind_group, + texture, + view, + downloader: Some(Downloader), + + } + + /// 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 align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; + let unpadded_bytes_per_row = + // Copy image to a buffer + encoder.copy_texture_to_buffer( + wgpu::TextureCopyView { + texture: &self.texture, + mip_level: 0, + orgin: wgpu::Origin3d::ZERO, + }, + wgpu::BufferCopyView { + buffer: &self.buffer, + layout: wgpu::TextureDataLayout { + offset: 0, + bytes_per_row: padded_bytes_per_row(self.width, self.bytes_per_pixel), + rows_per_image: 0, + } + }, + ) + // 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(|| { + + }); + .expect("Failed to spawn screenshot thread"); + } +} + +fn padded_bytes_per_row(width: u32, bytes_per_pixel: u8) -> u32 { + let unpadded_bytes_per_row = width * bytes_per_pixel; + let padding = (align - unpadded_bytes_per_row % align) % align; + unpadded_bytes_per_row + padding +}