1use drmkit_core::{Device, Result};
13use drmkit_core::{ObjectType, PropertyStore};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
21#[non_exhaustive]
22pub enum Colorspace {
23 Default,
25 Smpte170mYcc,
27 Bt709Ycc,
29 XvYcc601,
31 XvYcc709,
33 SYcc601,
35 OpYcc601,
37 OpRgb,
39 Bt2020Cycc,
41 Bt2020Rgb,
43 Bt2020Ycc,
45 DciP3RgbD65,
47 DciP3RgbTheater,
49 RgbWideFixed,
51 RgbWideFloat,
53}
54
55impl Colorspace {
56 pub const ALL: [Self; 15] = [
58 Self::Default,
59 Self::Smpte170mYcc,
60 Self::Bt709Ycc,
61 Self::XvYcc601,
62 Self::XvYcc709,
63 Self::SYcc601,
64 Self::OpYcc601,
65 Self::OpRgb,
66 Self::Bt2020Cycc,
67 Self::Bt2020Rgb,
68 Self::Bt2020Ycc,
69 Self::DciP3RgbD65,
70 Self::DciP3RgbTheater,
71 Self::RgbWideFixed,
72 Self::RgbWideFloat,
73 ];
74
75 #[must_use]
77 pub const fn name(self) -> &'static str {
78 match self {
79 Self::Default => "Default",
80 Self::Smpte170mYcc => "SMPTE_170M_YCC",
81 Self::Bt709Ycc => "BT709_YCC",
82 Self::XvYcc601 => "XVYCC_601",
83 Self::XvYcc709 => "XVYCC_709",
84 Self::SYcc601 => "SYCC_601",
85 Self::OpYcc601 => "opYCC_601",
86 Self::OpRgb => "opRGB",
87 Self::Bt2020Cycc => "BT2020_CYCC",
88 Self::Bt2020Rgb => "BT2020_RGB",
89 Self::Bt2020Ycc => "BT2020_YCC",
90 Self::DciP3RgbD65 => "DCI-P3_RGB_D65",
91 Self::DciP3RgbTheater => "DCI-P3_RGB_Theater",
92 Self::RgbWideFixed => "RGB_Wide_Gamut_Fixed_Point",
93 Self::RgbWideFloat => "RGB_Wide_Gamut_Floating_Point",
94 }
95 }
96
97 const fn index(self) -> usize {
98 self as usize
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
108#[non_exhaustive]
109pub enum BroadcastRgb {
110 Automatic,
112 Full,
114 Limited,
116}
117
118impl BroadcastRgb {
119 pub const ALL: [Self; 3] = [Self::Automatic, Self::Full, Self::Limited];
121
122 #[must_use]
124 pub const fn name(self) -> &'static str {
125 match self {
126 Self::Automatic => "Automatic",
127 Self::Full => "Full",
128 Self::Limited => "Limited 16:235",
129 }
130 }
131
132 const fn index(self) -> usize {
133 self as usize
134 }
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139#[allow(
140 clippy::exhaustive_structs,
141 reason = "a value defined by its fields; there is nothing to add to it"
142)]
143pub struct MaxBpc {
144 pub min: u64,
146 pub max: u64,
148 pub current: u64,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct ConnectorCapabilities {
155 pub connector_id: u32,
157 colorspace: [Option<u64>; Colorspace::ALL.len()],
165 broadcast_rgb: [Option<u64>; BroadcastRgb::ALL.len()],
167 pub max_bpc: Option<MaxBpc>,
169 pub has_hdr_output_metadata: bool,
171}
172
173impl ConnectorCapabilities {
174 #[must_use]
179 pub const fn new(connector_id: u32) -> Self {
180 Self {
181 connector_id,
182 colorspace: [None; Colorspace::ALL.len()],
183 broadcast_rgb: [None; BroadcastRgb::ALL.len()],
184 max_bpc: None,
185 has_hdr_output_metadata: false,
186 }
187 }
188
189 #[must_use]
191 pub const fn with_colorspace(mut self, entry: Colorspace, value: u64) -> Self {
192 self.colorspace[entry.index()] = Some(value);
193 self
194 }
195
196 #[must_use]
198 pub const fn with_broadcast_rgb(mut self, entry: BroadcastRgb, value: u64) -> Self {
199 self.broadcast_rgb[entry.index()] = Some(value);
200 self
201 }
202
203 #[must_use]
205 pub const fn with_max_bpc(mut self, max_bpc: MaxBpc) -> Self {
206 self.max_bpc = Some(max_bpc);
207 self
208 }
209
210 #[must_use]
212 pub const fn with_hdr_output_metadata(mut self, present: bool) -> Self {
213 self.has_hdr_output_metadata = present;
214 self
215 }
216
217 pub fn probe(device: &Device, connector_id: u32) -> Result<Self> {
228 let mut properties = PropertyStore::new();
229 properties.cache_properties(device, connector_id, ObjectType::Connector)?;
230 Ok(Self::from_properties(device, &properties, connector_id))
231 }
232
233 #[must_use]
236 pub fn from_properties(device: &Device, properties: &PropertyStore, connector_id: u32) -> Self {
237 let mut colorspace = [None; Colorspace::ALL.len()];
238 for entry in Colorspace::ALL {
239 colorspace[entry.index()] = properties
240 .enum_value(device, connector_id, "Colorspace", entry.name())
241 .ok();
242 }
243 let mut broadcast_rgb = [None; BroadcastRgb::ALL.len()];
244 for entry in BroadcastRgb::ALL {
245 broadcast_rgb[entry.index()] = properties
246 .enum_value(device, connector_id, "Broadcast RGB", entry.name())
247 .ok();
248 }
249
250 let max_bpc = properties
251 .range(connector_id, "max bpc")
252 .ok()
253 .flatten()
254 .and_then(|(min, max)| {
255 Some(MaxBpc {
256 min,
257 max,
258 current: properties.property_value(connector_id, "max bpc").ok()?,
259 })
260 });
261
262 Self {
263 connector_id,
264 colorspace,
265 broadcast_rgb,
266 max_bpc,
267 has_hdr_output_metadata: properties
268 .property_id(connector_id, "HDR_OUTPUT_METADATA")
269 .is_ok(),
270 }
271 }
272
273 #[must_use]
275 pub const fn colorspace(&self, entry: Colorspace) -> Option<u64> {
276 self.colorspace[entry.index()]
277 }
278
279 #[must_use]
281 pub const fn broadcast_rgb(&self, entry: BroadcastRgb) -> Option<u64> {
282 self.broadcast_rgb[entry.index()]
283 }
284
285 #[must_use]
287 pub fn has_colorspace(&self) -> bool {
288 self.colorspace.iter().any(Option::is_some)
289 }
290
291 #[must_use]
293 pub fn has_broadcast_rgb(&self) -> bool {
294 self.broadcast_rgb.iter().any(Option::is_some)
295 }
296
297 #[must_use]
305 pub fn can_signal_hdr(&self) -> bool {
306 self.has_hdr_output_metadata && self.max_bpc.is_some_and(|bpc| bpc.max >= 10)
307 }
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
312#[non_exhaustive]
313pub struct CrtcCapabilities {
314 pub crtc_id: u32,
316 pub degamma_lut_size: u32,
318 pub gamma_lut_size: u32,
320 pub has_degamma_lut: bool,
322 pub has_ctm: bool,
324 pub has_gamma_lut: bool,
326}
327
328impl CrtcCapabilities {
329 pub fn probe(device: &Device, crtc_id: u32) -> Result<Self> {
335 let mut properties = PropertyStore::new();
336 properties.cache_properties(device, crtc_id, ObjectType::Crtc)?;
337 Ok(Self::from_properties(&properties, crtc_id))
338 }
339
340 #[must_use]
343 pub fn from_properties(properties: &PropertyStore, crtc_id: u32) -> Self {
344 let size = |name: &str| {
348 properties
349 .property_value(crtc_id, name)
350 .ok()
351 .and_then(|value| u32::try_from(value).ok())
352 .unwrap_or(0)
353 };
354 let has = |name: &str| properties.property_id(crtc_id, name).is_ok();
355
356 Self {
357 crtc_id,
358 degamma_lut_size: size("DEGAMMA_LUT_SIZE"),
359 gamma_lut_size: size("GAMMA_LUT_SIZE"),
360 has_degamma_lut: has("DEGAMMA_LUT"),
361 has_ctm: has("CTM"),
362 has_gamma_lut: has("GAMMA_LUT"),
363 }
364 }
365
366 #[must_use]
371 pub const fn has_full_pipeline(&self) -> bool {
372 self.has_degamma_lut && self.has_ctm && self.has_gamma_lut
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::{BroadcastRgb, Colorspace, ConnectorCapabilities, CrtcCapabilities, MaxBpc};
379
380 fn amdgpu() -> ConnectorCapabilities {
382 ConnectorCapabilities::new(383)
383 .with_colorspace(Colorspace::Default, 0)
384 .with_colorspace(Colorspace::Bt709Ycc, 1)
385 .with_colorspace(Colorspace::OpRgb, 2)
386 .with_colorspace(Colorspace::Bt2020Rgb, 3)
387 .with_colorspace(Colorspace::Bt2020Ycc, 4)
388 .with_max_bpc(MaxBpc {
389 min: 8,
390 max: 16,
391 current: 16,
392 })
393 .with_hdr_output_metadata(true)
394 }
395
396 #[test]
397 fn an_entry_the_driver_does_not_have_is_absent_rather_than_zero() {
398 let connector = amdgpu();
402 assert_eq!(connector.colorspace(Colorspace::Default), Some(0));
403 assert_eq!(connector.colorspace(Colorspace::DciP3RgbD65), None);
404 }
405
406 #[test]
407 fn every_entry_is_stored_under_its_own_name() {
408 let mut connector = ConnectorCapabilities::new(1);
411 for (value, entry) in Colorspace::ALL.into_iter().enumerate() {
412 connector = connector.with_colorspace(entry, value as u64);
413 }
414 for (value, entry) in Colorspace::ALL.into_iter().enumerate() {
415 assert_eq!(
416 connector.colorspace(entry),
417 Some(value as u64),
418 "{}",
419 entry.name()
420 );
421 }
422 }
423
424 #[test]
425 fn broadcast_rgb_entries_are_stored_the_same_way() {
426 let mut connector = ConnectorCapabilities::new(1);
427 for (value, entry) in BroadcastRgb::ALL.into_iter().enumerate() {
428 connector = connector.with_broadcast_rgb(entry, 10 + value as u64);
429 }
430 for (value, entry) in BroadcastRgb::ALL.into_iter().enumerate() {
431 assert_eq!(connector.broadcast_rgb(entry), Some(10 + value as u64));
432 }
433 assert!(connector.has_broadcast_rgb());
434 }
435
436 #[test]
437 fn hdr_needs_the_metadata_and_the_bit_depth() {
438 assert!(amdgpu().can_signal_hdr());
439
440 assert!(
443 !ConnectorCapabilities::new(1)
444 .with_hdr_output_metadata(true)
445 .with_max_bpc(MaxBpc {
446 min: 6,
447 max: 8,
448 current: 8,
449 })
450 .can_signal_hdr()
451 );
452
453 assert!(
455 !ConnectorCapabilities::new(1)
456 .with_max_bpc(MaxBpc {
457 min: 8,
458 max: 10,
459 current: 8,
460 })
461 .can_signal_hdr()
462 );
463
464 assert!(
467 !ConnectorCapabilities::new(1)
468 .with_hdr_output_metadata(true)
469 .can_signal_hdr()
470 );
471 }
472
473 #[test]
474 fn the_current_bpc_is_what_is_set_not_what_is_possible() {
475 let connector = ConnectorCapabilities::new(1)
479 .with_hdr_output_metadata(true)
480 .with_max_bpc(MaxBpc {
481 min: 8,
482 max: 16,
483 current: 8,
484 });
485 assert!(connector.can_signal_hdr());
486 assert_eq!(connector.max_bpc.map(|bpc| bpc.current), Some(8));
487 }
488
489 #[test]
490 fn a_connector_with_nothing_on_it_says_so() {
491 let connector = ConnectorCapabilities::new(7);
492 assert!(!connector.has_colorspace());
493 assert!(!connector.has_broadcast_rgb());
494 assert!(!connector.can_signal_hdr());
495 assert_eq!(connector.connector_id, 7);
496 }
497
498 #[test]
499 fn a_partial_color_pipeline_is_not_a_full_one() {
500 let vkms = CrtcCapabilities {
504 crtc_id: 34,
505 gamma_lut_size: 256,
506 has_gamma_lut: true,
507 ..CrtcCapabilities::default()
508 };
509 assert!(!vkms.has_full_pipeline());
510
511 let amdgpu = CrtcCapabilities {
512 crtc_id: 369,
513 degamma_lut_size: 4096,
514 gamma_lut_size: 4096,
515 has_degamma_lut: true,
516 has_ctm: true,
517 has_gamma_lut: true,
518 };
519 assert!(amdgpu.has_full_pipeline());
520 }
521}