use std::fmt::Write;
use std::io;
use drm::control::{Device as ControlDevice, property};
use super::{Card, Output, planes_for_crtc, plane_type};
pub fn describe(card: &Card, out: &Output) -> io::Result<String> {
let mut s = String::new();
let conn = card.get_connector(out.connector, false)?;
let _ = writeln!(s, "connector {:?}-{} crtc {:?}", conn.interface(), conn.interface_id(), out.crtc);
for (&id, &value) in card.get_properties(out.connector)?.iter() {
let info = card.get_property(id)?;
match info.name().to_bytes() {
b"max bpc" | b"Colorspace" | b"HDR_OUTPUT_METADATA" | b"Broadcast RGB" | b"Output format" => {
let _ = writeln!(s, " {}", prop_line(&info, value));
}
b"EDID" if value != 0 => {
let _ = writeln!(s, " EDID: {}", edid_hdr(&card.get_property_blob(value)?));
}
_ => {}
}
}
let res = card.resource_handles()?;
let (primary, overlays) = planes_for_crtc(card, &res, out.crtc)?;
for plane in primary.into_iter().chain(overlays) {
let role = match plane {
p if p == out.ui_plane => " (ui)",
p if Some(p) == out.video_plane => " (video)",
_ => "",
};
let _ = writeln!(s, "plane {:?} {:?}{role}", plane, plane_type(card, plane));
let mut formats = Vec::new();
for (&id, &value) in card.get_properties(plane)?.iter() {
let info = card.get_property(id)?;
match info.name().to_bytes() {
b"zpos" | b"COLOR_ENCODING" | b"COLOR_RANGE" | b"alpha" | b"pixel blend mode" => {
let _ = writeln!(s, " {}", prop_line(&info, value));
}
b"IN_FORMATS" if value != 0 => formats = format_modifiers(&card.get_property_blob(value)?),
_ => {}
}
}
if formats.is_empty() {
formats = card.get_plane(plane)?.formats().iter().map(|&f| (f, Vec::new())).collect();
}
let mut groups: Vec<(Vec<u64>, Vec<String>)> = Vec::new();
for (fourcc, modifiers) in formats {
match groups.iter_mut().find(|g| g.0 == modifiers) {
Some(group) => group.1.push(fourcc_name(fourcc)),
None => groups.push((modifiers, vec![fourcc_name(fourcc)])),
}
}
for (modifiers, names) in groups {
let mods: Vec<String> = modifiers.iter().map(|m| format!("{m:#x}")).collect();
let _ = writeln!(s, " {}: [{}]", names.join(" "), mods.join(" "));
}
}
Ok(s)
}
fn prop_line(info: &property::Info, value: u64) -> String {
let name = info.name().to_string_lossy();
let mutable = if info.mutable() { "" } else { " immutable" };
match info.value_type() {
property::ValueType::UnsignedRange(min, max) => format!("{name}: {value} ({min}..{max}){mutable}"),
property::ValueType::Enum(values) => {
let names: Vec<String> = values.values().1.iter()
.map(|e| {
let mark = if e.value() == value { "*" } else { "" };
format!("{mark}{}", e.name().to_string_lossy())
})
.collect();
format!("{name}: {}{mutable}", names.join(" | "))
}
property::ValueType::Blob => format!("{name}: blob {value}{mutable}"),
_ => format!("{name}: {value}{mutable}"),
}
}
fn fourcc_name(fourcc: u32) -> String {
fourcc.to_le_bytes().iter().map(|&b| if b.is_ascii_graphic() { b as char } else { '?' }).collect()
}
fn format_modifiers(blob: &[u8]) -> Vec<(u32, Vec<u64>)> {
let word = |o: usize| blob.get(o..o + 4).map(|b| u32::from_ne_bytes(b.try_into().unwrap()) as usize);
let (Some(nf), Some(fo), Some(nm), Some(mo)) = (word(8), word(12), word(16), word(20)) else {
return Vec::new();
};
let mut out: Vec<(u32, Vec<u64>)> = (0..nf).filter_map(|i| Some((word(fo + i * 4)? as u32, Vec::new()))).collect();
for i in 0..nm {
let Some(rec) = blob.get(mo + i * 24..mo + i * 24 + 24) else { break };
let mask = u64::from_ne_bytes(rec[..8].try_into().unwrap());
let offset = u32::from_ne_bytes(rec[8..12].try_into().unwrap()) as usize;
let modifier = u64::from_ne_bytes(rec[16..24].try_into().unwrap());
for bit in (0..64).filter(|b| mask & (1u64 << b) != 0) {
if let Some(entry) = out.get_mut(offset + bit) {
entry.1.push(modifier);
}
}
}
out
}
fn edid_hdr(edid: &[u8]) -> String {
let sink = super::hdr::SinkHdr::parse(edid);
let list = |bits: u8, names: &[(u8, &str)]| -> String {
names.iter().filter(|n| bits & (1 << n.0) != 0).map(|n| n.1).collect::<Vec<_>>().join(" ")
};
let mut parts = Vec::new();
if sink.colorimetry != 0 {
parts.push(format!("colorimetry [{}]", list(sink.colorimetry, &[(5, "BT2020cYCC"), (6, "BT2020YCC"), (7, "BT2020RGB")])));
}
if sink.eotfs != 0 {
let mut hdr = format!("EOTF [{}]", list(sink.eotfs, &[(0, "SDR"), (1, "HDR-gamma"), (2, "PQ"), (3, "HLG")]));
if let Some(cv) = sink.max_luminance_cv {
let _ = write!(hdr, " max {:.0} cd/m2", 50.0 * 2f64.powf(f64::from(cv) / 32.0));
}
parts.push(hdr);
}
if parts.is_empty() { "no HDR data blocks".into() } else { parts.join(", ") }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn edid_hdr_blocks() {
let mut edid = vec![0u8; 256];
edid[128] = 0x02;
let blocks = [0xe3, 5, 0xc0, 0, 0xe4, 6, 0x05, 1, 96];
edid[132..132 + blocks.len()].copy_from_slice(&blocks);
edid[130] = 4 + blocks.len() as u8;
assert_eq!(edid_hdr(&edid), "colorimetry [BT2020YCC BT2020RGB], EOTF [SDR PQ] max 400 cd/m2");
assert_eq!(edid_hdr(&edid[..128]), "no HDR data blocks");
}
#[test]
fn modifiers_per_format() {
let mut blob = Vec::new();
for v in [1u32, 0, 2, 24, 1, 32] { blob.extend(v.to_ne_bytes()); }
for f in [u32::from_le_bytes(*b"NV12"), u32::from_le_bytes(*b"P030")] { blob.extend(f.to_ne_bytes()); }
blob.extend(2u64.to_ne_bytes());
blob.extend(0u32.to_ne_bytes());
blob.extend(0u32.to_ne_bytes());
blob.extend(7u64.to_ne_bytes());
let parsed = format_modifiers(&blob);
assert_eq!(parsed, [(u32::from_le_bytes(*b"NV12"), vec![]), (u32::from_le_bytes(*b"P030"), vec![7])]);
assert_eq!(fourcc_name(parsed[1].0), "P030");
assert!(format_modifiers(&blob[..10]).is_empty());
}
}