egui_wgpu/
capture.rs

1use egui::{UserData, ViewportId};
2use epaint::ColorImage;
3use std::sync::{Arc, mpsc};
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.
14pub struct CaptureState {
15    padding: BufferPadding,
16    pub texture: wgpu::Texture,
17    pipeline: wgpu::RenderPipeline,
18    bind_group: wgpu::BindGroup,
19}
20
21pub type CaptureReceiver = mpsc::Receiver<(ViewportId, Vec<UserData>, ColorImage)>;
22pub type CaptureSender = mpsc::Sender<(ViewportId, Vec<UserData>, ColorImage)>;
23pub use mpsc::channel as capture_channel;
24
25impl CaptureState {
26    pub fn new(device: &wgpu::Device, surface_texture: &wgpu::Texture) -> Self {
27        let shader = device.create_shader_module(wgpu::include_wgsl!("texture_copy.wgsl"));
28
29        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
30            label: Some("texture_copy"),
31            layout: None,
32            vertex: wgpu::VertexState {
33                module: &shader,
34                entry_point: Some("vs_main"),
35                compilation_options: Default::default(),
36                buffers: &[],
37            },
38            fragment: Some(wgpu::FragmentState {
39                module: &shader,
40                entry_point: Some("fs_main"),
41                compilation_options: Default::default(),
42                targets: &[Some(surface_texture.format().into())],
43            }),
44            primitive: wgpu::PrimitiveState {
45                topology: wgpu::PrimitiveTopology::TriangleList,
46                ..Default::default()
47            },
48            depth_stencil: None,
49            multisample: MultisampleState::default(),
50            multiview: None,
51            cache: None,
52        });
53
54        let bind_group_layout = pipeline.get_bind_group_layout(0);
55
56        let (texture, padding, bind_group) =
57            Self::create_texture(device, surface_texture, &bind_group_layout);
58
59        Self {
60            padding,
61            texture,
62            pipeline,
63            bind_group,
64        }
65    }
66
67    fn create_texture(
68        device: &wgpu::Device,
69        surface_texture: &wgpu::Texture,
70        layout: &BindGroupLayout,
71    ) -> (wgpu::Texture, BufferPadding, wgpu::BindGroup) {
72        let texture = device.create_texture(&wgpu::TextureDescriptor {
73            label: Some("egui_screen_capture_texture"),
74            size: surface_texture.size(),
75            mip_level_count: surface_texture.mip_level_count(),
76            sample_count: surface_texture.sample_count(),
77            dimension: surface_texture.dimension(),
78            format: surface_texture.format(),
79            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
80                | wgpu::TextureUsages::TEXTURE_BINDING
81                | wgpu::TextureUsages::COPY_SRC,
82            view_formats: &[],
83        });
84
85        let padding = BufferPadding::new(surface_texture.width());
86
87        let view = texture.create_view(&Default::default());
88
89        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
90            layout,
91            entries: &[wgpu::BindGroupEntry {
92                binding: 0,
93                resource: wgpu::BindingResource::TextureView(&view),
94            }],
95            label: None,
96        });
97
98        (texture, padding, bind_group)
99    }
100
101    /// Updates the [`CaptureState`] if the size of the surface texture has changed
102    pub fn update(&mut self, device: &wgpu::Device, texture: &wgpu::Texture) {
103        if self.texture.size() != texture.size() {
104            let (new_texture, padding, bind_group) =
105                Self::create_texture(device, texture, &self.pipeline.get_bind_group_layout(0));
106            self.texture = new_texture;
107            self.padding = padding;
108            self.bind_group = bind_group;
109        }
110    }
111
112    /// Handles copying from the [`CaptureState`] texture to the surface texture and the buffer.
113    /// Pass the returned buffer to [`CaptureState::read_screen_rgba`] to read the data back to the cpu.
114    pub fn copy_textures(
115        &mut self,
116        device: &wgpu::Device,
117        output_frame: &wgpu::SurfaceTexture,
118        encoder: &mut wgpu::CommandEncoder,
119    ) -> wgpu::Buffer {
120        debug_assert_eq!(
121            self.texture.size(),
122            output_frame.texture.size(),
123            "Texture sizes must match, `CaptureState::update` was probably not called"
124        );
125
126        // It would be more efficient to reuse the Buffer, e.g. via some kind of ring buffer, but
127        // for most screenshot use cases this should be fine. When taking many screenshots (e.g. for a video)
128        // it might make sense to revisit this and implement a more efficient solution.
129        #[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
130        let buffer = device.create_buffer(&wgpu::BufferDescriptor {
131            label: Some("egui_screen_capture_buffer"),
132            size: (self.padding.padded_bytes_per_row * self.texture.height()) as u64,
133            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
134            mapped_at_creation: false,
135        });
136        let padding = self.padding;
137        let tex = &mut self.texture;
138
139        let tex_extent = tex.size();
140
141        encoder.copy_texture_to_buffer(
142            tex.as_image_copy(),
143            wgpu::TexelCopyBufferInfo {
144                buffer: &buffer,
145                layout: wgpu::TexelCopyBufferLayout {
146                    offset: 0,
147                    bytes_per_row: Some(padding.padded_bytes_per_row),
148                    rows_per_image: None,
149                },
150            },
151            tex_extent,
152        );
153
154        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
155            label: Some("texture_copy"),
156            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
157                view: &output_frame.texture.create_view(&Default::default()),
158                resolve_target: None,
159                ops: wgpu::Operations {
160                    load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
161                    store: StoreOp::Store,
162                },
163                depth_slice: None,
164            })],
165            depth_stencil_attachment: None,
166            occlusion_query_set: None,
167            timestamp_writes: None,
168        });
169
170        pass.set_pipeline(&self.pipeline);
171        pass.set_bind_group(0, &self.bind_group, &[]);
172        pass.draw(0..3, 0..1);
173
174        buffer
175    }
176
177    /// Handles copying from the [`CaptureState`] texture to the surface texture and the cpu
178    /// This function is non-blocking and will send the data to the given sender when it's ready.
179    /// Pass in the buffer returned from [`CaptureState::copy_textures`].
180    /// Make sure to call this after the encoder has been submitted.
181    pub fn read_screen_rgba(
182        &self,
183        ctx: egui::Context,
184        buffer: wgpu::Buffer,
185        data: Vec<UserData>,
186        tx: CaptureSender,
187        viewport_id: ViewportId,
188    ) {
189        #[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
190        let buffer = Arc::new(buffer);
191        let buffer_clone = buffer.clone();
192        let buffer_slice = buffer_clone.slice(..);
193        let format = self.texture.format();
194        let tex_extent = self.texture.size();
195        let padding = self.padding;
196        let to_rgba = match format {
197            wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3],
198            wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3],
199            _ => {
200                log::error!(
201                    "Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {format:?}"
202                );
203                return;
204            }
205        };
206        buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
207            if let Err(err) = result {
208                log::error!("Failed to map buffer for reading: {err}");
209                return;
210            }
211            let buffer_slice = buffer.slice(..);
212
213            let mut pixels = Vec::with_capacity((tex_extent.width * tex_extent.height) as usize);
214            for padded_row in buffer_slice
215                .get_mapped_range()
216                .chunks(padding.padded_bytes_per_row as usize)
217            {
218                let row = &padded_row[..padding.unpadded_bytes_per_row as usize];
219                for color in row.chunks(4) {
220                    pixels.push(epaint::Color32::from_rgba_premultiplied(
221                        color[to_rgba[0]],
222                        color[to_rgba[1]],
223                        color[to_rgba[2]],
224                        color[to_rgba[3]],
225                    ));
226                }
227            }
228            buffer.unmap();
229
230            tx.send((
231                viewport_id,
232                data,
233                ColorImage::new(
234                    [tex_extent.width as usize, tex_extent.height as usize],
235                    pixels,
236                ),
237            ))
238            .ok();
239            ctx.request_repaint();
240        });
241    }
242}
243
244#[derive(Copy, Clone)]
245struct BufferPadding {
246    unpadded_bytes_per_row: u32,
247    padded_bytes_per_row: u32,
248}
249
250impl BufferPadding {
251    fn new(width: u32) -> Self {
252        let bytes_per_pixel = std::mem::size_of::<u32>() as u32;
253        let unpadded_bytes_per_row = width * bytes_per_pixel;
254        let padded_bytes_per_row =
255            wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
256        Self {
257            unpadded_bytes_per_row,
258            padded_bytes_per_row,
259        }
260    }
261}