diff --git a/README.rst b/README.rst index fbc78658668..209b1c13e1c 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ LizardByte has the full documentation hosted on `Read the Docs `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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/audio.cpp b/src/audio.cpp index 1995e380ea7..437e0e89b5d 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -20,16 +20,6 @@ namespace audio { using opus_t = util::safe_ptr; using sample_queue_t = std::shared_ptr>>; - struct audio_ctx_t { - // We want to change the sink for the first stream only - std::unique_ptr sink_flag; - - std::unique_ptr control; - - bool restore_sink; - platf::sink_t sink; - }; - static int start_audio_control(audio_ctx_t &ctx); static void @@ -93,8 +83,6 @@ namespace audio { }, }; - auto control_shared = safe::make_shared(start_audio_control, stop_audio_control); - void encodeThread(sample_queue_t samples, config_t config, void *channel_data) { auto packets = mail::man->queue(mail::audio_packets); @@ -137,7 +125,7 @@ namespace audio { auto shutdown_event = mail->event(mail::shutdown); auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])]; - auto ref = control_shared.ref(); + auto ref = get_audio_ctx_ref(); if (!ref) { return; } @@ -243,6 +231,12 @@ namespace audio { } } + audio_ctx_ref_t + get_audio_ctx_ref() { + static auto control_shared { safe::make_shared(start_audio_control, stop_audio_control) }; + return control_shared.ref(); + } + int map_stream(int channels, bool quality) { int shift = quality ? 1 : 0; diff --git a/src/audio.h b/src/audio.h index fe22c94611d..baf0ef5eea3 100644 --- a/src/audio.h +++ b/src/audio.h @@ -1,11 +1,10 @@ -/** - * @file src/audio.h - * @brief todo - */ #pragma once +// local includes +#include "platform/common.h" #include "thread_safe.h" #include "utility.h" + namespace audio { enum stream_config_e : int { STEREO, @@ -42,8 +41,34 @@ namespace audio { std::bitset flags; }; + struct audio_ctx_t { + // We want to change the sink for the first stream only + std::unique_ptr sink_flag; + + std::unique_ptr control; + + bool restore_sink; + platf::sink_t sink; + }; + using buffer_t = util::buffer_t; using packet_t = std::pair; + using audio_ctx_ref_t = safe::shared_t::ptr_t; + void capture(safe::mail_t mail, config_t config, void *channel_data); + + /** + * @brief Get the reference to the audio context. + * @returns A shared pointer reference to audio context. + * @note Aside from the configuration purposes, it can be used to extend the + * audio sink lifetime to capture sink earlier and restore it later. + * + * EXAMPLES: + * ```cpp + * audio_ctx_ref_t audio = get_audio_ctx_ref() + * ``` + */ + audio_ctx_ref_t + get_audio_ctx_ref(); } // namespace audio diff --git a/src/config.cpp b/src/config.cpp index ba2718aca7f..d5b82edcffa 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -23,6 +23,7 @@ #include "rtsp.h" #include "utility.h" +#include "display_device/parsed_config.h" #include "platform/common.h" #ifdef _WIN32 @@ -367,7 +368,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 { @@ -1001,7 +1009,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 6c48f466b8e..39b016e796e 100644 --- a/src/config.h +++ b/src/config.h @@ -72,7 +72,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/confighttp.cpp b/src/confighttp.cpp index 0657902dd16..f72d6a02d3a 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -29,6 +29,7 @@ #include "config.h" #include "confighttp.h" #include "crypto.h" +#include "display_device/session.h" #include "file_handler.h" #include "globals.h" #include "httpcommon.h" @@ -596,6 +597,23 @@ namespace confighttp { platf::restart(); } + void + resetDisplayDevicePersistence(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + pt::ptree outputTree; + auto g = util::fail_guard([&]() { + std::ostringstream data; + pt::write_json(data, outputTree); + response->write(data.str()); + }); + + display_device::session_t::get().reset_persistence(); + outputTree.put("status", true); + } + void savePassword(resp_https_t response, req_https_t request) { if (!config::sunshine.username.empty() && !authenticate(response, request)) return; @@ -744,6 +762,7 @@ namespace confighttp { server.resource["^/api/config$"]["GET"] = getConfig; server.resource["^/api/config$"]["POST"] = saveConfig; server.resource["^/api/restart$"]["POST"] = restart; + server.resource["^/api/reset-display-device-persistence$"]["POST"] = resetDisplayDevicePersistence; server.resource["^/api/password$"]["POST"] = savePassword; server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; server.resource["^/api/clients/unpair$"]["POST"] = unpairAll; diff --git a/src/display_device/display_device.h b/src/display_device/display_device.h new file mode 100644 index 00000000000..b0ba3d4449d --- /dev/null +++ b/src/display_device/display_device.h @@ -0,0 +1,307 @@ +#pragma once + +// standard includes +#include +#include +#include +#include + +// lib includes +#include +#include + +namespace display_device { + + /** + * @brief The device state in the operating system. + * @note On Windows you can have have multiple primary displays when they are duplicated. + */ + enum class device_state_e { + inactive, + active, + primary /**< Primary state is also implicitly active. */ + }; + + /** + * @brief The device's HDR state in the operating system. + */ + enum class hdr_state_e { + unknown, /**< HDR state could not be retrieved from the OS (even if the display supports 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" } }) + + /** + * @brief Ordered map of [DEVICE_ID -> hdr_state_e]. + */ + using hdr_state_map_t = std::map; + + /** + * @brief The device's HDR state in the operating system. + */ + struct device_info_t { + std::string display_name; /**< A name representing the OS display (source) the device is connected to. */ + std::string friendly_name; /**< A human-readable name for the device. */ + device_state_e device_state; /**< Device's state. @see device_state_e */ + hdr_state_e hdr_state; /**< Device's HDR state. @see hdr_state_e */ + }; + + /** + * @brief Ordered map of [DEVICE_ID -> device_info_t]. + * @see device_info_t + */ + using device_info_map_t = std::map; + + /** + * @brief Display's resolution. + */ + struct resolution_t { + unsigned int width; + unsigned int height; + + // For JSON serialization + NLOHMANN_DEFINE_TYPE_INTRUSIVE(resolution_t, width, height) + }; + + /** + * @brief Display's refresh rate. + * @note Floating point is stored 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) + }; + + /** + * @brief Display's mode (resolution + refresh rate). + * @see resolution_t + * @see refresh_rate_t + */ + 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) + }; + + /** + * @brief Ordered map of [DEVICE_ID -> display_mode_t]. + * @see display_mode_t + */ + using device_display_mode_map_t = std::map; + + /** + * @brief A LIST[LIST[DEVICE_ID]] structure which represents an active topology. + * + * Single display: + * [[DISPLAY_1]] + * 2 extended displays: + * [[DISPLAY_1], [DISPLAY_2]] + * 2 duplicated displays: + * [[DISPLAY_1, DISPLAY_2]] + * Mixed displays: + * [[EXTENDED_DISPLAY_1], [DUPLICATED_DISPLAY_1, DUPLICATED_DISPLAY_2], [EXTENDED_DISPLAY_2]] + * + * @note On Windows the order does not matter of both device ids or the inner lists. + */ + using active_topology_t = std::vector>; + + /** + * @brief Enumerate the available (active and inactive) devices. + * @returns A map of available devices. + * Empty map can also be returned if an error has occurred. + * + * EXAMPLES: + * ```cpp + * const auto devices { enum_available_devices() }; + * ``` + */ + device_info_map_t + enum_available_devices(); + + /** + * @brief Get display name associated with the device. + * @param device_id A device to get display name for. + * @returns A display name for the device, or an empty string if the device is inactive or not found. + * Empty string can also be returned if an error has occurred. + * @see device_info_t + * + * EXAMPLES: + * ```cpp + * const std::string device_name { "MY_DEVICE_ID" }; + * const std::string display_name = get_display_name(device_id); + * ``` + */ + std::string + get_display_name(const std::string &device_id); + + /** + * @brief Get current display modes for the devices. + * @param device_ids A list of devices to get the modes for. + * @returns A map of device modes per a device or an empty map if a mode could not be found (e.g. device is inactive). + * Empty map can also be returned if an error has occurred. + * + * EXAMPLES: + * ```cpp + * const std::unordered_set device_ids { "DEVICE_ID_1", "DEVICE_ID_2" }; + * const auto current_modes = get_current_display_modes(device_ids); + * ``` + */ + device_display_mode_map_t + get_current_display_modes(const std::unordered_set &device_ids); + + /** + * @brief Set new display modes for the devices. + * @param modes A map of modes to set. + * @returns True if modes were set, false otherwise. + * @warning if any of the specified devices are duplicated, modes modes be provided + * for duplicates too! + * + * EXAMPLES: + * ```cpp + * const std::string display_a { "MY_ID_1" }; + * const std::string display_b { "MY_ID_2" }; + * const auto success = set_display_modes({ { display_a, { { 1920, 1080 }, { 60, 1 } } }, + * { display_b, { { 1920, 1080 }, { 120, 1 } } } }); + * ``` + */ + bool + set_display_modes(const device_display_mode_map_t &modes); + + /** + * @brief Check whether the specified device is primary. + * @param device_id A device to perform the check for. + * @returns True if the device is primary, false otherwise. + * @see device_state_e + * + * EXAMPLES: + * ```cpp + * const std::string device_id { "MY_DEVICE_ID" }; + * const bool is_primary = is_primary_device(device_id); + * ``` + */ + bool + is_primary_device(const std::string &device_id); + + /** + * @brief Set the device as a primary display. + * @param device_id A device to set as primary. + * @returns True if the device is or was set as primary, false otherwise. + * @note On Windows if the device is duplicated, the other duplicated device(-s) will also become a primary device. + * + * EXAMPLES: + * ```cpp + * const std::string device_id { "MY_DEVICE_ID" }; + * const bool success = set_as_primary_device(device_id); + * `` + */ + bool + set_as_primary_device(const std::string &device_id); + + /** + * @brief Get HDR state for the devices. + * @param device_ids A list of devices to get the HDR states for. + * @returns A map of HDR states per a device or an empty map if an error has occurred. + * @note On Windows the state cannot be retrieved until the device is active even if it supports it. + * + * EXAMPLES: + * ```cpp + * const std::unordered_set device_ids { "DEVICE_ID_1", "DEVICE_ID_2" }; + * const auto current_hdr_states = get_current_hdr_states(device_ids); + * ``` + */ + hdr_state_map_t + get_current_hdr_states(const std::unordered_set &device_ids); + + /** + * @brief Set HDR states for the devices. + * @param modes A map of HDR states to set. + * @returns True if HDR states were set, false otherwise. + * @note If `unknown` states are provided, they will be silently ignored + * and current state will not be changed. + * + * EXAMPLES: + * ```cpp + * const std::string display_a { "MY_ID_1" }; + * const std::string display_b { "MY_ID_2" }; + * const auto success = set_hdr_states({ { display_a, hdr_state_e::enabled }, + * { display_b, hdr_state_e::disabled } }); + * ``` + */ + bool + set_hdr_states(const hdr_state_map_t &states); + + /** + * @brief Get the active (current) topology. + * @returns A list representing the current topology. + * Empty list can also be returned if an error has occurred. + * + * EXAMPLES: + * ```cpp + * const auto current_topology { get_current_topology() }; + * ``` + */ + active_topology_t + get_current_topology(); + + /** + * @brief Verify if the active topology is valid. + * + * This is mostly meant as a sanity check or to verify that it is still valid + * after a manual modification to an existing topology. + * + * @param topology Topology to validated. + * @returns True if it is valid, false otherwise. + * + * EXAMPLES: + * ```cpp + * auto current_topology { get_current_topology() }; + * // Modify the current_topology + * const bool is_valid = is_topology_valid(current_topology); + * ``` + */ + bool + is_topology_valid(const active_topology_t &topology); + + /** + * @brief Check if the topologies are close enough to be considered the same by the OS. + * @param topology_a First topology to compare. + * @param topology_b Second topology to compare. + * @returns True if topologies are close enough, false otherwise. + * + * EXAMPLES: + * ```cpp + * auto current_topology { get_current_topology() }; + * auto new_topology { current_topology }; + * // Modify the new_topology + * const bool is_the_same = is_topology_the_same(current_topology, new_topology); + * ``` + */ + bool + is_topology_the_same(const active_topology_t &topology_a, const active_topology_t &topology_b); + + /** + * @brief Set the a new active topology for the OS. + * @param new_topology New device topology to set. + * @returns True if the new topology has been set, false otherwise. + * + * EXAMPLES: + * ```cpp + * auto current_topology { get_current_topology() }; + * // Modify the current_topology + * const bool success = set_topology(current_topology); + * ``` + */ + 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..20066382deb --- /dev/null +++ b/src/display_device/parsed_config.cpp @@ -0,0 +1,281 @@ +// lib includes +#include +#include +#include + +// local includes +#include "parsed_config.h" +#include "src/config.h" +#include "src/logging.h" +#include "src/rtsp.h" + +namespace display_device { + + namespace { + /** + * @brief Parse resolution option from the user configuration and the session information. + * @param config User's video related configuration. + * @param session Session information. + * @param parsed_config A reference to a config object that will be modified on success. + * @returns True on successful parsing, false otherwise. + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * parsed_config_t parsed_config; + * const bool success = parse_resolution_option(video_config, *launch_session, parsed_config); + * ``` + */ + 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. Problem with MSYS2 packages (UCRT64), maybe? + + 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; + } + + /** + * @brief Parse refresh rate option from the user configuration and the session information. + * @param config User's video related configuration. + * @param session Session information. + * @param parsed_config A reference to a config object that will be modified on success. + * @returns True on successful parsing, false otherwise. + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * parsed_config_t parsed_config; + * const bool success = parse_refresh_rate_option(video_config, *launch_session, parsed_config); + * ``` + */ + 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 refresh_rate_regex { R"(^(\d+)(?:\.(\d+))?$)" }; // std::regex hangs in CTOR for some reason when called in a thread. Problem with MSYS2 packages (UCRT64), maybe? + + boost::smatch match; + if (boost::regex_match(trimmed_string, match, refresh_rate_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 + + // We have essentially removing the decimal point here: 59.995 -> 59995 + const std::string numerator_str { match[1].str() + match[2].str() }; + const auto numerator { static_cast(std::stol(numerator_str)) }; + + // Here we are counting decimal places and calculating denominator: 10^decimal_places + 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 { + // We do not have a decimal point, just a valid number. + // For example: + // 60: + // numerator = 60 + // denominator = 1 + 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; + } + + /** + * @brief Parse HDR option from the user configuration and the session information. + * @param config User's video related configuration. + * @param session Session information. + * @returns Parsed HDR state value we need to switch to (true == ON, false == OFF). + * Empty optional if no action is required. + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * const auto hdr_option = parse_hdr_option(video_config, *launch_session); + * ``` + */ + 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..585b7544d54 --- /dev/null +++ b/src/display_device/parsed_config.h @@ -0,0 +1,140 @@ +#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 { + + /** + * @brief Configuration containing parsed information from the user config (video related) + * and the current session. + */ + struct parsed_config_t { + /** + * @brief Enum detailing how to prepare the display device. + */ + enum class device_prep_e : int { + no_operation, /**< User has to make sure the display device is active, we will only verify. */ + 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 only. */ + }; + + /** + * @brief Convert the string to the matching value of device_prep_e. + * @param value String value to map to device_prep_e. + * @returns A device_prep_e value (converted to int) that matches the string + * or the default value if string does not match anything. + * @see device_prep_e + * + * EXAMPLES: + * ```cpp + * const int device_prep = device_prep_from_view("ensure_only_display"); + * ``` + */ + static int + device_prep_from_view(std::string_view value); + + /** + * @brief Enum detailing how to change the display's resolution. + */ + enum class resolution_change_e : int { + no_operation, /**< Keep the current resolution. */ + automatic, /**< Set the resolution to the one received from the client if the "Optimize game settings" option is also enabled in the client. */ + manual /**< User has to specify the resolution. */ + }; + + /** + * @brief Convert the string to the matching value of resolution_change_e. + * @param value String value to map to resolution_change_e. + * @returns A resolution_change_e value (converted to int) that matches the string + * or the default value if string does not match anything. + * @see resolution_change_e + * + * EXAMPLES: + * ```cpp + * const int resolution_change = resolution_change_from_view("manual"); + * ``` + */ + static int + resolution_change_from_view(std::string_view value); + + /** + * @brief Enum detailing how to change the display's refresh rate. + */ + 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. */ + }; + + /** + * @brief Convert the string to the matching value of refresh_rate_change_e. + * @param value String value to map to refresh_rate_change_e. + * @returns A refresh_rate_change_e value (converted to int) that matches the string + * or the default value if string does not match anything. + * @see refresh_rate_change_e + * + * EXAMPLES: + * ```cpp + * const int refresh_rate_change = refresh_rate_change_from_view("manual"); + * ``` + */ + static int + refresh_rate_change_from_view(std::string_view value); + + /** + * @brief Enum detailing how to change the display's HDR state. + */ + enum class hdr_prep_e : int { + no_operation, /**< User has to switch the HDR state manually */ + automatic /**< Switch HDR state based on the session settings and if display supports it. */ + }; + + /** + * @brief Convert the string to the matching value of hdr_prep_e. + * @param value String value to map to hdr_prep_e. + * @returns A hdr_prep_e value (converted to int) that matches the string + * or the default value if string does not match anything. + * @see hdr_prep_e + * + * EXAMPLES: + * ```cpp + * const int hdr_prep = hdr_prep_from_view("automatic"); + * ``` + */ + static int + hdr_prep_from_view(std::string_view value); + + std::string device_id; /**< Device id manually provided by the user via config. */ + device_prep_e device_prep; /**< The device_prep_e value taken from config. */ + boost::optional resolution; /**< Parsed resolution value we need to switch to. Empty optional if no action is required. */ + boost::optional refresh_rate; /**< Parsed refresh rate value we need to switch to. Empty optional if no action is required. */ + boost::optional change_hdr_state; /**< Parsed HDR state value we need to switch to (true == ON, false == OFF). Empty optional if no action is required. */ + }; + + /** + * @brief Parse the user configuration and the session information. + * @param config User's video related configuration. + * @param session Session information. + * @returns Parsed configuration or empty optional if parsing has failed. + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * const auto parsed_config = make_parsed_config(video_config, *launch_session); + * ``` + */ + 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..11ed9e06db8 --- /dev/null +++ b/src/display_device/session.cpp @@ -0,0 +1,174 @@ +// standard includes +#include + +// local includes +#include "session.h" +#include "src/platform/common.h" +#include "to_string.h" + +namespace display_device { + + class session_t::StateRestoreRetryTimer { + public: + /** + * @brief A constructor for the timer. + * @param mutex A shared mutex for synchronization. + * @param settings A shared settings instance for reverting settings. + * @param timeout_duration An amount of time to wait until retrying. + * @warning Because we are keeping references to shared parameters, we MUST ensure they outlive this object! + */ + StateRestoreRetryTimer(std::mutex &mutex, settings_t &settings, std::chrono::milliseconds timeout_duration): + mutex { mutex }, settings { settings }, timeout_duration { timeout_duration }, timer_thread { + std::thread { [this]() { + std::unique_lock lock { this->mutex }; + while (keep_alive) { + can_wake_up = false; + if (next_wake_up_time) { + // We're going to sleep forever until manually woken up or the time elapses + sleep_cv.wait_until(lock, *next_wake_up_time, [this]() { return can_wake_up; }); + } + else { + // We're going to sleep forever until manually woken up + sleep_cv.wait(lock, [this]() { return can_wake_up; }); + } + + if (next_wake_up_time) { + // Timer has just been started, or we have waited for the required amount of time. + // We can check which case it is by comparing time points. + + const auto now { std::chrono::steady_clock::now() }; + if (now < *next_wake_up_time) { + // Thread has been woken up manually to synchronize the time points. + // We do nothing and just go back to waiting with a new time point. + } + else { + const auto result { this->settings.revert_settings() }; + if (result) { + next_wake_up_time = boost::none; + } + else { + next_wake_up_time = now + this->timeout_duration; + } + } + } + else { + // Timer has been stopped. + // We do nothing and just go back to waiting until notified (unless we are killing the thread). + } + } + } } + } { + } + + /** + * @brief A destructor for the timer that gracefully shuts down the thread. + */ + ~StateRestoreRetryTimer() { + { + std::lock_guard lock { mutex }; + keep_alive = false; + next_wake_up_time = boost::none; + wake_up_thread(); + } + + timer_thread.join(); + } + + /** + * @brief Start or stop the timer thread. + * @param start Indicate whether to start or stop the timer. + * True - start or restart the timer to be executed after the specified duration from now. + * False - stop the timer and put the thread to sleep. + * @warning This method does NOT acquire the mutex! It is intended to be used from places + * where the mutex has already been locked. + */ + void + setup_timer(bool start) { + if (start) { + next_wake_up_time = std::chrono::steady_clock::now() + timeout_duration; + } + else { + if (!next_wake_up_time) { + return; + } + + next_wake_up_time = boost::none; + } + + wake_up_thread(); + } + + private: + /** + * @brief Manually wake up the thread. + */ + void + wake_up_thread() { + can_wake_up = true; + sleep_cv.notify_one(); + } + + std::mutex &mutex; /**< A reference to a shared mutex. */ + settings_t &settings; /**< A reference to a shared settings instance. */ + std::chrono::milliseconds timeout_duration; /**< A retry time for the timer. */ + + std::thread timer_thread; /**< A timer thread. */ + std::condition_variable sleep_cv; /**< Condition variable for waking up thread. */ + + bool can_wake_up { false }; /**< Safeguard for the condition variable to prevent sporadic thread wake ups. */ + bool keep_alive { true }; /**< A kill switch for the thread when it has been woken up. */ + boost::optional next_wake_up_time; /**< Next time point for thread to wake up. */ + }; + + 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() / "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) { + std::lock_guard lock { mutex }; + + const auto result { settings.apply_config(config, session) }; + timer->setup_timer(!result); + return result; + } + + void + session_t::restore_state() { + std::lock_guard lock { mutex }; + + const auto result { settings.revert_settings() }; + timer->setup_timer(!result); + } + + void + session_t::reset_persistence() { + std::lock_guard lock { mutex }; + + settings.reset_persistence(); + timer->setup_timer(false); + } + + session_t::session_t(): + timer { std::make_unique(mutex, settings, std::chrono::seconds { 30 }) } { + } + +} // namespace display_device diff --git a/src/display_device/session.h b/src/display_device/session.h new file mode 100644 index 00000000000..51ee450975c --- /dev/null +++ b/src/display_device/session.h @@ -0,0 +1,180 @@ +#pragma once + +// standard includes +#include + +// local includes +#include "settings.h" + +namespace display_device { + + /** + * @brief A singleton class for managing the display device configuration for the whole Sunshine session. + * + * This class is meant to be an entry point for applying the configuration and reverting it later + * from within the various places in the Sunshine's source code. + * + * It is similar to settings_t and is more or less a wrapper around it. + * However, this class ensures thread-safe usage for the methods and additionally + * performs automatic cleanups. + * + * @note A lazy-evaluated, correctly-destroyed, thread-safe singleton pattern is used here (https://stackoverflow.com/a/1008289). + */ + class session_t { + public: + /** + * @brief A class that uses RAII to perform cleanup when it's destroyed. + * @note The deinit_t usage pattern is used here instead of the session_t destructor + * to expedite the cleanup process in case of Sunshine termination. + * @see session_t::init() + */ + class deinit_t { + public: + /** + * @brief A destructor that restores (or tries to) the initial state. + */ + virtual ~deinit_t(); + }; + + /** + * @brief Get the singleton instance. + * @returns Singleton instance for the class. + * + * EXAMPLES: + * ```cpp + * session_t& session { session_t::get() }; + * ``` + */ + static session_t & + get(); + + /** + * @brief Initialize the singleton and perform the initial state recovery (if needed). + * @returns A deinit_t instance that performs cleanup when destroyed. + * @see deinit_t + * + * EXAMPLES: + * ```cpp + * const auto session_guard { session_t::init() }; + * ``` + */ + static std::unique_ptr + init(); + + /** + * @brief Configure the display device based on the user configuration and the session information. + * @param config User's video related configuration. + * @param session Session information. + * @returns The apply result value. + * @note Upon failing to completely apply configuration, the applied settings will be reverted. + * In case the settings cannot be reverted immediately, it will be retried again in 30 seconds + * (repeating indefinitely until success or until persistence is reset). + * @see settings_t::apply_result_t + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * const auto result = session_t::get().configure_display(video_config, *launch_session); + * ``` + */ + settings_t::apply_result_t + configure_display(const config::video_t &config, const rtsp_stream::launch_session_t &session); + + /** + * @brief Revert the display configuration and restore the previous state. + * @note This method automatically loads the persistence (if any) from the previous Sunshine session. + * @note In case the state could not be restored, it will be retried again in 30 seconds + * (repeating indefinitely until success or until persistence is reset). + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * const auto result = session_t::get().configure_display(video_config, *launch_session); + * if (result) { + * // Wait for some time + * session_t::get().restore_state(); + * } + * ``` + */ + void + restore_state(); + + /** + * @brief Reset the persistence and currently held initial display state. + * + * This is normally used to get out of the "broken" state where the algorithm wants + * to restore the initial display state and refuses start the stream in most cases. + * + * This could happen if the display is no longer available or the hardware was changed + * and the device ids no longer match. + * + * The user then accepts that Sunshine is not able to restore the state and "agrees" to + * do it manually. + * + * @note This also stops the 30 seconds retry timer. + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * const auto result = session_t::get().configure_display(video_config, *launch_session); + * if (!result) { + * // Wait for user to decide what to do + * const bool user_wants_reset { true }; + * if (user_wants_reset) { + * session_t::get().reset_persistence(); + * } + * } + * ``` + */ + void + reset_persistence(); + + /** + * @brief A deleted copy constructor for singleton pattern. + * @note Public to ensure better error message. + */ + session_t(session_t const &) = delete; + + /** + * @brief A deleted assignment operator for singleton pattern. + * @note Public to ensure better error message. + */ + void + operator=(session_t const &) = delete; + + private: + /** + * @brief A class for retrying to restore the original state. + * + * This timer class spins a thread which is mostly sleeping all the time, but can be + * configured to wake up every 30 seconds to try and restore the previous state. + * + * It is tightly synchronized with the session_t class via a shared mutex to ensure + * that stupid race conditions do not happen where we successfully apply settings + * for them to be reset by the timer thread immediately. + */ + class StateRestoreRetryTimer; + + /** + * @brief A private constructor to ensure the singleton pattern. + * @note Cannot be defaulted in declaration because of forward declared StateRestoreRetryTimer. + */ + explicit session_t(); + + settings_t settings; /**< A class for managing display device settings. */ + std::mutex mutex; /**< A mutex for ensuring thread-safety. */ + + /** + * @brief An instance of StateRestoreRetryTimer. + * @warning MUST BE declared after the settings and mutex members to ensure proper destruction order!. + */ + std::unique_ptr timer; + }; + +} // namespace display_device diff --git a/src/display_device/settings.cpp b/src/display_device/settings.cpp new file mode 100644 index 00000000000..565eb1ffe59 --- /dev/null +++ b/src/display_device/settings.cpp @@ -0,0 +1,46 @@ +// local includes +#include "settings.h" +#include "src/logging.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::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 (resolution + refresh rate)"; + 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 to persistent file"; + case result_e::revert_fail: + return "Failed to revert back to the original display settings"; + 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..b5b1c280231 --- /dev/null +++ b/src/display_device/settings.h @@ -0,0 +1,222 @@ +#pragma once + +// standard includes +#include +#include + +// local includes +#include "parsed_config.h" + +namespace display_device { + + /** + * @brief A platform specific class that can apply configuration to the display device and later revert it. + * + * Main goals of this class: + * - Apply the configuration to the display device. + * - Revert the applied configuration to get back to the initial state. + * - Save and load the previous state to/from a file. + */ + class settings_t { + public: + /** + * @brief Platform specific persistent data. + */ + struct persistent_data_t; + + /** + * @brief Platform specific non-persistent audio data in case we need to manipulate + * audio session and keep some temporary data around. + */ + struct audio_data_t; + + /** + * @brief The result value of the apply_config with additional metadata. + * @note Metadata is used when generating an XML status report to the client. + * @see apply_config + */ + struct apply_result_t { + /** + * @brief Possible result values/reasons from apply_config. + * @note There is no deeper meaning behind the values. They simply represent + * the stage where the method has failed to give some hints to the user. + * @note The value of 700 has no special meaning and is just arbitrary. + * @see apply_config + */ + enum class result_e : int { + success = 0, + config_parse_fail = 700, + topology_fail, + primary_display_fail, + modes_fail, + hdr_states_fail, + file_save_fail, + revert_fail + }; + + /** + * @brief Convert the result to boolean equivalent. + * @returns True if result means success, false otherwise. + * + * EXAMPLES: + * ```cpp + * const apply_result_t result { result_e::topology_fail }; + * if (result) { + * // Handle good result + * } + * else { + * // Handle bad result + * } + * ``` + */ + explicit + operator bool() const; + + /** + * @brief Convert the result to the underlying integer value. + * @returns Integer value of the result. + * + * EXAMPLES: + * ```cpp + * const apply_result_t result { result_e::topology_fail }; + * if (!result) { + * const int error_code = result.get_error_code(); + * } + * ``` + */ + [[nodiscard]] int + get_error_code() const; + + /** + * @brief Get a string message with better explanation for the result. + * @returns String message for the result. + * + * EXAMPLES: + * ```cpp + * const apply_result_t result { result_e::topology_fail }; + * if (!result) { + * const int error_message = result.get_error_message(); + * } + * ``` + */ + [[nodiscard]] std::string + get_error_message() const; + + result_e result; /**< The result value. */ + }; + + /** + * @brief A platform specific default constructor. + * @note Needed due to forwarding declarations used by the class. + */ + explicit settings_t(); + + /** + * @brief A platform specific destructor. + * @note Needed due to forwarding declarations used by the class. + */ + virtual ~settings_t(); + + /** + * @brief Set the file path for persistent data. + * + * EXAMPLES: + * ```cpp + * settings_t settings; + * settings.set_filepath("/foo/bar.json"); + * ``` + */ + void + set_filepath(std::filesystem::path filepath); + + /** + * @brief Apply configuration based on the user configuration and the session information. + * @param config User's video related configuration. + * @param session Session information. + * @returns The apply result value. + * @see apply_result_t + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * settings_t settings; + * const auto result = settings.apply_config(video_config, *launch_session); + * ``` + */ + apply_result_t + apply_config(const config::video_t &config, const rtsp_stream::launch_session_t &session); + + /** + * @brief Revert the applied configuration and restore the previous settings. + * @note It automatically loads the settings from persistence file if cached settings do not exist. + * @returns True if settings were reverted or there was nothing to revert, false otherwise. + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * settings_t settings; + * const auto result = settings.apply_config(video_config, *launch_session); + * if (result) { + * // Wait for some time + * settings.revert_settings(); + * } + * ``` + */ + bool + revert_settings(); + + /** + * @brief Reset the persistence and currently held initial display state. + * @see session_t::reset_persistence for more details. + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * settings_t settings; + * const auto result = settings.apply_config(video_config, *launch_session); + * if (result) { + * // Wait for some time + * if (settings.revert_settings()) { + * // Wait for user input + * const bool user_wants_reset { true }; + * if (user_wants_reset) { + * settings.reset_persistence(); + * } + * } + * } + * ``` + */ + void + reset_persistence(); + + private: + /** + * @brief Apply the parsed configuration. + * @param config A parsed and validated configuration. + * @returns The apply result value. + * @see apply_result_t + * @see parsed_config_t + * + * EXAMPLES: + * ```cpp + * const parsed_config_t config; + * + * settings_t settings; + * const auto result = settings.apply_config(config); + * ``` + */ + apply_result_t + apply_config(const parsed_config_t &config); + + std::unique_ptr persistent_data; /**< Platform specific persistent data. */ + std::unique_ptr audio_data; /**< Platform specific temporary audio data. */ + std::filesystem::path filepath; /**< Filepath for persistent file. */ + }; + +} // 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..82848b74e3b --- /dev/null +++ b/src/display_device/to_string.cpp @@ -0,0 +1,142 @@ +// local includes +#include "to_string.h" +#include "src/logging.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..c24bdd4565a --- /dev/null +++ b/src/display_device/to_string.h @@ -0,0 +1,138 @@ +#pragma once + +// local includes +#include "display_device.h" + +namespace display_device { + + /** + * @brief Stringify a device_state_e value. + * @param value Value to be stringified. + * @return A string representation of device_state_e value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(device_state_e { }); + * ``` + */ + std::string + to_string(device_state_e value); + + /** + * @brief Stringify a hdr_state_e value. + * @param value Value to be stringified. + * @return A string representation of hdr_state_e value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(hdr_state_e { }); + * ``` + */ + std::string + to_string(hdr_state_e value); + + /** + * @brief Stringify a hdr_state_map_t value. + * @param value Value to be stringified. + * @return A string representation of hdr_state_map_t value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(hdr_state_map_t { }); + * ``` + */ + std::string + to_string(const hdr_state_map_t &value); + + /** + * @brief Stringify a device_info_t value. + * @param value Value to be stringified. + * @return A string representation of device_info_t value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(device_info_t { }); + * ``` + */ + std::string + to_string(const device_info_t &value); + + /** + * @brief Stringify a device_info_map_t value. + * @param value Value to be stringified. + * @return A string representation of device_info_map_t value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(device_info_map_t { }); + * ``` + */ + std::string + to_string(const device_info_map_t &value); + + /** + * @brief Stringify a resolution_t value. + * @param value Value to be stringified. + * @return A string representation of resolution_t value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(resolution_t { }); + * ``` + */ + std::string + to_string(const resolution_t &value); + + /** + * @brief Stringify a refresh_rate_t value. + * @param value Value to be stringified. + * @return A string representation of refresh_rate_t value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(refresh_rate_t { }); + * ``` + */ + std::string + to_string(const refresh_rate_t &value); + + /** + * @brief Stringify a display_mode_t value. + * @param value Value to be stringified. + * @return A string representation of display_mode_t value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(display_mode_t { }); + * ``` + */ + std::string + to_string(const display_mode_t &value); + + /** + * @brief Stringify a device_display_mode_map_t value. + * @param value Value to be stringified. + * @return A string representation of device_display_mode_map_t value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(device_display_mode_map_t { }); + * ``` + */ + std::string + to_string(const device_display_mode_map_t &value); + + /** + * @brief Stringify a active_topology_t value. + * @param value Value to be stringified. + * @return A string representation of active_topology_t value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(active_topology_t { }); + * ``` + */ + std::string + to_string(const active_topology_t &value); + +} // namespace display_device diff --git a/src/main.cpp b/src/main.cpp index 5cc28f9f2a2..50b6810ed01 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,7 @@ // local includes #include "confighttp.h" +#include "display_device/session.h" #include "entry_handler.h" #include "globals.h" #include "httpcommon.h" @@ -215,6 +216,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()) { @@ -312,7 +321,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 = []() { @@ -323,9 +332,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 = []() { @@ -336,6 +346,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 b8bddb44bb7..fb05ce8dda5 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 "file_handler.h" #include "globals.h" #include "httpcommon.h" @@ -773,12 +774,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) { @@ -813,11 +819,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?"); @@ -827,9 +851,6 @@ namespace nvhttp { } } - host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); - auto launch_session = make_launch_session(host_audio, args); - auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv; @@ -859,6 +880,9 @@ namespace nvhttp { tree.put("root.gamesession", 1); rtsp_stream::launch_session_raise(launch_session); + + // Stream was started successfully, we will restore the state when the app or session terminates + need_to_restore_display_state = false; } void @@ -904,7 +928,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, @@ -916,17 +961,8 @@ 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")); - } } - auto launch_session = make_launch_session(host_audio, args); - auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv; @@ -976,6 +1012,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..72faf7ba852 --- /dev/null +++ b/src/platform/linux/display_device.cpp @@ -0,0 +1,117 @@ +// 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::audio_data_t { + // Not implemented + }; + + struct settings_t::persistent_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 }; + } + + bool + settings_t::revert_settings() { + // Not implemented + return true; + } + + void + settings_t::reset_persistence() { + // 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..72faf7ba852 --- /dev/null +++ b/src/platform/macos/display_device.cpp @@ -0,0 +1,117 @@ +// 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::audio_data_t { + // Not implemented + }; + + struct settings_t::persistent_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 }; + } + + bool + settings_t::revert_settings() { + // Not implemented + return true; + } + + void + settings_t::reset_persistence() { + // Not implemented + } + +} // namespace display_device diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index b258690e10e..2f746c9137f 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/logging.h" #include "src/platform/common.h" #include "src/stat_trackers.h" @@ -1083,7 +1084,8 @@ namespace platf { BOOST_LOG(debug) << "Detecting monitors..."sv; // 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..c6545911f07 --- /dev/null +++ b/src/platform/windows/display_device/device_hdr_states.cpp @@ -0,0 +1,108 @@ +// local includes +#include "src/display_device/to_string.h" +#include "src/logging.h" +#include "windows_utils.h" + +namespace display_device { + + namespace { + + /** + * @see set_hdr_states for a description as this was split off to reduce cognitive complexity. + */ + 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..0adfd840665 --- /dev/null +++ b/src/platform/windows/display_device/device_modes.cpp @@ -0,0 +1,336 @@ +// local includes +#include "src/logging.h" +#include "windows_utils.h" + +namespace display_device { + + namespace { + + /** + * @bried Check if the refresh rates are almost equal. + * @param r1 First refresh rate. + * @param r2 Second refresh rate. + * @return True if refresh rates are almost equal, false otherwise. + * + * EXAMPLES: + * ```cpp + * const bool almost_equal = fuzzy_compare_refresh_rates(refresh_rate_t { 60, 1 }, refresh_rate_t { 5985, 100 }); + * const bool not_equal = fuzzy_compare_refresh_rates(refresh_rate_t { 60, 1 }, refresh_rate_t { 5585, 100 }); + * ``` + */ + bool + fuzzy_compare_refresh_rates(const refresh_rate_t &r1, const refresh_rate_t &r2) { + if (r1.denominator > 0 && r2.denominator > 0) { + const float r1_f { static_cast(r1.numerator) / static_cast(r1.denominator) }; + const float r2_f { static_cast(r2.numerator) / static_cast(r2.denominator) }; + return (std::abs(r1_f - r2_f) <= 1.f); + } + + return false; + } + + /** + * @bried Check if the display modes are almost equal. + * @param mode_a First mode. + * @param mode_b Second mode. + * @return True if display modes are almost equal, false otherwise. + * + * EXAMPLES: + * ```cpp + * const bool almost_equal = fuzzy_compare_refresh_rates(display_mode_t { { 1920, 1080 }, { 60, 1 } }, + * display_mode_t { { 1920, 1080 }, { 5985, 100 } }); + * const bool not_equal = fuzzy_compare_refresh_rates(display_mode_t { { 1920, 1080 }, { 60, 1 } }, + * display_mode_t { { 1920, 1080 }, { 5585, 100 } }); + * ``` + */ + bool + fuzzy_compare_modes(const display_mode_t &mode_a, const display_mode_t &mode_b) { + return mode_a.resolution.width == mode_b.resolution.width && + mode_a.resolution.height == mode_b.resolution.height && + fuzzy_compare_refresh_rates(mode_a.refresh_rate, mode_b.refresh_rate); + } + + /** + * @brief Get all the missing duplicate device ids for the provided device ids. + * @param device_ids Device ids to find the missing duplicate ids for. + * @returns A list of device ids containing the provided device ids and all unspecified ids + * for duplicated displays. + * + * EXAMPLES: + * ```cpp + * const auto device_ids_with_duplicates = get_all_duplicated_devices({ "MY_ID1" }); + * ``` + */ + 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 {}; + } + + 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 the active paths (provided path included) and check if + // any of them are duplicated. + for (const auto &path : display_data->paths) { + const auto device_info { w_utils::get_device_info_for_valid_path(path, w_utils::ACTIVE_ONLY_DEVICES) }; + if (!device_info) { + continue; + } + + if (all_device_ids.count(device_info->device_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: " << device_info->device_id << "!"; + return {}; + } + + if (!w_utils::are_modes_duplicated(*provided_path_source_mode, *source_mode)) { + continue; + } + + all_device_ids.insert(device_info->device_id); + } + } + + return all_device_ids; + } + + /** + * @see set_display_modes for a description as this was split off to reduce cognitive complexity. + */ + 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/modify the target to best match the requirements. + 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_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 that all duplicate + // devices were provided instead of guessing modes 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)) { + // 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 + // 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 mode + // 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. + 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..9822e03453c --- /dev/null +++ b/src/platform/windows/display_device/device_topology.cpp @@ -0,0 +1,482 @@ +// lib includes +#include + +// local includes +#include "src/logging.h" +#include "windows_utils.h" + +namespace display_device { + + namespace { + + /** + * @brief Contains arbitrary data collected from queried display paths. + */ + struct path_data_t { + std::unordered_map source_id_to_path_index; /**< Maps source ids to its index in the path list. */ + LUID source_adapter_id {}; /**< Adapter id shared by all source ids. */ + boost::optional active_source; /**< Currently active source id. */ + }; + + /** + * @brief Ordered map of [DEVICE_ID -> path_data_t]. + * @see path_data_t + */ + using path_data_map_t = std::map; + + /** + * @brief Check if adapter ids are equal. + * @param id_a First id to check. + * @param id_b Second id to check. + * @return True if equal, false otherwise. + * + * EXAMPLES: + * ```cpp + * const bool equal = compareAdapterIds({ 12, 34 }, { 12, 34 }); + * const bool not_equal = compareAdapterIds({ 12, 34 }, { 12, 56 }); + * ``` + */ + bool + compareAdapterIds(const LUID &id_a, const LUID &id_b) { + return id_a.HighPart == id_b.HighPart && id_a.LowPart == id_b.LowPart; + } + + /** + * @brief Stringify adapter id. + * @param id Id to stringify. + * @return String representation of the id. + * + * EXAMPLES: + * ```cpp + * const bool id_string = to_string({ 12, 34 }); + * ``` + */ + std::string + to_string(const LUID &id) { + return std::to_string(id.HighPart) + std::to_string(id.LowPart); + } + + /** + * @brief Collect arbitrary data from provided paths. + * + * This function filters paths that can be used later on and + * collects some arbitrary data for a quick lookup. + * + * @param paths List of paths. + * @returns Data for valid paths. + * @see query_display_config on how to get paths from the system. + * @see make_new_paths_for_topology for the actual data use example. + * + * EXAMPLES: + * ```cpp + * std::vector paths; + * const auto path_data = make_device_path_data(paths); + * ``` + */ + path_data_map_t + make_device_path_data(const std::vector &paths) { + path_data_map_t path_data; + std::unordered_map paths_to_ids; + for (std::size_t index = 0; index < paths.size(); ++index) { + const auto &path { paths[index] }; + + const auto device_info { w_utils::get_device_info_for_valid_path(path, w_utils::ALL_DEVICES) }; + if (!device_info) { + // Path is not valid + continue; + } + + const auto prev_device_id_for_path_it { paths_to_ids.find(device_info->device_path) }; + if (prev_device_id_for_path_it != std::end(paths_to_ids)) { + if (prev_device_id_for_path_it->second != device_info->device_id) { + BOOST_LOG(error) << "duplicate display device id found: " << device_info->device_id << " (device path: " << device_info->device_path << ")"; + return {}; + } + } + else { + BOOST_LOG(verbose) << "new valid device id entry for device " << device_info->device_id << " (device path: " << device_info->device_path << ")"; + paths_to_ids[device_info->device_path] = device_info->device_id; + } + + auto path_data_it { path_data.find(device_info->device_id) }; + if (path_data_it != std::end(path_data)) { + if (!compareAdapterIds(path_data_it->second.source_adapter_id, path.sourceInfo.adapterId)) { + // Sanity check, should not be possible since adapter in embedded in the device path + BOOST_LOG(error) << "device path " << device_info->device_path << " has different adapters!"; + return {}; + } + + path_data_it->second.source_id_to_path_index[path.sourceInfo.id] = index; + } + else { + path_data[device_info->device_id] = path_data_t { + { { path.sourceInfo.id, index } }, + path.sourceInfo.adapterId, + // Since active paths are always in the front, this is the only time we check (when we add new entry) + w_utils::is_active(path) ? boost::make_optional(path.sourceInfo.id) : boost::none + }; + } + } + + return path_data; + } + + /** + * @brief Select the best possible paths to be used for the requested topology based on the data that is available to us. + * + * If the paths will be used for a completely new topology (Windows has never had it set), we need to take into + * account the source id availability per the adapter - duplicated displays must share the same source id + * (if they belong to the same adapter) and have different ids if they are not duplicated displays. + * + * There are limited amount of available ids (see comments in the code) so we will abort early if we are + * out of ids. + * + * The paths for a topology that already exists (Windows has set it at least once) does not have to follow + * the mentioned "source id" rule. Windows will simply ignore them (since we will ask it to later) and select + * paths that were previously configured (that might differ in source ids) based on the paths that we provide. + * + * @param new_topology Topology that we want to have in the end. + * @param path_data Collected arbitrary path data. + * @param paths Display paths. + * @return A list of path that will make up new topology, or an empty list if function fails. + */ + std::vector + make_new_paths_for_topology(const active_topology_t &new_topology, const path_data_map_t &path_data, const std::vector &paths) { + std::vector new_paths; + + UINT32 group_id { 0 }; + std::unordered_map> used_source_ids_per_adapter; + const auto is_source_id_already_used = [&used_source_ids_per_adapter](const LUID &adapter_id, UINT32 source_id) { + auto entry_it { used_source_ids_per_adapter.find(to_string(adapter_id)) }; + if (entry_it != std::end(used_source_ids_per_adapter)) { + return entry_it->second.count(source_id) > 0; + } + + return false; + }; + + for (const auto &group : new_topology) { + std::unordered_map used_source_ids_per_adapter_per_group; + const auto get_already_used_source_id_in_group = [&used_source_ids_per_adapter_per_group](const LUID &adapter_id) -> boost::optional { + auto entry_it { used_source_ids_per_adapter_per_group.find(to_string(adapter_id)) }; + if (entry_it != std::end(used_source_ids_per_adapter_per_group)) { + return entry_it->second; + } + + return boost::none; + }; + + for (const std::string &device_id : group) { + auto path_data_it { path_data.find(device_id) }; + if (path_data_it == std::end(path_data)) { + BOOST_LOG(error) << "device " << device_id << " does not exist in the available topology data!"; + return {}; + } + + std::size_t selected_path_index {}; + const auto &device_data { path_data_it->second }; + + const auto already_used_source_id { get_already_used_source_id_in_group(device_data.source_adapter_id) }; + if (already_used_source_id) { + // Some device in the group is already using the source id, and we belong to the same adapter. + // This means we must also use the path with matching source id. + auto path_source_it { device_data.source_id_to_path_index.find(*already_used_source_id) }; + if (path_source_it == std::end(device_data.source_id_to_path_index)) { + BOOST_LOG(error) << "device " << device_id << " does not have a path with a source id " << *already_used_source_id << "!"; + return {}; + } + + selected_path_index = path_source_it->second; + } + else { + // Here we want to select a path index that has the lowest index (the "best" of paths), but only + // if the source id is still free. Technically we don't need to find the lowest index, but that's + // what will match the Windows' behaviour the closest if we need to create new topology in the end. + boost::optional path_index_candidate; + UINT32 used_source_id {}; + for (const auto [source_id, index] : device_data.source_id_to_path_index) { + if (is_source_id_already_used(device_data.source_adapter_id, source_id)) { + continue; + } + + if (!path_index_candidate || index < *path_index_candidate) { + path_index_candidate = index; + used_source_id = source_id; + } + } + + if (!path_index_candidate) { + // Apparently nvidia GPU can only render 4 different sources at a time (according to Google). + // However, it seems to be true only for physical connections as we also have virtual displays. + // + // Virtual displays have different adapter ids than the physical connection ones, but GPU still + // has to render them, so I don't know how this 4 source limitation makes sense then? + // + // In short, this arbitrary limitation should not affect virtual displays when the GPU is at its limit. + BOOST_LOG(error) << "device " << device_id << " cannot be enabled as the adapter has no more free source id (GPU limitation)!"; + return {}; + } + + selected_path_index = *path_index_candidate; + used_source_ids_per_adapter[to_string(device_data.source_adapter_id)].insert(used_source_id); + used_source_ids_per_adapter_per_group[to_string(device_data.source_adapter_id)] = used_source_id; + } + + auto selected_path { paths.at(selected_path_index) }; + + // All the indexes must be cleared and only the group id specified + w_utils::set_source_index(selected_path, boost::none); + w_utils::set_target_index(selected_path, boost::none); + w_utils::set_desktop_index(selected_path, boost::none); + w_utils::set_clone_group_id(selected_path, group_id); + w_utils::set_active(selected_path); // We also need to mark it as active... + + new_paths.push_back(selected_path); + } + + group_id++; + } + + return new_paths; + } + + /** + * @see set_topology for a description as this was split off to reduce cognitive complexity. + */ + 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 path_data { make_device_path_data(display_data->paths) }; + if (path_data.empty()) { + // Error already logged + return false; + } + + auto paths { make_new_paths_for_topology(new_topology, path_data, display_data->paths) }; + if (paths.empty()) { + // Error already logged + return false; + } + + UINT32 flags { SDC_APPLY | SDC_TOPOLOGY_SUPPLIED | SDC_ALLOW_PATH_ORDER_CHANGES | SDC_VIRTUAL_MODE_AWARE }; + LONG result { SetDisplayConfig(paths.size(), paths.data(), 0, nullptr, flags) }; + if (result == ERROR_GEN_FAILURE) { + BOOST_LOG(warning) << w_utils::get_error_string(result) << " failed to change topology using the topology from Windows DB! Asking Windows to create the topology."; + + flags = SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES /* This flag is probably not needed, but who knows really... (not MSDOCS at least) */ | SDC_VIRTUAL_MODE_AWARE | SDC_SAVE_TO_DATABASE; + result = SetDisplayConfig(paths.size(), paths.data(), 0, nullptr, flags); + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << w_utils::get_error_string(result) << " failed to create new topology configuration!"; + return false; + } + } + else if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << w_utils::get_error_string(result) << " failed to create change topology configuration!"; + 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 topology_data { make_device_path_data(display_data->paths) }; + if (topology_data.empty()) { + // Error already logged + return {}; + } + + for (const auto &[device_id, data] : topology_data) { + const auto &path { display_data->paths.at(data.source_id_to_path_index.at(data.active_source.get_value_or(0))) }; + + 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 devices can have multiple display names, so it's just meaningless use any + 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 a + // "position to index" map 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 device_info { w_utils::get_device_info_for_valid_path(path, w_utils::ACTIVE_ONLY_DEVICES) }; + if (!device_info) { + 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: " << device_info->device_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({ device_info->device_id }); + } + else { + topology.at(index_it->second).push_back(device_info->device_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 &topology_a, const active_topology_t &topology_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 { topology_a }; + auto b_copy { topology_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..cc86bdcfa06 --- /dev/null +++ b/src/platform/windows/display_device/general_functions.cpp @@ -0,0 +1,138 @@ +// standard includes +#include + +// local includes +#include "src/logging.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(error) << "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 and others to their new positions. + 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_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_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..b2b8c265b6c --- /dev/null +++ b/src/platform/windows/display_device/settings.cpp @@ -0,0 +1,739 @@ +// standard includes +#include +#include + +// local includes +#include "settings_topology.h" +#include "src/audio.h" +#include "src/display_device/to_string.h" +#include "src/logging.h" + +namespace display_device { + + struct settings_t::persistent_data_t { + topology_pair_t topology; /**< Contains topology before the modification and the one we modified. */ + std::string original_primary_display; /**< Original primary display in the topology we modified. Empty value if we didn't modify it. */ + device_display_mode_map_t original_modes; /**< Original display modes in the topology we modified. Empty value if we didn't modify it. */ + hdr_state_map_t original_hdr_states; /**< Original display HDR states in the topology we modified. Empty value if we didn't modify it. */ + + /** + * @brief Check if the persistent data contains any meaningful modifications that need to be reverted. + * @returns True if the data contains something that needs to be reverted, false otherwise. + * + * EXAMPLES: + * ```cpp + * settings_t::persistent_data_t data; + * if (data.contains_modifications()) { + * // save persistent data + * } + * ``` + */ + [[nodiscard]] bool + contains_modifications() const { + return !is_topology_the_same(topology.initial, topology.modified) || + !original_primary_display.empty() || + !original_modes.empty() || + !original_hdr_states.empty(); + } + + // For JSON serialization + NLOHMANN_DEFINE_TYPE_INTRUSIVE(persistent_data_t, topology, original_primary_display, original_modes, original_hdr_states) + }; + + struct settings_t::audio_data_t { + /** + * @brief A reference to the audio context that will automatically extend the audio session. + * @note It is auto-initialized here for convenience. + */ + decltype(audio::get_audio_ctx_ref()) audio_ctx_ref { audio::get_audio_ctx_ref() }; + }; + + namespace { + + /** + * @brief Get one of the primary display ids found in the topology metadata. + * @param metadata Topology metadata that also includes current active topology. + * @return Device id for the primary device, or empty string if primary device not found somehow. + * + * EXAMPLES: + * ```cpp + * topology_metadata_t metadata; + * const std::string primary_device_id = get_current_primary_display(metadata); + * ``` + */ + 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 {}; + } + + /** + * @brief Compute the new primary display id based on the information we have. + * @param original_primary_display Original device id (the one before our first modification or from current topology). + * @param metadata The current metadata that we are evaluating. + * @return Primary display id that matches the requirements. + * + * EXAMPLES: + * ```cpp + * topology_metadata_t metadata; + * const std::string primary_device_id = determine_new_primary_display("MY_DEVICE_ID", metadata); + * ``` + */ + std::string + determine_new_primary_display(const std::string &original_primary_display, const topology_metadata_t &metadata) { + if (metadata.primary_device_requested) { + // Primary device was requested - no device was specified by user. + // This means we are keeping whatever display we have. + return original_primary_display; + } + + // For primary devices it is enough to set 1 as a primary display, as the whole duplicated group + // will become primary displays. + const auto new_primary_device { metadata.duplicated_devices.front() }; + return new_primary_device; + } + + /** + * @brief Change the primary display based on the configuration and previously configured primary display. + * + * The function performs the necessary steps for changing the primary display if needed. + * It also evaluates for possible changes in the configuration and undoes the changes + * we have made before. + * + * @param device_prep Device preparation value from the configuration. + * @param previous_primary_display Device id of the original primary display we have initially changed (can be empty). + * @param metadata Additional data with info about the current topology. + * @return Device id to be used when reverting all settings (can be empty string), or an empty optional if the function fails. + */ + boost::optional + handle_primary_display_configuration(const parsed_config_t::device_prep_e &device_prep, const std::string &previous_primary_display, const topology_metadata_t &metadata) { + if (device_prep == parsed_config_t::device_prep_e::ensure_primary) { + const auto original_primary_display { previous_primary_display.empty() ? get_current_primary_display(metadata) : previous_primary_display }; + const auto new_primary_display { determine_new_primary_display(original_primary_display, metadata) }; + + BOOST_LOG(debug) << "changing primary display to: " << new_primary_display; + if (!set_as_primary_device(new_primary_display)) { + // Error already logged + return boost::none; + } + + // Here we preserve the data from persistence (unless there's none) as in the end that is what we want to go back to. + return original_primary_display; + } + + if (!previous_primary_display.empty()) { + BOOST_LOG(debug) << "changing primary display back to: " << previous_primary_display; + if (!set_as_primary_device(previous_primary_display)) { + // Error already logged + return boost::none; + } + } + + return std::string {}; + } + + /** + * @brief Compute the new display modes based on the information we have. + * @param resolution Resolution value from the configuration. + * @param refresh_rate Refresh rate value from the configuration. + * @param original_display_modes Original display modes (the ones before our first modification or from current topology) + * that we use as a base we will apply changes to. + * @param metadata The current metadata that we are evaluating. + * @return New display modes for the topology. + */ + 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, otherwise + // they cannot be duplicated, which breaks Windows' rules. + 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; + } + + /** + * @brief Modify the display modes based on the configuration and previously configured display modes. + * + * The function performs the necessary steps for changing the display modes if needed. + * It also evaluates for possible changes in the configuration and undoes the changes + * we have made before. + * + * @param resolution Resolution value from the configuration. + * @param refresh_rate Refresh rate value from the configuration. + * @param previous_display_modes Original display modes that we have initially changed (can be empty). + * @param metadata Additional data with info about the current topology. + * @return Display modes to be used when reverting all settings (can be empty map), or an empty optional if the function fails. + */ + boost::optional + handle_display_mode_configuration(const boost::optional &resolution, const boost::optional &refresh_rate, const device_display_mode_map_t &previous_display_modes, const topology_metadata_t &metadata) { + if (resolution || refresh_rate) { + const auto original_display_modes { previous_display_modes.empty() ? get_current_display_modes(get_device_ids_from_topology(metadata.current_topology)) : previous_display_modes }; + const auto new_display_modes { determine_new_display_modes(resolution, refresh_rate, original_display_modes, metadata) }; + + BOOST_LOG(debug) << "changing display modes to: " << to_string(new_display_modes); + if (!set_display_modes(new_display_modes)) { + // Error already logged + return boost::none; + } + + // Here we preserve the data from persistence (unless there's none) as in the end that is what we want to go back to. + return original_display_modes; + } + + if (!previous_display_modes.empty()) { + BOOST_LOG(debug) << "changing display modes back to: " << to_string(previous_display_modes); + if (!set_display_modes(previous_display_modes)) { + // Error already logged + return boost::none; + } + } + + return device_display_mode_map_t {}; + } + + /** + * @brief Reverse ("blank") HDR states for newly enabled devices. + * + * 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 states 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. + * + * @param states Final states for the devices that we want to blank. + * @param newly_enabled_devices Devices to perform blanking for. + * @return False if the function has failed to set HDR states, true otherwise. + * + * EXAMPLES: + * ```cpp + * hdr_state_map_t new_states; + * const bool success = blank_hdr_states(new_states, { "DEVICE_ID" }); + * ``` + */ + 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(debug) << "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; + } + + /** + * @brief Compute the new HDR states based on the information we have. + * @param change_hdr_state HDR state value from the configuration. + * @param original_hdr_states Original HDR states (the ones before our first modification or from current topology) + * that we use as a base we will apply changes to. + * @param metadata The current metadata that we are evaluating. + * @return New HDR states for the topology. + */ + 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 final_state { *change_hdr_state ? hdr_state_e::enabled : hdr_state_e::disabled }; + const auto try_update_new_state = [&new_states, final_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] = final_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; + } + + /** + * @brief Modify the display HDR states based on the configuration and previously configured display HDR states. + * + * The function performs the necessary steps for changing the display HDR states if needed. + * It also evaluates for possible changes in the configuration and undoes the changes + * we have made before. + * + * @param change_hdr_state HDR state value from the configuration. + * @param previous_hdr_states Original display HDR states have initially changed (can be empty). + * @param metadata Additional data with info about the current topology. + * @return Display HDR states to be used when reverting all settings (can be empty map), or an empty optional if the function fails. + */ + boost::optional + handle_hdr_state_configuration(const boost::optional &change_hdr_state, const hdr_state_map_t &previous_hdr_states, const topology_metadata_t &metadata) { + if (change_hdr_state) { + const auto original_hdr_states { previous_hdr_states.empty() ? get_current_hdr_states(get_device_ids_from_topology(metadata.current_topology)) : previous_hdr_states }; + const auto new_hdr_states { determine_new_hdr_states(change_hdr_state, original_hdr_states, metadata) }; + + BOOST_LOG(debug) << "changing hdr states to: " << to_string(new_hdr_states); + if (!blank_hdr_states(new_hdr_states, metadata.newly_enabled_devices) || !set_hdr_states(new_hdr_states)) { + // Error already logged + return boost::none; + } + + // Here we preserve the data from persistence (unless there's none) as in the end that is what we want to go back to. + return original_hdr_states; + } + + if (!previous_hdr_states.empty()) { + BOOST_LOG(debug) << "changing hdr states back to: " << to_string(previous_hdr_states); + if (!blank_hdr_states(previous_hdr_states, metadata.newly_enabled_devices) || !set_hdr_states(previous_hdr_states)) { + // Error already logged + return boost::none; + } + } + + return hdr_state_map_t {}; + } + + /** + * @brief Revert settings to the ones found in the persistent data. + * @param data Reference to persistent data containing original settings. + * @param data_modified Reference to a boolean that is set to true if changes are made to the persistent data reference. + * @return True if all settings within persistent data have been reverted, false otherwise. + * + * EXAMPLES: + * ```cpp + * bool data_modified { false }; + * settings_t::persistent_data_t data; + * + * if (!try_revert_settings(data, data_modified)) { + * if (data_modified) { + * // Update the persistent file + * } + * } + * ``` + */ + bool + try_revert_settings(settings_t::persistent_data_t &data, bool &data_modified) { + if (!data.contains_modifications()) { + return true; + } + + const bool have_changes_for_modified_topology { !data.original_primary_display.empty() || !data.original_modes.empty() || !data.original_hdr_states.empty() }; + std::unordered_set newly_enabled_devices; + bool partially_failed { false }; + auto current_topology { get_current_topology() }; + + if (have_changes_for_modified_topology) { + if (set_topology(data.topology.modified)) { + newly_enabled_devices = get_newly_enabled_devices_from_topology(current_topology, data.topology.modified); + current_topology = data.topology.modified; + + if (!data.original_hdr_states.empty()) { + BOOST_LOG(debug) << "changing back the HDR states to: " << to_string(data.original_hdr_states); + if (set_hdr_states(data.original_hdr_states)) { + data.original_hdr_states.clear(); + data_modified = true; + } + else { + partially_failed = true; + } + } + + if (!data.original_modes.empty()) { + BOOST_LOG(debug) << "changing back the display modes to: " << to_string(data.original_modes); + if (set_display_modes(data.original_modes)) { + data.original_modes.clear(); + data_modified = true; + } + else { + partially_failed = true; + } + } + + if (!data.original_primary_display.empty()) { + BOOST_LOG(debug) << "changing back the primary device to: " << data.original_primary_display; + if (set_as_primary_device(data.original_primary_display)) { + data.original_primary_display.clear(); + data_modified = true; + } + else { + partially_failed = true; + } + } + } + else { + BOOST_LOG(error) << "cannot switch to the topology to undo changes!"; + partially_failed = true; + } + } + + if (set_topology(data.topology.initial)) { + newly_enabled_devices.merge(get_newly_enabled_devices_from_topology(current_topology, data.topology.initial)); + current_topology = data.topology.initial; + data_modified = true; + } + else { + BOOST_LOG(error) << "failed to switch back to the initial topology!"; + partially_failed = true; + } + + if (!newly_enabled_devices.empty()) { + const auto current_hdr_states { get_current_hdr_states(get_device_ids_from_topology(current_topology)) }; + + BOOST_LOG(debug) << "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 + } + + return !partially_failed; + } + + /** + * @brief Save settings to the JSON file. + * @param filepath Filepath for the persistent data. + * @param data Persistent data to save. + * @return True if the filepath is empty or the data was saved to the file, false otherwise. + * + * EXAMPLES: + * ```cpp + * settings_t::persistent_data_t data; + * + * if (save_settings("/foo/bar.json", data)) { + * // Do stuff... + * } + * ``` + */ + bool + save_settings(const std::filesystem::path &filepath, const settings_t::persistent_data_t &data) { + if (filepath.empty()) { + BOOST_LOG(warning) << "No filename was specified for persistent display device configuration."; + return true; + } + + 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; + } + + /** + * @brief Load persistent data from the JSON file. + * @param filepath Filepath to load data from. + * @return Unique pointer to the persistent data if it was loaded successfully, nullptr otherwise. + * + * EXAMPLES: + * ```cpp + * auto data = load_settings("/foo/bar.json"); + * ``` + */ + std::unique_ptr + load_settings(const std::filesystem::path &filepath) { + try { + if (!filepath.empty() && std::filesystem::exists(filepath)) { + std::ifstream file(filepath); + return std::make_unique(nlohmann::json::parse(file)); + } + } + catch (const std::exception &err) { + BOOST_LOG(info) << "Failed to load saved display settings: " << err.what(); + } + + return nullptr; + } + + /** + * @brief Remove the file. + * @param filepath Filepath to remove. + * + * EXAMPLES: + * ```cpp + * remove_file("/foo/bar.json"); + * ``` + */ + 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() = default; + + settings_t::~settings_t() = default; + + settings_t::apply_result_t + settings_t::apply_config(const config::video_t &config, const rtsp_stream::launch_session_t &session) { + BOOST_LOG(info) << "Applying configuration to the display device."; + const auto parsed_config { make_parsed_config(config, session) }; + if (!parsed_config) { + BOOST_LOG(error) << "Failed to apply configuration to the display device."; + return { apply_result_t::result_e::config_parse_fail }; + } + + const bool display_may_change { parsed_config->device_prep == parsed_config_t::device_prep_e::ensure_only_display }; + if (display_may_change && !audio_data) { + // It is very likely that in this situation our "current" audio device will be gone, so we + // want to capture the audio sink immediately and extend the audio session until we revert our changes. + BOOST_LOG(debug) << "Capturing audio sink before changing display"; + audio_data = std::make_unique(); + } + + const auto result { apply_config(*parsed_config) }; + if (result) { + if (!display_may_change && audio_data) { + // Just to be safe in the future when the video config can be reloaded + // without Sunshine restarting, we should clean up, because in this situation + // we have had to revert the changes that turned off other displays. Thus, extending + // the session for a display that again exist is pointless. + BOOST_LOG(debug) << "Releasing captured audio sink"; + audio_data = nullptr; + } + } + + BOOST_LOG(info) << "Display device configuration applied."; + return result; + } + + bool + settings_t::revert_settings() { + if (!persistent_data) { + BOOST_LOG(info) << "Loading persistent display device settings."; + persistent_data = load_settings(filepath); + } + + if (persistent_data) { + BOOST_LOG(info) << "Reverting display device settings."; + + bool data_updated { false }; + if (!try_revert_settings(*persistent_data, data_updated)) { + if (data_updated) { + save_settings(filepath, *persistent_data); // Ignoring return value + } + + BOOST_LOG(error) << "Failed to revert display device settings!"; + return false; + } + + remove_file(filepath); + persistent_data = nullptr; + + if (audio_data) { + BOOST_LOG(debug) << "Releasing captured audio sink"; + audio_data = nullptr; + } + + BOOST_LOG(info) << "Display device configuration reverted."; + } + + return true; + } + + void + settings_t::reset_persistence() { + BOOST_LOG(info) << "Purging persistent display device data (trying to reset settings one last time)."; + if (persistent_data && !revert_settings()) { + BOOST_LOG(info) << "Failed to revert settings - proceeding to reset persistence."; + } + + remove_file(filepath); + persistent_data = nullptr; + + if (audio_data) { + BOOST_LOG(debug) << "Releasing captured audio sink"; + audio_data = nullptr; + } + } + + settings_t::apply_result_t + settings_t::apply_config(const parsed_config_t &config) { + bool failed_while_reverting_settings { false }; + const boost::optional previously_configured_topology { persistent_data ? boost::make_optional(persistent_data->topology) : boost::none }; + + // On Windows the display settings are kept per an active topology list - each topology + // has separate configuration saved in the database. Therefore, we must always switch + // to the topology we want to modify before we actually start applying settings. + const auto topology_result { handle_device_topology_configuration(config, previously_configured_topology, [&]() { + const bool audio_sink_was_captured { audio_data != nullptr }; + if (!revert_settings()) { + failed_while_reverting_settings = true; + return false; + } + + if (audio_sink_was_captured && !audio_data) { + audio_data = std::make_unique(); + } + return true; + }) }; + if (!topology_result) { + // Error already logged + return { failed_while_reverting_settings ? apply_result_t::result_e::revert_fail : apply_result_t::result_e::topology_fail }; + } + + // Once we have switched to the correct topology, we need to select where we want to + // save persistent data. + // + // If we already have cached persistent data, we want to use that, however we must NOT + // take over the topology "pair" from the result as the initial topology doest not + // reflect the actual initial topology before we made our first changes. + // + // There is no better way to somehow always guess the initial topology we want to revert to. + // The user could have switched topology when the stream was paused, then technically we could + // try to switch back to that topology. However, the display could have also turned off and the + // topology was automatically changed by Windows. In this case we don't want to switch back to + // that topology since it was not the user's decision. + // + // Therefore, we are always sticking with the first initial topology before the first configuration + // was applied. + persistent_data_t new_settings { topology_result->pair }; + persistent_data_t ¤t_settings { persistent_data ? *persistent_data : new_settings }; + + const auto persist_settings = [&]() -> apply_result_t { + if (current_settings.contains_modifications()) { + if (!persistent_data) { + persistent_data = std::make_unique(new_settings); + } + + if (!save_settings(filepath, *persistent_data)) { + return { apply_result_t::result_e::file_save_fail }; + } + } + else if (persistent_data) { + if (!revert_settings()) { + // Sanity check, as the revert_settings should always pass + // at this point since our settings contain no modifications. + return { apply_result_t::result_e::revert_fail }; + } + } + + return { apply_result_t::result_e::success }; + }; + + // Since we will be modifying system state in multiple steps, we + // have no choice, but to save any changes we have made so + // that we can undo them if anything fails. + auto save_guard = util::fail_guard([&]() { + persist_settings(); // Ignoring the return value + }); + + // Here each of the handler returns full set of their specific settings for + // all the displays in the topology. + // + // We have the same train of though here as with the topology - if we are + // controlling some parts of the display settings, we are taking what + // we have before any modification by us are sticking with it until we + // release the control. + // + // Also, since we keep settings for all the displays (not only the ones that + // we modify), we can use these settings as a base that will revert whatever + // we did before if we are re-applying settings with different configuration. + // + // User modified the resolution manually? Well, he shouldn't have. If we + // are responsible for the resolution, then hands off! Initial settings + // will be re-applied when the paused session is resumed. + + const auto original_primary_display { handle_primary_display_configuration(config.device_prep, current_settings.original_primary_display, topology_result->metadata) }; + if (!original_primary_display) { + // Error already logged + return { apply_result_t::result_e::primary_display_fail }; + } + current_settings.original_primary_display = *original_primary_display; + + const auto original_modes { handle_display_mode_configuration(config.resolution, config.refresh_rate, current_settings.original_modes, topology_result->metadata) }; + if (!original_modes) { + // Error already logged + return { apply_result_t::result_e::modes_fail }; + } + current_settings.original_modes = *original_modes; + + const auto original_hdr_states { handle_hdr_state_configuration(config.change_hdr_state, current_settings.original_hdr_states, topology_result->metadata) }; + if (!original_hdr_states) { + // Error already logged + return { apply_result_t::result_e::hdr_states_fail }; + } + current_settings.original_hdr_states = *original_hdr_states; + + save_guard.disable(); + return persist_settings(); + } + +} // 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..69b3a4ae6a9 --- /dev/null +++ b/src/platform/windows/display_device/settings_topology.cpp @@ -0,0 +1,277 @@ +// local includes +#include "settings_topology.h" +#include "src/display_device/to_string.h" +#include "src/logging.h" + +namespace display_device { + + namespace { + + /** + * @brief Enumerate and get one of the devices matching the id or + * any of the primary devices if id is unspecified. + * @param device_id Id to find in enumerated devices. + * @return Device id, or empty string if an error has occurred. + * + * EXAMPLES: + * ```cpp + * const std::string primary_device = find_one_of_the_available_devices(""); + * const std::string id_that_matches_provided_id = find_one_of_the_available_devices(primary_device); + * ``` + */ + 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; + } + + /** + * @brief Get all device ids that belong in the same group as provided ids (duplicated displays). + * @param device_id Device id to search for in the topology. + * @param topology Topology to search. + * @return A list of device ids, with the provided device id always at the front. + * + * EXAMPLES: + * ```cpp + * const auto duplicated_devices = get_duplicate_devices("MY_DEVICE_ID", get_current_topology()); + * ``` + */ + std::vector + get_duplicate_devices(const std::string &device_id, const active_topology_t &topology) { + std::vector duplicated_devices; + + duplicated_devices.clear(); + duplicated_devices.push_back(device_id); + + for (const auto &group : 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; + } + + /** + * @brief Check if device id is found in the active topology. + * @param device_id Device id to search for in the topology. + * @param topology Topology to search. + * @return True if device id is in the topology, false otherwise. + * + * EXAMPLES: + * ```cpp + * const bool is_in_topology = is_device_found_in_active_topology("MY_DEVICE_ID", get_current_topology()); + * ``` + */ + bool + is_device_found_in_active_topology(const std::string &device_id, const active_topology_t &topology) { + for (const auto &group : topology) { + for (const auto &group_device_id : group) { + if (device_id == group_device_id) { + return true; + } + } + } + + return false; + } + + /** + * @brief Compute the final topology based on the information we have. + * @param device_prep The device preparation setting from user configuration. + * @param primary_device_requested Indicates that the user did NOT specify device id to be used. + * @param duplicated_devices Devices that we need to handle. + * @param topology The current topology that we are evaluating. + * @return Topology that matches requirements and should be set. + */ + active_topology_t + determine_final_topology(parsed_config_t::device_prep_e device_prep, const bool primary_device_requested, const std::vector &duplicated_devices, const active_topology_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 (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(), topology)) { + // Device is currently active in the active topology group + + if (duplicated_devices.size() > 1 || 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() } }; + } + } + } + // device_prep_e::ensure_active || device_prep_e::ensure_primary + else { + // The device needs to be active at least. + + if (primary_device_requested || is_device_found_in_active_topology(duplicated_devices.front(), 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 = topology; + final_topology->push_back({ duplicated_devices.front() }); + } + } + } + + return final_topology ? *final_topology : 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, const 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; + } + + // 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 the final topology has changed somehow. + if (previously_configured_topology) { + // Here we are pretending to be in an initial topology and want to perform reevaluation in case the + // user has changed the settings while the stream was paused. For the proper "evaluation" order, + // see logic outside this conditional. + const auto prev_duplicated_devices { get_duplicate_devices(requested_device_id, previously_configured_topology->initial) }; + const auto prev_final_topology { determine_final_topology(config.device_prep, primary_device_requested, prev_duplicated_devices, previously_configured_topology->initial) }; + + // There is also an edge case where we can have a different number of primary duplicated devices, which wasn't the case + // during the initial topology configuration. If the user requested to use the primary device, + // the prev_final_topology would not reflect that change in primary duplicated devices. Therefore, we also need + // to evaluate current topology (which would have the new state of primary devices) and arrive at the + // same final topology as the prev_final_topology. + const auto current_topology { get_current_topology() }; + const auto duplicated_devices { get_duplicate_devices(requested_device_id, current_topology) }; + const auto final_topology { determine_final_topology(config.device_prep, primary_device_requested, duplicated_devices, current_topology) }; + + // If the topology we are switching to is the same as the final topology we had before, that means + // user did not change anything, and we don't need to revert changes. + if (!is_topology_the_same(previously_configured_topology->modified, prev_final_topology) || + !is_topology_the_same(previously_configured_topology->modified, final_topology)) { + BOOST_LOG(warning) << "previous topology does not match the new one. Reverting previous changes!"; + if (!revert_settings()) { + return boost::none; + } + } + } + + // Regardless of whether the user has made any changes to the user configuration or not, we always + // need to evaluate the current topology and perform the switch if needed as the user might + // have been playing around with active displays while the stream was paused. + + const auto current_topology { get_current_topology() }; + if (!is_topology_valid(current_topology)) { + BOOST_LOG(error) << "display topology is invalid!"; + return boost::none; + } + BOOST_LOG(debug) << "current display topology: " << to_string(current_topology); + + // 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) }; + const auto 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(debug) << "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 have duplicate displays, 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 that 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_result_t { + topology_pair_t { + 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..4879b3423f5 --- /dev/null +++ b/src/platform/windows/display_device/settings_topology.h @@ -0,0 +1,88 @@ +#pragma once + +// local includes +#include "src/display_device/settings.h" + +namespace display_device { + + /** + * @brief Contains metadata about the current topology. + */ + struct topology_metadata_t { + active_topology_t current_topology; /**< The currently active topology. */ + std::unordered_set newly_enabled_devices; /**< A list of device ids that were newly enabled after changing topology. */ + bool primary_device_requested; /**< Indicates that the user did NOT specify device id to be used. */ + std::vector duplicated_devices; /**< A list of devices id that we need to handle. If user specified device id, it will always be the first entry. */ + }; + + /** + * @brief Container for active topologies. + * @note Both topologies can be the same. + */ + struct topology_pair_t { + active_topology_t initial; /**< The initial topology that we had before we switched. */ + active_topology_t modified; /**< The topology that we have modified. */ + + // For JSON serialization + NLOHMANN_DEFINE_TYPE_INTRUSIVE(topology_pair_t, initial, modified) + }; + + /** + * @brief Contains the result after handling the configuration. + * @see handle_device_topology_configuration + */ + struct handled_topology_result_t { + topology_pair_t pair; + topology_metadata_t metadata; + }; + + /** + * @brief Get all ids from the active topology structure. + * @param topology Topology to get ids from. + * @returns A list of device ids. + * + * EXAMPLES: + * ```cpp + * const auto device_ids = get_device_ids_from_topology(get_current_topology()); + * ``` + */ + std::unordered_set + get_device_ids_from_topology(const active_topology_t &topology); + + /** + * @brief Get new device ids that were not present in previous topology. + * @param previous_topology The previous topology. + * @param new_topology A new topology. + * @return A list of devices ids. + * + * EXAMPLES: + * ```cpp + * active_topology_t old_topology { { "ID_1" } }; + * active_topology_t new_topology { { "ID_1" }, { "ID_2" } }; + * const auto device_ids = get_newly_enabled_devices_from_topology(old_topology, new_topology); + * // device_ids contains "ID_2" + * ``` + */ + std::unordered_set + get_newly_enabled_devices_from_topology(const active_topology_t &previous_topology, const active_topology_t &new_topology); + + /** + * @brief Modify the topology based on the configuration and previously configured topology. + * + * The function performs the necessary steps for changing topology if needed. + * It evaluates the 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 + * parameter will be called to completely revert all changes. + * + * @param config Configuration to be evaluated. + * @param previously_configured_topology A result from a earlier call of this function. + * @param revert_settings A function-proxy that can be used to revert all of the changes made to the device displays. + * @return A result object, or an empty optional if the function fails. + */ + boost::optional + handle_device_topology_configuration(const parsed_config_t &config, const 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..cc5320fe070 --- /dev/null +++ b/src/platform/windows/display_device/windows_utils.cpp @@ -0,0 +1,602 @@ +// lib includes +#include +#include +#include +#include + +// standard includes +#include +#include + +// local includes +#include "src/logging.h" +#include "src/platform/windows/misc.h" +#include "src/utility.h" +#include "windows_utils.h" + +// Windows includes after "windows.h" +#include + +namespace display_device::w_utils { + + namespace { + + /** + * @see get_monitor_device_path description for more information as this + * function is identical except that it returns wide-string instead + * of a normal one. + */ + std::wstring + get_monitor_device_path_wstr(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_error_string(result) << " failed to get target device name!"; + return {}; + } + + return std::wstring { target_name.monitorDevicePath }; + } + + /** + * @brief Helper method for dealing with SetupAPI. + * @returns True if device interface path was retrieved and is non-empty, false otherwise. + * @see get_device_id implementation for more context regarding this madness. + */ + bool + get_device_interface_detail(HDEVINFO dev_info_handle, SP_DEVICE_INTERFACE_DATA &dev_interface_data, std::wstring &dev_interface_path, SP_DEVINFO_DATA &dev_info_data) { + DWORD required_size_in_bytes { 0 }; + if (SetupDiGetDeviceInterfaceDetailW(dev_info_handle, &dev_interface_data, nullptr, 0, &required_size_in_bytes, nullptr)) { + BOOST_LOG(error) << "\"SetupDiGetDeviceInterfaceDetailW\" did not fail, what?!"; + return false; + } + else if (required_size_in_bytes <= 0) { + BOOST_LOG(error) << get_error_string(static_cast(GetLastError())) << " \"SetupDiGetDeviceInterfaceDetailW\" failed while getting size."; + return false; + } + + std::vector buffer; + buffer.resize(required_size_in_bytes); + + // This part is just EVIL! + auto detail_data { reinterpret_cast(buffer.data()) }; + detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W); + + if (!SetupDiGetDeviceInterfaceDetailW(dev_info_handle, &dev_interface_data, detail_data, required_size_in_bytes, nullptr, &dev_info_data)) { + BOOST_LOG(error) << get_error_string(static_cast(GetLastError())) << " \"SetupDiGetDeviceInterfaceDetailW\" failed."; + return false; + } + + dev_interface_path = std::wstring { detail_data->DevicePath }; + return !dev_interface_path.empty(); + } + + /** + * @brief Helper method for dealing with SetupAPI. + * @returns True if instance id was retrieved and is non-empty, false otherwise. + * @see get_device_id implementation for more context regarding this madness. + */ + bool + get_device_instance_id(HDEVINFO dev_info_handle, SP_DEVINFO_DATA &dev_info_data, std::wstring &instance_id) { + DWORD required_size_in_characters { 0 }; + if (SetupDiGetDeviceInstanceIdW(dev_info_handle, &dev_info_data, nullptr, 0, &required_size_in_characters)) { + BOOST_LOG(error) << "\"SetupDiGetDeviceInstanceIdW\" did not fail, what?!"; + return false; + } + else if (required_size_in_characters <= 0) { + BOOST_LOG(error) << get_error_string(static_cast(GetLastError())) << " \"SetupDiGetDeviceInstanceIdW\" failed while getting size."; + return false; + } + + instance_id.resize(required_size_in_characters); + if (!SetupDiGetDeviceInstanceIdW(dev_info_handle, &dev_info_data, instance_id.data(), instance_id.size(), nullptr)) { + BOOST_LOG(error) << get_error_string(static_cast(GetLastError())) << " \"SetupDiGetDeviceInstanceIdW\" failed."; + return false; + } + + return !instance_id.empty(); + } + + /** + * @brief Helper method for dealing with SetupAPI. + * @returns True if EDID was retrieved and is non-empty, false otherwise. + * @see get_device_id implementation for more context regarding this madness. + */ + bool + get_device_edid(HDEVINFO dev_info_handle, SP_DEVINFO_DATA &dev_info_data, std::vector &edid) { + // We could just directly open the registry key as the path is known, but we can also use the this + HKEY reg_key { SetupDiOpenDevRegKey(dev_info_handle, &dev_info_data, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ) }; + if (reg_key == INVALID_HANDLE_VALUE) { + BOOST_LOG(error) << get_error_string(static_cast(GetLastError())) << " \"SetupDiOpenDevRegKey\" failed."; + return false; + } + + const auto reg_key_cleanup { + util::fail_guard([®_key]() { + const auto status { RegCloseKey(reg_key) }; + if (status != ERROR_SUCCESS) { + BOOST_LOG(error) << get_error_string(status) << " \"RegCloseKey\" failed."; + } + }) + }; + + DWORD required_size_in_bytes { 0 }; + auto status { RegQueryValueExW(reg_key, L"EDID", nullptr, nullptr, nullptr, &required_size_in_bytes) }; + if (status != ERROR_SUCCESS) { + BOOST_LOG(error) << get_error_string(status) << " \"RegQueryValueExW\" failed when getting size."; + return false; + } + + edid.resize(required_size_in_bytes); + + status = RegQueryValueExW(reg_key, L"EDID", nullptr, nullptr, edid.data(), &required_size_in_bytes); + if (status != ERROR_SUCCESS) { + BOOST_LOG(error) << get_error_string(status) << " \"RegQueryValueExW\" failed when getting size."; + return false; + } + + return !edid.empty(); + } + + } // namespace + + std::string + get_error_string(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(error_code)) << "]"; + return error.str(); + } + + bool + is_primary(const DISPLAYCONFIG_SOURCE_MODE &mode) { + return mode.position.x == 0 && mode.position.y == 0; + } + + bool + are_modes_duplicated(const DISPLAYCONFIG_SOURCE_MODE &mode_a, const DISPLAYCONFIG_SOURCE_MODE &mode_b) { + return mode_a.position.x == mode_b.position.x && mode_a.position.y == mode_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; + } + + std::string + get_device_id(const DISPLAYCONFIG_PATH_INFO &path) { + const auto device_path { get_monitor_device_path_wstr(path) }; + if (device_path.empty()) { + // Error already logged + return {}; + } + + static const GUID monitor_guid { 0xe6f07b5f, 0xee97, 0x4a90, { 0xb0, 0x76, 0x33, 0xf5, 0x7b, 0xf4, 0xea, 0xa7 } }; + std::vector device_id_data; + + HDEVINFO dev_info_handle { SetupDiGetClassDevsW(&monitor_guid, nullptr, nullptr, DIGCF_DEVICEINTERFACE) }; + if (dev_info_handle) { + const auto dev_info_handle_cleanup { + util::fail_guard([&dev_info_handle]() { + if (!SetupDiDestroyDeviceInfoList(dev_info_handle)) { + BOOST_LOG(error) << get_error_string(static_cast(GetLastError())) << " \"SetupDiDestroyDeviceInfoList\" failed."; + } + }) + }; + + SP_DEVICE_INTERFACE_DATA dev_interface_data {}; + dev_interface_data.cbSize = sizeof(dev_interface_data); + for (DWORD monitor_index = 0;; ++monitor_index) { + if (!SetupDiEnumDeviceInterfaces(dev_info_handle, nullptr, &monitor_guid, monitor_index, &dev_interface_data)) { + const DWORD error_code { GetLastError() }; + if (error_code == ERROR_NO_MORE_ITEMS) { + break; + } + + BOOST_LOG(warning) << get_error_string(static_cast(error_code)) << " \"SetupDiEnumDeviceInterfaces\" failed."; + continue; + } + + std::wstring dev_interface_path; + SP_DEVINFO_DATA dev_info_data {}; + dev_info_data.cbSize = sizeof(dev_info_data); + if (!get_device_interface_detail(dev_info_handle, dev_interface_data, dev_interface_path, dev_info_data)) { + // Error already logged + continue; + } + + if (!boost::iequals(dev_interface_path, device_path)) { + continue; + } + + // Instance ID is unique in the system and persists restarts, but not driver re-installs. + // It looks like this: + // DISPLAY\ACI27EC\5&4FD2DE4&5&UID4352 (also used in the device path it seems) + // a b c d e + // + // a) Hardware ID - stable + // b) Either a bus number or has something to do with device capabilities - stable + // c) Another ID, somehow tied to adapter (not an adapter ID from path object) - stable + // d) Some sort of rotating counter thing, changes after driver reinstall - unstable + // e) Seems to be the same as a target ID from path, it changes based on GPU port - semi-stable + // + // The instance ID also seems to be a part of the registry key (in case some other info is needed in the future): + // HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\DISPLAY\ACI27EC\5&4fd2de4&5&UID4352 + + std::wstring instance_id; + if (!get_device_instance_id(dev_info_handle, dev_info_data, instance_id)) { + // Error already logged + break; + } + + if (!get_device_edid(dev_info_handle, dev_info_data, device_id_data)) { + // Error already logged + break; + } + + // We are going to discard the unstable parts of the instance ID and merge the stable parts with the edid buffer (if available) + auto unstable_part_index = instance_id.find_first_of(L'&', 0); + if (unstable_part_index != std::wstring::npos) { + unstable_part_index = instance_id.find_first_of(L'&', unstable_part_index + 1); + } + + if (unstable_part_index == std::wstring::npos) { + BOOST_LOG(error) << "failed to split off the stable part from instance id string " << platf::to_utf8(instance_id); + break; + } + + auto semi_stable_part_index = instance_id.find_first_of(L'&', unstable_part_index + 1); + if (semi_stable_part_index == std::wstring::npos) { + BOOST_LOG(error) << "failed to split off the semi-stable part from instance id string " << platf::to_utf8(instance_id); + break; + } + + BOOST_LOG(verbose) << "creating device id for path " << platf::to_utf8(device_path) << " from EDID and instance ID: " << platf::to_utf8({ std::begin(instance_id), std::begin(instance_id) + unstable_part_index }) << platf::to_utf8({ std::begin(instance_id) + semi_stable_part_index, std::end(instance_id) }); + device_id_data.insert(std::end(device_id_data), + reinterpret_cast(instance_id.data()), + reinterpret_cast(instance_id.data() + unstable_part_index)); + device_id_data.insert(std::end(device_id_data), + reinterpret_cast(instance_id.data() + semi_stable_part_index), + reinterpret_cast(instance_id.data() + instance_id.size())); + break; + } + } + + if (device_id_data.empty()) { + // Using the device path as a fallback, which is always unique, but not as stable as the preferred one + BOOST_LOG(verbose) << "creating device id from path " << platf::to_utf8(device_path); + device_id_data.insert(std::end(device_id_data), + reinterpret_cast(device_path.data()), + reinterpret_cast(device_path.data() + device_path.size())); + } + + static constexpr boost::uuids::uuid ns_id {}; // null namespace = no salt + const auto boost_uuid { boost::uuids::name_generator_sha1 { ns_id }(device_id_data.data(), device_id_data.size()) }; + return "{" + boost::uuids::to_string(boost_uuid) + "}"; + } + + std::string + get_monitor_device_path(const DISPLAYCONFIG_PATH_INFO &path) { + return platf::to_utf8(get_monitor_device_path_wstr(path)); + } + + 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_error_string(result) << " failed to get target device name!"; + return {}; + } + + return target_name.flags.friendlyNameFromEdid ? platf::to_utf8(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_error_string(result) << " failed to get display name! "; + return {}; + } + + return platf::to_utf8(source_name.viewGdiDeviceName); + } + + hdr_state_e + get_hdr_state(const DISPLAYCONFIG_PATH_INFO &path) { + if (!is_active(path)) { + // Checking if active to suppress the error message below. + 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_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_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; + } + + 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 &id) { + if (path.flags & DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE) { + if (id) { + path.sourceInfo.cloneGroupId = *id; + } + 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; + } + + if (*index >= modes.size()) { + BOOST_LOG(error) << "source index " << *index << " is out of range " << modes.size(); + 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))); + } + + boost::optional + get_device_info_for_valid_path(const DISPLAYCONFIG_PATH_INFO &path, bool must_be_active) { + if (!is_available(path)) { + // Could be transient issue according to MSDOCS (no longer available, but still "active") + return boost::none; + } + + if (must_be_active) { + if (!is_active(path)) { + return boost::none; + } + } + + const auto device_path { get_monitor_device_path(path) }; + if (device_path.empty()) { + return boost::none; + } + + const auto device_id { get_device_id(path) }; + if (device_id.empty()) { + return boost::none; + } + + const auto display_name { get_display_name(path) }; + if (display_name.empty()) { + return boost::none; + } + + return device_info_t { device_path, device_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_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_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 device_info { get_device_info_for_valid_path(path, ACTIVE_ONLY_DEVICES) }; + if (!device_info) { + continue; + } + + if (device_info->device_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 display_device::w_utils 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..a75b0c50381 --- /dev/null +++ b/src/platform/windows/display_device/windows_utils.h @@ -0,0 +1,486 @@ +#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::w_utils { + + constexpr bool ACTIVE_ONLY_DEVICES { true }; /**< The device path must be active. */ + constexpr bool ALL_DEVICES { false }; /**< The device path can be active or inactive. */ + + /** + * @brief Contains currently available paths and associated modes. + */ + struct path_and_mode_data_t { + std::vector paths; /**< Available display paths. */ + std::vector modes; /**< Display modes for ACTIVE displays. */ + }; + + /** + * @brief Contains the device path and the id for a VALID device. + * @see get_device_info_for_valid_path for what is considered a valid device. + * @see get_device_id for how we make the device id. + */ + struct device_info_t { + std::string device_path; /**< Unique device path string. */ + std::string device_id; /**< A device id (made up by us) that is identifies the device. */ + }; + + /** + * @brief Stringify the error code from Windows API. + * @param error_code Error code to stringify. + * @returns String containing the error code in a readable format + a system message describing the code. + * + * EXAMPLES: + * ```cpp + * const std::string error_message = get_error_string(ERROR_NOT_SUPPORTED); + * ``` + */ + std::string + get_error_string(LONG error_code); + + /** + * @brief Check if the display's source mode is primary - if the associated device is a primary display device. + * @param mode Mode to check. + * @returns True if the mode's origin point is at (0, 0) coordinate (primary), false otherwise. + * @note It is possible to have multiple primary source modes at the same time. + * @see get_source_mode on how to get the source mode. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_SOURCE_MODE mode; + * const bool is_primary = is_primary(mode); + * ``` + */ + bool + is_primary(const DISPLAYCONFIG_SOURCE_MODE &mode); + + /** + * @brief Check if the source modes are duplicated (cloned). + * @param mode_a First mode to check. + * @param mode_b Second mode to check. + * @returns True if both mode have the same origin point, false otherwise. + * @note Windows enforces the behaviour that only the duplicate devices can + * have the same origin point as otherwise the configuration is considered invalid by the OS. + * @see get_source_mode on how to get the source mode. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_SOURCE_MODE mode_a; + * DISPLAYCONFIG_SOURCE_MODE mode_b; + * const bool are_duplicated = are_modes_duplicated(mode_a, mode_b); + * ``` + */ + bool + are_modes_duplicated(const DISPLAYCONFIG_SOURCE_MODE &mode_a, const DISPLAYCONFIG_SOURCE_MODE &mode_b); + + /** + * @brief Check if the display device path's target is available. + * + * In most cases this this would mean physically connected to the system, + * but it also possible force the path to persist. It is not clear if it be + * counted as available or not. + * + * @param path Path to check. + * @returns True if path's target is marked as available, false otherwise. + * @see query_display_config on how to get paths from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const bool available = is_available(path); + * ``` + */ + bool + is_available(const DISPLAYCONFIG_PATH_INFO &path); + + /** + * @brief Check if the display device path is marked as active. + * @param path Path to check. + * @returns True if path is marked as active, false otherwise. + * @see query_display_config on how to get paths from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const bool active = is_active(path); + * ``` + */ + bool + is_active(const DISPLAYCONFIG_PATH_INFO &path); + + /** + * @brief Mark the display device path as active. + * @param path Path to mark. + * @see query_display_config on how to get paths from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * if (!is_active(path)) { + * set_active(path); + * } + * ``` + */ + void + set_active(DISPLAYCONFIG_PATH_INFO &path); + + /** + * @brief Get a stable and persistent device id for the path. + * + * This function tries to generate a unique id for the path that + * is persistent between driver re-installs and physical unplugging and + * replugging of the device. + * + * The best candidate for it could have been a "ContainerID" from the + * registry, however it was found to be unstable for the virtual display + * (probably because it uses the EDID for the id generation and the current + * virtual displays have incomplete EDID information). The "ContainerID" + * also does not change if the physical device is plugged into a different + * port and seems to be very stable, however because of virtual displays + * other solution was used. + * + * The accepted solution was to use the "InstanceID" and EDID (just to be + * on the safe side). "InstanceID" is semi-stable, it has some parts that + * change between driver re-installs and it has a part that changes based + * on the GPU port that the display is connected to. It is most likely to + * be unique, but since the MS documentation is lacking we are also hashing + * EDID information (contains serial ids, timestamps and etc. that should + * guarantee that identical displays are differentiated like with the + * "ContainerID"). Most importantly this information is stable for the virtual + * displays. + * + * After we remove the unstable parts from the "InstanceID" and hash everything + * together, we get an id that changes only when you connect the display to + * a different GPU port which seems to be acceptable. + * + * As a fallback we are using a hashed device path, in case the "InstanceID" or + * EDID is not available. At least if you don't do driver re-installs often + * and change the GPU ports, it will be stable for a while. + * + * @param path Path to get the device id for. + * @returns Device id, or an empty string if it could not be generated. + * @see query_display_config on how to get paths from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const std::string device_path = get_device_id(path); + * ``` + */ + std::string + get_device_id(const DISPLAYCONFIG_PATH_INFO &path); + + /** + * @brief Get a string that represents a path from the adapter to the display target. + * @param path Path to get the string for. + * @returns String representation, or an empty string if it's not available. + * @see query_display_config on how to get paths from the system. + * @note In the rest of the code we refer to this string representation simply as the "device path". + * It is used as a simple way of grouping related path objects together and removing "bad" paths + * that don't have such string representation. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const std::string device_path = get_monitor_device_path(path); + * ``` + */ + std::string + get_monitor_device_path(const DISPLAYCONFIG_PATH_INFO &path); + + /** + * @brief Get the user friendly name for the path. + * @param path Path to get user friendly name for. + * @returns User friendly name for the path if available, empty string otherwise. + * @see query_display_config on how to get paths from the system. + * @note This is usually a monitor name (like "ROG PG279Q") and is most likely take from EDID. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const std::string friendly_name = get_friendly_name(path); + * ``` + */ + std::string + get_friendly_name(const DISPLAYCONFIG_PATH_INFO &path); + + /** + * @brief Get the logical display name for the path. + * + * These are the "\\\\.\\DISPLAY1", "\\\\.\\DISPLAY2" and etc. display names that can + * change whenever Windows wants to change them. + * + * @param path Path to get user display name for. + * @returns Display name for the path if available, empty string otherwise. + * @see query_display_config on how to get paths from the system. + * @note Inactive paths can have these names already assigned to them, even + * though they are not even in use! There can also be duplicates. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const std::string display_name = get_display_name(path); + * ``` + */ + std::string + get_display_name(const DISPLAYCONFIG_PATH_INFO &path); + + /** + * @brief Get the HDR state the path. + * @param path Path to get HDR state for. + * @returns hdr_state_e::unknown if the state could not be retrieved, or other enum values describing the state otherwise. + * @see query_display_config on how to get paths from the system. + * @see hdr_state_e + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const auto hdr_state = get_hdr_state(path); + * ``` + */ + hdr_state_e + get_hdr_state(const DISPLAYCONFIG_PATH_INFO &path); + + /** + * @brief Set the HDR state for the path. + * @param path Path to set HDR state for. + * @param enable Specify whether to enable or disable HDR state. + * @returns True if new HDR state was set, false otherwise. + * @see query_display_config on how to get paths from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const bool success = set_hdr_state(path, false); + * ``` + */ + bool + set_hdr_state(const DISPLAYCONFIG_PATH_INFO &path, bool enable); + + /** + * @brief Get the source mode index from the path. + * + * This function correctly retrieves the index from the path based on + * some flags that indicate how to access the union structure containing the index. + * + * It performs sanity checks on the modes list that the index is indeed correct. + * + * @param path Path to get the source mode index for. + * @param modes A list of various modes (source, target, desktop and probably more in the future). + * @returns Valid index value if it's found in the modes list and the mode at that index is of a type "source" mode, + * empty optional otherwise. + * @see query_display_config on how to get paths and modes from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * std::vector modes; + * const auto source_index = get_source_index(path, modes); + * ``` + */ + boost::optional + get_source_index(const DISPLAYCONFIG_PATH_INFO &path, const std::vector &modes); + + /** + * @brief Set the source mode index in the path. + * + * This function correctly sets the index in the path based on + * some flags that indicate how to access the union structure containing the index. + * + * @param path Path to modify. + * @param index Index value to set or empty optional to mark the index as invalid. + * @see query_display_config on how to get paths and modes from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * set_source_index(path, 5); + * set_source_index(path, boost::none); + * ``` + */ + void + set_source_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index); + + /** + * @brief Set the target mode index in the path. + * + * This function correctly sets the index in the path based on + * some flags that indicate how to access the union structure containing the index. + * + * @param path Path to modify. + * @param index Index value to set or empty optional to mark the index as invalid. + * @see query_display_config on how to get paths and modes from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * set_target_index(path, 5); + * set_target_index(path, boost::none); + * ``` + */ + void + set_target_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index); + + /** + * @brief Set the desktop mode index in the path. + * + * This function correctly sets the index in the path based on + * some flags that indicate how to access the union structure containing the index. + * + * @param path Path to modify. + * @param index Index value to set or empty optional to mark the index as invalid. + * @see query_display_config on how to get paths and modes from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * set_desktop_index(path, 5); + * set_desktop_index(path, boost::none); + * ``` + */ + void + set_desktop_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index); + + /** + * @brief Set the clone group id in the path. + * + * This function correctly sets the id in the path based on + * some flags that indicate how to access the union structure containing the id. + * + * @param path Path to modify. + * @param id Id value to set or empty optional to mark the id as invalid. + * @see query_display_config on how to get paths and modes from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * set_clone_group_id(path, 5); + * set_clone_group_id(path, boost::none); + * ``` + */ + void + set_clone_group_id(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &id); + + /** + * @brief Get the source mode from the list at the specified index. + * + * This function does additional sanity checks for the modes list and ensures + * that the mode at the specified index is indeed a source mode. + * + * @param index Index to get the mode for. It is of boost::optional type + * as the function is intended to be used with get_source_index function. + * @param modes List to get the mode from. + * @returns A pointer to a valid source mode from to list at the specified index, nullptr otherwise. + * @see query_display_config on how to get paths and modes from the system. + * @see get_source_index + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const std::vector modes; + * const DISPLAYCONFIG_SOURCE_MODE* source_mode = get_source_mode(get_source_index(path, modes), modes); + * ``` + */ + const DISPLAYCONFIG_SOURCE_MODE * + get_source_mode(const boost::optional &index, const std::vector &modes); + + /** + * @brief Get the source mode from the list at the specified index. + * + * This function does additional sanity checks for the modes list and ensures + * that the mode at the specified index is indeed a source mode. + * + * @param index Index to get the mode for. It is of boost::optional type + * as the function is intended to be used with get_source_index function. + * @param modes List to get the mode from. + * @returns A pointer to a valid source mode from to list at the specified index, nullptr otherwise. + * @see query_display_config on how to get paths and modes from the system. + * @see get_source_index + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * std::vector modes; + * DISPLAYCONFIG_SOURCE_MODE* source_mode = get_source_mode(get_source_index(path, modes), modes); + * ``` + */ + DISPLAYCONFIG_SOURCE_MODE * + get_source_mode(const boost::optional &index, std::vector &modes); + + /** + * @brief Validate the path and get the commonly used information from it. + * + * This a convenience function to ensure that our concept of "valid path" remains the + * same throughout the code. + * + * Currently, for use, a valid path is: + * - a path with and available display target; + * - a path that is active (optional); + * - a path that has a non-empty device path; + * - a path that has a non-empty device id; + * - a path that has a non-empty device name assigned. + * + * @param path Path to validate and get info for. + * @param must_be_active Optionally request that the valid path must also be active. + * @returns Commonly used info for the path, or empty optional if the path is invalid. + * @see query_display_config on how to get paths and modes from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const auto device_info = get_device_info_for_valid_path(path, true); + * ``` + */ + boost::optional + get_device_info_for_valid_path(const DISPLAYCONFIG_PATH_INFO &path, bool must_be_active); + + /** + * @brief Query Windows for the device paths and associated modes. + * @param active_only Specify to query for active devices only. + * @returns Data containing paths and modes, empty optional if we have failed to query. + * + * EXAMPLES: + * ```cpp + * const auto display_data = query_display_config(true); + * ``` + */ + boost::optional + query_display_config(bool active_only); + + /** + * @brief Get the active path matching the device id. + * @param device_id Id to search for in the the list. + * @param paths List to be searched. + * @returns A pointer to an active path matching our id, nullptr otherwise. + * @see query_display_config on how to get paths and modes from the system. + * + * EXAMPLES: + * ```cpp + * const std::vector paths; + * const DISPLAYCONFIG_PATH_INFO* active_path = get_active_path("MY_DEVICE_ID", paths); + * ``` + */ + const DISPLAYCONFIG_PATH_INFO * + get_active_path(const std::string &device_id, const std::vector &paths); + + /** + * @brief Get the active path matching the device id. + * @param device_id Id to search for in the the list. + * @param paths List to be searched. + * @returns A pointer to an active path matching our id, nullptr otherwise. + * @see query_display_config on how to get paths and modes from the system. + * + * EXAMPLES: + * ```cpp + * std::vector paths; + * DISPLAYCONFIG_PATH_INFO* active_path = get_active_path("MY_DEVICE_ID", paths); + * ``` + */ + DISPLAYCONFIG_PATH_INFO * + get_active_path(const std::string &device_id, std::vector &paths); + +} // namespace display_device::w_utils diff --git a/src/process.cpp b/src/process.cpp index 804291577c2..ec718e47743 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -23,6 +23,7 @@ #include "config.h" #include "crypto.h" +#include "display_device/session.h" #include "logging.h" #include "platform/common.h" #include "system_tray.h" @@ -340,16 +341,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 9c146804523..efe84aec66b 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -18,6 +18,7 @@ extern "C" { } #include "config.h" +#include "display_device/session.h" #include "globals.h" #include "input.h" #include "logging.h" @@ -1835,11 +1836,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: make this 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 f786aeb59f0..11f88f2c676 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -19,6 +19,7 @@ extern "C" { #include "cbs.h" #include "config.h" +#include "display_device/display_device.h" #include "globals.h" #include "input.h" #include "logging.h" @@ -1103,6 +1104,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 @@ -1121,7 +1124,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 @@ -1141,7 +1144,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; } @@ -2430,7 +2433,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; } @@ -2560,7 +2564,7 @@ namespace video { av1.videoFormat = 2; // Reset the display since we're switching from SDR to HDR - reset_display(disp, encoder.platform_formats->dev_type, config::video.output_name, config); + reset_display(disp, encoder.platform_formats->dev_type, output_display_name, config); if (!disp) { return false; } diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index a072e101ba0..c6afafe4e15 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -420,14 +420,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: {de9bb7e2-186e-505b-9e93-f48793333810}
+     DISPLAY NAME: \\.\DISPLAY1
+     FRIENDLY NAME: ROG PG279Q
+     DEVICE STATE: PRIMARY
+     HDR STATE: UNKNOWN
@@ -448,6 +454,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 +
+ +
+
+ + +
+ + +
+
+
+
+
+
@@ -1234,6 +1337,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]", }, diff --git a/src_assets/common/assets/web/troubleshooting.html b/src_assets/common/assets/web/troubleshooting.html index 6ad8afc59da..e8a208955f9 100644 --- a/src_assets/common/assets/web/troubleshooting.html +++ b/src_assets/common/assets/web/troubleshooting.html @@ -2,38 +2,38 @@ - <%- header %> - + .copy-icon:active { + color: rgba(0, 0, 0, 1); + } + @@ -81,6 +81,30 @@

Restart Sunshine

+ +
+
+

Reset Persistent Display Device Settings

+
+

+ If Sunshine is stuck trying to restore the changed display device settings, + you can reset the settings and proceed to restore the display state manually. + This could happen for various reasons: device is no longer available, has been plugged to + a different port and so on. +

+
+ Success resetting persistence! +
+
+ Error while resetting persistence! +
+
+ +
+
+
@@ -128,11 +152,14 @@

Logs

}, data() { return { + platform: "", closeAppPressed: false, closeAppStatus: null, unpairAllPressed: false, unpairAllStatus: null, restartPressed: false, + resetDisplayDevicePressed: false, + resetDisplayDeviceStatus: null, logs: 'Loading...', logFilter: null, logInterval: null, @@ -147,6 +174,12 @@

Logs

} }, created() { + fetch("/api/config") + .then((r) => r.json()) + .then((r) => { + this.platform = r.platform; + }); + this.logInterval = setInterval(() => { this.refreshLogs(); }, 5000); @@ -187,6 +220,18 @@

Logs

}, 5000); }); }, + resetDisplayDevicePersistence() { + this.resetDisplayDevicePressed = true; + fetch("/api/reset-display-device-persistence", { method: "POST" }) + .then((r) => r.json()) + .then((r) => { + this.resetDisplayDevicePressed = false; + this.resetDisplayDeviceStatus = r.status.toString() === "true"; + setTimeout(() => { + this.resetDisplayDeviceStatus = null; + }, 5000); + }); + }, copyLogs() { navigator.clipboard.writeText(this.actualLogs); }, @@ -206,4 +251,4 @@

Logs

- + \ No newline at end of file