use egui::ScreenshotCallback;
use epaint::ColorImage;
use std::sync::Arc;
use wgpu::{BindGroupLayout, MultisampleState, StoreOp};
pub struct CaptureState {
padding: BufferPadding,
pub texture: wgpu::Texture,
pipeline: wgpu::RenderPipeline,
bind_group: wgpu::BindGroup,
}
impl CaptureState {
pub fn new(device: &wgpu::Device, size: wgpu::Extent3d, format: wgpu::TextureFormat) -> 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(format.into())],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let bind_group_layout = pipeline.get_bind_group_layout(0);
let (texture, padding, bind_group) =
Self::create_texture(device, size, format, &bind_group_layout);
Self {
padding,
texture,
pipeline,
bind_group,
}
}
fn create_texture(
device: &wgpu::Device,
size: wgpu::Extent3d,
format: wgpu::TextureFormat,
layout: &BindGroupLayout,
) -> (wgpu::Texture, BufferPadding, wgpu::BindGroup) {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("egui_screen_capture_texture"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let padding = BufferPadding::new(size.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)
}
pub fn update(&mut self, device: &wgpu::Device, size: wgpu::Extent3d) {
if self.texture.size() != size {
let format = self.texture.format();
let (new_texture, padding, bind_group) = Self::create_texture(
device,
size,
format,
&self.pipeline.get_bind_group_layout(0),
);
self.texture = new_texture;
self.padding = padding;
self.bind_group = bind_group;
}
}
pub fn copy_textures(
&mut self,
device: &wgpu::Device,
output_frame: Option<&wgpu::SurfaceTexture>,
encoder: &mut wgpu::CommandEncoder,
) -> wgpu::Buffer {
debug_assert!(
output_frame.is_none_or(|frame| frame.texture.size() == self.texture.size()),
"Texture sizes must match, `CaptureState::update` was probably not called"
);
#[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] 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,
);
if let Some(output_frame) = output_frame {
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,
multiview_mask: None,
});
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bind_group, &[]);
pass.draw(0..3, 0..1);
}
buffer
}
pub fn read_screen_rgba(&self, buffer: wgpu::Buffer, callbacks: Vec<ScreenshotCallback>) {
#[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] let buffer = Arc::new(buffer);
let buffer_clone = Arc::clone(&buffer);
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);
let mapped_range = match buffer_slice.get_mapped_range() {
Ok(range) => range,
Err(err) => {
log::error!("Failed to get mapped range for reading: {err}");
return;
}
};
for padded_row in 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]],
));
}
}
drop(mapped_range);
buffer.unmap();
let image = Arc::new(ColorImage::new(
[tex_extent.width as usize, tex_extent.height as usize],
pixels,
));
for callback in callbacks {
callback.complete(Arc::clone(&image));
}
});
}
}
#[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 = core::mem::size_of::<u32>() 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,
}
}
}