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]
45 pub const fn is_wider_than(self, other: Self) -> bool {
46 (self as u8) > (other as u8)
47 }
48
49 #[must_use]
54 pub const fn colorimetry(self) -> ColorimetryInfo {
55 let (red, green, blue) = match self {
56 Self::Bt709 => ((0.640, 0.330), (0.300, 0.600), (0.150, 0.060)),
58 Self::Bt2020 => ((0.708, 0.292), (0.170, 0.797), (0.131, 0.046)),
60 Self::DciP3 => ((0.680, 0.320), (0.265, 0.690), (0.150, 0.060)),
62 Self::AdobeRgb => ((0.640, 0.330), (0.210, 0.710), (0.150, 0.060)),
65 };
66 ColorimetryInfo {
67 red: Chromaticity { x: red.0, y: red.1 },
68 green: Chromaticity {
69 x: green.0,
70 y: green.1,
71 },
72 blue: Chromaticity {
73 x: blue.0,
74 y: blue.1,
75 },
76 white: Chromaticity {
77 x: 0.3127,
78 y: 0.3290,
79 },
80 has_primaries: true,
81 has_default_white: true,
82 }
83 }
84
85 #[must_use]
87 pub const fn colorspace(self) -> Colorspace {
88 match self {
89 Self::Bt709 => Colorspace::Default,
93 Self::Bt2020 => Colorspace::Bt2020Rgb,
96 Self::DciP3 => Colorspace::DciP3RgbD65,
97 Self::AdobeRgb => Colorspace::OpRgb,
98 }
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Default)]
104#[non_exhaustive]
105pub struct OutputSignalling {
106 pub colorspace: Option<Colorspace>,
112 pub hdr_metadata: Option<HdrSourceMetadata>,
114 pub hdr_downgraded: bool,
121}
122
123#[must_use]
128pub fn widest_gamut<'a>(
129 layers: impl IntoIterator<Item = &'a DisplayParams>,
130) -> Option<ColorPrimaries> {
131 layers
132 .into_iter()
133 .filter_map(|layer| layer.color_primaries)
134 .max()
135}
136
137const fn is_hdr(eotf: TransferFunction) -> bool {
143 matches!(
144 eotf,
145 TransferFunction::SmpteSt2084Pq | TransferFunction::Bt2100Hlg
146 )
147}
148
149#[must_use]
165pub fn derive_output_signalling(
166 layers: &[DisplayParams],
167 capabilities: Option<&ConnectorCapabilities>,
168) -> OutputSignalling {
169 let gamut = widest_gamut(layers);
170
171 let eotf = layers
172 .iter()
173 .filter_map(|layer| layer.source_eotf)
174 .filter(|eotf| is_hdr(*eotf))
175 .min_by_key(|eotf| match eotf {
178 TransferFunction::SmpteSt2084Pq => 0_u8,
179 _ => 1,
180 });
181
182 let mut signalling = OutputSignalling {
183 colorspace: gamut.map(ColorPrimaries::colorspace),
184 hdr_metadata: eotf.map(|eotf| HdrSourceMetadata {
185 eotf,
186 display_primaries: gamut.unwrap_or(ColorPrimaries::Bt2020).colorimetry(),
187 max_display_mastering_luminance: 0,
188 min_display_mastering_luminance: 0,
189 max_content_light_level: 0,
190 max_frame_average_light_level: 0,
191 }),
192 hdr_downgraded: false,
193 };
194
195 if signalling.hdr_metadata.is_some() && capabilities.is_some_and(|caps| !caps.can_signal_hdr())
196 {
197 signalling.hdr_metadata = None;
201 signalling.hdr_downgraded = true;
202 }
203
204 signalling
205}
206
207#[cfg(test)]
208mod tests {
209 use super::{ColorPrimaries, derive_output_signalling, widest_gamut};
210 use crate::display::DisplayParams;
211 use drmkit_display::{Colorspace, ConnectorCapabilities, MaxBpc, TransferFunction};
212
213 fn plain() -> DisplayParams {
215 DisplayParams::default()
216 }
217
218 fn layer(primaries: Option<ColorPrimaries>, eotf: Option<TransferFunction>) -> DisplayParams {
219 DisplayParams {
220 color_primaries: primaries,
221 source_eotf: eotf,
222 ..DisplayParams::default()
223 }
224 }
225
226 fn hdr_capable() -> ConnectorCapabilities {
228 ConnectorCapabilities::new(1)
229 .with_hdr_output_metadata(true)
230 .with_max_bpc(MaxBpc {
231 min: 8,
232 max: 10,
233 current: 10,
234 })
235 }
236
237 #[test]
240 fn bt709_is_canonical_srgb() {
241 let c = ColorPrimaries::Bt709.colorimetry();
242 assert!((c.red.x - 0.640).abs() < 1e-6 && (c.red.y - 0.330).abs() < 1e-6);
243 assert!((c.green.x - 0.300).abs() < 1e-6 && (c.green.y - 0.600).abs() < 1e-6);
244 assert!((c.blue.x - 0.150).abs() < 1e-6 && (c.blue.y - 0.060).abs() < 1e-6);
245 assert!((c.white.x - 0.3127).abs() < 1e-6 && (c.white.y - 0.3290).abs() < 1e-6);
246 assert!(c.has_primaries && c.has_default_white);
247 }
248
249 #[test]
250 fn bt2020_matches_rec2100() {
251 let c = ColorPrimaries::Bt2020.colorimetry();
252 assert!((c.red.x - 0.708).abs() < 1e-6);
253 assert!((c.green.y - 0.797).abs() < 1e-6);
254 assert!((c.blue.x - 0.131).abs() < 1e-6);
255 }
256
257 #[test]
258 fn dci_p3_d65_matches_smpte_eg432() {
259 let c = ColorPrimaries::DciP3.colorimetry();
260 assert!((c.red.x - 0.680).abs() < 1e-6);
261 assert!((c.green.x - 0.265).abs() < 1e-6);
262 assert!((c.white.x - 0.3127).abs() < 1e-6);
264 }
265
266 #[test]
267 fn adobe_rgb_differs_from_bt709_only_in_green() {
268 let adobe = ColorPrimaries::AdobeRgb.colorimetry();
272 let bt709 = ColorPrimaries::Bt709.colorimetry();
273 assert_eq!(adobe.red, bt709.red);
274 assert_eq!(adobe.blue, bt709.blue);
275 assert_ne!(adobe.green, bt709.green);
276 assert!((adobe.green.y - 0.710).abs() < 1e-6);
277 }
278
279 #[test]
282 fn no_layers_name_no_gamut() {
283 assert_eq!(widest_gamut(&[] as &[DisplayParams]), None);
284 assert_eq!(widest_gamut(&[plain(), plain()]), None);
285 }
286
287 #[test]
288 fn one_layer_passes_its_own_through() {
289 let layers = [layer(Some(ColorPrimaries::DciP3), None)];
290 assert_eq!(widest_gamut(&layers), Some(ColorPrimaries::DciP3));
291 }
292
293 #[test]
294 fn the_ranking_runs_bt2020_over_p3_over_adobe_over_bt709() {
295 for (wider, narrower) in [
296 (ColorPrimaries::Bt2020, ColorPrimaries::DciP3),
297 (ColorPrimaries::DciP3, ColorPrimaries::AdobeRgb),
298 (ColorPrimaries::AdobeRgb, ColorPrimaries::Bt709),
299 ] {
300 assert!(wider.is_wider_than(narrower), "{wider:?} over {narrower:?}");
301 assert_eq!(
303 widest_gamut(&[layer(Some(narrower), None), layer(Some(wider), None)]),
304 Some(wider)
305 );
306 assert_eq!(
307 widest_gamut(&[layer(Some(wider), None), layer(Some(narrower), None)]),
308 Some(wider)
309 );
310 }
311 }
312
313 #[test]
314 fn a_mixed_set_selects_the_widest() {
315 let layers = [
316 layer(Some(ColorPrimaries::Bt709), None),
317 plain(),
318 layer(Some(ColorPrimaries::Bt2020), None),
319 layer(Some(ColorPrimaries::AdobeRgb), None),
320 ];
321 assert_eq!(widest_gamut(&layers), Some(ColorPrimaries::Bt2020));
322 }
323
324 #[test]
325 fn layers_that_say_nothing_are_passed_over_not_counted_as_bt709() {
326 let layers = [plain(), layer(Some(ColorPrimaries::DciP3), None), plain()];
330 assert_eq!(widest_gamut(&layers), Some(ColorPrimaries::DciP3));
331 }
332
333 #[test]
336 fn bt709_maps_to_default_rather_than_bt709_ycc() {
337 assert_eq!(ColorPrimaries::Bt709.colorspace(), Colorspace::Default);
341 }
342
343 #[test]
344 fn the_other_three_map_to_their_own_entries() {
345 assert_eq!(ColorPrimaries::Bt2020.colorspace(), Colorspace::Bt2020Rgb);
346 assert_eq!(ColorPrimaries::DciP3.colorspace(), Colorspace::DciP3RgbD65);
347 assert_eq!(ColorPrimaries::AdobeRgb.colorspace(), Colorspace::OpRgb);
348 }
349
350 #[test]
353 fn no_layers_signal_nothing() {
354 let signalling = derive_output_signalling(&[], None);
355 assert_eq!(signalling.colorspace, None);
356 assert_eq!(signalling.hdr_metadata, None);
357 assert!(!signalling.hdr_downgraded);
358 }
359
360 #[test]
361 fn an_all_sdr_frame_that_names_nothing_leaves_the_connector_alone() {
362 let signalling = derive_output_signalling(&[plain(), plain()], None);
363 assert_eq!(signalling.colorspace, None);
364 assert_eq!(signalling.hdr_metadata, None);
365 }
366
367 #[test]
368 fn an_sdr_bt709_layer_asks_for_default_and_no_hdr() {
369 let layers = [layer(Some(ColorPrimaries::Bt709), None)];
370 let signalling = derive_output_signalling(&layers, None);
371 assert_eq!(signalling.colorspace, Some(Colorspace::Default));
372 assert_eq!(signalling.hdr_metadata, None);
373 }
374
375 #[test]
376 fn a_bt2020_pq_layer_produces_hdr_signalling() {
377 let layers = [layer(
378 Some(ColorPrimaries::Bt2020),
379 Some(TransferFunction::SmpteSt2084Pq),
380 )];
381 let signalling = derive_output_signalling(&layers, None);
382 assert_eq!(signalling.colorspace, Some(Colorspace::Bt2020Rgb));
383 let hdr = signalling.hdr_metadata.expect("hdr");
384 assert_eq!(hdr.eotf, TransferFunction::SmpteSt2084Pq);
385 assert_eq!(
386 hdr.display_primaries,
387 ColorPrimaries::Bt2020.colorimetry(),
388 "the mastering primaries come from the widest-gamut layer"
389 );
390 assert_eq!(hdr.max_display_mastering_luminance, 0);
393 assert_eq!(hdr.max_content_light_level, 0);
394 }
395
396 #[test]
397 fn one_hdr_layer_among_sdr_ones_carries_the_frame() {
398 let layers = [
399 layer(Some(ColorPrimaries::Bt709), None),
400 layer(
401 Some(ColorPrimaries::Bt2020),
402 Some(TransferFunction::SmpteSt2084Pq),
403 ),
404 ];
405 let signalling = derive_output_signalling(&layers, None);
406 assert_eq!(signalling.colorspace, Some(Colorspace::Bt2020Rgb));
407 assert!(signalling.hdr_metadata.is_some());
408 }
409
410 #[test]
411 fn pq_wins_when_both_hdr_curves_are_present() {
412 for order in [[0, 1], [1, 0]] {
414 let both = [
415 layer(
416 Some(ColorPrimaries::Bt2020),
417 Some(TransferFunction::SmpteSt2084Pq),
418 ),
419 layer(
420 Some(ColorPrimaries::Bt2020),
421 Some(TransferFunction::Bt2100Hlg),
422 ),
423 ];
424 let layers = [both[order[0]], both[order[1]]];
425 let hdr = derive_output_signalling(&layers, None)
426 .hdr_metadata
427 .expect("hdr");
428 assert_eq!(hdr.eotf, TransferFunction::SmpteSt2084Pq);
429 }
430 }
431
432 #[test]
433 fn an_hlg_only_frame_signals_hlg() {
434 let layers = [layer(
435 Some(ColorPrimaries::Bt2020),
436 Some(TransferFunction::Bt2100Hlg),
437 )];
438 let hdr = derive_output_signalling(&layers, None)
439 .hdr_metadata
440 .expect("hdr");
441 assert_eq!(hdr.eotf, TransferFunction::Bt2100Hlg);
442 }
443
444 #[test]
445 fn an_hdr_layer_that_names_no_gamut_masters_in_bt2020() {
446 let layers = [layer(None, Some(TransferFunction::SmpteSt2084Pq))];
450 let signalling = derive_output_signalling(&layers, None);
451 assert_eq!(signalling.colorspace, None, "no layer named a gamut");
452 let hdr = signalling.hdr_metadata.expect("hdr");
453 assert_eq!(hdr.display_primaries, ColorPrimaries::Bt2020.colorimetry());
454 }
455
456 #[test]
457 fn traditional_gamma_hdr_is_not_hdr_signalling() {
458 let layers = [layer(
461 Some(ColorPrimaries::Bt2020),
462 Some(TransferFunction::TraditionalGammaHdr),
463 )];
464 let signalling = derive_output_signalling(&layers, None);
465 assert_eq!(signalling.hdr_metadata, None);
466 assert!(!signalling.hdr_downgraded, "nothing was dropped");
467 assert_eq!(signalling.colorspace, Some(Colorspace::Bt2020Rgb));
468 }
469
470 #[test]
473 fn a_capable_connector_keeps_the_metadata() {
474 let layers = [layer(
475 Some(ColorPrimaries::Bt2020),
476 Some(TransferFunction::SmpteSt2084Pq),
477 )];
478 let signalling = derive_output_signalling(&layers, Some(&hdr_capable()));
479 assert!(signalling.hdr_metadata.is_some());
480 assert!(!signalling.hdr_downgraded);
481 }
482
483 #[test]
484 fn a_connector_without_the_blob_downgrades() {
485 let layers = [layer(
486 Some(ColorPrimaries::Bt2020),
487 Some(TransferFunction::SmpteSt2084Pq),
488 )];
489 let no_blob = ConnectorCapabilities::new(1).with_max_bpc(MaxBpc {
490 min: 8,
491 max: 12,
492 current: 12,
493 });
494 let signalling = derive_output_signalling(&layers, Some(&no_blob));
495 assert_eq!(signalling.hdr_metadata, None);
496 assert!(signalling.hdr_downgraded);
497 assert_eq!(signalling.colorspace, Some(Colorspace::Bt2020Rgb));
499 }
500
501 #[test]
502 fn a_connector_capped_at_eight_bits_downgrades() {
503 let layers = [layer(
505 Some(ColorPrimaries::Bt2020),
506 Some(TransferFunction::SmpteSt2084Pq),
507 )];
508 let eight_bit = ConnectorCapabilities::new(1)
509 .with_hdr_output_metadata(true)
510 .with_max_bpc(MaxBpc {
511 min: 6,
512 max: 8,
513 current: 8,
514 });
515 let signalling = derive_output_signalling(&layers, Some(&eight_bit));
516 assert_eq!(signalling.hdr_metadata, None);
517 assert!(signalling.hdr_downgraded);
518 }
519
520 #[test]
521 fn ten_bits_is_enough() {
522 let layers = [layer(
524 Some(ColorPrimaries::Bt2020),
525 Some(TransferFunction::SmpteSt2084Pq),
526 )];
527 assert!(
528 derive_output_signalling(&layers, Some(&hdr_capable()))
529 .hdr_metadata
530 .is_some()
531 );
532 }
533
534 #[test]
535 fn an_sdr_frame_is_never_downgraded() {
536 let layers = [layer(Some(ColorPrimaries::Bt709), None)];
539 let eight_bit = ConnectorCapabilities::new(1).with_max_bpc(MaxBpc {
540 min: 6,
541 max: 8,
542 current: 8,
543 });
544 let signalling = derive_output_signalling(&layers, Some(&eight_bit));
545 assert!(!signalling.hdr_downgraded);
546 assert_eq!(signalling.colorspace, Some(Colorspace::Default));
547 }
548
549 #[test]
550 fn without_capabilities_the_constraint_is_not_checked() {
551 let layers = [layer(
554 Some(ColorPrimaries::Bt2020),
555 Some(TransferFunction::SmpteSt2084Pq),
556 )];
557 let signalling = derive_output_signalling(&layers, None);
558 assert!(signalling.hdr_metadata.is_some());
559 assert!(!signalling.hdr_downgraded);
560 }
561
562 #[test]
563 fn the_defaults_name_nothing() {
564 let params = DisplayParams::default();
565 assert_eq!(params.color_primaries, None);
566 assert_eq!(params.source_eotf, None);
567 }
568}