Skip to main content

wlr_capture/
render.rs

1//! Shared egui → `egui_glow` rendering core on an EGL/GLES context bound to a
2//! Wayland surface, plus zero-copy dma-buf → GL texture import.
3//!
4//! This is the toolkit half of `wlr-capture`: any windowing host (the
5//! `wlr-chooser` layer-shell overlay, the `wlr-pip` xdg-toplevel mirror, …) binds
6//! a [`Gpu`] to its `wl_surface` and drives one egui frame per repaint with
7//! [`Gpu::render`]. The host owns the GL context, so it (via the importer handed
8//! to the UI closure) turns capture dma-bufs into drawable textures.
9
10use crate::gl::{
11    DmabufEgl, EGL_LINUX_DMA_BUF_EXT, Egl, EglImage, GL_TEXTURE_2D, dmabuf_image_attribs,
12    load_dmabuf_egl,
13};
14use crate::wl;
15use edgefirst_egl as egl;
16use std::collections::HashMap;
17use std::ffi::c_void;
18use std::sync::Arc;
19use wayland_client::{Connection, Proxy, protocol::wl_surface::WlSurface};
20
21/// Host-side importer for GPU dma-buf frames. The windowing host owns the GL
22/// context, so it (not a toolkit-agnostic UI) turns a dma-buf into a drawable
23/// egui texture. Returns the texture id + source pixel size.
24pub trait DmabufImporter {
25    /// Import `frame` as a GL-backed egui texture, caching it under `key` (the
26    /// swapchain slot). Returns the texture id and the source pixel size, or `None`
27    /// if the import fails.
28    fn import(
29        &mut self,
30        key: &str,
31        frame: wl::DmabufFrame,
32    ) -> Option<(egui::TextureId, egui::Vec2)>;
33    /// Release any GPU resources cached for a source that went away.
34    fn forget(&mut self, key: &str);
35}
36
37// dma-buf → GL texture import: the EGL/GL core (entry points, EGLImage creation,
38// readback) lives in `crate::gl`; here we only sample the imported texture for
39// display. These two GL swizzle constants are display-only.
40const GL_TEXTURE_SWIZZLE_A: u32 = 0x8E45;
41const GL_ONE: i32 = 1;
42
43/// A dma-buf imported as a GL texture, cached per source key.
44struct NativeTex {
45    image: EglImage,
46    tex: glow::Texture,
47    id: egui::TextureId,
48    size: egui::Vec2,
49}
50
51/// Host-side [`DmabufImporter`]: turns a dma-buf fd into a GL texture egui can
52/// sample. Borrows the painter (to register native textures) and the persistent
53/// texture cache; `egl` is `None` if the driver can't import dma-bufs.
54struct HostImporter<'a> {
55    egl: Option<DmabufEgl>,
56    gl: Arc<glow::Context>,
57    painter: &'a mut egui_glow::Painter,
58    cache: &'a mut HashMap<String, NativeTex>,
59}
60
61impl DmabufImporter for HostImporter<'_> {
62    fn import(
63        &mut self,
64        key: &str,
65        frame: wl::DmabufFrame,
66    ) -> Option<(egui::TextureId, egui::Vec2)> {
67        use glow::HasContext as _;
68        let egl = self.egl?;
69        let size = egui::vec2(frame.width as f32, frame.height as f32);
70        let attribs = dmabuf_image_attribs(&frame);
71        // EGL_NO_CONTEXT for dma-buf import; EGL dups the fd, so we may close ours.
72        let image = unsafe {
73            (egl.create_image)(
74                egl.display,
75                std::ptr::null_mut(),
76                EGL_LINUX_DMA_BUF_EXT,
77                std::ptr::null_mut(),
78                attribs.as_ptr(),
79            )
80        };
81        if image.is_null() {
82            return None;
83        }
84
85        let ckey = key.to_string();
86        // Refresh the existing texture in place (the dma-buf is the same kernel
87        // object; just rebind the fresh image), keeping a stable egui texture id.
88        if let Some(nt) = self.cache.get_mut(&ckey) {
89            unsafe {
90                self.gl.bind_texture(GL_TEXTURE_2D, Some(nt.tex));
91                (egl.image_target)(GL_TEXTURE_2D, image);
92                self.gl.bind_texture(GL_TEXTURE_2D, None);
93                (egl.destroy_image)(egl.display, nt.image);
94            }
95            nt.image = image;
96            nt.size = size;
97            return Some((nt.id, nt.size));
98        }
99
100        // First frame for this slot: create the GL texture and register it.
101        let tex = unsafe {
102            let t = self.gl.create_texture().ok()?;
103            self.gl.bind_texture(GL_TEXTURE_2D, Some(t));
104            let lin = glow::LINEAR as i32;
105            let clamp = glow::CLAMP_TO_EDGE as i32;
106            self.gl
107                .tex_parameter_i32(GL_TEXTURE_2D, glow::TEXTURE_MIN_FILTER, lin);
108            self.gl
109                .tex_parameter_i32(GL_TEXTURE_2D, glow::TEXTURE_MAG_FILTER, lin);
110            self.gl
111                .tex_parameter_i32(GL_TEXTURE_2D, glow::TEXTURE_WRAP_S, clamp);
112            self.gl
113                .tex_parameter_i32(GL_TEXTURE_2D, glow::TEXTURE_WRAP_T, clamp);
114            // Captured buffers are XRGB (no real alpha): the X byte is undefined,
115            // so force sampled alpha to 1, else egui blends with garbage alpha.
116            self.gl
117                .tex_parameter_i32(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE);
118            (egl.image_target)(GL_TEXTURE_2D, image);
119            self.gl.bind_texture(GL_TEXTURE_2D, None);
120            t
121        };
122        let id = self.painter.register_native_texture(tex);
123        self.cache.insert(
124            ckey,
125            NativeTex {
126                image,
127                tex,
128                id,
129                size,
130            },
131        );
132        Some((id, size))
133    }
134
135    fn forget(&mut self, key: &str) {
136        use glow::HasContext as _;
137        let Some(egl) = self.egl else { return };
138        if let Some(nt) = self.cache.remove(key) {
139            self.painter.free_texture(nt.id);
140            unsafe {
141                self.gl.delete_texture(nt.tex);
142                (egl.destroy_image)(egl.display, nt.image);
143            }
144        }
145    }
146}
147
148/// EGL/GL state bound to a `wl_surface`, created once the surface has its first
149/// size. Owns the egui_glow painter and the dma-buf texture cache.
150pub struct Gpu {
151    egl: Egl,
152    display: egl::Display,
153    surface: egl::Surface,
154    context: egl::Context,
155    egl_window: wayland_egl::WlEglSurface,
156    painter: egui_glow::Painter,
157    /// dma-buf import entry points, if the driver supports them.
158    dmabuf_egl: Option<DmabufEgl>,
159    /// dma-buf textures imported for display, keyed by source key.
160    dmabuf_tex: HashMap<String, NativeTex>,
161}
162
163impl Gpu {
164    /// Build the EGL/GLES context for `surface` at physical size `pw`×`ph`.
165    /// Panics on EGL setup failure (the host can't render without it).
166    pub fn new(conn: &Connection, surface: &WlSurface, pw: i32, ph: i32) -> Gpu {
167        let lib = unsafe { egl::DynamicInstance::<egl::EGL1_4>::load_required() }
168            .expect("libEGL not found");
169        let egl: Egl = lib;
170
171        let display_ptr = conn.backend().display_ptr() as *mut c_void;
172        let display = unsafe { egl.get_display(display_ptr).expect("eglGetDisplay") };
173        egl.initialize(display).expect("eglInitialize");
174        egl.bind_api(egl::OPENGL_ES_API).expect("eglBindAPI");
175
176        let attribs = [
177            egl::SURFACE_TYPE,
178            egl::WINDOW_BIT,
179            egl::RENDERABLE_TYPE,
180            egl::OPENGL_ES2_BIT,
181            egl::RED_SIZE,
182            8,
183            egl::GREEN_SIZE,
184            8,
185            egl::BLUE_SIZE,
186            8,
187            egl::ALPHA_SIZE,
188            8,
189            egl::NONE,
190        ];
191        let config = egl
192            .choose_first_config(display, &attribs)
193            .expect("eglChooseConfig")
194            .expect("no EGL config with alpha");
195
196        let ctx_attribs = [egl::CONTEXT_CLIENT_VERSION, 3, egl::NONE];
197        let context = egl
198            .create_context(display, config, None, &ctx_attribs)
199            .or_else(|_| {
200                let a = [egl::CONTEXT_CLIENT_VERSION, 2, egl::NONE];
201                egl.create_context(display, config, None, &a)
202            })
203            .expect("eglCreateContext");
204
205        let egl_window = wayland_egl::WlEglSurface::new(surface.id(), pw, ph).expect("wl_egl");
206        let egl_surface = unsafe {
207            egl.create_window_surface(
208                display,
209                config,
210                egl_window.ptr() as egl::NativeWindowType,
211                None,
212            )
213            .expect("eglCreateWindowSurface")
214        };
215        egl.make_current(display, Some(egl_surface), Some(egl_surface), Some(context))
216            .expect("eglMakeCurrent");
217
218        let gl = unsafe {
219            glow::Context::from_loader_function(|s| {
220                egl.get_proc_address(s)
221                    .map_or(std::ptr::null(), |p| p as *const _)
222            })
223        };
224        let painter = egui_glow::Painter::new(Arc::new(gl), "", None, false).expect("egui_glow");
225        let dmabuf_egl = load_dmabuf_egl(&egl, display);
226        if dmabuf_egl.is_none() {
227            eprintln!("wlr-capture: EGL dma-buf import unavailable (GPU display disabled)");
228        }
229
230        Gpu {
231            egl,
232            display,
233            surface: egl_surface,
234            context,
235            egl_window,
236            painter,
237            dmabuf_egl,
238            dmabuf_tex: HashMap::new(),
239        }
240    }
241
242    /// Resize the EGL window to a new physical size (after a surface configure /
243    /// scale change).
244    pub fn resize(&self, pw: i32, ph: i32) {
245        self.egl_window.resize(pw, ph, 0, 0);
246    }
247
248    /// Run one egui frame and present it. `run_ui` builds the UI; it is handed the
249    /// dma-buf importer (this owns the GL context) so capture frames become
250    /// drawable textures. `backdrop` is the GL clear colour (premultiplied gamma).
251    pub fn render(
252        &mut self,
253        egui_ctx: &egui::Context,
254        mut raw_input: egui::RawInput,
255        ppp: f32,
256        size_px: (u32, u32),
257        backdrop: [f32; 4],
258        mut run_ui: impl FnMut(&mut egui::Ui, &mut dyn DmabufImporter),
259    ) {
260        // Lay text out at the same pixels-per-point we tessellate with, or epaint warns
261        // ("pixels_per_point have changed between text layout and tessellation") and
262        // text shapes can be mis-scaled — happens on fractional/HiDPI outputs where
263        // `ppp != 1.0` but the caller left it unset in `raw_input`.
264        raw_input
265            .viewports
266            .entry(egui::ViewportId::ROOT)
267            .or_default()
268            .native_pixels_per_point = Some(ppp);
269
270        let (pw, ph) = size_px;
271        self.egl
272            .make_current(
273                self.display,
274                Some(self.surface),
275                Some(self.surface),
276                Some(self.context),
277            )
278            .ok();
279
280        // Run the UI. GPU dma-buf frames are imported here via the host importer,
281        // since that needs the painter + GL context.
282        let (prims, textures_delta) = {
283            let gl = self.painter.gl().clone();
284            let mut importer = HostImporter {
285                egl: self.dmabuf_egl,
286                gl,
287                painter: &mut self.painter,
288                cache: &mut self.dmabuf_tex,
289            };
290            // `run_ui` hands the closure a full-screen root `Ui`; paint functions add
291            // their panels into it with `show_inside`.
292            let full = egui_ctx.run_ui(raw_input, |ui| run_ui(ui, &mut importer));
293            (egui_ctx.tessellate(full.shapes, ppp), full.textures_delta)
294        };
295
296        unsafe {
297            use glow::HasContext as _;
298            let gl = self.painter.gl();
299            gl.viewport(0, 0, pw as i32, ph as i32);
300            let [r, g, b, a] = backdrop;
301            gl.clear_color(r, g, b, a);
302            gl.clear(glow::COLOR_BUFFER_BIT);
303        }
304        self.painter
305            .paint_and_update_textures([pw, ph], ppp, &prims, &textures_delta);
306        self.egl.swap_buffers(self.display, self.surface).ok();
307    }
308}