Skip to main content

display_types/displayid/
mod.rs

1//! Types decoded from DisplayID 1.x and 2.x extension blocks (EDID extension tag `0x70`).
2
3#[cfg(any(feature = "alloc", feature = "std"))]
4use crate::Vec;
5
6/// DisplayID 1.x and 2.x data block tag constants.
7pub mod tag;
8
9/// DisplayID 1.x display product primary use case constants.
10pub mod product_type;
11
12/// A single CIE chromaticity coordinate pair encoded as 12-bit fixed-point integers.
13///
14/// Used by DisplayID 2.x block 0x21. Each raw value is in the range `0..4096`, representing
15/// a coordinate in `[0.0, 1.0)` with scale factor `2⁻¹²` (divide by 4096 to normalise).
16/// The encoding may use CIE 1931 (x, y) or CIE 1976 (u', v') coordinates depending on
17/// the `color_space_cie1976` flag in [`DisplayParamsV2`].
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub struct ChromaticityPoint12 {
21    /// Raw 12-bit x (or u') value.
22    pub x_raw: u16,
23    /// Raw 12-bit y (or v') value.
24    pub y_raw: u16,
25}
26
27impl ChromaticityPoint12 {
28    /// First coordinate (x or u'), normalised to `[0.0, 1.0)`.
29    pub fn x(&self) -> f32 {
30        self.x_raw as f32 / 4096.0
31    }
32
33    /// Second coordinate (y or v'), normalised to `[0.0, 1.0)`.
34    pub fn y(&self) -> f32 {
35        self.y_raw as f32 / 4096.0
36    }
37}
38
39/// Factory-calibrated color primaries and white point from DisplayID 2.x block 0x21.
40///
41/// Chromaticity values use 12-bit fixed-point encoding; see [`ChromaticityPoint12`].
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub struct Chromaticity12 {
45    /// Primary color 1 (red) chromaticity.
46    pub primary1: ChromaticityPoint12,
47    /// Primary color 2 (green) chromaticity.
48    pub primary2: ChromaticityPoint12,
49    /// Primary color 3 (blue) chromaticity.
50    pub primary3: ChromaticityPoint12,
51    /// White point chromaticity.
52    pub white: ChromaticityPoint12,
53}
54
55/// Display parameters decoded from DisplayID 2.x block 0x21.
56///
57/// Contains factory-calibrated colorimetry (12-bit chromaticity), HDR luminance
58/// levels, color depth, display technology, and gamma. Luminance values are
59/// transmitted on-wire as IEEE 754 half-precision (`f16`) and converted to `f32`
60/// by the decoder before storage. Image size and pixel count are exposed separately
61/// on [`DisplayCapabilities`][crate::DisplayCapabilities] via `preferred_image_size_mm`
62/// and `native_pixels`.
63#[non_exhaustive]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65#[derive(Debug, Clone, PartialEq, Default)]
66pub struct DisplayParamsV2 {
67    /// Factory-calibrated chromaticity for the three primaries and white point.
68    pub chromaticity: Chromaticity12,
69    /// `true` if chromaticity values use CIE 1976 (u', v') coordinates;
70    /// `false` (default) for CIE 1931 (x, y).
71    pub color_space_cie1976: bool,
72    /// Maximum luminance at full-screen coverage in cd/m². `None` if not specified.
73    pub max_luminance_full: Option<f32>,
74    /// Maximum luminance at 10% screen coverage in cd/m². `None` if not specified.
75    pub max_luminance_10pct: Option<f32>,
76    /// Minimum luminance in cd/m². `None` if not specified.
77    pub min_luminance: Option<f32>,
78    /// `true` if non-zero luminance values are source guidance rather than guaranteed minima.
79    pub luminance_guidance: bool,
80    /// Color bit depth per channel (6, 8, 10, 12, 14, or 16). `None` if not defined.
81    pub color_bit_depth: Option<u8>,
82    /// Display technology decoded from byte 10 of block 0x21.
83    pub display_technology: DisplayTechnology,
84    /// Gamma EOTF in range 1.00–3.54. `None` if unspecified (stored byte `0xFF`).
85    pub gamma: Option<f32>,
86    /// Scan orientation decoded from bits 2:0 of byte 11 of block 0x21.
87    pub scan_orientation: ScanOrientation,
88    /// `true` if audio output uses an external jack rather than integrated speakers.
89    pub audio_external: bool,
90}
91
92/// Display technology family, decoded from byte 10 of DisplayID 2.x block 0x21.
93///
94/// Unknown byte values are preserved via [`DisplayTechnology::Other`] so a spec-defined
95/// future value (e.g. LCoS, microLED) does not make the containing block un-decodable.
96///
97/// `Other` is `#[non_exhaustive]` and cannot be constructed outside this crate; use
98/// [`from_byte`][Self::from_byte] (which canonicalises known bytes) or [`other`][Self::other]
99/// (which rejects known bytes) so that every value satisfies the invariant
100/// `from_byte(t.as_byte()) == t`.
101#[non_exhaustive]
102#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
104pub enum DisplayTechnology {
105    /// Not specified (byte value `0x00`).
106    #[default]
107    Unspecified,
108    /// Active-matrix LCD (byte value `0x01`).
109    Amlcd,
110    /// Active-matrix OLED (byte value `0x02`).
111    Amoled,
112    /// Reserved or vendor-specific value the decoder did not recognise. Construction
113    /// is restricted to this crate; external callers must go through [`from_byte`][Self::from_byte]
114    /// or [`other`][Self::other].
115    #[non_exhaustive]
116    Other(u8),
117}
118
119impl DisplayTechnology {
120    /// Decodes the raw byte 10 value, mapping known bytes to their named variant.
121    pub fn from_byte(b: u8) -> Self {
122        match b {
123            0x00 => Self::Unspecified,
124            0x01 => Self::Amlcd,
125            0x02 => Self::Amoled,
126            other => Self::Other(other),
127        }
128    }
129
130    /// Constructs an `Other(b)` for a non-canonical byte value.
131    ///
132    /// Returns `None` for `0x00`, `0x01`, and `0x02` — those bytes have named variants
133    /// and constructing `Other` for them would break the round-trip invariant
134    /// `from_byte(t.as_byte()) == t`. Callers decoding raw EDID bytes should use
135    /// [`from_byte`][Self::from_byte] instead.
136    pub fn other(b: u8) -> Option<Self> {
137        match b {
138            0x00..=0x02 => None,
139            x => Some(Self::Other(x)),
140        }
141    }
142
143    /// Returns the raw byte 10 representation.
144    pub fn as_byte(self) -> u8 {
145        match self {
146            Self::Unspecified => 0x00,
147            Self::Amlcd => 0x01,
148            Self::Amoled => 0x02,
149            Self::Other(b) => b,
150        }
151    }
152}
153
154/// Pixel scan orientation, decoded from bits 2:0 of byte 11 of DisplayID 2.x block 0x21.
155///
156/// Each variant names the fast (pixel) axis followed by the slow (line) axis. For example,
157/// [`LeftRightTopBottom`][Self::LeftRightTopBottom] means pixels are painted left-to-right
158/// within a line and lines advance top-to-bottom — the conventional raster order.
159#[non_exhaustive]
160#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
162pub enum ScanOrientation {
163    /// Left-to-right, top-to-bottom (`0b000`). Default raster order.
164    #[default]
165    LeftRightTopBottom,
166    /// Right-to-left, top-to-bottom (`0b001`).
167    RightLeftTopBottom,
168    /// Top-to-bottom, right-to-left (`0b010`).
169    TopBottomRightLeft,
170    /// Bottom-to-top, right-to-left (`0b011`).
171    BottomTopRightLeft,
172    /// Right-to-left, bottom-to-top (`0b100`).
173    RightLeftBottomTop,
174    /// Left-to-right, bottom-to-top (`0b101`).
175    LeftRightBottomTop,
176    /// Bottom-to-top, left-to-right (`0b110`).
177    BottomTopLeftRight,
178    /// Top-to-bottom, left-to-right (`0b111`).
179    TopBottomLeftRight,
180}
181
182impl ScanOrientation {
183    /// Decodes bits 2:0 of byte 11. Upper bits are ignored.
184    pub fn from_bits(b: u8) -> Self {
185        match b & 0b111 {
186            0b000 => Self::LeftRightTopBottom,
187            0b001 => Self::RightLeftTopBottom,
188            0b010 => Self::TopBottomRightLeft,
189            0b011 => Self::BottomTopRightLeft,
190            0b100 => Self::RightLeftBottomTop,
191            0b101 => Self::LeftRightBottomTop,
192            0b110 => Self::BottomTopLeftRight,
193            _ => Self::TopBottomLeftRight,
194        }
195    }
196
197    /// Returns the 3-bit encoding (bits 2:0).
198    pub fn as_bits(self) -> u8 {
199        match self {
200            Self::LeftRightTopBottom => 0b000,
201            Self::RightLeftTopBottom => 0b001,
202            Self::TopBottomRightLeft => 0b010,
203            Self::BottomTopRightLeft => 0b011,
204            Self::RightLeftBottomTop => 0b100,
205            Self::LeftRightBottomTop => 0b101,
206            Self::BottomTopLeftRight => 0b110,
207            Self::TopBottomLeftRight => 0b111,
208        }
209    }
210}
211
212/// Dynamic video timing range decoded from DisplayID 2.x block 0x25.
213///
214/// Pixel clocks are in 1 kHz steps; vertical refresh rates cover the full 9-bit range
215/// introduced in block revision 1.
216#[non_exhaustive]
217#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
219pub struct DynamicTimingRange {
220    /// Minimum pixel clock in kHz (3-byte LE field).
221    pub min_pixel_clock_khz: u32,
222    /// Maximum pixel clock in kHz (3-byte LE field).
223    pub max_pixel_clock_khz: u32,
224    /// Minimum vertical refresh rate in Hz.
225    pub min_v_rate_hz: u8,
226    /// Maximum vertical refresh rate in Hz (9-bit value; upper 2 bits from revision-1 flag byte).
227    pub max_v_rate_hz: u16,
228    /// Seamless variable refresh rate supported (fixed H pixel rate, dynamic V blanking).
229    pub vrr_supported: bool,
230}
231
232bitflags::bitflags! {
233    /// Color depths supported for full-bandwidth encodings (RGB and YCbCr 4:4:4) per
234    /// DisplayID 2.x block 0x26 bytes 0–1. Bit positions match the on-wire encoding.
235    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
236    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
237    pub struct ColorDepthsFull: u8 {
238        /// 6 bits per channel.
239        const BPC_6  = 0x01;
240        /// 8 bits per channel.
241        const BPC_8  = 0x02;
242        /// 10 bits per channel.
243        const BPC_10 = 0x04;
244        /// 12 bits per channel.
245        const BPC_12 = 0x08;
246        /// 14 bits per channel.
247        const BPC_14 = 0x10;
248        /// 16 bits per channel.
249        const BPC_16 = 0x20;
250    }
251}
252
253bitflags::bitflags! {
254    /// Color depths supported for chroma-subsampled YCbCr encodings (4:2:2 and 4:2:0)
255    /// per DisplayID 2.x block 0x26 bytes 2–3. 6 bpc is not representable: subsampled
256    /// formats start at 8 bpc. Bit positions match the on-wire encoding.
257    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
258    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
259    pub struct ColorDepthsSubsampled: u8 {
260        /// 8 bits per channel.
261        const BPC_8  = 0x01;
262        /// 10 bits per channel.
263        const BPC_10 = 0x02;
264        /// 12 bits per channel.
265        const BPC_12 = 0x04;
266        /// 14 bits per channel.
267        const BPC_14 = 0x08;
268        /// 16 bits per channel.
269        const BPC_16 = 0x10;
270    }
271}
272
273/// A custom `(color space, EOTF)` pairing from DisplayID 2.x block 0x26 payload bytes 9+.
274///
275/// Constructed via [`CustomColorSpaceEotfCombo::new`].
276///
277/// Each byte encodes one pair: bits 7:4 = color space index, bits 3:0 = EOTF index.
278/// Index values are defined by the DisplayID 2.x spec §4.6:
279///
280/// Color space: 0 = follow interface, 1 = sRGB, 2 = BT.601, 3 = BT.709,
281/// 4 = Adobe RGB, 5 = DCI-P3, 6 = BT.2020, 7 = custom.
282///
283/// EOTF: 0 = follow interface, 1 = sRGB, 2 = BT.601, 3 = BT.1886,
284/// 4 = Adobe RGB, 5 = DCI-P3, 6 = BT.2020, 7 = gamma (from display params),
285/// 8 = SMPTE ST 2084, 9 = hybrid log-gamma, 10 = custom.
286#[non_exhaustive]
287#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
289pub struct CustomColorSpaceEotfCombo {
290    /// Color space index (bits 7:4 of the wire byte), values 0–7.
291    pub color_space: u8,
292    /// EOTF index (bits 3:0 of the wire byte), values 0–10.
293    pub eotf: u8,
294}
295
296impl CustomColorSpaceEotfCombo {
297    /// Constructs a custom combo from raw nibble values.
298    pub fn new(color_space: u8, eotf: u8) -> Self {
299        Self { color_space, eotf }
300    }
301}
302
303/// Code-space selector for a [`StereoTimingCode`] in a DisplayID 2.x block 0x27.
304#[non_exhaustive]
305#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
306#[derive(Debug, Clone, Copy, PartialEq, Eq)]
307pub enum StereoTimingCodeType {
308    /// VESA Display Monitor Timings identifier (bits 7:6 = `0b00`).
309    Dmt,
310    /// CTA-861 Video Identification Code (bits 7:6 = `0b01`).
311    Vic,
312    /// HDMI Forum VIC (bits 7:6 = `0b10`).
313    HdmiVic,
314    /// Reserved code type (bits 7:6 = `0b11`).
315    Reserved,
316}
317
318/// One timing code entry from the inline list in a DisplayID 2.x block 0x27.
319///
320/// Constructed via [`StereoTimingCode::new`].
321#[non_exhaustive]
322#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324pub struct StereoTimingCode {
325    /// Table to look up `code` in.
326    pub code_type: StereoTimingCodeType,
327    /// 1-byte timing code (DMT ID, CTA VIC, or HDMI VIC).
328    pub code: u8,
329}
330
331impl StereoTimingCode {
332    /// Constructs a timing code entry.
333    pub fn new(code_type: StereoTimingCodeType, code: u8) -> Self {
334        Self { code_type, code }
335    }
336}
337
338/// Display interface features decoded from DisplayID 2.x block 0x26.
339#[non_exhaustive]
340#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
341#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
342pub struct DisplayInterfaceFeatures {
343    /// Color depths supported for RGB encoding (payload byte 0).
344    pub color_depth_rgb: ColorDepthsFull,
345    /// Color depths supported for YCbCr 4:4:4 encoding (payload byte 1).
346    pub color_depth_ycbcr444: ColorDepthsFull,
347    /// Color depths supported for YCbCr 4:2:2 encoding (payload byte 2).
348    pub color_depth_ycbcr422: ColorDepthsSubsampled,
349    /// Color depths supported for YCbCr 4:2:0 encoding (payload byte 3).
350    pub color_depth_ycbcr420: ColorDepthsSubsampled,
351    /// Minimum pixel rate for YCbCr 4:2:0 in units of 74.25 MP/s (payload byte 4). `0` = all modes supported.
352    pub min_ycbcr420_pixel_rate: u8,
353    /// Audio capability flags (payload byte 5). Stored as a raw byte: bits 5–7 advertise
354    /// 32/44.1/48 kHz sample-rate support, lower bits carry audio override and additional
355    /// flags whose semantics this crate does not yet typify.
356    pub audio_flags: u8,
357    /// Color space and EOTF defined-combinations bitmask (payload byte 6). Each set bit
358    /// indicates support for one of the spec's pre-defined `(color space, EOTF)` pairs.
359    /// Stored as a raw byte; bit positions match the spec §4.6 table (bit 0 = sRGB, etc.).
360    pub color_space_eotf_combos: u8,
361    /// Custom color space and EOTF combinations (payload bytes 9+, up to 7 entries).
362    /// The count of valid entries is in [`DisplayInterfaceFeatures::custom_color_space_eotf_count`].
363    /// Payload byte 8 is the count; each subsequent byte encodes one combo (bits 7:4 =
364    /// color space index, bits 3:0 = EOTF index). See [`CustomColorSpaceEotfCombo`].
365    pub custom_color_space_eotf_combos: [CustomColorSpaceEotfCombo; 7],
366    /// Number of valid entries in [`DisplayInterfaceFeatures::custom_color_space_eotf_combos`] (0–7).
367    pub custom_color_space_eotf_count: u8,
368}
369
370/// Identifies the eye targeted by a stereo viewing-method parameter.
371///
372/// Encodes the `Left`/`Right` selector that several DisplayID 2.x stereo methods (0x27)
373/// use to label which half of the frame, or which physical interface, carries which eye.
374#[non_exhaustive]
375#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
376#[derive(Debug, Clone, Copy, PartialEq, Eq)]
377pub enum StereoEye {
378    /// Left eye view.
379    Left,
380    /// Right eye view.
381    Right,
382}
383
384/// Mirroring applied to one of the dual interfaces in [`StereoViewingMethodV2::DualInterface`].
385#[non_exhaustive]
386#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
387#[derive(Debug, Clone, Copy, PartialEq, Eq)]
388pub enum DualInterfaceMirroring {
389    /// No mirroring applied.
390    None,
391    /// Image is mirrored across the vertical axis (left/right swap).
392    LeftRight,
393    /// Image is mirrored across the horizontal axis (top/bottom swap).
394    TopBottom,
395    /// Reserved by the DisplayID 2.x specification.
396    Reserved,
397}
398
399/// Scope of the timings to which a 2.x Stereo Display Interface block (0x27) applies.
400///
401/// Encoded in bits 7:6 of the block's revision/flags byte. Variants `ExplicitAndListedTimings`
402/// and `ListedTimingCodesOnly` indicate that the block carries an inline list of timing codes
403/// (DMT/VIC/HDMI VIC); those codes are decoded into
404/// [`DisplayIdStereoInterfaceV2::timing_codes`].
405#[non_exhaustive]
406#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
408pub enum StereoTimingScopeV2 {
409    /// Applies only to timings that explicitly report 3D capability.
410    ExplicitTimingsOnly,
411    /// Applies to explicit-3D timings AND the timing codes listed in this block.
412    ExplicitAndListedTimings,
413    /// Applies to all listed timings.
414    AllListedTimings,
415    /// Applies only to the timing codes listed in this block.
416    ListedTimingCodesOnly,
417}
418
419impl StereoTimingScopeV2 {
420    /// Decodes the scope from bits 7:6 of the block's revision byte.
421    pub fn from_revision(revision: u8) -> Self {
422        match revision >> 6 {
423            0b00 => StereoTimingScopeV2::ExplicitTimingsOnly,
424            0b01 => StereoTimingScopeV2::ExplicitAndListedTimings,
425            0b10 => StereoTimingScopeV2::AllListedTimings,
426            _ => StereoTimingScopeV2::ListedTimingCodesOnly,
427        }
428    }
429
430    /// Returns `true` when the block payload includes an inline timing-code list.
431    pub fn has_timing_codes(self) -> bool {
432        matches!(
433            self,
434            StereoTimingScopeV2::ExplicitAndListedTimings
435                | StereoTimingScopeV2::ListedTimingCodesOnly
436        )
437    }
438}
439
440/// Stereo viewing method advertised by a DisplayID 2.x Stereo Display Interface block (0x27).
441///
442/// Each variant carries the method-specific parameters. Method codes that are reserved by the
443/// spec are surfaced as [`StereoViewingMethodV2::Reserved`] with the raw method byte.
444#[non_exhaustive]
445#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
446#[derive(Debug, Clone, Copy, PartialEq, Eq)]
447pub enum StereoViewingMethodV2 {
448    /// Method 0x00 — Field-sequential stereo. `eye_on_high_half` identifies which eye view
449    /// is delivered during the HIGH half of the sync signal; the other eye is delivered
450    /// during the LOW half. `Right` corresponds to payload bit 0 = 1 (spec wording
451    /// "L/R polarity 1"), `Left` to bit 0 = 0 ("L/R polarity 0").
452    FieldSequential {
453        /// Eye view delivered during the HIGH half of the sync signal.
454        eye_on_high_half: StereoEye,
455    },
456    /// Method 0x01 — Side-by-side stereo. `left_half` indicates which eye view occupies
457    /// the left half of the frame.
458    SideBySide {
459        /// Eye view that occupies the left half of the frame.
460        left_half: StereoEye,
461    },
462    /// Method 0x02 — Pixel-interleaved stereo. The 8-byte `pattern` describes an 8×8
463    /// L/R pixel mask (bit set = Left, clear = Right). MSB of each byte is the leftmost
464    /// pixel of that row.
465    PixelInterleaved {
466        /// 8×8 bitmap; row `i`, bit `7 − x` selects whether pixel `(x, i)` is Left (1) or Right (0).
467        pattern: [u8; 8],
468    },
469    /// Method 0x03 — Dual-interface stereo. Each physical interface carries a single
470    /// eye view; `eye` identifies which eye is delivered on this interface, and
471    /// `mirroring` describes how the image is oriented.
472    DualInterface {
473        /// Eye view delivered over this interface.
474        eye: StereoEye,
475        /// Mirroring applied to this interface's image.
476        mirroring: DualInterfaceMirroring,
477    },
478    /// Method 0x04 — Multi-view stereo. `view_count` is the number of views and
479    /// `interleaving_method_code` is a vendor-defined identifier for how they interleave.
480    MultiView {
481        /// Number of distinct views in the multi-view configuration.
482        view_count: u8,
483        /// Vendor-defined identifier describing how the views interleave.
484        interleaving_method_code: u8,
485    },
486    /// Method 0x05 — Stacked-frame stereo (top/bottom). `top_half` indicates which eye
487    /// view occupies the top half of the frame.
488    StackedFrame {
489        /// Eye view that occupies the top half of the frame.
490        top_half: StereoEye,
491    },
492    /// Method 0xFF — Proprietary / vendor-defined.
493    Proprietary,
494    /// Method codes reserved by the DisplayID 2.x specification (0x06–0xFE).
495    Reserved(u8),
496}
497
498/// Stereo display interface decoded from a DisplayID 2.x block 0x27.
499///
500/// When [`Self::has_timing_codes`] returns `true`, the block carries an inline list of
501/// timing codes (DMT/VIC/HDMI VIC) scoping the stereo configuration; those codes are
502/// decoded into [`timing_codes`][Self::timing_codes] (alloc/std builds only).
503#[non_exhaustive]
504#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
505#[derive(Debug, Clone, PartialEq, Eq)]
506pub struct DisplayIdStereoInterfaceV2 {
507    /// Scope of timings to which this stereo configuration applies.
508    pub timing_scope: StereoTimingScopeV2,
509    /// Stereo viewing method and method-specific parameters.
510    pub method: StereoViewingMethodV2,
511    /// Inline timing codes scoping this stereo configuration (alloc/std builds only).
512    ///
513    /// Non-empty only when [`has_timing_codes`][Self::has_timing_codes] returns `true`.
514    /// Each entry names one timing (by DMT ID, CTA VIC, or HDMI VIC) to which this
515    /// stereo interface block applies.
516    #[cfg(any(feature = "alloc", feature = "std"))]
517    pub timing_codes: crate::prelude::Vec<StereoTimingCode>,
518}
519
520impl Default for DisplayIdStereoInterfaceV2 {
521    fn default() -> Self {
522        Self {
523            timing_scope: StereoTimingScopeV2::ExplicitTimingsOnly,
524            method: StereoViewingMethodV2::Proprietary,
525            #[cfg(any(feature = "alloc", feature = "std"))]
526            timing_codes: crate::prelude::Vec::new(),
527        }
528    }
529}
530
531impl DisplayIdStereoInterfaceV2 {
532    /// Convenience accessor for [`StereoTimingScopeV2::has_timing_codes`].
533    pub fn has_timing_codes(&self) -> bool {
534        self.timing_scope.has_timing_codes()
535    }
536}
537
538/// Vendor-specific data block from DisplayID 2.x block 0x7E (§4.10).
539///
540/// The payload is an IEEE OUI identifying the vendor followed by `n` bytes of
541/// vendor-defined data whose semantics this crate does not interpret. Consumers
542/// match on `oui` to dispatch to a vendor-specific parser of their choice.
543///
544/// Multiple 0x7E blocks may appear in a single section — each is decoded and
545/// appended to `DisplayIdCapabilities::vendor_specific` in payload order.
546#[non_exhaustive]
547#[cfg(any(feature = "alloc", feature = "std"))]
548#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
549#[derive(Debug, Clone, PartialEq, Eq, Default)]
550pub struct DisplayIdVendorSpecific {
551    /// 3-byte IEEE OUI identifying the vendor (e.g. `[0x00, 0xD0, 0x46]` = Dolby).
552    /// Bytes are stored in the order they appear in the payload (high-order byte first).
553    pub oui: [u8; 3],
554    /// Opaque vendor-defined payload following the OUI. Empty when the block carries
555    /// only the OUI with no further data.
556    pub data: Vec<u8>,
557}
558
559/// Rich capabilities extracted from a DisplayID 1.x or 2.x extension section.
560///
561/// Stored in `DisplayCapabilities` via `set_extension_data(0x70, ...)` by the dynamic
562/// pipeline; retrieve with `caps.get_extension_data::<DisplayIdCapabilities>(0x70)`.
563#[non_exhaustive]
564#[cfg(any(feature = "alloc", feature = "std"))]
565#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
566#[derive(Debug, Clone, PartialEq)]
567pub struct DisplayIdCapabilities {
568    /// DisplayID version byte (0x10–0x1F for v1.x, 0x20 for v2.x).
569    pub version: u8,
570    /// Display product primary use case (bits 2:0 of header byte 3).
571    pub product_type: u8,
572    /// IEEE OUI from the 2.x Product Identification block (0x20). Not a PNP ID.
573    pub manufacturer_oui: Option<[u8; 3]>,
574    /// Display parameters from 2.x block 0x21 (chromaticity, luminance, gamma).
575    pub display_params_v2: Option<DisplayParamsV2>,
576    /// Dynamic video timing range from 2.x block 0x25.
577    pub dynamic_timing_range: Option<DynamicTimingRange>,
578    /// Display interface features from 2.x block 0x26.
579    pub interface_features: Option<DisplayInterfaceFeatures>,
580    /// Stereo display interface from 2.x block 0x27.
581    pub stereo_interface_v2: Option<DisplayIdStereoInterfaceV2>,
582    /// ContainerID UUID from 2.x block 0x29 (16 raw bytes).
583    pub container_id: Option<[u8; 16]>,
584    /// Vendor-specific data blocks from 2.x block 0x7E, in payload order.
585    /// Empty when no 0x7E blocks were present.
586    pub vendor_specific: Vec<DisplayIdVendorSpecific>,
587}
588
589#[cfg(any(feature = "alloc", feature = "std"))]
590impl DisplayIdCapabilities {
591    /// Constructs a `DisplayIdCapabilities`.
592    pub fn new(version: u8, product_type: u8) -> Self {
593        Self {
594            version,
595            product_type,
596            manufacturer_oui: None,
597            display_params_v2: None,
598            dynamic_timing_range: None,
599            interface_features: None,
600            stereo_interface_v2: None,
601            container_id: None,
602            vendor_specific: Vec::new(),
603        }
604    }
605}
606
607#[cfg(test)]
608mod tests {
609    use super::*;
610
611    #[test]
612    fn chromaticity_point_normalises_raw_to_unit_interval() {
613        let white_d65 = ChromaticityPoint12 {
614            x_raw: 1294, // ≈ 0.3158
615            y_raw: 1347, // ≈ 0.3289
616        };
617        assert!((white_d65.x() - 0.31591797).abs() < 1e-6);
618        assert!((white_d65.y() - 0.32885742).abs() < 1e-6);
619    }
620
621    #[test]
622    fn chromaticity_point_endpoints() {
623        let zero = ChromaticityPoint12::default();
624        assert_eq!(zero.x(), 0.0);
625        assert_eq!(zero.y(), 0.0);
626
627        let max = ChromaticityPoint12 {
628            x_raw: 4095,
629            y_raw: 4095,
630        };
631        assert!(max.x() < 1.0);
632        assert!(max.y() < 1.0);
633    }
634
635    #[test]
636    fn display_technology_decodes_known_bytes() {
637        assert_eq!(
638            DisplayTechnology::from_byte(0x00),
639            DisplayTechnology::Unspecified
640        );
641        assert_eq!(DisplayTechnology::from_byte(0x01), DisplayTechnology::Amlcd);
642        assert_eq!(
643            DisplayTechnology::from_byte(0x02),
644            DisplayTechnology::Amoled
645        );
646    }
647
648    #[test]
649    fn display_technology_preserves_unknown_bytes() {
650        assert_eq!(
651            DisplayTechnology::from_byte(0x42),
652            DisplayTechnology::Other(0x42)
653        );
654        assert_eq!(DisplayTechnology::Other(0x42).as_byte(), 0x42);
655    }
656
657    #[test]
658    fn display_technology_round_trips() {
659        for b in [0x00u8, 0x01, 0x02, 0x55, 0xFF] {
660            let t = DisplayTechnology::from_byte(b);
661            assert_eq!(t.as_byte(), b);
662            assert_eq!(DisplayTechnology::from_byte(t.as_byte()), t);
663        }
664    }
665
666    #[test]
667    fn display_technology_other_rejects_canonical_bytes() {
668        assert_eq!(DisplayTechnology::other(0x00), None);
669        assert_eq!(DisplayTechnology::other(0x01), None);
670        assert_eq!(DisplayTechnology::other(0x02), None);
671    }
672
673    #[test]
674    fn display_technology_other_accepts_reserved_bytes() {
675        assert_eq!(
676            DisplayTechnology::other(0x42),
677            Some(DisplayTechnology::Other(0x42))
678        );
679        assert_eq!(
680            DisplayTechnology::other(0xFF),
681            Some(DisplayTechnology::Other(0xFF))
682        );
683    }
684
685    #[test]
686    fn scan_orientation_round_trips_all_eight_codes() {
687        for bits in 0u8..8 {
688            let orient = ScanOrientation::from_bits(bits);
689            assert_eq!(orient.as_bits(), bits);
690        }
691    }
692
693    #[test]
694    fn scan_orientation_ignores_upper_bits() {
695        assert_eq!(
696            ScanOrientation::from_bits(0b1111_1000),
697            ScanOrientation::LeftRightTopBottom
698        );
699        assert_eq!(
700            ScanOrientation::from_bits(0b1111_1111),
701            ScanOrientation::TopBottomLeftRight
702        );
703    }
704
705    #[test]
706    fn defaults_match_raster_convention() {
707        assert_eq!(DisplayTechnology::default(), DisplayTechnology::Unspecified);
708        assert_eq!(
709            ScanOrientation::default(),
710            ScanOrientation::LeftRightTopBottom
711        );
712    }
713
714    #[test]
715    fn color_depths_full_bit_layout() {
716        // Wire byte 0b0010_1010 = 8 + 12 + 16 bpc.
717        let depths = ColorDepthsFull::from_bits_truncate(0b0010_1010);
718        assert!(depths.contains(ColorDepthsFull::BPC_8));
719        assert!(depths.contains(ColorDepthsFull::BPC_12));
720        assert!(depths.contains(ColorDepthsFull::BPC_16));
721        assert!(!depths.contains(ColorDepthsFull::BPC_6));
722        assert!(!depths.contains(ColorDepthsFull::BPC_10));
723        assert!(!depths.contains(ColorDepthsFull::BPC_14));
724        assert_eq!(depths.bits(), 0b0010_1010);
725    }
726
727    #[test]
728    fn color_depths_subsampled_bit_layout() {
729        // Wire byte 0b0001_0101 = 8 + 12 + 16 bpc (no 6 bpc bit).
730        let depths = ColorDepthsSubsampled::from_bits_truncate(0b0001_0101);
731        assert!(depths.contains(ColorDepthsSubsampled::BPC_8));
732        assert!(depths.contains(ColorDepthsSubsampled::BPC_12));
733        assert!(depths.contains(ColorDepthsSubsampled::BPC_16));
734        assert!(!depths.contains(ColorDepthsSubsampled::BPC_10));
735        assert!(!depths.contains(ColorDepthsSubsampled::BPC_14));
736        assert_eq!(depths.bits(), 0b0001_0101);
737    }
738
739    #[test]
740    fn color_depths_default_is_empty() {
741        assert!(ColorDepthsFull::default().is_empty());
742        assert!(ColorDepthsSubsampled::default().is_empty());
743        assert!(
744            DisplayInterfaceFeatures::default()
745                .color_depth_rgb
746                .is_empty()
747        );
748    }
749
750    #[test]
751    fn color_depths_subsampled_truncates_reserved_bits() {
752        // Bit 5 is reserved for subsampled; from_bits_truncate drops it.
753        let depths = ColorDepthsSubsampled::from_bits_truncate(0b0011_1111);
754        assert_eq!(depths.bits(), 0b0001_1111);
755    }
756}