mod metadata;
mod timing;
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::capabilities::DisplayCapabilities;
use crate::model::capabilities::StaticContext;
use crate::model::diagnostics::EdidWarning;
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::diagnostics::ParseWarning;
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::extension::ExtensionHandler;
use crate::model::extension::StaticExtensionHandler;
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::prelude::{Arc, Vec};
#[cfg(any(feature = "alloc", feature = "std"))]
use metadata::scan_all_metadata_blocks;
use timing::process_data_blocks;
#[cfg(any(feature = "alloc", feature = "std"))]
pub use display_types::DisplayIdCapabilities;
#[derive(Debug)]
pub struct DisplayIdHandler;
const DISPLAYID_V1_MIN: u8 = 0x10;
const DISPLAYID_V1_MAX: u8 = 0x1F;
const DISPLAYID_V2: u8 = 0x20;
use display_types::displayid::tag;
const TAG_PRODUCT_ID: u8 = tag::PRODUCT_ID;
const TAG_DISPLAY_PARAMS: u8 = tag::DISPLAY_PARAMS;
const TAG_COLOR_CHARACTERISTICS: u8 = tag::COLOR_CHARACTERISTICS;
const TAG_TYPE_I_TIMING: u8 = tag::TYPE_I_TIMING;
const TAG_TYPE_II_TIMING: u8 = tag::TYPE_II_TIMING;
const TAG_TYPE_III_TIMING: u8 = tag::TYPE_III_TIMING;
const TAG_TYPE_IV_TIMING: u8 = tag::TYPE_IV_TIMING;
const TAG_VESA_VIDEO_TIMING: u8 = tag::VESA_VIDEO_TIMING;
const TAG_CTA_VIDEO_TIMING: u8 = tag::CTA_VIDEO_TIMING;
const TAG_VIDEO_TIMING_RANGE: u8 = tag::VIDEO_TIMING_RANGE;
const TAG_SERIAL_NUMBER: u8 = tag::SERIAL_NUMBER;
const TAG_ASCII_STRING: u8 = tag::ASCII_STRING;
const TAG_DISPLAY_DEVICE_DATA: u8 = tag::DISPLAY_DEVICE_DATA;
const TAG_POWER_SEQUENCING: u8 = tag::POWER_SEQUENCING;
const TAG_TRANSFER_CHARACTERISTICS: u8 = tag::TRANSFER_CHARACTERISTICS;
const TAG_DISPLAY_INTERFACE: u8 = tag::DISPLAY_INTERFACE;
const TAG_STEREO_DISPLAY_INTERFACE: u8 = tag::STEREO_DISPLAY_INTERFACE;
const TAG_TYPE_V_TIMING: u8 = tag::TYPE_V_TIMING;
const TAG_TILED_TOPOLOGY: u8 = tag::TILED_TOPOLOGY;
const TAG_TYPE_VI_TIMING: u8 = tag::TYPE_VI_TIMING;
fn for_each_data_block(payload: &[u8], mut f: impl FnMut(u8, u8, &[u8])) {
let mut offset = 0;
while offset + 3 <= payload.len() {
let tag = payload[offset];
let revision = payload[offset + 1];
let length = payload[offset + 2] as usize;
if tag == 0x00 && length == 0 {
break;
}
let block_end = offset + 3 + length;
if block_end > payload.len() {
break;
}
f(tag, revision, &payload[offset + 3..block_end]);
offset = block_end;
}
}
fn parse_section_header(block: &[u8; 128]) -> (u8, u8, u8, u8) {
let version = block[1];
let section_byte_count = block[2];
let packed = block[3];
let product_type = packed & 0x07;
let extension_count = (packed >> 3) & 0x1F;
(version, section_byte_count, product_type, extension_count)
}
fn fragment_payload(block: &[u8; 128]) -> &[u8] {
let section_byte_count = block[2] as usize;
let end = (4 + section_byte_count).min(127);
if end > 4 { &block[4..end] } else { &[] }
}
fn check_displayid_section(block: &[u8; 128]) -> Option<EdidWarning> {
let n = block[2] as usize;
let checksum_pos = 4 + n;
if checksum_pos > 126 {
return Some(EdidWarning::DisplayIdSectionBytesOutOfRange(block[2]));
}
let ok = block[1..=checksum_pos]
.iter()
.fold(0u8, |acc, &x| acc.wrapping_add(x))
== 0;
if ok {
None
} else {
Some(EdidWarning::DisplayIdChecksumMismatch)
}
}
#[cfg(any(feature = "alloc", feature = "std"))]
impl ExtensionHandler for DisplayIdHandler {
fn process(
&self,
blocks: &[&[u8; 128]],
caps: &mut DisplayCapabilities,
warnings: &mut Vec<ParseWarning>,
) {
let Some(first) = blocks.first() else { return };
let (version, _section_byte_count, product_type, extension_count) =
parse_section_header(first);
match version {
DISPLAYID_V1_MIN..=DISPLAYID_V1_MAX | DISPLAYID_V2 => {}
v => {
warnings.push(Arc::new(EdidWarning::DisplayIdVersionUnknown(v)));
return;
}
}
let actual_continuation = blocks.len().saturating_sub(1);
if extension_count as usize != actual_continuation {
warnings.push(Arc::new(EdidWarning::DisplayIdExtensionCountMismatch {
declared: extension_count,
found: actual_continuation.min(u8::MAX as usize) as u8,
}));
}
caps.set_extension_data(0x70, DisplayIdCapabilities::new(version, product_type));
for block in blocks {
if let Some(w) = check_displayid_section(block) {
warnings.push(Arc::new(w));
}
let payload = fragment_payload(block);
process_data_blocks(payload, caps);
scan_all_metadata_blocks(payload, caps);
}
}
}
impl StaticExtensionHandler for DisplayIdHandler {
fn tag(&self) -> u8 {
0x70
}
fn process(&self, blocks: &[&[u8; 128]], ctx: &mut StaticContext<'_>) {
let Some(first) = blocks.first() else { return };
let (version, _section_byte_count, _product_type, extension_count) =
parse_section_header(first);
match version {
DISPLAYID_V1_MIN..=DISPLAYID_V1_MAX | DISPLAYID_V2 => {}
v => {
ctx.push_warning(EdidWarning::DisplayIdVersionUnknown(v));
return;
}
}
let actual_continuation = blocks.len().saturating_sub(1);
if extension_count as usize != actual_continuation {
ctx.push_warning(EdidWarning::DisplayIdExtensionCountMismatch {
declared: extension_count,
found: actual_continuation.min(u8::MAX as usize) as u8,
});
}
for block in blocks {
if let Some(w) = check_displayid_section(block) {
ctx.push_warning(w);
}
process_data_blocks(fragment_payload(block), ctx);
}
}
}
#[cfg(test)]
const IMPLEMENTED_BLOCK_TAGS: &[u8] = &[
TAG_PRODUCT_ID, TAG_DISPLAY_PARAMS, TAG_COLOR_CHARACTERISTICS, TAG_TYPE_I_TIMING, TAG_TYPE_II_TIMING, TAG_TYPE_III_TIMING, TAG_TYPE_IV_TIMING, TAG_VESA_VIDEO_TIMING, TAG_CTA_VIDEO_TIMING, TAG_VIDEO_TIMING_RANGE, TAG_SERIAL_NUMBER, TAG_ASCII_STRING, TAG_DISPLAY_DEVICE_DATA, TAG_POWER_SEQUENCING, TAG_TRANSFER_CHARACTERISTICS, TAG_DISPLAY_INTERFACE, TAG_STEREO_DISPLAY_INTERFACE, TAG_TYPE_V_TIMING, TAG_TILED_TOPOLOGY, TAG_TYPE_VI_TIMING, ];
#[cfg(test)]
const DEFERRED_OR_RESERVED_TAG_RANGES: &[(u8, u8)] = &[
(0x14, 0x7E), (0x7F, 0x7F), (0x80, 0xFF), ];
pub static DISPLAYID_HANDLER: &dyn StaticExtensionHandler = &DisplayIdHandler;
#[cfg(test)]
#[cfg(any(feature = "alloc", feature = "std"))]
mod tests {
use super::*;
use crate::model::extension::ExtensionHandler;
use crate::model::manufacture::ManufacturerId;
fn make_displayid_block(version: u8, data_blocks: &[u8]) -> [u8; 128] {
let mut block = [0u8; 128];
block[0] = 0x70; block[1] = version;
let section_byte_count = data_blocks.len().min(122);
block[2] = section_byte_count as u8;
block[3] = 0x00; let end = 4 + section_byte_count;
block[4..end].copy_from_slice(&data_blocks[..section_byte_count]);
let sum: u8 = block[1..end]
.iter()
.fold(0u8, |acc, &x| acc.wrapping_add(x));
block[end] = 0u8.wrapping_sub(sum);
let edid_sum: u8 = block[..127].iter().fold(0u8, |acc, &x| acc.wrapping_add(x));
block[127] = 0u8.wrapping_sub(edid_sum);
block
}
fn fix_checksums(block: &mut [u8; 128]) {
let n = block[2] as usize;
let checksum_pos = 4 + n;
if checksum_pos <= 126 {
let sum: u8 = block[1..checksum_pos]
.iter()
.fold(0u8, |acc, &x| acc.wrapping_add(x));
block[checksum_pos] = 0u8.wrapping_sub(sum);
}
let edid_sum: u8 = block[..127].iter().fold(0u8, |acc, &x| acc.wrapping_add(x));
block[127] = 0u8.wrapping_sub(edid_sum);
}
fn make_type_i_descriptor(
pixel_clock_10khz: u16,
h_active: u16,
h_blank: u16,
h_fp: u16,
h_sw: u16,
v_active: u16,
v_blank: u16,
v_fp: u16,
v_sw: u16,
flags: u8,
) -> [u8; 20] {
let mut d = [0u8; 20];
d[0] = 0x00;
d[1..3].copy_from_slice(&pixel_clock_10khz.to_le_bytes());
d[3..5].copy_from_slice(&h_active.to_le_bytes());
d[5..7].copy_from_slice(&h_blank.to_le_bytes());
d[7..9].copy_from_slice(&h_fp.to_le_bytes());
d[9..11].copy_from_slice(&h_sw.to_le_bytes());
d[11..13].copy_from_slice(&v_active.to_le_bytes());
d[13..15].copy_from_slice(&v_blank.to_le_bytes());
d[15..17].copy_from_slice(&v_fp.to_le_bytes());
d[17..19].copy_from_slice(&v_sw.to_le_bytes());
d[19] = flags;
d
}
fn make_type_i_data_block(descriptor: &[u8; 20]) -> [u8; 23] {
let mut db = [0u8; 23];
db[0] = TAG_TYPE_I_TIMING;
db[1] = 0x00;
db[2] = 20;
db[3..23].copy_from_slice(descriptor);
db
}
#[test]
fn test_unknown_version_emits_warning() {
let block = make_displayid_block(0x05, &[]);
let mut caps = DisplayCapabilities::default();
let mut warnings: Vec<ParseWarning> = Vec::new();
ExtensionHandler::process(&DisplayIdHandler, &[&block], &mut caps, &mut warnings);
assert_eq!(warnings.len(), 1);
let w = (*warnings[0]).downcast_ref::<EdidWarning>().unwrap();
assert_eq!(*w, EdidWarning::DisplayIdVersionUnknown(0x05));
}
#[test]
fn test_extension_count_mismatch_warning() {
let mut block = make_displayid_block(0x10, &[]);
block[3] = 0x08; fix_checksums(&mut block);
let mut caps = DisplayCapabilities::default();
let mut warnings: Vec<ParseWarning> = Vec::new();
ExtensionHandler::process(&DisplayIdHandler, &[&block], &mut caps, &mut warnings);
assert_eq!(warnings.len(), 1);
let w = (*warnings[0]).downcast_ref::<EdidWarning>().unwrap();
assert_eq!(
*w,
EdidWarning::DisplayIdExtensionCountMismatch {
declared: 1,
found: 0
}
);
}
#[test]
fn test_v2_accepted_without_warning() {
let block = make_displayid_block(0x20, &[]);
let mut caps = DisplayCapabilities::default();
let mut warnings: Vec<ParseWarning> = Vec::new();
ExtensionHandler::process(&DisplayIdHandler, &[&block], &mut caps, &mut warnings);
assert!(warnings.is_empty());
}
#[test]
fn test_displayid_capabilities_stored() {
let mut block = make_displayid_block(0x13, &[]);
block[3] = 0x02; fix_checksums(&mut block);
let mut caps = DisplayCapabilities::default();
let mut warnings: Vec<ParseWarning> = Vec::new();
ExtensionHandler::process(&DisplayIdHandler, &[&block], &mut caps, &mut warnings);
assert!(warnings.is_empty());
let did = caps
.get_extension_data::<DisplayIdCapabilities>(0x70)
.unwrap();
assert_eq!(did.version, 0x13);
assert_eq!(did.product_type, 2);
}
#[test]
fn test_multi_fragment_reassembly() {
let desc1 = make_type_i_descriptor(14850, 1920, 280, 88, 44, 1080, 45, 4, 5, 0x00);
let db1 = make_type_i_data_block(&desc1);
let mut block1 = make_displayid_block(0x10, &db1);
block1[3] = 0x08; fix_checksums(&mut block1);
let desc2 = make_type_i_descriptor(22118, 2560, 440, 80, 32, 1440, 41, 4, 5, 0x00);
let db2 = make_type_i_data_block(&desc2);
let block2 = make_displayid_block(0x10, &db2);
let mut caps = DisplayCapabilities::default();
let mut warnings: Vec<ParseWarning> = Vec::new();
ExtensionHandler::process(
&DisplayIdHandler,
&[&block1, &block2],
&mut caps,
&mut warnings,
);
assert!(warnings.is_empty(), "unexpected warnings: {:?}", warnings);
assert_eq!(caps.supported_modes.len(), 2);
assert!(
caps.supported_modes
.iter()
.any(|m| m.width == 1920 && m.height == 1080)
);
assert!(
caps.supported_modes
.iter()
.any(|m| m.width == 2560 && m.height == 1440)
);
}
#[test]
fn test_valid_checksum_no_warning() {
let block = make_displayid_block(0x10, &[]);
let mut caps = DisplayCapabilities::default();
let mut warnings: Vec<ParseWarning> = Vec::new();
ExtensionHandler::process(&DisplayIdHandler, &[&block], &mut caps, &mut warnings);
assert!(warnings.is_empty(), "unexpected warnings: {:?}", warnings);
}
#[test]
fn test_invalid_checksum_emits_warning() {
let mut block = make_displayid_block(0x10, &[]);
let n = block[2] as usize;
block[4 + n] ^= 0xFF;
let edid_sum: u8 = block[..127].iter().fold(0u8, |acc, &x| acc.wrapping_add(x));
block[127] = 0u8.wrapping_sub(edid_sum);
let mut caps = DisplayCapabilities::default();
let mut warnings: Vec<ParseWarning> = Vec::new();
ExtensionHandler::process(&DisplayIdHandler, &[&block], &mut caps, &mut warnings);
assert_eq!(warnings.len(), 1, "expected exactly one warning");
let w = (*warnings[0]).downcast_ref::<EdidWarning>().unwrap();
assert_eq!(*w, EdidWarning::DisplayIdChecksumMismatch);
}
#[test]
fn test_invalid_checksum_static_pipeline_emits_warning() {
let mut block = make_displayid_block(0x10, &[]);
let n = block[2] as usize;
block[4 + n] ^= 0xFF;
let edid_sum: u8 = block[..127].iter().fold(0u8, |acc, &x| acc.wrapping_add(x));
block[127] = 0u8.wrapping_sub(edid_sum);
let mut caps = crate::model::StaticDisplayCapabilities::<4>::default();
let mut ctx = crate::model::StaticContext::new(&mut caps);
StaticExtensionHandler::process(&DisplayIdHandler, &[&block], &mut ctx);
assert_eq!(caps.num_warnings, 1, "expected exactly one warning");
assert_eq!(
caps.warnings[0],
Some(EdidWarning::DisplayIdChecksumMismatch)
);
}
#[test]
fn test_product_id_and_timing_in_same_block() {
let ca = (b'S' - b'A' + 1) as u16;
let cb = (b'A' - b'A' + 1) as u16;
let cc = (b'M' - b'A' + 1) as u16;
let packed: u16 = (ca << 10) | (cb << 5) | cc;
let mut pid_payload = Vec::new();
pid_payload.extend_from_slice(&packed.to_be_bytes());
pid_payload.extend_from_slice(&0xABCDu16.to_le_bytes());
pid_payload.extend_from_slice(&0u32.to_le_bytes()); pid_payload.push(0); pid_payload.push(0); let mut pid_db = vec![TAG_PRODUCT_ID, 0x00, pid_payload.len() as u8];
pid_db.extend_from_slice(&pid_payload);
let desc = make_type_i_descriptor(14850, 1920, 280, 88, 44, 1080, 45, 4, 5, 0x00);
let timing_db = make_type_i_data_block(&desc);
let mut payload = Vec::new();
payload.extend_from_slice(&pid_db);
payload.extend_from_slice(&timing_db);
let block = make_displayid_block(0x10, &payload);
let mut caps = DisplayCapabilities::default();
let mut warnings: Vec<ParseWarning> = Vec::new();
ExtensionHandler::process(&DisplayIdHandler, &[&block], &mut caps, &mut warnings);
assert!(warnings.is_empty());
assert_eq!(caps.manufacturer, Some(ManufacturerId(*b"SAM")));
assert_eq!(caps.product_code, Some(0xABCD));
assert_eq!(caps.supported_modes.len(), 1);
assert_eq!(caps.supported_modes[0].width, 1920);
}
#[test]
fn test_all_block_tags_accounted_for() {
for tag in 0u16..=255 {
let tag = tag as u8;
let implemented = IMPLEMENTED_BLOCK_TAGS.contains(&tag);
let deferred_or_reserved = DEFERRED_OR_RESERVED_TAG_RANGES
.iter()
.any(|&(lo, hi)| tag >= lo && tag <= hi);
assert!(
implemented || deferred_or_reserved,
"DisplayID block tag 0x{:02X} is unaccounted for: \
add it to IMPLEMENTED_BLOCK_TAGS or DEFERRED_OR_RESERVED_TAG_RANGES",
tag
);
}
}
#[test]
fn test_implemented_and_deferred_are_disjoint() {
for &tag in IMPLEMENTED_BLOCK_TAGS {
let in_deferred = DEFERRED_OR_RESERVED_TAG_RANGES
.iter()
.any(|&(lo, hi)| tag >= lo && tag <= hi);
assert!(
!in_deferred,
"DisplayID block tag 0x{:02X} appears in both IMPLEMENTED_BLOCK_TAGS \
and DEFERRED_OR_RESERVED_TAG_RANGES",
tag
);
}
}
}