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 mod hdr;
pub mod libdrm;
pub mod probe;
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", "HDR_OUTPUT_METADATA", "Colorspace", "max bpc"])?;
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", "COLOR_ENCODING", "COLOR_RANGE",
])?;
}
}
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 base_mode: Mode,
pub rate_modes: Vec<Mode>,
pub ui_plane: control::plane::Handle,
pub video_plane: Option<control::plane::Handle>,
pub direct_plane: Option<control::plane::Handle>,
pub hdr: Option<hdr::Hdr>,
sink_hdr: hdr::SinkHdr,
colorspace: Option<(u64, u64)>,
max_bpc: Option<(u64, u64)>,
direct_plane_props: Option<Props>,
direct_color: Vec<(u32, u64)>,
connector_props: Props,
crtc_props: Props,
ui_plane_props: Props,
video_plane_props: Option<Props>,
zpos_sets: Vec<(control::plane::Handle, property::Handle, u64)>,
}
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 mode_for_fps(&self, fps: f64) -> Mode {
let rates: Vec<f64> = self.rate_modes.iter().map(refresh_hz).collect();
pick_rate(&rates, fps).map_or(self.base_mode, |i| self.rate_modes[i])
}
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 rate_modes = connector
.modes()
.iter()
.filter(|m| m.size() == mode.size())
.filter(|m| !m.flags().intersects(control::ModeFlags::INTERLACE | control::ModeFlags::DBLSCAN))
.copied()
.collect();
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 mut ui_plane = overlay.first().copied().or(primary).ok_or_else(|| {
io::Error::other("no usable plane for CRTC")
})?;
let mut video_plane = if overlay.first().is_some() {
primary.or_else(|| overlay.get(1).copied())
} else {
None
};
let mut zpos_sets = Vec::new();
if let Some(vp) = video_plane
&& let (Some(u), Some(v)) = (zpos(card, ui_plane), zpos(card, vp))
{
let (u, v) = if u.top() < v.bottom() {
(ui_plane, video_plane) = (vp, Some(ui_plane));
(v, u)
} else {
(u, v)
};
if v.mutable { zpos_sets.push((video_plane.unwrap(), v.handle, v.min)); }
if u.mutable { zpos_sets.push((ui_plane, u.handle, u.max)); }
}
let video_plane_props = match video_plane {
Some(p) => Some(props(card, p)?),
None => None,
};
let connector_props = props(card, connector.handle())?;
let mut sink_hdr = hdr::SinkHdr::default();
let mut max_bpc = None;
for (&id, &value) in card.get_properties(connector.handle())?.iter() {
let info = card.get_property(id)?;
match (info.name().to_bytes(), info.value_type()) {
(b"EDID", _) if value != 0 => sink_hdr = hdr::SinkHdr::parse(&card.get_property_blob(value)?),
(b"max bpc", property::ValueType::UnsignedRange(_, max)) => max_bpc = Some((value, *max)),
_ => {}
}
}
let colorspace = enum_value(&connector_props, "Colorspace", "Default").zip(
enum_value(&connector_props, "Colorspace", "BT2020_RGB")
.or_else(|| enum_value(&connector_props, "Colorspace", "BT2020_YCC")),
);
Ok(Output {
connector_props,
hdr: None,
sink_hdr,
colorspace,
max_bpc,
direct_plane_props: None,
direct_color: Vec::new(),
crtc_props: props(card, crtc)?,
ui_plane_props: props(card, ui_plane)?,
video_plane_props,
connector: connector.handle(),
crtc,
mode,
base_mode: mode,
rate_modes,
ui_plane,
video_plane,
direct_plane: None,
zpos_sets,
})
}
pub fn direct_verified(&self, card: &Card) -> bool {
self.video_plane
.and_then(|p| card.get_plane(p).ok())
.is_some_and(|info| info.formats().contains(&u32::from_le_bytes(*b"P030")))
}
pub fn enable_direct(&mut self, card: &Card) -> io::Result<(usize, usize)> {
let err = |m: &str| io::Error::other(m.to_string());
let res = card.resource_handles()?;
let (primary, overlays) = planes_for_crtc(card, &res, self.crtc)?;
let primary = primary.filter(|&p| Some(p) == self.video_plane)
.ok_or_else(|| err("the video plane is not the primary plane"))?;
let gl = overlays.iter().copied().find(|&p| p != self.ui_plane)
.ok_or_else(|| err("no second overlay plane"))?;
if !card.get_plane(gl)?.formats().contains(&(drm::buffer::DrmFourcc::Argb8888 as u32)) {
return Err(err("the second overlay plane lacks ARGB8888"));
}
let current = card.get_crtc(self.crtc)?.mode().map(|m| m.size());
if current != Some(self.mode.size()) {
return Err(err("the CRTC is not active at the output mode size"));
}
let (z_ui, z_gl) = (zpos(card, self.ui_plane), zpos(card, gl));
let mut sets = Vec::new();
match (z_ui, z_gl) {
(Some(u), Some(g)) if u.mutable && g.mutable && g.min < u.max => {
sets.push((gl, g.handle, g.min));
sets.push((self.ui_plane, u.handle, u.max));
}
(Some(u), Some(g)) if !u.mutable && !g.mutable && g.value < u.value => {}
_ => return Err(err("overlay zpos cannot place the UI above the second overlay")),
}
let mut index = (None, None);
let mut count = 0;
for &plane in card.plane_handles()?.iter() {
let info = card.get_plane(plane)?;
if !res.filter_crtcs(info.possible_crtcs()).contains(&self.crtc)
|| matches!(plane_type(card, plane), Some(PlaneType::Cursor))
{
continue;
}
if plane == gl { index.0 = Some(count); }
if plane == primary { index.1 = Some(count); }
count += 1;
}
let (Some(draw), Some(direct)) = index else { return Err(err("plane index not found")) };
self.video_plane_props = Some(props(card, gl)?);
self.direct_plane_props = Some(props(card, primary)?);
self.video_plane = Some(gl);
self.direct_plane = Some(primary);
self.zpos_sets = sets;
Ok((draw, direct))
}
pub fn hdr_capable(&self, eotf: u8) -> bool {
self.direct_plane.is_some()
&& self.sink_hdr.supports(eotf)
&& self.connector_props.contains_key("HDR_OUTPUT_METADATA")
}
pub fn set_direct_color(&mut self, matrix: &str, full_range: bool) {
let Some(props) = &self.direct_plane_props else { return };
let encoding = match matrix {
"bt.601" => "ITU-R BT.601 YCbCr",
"bt.2020-ncl" | "bt.2020-cl" | "dolbyvision" => "ITU-R BT.2020 YCbCr",
_ => "ITU-R BT.709 YCbCr",
};
let range = if full_range { "YCbCr full range" } else { "YCbCr limited range" };
self.direct_color = [("COLOR_ENCODING", encoding), ("COLOR_RANGE", range)]
.iter()
.filter_map(|(prop, name)| Some((props.get(*prop)?.handle().into(), enum_value(props, prop, name)?)))
.collect();
}
pub fn present_direct(
&self,
card: &Card,
slot: &mut libdrm::RequestSlot,
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 (w, h) = self.mode.size();
for (plane, props, fb, sw, sh) in [
(vp, self.video_plane_props.as_ref().unwrap(), video_fb, video_src.0, video_src.1),
(self.ui_plane, &self.ui_plane_props, ui_fb, w, h),
] {
let (plane_id, fb_id, crtc_id): (u32, u32, u32) = (plane.into(), fb.into(), self.crtc.into());
for (name, value) in [
("FB_ID", u64::from(fb_id)), ("CRTC_ID", u64::from(crtc_id)),
("SRC_X", 0), ("SRC_Y", 0), ("SRC_W", u64::from(sw) << 16), ("SRC_H", u64::from(sh) << 16),
("CRTC_X", 0), ("CRTC_Y", 0), ("CRTC_W", u64::from(w)), ("CRTC_H", u64::from(h)),
] {
slot.add(plane_id, props[name].handle().into(), value)?;
}
}
if let Some(dp) = self.direct_plane {
for &(prop, value) in &self.direct_color {
slot.add(dp.into(), prop, value)?;
}
}
slot.commit(card)
}
pub fn describe_planes(&self) -> String {
let z = |p| self.zpos_sets.iter().find(|s| s.0 == p).map_or("-".into(), |s| s.2.to_string());
let mut s = format!("ui {:?} zpos {}", self.ui_plane, z(self.ui_plane));
if let Some(vp) = self.video_plane {
s += &format!(", video {vp:?} zpos {}", z(vp));
}
if let Some(dp) = self.direct_plane {
s += &format!(", direct scanout {dp:?}");
}
s
}
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, vec![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, blobs) = self.two_plane_modeset(card, video_fb, video_src, ui_fb)?;
self.commit_modeset(card, req, blobs)
}
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, blobs) = self.two_plane_modeset(card, video_fb, video_src, ui_fb)?;
let result = card.atomic_commit(AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY, req);
for blob in blobs {
let _ = card.destroy_property_blob(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, Vec<property::Value<'static>>)> {
let mode_blob = card.create_property_blob(&self.mode)?;
let mut blobs = vec![mode_blob];
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)?;
for &(plane, prop, value) in &self.zpos_sets {
req.add_property(plane, prop, property::Value::UnsignedRange(value));
}
if let Some(info) = self.connector_props.get("HDR_OUTPUT_METADATA").filter(|_| self.direct_plane.is_some()) {
let blob = match &self.hdr {
Some(hdr) => {
let blob = card.create_property_blob(&hdr.blob())?;
blobs.push(blob);
blob
}
None => property::Value::Blob(0),
};
req.add_property(self.connector, info.handle(), blob);
if let (Some(info), Some((default, bt2020))) = (self.connector_props.get("Colorspace"), self.colorspace) {
let value = if self.hdr.is_some() { bt2020 } else { default };
req.add_raw_property(self.connector.into(), info.handle(), value);
}
if let (Some(info), Some((sdr, max))) = (self.connector_props.get("max bpc"), self.max_bpc) {
let value = if self.hdr.is_some() { max.min(12) } else { sdr };
req.add_raw_property(self.connector.into(), info.handle(), value);
}
}
Ok((req, blobs))
}
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, blobs: Vec<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));
for blob in blobs {
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) || Some(plane) == self.direct_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));
}
}
pub fn refresh_hz(mode: &Mode) -> f64 {
let (htotal, vtotal) = (mode.hsync().2 as f64, mode.vsync().2 as f64);
if htotal == 0.0 || vtotal == 0.0 { 0.0 } else { mode.clock() as f64 * 1000.0 / (htotal * vtotal) }
}
pub fn pick_rate(rates: &[f64], fps: f64) -> Option<usize> {
if !(fps > 1.0) {
return None;
}
let error = |r: f64| {
let k = (r / fps).round();
if k < 1.0 { f64::INFINITY } else { (r / (k * fps) - 1.0).abs() }
};
rates
.iter()
.enumerate()
.map(|(i, &r)| (i, r, error(r)))
.filter(|&(.., e)| e < 0.002)
.min_by(|a, b| {
let (ea, eb) = ((a.2 * 1e4).round(), (b.2 * 1e4).round());
ea.total_cmp(&eb).then(b.1.total_cmp(&a.1))
})
.map(|(i, ..)| i)
}
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 enum_value(props: &Props, prop: &str, name: &str) -> Option<u64> {
match props.get(prop)?.value_type() {
property::ValueType::Enum(values) => {
values.values().1.iter().find(|e| e.name().to_bytes() == name.as_bytes()).map(|e| e.value())
}
_ => None,
}
}
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)
}
struct Zpos {
handle: property::Handle,
value: u64,
min: u64,
max: u64,
mutable: bool,
}
impl Zpos {
fn top(&self) -> u64 { if self.mutable { self.max } else { self.value } }
fn bottom(&self) -> u64 { if self.mutable { self.min } else { self.value } }
}
fn zpos(card: &Card, plane: control::plane::Handle) -> Option<Zpos> {
let props = card.get_properties(plane).ok()?;
for (&id, &value) in props.iter() {
let info = card.get_property(id).ok()?;
if info.name().to_bytes() == b"zpos" {
let (min, max) = match *info.value_type() {
property::ValueType::UnsignedRange(a, b) => (a, b),
_ => (value, value),
};
return Some(Zpos { handle: id, value, min, max, mutable: info.mutable() });
}
}
None
}
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::*;
#[test]
fn rate_matches_fps_multiples() {
let rates = [60.0, 59.94006, 50.0, 30.0, 29.97003, 25.0, 24.0, 23.97602];
let pick = |fps| pick_rate(&rates, fps).map(|i| rates[i]);
assert_eq!(pick(23.976024), Some(23.97602));
assert_eq!(pick(24.0), Some(24.0));
assert_eq!(pick(25.0), Some(50.0));
assert_eq!(pick(29.97003), Some(59.94006));
assert_eq!(pick(30.0), Some(60.0));
assert_eq!(pick(59.94006), Some(59.94006));
assert_eq!(pick(50.0), Some(50.0));
assert_eq!(pick(15.0), Some(60.0));
assert_eq!(pick(1.0), None);
assert_eq!(pick(0.0), None);
assert_eq!(pick_rate(&[60.0, 24.0], 23.976024), Some(1));
assert_eq!(pick_rate(&[60.0], 23.976024), None);
assert_eq!(pick_rate(&[60.0], 12.5), None);
}
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"));
}
}