#![allow(unsafe_code)]
#[cfg(target_os = "linux")]
use std::ffi::c_void;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DrmError {
#[error("DRM not supported on this platform")]
NotSupported,
#[error("libdrm.so not found")]
LibraryNotFound,
#[error("DRM `{op}` failed: {detail}")]
CallFailed { op: &'static str, detail: String },
#[error("connector `{0}` not found on this device")]
ConnectorNotFound(String),
#[error("mode `{0}` not supported by connector")]
ModeNotSupported(String),
#[error("no CRTC available for connector `{0}`")]
NoCrtc(String),
#[error("invalid parameter: {0}")]
InvalidParameter(String),
}
pub type DrmResult<T> = Result<T, DrmError>;
pub fn drm_available() -> bool {
#[cfg(target_os = "linux")]
{
let h = unsafe { libc::dlopen(c"libdrm.so.2".as_ptr(), libc::RTLD_LAZY) };
if h.is_null() {
let h2 = unsafe { libc::dlopen(c"libdrm.so".as_ptr(), libc::RTLD_LAZY) };
if h2.is_null() {
return false;
}
unsafe { libc::dlclose(h2) };
return true;
}
unsafe { libc::dlclose(h) };
true
}
#[cfg(not(target_os = "linux"))]
false
}
#[cfg(target_os = "linux")]
const DRM_DISPLAY_MODE_LEN: usize = 32;
#[cfg(target_os = "linux")]
const DRM_MODE_CONNECTED: u32 = 1;
#[cfg(target_os = "linux")]
const DRM_MODE_PAGE_FLIP_EVENT: u32 = 0x01;
pub mod fourcc {
pub const NV12: u32 = u32::from_le_bytes(*b"NV12");
pub const XRGB8888: u32 = u32::from_le_bytes(*b"XR24");
pub const RGB888: u32 = u32::from_le_bytes(*b"RG24");
pub const YUYV: u32 = u32::from_le_bytes(*b"YUYV");
}
#[cfg(target_os = "linux")]
#[repr(C)]
struct DrmModeRes {
count_fbs: i32,
fbs: *const u32,
count_crtcs: i32,
crtcs: *const u32,
count_connectors: i32,
connectors: *const u32,
count_encoders: i32,
encoders: *const u32,
min_width: u32,
max_width: u32,
min_height: u32,
max_height: u32,
}
#[cfg(target_os = "linux")]
#[repr(C)]
#[derive(Clone, Copy)]
struct DrmModeModeInfo {
clock: u32,
hdisplay: u16,
hsync_start: u16,
hsync_end: u16,
htotal: u16,
hskew: u16,
vdisplay: u16,
vsync_start: u16,
vsync_end: u16,
vtotal: u16,
vscan: u16,
vrefresh: u32,
flags: u32,
typ: u32,
name: [u8; DRM_DISPLAY_MODE_LEN],
}
#[cfg(target_os = "linux")]
#[repr(C)]
struct DrmModeConnector {
connector_id: u32,
encoder_id: u32,
connector_type: u32,
connector_type_id: u32,
connection: u32,
mm_width: u32,
mm_height: u32,
subpixel: u32,
count_modes: i32,
modes: *const DrmModeModeInfo,
count_props: i32,
props: *const u32,
prop_values: *const u64,
count_encoders: i32,
encoders: *const u32,
}
#[cfg(target_os = "linux")]
#[repr(C)]
struct DrmModeEncoder {
encoder_id: u32,
encoder_type: u32,
crtc_id: u32,
possible_crtcs: u32,
possible_clones: u32,
}
#[cfg(target_os = "linux")]
type FnDrmOpen = unsafe extern "C" fn(*const u8, *const u8) -> i32;
#[cfg(target_os = "linux")]
type FnDrmClose = unsafe extern "C" fn(i32) -> i32;
#[cfg(target_os = "linux")]
type FnDrmModeGetResources = unsafe extern "C" fn(i32) -> *mut DrmModeRes;
#[cfg(target_os = "linux")]
type FnDrmModeFreeResources = unsafe extern "C" fn(*mut DrmModeRes);
#[cfg(target_os = "linux")]
type FnDrmModeGetConnector = unsafe extern "C" fn(i32, u32) -> *mut DrmModeConnector;
#[cfg(target_os = "linux")]
type FnDrmModeFreeConnector = unsafe extern "C" fn(*mut DrmModeConnector);
#[cfg(target_os = "linux")]
type FnDrmModeGetEncoder = unsafe extern "C" fn(i32, u32) -> *mut DrmModeEncoder;
#[cfg(target_os = "linux")]
type FnDrmModeFreeEncoder = unsafe extern "C" fn(*mut DrmModeEncoder);
#[cfg(target_os = "linux")]
type FnDrmModeAddFB = unsafe extern "C" fn(i32, u32, u32, u8, u8, u32, u32, *mut u32) -> i32;
#[cfg(target_os = "linux")]
type FnDrmModeAddFB2 = unsafe extern "C" fn(
i32,
u32, u32, u32, *const u32, *const u32, *const u32, *mut u32, u32, ) -> i32;
#[cfg(target_os = "linux")]
type FnDrmModeRmFB = unsafe extern "C" fn(i32, u32) -> i32;
#[cfg(target_os = "linux")]
type FnDrmModeSetCrtc = unsafe extern "C" fn(
i32, u32, u32, u32, u32, *const u32, i32, *const DrmModeModeInfo, ) -> i32;
#[cfg(target_os = "linux")]
type FnDrmModePageFlip = unsafe extern "C" fn(i32, u32, u32, u32, *mut c_void) -> i32;
#[cfg(target_os = "linux")]
type FnDrmPrimeFDToHandle = unsafe extern "C" fn(i32, i32, *mut u32) -> i32;
#[cfg(target_os = "linux")]
struct DrmLib {
handle: *mut c_void,
drm_open: FnDrmOpen,
drm_close: FnDrmClose,
get_resources: FnDrmModeGetResources,
free_resources: FnDrmModeFreeResources,
get_connector: FnDrmModeGetConnector,
free_connector: FnDrmModeFreeConnector,
get_encoder: FnDrmModeGetEncoder,
free_encoder: FnDrmModeFreeEncoder,
add_fb: FnDrmModeAddFB,
add_fb2: Option<FnDrmModeAddFB2>,
rm_fb: FnDrmModeRmFB,
set_crtc: FnDrmModeSetCrtc,
page_flip: FnDrmModePageFlip,
prime_fd_to_handle: FnDrmPrimeFDToHandle,
}
#[cfg(target_os = "linux")]
unsafe impl Send for DrmLib {}
#[cfg(target_os = "linux")]
unsafe impl Sync for DrmLib {}
#[cfg(target_os = "linux")]
impl Drop for DrmLib {
fn drop(&mut self) {
if !self.handle.is_null() {
unsafe { libc::dlclose(self.handle) };
}
}
}
#[cfg(target_os = "linux")]
fn load_drm_lib() -> DrmResult<Arc<DrmLib>> {
let mut h = unsafe { libc::dlopen(c"libdrm.so.2".as_ptr(), libc::RTLD_NOW) };
if h.is_null() {
h = unsafe { libc::dlopen(c"libdrm.so".as_ptr(), libc::RTLD_NOW) };
}
if h.is_null() {
return Err(DrmError::LibraryNotFound);
}
macro_rules! sym {
($name:expr, $ty:ty) => {{
let p = unsafe { libc::dlsym(h, $name.as_ptr().cast()) };
if p.is_null() {
unsafe { libc::dlclose(h) };
return Err(DrmError::CallFailed {
op: stringify!($name),
detail: "symbol not found in libdrm".into(),
});
}
unsafe { std::mem::transmute_copy::<*mut c_void, $ty>(&p) }
}};
}
macro_rules! sym_opt {
($name:expr, $ty:ty) => {{
let p = unsafe { libc::dlsym(h, $name.as_ptr().cast()) };
if p.is_null() {
None
} else {
Some(unsafe { std::mem::transmute_copy::<*mut c_void, $ty>(&p) })
}
}};
}
let lib = DrmLib {
handle: h,
drm_open: sym!(b"drmOpen\0", FnDrmOpen),
drm_close: sym!(b"drmClose\0", FnDrmClose),
get_resources: sym!(b"drmModeGetResources\0", FnDrmModeGetResources),
free_resources: sym!(b"drmModeFreeResources\0", FnDrmModeFreeResources),
get_connector: sym!(b"drmModeGetConnector\0", FnDrmModeGetConnector),
free_connector: sym!(b"drmModeFreeConnector\0", FnDrmModeFreeConnector),
get_encoder: sym!(b"drmModeGetEncoder\0", FnDrmModeGetEncoder),
free_encoder: sym!(b"drmModeFreeEncoder\0", FnDrmModeFreeEncoder),
add_fb: sym!(b"drmModeAddFB\0", FnDrmModeAddFB),
add_fb2: sym_opt!(b"drmModeAddFB2\0", FnDrmModeAddFB2),
rm_fb: sym!(b"drmModeRmFB\0", FnDrmModeRmFB),
set_crtc: sym!(b"drmModeSetCrtc\0", FnDrmModeSetCrtc),
page_flip: sym!(b"drmModePageFlip\0", FnDrmModePageFlip),
prime_fd_to_handle: sym!(b"drmPrimeFDToHandle\0", FnDrmPrimeFDToHandle),
};
Ok(Arc::new(lib))
}
#[cfg(not(target_os = "linux"))]
struct DrmLib;
#[cfg(not(target_os = "linux"))]
fn load_drm_lib() -> DrmResult<Arc<DrmLib>> {
Err(DrmError::NotSupported)
}
#[cfg(target_os = "linux")]
fn connector_type_name(typ: u32) -> &'static str {
match typ {
1 => "VGA",
2 => "DVI-I",
3 => "DVI-D",
4 => "DVI-A",
5 => "Composite",
6 => "SVIDEO",
7 => "LVDS",
8 => "Component",
9 => "DIN",
10 => "DP",
11 => "HDMI-A",
12 => "HDMI-B",
13 => "TV",
14 => "eDP",
15 => "Virtual",
16 => "DSI",
17 => "DPI",
_ => "Unknown",
}
}
#[cfg(target_os = "linux")]
fn mode_matches(mode: &DrmModeModeInfo, label: &str) -> bool {
let name_end = mode
.name
.iter()
.position(|&b| b == 0)
.unwrap_or(mode.name.len());
let name = std::str::from_utf8(&mode.name[..name_end]).unwrap_or("");
if name == label {
return true;
}
if let Some((w, h)) = label.split_once('x')
&& let (Ok(w), Ok(h)) = (w.parse::<u16>(), h.parse::<u16>())
{
return mode.hdisplay == w && mode.vdisplay == h;
}
if let Some((h, r)) = label.split_once('p')
&& let (Ok(h), Ok(r)) = (h.parse::<u16>(), r.parse::<u32>())
{
return mode.vdisplay == h && mode.vrefresh == r;
}
false
}
pub struct DrmOutput {
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
lib: Arc<DrmLib>,
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fd: i32,
width: u32,
height: u32,
pixel_format: u32,
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
current_fb_id: u32,
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
previous_fb_id: u32,
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
crtc_id: u32,
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
connector_id: u32,
#[cfg(target_os = "linux")]
mode: Option<DrmModeModeInfo>,
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
mode_set: bool,
}
impl DrmOutput {
pub fn open(width: u32, height: u32, pixel_format: u32) -> DrmResult<Self> {
if width == 0 || height == 0 {
return Err(DrmError::InvalidParameter(
"width/height must be > 0".into(),
));
}
let _lib = load_drm_lib()?;
#[cfg(target_os = "linux")]
{
let fd = unsafe { (_lib.drm_open)(std::ptr::null(), std::ptr::null()) };
if fd < 0 {
return Err(DrmError::CallFailed {
op: "drmOpen",
detail: format!("returned fd={fd}"),
});
}
Ok(Self {
lib: _lib,
fd,
width,
height,
pixel_format,
current_fb_id: 0,
previous_fb_id: 0,
crtc_id: 0,
connector_id: 0,
mode: None,
mode_set: false,
})
}
#[cfg(not(target_os = "linux"))]
{
let _ = (width, height, pixel_format);
Err(DrmError::NotSupported)
}
}
pub fn new(connector_name: &str, mode_label: &str, pixel_format: u32) -> DrmResult<Self> {
#[cfg(target_os = "linux")]
{
let mut out = Self::open(1, 1, pixel_format)?;
out.select_connector(connector_name)?;
out.set_mode(mode_label)?;
Ok(out)
}
#[cfg(not(target_os = "linux"))]
{
let _ = (connector_name, mode_label, pixel_format);
Err(DrmError::NotSupported)
}
}
pub fn select_connector(&mut self, name: &str) -> DrmResult<()> {
#[cfg(target_os = "linux")]
{
let res = unsafe { (self.lib.get_resources)(self.fd) };
if res.is_null() {
return Err(DrmError::CallFailed {
op: "drmModeGetResources",
detail: "returned null — kernel has no DRM device?".into(),
});
}
let res_ref = unsafe { &*res };
let connectors: &[u32] = unsafe {
std::slice::from_raw_parts(res_ref.connectors, res_ref.count_connectors as usize)
};
let mut picked: Option<(u32, u32, DrmModeModeInfo, u32)> = None;
for &cid in connectors {
let conn = unsafe { (self.lib.get_connector)(self.fd, cid) };
if conn.is_null() {
continue;
}
let conn_ref = unsafe { &*conn };
let formatted = format!(
"{}-{}",
connector_type_name(conn_ref.connector_type),
conn_ref.connector_type_id
);
let matches = formatted == name
&& conn_ref.connection == DRM_MODE_CONNECTED
&& conn_ref.count_modes > 0;
if matches {
let first_mode = unsafe { *conn_ref.modes };
picked = Some((
conn_ref.connector_id,
conn_ref.encoder_id,
first_mode,
conn_ref.count_modes as u32,
));
unsafe { (self.lib.free_connector)(conn) };
break;
}
unsafe { (self.lib.free_connector)(conn) };
}
unsafe { (self.lib.free_resources)(res) };
let (connector_id, encoder_id, default_mode, _n_modes) =
picked.ok_or_else(|| DrmError::ConnectorNotFound(name.into()))?;
let enc = unsafe { (self.lib.get_encoder)(self.fd, encoder_id) };
if enc.is_null() {
return Err(DrmError::NoCrtc(name.into()));
}
let crtc_id = unsafe { (*enc).crtc_id };
unsafe { (self.lib.free_encoder)(enc) };
if crtc_id == 0 {
return Err(DrmError::NoCrtc(name.into()));
}
self.connector_id = connector_id;
self.crtc_id = crtc_id;
self.mode = Some(default_mode);
self.width = default_mode.hdisplay as u32;
self.height = default_mode.vdisplay as u32;
Ok(())
}
#[cfg(not(target_os = "linux"))]
{
let _ = name;
Err(DrmError::NotSupported)
}
}
pub fn set_mode(&mut self, label: &str) -> DrmResult<()> {
#[cfg(target_os = "linux")]
{
if self.connector_id == 0 {
return Err(DrmError::InvalidParameter(
"set_mode called before select_connector".into(),
));
}
let conn = unsafe { (self.lib.get_connector)(self.fd, self.connector_id) };
if conn.is_null() {
return Err(DrmError::CallFailed {
op: "drmModeGetConnector",
detail: "returned null on re-fetch".into(),
});
}
let conn_ref = unsafe { &*conn };
let modes: &[DrmModeModeInfo] = unsafe {
std::slice::from_raw_parts(conn_ref.modes, conn_ref.count_modes as usize)
};
let matched = modes.iter().find(|m| mode_matches(m, label)).copied();
unsafe { (self.lib.free_connector)(conn) };
let chosen = matched.ok_or_else(|| DrmError::ModeNotSupported(label.into()))?;
self.mode = Some(chosen);
self.width = chosen.hdisplay as u32;
self.height = chosen.vdisplay as u32;
self.mode_set = false; Ok(())
}
#[cfg(not(target_os = "linux"))]
{
let _ = label;
Err(DrmError::NotSupported)
}
}
pub fn import_dmabuf(&self, dma_fd: i32) -> DrmResult<u32> {
#[cfg(target_os = "linux")]
{
if dma_fd < 0 {
return Err(DrmError::InvalidParameter(
"dma_fd must be non-negative".into(),
));
}
let mut handle: u32 = 0;
let ret = unsafe { (self.lib.prime_fd_to_handle)(self.fd, dma_fd, &mut handle) };
if ret != 0 || handle == 0 {
return Err(DrmError::CallFailed {
op: "drmPrimeFDToHandle",
detail: format!("ret={ret} handle={handle}"),
});
}
Ok(handle)
}
#[cfg(not(target_os = "linux"))]
{
let _ = dma_fd;
Err(DrmError::NotSupported)
}
}
pub fn present(&mut self, dma_fd: i32, stride: u32) -> DrmResult<()> {
#[cfg(target_os = "linux")]
{
if self.crtc_id == 0 || self.mode.is_none() {
return Err(DrmError::InvalidParameter(
"present() before select_connector + set_mode".into(),
));
}
let gem_handle = self.import_dmabuf(dma_fd)?;
let mut fb_id: u32 = 0;
let ret = if let Some(add2) = self.lib.add_fb2 {
let handles = [gem_handle, gem_handle, 0, 0];
let pitches = [stride, stride, 0, 0];
let offsets = [0u32, (stride * self.height), 0, 0];
unsafe {
add2(
self.fd,
self.width,
self.height,
self.pixel_format,
handles.as_ptr(),
pitches.as_ptr(),
offsets.as_ptr(),
&mut fb_id,
0,
)
}
} else {
unsafe {
(self.lib.add_fb)(
self.fd,
self.width,
self.height,
24,
32,
stride,
gem_handle,
&mut fb_id,
)
}
};
if ret != 0 || fb_id == 0 {
return Err(DrmError::CallFailed {
op: "drmModeAddFB/AddFB2",
detail: format!("ret={ret}"),
});
}
if !self.mode_set {
let mode = self.mode.unwrap();
let connectors = [self.connector_id];
let ret = unsafe {
(self.lib.set_crtc)(
self.fd,
self.crtc_id,
fb_id,
0,
0,
connectors.as_ptr(),
1,
&mode as *const DrmModeModeInfo,
)
};
if ret != 0 {
unsafe { (self.lib.rm_fb)(self.fd, fb_id) };
return Err(DrmError::CallFailed {
op: "drmModeSetCrtc",
detail: format!("ret={ret}"),
});
}
self.mode_set = true;
self.previous_fb_id = self.current_fb_id;
self.current_fb_id = fb_id;
} else {
let ret = unsafe {
(self.lib.page_flip)(
self.fd,
self.crtc_id,
fb_id,
DRM_MODE_PAGE_FLIP_EVENT,
std::ptr::null_mut(),
)
};
if ret != 0 {
unsafe { (self.lib.rm_fb)(self.fd, fb_id) };
return Err(DrmError::CallFailed {
op: "drmModePageFlip",
detail: format!("ret={ret}"),
});
}
if self.previous_fb_id != 0 {
unsafe { (self.lib.rm_fb)(self.fd, self.previous_fb_id) };
}
self.previous_fb_id = self.current_fb_id;
self.current_fb_id = fb_id;
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
{
let _ = (dma_fd, stride);
Err(DrmError::NotSupported)
}
}
pub fn flip(&mut self, dma_handle: u32, stride: u32) -> DrmResult<()> {
#[cfg(target_os = "linux")]
{
let mut fb_id: u32 = 0;
let ret = unsafe {
(self.lib.add_fb)(
self.fd,
self.width,
self.height,
24,
32,
stride,
dma_handle,
&mut fb_id,
)
};
if ret != 0 {
return Err(DrmError::CallFailed {
op: "drmModeAddFB",
detail: format!("ret={ret}"),
});
}
if self.current_fb_id != 0 {
unsafe { (self.lib.rm_fb)(self.fd, self.current_fb_id) };
}
self.current_fb_id = fb_id;
Ok(())
}
#[cfg(not(target_os = "linux"))]
{
let _ = (dma_handle, stride);
Err(DrmError::NotSupported)
}
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn pixel_format(&self) -> u32 {
self.pixel_format
}
pub fn crtc_id(&self) -> u32 {
#[cfg(target_os = "linux")]
{
self.crtc_id
}
#[cfg(not(target_os = "linux"))]
{
0
}
}
pub fn connector_id(&self) -> u32 {
#[cfg(target_os = "linux")]
{
self.connector_id
}
#[cfg(not(target_os = "linux"))]
{
0
}
}
}
impl Drop for DrmOutput {
fn drop(&mut self) {
#[cfg(target_os = "linux")]
{
if self.previous_fb_id != 0 {
let _ = unsafe { (self.lib.rm_fb)(self.fd, self.previous_fb_id) };
}
if self.current_fb_id != 0 {
let _ = unsafe { (self.lib.rm_fb)(self.fd, self.current_fb_id) };
}
if self.fd >= 0 {
let _ = unsafe { (self.lib.drm_close)(self.fd) };
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn drm_unavailable_on_dev_host() {
let _ = drm_available();
}
#[test]
fn open_rejects_zero_dims() {
let res = DrmOutput::open(0, 720, fourcc::NV12);
assert!(matches!(res, Err(DrmError::InvalidParameter(_))));
}
#[test]
fn new_returns_not_supported_off_linux() {
#[cfg(not(target_os = "linux"))]
{
let res = DrmOutput::new("HDMI-A-1", "720p60", fourcc::NV12);
assert!(matches!(
res,
Err(DrmError::NotSupported) | Err(DrmError::LibraryNotFound)
));
}
}
#[test]
fn present_rejects_negative_dma_fd() {
#[cfg(target_os = "linux")]
if let Ok(out) = DrmOutput::open(640, 480, fourcc::NV12) {
assert!(matches!(
out.import_dmabuf(-1),
Err(DrmError::InvalidParameter(_))
));
}
}
#[test]
fn fourcc_values_are_four_bytes() {
assert_eq!(&fourcc::NV12.to_le_bytes(), b"NV12");
assert_eq!(&fourcc::XRGB8888.to_le_bytes(), b"XR24");
assert_eq!(&fourcc::YUYV.to_le_bytes(), b"YUYV");
}
#[cfg(target_os = "linux")]
#[test]
fn connector_type_names_cover_common_outputs() {
assert_eq!(connector_type_name(11), "HDMI-A");
assert_eq!(connector_type_name(16), "DSI");
assert_eq!(connector_type_name(10), "DP");
assert_eq!(connector_type_name(14), "eDP");
}
#[cfg(target_os = "linux")]
#[test]
fn mode_matches_by_name_and_shape() {
let mut m = DrmModeModeInfo {
clock: 148500,
hdisplay: 1920,
hsync_start: 0,
hsync_end: 0,
htotal: 0,
hskew: 0,
vdisplay: 1080,
vsync_start: 0,
vsync_end: 0,
vtotal: 0,
vscan: 0,
vrefresh: 60,
flags: 0,
typ: 0,
name: [0; DRM_DISPLAY_MODE_LEN],
};
let s = b"1920x1080";
m.name[..s.len()].copy_from_slice(s);
assert!(mode_matches(&m, "1920x1080"), "exact name match");
assert!(mode_matches(&m, "1080p60"), "shape+refresh match");
assert!(!mode_matches(&m, "720p60"));
assert!(!mode_matches(&m, "1920x720"));
}
}