use std::os::fd::{AsFd as _, BorrowedFd, RawFd};
use std::ptr::NonNull;
use drm::buffer::DrmFourcc;
use drm::control::{Device as ControlDevice, FbCmd2Flags, framebuffer};
use drmkit_core::Device;
use drmkit_fmt::fourcc;
use crate::error::{DumbError, Result};
use crate::mapping::{MapAccess, Mapping};
const MAX_IMAGE_DIM: u32 = 16384;
#[derive(Debug, Clone, Copy)]
struct Allocation {
handle: u32,
size: u64,
}
#[derive(Debug)]
struct Mmap {
ptr: NonNull<u8>,
len: usize,
}
unsafe impl Send for Mmap {}
unsafe impl Sync for Mmap {}
impl Mmap {
fn as_slice(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
}
fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
}
}
impl Drop for Mmap {
fn drop(&mut self) {
unsafe {
let _ = rustix::mm::munmap(self.ptr.as_ptr().cast(), self.len);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(
clippy::exhaustive_structs,
reason = "built by callers with `..Default::default()`, which a new field does not break"
)]
pub struct Config {
pub width: u32,
pub height: u32,
pub fourcc: u32,
pub bpp: u32,
pub add_fb: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
width: 0,
height: 0,
fourcc: 0,
bpp: 32,
add_fb: true,
}
}
}
struct PlanarGeometry {
bpp: u32,
extra_rows_num: u64,
extra_rows_den: u64,
}
const fn planar_geometry(fourcc_code: u32) -> Option<PlanarGeometry> {
match fourcc_code {
fourcc::NV12 | fourcc::NV21 => Some(PlanarGeometry {
bpp: 8,
extra_rows_num: 1,
extra_rows_den: 2,
}),
fourcc::P010 | fourcc::P012 | fourcc::P016 => Some(PlanarGeometry {
bpp: 16,
extra_rows_num: 1,
extra_rows_den: 2,
}),
_ => None,
}
}
#[derive(Debug)]
pub struct Buffer {
fd: Option<RawFd>,
alloc: Option<Allocation>,
fb: Option<framebuffer::Handle>,
mapping: Option<Mmap>,
width: u32,
height: u32,
stride: u32,
}
impl Default for Buffer {
fn default() -> Self {
Self::new()
}
}
impl Buffer {
#[must_use]
pub const fn new() -> Self {
Self {
fd: None,
alloc: None,
fb: None,
mapping: None,
width: 0,
height: 0,
stride: 0,
}
}
pub fn create(device: &Device, config: &Config) -> Result<Self> {
if config.width == 0 || config.height == 0 {
return Err(DumbError::InvalidConfig {
reason: "width and height must be non-zero",
});
}
if config.fourcc == 0 {
return Err(DumbError::InvalidConfig {
reason: "format must be non-zero",
});
}
if config.bpp == 0 {
return Err(DumbError::InvalidConfig {
reason: "bits per pixel must be non-zero",
});
}
if config.width > MAX_IMAGE_DIM || config.height > MAX_IMAGE_DIM {
return Err(DumbError::InvalidConfig {
reason: "dimensions exceed 16384",
});
}
let fourcc_typed =
DrmFourcc::try_from(config.fourcc).map_err(|_| DumbError::InvalidConfig {
reason: "format is not a recognized DRM FourCC",
})?;
let created = drm_ffi::mode::dumbbuffer::create(
device.as_fd(),
config.width,
config.height,
config.bpp,
0,
)
.map_err(|e| io_to_errno(&e))?;
let mut buffer = Self {
fd: Some(device.raw_fd()),
width: config.width,
height: config.height,
stride: created.pitch,
alloc: Some(Allocation {
handle: created.handle,
size: created.size,
}),
fb: None,
mapping: None,
};
buffer.mapping = Some(buffer.map_region(device)?);
if config.add_fb {
let planes = PlaneLayout::single(&buffer, fourcc_typed);
let fb = device
.add_planar_framebuffer(&planes, FbCmd2Flags::empty())
.map_err(|e| io_to_errno(&e))?;
buffer.fb = Some(fb);
}
Ok(buffer)
}
pub fn create_planar(
device: &Device,
fourcc_code: u32,
width: u32,
height: u32,
) -> Result<Self> {
if width == 0 || height == 0 || width > MAX_IMAGE_DIM || height > MAX_IMAGE_DIM {
return Err(DumbError::InvalidConfig {
reason: "dimensions must be non-zero and at most 16384",
});
}
let geometry = planar_geometry(fourcc_code).ok_or(DumbError::UnsupportedFormat {
fourcc: fourcc_code,
})?;
let fourcc_typed =
DrmFourcc::try_from(fourcc_code).map_err(|_| DumbError::UnsupportedFormat {
fourcc: fourcc_code,
})?;
let extra_rows = u64::from(height)
.checked_mul(geometry.extra_rows_num)
.ok_or(DumbError::TooLarge {
what: "chroma row count",
})?
/ geometry.extra_rows_den;
let total_rows = u64::from(height)
.checked_add(extra_rows)
.and_then(|rows| u32::try_from(rows).ok())
.ok_or(DumbError::TooLarge {
what: "total row count",
})?;
let created =
drm_ffi::mode::dumbbuffer::create(device.as_fd(), width, total_rows, geometry.bpp, 0)
.map_err(|e| io_to_errno(&e))?;
let stride = created.pitch;
let mut buffer = Self {
fd: Some(device.raw_fd()),
width,
height,
stride,
alloc: Some(Allocation {
handle: created.handle,
size: created.size,
}),
fb: None,
mapping: None,
};
buffer.mapping = Some(buffer.map_region(device)?);
let y_size = u64::from(stride) * u64::from(height);
let chroma_offset = u32::try_from(y_size).map_err(|_| DumbError::TooLarge {
what: "chroma plane offset",
})?;
let planes = PlaneLayout::semi_planar(&buffer, fourcc_typed, chroma_offset);
let fb = device
.add_planar_framebuffer(&planes, FbCmd2Flags::empty())
.map_err(|e| io_to_errno(&e))?;
buffer.fb = Some(fb);
Ok(buffer)
}
fn map_region(&self, device: &Device) -> Result<Mmap> {
let alloc = self.alloc.ok_or(DumbError::InvalidConfig {
reason: "buffer has no allocation to map",
})?;
let len = usize::try_from(alloc.size).map_err(|_| DumbError::TooLarge {
what: "allocation size does not fit a usize",
})?;
let offset = drm_ffi::mode::dumbbuffer::map(device.as_fd(), alloc.handle, 0, 0)
.map_err(|e| io_to_errno(&e))?
.offset;
let ptr = unsafe {
rustix::mm::mmap(
std::ptr::null_mut(),
len,
rustix::mm::ProtFlags::READ | rustix::mm::ProtFlags::WRITE,
rustix::mm::MapFlags::SHARED,
device.as_fd(),
offset,
)
}?;
let ptr = NonNull::new(ptr.cast::<u8>()).ok_or(DumbError::Io(rustix::io::Errno::NOMEM))?;
Ok(Mmap { ptr, len })
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.alloc.is_none()
}
#[must_use]
pub const fn width(&self) -> u32 {
self.width
}
#[must_use]
pub const fn height(&self) -> u32 {
self.height
}
#[must_use]
pub const fn stride(&self) -> u32 {
self.stride
}
#[must_use]
pub fn size_bytes(&self) -> usize {
self.mapping.as_ref().map_or(0, |m| m.len)
}
#[must_use]
pub fn gem_handle(&self) -> Option<u32> {
self.alloc.map(|a| a.handle)
}
#[must_use]
pub fn fb_id(&self) -> Option<u32> {
self.fb.map(Into::into)
}
#[must_use]
pub fn data(&self) -> &[u8] {
self.mapping.as_ref().map_or(&[], Mmap::as_slice)
}
#[must_use]
pub fn data_mut(&mut self) -> &mut [u8] {
self.mapping.as_mut().map_or(&mut [], Mmap::as_mut_slice)
}
pub fn map(&mut self, access: MapAccess) -> Mapping<'_> {
let (stride, width, height) = (self.stride, self.width, self.height);
let pixels = self.data_mut();
Mapping::new(pixels, stride, width, height, access)
}
pub fn forget(&mut self) {
self.mapping = None; self.alloc = None;
self.fb = None;
self.fd = None;
self.width = 0;
self.height = 0;
self.stride = 0;
}
}
impl Drop for Buffer {
fn drop(&mut self) {
let (Some(fd), Some(alloc)) = (self.fd, self.alloc.take()) else {
return;
};
let borrowed = unsafe { BorrowedFd::borrow_raw(fd) };
let device = RawDevice { fd: borrowed };
if let Some(fb) = self.fb.take() {
let _ = device.destroy_framebuffer(fb);
}
self.mapping = None;
let _ = drm_ffi::mode::dumbbuffer::destroy(borrowed, alloc.handle);
}
}
struct RawDevice<'a> {
fd: BorrowedFd<'a>,
}
impl std::os::fd::AsFd for RawDevice<'_> {
fn as_fd(&self) -> BorrowedFd<'_> {
self.fd
}
}
impl drm::Device for RawDevice<'_> {}
impl drm::control::Device for RawDevice<'_> {}
struct PlaneLayout {
width: u32,
height: u32,
fourcc: DrmFourcc,
handles: [Option<drm::buffer::Handle>; 4],
pitches: [u32; 4],
offsets: [u32; 4],
}
impl PlaneLayout {
fn single(buffer: &Buffer, fourcc: DrmFourcc) -> Self {
let handle = buffer.alloc.and_then(|a| drm::control::from_u32(a.handle));
Self {
width: buffer.width,
height: buffer.height,
fourcc,
handles: [handle, None, None, None],
pitches: [buffer.stride, 0, 0, 0],
offsets: [0; 4],
}
}
fn semi_planar(buffer: &Buffer, fourcc: DrmFourcc, chroma_offset: u32) -> Self {
let handle = buffer.alloc.and_then(|a| drm::control::from_u32(a.handle));
Self {
width: buffer.width,
height: buffer.height,
fourcc,
handles: [handle, handle, None, None],
pitches: [buffer.stride, buffer.stride, 0, 0],
offsets: [0, chroma_offset, 0, 0],
}
}
}
impl drm::buffer::PlanarBuffer for PlaneLayout {
fn size(&self) -> (u32, u32) {
(self.width, self.height)
}
fn format(&self) -> DrmFourcc {
self.fourcc
}
fn modifier(&self) -> Option<drm::buffer::DrmModifier> {
None
}
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 io_to_errno(error: &std::io::Error) -> DumbError {
rustix::io::Errno::from_io_error(error)
.map_or(DumbError::Io(rustix::io::Errno::IO), DumbError::Io)
}