From 5721f8213c4522110676de419134c395d2bc918f Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Tue, 2 Jan 2024 21:00:27 +0200 Subject: [PATCH] Add new API to modify display devices for Windows --- cmake/compile_definitions/common.cmake | 9 + cmake/compile_definitions/linux.cmake | 1 + cmake/compile_definitions/macos.cmake | 1 + cmake/compile_definitions/windows.cmake | 10 + docs/source/about/advanced_usage.rst | 28 +- docs/source/source_code/source_code.rst | 14 + .../src/display_device/display_device.rst | 4 + .../src/display_device/parsed_config.rst | 4 + .../src/display_device/session.rst | 4 + .../src/display_device/settings.rst | 4 + .../src/display_device/to_string.rst | 4 + .../windows/display_device/settings_data.rst | 4 + .../display_device/settings_topology.rst | 4 + .../windows/display_device/windows_utils.rst | 4 + src/config.cpp | 15 + src/config.h | 7 + src/display_device/display_device.h | 182 +++++++ src/display_device/parsed_config.cpp | 227 +++++++++ src/display_device/parsed_config.h | 65 +++ src/display_device/session.cpp | 40 ++ src/display_device/session.h | 50 ++ src/display_device/settings.cpp | 46 ++ src/display_device/settings.h | 75 +++ src/display_device/to_string.cpp | 142 ++++++ src/display_device/to_string.h | 38 ++ src/main.cpp | 15 +- src/nvhttp.cpp | 71 ++- src/platform/linux/display_device.cpp | 123 +++++ src/platform/macos/display_device.cpp | 123 +++++ src/platform/windows/display_base.cpp | 4 +- .../display_device/device_hdr_states.cpp | 105 ++++ .../windows/display_device/device_modes.cpp | 306 ++++++++++++ .../display_device/device_topology.cpp | 454 ++++++++++++++++++ .../display_device/general_functions.cpp | 143 ++++++ .../windows/display_device/settings.cpp | 452 +++++++++++++++++ .../windows/display_device/settings_data.h | 47 ++ .../display_device/settings_topology.cpp | 293 +++++++++++ .../display_device/settings_topology.h | 46 ++ .../windows/display_device/windows_utils.cpp | 437 +++++++++++++++++ .../windows/display_device/windows_utils.h | 106 ++++ src/process.cpp | 8 +- src/stream.cpp | 14 +- src/video.cpp | 13 +- src_assets/common/assets/web/config.html | 119 ++++- 44 files changed, 3826 insertions(+), 35 deletions(-) create mode 100644 docs/source/source_code/src/display_device/display_device.rst create mode 100644 docs/source/source_code/src/display_device/parsed_config.rst create mode 100644 docs/source/source_code/src/display_device/session.rst create mode 100644 docs/source/source_code/src/display_device/settings.rst create mode 100644 docs/source/source_code/src/display_device/to_string.rst create mode 100644 docs/source/source_code/src/platform/windows/display_device/settings_data.rst create mode 100644 docs/source/source_code/src/platform/windows/display_device/settings_topology.rst create mode 100644 docs/source/source_code/src/platform/windows/display_device/windows_utils.rst create mode 100644 src/display_device/display_device.h create mode 100644 src/display_device/parsed_config.cpp create mode 100644 src/display_device/parsed_config.h create mode 100644 src/display_device/session.cpp create mode 100644 src/display_device/session.h create mode 100644 src/display_device/settings.cpp create mode 100644 src/display_device/settings.h create mode 100644 src/display_device/to_string.cpp create mode 100644 src/display_device/to_string.h create mode 100644 src/platform/linux/display_device.cpp create mode 100644 src/platform/macos/display_device.cpp create mode 100644 src/platform/windows/display_device/device_hdr_states.cpp create mode 100644 src/platform/windows/display_device/device_modes.cpp create mode 100644 src/platform/windows/display_device/device_topology.cpp create mode 100644 src/platform/windows/display_device/general_functions.cpp create mode 100644 src/platform/windows/display_device/settings.cpp create mode 100644 src/platform/windows/display_device/settings_data.h create mode 100644 src/platform/windows/display_device/settings_topology.cpp create mode 100644 src/platform/windows/display_device/settings_topology.h create mode 100644 src/platform/windows/display_device/windows_utils.cpp create mode 100644 src/platform/windows/display_device/windows_utils.h diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake index a2ae3948ec1..0182f5e63d3 100644 --- a/cmake/compile_definitions/common.cmake +++ b/cmake/compile_definitions/common.cmake @@ -68,6 +68,15 @@ set(SUNSHINE_TARGET_FILES src/audio.cpp src/audio.h src/platform/common.h + src/display_device/display_device.h + src/display_device/parsed_config.cpp + src/display_device/parsed_config.h + src/display_device/session.cpp + src/display_device/session.h + src/display_device/settings.cpp + src/display_device/settings.h + src/display_device/to_string.cpp + src/display_device/to_string.h src/process.cpp src/process.h src/network.cpp diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake index d4ebb597312..5b36094c57d 100644 --- a/cmake/compile_definitions/linux.cmake +++ b/cmake/compile_definitions/linux.cmake @@ -231,6 +231,7 @@ list(APPEND PLATFORM_TARGET_FILES src/platform/linux/misc.cpp src/platform/linux/audio.cpp src/platform/linux/input.cpp + src/platform/linux/display_device.cpp third-party/glad/src/egl.c third-party/glad/src/gl.c third-party/glad/include/EGL/eglplatform.h diff --git a/cmake/compile_definitions/macos.cmake b/cmake/compile_definitions/macos.cmake index 3bcf9528361..1d734f49985 100644 --- a/cmake/compile_definitions/macos.cmake +++ b/cmake/compile_definitions/macos.cmake @@ -37,6 +37,7 @@ set(PLATFORM_TARGET_FILES src/platform/macos/nv12_zero_device.cpp src/platform/macos/nv12_zero_device.h src/platform/macos/publish.cpp + src/platform/macos/display_device.cpp third-party/TPCircularBuffer/TPCircularBuffer.c third-party/TPCircularBuffer/TPCircularBuffer.h ${APPLE_PLIST_FILE}) diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 703106f189a..cb80af031cc 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -49,6 +49,16 @@ set(PLATFORM_TARGET_FILES src/platform/windows/display_vram.cpp src/platform/windows/display_ram.cpp src/platform/windows/audio.cpp + src/platform/windows/display_device/device_hdr_states.cpp + src/platform/windows/display_device/device_modes.cpp + src/platform/windows/display_device/device_topology.cpp + src/platform/windows/display_device/general_functions.cpp + src/platform/windows/display_device/settings_data.h + src/platform/windows/display_device/settings_topology.cpp + src/platform/windows/display_device/settings_topology.h + src/platform/windows/display_device/settings.cpp + src/platform/windows/display_device/windows_utils.h + src/platform/windows/display_device/windows_utils.cpp third-party/ViGEmClient/src/ViGEmClient.cpp third-party/ViGEmClient/include/ViGEm/Client.h third-party/ViGEmClient/include/ViGEm/Common.h diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst index 975e88076c4..cde1ba2d371 100644 --- a/docs/source/about/advanced_usage.rst +++ b/docs/source/about/advanced_usage.rst @@ -610,7 +610,7 @@ keybindings ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - Select the display number you want to stream. + Select the display you want to stream. .. tip:: To find the name of the appropriate values follow these instructions. @@ -631,9 +631,29 @@ keybindings .. todo:: macOS **Windows** - .. code-block:: batch + During Sunshine startup, you should see the list of detected display devices: - tools\dxgi-info.exe + .. code-block:: text + + DEVICE ID: \\?\DISPLAY-ACI27EC-5&4fd2de4&2&UID4355-{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7} + DISPLAY NAME: \\.\DISPLAY1 + FRIENDLY NAME: ROG PG279Q + DEVICE STATE: PRIMARY + HDR STATE: UNKNOWN + ----------------------- + DEVICE ID: \\?\DISPLAY-LNX0000-1&28a6823a&4&UID256-{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7} + DISPLAY NAME: NOT AVAILABLE + FRIENDLY NAME: IDD HDR + DEVICE STATE: INACTIVE + HDR STATE: UNKNOWN + ----------------------- + DEVICE ID: \\?\DISPLAY-XMD009A-5&4fd2de4&2&UID4354-{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7} + DISPLAY NAME: NOT AVAILABLE + FRIENDLY NAME: Mi TV + DEVICE STATE: INACTIVE + HDR STATE: UNKNOWN + + You need to use the ``DEVICE ID`` value. **Default** Sunshine will select the default display. @@ -649,7 +669,7 @@ keybindings **Windows** .. code-block:: text - output_name = \\.\DISPLAY1 + output_name = \\?\DISPLAY-LNX0000-1&28a6823a&4&UID256-{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7} `resolutions `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/source_code/source_code.rst b/docs/source/source_code/source_code.rst index fecc6801c92..24c67a67e9f 100644 --- a/docs/source/source_code/source_code.rst +++ b/docs/source/source_code/source_code.rst @@ -62,6 +62,13 @@ Source src/* +.. toctree:: + :caption: src/display_device + :maxdepth: 1 + :glob: + + src/display_device/* + .. toctree:: :caption: src/platform :maxdepth: 1 @@ -89,3 +96,10 @@ Source :glob: src/platform/windows/* + +.. toctree:: + :caption: src/platform/windows/display_device + :maxdepth: 1 + :glob: + + src/platform/windows/display_device/* diff --git a/docs/source/source_code/src/display_device/display_device.rst b/docs/source/source_code/src/display_device/display_device.rst new file mode 100644 index 00000000000..147db4ef91c --- /dev/null +++ b/docs/source/source_code/src/display_device/display_device.rst @@ -0,0 +1,4 @@ +display_device +============== + +.. todo:: Add display_device.h diff --git a/docs/source/source_code/src/display_device/parsed_config.rst b/docs/source/source_code/src/display_device/parsed_config.rst new file mode 100644 index 00000000000..24d23f1e807 --- /dev/null +++ b/docs/source/source_code/src/display_device/parsed_config.rst @@ -0,0 +1,4 @@ +parsed_config +============= + +.. todo:: Add parsed_config.h diff --git a/docs/source/source_code/src/display_device/session.rst b/docs/source/source_code/src/display_device/session.rst new file mode 100644 index 00000000000..47a4d49ebce --- /dev/null +++ b/docs/source/source_code/src/display_device/session.rst @@ -0,0 +1,4 @@ +session +======= + +.. todo:: Add session.h diff --git a/docs/source/source_code/src/display_device/settings.rst b/docs/source/source_code/src/display_device/settings.rst new file mode 100644 index 00000000000..f0f9dd82f2d --- /dev/null +++ b/docs/source/source_code/src/display_device/settings.rst @@ -0,0 +1,4 @@ +settings +======== + +.. todo:: Add settings.h diff --git a/docs/source/source_code/src/display_device/to_string.rst b/docs/source/source_code/src/display_device/to_string.rst new file mode 100644 index 00000000000..d0211b9423c --- /dev/null +++ b/docs/source/source_code/src/display_device/to_string.rst @@ -0,0 +1,4 @@ +to_string +========= + +.. todo:: Add to_string.h diff --git a/docs/source/source_code/src/platform/windows/display_device/settings_data.rst b/docs/source/source_code/src/platform/windows/display_device/settings_data.rst new file mode 100644 index 00000000000..893209c7ab0 --- /dev/null +++ b/docs/source/source_code/src/platform/windows/display_device/settings_data.rst @@ -0,0 +1,4 @@ +settings_data +============= + +.. todo:: Add settings_data.h diff --git a/docs/source/source_code/src/platform/windows/display_device/settings_topology.rst b/docs/source/source_code/src/platform/windows/display_device/settings_topology.rst new file mode 100644 index 00000000000..b0d242dc6b1 --- /dev/null +++ b/docs/source/source_code/src/platform/windows/display_device/settings_topology.rst @@ -0,0 +1,4 @@ +settings_topology +================= + +.. todo:: Add settings_topology.h diff --git a/docs/source/source_code/src/platform/windows/display_device/windows_utils.rst b/docs/source/source_code/src/platform/windows/display_device/windows_utils.rst new file mode 100644 index 00000000000..d1af6fde5fb --- /dev/null +++ b/docs/source/source_code/src/platform/windows/display_device/windows_utils.rst @@ -0,0 +1,4 @@ +windows_utils +============= + +.. todo:: Add windows_utils.h diff --git a/src/config.cpp b/src/config.cpp index 7418fc0c112..b3fccecd4fd 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -20,6 +20,7 @@ #include "rtsp.h" #include "utility.h" +#include "display_device/parsed_config.h" #include "platform/common.h" #ifdef _WIN32 @@ -363,7 +364,14 @@ namespace config { {}, // capture {}, // encoder {}, // adapter_name + {}, // output_name + (int) display_device::parsed_config_t::device_prep_e::no_operation, // display_device_prep + (int) display_device::parsed_config_t::resolution_change_e::automatic, // resolution_change + {}, // manual_resolution + (int) display_device::parsed_config_t::refresh_rate_change_e::automatic, // refresh_rate_change + {}, // manual_refresh_rate + (int) display_device::parsed_config_t::hdr_prep_e::automatic // hdr_prep }; audio_t audio { @@ -996,7 +1004,14 @@ namespace config { string_f(vars, "capture", video.capture); string_f(vars, "encoder", video.encoder); string_f(vars, "adapter_name", video.adapter_name); + string_f(vars, "output_name", video.output_name); + int_f(vars, "display_device_prep", video.display_device_prep, display_device::parsed_config_t::device_prep_from_view); + int_f(vars, "resolution_change", video.resolution_change, display_device::parsed_config_t::resolution_change_from_view); + string_f(vars, "manual_resolution", video.manual_resolution); + int_f(vars, "refresh_rate_change", video.refresh_rate_change, display_device::parsed_config_t::refresh_rate_change_from_view); + string_f(vars, "manual_refresh_rate", video.manual_refresh_rate); + int_f(vars, "hdr_prep", video.hdr_prep, display_device::parsed_config_t::hdr_prep_from_view); path_f(vars, "pkey", nvhttp.pkey); path_f(vars, "cert", nvhttp.cert); diff --git a/src/config.h b/src/config.h index e08a87f3c4d..9dbb4292395 100644 --- a/src/config.h +++ b/src/config.h @@ -71,7 +71,14 @@ namespace config { std::string capture; std::string encoder; std::string adapter_name; + std::string output_name; + int display_device_prep; + int resolution_change; + std::string manual_resolution; + int refresh_rate_change; + std::string manual_refresh_rate; + int hdr_prep; }; struct audio_t { diff --git a/src/display_device/display_device.h b/src/display_device/display_device.h new file mode 100644 index 00000000000..ea89c424615 --- /dev/null +++ b/src/display_device/display_device.h @@ -0,0 +1,182 @@ +#pragma once + +// standard includes +#include +#include +#include +#include + +// lib includes +#include +#include + +namespace display_device { + + enum class device_state_e { + inactive, + active, + primary //! On Windows we can have multiple primary displays (when they are duplicated). + }; + + enum class hdr_state_e { + unknown, //! HDR state could not be retrieved from the system (even if the display could support it). + disabled, + enabled + }; + + // For JSON serialization for hdr_state_e + NLOHMANN_JSON_SERIALIZE_ENUM(hdr_state_e, { { hdr_state_e::unknown, "unknown" }, + { hdr_state_e::disabled, "disabled" }, + { hdr_state_e::enabled, "enabled" } }) + + //! A map of device id to its HDR state (ordered, for predictable print order) + using hdr_state_map_t = std::map; + + struct device_info_t { + //! A name used by the system to represent the logical display this device is connected to. + std::string display_name; + + //! A more human-readable name for the device. + std::string friendly_name; + + //! Current state of the device. + device_state_e device_state; + + //! Current state of the HDR support. + hdr_state_e hdr_state; + }; + + //! A map of device id to its info data (ordered, for predictable print order). + using device_info_map_t = std::map; + + struct resolution_t { + unsigned int width; + unsigned int height; + + // For JSON serialization + NLOHMANN_DEFINE_TYPE_INTRUSIVE(resolution_t, width, height) + }; + + //! Stores a floating point number in a "numerator/denominator" form + struct refresh_rate_t { + unsigned int numerator; + unsigned int denominator; + + // For JSON serialization + NLOHMANN_DEFINE_TYPE_INTRUSIVE(refresh_rate_t, numerator, denominator) + }; + + struct display_mode_t { + resolution_t resolution; + refresh_rate_t refresh_rate; + + // For JSON serialization + NLOHMANN_DEFINE_TYPE_INTRUSIVE(display_mode_t, resolution, refresh_rate) + }; + + // A map of device id to its mode data (ordered, for predictable print order). + using device_display_mode_map_t = std::map; + + /*! + * A list of a list of device ids representing the current topology. + * + * For example: + * ``` + * [[EXTENDED_DISPLAY_1], [DUPLICATED_DISPLAY_1, DUPLICATED_DISPLAY_2], [EXTENDED_DISPLAY_2]] + * ``` + * + * @note On Windows the order does not matter as Windows will take care of the device the placement anyway. + */ + using active_topology_t = std::vector>; + + /*! + * Enumerates the available devices in the system. + */ + device_info_map_t + enum_available_devices(); + + /*! + * Gets display name associated with the device. + * @note returns empty string if the device_id is empty or device is inactive. + */ + std::string + get_display_name(const std::string &device_id); + + /*! + * Get current display mode for the provided devices. + * + * @note empty map will be returned if any of the devices does not have a mode. + */ + device_display_mode_map_t + get_current_display_modes(const std::unordered_set &device_ids); + + /*! + * Try to set the new display modes for the devices. + * + * @warning if any of the specified display are duplicated, modes MUST be provided + * for duplicates too! + */ + bool + set_display_modes(const device_display_mode_map_t &modes); + + /*! + * Check whether the specified device is primary. + */ + bool + is_primary_device(const std::string &device_id); + + /*! + * Try to set the device as a primary display. + * + * @note if the device is duplicated, the other paired device will also become a primary display. + */ + bool + set_as_primary_device(const std::string &device_id); + + /*! + * Try to get the HDR state for the provided devices. + * + * @note on Windows the state cannot be retrieved until the device is active. + */ + hdr_state_map_t + get_current_hdr_states(const std::unordered_set &device_ids); + + /*! + * Try to set the HDR state for the devices. + * + * @note if UNKNOWN states are provided, they will be ignored. + */ + bool + set_hdr_states(const hdr_state_map_t &states); + + /*! + * Get the currently active topology. + * + * @note empty list will be returned if topology could not be retrieved. + */ + active_topology_t + get_current_topology(); + + /*! + * Simply validates the topology to be valid. + */ + bool + is_topology_valid(const active_topology_t &topology); + + /*! + * Checks if the topologies are close enough to be considered the same by the system. + */ + bool + is_topology_the_same(const active_topology_t &a, const active_topology_t &b); + + /*! + * Try to set the active states for the devices. + * + * @warning there is a bug on Windows (yay) where it is unable to sometimes set + * topology correctly, but it thinks it did! See implementation for more + * details. + */ + bool + set_topology(const active_topology_t &new_topology); + +} // namespace display_device diff --git a/src/display_device/parsed_config.cpp b/src/display_device/parsed_config.cpp new file mode 100644 index 00000000000..aadc33ce666 --- /dev/null +++ b/src/display_device/parsed_config.cpp @@ -0,0 +1,227 @@ +// lib includes +#include +#include +#include + +// local includes +#include "parsed_config.h" +#include "src/config.h" +#include "src/main.h" +#include "src/rtsp.h" + +namespace display_device { + + namespace { + bool + parse_resolution_option(const config::video_t &config, const rtsp_stream::launch_session_t &session, parsed_config_t &parsed_config) { + const auto resolution_option { static_cast(config.resolution_change) }; + switch (resolution_option) { + case parsed_config_t::resolution_change_e::automatic: { + if (!session.enable_sops) { + // "Optimize game settings" must be enabled on the client side + parsed_config.resolution = boost::none; + } + else if (session.width >= 0 && session.height >= 0) { + parsed_config.resolution = resolution_t { + static_cast(session.width), + static_cast(session.height) + }; + } + else { + BOOST_LOG(error) << "resolution provided by client session config is invalid: " << session.width << "x" << session.height; + return false; + } + break; + } + case parsed_config_t::resolution_change_e::manual: { + const std::string trimmed_string { boost::algorithm::trim_copy(config.manual_resolution) }; + const boost::regex resolution_regex { R"(^(\d+)x(\d+)$)" }; // std::regex hangs in CTOR for some reason when called in a thread... + + boost::smatch match; + if (boost::regex_match(trimmed_string, match, resolution_regex)) { + try { + parsed_config.resolution = resolution_t { + static_cast(std::stol(match[1])), + static_cast(std::stol(match[2])) + }; + } + catch (const std::invalid_argument &err) { + BOOST_LOG(error) << "failed to parse manual resolution string (invalid argument):\n" + << err.what(); + return false; + } + catch (const std::out_of_range &err) { + BOOST_LOG(error) << "failed to parse manual resolution string (number out of range):\n" + << err.what(); + return false; + } + catch (const std::exception &err) { + BOOST_LOG(error) << "failed to parse manual resolution string:\n" + << err.what(); + return false; + } + } + else { + BOOST_LOG(error) << "failed to parse manual resolution string. It must match a \"WIDTHxHEIGHT\" pattern!"; + return false; + } + break; + } + case parsed_config_t::resolution_change_e::no_operation: + default: + break; + } + + return true; + } + + bool + parse_refresh_rate_option(const config::video_t &config, const rtsp_stream::launch_session_t &session, parsed_config_t &parsed_config) { + const auto refresh_rate_option { static_cast(config.refresh_rate_change) }; + switch (refresh_rate_option) { + case parsed_config_t::refresh_rate_change_e::automatic: { + if (session.fps >= 0) { + parsed_config.refresh_rate = refresh_rate_t { static_cast(session.fps), 1 }; + } + else { + BOOST_LOG(error) << "FPS value provided by client session config is invalid: " << session.fps; + return false; + } + break; + } + case parsed_config_t::refresh_rate_change_e::manual: { + const std::string trimmed_string { boost::algorithm::trim_copy(config.manual_refresh_rate) }; + const boost::regex resolution_regex { R"(^(\d+)(?:\.(\d+))?$)" }; // std::regex hangs in CTOR for some reason when called in a thread... + + boost::smatch match; + if (boost::regex_match(trimmed_string, match, resolution_regex)) { + try { + if (match[2].matched) { + // We have a decimal point and will have to split it into numerator and denominator. + // For example: + // 59.995: + // numerator = 59995 + // denominator = 1000 + + const std::string numerator_str { match[1].str() + match[2].str() }; + const auto numerator { static_cast(std::stol(numerator_str)) }; + const auto denominator { static_cast(std::pow(10, std::distance(match[2].first, match[2].second))) }; + + parsed_config.refresh_rate = refresh_rate_t { numerator, denominator }; + } + else { + parsed_config.refresh_rate = refresh_rate_t { static_cast(std::stol(match[1])), 1 }; + } + } + catch (const std::invalid_argument &err) { + BOOST_LOG(error) << "failed to parse manual refresh rate string (invalid argument):\n" + << err.what(); + return false; + } + catch (const std::out_of_range &err) { + BOOST_LOG(error) << "failed to parse manual refresh rate string (number out of range):\n" + << err.what(); + return false; + } + catch (const std::exception &err) { + BOOST_LOG(error) << "failed to parse manual refresh rate string:\n" + << err.what(); + return false; + } + } + else { + BOOST_LOG(error) << "failed to parse manual refresh rate string! Must have a pattern of \"123\" or \"123.456\"!"; + return false; + } + break; + } + case parsed_config_t::refresh_rate_change_e::no_operation: + default: + break; + } + + return true; + } + + boost::optional + parse_hdr_option(const config::video_t &config, const rtsp_stream::launch_session_t &session) { + const auto hdr_prep_option { static_cast(config.hdr_prep) }; + switch (hdr_prep_option) { + case parsed_config_t::hdr_prep_e::automatic: + return session.enable_hdr; + case parsed_config_t::hdr_prep_e::no_operation: + default: + return boost::none; + } + } + } // namespace + + int + parsed_config_t::device_prep_from_view(std::string_view value) { + using namespace std::string_view_literals; +#define _CONVERT_(x) \ + if (value == #x##sv) return static_cast(parsed_config_t::device_prep_e::x); + _CONVERT_(no_operation); + _CONVERT_(ensure_active); + _CONVERT_(ensure_primary); + _CONVERT_(ensure_only_display); +#undef _CONVERT_ + return static_cast(parsed_config_t::device_prep_e::no_operation); + } + + int + parsed_config_t::resolution_change_from_view(std::string_view value) { + using namespace std::string_view_literals; +#define _CONVERT_(x) \ + if (value == #x##sv) return static_cast(parsed_config_t::resolution_change_e::x); + _CONVERT_(no_operation); + _CONVERT_(automatic); + _CONVERT_(manual); +#undef _CONVERT_ + return static_cast(parsed_config_t::resolution_change_e::no_operation); + } + + int + parsed_config_t::refresh_rate_change_from_view(std::string_view value) { + using namespace std::string_view_literals; +#define _CONVERT_(x) \ + if (value == #x##sv) return static_cast(parsed_config_t::refresh_rate_change_e::x); + _CONVERT_(no_operation); + _CONVERT_(automatic); + _CONVERT_(manual); +#undef _CONVERT_ + return static_cast(parsed_config_t::refresh_rate_change_e::no_operation); + } + + int + parsed_config_t::hdr_prep_from_view(std::string_view value) { + using namespace std::string_view_literals; +#define _CONVERT_(x) \ + if (value == #x##sv) return static_cast(parsed_config_t::hdr_prep_e::x); + _CONVERT_(no_operation); + _CONVERT_(automatic); +#undef _CONVERT_ + return static_cast(parsed_config_t::hdr_prep_e::no_operation); + } + + boost::optional + make_parsed_config(const config::video_t &config, const rtsp_stream::launch_session_t &session) { + parsed_config_t parsed_config; + parsed_config.device_id = config.output_name; + parsed_config.device_prep = static_cast(config.display_device_prep); + parsed_config.change_hdr_state = parse_hdr_option(config, session); + + if (!parse_resolution_option(config, session, parsed_config)) { + // Error already logged + return boost::none; + } + + if (!parse_refresh_rate_option(config, session, parsed_config)) { + // Error already logged + return boost::none; + } + + return parsed_config; + } + +} // namespace display_device diff --git a/src/display_device/parsed_config.h b/src/display_device/parsed_config.h new file mode 100644 index 00000000000..4d527b1b181 --- /dev/null +++ b/src/display_device/parsed_config.h @@ -0,0 +1,65 @@ +#pragma once + +// local includes +#include "display_device.h" + +// forward declarations +namespace config { + struct video_t; +} +namespace rtsp_stream { + struct launch_session_t; +} + +namespace display_device { + + //! Config that was parsed from video config and session params, and is ready to be applied. + struct parsed_config_t { + enum class device_prep_e : int { + no_operation, //!< User has to make sure the display device is active + ensure_active, //!< Activate the device if needed + ensure_primary, //!< Activate the device if needed and make it a primary display + ensure_only_display //!< Deactivate other displays and turn on the specified one + }; + static int + device_prep_from_view(std::string_view value); + + enum class resolution_change_e : int { + no_operation, //!< Keep the current resolution + automatic, //!< Set the resolution to the one received from the client + manual //!< User has to specify the resolution + }; + static int + resolution_change_from_view(std::string_view value); + + enum class refresh_rate_change_e : int { + no_operation, //!< Keep the current refresh rate + automatic, //!< Set the refresh rate to the FPS value received from the client + manual //!< User has to specify the refresh rate + }; + static int + refresh_rate_change_from_view(std::string_view value); + + enum class hdr_prep_e : int { + no_operation, //!< User has to switch the HDR state manually + automatic //!< Switch HDR state based on session settings and if display supports it + }; + static int + hdr_prep_from_view(std::string_view value); + + std::string device_id; + device_prep_e device_prep; + boost::optional resolution; + boost::optional refresh_rate; + boost::optional change_hdr_state; + }; + + /*! + * Parses the configuration and session parameters. + * + * @returns config that is ready to be used or empty optional if some error has occurred. + */ + boost::optional + make_parsed_config(const config::video_t &config, const rtsp_stream::launch_session_t &session); + +} // namespace display_device diff --git a/src/display_device/session.cpp b/src/display_device/session.cpp new file mode 100644 index 00000000000..fd6a120c554 --- /dev/null +++ b/src/display_device/session.cpp @@ -0,0 +1,40 @@ +// local includes +#include "session.h" +#include "src/platform/common.h" +#include "to_string.h" + +namespace display_device { + + session_t::deinit_t::~deinit_t() { + session_t::get().restore_state(); + } + + session_t & + session_t::get() { + static session_t session; + return session; + } + + std::unique_ptr + session_t::init() { + const auto devices { enum_available_devices() }; + if (!devices.empty()) { + BOOST_LOG(info) << "available display devices: " << to_string(devices); + } + + session_t::get().settings.set_filepath(platf::appdata().string() + "/original_display_settings.json"); + session_t::get().restore_state(); + return std::make_unique(); + } + + settings_t::apply_result_t + session_t::configure_display(const config::video_t &config, const rtsp_stream::launch_session_t &session) { + return settings.apply_config(config, session); + } + + void + session_t::restore_state() { + return settings.revert_settings(); + } + +} // namespace display_device diff --git a/src/display_device/session.h b/src/display_device/session.h new file mode 100644 index 00000000000..d8ba00e2637 --- /dev/null +++ b/src/display_device/session.h @@ -0,0 +1,50 @@ +#pragma once + +// local includes +#include "settings.h" + +namespace display_device { + + //! A singleton for managing the display state for the current Sunshine session + class session_t { + public: + class deinit_t { + public: + virtual ~deinit_t(); + }; + + /*! + * Gets the current session instance. + */ + static session_t & + get(); + + /*! + * Initializes the session which will perform recovery and cleanup in case we crashed + * or did an unexpected shutdown. + */ + static std::unique_ptr + init(); + + /*! + * Prepares the display device based on the session and the configuration. + * + * @returns result structure indicating whether we can continue with the streaming session creation or not. + */ + settings_t::apply_result_t + configure_display(const config::video_t &config, const rtsp_stream::launch_session_t &session); + + /*! + * Try to restore the previous display state. + * + * @note Not everything can be restored if the display was unplugged, etc. + */ + void + restore_state(); + + private: + explicit session_t() = default; + settings_t settings; + }; + +} // namespace display_device diff --git a/src/display_device/settings.cpp b/src/display_device/settings.cpp new file mode 100644 index 00000000000..e9795a4045d --- /dev/null +++ b/src/display_device/settings.cpp @@ -0,0 +1,46 @@ +// local includes +#include "settings.h" +#include "src/main.h" + +namespace display_device { + + settings_t::apply_result_t::operator bool() const { + return result == result_e::success; + } + + int + settings_t::apply_result_t::get_error_code() const { + return static_cast(result); + } + + std::string + settings_t::apply_result_t::get_error_message() const { + switch (result) { + case result_e::success: + return "Success"; + case result_e::config_parse_fail: + return "Failed to parse configuration"; + case result_e::validation_fail: + return "Failed to validate display settings"; + case result_e::topology_fail: + return "Failed to change or validate the display topology"; + case result_e::primary_display_fail: + return "Failed to change primary display"; + case result_e::modes_fail: + return "Failed to set new display modes"; + case result_e::hdr_states_fail: + return "Failed to set new HDR states"; + case result_e::file_save_fail: + return "Failed to save the original settings file"; + default: + BOOST_LOG(fatal) << "result_e conversion not implemented!"; + return "FATAL"; + } + } + + void + settings_t::set_filepath(std::filesystem::path filepath) { + this->filepath = std::move(filepath); + } + +} // namespace display_device diff --git a/src/display_device/settings.h b/src/display_device/settings.h new file mode 100644 index 00000000000..2402aca62cf --- /dev/null +++ b/src/display_device/settings.h @@ -0,0 +1,75 @@ +#pragma once + +// standard includes +#include +#include + +// local includes +#include "parsed_config.h" + +namespace display_device { + + //! A platform specific class that applies and reverts the changes to the display devices. + class settings_t { + public: + //! Convenience structure for informing the user about the failure type. + struct apply_result_t { + enum class result_e : int { + success = 0, + config_parse_fail = 700, + validation_fail, + topology_fail, + primary_display_fail, + modes_fail, + hdr_states_fail, + file_save_fail + }; + + operator bool() const; + + int + get_error_code() const; + + std::string + get_error_message() const; + + result_e result; + }; + + explicit settings_t(); + virtual ~settings_t(); + + //! Sets the filepath to save persistent data to. + void + set_filepath(std::filesystem::path filepath); + + //! Parses the provided configurations and tries to apply them. + apply_result_t + apply_config(const config::video_t &config, const rtsp_stream::launch_session_t &session); + + //! Tries to apply the provided configuration. + apply_result_t + apply_config(const parsed_config_t &config); + + //! Reverts the applied settings either from cache or persistent file. + void + revert_settings(); + + private: + // Platform specific data + struct data_t; + + void + revert_settings(const data_t &data); + + bool + save_settings(const data_t &data); + + void + load_settings(); + + std::unique_ptr data; + std::filesystem::path filepath; + }; + +} // namespace display_device diff --git a/src/display_device/to_string.cpp b/src/display_device/to_string.cpp new file mode 100644 index 00000000000..626b443fd0c --- /dev/null +++ b/src/display_device/to_string.cpp @@ -0,0 +1,142 @@ +// local includes +#include "to_string.h" +#include "src/main.h" + +namespace display_device { + + std::string + to_string(device_state_e value) { + switch (value) { + case device_state_e::inactive: + return "INACTIVE"; + case device_state_e::active: + return "ACTIVE"; + case device_state_e::primary: + return "PRIMARY"; + default: + BOOST_LOG(fatal) << "device_state_e conversion not implemented!"; + return {}; + } + } + + std::string + to_string(hdr_state_e value) { + switch (value) { + case hdr_state_e::unknown: + return "UNKNOWN"; + case hdr_state_e::disabled: + return "DISABLED"; + case hdr_state_e::enabled: + return "ENABLED"; + default: + BOOST_LOG(fatal) << "hdr_state_e conversion not implemented!"; + return {}; + } + } + + std::string + to_string(const hdr_state_map_t &value) { + std::stringstream output; + for (const auto &item : value) { + output << std::endl + << item.first << " -> " << to_string(item.second); + } + return output.str(); + } + + std::string + to_string(const device_info_t &value) { + std::stringstream output; + output << "DISPLAY NAME: " << (value.display_name.empty() ? "NOT AVAILABLE" : value.display_name) << std::endl; + output << "FRIENDLY NAME: " << (value.friendly_name.empty() ? "NOT AVAILABLE" : value.friendly_name) << std::endl; + output << "DEVICE STATE: " << to_string(value.device_state) << std::endl; + output << "HDR STATE: " << to_string(value.hdr_state); + return output.str(); + } + + std::string + to_string(const device_info_map_t &value) { + std::stringstream output; + bool output_is_empty { true }; + for (const auto &item : value) { + output << std::endl; + if (!output_is_empty) { + output << "-----------------------" << std::endl; + } + + output << "DEVICE ID: " << item.first << std::endl; + output << to_string(item.second); + output_is_empty = false; + } + return output.str(); + } + + std::string + to_string(const resolution_t &value) { + std::stringstream output; + output << value.width << "x" << value.height; + return output.str(); + } + + std::string + to_string(const refresh_rate_t &value) { + std::stringstream output; + if (value.denominator > 0) { + output << (static_cast(value.numerator) / value.denominator); + } + else { + output << "INF"; + } + return output.str(); + } + + std::string + to_string(const display_mode_t &value) { + std::stringstream output; + output << to_string(value.resolution) << "x" << to_string(value.refresh_rate); + return output.str(); + } + + std::string + to_string(const device_display_mode_map_t &value) { + std::stringstream output; + for (const auto &item : value) { + output << std::endl + << item.first << " -> " << to_string(item.second); + } + return output.str(); + } + + std::string + to_string(const active_topology_t &value) { + std::stringstream output; + bool first_group { true }; + + output << std::endl + << "[" << std::endl; + for (const auto &group : value) { + if (!first_group) { + output << "," << std::endl; + } + first_group = false; + + output << " [" << std::endl; + bool first_group_item { true }; + for (const auto &group_item : group) { + if (!first_group_item) { + output << "," << std::endl; + } + first_group_item = false; + + output << " " << group_item; + } + output << std::endl + << " ]"; + } + output << std::endl + << "]"; + + return output.str(); + } + +} // namespace display_device diff --git a/src/display_device/to_string.h b/src/display_device/to_string.h new file mode 100644 index 00000000000..e44bfd84bdd --- /dev/null +++ b/src/display_device/to_string.h @@ -0,0 +1,38 @@ +#pragma once + +// local includes +#include "display_device.h" + +namespace display_device { + + std::string + to_string(device_state_e value); + + std::string + to_string(hdr_state_e value); + + std::string + to_string(const hdr_state_map_t &value); + + std::string + to_string(const device_info_t &value); + + std::string + to_string(const device_info_map_t &value); + + std::string + to_string(const resolution_t &value); + + std::string + to_string(const refresh_rate_t &value); + + std::string + to_string(const display_mode_t &value); + + std::string + to_string(const device_display_mode_map_t &value); + + std::string + to_string(const active_topology_t &value); + +} // namespace display_device diff --git a/src/main.cpp b/src/main.cpp index 45febf7025b..72045a85873 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,6 +20,7 @@ // local includes #include "config.h" #include "confighttp.h" +#include "display_device/session.h" #include "httpcommon.h" #include "main.h" #include "nvhttp.h" @@ -601,6 +602,14 @@ main(int argc, char *argv[]) { return fn->second(argv[0], config::sunshine.cmd.argc, config::sunshine.cmd.argv); } + // Adding this guard here first as it also performs recovery after crash, + // otherwise people could theoretically end up without display output. + // It also should be run be destroyed before forced shutdown. + auto display_device_deinit_guard = display_device::session_t::init(); + if (!display_device_deinit_guard) { + BOOST_LOG(error) << "Display device session failed to initialize"sv; + } + #ifdef WIN32 // Modify relevant NVIDIA control panel settings if the system has corresponding gpu if (nvprefs_instance.load()) { @@ -699,7 +708,7 @@ main(int argc, char *argv[]) { // Create signal handler after logging has been initialized auto shutdown_event = mail::man->event(mail::shutdown); - on_signal(SIGINT, [&force_shutdown, shutdown_event]() { + on_signal(SIGINT, [&force_shutdown, &display_device_deinit_guard, shutdown_event]() { BOOST_LOG(info) << "Interrupt handler called"sv; auto task = []() { @@ -710,9 +719,10 @@ main(int argc, char *argv[]) { force_shutdown = task_pool.pushDelayed(task, 10s).task_id; shutdown_event->raise(true); + display_device_deinit_guard.reset(); }); - on_signal(SIGTERM, [&force_shutdown, shutdown_event]() { + on_signal(SIGTERM, [&force_shutdown, &display_device_deinit_guard, shutdown_event]() { BOOST_LOG(info) << "Terminate handler called"sv; auto task = []() { @@ -723,6 +733,7 @@ main(int argc, char *argv[]) { force_shutdown = task_pool.pushDelayed(task, 10s).task_id; shutdown_event->raise(true); + display_device_deinit_guard.reset(); }); proc::refresh(config::stream.file_apps); diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 037503c9a54..405e1f64b21 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -22,6 +22,7 @@ // local includes #include "config.h" #include "crypto.h" +#include "display_device/session.h" #include "httpcommon.h" #include "main.h" #include "network.h" @@ -750,12 +751,17 @@ namespace nvhttp { print_req(request); pt::ptree tree; + bool need_to_restore_display_state { false }; auto g = util::fail_guard([&]() { std::ostringstream data; pt::write_xml(data, tree); response->write(data.str()); response->close_connection_after_response = true; + + if (need_to_restore_display_state) { + display_device::session_t::get().restore_state(); + } }); if (rtsp_stream::session_count() == config::stream.channels) { @@ -790,11 +796,29 @@ namespace nvhttp { return; } - // Probe encoders again before streaming to ensure our chosen - // encoder matches the active GPU (which could have changed - // due to hotplugging, driver crash, primary monitor change, - // or any number of other factors). + host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); + const auto launch_session = make_launch_session(host_audio, args); + if (rtsp_stream::session_count() == 0) { + // We want to prepare display only if there are no active sessions at + // the moment. This needs to be done before probing encoders as it could + // change display device's state. + const auto result { display_device::session_t::get().configure_display(config::video, launch_session) }; + if (!result) { + tree.put("root..status_code", result.get_error_code()); + tree.put("root..status_message", result.get_error_message()); + tree.put("root.gamesession", 0); + + return; + } + + // The display should be restored by the fail guard in case something happens. + need_to_restore_display_state = true; + + // Probe encoders again before streaming to ensure our chosen + // encoder matches the active GPU (which could have changed + // due to hotplugging, driver crash, primary monitor change, + // or any number of other factors). if (video::probe_encoders()) { tree.put("root..status_code", 503); tree.put("root..status_message", "Failed to initialize video capture/encoding. Is a display connected and turned on?"); @@ -804,9 +828,6 @@ namespace nvhttp { } } - host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); - auto launch_session = make_launch_session(host_audio, args); - if (appid > 0) { auto err = proc::proc.execute(appid, launch_session); if (err) { @@ -823,6 +844,9 @@ namespace nvhttp { tree.put("root..status_code", 200); tree.put("root.sessionUrl0", "rtsp://"s + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); tree.put("root.gamesession", 1); + + // Stream was started successfully, we will restore the state when the app or session terminates + need_to_restore_display_state = false; } void @@ -868,7 +892,28 @@ namespace nvhttp { return; } + // Newer Moonlight clients send localAudioPlayMode on /resume too, + // so we should use it if it's present in the args and there are + // no active sessions we could be interfering with. + if (rtsp_stream::session_count() == 0 && args.find("localAudioPlayMode"s) != std::end(args)) { + host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); + } + const auto launch_session = make_launch_session(host_audio, args); + if (rtsp_stream::session_count() == 0) { + // We want to prepare display only if there are no active sessions at + // the moment. This needs to be done before probing encoders as it could + // change display device's state. Since we are resuming the stream, + // the display state shall not be restored in case something else fails. + const auto result { display_device::session_t::get().configure_display(config::video, launch_session) }; + if (!result) { + tree.put("root..status_code", result.get_error_code()); + tree.put("root..status_message", result.get_error_message()); + tree.put("root.gamesession", 0); + + return; + } + // Probe encoders again before streaming to ensure our chosen // encoder matches the active GPU (which could have changed // due to hotplugging, driver crash, primary monitor change, @@ -880,16 +925,9 @@ namespace nvhttp { return; } - - // Newer Moonlight clients send localAudioPlayMode on /resume too, - // so we should use it if it's present in the args and there are - // no active sessions we could be interfering with. - if (args.find("localAudioPlayMode"s) != std::end(args)) { - host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); - } } - rtsp_stream::launch_session_raise(make_launch_session(host_audio, args)); + rtsp_stream::launch_session_raise(launch_session); tree.put("root..status_code", 200); tree.put("root.sessionUrl0", "rtsp://"s + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); @@ -925,6 +963,9 @@ namespace nvhttp { if (proc::proc.running() > 0) { proc::proc.terminate(); } + + // The state needs to be restored regardless of whether "proc::proc.terminate()" was called or not. + display_device::session_t::get().restore_state(); } void diff --git a/src/platform/linux/display_device.cpp b/src/platform/linux/display_device.cpp new file mode 100644 index 00000000000..18b12ddded1 --- /dev/null +++ b/src/platform/linux/display_device.cpp @@ -0,0 +1,123 @@ +// local includes +#include "src/display_device/settings.h" + +namespace display_device { + + device_info_map_t + enum_available_devices() { + // Not implemented + return {}; + } + + std::string + get_display_name(const std::string &value) { + // Not implemented, but just passthrough the value + return value; + } + + device_display_mode_map_t + get_current_display_modes(const std::unordered_set &) { + // Not implemented + return {}; + } + + bool + set_display_modes(const device_display_mode_map_t &) { + // Not implemented + return false; + } + + bool + is_primary_device(const std::string &) { + // Not implemented + return false; + } + + bool + set_as_primary_device(const std::string &) { + // Not implemented + return false; + } + + hdr_state_map_t + get_current_hdr_states(const std::unordered_set &) { + // Not implemented + return {}; + } + + bool + set_hdr_states(const hdr_state_map_t &) { + // Not implemented + return false; + } + + active_topology_t + get_current_topology() { + // Not implemented + return {}; + } + + bool + is_topology_valid(const active_topology_t &topology) { + // Not implemented + return false; + } + + bool + is_topology_the_same(const active_topology_t &a, const active_topology_t &b) { + // Not implemented + return false; + } + + bool + set_topology(const active_topology_t &) { + // Not implemented + return false; + } + + struct settings_t::data_t { + // Not implemented + }; + + settings_t::settings_t() { + // Not implemented + } + + settings_t::~settings_t() { + // Not implemented + } + + settings_t::apply_result_t + settings_t::apply_config(const config::video_t &, const rtsp_stream::launch_session_t &) { + // Not implemented + return { apply_result_t::result_e::success }; + } + + settings_t::apply_result_t + settings_t::apply_config(const parsed_config_t &) { + // Not implemented + return { apply_result_t::result_e::success }; + } + + void + settings_t::revert_settings() { + // Not implemented + } + + void + settings_t::revert_settings(const data_t &) { + // Not implemented + } + + bool + settings_t::save_settings(const data_t &) { + // Not implemented + return false; + } + + void + settings_t::load_settings() { + // Not implemented + } + +} // namespace display_device diff --git a/src/platform/macos/display_device.cpp b/src/platform/macos/display_device.cpp new file mode 100644 index 00000000000..18b12ddded1 --- /dev/null +++ b/src/platform/macos/display_device.cpp @@ -0,0 +1,123 @@ +// local includes +#include "src/display_device/settings.h" + +namespace display_device { + + device_info_map_t + enum_available_devices() { + // Not implemented + return {}; + } + + std::string + get_display_name(const std::string &value) { + // Not implemented, but just passthrough the value + return value; + } + + device_display_mode_map_t + get_current_display_modes(const std::unordered_set &) { + // Not implemented + return {}; + } + + bool + set_display_modes(const device_display_mode_map_t &) { + // Not implemented + return false; + } + + bool + is_primary_device(const std::string &) { + // Not implemented + return false; + } + + bool + set_as_primary_device(const std::string &) { + // Not implemented + return false; + } + + hdr_state_map_t + get_current_hdr_states(const std::unordered_set &) { + // Not implemented + return {}; + } + + bool + set_hdr_states(const hdr_state_map_t &) { + // Not implemented + return false; + } + + active_topology_t + get_current_topology() { + // Not implemented + return {}; + } + + bool + is_topology_valid(const active_topology_t &topology) { + // Not implemented + return false; + } + + bool + is_topology_the_same(const active_topology_t &a, const active_topology_t &b) { + // Not implemented + return false; + } + + bool + set_topology(const active_topology_t &) { + // Not implemented + return false; + } + + struct settings_t::data_t { + // Not implemented + }; + + settings_t::settings_t() { + // Not implemented + } + + settings_t::~settings_t() { + // Not implemented + } + + settings_t::apply_result_t + settings_t::apply_config(const config::video_t &, const rtsp_stream::launch_session_t &) { + // Not implemented + return { apply_result_t::result_e::success }; + } + + settings_t::apply_result_t + settings_t::apply_config(const parsed_config_t &) { + // Not implemented + return { apply_result_t::result_e::success }; + } + + void + settings_t::revert_settings() { + // Not implemented + } + + void + settings_t::revert_settings(const data_t &) { + // Not implemented + } + + bool + settings_t::save_settings(const data_t &) { + // Not implemented + return false; + } + + void + settings_t::load_settings() { + // Not implemented + } + +} // namespace display_device diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 64954bbf283..b73af99ac73 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -15,6 +15,7 @@ typedef long NTSTATUS; #include "display.h" #include "misc.h" #include "src/config.h" +#include "src/display_device/display_device.h" #include "src/main.h" #include "src/platform/common.h" #include "src/stat_trackers.h" @@ -1068,7 +1069,8 @@ namespace platf { std::wstring_convert, wchar_t> converter; // We must set the GPU preference before calling any DXGI APIs! - if (!dxgi::probe_for_gpu_preference(config::video.output_name)) { + const auto output_display_name { display_device::get_display_name(config::video.output_name) }; + if (!dxgi::probe_for_gpu_preference(output_display_name)) { BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv; } diff --git a/src/platform/windows/display_device/device_hdr_states.cpp b/src/platform/windows/display_device/device_hdr_states.cpp new file mode 100644 index 00000000000..145eb725d65 --- /dev/null +++ b/src/platform/windows/display_device/device_hdr_states.cpp @@ -0,0 +1,105 @@ +// local includes +#include "src/display_device/to_string.h" +#include "src/main.h" +#include "windows_utils.h" + +namespace display_device { + + namespace { + + bool + do_set_states(const hdr_state_map_t &states) { + const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return false; + } + + for (const auto &[device_id, state] : states) { + const auto path { w_utils::get_active_path(device_id, display_data->paths) }; + if (!path) { + BOOST_LOG(error) << "failed to find device for " << device_id << "!"; + return false; + } + + if (state == hdr_state_e::unknown) { + // We cannot change state to unknown, so we are just ignoring these entries + // for convenience. + continue; + } + + const auto current_state { w_utils::get_hdr_state(*path) }; + if (current_state == hdr_state_e::unknown) { + BOOST_LOG(error) << "HDR state cannot be changed for " << device_id << "!"; + return false; + } + + if (!w_utils::set_hdr_state(*path, state == hdr_state_e::enabled)) { + // Error already logged + return false; + } + } + + return true; + }; + + } // namespace + + hdr_state_map_t + get_current_hdr_states(const std::unordered_set &device_ids) { + if (device_ids.empty()) { + BOOST_LOG(error) << "device id set is empty!"; + return {}; + } + + const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return {}; + } + + hdr_state_map_t states; + for (const auto &device_id : device_ids) { + const auto path { w_utils::get_active_path(device_id, display_data->paths) }; + if (!path) { + BOOST_LOG(error) << "failed to find device for " << device_id << "!"; + return {}; + } + + states[device_id] = w_utils::get_hdr_state(*path); + } + + return states; + } + + bool + set_hdr_states(const hdr_state_map_t &states) { + if (states.empty()) { + BOOST_LOG(error) << "states map is empty!"; + return false; + } + + std::unordered_set device_ids; + for (const auto &[device_id, _] : states) { + if (!device_ids.insert(device_id).second) { + // Sanity check since, it's technically not possible with unordered map to have duplicate keys + BOOST_LOG(error) << "duplicate device id provided: " << device_id << "!"; + return false; + } + } + + const auto original_states { get_current_hdr_states(device_ids) }; + if (original_states.empty()) { + // Error already logged + return false; + } + + if (!do_set_states(states)) { + do_set_states(original_states); // return value does not matter + return false; + } + + return true; + } + +} // namespace display_device diff --git a/src/platform/windows/display_device/device_modes.cpp b/src/platform/windows/display_device/device_modes.cpp new file mode 100644 index 00000000000..8d959b4710a --- /dev/null +++ b/src/platform/windows/display_device/device_modes.cpp @@ -0,0 +1,306 @@ +// local includes +#include "src/main.h" +#include "windows_utils.h" + +namespace display_device { + + namespace { + + bool + fuzzy_compare_refresh_rates(const refresh_rate_t &r1, const refresh_rate_t &r2, const float max_diff = 1.f) { + if (r1.denominator > 0 && r2.denominator > 0) { + const float r1_f { static_cast(r1.numerator) / r1.denominator }; + const float r2_f { static_cast(r2.numerator) / r2.denominator }; + return (std::abs(r1_f - r2_f) <= max_diff); + } + + return false; + } + + bool + fuzzy_compare_modes(const display_mode_t &a, const display_mode_t &b) { + return a.resolution.width == b.resolution.width && + a.resolution.height == b.resolution.height && + fuzzy_compare_refresh_rates(a.refresh_rate, b.refresh_rate); + } + + /* + * Get all the devices that are duplicated ones. See comment where it is used as to + * why we need this. + */ + std::unordered_set + get_all_duplicated_devices(const std::unordered_set &device_ids) { + const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return {}; + } + + // We start by iterating over the provided device id (or paths) and try to get a source mode + // which contains the necessary info + std::unordered_set all_device_ids; + for (const auto &device_id : device_ids) { + if (device_id.empty()) { + BOOST_LOG(error) << "device it is empty!"; + return {}; + } + + const auto provided_path { w_utils::get_active_path(device_id, display_data->paths) }; + if (!provided_path) { + BOOST_LOG(warning) << "failed to find device for " << device_id << "!"; + return {}; + } + + const auto provided_path_source_mode { w_utils::get_source_mode(w_utils::get_source_index(*provided_path, display_data->modes), display_data->modes) }; + if (!provided_path_source_mode) { + BOOST_LOG(error) << "active device does not have a source mode: " << device_id << "!"; + return {}; + } + + // We will now iterate over all of the active paths (provided path included) and check if + // any of them are duplicated. + for (const auto &path : display_data->paths) { + const auto current_id { w_utils::get_device_id_for_valid_path(path, w_utils::ACTIVE_ONLY_DEVICES) }; + if (current_id.empty()) { + continue; + } + + if (all_device_ids.count(current_id) > 0) { + // Already checked + continue; + } + + const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(path, display_data->modes), display_data->modes) }; + if (!source_mode) { + BOOST_LOG(error) << "active device does not have a source mode: " << current_id << "!"; + return {}; + } + + if (!w_utils::are_duplicated_modes(*provided_path_source_mode, *source_mode)) { + continue; + } + + all_device_ids.insert(current_id); + } + } + + return all_device_ids; + } + + bool + do_set_modes(const device_display_mode_map_t &modes, bool allow_changes) { + auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return false; + } + + bool changes_applied { false }; + for (const auto &[device_id, mode] : modes) { + const auto path { w_utils::get_active_path(device_id, display_data->paths) }; + if (!path) { + BOOST_LOG(error) << "failed to find device for " << device_id << "!"; + return false; + } + + const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(*path, display_data->modes), display_data->modes) }; + if (!source_mode) { + BOOST_LOG(error) << "active device does not have a source mode: " << device_id << "!"; + return false; + } + + bool new_changes { false }; + const bool resolution_changed { source_mode->width != mode.resolution.width || source_mode->height != mode.resolution.height }; + const bool refresh_rate_changed { path->targetInfo.refreshRate.Numerator != mode.refresh_rate.numerator || + path->targetInfo.refreshRate.Denominator != mode.refresh_rate.denominator }; + + if (resolution_changed) { + source_mode->width = mode.resolution.width; + source_mode->height = mode.resolution.height; + new_changes = true; + } + + if (refresh_rate_changed) { + path->targetInfo.refreshRate = { mode.refresh_rate.numerator, mode.refresh_rate.denominator }; + new_changes = true; + } + + if (new_changes) { + // Clear the target index so that Windows has to select a new target mode. + w_utils::set_target_index(*path, boost::none); + w_utils::set_desktop_index(*path, boost::none); // Part of struct containing target index and so it needs to be cleared + } + + changes_applied = changes_applied || new_changes; + } + + if (!changes_applied) { + BOOST_LOG(debug) << "no changes were made to display modes."; + return true; + } + + UINT32 flags { SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_VIRTUAL_MODE_AWARE }; + if (allow_changes) { + // It's probably best for Windows to select the "best" display settings for us. However, in case we + // have custom resolution set in nvidia control panel for example, this flag will prevent successfully applying + // settings to it. + flags |= SDC_ALLOW_CHANGES; + } + + const LONG result { SetDisplayConfig(display_data->paths.size(), display_data->paths.data(), display_data->modes.size(), display_data->modes.data(), flags) }; + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << w_utils::get_ccd_error_string(result) << " failed to set display mode!"; + return false; + } + + return true; + }; + + } // namespace + + device_display_mode_map_t + get_current_display_modes(const std::unordered_set &device_ids) { + if (device_ids.empty()) { + BOOST_LOG(error) << "device id set is empty!"; + return {}; + } + + const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return {}; + } + + device_display_mode_map_t current_modes; + for (const auto &device_id : device_ids) { + if (device_id.empty()) { + BOOST_LOG(error) << "device id is empty!"; + return {}; + } + + const auto path { w_utils::get_active_path(device_id, display_data->paths) }; + if (!path) { + BOOST_LOG(error) << "failed to find device for " << device_id << "!"; + return {}; + } + + const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(*path, display_data->modes), display_data->modes) }; + if (!source_mode) { + BOOST_LOG(error) << "active device does not have a source mode: " << device_id << "!"; + return {}; + } + + // For whatever reason they put refresh rate into path, but not the resolution. + const auto target_refresh_rate { path->targetInfo.refreshRate }; + current_modes[device_id] = display_mode_t { + { source_mode->width, source_mode->height }, + { target_refresh_rate.Numerator, target_refresh_rate.Denominator } + }; + } + + return current_modes; + } + + bool + set_display_modes(const device_display_mode_map_t &modes) { + if (modes.empty()) { + BOOST_LOG(error) << "modes map is empty!"; + return false; + } + + std::unordered_set device_ids; + for (const auto &[device_id, _] : modes) { + if (!device_ids.insert(device_id).second) { + // Sanity check since, it's technically not possible with unordered map to have duplicate keys + BOOST_LOG(error) << "duplicate device id provided: " << device_id << "!"; + return false; + } + } + + // Here it is important to check that we have all the necessary modes, otherwise + // setting modes will fail with ambiguous message. + // + // Duplicated devices can have different target modes (monitor) with different refresh rate, + // however this does not apply to the source mode (frame buffer?) and they must have same + // resolution. + // + // Without SDC_VIRTUAL_MODE_AWARE, devices would share the same source mode entry, but now + // they have separate entries that are more or less identical. + // + // To avoid surprising end-user with unexpected source mode change, we validate the input + // instead of changing it automatically. This also resolve the problem of having to choose + // refresh rate for duplicate display - leave it to the end-user of this function... + const auto all_device_ids { get_all_duplicated_devices(device_ids) }; + if (all_device_ids.empty()) { + BOOST_LOG(error) << "failed to get all duplicated devices!"; + return false; + } + + if (all_device_ids.size() != device_ids.size()) { + BOOST_LOG(error) << "not all modes for duplicate displays were provided!"; + return false; + } + + const auto original_modes { get_current_display_modes(device_ids) }; + if (original_modes.empty()) { + // Error already logged + return false; + } + + constexpr bool allow_changes { true }; + if (!do_set_modes(modes, allow_changes)) { + // Error already logged + return false; + } + + const auto all_modes_match = [&modes](const device_display_mode_map_t ¤t_modes) { + for (const auto &[device_id, requested_mode] : modes) { + auto mode_it { current_modes.find(device_id) }; + if (mode_it == std::end(current_modes)) { + // I mean this race condition of disconnecting display device is technically possible... + return false; + } + + if (!fuzzy_compare_modes(mode_it->second, requested_mode)) { + return false; + } + } + + return true; + }; + + auto current_modes { get_current_display_modes(device_ids) }; + if (!current_modes.empty()) { + if (all_modes_match(current_modes)) { + return true; + } + + // We have a problem when using SetDisplayConfig with SDC_ALLOW_CHANGES + // (which we should use as otherwise we need to set EVERYTHING correctly) + // where it decides to use our new mode merely as a suggestion. + // + // This is good, since we don't have to be very precise with refresh rate, + // but also bad since it can just ignore our specified mode. + // + // However, it is possible that the user has created a custom display modes + // which is not exposed to the via Windows settings app. To allow this + // resolution to be selected, we actually need to omit SDC_ALLOW_CHANGES + // flag. + + // If the settings are completely bonkers, this could fail with the following message: + // [code: 1610, message: The configuration data for this product is corrupt. Contact your support personnel] failed to set display mode! + BOOST_LOG(info) << "failed to change display modes using Windows recommended modes, trying to set modes more strictly!"; + if (do_set_modes(modes, !allow_changes)) { + current_modes = get_current_display_modes(device_ids); + if (!current_modes.empty() && all_modes_match(current_modes)) { + return true; + } + } + } + + do_set_modes(original_modes, allow_changes); // Return value does not matter as we are trying out best to undo + BOOST_LOG(error) << "failed to set display mode(-s) completely!"; + return false; + } + +} // namespace display_device diff --git a/src/platform/windows/display_device/device_topology.cpp b/src/platform/windows/display_device/device_topology.cpp new file mode 100644 index 00000000000..4cba1fb686f --- /dev/null +++ b/src/platform/windows/display_device/device_topology.cpp @@ -0,0 +1,454 @@ +// lib includes +#include + +// local includes +#include "src/main.h" +#include "windows_utils.h" + +namespace display_device { + + namespace { + + using device_topology_map_t = std::unordered_map; + + /*! + * Parses the path into a map of ["valid" device id -> path index]. + * + * The paths are already ordered as the "best in front". This includes both + * inactive and active devices. What's important is to select only device + * per a device path (which we use for our device id in this case). + * + * There can be multiple valid paths per an adapter, but we only care + * about the "best one" - the first in the list. + * + * From experimentation it seems that it does not really matter for Windows + * if you select not the "best" valid path as it will just ignore your selection + * and still select the "best" path anyway. This can be verified by looking at the + * source ids from the structure and how they do not match the ones from the paths + * you give to Windows (unless they are not persistent or generated on the fly). + */ + device_topology_map_t + make_valid_topology_map(const std::vector &paths) { + device_topology_map_t current_topology; + for (std::size_t index = 0; index < paths.size(); ++index) { + const auto &path { paths[index] }; + + const auto current_id { w_utils::get_device_id_for_valid_path(path, w_utils::ALL_DEVICES) }; + if (current_id.empty()) { + // Path is not valid + continue; + } + + if (current_topology.count(current_id) > 0) { + // Path was already selected + continue; + } + + current_topology[current_id] = index; + } + + return current_topology; + } + + struct preexisting_topology_t { + UINT32 group_id; + std::size_t path_index; + }; + using preexisting_topology_map_t = std::unordered_map; + + /*! + * Here we try to generate a topology that we expect Windows to already know about and have settings. + * Devices that we want to have duplicated shall have the same group id and devices that we want + * to have extended shall have different group ids. + * + * Group id is just and arbitrary number that does have to be continuous. + */ + preexisting_topology_map_t + make_preexisting_topology(const active_topology_t &new_topology, const device_topology_map_t ¤t_topology) { + UINT32 group_id { 0 }; + preexisting_topology_map_t preexisting_topology; + + for (const auto &group : new_topology) { + for (const std::string &device_id : group) { + auto device_topology_it { current_topology.find(device_id) }; + if (device_topology_it == std::end(current_topology)) { + BOOST_LOG(error) << "device " << device_id << " does not exist in the current topology!"; + return {}; + } + + const auto path_index { device_topology_it->second }; + preexisting_topology[device_id] = { group_id, path_index }; + } + + group_id++; + } + + return preexisting_topology; + } + + struct updated_topology_t { + boost::variant group_id_or_path; + std::size_t path_index; + }; + using updated_topology_info_map_t = std::unordered_map; + + /*! + * Similar to "make_preexisting_topology", the only difference is that we try + * to preserve information from active paths. + */ + updated_topology_info_map_t + make_updated_topology(const active_topology_t &new_topology, const device_topology_map_t ¤t_topology, const std::vector &paths) { + UINT32 group_id { 0 }; + std::unordered_map> taken_source_ids; + updated_topology_info_map_t updated_topology; + + for (const auto &group : new_topology) { + int inactive_devices_per_group { 0 }; + + // First we want to save path info from already active devices + for (const std::string &device_id : group) { + auto device_topology_it { current_topology.find(device_id) }; + if (device_topology_it == std::end(current_topology)) { + BOOST_LOG(error) << "device " << device_id << " does not exist in the current topology!"; + return {}; + } + + const auto path_index { device_topology_it->second }; + if (!w_utils::is_active(paths.at(path_index))) { + continue; + } + + updated_topology[device_id] = { paths[path_index], path_index }; + } + + // Next we want to assign new groups for inactive devices only + for (const std::string &device_id : group) { + auto device_topology_it { current_topology.find(device_id) }; + if (device_topology_it == std::end(current_topology)) { + BOOST_LOG(error) << "device " << device_id << " does not exist in the current topology!"; + return {}; + } + + const auto path_index { device_topology_it->second }; + if (w_utils::is_active(paths.at(path_index))) { + continue; + } + + updated_topology[device_id] = { group_id, path_index }; + inactive_devices_per_group++; + } + + // In case we have duplicated displays with inactive devices we now want to discard the active device path infomation + // completely and let Windows take care of it. We just need to make sure they have the same group id. + if (inactive_devices_per_group > 1) { + for (const std::string &device_id : group) { + auto info_it { updated_topology.find(device_id) }; + if (info_it == std::end(updated_topology)) { + // Sanity check + BOOST_LOG(error) << "device " << device_id << " does not exist in the updated topology!"; + return {}; + } + + info_it->second.group_id_or_path = group_id; + } + } + + group_id++; + } + + return updated_topology; + } + + /*! + * Try to set to the new topology. + * Either by trying to reuse preexisting ones or creating a new + * topology that Windows has never seen before. + */ + bool + do_set_topology(const active_topology_t &new_topology) { + auto display_data { w_utils::query_display_config(w_utils::ALL_DEVICES) }; + if (!display_data) { + // Error already logged + return false; + } + + const auto current_topology { make_valid_topology_map(display_data->paths) }; + const auto clear_path_data = [&]() { + // These fields need to be cleared (according to MSDOCS) for devices we want to deactivate or modify. + // When modifying, we will restore some of them. + for (auto &path : display_data->paths) { + w_utils::set_source_index(path, boost::none); + w_utils::set_target_index(path, boost::none); + w_utils::set_desktop_index(path, boost::none); + w_utils::set_clone_group_id(path, boost::none); + w_utils::set_inactive(path); + w_utils::clear_path_refresh_rate(path); + } + }; + + // Try to reuse existing topology settings from Windows first + { + const auto preexisting_topology { make_preexisting_topology(new_topology, current_topology) }; + if (preexisting_topology.empty()) { + BOOST_LOG(error) << "could not make preexisting topology info!"; + return false; + } + + clear_path_data(); + for (const auto &topology : preexisting_topology) { + const auto &info { topology.second }; + auto &path { display_data->paths.at(info.path_index) }; + + // For Windows to find existing topology we need to only set the group id and mark the device as active + w_utils::set_clone_group_id(path, info.group_id); + w_utils::set_active(path); + } + + const UINT32 validate_flags { SDC_VALIDATE | SDC_TOPOLOGY_SUPPLIED | SDC_ALLOW_PATH_ORDER_CHANGES | SDC_VIRTUAL_MODE_AWARE }; + const UINT32 apply_flags { SDC_APPLY | SDC_TOPOLOGY_SUPPLIED | SDC_ALLOW_PATH_ORDER_CHANGES | SDC_VIRTUAL_MODE_AWARE }; + + LONG result { SetDisplayConfig(display_data->paths.size(), display_data->paths.data(), 0, nullptr, validate_flags) }; + if (result == ERROR_SUCCESS) { + result = SetDisplayConfig(display_data->paths.size(), display_data->paths.data(), 0, nullptr, apply_flags); + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << w_utils::get_ccd_error_string(result) << " failed to change topology using supplied topology!"; + return false; + } + else { + return true; + } + } + else { + BOOST_LOG(info) << w_utils::get_ccd_error_string(result) << " failed to change topology using supplied topology! Trying to update topology next."; + } + } + + // Try to create new/updated topology and save it to the Windows' database + const auto updated_topology_info { make_updated_topology(new_topology, current_topology, display_data->paths) }; + if (updated_topology_info.empty()) { + BOOST_LOG(error) << "could not make updated topology info!"; + return false; + } + + clear_path_data(); + for (const auto &updated_topology : updated_topology_info) { + const auto &info { updated_topology.second }; + auto &path { display_data->paths[info.path_index] }; + + if (const UINT32 *group_id = boost::get(&info.group_id_or_path)) { + // Same as when trying to reuse topology - let Windows handle it. Specify + // the group id which indicates just that + mark the device as active + w_utils::set_clone_group_id(path, *group_id); + w_utils::set_active(path); + } + else if (const auto *saved_path = boost::get(&info.group_id_or_path)) { + // The device will not be duplicated so let's just preserve the path data + // from the current topology + path = *saved_path; + } + } + + const UINT32 apply_flags { SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_VIRTUAL_MODE_AWARE | SDC_SAVE_TO_DATABASE }; + const LONG result { SetDisplayConfig(display_data->paths.size(), display_data->paths.data(), display_data->modes.size(), display_data->modes.data(), apply_flags) }; + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << w_utils::get_ccd_error_string(result) << " failed to change topology using supplied display config!"; + return false; + } + + return true; + } + + } // namespace + + device_info_map_t + enum_available_devices() { + auto display_data { w_utils::query_display_config(w_utils::ALL_DEVICES) }; + if (!display_data) { + // Error already logged + return {}; + } + + device_info_map_t available_devices; + const auto current_topology { make_valid_topology_map(display_data->paths) }; + for (const auto &topology : current_topology) { + const auto &device_id { topology.first }; + const auto path_index { topology.second }; + const auto &path { display_data->paths.at(path_index) }; + + if (w_utils::is_active(path)) { + const auto mode { w_utils::get_source_mode(w_utils::get_source_index(path, display_data->modes), display_data->modes) }; + + available_devices[device_id] = device_info_t { + w_utils::get_display_name(path), + w_utils::get_friendly_name(path), + mode && w_utils::is_primary(*mode) ? device_state_e::primary : device_state_e::active, + w_utils::get_hdr_state(path) + }; + } + else { + available_devices[device_id] = device_info_t { + std::string {}, // Inactive device can have multiple display names, so it's just meaningless + w_utils::get_friendly_name(path), + device_state_e::inactive, + hdr_state_e::unknown + }; + } + } + + return available_devices; + } + + active_topology_t + get_current_topology() { + const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return {}; + } + + // Duplicate displays can be identified by having the same x/y position. Here we have have a + // "position to index" for a simple and lazy lookup in case we have to add a device to the + // topology group. + std::unordered_map position_to_topology_index; + active_topology_t topology; + for (const auto &path : display_data->paths) { + const auto current_id { w_utils::get_device_id_for_valid_path(path, w_utils::ACTIVE_ONLY_DEVICES) }; + if (current_id.empty()) { + continue; + } + + const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(path, display_data->modes), display_data->modes) }; + if (!source_mode) { + BOOST_LOG(error) << "active device does not have a source mode: " << current_id << "!"; + return {}; + } + + const std::string lazy_lookup { std::to_string(source_mode->position.x) + std::to_string(source_mode->position.y) }; + auto index_it { position_to_topology_index.find(lazy_lookup) }; + + if (index_it == std::end(position_to_topology_index)) { + position_to_topology_index[lazy_lookup] = topology.size(); + topology.push_back({ current_id }); + } + else { + topology.at(index_it->second).push_back(current_id); + } + } + + return topology; + } + + bool + is_topology_valid(const active_topology_t &topology) { + if (topology.empty()) { + BOOST_LOG(warning) << "topology input is empty!"; + return false; + } + + std::unordered_set device_ids; + for (const auto &group : topology) { + // Size 2 is a Windows' limitation. + // You CAN set the group to be more than 2, but then + // Windows' settings app breaks since it was not designed for this :/ + if (group.empty() || group.size() > 2) { + BOOST_LOG(warning) << "topology group is invalid!"; + return false; + } + + for (const auto &device_id : group) { + if (device_ids.count(device_id) > 0) { + BOOST_LOG(warning) << "duplicate device ids found!"; + return false; + } + + device_ids.insert(device_id); + } + } + + return true; + } + + bool + is_topology_the_same(const active_topology_t &a, const active_topology_t &b) { + const auto sort_topology = [](active_topology_t &topology) { + for (auto &group : topology) { + std::sort(std::begin(group), std::end(group)); + } + + std::sort(std::begin(topology), std::end(topology)); + }; + + auto a_copy { a }; + auto b_copy { b }; + + // On Windows order does not matter. + sort_topology(a_copy); + sort_topology(b_copy); + + return a_copy == b_copy; + } + + bool + set_topology(const active_topology_t &new_topology) { + if (!is_topology_valid(new_topology)) { + BOOST_LOG(error) << "topology input is invalid!"; + return false; + } + + const auto current_topology { get_current_topology() }; + if (current_topology.empty()) { + BOOST_LOG(error) << "failed to get current topology!"; + return false; + } + + if (is_topology_the_same(current_topology, new_topology)) { + BOOST_LOG(debug) << "same topology provided."; + return true; + } + + if (do_set_topology(new_topology)) { + const auto updated_topology { get_current_topology() }; + if (!updated_topology.empty()) { + if (is_topology_the_same(new_topology, updated_topology)) { + return true; + } + else { + // There is an interesting bug in Windows when you have nearly + // identical devices, drivers or something. For example, imagine you have: + // AM - Actual Monitor + // IDD1 - Virtual display 1 + // IDD2 - Virtual display 2 + // + // You can have the following topology: + // [[AM, IDD1]] + // but not this: + // [[AM, IDD2]] + // + // Windows API will just default to: + // [[AM, IDD1]] + // even if you provide the second variant. Windows API will think + // it's OK and just return ERROR_SUCCESS in this case and there is + // nothing you can do. Even the Windows' settings app will not + // be able to set the desired topology. + // + // There seems to be a workaround - you need to make sure the IDD1 + // device is used somewhere else in the topology, like: + // [[AM, IDD2], [IDD1]] + // + // However, since we have this bug an additional sanity check is needed + // regardless of what Windows report back to us. + BOOST_LOG(error) << "failed to change topology due to Windows bug!"; + } + } + else { + BOOST_LOG(error) << "failed to get updated topology!"; + } + + // Revert back to the original topology + do_set_topology(current_topology); // Return value does not matter + } + + return false; + } + +} // namespace display_device diff --git a/src/platform/windows/display_device/general_functions.cpp b/src/platform/windows/display_device/general_functions.cpp new file mode 100644 index 00000000000..779cf1ca6d6 --- /dev/null +++ b/src/platform/windows/display_device/general_functions.cpp @@ -0,0 +1,143 @@ +// standard includes +#include + +// local includes +#include "src/main.h" +#include "windows_utils.h" + +namespace display_device { + + std::string + get_display_name(const std::string &device_id) { + if (device_id.empty()) { + // Valid return, no error + return {}; + } + + const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return {}; + } + + const auto path { w_utils::get_active_path(device_id, display_data->paths) }; + if (!path) { + // Debug level, because inactive device is valid case for this function + BOOST_LOG(debug) << "failed to find device for " << device_id << "!"; + return {}; + } + + const auto display_name { w_utils::get_display_name(*path) }; + if (display_name.empty()) { + BOOST_LOG(debug) << "device " << device_id << " has no display name assigned."; + } + + return display_name; + } + + bool + is_primary_device(const std::string &device_id) { + if (device_id.empty()) { + BOOST_LOG(error) << "device id is empty!"; + return false; + } + + auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return false; + } + + const auto path { w_utils::get_active_path(device_id, display_data->paths) }; + if (!path) { + BOOST_LOG(error) << "failed to find device for " << device_id << "!"; + return false; + } + + const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(*path, display_data->modes), display_data->modes) }; + if (!source_mode) { + BOOST_LOG(error) << "active device does not have a source mode: " << device_id << "!"; + return false; + } + + return w_utils::is_primary(*source_mode); + } + + bool + set_as_primary_device(const std::string &device_id) { + if (device_id.empty()) { + BOOST_LOG(error) << "device id is empty!"; + return false; + } + + auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return false; + } + + // Get the current origin point of the device (the one that we want to make primary) + POINTL origin; + { + const auto path { w_utils::get_active_path(device_id, display_data->paths) }; + if (!path) { + BOOST_LOG(error) << "failed to find device for " << device_id << "!"; + return false; + } + + const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(*path, display_data->modes), display_data->modes) }; + if (!source_mode) { + BOOST_LOG(error) << "active device does not have a source mode: " << device_id << "!"; + return false; + } + + if (w_utils::is_primary(*source_mode)) { + BOOST_LOG(debug) << "device " << device_id << " is already a primary device."; + return true; + } + + origin = source_mode->position; + } + + // Without verifying if the paths are valid or not (SetDisplayConfig will verify for us), + // shift, their source mode origin points accordingly, so that the provided + // device moves to (0, 0) position + std::unordered_set modified_modes; + for (auto &path : display_data->paths) { + const auto current_id { w_utils::get_device_id(path) }; + const auto source_index { w_utils::get_source_index(path, display_data->modes) }; + auto source_mode { w_utils::get_source_mode(source_index, display_data->modes) }; + + if (!source_mode) { + BOOST_LOG(error) << "active device does not have a source or target mode: " << current_id << "!"; + return false; + } + + if (!source_index || !source_mode) { + BOOST_LOG(error) << "active device does not have a source mode: " << current_id << "!"; + return false; + } + + if (modified_modes.find(*source_index) != std::end(modified_modes)) { + // Happens when VIRTUAL_MODE_AWARE is not specified when querying paths, probably will never happen in our case, but just to be safe... + BOOST_LOG(debug) << "device " << current_id << " shares the same mode index as a previous device. Device is duplicated. Skipping."; + continue; + } + + source_mode->position.x -= origin.x; + source_mode->position.y -= origin.y; + + modified_modes.insert(*source_index); + } + + const UINT32 flags { SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_VIRTUAL_MODE_AWARE }; + const LONG result { SetDisplayConfig(display_data->paths.size(), display_data->paths.data(), display_data->modes.size(), display_data->modes.data(), flags) }; + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << w_utils::get_ccd_error_string(result) << " failed to set primary mode for " << device_id << "!"; + return false; + } + + return true; + } + +} // namespace display_device diff --git a/src/platform/windows/display_device/settings.cpp b/src/platform/windows/display_device/settings.cpp new file mode 100644 index 00000000000..0745f9b5f9b --- /dev/null +++ b/src/platform/windows/display_device/settings.cpp @@ -0,0 +1,452 @@ +// standard includes +#include "fstream" + +// local includes +#include "settings_topology.h" +#include "src/display_device/to_string.h" +#include "src/main.h" + +namespace display_device { + + namespace { + + std::string + get_current_primary_display(const topology_metadata_t &metadata) { + for (const auto &group : metadata.current_topology) { + for (const auto &device_id : group) { + if (is_primary_device(device_id)) { + return device_id; + } + } + } + + return std::string {}; + } + + std::string + determine_new_primary_display(const parsed_config_t::device_prep_e &device_prep, const std::string &original_primary_display, const topology_metadata_t &metadata) { + if (device_prep == parsed_config_t::device_prep_e::ensure_primary) { + if (metadata.primary_device_requested) { + // Primary device was requested - no device was specified by user. + // This means we are keeping the original primary display. + } + else { + // For primary devices it is enough to set 1 as a primary as the whole duplicated group + // will become primary devices. + const auto new_primary_device { metadata.duplicated_devices.front() }; + return new_primary_device; + } + } + + return original_primary_display; + } + + device_display_mode_map_t + determine_new_display_modes(const boost::optional &resolution, const boost::optional &refresh_rate, const device_display_mode_map_t &original_display_modes, const topology_metadata_t &metadata) { + device_display_mode_map_t new_modes { original_display_modes }; + + if (resolution) { + // For duplicate devices the resolution must match no matter what + for (const auto &device_id : metadata.duplicated_devices) { + new_modes[device_id].resolution = *resolution; + } + } + + if (refresh_rate) { + if (metadata.primary_device_requested) { + // No device has been specified, so if they're all are primary devices + // we need to apply the refresh rate change to all duplicates + for (const auto &device_id : metadata.duplicated_devices) { + new_modes[device_id].refresh_rate = *refresh_rate; + } + } + else { + // Even if we have duplicate devices, their refresh rate may differ + // and since the device was specified, let's apply the refresh + // rate only to the specified device. + new_modes[metadata.duplicated_devices.front()].refresh_rate = *refresh_rate; + } + } + + return new_modes; + } + + hdr_state_map_t + determine_new_hdr_states(const boost::optional &change_hdr_state, const hdr_state_map_t &original_hdr_states, const topology_metadata_t &metadata) { + hdr_state_map_t new_states { original_hdr_states }; + + if (change_hdr_state) { + const hdr_state_e end_state { *change_hdr_state ? hdr_state_e::enabled : hdr_state_e::disabled }; + const auto try_update_new_state = [&new_states, end_state](const std::string &device_id) { + const auto current_state { new_states[device_id] }; + if (current_state == hdr_state_e::unknown) { + return; + } + + new_states[device_id] = end_state; + }; + + if (metadata.primary_device_requested) { + // No device has been specified, so if they're all are primary devices + // we need to apply the HDR state change to all duplicates + for (const auto &device_id : metadata.duplicated_devices) { + try_update_new_state(device_id); + } + } + else { + // Even if we have duplicate devices, their HDR states may differ + // and since the device was specified, let's apply the HDR state + // only to the specified device. + try_update_new_state(metadata.duplicated_devices.front()); + } + } + + return new_states; + } + + /*! + * Some newly enabled displays do not handle HDR state correctly (IDD HDR display for example). + * The colors can become very blown out/high contrast. A simple workaround is to toggle the HDR state + * once the display has "settled down" or something. + * + * This is what this function does, it changes the HDR state to the opposite once that we will have in the + * end, sleeps for a little and then allows us to continue changing HDR states to the final ones. + * + * "blank" comes as an inspiration from "vblank" as this function is meant to be used before changing the HDR + * states to clean up something. + */ + bool + blank_hdr_states(const hdr_state_map_t &states, const std::unordered_set &newly_enabled_devices) { + const std::chrono::milliseconds delay { 1500 }; + if (delay > std::chrono::milliseconds::zero()) { + bool state_changed { false }; + auto toggled_states { states }; + for (const auto &device_id : newly_enabled_devices) { + auto state_it { toggled_states.find(device_id) }; + if (state_it == std::end(toggled_states)) { + continue; + } + + if (state_it->second == hdr_state_e::enabled) { + state_it->second = hdr_state_e::disabled; + state_changed = true; + } + else if (state_it->second == hdr_state_e::disabled) { + state_it->second = hdr_state_e::enabled; + state_changed = true; + } + } + + if (state_changed) { + BOOST_LOG(info) << "toggling HDR states for newly enabled devices and waiting for " << delay.count() << "ms before actually applying the correct states."; + if (!set_hdr_states(toggled_states)) { + // Error already logged + return false; + } + + std::this_thread::sleep_for(delay); + } + } + + return true; + } + + void + remove_file(const std::filesystem::path &filepath) { + try { + if (!filepath.empty()) { + std::filesystem::remove(filepath); + } + } + catch (const std::exception &err) { + BOOST_LOG(error) << "failed to remove " << filepath << ". Error: " << err.what(); + } + } + + } // namespace + + settings_t::settings_t() { + } + + settings_t::~settings_t() { + } + + settings_t::apply_result_t + settings_t::apply_config(const config::video_t &config, const rtsp_stream::launch_session_t &session) { + const auto parsed_config { make_parsed_config(config, session) }; + if (!parsed_config) { + return { apply_result_t::result_e::config_parse_fail }; + } + + return apply_config(*parsed_config); + } + + settings_t::apply_result_t + settings_t::apply_config(const parsed_config_t &config) { + // The idea behind this method is simple. + // + // We take a original settings as our base. The original settings can be either the + // settings from when we applied configuration for the first time, or if we don't have + // a original settings from previous configuration, we take the current settings. + // + // We then apply new settings over our base settings. By doing this we make sure + // that we always have a clean slate - if we apply config multiple times, the settings + // will not accumulate and the things that we don't configure will be automatically + // reverted. + + const boost::optional previously_configured_topology { data ? boost::make_optional(data->topology) : boost::none }; + const auto topology_result { handle_device_topology_configuration(config, previously_configured_topology, [&]() { + revert_settings(); + }) }; + if (!topology_result) { + // Error already logged + return { apply_result_t::result_e::topology_fail }; + } + + data_t current_settings; + auto guard = util::fail_guard([&]() { + revert_settings(current_settings); + }); + + current_settings.topology = topology_result->temporary_topology_data; + current_settings.original_primary_display = get_current_primary_display(topology_result->metadata); + current_settings.original_modes = get_current_display_modes(get_device_ids_from_topology(topology_result->metadata.current_topology)); + current_settings.original_hdr_states = get_current_hdr_states(get_device_ids_from_topology(topology_result->metadata.current_topology)); + + // Sanity check + if (current_settings.original_primary_display.empty() || + current_settings.original_modes.empty() || + current_settings.original_hdr_states.empty()) { + // Some error should already be logged except for "original_primary_display" + return { apply_result_t::result_e::validation_fail }; + } + + // Gets the original field from either the previous data (preferred) or new original settings. + const auto get_original_field = [&](auto &&field) { + return (data ? *data : current_settings).*field; + }; + + const auto new_primary_display { determine_new_primary_display(config.device_prep, get_original_field(&data_t::original_primary_display), topology_result->metadata) }; + BOOST_LOG(info) << "changing primary display to: " << new_primary_display; + if (!set_as_primary_device(new_primary_display)) { + // Error already logged + return { apply_result_t::result_e::primary_display_fail }; + } + + const auto new_modes { determine_new_display_modes(config.resolution, config.refresh_rate, get_original_field(&data_t::original_modes), topology_result->metadata) }; + BOOST_LOG(info) << "changing display modes to: " << to_string(new_modes); + if (!set_display_modes(new_modes)) { + // Error already logged + return { apply_result_t::result_e::modes_fail }; + } + + const auto new_hdr_states { determine_new_hdr_states(config.change_hdr_state, get_original_field(&data_t::original_hdr_states), topology_result->metadata) }; + if (!blank_hdr_states(new_hdr_states, topology_result->metadata.newly_enabled_devices)) { + // Error already logged + return { apply_result_t::result_e::hdr_states_fail }; + } + + BOOST_LOG(info) << "changing HDR states to: " << to_string(new_hdr_states); + if (!set_hdr_states(new_hdr_states)) { + // Error already logged + return { apply_result_t::result_e::hdr_states_fail }; + } + + if (data) { + // This is the only value we will take over since the initial topology could have + // been changed by the user and this is the only change we will accept. + const auto prev_topology { data->topology }; + data->topology = topology_result->final_topology_data; + if (!save_settings(*data)) { + data->topology = prev_topology; + return { apply_result_t::result_e::file_save_fail }; + } + } + else { + data = std::make_unique(current_settings); + data->topology = topology_result->final_topology_data; + if (!save_settings(*data)) { + data = nullptr; + return { apply_result_t::result_e::file_save_fail }; + } + } + + guard.disable(); + return { apply_result_t::result_e::success }; + } + + void + settings_t::revert_settings() { + if (!data) { + load_settings(); + } + + if (data) { + revert_settings(*data); + remove_file(filepath); + data = nullptr; + } + } + + void + settings_t::revert_settings(const data_t &data) { + // On Windows settings are saved per an active topology list/pairing/set. + // This makes it complicated when having to revert the changes as we MUST + // be in the same topology we made those changes to (except for HDR, because it's + // not a part of a path/mode lists that are used for topology, but the display + // still needs to be activate to change it). + // + // Unplugging inactive devices does not change the topology, however plugging one + // in will (maybe), as Windows seems to try to activate the device automatically. Unplugging + // active device will also change the topology. + + const bool initial_topology_was_changed { !is_topology_the_same(data.topology.initial, data.topology.modified) }; + const bool primary_display_was_changed { !data.original_primary_display.empty() }; + const bool display_modes_were_changed { !data.original_modes.empty() }; + const bool hdr_states_were_changed { !data.original_hdr_states.empty() }; + const bool topology_change_is_needed_for_reverting_changes { primary_display_was_changed || display_modes_were_changed || hdr_states_were_changed }; + + if (!topology_change_is_needed_for_reverting_changes && !initial_topology_was_changed) { + return; + } + + const auto topology_we_modified { data.topology.modified }; + const auto current_topology { get_current_topology() }; + BOOST_LOG(info) << "current display topology: " << to_string(current_topology); + + bool changed_topology_as_last_effort { false }; + bool topology_is_same_as_when_we_modified { true }; + if (!is_topology_the_same(current_topology, topology_we_modified)) { + topology_is_same_as_when_we_modified = false; + BOOST_LOG(warning) << "topology is different from the one that was modified!"; + + if (topology_change_is_needed_for_reverting_changes) { + BOOST_LOG(info) << "changing back to the modified topology to revert the changes."; + if (!set_topology(topology_we_modified)) { + // Error already logged + } + else { + changed_topology_as_last_effort = true; + topology_is_same_as_when_we_modified = true; + } + } + } + + if (hdr_states_were_changed) { + if (topology_is_same_as_when_we_modified) { + BOOST_LOG(info) << "changing back the HDR states to: " << to_string(data.original_hdr_states); + if (!set_hdr_states(data.original_hdr_states)) { + // Error already logged + } + } + else { + BOOST_LOG(error) << "current topology is not the same when HDR states were changed. Cannot revert the changes!"; + } + } + + if (display_modes_were_changed) { + if (topology_is_same_as_when_we_modified) { + BOOST_LOG(info) << "changing back the display modes to: " << to_string(data.original_modes); + if (!set_display_modes(data.original_modes)) { + // Error already logged + } + } + else { + BOOST_LOG(error) << "current topology is not the same when display modes were changed. Cannot revert the changes!"; + } + } + + if (primary_display_was_changed) { + if (topology_is_same_as_when_we_modified) { + BOOST_LOG(info) << "changing back the primary device to: " << data.original_primary_display; + if (!set_as_primary_device(data.original_primary_display)) { + // Error already logged + } + } + else { + BOOST_LOG(error) << "current topology is not the same when primary display was changed. Cannot revert the changes!"; + } + } + + bool last_topology_lifeline { false }; + if (changed_topology_as_last_effort) { + BOOST_LOG(info) << "changing back to the original topology before recovery has started: " << to_string(current_topology); + if (!set_topology(current_topology)) { + // Error already logged + last_topology_lifeline = true; + } + } + else if (topology_is_same_as_when_we_modified && initial_topology_was_changed) { + BOOST_LOG(info) << "changing back to the initial topology before if was first modified: " << to_string(data.topology.initial); + if (!set_topology(data.topology.initial)) { + // Error already logged + last_topology_lifeline = true; + } + } + + if (last_topology_lifeline) { + // If we are here we don't know what's happening. + // User could end up with display that is not visible or something, so maybe + // the best choice now is to try to active every single display that + // is available? + + // Extended topology should be the one with the biggest chance of + // success. + active_topology_t extended_topology; + const auto devices { enum_available_devices() }; + for (const auto &[device_id, _] : devices) { + extended_topology.push_back({ device_id }); + } + + BOOST_LOG(warning) << "activating all displays as the last ditch effort."; + if (!set_topology(extended_topology)) { + // Error already logged + last_topology_lifeline = true; + } + } + + // Once we are are no longer changing topology, apply HDR fix for the final state + { + const auto final_topology { get_current_topology() }; + const auto current_hdr_states { get_current_hdr_states(get_device_ids_from_topology(final_topology)) }; + const auto newly_enabled_devices { get_newly_enabled_devices_from_topology(topology_we_modified, final_topology) }; + + BOOST_LOG(info) << "trying to fix HDR states (if needed)."; + blank_hdr_states(current_hdr_states, newly_enabled_devices); // Return value ignored + set_hdr_states(current_hdr_states); // Return value ignored + } + } + + bool + settings_t::save_settings(const data_t &data) { + if (!filepath.empty()) { + try { + std::ofstream file(filepath, std::ios::out | std::ios::trunc); + nlohmann::json json_data = data; + + // Write json with indentation + file << std::setw(4) << json_data << std::endl; + return true; + } + catch (const std::exception &err) { + BOOST_LOG(info) << "Failed to save display settings: " << err.what(); + } + } + + return false; + } + + void + settings_t::load_settings() { + try { + if (!filepath.empty() && std::filesystem::exists(filepath)) { + std::ifstream file(filepath); + data = std::make_unique(nlohmann::json::parse(file)); + } + } + catch (const std::exception &err) { + BOOST_LOG(info) << "Failed to load saved display settings: " << err.what(); + } + } + +} // namespace display_device diff --git a/src/platform/windows/display_device/settings_data.h b/src/platform/windows/display_device/settings_data.h new file mode 100644 index 00000000000..9a05a9df059 --- /dev/null +++ b/src/platform/windows/display_device/settings_data.h @@ -0,0 +1,47 @@ +#pragma once + +// local includes +#include "src/display_device/settings.h" + +namespace display_device { + + /*! + * Contains information from the latest topology change that was + * taken care off. It is used for determining display modes, HDR states and etc. + */ + struct topology_metadata_t { + active_topology_t current_topology; + std::unordered_set newly_enabled_devices; + bool primary_device_requested; + std::vector duplicated_devices; + }; + + /*! + * Contains the initial topology that we had before we switched + * to the topology that we have modified. They can be equal. + * Initial topology info is needed so that we could go back to it + * once we undo the changes in the modified topology. + */ + struct topology_data_t { + active_topology_t initial; + active_topology_t modified; + + // For JSON serialization + NLOHMANN_DEFINE_TYPE_INTRUSIVE(topology_data_t, initial, modified) + }; + + /*! + * Data needed for reverting the changes we have made. + * "Original" settings belong the the modified topology. + */ + struct settings_t::data_t { + topology_data_t topology; + std::string original_primary_display; + device_display_mode_map_t original_modes; + hdr_state_map_t original_hdr_states; + + // For JSON serialization + NLOHMANN_DEFINE_TYPE_INTRUSIVE(data_t, topology, original_primary_display, original_modes, original_hdr_states) + }; + +} // namespace display_device diff --git a/src/platform/windows/display_device/settings_topology.cpp b/src/platform/windows/display_device/settings_topology.cpp new file mode 100644 index 00000000000..c3da1083335 --- /dev/null +++ b/src/platform/windows/display_device/settings_topology.cpp @@ -0,0 +1,293 @@ +// local includes +#include "settings_topology.h" +#include "src/display_device/to_string.h" +#include "src/main.h" + +namespace display_device { + + namespace { + /*! + * Verifies that the specified (or a primary) device is available + * and returns one id, even if it belong to a duplicate display. + */ + std::string + find_one_of_the_available_devices(const std::string &device_id) { + const auto devices { enum_available_devices() }; + if (devices.empty()) { + BOOST_LOG(error) << "display device list is empty!"; + return {}; + } + BOOST_LOG(info) << "available display devices: " << to_string(devices); + + const auto device_it { std::find_if(std::begin(devices), std::end(devices), [&device_id](const auto &entry) { + return device_id.empty() ? entry.second.device_state == device_state_e::primary : entry.first == device_id; + }) }; + if (device_it == std::end(devices)) { + BOOST_LOG(error) << "device " << (device_id.empty() ? "PRIMARY" : device_id) << " not found in the list of available devices!"; + return {}; + } + + return device_it->first; + } + + boost::optional + get_and_validate_current_topology() { + auto initial_topology { get_current_topology() }; + if (!is_topology_valid(initial_topology)) { + BOOST_LOG(error) << "display topology is invalid!"; + return boost::none; + } + BOOST_LOG(debug) << "current display topology: " << to_string(initial_topology); + return initial_topology; + } + + /*! + * Finds duplicate devices for the device_id in the provided topology and appends them to the output list. + * The device_id itself is put to the front. + * + * It is possible that the device is inactive and is not in the current topology. + */ + std::vector + get_duplicate_devices(const std::string &device_id, const active_topology_t ¤t_topology) { + std::vector duplicated_devices; + + duplicated_devices.clear(); + duplicated_devices.push_back(device_id); + + for (const auto &group : current_topology) { + for (const auto &group_device_id : group) { + if (device_id == group_device_id) { + std::copy_if(std::begin(group), std::end(group), std::back_inserter(duplicated_devices), [&](const auto &id) { + return id != device_id; + }); + break; + } + } + } + + return duplicated_devices; + } + + // It is possible that the user has changed the topology while the stream was + // paused or something, so the current topology is no longer what it was when + // we last knew about it. + // + // This is fine, however if we are updating the existing setting we want to + // preserve the "initial" or the first topology when we started to change the + // settings. So, imagine we have 2 users, one did not change anything, the other did: + // + // Good user: + // Previous configuration: + // [[DISPLAY1]] -> [[DISPLAY2]] + // Current configuration: + // [[DISPLAY2]] -> [[DISPLAY2]] + // Conclusion: + // User did not change the topology manually since in the current configuration + // we are switching to the same topology, but maybe the user stopped stream + // to change resolution or something, so we should go back to DISPLAY1 + // + // Bad user: + // Previous configuration: + // [[DISPLAY1]] -> [[DISPLAY2]] + // Current configuration: + // [[DISPLAY4]] -> [[DISPLAY2]] + // Conclusion: + // User did change the topology manually to DISPLAY4 at some point, we should not + // go back to DISPLAY1, but to DISPLAY4 instead. + active_topology_t + determine_initial_topology_based_on_prev_config(const boost::optional &previously_configured_topology, const active_topology_t ¤t_topology) { + if (previously_configured_topology) { + if (is_topology_the_same(previously_configured_topology->modified, current_topology)) { + return previously_configured_topology->initial; + } + } + + return current_topology; + } + + bool + is_device_found_in_active_topology(const std::string &device_id, const active_topology_t ¤t_topology) { + for (const auto &group : current_topology) { + for (const auto &group_device_id : group) { + if (device_id == group_device_id) { + return true; + } + } + } + + return false; + } + + /*! + * Using all of the currently available data we try to determine + * what should the final topology be like. Multiple factors need to be taken into account, follow the + * comments in the code to understand them better. + */ + active_topology_t + determine_final_topology(const parsed_config_t::device_prep_e device_prep, const bool primary_device_requested, const std::vector &duplicated_devices, const active_topology_t ¤t_topology) { + boost::optional final_topology; + + const bool topology_change_requested { device_prep != parsed_config_t::device_prep_e::no_operation }; + if (topology_change_requested) { + if (device_prep == parsed_config_t::device_prep_e::ensure_only_display) { + // Device needs to be the only one that's active or if it's a PRIMARY device, + // only the whole PRIMARY group needs to be active (in case they are duplicated) + + if (primary_device_requested) { + if (current_topology.size() > 1) { + // There are other topology groups other than the primary devices, + // so we need to change that + final_topology = active_topology_t { { duplicated_devices } }; + } + else { + // Primary device group is the only one active, nothing to do + } + } + else { + // Since primary_device_requested == false, it means a device was specified via config by the user + // and is the only device that needs to be enabled + + if (is_device_found_in_active_topology(duplicated_devices.front(), current_topology)) { + // Device is currently active in the active topology group + + if (duplicated_devices.size() > 1 || current_topology.size() > 1) { + // We have more than 1 device in the group or we have more than 1 topology groups. + // We need to disable all other devices + final_topology = active_topology_t { { duplicated_devices.front() } }; + } + else { + // Our device is the only one that's active, nothing to do + } + } + else { + // Our device is not active, we need to activate it and ONLY it + final_topology = active_topology_t { { duplicated_devices.front() } }; + } + } + } + else { + // The device needs to be active at least. + + if (primary_device_requested || is_device_found_in_active_topology(duplicated_devices.front(), current_topology)) { + // Device is already active, nothing to do here + } + else { + // Create the extended topology as it's probably what makes sense the most... + final_topology = current_topology; + final_topology->push_back({ duplicated_devices.front() }); + } + } + } + + return final_topology ? *final_topology : current_topology; + } + + } // namespace + + std::unordered_set + get_device_ids_from_topology(const active_topology_t &topology) { + std::unordered_set device_ids; + for (const auto &group : topology) { + for (const auto &device_id : group) { + device_ids.insert(device_id); + } + } + + return device_ids; + } + + std::unordered_set + get_newly_enabled_devices_from_topology(const active_topology_t &previous_topology, const active_topology_t &new_topology) { + const auto prev_ids { get_device_ids_from_topology(previous_topology) }; + auto new_ids { get_device_ids_from_topology(new_topology) }; + + for (auto &id : prev_ids) { + new_ids.erase(id); + } + + return new_ids; + } + + boost::optional + handle_device_topology_configuration(const parsed_config_t &config, boost::optional previously_configured_topology, const std::function &revert_settings) { + const bool primary_device_requested { config.device_id.empty() }; + const std::string requested_device_id { find_one_of_the_available_devices(config.device_id) }; + if (requested_device_id.empty()) { + // Error already logged + return boost::none; + } + + auto current_topology { get_and_validate_current_topology() }; + if (!current_topology) { + // Error already logged + return boost::none; + } + + // When dealing with the "requested device" here and in other functions we need to keep + // in mind that it could belong to a duplicated display and thus all of them + // need to be taken into account, which complicates everything... + auto duplicated_devices { get_duplicate_devices(requested_device_id, *current_topology) }; + auto final_topology { determine_final_topology(config.device_prep, primary_device_requested, duplicated_devices, *current_topology) }; + + // If we still have a previously configured topology, we could potentially skip making any changes to the topology. + // However, it could also mean that we need to revert any previous changes in case we had missed that chance somehow. + if (previously_configured_topology) { + // If the topology we are switching to is the same as the final topology we had before, + // we don't need to revert anything as the other handlers will take care of it. + // Otherwise, we MUST revert the changes! + if (!is_topology_the_same(previously_configured_topology->modified, final_topology)) { + BOOST_LOG(warning) << "previous topology does not match the new one. Reverting previous changes!"; + revert_settings(); + + // Clearing the optional to reflect the current state. + previously_configured_topology = boost::none; + + // There is always a possibility that after reverting changes, we could + // fail to restore the original topology for whatever reason so we need to redo + // our previous steps just to be safe + current_topology = get_and_validate_current_topology(); + if (!current_topology) { + // Error already logged + return boost::none; + } + + duplicated_devices = get_duplicate_devices(requested_device_id, *current_topology); + final_topology = determine_final_topology(config.device_prep, primary_device_requested, duplicated_devices, *current_topology); + } + } + + if (!is_topology_the_same(*current_topology, final_topology)) { + BOOST_LOG(info) << "changing display topology to: " << to_string(final_topology); + if (!set_topology(final_topology)) { + // Error already logged. + return boost::none; + } + + // It is possible that we no longer has duplicate display, so we need to update the list + duplicated_devices = get_duplicate_devices(requested_device_id, final_topology); + } + + // This check is mainly to cover the case for "config.device_prep == no_operation" as we at least + // have to validate the device exists, but it doesn't hurt to double check it in all cases. + if (!is_device_found_in_active_topology(requested_device_id, final_topology)) { + BOOST_LOG(error) << "device " << requested_device_id << " is not active!"; + return boost::none; + } + + return handled_topology_data_t { + topology_data_t { + *current_topology, + final_topology }, + topology_data_t { + // We also need to take into account the previous configuration (if we still have one) + determine_initial_topology_based_on_prev_config(previously_configured_topology, *current_topology), + final_topology }, + topology_metadata_t { + final_topology, + get_newly_enabled_devices_from_topology(*current_topology, final_topology), + primary_device_requested, + duplicated_devices } + }; + } + +} // namespace display_device diff --git a/src/platform/windows/display_device/settings_topology.h b/src/platform/windows/display_device/settings_topology.h new file mode 100644 index 00000000000..1f7c38b0a2a --- /dev/null +++ b/src/platform/windows/display_device/settings_topology.h @@ -0,0 +1,46 @@ +#pragma once + +// local includes +#include "settings_data.h" + +namespace display_device { + + /*! + * Here we have 2 topology data items: + * - temporary is meant to be used when we fail to change + * something and have to revert back to the previous + * topology and settings. + * - final is meant to be used when we have made the + * changes successfully and to be used when we do + * a final setting reversion. It will try to go back + * to the very first topology we had, before we applied + * the very first changes. + */ + struct handled_topology_data_t { + topology_data_t temporary_topology_data; + topology_data_t final_topology_data; + topology_metadata_t metadata; + }; + + std::unordered_set + get_device_ids_from_topology(const active_topology_t &topology); + + //! Returns device ids that are found in new topology, but were not present in the previous topology. + std::unordered_set + get_newly_enabled_devices_from_topology(const active_topology_t &previous_topology, const active_topology_t &new_topology); + + /*! + * Performs necessary steps for changing topology based on the config parameters. + * Also makes sure to evaluate previous configuration in case we are just updating + * some of the settings (like resolution) where topology change might not be necessary. + * + * In case the function determines that we need to revert all of the previous settings + * since the new topology is not compatible with the previously configured one, the `revert_settings` + * function will be called to completely revert all changes. + * + * On failure it returns empty optional, otherwise topology data is returned. + */ + boost::optional + handle_device_topology_configuration(const parsed_config_t &config, boost::optional previously_configured_topology, const std::function &revert_settings); + +} // namespace display_device diff --git a/src/platform/windows/display_device/windows_utils.cpp b/src/platform/windows/display_device/windows_utils.cpp new file mode 100644 index 00000000000..31b2251e469 --- /dev/null +++ b/src/platform/windows/display_device/windows_utils.cpp @@ -0,0 +1,437 @@ +// standard includes +#include +#include + +// local includes +#include "src/main.h" +#include "windows_utils.h" + +namespace display_device { + + namespace w_utils { + + namespace { + + std::string + convert_to_string(const std::wstring &str) { + if (str.empty()) { + return {}; + } + + std::wstring_convert> conv; + return conv.to_bytes(str); + } + + } // namespace + + std::string + get_ccd_error_string(const LONG error_code) { + std::stringstream error; + error << "[code: "; + switch (error_code) { + case ERROR_INVALID_PARAMETER: + error << "ERROR_INVALID_PARAMETER"; + break; + case ERROR_NOT_SUPPORTED: + error << "ERROR_NOT_SUPPORTED"; + break; + case ERROR_ACCESS_DENIED: + error << "ERROR_ACCESS_DENIED"; + break; + case ERROR_INSUFFICIENT_BUFFER: + error << "ERROR_INSUFFICIENT_BUFFER"; + break; + case ERROR_GEN_FAILURE: + error << "ERROR_GEN_FAILURE"; + break; + case ERROR_SUCCESS: + error << "ERROR_SUCCESS"; + break; + default: + error << error_code; + break; + } + error << ", message: " << std::system_category().message(static_cast(HRESULT_FROM_WIN32(error_code))) << "]"; + return error.str(); + } + + bool + is_primary(const DISPLAYCONFIG_SOURCE_MODE &mode) { + return mode.position.x == 0 && mode.position.y == 0; + } + + bool + are_duplicated_modes(const DISPLAYCONFIG_SOURCE_MODE &a, const DISPLAYCONFIG_SOURCE_MODE &b) { + // Same mode position means they are duplicated + return a.position.x == b.position.x && a.position.y == b.position.y; + } + + bool + is_available(const DISPLAYCONFIG_PATH_INFO &path) { + return path.targetInfo.targetAvailable == TRUE; + } + + bool + is_active(const DISPLAYCONFIG_PATH_INFO &path) { + return static_cast(path.flags & DISPLAYCONFIG_PATH_ACTIVE); + } + + void + set_active(DISPLAYCONFIG_PATH_INFO &path) { + path.flags |= DISPLAYCONFIG_PATH_ACTIVE; + } + + void + set_inactive(DISPLAYCONFIG_PATH_INFO &path) { + path.flags &= ~DISPLAYCONFIG_PATH_ACTIVE; + } + + void + clear_path_refresh_rate(DISPLAYCONFIG_PATH_INFO &path) { + path.targetInfo.refreshRate.Denominator = 0; + path.targetInfo.refreshRate.Numerator = 0; + path.targetInfo.scanLineOrdering = DISPLAYCONFIG_SCANLINE_ORDERING_UNSPECIFIED; + } + + std::string + get_device_id(const DISPLAYCONFIG_PATH_INFO &path) { + DISPLAYCONFIG_TARGET_DEVICE_NAME target_name = {}; + target_name.header.adapterId = path.targetInfo.adapterId; + target_name.header.id = path.targetInfo.id; + target_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; + target_name.header.size = sizeof(target_name); + + LONG result { DisplayConfigGetDeviceInfo(&target_name.header) }; + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << get_ccd_error_string(result) << " failed to get target device name!"; + return {}; + } + + // This is not the prettiest id there is, but it seems to be unique. + // The MONITOR ID that MultiMonitorTool uses is not always unique in some combinations, so we'll just go with the device path. + auto device_id { convert_to_string(target_name.monitorDevicePath) }; + std::replace(std::begin(device_id), std::end(device_id), '#', '-'); // Hashtags are not supported by Sunshine config + return device_id; + } + + std::string + get_friendly_name(const DISPLAYCONFIG_PATH_INFO &path) { + DISPLAYCONFIG_TARGET_DEVICE_NAME target_name = {}; + target_name.header.adapterId = path.targetInfo.adapterId; + target_name.header.id = path.targetInfo.id; + target_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; + target_name.header.size = sizeof(target_name); + + LONG result { DisplayConfigGetDeviceInfo(&target_name.header) }; + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << get_ccd_error_string(result) << " failed to get target device name!"; + return {}; + } + + return target_name.flags.friendlyNameFromEdid ? convert_to_string(target_name.monitorFriendlyDeviceName) : std::string {}; + } + + std::string + get_display_name(const DISPLAYCONFIG_PATH_INFO &path) { + DISPLAYCONFIG_SOURCE_DEVICE_NAME source_name = {}; + source_name.header.id = path.sourceInfo.id; + source_name.header.adapterId = path.sourceInfo.adapterId; + source_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; + source_name.header.size = sizeof(source_name); + + LONG result { DisplayConfigGetDeviceInfo(&source_name.header) }; + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << get_ccd_error_string(result) << " failed to get display name! "; + return {}; + } + + return convert_to_string(source_name.viewGdiDeviceName); + } + + hdr_state_e + get_hdr_state(const DISPLAYCONFIG_PATH_INFO &path) { + if (!is_active(path)) { + return hdr_state_e::unknown; + } + + DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO color_info = {}; + color_info.header.adapterId = path.targetInfo.adapterId; + color_info.header.id = path.targetInfo.id; + color_info.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO; + color_info.header.size = sizeof(color_info); + + LONG result { DisplayConfigGetDeviceInfo(&color_info.header) }; + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << get_ccd_error_string(result) << " failed to get advanced color info! "; + return hdr_state_e::unknown; + } + + return color_info.advancedColorSupported ? color_info.advancedColorEnabled ? hdr_state_e::enabled : hdr_state_e::disabled : hdr_state_e::unknown; + } + + bool + set_hdr_state(const DISPLAYCONFIG_PATH_INFO &path, bool enable) { + DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE color_state = {}; + color_state.header.adapterId = path.targetInfo.adapterId; + color_state.header.id = path.targetInfo.id; + color_state.header.type = DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE; + color_state.header.size = sizeof(color_state); + + color_state.enableAdvancedColor = enable ? 1 : 0; + + LONG result { DisplayConfigSetDeviceInfo(&color_state.header) }; + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << get_ccd_error_string(result) << " failed to set advanced color info!"; + return false; + } + + return true; + } + + boost::optional + get_source_index(const DISPLAYCONFIG_PATH_INFO &path, const std::vector &modes) { + UINT32 index {}; + if (path.flags & DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE) { + index = path.sourceInfo.sourceModeInfoIdx; + if (index == DISPLAYCONFIG_PATH_SOURCE_MODE_IDX_INVALID) { + return boost::none; + } + } + else { + index = path.sourceInfo.modeInfoIdx; + if (index == DISPLAYCONFIG_PATH_MODE_IDX_INVALID) { + return boost::none; + } + } + + if (index >= modes.size()) { + BOOST_LOG(error) << "source index " << index << " is out of range " << modes.size(); + return boost::none; + } + + return index; + } + + boost::optional + get_target_index(const DISPLAYCONFIG_PATH_INFO &path, const std::vector &modes) { + UINT32 index {}; + if (path.flags & DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE) { + index = path.targetInfo.targetModeInfoIdx; + if (index == DISPLAYCONFIG_PATH_TARGET_MODE_IDX_INVALID) { + return boost::none; + } + } + else { + index = path.targetInfo.modeInfoIdx; + if (index == DISPLAYCONFIG_PATH_MODE_IDX_INVALID) { + return boost::none; + } + } + + if (index >= modes.size()) { + BOOST_LOG(error) << "target index " << index << " is out of range " << modes.size(); + return boost::none; + } + + return index; + } + + void + set_source_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index) { + if (path.flags & DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE) { + if (index) { + path.sourceInfo.sourceModeInfoIdx = *index; + } + else { + path.sourceInfo.sourceModeInfoIdx = DISPLAYCONFIG_PATH_SOURCE_MODE_IDX_INVALID; + } + } + else { + if (index) { + path.sourceInfo.modeInfoIdx = *index; + } + else { + path.sourceInfo.modeInfoIdx = DISPLAYCONFIG_PATH_MODE_IDX_INVALID; + } + } + } + + void + set_target_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index) { + if (path.flags & DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE) { + if (index) { + path.targetInfo.targetModeInfoIdx = *index; + } + else { + path.targetInfo.targetModeInfoIdx = DISPLAYCONFIG_PATH_TARGET_MODE_IDX_INVALID; + } + } + else { + if (index) { + path.targetInfo.modeInfoIdx = *index; + } + else { + path.targetInfo.modeInfoIdx = DISPLAYCONFIG_PATH_MODE_IDX_INVALID; + } + } + } + + void + set_desktop_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index) { + if (path.flags & DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE) { + if (index) { + path.targetInfo.desktopModeInfoIdx = *index; + } + else { + path.targetInfo.desktopModeInfoIdx = DISPLAYCONFIG_PATH_DESKTOP_IMAGE_IDX_INVALID; + } + } + } + + void + set_clone_group_id(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index) { + if (path.flags & DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE) { + if (index) { + path.sourceInfo.cloneGroupId = *index; + } + else { + path.sourceInfo.cloneGroupId = DISPLAYCONFIG_PATH_CLONE_GROUP_INVALID; + } + } + } + + const DISPLAYCONFIG_SOURCE_MODE * + get_source_mode(const boost::optional &index, const std::vector &modes) { + if (!index) { + return nullptr; + } + + const auto &mode { modes[*index] }; + if (mode.infoType != DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE) { + BOOST_LOG(error) << "mode at index " << *index << " is not source mode!"; + return nullptr; + } + + return &mode.sourceMode; + } + + DISPLAYCONFIG_SOURCE_MODE * + get_source_mode(const boost::optional &index, std::vector &modes) { + return const_cast(get_source_mode(index, const_cast &>(modes))); + } + + const DISPLAYCONFIG_TARGET_MODE * + get_target_mode(const boost::optional &index, const std::vector &modes) { + if (!index) { + return nullptr; + } + + const auto &mode { modes[*index] }; + if (mode.infoType != DISPLAYCONFIG_MODE_INFO_TYPE_TARGET) { + BOOST_LOG(error) << "mode at index " << *index << " is not target mode!"; + return nullptr; + } + + return &mode.targetMode; + } + + DISPLAYCONFIG_TARGET_MODE * + get_target_mode(const boost::optional &index, std::vector &modes) { + return const_cast(get_target_mode(index, const_cast &>(modes))); + } + + std::string + get_device_id_for_valid_path(const DISPLAYCONFIG_PATH_INFO &path, bool must_be_active) { + // As far as we are concerned, for us the path is valid as long as it is available, + // has a device id that we can use and has a display name. Optionally, we might + // want it to be active. + + if (!is_available(path)) { + // Could be transient issue according to MSDOCS (no longer available, but still "active") + return {}; + } + + if (must_be_active) { + if (!is_active(path)) { + return {}; + } + } + + const auto current_id { get_device_id(path) }; + if (current_id.empty()) { + return {}; + } + + const auto display_name { get_display_name(path) }; + if (display_name.empty()) { + return {}; + } + + return current_id; + } + + boost::optional + query_display_config(bool active_only) { + std::vector paths; + std::vector modes; + LONG result = ERROR_SUCCESS; + + // When we want to enable/disable displays, we need to get all paths as they will not be active. + // This will require some additional filtering of duplicate and otherwise useless paths. + UINT32 flags = active_only ? QDC_ONLY_ACTIVE_PATHS : QDC_ALL_PATHS; + flags |= QDC_VIRTUAL_MODE_AWARE; // supported from W10 onwards + + do { + UINT32 path_count { 0 }; + UINT32 mode_count { 0 }; + + result = GetDisplayConfigBufferSizes(flags, &path_count, &mode_count); + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << get_ccd_error_string(result) << " failed to get display paths and modes!"; + return boost::none; + } + + paths.resize(path_count); + modes.resize(mode_count); + result = QueryDisplayConfig(flags, &path_count, paths.data(), &mode_count, modes.data(), nullptr); + + // The function may have returned fewer paths/modes than estimated + paths.resize(path_count); + modes.resize(mode_count); + + // It's possible that between the call to GetDisplayConfigBufferSizes and QueryDisplayConfig + // that the display state changed, so loop on the case of ERROR_INSUFFICIENT_BUFFER. + } while (result == ERROR_INSUFFICIENT_BUFFER); + + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << get_ccd_error_string(result) << " failed to query display paths and modes!"; + return boost::none; + } + + return path_and_mode_data_t { paths, modes }; + } + + const DISPLAYCONFIG_PATH_INFO * + get_active_path(const std::string &device_id, const std::vector &paths) { + for (const auto &path : paths) { + const auto current_id { get_device_id_for_valid_path(path, ACTIVE_ONLY_DEVICES) }; + if (current_id.empty()) { + continue; + } + + if (current_id == device_id) { + return &path; + } + } + + return nullptr; + } + + DISPLAYCONFIG_PATH_INFO * + get_active_path(const std::string &device_id, std::vector &paths) { + return const_cast(get_active_path(device_id, const_cast &>(paths))); + } + + } // namespace w_utils + +} // namespace display_device diff --git a/src/platform/windows/display_device/windows_utils.h b/src/platform/windows/display_device/windows_utils.h new file mode 100644 index 00000000000..45f460df590 --- /dev/null +++ b/src/platform/windows/display_device/windows_utils.h @@ -0,0 +1,106 @@ +#pragma once + +// the most stupid windows include (because it needs to be first...) +#include + +// local includes +#include "src/display_device/display_device.h" + +namespace display_device { + + namespace w_utils { + + constexpr bool ACTIVE_ONLY_DEVICES { true }; + constexpr bool ALL_DEVICES { false }; + constexpr bool CURRENT_MODE { true }; + constexpr bool ALL_MODES { false }; + + struct path_and_mode_data_t { + std::vector paths; + std::vector modes; + }; + + std::string + get_ccd_error_string(const LONG error_code); + + bool + is_primary(const DISPLAYCONFIG_SOURCE_MODE &mode); + + bool + are_duplicated_modes(const DISPLAYCONFIG_SOURCE_MODE &a, const DISPLAYCONFIG_SOURCE_MODE &b); + + bool + is_available(const DISPLAYCONFIG_PATH_INFO &path); + + bool + is_active(const DISPLAYCONFIG_PATH_INFO &path); + + void + set_active(DISPLAYCONFIG_PATH_INFO &path); + + void + set_inactive(DISPLAYCONFIG_PATH_INFO &path); + + void + clear_path_refresh_rate(DISPLAYCONFIG_PATH_INFO &path); + + std::string + get_device_id(const DISPLAYCONFIG_PATH_INFO &path); + + std::string + get_friendly_name(const DISPLAYCONFIG_PATH_INFO &path); + + std::string + get_display_name(const DISPLAYCONFIG_PATH_INFO &path); + + hdr_state_e + get_hdr_state(const DISPLAYCONFIG_PATH_INFO &path); + + bool + set_hdr_state(const DISPLAYCONFIG_PATH_INFO &path, bool enable); + + boost::optional + get_source_index(const DISPLAYCONFIG_PATH_INFO &path, const std::vector &modes); + + boost::optional + get_target_index(const DISPLAYCONFIG_PATH_INFO &path, const std::vector &modes); + + void + set_source_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index); + + void + set_target_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index); + + void + set_desktop_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index); + + void + set_clone_group_id(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index); + + const DISPLAYCONFIG_SOURCE_MODE * + get_source_mode(const boost::optional &index, const std::vector &modes); + + DISPLAYCONFIG_SOURCE_MODE * + get_source_mode(const boost::optional &index, std::vector &modes); + + const DISPLAYCONFIG_TARGET_MODE * + get_target_mode(const boost::optional &index, const std::vector &modes); + + DISPLAYCONFIG_TARGET_MODE * + get_target_mode(const boost::optional &index, std::vector &modes); + + std::string + get_device_id_for_valid_path(const DISPLAYCONFIG_PATH_INFO &path, bool must_be_active); + + boost::optional + query_display_config(bool active_only); + + const DISPLAYCONFIG_PATH_INFO * + get_active_path(const std::string &device_id, const std::vector &paths); + + DISPLAYCONFIG_PATH_INFO * + get_active_path(const std::string &device_id, std::vector &paths); + + } // namespace w_utils + +} // namespace display_device diff --git a/src/process.cpp b/src/process.cpp index 7042dd00c71..9873659e7bc 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -22,6 +22,7 @@ #include "config.h" #include "crypto.h" +#include "display_device/session.h" #include "main.h" #include "platform/common.h" #include "system_tray.h" @@ -332,16 +333,19 @@ namespace proc { } _pipe.reset(); -#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 bool has_run = _app_id > 0; // Only show the Stopped notification if we actually have an app to stop // Since terminate() is always run when a new app has started if (proc::proc.get_last_run_app_name().length() > 0 && has_run) { +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_stopped(proc::proc.get_last_run_app_name()); - } #endif + // Same applies when restoring display state + display_device::session_t::get().restore_state(); + } + _app_id = -1; } diff --git a/src/stream.cpp b/src/stream.cpp index 2e0c0f521a2..2fff12144ce 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -18,6 +18,7 @@ extern "C" { } #include "config.h" +#include "display_device/session.h" #include "input.h" #include "main.h" #include "network.h" @@ -1826,11 +1827,20 @@ namespace stream { // If this is the last session, invoke the platform callbacks if (--running_sessions == 0) { -#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + bool restore_display_state { true }; if (proc::proc.running()) { +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_pausing(proc::proc.get_last_run_app_name()); - } #endif + + // TODO: cgutman said he would make it configurable per app + restore_display_state = false; + } + + if (restore_display_state) { + display_device::session_t::get().restore_state(); + } + platf::streaming_will_stop(); } diff --git a/src/video.cpp b/src/video.cpp index 206e7feeb62..20c0e686854 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -16,6 +16,7 @@ extern "C" { #include "cbs.h" #include "config.h" +#include "display_device/display_device.h" #include "input.h" #include "main.h" #include "nvenc/nvenc_base.h" @@ -1080,6 +1081,8 @@ namespace video { */ void refresh_displays(platf::mem_type_e dev_type, std::vector &display_names, int ¤t_display_index) { + // It is possible that the output display name may be empty even if it wasn't before (device disconnected) + const auto output_display_name { display_device::get_display_name(config::video.output_name) }; std::string current_display_name; // If we have a current display index, let's start with that @@ -1098,7 +1101,7 @@ namespace video { return; } else if (display_names.empty()) { - display_names.emplace_back(config::video.output_name); + display_names.emplace_back(output_display_name); } // We now have a new display name list, so reset the index back to 0 @@ -1118,7 +1121,7 @@ namespace video { } else { for (int x = 0; x < display_names.size(); ++x) { - if (display_names[x] == config::video.output_name) { + if (display_names[x] == output_display_name) { current_display_index = x; return; } @@ -2334,7 +2337,8 @@ namespace video { int validate_config(std::shared_ptr &disp, const encoder_t &encoder, const config_t &config) { - reset_display(disp, encoder.platform_formats->dev_type, config::video.output_name, config); + const auto output_display_name { display_device::get_display_name(config::video.output_name) }; + reset_display(disp, encoder.platform_formats->dev_type, output_display_name, config); if (!disp) { return -1; } @@ -2412,7 +2416,8 @@ namespace video { config_t config_autoselect { 1920, 1080, 60, 1000, 1, 0, 1, 0, 0 }; // If the encoder isn't supported at all (not even H.264), bail early - reset_display(disp, encoder.platform_formats->dev_type, config::video.output_name, config_autoselect); + const auto output_display_name { display_device::get_display_name(config::video.output_name) }; + reset_display(disp, encoder.platform_formats->dev_type, output_display_name, config_autoselect); if (!disp) { return false; } diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index a6a26670cd7..068450ae98f 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -469,14 +469,20 @@

- - Display Device Id +
- Manually specify a display to use for capture. If unset, the primary display is captured.
+ Manually specify a display device id to use for capture. If unset, the primary display is captured.
Note: If you specified a GPU above, this display must be connected to that GPU.
- The appropriate values can be found using the following command:
- tools\dxgi-info.exe
+
+ During Sunshine startup, you should see the list of detected display devices and their ids, e.g.:
+     DEVICE ID: \\?\DISPLAY-ACI27EC-5&4fd2de4&2&UID4355-{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}
+     DISPLAY NAME: \\.\DISPLAY1
+     FRIENDLY NAME: ROG PG279Q
+     DEVICE STATE: PRIMARY
+     HDR STATE: UNKNOWN
@@ -497,6 +503,103 @@

+ +
+
+

+ +

+
+
+
+ +
+ Windows saves various display settings for each combination of currently active displays.
+ Sunshine applies changes to a display(-s) belonging to such a display combination.
+ If you disconnect a device which was active when Sunshine applied the settings, the changes will not be reverted
+ back unless the combination is active again by the time Sunshine tries to revert changes!
+ The same is true if you connect a new device and Windows decides to activate it, which will
+ change the active display combination. +
+
+ + +
+ + +
+ + +
+ + +
+ "Optimize game settings" option must be enabled on the Moonlight client for this to work. +
+ + +
+
+ Enter the resolution to be used +
+ +
+
+ + +
+ + + + +
+
+ Enter the refresh rate to be used +
+ +
+
+ + +
+ + +
+
+
+
+
+
@@ -1241,6 +1344,12 @@

"install_steam_audio_drivers": "enabled", "adapter_name": "", "output_name": "", + "display_device_prep": "no_operation", + "resolution_change": "automatic", + "manual_resolution": "", + "refresh_rate_change": "automatic", + "manual_refresh_rate": "", + "hdr_prep": "automatic", "resolutions": "[352x240,480x360,858x480,1280x720,1920x1080,2560x1080,3440x1440,1920x1200,3840x2160,3840x1600]", "fps": "[10,30,60,90,120]", },