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>;
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;
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;
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(),
}
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}"))?;
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}"))?;
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)?;
if let Some(f) = egl.get_proc_address("glGetString") {
let get_string: extern "system" fn(u32) -> *const c_char = unsafe { std::mem::transmute(f) };
let s = |name| {
let p = get_string(name);
if p.is_null() { String::new() } else { unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned() }
};
println!("GL: {}; {}", s(0x1F01), s(0x1F02)); }
Ok(VideoGl {
egl,
display,
context,
gl,
dma_buf_modifiers,
})
}
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);
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 })
}
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
}
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)();
}
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);
}
}
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() {
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());
}
}
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(),
}
}