use std::num::NonZeroUsize;
use drm::control::Device as _;
use drmkit_core::{Device, ObjectType, PropertyStore};
use drmkit_fmt::fourcc;
use crate::CaptureError;
use crate::image::{Image, src_over};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SourceFormat {
Argb8888,
Xrgb8888,
Rgb565,
}
impl SourceFormat {
const fn bytes(self) -> usize {
match self {
Self::Argb8888 | Self::Xrgb8888 => 4,
Self::Rgb565 => 2,
}
}
fn read(self, chunk: &[u8]) -> u32 {
match self {
Self::Argb8888 => u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]),
Self::Xrgb8888 => {
u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]) | 0xff00_0000
}
Self::Rgb565 => {
let v = u32::from(u16::from_ne_bytes([chunk[0], chunk[1]]));
let r = (v >> 11) & 0x1f;
let g = (v >> 5) & 0x3f;
let b = v & 0x1f;
let r = (r << 3) | (r >> 2);
let g = (g << 2) | (g >> 4);
let b = (b << 3) | (b >> 2);
0xff00_0000 | (r << 16) | (g << 8) | b
}
}
}
}
pub(crate) struct Blit<'a> {
pub bytes: &'a [u8],
pub width: u32,
pub height: u32,
pub pitch: u32,
pub format: SourceFormat,
pub crtc_x: i32,
pub crtc_y: i32,
pub crtc_w: u32,
pub crtc_h: u32,
}
struct PlaneFrame {
plane_id: u32,
width: u32,
height: u32,
pitch: u32,
format: SourceFormat,
crtc_x: i32,
crtc_y: i32,
crtc_w: u32,
crtc_h: u32,
zpos: i64,
mapping: Mapping,
}
struct Mapping {
ptr: *mut core::ffi::c_void,
len: usize,
}
impl Mapping {
fn bytes(&self) -> &[u8] {
unsafe { core::slice::from_raw_parts(self.ptr.cast::<u8>(), self.len) }
}
}
impl Drop for Mapping {
fn drop(&mut self) {
unsafe {
let _ = rustix::mm::munmap(self.ptr, self.len);
}
}
}
pub fn snapshot(device: &Device, crtc_id: u32) -> Result<Image, CaptureError> {
let crtc = device
.get_crtc(drm::control::crtc::Handle::from(
core::num::NonZeroU32::new(crtc_id).ok_or(CaptureError::NoMode)?,
))
.map_err(|e| CaptureError::Io(std::io::Error::other(e.to_string())))?;
let Some(mode) = crtc.mode() else {
drmkit_log::log_warn!("capture: crtc {crtc_id} is not driving a mode; nothing to snapshot");
return Err(CaptureError::NoMode);
};
let (out_w, out_h) = (u32::from(mode.size().0), u32::from(mode.size().1));
let Collected {
mut frames,
enumerated,
unbound,
} = collect_planes(device, crtc_id)?;
if frames.is_empty() {
drmkit_log::log_warn!(
"capture: crtc {crtc_id} has {enumerated} enumerated planes, {unbound} unbound to \
this crtc, none readable -- snapshot aborted (plane geometry needs \
DRM_CLIENT_CAP_ATOMIC on the fd)"
);
return Err(CaptureError::NothingReadable);
}
frames.sort_by_key(|frame| frame.zpos);
let mut image = Image::new(out_w, out_h);
for frame in &frames {
compose(&mut image, frame);
}
Ok(image)
}
struct Collected {
frames: Vec<PlaneFrame>,
enumerated: usize,
unbound: usize,
}
fn collect_planes(device: &Device, crtc_id: u32) -> Result<Collected, CaptureError> {
let handles = device
.plane_handles()
.map_err(|e| CaptureError::Io(std::io::Error::other(e.to_string())))?;
let enumerated = handles.len();
let mut unbound = 0;
let mut frames = Vec::new();
for handle in handles {
let plane_id: u32 = handle.into();
let Ok(info) = device.get_plane(handle) else {
continue;
};
let (Some(fb), true) = (
info.framebuffer(),
info.crtc().map(u32::from) == Some(crtc_id),
) else {
unbound += 1;
continue;
};
match map_plane(device, plane_id, fb) {
Ok(frame) => frames.push(frame),
Err(reason) => {
drmkit_log::log_warn!("capture: skipping plane {plane_id} -- {reason}");
}
}
}
Ok(Collected {
frames,
enumerated,
unbound,
})
}
type Skipped = String;
fn map_plane(
device: &Device,
plane_id: u32,
fb: drm::control::framebuffer::Handle,
) -> Result<PlaneFrame, Skipped> {
let fb = device
.get_planar_framebuffer(fb)
.map_err(|e| format!("framebuffer query failed: {e}"))?;
let modifier = fb.modifier().map_or(0, u64::from);
let linear = fb.modifier().is_none() || modifier == 0;
if !linear {
return Err(format!("non-linear modifier {modifier:#018x}"));
}
let format = match fb.pixel_format() as u32 {
fourcc::ARGB8888 => SourceFormat::Argb8888,
fourcc::XRGB8888 => SourceFormat::Xrgb8888,
fourcc::RGB565 => SourceFormat::Rgb565,
other => {
return Err(format!(
"unsupported format {}",
drmkit_fmt::format_name(other)
));
}
};
let (width, height) = fb.size();
let pitch = fb.pitches()[0];
let Some(buffer) = fb.buffers().first().copied().flatten() else {
return Err("framebuffer reports no buffer object".to_owned());
};
let mut store = PropertyStore::new();
store
.cache_properties(device, plane_id, ObjectType::Plane)
.map_err(|e| format!("property cache failed: {e}"))?;
let geometry = |name: &str| {
store
.property_value(plane_id, name)
.map_err(|_| format!("{name} unavailable -- is the atomic client cap enabled?"))
};
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let crtc_x = geometry("CRTC_X")? as u32 as i32;
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let crtc_y = geometry("CRTC_Y")? as u32 as i32;
#[allow(clippy::cast_possible_truncation)]
let crtc_w = geometry("CRTC_W")? as u32;
#[allow(clippy::cast_possible_truncation)]
let crtc_h = geometry("CRTC_H")? as u32;
#[allow(clippy::cast_possible_wrap)]
let zpos = store
.property_value(plane_id, "zpos")
.map_or(0, |z| z as i64);
let len = pitch as usize * height as usize;
let mapping = map_buffer(device, buffer, len)?;
Ok(PlaneFrame {
plane_id,
width,
height,
pitch,
format,
crtc_x,
crtc_y,
crtc_w,
crtc_h,
zpos,
mapping,
})
}
fn map_buffer(
device: &Device,
buffer: drm::buffer::Handle,
len: usize,
) -> Result<Mapping, Skipped> {
let Some(len) = NonZeroUsize::new(len) else {
return Err("framebuffer has zero size".to_owned());
};
let dmabuf = device
.buffer_to_prime_fd(buffer, 0)
.map_err(|e| format!("dma-buf export failed: {e}"))?;
let ptr = unsafe {
rustix::mm::mmap(
core::ptr::null_mut(),
len.get(),
rustix::mm::ProtFlags::READ,
rustix::mm::MapFlags::SHARED,
&dmabuf,
0,
)
}
.map_err(|e| format!("dma-buf mmap failed: {e}"))?;
Ok(Mapping {
ptr,
len: len.get(),
})
}
fn compose(image: &mut Image, frame: &PlaneFrame) {
blit(
image,
&Blit {
bytes: frame.mapping.bytes(),
width: frame.width,
height: frame.height,
pitch: frame.pitch,
format: frame.format,
crtc_x: frame.crtc_x,
crtc_y: frame.crtc_y,
crtc_w: frame.crtc_w,
crtc_h: frame.crtc_h,
},
);
drmkit_log::log_debug!(
"capture: composed plane {} ({}x{} -> {}x{} at {},{}) zpos {}",
frame.plane_id,
frame.width,
frame.height,
frame.crtc_w,
frame.crtc_h,
frame.crtc_x,
frame.crtc_y,
frame.zpos
);
}
pub(crate) fn blit(image: &mut Image, src: &Blit<'_>) {
let dst_w = if src.crtc_w == 0 {
src.width
} else {
src.crtc_w
};
let dst_h = if src.crtc_h == 0 {
src.height
} else {
src.crtc_h
};
if dst_w == 0 || dst_h == 0 || src.width == 0 || src.height == 0 {
return;
}
let out_w = image.width();
let out_h = image.height();
for dy in 0..dst_h {
let Some(y) = src.crtc_y.checked_add_unsigned(dy) else {
continue;
};
if y < 0 || y.cast_unsigned() >= out_h {
continue;
}
let src_y = u32::try_from(u64::from(dy) * u64::from(src.height) / u64::from(dst_h))
.unwrap_or(src.height.saturating_sub(1));
let row = src_y as usize * src.pitch as usize;
for dx in 0..dst_w {
let Some(x) = src.crtc_x.checked_add_unsigned(dx) else {
continue;
};
if x < 0 || x.cast_unsigned() >= out_w {
continue;
}
let src_x = u32::try_from(u64::from(dx) * u64::from(src.width) / u64::from(dst_w))
.unwrap_or(src.width.saturating_sub(1));
let bpp = src.format.bytes();
let at = row + src_x as usize * bpp;
let Some(chunk) = src.bytes.get(at..at + bpp) else {
continue;
};
let pixel = src.format.read(chunk);
let index = y.cast_unsigned() as usize * out_w as usize + x.cast_unsigned() as usize;
let pixels = image.pixels_mut();
pixels[index] = src_over(pixels[index], pixel);
}
}
}