use egui::{UserData, ViewportId}; use epaint::ColorImage; use std::sync::{Arc, mpsc}; use wgpu::{BindGroupLayout, MultisampleState, StoreOp}; /// A texture and a buffer for reading the rendered frame back to the cpu. /// /// The texture is required since [`wgpu::TextureUsages::COPY_SRC`] is not an allowed /// flag for the surface texture on all platforms. This means that anytime we want to /// capture the frame, we first render it to this texture, and then we can copy it to /// both the surface texture (via a render pass) and the buffer (via a texture to buffer copy), /// from where we can pull it back /// to the cpu. pub struct CaptureState { padding: BufferPadding, pub texture: wgpu::Texture, pipeline: wgpu::RenderPipeline, bind_group: wgpu::BindGroup, } pub type CaptureReceiver = mpsc::Receiver<(ViewportId, Vec, ColorImage)>; pub type CaptureSender = mpsc::Sender<(ViewportId, Vec, ColorImage)>; pub use mpsc::channel as capture_channel; impl CaptureState { pub fn new(device: &wgpu::Device, surface_texture: &wgpu::Texture) -> Self { let shader = device.create_shader_module(wgpu::include_wgsl!("texture_copy.wgsl")); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("texture_copy"), layout: None, vertex: wgpu::VertexState { module: &shader, entry_point: Some("vs_main"), compilation_options: Default::default(), buffers: &[], }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: Some("fs_main"), compilation_options: Default::default(), targets: &[Some(surface_texture.format().into())], }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() }, depth_stencil: None, multisample: MultisampleState::default(), multiview: None, cache: None, }); let bind_group_layout = pipeline.get_bind_group_layout(0); let (texture, padding, bind_group) = Self::create_texture(device, surface_texture, &bind_group_layout); Self { padding, texture, pipeline, bind_group, } } fn create_texture( device: &wgpu::Device, surface_texture: &wgpu::Texture, layout: &BindGroupLayout, ) -> (wgpu::Texture, BufferPadding, wgpu::BindGroup) { let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("egui_screen_capture_texture"), size: surface_texture.size(), mip_level_count: surface_texture.mip_level_count(), sample_count: surface_texture.sample_count(), dimension: surface_texture.dimension(), format: surface_texture.format(), usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC, view_formats: &[], }); let padding = BufferPadding::new(surface_texture.width()); let view = texture.create_view(&Default::default()); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view), }], label: None, }); (texture, padding, bind_group) } /// Updates the [`CaptureState`] if the size of the surface texture has changed pub fn update(&mut self, device: &wgpu::Device, texture: &wgpu::Texture) { if self.texture.size() != texture.size() { let (new_texture, padding, bind_group) = Self::create_texture(device, texture, &self.pipeline.get_bind_group_layout(0)); self.texture = new_texture; self.padding = padding; self.bind_group = bind_group; } } /// Handles copying from the [`CaptureState`] texture to the surface texture and the buffer. /// Pass the returned buffer to [`CaptureState::read_screen_rgba`] to read the data back to the cpu. pub fn copy_textures( &mut self, device: &wgpu::Device, output_frame: &wgpu::SurfaceTexture, encoder: &mut wgpu::CommandEncoder, ) -> wgpu::Buffer { debug_assert_eq!( self.texture.size(), output_frame.texture.size(), "Texture sizes must match, `CaptureState::update` was probably not called" ); // It would be more efficient to reuse the Buffer, e.g. via some kind of ring buffer, but // for most screenshot use cases this should be fine. When taking many screenshots (e.g. for a video) // it might make sense to revisit this and implement a more efficient solution. #[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("egui_screen_capture_buffer"), size: (self.padding.padded_bytes_per_row * self.texture.height()) as u64, usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, mapped_at_creation: false, }); let padding = self.padding; let tex = &mut self.texture; let tex_extent = tex.size(); encoder.copy_texture_to_buffer( tex.as_image_copy(), wgpu::TexelCopyBufferInfo { buffer: &buffer, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padding.padded_bytes_per_row), rows_per_image: None, }, }, tex_extent, ); let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("texture_copy"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &output_frame.texture.create_view(&Default::default()), resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: StoreOp::Store, }, depth_slice: None, })], depth_stencil_attachment: None, occlusion_query_set: None, timestamp_writes: None, }); pass.set_pipeline(&self.pipeline); pass.set_bind_group(0, &self.bind_group, &[]); pass.draw(0..3, 0..1); buffer } /// Handles copying from the [`CaptureState`] texture to the surface texture and the cpu /// This function is non-blocking and will send the data to the given sender when it's ready. /// Pass in the buffer returned from [`CaptureState::copy_textures`]. /// Make sure to call this after the encoder has been submitted. pub fn read_screen_rgba( &self, ctx: egui::Context, buffer: wgpu::Buffer, data: Vec, tx: CaptureSender, viewport_id: ViewportId, ) { #[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm let buffer = Arc::new(buffer); let buffer_clone = buffer.clone(); let buffer_slice = buffer_clone.slice(..); let format = self.texture.format(); let tex_extent = self.texture.size(); let padding = self.padding; let to_rgba = match format { wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3], wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3], _ => { log::error!( "Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {format:?}" ); return; } }; buffer_slice.map_async(wgpu::MapMode::Read, move |result| { if let Err(err) = result { log::error!("Failed to map buffer for reading: {err}"); return; } let buffer_slice = buffer.slice(..); let mut pixels = Vec::with_capacity((tex_extent.width * tex_extent.height) as usize); for padded_row in buffer_slice .get_mapped_range() .chunks(padding.padded_bytes_per_row as usize) { let row = &padded_row[..padding.unpadded_bytes_per_row as usize]; for color in row.chunks(4) { pixels.push(epaint::Color32::from_rgba_premultiplied( color[to_rgba[0]], color[to_rgba[1]], color[to_rgba[2]], color[to_rgba[3]], )); } } buffer.unmap(); tx.send(( viewport_id, data, ColorImage::new( [tex_extent.width as usize, tex_extent.height as usize], pixels, ), )) .ok(); ctx.request_repaint(); }); } } #[derive(Copy, Clone)] struct BufferPadding { unpadded_bytes_per_row: u32, padded_bytes_per_row: u32, } impl BufferPadding { fn new(width: u32) -> Self { let bytes_per_pixel = std::mem::size_of::() as u32; let unpadded_bytes_per_row = width * bytes_per_pixel; let padded_bytes_per_row = wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT); Self { unpadded_bytes_per_row, padded_bytes_per_row, } } }