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}