medias 0.6.0

TV media player on bare DRM/KMS: Vulkan UI plane over libmpv video, for local files, UPnP/DLNA servers and YouTube, controlled by HDMI-CEC remote, evdev or terminal
//! `medias planes`: read-only dump of the connector's HDR-related properties
//! and of each CRTC plane's type, zpos, color properties, formats and modifiers.
//! Needs no DRM master.

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();
        }
        // One line per distinct modifier list.
        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()
}

/// (format, modifiers) from a drm_format_modifier_blob; empty when malformed.
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
}

/// The EDID's HDR static metadata and colorimetry blocks, as text.
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;
        // colorimetry: tag 7 len 3, ext 5, BT2020YCC|BT2020RGB; HDR: tag 7 len 4, ext 6, SDR|PQ, type 1, cv 96
        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());
    }
}