display_types/capabilities.rs
1/// A reference-counted, type-erased warning value.
2///
3/// Any type that implements [`core::error::Error`] + [`Send`] + [`Sync`] + `'static` can be
4/// wrapped in a `ParseWarning`. The built-in library variants use `EdidWarning`, but
5/// custom handlers may push their own error types without wrapping them in `EdidWarning`.
6///
7/// Using [`Arc`][crate::prelude::Arc] (rather than `Box`) means `ParseWarning` is
8/// [`Clone`], which lets warnings be copied from a parsed representation into
9/// [`DisplayCapabilities`] without consuming the parsed result.
10///
11/// To inspect a specific variant, use the inherent `downcast_ref` method available on
12/// `dyn core::error::Error + Send + Sync + 'static` in `std` builds:
13///
14/// ```text
15/// for w in caps.iter_warnings() {
16/// if let Some(ew) = (**w).downcast_ref::<EdidWarning>() { ... }
17/// }
18/// ```
19#[cfg(any(feature = "alloc", feature = "std"))]
20pub type ParseWarning = crate::prelude::Arc<dyn core::error::Error + Send + Sync + 'static>;
21
22/// Stereo viewing support decoded from DTD byte 17 bits 6, 5, and 0.
23#[non_exhaustive]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum StereoMode {
27 /// Normal display; no stereo (bits 6–5 = `0b00`; bit 0 is don't-care).
28 #[default]
29 None,
30 /// Field-sequential stereo, right image when stereo sync = 1 (bits 6–5 = `0b01`, bit 0 = 0).
31 FieldSequentialRightFirst,
32 /// Field-sequential stereo, left image when stereo sync = 1 (bits 6–5 = `0b10`, bit 0 = 0).
33 FieldSequentialLeftFirst,
34 /// 2-way interleaved stereo, right image on even lines (bits 6–5 = `0b01`, bit 0 = 1).
35 TwoWayInterleavedRightEven,
36 /// 2-way interleaved stereo, left image on even lines (bits 6–5 = `0b10`, bit 0 = 1).
37 TwoWayInterleavedLeftEven,
38 /// 4-way interleaved stereo (bits 6–5 = `0b11`, bit 0 = 0).
39 FourWayInterleaved,
40 /// Side-by-side interleaved stereo (bits 6–5 = `0b11`, bit 0 = 1).
41 SideBySideInterleaved,
42}
43
44/// CVT formula selector for DisplayID 2.x Type IX (`0x24`) and Type V (`0x11`)
45/// Formula-Based Timings.
46///
47/// Decoded from byte 0 bits 2:0. Identifies which CVT variant the consumer should use
48/// to derive blanking parameters and pixel clock from the `(width, height, refresh_rate)`
49/// triple stored on [`VideoMode`]. Codes `3`–`7` are reserved by the DisplayID 2.x spec;
50/// unknown encodings are surfaced as [`CvtAlgorithm::Reserved`] so a future spec value
51/// does not block decoding of the rest of the descriptor.
52#[non_exhaustive]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum CvtAlgorithm {
56 /// Standard CVT (no reduced blanking) (encoding `0`).
57 Cvt,
58 /// CVT-RB v1 (encoding `1`).
59 CvtRb,
60 /// CVT-R2 / CVT-RB v2 (encoding `2`).
61 CvtR2,
62 /// Spec-reserved encoding (`3`–`7`) preserved verbatim so unknown values do not block
63 /// decoding of the rest of the descriptor.
64 Reserved(u8),
65}
66
67impl CvtAlgorithm {
68 /// Decodes the 3-bit CVT algorithm field (Type V/IX descriptor byte 0 bits 2:0).
69 /// Upper bits of the input are ignored.
70 pub const fn from_bits(b: u8) -> Self {
71 match b & 0x07 {
72 0 => Self::Cvt,
73 1 => Self::CvtRb,
74 2 => Self::CvtR2,
75 other => Self::Reserved(other),
76 }
77 }
78}
79
80/// Stereo timing mode decoded from Type V (`0x11`) and Type IX (`0x24`) descriptor byte 0
81/// bits 6:5. Indicates whether the timing is for a mono display, stereo-only, or
82/// user-selectable.
83///
84/// This is distinct from [`StereoMode`], which describes the specific stereo viewing method
85/// decoded from a Detailed Timing Descriptor.
86#[non_exhaustive]
87#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum TypeIxStereoMode {
90 /// Mono timing (bits 6:5 = `0b00`).
91 Mono,
92 /// 3D stereo timing (bits 6:5 = `0b01`).
93 Stereo,
94 /// Mono or 3D stereo depending on user action (bits 6:5 = `0b10`).
95 MonoOrStereoByUser,
96 /// Reserved (bits 6:5 = `0b11`).
97 Reserved,
98}
99
100/// Sync signal definition decoded from DTD byte 17 bits 4–1.
101#[non_exhaustive]
102#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum SyncDefinition {
105 /// Analog composite sync (bit 4 = 0, bit 3 = 0).
106 AnalogComposite {
107 /// H-sync pulse present during V-sync (serrations).
108 serrations: bool,
109 /// Sync on all three RGB signals (`true`) or green only (`false`).
110 sync_on_all_rgb: bool,
111 },
112 /// Bipolar analog composite sync (bit 4 = 0, bit 3 = 1).
113 BipolarAnalogComposite {
114 /// H-sync pulse present during V-sync (serrations).
115 serrations: bool,
116 /// Sync on all three RGB signals (`true`) or green only (`false`).
117 sync_on_all_rgb: bool,
118 },
119 /// Digital composite sync on H-sync pin (bit 4 = 1, bit 3 = 0).
120 DigitalComposite {
121 /// H-sync pulse present during V-sync (serrations).
122 serrations: bool,
123 /// H-sync polarity outside V-sync: `true` = positive.
124 h_sync_positive: bool,
125 },
126 /// Digital separate sync (bit 4 = 1, bit 3 = 1).
127 DigitalSeparate {
128 /// V-sync polarity: `true` = positive.
129 v_sync_positive: bool,
130 /// H-sync polarity: `true` = positive.
131 h_sync_positive: bool,
132 },
133}
134
135/// The source from which a [`VideoMode`] was decoded.
136///
137/// Populated automatically by [`vic_to_mode`][crate::cea861::vic_to_mode] and
138/// [`dmt_to_mode`][crate::cea861::dmt_to_mode]; parsers that decode Detailed Timing
139/// Descriptors should set it via [`VideoMode::with_source`]. `None` for modes
140/// constructed directly via [`VideoMode::new`].
141#[non_exhaustive]
142#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum ModeSource {
145 /// A CTA-861 Video Identification Code, as used in Short Video Descriptors,
146 /// the Y420 Video Data Block, and the Y420 Capability Map Data Block.
147 Vic(u8),
148 /// A VESA Display Monitor Timings identifier (0x01–0x58).
149 DmtId(u16),
150 /// Zero-based index of a Detailed Timing Descriptor within its containing EDID block.
151 DtdIndex(u8),
152}
153
154/// A display refresh rate expressed as an exact rational number (numerator/denominator in Hz).
155///
156/// Integer rates (60 Hz, 120 Hz, etc.) use `denom = 1`. NTSC-derived fractional rates use
157/// `denom = 1001` (e.g. 60000/1001 ≈ 59.94 Hz, 24000/1001 ≈ 23.976 Hz).
158///
159/// Always stored in lowest terms: all constructors (including `Deserialize`) apply GCD
160/// reduction, so `==`, `Hash`, and `Ord` are consistent and a given rate has exactly one
161/// canonical representation.
162///
163/// Use [`RefreshRate::integral`] for integer rates and [`RefreshRate::fractional`] for all
164/// others. `From<u32>` and `From<u16>` are implemented as `integral` conversions, so
165/// integer literals work wherever `impl Into<RefreshRate>` is accepted.
166#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
167#[cfg_attr(
168 feature = "serde",
169 serde(try_from = "RefreshRateRepr", into = "RefreshRateRepr")
170)]
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
172pub struct RefreshRate {
173 numer: u32,
174 denom: u32,
175}
176
177#[cfg(feature = "serde")]
178#[derive(serde::Serialize, serde::Deserialize)]
179struct RefreshRateRepr {
180 numer: u32,
181 denom: u32,
182}
183
184#[cfg(feature = "serde")]
185impl core::convert::TryFrom<RefreshRateRepr> for RefreshRate {
186 type Error = &'static str;
187 fn try_from(r: RefreshRateRepr) -> Result<Self, Self::Error> {
188 if r.denom == 0 {
189 Err("RefreshRate denominator must not be zero")
190 } else {
191 Ok(Self::fractional(r.numer, r.denom))
192 }
193 }
194}
195
196#[cfg(feature = "serde")]
197impl From<RefreshRate> for RefreshRateRepr {
198 fn from(r: RefreshRate) -> Self {
199 Self {
200 numer: r.numer,
201 denom: r.denom,
202 }
203 }
204}
205
206fn gcd(mut a: u32, mut b: u32) -> u32 {
207 while b != 0 {
208 let t = b;
209 b = a % b;
210 a = t;
211 }
212 a
213}
214
215fn gcd_u64(mut a: u64, mut b: u64) -> u64 {
216 while b != 0 {
217 let t = b;
218 b = a % b;
219 a = t;
220 }
221 a
222}
223
224impl RefreshRate {
225 /// Constructs an integer refresh rate (e.g. `RefreshRate::integral(60)` → 60/1).
226 pub fn integral(hz: u32) -> Self {
227 Self {
228 numer: hz,
229 denom: 1,
230 }
231 }
232
233 /// Constructs an exact rational refresh rate, reduced to lowest terms.
234 ///
235 /// # Panics
236 ///
237 /// Panics if `denom` is zero.
238 pub fn fractional(numer: u32, denom: u32) -> Self {
239 assert!(denom != 0, "RefreshRate denominator must not be zero");
240 let g = gcd(numer, denom);
241 Self {
242 numer: numer / g,
243 denom: denom / g,
244 }
245 }
246
247 /// Constructs a refresh rate from a `numer/denom` ratio in Hz, reduced to lowest terms.
248 ///
249 /// Useful when the rate is computed from intermediate values that exceed `u32`, such as
250 /// `pixel_clock_hz / (h_total × v_total)` for detailed-timing descriptors. Reduces in
251 /// `u64` then narrows to `u32`.
252 ///
253 /// Returns `None` if `denom` is zero or if the reduced fraction does not fit in `u32`.
254 ///
255 /// ```
256 /// use display_types::RefreshRate;
257 ///
258 /// // NTSC-style fractional rate computed from a large numerator and denominator.
259 /// let r = RefreshRate::from_ratio(60_000_000, 1_001_000).unwrap();
260 /// assert_eq!(r, RefreshRate::fractional(60_000, 1_001));
261 /// ```
262 pub fn from_ratio(numer: u64, denom: u64) -> Option<Self> {
263 if denom == 0 {
264 return None;
265 }
266 let g = gcd_u64(numer, denom);
267 let n = u32::try_from(numer / g).ok()?;
268 let d = u32::try_from(denom / g).ok()?;
269 Some(Self { numer: n, denom: d })
270 }
271
272 /// Numerator of the reduced fraction, in Hz.
273 pub fn numer(self) -> u32 {
274 self.numer
275 }
276
277 /// Denominator of the reduced fraction (1 for integer rates, 1001 for NTSC-derived rates).
278 pub fn denom(self) -> u32 {
279 self.denom
280 }
281
282 /// Returns the refresh rate as `f64`.
283 pub fn as_f64(self) -> f64 {
284 self.numer as f64 / self.denom as f64
285 }
286}
287
288impl PartialOrd for RefreshRate {
289 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
290 Some(self.cmp(other))
291 }
292}
293
294impl core::cmp::Ord for RefreshRate {
295 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
296 (self.numer as u64 * other.denom as u64).cmp(&(other.numer as u64 * self.denom as u64))
297 }
298}
299
300impl core::fmt::Display for RefreshRate {
301 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
302 if self.denom == 1 {
303 write!(f, "{} Hz", self.numer)
304 } else {
305 write!(f, "{}/{} Hz", self.numer, self.denom)
306 }
307 }
308}
309
310impl From<u32> for RefreshRate {
311 fn from(hz: u32) -> Self {
312 Self::integral(hz)
313 }
314}
315
316impl From<u16> for RefreshRate {
317 fn from(hz: u16) -> Self {
318 Self::integral(hz as u32)
319 }
320}
321
322/// A display video mode expressed as resolution, refresh rate, and scan type.
323///
324/// Use [`VideoMode::new`] to construct a mode with only identity fields (the common case
325/// for modes decoded from standard timing or SVD entries). Use
326/// [`VideoMode::with_detailed_timing`] to add the blanking-interval and signal fields
327/// available from a Detailed Timing Descriptor or equivalent.
328#[non_exhaustive]
329#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
330#[derive(Debug, Clone, PartialEq, Default)]
331pub struct VideoMode {
332 /// Horizontal resolution in pixels.
333 pub width: u16,
334 /// Vertical resolution in pixels.
335 pub height: u16,
336 /// Refresh rate as an exact rational number in Hz, or `None` when unspecified
337 /// (e.g. a default-constructed `VideoMode` whose rate has not been set).
338 pub refresh_rate: Option<RefreshRate>,
339 /// `true` for interlaced modes; `false` for progressive (the common case).
340 pub interlaced: bool,
341 /// Horizontal front porch in pixels (0 when not decoded from a DTD).
342 pub h_front_porch: u16,
343 /// Horizontal sync pulse width in pixels (0 when not decoded from a DTD).
344 pub h_sync_width: u16,
345 /// Vertical front porch in lines (0 when not decoded from a DTD).
346 pub v_front_porch: u16,
347 /// Vertical sync pulse width in lines (0 when not decoded from a DTD).
348 pub v_sync_width: u16,
349 /// Horizontal border width in pixels on each side of the active area (0 when not from a DTD).
350 pub h_border: u8,
351 /// Vertical border height in lines on each side of the active area (0 when not from a DTD).
352 pub v_border: u8,
353 /// Stereo viewing support (default [`StereoMode::None`] for non-DTD modes).
354 pub stereo: StereoMode,
355 /// Sync signal definition (`None` for non-DTD modes).
356 pub sync: Option<SyncDefinition>,
357 /// Pixel clock in kHz (`None` for modes not decoded from a Detailed Timing Descriptor).
358 pub pixel_clock_khz: Option<u32>,
359 /// The source from which this mode was decoded, if known.
360 ///
361 /// `None` for modes constructed directly via [`VideoMode::new`] without a table lookup.
362 pub source: Option<ModeSource>,
363 /// CVT formula selector for DisplayID 2.x Type V/IX timings (`None` for all other sources).
364 /// Consumers can use this to derive blanking and pixel clock from `(width, height,
365 /// refresh_rate)` via the named CVT variant.
366 pub cvt_algorithm: Option<CvtAlgorithm>,
367 /// `true` when the timing is YCbCr 4:2:0 only. Set from CTA-861 Y420 capability data
368 /// and from DisplayID 2.x Type VII byte 3 bit 7 (block revision ≥ 2).
369 /// Defaults to `false` for all other sources.
370 pub y420: bool,
371 /// `true` when NTSC-style fractional refresh rate (× 1000/1001) is supported alongside
372 /// this timing. Decoded from Type V and Type IX descriptor byte 0 bit 3.
373 /// Defaults to `false` for all other sources.
374 pub ntsc_fractional_refresh: bool,
375 /// Per-mode stereo indicator from Type V (`0x11`) and Type IX (`0x24`) descriptor byte 0
376 /// bits 6:5. `None` for all other timing sources.
377 pub type_ix_stereo: Option<TypeIxStereoMode>,
378}
379
380impl VideoMode {
381 /// Constructs a `VideoMode` with the given identity fields.
382 ///
383 /// All blanking-interval fields (`h_front_porch`, `h_sync_width`, `v_front_porch`,
384 /// `v_sync_width`, `h_border`, `v_border`) default to `0`, `stereo` defaults to
385 /// [`StereoMode::None`], and `sync` defaults to `None`. Use
386 /// [`with_detailed_timing`][Self::with_detailed_timing] to set those fields when
387 /// decoding from a Detailed Timing Descriptor.
388 pub fn new(
389 width: u16,
390 height: u16,
391 refresh_rate: impl Into<RefreshRate>,
392 interlaced: bool,
393 ) -> Self {
394 Self {
395 width,
396 height,
397 refresh_rate: Some(refresh_rate.into()),
398 interlaced,
399 ..Self::default()
400 }
401 }
402
403 /// Sets the exact pixel clock in kHz, returning the updated mode.
404 ///
405 /// Use this when constructing a [`VideoMode`] from hardware timing registers or a
406 /// known-good mode table entry, where the exact pixel clock is available but full
407 /// Detailed Timing Descriptor fields are not. The supplied clock is returned verbatim
408 /// by [`pixel_clock_khz`][crate::pixel_clock_khz], bypassing the CVT-RB fallback
409 /// estimate.
410 ///
411 /// ```
412 /// use display_types::VideoMode;
413 /// use display_types::pixel_clock_khz;
414 ///
415 /// // Custom panel: 1920×1200 @ 60 Hz, exact pixel clock from PLL register.
416 /// let mode = VideoMode::new(1920, 1200, 60u32, false).with_pixel_clock(154_000);
417 /// assert_eq!(pixel_clock_khz(&mode), 154_000);
418 /// ```
419 pub fn with_pixel_clock(mut self, pixel_clock_khz: u32) -> Self {
420 self.pixel_clock_khz = Some(pixel_clock_khz);
421 self
422 }
423
424 /// Sets the CVT formula selector, returning the updated mode. Used by DisplayID 2.x
425 /// Type IX (`0x24`) descriptors which signal a CVT variant alongside `(width, height,
426 /// refresh_rate)`.
427 pub fn with_cvt_algorithm(mut self, alg: CvtAlgorithm) -> Self {
428 self.cvt_algorithm = Some(alg);
429 self
430 }
431
432 /// Sets the YCbCr 4:2:0 flag, returning the updated mode. Used by DisplayID 2.x
433 /// Type VII decoders (block revision ≥ 2) and by callers that derive Y420-only modes
434 /// from CTA-861 Y420 capability data.
435 pub fn with_y420(mut self, y420: bool) -> Self {
436 self.y420 = y420;
437 self
438 }
439
440 /// Sets the NTSC fractional refresh flag, returning the updated mode. Used by
441 /// Type V and Type IX decoders when byte 0 bit 3 is set.
442 pub fn with_ntsc_fractional_refresh(mut self, supported: bool) -> Self {
443 self.ntsc_fractional_refresh = supported;
444 self
445 }
446
447 /// Sets the per-mode stereo indicator from Type V/IX byte 0 bits 6:5, returning the
448 /// updated mode.
449 pub fn with_type_ix_stereo(mut self, stereo: TypeIxStereoMode) -> Self {
450 self.type_ix_stereo = Some(stereo);
451 self
452 }
453
454 /// Sets the mode source, returning the updated mode.
455 ///
456 /// Called automatically by [`vic_to_mode`][crate::cea861::vic_to_mode] and
457 /// [`dmt_to_mode`][crate::cea861::dmt_to_mode]. Parsers decoding Detailed Timing
458 /// Descriptors should call `.with_source(ModeSource::DtdIndex(n))` so that the
459 /// descriptor's position survives into negotiated output.
460 pub fn with_source(mut self, source: ModeSource) -> Self {
461 self.source = Some(source);
462 self
463 }
464
465 /// Adds blanking-interval and signal fields decoded from a Detailed Timing Descriptor
466 /// or equivalent source, returning the updated mode.
467 ///
468 /// The 9-parameter count mirrors the DTD fields directly (EDID §3.10.3 / DisplayID §4.4).
469 #[allow(clippy::too_many_arguments)]
470 pub fn with_detailed_timing(
471 mut self,
472 pixel_clock_khz: u32,
473 h_front_porch: u16,
474 h_sync_width: u16,
475 v_front_porch: u16,
476 v_sync_width: u16,
477 h_border: u8,
478 v_border: u8,
479 stereo: StereoMode,
480 sync: Option<SyncDefinition>,
481 ) -> Self {
482 self.pixel_clock_khz = Some(pixel_clock_khz);
483 self.h_front_porch = h_front_porch;
484 self.h_sync_width = h_sync_width;
485 self.v_front_porch = v_front_porch;
486 self.v_sync_width = v_sync_width;
487 self.h_border = h_border;
488 self.v_border = v_border;
489 self.stereo = stereo;
490 self.sync = sync;
491 self
492 }
493}
494
495/// EDID specification version and revision, decoded from base block bytes 18–19.
496///
497/// Most displays in use report version 1 with revision 3 or 4.
498#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
499#[derive(Debug, Clone, Copy, PartialEq, Eq)]
500pub struct EdidVersion {
501 /// EDID version number (byte 18). Always `1` for all current displays.
502 pub version: u8,
503 /// EDID revision number (byte 19).
504 pub revision: u8,
505}
506
507impl core::fmt::Display for EdidVersion {
508 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
509 write!(f, "{}.{}", self.version, self.revision)
510 }
511}
512
513/// Trait for typed data stored in [`DisplayCapabilities::extension_data`] by custom handlers.
514///
515/// A blanket implementation covers any type that is `Any + Debug + Send + Sync`, so consumers
516/// do not need to implement this trait manually — `#[derive(Debug)]` on a `Send + Sync` type
517/// is sufficient.
518#[cfg(any(feature = "alloc", feature = "std"))]
519pub trait ExtensionData: core::any::Any + core::fmt::Debug + Send + Sync {
520 /// Returns `self` as `&dyn Any` to enable downcasting.
521 fn as_any(&self) -> &dyn core::any::Any;
522}
523
524#[cfg(any(feature = "alloc", feature = "std"))]
525impl<T: core::any::Any + core::fmt::Debug + Send + Sync> ExtensionData for T {
526 fn as_any(&self) -> &dyn core::any::Any {
527 self
528 }
529}
530
531/// Consumer-facing display capability model produced by a display data parser.
532///
533/// All fields defined by the relevant specification are decoded and exposed here.
534/// No field is omitted because it appears obscure or unlikely to be needed — that
535/// judgement belongs to the consumer, not the library.
536///
537/// Fields are `Option` where the underlying data may be absent or undecodable.
538/// `None` means the value was not present or could not be reliably determined; it does
539/// not imply the field is unimportant. The library never invents or defaults data.
540#[non_exhaustive]
541#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
542#[derive(Debug, Clone, Default)]
543pub struct DisplayCapabilities {
544 /// Three-character PNP manufacturer ID (e.g. `GSM` for LG, `SAM` for Samsung).
545 pub manufacturer: Option<crate::manufacture::ManufacturerId>,
546 /// Manufacture date or model year.
547 pub manufacture_date: Option<crate::manufacture::ManufactureDate>,
548 /// EDID specification version and revision.
549 pub edid_version: Option<EdidVersion>,
550 /// Manufacturer-assigned product code.
551 pub product_code: Option<u16>,
552 /// Manufacturer-assigned serial number, if encoded numerically in the base block.
553 pub serial_number: Option<u32>,
554 /// Serial number string from the monitor serial number descriptor (`0xFF`), if present.
555 pub serial_number_string: Option<crate::manufacture::MonitorString>,
556 /// Human-readable display name from the monitor name descriptor, if present.
557 pub display_name: Option<crate::manufacture::MonitorString>,
558 /// Unspecified ASCII text strings from `0xFE` descriptors, in descriptor slot order.
559 ///
560 /// Up to four entries (one per descriptor slot). Each slot is `None` if the corresponding
561 /// descriptor was not a `0xFE` entry.
562 pub unspecified_text: [Option<crate::manufacture::MonitorString>; 4],
563 /// Additional white points from the `0xFB` descriptor.
564 ///
565 /// Up to two entries (the EDID `0xFB` descriptor has two fixed slots). Each slot is
566 /// `None` if the corresponding entry was unused (index byte `0x00`).
567 pub white_points: [Option<crate::color::WhitePoint>; 2],
568 /// `true` if the display uses a digital input interface.
569 pub digital: bool,
570 /// Color bit depth per primary channel.
571 /// `None` for analog displays or when the field is undefined or reserved.
572 pub color_bit_depth: Option<crate::color::ColorBitDepth>,
573 /// Physical display technology (e.g. TFT, OLED, PDP).
574 /// `None` when the Display Device Data Block is absent.
575 pub display_technology: Option<crate::panel::DisplayTechnology>,
576 /// Technology-specific sub-type code (raw, 0–15).
577 /// `None` when the Display Device Data Block is absent.
578 pub display_subtype: Option<u8>,
579 /// Panel operating mode (continuous or non-continuous refresh).
580 /// `None` when the Display Device Data Block is absent.
581 pub operating_mode: Option<crate::panel::OperatingMode>,
582 /// Backlight type.
583 /// `None` when the Display Device Data Block is absent.
584 pub backlight_type: Option<crate::panel::BacklightType>,
585 /// Whether the panel uses a Data Enable (DE) signal.
586 /// `None` when the Display Device Data Block is absent.
587 pub data_enable_used: Option<bool>,
588 /// Data Enable signal polarity: `true` = positive, `false` = negative.
589 /// Valid only when `data_enable_used` is `Some(true)`.
590 /// `None` when the Display Device Data Block is absent.
591 pub data_enable_positive: Option<bool>,
592 /// Native pixel format `(width_px, height_px)`.
593 /// `None` when the Display Device Data Block is absent or either dimension is zero.
594 pub native_pixels: Option<(u16, u16)>,
595 /// Panel aspect ratio encoded as `(AR − 1) × 100` (raw byte).
596 /// For example `77` represents approximately 16:9 (AR ≈ 1.77). `None` when the block is absent.
597 pub panel_aspect_ratio_100: Option<u8>,
598 /// Physical mounting orientation of the panel.
599 /// `None` when the Display Device Data Block is absent.
600 pub physical_orientation: Option<crate::panel::PhysicalOrientation>,
601 /// Panel rotation capability.
602 /// `None` when the Display Device Data Block is absent.
603 pub rotation_capability: Option<crate::panel::RotationCapability>,
604 /// Location of the zero (origin) pixel in the framebuffer.
605 /// `None` when the Display Device Data Block is absent.
606 pub zero_pixel_location: Option<crate::panel::ZeroPixelLocation>,
607 /// Fast-scan direction relative to H-sync.
608 /// `None` when the Display Device Data Block is absent.
609 pub scan_direction: Option<crate::panel::ScanDirection>,
610 /// Sub-pixel color filter arrangement.
611 /// `None` when the Display Device Data Block is absent.
612 pub subpixel_layout: Option<crate::panel::SubpixelLayout>,
613 /// Pixel pitch `(horizontal_hundredths_mm, vertical_hundredths_mm)` in 0.01 mm units.
614 /// `None` when the Display Device Data Block is absent or either pitch is zero.
615 pub pixel_pitch_hundredths_mm: Option<(u8, u8)>,
616 /// Pixel response time in milliseconds.
617 /// `None` when the Display Device Data Block is absent or the value is zero.
618 pub pixel_response_time_ms: Option<u8>,
619 /// Interface power sequencing timing parameters.
620 /// `None` when the Interface Power Sequencing Block is absent.
621 pub power_sequencing: Option<crate::panel::PowerSequencing>,
622 /// Display luminance transfer function.
623 /// `None` when the Transfer Characteristics Block is absent.
624 #[cfg(any(feature = "alloc", feature = "std"))]
625 pub transfer_characteristic: Option<crate::transfer::DisplayIdTransferCharacteristic>,
626 /// Physical display interface capabilities.
627 /// `None` when the Display Interface Data Block is absent.
628 pub display_id_interface: Option<crate::panel::DisplayIdInterface>,
629 /// Stereo display interface parameters.
630 /// `None` when the Stereo Display Interface Data Block is absent.
631 pub stereo_interface: Option<crate::panel::DisplayIdStereoInterface>,
632 /// Tiled display topology.
633 /// `None` when the Tiled Display Topology Data Block is absent.
634 pub tiled_topology: Option<crate::panel::DisplayIdTiledTopology>,
635 /// CIE xy chromaticity coordinates for the color primaries and white point.
636 pub chromaticity: crate::color::Chromaticity,
637 /// Display gamma. `None` if the display did not specify a gamma value.
638 pub gamma: Option<crate::color::DisplayGamma>,
639 /// Display feature support flags.
640 pub display_features: Option<crate::features::DisplayFeatureFlags>,
641 /// Supported color encoding formats. Only populated for EDID 1.4+ digital displays.
642 pub digital_color_encoding: Option<crate::color::DigitalColorEncoding>,
643 /// Color type for analog displays; `None` for the undefined value (`0b11`).
644 pub analog_color_type: Option<crate::color::AnalogColorType>,
645 /// Video interface type.
646 /// `None` for analog displays or when the field is undefined or reserved.
647 pub video_interface: Option<crate::input::VideoInterface>,
648 /// Analog sync and video white levels. Only populated for analog displays.
649 pub analog_sync_level: Option<crate::input::AnalogSyncLevel>,
650 /// Physical screen dimensions or aspect ratio.
651 /// `None` when both bytes are zero (undefined).
652 pub screen_size: Option<crate::screen::ScreenSize>,
653 /// Minimum supported vertical refresh rate in Hz.
654 pub min_v_rate: Option<u16>,
655 /// Maximum supported vertical refresh rate in Hz.
656 pub max_v_rate: Option<u16>,
657 /// Minimum supported horizontal scan rate in kHz.
658 pub min_h_rate_khz: Option<u16>,
659 /// Maximum supported horizontal scan rate in kHz.
660 pub max_h_rate_khz: Option<u16>,
661 /// Maximum pixel clock in MHz.
662 pub max_pixel_clock_mhz: Option<u16>,
663 /// Physical image area dimensions in millimetres `(width_mm, height_mm)`.
664 ///
665 /// More precise than [`screen_size`][Self::screen_size] (which is in cm).
666 /// `None` when all DTD image-size fields are zero.
667 pub preferred_image_size_mm: Option<(u16, u16)>,
668 /// Video timing formula reported in the display range limits descriptor.
669 pub timing_formula: Option<crate::timing::TimingFormula>,
670 /// DCM polynomial coefficients.
671 pub color_management: Option<crate::color::ColorManagementData>,
672 /// Video modes decoded from the display data.
673 #[cfg(any(feature = "alloc", feature = "std"))]
674 pub supported_modes: crate::prelude::Vec<VideoMode>,
675 /// Non-fatal conditions collected from the parser and all handlers.
676 ///
677 /// Not serialized — use a custom handler to map warnings to a serializable form.
678 #[cfg(any(feature = "alloc", feature = "std"))]
679 #[cfg_attr(feature = "serde", serde(skip))]
680 pub warnings: crate::prelude::Vec<ParseWarning>,
681 /// Typed data attached by extension handlers, keyed by extension tag byte.
682 ///
683 /// Uses a `Vec` of `(tag, data)` pairs rather than a `HashMap` so that this field is
684 /// available in `alloc`-only (no_std) builds. The number of distinct extension tags in
685 /// any real EDID is small enough that linear scan is negligible.
686 ///
687 /// Not serialized — use a custom handler to map this to a serializable form.
688 #[cfg(any(feature = "alloc", feature = "std"))]
689 #[cfg_attr(feature = "serde", serde(skip))]
690 pub extension_data: crate::prelude::Vec<(u8, crate::prelude::Arc<dyn ExtensionData>)>,
691}
692
693#[cfg(any(feature = "alloc", feature = "std"))]
694impl DisplayCapabilities {
695 /// Returns an iterator over all collected warnings.
696 pub fn iter_warnings(&self) -> impl Iterator<Item = &ParseWarning> {
697 self.warnings.iter()
698 }
699
700 /// Appends a warning, wrapping it in a [`ParseWarning`].
701 pub fn push_warning(&mut self, w: impl core::error::Error + Send + Sync + 'static) {
702 self.warnings.push(crate::prelude::Arc::new(w));
703 }
704
705 /// Store typed data from a handler, keyed by an extension tag.
706 /// Replaces any previously stored entry for the same tag.
707 pub fn set_extension_data<T: ExtensionData>(&mut self, tag: u8, data: T) {
708 if let Some(entry) = self.extension_data.iter_mut().find(|(t, _)| *t == tag) {
709 entry.1 = crate::prelude::Arc::new(data);
710 } else {
711 self.extension_data
712 .push((tag, crate::prelude::Arc::new(data)));
713 }
714 }
715
716 /// Retrieve typed data previously stored by a handler for the given tag.
717 /// Returns `None` if no data is stored for the tag or the type does not match.
718 pub fn get_extension_data<T: core::any::Any>(&self, tag: u8) -> Option<&T> {
719 self.extension_data
720 .iter()
721 .find(|(t, _)| *t == tag)
722 // `**data` deref-chains through `&` then through Arc's Deref to reach
723 // `dyn ExtensionData`, forcing vtable dispatch for `as_any()`.
724 // Calling `.as_any()` on `&Arc<dyn ExtensionData>` would hit the blanket
725 // `ExtensionData` impl for Arc itself and return the wrong TypeId.
726 .and_then(|(_, data)| (**data).as_any().downcast_ref::<T>())
727 }
728
729 /// Removes the extension data entry for `tag` and returns it as `T`.
730 ///
731 /// Intended for take-mutate-restore patterns where multiple input sources contribute
732 /// to a single extension's capability struct (e.g. CTA-861 data delivered both via
733 /// the CEA-861 extension block and via the DisplayID 2.x CTA DisplayID block 0x81).
734 /// The caller mutates the returned value and stores it back with
735 /// [`set_extension_data`][Self::set_extension_data].
736 ///
737 /// Returns `None` if no entry exists for `tag` or the stored type is not `T`.
738 /// When the type does not match, the entry is left in place.
739 pub fn take_extension_data<T: ExtensionData + Clone>(&mut self, tag: u8) -> Option<T> {
740 let pos = self.extension_data.iter().position(|(t, _)| *t == tag)?;
741 // Peek before removing — type mismatch must not destroy the entry.
742 (*self.extension_data[pos].1).as_any().downcast_ref::<T>()?;
743 let (_, arc) = self.extension_data.remove(pos);
744 (*arc).as_any().downcast_ref::<T>().cloned()
745 }
746}
747
748#[cfg(test)]
749mod refresh_rate_tests {
750 use super::*;
751
752 #[test]
753 fn integral_has_unit_denominator() {
754 let r = RefreshRate::integral(60);
755 assert_eq!(r.numer(), 60);
756 assert_eq!(r.denom(), 1);
757 }
758
759 #[test]
760 fn fractional_reduces_to_lowest_terms() {
761 let r = RefreshRate::fractional(120, 2);
762 assert_eq!(r.numer(), 60);
763 assert_eq!(r.denom(), 1);
764
765 let ntsc = RefreshRate::fractional(60000, 1001);
766 assert_eq!(ntsc.numer(), 60000);
767 assert_eq!(ntsc.denom(), 1001);
768 }
769
770 #[test]
771 #[should_panic(expected = "RefreshRate denominator must not be zero")]
772 fn fractional_panics_on_zero_denominator() {
773 let _ = RefreshRate::fractional(60, 0);
774 }
775
776 #[test]
777 fn equality_is_canonical() {
778 assert_eq!(RefreshRate::integral(60), RefreshRate::fractional(120, 2));
779 assert_ne!(
780 RefreshRate::integral(60),
781 RefreshRate::fractional(60000, 1001)
782 );
783 }
784
785 #[test]
786 fn ord_uses_cross_multiplication() {
787 let ntsc = RefreshRate::fractional(60000, 1001);
788 let sixty = RefreshRate::integral(60);
789 let fiftynine = RefreshRate::integral(59);
790 assert!(ntsc < sixty);
791 assert!(ntsc > fiftynine);
792 assert_eq!(
793 sixty.cmp(&RefreshRate::fractional(120, 2)),
794 core::cmp::Ordering::Equal
795 );
796 }
797
798 #[test]
799 #[cfg(any(feature = "alloc", feature = "std"))]
800 fn display_formats_integer_and_fractional() {
801 extern crate alloc;
802 use alloc::format;
803 assert_eq!(format!("{}", RefreshRate::integral(60)), "60 Hz");
804 assert_eq!(
805 format!("{}", RefreshRate::fractional(60000, 1001)),
806 "60000/1001 Hz"
807 );
808 }
809
810 #[test]
811 fn from_integer_uses_integral() {
812 let from_u32: RefreshRate = 144u32.into();
813 let from_u16: RefreshRate = 60u16.into();
814 assert_eq!(from_u32, RefreshRate::integral(144));
815 assert_eq!(from_u16, RefreshRate::integral(60));
816 }
817
818 #[test]
819 fn as_f64_normalises() {
820 let delta = RefreshRate::fractional(60000, 1001).as_f64() - 59.94;
821 assert!(delta.abs() < 0.01);
822 }
823
824 #[test]
825 fn from_ratio_reduces_large_values() {
826 // 1080p@59.94: pc = 148_352 kHz, h_total × v_total = 2200 × 1125 = 2_475_000
827 // 148_352_000 / 2_475_000 = 59.9402… (non-canonical reduction).
828 let r = RefreshRate::from_ratio(148_352_000, 2_475_000).unwrap();
829 // gcd(148_352_000, 2_475_000) = 1000 → 148_352 / 2_475
830 assert_eq!(r.numer(), 148_352);
831 assert_eq!(r.denom(), 2_475);
832 }
833
834 #[test]
835 fn from_ratio_canonicalises_integer_rate() {
836 // 60 Hz exact: pc = 148_500 kHz, total = 2_475_000 → 148_500_000 / 2_475_000 = 60.
837 let r = RefreshRate::from_ratio(148_500_000, 2_475_000).unwrap();
838 assert_eq!(r, RefreshRate::integral(60));
839 }
840
841 #[test]
842 fn from_ratio_returns_none_on_zero_denominator() {
843 assert_eq!(RefreshRate::from_ratio(60, 0), None);
844 }
845
846 #[test]
847 fn from_ratio_handles_zero_numerator() {
848 let r = RefreshRate::from_ratio(0, 1000).unwrap();
849 assert_eq!(r, RefreshRate::integral(0));
850 }
851
852 #[test]
853 fn from_ratio_returns_none_when_reduced_exceeds_u32() {
854 // Coprime values both ≥ 2^32 cannot reduce into u32.
855 let big = u64::from(u32::MAX) + 2;
856 assert_eq!(RefreshRate::from_ratio(big, big - 1), None);
857 }
858}
859
860#[cfg(test)]
861#[cfg(any(feature = "alloc", feature = "std"))]
862mod extension_data_tests {
863 use super::*;
864
865 #[derive(Debug, Clone, PartialEq)]
866 struct Foo(u32);
867
868 #[derive(Debug, Clone, PartialEq)]
869 struct Bar(u32);
870
871 #[test]
872 fn take_extension_data_returns_and_removes_entry() {
873 let mut caps = DisplayCapabilities::default();
874 caps.set_extension_data(0x70, Foo(42));
875 let taken: Foo = caps.take_extension_data(0x70).expect("entry present");
876 assert_eq!(taken, Foo(42));
877 assert!(caps.get_extension_data::<Foo>(0x70).is_none());
878 }
879
880 #[test]
881 fn take_extension_data_returns_none_for_missing_tag() {
882 let mut caps = DisplayCapabilities::default();
883 assert!(caps.take_extension_data::<Foo>(0x70).is_none());
884 }
885
886 #[test]
887 fn take_extension_data_leaves_entry_on_type_mismatch() {
888 let mut caps = DisplayCapabilities::default();
889 caps.set_extension_data(0x70, Foo(7));
890 // Wrong type — must return None and not destroy the entry.
891 assert!(caps.take_extension_data::<Bar>(0x70).is_none());
892 // Entry still retrievable as the original type.
893 assert_eq!(caps.get_extension_data::<Foo>(0x70), Some(&Foo(7)));
894 }
895
896 #[test]
897 fn take_extension_data_round_trip_via_set() {
898 let mut caps = DisplayCapabilities::default();
899 caps.set_extension_data(0x70, Foo(1));
900 let mut foo: Foo = caps.take_extension_data(0x70).unwrap();
901 foo.0 += 1;
902 caps.set_extension_data(0x70, foo);
903 assert_eq!(caps.get_extension_data::<Foo>(0x70), Some(&Foo(2)));
904 }
905}