Skip to content

Commit

Permalink
Add HDR enable/disable capability
Browse files Browse the repository at this point in the history
  • Loading branch information
Andre Bocchini committed Apr 3, 2024
1 parent 0212c49 commit 6718081
Show file tree
Hide file tree
Showing 7 changed files with 1,063 additions and 205 deletions.
36 changes: 28 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ResolutionSwitcher

A command line tool to change the display resolution of computers running Windows.
A command line tool to change the display resolution and HDR state of computers running Windows.

## Usage Examples

Expand All @@ -10,10 +10,10 @@ List all available devices on the system
ResolutionSwitcher
```

Display all available resolutions for device with identifier `\\.\DISPLAY2`
Display detailed information for device with identifier `\\.\DISPLAY2`

```shell
ResolutionSwitcher --device \\.\DISPLAY2
ResolutionSwitcher --monitor \\.\DISPLAY2
```

Change the resolution of the primary display device
Expand All @@ -25,7 +25,19 @@ ResolutionSwitcher --width 1920 --height 1080 --refresh 60
Change the resolution of device with identifier `\\.\DISPLAY2`

```shell
ResolutionSwitcher --width 1920 --height 1080 --refresh 60 --device \\.\DISPLAY2
ResolutionSwitcher --width 1920 --height 1080 --refresh 60 --monitor \\.\DISPLAY2
```

Enable HDR on device with identifier `\\.\DISPLAY2`

```shell
ResolutionSwitcher --hdr enable --monitor \\.\DISPLAY2
```

Disable HDR on the primary device

```shell
ResolutionSwitcher --hdr disable
```

Display available help information
Expand All @@ -41,21 +53,29 @@ during "do" and "undo" commands run as part of a [Moonlight](https://moonlight-s

These examples assume the application is installed at `C:\Program Files\ResolutionSwitcher\ResolutionSwitcher.exe`.

### Do Command
### Do Commands

```shell
cmd /C "C:\Program Files\ResolutionSwitcher\ResolutionSwitcher.exe" --width %SUNSHINE_CLIENT_WIDTH% --height %SUNSHINE_CLIENT_HEIGHT% --refresh %SUNSHINE_CLIENT_FPS%
```

### Undo Command

```shell
cmd /C "C:\Program Files\ResolutionSwitcher\ResolutionSwitcher.exe" --hdr disable
```

### Undo Commands

```shell
cmd /C "C:\Program Files\ResolutionSwitcher\ResolutionSwitcher.exe" --width 3840 --height 2160 --refresh 144
```

```shell
cmd /C "C:\Program Files\ResolutionSwitcher\ResolutionSwitcher.exe" --hdr enable
```

## Building

The tool is written in [Python](https://www.python.org/) and uses [`pywin32`](https://pypi.org/project/pywin32/) to
interact with the Windows API.
The tool is written in [Python](https://www.python.org/) and uses the [ctypes](https://docs.python.org/3/library/ctypes.html) library to interact with the Windows API.

For distribution, the Python script is compiled into an executable using [`pyinstaller`](https://www.pyinstaller.org/).
80 changes: 80 additions & 0 deletions src/custom_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from windows_types import (
DISPLAYCONFIG_MODE_INFO,
DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO,
)


class DisplayMode:
def __init__(self, width: int, height: int, refresh: int):
self.width: int = width
self.height: int = height
self.refresh: int = refresh

def __str__(self):
return str(self.width) + "x" + str(self.height) + " @ " + str(self.refresh) + "Hz"


class DisplayAdapter:
def __init__(
self,
identifier: str = "",
display_name: str = "",
active_mode: DisplayMode = None,
available_modes: list[DisplayMode] = None,
is_attached: bool = False,
is_primary: bool = False
):
self.identifier: str = identifier
self.display_name: str = display_name
self.active_mode: DisplayMode = active_mode
self.available_modes: list[DisplayMode] = available_modes
self.is_attached: bool = is_attached
self.is_primary: bool = is_primary


class DisplayMonitor:
def __init__(
self,
name: str = "",
adapter: DisplayAdapter = DisplayAdapter(),
mode_info: DISPLAYCONFIG_MODE_INFO = None,
color_info: DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO = None
):
self.name: str = name
self.adapter: DisplayAdapter = adapter
self.mode_info: DISPLAYCONFIG_MODE_INFO = mode_info
self.color_info: DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO = color_info

def identifier(self) -> str:
return self.adapter.identifier

def active_mode(self) -> DisplayMode:
return self.adapter.active_mode

def is_primary(self) -> bool:
return self.adapter.is_primary

def is_attached(self) -> bool:
return self.adapter.is_attached

def is_hdr_supported(self) -> bool:
return self.color_info.value & 0x1 == 0x1

def is_hdr_enabled(self) -> bool:
return self.color_info.value & 0x2 == 0x2


class DisplayMonitorException(Exception):
pass


class PrimaryMonitorException(Exception):
pass


class HdrException(Exception):
pass


class DisplayAdapterException(Exception):
pass
171 changes: 171 additions & 0 deletions src/display_adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
from windows_types import (
DISP_CHANGE_SUCCESSFUL,
DISP_CHANGE_RESTART,
DISP_CHANGE_BADFLAGS,
DISP_CHANGE_BADMODE,
DISP_CHANGE_BADPARAM,
DISP_CHANGE_FAILED,
DISP_CHANGE_NOTUPDATED,
DISP_CHANGE_BADDUALVIEW,
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP,
DISPLAY_DEVICE_PRIMARY_DEVICE,
ENUM_CURRENT_SETTINGS,
DEVMODEW,
DISPLAY_DEVICEW,
ChangeDisplaySettingsExW,
EnumDisplaySettingsW,
EnumDisplayDevicesW, DM_PELSHEIGHT, DM_PELSWIDTH, DM_DISPLAYFREQUENCY
)
from custom_types import DisplayAdapter, DisplayMode, DisplayAdapterException
from ctypes import sizeof, byref


def is_attached_to_desktop(adapter: DISPLAY_DEVICEW) -> bool:
state_flags: int = adapter.StateFlags

return state_flags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP == DISPLAY_DEVICE_ATTACHED_TO_DESKTOP


def is_primary_device(adapter: DISPLAY_DEVICEW) -> bool:
state_flags: int = adapter.StateFlags

return state_flags & DISPLAY_DEVICE_PRIMARY_DEVICE == DISPLAY_DEVICE_PRIMARY_DEVICE


def get_all_display_adapters() -> list[DisplayAdapter]:
adapters: list[DisplayAdapter] = []

# This will hold display device information on every iteration of the loop
display_device = DISPLAY_DEVICEW()
display_device.cb = sizeof(DISPLAY_DEVICEW)
display_device.StateFlags = DISPLAY_DEVICE_ATTACHED_TO_DESKTOP

try:
# Tell Windows to cache display device information before we start looping
EnumDisplayDevicesW(None, 0, byref(display_device))
except OSError:
raise DisplayAdapterException("Failed to get list of available display devices")

index_of_current_adapter: int = 0
finished_searching_for_devices: bool = False

while not finished_searching_for_devices:
result: int = EnumDisplayDevicesW(None, index_of_current_adapter, byref(display_device))

if result == 0:
finished_searching_for_devices = True
else:
try:
display_adapter = DisplayAdapter()
display_adapter.identifier = str(display_device.DeviceName)
display_adapter.display_name = str(display_device.DeviceString)
display_adapter.active_mode = get_active_display_mode_for_adapter(display_device)
display_adapter.available_modes = get_all_available_display_modes_for_adapter(display_device)
display_adapter.is_attached = is_attached_to_desktop(display_device)
display_adapter.is_primary = is_primary_device(display_device)

adapters.append(display_adapter)
except DisplayAdapterException:
pass
finally:
index_of_current_adapter += 1

return adapters


def get_all_available_display_modes_for_adapter(adapter: DISPLAY_DEVICEW) -> list[DisplayMode]:
identifier: str = adapter.DeviceName
display_modes: list[DisplayMode] = []

# This will store the display mode information on every loop iteration
devmodew: DEVMODEW = DEVMODEW()
devmodew.dmSize = sizeof(DEVMODEW)

try:
# Tell Windows to cache display mode information before we start looping
result: int = EnumDisplaySettingsW(identifier, 0, devmodew)

if result == 0:
raise DisplayAdapterException(
f"Failed to get available modes for {identifier}. Failed with result {result}"
)

except OSError as e:
raise DisplayAdapterException(f"Failed to get available modes for {identifier}. Failed with error {str(e)}")

index_of_current_mode: int = 1
finished_getting_modes: bool = False

while not finished_getting_modes:
try:
result: int = EnumDisplaySettingsW(identifier, index_of_current_mode, byref(devmodew))

if result == 0:
finished_getting_modes = True
else:
display_mode = DisplayMode(devmodew.dmPelsWidth, devmodew.dmPelsHeight, devmodew.dmDisplayFrequency)
display_modes.append(display_mode)

index_of_current_mode += 1
except OSError:
finished_getting_modes = True

return display_modes


def get_active_display_mode_for_adapter(adapter: DISPLAY_DEVICEW) -> DisplayMode:
identifier = adapter.DeviceName

try:
display_modew = DEVMODEW()
display_modew.dmSize = sizeof(DEVMODEW)

result: int = EnumDisplaySettingsW(identifier, ENUM_CURRENT_SETTINGS, byref(display_modew))

if result == 0:
raise DisplayAdapterException(f"Failed to get active mode for {identifier}. Failed with result {result}")

return DisplayMode(display_modew.dmPelsWidth, display_modew.dmPelsHeight, display_modew.dmDisplayFrequency)
except OSError as e:
raise DisplayAdapterException(f"Failed to get active mode for {identifier}. Failed with error {str(e)}")


def set_display_mode_for_device(display_mode: DisplayMode, device_identifier: str):
if device_identifier is None:
raise DisplayAdapterException("Device identifier cannot be empty")

if display_mode is None:
raise DisplayAdapterException("Display settings cannot be empty")

devmodew = DEVMODEW()
devmodew.dmDeviceName = device_identifier
devmodew.dmSize = sizeof(DEVMODEW)
devmodew.dmPelsWidth = display_mode.width
devmodew.dmPelsHeight = display_mode.height
devmodew.dmDisplayFrequency = display_mode.refresh
devmodew.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY

try:
result: int = ChangeDisplaySettingsExW(device_identifier, byref(devmodew), None, 0, None)

if result == DISP_CHANGE_SUCCESSFUL:
return
elif result == DISP_CHANGE_RESTART:
raise DisplayAdapterException("The computer must be restarted for the graphics mode to work")
elif result == DISP_CHANGE_BADFLAGS:
raise DisplayAdapterException("An invalid set of flags was passed in")
elif result == DISP_CHANGE_BADMODE:
raise DisplayAdapterException("The graphics mode is not supported")
elif result == DISP_CHANGE_BADPARAM:
raise DisplayAdapterException("An invalid parameter was passed in")
elif result == DISP_CHANGE_FAILED:
raise DisplayAdapterException("The display driver failed the specified graphics mode")
elif result == DISP_CHANGE_NOTUPDATED:
raise DisplayAdapterException("Unable to write settings to the registry")
elif result == DISP_CHANGE_BADDUALVIEW:
raise DisplayAdapterException("The settings change was unsuccessful because the system is DualView capable")
else:
raise DisplayAdapterException("An unknown error occurred")

except OSError as e:
raise DisplayAdapterException(f"Failed to change display settings. Failed with error {str(e)}")
Loading

0 comments on commit 6718081

Please sign in to comment.