use { bstr::{BString, ByteSlice}, std::{ cell::RefCell, fmt::{Debug, Formatter}, rc::Rc, }, thiserror::Error, }; trait BitflagsExt { fn contains(self, other: Self) -> bool; } impl BitflagsExt for u8 { fn contains(self, other: Self) -> bool { self & other == other } } struct Stack(RefCell>); impl Default for Stack { fn default() -> Self { Self(Default::default()) } } impl Stack { fn push(&self, v: T) { self.0.borrow_mut().push(v); } fn pop(&self) -> Option { self.0.borrow_mut().pop() } fn to_vec(&self) -> Vec where T: Clone, { self.0.borrow().clone() } } #[derive(Copy, Clone, Debug)] pub enum ColorBitDepth { Undefined, Bits6, Bits8, Bits10, Bits12, Bits14, Bits16, Reserved, } #[derive(Copy, Clone, Debug)] pub enum DigitalVideoInterfaceStandard { Undefined, Dvi, HdmiA, HdmiB, MDDI, DisplayPort, Unknown(u8), } #[derive(Copy, Clone)] pub struct SignalLevelStandard(u8); impl Debug for SignalLevelStandard { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let s = match self.0 { 0 => "+0.7/−0.3 V", 1 => "+0.714/−0.286 V", 2 => "+1.0/−0.4 V", _ => "+0.7/0 V", }; Debug::fmt(s, f) } } #[derive(Copy, Clone, Debug)] pub enum VideoInputDefinition { Analog { signal_level_standard: SignalLevelStandard, blank_to_black_setup_or_pedestal: bool, separate_h_v_sync_supported: bool, composite_sync_on_horizontal_supported: bool, composite_sync_on_green_supported: bool, serration_on_vertical_sync_supported: bool, }, Digital { bit_depth: ColorBitDepth, video_interface: DigitalVideoInterfaceStandard, }, } #[derive(Copy, Clone, Debug)] pub struct ScreenDimensions { pub horizontal_screen_size_cm: Option, pub vertical_screen_size_cm: Option, pub landscape_aspect_ration: Option, pub portrait_aspect_ration: Option, } #[derive(Copy, Clone, Debug)] pub struct ChromaticityCoordinates { pub red_x: u16, pub red_y: u16, pub green_x: u16, pub green_y: u16, pub blue_x: u16, pub blue_y: u16, pub white_x: u16, pub white_y: u16, } #[derive(Copy, Clone, Debug)] pub struct EstablishedTimings { pub s_720x400_70: bool, pub s_720x400_88: bool, pub s_640x480_60: bool, pub s_640x480_67: bool, pub s_640x480_72: bool, pub s_640x480_75: bool, pub s_800x600_56: bool, pub s_800x600_60: bool, pub s_800x600_72: bool, pub s_800x600_75: bool, pub s_832x624_75: bool, pub s_1024x768_87: bool, pub s_1024x768_60: bool, pub s_1024x768_70: bool, pub s_1024x768_75: bool, pub s_1280x1024_75: bool, pub s_1152x870_75: bool, } #[derive(Copy, Clone, Debug)] pub enum AspectRatio { A1_1, A16_10, A4_3, A5_4, A16_9, } #[derive(Copy, Clone, Debug)] pub struct StandardTiming { pub x_resolution: u16, pub aspect_ratio: AspectRatio, pub vertical_frequency: u8, } #[derive(Copy, Clone, Debug)] pub enum AnalogSyncType { AnalogComposite, BipolarAnalogComposite, } #[derive(Copy, Clone, Debug)] pub enum SyncSignal { Analog { ty: AnalogSyncType, with_serrations: bool, sync_on_all_signals: bool, }, DigitalComposite { with_serration: bool, horizontal_sync_is_positive: bool, }, DigitalSeparate { vertical_sync_is_positive: bool, horizontal_sync_is_positive: bool, }, } #[derive(Copy, Clone)] pub enum StereoViewingSupport { None, FieldSequentialRightDuringStereoSync, FieldSequentialLeftDuringStereoSync, TwoWayInterleavedRightImageOnEvenLines, TwoWayInterleavedLeftImageOnEvenLines, FourWayInterleaved, SideBySideInterleaved, } impl Debug for StereoViewingSupport { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let msg = match *self { StereoViewingSupport::None => "none", StereoViewingSupport::FieldSequentialRightDuringStereoSync => { "field sequential, right during stereo sync" } StereoViewingSupport::FieldSequentialLeftDuringStereoSync => { "field sequential, left during stereo sync" } StereoViewingSupport::TwoWayInterleavedRightImageOnEvenLines => { "2-way interleaved, right image on even lines" } StereoViewingSupport::TwoWayInterleavedLeftImageOnEvenLines => { "2-way interleaved, left image on even lines" } StereoViewingSupport::FourWayInterleaved => "4-way interleaved", StereoViewingSupport::SideBySideInterleaved => "side-by-side interleaved", }; write!(f, "\"{}\"", msg) } } #[derive(Copy, Clone, Debug)] pub struct DisplayRangeLimitsAndAdditionalTiming { pub vertical_field_rate_min: u16, pub vertical_field_rate_max: u16, pub horizontal_field_rate_min: u16, pub horizontal_field_rate_max: u16, pub maximum_pixel_clock_mhz: u16, pub extended_timing_information: ExtendedTimingInformation, } #[derive(Copy, Clone, Debug)] pub enum AspectRatioPreference { A4_3, A16_9, A16_10, A5_4, A15_9, Unknown(u8), } #[derive(Copy, Clone, Debug)] pub enum ExtendedTimingInformation { DefaultGtf, NoTimingInformation, SecondaryGtf { start_frequency: u16, c_value: u16, m_value: u16, k_value: u8, j_value: u16, }, Cvt { cvt_major_version: u8, cvt_minor_version: u8, additional_clock_precision: u8, maximum_active_pixels_per_line: Option, ar_4_3: bool, ar_16_9: bool, ar_16_10: bool, ar_5_4: bool, ar_15_9: bool, ar_preference: AspectRatioPreference, cvt_rb_reduced_blanking_preferred: bool, cvt_standard_blanking: bool, scaling_support_horizontal_shrink: bool, scaling_support_horizontal_stretch: bool, scaling_support_vertical_shrink: bool, scaling_support_vertical_stretch: bool, preferred_vertical_refresh_rate_hz: u8, }, Unknown(u8), } #[derive(Copy, Clone, Debug, Default)] pub struct ColorPoint { pub white_point_index: u8, pub white_point_x: u16, pub white_point_y: u16, pub gamma: Option, } #[derive(Copy, Clone, Debug)] pub struct EstablishedTimings3 { pub s640x350_85: bool, pub s640x400_85: bool, pub s720x400_85: bool, pub s640x480_85: bool, pub s848x480_60: bool, pub s800x600_85: bool, pub s1024x768_85: bool, pub s1152x864_75: bool, pub s1280x768_60_rb: bool, pub s1280x768_60: bool, pub s1280x768_75: bool, pub s1280x768_85: bool, pub s1280x960_60: bool, pub s1280x960_85: bool, pub s1280x1024_60: bool, pub s1280x1024_85: bool, pub s1360x768_60: bool, pub s1440x900_60_rb: bool, pub s1440x900_60: bool, pub s1440x900_75: bool, pub s1440x900_85: bool, pub s1400x1050_60_rb: bool, pub s1400x1050_60: bool, pub s1400x1050_75: bool, pub s1400x1050_85: bool, pub s1680x1050_60_rb: bool, pub s1680x1050_60: bool, pub s1680x1050_75: bool, pub s1680x1050_85: bool, pub s1600x1200_60: bool, pub s1600x1200_65: bool, pub s1600x1200_70: bool, pub s1600x1200_75: bool, pub s1600x1200_85: bool, pub s1792x1344_60: bool, pub s1792x1344_75: bool, pub s1856x1392_60: bool, pub s1856x1392_75: bool, pub s1920x1200_60_rb: bool, pub s1920x1200_60: bool, pub s1920x1200_75: bool, pub s1920x1200_85: bool, pub s1920x1440_60: bool, pub s1920x1440_75: bool, } #[derive(Copy, Clone, Debug)] pub struct ColorManagementData { pub red_a3: u16, pub red_a2: u16, pub green_a3: u16, pub green_a2: u16, pub blue_a3: u16, pub blue_a2: u16, } #[derive(Copy, Clone, Debug)] pub enum CvtAspectRatio { A4_3, A16_9, A16_10, A15_9, } #[derive(Copy, Clone, Debug)] pub enum CvtPreferredVerticalRate { R50, R60, R75, R85, } #[derive(Copy, Clone, Debug)] pub struct Cvt3ByteCode { pub addressable_lines_per_field: u16, pub aspect_ration: CvtAspectRatio, pub preferred_vertical_rate: CvtPreferredVerticalRate, pub r50: bool, pub r60: bool, pub r75: bool, pub r85: bool, pub r60_reduced_blanking: bool, } #[derive(Copy, Clone, Debug)] pub struct DetailedTimingDescriptor { pub pixel_clock_khz: u32, pub horizontal_addressable_pixels: u16, pub horizontal_blanking_pixels: u16, pub vertical_addressable_lines: u16, pub vertical_blanking_lines: u16, pub horizontal_front_porch_pixels: u16, pub horizontal_sync_pulse_pixels: u16, pub vertical_front_porch_lines: u8, pub vertical_sync_pulse_lines: u8, pub horizontal_addressable_mm: u16, pub vertical_addressable_mm: u16, pub horizontal_left_border_pixels: u8, pub vertical_top_border_pixels: u8, pub interlaced: bool, pub stereo_viewing_support: StereoViewingSupport, pub sync: SyncSignal, } #[derive(Clone, Debug)] pub enum Descriptor { Unknown(u8), DetailedTimingDescriptor(DetailedTimingDescriptor), DisplayProductSerialNumber(String), AlphanumericDataString(String), DisplayProductName(String), DisplayRangeLimitsAndAdditionalTiming(DisplayRangeLimitsAndAdditionalTiming), EstablishedTimings3(EstablishedTimings3), ColorManagementData(ColorManagementData), StandardTimingIdentifier([Option; 6]), ColorPoint(ColorPoint, Option), Cvt3ByteCode([Cvt3ByteCode; 4]), } type EdidContext = (usize, EdidParseContext); struct EdidParser<'a> { data: &'a [u8], pos: usize, context: Rc>, saved_ctx: Vec, errors: Vec<(EdidError, Vec)>, } macro_rules! bail { ($slf:expr, $err:expr) => {{ $slf.saved_ctx = $slf.context.to_vec(); return Err($err); }}; } #[derive(Clone, Debug)] pub enum EdidParseContext { ReadingBytes(usize), BaseBlock, Descriptors, Descriptor, ChromaticityCoordinates, EstablishedTimings, StandardTimings, ScreenDimensions, Gamma, FeatureSupport, Magic, Extension, IdManufacturerName, VideoInputDefinition, } struct EdidPushedContext { stack: Rc>, } impl Drop for EdidPushedContext { fn drop(&mut self) { self.stack.pop(); } } impl<'a> EdidParser<'a> { fn push_ctx(&self, pc: EdidParseContext) -> EdidPushedContext { self.context.push((self.pos, pc)); EdidPushedContext { stack: self.context.clone(), } } fn nest(&self, data: &'a [u8]) -> Self { Self { data, pos: 0, context: self.context.clone(), saved_ctx: vec![], errors: vec![], } } fn store_error(&mut self, error: EdidError) { self.errors.push((error, self.saved_ctx.clone())); } fn is_empty(&self) -> bool { self.pos >= self.data.len() } fn read_n(&mut self) -> Result<&'a [u8; N], EdidError> { let _ctx = self.push_ctx(EdidParseContext::ReadingBytes(N)); if self.data.len() - self.pos < N { bail!(self, EdidError::UnexpectedEof); } let v = self.data[self.pos..self.pos + N].try_into().unwrap(); self.pos += N; Ok(v) } fn read_var_n(&mut self, n: usize) -> Result<&'a [u8], EdidError> { let _ctx = self.push_ctx(EdidParseContext::ReadingBytes(n)); if self.data.len() - self.pos < n { bail!(self, EdidError::UnexpectedEof); } let v = &self.data[self.pos..self.pos + n]; self.pos += n; Ok(v) } fn read_u8(&mut self) -> Result { let &[a] = self.read_n()?; Ok(a) } fn read_u16(&mut self) -> Result { let &[lo, hi] = self.read_n()?; Ok(((hi as u16) << 8) + lo as u16) } fn read_u32(&mut self) -> Result { let &[a, b, c, d] = self.read_n()?; Ok(((d as u32) << 24) + ((c as u32) << 16) + ((b as u32) << 8) + a as u32) } fn parse_magic(&mut self) -> Result<(), EdidError> { let _ctx = self.push_ctx(EdidParseContext::Magic); let magic = self.read_n::<8>()?; if magic != &[0, 255, 255, 255, 255, 255, 255, 0] { bail!(self, EdidError::InvalidMagic(magic.as_bstr().to_owned())); } Ok(()) } fn parse_id_manufacturer_name(&mut self) -> Result { let _ctx = self.push_ctx(EdidParseContext::IdManufacturerName); let name = self.read_n::<2>()?; let a = (name[0] >> 2) & 0b11111; let b = ((name[0] & 0b11) << 3) | (name[1] >> 5); let c = name[1] & 0b11111; let name = [a + b'@', b + b'@', c + b'@'].as_bstr().to_owned(); Ok(name) } fn parse_video_input_definition(&mut self) -> Result { let _ctx = self.push_ctx(EdidParseContext::VideoInputDefinition); let val = self.read_u8()?; let res = if val.contains(0x80) { VideoInputDefinition::Digital { bit_depth: match (val >> 4) & 0b111 { 0b000 => ColorBitDepth::Undefined, 0b001 => ColorBitDepth::Bits6, 0b010 => ColorBitDepth::Bits8, 0b011 => ColorBitDepth::Bits10, 0b100 => ColorBitDepth::Bits12, 0b101 => ColorBitDepth::Bits14, 0b110 => ColorBitDepth::Bits16, _ => ColorBitDepth::Reserved, }, video_interface: match val & 0b1111 { 0b0000 => DigitalVideoInterfaceStandard::Undefined, 0b0001 => DigitalVideoInterfaceStandard::Dvi, 0b0010 => DigitalVideoInterfaceStandard::HdmiA, 0b0011 => DigitalVideoInterfaceStandard::HdmiB, 0b0100 => DigitalVideoInterfaceStandard::MDDI, 0b0101 => DigitalVideoInterfaceStandard::DisplayPort, n => DigitalVideoInterfaceStandard::Unknown(n), }, } } else { VideoInputDefinition::Analog { signal_level_standard: SignalLevelStandard((val >> 5) & 0b11), blank_to_black_setup_or_pedestal: (val >> 4).contains(1), separate_h_v_sync_supported: (val >> 3).contains(1), composite_sync_on_horizontal_supported: (val >> 2).contains(1), composite_sync_on_green_supported: (val >> 1).contains(1), serration_on_vertical_sync_supported: (val >> 0).contains(1), } }; Ok(res) } fn parse_screen_dimensions(&mut self) -> Result { let _ctx = self.push_ctx(EdidParseContext::ScreenDimensions); let &[hor, vert] = self.read_n()?; let mut res = ScreenDimensions { horizontal_screen_size_cm: None, vertical_screen_size_cm: None, landscape_aspect_ration: None, portrait_aspect_ration: None, }; if hor != 0 && vert != 0 { res.horizontal_screen_size_cm = Some(hor); res.vertical_screen_size_cm = Some(vert); } else if vert != 0 { res.portrait_aspect_ration = Some(100.0 / (vert as f64 + 99.0)); } else if hor != 0 { res.landscape_aspect_ration = Some((hor as f64 + 99.0) / 100.0); } Ok(res) } fn parse_gamma(&mut self) -> Result, EdidError> { let _ctx = self.push_ctx(EdidParseContext::Gamma); let val = self.read_u8()?; if val == 0xff { Ok(None) } else { Ok(Some((val as f64 + 100.0) / 100.0)) } } fn parse_feature_support(&mut self, digital: bool) -> Result { let _ctx = self.push_ctx(EdidParseContext::FeatureSupport); let val = self.read_u8()?; Ok(FeatureSupport { standby_supported: val.contains(0x80), suspend_supported: val.contains(0x40), active_off_supported: val.contains(0x20), features: if digital { FeatureSupport2::Digital { rgb444_supported: true, ycrcb422_supported: val.contains(0x10), ycrcb444_supported: val.contains(0x08), } } else { FeatureSupport2::Analog { display_color_type: match (val >> 3) & 0b11 { 0b00 => DisplayColorType::Monochrome, 0b01 => DisplayColorType::Rgb, 0b10 => DisplayColorType::NonRgb, _ => DisplayColorType::Undefined, }, } }, srgb_is_default_color_space: val.contains(0x04), preferred_mode_is_native: val.contains(0x02), display_is_continuous_frequency: val.contains(0x01), }) } fn parse_chromaticity_coordinates(&mut self) -> Result { let _ctx = self.push_ctx(EdidParseContext::ChromaticityCoordinates); let b = self.read_n::<10>()?; let rx = ((b[0] as u16 >> 6) & 0b11) + ((b[2] as u16) << 2); let ry = ((b[0] as u16 >> 4) & 0b11) + ((b[3] as u16) << 2); let gx = ((b[0] as u16 >> 2) & 0b11) + ((b[4] as u16) << 2); let gy = ((b[0] as u16 >> 0) & 0b11) + ((b[5] as u16) << 2); let bx = ((b[1] as u16 >> 6) & 0b11) + ((b[6] as u16) << 2); let by = ((b[1] as u16 >> 4) & 0b11) + ((b[7] as u16) << 2); let wx = ((b[1] as u16 >> 2) & 0b11) + ((b[8] as u16) << 2); let wy = ((b[1] as u16 >> 0) & 0b11) + ((b[9] as u16) << 2); Ok(ChromaticityCoordinates { red_x: rx, red_y: ry, green_x: gx, green_y: gy, blue_x: bx, blue_y: by, white_x: wx, white_y: wy, }) } fn parse_established_timings(&mut self) -> Result { let _ctx = self.push_ctx(EdidParseContext::EstablishedTimings); let b = self.read_n::<3>()?; Ok(EstablishedTimings { s_720x400_70: b[0].contains(0x80), s_720x400_88: b[0].contains(0x40), s_640x480_60: b[0].contains(0x20), s_640x480_67: b[0].contains(0x10), s_640x480_72: b[0].contains(0x08), s_640x480_75: b[0].contains(0x04), s_800x600_56: b[0].contains(0x02), s_800x600_60: b[0].contains(0x01), s_800x600_72: b[0].contains(0x80), s_800x600_75: b[0].contains(0x40), s_832x624_75: b[0].contains(0x20), s_1024x768_87: b[0].contains(0x10), s_1024x768_60: b[0].contains(0x08), s_1024x768_70: b[0].contains(0x04), s_1024x768_75: b[0].contains(0x02), s_1280x1024_75: b[0].contains(0x01), s_1152x870_75: b[0].contains(0x80), }) } fn parse_standard_timing(&mut self, revision: u8, a: u8, b: u8) -> Option { if a == 0 { return None; } Some(StandardTiming { x_resolution: (a as u16 + 31) * 8, aspect_ratio: match b >> 6 { 0b00 if revision < 3 => AspectRatio::A1_1, 0b00 => AspectRatio::A16_10, 0b01 => AspectRatio::A4_3, 0b10 => AspectRatio::A5_4, _ => AspectRatio::A16_9, }, vertical_frequency: 60 + (b & 0b111111), }) } fn parse_standard_timings2( &mut self, revision: u8, b: &[u8; 18], ) -> [Option; 6] { let mut res = [None; 6]; for i in 0..6 { let x = b[5 + 2 * i]; let y = b[5 + 2 * i + 1]; res[i] = self.parse_standard_timing(revision, x, y); } res } fn parse_color_point(&mut self, b: &[u8; 18]) -> (ColorPoint, Option) { let mut res = [Default::default(); 2]; for n in 0..2 { let b = &b[5 * (n + 1)..]; res[n] = ColorPoint { white_point_index: b[0], white_point_x: ((b[2] as u16) << 2) | ((b[1] as u16) >> 2), white_point_y: ((b[3] as u16) << 2) | ((b[1] as u16) & 0b11), gamma: if b[4] == 0xff { None } else { Some((b[5] as f64 + 100.0) / 100.0) }, }; } let second = if res[1].white_point_index != 0 { Some(res[1]) } else { None }; (res[0], second) } fn parse_standard_timings( &mut self, revision: u8, ) -> Result<[Option; 8], EdidError> { let _ctx = self.push_ctx(EdidParseContext::StandardTimings); let bytes = self.read_n::<16>()?; let mut res = [None; 8]; for i in 0..8 { let a = bytes[2 * i]; let b = bytes[2 * i + 1]; if (a, b) != (1, 1) { res[i] = self.parse_standard_timing(revision, a, b); } } Ok(res) } fn parse_detailed_timing_descriptor(&self, b: &[u8; 18]) -> DetailedTimingDescriptor { let l = b[17]; DetailedTimingDescriptor { pixel_clock_khz: u16::from_le_bytes([b[0], b[1]]) as u32 * 10_000, horizontal_addressable_pixels: u16::from_le_bytes([b[2], b[4] >> 4]), horizontal_blanking_pixels: u16::from_le_bytes([b[3], b[4] & 0b1111]), vertical_addressable_lines: u16::from_le_bytes([b[5], b[7] >> 4]), vertical_blanking_lines: u16::from_le_bytes([b[6], b[7] & 0b1111]), horizontal_front_porch_pixels: u16::from_le_bytes([b[8], b[11] >> 6]), horizontal_sync_pulse_pixels: u16::from_le_bytes([b[9], (b[11] >> 4) & 0b11]), vertical_front_porch_lines: (b[10] >> 4) | ((b[11] & 0b1100) << 2), vertical_sync_pulse_lines: (b[10] & 0b1111) | ((b[11] & 0b11) << 4), horizontal_addressable_mm: u16::from_le_bytes([b[12], b[14] >> 4]), vertical_addressable_mm: u16::from_le_bytes([b[13], b[14] & 0b1111]), horizontal_left_border_pixels: b[15], vertical_top_border_pixels: b[16], interlaced: l.contains(0x80), stereo_viewing_support: match ((l >> 4) & 0b110) | (l & 0b1) { 0b010 => StereoViewingSupport::FieldSequentialRightDuringStereoSync, 0b100 => StereoViewingSupport::FieldSequentialLeftDuringStereoSync, 0b011 => StereoViewingSupport::TwoWayInterleavedRightImageOnEvenLines, 0b101 => StereoViewingSupport::TwoWayInterleavedLeftImageOnEvenLines, 0b110 => StereoViewingSupport::FourWayInterleaved, 0b111 => StereoViewingSupport::SideBySideInterleaved, _ => StereoViewingSupport::None, }, sync: if l.contains(0b10000) { if l.contains(0b01000) { SyncSignal::DigitalSeparate { vertical_sync_is_positive: l.contains(0b100), horizontal_sync_is_positive: l.contains(0b10), } } else { SyncSignal::DigitalComposite { with_serration: l.contains(0b100), horizontal_sync_is_positive: l.contains(0b10), } } } else { SyncSignal::Analog { ty: if l.contains(0b1000) { AnalogSyncType::BipolarAnalogComposite } else { AnalogSyncType::AnalogComposite }, with_serrations: l.contains(0b100), sync_on_all_signals: l.contains(0b10), } }, } } fn parse_display_range_limits_and_additional_timing( &self, b: &[u8; 18], ) -> DisplayRangeLimitsAndAdditionalTiming { let min_vert_off = b[4].contains(0b0001); let max_vert_off = min_vert_off || b[4].contains(0b0010); let min_horz_off = b[4].contains(0b0100); let max_horz_off = min_horz_off || b[4].contains(0b1000); DisplayRangeLimitsAndAdditionalTiming { vertical_field_rate_min: b[5] as u16 + if min_vert_off { 255 } else { 0 }, vertical_field_rate_max: b[6] as u16 + if max_vert_off { 255 } else { 0 }, horizontal_field_rate_min: b[7] as u16 + if min_horz_off { 255 } else { 0 }, horizontal_field_rate_max: b[8] as u16 + if max_horz_off { 255 } else { 0 }, maximum_pixel_clock_mhz: b[9] as u16 * 10, extended_timing_information: match b[10] { 0x0 => ExtendedTimingInformation::DefaultGtf, 0x1 => ExtendedTimingInformation::NoTimingInformation, 0x2 => ExtendedTimingInformation::SecondaryGtf { start_frequency: b[12] as u16, c_value: b[13] as u16, m_value: u16::from_le_bytes([b[14], b[15]]), k_value: b[16], j_value: b[17] as u16, }, 0x4 => ExtendedTimingInformation::Cvt { cvt_major_version: b[11] >> 4, cvt_minor_version: b[11] & 0b1111, additional_clock_precision: b[12] >> 2, maximum_active_pixels_per_line: if b[13] == 0 { None } else { Some((((b[12] as u16 & 0b11) << 8) | b[13] as u16) * 8) }, ar_4_3: b[14].contains(0x80), ar_16_9: b[14].contains(0x40), ar_16_10: b[14].contains(0x20), ar_5_4: b[14].contains(0x10), ar_15_9: b[14].contains(0x08), ar_preference: match b[15] >> 5 { 0b000 => AspectRatioPreference::A4_3, 0b001 => AspectRatioPreference::A16_9, 0b010 => AspectRatioPreference::A16_10, 0b011 => AspectRatioPreference::A5_4, 0b100 => AspectRatioPreference::A15_9, n => AspectRatioPreference::Unknown(n), }, cvt_rb_reduced_blanking_preferred: b[15].contains(0b10000), cvt_standard_blanking: b[15].contains(0b1000), scaling_support_horizontal_shrink: b[16].contains(0x80), scaling_support_horizontal_stretch: b[16].contains(0x40), scaling_support_vertical_shrink: b[16].contains(0x20), scaling_support_vertical_stretch: b[16].contains(0x10), preferred_vertical_refresh_rate_hz: b[17], }, n => ExtendedTimingInformation::Unknown(n), }, } } fn parse_established_timings3(&self, b: &[u8; 18]) -> EstablishedTimings3 { EstablishedTimings3 { s640x350_85: b[6].contains(0x80), s640x400_85: b[6].contains(0x40), s720x400_85: b[6].contains(0x20), s640x480_85: b[6].contains(0x10), s848x480_60: b[6].contains(0x08), s800x600_85: b[6].contains(0x04), s1024x768_85: b[6].contains(0x02), s1152x864_75: b[6].contains(0x01), s1280x768_60_rb: b[7].contains(0x80), s1280x768_60: b[7].contains(0x40), s1280x768_75: b[7].contains(0x20), s1280x768_85: b[7].contains(0x10), s1280x960_60: b[7].contains(0x08), s1280x960_85: b[7].contains(0x04), s1280x1024_60: b[7].contains(0x02), s1280x1024_85: b[7].contains(0x01), s1360x768_60: b[8].contains(0x80), s1440x900_60_rb: b[8].contains(0x40), s1440x900_60: b[8].contains(0x20), s1440x900_75: b[8].contains(0x10), s1440x900_85: b[8].contains(0x08), s1400x1050_60_rb: b[8].contains(0x04), s1400x1050_60: b[8].contains(0x02), s1400x1050_75: b[8].contains(0x01), s1400x1050_85: b[9].contains(0x80), s1680x1050_60_rb: b[9].contains(0x40), s1680x1050_60: b[9].contains(0x20), s1680x1050_75: b[9].contains(0x10), s1680x1050_85: b[9].contains(0x08), s1600x1200_60: b[9].contains(0x04), s1600x1200_65: b[9].contains(0x02), s1600x1200_70: b[9].contains(0x01), s1600x1200_75: b[10].contains(0x80), s1600x1200_85: b[10].contains(0x40), s1792x1344_60: b[10].contains(0x20), s1792x1344_75: b[10].contains(0x10), s1856x1392_60: b[10].contains(0x08), s1856x1392_75: b[10].contains(0x04), s1920x1200_60_rb: b[10].contains(0x02), s1920x1200_60: b[10].contains(0x01), s1920x1200_75: b[11].contains(0x80), s1920x1200_85: b[11].contains(0x40), s1920x1440_60: b[11].contains(0x20), s1920x1440_75: b[11].contains(0x10), } } fn parse_color_management_data(&self, b: &[u8; 18]) -> ColorManagementData { ColorManagementData { red_a3: u16::from_le_bytes([b[6], b[7]]), red_a2: u16::from_le_bytes([b[8], b[9]]), green_a3: u16::from_le_bytes([b[10], b[11]]), green_a2: u16::from_le_bytes([b[12], b[13]]), blue_a3: u16::from_le_bytes([b[14], b[15]]), blue_a2: u16::from_le_bytes([b[16], b[17]]), } } fn parse_cvt3_byte_codes(&self, b: &[u8; 18]) -> [Cvt3ByteCode; 4] { let parse = |n: usize| { let b = &b[6 + 3 * n..]; Cvt3ByteCode { addressable_lines_per_field: u16::from_le_bytes([b[0], b[1] >> 4]), aspect_ration: match (b[1] >> 2) & 0b11 { 0 => CvtAspectRatio::A4_3, 1 => CvtAspectRatio::A16_9, 2 => CvtAspectRatio::A16_10, _ => CvtAspectRatio::A15_9, }, preferred_vertical_rate: match (b[2] >> 5) & 0b11 { 0 => CvtPreferredVerticalRate::R50, 1 => CvtPreferredVerticalRate::R60, 2 => CvtPreferredVerticalRate::R75, _ => CvtPreferredVerticalRate::R85, }, r50: b[2].contains(0b10000), r60: b[2].contains(0b01000), r75: b[2].contains(0b00100), r85: b[2].contains(0b00010), r60_reduced_blanking: b[2].contains(0b00001), } }; [parse(0), parse(1), parse(2), parse(3)] } fn parse_descriptor(&mut self, revision: u8) -> Result, EdidError> { let _ctx = self.push_ctx(EdidParseContext::Descriptor); let b = self.read_n::<18>()?; let str = || { let mut s = &b[5..]; if let Some(n) = s.find_byte(b'\n') { s = &s[..n]; }; let mut res = String::new(); for &b in s { res.push_str(CP437[b as usize]); } res }; let res = if (b[0], b[1]) == (0, 0) { match b[3] { 0xff => Descriptor::DisplayProductSerialNumber(str()), 0xfe => Descriptor::AlphanumericDataString(str()), 0xfd => Descriptor::DisplayRangeLimitsAndAdditionalTiming( self.parse_display_range_limits_and_additional_timing(b), ), 0xfc => Descriptor::DisplayProductName(str()), 0xfb => { let (first, second) = self.parse_color_point(b); Descriptor::ColorPoint(first, second) } 0xfa => { Descriptor::StandardTimingIdentifier(self.parse_standard_timings2(revision, b)) } 0xf9 => Descriptor::ColorManagementData(self.parse_color_management_data(b)), 0xf8 => Descriptor::Cvt3ByteCode(self.parse_cvt3_byte_codes(b)), 0xf7 => Descriptor::EstablishedTimings3(self.parse_established_timings3(b)), 0x10 => return Ok(None), n => Descriptor::Unknown(n), } } else { Descriptor::DetailedTimingDescriptor(self.parse_detailed_timing_descriptor(b)) }; Ok(Some(res)) } fn parse_descriptors(&mut self, revision: u8) -> Result<[Option; 4], EdidError> { let _ctx = self.push_ctx(EdidParseContext::Descriptors); let mut res = [None, None, None, None]; for res in &mut res { *res = self.parse_descriptor(revision)?; } Ok(res) } fn parse_base_block(&mut self) -> Result { let _ctx = self.push_ctx(EdidParseContext::BaseBlock); self.parse_magic()?; let id_manufacturer_name = self.parse_id_manufacturer_name()?; let id_product_code = self.read_u16()?; let id_serial_number = self.read_u32()?; let mut week_of_manufacture = None; let mut model_year = None; let mut year_of_manufacture = None; { let &[a, b] = self.read_n()?; if matches!(a, 1..=0x36) { week_of_manufacture = Some(a); } let year = b as u16 + 1990; if a == 0xff { model_year = Some(year); } else { year_of_manufacture = Some(year); } } let &[edid_version, edid_revision] = self.read_n()?; let video_input_definition = self.parse_video_input_definition()?; let is_digital = matches!(video_input_definition, VideoInputDefinition::Digital { .. }); let screen_dimensions = self.parse_screen_dimensions()?; let gamma = self.parse_gamma()?; let feature_support = self.parse_feature_support(is_digital)?; let chromaticity_coordinates = self.parse_chromaticity_coordinates()?; let established_timings = self.parse_established_timings()?; let standard_timings = self.parse_standard_timings(edid_revision)?; let descriptors = self.parse_descriptors(edid_revision)?; let num_extensions = self.read_u8()?; let _checksum = self.read_u8()?; Ok(EdidBaseBlock { id_manufacturer_name, id_product_code, id_serial_number, week_of_manufacture, model_year, year_of_manufacture, edid_version, edid_revision, video_input_definition, screen_dimensions, gamma, feature_support, chromaticity_coordinates, established_timings, standard_timings, descriptors, num_extensions, }) } fn parse_cta_amd_vendor_data_block(&mut self) -> Result { let _ = self.read_n::<2>()?; Ok(CtaDataBlock::VendorAmd(CtaAmdVendorDataBlock { minimum_refresh_hz: self.read_u8()?, maximum_refresh_hz: self.read_u8()?, })) } fn parse_cta_vendor_data_block(&mut self) -> Result { match self.read_n::<3>()? { [0x1A, 0x00, 0x00] => self.parse_cta_amd_vendor_data_block(), _ => Ok(CtaDataBlock::Unknown), } } fn parse_cta_colorimetry_data_block(&mut self) -> Result { let [lo, hi] = *self.read_n::<2>()?; Ok(CtaDataBlock::Colorimetry(CtaColorimetryDataBlock { bt2020_rgb: lo.contains(0x80), bt2020_ycc: lo.contains(0x40), bt2020_cycc: lo.contains(0x20), op_rgb: lo.contains(0x10), op_ycc_601601: lo.contains(0x08), s_ycc_601: lo.contains(0x04), xv_ycc_709: lo.contains(0x02), xv_ycc_601: lo.contains(0x01), dci_p3: hi.contains(0x80), })) } fn parse_cta_hdr_static_metadata_data_block(&mut self) -> Result { let et = self.read_u8()?; let _ = self.read_u8()?; let mut read_luminance = |min: bool| { let v = self.read_u8().unwrap_or_default(); if v == 0 { None } else if min { Some((v as f64 / 255.0).powi(2) / 100.0) } else { Some(50.0 * 2.0f64.powf(v as f64 / 32.0)) } }; Ok(CtaDataBlock::StaticHdrMetadata( CtaStaticHdrMetadataDataBlock { traditional_gamma_sdr_luminance: et.contains(0x01), traditional_gamma_hdr_luminance: et.contains(0x02), smpte_st_2084: et.contains(0x04), hlg: et.contains(0x08), max_luminance: read_luminance(false), max_frame_average_luminance: read_luminance(false), min_luminance: read_luminance(true), }, )) } fn parse_cta_extended_data_block(&mut self) -> Result { match self.read_u8()? { 0x5 => self.parse_cta_colorimetry_data_block(), 0x6 => self.parse_cta_hdr_static_metadata_data_block(), _ => Ok(CtaDataBlock::Unknown), } } fn parse_cta_data_block(&mut self, tag: u8) -> Result { match tag { 0x3 => self.parse_cta_vendor_data_block(), 0x7 => self.parse_cta_extended_data_block(), _ => Ok(CtaDataBlock::Unknown), } } fn parse_cta_extension_v3(&mut self) -> Result { let detailed_timing_descriptors_offset = self.read_u8()? as usize; let _ = self.read_u8()?; let mut data_blocks = vec![]; while self.pos < detailed_timing_descriptors_offset { let b1 = self.read_u8()?; let data = self.read_var_n(b1 as usize & 0x1f)?; let mut parser = self.nest(data); match parser.parse_cta_data_block(b1 >> 5) { Ok(d) => data_blocks.push(d), Err(e) => { self.saved_ctx = parser.saved_ctx; self.store_error(e); } } } Ok(EdidExtension::CtaV3(CtaExtensionV3 { data_blocks })) } fn parse_cta_extension(&mut self) -> Result { // https://web.archive.org/web/20171201033424/https://standards.cta.tech/kwspub/published_docs/CTA-861-G_FINAL_revised_2017.pdf match self.read_u8()? { 0x3 => self.parse_cta_extension_v3(), _ => Ok(EdidExtension::Unknown), } } fn parse_extension_impl(&mut self) -> Result { match self.read_u8()? { 0x2 => self.parse_cta_extension(), _ => Ok(EdidExtension::Unknown), } } fn parse_extension(&mut self) -> Result { let _ctx = self.push_ctx(EdidParseContext::Extension); let data = self.read_n::<128>()?; let mut parser = self.nest(data); let res = parser.parse_extension_impl(); if res.is_err() { self.saved_ctx = parser.saved_ctx; } res } fn parse(&mut self) -> Result { let bb = self.parse_base_block()?; let mut exts = vec![]; while !self.is_empty() { match self.parse_extension() { Ok(e) => exts.push(e), Err(e) => self.store_error(e), } } Ok(EdidFile { base_block: bb, extension_blocks: exts, }) } } #[derive(Debug)] pub enum DisplayColorType { Monochrome, Rgb, NonRgb, Undefined, } #[derive(Debug)] pub enum FeatureSupport2 { Analog { display_color_type: DisplayColorType, }, Digital { rgb444_supported: bool, ycrcb444_supported: bool, ycrcb422_supported: bool, }, } #[derive(Debug)] pub struct FeatureSupport { pub standby_supported: bool, pub suspend_supported: bool, pub active_off_supported: bool, pub features: FeatureSupport2, pub srgb_is_default_color_space: bool, pub preferred_mode_is_native: bool, pub display_is_continuous_frequency: bool, } #[derive(Debug)] pub struct EdidBaseBlock { pub id_manufacturer_name: BString, pub id_product_code: u16, pub id_serial_number: u32, pub week_of_manufacture: Option, pub model_year: Option, pub year_of_manufacture: Option, pub edid_version: u8, pub edid_revision: u8, pub video_input_definition: VideoInputDefinition, pub screen_dimensions: ScreenDimensions, pub gamma: Option, pub feature_support: FeatureSupport, pub chromaticity_coordinates: ChromaticityCoordinates, pub established_timings: EstablishedTimings, pub standard_timings: [Option; 8], pub descriptors: [Option; 4], pub num_extensions: u8, } #[derive(Debug)] pub enum EdidExtension { Unknown, CtaV3(CtaExtensionV3), } #[derive(Debug)] pub struct CtaExtensionV3 { pub data_blocks: Vec, } #[derive(Debug)] pub enum CtaDataBlock { Unknown, VendorAmd(CtaAmdVendorDataBlock), Colorimetry(CtaColorimetryDataBlock), StaticHdrMetadata(CtaStaticHdrMetadataDataBlock), } #[derive(Debug)] pub struct CtaAmdVendorDataBlock { pub minimum_refresh_hz: u8, pub maximum_refresh_hz: u8, } #[derive(Copy, Clone, Debug)] pub struct CtaColorimetryDataBlock { pub bt2020_rgb: bool, pub bt2020_ycc: bool, pub bt2020_cycc: bool, pub op_rgb: bool, pub op_ycc_601601: bool, pub s_ycc_601: bool, pub xv_ycc_709: bool, pub xv_ycc_601: bool, pub dci_p3: bool, } #[derive(Copy, Clone, Debug)] pub struct CtaStaticHdrMetadataDataBlock { pub traditional_gamma_sdr_luminance: bool, pub traditional_gamma_hdr_luminance: bool, pub smpte_st_2084: bool, pub hlg: bool, pub max_luminance: Option, pub max_frame_average_luminance: Option, pub min_luminance: Option, } #[derive(Debug)] pub struct EdidFile { pub base_block: EdidBaseBlock, pub extension_blocks: Vec, } #[derive(Debug, Error)] pub enum EdidError { #[error("Unexpected end-of-file")] UnexpectedEof, #[error("Invalid magic header")] InvalidMagic(BString), } pub fn parse(data: &[u8]) -> Result { let mut parser = EdidParser { data, pos: 0, context: Rc::new(Default::default()), saved_ctx: vec![], errors: vec![], }; parser.parse() } const CP437: &[&str] = &[ "\u{0}", "☺", "☻", "♥", "♦", "♣", "♠", "•", "◘", "○", "◙", "♂", "♀", "♪", "♫", "☼", "►", "◄", "↕", "‼", "¶", "§", "▬", "↨", "↑", "↓", "→", "←", "∟", "↔", "▲", "▼", " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~", "⌂", "Ç", "ü", "é", "â", "ä", "à", "å", "ç", "ê", "ë", "è", "ï", "î", "ì", "Ä", "Å", "É", "æ", "Æ", "ô", "ö", "ò", "û", "ù", "ÿ", "Ö", "Ü", "¢", "£", "¥", "₧", "ƒ", "á", "í", "ó", "ú", "ñ", "Ñ", "ª", "º", "¿", "⌐", "¬", "½", "¼", "¡", "«", "»", "░", "▒", "▓", "│", "┤", "╡", "╢", "╖", "╕", "╣", "║", "╗", "╝", "╜", "╛", "┐", "└", "┴", "┬", "├", "─", "┼", "╞", "╟", "╚", "╔", "╩", "╦", "╠", "═", "╬", "╧", "╨", "╤", "╥", "╙", "╘", "╒", "╓", "╫", "╪", "┘", "┌", "█", "▄", "▌", "▐", "▀", "α", "ß", "Γ", "π", "Σ", "σ", "µ", "τ", "Φ", "Θ", "Ω", "δ", "∞", "φ", "ε", "∩", "≡", "±", "≥", "≤", "⌠", "⌡", "÷", "≈", "°", "∙", "·", "√", "ⁿ", "²", "■", "\u{a0}", ];