Skip to main content

wlr_capture/
gl.rs

1//! EGL/GLES core: dma-buf → GL texture import and headless readback.
2//!
3//! This is the windowing- and egui-free half of the GPU path. It loads the
4//! `EGL_EXT_image_dma_buf_import` entry points and wraps a capture dma-buf as an
5//! `EGLImage`, and provides [`GpuReadback`] — an offscreen context that reads such a
6//! dma-buf back to CPU RGBA8. The egui display toolkit ([`crate::render`], gated by
7//! the `toolkit` feature) builds on these primitives; this module stays available in
8//! headless builds (record/timelapse) that need readback but no UI.
9//!
10//! Extension function pointers are loaded at runtime via `eglGetProcAddress`
11//! (edgefirst-egl has no typed bindings for these).
12
13use crate::wl;
14use anyhow::{Context as _, Result, anyhow, bail};
15use edgefirst_egl as egl;
16use std::ffi::c_void;
17use std::os::fd::AsRawFd;
18use std::sync::Arc;
19use wayland_client::Connection;
20
21pub(crate) type Egl = egl::Instance<egl::Dynamic<libloading::Library, egl::EGL1_4>>;
22
23pub(crate) type EglImage = *mut c_void;
24pub(crate) const EGL_LINUX_DMA_BUF_EXT: u32 = 0x3270;
25const EGL_WIDTH: i32 = 0x3057;
26const EGL_HEIGHT: i32 = 0x3056;
27const EGL_LINUX_DRM_FOURCC_EXT: i32 = 0x3271;
28const EGL_DMA_BUF_PLANE0_FD_EXT: i32 = 0x3272;
29const EGL_DMA_BUF_PLANE0_OFFSET_EXT: i32 = 0x3273;
30const EGL_DMA_BUF_PLANE0_PITCH_EXT: i32 = 0x3274;
31const EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT: i32 = 0x3443;
32const EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT: i32 = 0x3444;
33const EGL_ATTRIB_NONE: i32 = 0x3038;
34pub(crate) const GL_TEXTURE_2D: u32 = 0x0DE1;
35
36type EglCreateImageKhr =
37    unsafe extern "system" fn(*mut c_void, *mut c_void, u32, *mut c_void, *const i32) -> EglImage;
38type EglDestroyImageKhr = unsafe extern "system" fn(*mut c_void, EglImage) -> u32;
39type GlEglImageTargetTexture2dOes = unsafe extern "system" fn(u32, EglImage);
40
41/// Resolved EGL/GL extension entry points + the EGL display, for dma-buf import.
42#[derive(Clone, Copy)]
43pub(crate) struct DmabufEgl {
44    pub(crate) display: *mut c_void,
45    pub(crate) create_image: EglCreateImageKhr,
46    pub(crate) destroy_image: EglDestroyImageKhr,
47    pub(crate) image_target: GlEglImageTargetTexture2dOes,
48}
49
50/// Load the dma-buf import entry points. `None` if the driver lacks them (then there
51/// is no GPU import path and callers fall back to whatever shm provided).
52pub(crate) fn load_dmabuf_egl(egl: &Egl, display: egl::Display) -> Option<DmabufEgl> {
53    let create = egl.get_proc_address("eglCreateImageKHR")?;
54    let destroy = egl.get_proc_address("eglDestroyImageKHR")?;
55    let target = egl.get_proc_address("glEGLImageTargetTexture2DOES")?;
56    // Same calling convention (extern "system"), just typed signatures.
57    Some(unsafe {
58        DmabufEgl {
59            display: display.as_ptr(),
60            create_image: std::mem::transmute::<extern "system" fn(), EglCreateImageKhr>(create),
61            destroy_image: std::mem::transmute::<extern "system" fn(), EglDestroyImageKhr>(destroy),
62            image_target: std::mem::transmute::<extern "system" fn(), GlEglImageTargetTexture2dOes>(
63                target,
64            ),
65        }
66    })
67}
68
69/// Build the `EGL_LINUX_DMA_BUF_EXT` attribute list for a single-plane frame. The
70/// fd is only borrowed (EGL dups it in `eglCreateImageKHR`), so `frame` must outlive
71/// the import call.
72pub(crate) fn dmabuf_image_attribs(frame: &wl::DmabufFrame) -> [i32; 17] {
73    [
74        EGL_WIDTH,
75        frame.width as i32,
76        EGL_HEIGHT,
77        frame.height as i32,
78        EGL_LINUX_DRM_FOURCC_EXT,
79        frame.fourcc as i32,
80        EGL_DMA_BUF_PLANE0_FD_EXT,
81        frame.fd.as_raw_fd(),
82        EGL_DMA_BUF_PLANE0_OFFSET_EXT,
83        frame.offset as i32,
84        EGL_DMA_BUF_PLANE0_PITCH_EXT,
85        frame.stride as i32,
86        EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
87        (frame.modifier & 0xffff_ffff) as i32,
88        EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
89        (frame.modifier >> 32) as i32,
90        EGL_ATTRIB_NONE,
91    ]
92}
93
94/// Headless EGL/GLES context for reading a capture dma-buf back to CPU RGBA pixels.
95///
96/// The GPU capture path hands out a [`wl::DmabufFrame`] (zero-copy, meant for
97/// display). Tools that ultimately need CPU pixels — screenshot encoding today,
98/// video/timelapse on the roadmap — use this to import that dma-buf as a GL texture
99/// and `glReadPixels` it into RGBA8. It runs without a window: a 1×1 pbuffer keeps
100/// it portable across drivers that lack surfaceless contexts. Build one and reuse it
101/// across frames (the EGL setup is not free).
102pub struct GpuReadback {
103    // Keeps the Wayland display (whose ptr backs the EGL display) alive.
104    _conn: Connection,
105    egl: Egl,
106    display: egl::Display,
107    surface: egl::Surface,
108    context: egl::Context,
109    gl: Arc<glow::Context>,
110    dmabuf_egl: Option<DmabufEgl>,
111}
112
113impl GpuReadback {
114    /// Create the offscreen context. Errors (rather than panicking, unlike
115    /// `Gpu::new`) since callers can fall back to the shm capture path.
116    pub fn new() -> Result<Self> {
117        let conn = Connection::connect_to_env().context("Wayland connection")?;
118        let lib = unsafe { egl::DynamicInstance::<egl::EGL1_4>::load_required() }
119            .map_err(|e| anyhow!("libEGL not found: {e}"))?;
120        let egl: Egl = lib;
121
122        let display_ptr = conn.backend().display_ptr() as *mut c_void;
123        let display = unsafe { egl.get_display(display_ptr) }.context("eglGetDisplay")?;
124        egl.initialize(display).context("eglInitialize")?;
125        egl.bind_api(egl::OPENGL_ES_API).context("eglBindAPI")?;
126
127        let attribs = [
128            egl::SURFACE_TYPE,
129            egl::PBUFFER_BIT,
130            egl::RENDERABLE_TYPE,
131            egl::OPENGL_ES2_BIT,
132            egl::RED_SIZE,
133            8,
134            egl::GREEN_SIZE,
135            8,
136            egl::BLUE_SIZE,
137            8,
138            egl::ALPHA_SIZE,
139            8,
140            egl::NONE,
141        ];
142        let config = egl
143            .choose_first_config(display, &attribs)
144            .context("eglChooseConfig")?
145            .context("no EGL pbuffer config")?;
146
147        let ctx_attribs = [egl::CONTEXT_CLIENT_VERSION, 3, egl::NONE];
148        let context = egl
149            .create_context(display, config, None, &ctx_attribs)
150            .or_else(|_| {
151                let a = [egl::CONTEXT_CLIENT_VERSION, 2, egl::NONE];
152                egl.create_context(display, config, None, &a)
153            })
154            .context("eglCreateContext")?;
155
156        let pb_attribs = [egl::WIDTH, 1, egl::HEIGHT, 1, egl::NONE];
157        let surface = egl
158            .create_pbuffer_surface(display, config, &pb_attribs)
159            .context("eglCreatePbufferSurface")?;
160        egl.make_current(display, Some(surface), Some(surface), Some(context))
161            .context("eglMakeCurrent")?;
162
163        let gl = unsafe {
164            glow::Context::from_loader_function(|s| {
165                egl.get_proc_address(s)
166                    .map_or(std::ptr::null(), |p| p as *const _)
167            })
168        };
169        let dmabuf_egl = load_dmabuf_egl(&egl, display);
170
171        Ok(GpuReadback {
172            _conn: conn,
173            egl,
174            display,
175            surface,
176            context,
177            gl: Arc::new(gl),
178            dmabuf_egl,
179        })
180    }
181
182    /// Import `frame`'s dma-buf as a GL texture and read it back to RGBA8. Alpha is
183    /// forced opaque: captures are opaque and XRGB dma-bufs leave the X byte
184    /// undefined (mirrors the shm path's handling of alpha-less formats).
185    pub fn readback(&mut self, frame: wl::DmabufFrame) -> Result<wl::CapturedImage> {
186        let egl = self
187            .dmabuf_egl
188            .context("EGL dma-buf import unavailable (driver)")?;
189        let (w, h) = (frame.width, frame.height);
190        if w == 0 || h == 0 {
191            bail!("dimensions de readback nulles");
192        }
193
194        self.egl
195            .make_current(
196                self.display,
197                Some(self.surface),
198                Some(self.surface),
199                Some(self.context),
200            )
201            .context("eglMakeCurrent")?;
202
203        let attribs = dmabuf_image_attribs(&frame);
204        let image = unsafe {
205            (egl.create_image)(
206                egl.display,
207                std::ptr::null_mut(),
208                EGL_LINUX_DMA_BUF_EXT,
209                std::ptr::null_mut(),
210                attribs.as_ptr(),
211            )
212        };
213        if image.is_null() {
214            bail!("eglCreateImageKHR failed");
215        }
216
217        // Import → bind to a texture → attach to an FBO → glReadPixels. Always
218        // destroy the EGLImage afterwards, success or not.
219        let read = self.read_image_to_rgba(&egl, image, w, h);
220        unsafe { (egl.destroy_image)(egl.display, image) };
221        let mut rgba = read?;
222
223        for px in rgba.chunks_exact_mut(4) {
224            px[3] = 255;
225        }
226        Ok(wl::CapturedImage {
227            width: w,
228            height: h,
229            rgba,
230        })
231    }
232
233    /// Bind `image` to a fresh texture + FBO and read it back, cleaning up the GL
234    /// objects before returning. Split out so [`Self::readback`] can destroy the
235    /// EGLImage on every path.
236    fn read_image_to_rgba(
237        &self,
238        egl: &DmabufEgl,
239        image: EglImage,
240        w: u32,
241        h: u32,
242    ) -> Result<Vec<u8>> {
243        use glow::HasContext as _;
244        unsafe {
245            let tex = self
246                .gl
247                .create_texture()
248                .map_err(|e| anyhow!("glGenTextures: {e}"))?;
249            self.gl.bind_texture(GL_TEXTURE_2D, Some(tex));
250            (egl.image_target)(GL_TEXTURE_2D, image);
251
252            let fbo = self.gl.create_framebuffer().map_err(|e| {
253                self.gl.delete_texture(tex);
254                anyhow!("glGenFramebuffers: {e}")
255            })?;
256            self.gl.bind_framebuffer(glow::FRAMEBUFFER, Some(fbo));
257            self.gl.framebuffer_texture_2d(
258                glow::FRAMEBUFFER,
259                glow::COLOR_ATTACHMENT0,
260                GL_TEXTURE_2D,
261                Some(tex),
262                0,
263            );
264
265            let status = self.gl.check_framebuffer_status(glow::FRAMEBUFFER);
266            let result = if status == glow::FRAMEBUFFER_COMPLETE {
267                let mut buf = vec![0u8; w as usize * h as usize * 4];
268                self.gl.read_pixels(
269                    0,
270                    0,
271                    w as i32,
272                    h as i32,
273                    glow::RGBA,
274                    glow::UNSIGNED_BYTE,
275                    glow::PixelPackData::Slice(Some(&mut buf)),
276                );
277                Ok(buf)
278            } else {
279                Err(anyhow!("FBO de readback incomplet (0x{status:x})"))
280            };
281
282            self.gl.bind_framebuffer(glow::FRAMEBUFFER, None);
283            self.gl.delete_framebuffer(fbo);
284            self.gl.bind_texture(GL_TEXTURE_2D, None);
285            self.gl.delete_texture(tex);
286            result
287        }
288    }
289}