medias 0.3.4

TV media player on bare DRM/KMS: Vulkan UI plane over libmpv video, for local files, UPnP/DLNA servers and YouTube, controlled by HDMI-CEC remote, evdev or terminal
//! EGL/GLES context and dma-buf framebuffer import for mpv rendering.
//! EGL and KMS reference the same GBM allocation.

use std::ffi::{CStr, c_char, c_void};
use std::os::fd::{AsRawFd, RawFd};
use std::sync::atomic::{AtomicPtr, Ordering};

use khronos_egl as egl;

use crate::gbm::{Allocator, Scanout, ScanoutLayout, validate_plane_count};

type Egl = egl::DynamicInstance<egl::EGL1_5>;

// EGL_KHR_platform_gbm / EGL_EXT_image_dma_buf_import(_modifiers)
const PLATFORM_GBM_KHR: egl::Enum = 0x31D7;
const LINUX_DMA_BUF_EXT: egl::Enum = 0x3270;
const EXTENSIONS: egl::Int = 0x3055;
const A_WIDTH: egl::Attrib = 0x3057;
const A_HEIGHT: egl::Attrib = 0x3056;
const A_FOURCC: egl::Attrib = 0x3271;
const A_PLANE0_FD: egl::Attrib = 0x3272;
const A_PLANE0_OFFSET: egl::Attrib = 0x3273;
const A_PLANE0_PITCH: egl::Attrib = 0x3274;
const A_PLANE0_MOD_LO: egl::Attrib = 0x3443;
const A_PLANE0_MOD_HI: egl::Attrib = 0x3444;
const A_PLANE3_FD: egl::Attrib = 0x3440;

// GLES
const GL_RENDERBUFFER: u32 = 0x8D41;
const GL_FRAMEBUFFER: u32 = 0x8D40;
const GL_COLOR_ATTACHMENT0: u32 = 0x8CE0;
const GL_FRAMEBUFFER_COMPLETE: u32 = 0x8CD5;
const GL_COLOR_BUFFER_BIT: u32 = 0x4000;

/// Loaded GLES entry points (mpv needs only these from us).
struct Gl {
    gen_renderbuffers: extern "system" fn(i32, *mut u32),
    delete_renderbuffers: extern "system" fn(i32, *const u32),
    bind_renderbuffer: extern "system" fn(u32, u32),
    image_target_renderbuffer: extern "system" fn(u32, *mut c_void),
    gen_framebuffers: extern "system" fn(i32, *mut u32),
    delete_framebuffers: extern "system" fn(i32, *const u32),
    bind_framebuffer: extern "system" fn(u32, u32),
    framebuffer_renderbuffer: extern "system" fn(u32, u32, u32, u32),
    check_framebuffer_status: extern "system" fn(u32) -> u32,
    clear_color: extern "system" fn(f32, f32, f32, f32),
    clear: extern "system" fn(u32),
    finish: extern "system" fn(),
}

/// The EGL instance remains valid for the lifetime of mpv's render context.
static EGL_PTR: AtomicPtr<Egl> = AtomicPtr::new(std::ptr::null_mut());

pub struct VideoGl {
    egl: Box<Egl>,
    display: egl::Display,
    context: egl::Context,
    gl: Gl,
    dma_buf_modifiers: bool,
}

impl VideoGl {
    pub fn new(alloc: &Allocator) -> Result<VideoGl, String> {
        let egl = unsafe { egl::DynamicInstance::<egl::EGL1_5>::load_required() }
            .map_err(|e| format!("load libEGL: {e}"))?;
        let egl = Box::new(egl);

        let display = unsafe {
            egl.get_platform_display(PLATFORM_GBM_KHR, alloc.device_ptr(), &[egl::ATTRIB_NONE])
        }
        .map_err(|e| format!("get_platform_display: {e}"))?;
        let (maj, min) = egl
            .initialize(display)
            .map_err(|e| format!("initialize: {e}"))?;

        let exts = egl
            .query_string(Some(display), EXTENSIONS)
            .map(|s| s.to_string_lossy().into_owned())
            .unwrap_or_default();
        let has_extension = |name| exts.split_ascii_whitespace().any(|ext| ext == name);
        let dma_buf_modifiers = has_extension("EGL_EXT_image_dma_buf_import_modifiers");
        println!(
            "EGL {maj}.{min}; dma_buf_import={}; dma_buf_modifiers={dma_buf_modifiers}",
            has_extension("EGL_EXT_image_dma_buf_import")
        );
        if !has_extension("EGL_EXT_image_dma_buf_import") {
            return Err("EGL_EXT_image_dma_buf_import missing".into());
        }

        egl.bind_api(egl::OPENGL_ES_API)
            .map_err(|e| format!("bind_api: {e}"))?;
        // Mesa/GBM exposes WINDOW_BIT configs; NVIDIA may expose PBUFFER_BIT.
        let mut config = None;
        for surf in [egl::WINDOW_BIT, egl::PBUFFER_BIT] {
            let mut configs = Vec::with_capacity(1);
            egl.choose_config(
                display,
                &[
                    egl::RENDERABLE_TYPE,
                    egl::OPENGL_ES2_BIT,
                    egl::SURFACE_TYPE,
                    surf,
                    egl::NONE,
                ],
                &mut configs,
            )
            .map_err(|e| format!("choose_config: {e}"))?;
            if let Some(c) = configs.first() {
                config = Some(*c);
                break;
            }
        }
        let config = config.ok_or("no ES2-renderable EGL config")?;

        let context = egl
            .create_context(display, config, None, &[egl::CONTEXT_MAJOR_VERSION, 3, egl::NONE])
            .map_err(|e| format!("create_context: {e}"))?;
        // Surfaceless: we only render into FBOs (EGL_KHR_surfaceless_context).
        egl.make_current(display, None, None, Some(context))
            .map_err(|e| format!("make_current: {e}"))?;

        EGL_PTR.store(egl.as_ref() as *const Egl as *mut Egl, Ordering::Release);
        let gl = Gl::load(&egl)?;

        Ok(VideoGl {
            egl,
            display,
            context,
            gl,
            dma_buf_modifiers,
        })
    }

    /// Imports all scanout memory planes as a renderbuffer-backed framebuffer.
    pub fn import(&self, s: &Scanout) -> Result<GlFbo, String> {
        let exported = s.export().map_err(|e| format!("dmabuf: {e}"))?;
        let fds = exported
            .planes
            .iter()
            .map(AsRawFd::as_raw_fd)
            .collect::<Vec<_>>();
        let attribs = dma_buf_attributes(&exported.layout, &fds, self.dma_buf_modifiers)?;
        let image = self
            .egl
            .create_image(
                self.display,
                unsafe { egl::Context::from_ptr(egl::NO_CONTEXT) },
                LINUX_DMA_BUF_EXT,
                unsafe { egl::ClientBuffer::from_ptr(std::ptr::null_mut()) },
                &attribs,
            )
            .map_err(|e| format!("create_image: {e}"))?;
        drop(exported); // EGL retains references to the imported dma-bufs.

        let g = &self.gl;
        let (mut rb, mut fbo) = (0u32, 0u32);
        (g.gen_renderbuffers)(1, &mut rb);
        (g.bind_renderbuffer)(GL_RENDERBUFFER, rb);
        (g.image_target_renderbuffer)(GL_RENDERBUFFER, image.as_ptr());
        (g.gen_framebuffers)(1, &mut fbo);
        (g.bind_framebuffer)(GL_FRAMEBUFFER, fbo);
        (g.framebuffer_renderbuffer)(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rb);
        let status = (g.check_framebuffer_status)(GL_FRAMEBUFFER);
        if status != GL_FRAMEBUFFER_COMPLETE {
            (g.delete_framebuffers)(1, &fbo);
            (g.delete_renderbuffers)(1, &rb);
            let _ = self.egl.destroy_image(self.display, image);
            return Err(format!("framebuffer incomplete: {status:#x}"));
        }
        Ok(GlFbo { image, rb, fbo })
    }

    /// Delete the FBO, renderbuffer and EGLImage of `f`.
    pub fn release(&self, f: GlFbo) {
        let g = &self.gl;
        (g.delete_framebuffers)(1, &f.fbo);
        (g.delete_renderbuffers)(1, &f.rb);
        let _ = self.egl.destroy_image(self.display, f.image);
    }

    pub fn fbo_id(&self, f: &GlFbo) -> i32 {
        f.fbo as i32
    }

    /// Fill `fbo` with opaque black and wait for completion.
    pub fn clear(&self, fbo: i32) {
        let g = &self.gl;
        (g.bind_framebuffer)(GL_FRAMEBUFFER, fbo as u32);
        (g.clear_color)(0.0, 0.0, 0.0, 1.0);
        (g.clear)(GL_COLOR_BUFFER_BIT);
        (g.finish)();
    }

    /// Completes queued GL rendering before KMS scanout.
    pub fn finish(&self) {
        (self.gl.finish)();
    }
}

impl Drop for VideoGl {
    fn drop(&mut self) {
        EGL_PTR.store(std::ptr::null_mut(), Ordering::Release);
        let _ = self.egl.make_current(self.display, None, None, None);
        let _ = self.egl.destroy_context(self.display, self.context);
        let _ = self.egl.terminate(self.display);
    }
}

/// A dmabuf wrapped as a GL FBO.
pub struct GlFbo {
    image: egl::Image,
    rb: u32,
    fbo: u32,
}

impl Gl {
    fn load(egl: &Egl) -> Result<Gl, String> {
        macro_rules! load {
            ($name:literal) => {{
                let f = egl
                    .get_proc_address($name)
                    .ok_or(concat!("missing GL function ", $name))?;
                unsafe { std::mem::transmute(f) }
            }};
        }
        Ok(Gl {
            gen_renderbuffers: load!("glGenRenderbuffers"),
            delete_renderbuffers: load!("glDeleteRenderbuffers"),
            bind_renderbuffer: load!("glBindRenderbuffer"),
            image_target_renderbuffer: load!("glEGLImageTargetRenderbufferStorageOES"),
            gen_framebuffers: load!("glGenFramebuffers"),
            delete_framebuffers: load!("glDeleteFramebuffers"),
            bind_framebuffer: load!("glBindFramebuffer"),
            framebuffer_renderbuffer: load!("glFramebufferRenderbuffer"),
            check_framebuffer_status: load!("glCheckFramebufferStatus"),
            clear_color: load!("glClearColor"),
            clear: load!("glClear"),
            finish: load!("glFinish"),
        })
    }
}

fn dma_buf_attributes(
    layout: &ScanoutLayout,
    fds: &[RawFd],
    modifiers_supported: bool,
) -> Result<Vec<egl::Attrib>, String> {
    validate_plane_count(layout.planes.len()).map_err(|e| e.to_string())?;
    if fds.len() != layout.planes.len() {
        return Err("EGL import: fd count differs from memory plane count".into());
    }
    if !modifiers_supported && (layout.modifier.is_some() || layout.planes.len() == 4) {
        return Err(
            "EGL import requires EGL_EXT_image_dma_buf_import_modifiers for this scanout layout"
                .into(),
        );
    }
    let mut attrs = vec![
        A_WIDTH,
        layout.width as egl::Attrib,
        A_HEIGHT,
        layout.height as egl::Attrib,
        A_FOURCC,
        layout.fourcc as egl::Attrib,
    ];
    for (i, (plane, fd)) in layout.planes.iter().zip(fds).enumerate() {
        // EXT_image_dma_buf_import_modifiers assigns a separate range to plane 3.
        let base = if i == 3 {
            A_PLANE3_FD
        } else {
            A_PLANE0_FD + 3 * i as egl::Attrib
        };
        attrs.extend_from_slice(&[
            base,
            *fd as egl::Attrib,
            base + (A_PLANE0_OFFSET - A_PLANE0_FD),
            plane.offset as egl::Attrib,
            base + (A_PLANE0_PITCH - A_PLANE0_FD),
            plane.pitch as egl::Attrib,
        ]);
        if let Some(modifier) = layout.modifier {
            attrs.extend_from_slice(&[
                A_PLANE0_MOD_LO + 2 * i as egl::Attrib,
                (modifier & 0xffff_ffff) as egl::Attrib,
                A_PLANE0_MOD_HI + 2 * i as egl::Attrib,
                (modifier >> 32) as egl::Attrib,
            ]);
        }
    }
    attrs.push(egl::ATTRIB_NONE);
    Ok(attrs)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::gbm::PlaneLayout;

    fn layout(modifier: Option<u64>, count: usize) -> ScanoutLayout {
        ScanoutLayout {
            width: 1920,
            height: 1080,
            fourcc: drm::buffer::DrmFourcc::Xrgb8888 as u32,
            modifier,
            planes: (0..count)
                .map(|i| PlaneLayout {
                    offset: i as u32 * 8192,
                    pitch: 7680 >> i,
                })
                .collect(),
        }
    }

    fn value(attrs: &[egl::Attrib], key: egl::Attrib) -> Option<egl::Attrib> {
        attrs[..attrs.len() - 1]
            .chunks_exact(2)
            .find_map(|p| (p[0] == key).then_some(p[1]))
    }

    #[test]
    fn dcc_auxiliary_planes_keep_their_own_fd_offset_and_pitch() {
        let layout = layout(Some(0x0200_0000_1234_5678), 3);
        let attrs = dma_buf_attributes(&layout, &[10, 11, 12], true).unwrap();
        for (i, plane) in layout.planes.iter().enumerate() {
            let i = i as egl::Attrib;
            assert_eq!(value(&attrs, A_PLANE0_FD + i * 3), Some(10 + i));
            assert_eq!(value(&attrs, A_PLANE0_OFFSET + i * 3), Some(plane.offset as _));
            assert_eq!(value(&attrs, A_PLANE0_PITCH + i * 3), Some(plane.pitch as _));
            assert_eq!(value(&attrs, A_PLANE0_MOD_LO + i * 2), Some(0x1234_5678));
            assert_eq!(value(&attrs, A_PLANE0_MOD_HI + i * 2), Some(0x0200_0000));
        }
        assert_eq!(attrs.last(), Some(&egl::ATTRIB_NONE));
    }

    #[test]
    fn fourth_plane_uses_extension_tokens() {
        let attrs = dma_buf_attributes(&layout(Some(0), 4), &[10, 11, 12, 13], true).unwrap();
        assert_eq!(value(&attrs, A_PLANE3_FD), Some(13));
        assert_eq!(value(&attrs, A_PLANE3_FD + 1), Some(3 * 8192));
        assert_eq!(value(&attrs, 0x3449), Some(0));
        assert_eq!(value(&attrs, 0x344a), Some(0));
    }

    #[test]
    fn implicit_layout_omits_modifier_attributes() {
        for supported in [false, true] {
            let attrs = dma_buf_attributes(&layout(None, 1), &[10], supported).unwrap();
            assert_eq!(value(&attrs, A_PLANE0_MOD_LO), None);
            assert_eq!(value(&attrs, A_PLANE0_MOD_HI), None);
        }
    }

    #[test]
    fn explicit_layout_requires_modifiers_extension() {
        assert!(dma_buf_attributes(&layout(Some(0), 1), &[10], false).is_err());
        assert!(dma_buf_attributes(&layout(None, 4), &[10, 11, 12, 13], false).is_err());
        assert!(dma_buf_attributes(&layout(Some(0), 2), &[10], true).is_err());
    }
}

/// get_proc_address for mpv's OpenGL render API. Resolves GL symbols via EGL.
pub extern "C" fn mpv_get_proc_address(_ctx: *mut c_void, name: *const c_char) -> *mut c_void {
    let egl = EGL_PTR.load(Ordering::Acquire);
    if egl.is_null() || name.is_null() {
        return std::ptr::null_mut();
    }
    let name = unsafe { CStr::from_ptr(name) };
    let Ok(name) = name.to_str() else {
        return std::ptr::null_mut();
    };
    match unsafe { &*egl }.get_proc_address(name) {
        Some(f) => f as usize as *mut c_void,
        None => std::ptr::null_mut(),
    }
}