waycap_rs/
waycap_egl.rs

1use std::ffi::{c_void, CStr};
2
3use khronos_egl::{self as egl, ClientBuffer, Dynamic, Instance};
4
5use crate::types::video_frame::DmaBufPlane;
6
7type PFNGLEGLIMAGETARGETTEXTURE2DOESPROC =
8    unsafe extern "C" fn(target: gl::types::GLenum, image: *const c_void);
9
10unsafe impl Sync for EglContext {}
11unsafe impl Send for EglContext {}
12
13#[derive(Clone, Copy)]
14#[allow(clippy::upper_case_acronyms)]
15pub enum GpuVendor {
16    NVIDIA,
17    AMD,
18    UNKNOWN,
19}
20
21impl From<&CStr> for GpuVendor {
22    fn from(value: &CStr) -> Self {
23        match value.to_str() {
24            Ok(s) if s.eq_ignore_ascii_case("nvidia") => Self::NVIDIA,
25            Ok(s) if s.eq_ignore_ascii_case("amd") => Self::AMD,
26            _ => Self::UNKNOWN,
27        }
28    }
29}
30
31pub struct EglContext {
32    egl_instance: Instance<Dynamic<libloading::Library, egl::EGL1_5>>,
33    display: egl::Display,
34    context: egl::Context,
35    surface: egl::Surface,
36    _config: egl::Config,
37    dmabuf_supported: bool,
38    dmabuf_modifiers_supported: bool,
39    persistent_texture_id: u32,
40    #[allow(dead_code)]
41    gpu_vendor: GpuVendor,
42
43    // Keep Wayland display alive
44    _wayland_display: wayland_client::Display,
45}
46
47impl EglContext {
48    pub fn new(width: i32, height: i32) -> Result<Self, egl::Error> {
49        let lib =
50            unsafe { libloading::Library::new("libEGL.so.1") }.expect("unable to find libEGL.so.1");
51        let egl_instance = unsafe { egl::DynamicInstance::<egl::EGL1_5>::load_required_from(lib) }
52            .expect("unable to load libEGL.so.1");
53
54        egl_instance.bind_api(egl::OPENGL_ES_API)?;
55
56        let wayland_display = wayland_client::Display::connect_to_env().unwrap();
57        let display =
58            unsafe { egl_instance.get_display(wayland_display.c_ptr() as *mut std::ffi::c_void) }
59                .unwrap();
60
61        egl_instance.initialize(display)?;
62
63        let attributes = [
64            egl::BUFFER_SIZE,
65            24,
66            egl::RENDERABLE_TYPE,
67            egl::OPENGL_ES_BIT,
68            egl::NONE,
69            egl::NONE,
70        ];
71
72        let config = egl_instance
73            .choose_first_config(display, &attributes)?
74            .expect("unable to find an appropriate ELG configuration");
75
76        egl_instance.bind_api(egl::OPENGL_ES_API)?;
77
78        let context_attributes = [egl::CONTEXT_CLIENT_VERSION, 2, egl::NONE];
79
80        let context = egl_instance.create_context(display, config, None, &context_attributes)?;
81
82        let surface_attributes = [egl::WIDTH, width, egl::HEIGHT, height, egl::NONE];
83
84        let surface = egl_instance.create_pbuffer_surface(display, config, &surface_attributes)?;
85        egl_instance.make_current(display, Some(surface), Some(surface), Some(context))?;
86
87        gl::load_with(|symbol| egl_instance.get_proc_address(symbol).unwrap() as *const _);
88
89        let (dmabuf_supported, dmabuf_modifiers_supported) =
90            Self::check_dmabuf_support(&egl_instance, display).unwrap();
91
92        let persistent_texture =
93            Self::create_persistent_texture(width as u32, height as u32).unwrap();
94
95        let gpu_vendor = GpuVendor::from(egl_instance.query_string(Some(display), egl::VENDOR)?);
96
97        Ok(Self {
98            egl_instance,
99            display,
100            _config: config,
101            context,
102            surface,
103            dmabuf_supported,
104            dmabuf_modifiers_supported,
105            persistent_texture_id: persistent_texture,
106            gpu_vendor,
107
108            _wayland_display: wayland_display,
109        })
110    }
111
112    pub fn update_texture_from_image(
113        &self,
114        egl_image: egl::Image,
115    ) -> Result<(), Box<dyn std::error::Error>> {
116        unsafe {
117            // Create a temporary texture from the EGL image
118            let mut temp_texture = 0;
119            gl::GenTextures(1, &mut temp_texture);
120            gl::BindTexture(gl::TEXTURE_2D, temp_texture);
121
122            // Bind EGL image to temporary texture
123            let egl_texture_2d = {
124                let proc_name = "glEGLImageTargetTexture2DOES";
125                let proc_addr = self.egl_instance.get_proc_address(proc_name);
126
127                if proc_addr.is_none() {
128                    gl::DeleteTextures(1, &temp_texture);
129                    return Err("glEGLImageTargetTexture2DOES not available".into());
130                } else {
131                    std::mem::transmute::<Option<extern "system" fn()>, PFNGLEGLIMAGETARGETTEXTURE2DOESPROC>(proc_addr)
132                }
133            };
134
135            egl_texture_2d(gl::TEXTURE_2D, egl_image.as_ptr());
136
137            let gl_error = gl::GetError();
138            if gl_error != gl::NO_ERROR {
139                gl::DeleteTextures(1, &temp_texture);
140                return Err(
141                    format!("Failed to bind EGL image to temp texture: 0x{:x}", gl_error).into(),
142                );
143            }
144
145            // Get dimensions from the EGL image texture
146            let mut width = 0;
147            let mut height = 0;
148            gl::GetTexLevelParameteriv(gl::TEXTURE_2D, 0, gl::TEXTURE_WIDTH, &mut width);
149            gl::GetTexLevelParameteriv(gl::TEXTURE_2D, 0, gl::TEXTURE_HEIGHT, &mut height);
150
151            // Create framebuffer for copying
152            let mut fbo = 0;
153            gl::GenFramebuffers(1, &mut fbo);
154            gl::BindFramebuffer(gl::FRAMEBUFFER, fbo);
155
156            // Attach temporary EGL texture as source
157            gl::FramebufferTexture2D(
158                gl::FRAMEBUFFER,
159                gl::COLOR_ATTACHMENT0,
160                gl::TEXTURE_2D,
161                temp_texture,
162                0,
163            );
164
165            // Check framebuffer status
166            let status = gl::CheckFramebufferStatus(gl::FRAMEBUFFER);
167            if status != gl::FRAMEBUFFER_COMPLETE {
168                gl::BindFramebuffer(gl::FRAMEBUFFER, 0);
169                gl::DeleteFramebuffers(1, &fbo);
170                gl::DeleteTextures(1, &temp_texture);
171                return Err(format!("Framebuffer not complete: 0x{:x}", status).into());
172            }
173
174            // Bind persistent texture as destination
175            gl::BindTexture(gl::TEXTURE_2D, self.persistent_texture_id);
176
177            // Use CopyTexSubImage2D instead of CopyTexImage2D
178            // This updates existing texture data rather than reallocating
179            gl::CopyTexSubImage2D(
180                gl::TEXTURE_2D,
181                0, // mipmap level
182                0,
183                0, // destination x, y offset in texture
184                0,
185                0,      // source x, y offset in framebuffer
186                width,  // width to copy
187                height, // height to copy
188            );
189
190            let gl_error = gl::GetError();
191            if gl_error != gl::NO_ERROR {
192                gl::BindTexture(gl::TEXTURE_2D, 0);
193                gl::BindFramebuffer(gl::FRAMEBUFFER, 0);
194                gl::DeleteFramebuffers(1, &fbo);
195                gl::DeleteTextures(1, &temp_texture);
196                return Err(format!("Failed to copy texture data: 0x{:x}", gl_error).into());
197            }
198
199            // Cleanup
200            gl::BindTexture(gl::TEXTURE_2D, 0);
201            gl::BindFramebuffer(gl::FRAMEBUFFER, 0);
202            gl::DeleteFramebuffers(1, &fbo);
203            gl::DeleteTextures(1, &temp_texture);
204
205            Ok(())
206        }
207    }
208
209    pub fn create_persistent_texture(
210        width: u32,
211        height: u32,
212    ) -> Result<u32, Box<dyn std::error::Error>> {
213        unsafe {
214            let mut texture_id = 0;
215            gl::GenTextures(1, &mut texture_id);
216            gl::BindTexture(gl::TEXTURE_2D, texture_id);
217
218            // Allocate texture storage with CUDA-compatible RGBA8 format
219            gl::TexImage2D(
220                gl::TEXTURE_2D,
221                0,
222                gl::RGBA8 as i32, // CUDA-compatible format
223                width as i32,
224                height as i32,
225                0,
226                gl::RGBA,
227                gl::UNSIGNED_BYTE,
228                std::ptr::null(), // No initial data
229            );
230
231            // Set texture parameters for better performance
232            gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as i32);
233            gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as i32);
234            gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32);
235            gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32);
236
237            gl::BindTexture(gl::TEXTURE_2D, 0);
238
239            let gl_error = gl::GetError();
240            if gl_error != gl::NO_ERROR {
241                gl::DeleteTextures(1, &texture_id);
242                return Err(
243                    format!("Failed to create persistent texture: 0x{:x}", gl_error).into(),
244                );
245            }
246
247            log::trace!(
248                "✓ Created persistent texture: ID {} ({}x{})",
249                texture_id,
250                width,
251                height
252            );
253            Ok(texture_id)
254        }
255    }
256
257    fn check_dmabuf_support(
258        egl_instance: &Instance<Dynamic<libloading::Library, egl::EGL1_5>>,
259        display: egl::Display,
260    ) -> Result<(bool, bool), Box<dyn std::error::Error>> {
261        let extensions = egl_instance.query_string(Some(display), egl::EXTENSIONS)?;
262        let ext_str = extensions.to_string_lossy();
263
264        let dmabuf_import = ext_str.contains("EGL_EXT_image_dma_buf_import");
265        let dmabuf_modifiers = ext_str.contains("EGL_EXT_image_dma_buf_import_modifiers");
266
267        if !dmabuf_import {
268            return Err("EGL_EXT_image_dma_buf_import not supported".into());
269        }
270
271        Ok((dmabuf_import, dmabuf_modifiers))
272    }
273
274    pub fn create_image_from_dmabuf(
275        &self,
276        planes: &[DmaBufPlane],
277        format: u32,
278        width: u32,
279        height: u32,
280        modifier: u64,
281    ) -> Result<egl::Image, Box<dyn std::error::Error>> {
282        if !self.dmabuf_supported {
283            return Err("DMA-BUF import not supported".into());
284        }
285
286        let mut attributes = vec![
287            // EGL_LINUX_DRM_FOURCC_EXT
288            0x3271,
289            format as usize,
290            egl::WIDTH as usize,
291            width as usize,
292            egl::HEIGHT as usize,
293            height as usize,
294        ];
295
296        for (i, plane) in planes.iter().enumerate() {
297            let plane_attrs = match i {
298                0 => vec![
299                    // EGL_DMA_BUF_PLANE0_FD_EXT
300                    0x3272,
301                    plane.fd as usize,
302                    // EGL_DMA_BUF_PLANE0_OFFSET_EXT
303                    0x3273,
304                    plane.offset as usize,
305                    // EGL_DMA_BUF_PLANE0_PITCH_EXT
306                    0x3274,
307                    plane.stride as usize,
308                ],
309                1 => vec![
310                    // EGL_DMA_BUF_PLANE1_FD_EXT
311                    0x3275,
312                    plane.fd as usize,
313                    // EGL_DMA_BUF_PLANE1_OFFSET_EXT
314                    0x3276,
315                    plane.offset as usize,
316                    // EGL_DMA_BUF_PLANE1_PITCH_EXT
317                    0x3277,
318                    plane.stride as usize,
319                ],
320                2 => vec![
321                    // EGL_DMA_BUF_PLANE2_FD_EXT
322                    0x3278,
323                    plane.fd as usize,
324                    // EGL_DMA_BUF_PLANE2_OFFSET_EXT
325                    0x3279,
326                    plane.offset as usize,
327                    // EGL_DMA_BUF_PLANE2_PITCH_EXT
328                    0x327A,
329                    plane.stride as usize,
330                ],
331                _ => break,
332            };
333
334            attributes.extend(plane_attrs);
335
336            // Add modifiers if supported
337            if self.dmabuf_modifiers_supported {
338                let modifier_attrs = match i {
339                    0 => vec![
340                        // EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT
341                        0x3443,
342                        (modifier & 0xFFFFFFFF) as usize,
343                        // EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT
344                        0x3444,
345                        (modifier >> 32) as usize,
346                    ],
347                    1 => vec![
348                        // EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT
349                        0x3445,
350                        (modifier & 0xFFFFFFFF) as usize,
351                        // EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT
352                        0x3446,
353                        (modifier >> 32) as usize,
354                    ],
355                    2 => vec![
356                        // EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT
357                        0x3447,
358                        (modifier & 0xFFFFFFFF) as usize,
359                        // EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT
360                        0x3448,
361                        (modifier >> 32) as usize,
362                    ],
363                    _ => break,
364                };
365                attributes.extend(modifier_attrs);
366            }
367        }
368
369        attributes.push(egl::NONE as usize);
370
371        // Create EGL image
372        let image = self
373            .egl_instance
374            .create_image(
375                self.display,
376                unsafe { egl::Context::from_ptr(egl::NO_CONTEXT) },
377                // EGL_LINUX_DMA_BUF_EXT
378                0x3270,
379                unsafe { ClientBuffer::from_ptr(std::ptr::null_mut()) },
380                &attributes,
381            )
382            .map_err(|e| format!("Failed to create EGL image from DMA-BUF: {:?}", e))?;
383
384        Ok(image)
385    }
386
387    pub fn destroy_image(&self, image: egl::Image) -> Result<(), Box<dyn std::error::Error>> {
388        self.egl_instance
389            .destroy_image(self.display, image)
390            .map_err(|e| format!("Failed to destroy EGL image: {:?}", e).into())
391    }
392
393    pub fn delete_texture(&self, texture_id: u32) {
394        unsafe {
395            gl::DeleteTextures(1, &texture_id);
396        }
397    }
398
399    pub fn make_current(&self) -> Result<(), egl::Error> {
400        self.egl_instance.make_current(
401            self.display,
402            Some(self.surface),
403            Some(self.surface),
404            Some(self.context),
405        )?;
406        Ok(())
407    }
408
409    pub fn release_current(&self) -> Result<(), egl::Error> {
410        self.egl_instance
411            .make_current(self.display, None, None, None)?;
412        Ok(())
413    }
414
415    pub fn get_texture_id(&self) -> u32 {
416        self.persistent_texture_id
417    }
418
419    // TODO: We can probably leverage this to dynamically set the encoder and deprecate
420    // the need to pass it in
421    #[allow(unused)]
422    pub fn get_gpu_vendor(&self) -> GpuVendor {
423        self.gpu_vendor
424    }
425}
426
427impl Drop for EglContext {
428    fn drop(&mut self) {
429        let _ = self
430            .egl_instance
431            .make_current(self.display, None, None, None);
432        let _ = self
433            .egl_instance
434            .destroy_surface(self.display, self.surface);
435        let _ = self
436            .egl_instance
437            .destroy_context(self.display, self.context);
438        let _ = self.egl_instance.terminate(self.display);
439        self.delete_texture(self.persistent_texture_id);
440    }
441}