Skip to content
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

Windows: Implement resolution #147

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ windows = { version = "0.39.0", features = [
"Win32_Foundation",
"Win32_System_Power",
"Win32_System_SystemInformation",
"Win32_System_WindowsProgramming"
"Win32_System_WindowsProgramming",
"Win32_Graphics_Gdi",
"Win32_UI_HiDpi",
"Win32_UI_WindowsAndMessaging",
]}

[target.'cfg(not(target_os = "windows"))'.dependencies]
Expand Down
169 changes: 167 additions & 2 deletions src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@ use wmi::WMIResult;
use wmi::{COMLibrary, Variant, WMIConnection};

use windows::{
core::PSTR, Win32::System::Power::GetSystemPowerStatus,
core::{PCWSTR, PSTR},
Win32::Foundation::{BOOL, LPARAM, RECT},
Win32::Graphics::Gdi::{
EnumDisplayDevicesW, EnumDisplayMonitors, EnumDisplaySettingsW, GetMonitorInfoW, DEVMODEW,
DISPLAY_DEVICEW, ENUM_CURRENT_SETTINGS, HDC, HMONITOR, MONITORINFO,
},
Win32::System::Power::GetSystemPowerStatus,
Win32::System::Power::SYSTEM_POWER_STATUS,
Win32::System::SystemInformation::GetComputerNameExA,
Win32::System::SystemInformation::GetTickCount64,
Win32::System::SystemInformation::GlobalMemoryStatusEx,
Win32::System::SystemInformation::MEMORYSTATUSEX,
Win32::System::WindowsProgramming::GetUserNameA,
Win32::UI::HiDpi::{SetProcessDpiAwareness, PROCESS_SYSTEM_DPI_AWARE},
Win32::UI::WindowsAndMessaging::EDD_GET_DEVICE_INTERFACE_NAME,
};

impl From<wmi::WMIError> for ReadoutError {
Expand Down Expand Up @@ -170,7 +178,164 @@ impl GeneralReadout for WindowsGeneralReadout {
}

fn resolution(&self) -> Result<String, ReadoutError> {
Err(ReadoutError::NotImplemented)
let mut devices = Vec::new();
let mut index = 0;
let mut status = true;
// Iterate over EnumDisplayDevicesW until it returns false
while status {
devices.push(DISPLAY_DEVICEW::default());
devices[index].cb = std::mem::size_of::<DISPLAY_DEVICEW>() as u32;
status = unsafe {
EnumDisplayDevicesW(
PCWSTR::null(),
index as u32,
&mut devices[index],
EDD_GET_DEVICE_INTERFACE_NAME,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't use DISPLAY_DEVICE::DeviceID, don't set EDD_GET_DEVICE_INTERFACE_NAME

)
.as_bool()
};
index += 1;
}
// Remove the last element, which will be invalid
devices.pop();

// Create a vector to store each monitor's information
let mut resolutions = Vec::new();

// Iterate over each device
let mut index = 0;
for device in devices {
resolutions.push(DEVMODEW::default());
resolutions[index].dmSize = std::mem::size_of::<DEVMODEW>() as u16;

// Get the current display settings for the device
unsafe {
EnumDisplaySettingsW(
PCWSTR(device.DeviceName.as_ptr()),
ENUM_CURRENT_SETTINGS,
&mut resolutions[index],
);
}

// Ensure that the resolution is valid
if resolutions[index].dmPelsWidth != 0 && resolutions[index].dmPelsHeight != 0 {
index += 1;
} else {
resolutions.pop();
}
}

if !resolutions.is_empty() {
// Format and return the display resolutions and refresh rates
return Ok(resolutions
.iter()
.map(
|resolution| match (resolution.dmDisplayFrequency, resolution.dmLogPixels) {
Copy link

@CarterLi CarterLi Apr 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try updating the scale factor of your monitors? For me, dmLogPixels won't be updated before I restarting my laptap.

In addition, according to MSDN, EnumDisplaySettings sets only these 5 DEMMODE members.

image

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you test multiple monitors?

Copy link
Author

@Carterpersall Carterpersall Apr 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try updating the scale factor of your monitors? For me, dmLogPixels won't be updated before I restarting my laptap.

In addition, according to MSDN, EnumDisplaySettings sets only these 5 DEMMODE members.

image

I didn't before now, and I can confirm that the issue exists for me as well. Along with that, I found that the current implementation doesn't properly get the scale for a second monitor even after a restart. I see a few options moving forward:

  1. Only show the scaled resolution of the primary monitor and have the bug where it doesn't update as a known issue
  2. Don't show a scaled resolution
  3. Do what Fastfetch does and merge the two implementations into a single one that uses EnumDisplaySettingsW to get the resolution and refresh rate and uses EnumDisplayMonitors without SetProcessDpiAwareness to get the scaled resolution. This would likely be about twice as slow, but would add a maximum of a few milliseconds.

I'm going to try implementing option 3 and see how much slower it is.

Did you test multiple monitors?

When a monitor is added or removed, the output correctly updates

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is actually another method by using HDC and GetDeviceCaps.

CarterLi/fastfetch@4d7cb25#diff-1f993416a36137ee7af25d94be4928701d89ca08d9ab322fcb3eb0b5ec6d80b6R39

I dont know if it is faster than EnumDisplayMonitors. The code is much simplier at least.

(0, 0) => format!("{}x{}", resolution.dmPelsWidth, resolution.dmPelsHeight),
(0, _) => format!(
"{}x{} (as {}x{})",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the resolution is not scaled. Don't print (as {}x{})

resolution.dmPelsWidth,
resolution.dmPelsHeight,
resolution.dmPelsWidth as f32 / (resolution.dmLogPixels as f32 / 96.0),
resolution.dmPelsHeight as f32 / (resolution.dmLogPixels as f32 / 96.0)
),
(_, 0) => format!(
"{}x{}@{}Hz",
resolution.dmPelsWidth,
resolution.dmPelsHeight,
resolution.dmDisplayFrequency
),
(_, _) => format!(
"{}x{}@{}Hz (as {}x{})",
resolution.dmPelsWidth,
resolution.dmPelsHeight,
resolution.dmDisplayFrequency,
resolution.dmPelsWidth as f32 / (resolution.dmLogPixels as f32 / 96.0),
resolution.dmPelsHeight as f32 / (resolution.dmLogPixels as f32 / 96.0)
),
},
)
.collect::<Vec<String>>()
.join(", "));
}

// Backup Implementation 1
// Sources:
// https://github.com/lptstr/winfetch/pull/156/
// https://patriksvensson.se/posts/2020/06/enumerating-monitors-in-rust-using-win32-api

// Create callback function for EnumDisplayMonitors to use
#[allow(non_snake_case, unused_variables)]
extern "system" fn EnumProc(
hMonitor: HMONITOR,
hdcMonitor: HDC,
lprcMonitor: *mut RECT,
dwData: LPARAM,
) -> BOOL {
unsafe {
// Get the userdata where we will store the result
let monitors: &mut Vec<MONITORINFO> = std::mem::transmute(dwData);

// Initialize the MONITORINFO structure and get a pointer to it
let mut monitor_info: MONITORINFO = std::mem::zeroed();
monitor_info.cbSize = std::mem::size_of::<MONITORINFO>() as u32;
let monitor_info_ptr = <*mut _>::cast(&mut monitor_info);

// Call the GetMonitorInfoW Win32 API
let result = GetMonitorInfoW(hMonitor, monitor_info_ptr);
if result.as_bool() {
// Push the information we received to the vector
monitors.push(monitor_info);
}
}

true.into()
}

// Set DPI awareness to ensure we get the correct resolution
match unsafe { SetProcessDpiAwareness(PROCESS_SYSTEM_DPI_AWARE) } {
Ok(_) => (),
Err(_) => {
return Err(ReadoutError::Other(String::from(
"Failed to set DPI awareness.",
)))
}
}

// Create vector to store the resulting monitors in
let mut monitors = Vec::<MONITORINFO>::new();
let userdata = &mut monitors as *mut _;

// Call the EnumDisplayMonitors win32 API to get each monitor's information
let result = unsafe {
EnumDisplayMonitors(
HDC(0),
std::ptr::null(),
Some(EnumProc),
LPARAM(userdata as isize),
)
};

if result.as_bool() {
// Create a vector of strings containing the resolution of each monitor
let monitors_info: Vec<String> = monitors
.iter()
.map(|monitor| {
format!(
"{}x{}",
monitor.rcMonitor.right - monitor.rcMonitor.left,
monitor.rcMonitor.bottom - monitor.rcMonitor.top
)
})
.collect();

return Ok(monitors_info.join(", "));
}

// If every implementation failed
Err(ReadoutError::Other(
"Failed to get display information".to_string(),
))
}

fn username(&self) -> Result<String, ReadoutError> {
Expand Down