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}