1use drmkit_display::color::{Chromaticity, ColorimetryInfo};
13use drmkit_display::{Colorspace, ConnectorCapabilities, HdrSourceMetadata, TransferFunction};
14
15use crate::display::DisplayParams;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
24#[non_exhaustive]
25pub enum ColorPrimaries {
26 #[default]
28 Bt709,
29 AdobeRgb,
31 DciP3,
33 Bt2020,
35}
36
37impl ColorPrimaries {
38 #[must_use]
48 pub const fn is_wider_than(self, other: Self) -> bool {
49 (self as u8) > (other as u8)
50 }
51
52 #[must_use]
57 pub const fn colorimetry(self) -> ColorimetryInfo {
58 let (red, green, blue) = match self {
59 Self::Bt709 => ((0.640, 0.330), (0.300, 0.600), (0.150, 0.060)),
61 Self::Bt2020 => ((0.708, 0.292), (0.170, 0.797), (0.131, 0.046)),
63 Self::DciP3 => ((0.680, 0.320), (0.265, 0.690), (0.150, 0.060)),
65 Self::AdobeRgb => ((0.640, 0.330), (0.210, 0.710), (0.150, 0.060)),
68 };
69 ColorimetryInfo {
70 red: Chromaticity { x: red.0, y: red.1 },
71 green: Chromaticity {
72 x: green.0,
73 y: green.1,
74 },
75 blue: Chromaticity {
76 x: blue.0,
77 y: blue.1,
78 },
79 white: Chromaticity {
80 x: 0.3127,
81 y: 0.3290,
82 },
83 has_primaries: true,
84 has_default_white: true,
85 }
86 }
87
88 #[must_use]
90 pub const fn colorspace(self) -> Colorspace {
91 match self {
92 Self::Bt709 => Colorspace::Default,
96 Self::Bt2020 => Colorspace::Bt2020Rgb,
99 Self::DciP3 => Colorspace::DciP3RgbD65,
100 Self::AdobeRgb => Colorspace::OpRgb,
101 }
102 }
103}
104
105#[deprecated(since = "0.1.2", note = "renamed to `OutputSignaling`")]
108pub type OutputSignalling = OutputSignaling;
109
110#[derive(Debug, Clone, PartialEq, Default)]
112#[non_exhaustive]
113pub struct OutputSignaling {
114 pub colorspace: Option<Colorspace>,
120 pub hdr_metadata: Option<HdrSourceMetadata>,
122 pub hdr_downgraded: bool,
129}
130
131#[must_use]
136pub fn widest_gamut<'a>(
137 layers: impl IntoIterator<Item = &'a DisplayParams>,
138) -> Option<ColorPrimaries> {
139 layers
140 .into_iter()
141 .filter_map(|layer| layer.color_primaries)
142 .max()
143}
144
145const fn is_hdr(eotf: TransferFunction) -> bool {
151 matches!(
152 eotf,
153 TransferFunction::SmpteSt2084Pq | TransferFunction::Bt2100Hlg
154 )
155}
156
157#[must_use]
173pub fn derive_output_signaling(
174 layers: &[DisplayParams],
175 capabilities: Option<&ConnectorCapabilities>,
176) -> OutputSignaling {
177 let gamut = widest_gamut(layers);
178
179 let eotf = layers
180 .iter()
181 .filter_map(|layer| layer.source_eotf)
182 .filter(|eotf| is_hdr(*eotf))
183 .min_by_key(|eotf| match eotf {
186 TransferFunction::SmpteSt2084Pq => 0_u8,
187 _ => 1,
188 });
189
190 let mut signaling = OutputSignaling {
191 colorspace: gamut.map(ColorPrimaries::colorspace),
192 hdr_metadata: eotf.map(|eotf| HdrSourceMetadata {
193 eotf,
194 display_primaries: gamut.unwrap_or(ColorPrimaries::Bt2020).colorimetry(),
195 max_display_mastering_luminance: 0,
196 min_display_mastering_luminance: 0,
197 max_content_light_level: 0,
198 max_frame_average_light_level: 0,
199 }),
200 hdr_downgraded: false,
201 };
202
203 if signaling.hdr_metadata.is_some() && capabilities.is_some_and(|caps| !caps.can_signal_hdr()) {
204 signaling.hdr_metadata = None;
208 signaling.hdr_downgraded = true;
209 }
210
211 signaling
212}
213
214#[deprecated(since = "0.1.2", note = "renamed to `derive_output_signaling`")]
217#[must_use]
218pub fn derive_output_signalling(
219 layers: &[DisplayParams],
220 capabilities: Option<&ConnectorCapabilities>,
221) -> OutputSignaling {
222 derive_output_signaling(layers, capabilities)
223}
224
225#[cfg(test)]
226mod tests {
227 use super::{ColorPrimaries, derive_output_signaling, widest_gamut};
228 use crate::display::DisplayParams;
229 use drmkit_display::{Colorspace, ConnectorCapabilities, MaxBpc, TransferFunction};
230
231 fn plain() -> DisplayParams {
233 DisplayParams::default()
234 }
235
236 fn layer(primaries: Option<ColorPrimaries>, eotf: Option<TransferFunction>) -> DisplayParams {
237 DisplayParams {
238 color_primaries: primaries,
239 source_eotf: eotf,
240 ..DisplayParams::default()
241 }
242 }
243
244 fn hdr_capable() -> ConnectorCapabilities {
246 ConnectorCapabilities::new(1)
247 .with_hdr_output_metadata(true)
248 .with_max_bpc(MaxBpc {
249 min: 8,
250 max: 10,
251 current: 10,
252 })
253 }
254
255 #[test]
258 fn bt709_is_canonical_srgb() {
259 let c = ColorPrimaries::Bt709.colorimetry();
260 assert!((c.red.x - 0.640).abs() < 1e-6 && (c.red.y - 0.330).abs() < 1e-6);
261 assert!((c.green.x - 0.300).abs() < 1e-6 && (c.green.y - 0.600).abs() < 1e-6);
262 assert!((c.blue.x - 0.150).abs() < 1e-6 && (c.blue.y - 0.060).abs() < 1e-6);
263 assert!((c.white.x - 0.3127).abs() < 1e-6 && (c.white.y - 0.3290).abs() < 1e-6);
264 assert!(c.has_primaries && c.has_default_white);
265 }
266
267 #[test]
268 fn bt2020_matches_rec2100() {
269 let c = ColorPrimaries::Bt2020.colorimetry();
270 assert!((c.red.x - 0.708).abs() < 1e-6);
271 assert!((c.green.y - 0.797).abs() < 1e-6);
272 assert!((c.blue.x - 0.131).abs() < 1e-6);
273 }
274
275 #[test]
276 fn dci_p3_d65_matches_smpte_eg432() {
277 let c = ColorPrimaries::DciP3.colorimetry();
278 assert!((c.red.x - 0.680).abs() < 1e-6);
279 assert!((c.green.x - 0.265).abs() < 1e-6);
280 assert!((c.white.x - 0.3127).abs() < 1e-6);
282 }
283
284 #[test]
285 fn adobe_rgb_differs_from_bt709_only_in_green() {
286 let adobe = ColorPrimaries::AdobeRgb.colorimetry();
290 let bt709 = ColorPrimaries::Bt709.colorimetry();
291 assert_eq!(adobe.red, bt709.red);
292 assert_eq!(adobe.blue, bt709.blue);
293 assert_ne!(adobe.green, bt709.green);
294 assert!((adobe.green.y - 0.710).abs() < 1e-6);
295 }
296
297 #[test]
300 fn no_layers_name_no_gamut() {
301 assert_eq!(widest_gamut(&[] as &[DisplayParams]), None);
302 assert_eq!(widest_gamut(&[plain(), plain()]), None);
303 }
304
305 #[test]
306 fn one_layer_passes_its_own_through() {
307 let layers = [layer(Some(ColorPrimaries::DciP3), None)];
308 assert_eq!(widest_gamut(&layers), Some(ColorPrimaries::DciP3));
309 }
310
311 #[test]
312 fn the_ranking_runs_bt2020_over_p3_over_adobe_over_bt709() {
313 for (wider, narrower) in [
314 (ColorPrimaries::Bt2020, ColorPrimaries::DciP3),
315 (ColorPrimaries::DciP3, ColorPrimaries::AdobeRgb),
316 (ColorPrimaries::AdobeRgb, ColorPrimaries::Bt709),
317 ] {
318 assert!(wider.is_wider_than(narrower), "{wider:?} over {narrower:?}");
319 assert_eq!(
321 widest_gamut(&[layer(Some(narrower), None), layer(Some(wider), None)]),
322 Some(wider)
323 );
324 assert_eq!(
325 widest_gamut(&[layer(Some(wider), None), layer(Some(narrower), None)]),
326 Some(wider)
327 );
328 }
329 }
330
331 #[test]
332 fn a_mixed_set_selects_the_widest() {
333 let layers = [
334 layer(Some(ColorPrimaries::Bt709), None),
335 plain(),
336 layer(Some(ColorPrimaries::Bt2020), None),
337 layer(Some(ColorPrimaries::AdobeRgb), None),
338 ];
339 assert_eq!(widest_gamut(&layers), Some(ColorPrimaries::Bt2020));
340 }
341
342 #[test]
343 fn layers_that_say_nothing_are_passed_over_not_counted_as_bt709() {
344 let layers = [plain(), layer(Some(ColorPrimaries::DciP3), None), plain()];
348 assert_eq!(widest_gamut(&layers), Some(ColorPrimaries::DciP3));
349 }
350
351 #[test]
354 fn bt709_maps_to_default_rather_than_bt709_ycc() {
355 assert_eq!(ColorPrimaries::Bt709.colorspace(), Colorspace::Default);
359 }
360
361 #[test]
362 fn the_other_three_map_to_their_own_entries() {
363 assert_eq!(ColorPrimaries::Bt2020.colorspace(), Colorspace::Bt2020Rgb);
364 assert_eq!(ColorPrimaries::DciP3.colorspace(), Colorspace::DciP3RgbD65);
365 assert_eq!(ColorPrimaries::AdobeRgb.colorspace(), Colorspace::OpRgb);
366 }
367
368 #[test]
371 fn no_layers_signal_nothing() {
372 let signaling = derive_output_signaling(&[], None);
373 assert_eq!(signaling.colorspace, None);
374 assert_eq!(signaling.hdr_metadata, None);
375 assert!(!signaling.hdr_downgraded);
376 }
377
378 #[test]
379 fn an_all_sdr_frame_that_names_nothing_leaves_the_connector_alone() {
380 let signaling = derive_output_signaling(&[plain(), plain()], None);
381 assert_eq!(signaling.colorspace, None);
382 assert_eq!(signaling.hdr_metadata, None);
383 }
384
385 #[test]
386 fn an_sdr_bt709_layer_asks_for_default_and_no_hdr() {
387 let layers = [layer(Some(ColorPrimaries::Bt709), None)];
388 let signaling = derive_output_signaling(&layers, None);
389 assert_eq!(signaling.colorspace, Some(Colorspace::Default));
390 assert_eq!(signaling.hdr_metadata, None);
391 }
392
393 #[test]
394 fn a_bt2020_pq_layer_produces_hdr_signaling() {
395 let layers = [layer(
396 Some(ColorPrimaries::Bt2020),
397 Some(TransferFunction::SmpteSt2084Pq),
398 )];
399 let signaling = derive_output_signaling(&layers, None);
400 assert_eq!(signaling.colorspace, Some(Colorspace::Bt2020Rgb));
401 let hdr = signaling.hdr_metadata.expect("hdr");
402 assert_eq!(hdr.eotf, TransferFunction::SmpteSt2084Pq);
403 assert_eq!(
404 hdr.display_primaries,
405 ColorPrimaries::Bt2020.colorimetry(),
406 "the mastering primaries come from the widest-gamut layer"
407 );
408 assert_eq!(hdr.max_display_mastering_luminance, 0);
411 assert_eq!(hdr.max_content_light_level, 0);
412 }
413
414 #[test]
415 fn one_hdr_layer_among_sdr_ones_carries_the_frame() {
416 let layers = [
417 layer(Some(ColorPrimaries::Bt709), None),
418 layer(
419 Some(ColorPrimaries::Bt2020),
420 Some(TransferFunction::SmpteSt2084Pq),
421 ),
422 ];
423 let signaling = derive_output_signaling(&layers, None);
424 assert_eq!(signaling.colorspace, Some(Colorspace::Bt2020Rgb));
425 assert!(signaling.hdr_metadata.is_some());
426 }
427
428 #[test]
429 fn pq_wins_when_both_hdr_curves_are_present() {
430 for order in [[0, 1], [1, 0]] {
432 let both = [
433 layer(
434 Some(ColorPrimaries::Bt2020),
435 Some(TransferFunction::SmpteSt2084Pq),
436 ),
437 layer(
438 Some(ColorPrimaries::Bt2020),
439 Some(TransferFunction::Bt2100Hlg),
440 ),
441 ];
442 let layers = [both[order[0]], both[order[1]]];
443 let hdr = derive_output_signaling(&layers, None)
444 .hdr_metadata
445 .expect("hdr");
446 assert_eq!(hdr.eotf, TransferFunction::SmpteSt2084Pq);
447 }
448 }
449
450 #[test]
451 fn an_hlg_only_frame_signals_hlg() {
452 let layers = [layer(
453 Some(ColorPrimaries::Bt2020),
454 Some(TransferFunction::Bt2100Hlg),
455 )];
456 let hdr = derive_output_signaling(&layers, None)
457 .hdr_metadata
458 .expect("hdr");
459 assert_eq!(hdr.eotf, TransferFunction::Bt2100Hlg);
460 }
461
462 #[test]
463 fn an_hdr_layer_that_names_no_gamut_masters_in_bt2020() {
464 let layers = [layer(None, Some(TransferFunction::SmpteSt2084Pq))];
468 let signaling = derive_output_signaling(&layers, None);
469 assert_eq!(signaling.colorspace, None, "no layer named a gamut");
470 let hdr = signaling.hdr_metadata.expect("hdr");
471 assert_eq!(hdr.display_primaries, ColorPrimaries::Bt2020.colorimetry());
472 }
473
474 #[test]
475 fn traditional_gamma_hdr_is_not_hdr_signaling() {
476 let layers = [layer(
479 Some(ColorPrimaries::Bt2020),
480 Some(TransferFunction::TraditionalGammaHdr),
481 )];
482 let signaling = derive_output_signaling(&layers, None);
483 assert_eq!(signaling.hdr_metadata, None);
484 assert!(!signaling.hdr_downgraded, "nothing was dropped");
485 assert_eq!(signaling.colorspace, Some(Colorspace::Bt2020Rgb));
486 }
487
488 #[test]
491 fn a_capable_connector_keeps_the_metadata() {
492 let layers = [layer(
493 Some(ColorPrimaries::Bt2020),
494 Some(TransferFunction::SmpteSt2084Pq),
495 )];
496 let signaling = derive_output_signaling(&layers, Some(&hdr_capable()));
497 assert!(signaling.hdr_metadata.is_some());
498 assert!(!signaling.hdr_downgraded);
499 }
500
501 #[test]
502 fn a_connector_without_the_blob_downgrades() {
503 let layers = [layer(
504 Some(ColorPrimaries::Bt2020),
505 Some(TransferFunction::SmpteSt2084Pq),
506 )];
507 let no_blob = ConnectorCapabilities::new(1).with_max_bpc(MaxBpc {
508 min: 8,
509 max: 12,
510 current: 12,
511 });
512 let signaling = derive_output_signaling(&layers, Some(&no_blob));
513 assert_eq!(signaling.hdr_metadata, None);
514 assert!(signaling.hdr_downgraded);
515 assert_eq!(signaling.colorspace, Some(Colorspace::Bt2020Rgb));
517 }
518
519 #[test]
520 fn a_connector_capped_at_eight_bits_downgrades() {
521 let layers = [layer(
523 Some(ColorPrimaries::Bt2020),
524 Some(TransferFunction::SmpteSt2084Pq),
525 )];
526 let eight_bit = ConnectorCapabilities::new(1)
527 .with_hdr_output_metadata(true)
528 .with_max_bpc(MaxBpc {
529 min: 6,
530 max: 8,
531 current: 8,
532 });
533 let signaling = derive_output_signaling(&layers, Some(&eight_bit));
534 assert_eq!(signaling.hdr_metadata, None);
535 assert!(signaling.hdr_downgraded);
536 }
537
538 #[test]
539 fn ten_bits_is_enough() {
540 let layers = [layer(
542 Some(ColorPrimaries::Bt2020),
543 Some(TransferFunction::SmpteSt2084Pq),
544 )];
545 assert!(
546 derive_output_signaling(&layers, Some(&hdr_capable()))
547 .hdr_metadata
548 .is_some()
549 );
550 }
551
552 #[test]
553 fn an_sdr_frame_is_never_downgraded() {
554 let layers = [layer(Some(ColorPrimaries::Bt709), None)];
557 let eight_bit = ConnectorCapabilities::new(1).with_max_bpc(MaxBpc {
558 min: 6,
559 max: 8,
560 current: 8,
561 });
562 let signaling = derive_output_signaling(&layers, Some(&eight_bit));
563 assert!(!signaling.hdr_downgraded);
564 assert_eq!(signaling.colorspace, Some(Colorspace::Default));
565 }
566
567 #[test]
568 fn without_capabilities_the_constraint_is_not_checked() {
569 let layers = [layer(
572 Some(ColorPrimaries::Bt2020),
573 Some(TransferFunction::SmpteSt2084Pq),
574 )];
575 let signaling = derive_output_signaling(&layers, None);
576 assert!(signaling.hdr_metadata.is_some());
577 assert!(!signaling.hdr_downgraded);
578 }
579
580 #[test]
581 fn the_defaults_name_nothing() {
582 let params = DisplayParams::default();
583 assert_eq!(params.color_primaries, None);
584 assert_eq!(params.source_eotf, None);
585 }
586}