use crate::{CvtAlgorithm, RefreshRate, VideoMode};
pub fn pixel_clock_khz(mode: &VideoMode) -> u32 {
if let Some(clk) = mode.pixel_clock_khz {
return clk;
}
let Some(rr) = mode.refresh_rate else {
return 0;
};
let h_total = mode.width as u64 + 160;
let v_total = mode.height as u64 + 8;
let numer = rr.numer() as u64;
let denom = rr.denom() as u64;
(h_total * v_total * numer / (denom * 1000)) as u32
}
#[cfg(test)]
mod tests {
use super::*;
use crate::VideoMode;
#[test]
fn exact_clock_returned_unchanged() {
let mode = VideoMode::new(1920, 1080, 60u32, false).with_detailed_timing(
148_500,
88,
44,
4,
5,
0,
0,
Default::default(),
None,
);
assert_eq!(pixel_clock_khz(&mode), 148_500);
}
#[test]
fn with_pixel_clock_bypasses_estimate() {
let mode = VideoMode::new(1920, 1200, 60u32, false).with_pixel_clock(154_000);
assert_eq!(pixel_clock_khz(&mode), 154_000);
}
#[test]
fn non_dtd_mode_uses_cvt_rb_formula() {
let mode = VideoMode::new(1920, 1080, 60u32, false);
assert_eq!(pixel_clock_khz(&mode), 135_782);
}
#[test]
fn zero_refresh_rate_returns_zero() {
let mode = VideoMode::new(1920, 1080, 0u32, false);
assert_eq!(pixel_clock_khz(&mode), 0);
}
#[test]
fn unset_refresh_rate_returns_zero() {
let mode = VideoMode {
width: 1920,
height: 1080,
refresh_rate: None,
..Default::default()
};
assert_eq!(pixel_clock_khz(&mode), 0);
}
}
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ComputedTiming {
pub pixel_clock_khz: u32,
pub h_total: u16,
pub v_total: u16,
pub h_front_porch: u16,
pub h_sync_width: u16,
pub v_front_porch: u16,
pub v_sync_width: u16,
}
const RB_V1_CLOCK_STEP_KHZ: u32 = 250; const RB_V1_MIN_V_BLANK_US: u32 = 460; const RB_V1_H_BLANK: u16 = 160; const RB_V1_H_SYNC: u16 = 32; const RB_V1_H_BPORCH: u16 = 80; const RB_V1_H_FPORCH: u16 = RB_V1_H_BLANK - RB_V1_H_SYNC - RB_V1_H_BPORCH; const RB_V1_V_FPORCH: u16 = 3; const RB_V1_V_SYNC: u16 = 4; const RB_V1_MIN_V_BPORCH: u16 = 6;
const RB_V2_CLOCK_STEP_KHZ: u32 = 1; const RB_V2_MIN_V_BLANK_US: u32 = 460; const RB_V2_H_BLANK: u16 = 80; const RB_V2_H_SYNC: u16 = 32; const RB_V2_H_BPORCH: u16 = 40; const RB_V2_H_FPORCH: u16 = RB_V2_H_BLANK - RB_V2_H_SYNC - RB_V2_H_BPORCH; const RB_V2_MIN_V_FPORCH: u16 = 1; const RB_V2_V_SYNC: u16 = 8; const RB_V2_V_BPORCH: u16 = 6;
pub fn compute_type_ix_timing(
width: u16,
height: u16,
refresh_rate: RefreshRate,
algorithm: CvtAlgorithm,
) -> Option<ComputedTiming> {
match algorithm {
CvtAlgorithm::CvtRb => cvt_rb_v1(width, height, refresh_rate),
CvtAlgorithm::CvtR2 => cvt_rb_v2(width, height, refresh_rate),
_ => None,
}
}
fn floor_f64(x: f64) -> f64 {
let n = x as i64;
if (n as f64) > x {
(n - 1) as f64
} else {
n as f64
}
}
fn ceil_f64(x: f64) -> f64 {
let n = x as i64;
if (n as f64) < x {
(n + 1) as f64
} else {
n as f64
}
}
fn cvt_rb_v1(width: u16, height: u16, refresh_rate: RefreshRate) -> Option<ComputedTiming> {
if width == 0 || height == 0 {
return None;
}
let v_field_rate_hz = refresh_rate.as_f64();
if !v_field_rate_hz.is_finite() || v_field_rate_hz <= 0.0 {
return None;
}
let v_active = u32::from(height);
let frame_period_us = 1_000_000.0 / v_field_rate_hz;
let h_period_est_us = (frame_period_us - f64::from(RB_V1_MIN_V_BLANK_US))
/ f64::from(v_active + u32::from(RB_V1_V_FPORCH));
if h_period_est_us <= 0.0 {
return None; }
let vbi_lines = ceil_f64(f64::from(RB_V1_MIN_V_BLANK_US) / h_period_est_us) as u32;
let rb_min_vbi =
u32::from(RB_V1_V_FPORCH) + u32::from(RB_V1_V_SYNC) + u32::from(RB_V1_MIN_V_BPORCH);
let actual_vbi_lines = vbi_lines.max(rb_min_vbi);
let v_total = v_active + actual_vbi_lines;
let h_total = u32::from(width) + u32::from(RB_V1_H_BLANK);
let pixel_clock_hz = v_field_rate_hz * f64::from(v_total) * f64::from(h_total);
let pixel_clock_khz_steps =
floor_f64(pixel_clock_hz / 1000.0 / f64::from(RB_V1_CLOCK_STEP_KHZ)) as u32;
let pixel_clock_khz = pixel_clock_khz_steps * RB_V1_CLOCK_STEP_KHZ;
let h_total = u16::try_from(h_total).ok()?;
let v_total = u16::try_from(v_total).ok()?;
Some(ComputedTiming {
pixel_clock_khz,
h_total,
v_total,
h_front_porch: RB_V1_H_FPORCH,
h_sync_width: RB_V1_H_SYNC,
v_front_porch: RB_V1_V_FPORCH,
v_sync_width: RB_V1_V_SYNC,
})
}
fn cvt_rb_v2(width: u16, height: u16, refresh_rate: RefreshRate) -> Option<ComputedTiming> {
if width == 0 || height == 0 {
return None;
}
let v_field_rate_hz = refresh_rate.as_f64();
if !v_field_rate_hz.is_finite() || v_field_rate_hz <= 0.0 {
return None;
}
let v_active = u32::from(height);
let frame_period_us = 1_000_000.0 / v_field_rate_hz;
let h_period_est_us = (frame_period_us - f64::from(RB_V2_MIN_V_BLANK_US)) / f64::from(v_active);
if h_period_est_us <= 0.0 {
return None; }
let vbi_lines = ceil_f64(f64::from(RB_V2_MIN_V_BLANK_US) / h_period_est_us) as u32;
let rb_min_vbi =
u32::from(RB_V2_MIN_V_FPORCH) + u32::from(RB_V2_V_SYNC) + u32::from(RB_V2_V_BPORCH);
let actual_vbi_lines = vbi_lines.max(rb_min_vbi);
let v_total = v_active + actual_vbi_lines;
let h_total = u32::from(width) + u32::from(RB_V2_H_BLANK);
let pixel_clock_hz = v_field_rate_hz * f64::from(v_total) * f64::from(h_total);
let pixel_clock_khz_steps =
floor_f64(pixel_clock_hz / 1000.0 / f64::from(RB_V2_CLOCK_STEP_KHZ)) as u32;
let pixel_clock_khz = pixel_clock_khz_steps * RB_V2_CLOCK_STEP_KHZ;
let v_front_porch_u32 = actual_vbi_lines - u32::from(RB_V2_V_SYNC) - u32::from(RB_V2_V_BPORCH);
let v_front_porch = u16::try_from(v_front_porch_u32).ok()?;
let h_total = u16::try_from(h_total).ok()?;
let v_total = u16::try_from(v_total).ok()?;
Some(ComputedTiming {
pixel_clock_khz,
h_total,
v_total,
h_front_porch: RB_V2_H_FPORCH,
h_sync_width: RB_V2_H_SYNC,
v_front_porch,
v_sync_width: RB_V2_V_SYNC,
})
}
#[cfg(test)]
mod cvt_tests {
use super::*;
#[test]
fn cvt_rb_v1_1920x1080_at_60() {
let t = compute_type_ix_timing(1920, 1080, RefreshRate::integral(60), CvtAlgorithm::CvtRb)
.expect("CVT-RB v1 must produce a timing");
assert_eq!(t.pixel_clock_khz, 138_500);
assert_eq!(t.h_total, 2080);
assert_eq!(t.v_total, 1111);
assert_eq!(t.h_front_porch, 48);
assert_eq!(t.h_sync_width, 32);
assert_eq!(t.v_front_porch, 3);
assert_eq!(t.v_sync_width, 4);
}
#[test]
fn cvt_rb_v1_2560x1440_at_60() {
let t = compute_type_ix_timing(2560, 1440, RefreshRate::integral(60), CvtAlgorithm::CvtRb)
.expect("CVT-RB v1 must produce a timing");
assert_eq!(t.pixel_clock_khz, 241_500);
assert_eq!(t.h_total, 2720);
assert_eq!(t.v_total, 1481);
}
#[test]
fn cvt_rb_v1_3840x2160_at_30() {
let t = compute_type_ix_timing(3840, 2160, RefreshRate::integral(30), CvtAlgorithm::CvtRb)
.expect("CVT-RB v1 must produce a timing");
assert_eq!(t.pixel_clock_khz, 262_750);
assert_eq!(t.h_total, 4000);
assert_eq!(t.v_total, 2191);
}
#[test]
fn cvt_rb_v1_zero_width_returns_none() {
assert!(
compute_type_ix_timing(0, 1080, RefreshRate::integral(60), CvtAlgorithm::CvtRb)
.is_none()
);
}
#[test]
fn cvt_rb_v1_zero_height_returns_none() {
assert!(
compute_type_ix_timing(1920, 0, RefreshRate::integral(60), CvtAlgorithm::CvtRb)
.is_none()
);
}
#[test]
fn cvt_rb_v1_unreachable_refresh_returns_none() {
assert!(
compute_type_ix_timing(1920, 1080, RefreshRate::integral(3000), CvtAlgorithm::CvtRb)
.is_none()
);
}
#[test]
fn cvt_rb_v2_1920x1080_at_60() {
let t = compute_type_ix_timing(1920, 1080, RefreshRate::integral(60), CvtAlgorithm::CvtR2)
.expect("CVT-RB v2 must produce a timing");
assert_eq!(t.pixel_clock_khz, 133_320);
assert_eq!(t.h_total, 2000);
assert_eq!(t.v_total, 1111);
assert_eq!(t.h_front_porch, 8);
assert_eq!(t.h_sync_width, 32);
assert_eq!(t.v_front_porch, 17);
assert_eq!(t.v_sync_width, 8);
}
#[test]
fn cvt_rb_v2_2560x1440_at_120() {
let t = compute_type_ix_timing(2560, 1440, RefreshRate::integral(120), CvtAlgorithm::CvtR2)
.expect("CVT-RB v2 must produce a timing");
assert_eq!(t.pixel_clock_khz, 483_120);
assert_eq!(t.h_total, 2640);
assert_eq!(t.v_total, 1525);
}
#[test]
fn cvt_rb_v2_3840x2160_at_60() {
let t = compute_type_ix_timing(3840, 2160, RefreshRate::integral(60), CvtAlgorithm::CvtR2)
.expect("CVT-RB v2 must produce a timing");
assert_eq!(t.pixel_clock_khz, 522_614);
assert_eq!(t.h_total, 3920);
assert_eq!(t.v_total, 2222);
}
#[test]
fn cvt_rb_v2_zero_width_returns_none() {
assert!(
compute_type_ix_timing(0, 1080, RefreshRate::integral(60), CvtAlgorithm::CvtR2)
.is_none()
);
}
#[test]
fn cvt_rb_v2_unreachable_refresh_returns_none() {
assert!(
compute_type_ix_timing(1920, 1080, RefreshRate::integral(3000), CvtAlgorithm::CvtR2)
.is_none()
);
}
#[test]
fn cvt_standard_returns_none() {
assert!(
compute_type_ix_timing(1920, 1080, RefreshRate::integral(60), CvtAlgorithm::Cvt)
.is_none()
);
}
#[test]
fn reserved_algorithm_returns_none() {
assert!(
compute_type_ix_timing(
1920,
1080,
RefreshRate::integral(60),
CvtAlgorithm::Reserved(7)
)
.is_none()
);
}
}
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TimingFormula {
DefaultGtf,
RangeLimitsOnly,
SecondaryGtf(GtfSecondaryParams),
Cvt(CvtSupportParams),
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GtfSecondaryParams {
pub start_freq_khz: u16,
pub c: u8,
pub m: u16,
pub k: u8,
pub j: u8,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CvtSupportParams {
pub version: u8,
pub pixel_clock_adjust: u8,
pub max_h_active_pixels: Option<u16>,
pub supported_aspect_ratios: CvtAspectRatios,
pub preferred_aspect_ratio: Option<CvtAspectRatio>,
pub standard_blanking: bool,
pub reduced_blanking: bool,
pub scaling: CvtScaling,
pub preferred_v_rate: Option<u8>,
}
bitflags::bitflags! {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CvtAspectRatios: u8 {
const R4_3 = 0x80;
const R16_9 = 0x40;
const R16_10 = 0x20;
const R5_4 = 0x10;
const R15_9 = 0x08;
}
}
bitflags::bitflags! {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CvtScaling: u8 {
const HORIZONTAL_SHRINK = 0x80;
const HORIZONTAL_STRETCH = 0x40;
const VERTICAL_SHRINK = 0x20;
const VERTICAL_STRETCH = 0x10;
}
}
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CvtAspectRatio {
R4_3,
R16_9,
R16_10,
R5_4,
R15_9,
}