use bitvec::{field::BitField as _, order::Lsb0, view::BitView};
use nobcd::BcdNumber;
use crate::prelude::internal::*;
#[tracing::instrument(skip_all)]
pub(crate) fn parse(input: &[u8; 18], edid: &[u8]) -> Result<RangeLimitsDesc, EdidError> {
let limits = just_limits(input)?;
let cf = edid[0x18].view_bits::<Lsb0>()[0];
let check_supports_cont_freq = || {
if !cf {
tracing::warn!("The EDID reported supporting GTF, but its feature support bit is off!");
}
};
Ok(match input[10] {
0x00 => {
check_supports_cont_freq();
RangeLimitsDesc::GtfSupported { limits }
}
0x01 => RangeLimitsDesc::LimitsOnly {
limits,
flexible: cf,
},
0x02 => {
check_supports_cont_freq();
gtf_secondary_curve(limits, input)
}
0x04 => {
check_supports_cont_freq();
cvt(limits, input)?
}
reserved => {
tracing::error!(
"The given descriptor used a reserved video timing support flag! (`reserved`)"
);
return Err(EdidError::DescriptorRangeLimitsUsedReservedVTSFlag { flag: reserved });
}
})
}
#[tracing::instrument]
fn gtf_secondary_curve(limits: RangeLimits, input: &[u8; 18]) -> RangeLimitsDesc {
if input[11] != 0x00 {
tracing::warn!(
"GTF Secondary Curve reserved byte was non-zero! (byte: {})",
input[11]
);
}
let start_break_freq = input[12] as u16 * 2;
let c2 = input[13];
let m: u16 = [input[14], input[15]].view_bits::<Lsb0>().load();
let k = input[16];
let j2 = input[17];
RangeLimitsDesc::GtfSecondaryCurveSupported {
limits,
start_break_freq,
c2,
m,
k,
j2,
}
}
#[tracing::instrument]
fn cvt(limits: RangeLimits, input: &[u8; 18]) -> Result<RangeLimitsDesc, EdidError> {
let cvt_version = input[11];
let enhanced_px_clk = {
let to_sub: u8 = input[12].view_bits::<Lsb0>()[2..=7].load();
let decimal = Decimal::from(limits.max_pixel_clock_mhz);
decimal - (Decimal::from(to_sub) / Decimal::from(4))
};
let maximum_active_pxls_per_line = {
let lower = input[13];
if lower == 0x00 {
None
} else {
let upper: u16 = input[12].view_bits::<Lsb0>()[0..=1].load::<u16>() * 256_u16;
let combined = upper + lower as u16;
Some(combined * 8)
}
};
let supported_aspect_ratios = {
let bits = input[14].view_bits::<Lsb0>();
if !bits[0..=2].iter().all(|b| b == false) {
tracing::warn!("Reserved bits for supported aspect ratios are in use.");
}
SupportedAspectRatios {
_4x3: bits[7],
_16x9: bits[6],
_16x10: bits[7],
_5x4: bits[4],
_15x9: bits[3],
}
};
let preferred_aspect_ratio = {
let bits = input[15].view_bits::<Lsb0>();
match (bits[7], bits[6], bits[5]) {
(false, false, false) => PreferredAspectRatio::_4x3,
(false, false, true) => PreferredAspectRatio::_16x9,
(false, true, false) => PreferredAspectRatio::_16x10,
(false, true, true) => PreferredAspectRatio::_5x4,
(true, false, false) => PreferredAspectRatio::_15x9,
other => {
tracing::error!(
"Used reserved combination of pref. aspect ratio bits. ({other:x?})"
);
return Err(EdidError::DescriptorRangeLimitsCvtReservedBits);
}
}
};
let (supports_standard_cvt_blanking, supports_reduced_cvt_blanking) = {
let bits = input[15].view_bits::<Lsb0>();
(bits[3], bits[4])
};
let (
supports_h_shrink_scaling,
supports_h_stretch_scaling,
supports_v_shrink_scaling,
supports_v_stretch_scaling,
) = {
let bits = input[16].view_bits::<Lsb0>();
(bits[7], bits[6], bits[5], bits[4])
};
let preferred_v_refresh_rate_hz = input[17];
Ok(RangeLimitsDesc::CvtSupported {
limits,
enhanced_px_clk,
cvt_version,
maximum_active_pxls_per_line,
supported_aspect_ratios,
preferred_aspect_ratio,
supports_standard_cvt_blanking,
supports_reduced_cvt_blanking,
supports_h_shrink_scaling,
supports_h_stretch_scaling,
supports_v_shrink_scaling,
supports_v_stretch_scaling,
preferred_v_refresh_rate_hz,
})
}
#[tracing::instrument(skip_all)]
fn just_limits(input: &[u8; 18]) -> Result<RangeLimits, EdidError> {
let offsets = limit_offsets(input[4])?;
let get_rate = |idx| -> Result<u16, EdidError> {
let val = from_bcd(input[idx])?;
if val == 0x0 {
tracing::warn!("a min/max rate incorrectly has a value of zero! (at `input[{idx}]`)");
}
Ok(val)
};
let min_vt = {
let mut val = get_rate(5)?;
if offsets.vertical.has_min() {
val += 255;
}
val
};
let max_vt = {
let mut val = get_rate(6)?;
if offsets.vertical.has_max() {
val += 255;
}
val
};
let min_hz = {
let mut val = get_rate(7)?;
if offsets.horizontal.has_min() {
val += 255;
}
val
};
let max_hz = {
let mut val = get_rate(8)?;
if offsets.horizontal.has_max() {
val += 255;
}
val
};
let pixel_clock = from_bcd(input[9])? * 10;
Ok(RangeLimits {
min_v_rate_hz: min_vt,
max_v_rate_hz: max_vt,
min_h_rate_khz: min_hz,
max_h_rate_khz: max_hz,
offsets,
max_pixel_clock_mhz: pixel_clock,
})
}
#[tracing::instrument]
fn limit_offsets(byte: u8) -> Result<Offsets, EdidError> {
let bits = byte.view_bits::<Lsb0>();
let rev_bits = &bits[4..=7];
if !rev_bits.iter().all(|b| b == false) {
tracing::error!("Reserved display range limit offset bits were used: {rev_bits:x?}");
return Err(EdidError::DescriptorRangeLimitsUsedReservedBits { input: byte });
}
let hz_bits = (bits[3], bits[2]);
let horizontal = match hz_bits {
(false, false) => HorizontalOffset::Zero,
(true, false) => HorizontalOffset::Max255kHz_MinNotOffset,
(true, true) => HorizontalOffset::Max255kHz_Min255kHz,
_ => {
tracing::error!("Horizontal bytes were weird: {hz_bits:#?}");
return Err(EdidError::DescriptorRangeLimitsUsedReservedBits { input: byte });
}
};
let vt_bits = (bits[1], bits[0]);
let vertical = match vt_bits {
(false, false) => VerticalOffset::Zero,
(true, false) => VerticalOffset::Max255Hz_MinNotOffset,
(true, true) => VerticalOffset::Max255Hz_Min255Hz,
_ => {
tracing::error!("Horizontal bytes were weird: {hz_bits:#?}");
return Err(EdidError::DescriptorRangeLimitsUsedReservedBits { input: byte });
}
};
Ok(Offsets {
horizontal,
vertical,
})
}
#[tracing::instrument]
fn typed_bcd(input: u8) -> Result<BcdNumber<2>, EdidError> {
let bcd = BcdNumber::new(input).map_err(|e| {
tracing::error!("Failed to create BcdNumber. (err: {e:?})");
EdidError::BcdError
});
tracing::debug!("made typed bcd (from: `{input}`, to: `{bcd:?}`)");
bcd
}
#[tracing::instrument]
fn from_bcd(input: u8) -> Result<u16, EdidError> {
let val = typed_bcd(input)?.value();
tracing::debug!("converted bcd (from: `{input}`, to: `{val}`)");
Ok(val)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn _sam02e3_2c47316eff13_range_limits() {
logger();
let path = "linuxhw_edid_EDID_Digital_Samsung_SAM02E3_2C47316EFF13.input";
let input = edid_by_filename(path);
let bytes: [u8; 18] = input[0x48..=0x59].try_into().unwrap();
let _t: i32 = 0b0000_0000_0010_1000;
let got = parse(&bytes, &input).unwrap();
let expected = RangeLimitsDesc::GtfSupported {
limits: RangeLimits {
min_v_rate_hz: 56,
max_v_rate_hz: 75,
min_h_rate_khz: 30,
max_h_rate_khz: 81,
offsets: Offsets {
vertical: VerticalOffset::Zero,
horizontal: HorizontalOffset::Zero,
},
max_pixel_clock_mhz: 140,
},
};
assert_eq!(got, expected);
}
#[test]
fn lgd0555_7d17e3014129() {
logger();
let path = "bad/linuxhw_edid_EDID_Digital_LG Display_LGD0555_7D17E3014129.input";
let input = edid_by_filename(path);
let bytes: [u8; 18] = input[0x48..0x5A].try_into().unwrap();
let got = parse(&bytes, &input).unwrap();
tracing::info!("{:#?}", got);
let expected = RangeLimitsDesc::CvtSupported {
limits: RangeLimits {
min_v_rate_hz: 48,
max_v_rate_hz: 60,
min_h_rate_khz: 0,
max_h_rate_khz: 0,
offsets: Offsets {
vertical: VerticalOffset::Zero,
horizontal: HorizontalOffset::Zero,
},
max_pixel_clock_mhz: 330,
},
enhanced_px_clk: Decimal::from(328) + (Decimal::from(3) / Decimal::from(4)),
cvt_version: 10,
maximum_active_pxls_per_line: Some(160),
supported_aspect_ratios: SupportedAspectRatios {
_4x3: false,
_16x9: false,
_16x10: false,
_5x4: true,
_15x9: false,
},
preferred_aspect_ratio: PreferredAspectRatio::_4x3,
supports_standard_cvt_blanking: false,
supports_reduced_cvt_blanking: true,
supports_h_shrink_scaling: false,
supports_h_stretch_scaling: false,
supports_v_shrink_scaling: false,
supports_v_stretch_scaling: true,
preferred_v_refresh_rate_hz: 20,
};
tracing::warn!("{:#?}", expected);
assert_eq!(got, expected);
}
}