use crate::{Error, blocking::BrightnessDevice};
use itertools::Either;
use std::{
collections::HashMap,
ffi::{OsString, c_void},
fmt,
iter::once,
mem::size_of,
os::windows::ffi::OsStringExt,
};
use windows::{
Win32::{
Devices::Display::{
DISPLAY_BRIGHTNESS, DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME, DISPLAYCONFIG_MODE_INFO,
DISPLAYCONFIG_MODE_INFO_TYPE_TARGET, DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL,
DISPLAYCONFIG_PATH_INFO, DISPLAYCONFIG_TARGET_DEVICE_NAME,
DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY, DISPLAYPOLICY_AC, DISPLAYPOLICY_DC,
DestroyPhysicalMonitor, DisplayConfigGetDeviceInfo, GetDisplayConfigBufferSizes,
GetMonitorBrightness, GetNumberOfPhysicalMonitorsFromHMONITOR,
GetPhysicalMonitorsFromHMONITOR, IOCTL_VIDEO_QUERY_DISPLAY_BRIGHTNESS,
IOCTL_VIDEO_QUERY_SUPPORTED_BRIGHTNESS, IOCTL_VIDEO_SET_DISPLAY_BRIGHTNESS,
PHYSICAL_MONITOR, QDC_ONLY_ACTIVE_PATHS, QueryDisplayConfig, SetMonitorBrightness,
},
Foundation::{
CloseHandle, ERROR_ACCESS_DENIED, ERROR_SUCCESS, HANDLE, LPARAM, RECT, WIN32_ERROR,
},
Graphics::Gdi::{
DISPLAY_DEVICE_ACTIVE, DISPLAY_DEVICEW, EnumDisplayDevicesW, EnumDisplayMonitors,
GetMonitorInfoW, HDC, HMONITOR, MONITORINFO, MONITORINFOEXW,
},
Storage::FileSystem::{
CreateFileW, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_READ, FILE_SHARE_WRITE,
OPEN_EXISTING,
},
System::IO::DeviceIoControl,
UI::WindowsAndMessaging::EDD_GET_DEVICE_INTERFACE_NAME,
},
core::{BOOL, Error as WinError, PCWSTR},
};
pub trait BrightnessExt {
fn device_description(&self) -> Result<String, Error>;
fn device_registry_key(&self) -> Result<String, Error>;
fn device_path(&self) -> Result<String, Error>;
}
#[derive(Debug)]
pub(crate) struct BlockingDeviceImpl {
physical_monitor: WrappedPhysicalMonitor,
file_handle: WrappedFileHandle,
device_name: String,
friendly_name: String,
pub(crate) device_description: String,
pub(crate) device_key: String,
pub(crate) device_path: String,
output_technology: DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY,
}
impl BlockingDeviceImpl {
fn is_internal(&self) -> bool {
self.output_technology == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL
}
}
struct WrappedPhysicalMonitor(HANDLE);
impl fmt::Debug for WrappedPhysicalMonitor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl Drop for WrappedPhysicalMonitor {
fn drop(&mut self) {
unsafe {
let _ = DestroyPhysicalMonitor(self.0);
}
}
}
unsafe impl Send for WrappedPhysicalMonitor {}
unsafe impl Sync for WrappedPhysicalMonitor {}
struct WrappedFileHandle(HANDLE);
impl fmt::Debug for WrappedFileHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl Drop for WrappedFileHandle {
fn drop(&mut self) {
unsafe {
let _ = CloseHandle(self.0);
}
}
}
unsafe impl Send for WrappedFileHandle {}
unsafe impl Sync for WrappedFileHandle {}
#[inline]
fn flag_set<T: std::ops::BitAnd<Output = T> + std::cmp::PartialEq + Copy>(t: T, flag: T) -> bool {
t & flag == flag
}
impl crate::blocking::Brightness for BlockingDeviceImpl {
fn device_name(&self) -> Result<String, Error> {
Ok(self.device_name.clone())
}
fn friendly_device_name(&self) -> Result<String, Error> {
Ok(self.friendly_name.clone())
}
fn get(&self) -> Result<u32, Error> {
Ok(if self.is_internal() {
ioctl_query_display_brightness(self)?
} else {
ddcci_get_monitor_brightness(self)?.get_current_percentage()
})
}
fn set(&self, percentage: u32) -> Result<(), Error> {
if self.is_internal() {
let supported = ioctl_query_supported_brightness(self)?;
let new_value = supported.get_nearest(percentage);
ioctl_set_display_brightness(self, new_value)?;
} else {
let current = ddcci_get_monitor_brightness(self)?;
let new_value = current.percentage_to_current(percentage);
ddcci_set_monitor_brightness(self, new_value)?;
}
Ok(())
}
}
pub(crate) fn brightness_devices() -> impl Iterator<Item = Result<BlockingDeviceImpl, SysError>> {
unsafe {
let device_info_map = match get_device_info_map() {
Ok(info) => info,
Err(e) => return Either::Right(once(Err(e))),
};
let hmonitors = match enum_display_monitors() {
Ok(monitors) => monitors,
Err(e) => return Either::Right(once(Err(e))),
};
Either::Left(
hmonitors
.into_iter()
.flat_map(move |hmonitor| {
let physical_monitors = match get_physical_monitors_from_hmonitor(hmonitor) {
Ok(p) => p,
Err(e) => return vec![Err(e)],
};
let display_devices = match get_display_devices_from_hmonitor(hmonitor) {
Ok(p) => p,
Err(e) => return vec![Err(e)],
};
if display_devices.len() != physical_monitors.len() {
return vec![Err(SysError::EnumerationMismatch)];
}
physical_monitors
.into_iter()
.zip(display_devices)
.filter_map(|(physical_monitor, display_device)| {
get_file_handle_for_display_device(&display_device)
.transpose()
.map(|file_handle| (physical_monitor, display_device, file_handle))
})
.map(|(physical_monitor, display_device, file_handle)| {
let file_handle = file_handle?;
let info = device_info_map
.get(&display_device.DeviceID)
.ok_or(SysError::DeviceInfoMissing)?;
Ok(BlockingDeviceImpl {
physical_monitor,
file_handle,
device_name: wchar_to_string(&display_device.DeviceName),
friendly_name: wchar_to_string(&info.monitorFriendlyDeviceName),
device_description: wchar_to_string(&display_device.DeviceString),
device_key: wchar_to_string(&display_device.DeviceKey),
device_path: wchar_to_string(&display_device.DeviceID),
output_technology: info.outputTechnology,
})
})
.collect()
})
.collect::<Vec<_>>()
.into_iter(),
)
}
}
unsafe fn get_device_info_map()
-> Result<HashMap<[u16; 128], DISPLAYCONFIG_TARGET_DEVICE_NAME>, SysError> {
let mut path_count = 0;
let mut mode_count = 0;
check_status(
unsafe {
GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &mut path_count, &mut mode_count)
},
SysError::GetDisplayConfigBufferSizes,
)?;
let mut display_paths = vec![DISPLAYCONFIG_PATH_INFO::default(); path_count as usize];
let mut display_modes = vec![DISPLAYCONFIG_MODE_INFO::default(); mode_count as usize];
check_status(
unsafe {
QueryDisplayConfig(
QDC_ONLY_ACTIVE_PATHS,
&mut path_count,
display_paths.as_mut_ptr(),
&mut mode_count,
display_modes.as_mut_ptr(),
None,
)
},
SysError::QueryDisplayConfig,
)?;
display_modes
.into_iter()
.filter(|mode| mode.infoType == DISPLAYCONFIG_MODE_INFO_TYPE_TARGET)
.flat_map(|mode| {
let mut device_name = DISPLAYCONFIG_TARGET_DEVICE_NAME::default();
device_name.header.size = size_of::<DISPLAYCONFIG_TARGET_DEVICE_NAME>() as u32;
device_name.header.adapterId = mode.adapterId;
device_name.header.id = mode.id;
device_name.header.r#type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;
let result =
to_win32_error(unsafe { DisplayConfigGetDeviceInfo(&mut device_name.header) });
match result {
ERROR_SUCCESS => Some(Ok((device_name.monitorDevicePath, device_name))),
ERROR_ACCESS_DENIED => None,
_ => Some(Err(SysError::DisplayConfigGetDeviceInfo(result.into()))),
}
})
.collect()
}
unsafe fn enum_display_monitors() -> Result<Vec<HMONITOR>, SysError> {
unsafe extern "system" fn enum_monitors(
handle: HMONITOR,
_: HDC,
_: *mut RECT,
data: LPARAM,
) -> BOOL {
let monitors = unsafe { &mut *(data.0 as *mut Vec<HMONITOR>) };
monitors.push(handle);
true.into()
}
let mut hmonitors = Vec::<HMONITOR>::new();
unsafe {
EnumDisplayMonitors(
None,
None,
Some(enum_monitors),
LPARAM(&mut hmonitors as *mut _ as isize),
)
}
.ok()
.map_err(SysError::EnumDisplayMonitors)?;
Ok(hmonitors)
}
unsafe fn get_physical_monitors_from_hmonitor(
hmonitor: HMONITOR,
) -> Result<Vec<WrappedPhysicalMonitor>, SysError> {
let mut physical_number: u32 = 0;
unsafe { GetNumberOfPhysicalMonitorsFromHMONITOR(hmonitor, &mut physical_number) }
.map_err(SysError::GetPhysicalMonitors)?;
let mut raw_physical_monitors = vec![PHYSICAL_MONITOR::default(); physical_number as usize];
let mut physical_monitors = Vec::with_capacity(raw_physical_monitors.len());
unsafe { GetPhysicalMonitorsFromHMONITOR(hmonitor, &mut raw_physical_monitors) }
.map_err(SysError::GetPhysicalMonitors)?;
raw_physical_monitors
.into_iter()
.for_each(|pm| physical_monitors.push(WrappedPhysicalMonitor(pm.hPhysicalMonitor)));
Ok(physical_monitors)
}
unsafe fn get_display_devices_from_hmonitor(
hmonitor: HMONITOR,
) -> Result<Vec<DISPLAY_DEVICEW>, SysError> {
let mut info = MONITORINFOEXW::default();
info.monitorInfo.cbSize = size_of::<MONITORINFOEXW>() as u32;
let info_ptr = &mut info as *mut _ as *mut MONITORINFO;
unsafe { GetMonitorInfoW(hmonitor, info_ptr) }
.ok()
.map_err(SysError::GetMonitorInfo)?;
Ok((0..)
.map_while(|device_number| {
let mut device = DISPLAY_DEVICEW {
cb: size_of::<DISPLAY_DEVICEW>() as u32,
..Default::default()
};
unsafe {
EnumDisplayDevicesW(
PCWSTR(info.szDevice.as_ptr()),
device_number,
&mut device,
EDD_GET_DEVICE_INTERFACE_NAME,
)
}
.as_bool()
.then_some(device)
})
.filter(|device| flag_set(device.StateFlags, DISPLAY_DEVICE_ACTIVE))
.collect())
}
unsafe fn get_file_handle_for_display_device(
display_device: &DISPLAY_DEVICEW,
) -> Result<Option<WrappedFileHandle>, SysError> {
unsafe {
CreateFileW(
PCWSTR(display_device.DeviceID.as_ptr()),
(FILE_GENERIC_READ | FILE_GENERIC_WRITE).0,
FILE_SHARE_READ | FILE_SHARE_WRITE,
None,
OPEN_EXISTING,
Default::default(),
None,
)
}
.map(|h| Some(WrappedFileHandle(h)))
.or_else(|e| {
(e.code() == ERROR_ACCESS_DENIED.to_hresult())
.then_some(None)
.ok_or_else(|| SysError::OpeningMonitorDeviceInterfaceHandle {
device_name: wchar_to_string(&display_device.DeviceName),
source: e,
})
})
}
#[derive(Clone, Debug, Error)]
pub(crate) enum SysError {
#[error("Failed to enumerate device monitors")]
EnumDisplayMonitors(#[source] WinError),
#[error("Failed to get display config buffer sizes")]
GetDisplayConfigBufferSizes(#[source] WinError),
#[error("Failed to query display config")]
QueryDisplayConfig(#[source] WinError),
#[error("Failed to get display config device info")]
DisplayConfigGetDeviceInfo(#[source] WinError),
#[error("Failed to get monitor info")]
GetMonitorInfo(#[source] WinError),
#[error("Failed to get physical monitors from the HMONITOR")]
GetPhysicalMonitors(#[source] WinError),
#[error(
"The length of GetPhysicalMonitorsFromHMONITOR() and EnumDisplayDevicesW() results did not \
match, this could be because monitors were connected/disconnected while loading devices"
)]
EnumerationMismatch,
#[error(
"Unable to find a matching device info for this display device, this could be because monitors \
were connected while loading devices"
)]
DeviceInfoMissing,
#[error("Failed to open monitor interface handle (CreateFileW)")]
OpeningMonitorDeviceInterfaceHandle {
device_name: String,
source: WinError,
},
#[error("Failed to query supported brightness (IOCTL)")]
IoctlQuerySupportedBrightness {
device_name: String,
source: WinError,
},
#[error("Failed to query display brightness (IOCTL)")]
IoctlQueryDisplayBrightness {
device_name: String,
source: WinError,
},
#[error("Unexpected response when querying display brightness (IOCTL)")]
IoctlQueryDisplayBrightnessUnexpectedResponse { device_name: String },
#[error("Failed to get monitor brightness (DDCCI)")]
GettingMonitorBrightness {
device_name: String,
source: WinError,
},
#[error("Failed to set monitor brightness (IOCTL)")]
IoctlSetBrightness {
device_name: String,
source: WinError,
},
#[error("Failed to set monitor brightness (DDCCI)")]
SettingBrightness {
device_name: String,
source: WinError,
},
}
impl From<SysError> for Error {
fn from(e: SysError) -> Self {
match &e {
SysError::EnumerationMismatch
| SysError::DeviceInfoMissing
| SysError::GetDisplayConfigBufferSizes(..)
| SysError::QueryDisplayConfig(..)
| SysError::DisplayConfigGetDeviceInfo(..)
| SysError::GetPhysicalMonitors(..)
| SysError::EnumDisplayMonitors(..)
| SysError::GetMonitorInfo(..)
| SysError::OpeningMonitorDeviceInterfaceHandle { .. } => {
Error::ListingDevices(Box::new(e))
}
SysError::IoctlQuerySupportedBrightness { device_name, .. }
| SysError::IoctlQueryDisplayBrightness { device_name, .. }
| SysError::IoctlQueryDisplayBrightnessUnexpectedResponse { device_name }
| SysError::GettingMonitorBrightness { device_name, .. } => Error::GettingDeviceInfo {
device: device_name.clone(),
source: Box::new(e),
},
SysError::SettingBrightness { device_name, .. }
| SysError::IoctlSetBrightness { device_name, .. } => Error::SettingBrightness {
device: device_name.clone(),
source: Box::new(e),
},
}
}
}
fn wchar_to_string(s: &[u16]) -> String {
let end = s.iter().position(|&x| x == 0).unwrap_or(s.len());
let truncated = &s[0..end];
OsString::from_wide(truncated).to_string_lossy().into()
}
fn to_win32_error(status: i32) -> WIN32_ERROR {
WIN32_ERROR(status as u32)
}
fn check_status<F, E>(status: WIN32_ERROR, f: F) -> Result<(), E>
where
F: FnOnce(WinError) -> E,
{
status.ok().map_err(f)
}
#[derive(Debug, Default)]
struct DdcciBrightnessValues {
min: u32,
current: u32,
max: u32,
}
impl DdcciBrightnessValues {
fn get_current_percentage(&self) -> u32 {
let normalised_max = (self.max - self.min) as f64;
let normalised_current = (self.current - self.min) as f64;
(normalised_current / normalised_max * 100.0).round() as u32
}
fn percentage_to_current(&self, percentage: u32) -> u32 {
let normalised_max = (self.max - self.min) as f64;
let fraction = percentage as f64 / 100.0;
let normalised_current = fraction * normalised_max;
normalised_current.round() as u32 + self.min
}
}
fn ddcci_get_monitor_brightness(
device: &BlockingDeviceImpl,
) -> Result<DdcciBrightnessValues, SysError> {
unsafe {
let mut v = DdcciBrightnessValues::default();
BOOL(GetMonitorBrightness(
device.physical_monitor.0,
&mut v.min,
&mut v.current,
&mut v.max,
))
.ok()
.map(|_| v)
.map_err(|e| SysError::GettingMonitorBrightness {
device_name: device.device_name.clone(),
source: e,
})
}
}
fn ddcci_set_monitor_brightness(device: &BlockingDeviceImpl, value: u32) -> Result<(), SysError> {
unsafe {
BOOL(SetMonitorBrightness(device.physical_monitor.0, value))
.ok()
.map_err(|e| SysError::SettingBrightness {
device_name: device.device_name.clone(),
source: e,
})
}
}
#[derive(Debug)]
struct IoctlSupportedBrightnessLevels(Vec<u8>);
impl IoctlSupportedBrightnessLevels {
fn get_nearest(&self, percentage: u32) -> u8 {
self.0
.iter()
.copied()
.min_by_key(|&num| (num as i64 - percentage as i64).abs())
.unwrap_or(0)
}
}
fn ioctl_query_supported_brightness(
device: &BlockingDeviceImpl,
) -> Result<IoctlSupportedBrightnessLevels, SysError> {
unsafe {
let mut bytes_returned = 0;
let mut out_buffer = Vec::<u8>::with_capacity(256);
DeviceIoControl(
device.file_handle.0,
IOCTL_VIDEO_QUERY_SUPPORTED_BRIGHTNESS,
None,
0,
Some(out_buffer.as_mut_ptr() as *mut c_void),
out_buffer.capacity() as u32,
Some(&mut bytes_returned),
None,
)
.map(|_| {
out_buffer.set_len(bytes_returned as usize);
IoctlSupportedBrightnessLevels(out_buffer)
})
.map_err(|e| SysError::IoctlQuerySupportedBrightness {
device_name: device.device_name.clone(),
source: e,
})
}
}
fn ioctl_query_display_brightness(device: &BlockingDeviceImpl) -> Result<u32, SysError> {
unsafe {
let mut bytes_returned = 0;
let mut display_brightness = DISPLAY_BRIGHTNESS::default();
DeviceIoControl(
device.file_handle.0,
IOCTL_VIDEO_QUERY_DISPLAY_BRIGHTNESS,
None,
0,
Some(&mut display_brightness as *mut DISPLAY_BRIGHTNESS as *mut c_void),
size_of::<DISPLAY_BRIGHTNESS>() as u32,
Some(&mut bytes_returned),
None,
)
.map_err(|e| SysError::IoctlQueryDisplayBrightness {
device_name: device.device_name.clone(),
source: e,
})
.and_then(|_| match display_brightness.ucDisplayPolicy as u32 {
DISPLAYPOLICY_AC => {
Ok(display_brightness.ucACBrightness as u32)
}
DISPLAYPOLICY_DC => {
Ok(display_brightness.ucDCBrightness as u32)
}
_ => Err(SysError::IoctlQueryDisplayBrightnessUnexpectedResponse {
device_name: device.device_name.clone(),
}),
})
}
}
fn ioctl_set_display_brightness(device: &BlockingDeviceImpl, value: u8) -> Result<(), SysError> {
const DISPLAYPOLICY_BOTH: u8 = 3;
unsafe {
let mut display_brightness = DISPLAY_BRIGHTNESS {
ucACBrightness: value,
ucDCBrightness: value,
ucDisplayPolicy: DISPLAYPOLICY_BOTH,
};
let mut bytes_returned = 0;
DeviceIoControl(
device.file_handle.0,
IOCTL_VIDEO_SET_DISPLAY_BRIGHTNESS,
Some(&mut display_brightness as *mut DISPLAY_BRIGHTNESS as *mut c_void),
size_of::<DISPLAY_BRIGHTNESS>() as u32,
None,
0,
Some(&mut bytes_returned),
None,
)
.map(|_| {
std::thread::sleep(std::time::Duration::from_nanos(1));
})
.map_err(|e| SysError::IoctlSetBrightness {
device_name: device.device_name.clone(),
source: e,
})
}
}
impl BrightnessExt for BrightnessDevice {
fn device_description(&self) -> Result<String, Error> {
Ok(self.0.device_description.clone())
}
fn device_registry_key(&self) -> Result<String, Error> {
Ok(self.0.device_key.clone())
}
fn device_path(&self) -> Result<String, Error> {
Ok(self.0.device_path.clone())
}
}