use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
use drm::control::{Device as ControlDevice, FbCmd2Flags, framebuffer};
use drmkit_core::Device;
use drmkit_scene::{
AcquiredBuffer, BindingModel, DmaBufDesc, LayerBufferSource, SourceError, SourceFormat,
};
use drmkit_sync::SyncFence;
#[derive(Debug, Clone, Copy)]
#[allow(
clippy::exhaustive_structs,
reason = "a value defined by its fields; there is nothing to add to it"
)]
pub struct ExternalPlane<'a> {
pub fd: BorrowedFd<'a>,
pub offset: u32,
pub pitch: u32,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ExternalError {
#[error("invalid external buffer: {reason}")]
Invalid {
reason: &'static str,
},
#[error("external buffer operation failed: {0}")]
Io(#[from] rustix::io::Errno),
#[error("buffer key retired and not yet torn down")]
Retired,
}
const MAX_PLANES: usize = 4;
pub(crate) struct ImportedFramebuffer {
duped_fds: Vec<OwnedFd>,
offsets: Vec<u32>,
pitches: Vec<u32>,
fb: Option<framebuffer::Handle>,
handles: Vec<drm::buffer::Handle>,
device: Option<OwnedFd>,
}
impl ImportedFramebuffer {
pub(crate) fn dma_buf_planes(&self) -> (&[OwnedFd], &[u32], &[u32]) {
(&self.duped_fds, &self.offsets, &self.pitches)
}
pub(crate) fn import(
device: &Device,
format: SourceFormat,
planes: &[ExternalPlane<'_>],
) -> Result<Self, ExternalError> {
let source = ExternalDmaBufSource::create(device, format, planes, None)?;
Ok(source.into_imported())
}
pub(crate) fn fb_id(&self) -> Option<u32> {
self.fb.map(Into::into)
}
pub(crate) fn forget(&mut self) {
self.fb = None;
self.handles.clear();
self.device = None;
}
pub(crate) fn reimport(
&mut self,
device: &Device,
format: SourceFormat,
) -> Result<(), ExternalError> {
self.forget();
if self.duped_fds.is_empty() {
return Ok(());
}
let fourcc = drm::buffer::DrmFourcc::try_from(format.fourcc).map_err(|_| {
ExternalError::Invalid {
reason: "format is not a recognized DRM FourCC",
}
})?;
let device_duplicate = duplicate(device.as_fd())?;
let (fb, handles) = import_planes(
device,
format,
fourcc,
&self.duped_fds,
&self.offsets,
&self.pitches,
)?;
self.fb = Some(fb);
self.handles = handles;
self.device = Some(device_duplicate);
Ok(())
}
pub(crate) fn release(&mut self) {
self.tear_down();
}
fn tear_down(&mut self) {
let Some(file) = self.device.take() else {
self.fb = None;
self.handles.clear();
return;
};
let device = RawDevice { fd: file.as_fd() };
if let Some(fb) = self.fb.take() {
let _ = device.destroy_framebuffer(fb);
}
close_handles(&device, &mut self.handles);
}
}
impl Drop for ImportedFramebuffer {
fn drop(&mut self) {
self.tear_down();
}
}
fn duplicate(fd: BorrowedFd<'_>) -> Result<OwnedFd, ExternalError> {
fd.try_clone_to_owned().map_err(|error| {
ExternalError::Io(
rustix::io::Errno::from_io_error(&error).unwrap_or(rustix::io::Errno::MFILE),
)
})
}
fn close_handles(device: &impl drm::control::Device, handles: &mut Vec<drm::buffer::Handle>) {
handles.sort_unstable_by_key(|handle| u32::from(*handle));
handles.dedup();
for handle in handles.drain(..) {
let _ = device.close_buffer(handle);
}
}
fn import_planes(
device: &Device,
format: SourceFormat,
fourcc: drm::buffer::DrmFourcc,
fds: &[OwnedFd],
offsets: &[u32],
pitches: &[u32],
) -> Result<(framebuffer::Handle, Vec<drm::buffer::Handle>), ExternalError> {
let mut layout_handles = [None; MAX_PLANES];
let mut layout_offsets = [0u32; MAX_PLANES];
let mut layout_pitches = [0u32; MAX_PLANES];
let mut imported = Vec::with_capacity(fds.len());
for (index, fd) in fds.iter().enumerate() {
let handle = match device.prime_fd_to_buffer(fd.as_fd()) {
Ok(handle) => handle,
Err(e) => {
close_handles(device, &mut imported);
return Err(ExternalError::Io(errno_of(&e)));
}
};
imported.push(handle);
layout_handles[index] = Some(handle);
layout_offsets[index] = offsets[index];
layout_pitches[index] = pitches[index];
}
let declared = drmkit_core::framebuffer_modifier(device, format.modifier);
let layout = ImportedLayout {
width: format.width,
height: format.height,
fourcc,
modifier: declared,
handles: layout_handles,
offsets: layout_offsets,
pitches: layout_pitches,
};
let flags = if declared.is_some() {
FbCmd2Flags::MODIFIERS
} else {
FbCmd2Flags::empty()
};
match device.add_planar_framebuffer(&layout, flags) {
Ok(fb) => Ok((fb, imported)),
Err(e) => {
close_handles(device, &mut imported);
Err(ExternalError::Io(errno_of(&e)))
}
}
}
pub struct ExternalDmaBufSource {
imported: ImportedFramebuffer,
format: SourceFormat,
pending_fence: Option<SyncFence>,
on_release: Option<Box<dyn FnOnce()>>,
}
impl std::fmt::Debug for ExternalDmaBufSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExternalDmaBufSource")
.field("format", &self.format)
.field("fb", &self.imported.fb_id())
.field("has_fence", &self.pending_fence.is_some())
.finish_non_exhaustive()
}
}
impl ExternalDmaBufSource {
pub fn create(
device: &Device,
format: SourceFormat,
planes: &[ExternalPlane<'_>],
on_release: Option<Box<dyn FnOnce()>>,
) -> Result<Self, ExternalError> {
if format.width == 0 || format.height == 0 {
return Err(ExternalError::Invalid {
reason: "width and height must be non-zero",
});
}
if format.fourcc == 0 {
return Err(ExternalError::Invalid {
reason: "format must be non-zero",
});
}
if planes.is_empty() || planes.len() > MAX_PLANES {
return Err(ExternalError::Invalid {
reason: "expected between 1 and 4 planes",
});
}
if planes.iter().any(|plane| plane.pitch == 0) {
return Err(ExternalError::Invalid {
reason: "every plane needs a non-zero pitch",
});
}
let fourcc = drm::buffer::DrmFourcc::try_from(format.fourcc).map_err(|_| {
ExternalError::Invalid {
reason: "format is not a recognized DRM FourCC",
}
})?;
let mut duped = Vec::with_capacity(planes.len());
for plane in planes {
duped.push(plane.fd.try_clone_to_owned().map_err(|e| {
ExternalError::Io(
rustix::io::Errno::from_io_error(&e).unwrap_or(rustix::io::Errno::MFILE),
)
})?);
}
let device_duplicate = duplicate(device.as_fd())?;
let offsets: Vec<u32> = planes.iter().map(|plane| plane.offset).collect();
let pitches: Vec<u32> = planes.iter().map(|plane| plane.pitch).collect();
let (fb, imported) = import_planes(device, format, fourcc, &duped, &offsets, &pitches)?;
Ok(Self {
imported: ImportedFramebuffer {
duped_fds: duped,
offsets,
pitches,
fb: Some(fb),
handles: imported,
device: Some(device_duplicate),
},
format,
pending_fence: None,
on_release,
})
}
pub fn set_acquire_fence(&mut self, fence: SyncFence) {
self.pending_fence = Some(fence);
}
#[must_use]
pub const fn has_acquire_fence(&self) -> bool {
self.pending_fence.is_some()
}
#[must_use]
pub fn fb_id(&self) -> Option<u32> {
self.imported.fb_id()
}
fn into_imported(mut self) -> ImportedFramebuffer {
self.on_release = None;
std::mem::replace(
&mut self.imported,
ImportedFramebuffer {
duped_fds: Vec::new(),
offsets: Vec::new(),
pitches: Vec::new(),
fb: None,
handles: Vec::new(),
device: None,
},
)
}
fn fire_on_release_once(&mut self) {
if let Some(callback) = self.on_release.take() {
callback();
}
}
}
impl LayerBufferSource for ExternalDmaBufSource {
fn acquire(&mut self) -> Result<AcquiredBuffer, SourceError> {
let fb_id = self
.imported
.fb_id()
.ok_or(SourceError::Failed(rustix::io::Errno::INVAL))?;
let acquire_fence = self
.pending_fence
.as_ref()
.and_then(SyncFence::as_fd)
.and_then(|fd| SyncFence::import(fd).ok());
Ok(AcquiredBuffer {
fb_id,
token: 0,
acquire_fence,
damage: Vec::new(),
})
}
fn export_dma_buf(&mut self) -> Result<DmaBufDesc<'_>, SourceError> {
let (fds, offsets, pitches) = self.imported.dma_buf_planes();
if fds.is_empty() {
return Err(SourceError::Unsupported);
}
Ok(DmaBufDesc {
fds: fds.iter().map(std::os::fd::AsFd::as_fd).collect(),
offsets: offsets.to_vec(),
pitches: pitches.to_vec(),
format: self.format,
})
}
fn release(&mut self, _acquired: AcquiredBuffer) {
self.fire_on_release_once();
}
fn binding_model(&self) -> BindingModel {
BindingModel::SceneSubmitsFbId
}
fn format(&self) -> SourceFormat {
self.format
}
fn on_session_resumed(&mut self, device: &Device) -> Result<(), SourceError> {
self.imported
.reimport(device, self.format)
.map_err(|error| source_error(&error))
}
}
pub(crate) fn source_error(error: &ExternalError) -> SourceError {
match error {
ExternalError::Io(errno) => SourceError::Failed(*errno),
ExternalError::Invalid { .. } => SourceError::Failed(rustix::io::Errno::INVAL),
ExternalError::Retired => SourceError::WouldBlock,
}
}
impl ExternalDmaBufSource {
#[cfg(feature = "gbm")]
pub(crate) fn forget(&mut self) {
self.imported.forget();
}
}
impl Drop for ExternalDmaBufSource {
fn drop(&mut self) {
self.fire_on_release_once();
}
}
struct RawDevice<'a> {
fd: BorrowedFd<'a>,
}
impl AsFd for RawDevice<'_> {
fn as_fd(&self) -> BorrowedFd<'_> {
self.fd
}
}
impl drm::Device for RawDevice<'_> {}
impl drm::control::Device for RawDevice<'_> {}
struct ImportedLayout {
width: u32,
height: u32,
fourcc: drm::buffer::DrmFourcc,
modifier: Option<u64>,
handles: [Option<drm::buffer::Handle>; MAX_PLANES],
offsets: [u32; MAX_PLANES],
pitches: [u32; MAX_PLANES],
}
impl drm::buffer::PlanarBuffer for ImportedLayout {
fn size(&self) -> (u32, u32) {
(self.width, self.height)
}
fn format(&self) -> drm::buffer::DrmFourcc {
self.fourcc
}
fn modifier(&self) -> Option<drm::buffer::DrmModifier> {
self.modifier.map(drm::buffer::DrmModifier::from)
}
fn pitches(&self) -> [u32; 4] {
self.pitches
}
fn handles(&self) -> [Option<drm::buffer::Handle>; 4] {
self.handles
}
fn offsets(&self) -> [u32; 4] {
self.offsets
}
}
fn errno_of(error: &std::io::Error) -> rustix::io::Errno {
rustix::io::Errno::from_io_error(error).unwrap_or(rustix::io::Errno::IO)
}