#[derive(Debug, Clone)]
pub struct DisplayEdid {
pub key: MatchKey,
pub bytes: Vec<u8>,
}
#[allow(dead_code, reason = "Windows and macOS sources land next")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MatchKey {
Connector(String),
Monitor(usize),
CoreGraphics(u32),
}
#[must_use]
pub fn read_all() -> Vec<DisplayEdid> {
platform::read_all()
}
#[cfg(any(target_os = "windows", test))]
pub(crate) fn registry_path(device_id: &str) -> Option<String> {
let mut parts = device_id.split('#');
let _prefix = parts.next()?;
let hardware = parts.next()?;
let instance = parts.next()?;
Some(format!(
"SYSTEM\\CurrentControlSet\\Enum\\DISPLAY\\{hardware}\\{instance}\\Device Parameters"
))
}
#[cfg(target_os = "linux")]
mod platform {
use super::{DisplayEdid, MatchKey};
pub fn read_all() -> Vec<DisplayEdid> {
let Ok(entries) = std::fs::read_dir("/sys/class/drm") else {
return Vec::new();
};
let mut out = Vec::new();
for entry in entries.flatten() {
let Ok(bytes) = std::fs::read(entry.path().join("edid")) else {
continue;
};
if bytes.is_empty() {
continue;
}
let dir = entry.file_name().to_string_lossy().into_owned();
let connector = dir
.split_once('-')
.map_or(dir.clone(), |(_, c)| c.to_owned());
out.push(DisplayEdid {
key: MatchKey::Connector(connector),
bytes,
});
}
out.sort_by(|a, b| format!("{:?}", a.key).cmp(&format!("{:?}", b.key)));
out
}
}
#[cfg(target_os = "windows")]
mod platform {
use super::{DisplayEdid, MatchKey};
use windows::Win32::Foundation::{ERROR_SUCCESS, HWND, LPARAM, RECT};
use windows::Win32::Graphics::Gdi::{
DISPLAY_DEVICEW, EnumDisplayDevicesW, EnumDisplayMonitors, GetMonitorInfoW, HDC, HMONITOR,
MONITORINFOEXW,
};
const EDD_GET_DEVICE_INTERFACE_NAME: u32 = 0x0000_0001;
use windows::Win32::System::Registry::{
HKEY, HKEY_LOCAL_MACHINE, KEY_READ, RRF_RT_REG_BINARY, RegCloseKey, RegGetValueW,
RegOpenKeyExW,
};
use windows::core::PCWSTR;
pub fn read_all() -> Vec<DisplayEdid> {
let mut out: Vec<DisplayEdid> = Vec::new();
unsafe {
let _ = EnumDisplayMonitors(
None,
None,
Some(collect),
LPARAM(std::ptr::from_mut(&mut out) as isize),
);
}
out
}
unsafe extern "system" fn collect(
monitor: HMONITOR,
_hdc: HDC,
_clip: *mut RECT,
data: LPARAM,
) -> windows::core::BOOL {
let out = unsafe { &mut *(data.0 as *mut Vec<DisplayEdid>) };
if let Some(bytes) = unsafe { edid_for_monitor(monitor) } {
out.push(DisplayEdid {
key: MatchKey::Monitor(monitor.0 as usize),
bytes,
});
}
true.into()
}
unsafe fn edid_for_monitor(monitor: HMONITOR) -> Option<Vec<u8>> {
let mut info = MONITORINFOEXW::default();
info.monitorInfo.cbSize = u32::try_from(size_of::<MONITORINFOEXW>()).ok()?;
if !unsafe { GetMonitorInfoW(monitor, std::ptr::from_mut(&mut info).cast()) }.as_bool() {
return None;
}
let mut device = DISPLAY_DEVICEW {
cb: u32::try_from(size_of::<DISPLAY_DEVICEW>()).ok()?,
..Default::default()
};
let ok = unsafe {
EnumDisplayDevicesW(
PCWSTR(info.szDevice.as_ptr()),
0,
&mut device,
EDD_GET_DEVICE_INTERFACE_NAME,
)
};
if !ok.as_bool() {
return None;
}
read_edid_value(&super::registry_path(&wide_to_string(&device.DeviceID))?)
}
fn read_edid_value(path: &str) -> Option<Vec<u8>> {
let path_w = wide(path);
let mut key = HKEY::default();
let opened = unsafe {
RegOpenKeyExW(
HKEY_LOCAL_MACHINE,
PCWSTR(path_w.as_ptr()),
Some(0),
KEY_READ,
&mut key,
)
};
if opened != ERROR_SUCCESS {
return None;
}
let name = wide("EDID");
let mut len: u32 = 0;
let sized = unsafe {
RegGetValueW(
key,
None,
PCWSTR(name.as_ptr()),
RRF_RT_REG_BINARY,
None,
None,
Some(&mut len),
)
};
let mut buf = vec![0u8; len as usize];
let read = if sized == ERROR_SUCCESS && len > 0 {
unsafe {
RegGetValueW(
key,
None,
PCWSTR(name.as_ptr()),
RRF_RT_REG_BINARY,
None,
Some(buf.as_mut_ptr().cast()),
Some(&mut len),
)
}
} else {
sized
};
unsafe {
let _ = RegCloseKey(key);
}
(read == ERROR_SUCCESS && len > 0).then(|| {
buf.truncate(len as usize);
buf
})
}
fn wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
fn wide_to_string(buf: &[u16]) -> String {
let end = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
String::from_utf16_lossy(&buf[..end])
}
const _: Option<HWND> = None;
}
#[cfg(target_os = "macos")]
mod platform {
use super::{DisplayEdid, MatchKey};
use objc2_core_foundation::{CFData, CFDictionary, CFNumber, CFRetained, CFString, CFType};
use objc2_core_graphics::{
CGDisplayModelNumber, CGDisplayVendorNumber, CGError, CGGetActiveDisplayList,
};
use objc2_io_kit::{
IOIteratorNext, IOObjectRelease, IORegistryEntryCreateCFProperty,
IORegistryEntryCreateIterator, kIOMainPortDefault, kIORegistryIterateRecursively,
};
pub fn read_all() -> Vec<DisplayEdid> {
let panels = registry_panels();
if panels.is_empty() {
return Vec::new();
}
let mut out = Vec::new();
for id in active_displays() {
let (vendor, product) =
unsafe { (CGDisplayVendorNumber(id), CGDisplayModelNumber(id)) };
if let Some(bytes) = panels
.iter()
.find(|p| p.vendor == vendor && p.product == product)
.map(|p| p.edid.clone())
{
out.push(DisplayEdid {
key: MatchKey::CoreGraphics(id),
bytes,
});
}
}
out
}
fn active_displays() -> Vec<u32> {
let mut count: u32 = 0;
let sized = unsafe { CGGetActiveDisplayList(0, std::ptr::null_mut(), &mut count) };
if sized != CGError::Success {
return Vec::new();
}
let mut ids = vec![0u32; count as usize];
let filled = unsafe { CGGetActiveDisplayList(count, ids.as_mut_ptr(), &mut count) };
if filled != CGError::Success {
return Vec::new();
}
ids.truncate(count as usize);
ids
}
struct Panel {
vendor: u32,
product: u32,
edid: Vec<u8>,
}
fn registry_panels() -> Vec<Panel> {
let mut out = Vec::new();
let mut iter = 0;
let mut plane = [0 as std::os::raw::c_char; 128];
for (slot, byte) in plane.iter_mut().zip(b"IOService") {
*slot = *byte as std::os::raw::c_char;
}
let ok = unsafe {
IORegistryEntryCreateIterator(
objc2_io_kit::IORegistryGetRootEntry(kIOMainPortDefault),
&mut plane,
kIORegistryIterateRecursively,
&mut iter,
)
};
if ok != 0 {
return out;
}
loop {
let entry = unsafe { IOIteratorNext(iter) };
if entry == 0 {
break;
}
if let (Some(edid), Some(vendor), Some(product)) = (
data_property(entry, "IODisplayEDID"),
number_property(entry, "DisplayVendorID"),
number_property(entry, "DisplayProductID"),
) {
out.push(Panel {
vendor,
product,
edid,
});
}
unsafe { IOObjectRelease(entry) };
}
unsafe { IOObjectRelease(iter) };
out
}
fn property(entry: u32, key: &str) -> Option<CFRetained<CFType>> {
let name = CFString::from_str(key);
unsafe { IORegistryEntryCreateCFProperty(entry, Some(&name), None, 0) }
}
fn data_property(entry: u32, key: &str) -> Option<Vec<u8>> {
let value = property(entry, key)?;
let data = value.downcast_ref::<CFData>()?;
Some(data.to_vec())
}
fn number_property(entry: u32, key: &str) -> Option<u32> {
let value = property(entry, key)?;
let number = value.downcast_ref::<CFNumber>()?;
number.as_i32().and_then(|n| u32::try_from(n).ok())
}
const _: Option<&CFDictionary> = None;
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
mod platform {
use super::DisplayEdid;
pub fn read_all() -> Vec<DisplayEdid> {
Vec::new()
}
}
#[cfg(test)]
mod tests {
#[test]
fn a_device_interface_name_becomes_its_device_parameters_key() {
let got = super::registry_path(
r"\\?\DISPLAY#SAM0F99#5&1234abcd&0&UID4353#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}",
);
assert_eq!(
got.as_deref(),
Some(
"SYSTEM\\CurrentControlSet\\Enum\\DISPLAY\\SAM0F99\\5&1234abcd&0&UID4353\\Device Parameters"
)
);
assert!(super::registry_path("nonsense").is_none());
}
}