Skip to main content

brep_app/
offscreen.rs

1//! Render one egui pass into an offscreen texture and read it back.
2//!
3//! The desktop loop's on-demand pass (`desktop_loop`) never presents to the
4//! window: a hidden Wayland surface gets no frame callbacks, and presenting to
5//! it can block. A capture asked for during such a pass is drawn here instead,
6//! with the window's own `egui_wgpu::RenderState`: the same device, the same
7//! renderer (so its textures and the 3D viewport's paint-callback resources
8//! are the ones the window draws with) and the same target format.
9use eframe::egui_wgpu::{self, wgpu};
10
11/// Upload the pass's new and changed textures to the shared renderer. The
12/// window's next real frame never sees this pass's texture delta, so the
13/// pass must apply it itself (a font atlas page added here is drawn there).
14pub fn apply_texture_sets(render: &egui_wgpu::RenderState, delta: &egui::TexturesDelta) {
15    let mut renderer = render.renderer.write();
16    for (id, image) in &delta.set {
17        renderer.update_texture(&render.device, &render.queue, *id, image);
18    }
19}
20
21/// Free the textures the pass released, after anything that drew with them.
22pub fn apply_texture_frees(render: &egui_wgpu::RenderState, delta: &egui::TexturesDelta) {
23    let mut renderer = render.renderer.write();
24    for id in &delta.free {
25        renderer.free_texture(id);
26    }
27}
28
29/// Draw `shapes` at `size_px` (`pixels_per_point`) over `clear` and return the
30/// picture. Its texture delta must already be applied ([`apply_texture_sets`]).
31pub fn capture(
32    render: &egui_wgpu::RenderState,
33    ctx: &egui::Context,
34    shapes: Vec<egui::epaint::ClippedShape>,
35    pixels_per_point: f32,
36    size_px: [u32; 2],
37    clear: [f32; 4],
38) -> Result<egui::ColorImage, String> {
39    let [width, height] = [size_px[0].max(1), size_px[1].max(1)];
40    let screen = egui_wgpu::ScreenDescriptor { size_in_pixels: [width, height], pixels_per_point };
41    let primitives = ctx.tessellate(shapes, pixels_per_point);
42    let device = &render.device;
43    let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("offscreen pass") });
44    let texture = device.create_texture(&wgpu::TextureDescriptor {
45        label: Some("offscreen pass"),
46        size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
47        mip_level_count: 1,
48        sample_count: 1,
49        dimension: wgpu::TextureDimension::D2,
50        format: render.target_format,
51        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
52        view_formats: &[],
53    });
54    let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
55    let user_buffers = {
56        let mut renderer = render.renderer.write();
57        let user_buffers = renderer.update_buffers(device, &render.queue, &mut encoder, &primitives, &screen);
58        let [r, g, b, a] = clear;
59        let mut pass = encoder
60            .begin_render_pass(&wgpu::RenderPassDescriptor {
61                label: Some("offscreen pass"),
62                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
63                    view: &view,
64                    resolve_target: None,
65                    ops: wgpu::Operations {
66                        load: wgpu::LoadOp::Clear(wgpu::Color { r: r.into(), g: g.into(), b: b.into(), a: a.into() }),
67                        store: wgpu::StoreOp::Store,
68                    },
69                    depth_slice: None,
70                })],
71                ..Default::default()
72            })
73            .forget_lifetime();
74        renderer.render(&mut pass, &primitives, &screen);
75        drop(pass);
76        user_buffers
77    };
78    render.queue.submit(user_buffers.into_iter().chain(std::iter::once(encoder.finish())));
79    read_back(render, &texture)
80}
81
82/// Copy `texture` to the CPU as straight RGBA, whatever channel order the
83/// surface format uses.
84fn read_back(render: &egui_wgpu::RenderState, texture: &wgpu::Texture) -> Result<egui::ColorImage, String> {
85    let (width, height) = (texture.width(), texture.height());
86    let unpadded = width as usize * 4;
87    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
88    let padded = unpadded.div_ceil(align) * align;
89    let device = &render.device;
90    let buffer = device.create_buffer(&wgpu::BufferDescriptor {
91        label: Some("offscreen read-back"),
92        size: (padded * height as usize) as u64,
93        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
94        mapped_at_creation: false,
95    });
96    let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("offscreen read-back") });
97    encoder.copy_texture_to_buffer(
98        texture.as_image_copy(),
99        wgpu::TexelCopyBufferInfo {
100            buffer: &buffer,
101            layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded as u32), rows_per_image: None },
102        },
103        wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
104    );
105    let submission = render.queue.submit(std::iter::once(encoder.finish()));
106    let slice = buffer.slice(..);
107    let (tx, rx) = std::sync::mpsc::channel();
108    slice.map_async(wgpu::MapMode::Read, move |result| drop(tx.send(result)));
109    device
110        .poll(wgpu::PollType::Wait { submission_index: Some(submission), timeout: Some(std::time::Duration::from_secs(10)) })
111        .map_err(|e| format!("offscreen capture: device poll: {e}"))?;
112    rx.recv()
113        .map_err(|_| "offscreen capture: the read-back never mapped".to_string())?
114        .map_err(|e| format!("offscreen capture: map: {e}"))?;
115    let bgra = matches!(texture.format(), wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb);
116    let data = slice.get_mapped_range();
117    let mut pixels = Vec::with_capacity(width as usize * height as usize);
118    for row in data.chunks_exact(padded) {
119        for px in row[..unpadded].chunks_exact(4) {
120            let (r, b) = if bgra { (px[2], px[0]) } else { (px[0], px[2]) };
121            pixels.push(egui::Color32::from_rgba_premultiplied(r, px[1], b, px[3]));
122        }
123    }
124    drop(data);
125    buffer.unmap();
126    Ok(egui::ColorImage {
127        size: [width as usize, height as usize],
128        source_size: egui::vec2(width as f32, height as f32),
129        pixels,
130    })
131}