use std::ffi::{CString, c_char, c_int, c_void};
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
use super::{ExportBackend, ExportBuffer};
use crate::error::{Error, Result};
#[repr(C)]
struct GbmDevice {
_opaque: [u8; 0],
}
#[repr(C)]
struct GbmBo {
_opaque: [u8; 0],
}
const GBM_USE_RENDERING: u32 = 1 << 2;
const GBM_USE_LINEAR: u32 = 1 << 4;
#[link(name = "gbm")]
unsafe extern "C" {
fn gbm_create_device(fd: c_int) -> *mut GbmDevice;
fn gbm_device_destroy(gbm: *mut GbmDevice);
fn gbm_bo_create(
gbm: *mut GbmDevice,
width: u32,
height: u32,
format: u32,
flags: u32,
) -> *mut GbmBo;
fn gbm_bo_destroy(bo: *mut GbmBo);
fn gbm_bo_get_fd(bo: *mut GbmBo) -> c_int;
fn gbm_bo_get_stride(bo: *mut GbmBo) -> u32;
}
type EGLDisplay = *mut c_void;
type EGLConfig = *mut c_void;
type EGLContext = *mut c_void;
type EGLSurface = *mut c_void;
type EGLImage = *mut c_void;
type EGLBoolean = u32;
type EGLint = i32;
type EGLenum = u32;
type EGLAttrib = isize;
const EGL_PLATFORM_GBM: EGLenum = 0x31D7;
const EGL_OPENGL_API: EGLenum = 0x30A2;
const EGL_OPENGL_ES_API: EGLenum = 0x30A0;
const EGL_OPENGL_BIT: EGLint = 0x0008;
const EGL_OPENGL_ES2_BIT: EGLint = 0x0004;
const EGL_SURFACE_TYPE: EGLint = 0x3033;
const EGL_RENDERABLE_TYPE: EGLint = 0x3040;
const EGL_CONTEXT_CLIENT_VERSION: EGLint = 0x3098;
const EGL_EXTENSIONS: EGLint = 0x3055;
const EGL_NONE: EGLint = 0x3038;
const EGL_WIDTH: EGLint = 0x3057;
const EGL_HEIGHT: EGLint = 0x3056;
const EGL_LINUX_DMA_BUF: EGLenum = 0x3270;
const EGL_LINUX_DRM_FOURCC: EGLint = 0x3271;
const EGL_DMA_BUF_PLANE0_FD: EGLint = 0x3272;
const EGL_DMA_BUF_PLANE0_OFFSET: EGLint = 0x3273;
const EGL_DMA_BUF_PLANE0_PITCH: EGLint = 0x3274;
const EGL_NO_DISPLAY: EGLDisplay = std::ptr::null_mut();
const EGL_NO_CONTEXT: EGLContext = std::ptr::null_mut();
const EGL_NO_SURFACE: EGLSurface = std::ptr::null_mut();
const EGL_NO_CONFIG: EGLConfig = std::ptr::null_mut();
#[link(name = "EGL")]
unsafe extern "C" {
fn eglGetPlatformDisplay(
platform: EGLenum,
native_display: *mut c_void,
attrib_list: *const EGLAttrib,
) -> EGLDisplay;
fn eglInitialize(dpy: EGLDisplay, major: *mut EGLint, minor: *mut EGLint) -> EGLBoolean;
fn eglTerminate(dpy: EGLDisplay) -> EGLBoolean;
fn eglBindAPI(api: EGLenum) -> EGLBoolean;
fn eglChooseConfig(
dpy: EGLDisplay,
attribs: *const EGLint,
configs: *mut EGLConfig,
config_size: EGLint,
num_config: *mut EGLint,
) -> EGLBoolean;
fn eglCreateContext(
dpy: EGLDisplay,
config: EGLConfig,
share: EGLContext,
attribs: *const EGLint,
) -> EGLContext;
fn eglDestroyContext(dpy: EGLDisplay, ctx: EGLContext) -> EGLBoolean;
fn eglMakeCurrent(
dpy: EGLDisplay,
draw: EGLSurface,
read: EGLSurface,
ctx: EGLContext,
) -> EGLBoolean;
fn eglGetError() -> EGLint;
fn eglQueryString(dpy: EGLDisplay, name: EGLint) -> *const c_char;
fn eglGetProcAddress(name: *const c_char) -> *mut c_void;
}
type PfnEglCreateImage =
unsafe extern "C" fn(EGLDisplay, EGLContext, EGLenum, *mut c_void, *const EGLint) -> EGLImage;
type PfnEglDestroyImage = unsafe extern "C" fn(EGLDisplay, EGLImage) -> EGLBoolean;
use super::gl_consts::{
GL_COLOR_ATTACHMENT0, GL_FRAMEBUFFER, GL_FRAMEBUFFER_COMPLETE, GL_NEAREST, GL_RGBA8,
GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_TEXTURE_MIN_FILTER,
};
const GL_NO_ERROR: u32 = 0;
pub(crate) const FOURCC_ARGB8888: u32 = 0x3432_5241;
pub(crate) const FOURCC_XRGB8888: u32 = 0x3432_5258;
pub(crate) const MODIFIER_LINEAR: u64 = 0;
#[allow(non_snake_case)]
struct GlFns {
GenTextures: unsafe extern "C" fn(i32, *mut u32),
DeleteTextures: unsafe extern "C" fn(i32, *const u32),
BindTexture: unsafe extern "C" fn(u32, u32),
TexParameteri: unsafe extern "C" fn(u32, u32, i32),
EGLImageTargetTexture2DOES: unsafe extern "C" fn(u32, *mut c_void),
GenFramebuffers: unsafe extern "C" fn(i32, *mut u32),
DeleteFramebuffers: unsafe extern "C" fn(i32, *const u32),
BindFramebuffer: unsafe extern "C" fn(u32, u32),
FramebufferTexture2D: unsafe extern "C" fn(u32, u32, u32, u32, i32),
CheckFramebufferStatus: unsafe extern "C" fn(u32) -> u32,
GetError: unsafe extern "C" fn() -> u32,
Finish: unsafe extern "C" fn(),
}
fn egl_error(what: &str) -> Error {
let code = unsafe { eglGetError() };
Error::ExportSetup(format!("{what}: EGL error {code:#06x}"))
}
macro_rules! load_fn {
($name:literal as $ty:ty) => {{
let ptr = gl_proc_address($name);
if ptr.is_null() {
return Err(Error::ExportSetup(
concat!("required GL/EGL entry point missing: ", $name).into(),
));
}
unsafe { std::mem::transmute::<*mut c_void, $ty>(ptr) }
}};
}
fn open_node(minor: u32) -> Option<OwnedFd> {
let path = format!("/dev/dri/renderD{minor}\0");
let fd = unsafe {
libc::open(
path.as_ptr().cast::<c_char>(),
libc::O_RDWR | libc::O_CLOEXEC,
)
};
if fd < 0 {
return None;
}
Some(unsafe { OwnedFd::from_raw_fd(fd) })
}
struct GbmHandle(*mut GbmDevice);
impl Drop for GbmHandle {
fn drop(&mut self) {
unsafe { gbm_device_destroy(self.0) };
}
}
struct DisplayHandle(EGLDisplay);
impl Drop for DisplayHandle {
fn drop(&mut self) {
unsafe { eglTerminate(self.0) };
}
}
fn display_has_extension(display: EGLDisplay, name: &str) -> bool {
let exts = unsafe { eglQueryString(display, EGL_EXTENSIONS) };
if exts.is_null() {
return false;
}
let exts = unsafe { std::ffi::CStr::from_ptr(exts) }.to_string_lossy();
exts.split_ascii_whitespace().any(|e| e == name)
}
fn create_context(display: EGLDisplay, no_config: bool) -> Result<EGLContext> {
const ATTEMPTS: [(EGLenum, EGLint, &[EGLint]); 3] = [
(EGL_OPENGL_API, EGL_OPENGL_BIT, &[EGL_NONE]),
(
EGL_OPENGL_ES_API,
EGL_OPENGL_ES2_BIT,
&[EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE],
),
(
EGL_OPENGL_ES_API,
EGL_OPENGL_ES2_BIT,
&[EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE],
),
];
for (api, renderable_bit, ctx_attribs) in ATTEMPTS {
if unsafe { eglBindAPI(api) } == 0 {
continue;
}
let config = if no_config {
EGL_NO_CONFIG
} else {
let attribs = [
EGL_SURFACE_TYPE,
0,
EGL_RENDERABLE_TYPE,
renderable_bit,
EGL_NONE,
];
let mut config: EGLConfig = std::ptr::null_mut();
let mut count: EGLint = 0;
let ok =
unsafe { eglChooseConfig(display, attribs.as_ptr(), &mut config, 1, &mut count) };
if ok == 0 || count < 1 {
continue;
}
config
};
let ctx =
unsafe { eglCreateContext(display, config, EGL_NO_CONTEXT, ctx_attribs.as_ptr()) };
if !ctx.is_null() {
return Ok(ctx);
}
}
Err(egl_error("eglCreateContext (GL, GLES3, GLES2 all refused)"))
}
pub(crate) struct GlContext {
context: EGLContext,
display: DisplayHandle,
gbm: GbmHandle,
_drm: OwnedFd,
egl_create_image: PfnEglCreateImage,
egl_destroy_image: PfnEglDestroyImage,
fns: GlFns,
}
impl GlContext {
fn on_node(drm: OwnedFd) -> Result<Self> {
let gbm = unsafe { gbm_create_device(drm.as_raw_fd()) };
if gbm.is_null() {
return Err(Error::ExportSetup(
"gbm_create_device failed on the render node".into(),
));
}
let gbm = GbmHandle(gbm);
let display = unsafe {
eglGetPlatformDisplay(EGL_PLATFORM_GBM, gbm.0.cast::<c_void>(), std::ptr::null())
};
if display == EGL_NO_DISPLAY {
return Err(egl_error("eglGetPlatformDisplay(GBM)"));
}
if unsafe { eglInitialize(display, std::ptr::null_mut(), std::ptr::null_mut()) } == 0 {
return Err(egl_error("eglInitialize"));
}
let display = DisplayHandle(display);
for required in [
"EGL_KHR_image_base",
"EGL_EXT_image_dma_buf_import",
"EGL_KHR_surfaceless_context",
] {
if !display_has_extension(display.0, required) {
return Err(Error::ExportSetup(format!(
"EGL display lacks {required} (driver too old for DMA-BUF export)"
)));
}
}
let egl_create_image = load_fn!("eglCreateImageKHR" as PfnEglCreateImage);
let egl_destroy_image = load_fn!("eglDestroyImageKHR" as PfnEglDestroyImage);
let fns = GlFns {
GenTextures: load_fn!("glGenTextures" as unsafe extern "C" fn(i32, *mut u32)),
DeleteTextures: load_fn!("glDeleteTextures" as unsafe extern "C" fn(i32, *const u32)),
BindTexture: load_fn!("glBindTexture" as unsafe extern "C" fn(u32, u32)),
TexParameteri: load_fn!("glTexParameteri" as unsafe extern "C" fn(u32, u32, i32)),
EGLImageTargetTexture2DOES: load_fn!(
"glEGLImageTargetTexture2DOES" as unsafe extern "C" fn(u32, *mut c_void)
),
GenFramebuffers: load_fn!("glGenFramebuffers" as unsafe extern "C" fn(i32, *mut u32)),
DeleteFramebuffers: load_fn!(
"glDeleteFramebuffers" as unsafe extern "C" fn(i32, *const u32)
),
BindFramebuffer: load_fn!("glBindFramebuffer" as unsafe extern "C" fn(u32, u32)),
FramebufferTexture2D: load_fn!(
"glFramebufferTexture2D" as unsafe extern "C" fn(u32, u32, u32, u32, i32)
),
CheckFramebufferStatus: load_fn!(
"glCheckFramebufferStatus" as unsafe extern "C" fn(u32) -> u32
),
GetError: load_fn!("glGetError" as unsafe extern "C" fn() -> u32),
Finish: load_fn!("glFinish" as unsafe extern "C" fn()),
};
let no_config = display_has_extension(display.0, "EGL_KHR_no_config_context");
let context = create_context(display.0, no_config)?;
Ok(Self {
context,
display,
gbm,
_drm: drm,
egl_create_image,
egl_destroy_image,
fns,
})
}
}
impl ExportBackend for GlContext {
type Buffer = SurfaceBuffer;
const FBO_INTERNAL_FORMAT: i32 = GL_RGBA8 as i32;
fn new() -> Result<Self> {
let mut last_error = None;
for minor in 128..192 {
let Some(drm) = open_node(minor) else {
continue;
};
match Self::on_node(drm) {
Ok(gl) => return Ok(gl),
Err(e) => {
tracing::debug!("export: render node renderD{minor} unusable: {e}");
last_error = Some(e);
}
}
}
Err(match last_error {
Some(Error::ExportSetup(msg)) => {
Error::ExportSetup(format!("no usable DRM render node (last tried: {msg})"))
}
Some(e) => e,
None => Error::ExportSetup(
"no DRM render node under /dev/dri (no GPU, or no permission — user not in the render/video group?)"
.into(),
),
})
}
fn make_current(&self) -> Result<()> {
let ok =
unsafe { eglMakeCurrent(self.display.0, EGL_NO_SURFACE, EGL_NO_SURFACE, self.context) };
if ok == 0 {
return Err(egl_error("eglMakeCurrent (surfaceless)"));
}
Ok(())
}
fn begin_render(&self, _buffer: &SurfaceBuffer) {}
fn publish_barrier(&self, _buffer: &SurfaceBuffer) {
unsafe { (self.fns.Finish)() };
}
fn proc_address(name: &str) -> *mut c_void {
gl_proc_address(name)
}
}
impl Drop for GlContext {
fn drop(&mut self) {
unsafe {
eglMakeCurrent(
self.display.0,
EGL_NO_SURFACE,
EGL_NO_SURFACE,
EGL_NO_CONTEXT,
);
eglDestroyContext(self.display.0, self.context);
}
}
}
pub(crate) struct SurfaceBuffer {
fd: OwnedFd,
image: EGLImage,
texture: u32,
fbo: u32,
stride: u32,
fourcc: u32,
width: u32,
height: u32,
}
unsafe impl Send for SurfaceBuffer {}
unsafe impl Sync for SurfaceBuffer {}
fn create_bo(gbm: *mut GbmDevice, width: u32, height: u32) -> Result<(*mut GbmBo, u32)> {
for fourcc in [FOURCC_ARGB8888, FOURCC_XRGB8888] {
let bo = unsafe {
gbm_bo_create(
gbm,
width,
height,
fourcc,
GBM_USE_RENDERING | GBM_USE_LINEAR,
)
};
if !bo.is_null() {
return Ok((bo, fourcc));
}
}
Err(Error::ExportSetup(format!(
"gbm_bo_create failed for {width}x{height} linear AR24/XR24"
)))
}
impl ExportBuffer for SurfaceBuffer {
type Backend = GlContext;
fn new(gl: &GlContext, width: u32, height: u32) -> Result<Self> {
let (bo, fourcc) = create_bo(gl.gbm.0, width, height)?;
let raw_fd = unsafe { gbm_bo_get_fd(bo) };
if raw_fd < 0 {
unsafe { gbm_bo_destroy(bo) };
return Err(Error::ExportSetup("gbm_bo_get_fd failed".into()));
}
let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
let stride = unsafe { gbm_bo_get_stride(bo) };
let attribs = [
EGL_WIDTH,
width as EGLint,
EGL_HEIGHT,
height as EGLint,
EGL_LINUX_DRM_FOURCC,
fourcc as EGLint,
EGL_DMA_BUF_PLANE0_FD,
fd.as_raw_fd(),
EGL_DMA_BUF_PLANE0_OFFSET,
0,
EGL_DMA_BUF_PLANE0_PITCH,
stride as EGLint,
EGL_NONE,
];
let image = unsafe {
(gl.egl_create_image)(
gl.display.0,
EGL_NO_CONTEXT,
EGL_LINUX_DMA_BUF,
std::ptr::null_mut(),
attribs.as_ptr(),
)
};
unsafe { gbm_bo_destroy(bo) };
if image.is_null() {
return Err(egl_error("eglCreateImageKHR(DMA_BUF)"));
}
let f = &gl.fns;
let mut texture: u32 = 0;
unsafe {
(f.GetError)(); (f.GenTextures)(1, &mut texture);
(f.BindTexture)(GL_TEXTURE_2D, texture);
(f.EGLImageTargetTexture2DOES)(GL_TEXTURE_2D, image);
}
let bind_error = unsafe { (f.GetError)() };
unsafe {
(f.TexParameteri)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
(f.TexParameteri)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
(f.BindTexture)(GL_TEXTURE_2D, 0);
}
if bind_error != GL_NO_ERROR {
unsafe {
(f.DeleteTextures)(1, &texture);
(gl.egl_destroy_image)(gl.display.0, image);
}
return Err(Error::ExportSetup(format!(
"glEGLImageTargetTexture2DOES failed (GL error {bind_error:#06x})"
)));
}
let mut fbo: u32 = 0;
let status = unsafe {
(f.GenFramebuffers)(1, &mut fbo);
(f.BindFramebuffer)(GL_FRAMEBUFFER, fbo);
(f.FramebufferTexture2D)(
GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
texture,
0,
);
let status = (f.CheckFramebufferStatus)(GL_FRAMEBUFFER);
(f.BindFramebuffer)(GL_FRAMEBUFFER, 0);
status
};
if status != GL_FRAMEBUFFER_COMPLETE {
unsafe {
(f.DeleteFramebuffers)(1, &fbo);
(f.DeleteTextures)(1, &texture);
(gl.egl_destroy_image)(gl.display.0, image);
}
return Err(Error::ExportSetup(format!(
"DMA-BUF framebuffer incomplete (status {status:#x})"
)));
}
Ok(Self {
fd,
image,
texture,
fbo,
stride,
fourcc,
width,
height,
})
}
fn fbo(&self) -> u32 {
self.fbo
}
fn size(&self) -> (u32, u32) {
(self.width, self.height)
}
fn delete_gl(&mut self, gl: &GlContext) {
let f = &gl.fns;
unsafe {
if self.fbo != 0 {
(f.DeleteFramebuffers)(1, &self.fbo);
self.fbo = 0;
}
if self.texture != 0 {
(f.DeleteTextures)(1, &self.texture);
self.texture = 0;
}
if !self.image.is_null() {
(gl.egl_destroy_image)(gl.display.0, self.image);
self.image = std::ptr::null_mut();
}
}
}
fn copy_pixels(&self) -> Vec<u8> {
let (w, h) = (self.width as usize, self.height as usize);
let stride = self.stride as usize;
let len = stride * h;
if len == 0 {
return vec![0u8; w * h * 4];
}
unsafe {
let base = libc::mmap(
std::ptr::null_mut(),
len,
libc::PROT_READ,
libc::MAP_SHARED,
self.fd.as_raw_fd(),
0,
);
if base == libc::MAP_FAILED {
return vec![0u8; w * h * 4];
}
dma_buf_sync(&self.fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ);
let out = super::pack_bgra_rows(base.cast::<u8>(), stride, w, h);
dma_buf_sync(&self.fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ);
libc::munmap(base, len);
out
}
}
}
fn gl_proc_address(name: &str) -> *mut c_void {
let Ok(cname) = CString::new(name) else {
return std::ptr::null_mut();
};
let ptr = unsafe { eglGetProcAddress(cname.as_ptr()) };
if !ptr.is_null() {
return ptr;
}
unsafe { libc::dlsym(libc::RTLD_DEFAULT, cname.as_ptr()) }
}
impl SurfaceBuffer {
pub(crate) fn dma_buf_fd(&self) -> BorrowedFd<'_> {
self.fd.as_fd()
}
pub(crate) fn stride(&self) -> u32 {
self.stride
}
pub(crate) fn fourcc(&self) -> u32 {
self.fourcc
}
}
const DMA_BUF_SYNC_READ: u64 = 1 << 0;
const DMA_BUF_SYNC_START: u64 = 0;
const DMA_BUF_SYNC_END: u64 = 1 << 2;
const DMA_BUF_IOCTL_SYNC: libc::c_ulong = 0x4008_6200;
fn dma_buf_sync(fd: &OwnedFd, flags: u64) {
let sync = flags;
unsafe {
libc::ioctl(fd.as_raw_fd(), DMA_BUF_IOCTL_SYNC, &raw const sync);
}
}