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