Skip to main content

agg_gui_wgpu/
screenshot_capture.rs

1//! `DrawCtx` screenshot-capture texture path for the wgpu backend.
2//!
3//! Split out of `draw_ctx_impl.rs` (800-line guardrail).  Owns everything that
4//! touches [`WgpuGfxCtx::capture_texture`]: the GPU-direct surface snapshot, the
5//! textured re-draw of that snapshot into the scene, and both readback flavours
6//! (blocking for native Save/Copy, non-blocking for the web screen-share sender).
7//!
8//! The `DrawCtx` trait methods in `draw_ctx_impl.rs` are thin delegates onto the
9//! `*_impl` inherent methods here, following the same convention used by
10//! `image_blit.rs` and `layers.rs`.  Sibling module `screenshot_readback.rs`
11//! handles the *other* capture route — copying the frame's surface texture for
12//! `agg_gui::screenshot::run_frame_with_capture`.
13
14use crate::{DrawCommand, WgpuGfxCtx};
15
16impl WgpuGfxCtx {
17    /// Snapshot the surface texture into our internal capture texture
18    /// via a single `copy_texture_to_texture` — pure GPU work, no CPU
19    /// readback.  Re-allocate the capture texture when its size doesn't
20    /// match the surface (resize, first capture).
21    pub(crate) fn capture_screenshot_impl(&mut self) -> bool {
22        let Some(src) = self.surface_texture.clone() else {
23            return false;
24        };
25        let src_size = src.size();
26        let (sw, sh) = (src_size.width, src_size.height);
27        if sw == 0 || sh == 0 {
28            return false;
29        }
30
31        let need_alloc = match &self.capture_texture {
32            Some((_, _, w, h)) => *w != sw || *h != sh,
33            None => true,
34        };
35        if need_alloc {
36            let tex = self.device.create_texture(&wgpu::TextureDescriptor {
37                label: Some("screenshot_capture"),
38                size: wgpu::Extent3d {
39                    width: sw,
40                    height: sh,
41                    depth_or_array_layers: 1,
42                },
43                mip_level_count: 1,
44                sample_count: 1,
45                dimension: wgpu::TextureDimension::D2,
46                format: self.surface_format,
47                // Sampled by the screenshot widget (TEXTURE_BINDING),
48                // written by `copy_texture_to_texture` (COPY_DST), and
49                // read back to CPU on Save / Copy (COPY_SRC).
50                usage: wgpu::TextureUsages::TEXTURE_BINDING
51                    | wgpu::TextureUsages::COPY_DST
52                    | wgpu::TextureUsages::COPY_SRC,
53                view_formats: &[],
54            });
55            let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
56            self.capture_texture = Some((std::sync::Arc::new(tex), view, sw, sh));
57        }
58
59        let dst_tex = std::sync::Arc::clone(&self.capture_texture.as_ref().unwrap().0);
60        let mut encoder = self
61            .device
62            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
63                label: Some("screenshot_capture_copy"),
64            });
65        encoder.copy_texture_to_texture(
66            wgpu::TexelCopyTextureInfo {
67                texture: &src,
68                mip_level: 0,
69                origin: wgpu::Origin3d::ZERO,
70                aspect: wgpu::TextureAspect::All,
71            },
72            wgpu::TexelCopyTextureInfo {
73                texture: &dst_tex,
74                mip_level: 0,
75                origin: wgpu::Origin3d::ZERO,
76                aspect: wgpu::TextureAspect::All,
77            },
78            wgpu::Extent3d {
79                width: sw,
80                height: sh,
81                depth_or_array_layers: 1,
82            },
83        );
84        self.queue.submit(std::iter::once(encoder.finish()));
85        true
86    }
87
88    /// Draw the held capture texture as a textured quad in the current CTM.
89    ///
90    /// The capture texture is sample_count=1 with TEXTURE_BINDING, so it
91    /// slots straight into the existing `Textured` deferred command —
92    /// same pipeline / sampler as `draw_image_rgba_arc`.  We pick the
93    /// linear sampler (trilinear when mipmaps are present, bilinear at
94    /// base mip otherwise) so heavy downsampling in the preview pane
95    /// doesn't point-alias.  No mipmap chain on the capture texture
96    /// for now — sampling at typical preview-pane scales (2-4× shrink)
97    /// is acceptable with bilinear; we can add a GPU mip-chain pass if
98    /// visual review shows aliasing.
99    pub(crate) fn draw_captured_screenshot_impl(
100        &mut self,
101        dst_x: f64,
102        dst_y: f64,
103        dst_w: f64,
104        dst_h: f64,
105    ) -> bool {
106        let Some((texture, view, _, _)) = self.capture_texture.as_ref() else {
107            return false;
108        };
109        let texture = std::sync::Arc::clone(texture);
110        let view = view.clone();
111
112        let bl = self.transform_pt(dst_x, dst_y);
113        let br = self.transform_pt(dst_x + dst_w, dst_y);
114        let tr = self.transform_pt(dst_x + dst_w, dst_y + dst_h);
115        let tl = self.transform_pt(dst_x, dst_y + dst_h);
116        // wgpu UV convention: v=0 is the top of the texture; bottom of
117        // the destination quad samples v=1.  Same flip used by `image_blit`.
118        let verts: [f32; 24] = [
119            bl[0], bl[1], 0.0, 1.0, br[0], br[1], 1.0, 1.0, tr[0], tr[1], 1.0, 0.0, bl[0], bl[1],
120            0.0, 1.0, tr[0], tr[1], 1.0, 0.0, tl[0], tl[1], 0.0, 0.0,
121        ];
122        let clip = self.current_clip();
123        let alpha = self.global_alpha as f32;
124        self.commands.push(DrawCommand::Textured {
125            verts,
126            texture,
127            view,
128            nearest: false,
129            tint: [1.0, 1.0, 1.0, alpha],
130            clip,
131        });
132        true
133    }
134
135    /// Single-shot synchronous readback for Save / Copy.  Issues a
136    /// copy from the held capture texture to a `MAP_READ` staging
137    /// buffer, polls the device until done, and unrolls the
138    /// 256-byte-row-aligned blocks into a tight RGBA8 Y-down `Vec<u8>`.
139    /// Format is normalized to RGBA8 (swapping R↔B for Bgra8 surface
140    /// formats) so callers don't need to know the surface format.
141    pub(crate) fn read_captured_screenshot_impl(&mut self) -> (Vec<u8>, u32, u32) {
142        let Some((texture, _, w, h)) = self.capture_texture.as_ref() else {
143            return (Vec::new(), 0, 0);
144        };
145        let (w, h) = (*w, *h);
146        if w == 0 || h == 0 {
147            return (Vec::new(), 0, 0);
148        }
149        let texture = std::sync::Arc::clone(texture);
150
151        const ALIGN: u32 = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
152        let unpadded_bpr = w * 4;
153        let padded_bpr = unpadded_bpr.div_ceil(ALIGN) * ALIGN;
154        let buffer_size = (padded_bpr as u64) * (h as u64);
155
156        let staging = self.device.create_buffer(&wgpu::BufferDescriptor {
157            label: Some("capture_readback"),
158            size: buffer_size,
159            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
160            mapped_at_creation: false,
161        });
162        let mut encoder = self
163            .device
164            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
165                label: Some("capture_readback_copy"),
166            });
167        encoder.copy_texture_to_buffer(
168            wgpu::TexelCopyTextureInfo {
169                texture: &texture,
170                mip_level: 0,
171                origin: wgpu::Origin3d::ZERO,
172                aspect: wgpu::TextureAspect::All,
173            },
174            wgpu::TexelCopyBufferInfo {
175                buffer: &staging,
176                layout: wgpu::TexelCopyBufferLayout {
177                    offset: 0,
178                    bytes_per_row: Some(padded_bpr),
179                    rows_per_image: Some(h),
180                },
181            },
182            wgpu::Extent3d {
183                width: w,
184                height: h,
185                depth_or_array_layers: 1,
186            },
187        );
188        self.queue.submit(std::iter::once(encoder.finish()));
189
190        let slice = staging.slice(..);
191        let (sender, receiver) = std::sync::mpsc::channel();
192        slice.map_async(wgpu::MapMode::Read, move |res| {
193            let _ = sender.send(res);
194        });
195        let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
196        if receiver.recv().ok().and_then(|r| r.ok()).is_none() {
197            return (Vec::new(), 0, 0);
198        }
199
200        let bgra = matches!(
201            self.surface_format,
202            wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
203        );
204        let mut out = Vec::with_capacity((w as usize) * (h as usize) * 4);
205        {
206            let view = slice.get_mapped_range();
207            for row in 0..h as usize {
208                let start = row * padded_bpr as usize;
209                let end = start + unpadded_bpr as usize;
210                let src = &view[start..end];
211                if bgra {
212                    for px in src.chunks_exact(4) {
213                        out.extend_from_slice(&[px[2], px[1], px[0], px[3]]);
214                    }
215                } else {
216                    out.extend_from_slice(src);
217                }
218            }
219        }
220        staging.unmap();
221        (out, w, h)
222    }
223}
224
225// ── Non-blocking capture readback (web screen-share sender) ────────────────────
226//
227// The trait's `read_captured_screenshot` blocks the thread on `poll(Wait)`, which
228// deadlocks on the single-threaded web build.  These inherent methods split that
229// into start-now / harvest-later so `render()` never blocks.  See
230// [`crate::PendingReadback`].
231impl WgpuGfxCtx {
232    /// Whether a capture readback is already in flight.
233    pub fn has_pending_readback(&self) -> bool {
234        self.pending_readback.is_some()
235    }
236
237    /// Kick off an async readback of the current capture texture.  Returns false
238    /// if nothing is captured or a readback is already in flight.  Pure GPU
239    /// submit + `map_async`; never blocks.
240    pub fn begin_capture_readback(&mut self) -> bool {
241        if self.pending_readback.is_some() {
242            return false;
243        }
244        let Some((texture, _, w, h)) = self.capture_texture.as_ref() else {
245            return false;
246        };
247        let (w, h) = (*w, *h);
248        if w == 0 || h == 0 {
249            return false;
250        }
251        let texture = std::sync::Arc::clone(texture);
252
253        const ALIGN: u32 = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
254        let padded_bpr = (w * 4).div_ceil(ALIGN) * ALIGN;
255        let staging = self.device.create_buffer(&wgpu::BufferDescriptor {
256            label: Some("capture_readback_async"),
257            size: (padded_bpr as u64) * (h as u64),
258            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
259            mapped_at_creation: false,
260        });
261        let mut encoder = self
262            .device
263            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
264                label: Some("capture_readback_async_copy"),
265            });
266        encoder.copy_texture_to_buffer(
267            wgpu::TexelCopyTextureInfo {
268                texture: &texture,
269                mip_level: 0,
270                origin: wgpu::Origin3d::ZERO,
271                aspect: wgpu::TextureAspect::All,
272            },
273            wgpu::TexelCopyBufferInfo {
274                buffer: &staging,
275                layout: wgpu::TexelCopyBufferLayout {
276                    offset: 0,
277                    bytes_per_row: Some(padded_bpr),
278                    rows_per_image: Some(h),
279                },
280            },
281            wgpu::Extent3d {
282                width: w,
283                height: h,
284                depth_or_array_layers: 1,
285            },
286        );
287        self.queue.submit(std::iter::once(encoder.finish()));
288
289        let (tx, rx) = std::sync::mpsc::channel();
290        staging
291            .slice(..)
292            .map_async(wgpu::MapMode::Read, move |res| {
293                let _ = tx.send(res);
294            });
295        self.pending_readback = Some(crate::PendingReadback {
296            staging,
297            w,
298            h,
299            padded_bpr,
300            done: rx,
301        });
302        true
303    }
304
305    /// Harvest a completed readback's pixels, or `None` if still pending / none
306    /// in flight.  Never blocks: on the web the map resolves on a later event
307    /// loop turn; the cheap non-blocking `poll` keeps native progressing too.
308    pub fn poll_capture_readback(&mut self) -> Option<(Vec<u8>, u32, u32)> {
309        let _ = self.device.poll(wgpu::PollType::Poll);
310        match self.pending_readback.as_ref()?.done.try_recv() {
311            Ok(Ok(())) => {}
312            Err(std::sync::mpsc::TryRecvError::Empty) => return None, // not ready yet
313            Ok(Err(_)) | Err(std::sync::mpsc::TryRecvError::Disconnected) => {
314                self.pending_readback = None; // failed map — drop it
315                return None;
316            }
317        }
318
319        let crate::PendingReadback {
320            staging,
321            w,
322            h,
323            padded_bpr,
324            ..
325        } = self.pending_readback.take().unwrap();
326
327        let bgra = matches!(
328            self.surface_format,
329            wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
330        );
331        let unpadded_bpr = (w * 4) as usize;
332        let mut out = Vec::with_capacity((w as usize) * (h as usize) * 4);
333        {
334            let view = staging.slice(..).get_mapped_range();
335            for row in 0..h as usize {
336                let start = row * padded_bpr as usize;
337                let src = &view[start..start + unpadded_bpr];
338                if bgra {
339                    for px in src.chunks_exact(4) {
340                        out.extend_from_slice(&[px[2], px[1], px[0], px[3]]);
341                    }
342                } else {
343                    out.extend_from_slice(src);
344                }
345            }
346        }
347        staging.unmap();
348        Some((out, w, h))
349    }
350}