use drmkit_core::{Device, Result};
use drmkit_core::{ObjectType, PropertyStore};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Colorspace {
Default,
Smpte170mYcc,
Bt709Ycc,
XvYcc601,
XvYcc709,
SYcc601,
OpYcc601,
OpRgb,
Bt2020Cycc,
Bt2020Rgb,
Bt2020Ycc,
DciP3RgbD65,
DciP3RgbTheater,
RgbWideFixed,
RgbWideFloat,
}
impl Colorspace {
pub const ALL: [Self; 15] = [
Self::Default,
Self::Smpte170mYcc,
Self::Bt709Ycc,
Self::XvYcc601,
Self::XvYcc709,
Self::SYcc601,
Self::OpYcc601,
Self::OpRgb,
Self::Bt2020Cycc,
Self::Bt2020Rgb,
Self::Bt2020Ycc,
Self::DciP3RgbD65,
Self::DciP3RgbTheater,
Self::RgbWideFixed,
Self::RgbWideFloat,
];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Default => "Default",
Self::Smpte170mYcc => "SMPTE_170M_YCC",
Self::Bt709Ycc => "BT709_YCC",
Self::XvYcc601 => "XVYCC_601",
Self::XvYcc709 => "XVYCC_709",
Self::SYcc601 => "SYCC_601",
Self::OpYcc601 => "opYCC_601",
Self::OpRgb => "opRGB",
Self::Bt2020Cycc => "BT2020_CYCC",
Self::Bt2020Rgb => "BT2020_RGB",
Self::Bt2020Ycc => "BT2020_YCC",
Self::DciP3RgbD65 => "DCI-P3_RGB_D65",
Self::DciP3RgbTheater => "DCI-P3_RGB_Theater",
Self::RgbWideFixed => "RGB_Wide_Gamut_Fixed_Point",
Self::RgbWideFloat => "RGB_Wide_Gamut_Floating_Point",
}
}
const fn index(self) -> usize {
self as usize
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum BroadcastRgb {
Automatic,
Full,
Limited,
}
impl BroadcastRgb {
pub const ALL: [Self; 3] = [Self::Automatic, Self::Full, Self::Limited];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Automatic => "Automatic",
Self::Full => "Full",
Self::Limited => "Limited 16:235",
}
}
const fn index(self) -> usize {
self as usize
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(
clippy::exhaustive_structs,
reason = "a value defined by its fields; there is nothing to add to it"
)]
pub struct MaxBpc {
pub min: u64,
pub max: u64,
pub current: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConnectorCapabilities {
pub connector_id: u32,
colorspace: [Option<u64>; Colorspace::ALL.len()],
broadcast_rgb: [Option<u64>; BroadcastRgb::ALL.len()],
pub max_bpc: Option<MaxBpc>,
pub has_hdr_output_metadata: bool,
}
impl ConnectorCapabilities {
#[must_use]
pub const fn new(connector_id: u32) -> Self {
Self {
connector_id,
colorspace: [None; Colorspace::ALL.len()],
broadcast_rgb: [None; BroadcastRgb::ALL.len()],
max_bpc: None,
has_hdr_output_metadata: false,
}
}
#[must_use]
pub const fn with_colorspace(mut self, entry: Colorspace, value: u64) -> Self {
self.colorspace[entry.index()] = Some(value);
self
}
#[must_use]
pub const fn with_broadcast_rgb(mut self, entry: BroadcastRgb, value: u64) -> Self {
self.broadcast_rgb[entry.index()] = Some(value);
self
}
#[must_use]
pub const fn with_max_bpc(mut self, max_bpc: MaxBpc) -> Self {
self.max_bpc = Some(max_bpc);
self
}
#[must_use]
pub const fn with_hdr_output_metadata(mut self, present: bool) -> Self {
self.has_hdr_output_metadata = present;
self
}
pub fn probe(device: &Device, connector_id: u32) -> Result<Self> {
let mut properties = PropertyStore::new();
properties.cache_properties(device, connector_id, ObjectType::Connector)?;
Ok(Self::from_properties(device, &properties, connector_id))
}
#[must_use]
pub fn from_properties(device: &Device, properties: &PropertyStore, connector_id: u32) -> Self {
let mut colorspace = [None; Colorspace::ALL.len()];
for entry in Colorspace::ALL {
colorspace[entry.index()] = properties
.enum_value(device, connector_id, "Colorspace", entry.name())
.ok();
}
let mut broadcast_rgb = [None; BroadcastRgb::ALL.len()];
for entry in BroadcastRgb::ALL {
broadcast_rgb[entry.index()] = properties
.enum_value(device, connector_id, "Broadcast RGB", entry.name())
.ok();
}
let max_bpc = properties
.range(connector_id, "max bpc")
.ok()
.flatten()
.and_then(|(min, max)| {
Some(MaxBpc {
min,
max,
current: properties.property_value(connector_id, "max bpc").ok()?,
})
});
Self {
connector_id,
colorspace,
broadcast_rgb,
max_bpc,
has_hdr_output_metadata: properties
.property_id(connector_id, "HDR_OUTPUT_METADATA")
.is_ok(),
}
}
#[must_use]
pub const fn colorspace(&self, entry: Colorspace) -> Option<u64> {
self.colorspace[entry.index()]
}
#[must_use]
pub const fn broadcast_rgb(&self, entry: BroadcastRgb) -> Option<u64> {
self.broadcast_rgb[entry.index()]
}
#[must_use]
pub fn has_colorspace(&self) -> bool {
self.colorspace.iter().any(Option::is_some)
}
#[must_use]
pub fn has_broadcast_rgb(&self) -> bool {
self.broadcast_rgb.iter().any(Option::is_some)
}
#[must_use]
pub fn can_signal_hdr(&self) -> bool {
self.has_hdr_output_metadata && self.max_bpc.is_some_and(|bpc| bpc.max >= 10)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct CrtcCapabilities {
pub crtc_id: u32,
pub degamma_lut_size: u32,
pub gamma_lut_size: u32,
pub has_degamma_lut: bool,
pub has_ctm: bool,
pub has_gamma_lut: bool,
}
impl CrtcCapabilities {
pub fn probe(device: &Device, crtc_id: u32) -> Result<Self> {
let mut properties = PropertyStore::new();
properties.cache_properties(device, crtc_id, ObjectType::Crtc)?;
Ok(Self::from_properties(&properties, crtc_id))
}
#[must_use]
pub fn from_properties(properties: &PropertyStore, crtc_id: u32) -> Self {
let size = |name: &str| {
properties
.property_value(crtc_id, name)
.ok()
.and_then(|value| u32::try_from(value).ok())
.unwrap_or(0)
};
let has = |name: &str| properties.property_id(crtc_id, name).is_ok();
Self {
crtc_id,
degamma_lut_size: size("DEGAMMA_LUT_SIZE"),
gamma_lut_size: size("GAMMA_LUT_SIZE"),
has_degamma_lut: has("DEGAMMA_LUT"),
has_ctm: has("CTM"),
has_gamma_lut: has("GAMMA_LUT"),
}
}
#[must_use]
pub const fn has_full_pipeline(&self) -> bool {
self.has_degamma_lut && self.has_ctm && self.has_gamma_lut
}
}
#[cfg(test)]
mod tests {
use super::{BroadcastRgb, Colorspace, ConnectorCapabilities, CrtcCapabilities, MaxBpc};
fn amdgpu() -> ConnectorCapabilities {
ConnectorCapabilities::new(383)
.with_colorspace(Colorspace::Default, 0)
.with_colorspace(Colorspace::Bt709Ycc, 1)
.with_colorspace(Colorspace::OpRgb, 2)
.with_colorspace(Colorspace::Bt2020Rgb, 3)
.with_colorspace(Colorspace::Bt2020Ycc, 4)
.with_max_bpc(MaxBpc {
min: 8,
max: 16,
current: 16,
})
.with_hdr_output_metadata(true)
}
#[test]
fn an_entry_the_driver_does_not_have_is_absent_rather_than_zero() {
let connector = amdgpu();
assert_eq!(connector.colorspace(Colorspace::Default), Some(0));
assert_eq!(connector.colorspace(Colorspace::DciP3RgbD65), None);
}
#[test]
fn every_entry_is_stored_under_its_own_name() {
let mut connector = ConnectorCapabilities::new(1);
for (value, entry) in Colorspace::ALL.into_iter().enumerate() {
connector = connector.with_colorspace(entry, value as u64);
}
for (value, entry) in Colorspace::ALL.into_iter().enumerate() {
assert_eq!(
connector.colorspace(entry),
Some(value as u64),
"{}",
entry.name()
);
}
}
#[test]
fn broadcast_rgb_entries_are_stored_the_same_way() {
let mut connector = ConnectorCapabilities::new(1);
for (value, entry) in BroadcastRgb::ALL.into_iter().enumerate() {
connector = connector.with_broadcast_rgb(entry, 10 + value as u64);
}
for (value, entry) in BroadcastRgb::ALL.into_iter().enumerate() {
assert_eq!(connector.broadcast_rgb(entry), Some(10 + value as u64));
}
assert!(connector.has_broadcast_rgb());
}
#[test]
fn hdr_needs_the_metadata_and_the_bit_depth() {
assert!(amdgpu().can_signal_hdr());
assert!(
!ConnectorCapabilities::new(1)
.with_hdr_output_metadata(true)
.with_max_bpc(MaxBpc {
min: 6,
max: 8,
current: 8,
})
.can_signal_hdr()
);
assert!(
!ConnectorCapabilities::new(1)
.with_max_bpc(MaxBpc {
min: 8,
max: 10,
current: 8,
})
.can_signal_hdr()
);
assert!(
!ConnectorCapabilities::new(1)
.with_hdr_output_metadata(true)
.can_signal_hdr()
);
}
#[test]
fn the_current_bpc_is_what_is_set_not_what_is_possible() {
let connector = ConnectorCapabilities::new(1)
.with_hdr_output_metadata(true)
.with_max_bpc(MaxBpc {
min: 8,
max: 16,
current: 8,
});
assert!(connector.can_signal_hdr());
assert_eq!(connector.max_bpc.map(|bpc| bpc.current), Some(8));
}
#[test]
fn a_connector_with_nothing_on_it_says_so() {
let connector = ConnectorCapabilities::new(7);
assert!(!connector.has_colorspace());
assert!(!connector.has_broadcast_rgb());
assert!(!connector.can_signal_hdr());
assert_eq!(connector.connector_id, 7);
}
#[test]
fn a_partial_color_pipeline_is_not_a_full_one() {
let vkms = CrtcCapabilities {
crtc_id: 34,
gamma_lut_size: 256,
has_gamma_lut: true,
..CrtcCapabilities::default()
};
assert!(!vkms.has_full_pipeline());
let amdgpu = CrtcCapabilities {
crtc_id: 369,
degamma_lut_size: 4096,
gamma_lut_size: 4096,
has_degamma_lut: true,
has_ctm: true,
has_gamma_lut: true,
};
assert!(amdgpu.has_full_pipeline());
}
}