Skip to content

Conversation

remimimimimi
Copy link

@remimimimimi remimimimimi commented May 2, 2025

Add support for querying the physical size of a monitor (in millimeters) via a new physical_size() method on the MonitorHandleProvider 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 return None.

Resolves #3858 and partially resolves #4187.

  • Tested on all platforms changed: I manually tested on Linux, MacOS, and Windows, other platforms return None.
  • Added an entry to the changelog module if knowledge of this change could be valuable to users
  • Updated documentation to reflect any user-facing changes, including notes of platform-specific behavior
  • Created or updated an example program if it would help users understand this functionality

@remimimimimi remimimimimi force-pushed the physical-size branch 5 times, most recently from 73343bf to 01b3f7f Compare May 2, 2025 20:47
@remimimimimi
Copy link
Author

The CI failure is due to typos in files from previous commits.

@remimimimimi remimimimimi marked this pull request as ready for review May 2, 2025 20:55
@madsmtm
Copy link
Member

madsmtm commented May 2, 2025

Thanks for the PR!

@madsmtm madsmtm added S - enhancement Wouldn't this be the coolest? DS - appkit Affects the AppKit/macOS backend DS - win32 Affects the Win32/Windows backend DS - x11 Affects the X11 backend, or generally free Unix platforms DS - wayland Affects the Wayland backend, or generally free Unix platforms S - api Design and usability labels May 2, 2025
@remimimimimi remimimimimi requested a review from madsmtm May 4, 2025 21:53
Copy link
Member

@kchibisov kchibisov left a 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.

@remimimimimi remimimimimi force-pushed the physical-size branch 4 times, most recently from 03af2d7 to 49c1cb9 Compare May 6, 2025 21:52
Currently only X11 and Wayland are supported.
@remimimimimi
Copy link
Author

CI fail seems unrelated to PR.

Couple of notes after testing different platforms:

  1. Wayland and MacOS swaps values when original orientation is changed by 90 or 270 degrees. Since we don't expose display orientation yet, I decided to adjust implementation for other systems to match this behavior.
  2. Wayland returns only centimeter precision. It seems that to change it, contribution to libdisplay-info is needed, as smithay uses this library.
  3. Previous implementation for windows returned incorrect values on resolution change. Thanks to @madsmtm comment this problem is now fixed.

Relevant comments were added to the documentation.

@remimimimimi remimimimimi requested review from kchibisov and madsmtm May 6, 2025 22:17
Copy link
Member

@madsmtm madsmtm left a 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),
Copy link
Member

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.

Comment on lines +166 to +178
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,
};
Copy link
Member

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> {
Copy link
Member

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

Comment on lines +186 to +378
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(&params);
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)
}
}
Copy link
Member

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?)

Copy link

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.

Copy link

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"
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
DS - appkit Affects the AppKit/macOS backend DS - wayland Affects the Wayland backend, or generally free Unix platforms DS - win32 Affects the Win32/Windows backend DS - x11 Affects the X11 backend, or generally free Unix platforms S - api Design and usability S - enhancement Wouldn't this be the coolest?
Development

Successfully merging this pull request may close these issues.

More comprehensive resolution and scaling information. Get physical size of a monitor/window in millimeters
4 participants