-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expose monitor physical size as MonitorHandleProvider
method
#4212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
73343bf
to
01b3f7f
Compare
The CI failure is due to typos in files from previous commits. |
Thanks for the PR! |
01b3f7f
to
d6613e7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you make the API the following Option<(NonZeroU32, NonZeroU32)>
, so we at least fight the cases where 0
is returned and users won't have to think about that.
03af2d7
to
49c1cb9
Compare
Currently only X11 and Wayland are supported.
49c1cb9
to
27590b7
Compare
CI fail seems unrelated to PR. Couple of notes after testing different platforms:
Relevant comments were added to the documentation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, API-wise this looks good now, only blocker is getting the new Windows code reviewed to a satisfactory degree.
If that drags out, and you want to make sure to move forwards on this, you could consider ripping out the Windows implementation and return None
there for now, and then submit the impl in a different PR.
/// If monitor is rotated by 90° or 270°, width and height will be swapped. | ||
/// | ||
/// Every major platform gets this data from | ||
/// [EDID](https://en.wikipedia.org/wiki/Extended_Display_Identification_Data), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Use [EDID]
and [EDID]: https://en.wikipedia.org/wiki/Extended_Display_Identification_Data
further down, reads more cleanly IMO.
use std::ffi::OsStr; | ||
use std::mem::zeroed; | ||
use std::os::windows::ffi::OsStrExt; | ||
use std::ptr::null_mut; | ||
|
||
use windows_sys::Win32::Foundation::ERROR_SUCCESS; | ||
use windows_sys::Win32::Graphics::Gdi::{ | ||
EnumDisplayDevicesW, DISPLAY_DEVICEW, DISPLAY_DEVICE_ACTIVE, DISPLAY_DEVICE_ATTACHED, DMDO_270, | ||
DMDO_90, | ||
}; | ||
use windows_sys::Win32::System::Registry::{ | ||
RegCloseKey, RegEnumKeyExW, RegOpenKeyExW, RegQueryValueExW, HKEY, HKEY_LOCAL_MACHINE, KEY_READ, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Either put these imports at the top, or if you feel it's complex enough, move the EDID logic to a separate file.
RegCloseKey, RegEnumKeyExW, RegOpenKeyExW, RegQueryValueExW, HKEY, HKEY_LOCAL_MACHINE, KEY_READ, | ||
}; | ||
|
||
fn to_wide(s: &str) -> Vec<u16> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we have helper methods for this elsewhere
unsafe fn get_monitor_device(adapter_name: *const u16) -> Option<DISPLAY_DEVICEW> { | ||
let mut dd_mon: DISPLAY_DEVICEW = unsafe { zeroed() }; | ||
dd_mon.cb = mem::size_of::<DISPLAY_DEVICEW>() as u32; | ||
|
||
// 1. find ACTIVE + ATTACHED | ||
let mut idx = 0; | ||
loop { | ||
let ok = unsafe { EnumDisplayDevicesW(adapter_name, idx, &mut dd_mon, 0) } != 0; | ||
if !ok { | ||
break; | ||
} | ||
if (dd_mon.StateFlags & DISPLAY_DEVICE_ACTIVE) != 0 | ||
&& (dd_mon.StateFlags & DISPLAY_DEVICE_ATTACHED) != 0 | ||
{ | ||
break; | ||
} | ||
idx += 1; | ||
} | ||
|
||
// 2. fallback to first if no DeviceString | ||
if dd_mon.DeviceString[0] == 0 { | ||
let ok = unsafe { EnumDisplayDevicesW(adapter_name, 0, &mut dd_mon, 0) } != 0; | ||
if !ok || dd_mon.DeviceString[0] == 0 { | ||
let def = to_wide("Default Monitor"); | ||
dd_mon.DeviceString[..def.len()].copy_from_slice(&def); | ||
} | ||
} | ||
|
||
(dd_mon.DeviceID[0] != 0).then_some(dd_mon) | ||
} | ||
|
||
unsafe fn read_size_from_edid(dd: &DISPLAY_DEVICEW) -> Option<(u32, u32)> { | ||
// Parse DeviceID: "DISPLAY\\<model>\\<inst>" | ||
let id_buf = &dd.DeviceID; | ||
let len = id_buf.iter().position(|&c| c == 0).unwrap_or(id_buf.len()); | ||
let id_str = String::from_utf16_lossy(&id_buf[..len]); | ||
let mut parts = id_str.split('\\'); | ||
let _ = parts.next(); | ||
let model = parts.next().unwrap_or(""); | ||
let inst = parts.next().unwrap_or(""); | ||
let model = if model.len() > 7 { &model[..7] } else { model }; | ||
|
||
// Open HKLM\SYSTEM\CurrentControlSet\Enum\DISPLAY\<model> | ||
let base = format!("SYSTEM\\CurrentControlSet\\Enum\\DISPLAY\\{model}"); | ||
let base_w = to_wide(&base); | ||
let mut hkey: ScopeGuard<HKEY, _> = guard(std::ptr::null_mut(), |v| unsafe { | ||
RegCloseKey(v); | ||
}); | ||
if unsafe { | ||
RegOpenKeyExW(HKEY_LOCAL_MACHINE, base_w.as_ptr(), 0, KEY_READ, &mut *hkey) != ERROR_SUCCESS | ||
} { | ||
return None; | ||
} | ||
|
||
// enumerate instances | ||
let mut i = 0; | ||
loop { | ||
let mut name_buf = [0u16; 128]; | ||
let mut name_len = name_buf.len() as u32; | ||
let mut ft = unsafe { mem::zeroed() }; | ||
let r = unsafe { | ||
RegEnumKeyExW( | ||
*hkey, | ||
i, | ||
name_buf.as_mut_ptr(), | ||
&mut name_len, | ||
null_mut(), | ||
null_mut(), | ||
null_mut(), | ||
&mut ft, | ||
) | ||
}; | ||
if r != ERROR_SUCCESS { | ||
break; | ||
} | ||
i += 1; | ||
|
||
let name = String::from_utf16_lossy(&name_buf[..name_len as usize]); | ||
let subkey = format!("{base}\\{name}"); | ||
let sub_w = to_wide(&subkey); | ||
let mut hkey2: ScopeGuard<HKEY, _> = guard(std::ptr::null_mut(), |v| unsafe { | ||
RegCloseKey(v); | ||
}); | ||
if unsafe { | ||
RegOpenKeyExW(HKEY_LOCAL_MACHINE, sub_w.as_ptr(), 0, KEY_READ, &mut *hkey2) | ||
!= ERROR_SUCCESS | ||
} { | ||
continue; | ||
} | ||
|
||
// Check Driver == inst | ||
let drv_w = to_wide("Driver"); | ||
let mut drv_buf = [0u16; 128]; | ||
let mut drv_len = (drv_buf.len() * 2) as u32; | ||
|
||
if !unsafe { | ||
RegQueryValueExW( | ||
*hkey2, | ||
drv_w.as_ptr(), | ||
null_mut(), | ||
null_mut(), | ||
drv_buf.as_mut_ptr() as *mut u8, | ||
&mut drv_len, | ||
) == ERROR_SUCCESS | ||
} { | ||
continue; | ||
} | ||
|
||
let got = String::from_utf16_lossy(&drv_buf[..(drv_len as usize / 2)]); | ||
|
||
if !got.starts_with(inst) { | ||
continue; | ||
} | ||
|
||
// Open Device Parameters | ||
let params = format!("{subkey}\\Device Parameters"); | ||
let params_w = to_wide(¶ms); | ||
let mut hkey3: ScopeGuard<HKEY, _> = guard(std::ptr::null_mut(), |v| unsafe { | ||
RegCloseKey(v); | ||
}); | ||
|
||
if unsafe { | ||
RegOpenKeyExW(HKEY_LOCAL_MACHINE, params_w.as_ptr(), 0, KEY_READ, &mut *hkey3) | ||
!= ERROR_SUCCESS | ||
} { | ||
continue; | ||
} | ||
|
||
let edid_w = to_wide("EDID"); | ||
let mut edid = [0u8; 256]; | ||
let mut edid_len = edid.len() as u32; | ||
if unsafe { | ||
RegQueryValueExW( | ||
*hkey3, | ||
edid_w.as_ptr(), | ||
null_mut(), | ||
null_mut(), | ||
edid.as_mut_ptr(), | ||
&mut edid_len, | ||
) != ERROR_SUCCESS | ||
} || edid_len < 23 | ||
{ | ||
continue; | ||
} | ||
let width_mm: u32; | ||
let height_mm: u32; | ||
|
||
// We want to have more detailed resolution than centimeters, | ||
// specifically millimeters. EDID provides Detailed Timing | ||
// Descriptor (DTD) table which can contain desired | ||
// size. There can be up to 4 DTDs in EDID, but the first one | ||
// non-zero-clock DTD is the monitor’s preferred (native) | ||
// mode, so we try only it. | ||
const DTD0: usize = 54; | ||
|
||
let pixel_clock = (edid[DTD0] as u16) | ((edid[DTD0 + 1] as u16) << 8); | ||
|
||
if pixel_clock != 0 { | ||
// For mm precision we need 12-14 bits from Detailed | ||
// Timing Descriptor | ||
// https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#Detailed_Timing_Descriptor. | ||
let h_size_lsb = edid[DTD0 + 12] as u16; | ||
let v_size_lsb = edid[DTD0 + 13] as u16; | ||
let size_msb = edid[DTD0 + 14] as u16; | ||
let h_msb = (size_msb >> 4) & 0x0f; | ||
let v_msb = size_msb & 0x0f; | ||
|
||
width_mm = ((h_msb << 8) | h_size_lsb) as u32; | ||
height_mm = ((v_msb << 8) | v_size_lsb) as u32; | ||
} else { | ||
let width_cm = edid[21] as u32; | ||
let height_cm = edid[22] as u32; | ||
|
||
width_mm = width_cm * 10; | ||
height_mm = height_cm * 10; | ||
} | ||
|
||
return Some((width_mm, height_mm)); | ||
} | ||
|
||
None | ||
} | ||
|
||
fn monitor_physical_size(mi: &MONITORINFOEXW) -> Option<(u32, u32)> { | ||
unsafe { | ||
// adapter_name is the wide-string in MONITORINFOEXW.szDevice | ||
let adapter_name = mi.szDevice.as_ptr(); | ||
// find the matching DISPLAY_DEVICEW | ||
let dd = get_monitor_device(adapter_name)?; | ||
// read EDID | ||
read_size_from_edid(&dd) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uff. This suddenly got a lot more complex and error-prone, I not longer feel confident enough in my Windows abilities to review this.
@wareya, do you have any input here? For context, see #4212 (comment).
(Also, looking at prior art, it seems that GLFW uses GetDeviceCaps
, and that neither SDL nor FLTK supports querying the physical size of the screen at all. So maybe it's actually fine for us to use GetDeviceCaps
?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not intimately familiar with this part of the win32 API, I asked a friend (actual friend not euphemism for talking to an LLM) that knows more about it than me. My friend pointed me at this article, which, while historical, is probably more relevant than the first however many results on google for this topic: https://ofekshilon.com/2011/11/13/reading-monitor-physical-dimensions-or-getting-the-edid-the-right-way/
tldr: GetDeviceCaps doesn't work as advertised (so GLFW is broken), poking at the registry is the second-best way to get display dimensions, the best way is using SetupAPI. Aside from SetupAPI and poking at the registry, the alternatives don't actually work or are insanely complicated for no good reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PS: I missed the email notification for this ping and only noticed it because I was poking around the issue tracker; if you ping me again and I don't respond, feel free to ping me on element/matrix too.
|
||
# Windows | ||
[target.'cfg(target_os = "windows")'.dependencies] | ||
scopeguard = "1.2.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer a helper struct with a custom Drop
impl that calls RegCloseKey
over the added dependency.
Add support for querying the physical size of a monitor (in millimeters) via a new
physical_size()
method on theMonitorHandleProvider
trait. Currently X11, Wayland, Windows, MacOS backends are updated. Other platforms are either don't support this functionality or it is hard to implement it. If no implementation possible, method will simply returnNone
.Resolves #3858 and partially resolves #4187.
None
.changelog
module if knowledge of this change could be valuable to users