display-types 0.4.0

Shared display capability types for display connection negotiation.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
/// A reference-counted, type-erased warning value.
///
/// Any type that implements [`core::error::Error`] + [`Send`] + [`Sync`] + `'static` can be
/// wrapped in a `ParseWarning`. The built-in library variants use `EdidWarning`, but
/// custom handlers may push their own error types without wrapping them in `EdidWarning`.
///
/// Using [`Arc`][crate::prelude::Arc] (rather than `Box`) means `ParseWarning` is
/// [`Clone`], which lets warnings be copied from a parsed representation into
/// [`DisplayCapabilities`] without consuming the parsed result.
///
/// To inspect a specific variant, use the inherent `downcast_ref` method available on
/// `dyn core::error::Error + Send + Sync + 'static` in `std` builds:
///
/// ```text
/// for w in caps.iter_warnings() {
///     if let Some(ew) = (**w).downcast_ref::<EdidWarning>() { ... }
/// }
/// ```
#[cfg(any(feature = "alloc", feature = "std"))]
pub type ParseWarning = crate::prelude::Arc<dyn core::error::Error + Send + Sync + 'static>;

/// Stereo viewing support decoded from DTD byte 17 bits 6, 5, and 0.
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StereoMode {
    /// Normal display; no stereo (bits 6–5 = `0b00`; bit 0 is don't-care).
    #[default]
    None,
    /// Field-sequential stereo, right image when stereo sync = 1 (bits 6–5 = `0b01`, bit 0 = 0).
    FieldSequentialRightFirst,
    /// Field-sequential stereo, left image when stereo sync = 1 (bits 6–5 = `0b10`, bit 0 = 0).
    FieldSequentialLeftFirst,
    /// 2-way interleaved stereo, right image on even lines (bits 6–5 = `0b01`, bit 0 = 1).
    TwoWayInterleavedRightEven,
    /// 2-way interleaved stereo, left image on even lines (bits 6–5 = `0b10`, bit 0 = 1).
    TwoWayInterleavedLeftEven,
    /// 4-way interleaved stereo (bits 6–5 = `0b11`, bit 0 = 0).
    FourWayInterleaved,
    /// Side-by-side interleaved stereo (bits 6–5 = `0b11`, bit 0 = 1).
    SideBySideInterleaved,
}

/// CVT formula selector for DisplayID 2.x Type IX (`0x24`) and Type V (`0x11`)
/// Formula-Based Timings.
///
/// Decoded from byte 0 bits 2:0. Identifies which CVT variant the consumer should use
/// to derive blanking parameters and pixel clock from the `(width, height, refresh_rate)`
/// triple stored on [`VideoMode`]. Codes `3`–`7` are reserved by the DisplayID 2.x spec;
/// unknown encodings are surfaced as [`CvtAlgorithm::Reserved`] so a future spec value
/// does not block decoding of the rest of the descriptor.
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CvtAlgorithm {
    /// Standard CVT (no reduced blanking) (encoding `0`).
    Cvt,
    /// CVT-RB v1 (encoding `1`).
    CvtRb,
    /// CVT-R2 / CVT-RB v2 (encoding `2`).
    CvtR2,
    /// Spec-reserved encoding (`3`–`7`) preserved verbatim so unknown values do not block
    /// decoding of the rest of the descriptor.
    Reserved(u8),
}

impl CvtAlgorithm {
    /// Decodes the 3-bit CVT algorithm field (Type V/IX descriptor byte 0 bits 2:0).
    /// Upper bits of the input are ignored.
    pub const fn from_bits(b: u8) -> Self {
        match b & 0x07 {
            0 => Self::Cvt,
            1 => Self::CvtRb,
            2 => Self::CvtR2,
            other => Self::Reserved(other),
        }
    }
}

/// Stereo timing mode decoded from Type V (`0x11`) and Type IX (`0x24`) descriptor byte 0
/// bits 6:5. Indicates whether the timing is for a mono display, stereo-only, or
/// user-selectable.
///
/// This is distinct from [`StereoMode`], which describes the specific stereo viewing method
/// decoded from a Detailed Timing Descriptor.
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TypeIxStereoMode {
    /// Mono timing (bits 6:5 = `0b00`).
    Mono,
    /// 3D stereo timing (bits 6:5 = `0b01`).
    Stereo,
    /// Mono or 3D stereo depending on user action (bits 6:5 = `0b10`).
    MonoOrStereoByUser,
    /// Reserved (bits 6:5 = `0b11`).
    Reserved,
}

/// Sync signal definition decoded from DTD byte 17 bits 4–1.
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncDefinition {
    /// Analog composite sync (bit 4 = 0, bit 3 = 0).
    AnalogComposite {
        /// H-sync pulse present during V-sync (serrations).
        serrations: bool,
        /// Sync on all three RGB signals (`true`) or green only (`false`).
        sync_on_all_rgb: bool,
    },
    /// Bipolar analog composite sync (bit 4 = 0, bit 3 = 1).
    BipolarAnalogComposite {
        /// H-sync pulse present during V-sync (serrations).
        serrations: bool,
        /// Sync on all three RGB signals (`true`) or green only (`false`).
        sync_on_all_rgb: bool,
    },
    /// Digital composite sync on H-sync pin (bit 4 = 1, bit 3 = 0).
    DigitalComposite {
        /// H-sync pulse present during V-sync (serrations).
        serrations: bool,
        /// H-sync polarity outside V-sync: `true` = positive.
        h_sync_positive: bool,
    },
    /// Digital separate sync (bit 4 = 1, bit 3 = 1).
    DigitalSeparate {
        /// V-sync polarity: `true` = positive.
        v_sync_positive: bool,
        /// H-sync polarity: `true` = positive.
        h_sync_positive: bool,
    },
}

/// The source from which a [`VideoMode`] was decoded.
///
/// Populated automatically by [`vic_to_mode`][crate::cea861::vic_to_mode] and
/// [`dmt_to_mode`][crate::cea861::dmt_to_mode]; parsers that decode Detailed Timing
/// Descriptors should set it via [`VideoMode::with_source`]. `None` for modes
/// constructed directly via [`VideoMode::new`].
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModeSource {
    /// A CTA-861 Video Identification Code, as used in Short Video Descriptors,
    /// the Y420 Video Data Block, and the Y420 Capability Map Data Block.
    Vic(u8),
    /// A VESA Display Monitor Timings identifier (0x01–0x58).
    DmtId(u16),
    /// Zero-based index of a Detailed Timing Descriptor within its containing EDID block.
    DtdIndex(u8),
}

/// A display refresh rate expressed as an exact rational number (numerator/denominator in Hz).
///
/// Integer rates (60 Hz, 120 Hz, etc.) use `denom = 1`. NTSC-derived fractional rates use
/// `denom = 1001` (e.g. 60000/1001 ≈ 59.94 Hz, 24000/1001 ≈ 23.976 Hz).
///
/// Always stored in lowest terms: all constructors (including `Deserialize`) apply GCD
/// reduction, so `==`, `Hash`, and `Ord` are consistent and a given rate has exactly one
/// canonical representation.
///
/// Use [`RefreshRate::integral`] for integer rates and [`RefreshRate::fractional`] for all
/// others. `From<u32>` and `From<u16>` are implemented as `integral` conversions, so
/// integer literals work wherever `impl Into<RefreshRate>` is accepted.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
    feature = "serde",
    serde(try_from = "RefreshRateRepr", into = "RefreshRateRepr")
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RefreshRate {
    numer: u32,
    denom: u32,
}

#[cfg(feature = "serde")]
#[derive(serde::Serialize, serde::Deserialize)]
struct RefreshRateRepr {
    numer: u32,
    denom: u32,
}

#[cfg(feature = "serde")]
impl core::convert::TryFrom<RefreshRateRepr> for RefreshRate {
    type Error = &'static str;
    fn try_from(r: RefreshRateRepr) -> Result<Self, Self::Error> {
        if r.denom == 0 {
            Err("RefreshRate denominator must not be zero")
        } else {
            Ok(Self::fractional(r.numer, r.denom))
        }
    }
}

#[cfg(feature = "serde")]
impl From<RefreshRate> for RefreshRateRepr {
    fn from(r: RefreshRate) -> Self {
        Self {
            numer: r.numer,
            denom: r.denom,
        }
    }
}

fn gcd(mut a: u32, mut b: u32) -> u32 {
    while b != 0 {
        let t = b;
        b = a % b;
        a = t;
    }
    a
}

fn gcd_u64(mut a: u64, mut b: u64) -> u64 {
    while b != 0 {
        let t = b;
        b = a % b;
        a = t;
    }
    a
}

impl RefreshRate {
    /// Constructs an integer refresh rate (e.g. `RefreshRate::integral(60)` → 60/1).
    pub fn integral(hz: u32) -> Self {
        Self {
            numer: hz,
            denom: 1,
        }
    }

    /// Constructs an exact rational refresh rate, reduced to lowest terms.
    ///
    /// # Panics
    ///
    /// Panics if `denom` is zero.
    pub fn fractional(numer: u32, denom: u32) -> Self {
        assert!(denom != 0, "RefreshRate denominator must not be zero");
        let g = gcd(numer, denom);
        Self {
            numer: numer / g,
            denom: denom / g,
        }
    }

    /// Constructs a refresh rate from a `numer/denom` ratio in Hz, reduced to lowest terms.
    ///
    /// Useful when the rate is computed from intermediate values that exceed `u32`, such as
    /// `pixel_clock_hz / (h_total × v_total)` for detailed-timing descriptors. Reduces in
    /// `u64` then narrows to `u32`.
    ///
    /// Returns `None` if `denom` is zero or if the reduced fraction does not fit in `u32`.
    ///
    /// ```
    /// use display_types::RefreshRate;
    ///
    /// // NTSC-style fractional rate computed from a large numerator and denominator.
    /// let r = RefreshRate::from_ratio(60_000_000, 1_001_000).unwrap();
    /// assert_eq!(r, RefreshRate::fractional(60_000, 1_001));
    /// ```
    pub fn from_ratio(numer: u64, denom: u64) -> Option<Self> {
        if denom == 0 {
            return None;
        }
        let g = gcd_u64(numer, denom);
        let n = u32::try_from(numer / g).ok()?;
        let d = u32::try_from(denom / g).ok()?;
        Some(Self { numer: n, denom: d })
    }

    /// Numerator of the reduced fraction, in Hz.
    pub fn numer(self) -> u32 {
        self.numer
    }

    /// Denominator of the reduced fraction (1 for integer rates, 1001 for NTSC-derived rates).
    pub fn denom(self) -> u32 {
        self.denom
    }

    /// Returns the refresh rate as `f64`.
    pub fn as_f64(self) -> f64 {
        self.numer as f64 / self.denom as f64
    }
}

impl PartialOrd for RefreshRate {
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl core::cmp::Ord for RefreshRate {
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
        (self.numer as u64 * other.denom as u64).cmp(&(other.numer as u64 * self.denom as u64))
    }
}

impl core::fmt::Display for RefreshRate {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        if self.denom == 1 {
            write!(f, "{} Hz", self.numer)
        } else {
            write!(f, "{}/{} Hz", self.numer, self.denom)
        }
    }
}

impl From<u32> for RefreshRate {
    fn from(hz: u32) -> Self {
        Self::integral(hz)
    }
}

impl From<u16> for RefreshRate {
    fn from(hz: u16) -> Self {
        Self::integral(hz as u32)
    }
}

/// A display video mode expressed as resolution, refresh rate, and scan type.
///
/// Use [`VideoMode::new`] to construct a mode with only identity fields (the common case
/// for modes decoded from standard timing or SVD entries). Use
/// [`VideoMode::with_detailed_timing`] to add the blanking-interval and signal fields
/// available from a Detailed Timing Descriptor or equivalent.
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct VideoMode {
    /// Horizontal resolution in pixels.
    pub width: u16,
    /// Vertical resolution in pixels.
    pub height: u16,
    /// Refresh rate as an exact rational number in Hz, or `None` when unspecified
    /// (e.g. a default-constructed `VideoMode` whose rate has not been set).
    pub refresh_rate: Option<RefreshRate>,
    /// `true` for interlaced modes; `false` for progressive (the common case).
    pub interlaced: bool,
    /// Horizontal front porch in pixels (0 when not decoded from a DTD).
    pub h_front_porch: u16,
    /// Horizontal sync pulse width in pixels (0 when not decoded from a DTD).
    pub h_sync_width: u16,
    /// Vertical front porch in lines (0 when not decoded from a DTD).
    pub v_front_porch: u16,
    /// Vertical sync pulse width in lines (0 when not decoded from a DTD).
    pub v_sync_width: u16,
    /// Horizontal border width in pixels on each side of the active area (0 when not from a DTD).
    pub h_border: u8,
    /// Vertical border height in lines on each side of the active area (0 when not from a DTD).
    pub v_border: u8,
    /// Stereo viewing support (default [`StereoMode::None`] for non-DTD modes).
    pub stereo: StereoMode,
    /// Sync signal definition (`None` for non-DTD modes).
    pub sync: Option<SyncDefinition>,
    /// Pixel clock in kHz (`None` for modes not decoded from a Detailed Timing Descriptor).
    pub pixel_clock_khz: Option<u32>,
    /// The source from which this mode was decoded, if known.
    ///
    /// `None` for modes constructed directly via [`VideoMode::new`] without a table lookup.
    pub source: Option<ModeSource>,
    /// CVT formula selector for DisplayID 2.x Type V/IX timings (`None` for all other sources).
    /// Consumers can use this to derive blanking and pixel clock from `(width, height,
    /// refresh_rate)` via the named CVT variant.
    pub cvt_algorithm: Option<CvtAlgorithm>,
    /// `true` when the timing is YCbCr 4:2:0 only. Set from CTA-861 Y420 capability data
    /// and from DisplayID 2.x Type VII byte 3 bit 7 (block revision ≥ 2).
    /// Defaults to `false` for all other sources.
    pub y420: bool,
    /// `true` when NTSC-style fractional refresh rate (× 1000/1001) is supported alongside
    /// this timing. Decoded from Type V and Type IX descriptor byte 0 bit 3.
    /// Defaults to `false` for all other sources.
    pub ntsc_fractional_refresh: bool,
    /// Per-mode stereo indicator from Type V (`0x11`) and Type IX (`0x24`) descriptor byte 0
    /// bits 6:5. `None` for all other timing sources.
    pub type_ix_stereo: Option<TypeIxStereoMode>,
}

impl VideoMode {
    /// Constructs a `VideoMode` with the given identity fields.
    ///
    /// All blanking-interval fields (`h_front_porch`, `h_sync_width`, `v_front_porch`,
    /// `v_sync_width`, `h_border`, `v_border`) default to `0`, `stereo` defaults to
    /// [`StereoMode::None`], and `sync` defaults to `None`. Use
    /// [`with_detailed_timing`][Self::with_detailed_timing] to set those fields when
    /// decoding from a Detailed Timing Descriptor.
    pub fn new(
        width: u16,
        height: u16,
        refresh_rate: impl Into<RefreshRate>,
        interlaced: bool,
    ) -> Self {
        Self {
            width,
            height,
            refresh_rate: Some(refresh_rate.into()),
            interlaced,
            ..Self::default()
        }
    }

    /// Sets the exact pixel clock in kHz, returning the updated mode.
    ///
    /// Use this when constructing a [`VideoMode`] from hardware timing registers or a
    /// known-good mode table entry, where the exact pixel clock is available but full
    /// Detailed Timing Descriptor fields are not. The supplied clock is returned verbatim
    /// by [`pixel_clock_khz`][crate::pixel_clock_khz], bypassing the CVT-RB fallback
    /// estimate.
    ///
    /// ```
    /// use display_types::VideoMode;
    /// use display_types::pixel_clock_khz;
    ///
    /// // Custom panel: 1920×1200 @ 60 Hz, exact pixel clock from PLL register.
    /// let mode = VideoMode::new(1920, 1200, 60u32, false).with_pixel_clock(154_000);
    /// assert_eq!(pixel_clock_khz(&mode), 154_000);
    /// ```
    pub fn with_pixel_clock(mut self, pixel_clock_khz: u32) -> Self {
        self.pixel_clock_khz = Some(pixel_clock_khz);
        self
    }

    /// Sets the CVT formula selector, returning the updated mode. Used by DisplayID 2.x
    /// Type IX (`0x24`) descriptors which signal a CVT variant alongside `(width, height,
    /// refresh_rate)`.
    pub fn with_cvt_algorithm(mut self, alg: CvtAlgorithm) -> Self {
        self.cvt_algorithm = Some(alg);
        self
    }

    /// Sets the YCbCr 4:2:0 flag, returning the updated mode. Used by DisplayID 2.x
    /// Type VII decoders (block revision ≥ 2) and by callers that derive Y420-only modes
    /// from CTA-861 Y420 capability data.
    pub fn with_y420(mut self, y420: bool) -> Self {
        self.y420 = y420;
        self
    }

    /// Sets the NTSC fractional refresh flag, returning the updated mode. Used by
    /// Type V and Type IX decoders when byte 0 bit 3 is set.
    pub fn with_ntsc_fractional_refresh(mut self, supported: bool) -> Self {
        self.ntsc_fractional_refresh = supported;
        self
    }

    /// Sets the per-mode stereo indicator from Type V/IX byte 0 bits 6:5, returning the
    /// updated mode.
    pub fn with_type_ix_stereo(mut self, stereo: TypeIxStereoMode) -> Self {
        self.type_ix_stereo = Some(stereo);
        self
    }

    /// Sets the mode source, returning the updated mode.
    ///
    /// Called automatically by [`vic_to_mode`][crate::cea861::vic_to_mode] and
    /// [`dmt_to_mode`][crate::cea861::dmt_to_mode]. Parsers decoding Detailed Timing
    /// Descriptors should call `.with_source(ModeSource::DtdIndex(n))` so that the
    /// descriptor's position survives into negotiated output.
    pub fn with_source(mut self, source: ModeSource) -> Self {
        self.source = Some(source);
        self
    }

    /// Adds blanking-interval and signal fields decoded from a Detailed Timing Descriptor
    /// or equivalent source, returning the updated mode.
    ///
    /// The 9-parameter count mirrors the DTD fields directly (EDID §3.10.3 / DisplayID §4.4).
    #[allow(clippy::too_many_arguments)]
    pub fn with_detailed_timing(
        mut self,
        pixel_clock_khz: u32,
        h_front_porch: u16,
        h_sync_width: u16,
        v_front_porch: u16,
        v_sync_width: u16,
        h_border: u8,
        v_border: u8,
        stereo: StereoMode,
        sync: Option<SyncDefinition>,
    ) -> Self {
        self.pixel_clock_khz = Some(pixel_clock_khz);
        self.h_front_porch = h_front_porch;
        self.h_sync_width = h_sync_width;
        self.v_front_porch = v_front_porch;
        self.v_sync_width = v_sync_width;
        self.h_border = h_border;
        self.v_border = v_border;
        self.stereo = stereo;
        self.sync = sync;
        self
    }
}

/// EDID specification version and revision, decoded from base block bytes 18–19.
///
/// Most displays in use report version 1 with revision 3 or 4.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EdidVersion {
    /// EDID version number (byte 18). Always `1` for all current displays.
    pub version: u8,
    /// EDID revision number (byte 19).
    pub revision: u8,
}

impl core::fmt::Display for EdidVersion {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "{}.{}", self.version, self.revision)
    }
}

/// Trait for typed data stored in [`DisplayCapabilities::extension_data`] by custom handlers.
///
/// A blanket implementation covers any type that is `Any + Debug + Send + Sync`, so consumers
/// do not need to implement this trait manually — `#[derive(Debug)]` on a `Send + Sync` type
/// is sufficient.
#[cfg(any(feature = "alloc", feature = "std"))]
pub trait ExtensionData: core::any::Any + core::fmt::Debug + Send + Sync {
    /// Returns `self` as `&dyn Any` to enable downcasting.
    fn as_any(&self) -> &dyn core::any::Any;
}

#[cfg(any(feature = "alloc", feature = "std"))]
impl<T: core::any::Any + core::fmt::Debug + Send + Sync> ExtensionData for T {
    fn as_any(&self) -> &dyn core::any::Any {
        self
    }
}

/// Consumer-facing display capability model produced by a display data parser.
///
/// All fields defined by the relevant specification are decoded and exposed here.
/// No field is omitted because it appears obscure or unlikely to be needed — that
/// judgement belongs to the consumer, not the library.
///
/// Fields are `Option` where the underlying data may be absent or undecodable.
/// `None` means the value was not present or could not be reliably determined; it does
/// not imply the field is unimportant. The library never invents or defaults data.
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)]
pub struct DisplayCapabilities {
    /// Three-character PNP manufacturer ID (e.g. `GSM` for LG, `SAM` for Samsung).
    pub manufacturer: Option<crate::manufacture::ManufacturerId>,
    /// Manufacture date or model year.
    pub manufacture_date: Option<crate::manufacture::ManufactureDate>,
    /// EDID specification version and revision.
    pub edid_version: Option<EdidVersion>,
    /// Manufacturer-assigned product code.
    pub product_code: Option<u16>,
    /// Manufacturer-assigned serial number, if encoded numerically in the base block.
    pub serial_number: Option<u32>,
    /// Serial number string from the monitor serial number descriptor (`0xFF`), if present.
    pub serial_number_string: Option<crate::manufacture::MonitorString>,
    /// Human-readable display name from the monitor name descriptor, if present.
    pub display_name: Option<crate::manufacture::MonitorString>,
    /// Unspecified ASCII text strings from `0xFE` descriptors, in descriptor slot order.
    ///
    /// Up to four entries (one per descriptor slot). Each slot is `None` if the corresponding
    /// descriptor was not a `0xFE` entry.
    pub unspecified_text: [Option<crate::manufacture::MonitorString>; 4],
    /// Additional white points from the `0xFB` descriptor.
    ///
    /// Up to two entries (the EDID `0xFB` descriptor has two fixed slots). Each slot is
    /// `None` if the corresponding entry was unused (index byte `0x00`).
    pub white_points: [Option<crate::color::WhitePoint>; 2],
    /// `true` if the display uses a digital input interface.
    pub digital: bool,
    /// Color bit depth per primary channel.
    /// `None` for analog displays or when the field is undefined or reserved.
    pub color_bit_depth: Option<crate::color::ColorBitDepth>,
    /// Physical display technology (e.g. TFT, OLED, PDP).
    /// `None` when the Display Device Data Block is absent.
    pub display_technology: Option<crate::panel::DisplayTechnology>,
    /// Technology-specific sub-type code (raw, 0–15).
    /// `None` when the Display Device Data Block is absent.
    pub display_subtype: Option<u8>,
    /// Panel operating mode (continuous or non-continuous refresh).
    /// `None` when the Display Device Data Block is absent.
    pub operating_mode: Option<crate::panel::OperatingMode>,
    /// Backlight type.
    /// `None` when the Display Device Data Block is absent.
    pub backlight_type: Option<crate::panel::BacklightType>,
    /// Whether the panel uses a Data Enable (DE) signal.
    /// `None` when the Display Device Data Block is absent.
    pub data_enable_used: Option<bool>,
    /// Data Enable signal polarity: `true` = positive, `false` = negative.
    /// Valid only when `data_enable_used` is `Some(true)`.
    /// `None` when the Display Device Data Block is absent.
    pub data_enable_positive: Option<bool>,
    /// Native pixel format `(width_px, height_px)`.
    /// `None` when the Display Device Data Block is absent or either dimension is zero.
    pub native_pixels: Option<(u16, u16)>,
    /// Panel aspect ratio encoded as `(AR − 1) × 100` (raw byte).
    /// For example `77` represents approximately 16:9 (AR ≈ 1.77). `None` when the block is absent.
    pub panel_aspect_ratio_100: Option<u8>,
    /// Physical mounting orientation of the panel.
    /// `None` when the Display Device Data Block is absent.
    pub physical_orientation: Option<crate::panel::PhysicalOrientation>,
    /// Panel rotation capability.
    /// `None` when the Display Device Data Block is absent.
    pub rotation_capability: Option<crate::panel::RotationCapability>,
    /// Location of the zero (origin) pixel in the framebuffer.
    /// `None` when the Display Device Data Block is absent.
    pub zero_pixel_location: Option<crate::panel::ZeroPixelLocation>,
    /// Fast-scan direction relative to H-sync.
    /// `None` when the Display Device Data Block is absent.
    pub scan_direction: Option<crate::panel::ScanDirection>,
    /// Sub-pixel color filter arrangement.
    /// `None` when the Display Device Data Block is absent.
    pub subpixel_layout: Option<crate::panel::SubpixelLayout>,
    /// Pixel pitch `(horizontal_hundredths_mm, vertical_hundredths_mm)` in 0.01 mm units.
    /// `None` when the Display Device Data Block is absent or either pitch is zero.
    pub pixel_pitch_hundredths_mm: Option<(u8, u8)>,
    /// Pixel response time in milliseconds.
    /// `None` when the Display Device Data Block is absent or the value is zero.
    pub pixel_response_time_ms: Option<u8>,
    /// Interface power sequencing timing parameters.
    /// `None` when the Interface Power Sequencing Block is absent.
    pub power_sequencing: Option<crate::panel::PowerSequencing>,
    /// Display luminance transfer function.
    /// `None` when the Transfer Characteristics Block is absent.
    #[cfg(any(feature = "alloc", feature = "std"))]
    pub transfer_characteristic: Option<crate::transfer::DisplayIdTransferCharacteristic>,
    /// Physical display interface capabilities.
    /// `None` when the Display Interface Data Block is absent.
    pub display_id_interface: Option<crate::panel::DisplayIdInterface>,
    /// Stereo display interface parameters.
    /// `None` when the Stereo Display Interface Data Block is absent.
    pub stereo_interface: Option<crate::panel::DisplayIdStereoInterface>,
    /// Tiled display topology.
    /// `None` when the Tiled Display Topology Data Block is absent.
    pub tiled_topology: Option<crate::panel::DisplayIdTiledTopology>,
    /// CIE xy chromaticity coordinates for the color primaries and white point.
    pub chromaticity: crate::color::Chromaticity,
    /// Display gamma. `None` if the display did not specify a gamma value.
    pub gamma: Option<crate::color::DisplayGamma>,
    /// Display feature support flags.
    pub display_features: Option<crate::features::DisplayFeatureFlags>,
    /// Supported color encoding formats. Only populated for EDID 1.4+ digital displays.
    pub digital_color_encoding: Option<crate::color::DigitalColorEncoding>,
    /// Color type for analog displays; `None` for the undefined value (`0b11`).
    pub analog_color_type: Option<crate::color::AnalogColorType>,
    /// Video interface type.
    /// `None` for analog displays or when the field is undefined or reserved.
    pub video_interface: Option<crate::input::VideoInterface>,
    /// Analog sync and video white levels. Only populated for analog displays.
    pub analog_sync_level: Option<crate::input::AnalogSyncLevel>,
    /// Physical screen dimensions or aspect ratio.
    /// `None` when both bytes are zero (undefined).
    pub screen_size: Option<crate::screen::ScreenSize>,
    /// Minimum supported vertical refresh rate in Hz.
    pub min_v_rate: Option<u16>,
    /// Maximum supported vertical refresh rate in Hz.
    pub max_v_rate: Option<u16>,
    /// Minimum supported horizontal scan rate in kHz.
    pub min_h_rate_khz: Option<u16>,
    /// Maximum supported horizontal scan rate in kHz.
    pub max_h_rate_khz: Option<u16>,
    /// Maximum pixel clock in MHz.
    pub max_pixel_clock_mhz: Option<u16>,
    /// Physical image area dimensions in millimetres `(width_mm, height_mm)`.
    ///
    /// More precise than [`screen_size`][Self::screen_size] (which is in cm).
    /// `None` when all DTD image-size fields are zero.
    pub preferred_image_size_mm: Option<(u16, u16)>,
    /// Video timing formula reported in the display range limits descriptor.
    pub timing_formula: Option<crate::timing::TimingFormula>,
    /// DCM polynomial coefficients.
    pub color_management: Option<crate::color::ColorManagementData>,
    /// Video modes decoded from the display data.
    #[cfg(any(feature = "alloc", feature = "std"))]
    pub supported_modes: crate::prelude::Vec<VideoMode>,
    /// Non-fatal conditions collected from the parser and all handlers.
    ///
    /// Not serialized — use a custom handler to map warnings to a serializable form.
    #[cfg(any(feature = "alloc", feature = "std"))]
    #[cfg_attr(feature = "serde", serde(skip))]
    pub warnings: crate::prelude::Vec<ParseWarning>,
    /// Typed data attached by extension handlers, keyed by extension tag byte.
    ///
    /// Uses a `Vec` of `(tag, data)` pairs rather than a `HashMap` so that this field is
    /// available in `alloc`-only (no_std) builds. The number of distinct extension tags in
    /// any real EDID is small enough that linear scan is negligible.
    ///
    /// Not serialized — use a custom handler to map this to a serializable form.
    #[cfg(any(feature = "alloc", feature = "std"))]
    #[cfg_attr(feature = "serde", serde(skip))]
    pub extension_data: crate::prelude::Vec<(u8, crate::prelude::Arc<dyn ExtensionData>)>,
}

#[cfg(any(feature = "alloc", feature = "std"))]
impl DisplayCapabilities {
    /// Returns an iterator over all collected warnings.
    pub fn iter_warnings(&self) -> impl Iterator<Item = &ParseWarning> {
        self.warnings.iter()
    }

    /// Appends a warning, wrapping it in a [`ParseWarning`].
    pub fn push_warning(&mut self, w: impl core::error::Error + Send + Sync + 'static) {
        self.warnings.push(crate::prelude::Arc::new(w));
    }

    /// Store typed data from a handler, keyed by an extension tag.
    /// Replaces any previously stored entry for the same tag.
    pub fn set_extension_data<T: ExtensionData>(&mut self, tag: u8, data: T) {
        if let Some(entry) = self.extension_data.iter_mut().find(|(t, _)| *t == tag) {
            entry.1 = crate::prelude::Arc::new(data);
        } else {
            self.extension_data
                .push((tag, crate::prelude::Arc::new(data)));
        }
    }

    /// Retrieve typed data previously stored by a handler for the given tag.
    /// Returns `None` if no data is stored for the tag or the type does not match.
    pub fn get_extension_data<T: core::any::Any>(&self, tag: u8) -> Option<&T> {
        self.extension_data
            .iter()
            .find(|(t, _)| *t == tag)
            // `**data` deref-chains through `&` then through Arc's Deref to reach
            // `dyn ExtensionData`, forcing vtable dispatch for `as_any()`.
            // Calling `.as_any()` on `&Arc<dyn ExtensionData>` would hit the blanket
            // `ExtensionData` impl for Arc itself and return the wrong TypeId.
            .and_then(|(_, data)| (**data).as_any().downcast_ref::<T>())
    }

    /// Removes the extension data entry for `tag` and returns it as `T`.
    ///
    /// Intended for take-mutate-restore patterns where multiple input sources contribute
    /// to a single extension's capability struct (e.g. CTA-861 data delivered both via
    /// the CEA-861 extension block and via the DisplayID 2.x CTA DisplayID block 0x81).
    /// The caller mutates the returned value and stores it back with
    /// [`set_extension_data`][Self::set_extension_data].
    ///
    /// Returns `None` if no entry exists for `tag` or the stored type is not `T`.
    /// When the type does not match, the entry is left in place.
    pub fn take_extension_data<T: ExtensionData + Clone>(&mut self, tag: u8) -> Option<T> {
        let pos = self.extension_data.iter().position(|(t, _)| *t == tag)?;
        // Peek before removing — type mismatch must not destroy the entry.
        (*self.extension_data[pos].1).as_any().downcast_ref::<T>()?;
        let (_, arc) = self.extension_data.remove(pos);
        (*arc).as_any().downcast_ref::<T>().cloned()
    }
}

#[cfg(test)]
mod refresh_rate_tests {
    use super::*;

    #[test]
    fn integral_has_unit_denominator() {
        let r = RefreshRate::integral(60);
        assert_eq!(r.numer(), 60);
        assert_eq!(r.denom(), 1);
    }

    #[test]
    fn fractional_reduces_to_lowest_terms() {
        let r = RefreshRate::fractional(120, 2);
        assert_eq!(r.numer(), 60);
        assert_eq!(r.denom(), 1);

        let ntsc = RefreshRate::fractional(60000, 1001);
        assert_eq!(ntsc.numer(), 60000);
        assert_eq!(ntsc.denom(), 1001);
    }

    #[test]
    #[should_panic(expected = "RefreshRate denominator must not be zero")]
    fn fractional_panics_on_zero_denominator() {
        let _ = RefreshRate::fractional(60, 0);
    }

    #[test]
    fn equality_is_canonical() {
        assert_eq!(RefreshRate::integral(60), RefreshRate::fractional(120, 2));
        assert_ne!(
            RefreshRate::integral(60),
            RefreshRate::fractional(60000, 1001)
        );
    }

    #[test]
    fn ord_uses_cross_multiplication() {
        let ntsc = RefreshRate::fractional(60000, 1001);
        let sixty = RefreshRate::integral(60);
        let fiftynine = RefreshRate::integral(59);
        assert!(ntsc < sixty);
        assert!(ntsc > fiftynine);
        assert_eq!(
            sixty.cmp(&RefreshRate::fractional(120, 2)),
            core::cmp::Ordering::Equal
        );
    }

    #[test]
    #[cfg(any(feature = "alloc", feature = "std"))]
    fn display_formats_integer_and_fractional() {
        extern crate alloc;
        use alloc::format;
        assert_eq!(format!("{}", RefreshRate::integral(60)), "60 Hz");
        assert_eq!(
            format!("{}", RefreshRate::fractional(60000, 1001)),
            "60000/1001 Hz"
        );
    }

    #[test]
    fn from_integer_uses_integral() {
        let from_u32: RefreshRate = 144u32.into();
        let from_u16: RefreshRate = 60u16.into();
        assert_eq!(from_u32, RefreshRate::integral(144));
        assert_eq!(from_u16, RefreshRate::integral(60));
    }

    #[test]
    fn as_f64_normalises() {
        let delta = RefreshRate::fractional(60000, 1001).as_f64() - 59.94;
        assert!(delta.abs() < 0.01);
    }

    #[test]
    fn from_ratio_reduces_large_values() {
        // 1080p@59.94: pc = 148_352 kHz, h_total × v_total = 2200 × 1125 = 2_475_000
        // 148_352_000 / 2_475_000 = 59.9402… (non-canonical reduction).
        let r = RefreshRate::from_ratio(148_352_000, 2_475_000).unwrap();
        // gcd(148_352_000, 2_475_000) = 1000 → 148_352 / 2_475
        assert_eq!(r.numer(), 148_352);
        assert_eq!(r.denom(), 2_475);
    }

    #[test]
    fn from_ratio_canonicalises_integer_rate() {
        // 60 Hz exact: pc = 148_500 kHz, total = 2_475_000 → 148_500_000 / 2_475_000 = 60.
        let r = RefreshRate::from_ratio(148_500_000, 2_475_000).unwrap();
        assert_eq!(r, RefreshRate::integral(60));
    }

    #[test]
    fn from_ratio_returns_none_on_zero_denominator() {
        assert_eq!(RefreshRate::from_ratio(60, 0), None);
    }

    #[test]
    fn from_ratio_handles_zero_numerator() {
        let r = RefreshRate::from_ratio(0, 1000).unwrap();
        assert_eq!(r, RefreshRate::integral(0));
    }

    #[test]
    fn from_ratio_returns_none_when_reduced_exceeds_u32() {
        // Coprime values both ≥ 2^32 cannot reduce into u32.
        let big = u64::from(u32::MAX) + 2;
        assert_eq!(RefreshRate::from_ratio(big, big - 1), None);
    }
}

#[cfg(test)]
#[cfg(any(feature = "alloc", feature = "std"))]
mod extension_data_tests {
    use super::*;

    #[derive(Debug, Clone, PartialEq)]
    struct Foo(u32);

    #[derive(Debug, Clone, PartialEq)]
    struct Bar(u32);

    #[test]
    fn take_extension_data_returns_and_removes_entry() {
        let mut caps = DisplayCapabilities::default();
        caps.set_extension_data(0x70, Foo(42));
        let taken: Foo = caps.take_extension_data(0x70).expect("entry present");
        assert_eq!(taken, Foo(42));
        assert!(caps.get_extension_data::<Foo>(0x70).is_none());
    }

    #[test]
    fn take_extension_data_returns_none_for_missing_tag() {
        let mut caps = DisplayCapabilities::default();
        assert!(caps.take_extension_data::<Foo>(0x70).is_none());
    }

    #[test]
    fn take_extension_data_leaves_entry_on_type_mismatch() {
        let mut caps = DisplayCapabilities::default();
        caps.set_extension_data(0x70, Foo(7));
        // Wrong type — must return None and not destroy the entry.
        assert!(caps.take_extension_data::<Bar>(0x70).is_none());
        // Entry still retrievable as the original type.
        assert_eq!(caps.get_extension_data::<Foo>(0x70), Some(&Foo(7)));
    }

    #[test]
    fn take_extension_data_round_trip_via_set() {
        let mut caps = DisplayCapabilities::default();
        caps.set_extension_data(0x70, Foo(1));
        let mut foo: Foo = caps.take_extension_data(0x70).unwrap();
        foo.0 += 1;
        caps.set_extension_data(0x70, foo);
        assert_eq!(caps.get_extension_data::<Foo>(0x70), Some(&Foo(2)));
    }
}