use crate::model::capabilities::VideoMode;
use crate::model::prelude::Vec;
pub use display_types::cea861::colorimetry::{ColorimetryBlock, ColorimetryFlags};
pub use display_types::cea861::hdmi_forum::{
HdmiDscMaxSlices, HdmiForumDsc, HdmiForumFrl, HdmiForumSinkCap,
};
pub use display_types::cea861::hdr::{HdrDynamicMetadataDescriptor, HdrEotf, HdrStaticMetadata};
pub use display_types::cea861::misc::{InfoFrameDescriptor, VendorSpecificBlock, infoframe_type};
pub use display_types::cea861::speaker::{
RoomConfigurationBlock, SpeakerAllocation, SpeakerAllocationFlags, SpeakerAllocationFlags2,
SpeakerAllocationFlags3, SpeakerLocationEntry,
};
pub use display_types::cea861::vesa_dddb::VesaDisplayDeviceBlock;
pub use display_types::cea861::vesa_transfer::{DtcPointEncoding, VesaTransferCharacteristic};
pub use display_types::cea861::video_capability::{VideoCapability, VideoCapabilityFlags};
pub use display_types::cea861::vtdb::{
T7VtdbBlock, T8VtdbBlock, T10VtdbBlock, T10VtdbEntry, VtbExtBlock,
};
pub(super) const EXT_TAG_VSVDB: u8 = 0x01;
pub(super) const EXT_TAG_VESA_DDDB: u8 = 0x02;
pub(super) const EXT_TAG_VTB_EXT: u8 = 0x03;
pub(super) const EXT_TAG_VIDEO_CAPABILITY: u8 = 0x00;
pub(super) const EXT_TAG_VSADB: u8 = 0x11;
pub(super) const EXT_TAG_T7VTDB: u8 = 0x22;
pub(super) const EXT_TAG_T8VTDB: u8 = 0x23;
pub(super) const EXT_TAG_T10VTDB: u8 = 0x2A;
pub(super) const EXT_TAG_HF_EEODB: u8 = 0x78;
pub(super) const EXT_TAG_HF_SCDB: u8 = 0x79;
pub(super) const EXT_TAG_COLORIMETRY: u8 = 0x05;
pub(super) const EXT_TAG_HDR_STATIC_METADATA: u8 = 0x06;
pub(super) const EXT_TAG_HDR_DYNAMIC_METADATA: u8 = 0x07;
pub(super) const EXT_TAG_VIDEO_FORMAT_PREFERENCE: u8 = 0x0D;
pub(super) const EXT_TAG_Y420_VIDEO: u8 = 0x0E;
pub(super) const EXT_TAG_Y420_CAPABILITY_MAP: u8 = 0x0F;
pub(super) const EXT_TAG_HDMI_AUDIO: u8 = 0x12;
pub(super) const EXT_TAG_ROOM_CONFIGURATION: u8 = 0x13;
pub(super) const EXT_TAG_SPEAKER_LOCATION: u8 = 0x14;
pub(super) const EXT_TAG_INFOFRAME: u8 = 0x20;
#[cfg(test)]
pub(super) const IMPLEMENTED_EXTENDED_TAGS: &[u8] = &[
EXT_TAG_VIDEO_CAPABILITY, EXT_TAG_VSVDB, EXT_TAG_VESA_DDDB, EXT_TAG_VTB_EXT, EXT_TAG_COLORIMETRY, EXT_TAG_HDR_STATIC_METADATA, EXT_TAG_HDR_DYNAMIC_METADATA, EXT_TAG_VIDEO_FORMAT_PREFERENCE, EXT_TAG_Y420_VIDEO, EXT_TAG_Y420_CAPABILITY_MAP, EXT_TAG_VSADB, EXT_TAG_HDMI_AUDIO, EXT_TAG_ROOM_CONFIGURATION, EXT_TAG_SPEAKER_LOCATION, EXT_TAG_INFOFRAME, EXT_TAG_T7VTDB, EXT_TAG_T8VTDB, EXT_TAG_T10VTDB, EXT_TAG_HF_EEODB, EXT_TAG_HF_SCDB, ];
#[cfg(test)]
pub(super) const RESERVED_EXTENDED_TAG_RANGES: &[(u8, u8)] = &[
(0x04, 0x04), (0x08, 0x0C), (0x10, 0x10), (0x15, 0x1F), (0x21, 0x21), (0x24, 0x29), (0x2B, 0x77), (0x7A, 0xFF), ];
pub(super) fn parse_video_capability(block_data: &[u8]) -> Option<VideoCapability> {
let b = *block_data.get(1)?;
Some(VideoCapability::new(
VideoCapabilityFlags::from_bits_truncate(b & 0xC0),
(b >> 4) & 0x03,
(b >> 2) & 0x03,
b & 0x03,
))
}
pub(super) fn parse_colorimetry(block_data: &[u8]) -> Option<ColorimetryBlock> {
let colorimetry = ColorimetryFlags::from_bits_truncate(*block_data.get(1)?);
let metadata_profiles = block_data.get(2).map_or(0, |&b| b & 0x0F);
Some(ColorimetryBlock::new(colorimetry, metadata_profiles))
}
fn decode_luminance(raw: u8) -> f32 {
50.0 * 2f32.powf(raw as f32 / 32.0)
}
pub(super) fn parse_hdr_static_metadata(block_data: &[u8]) -> Option<HdrStaticMetadata> {
let eotf = HdrEotf::from_bits_truncate(*block_data.get(1)?);
let static_metadata_descriptors = *block_data.get(2).unwrap_or(&0);
let max_luminance = block_data.get(3).map(|&b| decode_luminance(b));
let max_fall = block_data.get(4).map(|&b| decode_luminance(b));
let min_luminance = block_data
.get(5)
.and_then(|&b| max_luminance.map(|max| max * (b as f32 / 255.0).powi(2) / 100.0));
Some(HdrStaticMetadata::new(
eotf,
static_metadata_descriptors,
max_luminance,
max_fall,
min_luminance,
))
}
pub(super) fn parse_vesa_transfer_characteristic(
block_data: &[u8],
) -> Option<VesaTransferCharacteristic> {
let first = *block_data.first()?;
let encoding = match (first >> 6) & 0x03 {
0x00 => DtcPointEncoding::Bits8,
0x01 => DtcPointEncoding::Bits10,
0x02 => DtcPointEncoding::Bits12,
_ => return None, };
let data = &block_data[1..];
let points = match encoding {
DtcPointEncoding::Bits8 => data.iter().map(|&b| b as f32 / 255.0).collect(),
DtcPointEncoding::Bits10 => {
let mut pts = Vec::new();
let mut i = 0;
while i + 5 <= data.len() {
let [b0, b1, b2, b3, b4] = [
data[i] as u16,
data[i + 1] as u16,
data[i + 2] as u16,
data[i + 3] as u16,
data[i + 4] as u16,
];
pts.push(((b0 << 2) | (b1 >> 6)) as f32 / 1023.0);
pts.push((((b1 & 0x3F) << 4) | (b2 >> 4)) as f32 / 1023.0);
pts.push((((b2 & 0x0F) << 6) | (b3 >> 2)) as f32 / 1023.0);
pts.push((((b3 & 0x03) << 8) | b4) as f32 / 1023.0);
i += 5;
}
pts
}
DtcPointEncoding::Bits12 => {
let mut pts = Vec::new();
let mut i = 0;
while i + 3 <= data.len() {
let [b0, b1, b2] = [data[i] as u16, data[i + 1] as u16, data[i + 2] as u16];
pts.push(((b0 << 4) | (b1 >> 4)) as f32 / 4095.0);
pts.push((((b1 & 0x0F) << 8) | b2) as f32 / 4095.0);
i += 3;
}
pts
}
_ => return None,
};
Some(VesaTransferCharacteristic::new(encoding, points))
}
pub(super) fn parse_speaker_allocation(block_data: &[u8]) -> Option<SpeakerAllocation> {
let channels = SpeakerAllocationFlags::from_bits_truncate(*block_data.first()?);
let channels_2 =
SpeakerAllocationFlags2::from_bits_truncate(block_data.get(1).copied().unwrap_or(0));
let channels_3 =
SpeakerAllocationFlags3::from_bits_truncate(block_data.get(2).copied().unwrap_or(0));
Some(SpeakerAllocation::new(channels, channels_2, channels_3))
}
pub(super) fn parse_hdr_dynamic_metadata(block_data: &[u8]) -> Vec<HdrDynamicMetadataDescriptor> {
let mut out = Vec::new();
if let Some(&b) = block_data.get(1) {
out.push(HdrDynamicMetadataDescriptor::new(b & 0x3F, (b >> 6) & 0x03));
}
out
}
pub(super) fn parse_video_format_preferences(block_data: &[u8]) -> Vec<u8> {
block_data[1..].to_vec()
}
pub(super) fn parse_y420_vdb(block_data: &[u8]) -> Vec<u8> {
let svds = &block_data[1..];
let mut out = Vec::new();
let mut j = 0;
while j < svds.len() {
let vic_low = svds[j] & 0x7F;
let vic = if vic_low == 0 {
j += 1;
match svds.get(j).copied() {
Some(0) | None => {
j += 1;
continue;
}
Some(v) => v,
}
} else {
vic_low
};
j += 1;
out.push(vic);
}
out
}
pub(super) fn parse_y420_capability_map(block_data: &[u8]) -> Vec<u8> {
block_data[1..].to_vec()
}
pub(super) fn parse_room_configuration(block_data: &[u8]) -> Option<RoomConfigurationBlock> {
let b = *block_data.get(1)?;
Some(RoomConfigurationBlock::new(b & 0x1F, b & 0x40 != 0))
}
pub(super) fn parse_speaker_location(block_data: &[u8]) -> Vec<SpeakerLocationEntry> {
block_data[1..]
.chunks_exact(2)
.map(|c| SpeakerLocationEntry::new(c[0], c[1]))
.collect()
}
pub(super) fn parse_infoframe_db(block_data: &[u8]) -> Vec<InfoFrameDescriptor> {
let mut out = Vec::new();
let payload = &block_data[1..];
let mut i = 0;
while i < payload.len() {
let b = payload[i];
let type_code = b & 0x1F;
let extra = ((b >> 5) & 0x07) as usize;
i += 1;
let vendor_oui = if type_code == infoframe_type::VENDOR_SPECIFIC && extra >= 3 {
let oui = ((payload.get(i).copied().unwrap_or(0) as u32) << 16)
| ((payload.get(i + 1).copied().unwrap_or(0) as u32) << 8)
| (payload.get(i + 2).copied().unwrap_or(0) as u32);
Some(oui)
} else {
None
};
out.push(InfoFrameDescriptor::new(type_code, vendor_oui));
i += extra.min(payload.len().saturating_sub(i));
}
out
}
pub(super) fn parse_vendor_specific_block(block_data: &[u8]) -> Option<VendorSpecificBlock> {
if block_data.len() < 3 {
return None;
}
let oui =
((block_data[2] as u32) << 16) | ((block_data[1] as u32) << 8) | (block_data[0] as u32);
let payload = block_data[3..].to_vec();
Some(VendorSpecificBlock::new(oui, payload))
}
pub(super) fn parse_t7vtdb(block_data: &[u8]) -> Option<T7VtdbBlock> {
if block_data.len() < 22 {
return None;
}
let version = block_data[0] & 0x07;
let y420 = (block_data[1] >> 6) & 1 != 0;
let pixel_clock_khz =
(block_data[2] as u32) | ((block_data[3] as u32) << 8) | ((block_data[4] as u32) << 16);
if pixel_clock_khz == 0 {
return None;
}
let interlaced = (block_data[5] >> 4) & 1 != 0;
let hactive = u16::from_le_bytes([block_data[6], block_data[7]]);
let hblank = u16::from_le_bytes([block_data[8], block_data[9]]);
let vactive = u16::from_le_bytes([block_data[14], block_data[15]]);
let vblank = u16::from_le_bytes([block_data[16], block_data[17]]);
if hactive == 0 || vactive == 0 || hblank == 0 || vblank == 0 {
return None;
}
let h_total = hactive as u64 + hblank as u64;
let v_total = vactive as u64 + vblank as u64;
let refresh_hz = (pixel_clock_khz as u64 * 1000) / (h_total * v_total);
let refresh_rate = u8::try_from(refresh_hz).ok()?;
let mode = VideoMode::new(hactive, vactive, refresh_rate, interlaced);
Some(T7VtdbBlock::new(version, mode, y420))
}
use display_types::cea861::dmt_to_mode;
pub(super) fn parse_t8vtdb(block_data: &[u8]) -> Option<T8VtdbBlock> {
let header = *block_data.first()?;
let code_type = (header >> 6) & 0x03;
if code_type != 0x00 {
return None; }
let y420 = (header >> 5) & 1 != 0;
let tcs = (header >> 3) & 1 != 0; let version = header & 0x07;
let payload = &block_data[1..];
let mut codes: Vec<u16> = Vec::new();
let mut timings: Vec<VideoMode> = Vec::new();
if tcs {
let mut i = 0;
while i + 2 <= payload.len() {
let code = u16::from_le_bytes([payload[i], payload[i + 1]]);
codes.push(code);
if let Some(mode) = dmt_to_mode(code) {
timings.push(mode);
}
i += 2;
}
} else {
for &byte in payload {
let code = byte as u16;
codes.push(code);
if let Some(mode) = dmt_to_mode(code) {
timings.push(mode);
}
}
}
Some(T8VtdbBlock::new(version, y420, codes, timings))
}
pub(super) fn parse_t10vtdb(block_data: &[u8]) -> Option<T10VtdbBlock> {
let rev = *block_data.first()?;
let m = (rev >> 4) & 0x07;
if m > 2 {
return None; }
let sz = 6 + m as usize;
let payload = &block_data[1..];
let mut entries = Vec::new();
let mut i = 0;
while i + sz <= payload.len() {
let d = &payload[i..i + sz];
let flags = d[0];
let y420 = (flags >> 7) & 1 != 0;
let width = u16::from_le_bytes([d[1], d[2]]).saturating_add(1);
let height = u16::from_le_bytes([d[3], d[4]]).saturating_add(1);
let refresh_lsb = d[5] as u16;
let refresh_hz = if m >= 1 {
let msb = (d[6] & 0x03) as u16;
(refresh_lsb | (msb << 8)) + 1
} else {
refresh_lsb + 1
};
entries.push(T10VtdbEntry::new(width, height, refresh_hz, y420));
i += sz;
}
Some(T10VtdbBlock::new(entries))
}
pub(super) fn parse_hf_eeodb(block_data: &[u8]) -> Option<u8> {
let count = *block_data.first()?;
if count == 0 {
return None;
}
Some(count)
}
pub(super) fn parse_hdmi_scds(scds: &[u8]) -> Option<HdmiForumSinkCap> {
if scds.len() < 4 {
return None;
}
let version = scds[0];
let max_tmds_rate_mhz = (scds[1] as u16) * 5;
let scdc = scds[2];
let scdc_present = (scdc >> 7) & 1 != 0;
let rr_capable = (scdc >> 6) & 1 != 0;
let cable_status = (scdc >> 5) & 1 != 0;
let ccbpci = (scdc >> 4) & 1 != 0;
let lte_340mcsc_scramble = (scdc >> 3) & 1 != 0;
let independent_view_3d = (scdc >> 2) & 1 != 0;
let dual_view_3d = (scdc >> 1) & 1 != 0;
let osd_disparity_3d = scdc & 1 != 0;
let frl_dc = scds[3];
let max_frl_rate = HdmiForumFrl::from_raw((frl_dc >> 4) & 0x0F);
let uhd_vic = (frl_dc >> 3) & 1 != 0;
let dc_48bit_420 = (frl_dc >> 2) & 1 != 0;
let dc_36bit_420 = (frl_dc >> 1) & 1 != 0;
let dc_30bit_420 = frl_dc & 1 != 0;
let (
fapa_end_extended,
qms,
m_delta,
fva,
allm,
fapa_start_location,
neg_mvrr,
vrr_min_hz,
vrr_max_hz,
) = if let Some(&ext) = scds.get(4) {
let fapa_end_extended = (ext >> 7) & 1 != 0;
let qms = (ext >> 6) & 1 != 0;
let m_delta = (ext >> 5) & 1 != 0;
let neg_mvrr = (ext >> 3) & 1 != 0;
let fva = (ext >> 2) & 1 != 0;
let allm = (ext >> 1) & 1 != 0;
let fapa_start_location = ext & 1 != 0;
let vrr = scds.get(5).and_then(|&b5| {
scds.get(6).map(|&b6| {
let vrr_min = b5 & 0x3F;
let vrr_max = ((b5 >> 6) as u16) << 8 | b6 as u16;
(vrr_min, vrr_max)
})
});
let (vrr_min_hz, vrr_max_hz) = match vrr {
Some((min, max)) => (Some(min), Some(max)),
None => (None, None),
};
(
fapa_end_extended,
qms,
m_delta,
fva,
allm,
fapa_start_location,
neg_mvrr,
vrr_min_hz,
vrr_max_hz,
)
} else {
(false, false, false, false, false, false, false, None, None)
};
let dsc = scds.get(7).and_then(|&dsc_flags| {
let dsc_frl_slices = *scds.get(8)?;
let chunk_raw = scds.get(9).map_or(0, |&b| b & 0x3F);
let max_chunk_bytes = if chunk_raw == 0 {
0
} else {
1024 * (1 + chunk_raw as u32)
};
Some(HdmiForumDsc::new(
(dsc_flags >> 7) & 1 != 0,
(dsc_flags >> 6) & 1 != 0,
(dsc_flags >> 5) & 1 != 0,
(dsc_flags >> 4) & 1 != 0,
(dsc_flags >> 3) & 1 != 0,
(dsc_flags >> 1) & 1 != 0,
dsc_flags & 1 != 0,
HdmiForumFrl::from_raw((dsc_frl_slices >> 4) & 0x0F),
HdmiDscMaxSlices::from_raw(dsc_frl_slices & 0x0F),
max_chunk_bytes,
))
});
Some(HdmiForumSinkCap::new(
version,
max_tmds_rate_mhz,
scdc_present,
rr_capable,
cable_status,
ccbpci,
lte_340mcsc_scramble,
independent_view_3d,
dual_view_3d,
osd_disparity_3d,
max_frl_rate,
uhd_vic,
dc_48bit_420,
dc_36bit_420,
dc_30bit_420,
fapa_end_extended,
qms,
m_delta,
fva,
allm,
fapa_start_location,
neg_mvrr,
vrr_min_hz,
vrr_max_hz,
dsc,
))
}
pub(super) fn parse_hf_scdb(block_data: &[u8]) -> Option<HdmiForumSinkCap> {
if block_data.len() < 6 {
return None;
}
parse_hdmi_scds(&block_data[2..])
}
pub(super) const HF_VSDB_OUI: [u8; 3] = {
let v = display_types::cea861::oui::HDMI_FORUM;
[
(v & 0xFF) as u8,
((v >> 8) & 0xFF) as u8,
((v >> 16) & 0xFF) as u8,
]
};
pub(super) fn parse_hf_vsdb(block_data: &[u8]) -> Option<HdmiForumSinkCap> {
if block_data.len() < 7 {
return None;
}
if block_data[0..3] != HF_VSDB_OUI {
return None;
}
parse_hdmi_scds(&block_data[3..])
}
pub(super) fn parse_vesa_display_device(block_data: &[u8]) -> Option<VesaDisplayDeviceBlock> {
if block_data.len() < 30 {
return None;
}
let interface_type = block_data[0] >> 4;
let num_links = block_data[0] & 0x0F;
let interface_version = block_data[1] >> 4;
let interface_release = block_data[1] & 0x0F;
let content_protection = block_data[2];
let min_clock_mhz = block_data[3] >> 2;
let max_clock_mhz = ((block_data[3] as u16 & 0x03) << 8) | block_data[4] as u16;
let h_raw = u16::from_le_bytes([block_data[5], block_data[6]]);
let v_raw = u16::from_le_bytes([block_data[7], block_data[8]]);
let (native_width, native_height) = if h_raw == 0 && v_raw == 0 {
(None, None)
} else {
(Some(h_raw.saturating_add(1)), Some(v_raw.saturating_add(1)))
};
let aspect_ratio_raw = block_data[9];
let default_orientation = (block_data[10] >> 6) & 0x03;
let rotation_capability = (block_data[10] >> 4) & 0x03;
let zero_pixel_location = (block_data[10] >> 2) & 0x03;
let scan_direction = block_data[10] & 0x03;
let subpixel_layout = block_data[11];
let h_pitch_hundredths_mm = block_data[12];
let v_pitch_hundredths_mm = block_data[13];
let misc = block_data[14];
let dithering = (misc >> 6) & 0x03;
let direct_drive = misc & 0x20 != 0;
let overdrive_not_recommended = misc & 0x10 != 0;
let deinterlacing = misc & 0x08 != 0;
let audio = block_data[15];
let audio_on_video_interface = audio & 0x80 != 0;
let separate_audio_inputs = audio & 0x40 != 0;
let audio_input_override = audio & 0x20 != 0;
let delay_raw = block_data[16];
let audio_delay_ms = if delay_raw == 0 {
None
} else {
let mag = (delay_raw & 0x7F) as i16 * 2;
Some(if delay_raw & 0x80 != 0 { mag } else { -mag })
};
let frame_rate_byte = block_data[17];
let frame_rate_conversion = (frame_rate_byte >> 6) & 0x03;
let frame_rate_range = frame_rate_byte & 0x3F;
let native_frame_rate = block_data[18];
let cbd = block_data[19];
let interface_color_depth = (cbd >> 4) + 1;
let display_color_depth = (cbd & 0x0F) + 1;
let mut additional_chromaticities = [0u8; 8];
additional_chromaticities.copy_from_slice(&block_data[20..28]);
let rt = block_data[28];
let response_time_ms = rt & 0x7F;
let response_time_white_to_black = rt & 0x80 != 0;
let overscan = block_data[29];
let h_overscan_pct = (overscan >> 4) & 0x0F;
let v_overscan_pct = overscan & 0x0F;
Some(VesaDisplayDeviceBlock::new(
interface_type,
num_links,
interface_version,
interface_release,
content_protection,
min_clock_mhz,
max_clock_mhz,
native_width,
native_height,
aspect_ratio_raw,
default_orientation,
rotation_capability,
zero_pixel_location,
scan_direction,
subpixel_layout,
h_pitch_hundredths_mm,
v_pitch_hundredths_mm,
dithering,
direct_drive,
overdrive_not_recommended,
deinterlacing,
audio_on_video_interface,
separate_audio_inputs,
audio_input_override,
audio_delay_ms,
frame_rate_conversion,
frame_rate_range,
native_frame_rate,
interface_color_depth,
display_color_depth,
additional_chromaticities,
response_time_ms,
response_time_white_to_black,
h_overscan_pct,
v_overscan_pct,
))
}
fn decode_dtd_to_mode(dtd: &[u8]) -> Option<VideoMode> {
if dtd.len() < 18 || (dtd[0] == 0 && dtd[1] == 0) {
return None;
}
let pixel_clock = ((dtd[1] as u32) << 8) | (dtd[0] as u32);
if pixel_clock == 0 {
return None;
}
let hactive = (((dtd[4] as u16) & 0xF0) << 4) | (dtd[2] as u16);
let hblank = (((dtd[4] as u16) & 0x0F) << 8) | (dtd[3] as u16);
let vactive = (((dtd[7] as u16) & 0xF0) << 4) | (dtd[5] as u16);
let vblank = (((dtd[7] as u16) & 0x0F) << 8) | (dtd[6] as u16);
if hactive == 0 || vactive == 0 || hblank == 0 || vblank == 0 {
return None;
}
let total = (hactive + hblank) as u32 * (vactive + vblank) as u32;
let rate = (pixel_clock * 10_000) / total;
let refresh_rate = u8::try_from(rate).ok()?;
Some(VideoMode::new(
hactive,
vactive,
refresh_rate,
dtd[17] & 0x80 != 0,
))
}
pub(super) fn parse_vtb_ext(block_data: &[u8]) -> Option<VtbExtBlock> {
if block_data.len() < 4 {
return None;
}
let version = block_data[0];
let w = block_data[1] as usize;
let y = block_data[2] as usize;
let z = block_data[3] as usize;
let data = &block_data[4..];
let required = w * 18 + y * 3 + z * 2;
if data.len() < required {
return None;
}
let mut timings = Vec::new();
for i in 0..w {
if let Some(mode) = decode_dtd_to_mode(&data[i * 18..(i + 1) * 18]) {
if !timings.contains(&mode) {
timings.push(mode);
}
}
}
let cvt_base = w * 18;
for i in 0..y {
let off = cvt_base + i * 3;
let (b0, b1, b2) = (data[off], data[off + 1], data[off + 2]);
if b0 == 0 {
continue; }
let lines_raw = (((b1 as u16) & 0xF0) << 4) | (b0 as u16);
let v_add = (lines_raw + 1) * 2;
let h_add = {
let v = v_add as u32;
let h = match (b1 >> 2) & 0x03 {
0b00 => v * 4 / 3,
0b01 => v * 16 / 9,
0b10 => v * 16 / 10,
_ => continue, };
((h / 8) * 8) as u16
};
for (mask, rate) in [
(0x10u8, 50u8),
(0x08, 60),
(0x04, 75),
(0x02, 85),
(0x01, 60),
] {
if b2 & mask != 0 {
let mode = VideoMode::new(h_add, v_add, rate, false);
if !timings.contains(&mode) {
timings.push(mode);
}
}
}
}
let st_base = w * 18 + y * 3;
for i in 0..z {
let off = st_base + i * 2;
let (b1, b2) = (data[off], data[off + 1]);
if (b1 == 0x01 && b2 == 0x01) || b1 == 0x00 {
continue; }
let width = (b1 as u16 + 31) * 8;
let height = match (b2 >> 6) & 0x03 {
0x00 => (width * 10) / 16, 0x01 => (width * 3) / 4, 0x02 => (width * 4) / 5, _ => (width * 9) / 16, };
let mode = VideoMode::new(width, height, (b2 & 0x3F) + 60, false);
if !timings.contains(&mode) {
timings.push(mode);
}
}
Some(VtbExtBlock::new(version, timings))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_video_capability() {
let data = [EXT_TAG_VIDEO_CAPABILITY, 0b0101_0011];
let vc = parse_video_capability(&data).unwrap();
assert!(vc.flags.contains(VideoCapabilityFlags::QS));
assert!(!vc.flags.contains(VideoCapabilityFlags::QY));
assert_eq!(vc.pt_behaviour, 1);
assert_eq!(vc.it_behaviour, 0);
assert_eq!(vc.ce_behaviour, 3);
}
#[test]
fn test_colorimetry_bt2020() {
let data = [EXT_TAG_COLORIMETRY, 0xC0, 0x05];
let cb = parse_colorimetry(&data).unwrap();
assert!(cb.colorimetry.contains(ColorimetryFlags::BT2020RGB));
assert!(cb.colorimetry.contains(ColorimetryFlags::BT2020YCC));
assert!(!cb.colorimetry.contains(ColorimetryFlags::XVYCC601));
assert_eq!(cb.metadata_profiles, 5);
}
#[test]
fn test_colorimetry_no_metadata_byte() {
let data = [EXT_TAG_COLORIMETRY, 0x03];
let cb = parse_colorimetry(&data).unwrap();
assert_eq!(cb.metadata_profiles, 0);
}
#[test]
fn test_hdr_metadata_basic() {
let data = [EXT_TAG_HDR_STATIC_METADATA, 0x05, 0x01];
let hdr = parse_hdr_static_metadata(&data).unwrap();
assert!(hdr.eotf.contains(HdrEotf::SDR));
assert!(hdr.eotf.contains(HdrEotf::ST2084));
assert!(!hdr.eotf.contains(HdrEotf::HLG));
assert_eq!(hdr.static_metadata_descriptors, 1);
assert!(hdr.max_luminance.is_none());
assert!(hdr.min_luminance.is_none());
}
#[test]
fn test_hdr_metadata_luminance() {
let data = [EXT_TAG_HDR_STATIC_METADATA, 0x04, 0x01, 96, 96, 128];
let hdr = parse_hdr_static_metadata(&data).unwrap();
let max = hdr.max_luminance.unwrap();
assert!((max - 400.0).abs() < 0.1, "max={max}");
let min = hdr.min_luminance.unwrap();
assert!(min > 0.9 && min < 1.1, "min={min}");
}
#[test]
fn test_hdr_too_short_returns_none() {
let data = [EXT_TAG_HDR_STATIC_METADATA];
assert!(parse_hdr_static_metadata(&data).is_none());
}
#[test]
fn test_speaker_allocation_basic() {
let data = [0x07u8]; let sa = parse_speaker_allocation(&data).unwrap();
assert!(sa.channels.contains(SpeakerAllocationFlags::FL_FR));
assert!(sa.channels.contains(SpeakerAllocationFlags::LFE1));
assert!(sa.channels.contains(SpeakerAllocationFlags::FC));
assert!(!sa.channels.contains(SpeakerAllocationFlags::BL_BR));
assert_eq!(sa.channels_2, SpeakerAllocationFlags2::empty());
assert_eq!(sa.channels_3, SpeakerAllocationFlags3::empty());
}
#[test]
fn test_speaker_allocation_extended_bytes() {
let data = [0x01u8, 0x01u8, 0x01u8];
let sa = parse_speaker_allocation(&data).unwrap();
assert!(sa.channels.contains(SpeakerAllocationFlags::FL_FR));
assert!(sa.channels_2.contains(SpeakerAllocationFlags2::TP_FL_FR));
assert!(sa.channels_3.contains(SpeakerAllocationFlags3::TP_BL_TP_BR));
}
#[test]
fn test_speaker_allocation_empty_returns_none() {
assert!(parse_speaker_allocation(&[]).is_none());
}
#[test]
fn test_hdr_dynamic_metadata_hdr10_plus() {
let data = [EXT_TAG_HDR_DYNAMIC_METADATA, 0x01];
let descs = parse_hdr_dynamic_metadata(&data);
assert_eq!(descs.len(), 1);
assert_eq!(descs[0].application_type, 1);
assert_eq!(descs[0].application_version, 0);
}
#[test]
fn test_hdr_dynamic_metadata_empty_block() {
let data = [EXT_TAG_HDR_DYNAMIC_METADATA];
let descs = parse_hdr_dynamic_metadata(&data);
assert!(descs.is_empty());
}
#[test]
fn test_video_format_preferences() {
let data = [EXT_TAG_VIDEO_FORMAT_PREFERENCE, 16, 129, 145];
let prefs = parse_video_format_preferences(&data);
assert_eq!(prefs, vec![16, 129, 145]);
}
#[test]
fn test_y420_vdb_filters_vic0() {
let data = [EXT_TAG_Y420_VIDEO, 0x80 | 93, 0x80];
let vics = parse_y420_vdb(&data);
assert_eq!(vics, vec![93]);
}
#[test]
fn test_y420_capability_map() {
let data = [EXT_TAG_Y420_CAPABILITY_MAP, 0b0000_0101, 0xFF];
let bitmap = parse_y420_capability_map(&data);
assert_eq!(bitmap, vec![0b0000_0101, 0xFF]);
}
#[test]
fn test_vesa_dtc_8bit() {
let data = [0x00u8, 0x00, 0x80, 0xFF];
let dtc = parse_vesa_transfer_characteristic(&data).unwrap();
assert_eq!(dtc.encoding, DtcPointEncoding::Bits8);
assert_eq!(dtc.points.len(), 3);
assert!((dtc.points[0] - 0.0).abs() < 0.001);
assert!((dtc.points[1] - 0x80 as f32 / 255.0).abs() < 0.001);
assert!((dtc.points[2] - 1.0).abs() < 0.001);
}
#[test]
fn test_vesa_dtc_10bit() {
let data = [0x40u8, 0xFF, 0xC0, 0x00, 0x00, 0x00]; let dtc = parse_vesa_transfer_characteristic(&data).unwrap();
assert_eq!(dtc.encoding, DtcPointEncoding::Bits10);
assert_eq!(dtc.points.len(), 4);
assert!(
(dtc.points[0] - 1.0).abs() < 0.001,
"point0={}",
dtc.points[0]
);
assert!((dtc.points[1] - 0.0).abs() < 0.001);
assert!((dtc.points[2] - 0.0).abs() < 0.001);
assert!((dtc.points[3] - 0.0).abs() < 0.001);
}
#[test]
fn test_vesa_dtc_12bit() {
let data = [0x80u8, 0xFF, 0xF0, 0x00]; let dtc = parse_vesa_transfer_characteristic(&data).unwrap();
assert_eq!(dtc.encoding, DtcPointEncoding::Bits12);
assert_eq!(dtc.points.len(), 2);
assert!(
(dtc.points[0] - 1.0).abs() < 0.001,
"point0={}",
dtc.points[0]
);
assert!((dtc.points[1] - 0.0).abs() < 0.001);
}
#[test]
fn test_vesa_dtc_reserved_encoding_returns_none() {
let data = [0xC0u8, 0x00];
assert!(parse_vesa_transfer_characteristic(&data).is_none());
}
#[test]
fn test_vesa_dtc_empty_returns_none() {
assert!(parse_vesa_transfer_characteristic(&[]).is_none());
}
#[test]
fn test_room_configuration_basic() {
let data = [EXT_TAG_ROOM_CONFIGURATION, 0x45]; let rc = parse_room_configuration(&data).unwrap();
assert_eq!(rc.speaker_count, 5);
assert!(rc.has_speaker_locations);
}
#[test]
fn test_room_configuration_no_locations() {
let data = [EXT_TAG_ROOM_CONFIGURATION, 0x07]; let rc = parse_room_configuration(&data).unwrap();
assert_eq!(rc.speaker_count, 7);
assert!(!rc.has_speaker_locations);
}
#[test]
fn test_room_configuration_too_short_returns_none() {
let data = [EXT_TAG_ROOM_CONFIGURATION];
assert!(parse_room_configuration(&data).is_none());
}
#[test]
fn test_speaker_location_entries() {
let data = [EXT_TAG_SPEAKER_LOCATION, 0x00, 100, 0x01, 80];
let entries = parse_speaker_location(&data);
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].channel_assignment, 0x00);
assert_eq!(entries[0].distance, 100);
assert_eq!(entries[1].channel_assignment, 0x01);
assert_eq!(entries[1].distance, 80);
}
#[test]
fn test_speaker_location_odd_trailing_byte_ignored() {
let data = [EXT_TAG_SPEAKER_LOCATION, 0x02, 50, 0xFF];
let entries = parse_speaker_location(&data);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].channel_assignment, 0x02);
assert_eq!(entries[0].distance, 50);
}
#[test]
fn test_speaker_location_empty_block() {
let data = [EXT_TAG_SPEAKER_LOCATION];
assert!(parse_speaker_location(&data).is_empty());
}
#[test]
fn test_infoframe_avi_and_audio() {
let data = [EXT_TAG_INFOFRAME, 0x02, 0x04];
let descs = parse_infoframe_db(&data);
assert_eq!(descs.len(), 2);
assert_eq!(descs[0].type_code, infoframe_type::AVI);
assert!(descs[0].vendor_oui.is_none());
assert_eq!(descs[1].type_code, infoframe_type::AUDIO);
assert!(descs[1].vendor_oui.is_none());
}
#[test]
fn test_infoframe_vendor_specific_with_oui() {
let data = [EXT_TAG_INFOFRAME, 0x61, 0x00, 0x0C, 0x03];
let descs = parse_infoframe_db(&data);
assert_eq!(descs.len(), 1);
assert_eq!(descs[0].type_code, infoframe_type::VENDOR_SPECIFIC);
assert_eq!(descs[0].vendor_oui, Some(0x000C03));
}
#[test]
fn test_infoframe_mixed_vsi_and_standard() {
let data = [EXT_TAG_INFOFRAME, 0x61, 0x00, 0x0C, 0x03, 0x07];
let descs = parse_infoframe_db(&data);
assert_eq!(descs.len(), 2);
assert_eq!(descs[0].type_code, infoframe_type::VENDOR_SPECIFIC);
assert_eq!(descs[0].vendor_oui, Some(0x000C03));
assert_eq!(descs[1].type_code, infoframe_type::DYNAMIC_RANGE_MASTERING);
assert!(descs[1].vendor_oui.is_none());
}
#[test]
fn test_infoframe_empty_block() {
let data = [EXT_TAG_INFOFRAME];
assert!(parse_infoframe_db(&data).is_empty());
}
fn minimal_dddb_payload() -> [u8; 30] {
let mut d = [0u8; 30];
d[0] = 0xA2;
d[1] = 0x10;
d[2] = 0x01;
d[3] = 0xA0;
d[4] = 0xF0;
let w: u16 = 1919;
let h: u16 = 1079;
d[5] = (w & 0xFF) as u8;
d[6] = (w >> 8) as u8;
d[7] = (h & 0xFF) as u8;
d[8] = (h >> 8) as u8;
d[9] = 0x09;
d[10] = 0x00;
d[11] = 0x01;
d[15] = 0x40;
d[16] = 0x82;
d[19] = 0x75;
d[28] = 0x05;
d[29] = 0x23;
d
}
#[test]
fn test_dddb_basic_fields() {
let payload = minimal_dddb_payload();
let block = parse_vesa_display_device(&payload).unwrap();
assert_eq!(block.interface_type, 0x0A);
assert_eq!(block.num_links, 2);
assert_eq!(block.interface_version, 1);
assert_eq!(block.interface_release, 0);
assert_eq!(block.content_protection, 0x01);
}
#[test]
fn test_dddb_clock_frequency() {
let payload = minimal_dddb_payload();
let block = parse_vesa_display_device(&payload).unwrap();
assert_eq!(block.min_clock_mhz, 40);
assert_eq!(block.max_clock_mhz, 0xF0);
}
#[test]
fn test_dddb_native_resolution() {
let payload = minimal_dddb_payload();
let block = parse_vesa_display_device(&payload).unwrap();
assert_eq!(block.native_width, Some(1920));
assert_eq!(block.native_height, Some(1080));
}
#[test]
fn test_dddb_native_resolution_none_when_zero() {
let mut payload = minimal_dddb_payload();
payload[5] = 0;
payload[6] = 0;
payload[7] = 0;
payload[8] = 0;
let block = parse_vesa_display_device(&payload).unwrap();
assert_eq!(block.native_width, None);
assert_eq!(block.native_height, None);
}
#[test]
fn test_dddb_audio_and_delay() {
let payload = minimal_dddb_payload();
let block = parse_vesa_display_device(&payload).unwrap();
assert!(!block.audio_on_video_interface);
assert!(block.separate_audio_inputs);
assert!(!block.audio_input_override);
assert_eq!(block.audio_delay_ms, Some(4));
}
#[test]
fn test_dddb_audio_delay_none_when_zero() {
let mut payload = minimal_dddb_payload();
payload[16] = 0;
let block = parse_vesa_display_device(&payload).unwrap();
assert!(block.audio_delay_ms.is_none());
}
#[test]
fn test_dddb_audio_delay_negative() {
let mut payload = minimal_dddb_payload();
payload[16] = 0x02;
let block = parse_vesa_display_device(&payload).unwrap();
assert_eq!(block.audio_delay_ms, Some(-4));
}
#[test]
fn test_dddb_color_depth() {
let payload = minimal_dddb_payload();
let block = parse_vesa_display_device(&payload).unwrap();
assert_eq!(block.interface_color_depth, 8);
assert_eq!(block.display_color_depth, 6);
}
#[test]
fn test_dddb_response_time_and_overscan() {
let payload = minimal_dddb_payload();
let block = parse_vesa_display_device(&payload).unwrap();
assert_eq!(block.response_time_ms, 5);
assert!(!block.response_time_white_to_black);
assert_eq!(block.h_overscan_pct, 2);
assert_eq!(block.v_overscan_pct, 3);
}
#[test]
fn test_dddb_too_short_returns_none() {
let payload = [0u8; 29];
assert!(parse_vesa_display_device(&payload).is_none());
}
#[test]
fn test_vtb_ext_empty_version_only() {
let data = [0x01u8, 0x00, 0x00, 0x00];
let vtb = parse_vtb_ext(&data).unwrap();
assert_eq!(vtb.version, 1);
assert!(vtb.timings.is_empty());
}
#[test]
fn test_vtb_ext_too_short_returns_none() {
let data = [0x01u8, 0x00, 0x00];
assert!(parse_vtb_ext(&data).is_none());
}
#[test]
fn test_vtb_ext_standard_timings() {
let data = [0x01u8, 0x00, 0x00, 0x01, 0x81, 0x00];
let vtb = parse_vtb_ext(&data).unwrap();
assert_eq!(vtb.timings.len(), 1);
assert_eq!(vtb.timings[0].width, 1280);
assert_eq!(vtb.timings[0].height, 800); assert_eq!(vtb.timings[0].refresh_rate, 60);
}
#[test]
fn test_vtb_ext_cvt_descriptor_16_9() {
let data = [0x01u8, 0x00, 0x01, 0x00, 0x1B, 0x24, 0x08];
let vtb = parse_vtb_ext(&data).unwrap();
assert!(!vtb.timings.is_empty());
let t = vtb.timings.iter().find(|m| m.refresh_rate == 60).unwrap();
assert_eq!(t.height, 1080);
assert_eq!(t.width, 1920);
}
#[test]
fn test_vtb_ext_insufficient_data_for_dtb_returns_none() {
let data = [0x01u8, 0x01, 0x00, 0x00];
assert!(parse_vtb_ext(&data).is_none());
}
#[test]
fn test_vsvdb_basic() {
let payload = [0x46u8, 0xD0, 0x00, 0xAB, 0xCD];
let b = parse_vendor_specific_block(&payload).unwrap();
assert_eq!(b.oui, 0x00D046);
assert_eq!(b.payload, vec![0xAB, 0xCD]);
}
#[test]
fn test_vsvdb_oui_only_no_payload() {
let payload = [0x03u8, 0x0C, 0x00]; let b = parse_vendor_specific_block(&payload).unwrap();
assert_eq!(b.oui, 0x000C03);
assert!(b.payload.is_empty());
}
#[test]
fn test_vsvdb_too_short_returns_none() {
assert!(parse_vendor_specific_block(&[0x46u8, 0xD0]).is_none());
}
#[test]
fn test_vsadb_same_structure() {
let payload = [0x8Bu8, 0x84, 0x90, 0x01]; let b = parse_vendor_specific_block(&payload).unwrap();
assert_eq!(b.oui, 0x90848B);
assert_eq!(b.payload, vec![0x01]);
}
fn t7_1080p60() -> [u8; 22] {
let mut d = [0u8; 22];
d[0] = 0x02; d[1] = 0x00; d[2] = 0x14;
d[3] = 0x44;
d[4] = 0x02;
d[5] = 0x00; d[6] = 0x80;
d[7] = 0x07;
d[8] = 0x18;
d[9] = 0x01;
d[14] = 0x38;
d[15] = 0x04;
d[16] = 0x2D;
d[17] = 0x00;
d
}
#[test]
fn test_t7vtdb_1080p60() {
let d = t7_1080p60();
let t7 = parse_t7vtdb(&d).unwrap();
assert_eq!(t7.version, 2);
assert_eq!(t7.mode.width, 1920);
assert_eq!(t7.mode.height, 1080);
assert_eq!(t7.mode.refresh_rate, 60);
assert!(!t7.mode.interlaced);
assert!(!t7.y420);
}
#[test]
fn test_t7vtdb_y420_flag() {
let mut d = t7_1080p60();
d[1] = 0x40; let t7 = parse_t7vtdb(&d).unwrap();
assert!(t7.y420);
}
#[test]
fn test_t7vtdb_interlaced_flag() {
let mut d = t7_1080p60();
d[5] = 0x10; let t7 = parse_t7vtdb(&d).unwrap();
assert!(t7.mode.interlaced);
}
#[test]
fn test_t7vtdb_too_short_returns_none() {
let d = [0u8; 21];
assert!(parse_t7vtdb(&d).is_none());
}
#[test]
fn test_t7vtdb_zero_pixel_clock_returns_none() {
let mut d = t7_1080p60();
d[2] = 0;
d[3] = 0;
d[4] = 0;
assert!(parse_t7vtdb(&d).is_none());
}
#[test]
fn test_t7vtdb_zero_hactive_returns_none() {
let mut d = t7_1080p60();
d[6] = 0;
d[7] = 0;
assert!(parse_t7vtdb(&d).is_none());
}
#[test]
fn test_t7vtdb_720p60() {
let mut d = [0u8; 22];
d[0] = 0x02;
d[2] = 0x4A;
d[3] = 0x22;
d[4] = 0x01;
d[6] = 0x00;
d[7] = 0x05;
d[8] = 0x72;
d[9] = 0x01;
d[14] = 0xD0;
d[15] = 0x02;
d[16] = 0x1E;
let t7 = parse_t7vtdb(&d).unwrap();
assert_eq!(t7.mode.width, 1280);
assert_eq!(t7.mode.height, 720);
assert_eq!(t7.mode.refresh_rate, 60);
}
#[test]
fn test_t8vtdb_single_1080p60() {
let data = [0x00u8, 0x52];
let t8 = parse_t8vtdb(&data).unwrap();
assert_eq!(t8.version, 0);
assert!(!t8.y420);
assert_eq!(t8.codes, vec![0x52]);
assert_eq!(t8.timings.len(), 1);
assert_eq!(t8.timings[0].width, 1920);
assert_eq!(t8.timings[0].height, 1080);
assert_eq!(t8.timings[0].refresh_rate, 60);
assert!(!t8.timings[0].interlaced);
}
#[test]
fn test_t8vtdb_multiple_codes() {
let data = [0x00u8, 0x04, 0x09, 0x10];
let t8 = parse_t8vtdb(&data).unwrap();
assert_eq!(t8.codes, vec![0x04, 0x09, 0x10]);
assert_eq!(t8.timings.len(), 3);
assert_eq!(t8.timings[0].width, 640);
assert_eq!(t8.timings[1].width, 800);
assert_eq!(t8.timings[2].width, 1024);
}
#[test]
fn test_t8vtdb_interlaced_0x0f() {
let data = [0x00u8, 0x0F];
let t8 = parse_t8vtdb(&data).unwrap();
assert_eq!(t8.timings[0].width, 1024);
assert_eq!(t8.timings[0].height, 768);
assert_eq!(t8.timings[0].refresh_rate, 43);
assert!(t8.timings[0].interlaced);
}
#[test]
fn test_t8vtdb_two_byte_codes() {
let data = [0x08u8, 0x52, 0x00];
let t8 = parse_t8vtdb(&data).unwrap();
assert_eq!(t8.codes, vec![0x0052]);
assert_eq!(t8.timings[0].width, 1920);
assert_eq!(t8.timings[0].refresh_rate, 60);
}
#[test]
fn test_t8vtdb_two_byte_codes_odd_trailing_byte_ignored() {
let data = [0x08u8, 0x52, 0x00, 0x55];
let t8 = parse_t8vtdb(&data).unwrap();
assert_eq!(t8.codes, vec![0x0052]); }
#[test]
fn test_t8vtdb_unknown_code_in_codes_but_not_timings() {
let data = [0x00u8, 0xFF];
let t8 = parse_t8vtdb(&data).unwrap();
assert_eq!(t8.codes, vec![0xFF]);
assert!(t8.timings.is_empty());
}
#[test]
fn test_t8vtdb_y420_flag() {
let data = [0x20u8, 0x52];
let t8 = parse_t8vtdb(&data).unwrap();
assert!(t8.y420);
}
#[test]
fn test_t8vtdb_non_dmt_returns_none() {
let data = [0x40u8, 0x52];
assert!(parse_t8vtdb(&data).is_none());
}
#[test]
fn test_t8vtdb_empty_returns_none() {
assert!(parse_t8vtdb(&[]).is_none());
}
#[test]
fn test_dmt_to_mode_spot_checks() {
let m = dmt_to_mode(0x55).unwrap();
assert_eq!(m.width, 1280);
assert_eq!(m.height, 720);
assert_eq!(m.refresh_rate, 60);
assert!(!m.interlaced);
let m = dmt_to_mode(0x0F).unwrap();
assert!(m.interlaced);
assert_eq!(m.refresh_rate, 43);
assert!(dmt_to_mode(0x00).is_none());
assert!(dmt_to_mode(0x59).is_none());
}
fn t10_desc_6(width: u16, height: u16, refresh: u16, y420: bool) -> [u8; 6] {
let flags = if y420 { 0x80 } else { 0x00 };
let h = (width - 1).to_le_bytes();
let v = (height - 1).to_le_bytes();
[flags, h[0], h[1], v[0], v[1], (refresh - 1) as u8]
}
#[test]
fn test_t10vtdb_6byte_single() {
let desc = t10_desc_6(640, 480, 60, false);
let mut data = vec![0x00u8]; data.extend_from_slice(&desc);
let t10 = parse_t10vtdb(&data).unwrap();
assert_eq!(t10.entries.len(), 1);
assert_eq!(t10.entries[0].width, 640);
assert_eq!(t10.entries[0].height, 480);
assert_eq!(t10.entries[0].refresh_hz, 60);
assert!(!t10.entries[0].y420);
}
#[test]
fn test_t10vtdb_7byte_high_refresh() {
let flags = 0x00u8;
let h = (1919u16).to_le_bytes(); let v = (1079u16).to_le_bytes(); let data = [
0x10, flags, h[0], h[1], v[0], v[1], 0xFF, 0x01, ];
let t10 = parse_t10vtdb(&data).unwrap();
assert_eq!(t10.entries[0].width, 1920);
assert_eq!(t10.entries[0].height, 1080);
assert_eq!(t10.entries[0].refresh_hz, 512);
}
#[test]
fn test_t10vtdb_multiple_descriptors() {
let d1 = t10_desc_6(1280, 720, 60, false);
let d2 = t10_desc_6(1920, 1080, 60, false);
let mut data = vec![0x00u8];
data.extend_from_slice(&d1);
data.extend_from_slice(&d2);
let t10 = parse_t10vtdb(&data).unwrap();
assert_eq!(t10.entries.len(), 2);
assert_eq!(t10.entries[0].width, 1280);
assert_eq!(t10.entries[1].width, 1920);
}
#[test]
fn test_t10vtdb_y420_flag() {
let desc = t10_desc_6(3840, 2160, 60, true);
let mut data = vec![0x00u8];
data.extend_from_slice(&desc);
let t10 = parse_t10vtdb(&data).unwrap();
assert!(t10.entries[0].y420);
}
#[test]
fn test_t10vtdb_invalid_m_returns_none() {
let data = [0x30u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
assert!(parse_t10vtdb(&data).is_none());
}
#[test]
fn test_t10vtdb_empty_returns_none() {
assert!(parse_t10vtdb(&[]).is_none());
}
#[test]
fn test_t10vtdb_trailing_partial_descriptor_ignored() {
let desc = t10_desc_6(800, 600, 60, false);
let mut data = vec![0x00u8];
data.extend_from_slice(&desc);
data.push(0xFF); let t10 = parse_t10vtdb(&data).unwrap();
assert_eq!(t10.entries.len(), 1); }
#[test]
fn test_hf_eeodb_valid() {
assert_eq!(parse_hf_eeodb(&[5]), Some(5));
assert_eq!(parse_hf_eeodb(&[255]), Some(255));
assert_eq!(parse_hf_eeodb(&[1]), Some(1));
}
#[test]
fn test_hf_eeodb_zero_returns_none() {
assert!(parse_hf_eeodb(&[0]).is_none());
}
#[test]
fn test_hf_eeodb_empty_returns_none() {
assert!(parse_hf_eeodb(&[]).is_none());
}
fn hf_scdb_min(version: u8, tmds_raw: u8, scdc: u8, frl_dc: u8) -> Vec<u8> {
vec![0x00, 0x00, version, tmds_raw, scdc, frl_dc]
}
#[test]
fn test_hf_scdb_basic_fields() {
let data = hf_scdb_min(1, 0x78, 0xC0, 0x68);
let cap = parse_hf_scdb(&data).unwrap();
assert_eq!(cap.version, 1);
assert_eq!(cap.max_tmds_rate_mhz, 600);
assert!(cap.scdc_present);
assert!(cap.rr_capable);
assert!(!cap.cable_status);
assert_eq!(cap.max_frl_rate, HdmiForumFrl::Rate12Gbps4Lanes);
assert!(cap.uhd_vic);
assert!(!cap.dc_30bit_420);
assert!(cap.vrr_min_hz.is_none());
assert!(cap.dsc.is_none());
}
#[test]
fn test_hf_scdb_dc_flags() {
let data = hf_scdb_min(1, 0x00, 0x00, 0x07);
let cap = parse_hf_scdb(&data).unwrap();
assert!(cap.dc_48bit_420);
assert!(cap.dc_36bit_420);
assert!(cap.dc_30bit_420);
}
#[test]
fn test_hf_scdb_extended_flags_and_vrr() {
let mut data = hf_scdb_min(1, 0x78, 0xC0, 0x68);
data.push(0x02); data.push(0x30); data.push(0x90); let cap = parse_hf_scdb(&data).unwrap();
assert!(cap.allm);
assert!(!cap.fva);
assert_eq!(cap.vrr_min_hz, Some(48));
assert_eq!(cap.vrr_max_hz, Some(144));
}
#[test]
fn test_hf_scdb_vrr_10bit_max() {
let mut data = hf_scdb_min(1, 0x00, 0x00, 0x00);
data.push(0x00); data.push(0xC0); data.push(0xFF); let cap = parse_hf_scdb(&data).unwrap();
assert_eq!(cap.vrr_max_hz, Some(0x03FF)); }
#[test]
fn test_hf_scdb_dsc_section() {
let mut data = hf_scdb_min(1, 0x78, 0xC0, 0x68);
data.extend_from_slice(&[0x00, 0x00, 0x00]); data.extend_from_slice(&[0xC0, 0x64, 0x02]); let cap = parse_hf_scdb(&data).unwrap();
let dsc = cap.dsc.unwrap();
assert!(dsc.dsc_1p2);
assert!(dsc.native_420);
assert!(!dsc.bpc12);
assert_eq!(dsc.max_frl_rate, HdmiForumFrl::Rate12Gbps4Lanes);
assert_eq!(dsc.max_slices, HdmiDscMaxSlices::Slices8At340Mhz);
assert_eq!(dsc.max_chunk_bytes, 3072);
}
#[test]
fn test_hf_scdb_too_short_returns_none() {
assert!(parse_hf_scdb(&[0x00, 0x00, 0x01, 0x78, 0xC0]).is_none());
}
#[test]
fn test_hf_scdb_empty_returns_none() {
assert!(parse_hf_scdb(&[]).is_none());
}
#[test]
fn test_hf_scdb_partial_vrr_gives_none() {
let mut data = hf_scdb_min(1, 0x00, 0x00, 0x00);
data.push(0x02); data.push(0x30); let cap = parse_hf_scdb(&data).unwrap();
assert!(cap.allm); assert!(cap.vrr_min_hz.is_none()); assert!(cap.vrr_max_hz.is_none());
}
fn hf_vsdb_min(version: u8, tmds_div5: u8, scdc: u8, frl_dc: u8) -> Vec<u8> {
vec![0xD8, 0x5D, 0xC4, version, tmds_div5, scdc, frl_dc]
}
#[test]
fn test_hf_vsdb_basic_fields() {
let data = hf_vsdb_min(1, 120, 0x80, (6 << 4) | 0x04);
let cap = parse_hf_vsdb(&data).unwrap();
assert_eq!(cap.version, 1);
assert_eq!(cap.max_tmds_rate_mhz, 600);
assert!(cap.scdc_present);
assert!(!cap.rr_capable);
assert_eq!(cap.max_frl_rate, HdmiForumFrl::Rate12Gbps4Lanes);
assert!(cap.dc_48bit_420);
assert!(!cap.dc_36bit_420);
assert!(!cap.dc_30bit_420);
}
#[test]
fn test_hf_vsdb_wrong_oui_returns_none() {
let data = vec![0x03, 0x0C, 0x00, 1, 0, 0, 0];
assert!(parse_hf_vsdb(&data).is_none());
}
#[test]
fn test_hf_vsdb_too_short_returns_none() {
let data = vec![0xD8, 0x5D, 0xC4, 1, 0, 0];
assert!(parse_hf_vsdb(&data).is_none());
}
#[test]
fn test_hf_vsdb_allm_and_vrr() {
let mut data = hf_vsdb_min(1, 0, 0, 0);
data.push(0x02); data.push(48); data.push(144); let cap = parse_hf_vsdb(&data).unwrap();
assert!(cap.allm);
assert_eq!(cap.vrr_min_hz, Some(48));
assert_eq!(cap.vrr_max_hz, Some(144));
}
#[test]
fn test_hf_vsdb_scds_same_as_scdb() {
let version = 1u8;
let tmds = 120u8;
let scdc = 0xC8u8; let frl_dc = (5u8 << 4) | 0x03;
let vsdb_data = vec![0xD8, 0x5D, 0xC4, version, tmds, scdc, frl_dc];
let vsdb_cap = parse_hf_vsdb(&vsdb_data).unwrap();
let scdb_data = vec![0x00, 0x00, version, tmds, scdc, frl_dc];
let scdb_cap = parse_hf_scdb(&scdb_data).unwrap();
assert_eq!(vsdb_cap, scdb_cap);
}
#[test]
fn test_all_extended_tags_accounted_for() {
for tag in 0u16..=255 {
let tag = tag as u8;
let implemented = IMPLEMENTED_EXTENDED_TAGS.contains(&tag);
let reserved = RESERVED_EXTENDED_TAG_RANGES
.iter()
.any(|&(lo, hi)| tag >= lo && tag <= hi);
assert!(
implemented || reserved,
"Extended tag 0x{:02X} is unaccounted for: \
add it to IMPLEMENTED_EXTENDED_TAGS or RESERVED_EXTENDED_TAG_RANGES",
tag
);
}
}
#[test]
fn test_implemented_and_reserved_are_disjoint() {
for &tag in IMPLEMENTED_EXTENDED_TAGS {
let in_reserved = RESERVED_EXTENDED_TAG_RANGES
.iter()
.any(|&(lo, hi)| tag >= lo && tag <= hi);
assert!(
!in_reserved,
"Extended tag 0x{:02X} appears in both IMPLEMENTED and RESERVED",
tag
);
}
}
}