1use egui::ScreenshotCallback;
2use epaint::ColorImage;
3use std::sync::Arc;
4use wgpu::{BindGroupLayout, MultisampleState, StoreOp};
5
6pub struct CaptureState {
19 padding: BufferPadding,
20 pub texture: wgpu::Texture,
21 pipeline: wgpu::RenderPipeline,
22 bind_group: wgpu::BindGroup,
23}
24
25impl CaptureState {
26 pub fn new(device: &wgpu::Device, size: wgpu::Extent3d, format: wgpu::TextureFormat) -> Self {
28 let shader = device.create_shader_module(wgpu::include_wgsl!("texture_copy.wgsl"));
29
30 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
31 label: Some("texture_copy"),
32 layout: None,
33 vertex: wgpu::VertexState {
34 module: &shader,
35 entry_point: Some("vs_main"),
36 compilation_options: Default::default(),
37 buffers: &[],
38 },
39 fragment: Some(wgpu::FragmentState {
40 module: &shader,
41 entry_point: Some("fs_main"),
42 compilation_options: Default::default(),
43 targets: &[Some(format.into())],
44 }),
45 primitive: wgpu::PrimitiveState {
46 topology: wgpu::PrimitiveTopology::TriangleList,
47 ..Default::default()
48 },
49 depth_stencil: None,
50 multisample: MultisampleState::default(),
51 multiview_mask: None,
52 cache: None,
53 });
54
55 let bind_group_layout = pipeline.get_bind_group_layout(0);
56
57 let (texture, padding, bind_group) =
58 Self::create_texture(device, size, format, &bind_group_layout);
59
60 Self {
61 padding,
62 texture,
63 pipeline,
64 bind_group,
65 }
66 }
67
68 fn create_texture(
69 device: &wgpu::Device,
70 size: wgpu::Extent3d,
71 format: wgpu::TextureFormat,
72 layout: &BindGroupLayout,
73 ) -> (wgpu::Texture, BufferPadding, wgpu::BindGroup) {
74 let texture = device.create_texture(&wgpu::TextureDescriptor {
75 label: Some("egui_screen_capture_texture"),
76 size,
77 mip_level_count: 1,
78 sample_count: 1,
79 dimension: wgpu::TextureDimension::D2,
80 format,
81 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
82 | wgpu::TextureUsages::TEXTURE_BINDING
83 | wgpu::TextureUsages::COPY_SRC,
84 view_formats: &[],
85 });
86
87 let padding = BufferPadding::new(size.width);
88
89 let view = texture.create_view(&Default::default());
90
91 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
92 layout,
93 entries: &[wgpu::BindGroupEntry {
94 binding: 0,
95 resource: wgpu::BindingResource::TextureView(&view),
96 }],
97 label: None,
98 });
99
100 (texture, padding, bind_group)
101 }
102
103 pub fn update(&mut self, device: &wgpu::Device, size: wgpu::Extent3d) {
105 if self.texture.size() != size {
106 let format = self.texture.format();
107 let (new_texture, padding, bind_group) = Self::create_texture(
108 device,
109 size,
110 format,
111 &self.pipeline.get_bind_group_layout(0),
112 );
113 self.texture = new_texture;
114 self.padding = padding;
115 self.bind_group = bind_group;
116 }
117 }
118
119 pub fn copy_textures(
123 &mut self,
124 device: &wgpu::Device,
125 output_frame: Option<&wgpu::SurfaceTexture>,
126 encoder: &mut wgpu::CommandEncoder,
127 ) -> wgpu::Buffer {
128 debug_assert!(
129 output_frame.is_none_or(|frame| frame.texture.size() == self.texture.size()),
130 "Texture sizes must match, `CaptureState::update` was probably not called"
131 );
132
133 #[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] let buffer = device.create_buffer(&wgpu::BufferDescriptor {
138 label: Some("egui_screen_capture_buffer"),
139 size: (self.padding.padded_bytes_per_row * self.texture.height()) as u64,
140 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
141 mapped_at_creation: false,
142 });
143 let padding = self.padding;
144 let tex = &mut self.texture;
145
146 let tex_extent = tex.size();
147
148 encoder.copy_texture_to_buffer(
149 tex.as_image_copy(),
150 wgpu::TexelCopyBufferInfo {
151 buffer: &buffer,
152 layout: wgpu::TexelCopyBufferLayout {
153 offset: 0,
154 bytes_per_row: Some(padding.padded_bytes_per_row),
155 rows_per_image: None,
156 },
157 },
158 tex_extent,
159 );
160
161 if let Some(output_frame) = output_frame {
164 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
165 label: Some("texture_copy"),
166 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
167 view: &output_frame.texture.create_view(&Default::default()),
168 resolve_target: None,
169 ops: wgpu::Operations {
170 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
171 store: StoreOp::Store,
172 },
173 depth_slice: None,
174 })],
175 depth_stencil_attachment: None,
176 occlusion_query_set: None,
177 timestamp_writes: None,
178 multiview_mask: None,
179 });
180
181 pass.set_pipeline(&self.pipeline);
182 pass.set_bind_group(0, &self.bind_group, &[]);
183 pass.draw(0..3, 0..1);
184 }
185
186 buffer
187 }
188
189 pub fn read_screen_rgba(&self, buffer: wgpu::Buffer, callbacks: Vec<ScreenshotCallback>) {
194 #[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] let buffer = Arc::new(buffer);
196 let buffer_clone = Arc::clone(&buffer);
197 let buffer_slice = buffer_clone.slice(..);
198 let format = self.texture.format();
199 let tex_extent = self.texture.size();
200 let padding = self.padding;
201 let to_rgba = match format {
202 wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3],
203 wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3],
204 _ => {
205 log::error!(
206 "Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {format:?}"
207 );
208 return;
209 }
210 };
211 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
212 if let Err(err) = result {
213 log::error!("Failed to map buffer for reading: {err}");
214 return;
215 }
216 let buffer_slice = buffer.slice(..);
217
218 let mut pixels = Vec::with_capacity((tex_extent.width * tex_extent.height) as usize);
219 let mapped_range = match buffer_slice.get_mapped_range() {
220 Ok(range) => range,
221 Err(err) => {
222 log::error!("Failed to get mapped range for reading: {err}");
223 return;
224 }
225 };
226 for padded_row in mapped_range.chunks(padding.padded_bytes_per_row as usize) {
227 let row = &padded_row[..padding.unpadded_bytes_per_row as usize];
228 for color in row.chunks(4) {
229 pixels.push(epaint::Color32::from_rgba_premultiplied(
230 color[to_rgba[0]],
231 color[to_rgba[1]],
232 color[to_rgba[2]],
233 color[to_rgba[3]],
234 ));
235 }
236 }
237 drop(mapped_range);
238 buffer.unmap();
239
240 let image = Arc::new(ColorImage::new(
241 [tex_extent.width as usize, tex_extent.height as usize],
242 pixels,
243 ));
244 for callback in callbacks {
245 callback.complete(Arc::clone(&image));
246 }
247 });
248 }
249}
250
251#[derive(Copy, Clone)]
252struct BufferPadding {
253 unpadded_bytes_per_row: u32,
254 padded_bytes_per_row: u32,
255}
256
257impl BufferPadding {
258 fn new(width: u32) -> Self {
259 let bytes_per_pixel = core::mem::size_of::<u32>() as u32;
260 let unpadded_bytes_per_row = width * bytes_per_pixel;
261 let padded_bytes_per_row =
262 wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
263 Self {
264 unpadded_bytes_per_row,
265 padded_bytes_per_row,
266 }
267 }
268}