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| {
350 properties
351 .property_value(crtc_id, name)
352 .ok()
353 .and_then(|value| u32::try_from(value).ok())
354 .unwrap_or(0)
355 };
356 let has = |name: &str| properties.property_id(crtc_id, name).is_ok();
357
358 Self {
359 crtc_id,
360 degamma_lut_size: size("DEGAMMA_LUT_SIZE"),
361 gamma_lut_size: size("GAMMA_LUT_SIZE"),
362 has_degamma_lut: has("DEGAMMA_LUT"),
363 has_ctm: has("CTM"),
364 has_gamma_lut: has("GAMMA_LUT"),
365 }
366 }
367
368 #[must_use]
373 pub const fn has_full_pipeline(&self) -> bool {
374 self.has_degamma_lut && self.has_ctm && self.has_gamma_lut
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::{BroadcastRgb, Colorspace, ConnectorCapabilities, CrtcCapabilities, MaxBpc};
381
382 fn amdgpu() -> ConnectorCapabilities {
384 ConnectorCapabilities::new(383)
385 .with_colorspace(Colorspace::Default, 0)
386 .with_colorspace(Colorspace::Bt709Ycc, 1)
387 .with_colorspace(Colorspace::OpRgb, 2)
388 .with_colorspace(Colorspace::Bt2020Rgb, 3)
389 .with_colorspace(Colorspace::Bt2020Ycc, 4)
390 .with_max_bpc(MaxBpc {
391 min: 8,
392 max: 16,
393 current: 16,
394 })
395 .with_hdr_output_metadata(true)
396 }
397
398 #[test]
399 fn an_entry_the_driver_does_not_have_is_absent_rather_than_zero() {
400 let connector = amdgpu();
404 assert_eq!(connector.colorspace(Colorspace::Default), Some(0));
405 assert_eq!(connector.colorspace(Colorspace::DciP3RgbD65), None);
406 }
407
408 #[test]
409 fn every_entry_is_stored_under_its_own_name() {
410 let mut connector = ConnectorCapabilities::new(1);
413 for (value, entry) in Colorspace::ALL.into_iter().enumerate() {
414 connector = connector.with_colorspace(entry, value as u64);
415 }
416 for (value, entry) in Colorspace::ALL.into_iter().enumerate() {
417 assert_eq!(
418 connector.colorspace(entry),
419 Some(value as u64),
420 "{}",
421 entry.name()
422 );
423 }
424 }
425
426 #[test]
427 fn broadcast_rgb_entries_are_stored_the_same_way() {
428 let mut connector = ConnectorCapabilities::new(1);
429 for (value, entry) in BroadcastRgb::ALL.into_iter().enumerate() {
430 connector = connector.with_broadcast_rgb(entry, 10 + value as u64);
431 }
432 for (value, entry) in BroadcastRgb::ALL.into_iter().enumerate() {
433 assert_eq!(connector.broadcast_rgb(entry), Some(10 + value as u64));
434 }
435 assert!(connector.has_broadcast_rgb());
436 }
437
438 #[test]
439 fn hdr_needs_the_metadata_and_the_bit_depth() {
440 assert!(amdgpu().can_signal_hdr());
441
442 assert!(
445 !ConnectorCapabilities::new(1)
446 .with_hdr_output_metadata(true)
447 .with_max_bpc(MaxBpc {
448 min: 6,
449 max: 8,
450 current: 8,
451 })
452 .can_signal_hdr()
453 );
454
455 assert!(
457 !ConnectorCapabilities::new(1)
458 .with_max_bpc(MaxBpc {
459 min: 8,
460 max: 10,
461 current: 8,
462 })
463 .can_signal_hdr()
464 );
465
466 assert!(
469 !ConnectorCapabilities::new(1)
470 .with_hdr_output_metadata(true)
471 .can_signal_hdr()
472 );
473 }
474
475 #[test]
476 fn the_current_bpc_is_what_is_set_not_what_is_possible() {
477 let connector = ConnectorCapabilities::new(1)
481 .with_hdr_output_metadata(true)
482 .with_max_bpc(MaxBpc {
483 min: 8,
484 max: 16,
485 current: 8,
486 });
487 assert!(connector.can_signal_hdr());
488 assert_eq!(connector.max_bpc.map(|bpc| bpc.current), Some(8));
489 }
490
491 #[test]
492 fn a_connector_with_nothing_on_it_says_so() {
493 let connector = ConnectorCapabilities::new(7);
494 assert!(!connector.has_colorspace());
495 assert!(!connector.has_broadcast_rgb());
496 assert!(!connector.can_signal_hdr());
497 assert_eq!(connector.connector_id, 7);
498 }
499
500 #[test]
501 fn a_partial_color_pipeline_is_not_a_full_one() {
502 let vkms = CrtcCapabilities {
506 crtc_id: 34,
507 gamma_lut_size: 256,
508 has_gamma_lut: true,
509 ..CrtcCapabilities::default()
510 };
511 assert!(!vkms.has_full_pipeline());
512
513 let amdgpu = CrtcCapabilities {
514 crtc_id: 369,
515 degamma_lut_size: 4096,
516 gamma_lut_size: 4096,
517 has_degamma_lut: true,
518 has_ctm: true,
519 has_gamma_lut: true,
520 };
521 assert!(amdgpu.has_full_pipeline());
522 }
523}