use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io;
use std::os::unix::io::{AsFd, BorrowedFd};
use drm::control::property;
use drm::control::{
self, AtomicCommitFlags, Device as ControlDevice, Mode, PlaneType, atomic, connector, crtc,
};
use drm::{ClientCapability, Device};
use crate::console::Console;
pub struct Card(File);
impl AsFd for Card {
fn as_fd(&self) -> BorrowedFd<'_> {
self.0.as_fd()
}
}
impl Device for Card {}
impl ControlDevice for Card {}
impl Card {
pub fn open(path: &str) -> io::Result<Self> {
let card = Card(OpenOptions::new().read(true).write(true).open(path)?);
card.set_client_capability(ClientCapability::UniversalPlanes, true)?;
card.set_client_capability(ClientCapability::Atomic, true)?;
Ok(card)
}
}
pub struct DisplaySession {
card: Card,
saved: Option<SavedDisplay>,
console: Option<Console>,
active: bool,
}
impl DisplaySession {
pub fn owns_vt(&self) -> bool {
self.console.as_ref().is_some_and(Console::owns_vt)
}
pub fn new(card: &Card, output: &Output) -> io::Result<Self> {
let card = Card(card.0.try_clone()?);
let mut session = Self { card, saved: None, console: None, active: true };
session.console = Some(Console::acquire()?);
session.saved = Some(SavedDisplay::capture(&session.card, output)?);
Ok(session)
}
pub fn restore(&mut self) {
if self.active {
if let Some(saved) = self.saved.take() {
let result = self.card.atomic_commit(AtomicCommitFlags::ALLOW_MODESET, saved.request);
if let Err(error) = result {
eprintln!("restore display state: {error}");
}
for blob in saved.blobs {
let _ = self.card.destroy_property_blob(blob);
}
}
if let Err(error) = self.card.release_master_lock() {
eprintln!("release DRM master: {error}");
}
self.active = false;
}
drop(self.console.take());
}
}
impl Drop for DisplaySession {
fn drop(&mut self) {
self.restore();
}
}
struct SavedDisplay {
request: atomic::AtomicModeReq,
blobs: Vec<u64>,
}
impl SavedDisplay {
fn capture(card: &Card, output: &Output) -> io::Result<Self> {
let mut saved = Self { request: atomic::AtomicModeReq::new(), blobs: Vec::new() };
saved.object(card, output.connector, &["CRTC_ID"])?;
saved.object(card, output.crtc, &["MODE_ID", "ACTIVE"])?;
for &plane in card.plane_handles()?.iter() {
let info = card.get_plane(plane)?;
if info.crtc() == Some(output.crtc)
|| plane == output.ui_plane || Some(plane) == output.video_plane
{
saved.object(card, plane, &[
"FB_ID", "CRTC_ID", "SRC_X", "SRC_Y", "SRC_W", "SRC_H",
"CRTC_X", "CRTC_Y", "CRTC_W", "CRTC_H",
])?;
}
}
Ok(saved)
}
fn object<H: control::ResourceHandle + Copy>(
&mut self, card: &Card, object: H, names: &[&str],
) -> io::Result<()> {
let values = card.get_properties(object)?;
for (&handle, &value) in values.iter() {
let info = card.get_property(handle)?;
if !info.mutable() || !info.atomic() {
continue;
}
let name = info.name().to_str().unwrap_or("");
if !names.contains(&name) {
continue;
}
let value = if name == "MODE_ID" && value != 0 {
let bytes = card.get_property_blob(value)?;
let copy: u64 = card.create_property_blob(bytes.as_slice())?.into();
self.blobs.push(copy);
copy
} else {
value
};
self.request.add_raw_property(object.into(), handle, value);
}
Ok(())
}
}
pub fn open_primary() -> io::Result<(Card, String)> {
let mut tried = Vec::new();
for n in 0..16 {
let path = format!("/dev/dri/card{n}");
if !std::path::Path::new(&path).exists() {
continue;
}
let reason = match Card::open(&path) {
Ok(card) => match Output::discover(&card) {
Ok(_) => return Ok((card, path)),
Err(e) => format!("{path}: output discovery: {e}"),
},
Err(e) => format!("{path}: open/atomic setup: {e}"),
};
eprintln!("display: skipping {reason}");
tried.push(reason);
}
Err(io::Error::other(format!(
"no KMS display node found (need atomic + a connected output).\n {}\n\
If every card rejected atomic, enable full KMS: dtoverlay=vc4-kms-v3d in config.txt.",
tried.join("\n ")
)))
}
type Props = HashMap<String, property::Info>;
pub struct Output {
pub connector: connector::Handle,
pub crtc: crtc::Handle,
pub mode: Mode,
pub ui_plane: control::plane::Handle,
pub video_plane: Option<control::plane::Handle>,
connector_props: Props,
crtc_props: Props,
ui_plane_props: Props,
video_plane_props: Option<Props>,
}
impl Output {
pub fn scanout_modifiers(&self, card: &Card, format: drm::buffer::DrmFourcc) -> io::Result<Vec<u64>> {
let fourcc = format as u32;
if !card.get_plane(self.ui_plane)?.formats().contains(&fourcc) {
return Err(io::Error::other(format!("UI plane {:?} does not support {format:?}", self.ui_plane)));
}
for (&handle, &value) in card.get_properties(self.ui_plane)?.iter() {
let info = card.get_property(handle)?;
if info.name().to_bytes() == b"IN_FORMATS" {
if value == 0 {
return Err(io::Error::other("UI plane IN_FORMATS has no modifier blob"));
}
return parse_scanout_modifiers(&card.get_property_blob(value)?, fourcc);
}
}
Err(io::Error::other("UI plane has no IN_FORMATS; explicit scanout modifiers are unavailable"))
}
pub fn discover(card: &Card) -> io::Result<Output> {
let res = card.resource_handles()?;
let connector = res
.connectors()
.iter()
.filter_map(|&c| card.get_connector(c, true).ok())
.find(|c| c.state() == connector::State::Connected)
.ok_or_else(|| io::Error::other("no connected connector"))?;
let mode = preferred_mode(connector.modes())
.ok_or_else(|| io::Error::other("connector has no modes"))?;
let crtc = connector
.current_encoder()
.and_then(|e| card.get_encoder(e).ok())
.and_then(|e| e.crtc())
.or_else(|| res.crtcs().first().copied())
.ok_or_else(|| io::Error::other("no CRTC"))?;
let (primary, overlay) = planes_for_crtc(card, &res, crtc)?;
let ui_plane = overlay.first().copied().or(primary).ok_or_else(|| {
io::Error::other("no usable plane for CRTC")
})?;
let video_plane = if overlay.first().is_some() {
primary.or_else(|| overlay.get(1).copied())
} else {
None
};
let video_plane_props = match video_plane {
Some(p) => Some(props(card, p)?),
None => None,
};
Ok(Output {
connector_props: props(card, connector.handle())?,
crtc_props: props(card, crtc)?,
ui_plane_props: props(card, ui_plane)?,
video_plane_props,
connector: connector.handle(),
crtc,
mode,
ui_plane,
video_plane,
})
}
pub fn modeset(&self, card: &Card, fb: control::framebuffer::Handle) -> io::Result<()> {
let mode_blob = card.create_property_blob(&self.mode)?;
let mut req = atomic::AtomicModeReq::new();
req.add_property(
self.connector,
self.connector_props["CRTC_ID"].handle(),
property::Value::CRTC(Some(self.crtc)),
);
req.add_property(self.crtc, self.crtc_props["MODE_ID"].handle(), mode_blob);
req.add_property(
self.crtc,
self.crtc_props["ACTIVE"].handle(),
property::Value::Boolean(true),
);
let (w, h) = self.mode.size();
self.disable_unused_planes(card, &mut req, false)?;
self.add_plane(&mut req, self.ui_plane, &self.ui_plane_props, fb, w, h);
self.commit_modeset(card, req, mode_blob)
}
pub fn present(&self, card: &Card, fb: control::framebuffer::Handle) -> io::Result<()> {
let (w, h) = self.mode.size();
let mut req = atomic::AtomicModeReq::new();
self.add_plane(&mut req, self.ui_plane, &self.ui_plane_props, fb, w, h);
card.atomic_commit(AtomicCommitFlags::empty(), req)
.map_err(io::Error::other)
}
pub fn modeset_two(
&self,
card: &Card,
video_fb: control::framebuffer::Handle,
video_src: (u16, u16),
ui_fb: control::framebuffer::Handle,
) -> io::Result<()> {
let (req, mode_blob) = self.two_plane_modeset(card, video_fb, video_src, ui_fb)?;
self.commit_modeset(card, req, mode_blob)
}
pub fn test_two(
&self,
card: &Card,
video_fb: control::framebuffer::Handle,
video_src: (u16, u16),
ui_fb: control::framebuffer::Handle,
) -> io::Result<()> {
let (req, mode_blob) = self.two_plane_modeset(card, video_fb, video_src, ui_fb)?;
let result = card.atomic_commit(AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY, req);
let _ = card.destroy_property_blob(mode_blob.into());
result
}
fn two_plane_modeset(
&self,
card: &Card,
video_fb: control::framebuffer::Handle,
video_src: (u16, u16),
ui_fb: control::framebuffer::Handle,
) -> io::Result<(atomic::AtomicModeReq, property::Value<'static>)> {
let mode_blob = card.create_property_blob(&self.mode)?;
let mut req = atomic::AtomicModeReq::new();
req.add_property(
self.connector,
self.connector_props["CRTC_ID"].handle(),
property::Value::CRTC(Some(self.crtc)),
);
req.add_property(self.crtc, self.crtc_props["MODE_ID"].handle(), mode_blob);
req.add_property(
self.crtc,
self.crtc_props["ACTIVE"].handle(),
property::Value::Boolean(true),
);
self.disable_unused_planes(card, &mut req, true)?;
self.add_two_planes(&mut req, video_fb, video_src, ui_fb)?;
Ok((req, mode_blob))
}
pub fn present_two(
&self,
card: &Card,
video_fb: control::framebuffer::Handle,
video_src: (u16, u16),
ui_fb: control::framebuffer::Handle,
) -> io::Result<()> {
let mut req = atomic::AtomicModeReq::new();
self.add_two_planes(&mut req, video_fb, video_src, ui_fb)?;
card.atomic_commit(AtomicCommitFlags::empty(), req)
.map_err(io::Error::other)
}
fn add_two_planes(
&self,
req: &mut atomic::AtomicModeReq,
video_fb: control::framebuffer::Handle,
video_src: (u16, u16),
ui_fb: control::framebuffer::Handle,
) -> io::Result<()> {
let vp = self.video_plane.ok_or_else(|| io::Error::other("no video plane"))?;
let vprops = self.video_plane_props.as_ref().unwrap();
self.add_plane(req, vp, vprops, video_fb, video_src.0, video_src.1);
let (w, h) = self.mode.size();
self.add_plane(req, self.ui_plane, &self.ui_plane_props, ui_fb, w, h);
Ok(())
}
fn commit_modeset(
&self, card: &Card, req: atomic::AtomicModeReq, blob: property::Value<'static>,
) -> io::Result<()> {
let result = card.atomic_commit(
AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY, req.clone(),
).and_then(|_| card.atomic_commit(AtomicCommitFlags::ALLOW_MODESET, req));
let _ = card.destroy_property_blob(blob.into());
result
}
fn disable_unused_planes(
&self, card: &Card, req: &mut atomic::AtomicModeReq, video: bool,
) -> io::Result<()> {
for &plane in card.plane_handles()?.iter() {
if plane == self.ui_plane || (video && Some(plane) == self.video_plane) {
continue;
}
if card.get_plane(plane)?.crtc() != Some(self.crtc) {
continue;
}
let properties = props(card, plane)?;
req.add_property(plane, properties["FB_ID"].handle(), property::Value::Framebuffer(None));
req.add_property(plane, properties["CRTC_ID"].handle(), property::Value::CRTC(None));
}
Ok(())
}
fn add_plane(
&self,
req: &mut atomic::AtomicModeReq,
plane: control::plane::Handle,
props: &Props,
fb: control::framebuffer::Handle,
src_w: u16,
src_h: u16,
) {
let (w, h) = self.mode.size();
let set = |req: &mut atomic::AtomicModeReq, name: &str, v: property::Value<'static>| {
req.add_property(plane, props[name].handle(), v);
};
set(req, "FB_ID", property::Value::Framebuffer(Some(fb)));
set(req, "CRTC_ID", property::Value::CRTC(Some(self.crtc)));
set(req, "SRC_X", property::Value::UnsignedRange(0));
set(req, "SRC_Y", property::Value::UnsignedRange(0));
set(req, "SRC_W", property::Value::UnsignedRange((src_w as u64) << 16));
set(req, "SRC_H", property::Value::UnsignedRange((src_h as u64) << 16));
set(req, "CRTC_X", property::Value::SignedRange(0));
set(req, "CRTC_Y", property::Value::SignedRange(0));
set(req, "CRTC_W", property::Value::UnsignedRange(w as u64));
set(req, "CRTC_H", property::Value::UnsignedRange(h as u64));
}
}
fn preferred_mode(modes: &[Mode]) -> Option<Mode> {
modes
.iter()
.find(|m| m.mode_type().contains(drm::control::ModeTypeFlags::PREFERRED))
.or_else(|| modes.first())
.copied()
}
fn props(card: &Card, handle: impl drm::control::ResourceHandle) -> io::Result<Props>
where
Card: drm::control::Device,
{
card.get_properties(handle)?
.as_hashmap(card)
.map_err(io::Error::other)
}
fn planes_for_crtc(
card: &Card,
res: &control::ResourceHandles,
crtc: crtc::Handle,
) -> io::Result<(Option<control::plane::Handle>, Vec<control::plane::Handle>)> {
let mut primary = None;
let mut overlays = Vec::new();
for &plane in card.plane_handles()?.iter() {
let Ok(info) = card.get_plane(plane) else {
continue;
};
if info.crtc().is_some_and(|active| active != crtc) {
continue;
}
if !res.filter_crtcs(info.possible_crtcs()).contains(&crtc) {
continue;
}
match plane_type(card, plane) {
Some(PlaneType::Primary) => primary = Some(plane),
Some(PlaneType::Overlay) => overlays.push(plane),
_ => {} }
}
Ok((primary, overlays))
}
fn parse_scanout_modifiers(blob: &[u8], fourcc: u32) -> io::Result<Vec<u64>> {
fn invalid() -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, "malformed UI plane IN_FORMATS blob")
}
fn word(bytes: &[u8], offset: usize) -> io::Result<u32> {
Ok(u32::from_ne_bytes(bytes.get(offset..offset + 4).ok_or_else(invalid)?.try_into().unwrap()))
}
fn region(blob: &[u8], count: u32, offset: u32, stride: usize) -> io::Result<std::ops::Range<usize>> {
let start = offset as usize;
let length = (count as usize).checked_mul(stride).ok_or_else(invalid)?;
let end = start.checked_add(length).ok_or_else(invalid)?;
if start < 24 || start % 4 != 0 || end > blob.len() { return Err(invalid()); }
Ok(start..end)
}
if blob.len() < 24 || word(blob, 0)? != 1 || word(blob, 4)? != 0 {
return Err(invalid());
}
let count_formats = word(blob, 8)?;
let formats = region(blob, count_formats, word(blob, 12)?, 4)?;
let modifiers = region(blob, word(blob, 16)?, word(blob, 20)?, 24)?;
if formats.start < modifiers.end && modifiers.start < formats.end { return Err(invalid()); }
let mut result = Vec::new();
for record in blob[modifiers].chunks_exact(24) {
let mask = u64::from_ne_bytes(record[..8].try_into().unwrap());
let offset = word(record, 8)? as usize;
if word(record, 12)? != 0 { return Err(invalid()); }
let modifier = u64::from_ne_bytes(record[16..24].try_into().unwrap());
for bit in 0..64 {
if mask & (1u64 << bit) == 0 { continue; }
let index = offset.checked_add(bit).ok_or_else(invalid)?;
if index >= count_formats as usize { return Err(invalid()); }
if word(&blob[formats.clone()], index * 4)? == fourcc
&& modifier != u64::from(drm::buffer::DrmModifier::Invalid)
&& !result.contains(&modifier)
{
result.push(modifier);
}
}
}
if result.is_empty() {
return Err(io::Error::other(format!("UI plane has no explicit scanout modifier for FourCC {fourcc:#010x}")));
}
Ok(result)
}
fn plane_type(card: &Card, plane: control::plane::Handle) -> Option<PlaneType> {
let props = card.get_properties(plane).ok()?;
for (&id, &val) in props.iter() {
let info = card.get_property(id).ok()?;
if info.name().to_str().ok() == Some("type") {
return match val {
v if v == PlaneType::Primary as u64 => Some(PlaneType::Primary),
v if v == PlaneType::Overlay as u64 => Some(PlaneType::Overlay),
v if v == PlaneType::Cursor as u64 => Some(PlaneType::Cursor),
_ => None,
};
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn format_blob(formats: &[u32], records: &[(u64, u32, u64)]) -> Vec<u8> {
let modifiers_offset = 24 + formats.len() * 4;
let mut blob = Vec::new();
for value in [1, 0, formats.len() as u32, 24, records.len() as u32, modifiers_offset as u32] {
blob.extend(value.to_ne_bytes());
}
for format in formats { blob.extend(format.to_ne_bytes()); }
for &(mask, offset, modifier) in records {
blob.extend(mask.to_ne_bytes());
blob.extend(offset.to_ne_bytes());
blob.extend(0u32.to_ne_bytes());
blob.extend(modifier.to_ne_bytes());
}
blob
}
#[test]
fn modifier_masks_use_sparse_bits_and_sliding_offsets() {
let formats: Vec<u32> = (0..140).collect();
let blob = format_blob(&formats, &[(5, 0, 10), (5, 64, 20), (1 << 63, 64, 30), (1, 130, 40)]);
assert_eq!(parse_scanout_modifiers(&blob, 0).unwrap(), [10]);
assert!(parse_scanout_modifiers(&blob, 1).is_err());
assert_eq!(parse_scanout_modifiers(&blob, 2).unwrap(), [10]);
assert_eq!(parse_scanout_modifiers(&blob, 66).unwrap(), [20]);
assert_eq!(parse_scanout_modifiers(&blob, 127).unwrap(), [30]);
assert_eq!(parse_scanout_modifiers(&blob, 130).unwrap(), [40]);
}
#[test]
fn explicit_linear_is_distinct_from_invalid_and_is_deduplicated() {
let invalid = u64::from(drm::buffer::DrmModifier::Invalid);
let blob = format_blob(&[42], &[(1, 0, invalid), (1, 0, 0), (1, 0, 0)]);
assert_eq!(parse_scanout_modifiers(&blob, 42).unwrap(), [0]);
assert!(parse_scanout_modifiers(&format_blob(&[42], &[(1, 0, invalid)]), 42).is_err());
}
#[test]
fn malformed_modifier_blobs_are_rejected() {
let blob = format_blob(&[42], &[(1, 0, 0)]);
for length in 0..blob.len() {
assert_eq!(parse_scanout_modifiers(&blob[..length], 42).unwrap_err().kind(), io::ErrorKind::InvalidData);
}
for (offset, value) in [(0, 2u32), (4, 1), (8, u32::MAX), (12, 0), (12, 25), (16, u32::MAX), (20, 24), (20, u32::MAX), (36, 1), (40, 1)] {
let mut malformed = blob.clone();
malformed[offset..offset + 4].copy_from_slice(&value.to_ne_bytes());
assert_eq!(parse_scanout_modifiers(&malformed, 42).unwrap_err().kind(), io::ErrorKind::InvalidData);
}
let outside_mask = format_blob(&[42], &[(2, 0, 0)]);
assert_eq!(parse_scanout_modifiers(&outside_mask, 42).unwrap_err().kind(), io::ErrorKind::InvalidData);
}
#[test]
fn unsupported_format_has_no_modifier() {
let blob = format_blob(&[42], &[(1, 0, 0)]);
let error = parse_scanout_modifiers(&blob, 43).unwrap_err();
assert!(error.to_string().contains("no explicit scanout modifier"));
}
}