egui_wgpu/
capture.rs

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