From d9c75e8914ba65a44a59981064dfce48bf3c1f46 Mon Sep 17 00:00:00 2001 From: Andre Bocchini Date: Sun, 19 May 2024 10:17:06 -0400 Subject: [PATCH] Separate HDR and resolution switches into mutually exclusive settings. This means that when executing the program, you can only execute an HDR change, or resolution change. --- README.md | 18 ++++++- src/display_monitors.py | 15 ++++++ src/main.py | 108 +++++++++++++++++++++------------------- src/windows_types.py | 11 ++++ 4 files changed, 100 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index cb058d5..376ac0f 100644 --- a/README.md +++ b/README.md @@ -77,13 +77,21 @@ These examples assume the application is installed at `C:\Program Files\Resoluti ## Do Commands ```shell -cmd /C "C:\Program Files\ResolutionSwitcher\ResolutionSwitcher.exe" --width %SUNSHINE_CLIENT_WIDTH% --height %SUNSHINE_CLIENT_HEIGHT% --refresh %SUNSHINE_CLIENT_FPS% --hdr %SUNSHINE_CLIENT_HDR% +cmd /C "C:\Program Files\ResolutionSwitcher\ResolutionSwitcher.exe" --width %SUNSHINE_CLIENT_WIDTH% --height %SUNSHINE_CLIENT_HEIGHT% --refresh %SUNSHINE_CLIENT_FPS% +``` + +```shell +cmd /C "C:\Program Files\ResolutionSwitcher\ResolutionSwitcher.exe" --hdr %SUNSHINE_CLIENT_HDR% ``` ## Undo Commands ```shell -cmd /C "C:\Program Files\ResolutionSwitcher\ResolutionSwitcher.exe" --width 3840 --height 2160 --refresh 144 --hdr false +cmd /C "C:\Program Files\ResolutionSwitcher\ResolutionSwitcher.exe" --width 3840 --height 2160 --refresh 144 +``` + +```shell +cmd /C "C:\Program Files\ResolutionSwitcher\ResolutionSwitcher.exe" --hdr false ``` # Building @@ -91,3 +99,9 @@ cmd /C "C:\Program Files\ResolutionSwitcher\ResolutionSwitcher.exe" --width 3840 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/). + +# Known Issues + +Sometimes when changing HDR state, the screen resolution may reset to a previous +resolution. As far as I can tell, this is a behavior of the Windows API, so if that's +happening to you, you might want to do an HDR change followed by a resolution change. diff --git a/src/display_monitors.py b/src/display_monitors.py index e2c1c46..81b2aee 100644 --- a/src/display_monitors.py +++ b/src/display_monitors.py @@ -1,4 +1,6 @@ from ctypes import byref, c_ulong, sizeof +from ctypes.wintypes import BOOL +from time import sleep from typing import Optional from custom_types import ( @@ -22,7 +24,9 @@ DisplayConfigGetDeviceInfo, DisplayConfigSetDeviceInfo, GetDisplayConfigBufferSizes, + InternalRefreshCalibration, QueryDisplayConfig, + WcsGetCalibrationManagementState, ) @@ -145,6 +149,17 @@ def set_hdr_state_for_monitor(enabled: bool, monitor: DisplayMonitor): f"Failed to change HDR state with result {result}" ) + sleep(3) + + is_calibration_management_enabled = BOOL() + + if not WcsGetCalibrationManagementState( + byref(is_calibration_management_enabled) + ): + raise DisplayMonitorException("Failed to get calibration management state") + + InternalRefreshCalibration(0, 0) + except OSError as e: raise DisplayMonitorException(f"Failed to change HDR state with error {e}") diff --git a/src/main.py b/src/main.py index 542e5e7..eb0189d 100644 --- a/src/main.py +++ b/src/main.py @@ -14,7 +14,7 @@ from termcolor._types import Attribute, Color # Application metadata -VERSION: str = "v3.0.1" +VERSION: str = "v3.0.2" NAME: str = "ResolutionSwitcher" @@ -117,6 +117,32 @@ def print_error(error: str): print_message("Error: " + error, "red", attrs=["bold"]) +def change_resolution(monitor_identifier: str, width: int, height: int, refresh: int): + display_mode: DisplayMode = DisplayMode(width, height, refresh) + print_message( + f"Attempting to change {monitor_identifier} settings to {str(display_mode)}" + ) + set_display_mode_for_device(display_mode, monitor_identifier) + print_success("Display settings changed successfully") + + +def change_hdr(monitor_identifier: str, hdr: str): + hdr_state = True if hdr.lower() == "true" else False + + for monitor in all_monitors: + if monitor.adapter.identifier == monitor_identifier: + if not monitor.is_hdr_supported(): + print_success(f"{monitor.adapter.identifier} does not support HDR") + print_success("Exiting...") + exit(-1) + + print_message( + f'Attempting to {"enable" if hdr_state else "disable"} HDR on {monitor_identifier}' + ) + set_hdr_state_for_monitor(hdr_state, monitor) + print_success(f"HDR {'enabled' if hdr_state else 'disabled'} successfully") + + if __name__ == "__main__": parser = argument_parser() args = parser.parse_args() @@ -127,63 +153,51 @@ def print_error(error: str): print_error("No monitors found") exit(-1) + if args.hdr is not None: + if args.hdr.lower() not in ["true", "false"]: + print_error("Valid values for HDR are 'true' or 'false'") + exit(-1) + + try: + identifier: str = args.monitor + + if identifier is None: + identifier = get_primary_monitor(all_monitors).identifier() + + change_hdr(identifier, args.hdr) + + exit(0) + + except PrimaryMonitorException as e: + print_error(str(e)) + exit(-1) + + except HdrException as e: + print_error( + f"Error when trying to change HDR state. Failed with error {str(e)}" + ) + exit(-1) + if args.width or args.height or args.refresh: should_change_resolution: bool = ( args.width is not None and args.height is not None and args.refresh is not None ) - should_change_hdr: bool = args.hdr is not None if not should_change_resolution: - print_error("Width, height and refresh rate are required") - - if not should_change_hdr: - exit(-1) - - if should_change_hdr: - if args.hdr.lower() not in ["true", "false"]: - print_error("Valid values for HDR are 'true' or 'false'") - exit(-1) + print_error("Width, height, and refresh rate are required for resolution change") + exit(-1) try: identifier: str = args.monitor if identifier is None: - print_message("Monitor ID not specified") - print_message("Will attempt to identify primary monitor") identifier = get_primary_monitor(all_monitors).identifier() - if should_change_resolution: - display_mode: DisplayMode = DisplayMode( - args.width, args.height, args.refresh - ) - - print_message( - f"Attempting to change {identifier} settings to {str(display_mode)}" - ) - set_display_mode_for_device(display_mode, identifier) - print_success("Display settings changed successfully") - - if should_change_hdr: - for target_monitor in all_monitors: - if target_monitor.adapter.identifier == identifier: - if not target_monitor.is_hdr_supported(): - print_success( - f"{target_monitor.adapter.identifier} does not support HDR" - ) - print_success("Exiting...") - exit(-1) - - hdr_state = True if args.hdr.lower() == "true" else False - - print_message( - f'Attempting to {"enable" if hdr_state else "disable"} HDR on {identifier}' - ) - set_hdr_state_for_monitor(hdr_state, target_monitor) - print_success( - f"HDR {'enabled' if hdr_state else 'disabled'} successfully" - ) + change_resolution(identifier, args.width, args.height, args.refresh) + + exit(0) except DisplayAdapterException as e: print_error(str(e)) @@ -193,13 +207,7 @@ def print_error(error: str): print_error(str(e)) exit(-1) - except HdrException as e: - print_error( - f"Error when trying to change HDR state. Failed with error {str(e)}" - ) - exit(-1) - - elif args.monitor is not None: + if args.monitor is not None: identifier: str = args.monitor for target_monitor in all_monitors: diff --git a/src/windows_types.py b/src/windows_types.py index 0cb9b39..d9769bc 100644 --- a/src/windows_types.py +++ b/src/windows_types.py @@ -1,5 +1,6 @@ from ctypes import POINTER, Structure, Union, WinDLL, c_uint16, c_uint32, c_uint64 from ctypes.wintypes import ( + BOOL, DWORD, HWND, LONG, @@ -392,6 +393,7 @@ class DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE(Structure): # Imported API functions user32DLL = WinDLL("user32") +mscmsDLL = WinDLL("mscms") # https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-changedisplaysettingsexw ChangeDisplaySettingsExW = user32DLL.ChangeDisplaySettingsExW @@ -434,3 +436,12 @@ class DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE(Structure): POINTER(DISPLAYCONFIG_MODE_INFO), POINTER(c_uint32), ] + +# https://learn.microsoft.com/en-us/windows/win32/api/icm/nf-icm-wcsgetcalibrationmanagementstate +WcsGetCalibrationManagementState = mscmsDLL.WcsGetCalibrationManagementState +WcsGetCalibrationManagementState.restype = BOOL +WcsGetCalibrationManagementState.argtypes = [POINTER(BOOL)] + +InternalRefreshCalibration = mscmsDLL.InternalRefreshCalibration +InternalRefreshCalibration.restype = LONG +InternalRefreshCalibration.argtypes = [LONG, LONG]