use super::{
TAG_ASCII_STRING, TAG_COLOR_CHARACTERISTICS, TAG_DISPLAY_DEVICE_DATA, TAG_DISPLAY_INTERFACE,
TAG_DISPLAY_PARAMS, TAG_POWER_SEQUENCING, TAG_PRODUCT_ID, TAG_SERIAL_NUMBER,
TAG_STEREO_DISPLAY_INTERFACE, TAG_TILED_TOPOLOGY, TAG_TRANSFER_CHARACTERISTICS,
TAG_VIDEO_TIMING_RANGE, for_each_data_block,
};
use crate::capabilities::base::{decode_color_bit_depth, decode_manufacture_date};
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::capabilities::DisplayCapabilities;
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::color::{Chromaticity, ChromaticityPoint};
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::diagnostics::EdidWarning;
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::manufacture::{ManufacturerId, MonitorString};
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::panel::{
BacklightType, DisplayIdInterface, DisplayIdStereoInterface, DisplayIdTiledTopology,
DisplayInterfaceType, DisplayTechnology, InterfaceContentProtection, OperatingMode,
PhysicalOrientation, PowerSequencing, RotationCapability, ScanDirection, StereoSyncInterface,
StereoViewingMode, SubpixelLayout, TileBezelInfo, TileTopologyBehavior, ZeroPixelLocation,
};
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::prelude::{Arc, Vec};
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::transfer::{
DisplayIdTransferCharacteristic, TransferCurve, TransferPointEncoding,
};
#[cfg(any(feature = "alloc", feature = "std"))]
pub(super) fn decode_product_id_block(payload: &[u8], caps: &mut DisplayCapabilities) {
if payload.len() >= 2 {
let id_raw = ((payload[0] as u16) << 8) | (payload[1] as u16);
let char1 = ((id_raw >> 10) & 0x1F) as u8;
let char2 = ((id_raw >> 5) & 0x1F) as u8;
let char3 = (id_raw & 0x1F) as u8;
if (1..=26).contains(&char1) && (1..=26).contains(&char2) && (1..=26).contains(&char3) {
caps.manufacturer = Some(ManufacturerId([
char1 + b'A' - 1,
char2 + b'A' - 1,
char3 + b'A' - 1,
]));
}
}
if payload.len() >= 4 {
caps.product_code = Some(u16::from_le_bytes([payload[2], payload[3]]));
}
if payload.len() >= 8 {
let sn = u32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]);
if sn != 0 {
caps.serial_number = Some(sn);
}
}
if payload.len() >= 10 {
caps.manufacture_date = Some(decode_manufacture_date(payload[8], payload[9]));
}
if payload.len() >= 11 {
let name_bytes = &payload[10..];
let mut buf = [b' '; 13];
let copy_len = name_bytes.len().min(13);
buf[..copy_len].copy_from_slice(&name_bytes[..copy_len]);
if !buf.contains(&0x0A) {
let term_pos = copy_len.min(12);
buf[term_pos] = 0x0A;
}
caps.display_name = Some(MonitorString(buf));
}
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg(test)]
pub(super) fn scan_product_id_block(payload: &[u8], caps: &mut DisplayCapabilities) {
let mut found = false;
for_each_data_block(payload, |tag, _revision, block_payload| {
if tag == TAG_PRODUCT_ID && !found {
found = true;
decode_product_id_block(block_payload, caps);
}
});
}
#[cfg(any(feature = "alloc", feature = "std"))]
pub(super) fn decode_display_params_block(payload: &[u8], caps: &mut DisplayCapabilities) {
if payload.len() >= 4 {
let h = u16::from_le_bytes([payload[0], payload[1]]);
let v = u16::from_le_bytes([payload[2], payload[3]]);
if h != 0 && v != 0 {
caps.preferred_image_size_mm = Some((h, v));
}
}
if payload.len() >= 8 {
let h_px = u16::from_le_bytes([payload[4], payload[5]]);
let v_px = u16::from_le_bytes([payload[6], payload[7]]);
if h_px != 0 && v_px != 0 {
caps.native_pixels = Some((h_px, v_px));
}
}
if payload.len() >= 11 {
caps.panel_aspect_ratio_100 = Some(payload[10]);
}
if payload.len() >= 12 {
let bpc = (payload[11] & 0x0F) + 1;
let edid_bits: u8 = match bpc {
6 => 1,
8 => 2,
10 => 3,
12 => 4,
14 => 5,
16 => 6,
_ => 0,
};
caps.color_bit_depth = decode_color_bit_depth(edid_bits);
}
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg(test)]
pub(super) fn scan_display_params_block(payload: &[u8], caps: &mut DisplayCapabilities) {
let mut found = false;
for_each_data_block(payload, |tag, _revision, block_payload| {
if tag == TAG_DISPLAY_PARAMS && !found {
found = true;
decode_display_params_block(block_payload, caps);
}
});
}
#[cfg(any(feature = "alloc", feature = "std"))]
pub(super) fn decode_color_characteristics_block(payload: &[u8], caps: &mut DisplayCapabilities) {
if payload.len() < 16 {
return;
}
let read_point = |i: usize| ChromaticityPoint {
x_raw: u16::from_le_bytes([payload[i], payload[i + 1]]) & 0x03FF,
y_raw: u16::from_le_bytes([payload[i + 2], payload[i + 3]]) & 0x03FF,
};
caps.chromaticity = Chromaticity {
red: read_point(0),
green: read_point(4),
blue: read_point(8),
white: read_point(12),
};
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg(test)]
pub(super) fn scan_color_characteristics_block(payload: &[u8], caps: &mut DisplayCapabilities) {
let mut found = false;
for_each_data_block(payload, |tag, _revision, block_payload| {
if tag == TAG_COLOR_CHARACTERISTICS && !found {
found = true;
decode_color_characteristics_block(block_payload, caps);
}
});
}
#[cfg(any(feature = "alloc", feature = "std"))]
pub(super) fn decode_video_timing_range_block(payload: &[u8], caps: &mut DisplayCapabilities) {
if payload.len() >= 6 {
let raw = (payload[3] as u32) | ((payload[4] as u32) << 8) | ((payload[5] as u32) << 16);
caps.max_pixel_clock_mhz = Some((raw / 100) as u16);
}
if payload.len() >= 8 {
caps.min_h_rate_khz = Some(payload[6] as u16);
caps.max_h_rate_khz = Some(payload[7] as u16);
}
if payload.len() >= 12 {
caps.min_v_rate = Some(payload[10] as u16);
caps.max_v_rate = Some(payload[11] as u16);
}
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg(test)]
pub(super) fn scan_video_timing_range_block(payload: &[u8], caps: &mut DisplayCapabilities) {
let mut found = false;
for_each_data_block(payload, |tag, _revision, block_payload| {
if tag == TAG_VIDEO_TIMING_RANGE && !found {
found = true;
decode_video_timing_range_block(block_payload, caps);
}
});
}
#[cfg(any(feature = "alloc", feature = "std"))]
pub(super) fn decode_serial_number_block(payload: &[u8], caps: &mut DisplayCapabilities) {
if payload.is_empty() {
return;
}
let mut buf = [b' '; 13];
let copy_len = payload.len().min(13);
buf[..copy_len].copy_from_slice(&payload[..copy_len]);
if !buf.contains(&0x0A) {
buf[copy_len.min(12)] = 0x0A;
}
caps.serial_number_string = Some(MonitorString(buf));
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg(test)]
pub(super) fn scan_serial_number_block(payload: &[u8], caps: &mut DisplayCapabilities) {
let mut found = false;
for_each_data_block(payload, |tag, _revision, block_payload| {
if tag == TAG_SERIAL_NUMBER && !found {
found = true;
decode_serial_number_block(block_payload, caps);
}
});
}
#[cfg(any(feature = "alloc", feature = "std"))]
pub(super) fn decode_ascii_string_block(payload: &[u8], caps: &mut DisplayCapabilities) {
if payload.is_empty() {
return;
}
let Some(slot) = caps.unspecified_text.iter_mut().find(|s| s.is_none()) else {
return;
};
let mut buf = [b' '; 13];
let copy_len = payload.len().min(13);
buf[..copy_len].copy_from_slice(&payload[..copy_len]);
if !buf.contains(&0x0A) {
buf[copy_len.min(12)] = 0x0A;
}
*slot = Some(MonitorString(buf));
}
#[cfg(any(feature = "alloc", feature = "std"))]
pub(super) fn decode_display_device_data_block(payload: &[u8], caps: &mut DisplayCapabilities) {
if !payload.is_empty() {
caps.display_technology = Some(DisplayTechnology::from_nibble(payload[0] >> 4));
caps.display_subtype = Some(payload[0] & 0x0F);
}
if payload.len() >= 2 {
caps.operating_mode = Some(OperatingMode::from_nibble(payload[1] & 0x0F));
caps.backlight_type = Some(BacklightType::from_bits((payload[1] >> 4) & 0x03));
caps.data_enable_used = Some((payload[1] & 0x40) != 0);
caps.data_enable_positive = Some((payload[1] & 0x80) != 0);
}
if payload.len() >= 6 {
let h = u16::from_le_bytes([payload[2], payload[3]]);
let v = u16::from_le_bytes([payload[4], payload[5]]);
if h != 0 && v != 0 {
caps.native_pixels = Some((h, v));
}
}
if payload.len() >= 7 {
caps.panel_aspect_ratio_100 = Some(payload[6]);
}
if payload.len() >= 8 {
caps.physical_orientation = Some(PhysicalOrientation::from_bits(payload[7] & 0x03));
caps.rotation_capability = Some(RotationCapability::from_bits((payload[7] >> 2) & 0x03));
caps.zero_pixel_location = Some(ZeroPixelLocation::from_bits((payload[7] >> 4) & 0x03));
caps.scan_direction = Some(ScanDirection::from_bits((payload[7] >> 6) & 0x03));
}
if payload.len() >= 9 {
caps.subpixel_layout = Some(SubpixelLayout::from_byte(payload[8]));
}
if payload.len() >= 11 {
let h_pitch = payload[9];
let v_pitch = payload[10];
if h_pitch != 0 && v_pitch != 0 {
caps.pixel_pitch_hundredths_mm = Some((h_pitch, v_pitch));
}
}
if payload.len() >= 12 {
let bpc = (payload[11] & 0x0F) + 1;
let edid_bits: u8 = match bpc {
6 => 1,
8 => 2,
10 => 3,
12 => 4,
14 => 5,
16 => 6,
_ => 0,
};
caps.color_bit_depth = decode_color_bit_depth(edid_bits);
}
if payload.len() >= 13 {
let rt = payload[12];
if rt != 0 {
caps.pixel_response_time_ms = Some(rt);
}
}
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg(test)]
pub(super) fn scan_display_device_data_block(payload: &[u8], caps: &mut DisplayCapabilities) {
let mut found = false;
for_each_data_block(payload, |tag, _revision, block_payload| {
if tag == TAG_DISPLAY_DEVICE_DATA && !found {
found = true;
decode_display_device_data_block(block_payload, caps);
}
});
}
#[cfg(any(feature = "alloc", feature = "std"))]
pub(super) fn decode_power_sequencing_block(payload: &[u8], caps: &mut DisplayCapabilities) {
if payload.len() >= 6 {
caps.power_sequencing = Some(PowerSequencing::new(
payload[0], payload[1], payload[2], payload[3], payload[4], payload[5],
));
}
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg(test)]
pub(super) fn scan_power_sequencing_block(payload: &[u8], caps: &mut DisplayCapabilities) {
let mut found = false;
for_each_data_block(payload, |tag, _revision, block_payload| {
if tag == TAG_POWER_SEQUENCING && !found {
found = true;
decode_power_sequencing_block(block_payload, caps);
}
});
}
#[cfg(any(feature = "alloc", feature = "std"))]
pub(super) fn decode_transfer_characteristics_block(
payload: &[u8],
caps: &mut DisplayCapabilities,
) {
if payload.len() < 2 {
return;
}
let encoding = match (payload[0] >> 6) & 0x03 {
0x00 => TransferPointEncoding::Bits8,
0x01 => TransferPointEncoding::Bits10,
0x02 => TransferPointEncoding::Bits12,
bits => {
caps.warnings
.push(Arc::new(EdidWarning::UnknownTransferEncoding(bits)));
return;
}
};
let multi_channel = (payload[0] & 0x20) != 0;
let data = &payload[1..];
fn unpack8(src: &[u8]) -> Vec<f32> {
src.iter().map(|&b| b as f32 / 255.0).collect()
}
fn unpack10(src: &[u8]) -> Vec<f32> {
let mut pts = Vec::new();
let mut i = 0;
while i + 5 <= src.len() {
let [b0, b1, b2, b3, b4] = [
src[i] as u16,
src[i + 1] as u16,
src[i + 2] as u16,
src[i + 3] as u16,
src[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
}
fn unpack12(src: &[u8]) -> Vec<f32> {
let mut pts = Vec::new();
let mut i = 0;
while i + 3 <= src.len() {
let [b0, b1, b2] = [src[i] as u16, src[i + 1] as u16, src[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
}
let curve = if multi_channel {
let total = data.len();
if total % 3 != 0 {
return; }
let region = total / 3;
let (r_data, rest) = data.split_at(region);
let (g_data, b_data) = rest.split_at(region);
let (red, green, blue) = match encoding {
TransferPointEncoding::Bits8 => (unpack8(r_data), unpack8(g_data), unpack8(b_data)),
TransferPointEncoding::Bits10 => (unpack10(r_data), unpack10(g_data), unpack10(b_data)),
TransferPointEncoding::Bits12 => (unpack12(r_data), unpack12(g_data), unpack12(b_data)),
_ => return,
};
TransferCurve::Rgb { red, green, blue }
} else {
let points = match encoding {
TransferPointEncoding::Bits8 => unpack8(data),
TransferPointEncoding::Bits10 => unpack10(data),
TransferPointEncoding::Bits12 => unpack12(data),
_ => return,
};
TransferCurve::Luminance(points)
};
caps.transfer_characteristic = Some(DisplayIdTransferCharacteristic::new(encoding, curve));
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg(test)]
pub(super) fn scan_transfer_characteristics_block(payload: &[u8], caps: &mut DisplayCapabilities) {
let mut found = false;
for_each_data_block(payload, |tag, _revision, block_payload| {
if tag == TAG_TRANSFER_CHARACTERISTICS && !found {
found = true;
decode_transfer_characteristics_block(block_payload, caps);
}
});
}
#[cfg(any(feature = "alloc", feature = "std"))]
pub(super) fn decode_display_interface_block(payload: &[u8], caps: &mut DisplayCapabilities) {
if payload.len() < 7 {
return;
}
let interface_type = DisplayInterfaceType::from_nibble(payload[0]);
let spread_spectrum = (payload[0] & 0x10) != 0;
let num_lanes = payload[1] & 0x0F;
let min_pixel_clock_10khz = u32::from(u16::from_le_bytes([payload[2], payload[3]]));
let max_pixel_clock_10khz = u32::from(u16::from_le_bytes([payload[4], payload[5]]));
let content_protection = InterfaceContentProtection::from_bits(payload[6]);
caps.display_id_interface = Some(DisplayIdInterface::new(
interface_type,
spread_spectrum,
num_lanes,
min_pixel_clock_10khz,
max_pixel_clock_10khz,
content_protection,
));
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg(test)]
pub(super) fn scan_display_interface_block(payload: &[u8], caps: &mut DisplayCapabilities) {
let mut found = false;
for_each_data_block(payload, |tag, _revision, block_payload| {
if tag == TAG_DISPLAY_INTERFACE && !found {
found = true;
decode_display_interface_block(block_payload, caps);
}
});
}
#[cfg(any(feature = "alloc", feature = "std"))]
pub(super) fn decode_stereo_display_interface_block(
payload: &[u8],
caps: &mut DisplayCapabilities,
) {
if payload.len() < 2 {
return;
}
let viewing_mode = StereoViewingMode::from_nibble(payload[0]);
let sync_polarity_positive = (payload[0] & 0x10) != 0;
let sync_interface = StereoSyncInterface::from_byte(payload[1]);
caps.stereo_interface = Some(DisplayIdStereoInterface::new(
viewing_mode,
sync_polarity_positive,
sync_interface,
));
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg(test)]
pub(super) fn scan_stereo_display_interface_block(payload: &[u8], caps: &mut DisplayCapabilities) {
let mut found = false;
for_each_data_block(payload, |tag, _revision, block_payload| {
if tag == TAG_STEREO_DISPLAY_INTERFACE && !found {
found = true;
decode_stereo_display_interface_block(block_payload, caps);
}
});
}
#[cfg(any(feature = "alloc", feature = "std"))]
pub(super) fn decode_tiled_topology_block(payload: &[u8], caps: &mut DisplayCapabilities) {
if payload.len() < 7 {
return;
}
let single_enclosure = (payload[0] & 0x80) != 0;
let has_bezel_info = (payload[0] & 0x40) != 0;
let topology_behavior = TileTopologyBehavior::from_bits((payload[0] >> 4) & 0x03);
let h_tile_count = (payload[1] >> 4) + 1;
let v_tile_count = (payload[1] & 0x0F) + 1;
let h_tile_location = payload[2] >> 4;
let v_tile_location = payload[2] & 0x0F;
let tile_width_px = u16::from_le_bytes([payload[3], payload[4]]);
let tile_height_px = u16::from_le_bytes([payload[5], payload[6]]);
let bezel = if has_bezel_info && payload.len() >= 11 {
Some(TileBezelInfo::new(
payload[7],
payload[8],
payload[9],
payload[10],
))
} else {
None
};
caps.tiled_topology = Some(DisplayIdTiledTopology::new(
single_enclosure,
topology_behavior,
h_tile_count,
v_tile_count,
h_tile_location,
v_tile_location,
tile_width_px,
tile_height_px,
bezel,
));
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg(test)]
pub(super) fn scan_tiled_topology_block(payload: &[u8], caps: &mut DisplayCapabilities) {
let mut found = false;
for_each_data_block(payload, |tag, _revision, block_payload| {
if tag == TAG_TILED_TOPOLOGY && !found {
found = true;
decode_tiled_topology_block(block_payload, caps);
}
});
}
#[cfg(any(feature = "alloc", feature = "std"))]
pub(super) fn scan_all_metadata_blocks(payload: &[u8], caps: &mut DisplayCapabilities) {
let mut found_product_id = false;
let mut found_display_params = false;
let mut found_color_characteristics = false;
let mut found_video_timing_range = false;
let mut found_serial_number = false;
let mut found_display_device_data = false;
let mut found_power_sequencing = false;
let mut found_transfer_characteristics = false;
let mut found_display_interface = false;
let mut found_stereo_display_interface = false;
let mut found_tiled_topology = false;
for_each_data_block(payload, |tag, _revision, block_payload| match tag {
TAG_PRODUCT_ID if !found_product_id => {
found_product_id = true;
decode_product_id_block(block_payload, caps);
}
TAG_DISPLAY_PARAMS if !found_display_params => {
found_display_params = true;
decode_display_params_block(block_payload, caps);
}
TAG_COLOR_CHARACTERISTICS if !found_color_characteristics => {
found_color_characteristics = true;
decode_color_characteristics_block(block_payload, caps);
}
TAG_VIDEO_TIMING_RANGE if !found_video_timing_range => {
found_video_timing_range = true;
decode_video_timing_range_block(block_payload, caps);
}
TAG_SERIAL_NUMBER if !found_serial_number => {
found_serial_number = true;
decode_serial_number_block(block_payload, caps);
}
TAG_ASCII_STRING => {
decode_ascii_string_block(block_payload, caps);
}
TAG_DISPLAY_DEVICE_DATA if !found_display_device_data => {
found_display_device_data = true;
decode_display_device_data_block(block_payload, caps);
}
TAG_POWER_SEQUENCING if !found_power_sequencing => {
found_power_sequencing = true;
decode_power_sequencing_block(block_payload, caps);
}
TAG_TRANSFER_CHARACTERISTICS if !found_transfer_characteristics => {
found_transfer_characteristics = true;
decode_transfer_characteristics_block(block_payload, caps);
}
TAG_DISPLAY_INTERFACE if !found_display_interface => {
found_display_interface = true;
decode_display_interface_block(block_payload, caps);
}
TAG_STEREO_DISPLAY_INTERFACE if !found_stereo_display_interface => {
found_stereo_display_interface = true;
decode_stereo_display_interface_block(block_payload, caps);
}
TAG_TILED_TOPOLOGY if !found_tiled_topology => {
found_tiled_topology = true;
decode_tiled_topology_block(block_payload, caps);
}
_ => {}
});
}
#[cfg(test)]
#[cfg(any(feature = "alloc", feature = "std"))]
mod tests {
use super::*;
use crate::model::color::{Chromaticity, ColorBitDepth};
use crate::model::manufacture::{ManufactureDate, ManufacturerId, MonitorString};
fn make_product_id_payload(
manufacturer_raw: u16, product_code: u16,
serial: u32,
week: u8,
year_offset: u8, name: Option<&[u8]>,
) -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(&manufacturer_raw.to_be_bytes());
v.extend_from_slice(&product_code.to_le_bytes());
v.extend_from_slice(&serial.to_le_bytes());
v.push(week);
v.push(year_offset);
if let Some(n) = name {
v.extend_from_slice(n);
}
v
}
fn make_display_params_payload(
h_tenths_mm: u16,
v_tenths_mm: u16,
h_native_px: u16,
v_native_px: u16,
aspect_byte: u8, depth_byte: u8, ) -> [u8; 12] {
let mut p = [0u8; 12];
p[0..2].copy_from_slice(&h_tenths_mm.to_le_bytes());
p[2..4].copy_from_slice(&v_tenths_mm.to_le_bytes());
p[4..6].copy_from_slice(&h_native_px.to_le_bytes());
p[6..8].copy_from_slice(&v_native_px.to_le_bytes());
p[10] = aspect_byte;
p[11] = depth_byte;
p
}
fn make_color_characteristics_payload(
red: (u16, u16),
green: (u16, u16),
blue: (u16, u16),
white: (u16, u16),
) -> [u8; 16] {
let mut p = [0u8; 16];
let mut write = |offset: usize, val: (u16, u16)| {
p[offset..offset + 2].copy_from_slice(&val.0.to_le_bytes());
p[offset + 2..offset + 4].copy_from_slice(&val.1.to_le_bytes());
};
write(0, red);
write(4, green);
write(8, blue);
write(12, white);
p
}
fn pack_manufacturer_id(a: u8, b: u8, c: u8) -> u16 {
let ca = (a - b'A' + 1) as u16;
let cb = (b - b'A' + 1) as u16;
let cc = (c - b'A' + 1) as u16;
(ca << 10) | (cb << 5) | cc
}
#[test]
fn test_product_id_manufacturer_and_product_code() {
let packed = pack_manufacturer_id(b'S', b'A', b'M');
let payload = make_product_id_payload(packed, 0x1234, 0, 0, 0, None);
let mut caps = DisplayCapabilities::default();
decode_product_id_block(&payload, &mut caps);
assert_eq!(caps.manufacturer, Some(ManufacturerId(*b"SAM")));
assert_eq!(caps.product_code, Some(0x1234));
}
#[test]
fn test_product_id_serial_number() {
let packed = pack_manufacturer_id(b'D', b'E', b'L');
let payload = make_product_id_payload(packed, 0x0001, 0xDEADBEEF, 0, 0, None);
let mut caps = DisplayCapabilities::default();
decode_product_id_block(&payload, &mut caps);
assert_eq!(caps.serial_number, Some(0xDEAD_BEEF));
}
#[test]
fn test_product_id_zero_serial_not_stored() {
let packed = pack_manufacturer_id(b'G', b'S', b'M');
let payload = make_product_id_payload(packed, 0x0001, 0, 0, 0, None);
let mut caps = DisplayCapabilities::default();
decode_product_id_block(&payload, &mut caps);
assert_eq!(caps.serial_number, None);
}
#[test]
fn test_product_id_manufacture_date() {
let packed = pack_manufacturer_id(b'A', b'P', b'L');
let payload = make_product_id_payload(packed, 0x0001, 0, 10, 30, None);
let mut caps = DisplayCapabilities::default();
decode_product_id_block(&payload, &mut caps);
assert_eq!(
caps.manufacture_date,
Some(ManufactureDate::Manufactured {
week: Some(10),
year: 2020
})
);
}
#[test]
fn test_product_id_display_name() {
let packed = pack_manufacturer_id(b'H', b'W', b'P');
let name: &[u8] = b"Z27k G2\x0a ";
let payload = make_product_id_payload(packed, 0x0042, 0, 0, 34, Some(name));
let mut caps = DisplayCapabilities::default();
decode_product_id_block(&payload, &mut caps);
assert_eq!(caps.display_name.as_deref(), Some("Z27k G2"));
}
#[test]
fn test_product_id_too_short_does_not_panic() {
let payload = [0xFFu8];
let mut caps = DisplayCapabilities::default();
decode_product_id_block(&payload, &mut caps);
assert_eq!(caps.manufacturer, None);
assert_eq!(caps.product_code, None);
}
#[test]
fn test_display_params_image_size() {
let payload = make_display_params_payload(597, 336, 0, 0, 0, 0x00);
let mut caps = DisplayCapabilities::default();
decode_display_params_block(&payload, &mut caps);
assert_eq!(caps.preferred_image_size_mm, Some((597, 336)));
}
#[test]
fn test_display_params_zero_size_not_stored() {
let payload = make_display_params_payload(0, 0, 0, 0, 0, 0x00);
let mut caps = DisplayCapabilities::default();
decode_display_params_block(&payload, &mut caps);
assert_eq!(caps.preferred_image_size_mm, None);
}
#[test]
fn test_display_params_partial_zero_size_not_stored() {
let payload = make_display_params_payload(597, 0, 0, 0, 0, 0x00);
let mut caps = DisplayCapabilities::default();
decode_display_params_block(&payload, &mut caps);
assert_eq!(caps.preferred_image_size_mm, None);
}
#[test]
fn test_display_params_native_pixels() {
let payload = make_display_params_payload(597, 336, 1920, 1080, 0, 0x00);
let mut caps = DisplayCapabilities::default();
decode_display_params_block(&payload, &mut caps);
assert_eq!(caps.native_pixels, Some((1920, 1080)));
}
#[test]
fn test_display_params_zero_native_pixels_not_stored() {
let payload = make_display_params_payload(597, 336, 0, 0, 0, 0x00);
let mut caps = DisplayCapabilities::default();
decode_display_params_block(&payload, &mut caps);
assert_eq!(caps.native_pixels, None);
}
#[test]
fn test_display_params_aspect_ratio() {
let payload = make_display_params_payload(597, 336, 0, 0, 78, 0x00);
let mut caps = DisplayCapabilities::default();
decode_display_params_block(&payload, &mut caps);
assert_eq!(caps.panel_aspect_ratio_100, Some(78));
}
#[test]
fn test_display_params_color_bit_depth_8bpc() {
let payload = make_display_params_payload(597, 336, 0, 0, 0, 0x07);
let mut caps = DisplayCapabilities::default();
decode_display_params_block(&payload, &mut caps);
assert_eq!(caps.color_bit_depth, Some(ColorBitDepth::Depth8));
}
#[test]
fn test_display_params_color_bit_depth_10bpc() {
let payload = make_display_params_payload(597, 336, 0, 0, 0, 0x09);
let mut caps = DisplayCapabilities::default();
decode_display_params_block(&payload, &mut caps);
assert_eq!(caps.color_bit_depth, Some(ColorBitDepth::Depth10));
}
#[test]
fn test_display_params_undefined_bit_depth_not_stored() {
let payload = make_display_params_payload(597, 336, 0, 0, 0, 0x00);
let mut caps = DisplayCapabilities::default();
decode_display_params_block(&payload, &mut caps);
assert_eq!(caps.color_bit_depth, None);
}
#[test]
fn test_display_params_too_short_does_not_panic() {
let payload = [0x55u8, 0x01, 0x00];
let mut caps = DisplayCapabilities::default();
decode_display_params_block(&payload, &mut caps);
assert_eq!(caps.preferred_image_size_mm, None);
}
#[test]
fn test_color_characteristics_primaries_decoded() {
let payload =
make_color_characteristics_payload((655, 338), (307, 614), (154, 61), (320, 337));
let mut caps = DisplayCapabilities::default();
decode_color_characteristics_block(&payload, &mut caps);
assert_eq!(caps.chromaticity.red.x_raw, 655);
assert_eq!(caps.chromaticity.red.y_raw, 338);
assert_eq!(caps.chromaticity.green.x_raw, 307);
assert_eq!(caps.chromaticity.green.y_raw, 614);
assert_eq!(caps.chromaticity.blue.x_raw, 154);
assert_eq!(caps.chromaticity.blue.y_raw, 61);
assert_eq!(caps.chromaticity.white.x_raw, 320);
assert_eq!(caps.chromaticity.white.y_raw, 337);
}
#[test]
fn test_color_characteristics_upper_bits_masked() {
let payload = make_color_characteristics_payload(
(0x04FF, 0x04FF),
(0x04FF, 0x04FF),
(0x04FF, 0x04FF),
(0x04FF, 0x04FF),
);
let mut caps = DisplayCapabilities::default();
decode_color_characteristics_block(&payload, &mut caps);
assert_eq!(caps.chromaticity.red.x_raw, 0x00FF);
}
#[test]
fn test_color_characteristics_short_payload_ignored() {
let payload = [0u8; 15];
let mut caps = DisplayCapabilities::default();
decode_color_characteristics_block(&payload, &mut caps);
assert_eq!(caps.chromaticity, Chromaticity::default());
}
fn make_video_timing_range_payload(
min_pixel_clock_10khz: u32,
max_pixel_clock_10khz: u32,
min_h_khz: u8,
max_h_khz: u8,
min_h_blank: u16,
min_v_rate: u8,
max_v_rate: u8,
min_v_blank: u16,
flags: u8,
) -> [u8; 15] {
let mut p = [0u8; 15];
p[0] = (min_pixel_clock_10khz & 0xFF) as u8;
p[1] = ((min_pixel_clock_10khz >> 8) & 0xFF) as u8;
p[2] = ((min_pixel_clock_10khz >> 16) & 0xFF) as u8;
p[3] = (max_pixel_clock_10khz & 0xFF) as u8;
p[4] = ((max_pixel_clock_10khz >> 8) & 0xFF) as u8;
p[5] = ((max_pixel_clock_10khz >> 16) & 0xFF) as u8;
p[6] = min_h_khz;
p[7] = max_h_khz;
p[8..10].copy_from_slice(&min_h_blank.to_le_bytes());
p[10] = min_v_rate;
p[11] = max_v_rate;
p[12..14].copy_from_slice(&min_v_blank.to_le_bytes());
p[14] = flags;
p
}
#[test]
fn test_video_timing_range_all_fields_decoded() {
let payload = make_video_timing_range_payload(1000, 33750, 30, 135, 160, 48, 240, 45, 0);
let mut caps = DisplayCapabilities::default();
decode_video_timing_range_block(&payload, &mut caps);
assert_eq!(caps.max_pixel_clock_mhz, Some(337));
assert_eq!(caps.min_h_rate_khz, Some(30));
assert_eq!(caps.max_h_rate_khz, Some(135));
assert_eq!(caps.min_v_rate, Some(48));
assert_eq!(caps.max_v_rate, Some(240));
}
#[test]
fn test_video_timing_range_typical_monitor() {
let payload = make_video_timing_range_payload(3000, 14850, 30, 83, 160, 56, 75, 45, 0x00);
let mut caps = DisplayCapabilities::default();
decode_video_timing_range_block(&payload, &mut caps);
assert_eq!(caps.max_pixel_clock_mhz, Some(148));
assert_eq!(caps.min_h_rate_khz, Some(30));
assert_eq!(caps.max_h_rate_khz, Some(83));
assert_eq!(caps.min_v_rate, Some(56));
assert_eq!(caps.max_v_rate, Some(75));
}
#[test]
fn test_video_timing_range_short_payload_partial_decode() {
let mut payload = [0u8; 8];
payload[3] = 0x52; payload[4] = 0x39; payload[6] = 25; payload[7] = 90; let mut caps = DisplayCapabilities::default();
decode_video_timing_range_block(&payload, &mut caps);
assert_eq!(caps.max_pixel_clock_mhz, Some(146));
assert_eq!(caps.min_h_rate_khz, Some(25));
assert_eq!(caps.max_h_rate_khz, Some(90));
assert_eq!(caps.min_v_rate, None);
assert_eq!(caps.max_v_rate, None);
}
#[test]
fn test_video_timing_range_too_short_does_not_panic() {
let payload = [0u8; 5];
let mut caps = DisplayCapabilities::default();
decode_video_timing_range_block(&payload, &mut caps);
assert_eq!(caps.max_pixel_clock_mhz, None);
assert_eq!(caps.min_h_rate_khz, None);
assert_eq!(caps.min_v_rate, None);
}
#[test]
fn test_serial_number_short_string() {
let payload = b"SN12345\x0a ";
let mut caps = DisplayCapabilities::default();
decode_serial_number_block(payload, &mut caps);
assert_eq!(caps.serial_number_string.as_deref(), Some("SN12345"));
}
#[test]
fn test_serial_number_full_13_bytes_no_terminator_gets_one_added() {
let payload = b"ABCDEFGHIJKLM";
let mut caps = DisplayCapabilities::default();
decode_serial_number_block(payload, &mut caps);
let s = caps.serial_number_string.unwrap();
assert_eq!(s.0[12], 0x0A);
}
#[test]
fn test_serial_number_truncated_at_13_bytes() {
let payload = b"ABCDEFGHIJKLMNOPQRST";
let mut caps = DisplayCapabilities::default();
decode_serial_number_block(payload, &mut caps);
assert_eq!(caps.serial_number_string.as_deref(), Some("ABCDEFGHIJKL"));
}
#[test]
fn test_serial_number_empty_payload_not_stored() {
let payload: &[u8] = &[];
let mut caps = DisplayCapabilities::default();
decode_serial_number_block(payload, &mut caps);
assert_eq!(caps.serial_number_string, None);
}
#[test]
fn test_ascii_string_stored_in_first_slot() {
let payload = b"Hello\x0a ";
let mut caps = DisplayCapabilities::default();
decode_ascii_string_block(payload, &mut caps);
assert_eq!(caps.unspecified_text[0].as_deref(), Some("Hello"));
assert!(caps.unspecified_text[1].is_none());
}
#[test]
fn test_ascii_string_multiple_blocks_fill_slots() {
let mut caps = DisplayCapabilities::default();
decode_ascii_string_block(b"First\x0a ", &mut caps);
decode_ascii_string_block(b"Second\x0a ", &mut caps);
assert_eq!(caps.unspecified_text[0].as_deref(), Some("First"));
assert_eq!(caps.unspecified_text[1].as_deref(), Some("Second"));
assert!(caps.unspecified_text[2].is_none());
}
#[test]
fn test_ascii_string_overflow_beyond_four_slots_dropped() {
let mut caps = DisplayCapabilities::default();
for i in 0u8..5 {
decode_ascii_string_block(&[b'A' + i, 0x0A], &mut caps);
}
assert!(caps.unspecified_text.iter().all(|s| s.is_some()));
assert_eq!(caps.unspecified_text[3].as_deref(), Some("D"));
}
#[test]
fn test_ascii_string_empty_payload_not_stored() {
let mut caps = DisplayCapabilities::default();
decode_ascii_string_block(&[], &mut caps);
assert!(caps.unspecified_text[0].is_none());
}
use crate::model::panel::{
BacklightType, DisplayTechnology, OperatingMode, PhysicalOrientation, RotationCapability,
ScanDirection, SubpixelLayout, ZeroPixelLocation,
};
#[allow(clippy::too_many_arguments)]
fn make_display_device_data_payload(
tech: u8, subtype: u8, b1: u8, h_native: u16, v_native: u16, ar_100: u8, orient: u8, subpixel: u8, h_pitch: u8, v_pitch: u8, bpc_raw: u8, response: u8, ) -> [u8; 13] {
let mut p = [0u8; 13];
p[0] = (tech << 4) | (subtype & 0x0F);
p[1] = b1;
p[2..4].copy_from_slice(&h_native.to_le_bytes());
p[4..6].copy_from_slice(&v_native.to_le_bytes());
p[6] = ar_100;
p[7] = orient;
p[8] = subpixel;
p[9] = h_pitch;
p[10] = v_pitch;
p[11] = bpc_raw & 0x0F;
p[12] = response;
p
}
#[test]
fn test_display_device_data_technology_decoded() {
let p = make_display_device_data_payload(6, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&p, &mut caps);
assert_eq!(caps.display_technology, Some(DisplayTechnology::Oled));
assert_eq!(caps.display_subtype, Some(2));
}
#[test]
fn test_display_device_data_unknown_technology() {
let p = make_display_device_data_payload(0xF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&p, &mut caps);
assert_eq!(
caps.display_technology,
Some(DisplayTechnology::Unknown(15))
);
}
#[test]
fn test_display_device_data_operating_mode_and_backlight() {
let p = make_display_device_data_payload(0, 0, 0xE1, 0, 0, 0, 0, 0, 0, 0, 0, 0);
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&p, &mut caps);
assert_eq!(caps.operating_mode, Some(OperatingMode::NonContinuous));
assert_eq!(caps.backlight_type, Some(BacklightType::Dc));
assert_eq!(caps.data_enable_used, Some(true));
assert_eq!(caps.data_enable_positive, Some(true));
}
#[test]
fn test_display_device_data_no_de_signal() {
let p = make_display_device_data_payload(0, 0, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0);
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&p, &mut caps);
assert_eq!(caps.data_enable_used, Some(false));
}
#[test]
fn test_display_device_data_native_pixels_decoded() {
let p = make_display_device_data_payload(0, 0, 0, 1920, 1080, 0, 0, 0, 0, 0, 0, 0);
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&p, &mut caps);
assert_eq!(caps.native_pixels, Some((1920, 1080)));
}
#[test]
fn test_display_device_data_zero_native_pixels_not_stored() {
let p = make_display_device_data_payload(0, 0, 0, 0, 1080, 0, 0, 0, 0, 0, 0, 0);
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&p, &mut caps);
assert_eq!(caps.native_pixels, None);
}
#[test]
fn test_display_device_data_aspect_ratio_stored() {
let p = make_display_device_data_payload(0, 0, 0, 0, 0, 78, 0, 0, 0, 0, 0, 0);
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&p, &mut caps);
assert_eq!(caps.panel_aspect_ratio_100, Some(78));
}
#[test]
fn test_display_device_data_orientation_flags() {
let p = make_display_device_data_payload(0, 0, 0, 0, 0, 0, 0x65, 0, 0, 0, 0, 0);
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&p, &mut caps);
assert_eq!(
caps.physical_orientation,
Some(PhysicalOrientation::Portrait)
);
assert_eq!(caps.rotation_capability, Some(RotationCapability::Cw90));
assert_eq!(caps.zero_pixel_location, Some(ZeroPixelLocation::LowerLeft));
assert_eq!(caps.scan_direction, Some(ScanDirection::Normal));
}
#[test]
fn test_display_device_data_subpixel_layout_rgb_vertical() {
let p = make_display_device_data_payload(0, 0, 0, 0, 0, 0, 0, 0x01, 0, 0, 0, 0);
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&p, &mut caps);
assert_eq!(caps.subpixel_layout, Some(SubpixelLayout::RgbVertical));
}
#[test]
fn test_display_device_data_subpixel_layout_unknown() {
let p = make_display_device_data_payload(0, 0, 0, 0, 0, 0, 0, 0xAB, 0, 0, 0, 0);
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&p, &mut caps);
assert_eq!(caps.subpixel_layout, Some(SubpixelLayout::Unknown(0xAB)));
}
#[test]
fn test_display_device_data_pixel_pitch_decoded() {
let p = make_display_device_data_payload(0, 0, 0, 0, 0, 0, 0, 0, 28, 29, 0, 0);
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&p, &mut caps);
assert_eq!(caps.pixel_pitch_hundredths_mm, Some((28, 29)));
}
#[test]
fn test_display_device_data_zero_pixel_pitch_not_stored() {
let p = make_display_device_data_payload(0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0);
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&p, &mut caps);
assert_eq!(caps.pixel_pitch_hundredths_mm, None);
}
#[test]
fn test_display_device_data_8bpc_decoded() {
let p = make_display_device_data_payload(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0);
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&p, &mut caps);
assert_eq!(caps.color_bit_depth, Some(ColorBitDepth::Depth8));
}
#[test]
fn test_display_device_data_10bpc_decoded() {
let p = make_display_device_data_payload(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0);
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&p, &mut caps);
assert_eq!(caps.color_bit_depth, Some(ColorBitDepth::Depth10));
}
#[test]
fn test_display_device_data_unknown_bpc_clears_field() {
let p = make_display_device_data_payload(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
let mut caps = DisplayCapabilities::default();
caps.color_bit_depth = Some(ColorBitDepth::Depth8); decode_display_device_data_block(&p, &mut caps);
assert_eq!(caps.color_bit_depth, None);
}
#[test]
fn test_display_device_data_response_time_decoded() {
let p = make_display_device_data_payload(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5);
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&p, &mut caps);
assert_eq!(caps.pixel_response_time_ms, Some(5));
}
#[test]
fn test_display_device_data_zero_response_time_not_stored() {
let p = make_display_device_data_payload(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&p, &mut caps);
assert_eq!(caps.pixel_response_time_ms, None);
}
#[test]
fn test_display_device_data_short_payload_decodes_available_bytes() {
let payload = [0x60u8, 0x01]; let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&payload, &mut caps);
assert_eq!(caps.display_technology, Some(DisplayTechnology::Oled));
assert_eq!(caps.operating_mode, Some(OperatingMode::NonContinuous));
assert_eq!(caps.native_pixels, None);
assert_eq!(caps.color_bit_depth, None);
}
#[test]
fn test_display_device_data_empty_payload_does_not_panic() {
let mut caps = DisplayCapabilities::default();
decode_display_device_data_block(&[], &mut caps);
assert_eq!(caps.display_technology, None);
assert_eq!(caps.color_bit_depth, None);
}
fn make_power_sequencing_payload(t1: u8, t2: u8, t3: u8, t4: u8, t5: u8, t6: u8) -> [u8; 8] {
[t1, t2, t3, t4, t5, t6, 0x00, 0x00]
}
#[test]
fn test_power_sequencing_all_fields_decoded() {
let payload = make_power_sequencing_payload(10, 5, 3, 2, 50, 20);
let mut caps = DisplayCapabilities::default();
decode_power_sequencing_block(&payload, &mut caps);
let ps = caps
.power_sequencing
.expect("power_sequencing should be Some");
assert_eq!(ps.t1_power_to_signal, 10);
assert_eq!(ps.t2_signal_to_backlight, 5);
assert_eq!(ps.t3_backlight_to_signal_off, 3);
assert_eq!(ps.t4_signal_to_power_off, 2);
assert_eq!(ps.t5_power_off_min, 50);
assert_eq!(ps.t6_backlight_off_min, 20);
}
#[test]
fn test_power_sequencing_zero_delays_stored() {
let payload = make_power_sequencing_payload(0, 0, 0, 0, 0, 0);
let mut caps = DisplayCapabilities::default();
decode_power_sequencing_block(&payload, &mut caps);
assert!(caps.power_sequencing.is_some());
let ps = caps.power_sequencing.unwrap();
assert_eq!(ps.t1_power_to_signal, 0);
assert_eq!(ps.t5_power_off_min, 0);
}
#[test]
fn test_power_sequencing_reserved_bytes_ignored() {
let mut payload = make_power_sequencing_payload(1, 2, 3, 4, 5, 6);
payload[6] = 0xFF;
payload[7] = 0xFF;
let mut caps = DisplayCapabilities::default();
decode_power_sequencing_block(&payload, &mut caps);
let ps = caps.power_sequencing.unwrap();
assert_eq!(ps.t6_backlight_off_min, 6);
}
#[test]
fn test_power_sequencing_exact_6_bytes_accepted() {
let payload = [10u8, 5, 3, 2, 50, 20];
let mut caps = DisplayCapabilities::default();
decode_power_sequencing_block(&payload, &mut caps);
assert!(caps.power_sequencing.is_some());
}
#[test]
fn test_power_sequencing_short_payload_skipped() {
let payload = [10u8, 5, 3, 2, 50]; let mut caps = DisplayCapabilities::default();
decode_power_sequencing_block(&payload, &mut caps);
assert_eq!(caps.power_sequencing, None);
}
#[test]
fn test_power_sequencing_empty_payload_skipped() {
let mut caps = DisplayCapabilities::default();
decode_power_sequencing_block(&[], &mut caps);
assert_eq!(caps.power_sequencing, None);
}
#[test]
fn test_transfer_characteristics_8bit_luminance() {
let payload = [0x00u8, 0x00, 0x80, 0xFF];
let mut caps = DisplayCapabilities::default();
decode_transfer_characteristics_block(&payload, &mut caps);
let tc = caps.transfer_characteristic.expect("should be Some");
assert_eq!(tc.encoding, TransferPointEncoding::Bits8);
let TransferCurve::Luminance(pts) = tc.curve else {
panic!("expected Luminance")
};
assert_eq!(pts.len(), 3);
assert!((pts[0] - 0.0).abs() < 0.001);
assert!((pts[1] - 0x80 as f32 / 255.0).abs() < 0.001);
assert!((pts[2] - 1.0).abs() < 0.001);
}
#[test]
fn test_transfer_characteristics_10bit_luminance() {
let payload = [0x40u8, 0xFF, 0xC0, 0x00, 0x00, 0x00];
let mut caps = DisplayCapabilities::default();
decode_transfer_characteristics_block(&payload, &mut caps);
let tc = caps.transfer_characteristic.expect("should be Some");
assert_eq!(tc.encoding, TransferPointEncoding::Bits10);
let TransferCurve::Luminance(pts) = tc.curve else {
panic!("expected Luminance")
};
assert_eq!(pts.len(), 4);
assert!((pts[0] - 1.0).abs() < 0.001);
assert!((pts[1] - 0.0).abs() < 0.001);
}
#[test]
fn test_transfer_characteristics_12bit_luminance() {
let payload = [0x80u8, 0xFF, 0xF0, 0x00];
let mut caps = DisplayCapabilities::default();
decode_transfer_characteristics_block(&payload, &mut caps);
let tc = caps.transfer_characteristic.expect("should be Some");
assert_eq!(tc.encoding, TransferPointEncoding::Bits12);
let TransferCurve::Luminance(pts) = tc.curve else {
panic!("expected Luminance")
};
assert_eq!(pts.len(), 2);
assert!((pts[0] - 1.0).abs() < 0.001);
assert!((pts[1] - 0.0).abs() < 0.001);
}
#[test]
fn test_transfer_characteristics_8bit_rgb() {
let payload = [0x20u8, 0xFF, 0x80, 0x40, 0x20, 0x10, 0x08];
let mut caps = DisplayCapabilities::default();
decode_transfer_characteristics_block(&payload, &mut caps);
let tc = caps.transfer_characteristic.expect("should be Some");
assert_eq!(tc.encoding, TransferPointEncoding::Bits8);
let TransferCurve::Rgb { red, green, blue } = tc.curve else {
panic!("expected Rgb")
};
assert_eq!(red.len(), 2);
assert_eq!(green.len(), 2);
assert_eq!(blue.len(), 2);
assert!((red[0] - 1.0).abs() < 0.001);
assert!((red[1] - 0x80 as f32 / 255.0).abs() < 0.001);
assert!((green[0] - 0x40 as f32 / 255.0).abs() < 0.001);
assert!((blue[1] - 0x08 as f32 / 255.0).abs() < 0.001);
}
#[test]
fn test_transfer_characteristics_reserved_encoding_skipped() {
let payload = [0xC0u8, 0x00, 0x80, 0xFF];
let mut caps = DisplayCapabilities::default();
decode_transfer_characteristics_block(&payload, &mut caps);
assert_eq!(caps.transfer_characteristic, None);
}
#[test]
fn test_transfer_characteristics_too_short_skipped() {
let payload = [0x00u8]; let mut caps = DisplayCapabilities::default();
decode_transfer_characteristics_block(&payload, &mut caps);
assert_eq!(caps.transfer_characteristic, None);
}
#[test]
fn test_transfer_characteristics_empty_skipped() {
let mut caps = DisplayCapabilities::default();
decode_transfer_characteristics_block(&[], &mut caps);
assert_eq!(caps.transfer_characteristic, None);
}
#[test]
fn test_transfer_characteristics_rgb_non_divisible_by_3_skipped() {
let payload = [0x20u8, 0xFF, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04]; let mut caps = DisplayCapabilities::default();
decode_transfer_characteristics_block(&payload, &mut caps);
assert_eq!(caps.transfer_characteristic, None);
}
fn make_display_interface_payload(
interface_type: u8, spread_spectrum: bool,
num_lanes: u8, min_clock_10khz: u16,
max_clock_10khz: u16,
content_protection: u8, ) -> [u8; 7] {
let mut p = [0u8; 7];
p[0] = (interface_type & 0x0F) | if spread_spectrum { 0x10 } else { 0x00 };
p[1] = num_lanes & 0x0F;
p[2..4].copy_from_slice(&min_clock_10khz.to_le_bytes());
p[4..6].copy_from_slice(&max_clock_10khz.to_le_bytes());
p[6] = content_protection & 0x03;
p
}
#[test]
fn test_display_interface_displayport_no_cp() {
let payload = make_display_interface_payload(0x07, false, 4, 10, 33750, 0);
let mut caps = DisplayCapabilities::default();
decode_display_interface_block(&payload, &mut caps);
let iface = caps.display_id_interface.expect("should be Some");
assert_eq!(iface.interface_type, DisplayInterfaceType::DisplayPort);
assert!(!iface.spread_spectrum);
assert_eq!(iface.num_lanes, 4);
assert_eq!(iface.min_pixel_clock_10khz, 10);
assert_eq!(iface.max_pixel_clock_10khz, 33750);
assert_eq!(iface.content_protection, InterfaceContentProtection::None);
}
#[test]
fn test_display_interface_edp_with_spread_spectrum() {
let payload = make_display_interface_payload(0x06, true, 2, 500, 14850, 0);
let mut caps = DisplayCapabilities::default();
decode_display_interface_block(&payload, &mut caps);
let iface = caps.display_id_interface.expect("should be Some");
assert_eq!(
iface.interface_type,
DisplayInterfaceType::EmbeddedDisplayPort
);
assert!(iface.spread_spectrum);
assert_eq!(iface.num_lanes, 2);
}
#[test]
fn test_display_interface_lvds_dual_hdcp() {
let payload = make_display_interface_payload(0x03, false, 1, 200, 8000, 1);
let mut caps = DisplayCapabilities::default();
decode_display_interface_block(&payload, &mut caps);
let iface = caps.display_id_interface.expect("should be Some");
assert_eq!(iface.interface_type, DisplayInterfaceType::LvdsDual);
assert_eq!(iface.content_protection, InterfaceContentProtection::Hdcp);
}
#[test]
fn test_display_interface_tmds_single_dpcp() {
let payload = make_display_interface_payload(0x04, false, 0, 1000, 14850, 2);
let mut caps = DisplayCapabilities::default();
decode_display_interface_block(&payload, &mut caps);
let iface = caps.display_id_interface.expect("should be Some");
assert_eq!(iface.interface_type, DisplayInterfaceType::TmdsSingle);
assert_eq!(iface.content_protection, InterfaceContentProtection::Dpcp);
}
#[test]
fn test_display_interface_reserved_type_stored() {
let payload = make_display_interface_payload(0x0A, false, 0, 0, 0, 0);
let mut caps = DisplayCapabilities::default();
decode_display_interface_block(&payload, &mut caps);
let iface = caps.display_id_interface.expect("should be Some");
assert_eq!(iface.interface_type, DisplayInterfaceType::Reserved(0x0A));
}
#[test]
fn test_display_interface_short_payload_skipped() {
let payload = [0x07u8, 0x04, 0x0A, 0x00, 0x00, 0x82];
let mut caps = DisplayCapabilities::default();
decode_display_interface_block(&payload, &mut caps);
assert_eq!(caps.display_id_interface, None);
}
#[test]
fn test_display_interface_empty_payload_skipped() {
let mut caps = DisplayCapabilities::default();
decode_display_interface_block(&[], &mut caps);
assert_eq!(caps.display_id_interface, None);
}
fn make_stereo_payload(
viewing_mode: u8, sync_positive: bool, sync_interface: u8, ) -> [u8; 2] {
let b0 = (viewing_mode & 0x0F) | if sync_positive { 0x10 } else { 0x00 };
[b0, sync_interface]
}
#[test]
fn test_stereo_field_sequential_ir() {
let payload = make_stereo_payload(0, true, 2); let mut caps = DisplayCapabilities::default();
decode_stereo_display_interface_block(&payload, &mut caps);
let s = caps.stereo_interface.expect("should be Some");
assert_eq!(s.viewing_mode, StereoViewingMode::FieldSequential);
assert!(s.sync_polarity_positive);
assert_eq!(s.sync_interface, StereoSyncInterface::Infrared);
}
#[test]
fn test_stereo_side_by_side_display_connector() {
let payload = make_stereo_payload(1, false, 0);
let mut caps = DisplayCapabilities::default();
decode_stereo_display_interface_block(&payload, &mut caps);
let s = caps.stereo_interface.expect("should be Some");
assert_eq!(s.viewing_mode, StereoViewingMode::SideBySide);
assert!(!s.sync_polarity_positive);
assert_eq!(s.sync_interface, StereoSyncInterface::DisplayConnector);
}
#[test]
fn test_stereo_top_and_bottom_vesa_din() {
let payload = make_stereo_payload(2, false, 1);
let mut caps = DisplayCapabilities::default();
decode_stereo_display_interface_block(&payload, &mut caps);
let s = caps.stereo_interface.expect("should be Some");
assert_eq!(s.viewing_mode, StereoViewingMode::TopAndBottom);
assert_eq!(s.sync_interface, StereoSyncInterface::VesaDin);
}
#[test]
fn test_stereo_row_interleaved_rf() {
let payload = make_stereo_payload(3, false, 3);
let mut caps = DisplayCapabilities::default();
decode_stereo_display_interface_block(&payload, &mut caps);
let s = caps.stereo_interface.expect("should be Some");
assert_eq!(s.viewing_mode, StereoViewingMode::RowInterleaved);
assert_eq!(s.sync_interface, StereoSyncInterface::RadioFrequency);
}
#[test]
fn test_stereo_pixel_interleaved_reserved_sync() {
let payload = make_stereo_payload(5, false, 0x0A);
let mut caps = DisplayCapabilities::default();
decode_stereo_display_interface_block(&payload, &mut caps);
let s = caps.stereo_interface.expect("should be Some");
assert_eq!(s.viewing_mode, StereoViewingMode::PixelInterleaved);
assert_eq!(s.sync_interface, StereoSyncInterface::Reserved(0x0A));
}
#[test]
fn test_stereo_reserved_viewing_mode_stored() {
let payload = make_stereo_payload(0x09, false, 0);
let mut caps = DisplayCapabilities::default();
decode_stereo_display_interface_block(&payload, &mut caps);
let s = caps.stereo_interface.expect("should be Some");
assert_eq!(s.viewing_mode, StereoViewingMode::Reserved(0x09));
}
#[test]
fn test_stereo_short_payload_skipped() {
let payload = [0x00u8];
let mut caps = DisplayCapabilities::default();
decode_stereo_display_interface_block(&payload, &mut caps);
assert_eq!(caps.stereo_interface, None);
}
#[test]
fn test_stereo_empty_payload_skipped() {
let mut caps = DisplayCapabilities::default();
decode_stereo_display_interface_block(&[], &mut caps);
assert_eq!(caps.stereo_interface, None);
}
fn make_tiled_topology_payload(
single_enclosure: bool,
has_bezel: bool,
behavior: u8, h_tiles_minus1: u8, v_tiles_minus1: u8, h_location: u8,
v_location: u8,
tile_w: u16,
tile_h: u16,
bezel: Option<(u8, u8, u8, u8)>, ) -> Vec<u8> {
let mut v = Vec::new();
let b0 = if single_enclosure { 0x80 } else { 0 }
| if has_bezel { 0x40 } else { 0 }
| ((behavior & 0x03) << 4);
v.push(b0);
v.push((h_tiles_minus1 << 4) | (v_tiles_minus1 & 0x0F));
v.push((h_location << 4) | (v_location & 0x0F));
v.extend_from_slice(&tile_w.to_le_bytes());
v.extend_from_slice(&tile_h.to_le_bytes());
if let Some((top, bot, right, left)) = bezel {
v.extend_from_slice(&[top, bot, right, left]);
}
v
}
#[test]
fn test_tiled_topology_2x2_grid_top_left_tile() {
let payload = make_tiled_topology_payload(true, false, 1, 1, 1, 0, 0, 1920, 1080, None);
let mut caps = DisplayCapabilities::default();
decode_tiled_topology_block(&payload, &mut caps);
let t = caps.tiled_topology.expect("should be Some");
assert!(t.single_enclosure);
assert_eq!(t.topology_behavior, TileTopologyBehavior::RequireAllTiles);
assert_eq!(t.h_tile_count, 2);
assert_eq!(t.v_tile_count, 2);
assert_eq!(t.h_tile_location, 0);
assert_eq!(t.v_tile_location, 0);
assert_eq!(t.tile_width_px, 1920);
assert_eq!(t.tile_height_px, 1080);
assert_eq!(t.bezel, None);
}
#[test]
fn test_tiled_topology_with_bezel_info() {
let payload =
make_tiled_topology_payload(false, true, 2, 2, 0, 1, 0, 2560, 1440, Some((8, 8, 4, 4)));
let mut caps = DisplayCapabilities::default();
decode_tiled_topology_block(&payload, &mut caps);
let t = caps.tiled_topology.expect("should be Some");
assert!(!t.single_enclosure);
assert_eq!(t.topology_behavior, TileTopologyBehavior::ScaleWhenMissing);
assert_eq!(t.h_tile_count, 3);
assert_eq!(t.v_tile_count, 1);
assert_eq!(t.h_tile_location, 1);
assert_eq!(t.v_tile_location, 0);
assert_eq!(t.tile_width_px, 2560);
let bezel = t.bezel.expect("bezel should be Some");
assert_eq!(bezel.top_px, 8);
assert_eq!(bezel.bottom_px, 8);
assert_eq!(bezel.right_px, 4);
assert_eq!(bezel.left_px, 4);
}
#[test]
fn test_tiled_topology_bezel_flag_set_but_payload_too_short_gives_none() {
let payload = make_tiled_topology_payload(
true, true, 0, 1, 1, 0, 0, 1920, 1080, None, );
assert_eq!(payload.len(), 7);
let mut caps = DisplayCapabilities::default();
decode_tiled_topology_block(&payload, &mut caps);
let t = caps.tiled_topology.expect("should be Some");
assert_eq!(t.bezel, None); }
#[test]
fn test_tiled_topology_max_grid_16x16() {
let payload = make_tiled_topology_payload(false, false, 0, 15, 15, 15, 15, 800, 600, None);
let mut caps = DisplayCapabilities::default();
decode_tiled_topology_block(&payload, &mut caps);
let t = caps.tiled_topology.expect("should be Some");
assert_eq!(t.h_tile_count, 16);
assert_eq!(t.v_tile_count, 16);
assert_eq!(t.h_tile_location, 15);
assert_eq!(t.v_tile_location, 15);
}
#[test]
fn test_tiled_topology_short_payload_skipped() {
let payload = vec![0x80u8, 0x11, 0x00, 0x80, 0x07, 0x38];
let mut caps = DisplayCapabilities::default();
decode_tiled_topology_block(&payload, &mut caps);
assert_eq!(caps.tiled_topology, None);
}
#[test]
fn test_tiled_topology_empty_payload_skipped() {
let mut caps = DisplayCapabilities::default();
decode_tiled_topology_block(&[], &mut caps);
assert_eq!(caps.tiled_topology, None);
}
fn make_block(tag: u8, payload: &[u8]) -> Vec<u8> {
let mut v = vec![tag, 0x00, payload.len() as u8];
v.extend_from_slice(payload);
v
}
#[test]
fn test_scan_product_id_first_wins() {
let first = make_block(
0x00,
&make_product_id_payload(
pack_manufacturer_id(b'S', b'A', b'M'),
0x1111,
0,
0,
0,
None,
),
);
let second = make_block(
0x00,
&make_product_id_payload(
pack_manufacturer_id(b'D', b'E', b'L'),
0x2222,
0,
0,
0,
None,
),
);
let payload = [first, second].concat();
let mut caps = DisplayCapabilities::default();
scan_product_id_block(&payload, &mut caps);
assert_eq!(caps.manufacturer, Some(ManufacturerId(*b"SAM")));
assert_eq!(caps.product_code, Some(0x1111));
}
#[test]
fn test_scan_display_params_first_wins() {
let first = make_block(0x01, &make_display_params_payload(600, 400, 0, 0, 0, 0));
let second = make_block(0x01, &make_display_params_payload(300, 200, 0, 0, 0, 0));
let payload = [first, second].concat();
let mut caps = DisplayCapabilities::default();
scan_display_params_block(&payload, &mut caps);
assert_eq!(caps.preferred_image_size_mm, Some((600, 400)));
}
#[test]
fn test_scan_color_characteristics_first_wins() {
let first = make_block(
0x02,
&make_color_characteristics_payload((100, 50), (200, 150), (50, 25), (300, 300)),
);
let second = make_block(
0x02,
&make_color_characteristics_payload((900, 800), (700, 600), (500, 400), (512, 512)),
);
let payload = [first, second].concat();
let mut caps = DisplayCapabilities::default();
scan_color_characteristics_block(&payload, &mut caps);
assert_eq!(caps.chromaticity.red.x_raw, 100);
assert_eq!(caps.chromaticity.red.y_raw, 50);
}
#[test]
fn test_scan_video_timing_range_first_wins() {
let first = make_block(
0x09,
&make_video_timing_range_payload(0, 14850, 30, 83, 0, 48, 75, 0, 0),
);
let second = make_block(
0x09,
&make_video_timing_range_payload(0, 30000, 10, 200, 0, 24, 240, 0, 0),
);
let payload = [first, second].concat();
let mut caps = DisplayCapabilities::default();
scan_video_timing_range_block(&payload, &mut caps);
assert_eq!(caps.max_pixel_clock_mhz, Some(148));
assert_eq!(caps.max_h_rate_khz, Some(83));
}
#[test]
fn test_scan_serial_number_first_wins() {
let first = make_block(0x0A, b"FIRST\x0a ");
let second = make_block(0x0A, b"SECOND\x0a ");
let payload = [first, second].concat();
let mut caps = DisplayCapabilities::default();
scan_serial_number_block(&payload, &mut caps);
assert_eq!(
caps.serial_number_string,
Some(MonitorString(*b"FIRST\x0a "))
);
}
#[test]
fn test_scan_display_device_data_first_wins() {
let first = make_block(0x0C, &[0x00]); let second = make_block(0x0C, &[0x60]); let payload = [first, second].concat();
let mut caps = DisplayCapabilities::default();
scan_display_device_data_block(&payload, &mut caps);
assert_eq!(caps.display_technology, Some(DisplayTechnology::Tft));
}
#[test]
fn test_scan_power_sequencing_first_wins() {
let first = make_block(0x0D, &[10, 20, 30, 40, 50, 60]);
let second = make_block(0x0D, &[99, 99, 99, 99, 99, 99]);
let payload = [first, second].concat();
let mut caps = DisplayCapabilities::default();
scan_power_sequencing_block(&payload, &mut caps);
let ps = caps.power_sequencing.unwrap();
assert_eq!(ps.t1_power_to_signal, 10);
assert_eq!(ps.t2_signal_to_backlight, 20);
}
#[test]
fn test_scan_transfer_characteristics_first_wins() {
let first = make_block(0x0E, &[0x00, 0xFF]); let second = make_block(0x0E, &[0x00, 0x00]); let payload = [first, second].concat();
let mut caps = DisplayCapabilities::default();
scan_transfer_characteristics_block(&payload, &mut caps);
let tc = caps.transfer_characteristic.unwrap();
if let TransferCurve::Luminance(pts) = tc.curve {
assert_eq!(pts.len(), 1);
assert_eq!(pts[0], 1.0_f32);
} else {
panic!("expected Luminance curve");
}
}
#[test]
fn test_scan_display_interface_first_wins() {
let first = make_block(0x0F, &[0x07, 0, 0, 0, 0, 0, 0]);
let second = make_block(0x0F, &[0x02, 0, 0, 0, 0, 0, 0]);
let payload = [first, second].concat();
let mut caps = DisplayCapabilities::default();
scan_display_interface_block(&payload, &mut caps);
assert_eq!(
caps.display_id_interface.unwrap().interface_type,
DisplayInterfaceType::DisplayPort,
);
}
#[test]
fn test_scan_stereo_display_interface_first_wins() {
let first = make_block(0x10, &[0x00, 0x00]); let second = make_block(0x10, &[0x01, 0x00]); let payload = [first, second].concat();
let mut caps = DisplayCapabilities::default();
scan_stereo_display_interface_block(&payload, &mut caps);
assert_eq!(
caps.stereo_interface.unwrap().viewing_mode,
StereoViewingMode::FieldSequential,
);
}
#[test]
fn test_scan_tiled_topology_first_wins() {
let first = make_block(0x12, &[0x00, 0x11, 0x00, 0, 0, 0, 0]); let second = make_block(0x12, &[0x00, 0x22, 0x00, 0, 0, 0, 0]); let payload = [first, second].concat();
let mut caps = DisplayCapabilities::default();
scan_tiled_topology_block(&payload, &mut caps);
let topo = caps.tiled_topology.unwrap();
assert_eq!(topo.h_tile_count, 2);
assert_eq!(topo.v_tile_count, 2);
}
}