Skip to main content

brep_app/
offscreen.rs

1//! Render one egui pass into an offscreen texture and read it back.
2//!
3//! An on-demand pass (`on_demand`: the desktop loop's, the browser pump's)
4//! never presents: a hidden Wayland surface gets no frame callbacks and
5//! presenting to it can block, and a hidden tab gets no animation frames. A
6//! capture asked for during such a pass is drawn here instead,
7//! with the window's own `egui_wgpu::RenderState`: the same device, the same
8//! renderer (so its textures and the 3D viewport's paint-callback resources
9//! are the ones the window draws with) and the same target format.
10use eframe::egui_wgpu::{self, wgpu};
11
12/// Upload the pass's new and changed textures to the shared renderer. The
13/// window's next real frame never sees this pass's texture delta, so the
14/// pass must apply it itself (a font atlas page added here is drawn there).
15pub fn apply_texture_sets(render: &egui_wgpu::RenderState, delta: &egui::TexturesDelta) {
16    let mut renderer = render.renderer.write();
17    for (id, image) in &delta.set {
18        renderer.update_texture(&render.device, &render.queue, *id, image);
19    }
20}
21
22/// Free the textures the pass released, after anything that drew with them.
23pub fn apply_texture_frees(render: &egui_wgpu::RenderState, delta: &egui::TexturesDelta) {
24    let mut renderer = render.renderer.write();
25    for id in &delta.free {
26        renderer.free_texture(id);
27    }
28}
29
30/// Draw `shapes` at `size_px` (`pixels_per_point`) over `clear` and return the
31/// picture, waiting for the GPU. Its texture delta must already be applied
32/// ([`apply_texture_sets`]). Native only: the browser reads back with
33/// [`Drawn::on_ready`].
34#[cfg(not(target_arch = "wasm32"))]
35pub fn capture(
36    render: &egui_wgpu::RenderState,
37    ctx: &egui::Context,
38    shapes: Vec<egui::epaint::ClippedShape>,
39    pixels_per_point: f32,
40    size_px: [u32; 2],
41    clear: [f32; 4],
42) -> Result<egui::ColorImage, String> {
43    draw(render, ctx, shapes, pixels_per_point, size_px, clear).wait(render)
44}
45
46/// A pass drawn offscreen and copied into a read-back buffer, on its way to
47/// the CPU.
48pub struct Drawn {
49    buffer: wgpu::Buffer,
50    /// What [`Drawn::wait`] waits for (the browser's mapping waits by itself).
51    #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
52    submission: wgpu::SubmissionIndex,
53    width: u32,
54    height: u32,
55    padded: usize,
56    bgra: bool,
57}
58
59/// Draw `shapes` into an offscreen texture of `size_px` over `clear`, and copy
60/// it into a buffer the CPU can map: [`Drawn::wait`] (native) or
61/// [`Drawn::on_ready`] (any target) delivers the picture.
62pub fn draw(
63    render: &egui_wgpu::RenderState,
64    ctx: &egui::Context,
65    shapes: Vec<egui::epaint::ClippedShape>,
66    pixels_per_point: f32,
67    size_px: [u32; 2],
68    clear: [f32; 4],
69) -> Drawn {
70    let [width, height] = [size_px[0].max(1), size_px[1].max(1)];
71    let screen = egui_wgpu::ScreenDescriptor { size_in_pixels: [width, height], pixels_per_point };
72    let primitives = ctx.tessellate(shapes, pixels_per_point);
73    let device = &render.device;
74    let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("offscreen pass") });
75    let texture = device.create_texture(&wgpu::TextureDescriptor {
76        label: Some("offscreen pass"),
77        size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
78        mip_level_count: 1,
79        sample_count: 1,
80        dimension: wgpu::TextureDimension::D2,
81        format: render.target_format,
82        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
83        view_formats: &[],
84    });
85    let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
86    let user_buffers = {
87        let mut renderer = render.renderer.write();
88        let user_buffers = renderer.update_buffers(device, &render.queue, &mut encoder, &primitives, &screen);
89        let [r, g, b, a] = clear;
90        let mut pass = encoder
91            .begin_render_pass(&wgpu::RenderPassDescriptor {
92                label: Some("offscreen pass"),
93                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
94                    view: &view,
95                    resolve_target: None,
96                    ops: wgpu::Operations {
97                        load: wgpu::LoadOp::Clear(wgpu::Color { r: r.into(), g: g.into(), b: b.into(), a: a.into() }),
98                        store: wgpu::StoreOp::Store,
99                    },
100                    depth_slice: None,
101                })],
102                ..Default::default()
103            })
104            .forget_lifetime();
105        renderer.render(&mut pass, &primitives, &screen);
106        drop(pass);
107        user_buffers
108    };
109    // The copy rides in the same submission as the draw.
110    let unpadded = width as usize * 4;
111    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
112    let padded = unpadded.div_ceil(align) * align;
113    let buffer = device.create_buffer(&wgpu::BufferDescriptor {
114        label: Some("offscreen read-back"),
115        size: (padded * height as usize) as u64,
116        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
117        mapped_at_creation: false,
118    });
119    encoder.copy_texture_to_buffer(
120        texture.as_image_copy(),
121        wgpu::TexelCopyBufferInfo {
122            buffer: &buffer,
123            layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded as u32), rows_per_image: None },
124        },
125        wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
126    );
127    let submission = render.queue.submit(user_buffers.into_iter().chain(std::iter::once(encoder.finish())));
128    let bgra = matches!(texture.format(), wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb);
129    Drawn { buffer, submission, width, height, padded, bgra }
130}
131
132impl Drawn {
133    /// Wait for the GPU and return the picture.
134    #[cfg(not(target_arch = "wasm32"))]
135    pub fn wait(self, render: &egui_wgpu::RenderState) -> Result<egui::ColorImage, String> {
136        let (tx, rx) = std::sync::mpsc::channel();
137        self.buffer.slice(..).map_async(wgpu::MapMode::Read, move |result| drop(tx.send(result)));
138        render
139            .device
140            .poll(wgpu::PollType::Wait { submission_index: Some(self.submission.clone()), timeout: Some(std::time::Duration::from_secs(10)) })
141            .map_err(|e| format!("offscreen capture: device poll: {e}"))?;
142        rx.recv()
143            .map_err(|_| "offscreen capture: the read-back never mapped".to_string())?
144            .map_err(|e| format!("offscreen capture: map: {e}"))?;
145        Ok(self.image())
146    }
147
148    /// Deliver the picture to `done` once the GPU has it, without blocking:
149    /// in the browser the mapping resolves on the page's event loop (a
150    /// WebGL device also needs `device.poll`, which the caller keeps doing
151    /// while a capture is out).
152    pub fn on_ready(self, done: impl FnOnce(Result<egui::ColorImage, String>) + wgpu::WasmNotSend + 'static) {
153        let buffer = self.buffer.clone();
154        buffer.slice(..).map_async(wgpu::MapMode::Read, move |result| {
155            done(result.map_err(|e| format!("offscreen capture: map: {e}")).map(|()| self.image()));
156        });
157    }
158
159    /// The mapped buffer as straight RGBA, whatever channel order the surface
160    /// format uses; unmaps it.
161    fn image(&self) -> egui::ColorImage {
162        let unpadded = self.width as usize * 4;
163        let data = self.buffer.slice(..).get_mapped_range();
164        let mut pixels = Vec::with_capacity(self.width as usize * self.height as usize);
165        for row in data.chunks_exact(self.padded) {
166            for px in row[..unpadded].chunks_exact(4) {
167                let (r, b) = if self.bgra { (px[2], px[0]) } else { (px[0], px[2]) };
168                pixels.push(egui::Color32::from_rgba_premultiplied(r, px[1], b, px[3]));
169            }
170        }
171        drop(data);
172        self.buffer.unmap();
173        egui::ColorImage {
174            size: [self.width as usize, self.height as usize],
175            source_size: egui::vec2(self.width as f32, self.height as f32),
176            pixels,
177        }
178    }
179}