Skip to main content

display_types/
timing.rs

1use crate::{CvtAlgorithm, RefreshRate, VideoMode};
2
3/// Returns the pixel clock in kHz for a [`VideoMode`].
4///
5/// When `mode.pixel_clock_khz` is `Some` (set from a Detailed Timing Descriptor), returns
6/// that exact value directly. When it is `None` (modes decoded from standard timings,
7/// established timings, or SVD entries that lack a DTD), falls back to a CVT Reduced
8/// Blanking estimate:
9///
10/// - **Horizontal blanking:** 160 pixels (CVT-RB fixed blank, VESA CVT 1.2 §2.2).
11/// - **Vertical blanking:** 8 lines (minimum RB frame-height adjustment).
12///
13/// ```text
14/// pixel_clock_khz ≈ (width + 160) × (height + 8) × refresh_rate / 1000
15/// ```
16///
17/// Returns `0` when neither `mode.pixel_clock_khz` nor `mode.refresh_rate` is set.
18///
19/// # Accuracy of the fallback estimate
20///
21/// CVT-RB is the dominant timing standard for modern display modes. For typical consumer
22/// resolutions the estimate is within ~2% of the actual clock. HDMI Forum-specified CTA
23/// modes (e.g. 4K@60, VIC 97) use larger blanking than CVT-RB predicts and may be
24/// under-estimated by ~10–15%, which can produce false accepts in bandwidth ceiling checks.
25/// Interlaced modes diverge further.
26///
27/// The fallback is only used when no exact clock is available. Prefer populating
28/// `pixel_clock_khz` from the EDID Detailed Timing Descriptor wherever possible.
29pub fn pixel_clock_khz(mode: &VideoMode) -> u32 {
30    if let Some(clk) = mode.pixel_clock_khz {
31        return clk;
32    }
33    let Some(rr) = mode.refresh_rate else {
34        return 0;
35    };
36    let h_total = mode.width as u64 + 160;
37    let v_total = mode.height as u64 + 8;
38    let numer = rr.numer() as u64;
39    let denom = rr.denom() as u64;
40    (h_total * v_total * numer / (denom * 1000)) as u32
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46    use crate::VideoMode;
47
48    #[test]
49    fn exact_clock_returned_unchanged() {
50        let mode = VideoMode::new(1920, 1080, 60u32, false).with_detailed_timing(
51            148_500,
52            88,
53            44,
54            4,
55            5,
56            0,
57            0,
58            Default::default(),
59            None,
60        );
61        assert_eq!(pixel_clock_khz(&mode), 148_500);
62    }
63
64    #[test]
65    fn with_pixel_clock_bypasses_estimate() {
66        let mode = VideoMode::new(1920, 1200, 60u32, false).with_pixel_clock(154_000);
67        assert_eq!(pixel_clock_khz(&mode), 154_000);
68    }
69
70    #[test]
71    fn non_dtd_mode_uses_cvt_rb_formula() {
72        // 1920×1080@60: (1920+160) × (1080+8) × 60 / 1000 = 135_782
73        let mode = VideoMode::new(1920, 1080, 60u32, false);
74        assert_eq!(pixel_clock_khz(&mode), 135_782);
75    }
76
77    #[test]
78    fn zero_refresh_rate_returns_zero() {
79        let mode = VideoMode::new(1920, 1080, 0u32, false);
80        assert_eq!(pixel_clock_khz(&mode), 0);
81    }
82
83    #[test]
84    fn unset_refresh_rate_returns_zero() {
85        let mode = VideoMode {
86            width: 1920,
87            height: 1080,
88            refresh_rate: None,
89            ..Default::default()
90        };
91        assert_eq!(pixel_clock_khz(&mode), 0);
92    }
93}
94
95/// Pixel clock and blanking parameters computed from a CVT formula.
96///
97/// All fields are derived from `(width, height, refresh_rate, cvt_algorithm)`. Designed
98/// to feed [`VideoMode::with_detailed_timing`][crate::VideoMode::with_detailed_timing]
99/// directly: the five fields it carries map 1:1 onto that builder's
100/// `pixel_clock_khz` / `h_front_porch` / `h_sync_width` / `v_front_porch` / `v_sync_width`
101/// arguments. `h_total` and `v_total` are exposed for sanity checks (e.g. bandwidth
102/// estimation against the CVT-rounded clock).
103#[non_exhaustive]
104#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct ComputedTiming {
107    /// Pixel clock in kHz, rounded down to the algorithm's clock step (CVT-RB v1: 250 kHz).
108    pub pixel_clock_khz: u32,
109    /// `width + horizontal blanking`. Useful for bandwidth checks.
110    pub h_total: u16,
111    /// `height + vertical blanking`. Useful for bandwidth checks.
112    pub v_total: u16,
113    /// Horizontal front porch in pixels.
114    pub h_front_porch: u16,
115    /// Horizontal sync pulse width in pixels.
116    pub h_sync_width: u16,
117    /// Vertical front porch in lines.
118    pub v_front_porch: u16,
119    /// Vertical sync pulse width in lines.
120    pub v_sync_width: u16,
121}
122
123// CVT-RB v1 constants (VESA CVT 1.1 §3.4 "Reduced Blanking" timing).
124const RB_V1_CLOCK_STEP_KHZ: u32 = 250; // 0.25 MHz pixel-clock granularity
125const RB_V1_MIN_V_BLANK_US: u32 = 460; // microseconds, fixed
126const RB_V1_H_BLANK: u16 = 160; // pixels, fixed total H blanking
127const RB_V1_H_SYNC: u16 = 32; // pixels, fixed
128const RB_V1_H_BPORCH: u16 = 80; // pixels, fixed
129const RB_V1_H_FPORCH: u16 = RB_V1_H_BLANK - RB_V1_H_SYNC - RB_V1_H_BPORCH; // 48
130const RB_V1_V_FPORCH: u16 = 3; // lines, fixed
131const RB_V1_V_SYNC: u16 = 4; // lines, fixed
132const RB_V1_MIN_V_BPORCH: u16 = 6; // lines, lower bound
133
134// CVT-RB v2 constants (VESA CVT 1.2 §4 "Reduced Blanking version 2" timing).
135const RB_V2_CLOCK_STEP_KHZ: u32 = 1; // 0.001 MHz (1 kHz) pixel-clock granularity
136const RB_V2_MIN_V_BLANK_US: u32 = 460; // microseconds, fixed
137const RB_V2_H_BLANK: u16 = 80; // pixels, fixed total H blanking (half of RB v1)
138const RB_V2_H_SYNC: u16 = 32; // pixels, fixed
139const RB_V2_H_BPORCH: u16 = 40; // pixels, fixed
140const RB_V2_H_FPORCH: u16 = RB_V2_H_BLANK - RB_V2_H_SYNC - RB_V2_H_BPORCH; // 8
141const RB_V2_MIN_V_FPORCH: u16 = 1; // lines, lower bound (slack lives here, not in back porch)
142const RB_V2_V_SYNC: u16 = 8; // lines, fixed (v1 used 4)
143const RB_V2_V_BPORCH: u16 = 6; // lines, fixed (v1 had a 6-line minimum but variable back porch)
144
145/// Computes pixel clock and blanking parameters for a DisplayID 2.x Type IX
146/// (Formula-Based Timing) descriptor using the named CVT variant.
147///
148/// Returns `None` for:
149/// - degenerate input (`width == 0`, `height == 0`, non-positive or non-finite refresh rate)
150/// - `CvtAlgorithm::Cvt` (standard CVT, not reduced blanking — no evaluator implemented)
151/// - the `Reserved(_)` algorithm encoding
152///
153/// The returned [`ComputedTiming`] feeds [`VideoMode::with_detailed_timing`] directly.
154///
155/// # Algorithm coverage
156///
157/// | `CvtAlgorithm` variant | Status |
158/// |------------------------|--------|
159/// | `Cvt`                  | not implemented (standard CVT, no reduced blanking) — returns `None` |
160/// | `CvtRb`                | implemented (CVT-RB v1, VESA CVT 1.1 §3.4) |
161/// | `CvtR2`                | implemented (CVT-RB v2, VESA CVT 1.2 §4) |
162/// | `Reserved(_)`          | always `None` |
163pub fn compute_type_ix_timing(
164    width: u16,
165    height: u16,
166    refresh_rate: RefreshRate,
167    algorithm: CvtAlgorithm,
168) -> Option<ComputedTiming> {
169    match algorithm {
170        CvtAlgorithm::CvtRb => cvt_rb_v1(width, height, refresh_rate),
171        CvtAlgorithm::CvtR2 => cvt_rb_v2(width, height, refresh_rate),
172        _ => None,
173    }
174}
175
176// f64::floor() and f64::ceil() are not available in no_std (they require libm).
177// These helpers use integer truncation, which is correct for all finite positive
178// inputs — the only values the CVT evaluators ever produce.
179fn floor_f64(x: f64) -> f64 {
180    let n = x as i64;
181    if (n as f64) > x {
182        (n - 1) as f64
183    } else {
184        n as f64
185    }
186}
187fn ceil_f64(x: f64) -> f64 {
188    let n = x as i64;
189    if (n as f64) < x {
190        (n + 1) as f64
191    } else {
192        n as f64
193    }
194}
195
196/// CVT-RB v1 evaluator. See [`compute_type_ix_timing`] for the public entry point.
197fn cvt_rb_v1(width: u16, height: u16, refresh_rate: RefreshRate) -> Option<ComputedTiming> {
198    if width == 0 || height == 0 {
199        return None;
200    }
201    let v_field_rate_hz = refresh_rate.as_f64();
202    if !v_field_rate_hz.is_finite() || v_field_rate_hz <= 0.0 {
203        return None;
204    }
205
206    let v_active = u32::from(height);
207    let frame_period_us = 1_000_000.0 / v_field_rate_hz;
208
209    // Horizontal line period estimate, used to derive how many lines fit in the fixed
210    // 460 µs minimum vertical blanking.
211    let h_period_est_us = (frame_period_us - f64::from(RB_V1_MIN_V_BLANK_US))
212        / f64::from(v_active + u32::from(RB_V1_V_FPORCH));
213    if h_period_est_us <= 0.0 {
214        return None; // refresh too high to fit RB1's minimum blanking budget
215    }
216
217    let vbi_lines = ceil_f64(f64::from(RB_V1_MIN_V_BLANK_US) / h_period_est_us) as u32;
218    let rb_min_vbi =
219        u32::from(RB_V1_V_FPORCH) + u32::from(RB_V1_V_SYNC) + u32::from(RB_V1_MIN_V_BPORCH);
220    let actual_vbi_lines = vbi_lines.max(rb_min_vbi);
221
222    let v_total = v_active + actual_vbi_lines;
223    let h_total = u32::from(width) + u32::from(RB_V1_H_BLANK);
224
225    // Pixel clock: V_FIELD_RATE × V_TOTAL × H_TOTAL, floored to the 250 kHz step.
226    let pixel_clock_hz = v_field_rate_hz * f64::from(v_total) * f64::from(h_total);
227    let pixel_clock_khz_steps =
228        floor_f64(pixel_clock_hz / 1000.0 / f64::from(RB_V1_CLOCK_STEP_KHZ)) as u32;
229    let pixel_clock_khz = pixel_clock_khz_steps * RB_V1_CLOCK_STEP_KHZ;
230
231    // Sanity: H_TOTAL and V_TOTAL must fit in u16 for VideoMode.
232    let h_total = u16::try_from(h_total).ok()?;
233    let v_total = u16::try_from(v_total).ok()?;
234
235    Some(ComputedTiming {
236        pixel_clock_khz,
237        h_total,
238        v_total,
239        h_front_porch: RB_V1_H_FPORCH,
240        h_sync_width: RB_V1_H_SYNC,
241        v_front_porch: RB_V1_V_FPORCH,
242        v_sync_width: RB_V1_V_SYNC,
243    })
244}
245
246/// CVT-RB v2 evaluator. See [`compute_type_ix_timing`] for the public entry point.
247///
248/// Differences from v1: half the horizontal blanking (80 vs 160 px), 1 kHz pixel-clock
249/// step (vs 0.25 MHz), V_SYNC widened to 8 lines (was 4), V_BPORCH fixed at 6 lines
250/// (was a 6-line minimum), and the variable slack lives in V_FPORCH (≥ 1 line) rather
251/// than V_BPORCH.
252fn cvt_rb_v2(width: u16, height: u16, refresh_rate: RefreshRate) -> Option<ComputedTiming> {
253    if width == 0 || height == 0 {
254        return None;
255    }
256    let v_field_rate_hz = refresh_rate.as_f64();
257    if !v_field_rate_hz.is_finite() || v_field_rate_hz <= 0.0 {
258        return None;
259    }
260
261    let v_active = u32::from(height);
262    let frame_period_us = 1_000_000.0 / v_field_rate_hz;
263
264    // RB v2 doesn't add V_FPORCH to the divisor (unlike v1's `(v_active + V_FPORCH)`).
265    let h_period_est_us = (frame_period_us - f64::from(RB_V2_MIN_V_BLANK_US)) / f64::from(v_active);
266    if h_period_est_us <= 0.0 {
267        return None; // refresh too high to fit RB v2's minimum blanking budget
268    }
269
270    let vbi_lines = ceil_f64(f64::from(RB_V2_MIN_V_BLANK_US) / h_period_est_us) as u32;
271    let rb_min_vbi =
272        u32::from(RB_V2_MIN_V_FPORCH) + u32::from(RB_V2_V_SYNC) + u32::from(RB_V2_V_BPORCH);
273    let actual_vbi_lines = vbi_lines.max(rb_min_vbi);
274
275    let v_total = v_active + actual_vbi_lines;
276    let h_total = u32::from(width) + u32::from(RB_V2_H_BLANK);
277
278    // Pixel clock: V_FIELD_RATE × V_TOTAL × H_TOTAL, floored to the 1 kHz step.
279    let pixel_clock_hz = v_field_rate_hz * f64::from(v_total) * f64::from(h_total);
280    let pixel_clock_khz_steps =
281        floor_f64(pixel_clock_hz / 1000.0 / f64::from(RB_V2_CLOCK_STEP_KHZ)) as u32;
282    let pixel_clock_khz = pixel_clock_khz_steps * RB_V2_CLOCK_STEP_KHZ;
283
284    // V_FPORCH carries the slack in v2: VBI = V_FPORCH + V_SYNC + V_BPORCH.
285    let v_front_porch_u32 = actual_vbi_lines - u32::from(RB_V2_V_SYNC) - u32::from(RB_V2_V_BPORCH);
286    let v_front_porch = u16::try_from(v_front_porch_u32).ok()?;
287
288    // Sanity: H_TOTAL and V_TOTAL must fit in u16 for VideoMode.
289    let h_total = u16::try_from(h_total).ok()?;
290    let v_total = u16::try_from(v_total).ok()?;
291
292    Some(ComputedTiming {
293        pixel_clock_khz,
294        h_total,
295        v_total,
296        h_front_porch: RB_V2_H_FPORCH,
297        h_sync_width: RB_V2_H_SYNC,
298        v_front_porch,
299        v_sync_width: RB_V2_V_SYNC,
300    })
301}
302
303#[cfg(test)]
304mod cvt_tests {
305    use super::*;
306
307    #[test]
308    fn cvt_rb_v1_1920x1080_at_60() {
309        // Canonical CVT-RB v1 mode. VESA-published value: 138.500 MHz pixel clock,
310        // h_total = 2080, v_total = 1111.
311        let t = compute_type_ix_timing(1920, 1080, RefreshRate::integral(60), CvtAlgorithm::CvtRb)
312            .expect("CVT-RB v1 must produce a timing");
313        assert_eq!(t.pixel_clock_khz, 138_500);
314        assert_eq!(t.h_total, 2080);
315        assert_eq!(t.v_total, 1111);
316        assert_eq!(t.h_front_porch, 48);
317        assert_eq!(t.h_sync_width, 32);
318        assert_eq!(t.v_front_porch, 3);
319        assert_eq!(t.v_sync_width, 4);
320    }
321
322    #[test]
323    fn cvt_rb_v1_2560x1440_at_60() {
324        // CVT-RB v1 reference: 241.500 MHz, h_total = 2720, v_total = 1481.
325        let t = compute_type_ix_timing(2560, 1440, RefreshRate::integral(60), CvtAlgorithm::CvtRb)
326            .expect("CVT-RB v1 must produce a timing");
327        assert_eq!(t.pixel_clock_khz, 241_500);
328        assert_eq!(t.h_total, 2720);
329        assert_eq!(t.v_total, 1481);
330    }
331
332    #[test]
333    fn cvt_rb_v1_3840x2160_at_30() {
334        // CVT-RB v1 reference: 262.750 MHz, h_total = 4000, v_total = 2191.
335        let t = compute_type_ix_timing(3840, 2160, RefreshRate::integral(30), CvtAlgorithm::CvtRb)
336            .expect("CVT-RB v1 must produce a timing");
337        assert_eq!(t.pixel_clock_khz, 262_750);
338        assert_eq!(t.h_total, 4000);
339        assert_eq!(t.v_total, 2191);
340    }
341
342    #[test]
343    fn cvt_rb_v1_zero_width_returns_none() {
344        assert!(
345            compute_type_ix_timing(0, 1080, RefreshRate::integral(60), CvtAlgorithm::CvtRb)
346                .is_none()
347        );
348    }
349
350    #[test]
351    fn cvt_rb_v1_zero_height_returns_none() {
352        assert!(
353            compute_type_ix_timing(1920, 0, RefreshRate::integral(60), CvtAlgorithm::CvtRb)
354                .is_none()
355        );
356    }
357
358    #[test]
359    fn cvt_rb_v1_unreachable_refresh_returns_none() {
360        // Refresh so high that frame period is below the 460 µs RB minimum.
361        // 1/3000 s = 333 µs < 460 µs → no time for active video.
362        assert!(
363            compute_type_ix_timing(1920, 1080, RefreshRate::integral(3000), CvtAlgorithm::CvtRb)
364                .is_none()
365        );
366    }
367
368    #[test]
369    fn cvt_rb_v2_1920x1080_at_60() {
370        // CVT-RB v2 1920×1080@60: half the H blanking of v1, slack in V_FPORCH.
371        // h_total = 2000, v_total = 1111 → 60 × 2000 × 1111 = 133_320_000 Hz → 133_320 kHz.
372        let t = compute_type_ix_timing(1920, 1080, RefreshRate::integral(60), CvtAlgorithm::CvtR2)
373            .expect("CVT-RB v2 must produce a timing");
374        assert_eq!(t.pixel_clock_khz, 133_320);
375        assert_eq!(t.h_total, 2000);
376        assert_eq!(t.v_total, 1111);
377        assert_eq!(t.h_front_porch, 8);
378        assert_eq!(t.h_sync_width, 32);
379        // VBI = 31, V_SYNC = 8, V_BPORCH = 6 → V_FPORCH = 31 - 8 - 6 = 17.
380        assert_eq!(t.v_front_porch, 17);
381        assert_eq!(t.v_sync_width, 8);
382    }
383
384    #[test]
385    fn cvt_rb_v2_2560x1440_at_120() {
386        // 144/240 Hz panels typically use RB v2. h_total = 2640, v_total = 1525.
387        // 120 × 2640 × 1525 = 483_120_000 Hz → 483_120 kHz.
388        let t = compute_type_ix_timing(2560, 1440, RefreshRate::integral(120), CvtAlgorithm::CvtR2)
389            .expect("CVT-RB v2 must produce a timing");
390        assert_eq!(t.pixel_clock_khz, 483_120);
391        assert_eq!(t.h_total, 2640);
392        assert_eq!(t.v_total, 1525);
393    }
394
395    #[test]
396    fn cvt_rb_v2_3840x2160_at_60() {
397        // 4K@60 RB v2: h_total = 3920, v_total = 2222.
398        // 60 × 3920 × 2222 = 522_614_400 Hz → 522_614 kHz (floor of 522614.4).
399        let t = compute_type_ix_timing(3840, 2160, RefreshRate::integral(60), CvtAlgorithm::CvtR2)
400            .expect("CVT-RB v2 must produce a timing");
401        assert_eq!(t.pixel_clock_khz, 522_614);
402        assert_eq!(t.h_total, 3920);
403        assert_eq!(t.v_total, 2222);
404    }
405
406    #[test]
407    fn cvt_rb_v2_zero_width_returns_none() {
408        assert!(
409            compute_type_ix_timing(0, 1080, RefreshRate::integral(60), CvtAlgorithm::CvtR2)
410                .is_none()
411        );
412    }
413
414    #[test]
415    fn cvt_rb_v2_unreachable_refresh_returns_none() {
416        // Frame period below the 460 µs RB v2 minimum.
417        assert!(
418            compute_type_ix_timing(1920, 1080, RefreshRate::integral(3000), CvtAlgorithm::CvtR2)
419                .is_none()
420        );
421    }
422
423    #[test]
424    fn cvt_standard_returns_none() {
425        // Standard CVT (no reduced blanking) has no evaluator implemented.
426        assert!(
427            compute_type_ix_timing(1920, 1080, RefreshRate::integral(60), CvtAlgorithm::Cvt)
428                .is_none()
429        );
430    }
431
432    #[test]
433    fn reserved_algorithm_returns_none() {
434        assert!(
435            compute_type_ix_timing(
436                1920,
437                1080,
438                RefreshRate::integral(60),
439                CvtAlgorithm::Reserved(7)
440            )
441            .is_none()
442        );
443    }
444}
445
446/// Video timing support reported in the display range limits descriptor (`0xFD`), byte 10.
447///
448/// Indicates which timing generation formula (if any) the display supports beyond the
449/// explicitly listed modes.
450#[non_exhaustive]
451#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
452#[derive(Debug, Clone, PartialEq, Eq)]
453pub enum TimingFormula {
454    /// Default GTF supported (byte 10 = `0x00`).
455    ///
456    /// The display accepts any timing within its range limits that satisfies the
457    /// default GTF parameters. Requires bit 0 of the Feature Support byte (`0x18`) to be set.
458    DefaultGtf,
459    /// Range limits only; no secondary timing formula (byte 10 = `0x01`).
460    ///
461    /// The display supports only the video timing modes explicitly listed in the EDID.
462    RangeLimitsOnly,
463    /// Secondary GTF curve supported (byte 10 = `0x02`).
464    ///
465    /// The display accepts timings using either the default GTF or the secondary GTF curve
466    /// whose parameters are stored in bytes 12–17.
467    SecondaryGtf(GtfSecondaryParams),
468    /// CVT timing supported (byte 10 = `0x04`), with parameters from bytes 11–17.
469    ///
470    /// The display accepts Coordinated Video Timings within its range limits.
471    /// Requires bit 0 of the Feature Support byte (`0x18`) to be set.
472    Cvt(CvtSupportParams),
473}
474
475/// GTF secondary curve parameters decoded from a display range limits descriptor (`0xFD`).
476///
477/// Used when [`TimingFormula::SecondaryGtf`] is active (byte 10 = `0x02`).
478/// The GTF formula selects the secondary curve for horizontal frequencies at or above
479/// [`start_freq_khz`][Self::start_freq_khz] and the default curve below it.
480#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
481#[derive(Debug, Clone, PartialEq, Eq)]
482pub struct GtfSecondaryParams {
483    /// Start break frequency in kHz (byte 12 × 2).
484    pub start_freq_khz: u16,
485    /// GTF `C` parameter (0–127); byte 13 ÷ 2.
486    pub c: u8,
487    /// GTF `M` parameter (0–65535); bytes 14–15, little-endian.
488    pub m: u16,
489    /// GTF `K` parameter (0–255); byte 16.
490    pub k: u8,
491    /// GTF `J` parameter (0–127); byte 17 ÷ 2.
492    pub j: u8,
493}
494
495/// CVT support parameters decoded from a display range limits descriptor (`0xFD`).
496///
497/// Used when [`TimingFormula::Cvt`] is active (byte 10 = `0x04`).
498#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
499#[derive(Debug, Clone, PartialEq, Eq)]
500pub struct CvtSupportParams {
501    /// CVT standard version, encoded as two BCD nibbles (e.g., `0x11` = version 1.1).
502    pub version: u8,
503    /// Additional pixel clock precision: 6-bit value from byte 12 bits 7–2.
504    ///
505    /// The maximum pixel clock is: `(descriptor byte 9 × 10 MHz) − (pixel_clock_adjust × 0.25 MHz)`.
506    /// When all six bits are set (`63`), byte 9 was already rounded up to the nearest 10 MHz.
507    pub pixel_clock_adjust: u8,
508    /// Maximum number of horizontal active pixels, or `None` if there is no limit.
509    ///
510    /// Computed as `8 × (byte 13 + 256 × (byte 12 bits 1–0))`. `None` when the 10-bit
511    /// combined value is zero.
512    pub max_h_active_pixels: Option<u16>,
513    /// Aspect ratios the display supports for CVT-generated timings.
514    pub supported_aspect_ratios: CvtAspectRatios,
515    /// Preferred aspect ratio for CVT-generated timings, or `None` for a reserved value.
516    pub preferred_aspect_ratio: Option<CvtAspectRatio>,
517    /// Standard CVT blanking (normal blanking) is supported.
518    pub standard_blanking: bool,
519    /// Reduced CVT blanking is supported (preferred over standard blanking).
520    pub reduced_blanking: bool,
521    /// Display scaling capabilities.
522    pub scaling: CvtScaling,
523    /// Preferred vertical refresh rate in Hz, or `None` if byte 17 = `0x00` (reserved).
524    pub preferred_v_rate: Option<u8>,
525}
526
527bitflags::bitflags! {
528    /// Aspect ratios supported for CVT-generated timings (byte 14 of a `0xFD` descriptor).
529    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
530    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
531    pub struct CvtAspectRatios: u8 {
532        /// 4∶3 aspect ratio supported.
533        const R4_3   = 0x80;
534        /// 16∶9 aspect ratio supported.
535        const R16_9  = 0x40;
536        /// 16∶10 aspect ratio supported.
537        const R16_10 = 0x20;
538        /// 5∶4 aspect ratio supported.
539        const R5_4   = 0x10;
540        /// 15∶9 aspect ratio supported.
541        const R15_9  = 0x08;
542    }
543}
544
545bitflags::bitflags! {
546    /// Display scaling capabilities reported in byte 16 of a `0xFD` CVT descriptor.
547    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
548    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
549    pub struct CvtScaling: u8 {
550        /// Input horizontal active pixels can exceed the display's preferred horizontal count.
551        const HORIZONTAL_SHRINK  = 0x80;
552        /// Input horizontal active pixels can be fewer than the display's preferred horizontal count.
553        const HORIZONTAL_STRETCH = 0x40;
554        /// Input vertical active lines can exceed the display's preferred vertical count.
555        const VERTICAL_SHRINK    = 0x20;
556        /// Input vertical active lines can be fewer than the display's preferred vertical count.
557        const VERTICAL_STRETCH   = 0x10;
558    }
559}
560
561/// Preferred aspect ratio for CVT-generated timings, decoded from byte 15 bits 7–5.
562#[non_exhaustive]
563#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
564#[derive(Debug, Clone, Copy, PartialEq, Eq)]
565pub enum CvtAspectRatio {
566    /// 4∶3 preferred aspect ratio.
567    R4_3,
568    /// 16∶9 preferred aspect ratio.
569    R16_9,
570    /// 16∶10 preferred aspect ratio.
571    R16_10,
572    /// 5∶4 preferred aspect ratio.
573    R5_4,
574    /// 15∶9 preferred aspect ratio.
575    R15_9,
576}