Skip to main content

egui_wgpu/
capture.rs

1use egui::ScreenshotCallback;
2use epaint::ColorImage;
3use std::sync::Arc;
4use wgpu::{BindGroupLayout, MultisampleState, StoreOp};
5
6/// A texture and a buffer for reading the rendered frame back to the cpu.
7///
8/// The texture is required since [`wgpu::TextureUsages::COPY_SRC`] is not an allowed
9/// flag for the surface texture on all platforms. This means that anytime we want to
10/// capture the frame, we first render it to this texture, and then we can copy it to
11/// both the surface texture (via a render pass) and the buffer (via a texture to buffer copy),
12/// from where we can pull it back
13/// to the cpu.
14///
15/// Since the frame is rendered to this texture and not to the surface, a capture needs no
16/// surface texture at all: an occluded or minimized window, which has none to give, can still
17/// be captured.
18pub struct CaptureState {
19    padding: BufferPadding,
20    pub texture: wgpu::Texture,
21    pipeline: wgpu::RenderPipeline,
22    bind_group: wgpu::BindGroup,
23}
24
25impl CaptureState {
26    /// `size` and `format` are those of the surface we would otherwise render to.
27    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    /// Updates the [`CaptureState`] if the size of the surface has changed
104    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    /// Handles copying from the [`CaptureState`] texture to the buffer, and to the surface
120    /// texture when there is one to present to.
121    /// Pass the returned buffer to [`CaptureState::read_screen_rgba`] to read the data back to the cpu.
122    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        // It would be more efficient to reuse the Buffer, e.g. via some kind of ring buffer, but
134        // for most screenshot use cases this should be fine. When taking many screenshots (e.g. for a video)
135        // it might make sense to revisit this and implement a more efficient solution.
136        #[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
137        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        // Blit to the surface, for the frame that is actually shown. A hidden window has no
162        // surface texture to blit to, and nothing to show: the capture above is all we want.
163        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    /// Handles copying from the [`CaptureState`] texture to the surface texture and the cpu
190    /// This function is non-blocking and will send the data to the given sender when it's ready.
191    /// Pass in the buffer returned from [`CaptureState::copy_textures`].
192    /// Make sure to call this after the encoder has been submitted.
193    pub fn read_screen_rgba(&self, buffer: wgpu::Buffer, callbacks: Vec<ScreenshotCallback>) {
194        #[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
195        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}