Skip to main content

display_types/
timing.rs

1use crate::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_hz / 1000
15/// ```
16///
17/// # Accuracy of the fallback estimate
18///
19/// CVT-RB is the dominant timing standard for modern display modes. For typical consumer
20/// resolutions the estimate is within ~2% of the actual clock. HDMI Forum-specified CTA
21/// modes (e.g. 4K@60, VIC 97) use larger blanking than CVT-RB predicts and may be
22/// under-estimated by ~10–15%, which can produce false accepts in bandwidth ceiling checks.
23/// Interlaced modes diverge further.
24///
25/// The fallback is only used when no exact clock is available. Prefer populating
26/// `pixel_clock_khz` from the EDID Detailed Timing Descriptor wherever possible.
27pub fn pixel_clock_khz(mode: &VideoMode) -> u32 {
28    if let Some(clk) = mode.pixel_clock_khz {
29        return clk;
30    }
31    let h_total = mode.width as u64 + 160;
32    let v_total = mode.height as u64 + 8;
33    (h_total * v_total * mode.refresh_rate as u64 / 1000) as u32
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39    use crate::VideoMode;
40
41    #[test]
42    fn exact_clock_returned_unchanged() {
43        let mode = VideoMode::new(1920, 1080, 60, false).with_detailed_timing(
44            148_500,
45            88,
46            44,
47            4,
48            5,
49            0,
50            0,
51            Default::default(),
52            None,
53        );
54        assert_eq!(pixel_clock_khz(&mode), 148_500);
55    }
56
57    #[test]
58    fn with_pixel_clock_bypasses_estimate() {
59        let mode = VideoMode::new(1920, 1200, 60, false).with_pixel_clock(154_000);
60        assert_eq!(pixel_clock_khz(&mode), 154_000);
61    }
62
63    #[test]
64    fn non_dtd_mode_uses_cvt_rb_formula() {
65        // 1920×1080@60: (1920+160) × (1080+8) × 60 / 1000 = 135_782
66        let mode = VideoMode::new(1920, 1080, 60, false);
67        assert_eq!(pixel_clock_khz(&mode), 135_782);
68    }
69
70    #[test]
71    fn zero_refresh_rate_returns_zero() {
72        let mode = VideoMode::new(1920, 1080, 0, false);
73        assert_eq!(pixel_clock_khz(&mode), 0);
74    }
75}
76
77/// Video timing support reported in the display range limits descriptor (`0xFD`), byte 10.
78///
79/// Indicates which timing generation formula (if any) the display supports beyond the
80/// explicitly listed modes.
81#[non_exhaustive]
82#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum TimingFormula {
85    /// Default GTF supported (byte 10 = `0x00`).
86    ///
87    /// The display accepts any timing within its range limits that satisfies the
88    /// default GTF parameters. Requires bit 0 of the Feature Support byte (`0x18`) to be set.
89    DefaultGtf,
90    /// Range limits only; no secondary timing formula (byte 10 = `0x01`).
91    ///
92    /// The display supports only the video timing modes explicitly listed in the EDID.
93    RangeLimitsOnly,
94    /// Secondary GTF curve supported (byte 10 = `0x02`).
95    ///
96    /// The display accepts timings using either the default GTF or the secondary GTF curve
97    /// whose parameters are stored in bytes 12–17.
98    SecondaryGtf(GtfSecondaryParams),
99    /// CVT timing supported (byte 10 = `0x04`), with parameters from bytes 11–17.
100    ///
101    /// The display accepts Coordinated Video Timings within its range limits.
102    /// Requires bit 0 of the Feature Support byte (`0x18`) to be set.
103    Cvt(CvtSupportParams),
104}
105
106/// GTF secondary curve parameters decoded from a display range limits descriptor (`0xFD`).
107///
108/// Used when [`TimingFormula::SecondaryGtf`] is active (byte 10 = `0x02`).
109/// The GTF formula selects the secondary curve for horizontal frequencies at or above
110/// [`start_freq_khz`][Self::start_freq_khz] and the default curve below it.
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct GtfSecondaryParams {
114    /// Start break frequency in kHz (byte 12 × 2).
115    pub start_freq_khz: u16,
116    /// GTF `C` parameter (0–127); byte 13 ÷ 2.
117    pub c: u8,
118    /// GTF `M` parameter (0–65535); bytes 14–15, little-endian.
119    pub m: u16,
120    /// GTF `K` parameter (0–255); byte 16.
121    pub k: u8,
122    /// GTF `J` parameter (0–127); byte 17 ÷ 2.
123    pub j: u8,
124}
125
126/// CVT support parameters decoded from a display range limits descriptor (`0xFD`).
127///
128/// Used when [`TimingFormula::Cvt`] is active (byte 10 = `0x04`).
129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct CvtSupportParams {
132    /// CVT standard version, encoded as two BCD nibbles (e.g., `0x11` = version 1.1).
133    pub version: u8,
134    /// Additional pixel clock precision: 6-bit value from byte 12 bits 7–2.
135    ///
136    /// The maximum pixel clock is: `(descriptor byte 9 × 10 MHz) − (pixel_clock_adjust × 0.25 MHz)`.
137    /// When all six bits are set (`63`), byte 9 was already rounded up to the nearest 10 MHz.
138    pub pixel_clock_adjust: u8,
139    /// Maximum number of horizontal active pixels, or `None` if there is no limit.
140    ///
141    /// Computed as `8 × (byte 13 + 256 × (byte 12 bits 1–0))`. `None` when the 10-bit
142    /// combined value is zero.
143    pub max_h_active_pixels: Option<u16>,
144    /// Aspect ratios the display supports for CVT-generated timings.
145    pub supported_aspect_ratios: CvtAspectRatios,
146    /// Preferred aspect ratio for CVT-generated timings, or `None` for a reserved value.
147    pub preferred_aspect_ratio: Option<CvtAspectRatio>,
148    /// Standard CVT blanking (normal blanking) is supported.
149    pub standard_blanking: bool,
150    /// Reduced CVT blanking is supported (preferred over standard blanking).
151    pub reduced_blanking: bool,
152    /// Display scaling capabilities.
153    pub scaling: CvtScaling,
154    /// Preferred vertical refresh rate in Hz, or `None` if byte 17 = `0x00` (reserved).
155    pub preferred_v_rate: Option<u8>,
156}
157
158bitflags::bitflags! {
159    /// Aspect ratios supported for CVT-generated timings (byte 14 of a `0xFD` descriptor).
160    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
161    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
162    pub struct CvtAspectRatios: u8 {
163        /// 4∶3 aspect ratio supported.
164        const R4_3   = 0x80;
165        /// 16∶9 aspect ratio supported.
166        const R16_9  = 0x40;
167        /// 16∶10 aspect ratio supported.
168        const R16_10 = 0x20;
169        /// 5∶4 aspect ratio supported.
170        const R5_4   = 0x10;
171        /// 15∶9 aspect ratio supported.
172        const R15_9  = 0x08;
173    }
174}
175
176bitflags::bitflags! {
177    /// Display scaling capabilities reported in byte 16 of a `0xFD` CVT descriptor.
178    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
179    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
180    pub struct CvtScaling: u8 {
181        /// Input horizontal active pixels can exceed the display's preferred horizontal count.
182        const HORIZONTAL_SHRINK  = 0x80;
183        /// Input horizontal active pixels can be fewer than the display's preferred horizontal count.
184        const HORIZONTAL_STRETCH = 0x40;
185        /// Input vertical active lines can exceed the display's preferred vertical count.
186        const VERTICAL_SHRINK    = 0x20;
187        /// Input vertical active lines can be fewer than the display's preferred vertical count.
188        const VERTICAL_STRETCH   = 0x10;
189    }
190}
191
192/// Preferred aspect ratio for CVT-generated timings, decoded from byte 15 bits 7–5.
193#[non_exhaustive]
194#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub enum CvtAspectRatio {
197    /// 4∶3 preferred aspect ratio.
198    R4_3,
199    /// 16∶9 preferred aspect ratio.
200    R16_9,
201    /// 16∶10 preferred aspect ratio.
202    R16_10,
203    /// 5∶4 preferred aspect ratio.
204    R5_4,
205    /// 15∶9 preferred aspect ratio.
206    R15_9,
207}