diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index cd8cfc03f43..de7d140c571 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1067,7 +1067,6 @@ jobs: mingw-w64-ucrt-x86_64-cppwinrt \ mingw-w64-ucrt-x86_64-graphviz \ mingw-w64-ucrt-x86_64-miniupnpc \ - mingw-w64-ucrt-x86_64-nlohmann-json \ mingw-w64-ucrt-x86_64-nodejs \ mingw-w64-ucrt-x86_64-nsis \ mingw-w64-ucrt-x86_64-onevpl \ diff --git a/.gitmodules b/.gitmodules index 4aa76d5206f..5dc72510f81 100644 --- a/.gitmodules +++ b/.gitmodules @@ -26,6 +26,10 @@ path = third-party/nanors url = https://github.com/sleepybishop/nanors.git branch = master +[submodule "third-party/nlohmann_json"] + path = third-party/nlohmann_json + url = https://github.com/nlohmann/json + branch = master [submodule "third-party/nv-codec-headers"] path = third-party/nv-codec-headers url = https://github.com/FFmpeg/nv-codec-headers diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake index e7436825cce..c2071fe036d 100644 --- a/cmake/compile_definitions/common.cmake +++ b/cmake/compile_definitions/common.cmake @@ -56,6 +56,15 @@ set(SUNSHINE_TARGET_FILES "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/RtspParser.c" "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Video.h" "${CMAKE_SOURCE_DIR}/third-party/tray/src/tray.h" + "${CMAKE_SOURCE_DIR}/src/display_device/display_device.h" + "${CMAKE_SOURCE_DIR}/src/display_device/parsed_config.cpp" + "${CMAKE_SOURCE_DIR}/src/display_device/parsed_config.h" + "${CMAKE_SOURCE_DIR}/src/display_device/session.cpp" + "${CMAKE_SOURCE_DIR}/src/display_device/session.h" + "${CMAKE_SOURCE_DIR}/src/display_device/settings.cpp" + "${CMAKE_SOURCE_DIR}/src/display_device/settings.h" + "${CMAKE_SOURCE_DIR}/src/display_device/to_string.cpp" + "${CMAKE_SOURCE_DIR}/src/display_device/to_string.h" "${CMAKE_SOURCE_DIR}/src/upnp.cpp" "${CMAKE_SOURCE_DIR}/src/upnp.h" "${CMAKE_SOURCE_DIR}/src/cbs.cpp" @@ -137,4 +146,5 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${FFMPEG_LIBRARIES} ${Boost_LIBRARIES} ${OPENSSL_LIBRARIES} + ${JSON_LIBRARIES} ${PLATFORM_LIBRARIES}) diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake index e07c2a55d8f..1c86e173183 100644 --- a/cmake/compile_definitions/linux.cmake +++ b/cmake/compile_definitions/linux.cmake @@ -249,6 +249,7 @@ list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/linux/misc.h" "${CMAKE_SOURCE_DIR}/src/platform/linux/misc.cpp" "${CMAKE_SOURCE_DIR}/src/platform/linux/audio.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/linux/display_device.cpp" "${CMAKE_SOURCE_DIR}/third-party/glad/src/egl.c" "${CMAKE_SOURCE_DIR}/third-party/glad/src/gl.c" "${CMAKE_SOURCE_DIR}/third-party/glad/include/EGL/eglplatform.h" diff --git a/cmake/compile_definitions/macos.cmake b/cmake/compile_definitions/macos.cmake index fb33d3bf235..25529a981d8 100644 --- a/cmake/compile_definitions/macos.cmake +++ b/cmake/compile_definitions/macos.cmake @@ -38,6 +38,7 @@ set(PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/macos/av_video.h" "${CMAKE_SOURCE_DIR}/src/platform/macos/av_video.m" "${CMAKE_SOURCE_DIR}/src/platform/macos/display.mm" + "${CMAKE_SOURCE_DIR}/src/platform/macos/display_device.cpp" "${CMAKE_SOURCE_DIR}/src/platform/macos/input.cpp" "${CMAKE_SOURCE_DIR}/src/platform/macos/microphone.mm" "${CMAKE_SOURCE_DIR}/src/platform/macos/misc.mm" diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 7643d1d9efc..8ebe5aa664e 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -52,6 +52,15 @@ set(PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/windows/display_ram.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/display_wgc.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/audio.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/device_hdr_states.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/device_modes.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/device_topology.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/general_functions.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/settings_topology.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/settings_topology.h" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/settings.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/windows_utils.h" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/windows_utils.cpp" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Client.h" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Common.h" @@ -79,7 +88,6 @@ list(PREPEND PLATFORM_LIBRARIES avrt iphlpapi shlwapi - PkgConfig::NLOHMANN_JSON ${CURL_STATIC_LIBRARIES}) if(SUNSHINE_ENABLE_TRAY) diff --git a/cmake/dependencies/common.cmake b/cmake/dependencies/common.cmake index 01065b387e0..6eb71fdc705 100644 --- a/cmake/dependencies/common.cmake +++ b/cmake/dependencies/common.cmake @@ -22,6 +22,15 @@ pkg_check_modules(CURL REQUIRED libcurl) pkg_check_modules(MINIUPNP miniupnpc REQUIRED) include_directories(SYSTEM ${MINIUPNP_INCLUDE_DIRS}) +# nlohmann_json +if(SUNSHINE_SYSTEM_NLOHMANN_JSON) + pkg_check_modules(NLOHMANN_JSON nlohmann_json>=3.9.0 REQUIRED IMPORTED_TARGET) + set(JSON_LIBRARIES PkgConfig::NLOHMANN_JSON) +else() + add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/nlohmann_json") + set(JSON_LIBRARIES nlohmann_json::nlohmann_json) +endif() + # ffmpeg pre-compiled binaries if(NOT DEFINED FFMPEG_PREPARED_BINARIES) if(WIN32) diff --git a/cmake/dependencies/windows.cmake b/cmake/dependencies/windows.cmake index 0563e567817..a392ed2d64d 100644 --- a/cmake/dependencies/windows.cmake +++ b/cmake/dependencies/windows.cmake @@ -1,4 +1 @@ # windows specific dependencies - -# nlohmann_json -pkg_check_modules(NLOHMANN_JSON nlohmann_json REQUIRED IMPORTED_TARGET) diff --git a/cmake/prep/options.cmake b/cmake/prep/options.cmake index f358f7273fe..9a4c5da34cf 100644 --- a/cmake/prep/options.cmake +++ b/cmake/prep/options.cmake @@ -14,6 +14,7 @@ option(SUNSHINE_CONFIGURE_ONLY "Configure special files only, then exit." OFF) option(SUNSHINE_ENABLE_TRAY "Enable system tray icon. This option will be ignored on macOS." ON) option(SUNSHINE_REQUIRE_TRAY "Require system tray icon. Fail the build if tray requirements are not met." ON) +option(SUNSHINE_SYSTEM_NLOHMANN_JSON "Use system installation of nlohmann_json rather than the submodule." OFF) option(SUNSHINE_SYSTEM_WAYLAND_PROTOCOLS "Use system installation of wayland-protocols rather than the submodule." OFF) if(APPLE) diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst index 95025b91358..80154616808 100644 --- a/docs/source/about/advanced_usage.rst +++ b/docs/source/about/advanced_usage.rst @@ -586,7 +586,7 @@ keybindings ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - Select the display number you want to stream. + Select the display you want to stream. .. tip:: To find the name of the appropriate values follow these instructions. @@ -616,9 +616,35 @@ keybindings You need to use the id value inside the parenthesis, e.g. ``3``. **Windows** - .. code-block:: batch + During Sunshine startup, you should see the list of detected display devices: - tools\dxgi-info.exe + .. code-block:: text + + DEVICE ID: {de9bb7e2-186e-505b-9e93-f48793333810} + DISPLAY NAME: \\.\DISPLAY1 + FRIENDLY NAME: ROG PG279Q + DEVICE STATE: PRIMARY + HDR STATE: UNKNOWN + ----------------------- + DEVICE ID: {3bd008cd-0465-547c-8da5-c28749c041e6} + DISPLAY NAME: NOT AVAILABLE + FRIENDLY NAME: IDD HDR + DEVICE STATE: INACTIVE + HDR STATE: UNKNOWN + ----------------------- + DEVICE ID: {77f67f3e-754f-5d31-af64-ee037e18100a} + DISPLAY NAME: NOT AVAILABLE + FRIENDLY NAME: SunshineHDR + DEVICE STATE: INACTIVE + HDR STATE: UNKNOWN + ----------------------- + DEVICE ID: {bc172e6d-86eb-5851-aeca-56525ed716e9} + DISPLAY NAME: NOT AVAILABLE + FRIENDLY NAME: ROG PG279Q + DEVICE STATE: INACTIVE + HDR STATE: UNKNOWN + + You need to use the ``DEVICE ID`` value. **Default** Sunshine will select the default display. @@ -637,7 +663,7 @@ keybindings **Windows** .. code-block:: text - output_name = \\.\DISPLAY1 + output_name = {de9bb7e2-186e-505b-9e93-f48793333810} `resolutions `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/building/windows.rst b/docs/source/building/windows.rst index 76cf6ac3ffb..9bce771c1b4 100644 --- a/docs/source/building/windows.rst +++ b/docs/source/building/windows.rst @@ -22,7 +22,6 @@ Install dependencies: mingw-w64-ucrt-x86_64-curl \ mingw-w64-ucrt-x86_64-graphviz \ mingw-w64-ucrt-x86_64-miniupnpc \ - mingw-w64-ucrt-x86_64-nlohmann-json \ mingw-w64-ucrt-x86_64-nodejs \ mingw-w64-ucrt-x86_64-nsis \ mingw-w64-ucrt-x86_64-onevpl \ diff --git a/src/audio.cpp b/src/audio.cpp index ac1947fec74..2251dc084c9 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 @@ -95,8 +85,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); @@ -149,7 +137,7 @@ namespace audio { apply_surround_params(stream, config.customStreamParams); } - auto ref = control_shared.ref(); + auto ref = get_audio_ctx_ref(); if (!ref) { return; } @@ -255,6 +243,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 6d04d242b4c..5a4027fd946 100644 --- a/src/audio.h +++ b/src/audio.h @@ -4,8 +4,11 @@ */ #pragma once +// local includes +#include "platform/common.h" #include "thread_safe.h" #include "utility.h" + namespace audio { enum stream_config_e : int { STEREO, ///< Stereo @@ -52,8 +55,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 7f5888633ae..e76831c9779 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -24,6 +24,7 @@ #include "rtsp.h" #include "utility.h" +#include "display_device/parsed_config.h" #include "platform/common.h" #ifdef _WIN32 @@ -377,7 +378,15 @@ 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 + {} // display_mode_remapping }; audio_t audio { @@ -829,6 +838,40 @@ namespace config { } } + void + list_display_mode_remapping_f(std::unordered_map &vars, const std::string &name, std::vector &input) { + std::string string; + string_f(vars, name, string); + + std::stringstream jsonStream; + + // check if string is empty, i.e. when the value doesn't exist in the config file + if (string.empty()) { + return; + } + + // We need to add a wrapping object to make it valid JSON, otherwise ptree cannot parse it. + jsonStream << "{\"display_mode_remapping\":" << string << "}"; + + boost::property_tree::ptree jsonTree; + boost::property_tree::read_json(jsonStream, jsonTree); + + for (auto &[_, entry] : jsonTree.get_child("display_mode_remapping"s)) { + auto type = entry.get_optional("type"s); + auto received_resolution = entry.get_optional("received_resolution"s); + auto received_fps = entry.get_optional("received_fps"s); + auto final_resolution = entry.get_optional("final_resolution"s); + auto final_refresh_rate = entry.get_optional("final_refresh_rate"s); + + input.push_back(video_t::display_mode_remapping_t { + type.value_or(""), + received_resolution.value_or(""), + received_fps.value_or(""), + final_resolution.value_or(""), + final_refresh_rate.value_or("") }); + } + } + void list_prep_cmd_f(std::unordered_map &vars, const std::string &name, std::vector &input) { std::string string; @@ -961,6 +1004,7 @@ namespace config { } int_f(vars, "qp", video.qp); + int_between_f(vars, "min_fps_factor", video.min_fps_factor, { 1, 3 }); int_f(vars, "min_threads", video.min_threads); int_between_f(vars, "hevc_mode", video.hevc_mode, { 0, 3 }); int_between_f(vars, "av1_mode", video.av1_mode, { 0, 3 }); @@ -1030,8 +1074,15 @@ 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_between_f(vars, "min_fps_factor", video.min_fps_factor, { 1, 3 }); + 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); + list_display_mode_remapping_f(vars, "display_mode_remapping", video.display_mode_remapping); + 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 d2c4783ce93..076fda156bc 100644 --- a/src/config.h +++ b/src/config.h @@ -74,7 +74,23 @@ namespace config { std::string capture; std::string encoder; std::string adapter_name; + + struct display_mode_remapping_t { + std::string type; + std::string received_resolution; + std::string received_fps; + std::string final_resolution; + std::string final_refresh_rate; + }; + 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; + std::vector display_mode_remapping; }; struct audio_t { diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 6d80220612c..cf73d9a4069 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -28,6 +28,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" @@ -617,6 +618,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; @@ -821,6 +839,7 @@ namespace confighttp { server.resource["^/api/config$"]["POST"] = saveConfig; server.resource["^/api/configLocale$"]["GET"] = getLocale; 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-all$"]["POST"] = unpairAll; diff --git a/src/display_device/display_device.h b/src/display_device/display_device.h new file mode 100644 index 00000000000..0ed586c29b8 --- /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..915be8873c5 --- /dev/null +++ b/src/display_device/parsed_config.cpp @@ -0,0 +1,561 @@ +// lib includes +#include +#include +#include + +// local includes +#include "parsed_config.h" +#include "src/config.h" +#include "src/logging.h" +#include "src/rtsp.h" +#include "to_string.h" + +namespace display_device { + + namespace { + /** + * @brief Parse resolution value from the string. + * @param input String to be parsed. + * @param output Reference to output variable. + * @returns True on successful parsing (empty string allowed), false otherwise. + * + * EXAMPLES: + * ```cpp + * boost::optional resolution; + * if (parse_resolution_string("1920x1080", resolution)) { + * if (resolution) { + * // Value was specified + * } + * else { + * // Value was empty + * } + * } + * ``` + */ + bool + parse_resolution_string(const std::string &input, boost::optional &output) { + const std::string trimmed_input { boost::algorithm::trim_copy(input) }; + 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_input, match, resolution_regex)) { + try { + output = 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 resolution string " << trimmed_input << " (invalid argument):\n" + << err.what(); + return false; + } + catch (const std::out_of_range &err) { + BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << " (number out of range):\n" + << err.what(); + return false; + } + catch (const std::exception &err) { + BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << ":\n" + << err.what(); + return false; + } + } + else { + output = boost::none; + + if (!trimmed_input.empty()) { + BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << ". It must match a \"1920x1080\" pattern!"; + return false; + } + } + + return true; + } + + /** + * @brief Parse refresh rate value from the string. + * @param input String to be parsed. + * @param output Reference to output variable. + * @param allow_decimal_point Specify whether the decimal point is allowed in the string. + * @returns True on successful parsing (empty string allowed), false otherwise. + * + * EXAMPLES: + * ```cpp + * boost::optional refresh_rate; + * if (parse_refresh_rate_string("59.95", refresh_rate)) { + * if (refresh_rate) { + * // Value was specified + * } + * else { + * // Value was empty + * } + * } + * ``` + */ + bool + parse_refresh_rate_string(const std::string &input, boost::optional &output, bool allow_decimal_point = true) { + const std::string trimmed_input { boost::algorithm::trim_copy(input) }; + // std::regex hangs in CTOR for some reason when called in a thread. Problem with MSYS2 packages (UCRT64), maybe? + const boost::regex refresh_rate_regex { allow_decimal_point ? R"(^(\d+)(?:\.(\d+))?$)" : R"(^(\d+)$)" }; + + boost::smatch match; + if (boost::regex_match(trimmed_input, match, refresh_rate_regex)) { + try { + if (allow_decimal_point && 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 are 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))) }; + + output = refresh_rate_t { numerator, denominator }; + } + else { + // We do not have a decimal point, just a valid number. + // For example: + // 60: + // numerator = 60 + // denominator = 1 + output = refresh_rate_t { static_cast(std::stol(match[1])), 1 }; + } + } + catch (const std::invalid_argument &err) { + BOOST_LOG(error) << "Failed to parse refresh rate or FPS string " << trimmed_input << " (invalid argument):\n" + << err.what(); + return false; + } + catch (const std::out_of_range &err) { + BOOST_LOG(error) << "Failed to parse refresh rate or FPS string " << trimmed_input << " (number out of range):\n" + << err.what(); + return false; + } + catch (const std::exception &err) { + BOOST_LOG(error) << "Failed to parse refresh rate or FPS string " << trimmed_input << ":\n" + << err.what(); + return false; + } + } + else { + output = boost::none; + + if (!trimmed_input.empty()) { + BOOST_LOG(error) << "Failed to parse refresh rate or FPS string " << trimmed_input << ". Must have a pattern of " << (allow_decimal_point ? "\"123\" or \"123.456\"" : "\"123\"") << "!"; + return false; + } + } + + return true; + } + + /** + * @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) { + BOOST_LOG(warning) << "Sunshine is configured to change resolution automatically, but the \"Optimize game settings\" is not set in the client! Resolution will not be changed."; + 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: { + if (!session.enable_sops) { + BOOST_LOG(warning) << "Sunshine is configured to change resolution manually, but the \"Optimize game settings\" is not set in the client! Resolution will not be changed."; + parsed_config.resolution = boost::none; + } + else { + if (!parse_resolution_string(config.manual_resolution, parsed_config.resolution)) { + BOOST_LOG(error) << "Failed to parse manual resolution string!"; + return false; + } + + if (!parsed_config.resolution) { + BOOST_LOG(error) << "Manual resolution must be specified!"; + 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: { + if (!parse_refresh_rate_string(config.manual_refresh_rate, parsed_config.refresh_rate)) { + BOOST_LOG(error) << "Failed to parse manual refresh rate string!"; + return false; + } + + if (!parsed_config.refresh_rate) { + BOOST_LOG(error) << "Manual refresh rate must be specified!"; + return false; + } + break; + } + case parsed_config_t::refresh_rate_change_e::no_operation: + default: + break; + } + + return true; + } + + /** + * @brief Remap the already parsed display mode based on the user configuration. + * @param config User's video related configuration. + * @param parsed_config A reference to a config object that will be modified on success. + * @returns True is display mode was remapped or no remapping was needed, 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 = remap_display_modes_if_needed(video_config, *launch_session, parsed_config); + * ``` + */ + bool + remap_display_modes_if_needed(const config::video_t &config, const rtsp_stream::launch_session_t &session, parsed_config_t &parsed_config) { + constexpr auto mixed_remapping { "" }; + constexpr auto resolution_only_remapping { "resolution_only" }; + constexpr auto refresh_rate_only_remapping { "refresh_rate_only" }; + + const auto resolution_option { static_cast(config.resolution_change) }; + const auto refresh_rate_option { static_cast(config.refresh_rate_change) }; + + // Copy only the remapping values that we can actually use with our configuration options + std::vector remapping_values; + std::copy_if(std::begin(config.display_mode_remapping), std::end(config.display_mode_remapping), std::back_inserter(remapping_values), [&](const auto &value) { + if (resolution_option == parsed_config_t::resolution_change_e::automatic && refresh_rate_option == parsed_config_t::refresh_rate_change_e::automatic) { + return value.type == mixed_remapping; // Comparison instead of empty check to be explicit + } + else if (resolution_option == parsed_config_t::resolution_change_e::automatic) { + return value.type == resolution_only_remapping; + } + else if (refresh_rate_option == parsed_config_t::refresh_rate_change_e::automatic) { + return value.type == refresh_rate_only_remapping; + } + + return false; + }); + + if (remapping_values.empty()) { + BOOST_LOG(debug) << "No values are available for display mode remapping."; + return true; + } + BOOST_LOG(debug) << "Trying to remap display modes..."; + + struct parsed_remapping_values_t { + boost::optional received_resolution; + boost::optional received_fps; + boost::optional final_resolution; + boost::optional final_refresh_rate; + }; + + std::vector parsed_values; + for (const auto &entry : remapping_values) { + boost::optional received_resolution; + boost::optional received_fps; + boost::optional final_resolution; + boost::optional final_refresh_rate; + + if (entry.type == resolution_only_remapping) { + if (!parse_resolution_string(entry.received_resolution, received_resolution) || + !parse_resolution_string(entry.final_resolution, final_resolution)) { + BOOST_LOG(error) << "Failed to parse entry value: " << entry.received_resolution << " -> " << entry.final_resolution; + return false; + } + + if (!received_resolution || !final_resolution) { + BOOST_LOG(error) << "Both values must be set for remapping resolution! Current entry value: " << entry.received_resolution << " -> " << entry.final_resolution; + return false; + } + + if (!session.enable_sops) { + BOOST_LOG(warning) << "Skipping remapping resolution, because the \"Optimize game settings\" is not set in the client!"; + return true; + } + } + else if (entry.type == refresh_rate_only_remapping) { + if (!parse_refresh_rate_string(entry.received_fps, received_fps, false) || + !parse_refresh_rate_string(entry.final_refresh_rate, final_refresh_rate)) { + BOOST_LOG(error) << "Failed to parse entry value: " << entry.received_fps << " -> " << entry.final_refresh_rate; + return false; + } + + if (!received_fps || !final_refresh_rate) { + BOOST_LOG(error) << "Both values must be set for remapping refresh rate! Current entry value: " << entry.received_fps << " -> " << entry.final_refresh_rate; + return false; + } + } + else { + if (!parse_resolution_string(entry.received_resolution, received_resolution) || + !parse_refresh_rate_string(entry.received_fps, received_fps, false) || + !parse_resolution_string(entry.final_resolution, final_resolution) || + !parse_refresh_rate_string(entry.final_refresh_rate, final_refresh_rate)) { + BOOST_LOG(error) << "Failed to parse entry value: " + << "[" << entry.received_resolution << "|" << entry.received_fps << "] -> [" << entry.final_resolution << "|" << entry.final_refresh_rate << "]"; + return false; + } + + if ((!received_resolution && !received_fps) || (!final_resolution && !final_refresh_rate)) { + BOOST_LOG(error) << "At least one received and final value must be set for remapping display modes! Entry: " + << "[" << entry.received_resolution << "|" << entry.received_fps << "] -> [" << entry.final_resolution << "|" << entry.final_refresh_rate << "]"; + return false; + } + + if (!session.enable_sops && (received_resolution || final_resolution)) { + BOOST_LOG(warning) << "Skipping remapping entry, because the \"Optimize game settings\" is not set in the client! Entry: " + << "[" << entry.received_resolution << "|" << entry.received_fps << "] -> [" << entry.final_resolution << "|" << entry.final_refresh_rate << "]"; + continue; + } + } + + parsed_values.push_back({ received_resolution, received_fps, final_resolution, final_refresh_rate }); + } + + const auto compare_resolution { [](const resolution_t &a, const resolution_t &b) { + return a.width == b.width && a.height == b.height; + } }; + const auto compare_refresh_rate { [](const refresh_rate_t &a, const refresh_rate_t &b) { + return a.numerator == b.numerator && a.denominator == b.denominator; + } }; + + for (const auto &entry : parsed_values) { + bool do_remap { false }; + if (entry.received_resolution && entry.received_fps) { + if (parsed_config.resolution && parsed_config.refresh_rate) { + do_remap = compare_resolution(*entry.received_resolution, *parsed_config.resolution) && compare_refresh_rate(*entry.received_fps, *parsed_config.refresh_rate); + } + else { + // Sanity check + BOOST_LOG(error) << "Cannot remap: (parsed_config.resolution && parsed_config.refresh_rate) == false!"; + return false; + } + } + else if (entry.received_resolution) { + if (parsed_config.resolution) { + do_remap = compare_resolution(*entry.received_resolution, *parsed_config.resolution); + } + else { + // Sanity check + BOOST_LOG(error) << "Cannot remap: parsed_config.resolution == false!"; + return false; + } + } + else if (entry.received_fps) { + if (parsed_config.refresh_rate) { + do_remap = compare_refresh_rate(*entry.received_fps, *parsed_config.refresh_rate); + } + else { + // Sanity check + BOOST_LOG(error) << "Cannot remap: parsed_config.refresh_rate == false!"; + return false; + } + } + else { + // Sanity check + BOOST_LOG(error) << "Cannot remap: (entry.received_resolution || entry.received_fps) == false!"; + return false; + } + + if (do_remap) { + if (!entry.final_resolution && !entry.final_refresh_rate) { + // Sanity check + BOOST_LOG(error) << "Cannot remap: (!entry.final_resolution && !entry.final_refresh_rate) == true!"; + return false; + } + + if (entry.final_resolution) { + BOOST_LOG(debug) << "Remapping resolution to: " << to_string(*entry.final_resolution); + parsed_config.resolution = entry.final_resolution; + } + if (entry.final_refresh_rate) { + BOOST_LOG(debug) << "Remapping refresh rate to: " << to_string(*entry.final_refresh_rate); + parsed_config.refresh_rate = entry.final_refresh_rate; + } + + 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; + } + + if (!remap_display_modes_if_needed(config, session, parsed_config)) { + // Error already logged + return boost::none; + } + + BOOST_LOG(debug) << "Parsed display device config:\n" + << "device_id: " << parsed_config.device_id << "\n" + << "device_prep: " << static_cast(parsed_config.device_prep) << "\n" + << "change_hdr_state: " << (parsed_config.change_hdr_state ? *parsed_config.change_hdr_state ? "true" : "false" : "none") << "\n" + << "resolution: " << (parsed_config.resolution ? to_string(*parsed_config.resolution) : "none") << "\n" + << "refresh_rate: " << (parsed_config.refresh_rate ? to_string(*parsed_config.refresh_rate) : "none") << "\n"; + + 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..b7b840c369d --- /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 ("Optimize game settings" option must be enabled in the client). */ + }; + + /** + * @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..c1cf38dba1b --- /dev/null +++ b/src/display_device/session.cpp @@ -0,0 +1,225 @@ +// standard includes +#include + +// local includes +#include "session.h" +#include "src/platform/common.h" +#include "to_string.h" + +namespace display_device { + + class session_t::StateRetryTimer { + public: + /** + * @brief A constructor for the timer. + * @param mutex A shared mutex for synchronization. + * @warning Because we are keeping references to shared parameters, we MUST ensure they outlive this object! + */ + StateRetryTimer(std::mutex &mutex): + mutex { mutex }, 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 { + next_wake_up_time = boost::none; + + const auto result { !this->retry_function || this->retry_function() }; + if (!result) { + 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. + */ + ~StateRetryTimer() { + { + 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 retry_function Function to be executed every X seconds. + * If the function returns true, the loop is stopped. + * If the function is of type nullptr_t, the loop is stopped. + * @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(std::function retry_function) { + this->retry_function = std::move(retry_function); + + if (this->retry_function) { + 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. */ + std::chrono::seconds timeout_duration { 5 }; /**< A retry time for the timer. */ + std::function retry_function; /**< Function to be executed until it succeeds. */ + + 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(); + } + + void + session_t::configure_display(const config::video_t &config, const rtsp_stream::launch_session_t &session) { + std::lock_guard lock { mutex }; + + const auto parsed_config { make_parsed_config(config, session) }; + if (!parsed_config) { + BOOST_LOG(error) << "Failed to parse configuration for the the display device settings!"; + return; + } + + if (settings.is_changing_settings_going_to_fail()) { + timer->setup_timer([this, config_copy = *parsed_config]() { + if (settings.is_changing_settings_going_to_fail()) { + BOOST_LOG(warning) << "Applying display settings will fail - retrying later..."; + return false; + } + + const auto result { settings.apply_config(config_copy) }; + if (!result) { + BOOST_LOG(warning) << "Failed to apply display settings - will stop trying, but will allow stream to continue."; + + // WARNING! After call to the method below, this lambda function is no be longer valid! + // DO NOT access anything from the capture list! + restore_state_impl(); + } + return true; + }); + + BOOST_LOG(warning) << "It is already known that display settings cannot be changed. Allowing stream to start without changing the settings, but will retry changing settings later..."; + return; + } + + const auto result { settings.apply_config(*parsed_config) }; + if (result) { + timer->setup_timer(nullptr); + } + else { + restore_state_impl(); + } + } + + void + session_t::restore_state() { + std::lock_guard lock { mutex }; + restore_state_impl(); + } + + void + session_t::reset_persistence() { + std::lock_guard lock { mutex }; + + settings.reset_persistence(); + timer->setup_timer(nullptr); + } + + void + session_t::restore_state_impl() { + const auto result { !settings.is_changing_settings_going_to_fail() && settings.revert_settings() }; + if (result) { + timer->setup_timer(nullptr); + } + else { + if (settings.is_changing_settings_going_to_fail()) { + BOOST_LOG(warning) << "Reverting display settings will fail - retrying later..."; + } + + timer->setup_timer([this]() { + if (settings.is_changing_settings_going_to_fail()) { + BOOST_LOG(warning) << "Reverting display settings will still fail - retrying later..."; + return false; + } + + return settings.revert_settings(); + }); + } + } + + session_t::session_t(): + timer { std::make_unique(mutex) } { + } + +} // namespace display_device diff --git a/src/display_device/session.h b/src/display_device/session.h new file mode 100644 index 00000000000..68fc935d53a --- /dev/null +++ b/src/display_device/session.h @@ -0,0 +1,190 @@ +#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. + * + * Upon failing to completely apply configuration, the applied settings will be reverted. + * Or, in some cases, we will keep retrying even when the stream has already started as there + * is no possibility to apply settings before the stream start. + * + * @param config User's video related configuration. + * @param session Session information. + * @note There is no return value as we still want to continue with the stream, so that + * users can do something about it once they are connected. Otherwise, we might + * prevent users from logging in at all... + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * session_t::get().configure_display(video_config, *launch_session); + * ``` + */ + void + 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 X 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 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 set/reset state. + * + * This timer class spins a thread which is mostly sleeping all the time, but can be + * configured to wake up every X seconds. + * + * 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 StateRetryTimer; + + /** + * @brief A private constructor to ensure the singleton pattern. + * @note Cannot be defaulted in declaration because of forward declared StateRetryTimer. + */ + explicit session_t(); + + /** + * @brief An implementation of `restore_state` without a mutex lock. + * @see restore_state for the description. + */ + void + restore_state_impl(); + + settings_t settings; /**< A class for managing display device settings. */ + std::mutex mutex; /**< A mutex for ensuring thread-safety. */ + + /** + * @brief An instance of StateRetryTimer. + * @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..dab8ee89103 --- /dev/null +++ b/src/display_device/settings.cpp @@ -0,0 +1,39 @@ +// 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; + } + + std::string + settings_t::apply_result_t::get_error_message() const { + switch (result) { + case result_e::success: + return "Success"; + 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..4f353c29080 --- /dev/null +++ b/src/display_device/settings.h @@ -0,0 +1,200 @@ +#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, /**< Workaround for doxygen */ + topology_fail, /**< Workaround for doxygen */ + primary_display_fail, /**< Workaround for doxygen */ + modes_fail, /**< Workaround for doxygen */ + hdr_states_fail, /**< Workaround for doxygen */ + file_save_fail, /**< Workaround for doxygen */ + revert_fail /**< Workaround for doxygen */ + }; + + /** + * @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 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 Check whether it is already known that changing settings will fail due to various reasons. + * @returns True if it's definitely known that changing settings will fail, false otherwise. + * + * EXAMPLES: + * ```cpp + * settings_t settings; + * const bool will_fail { settings.is_changing_settings_going_to_fail() }; + * ``` + */ + bool + is_changing_settings_going_to_fail() const; + + /** + * @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 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); + + /** + * @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: + 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 f70cc637c61..8e61c678f31 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ // local includes #include "confighttp.h" +#include "display_device/session.h" #include "entry_handler.h" #include "globals.h" #include "httpcommon.h" @@ -121,6 +122,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()) { @@ -218,7 +227,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 = []() { @@ -229,9 +238,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 = []() { @@ -242,6 +252,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 af32391bbf6..70e745292e0 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" @@ -819,12 +820,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) { @@ -859,11 +865,22 @@ 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 should to be done before probing encoders as it could + // change display device's state. + display_device::session_t::get().configure_display(config::video, *launch_session); + + // 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?"); @@ -873,9 +890,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; @@ -905,6 +919,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 @@ -950,7 +967,20 @@ 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 should to be done before probing encoders as it could + // change display device's state. + display_device::session_t::get().configure_display(config::video, *launch_session); + // 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, @@ -962,17 +992,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; @@ -1022,6 +1043,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..23e7aa923ba --- /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 + } + + bool + settings_t::is_changing_settings_going_to_fail() const { + // Not implemented + return false; + } + + 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..23e7aa923ba --- /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 + } + + bool + settings_t::is_changing_settings_going_to_fail() const { + // Not implemented + return false; + } + + 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 6ab0f0c2c81..5a094988f35 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" @@ -1080,7 +1081,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..bebc626b520 --- /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..718f8bec7e4 --- /dev/null +++ b/src/platform/windows/display_device/device_modes.cpp @@ -0,0 +1,344 @@ +// local includes +#include "src/logging.h" +#include "windows_utils.h" + +namespace display_device { + + namespace { + + /** + * @brief 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; + } + + /** + * @brief 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 }; + + bool refresh_rate_changed { false }; + if (allow_changes) { + refresh_rate_changed = !fuzzy_compare_refresh_rates(refresh_rate_t { path->targetInfo.refreshRate.Numerator, path->targetInfo.refreshRate.Denominator }, mode.refresh_rate); + } + else { + // Since we are in strict mode, do not fuzzy compare it + 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 as they are equal."; + 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..e01e311537f --- /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 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 or because the display is in deep sleep!"; + } + } + 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..0e2cf06e6f2 --- /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..bd42e0914f3 --- /dev/null +++ b/src/platform/windows/display_device/settings.cpp @@ -0,0 +1,756 @@ +// 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" +#include "windows_utils.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(info) << "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(info) << "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(info) << "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(info) << "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(info) << "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(info) << "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) { + try { + nlohmann::json json_data = data; + BOOST_LOG(debug) << "Reverting persistent display settings from:\n" + << json_data.dump(4); + } + catch (const std::exception &err) { + BOOST_LOG(error) << "Failed to dump persistent display settings: " << err.what(); + } + + 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(info) << "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(info) << "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(info) << "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; + } + } + + BOOST_LOG(info) << "Changing display topology back to: " << to_string(data.topology.initial); + 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; + BOOST_LOG(debug) << "Saved persistent display settings:\n" + << json_data.dump(4); + return true; + } + catch (const std::exception &err) { + BOOST_LOG(error) << "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(error) << "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; + + bool + settings_t::is_changing_settings_going_to_fail() const { + return w_utils::is_user_session_locked() || w_utils::test_no_access_to_ccd_api(); + } + + settings_t::apply_result_t + settings_t::apply_config(const parsed_config_t &config) { + const auto do_apply_config { [this](const parsed_config_t &config) -> settings_t::apply_result_t { + 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(); + } }; + + BOOST_LOG(info) << "Applying configuration to the display device."; + const bool display_may_change { 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 { do_apply_config(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; + } + } + + if (!result) { + BOOST_LOG(error) << "Failed to configure display:\n" + << result.get_error_message(); + } + else { + 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; + } + } + +} // 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..abdce2a03a0 --- /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; + } + + // 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) }; + + BOOST_LOG(debug) << "Current display topology: " << to_string(current_topology); + if (!is_topology_the_same(current_topology, final_topology)) { + BOOST_LOG(info) << "Changing display topology to: " << to_string(final_topology); + if (!set_topology(final_topology)) { + // Error already logged. + return boost::none; + } + + // It is possible that we no longer 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..176c8a7b7fa --- /dev/null +++ b/src/platform/windows/display_device/windows_utils.cpp @@ -0,0 +1,654 @@ +// 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 +#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) { + // The MS docs is not clear when to access union struct or not. It appears that union struct is available, + // whenever QDC_VIRTUAL_MODE_AWARE is specified when querying. + // + // The docs state, however, that it is only available when + // DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE flag is set, but that is just BS (maybe copy-pasta mistake), because some cases + // were found where the flag is not set and the union is still being used. + + const UINT32 index { path.sourceInfo.sourceModeInfoIdx }; + if (index == DISPLAYCONFIG_PATH_SOURCE_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) { + // The MS docs is not clear when to access union struct or not. It appears that union struct is available, + // whenever QDC_VIRTUAL_MODE_AWARE is specified when querying. + // + // The docs state, however, that it is only available when + // DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE flag is set, but that is just BS (maybe copy-pasta mistake), because some cases + // were found where the flag is not set and the union is still being used. + + if (index) { + path.sourceInfo.sourceModeInfoIdx = *index; + } + else { + path.sourceInfo.sourceModeInfoIdx = DISPLAYCONFIG_PATH_SOURCE_MODE_IDX_INVALID; + } + } + + void + set_target_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index) { + // The MS docs is not clear when to access union struct or not. It appears that union struct is available, + // whenever QDC_VIRTUAL_MODE_AWARE is specified when querying. + // + // The docs state, however, that it is only available when + // DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE flag is set, but that is just BS (maybe copy-pasta mistake), because some cases + // were found where the flag is not set and the union is still being used. + + if (index) { + path.targetInfo.targetModeInfoIdx = *index; + } + else { + path.targetInfo.targetModeInfoIdx = DISPLAYCONFIG_PATH_TARGET_MODE_IDX_INVALID; + } + } + + void + set_desktop_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index) { + // The MS docs is not clear when to access union struct or not. It appears that union struct is available, + // whenever QDC_VIRTUAL_MODE_AWARE is specified when querying. + // + // The docs state, however, that it is only available when + // DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE flag is set, but that is just BS (maybe copy-pasta mistake), because some cases + // were found where the flag is not set and the union is still being used. + + 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) { + // The MS docs is not clear when to access union struct or not. It appears that union struct is available, + // whenever QDC_VIRTUAL_MODE_AWARE is specified when querying. + // + // The docs state, however, that it is only available when + // DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE flag is set, but that is just BS (maybe copy-pasta mistake), because some cases + // were found where the flag is not set and the union is still being used. + + 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))); + } + + bool + is_user_session_locked() { + LPWSTR buffer { nullptr }; + const auto cleanup_guard { + util::fail_guard([&buffer]() { + if (buffer) { + WTSFreeMemory(buffer); + } + }) + }; + + DWORD buffer_size_in_bytes { 0 }; + if (WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, WTSGetActiveConsoleSessionId(), WTSSessionInfoEx, &buffer, &buffer_size_in_bytes)) { + if (buffer_size_in_bytes > 0) { + const auto wts_info { reinterpret_cast(buffer) }; + if (wts_info && wts_info->Level == 1) { + const bool is_locked { wts_info->Data.WTSInfoExLevel1.SessionFlags == WTS_SESSIONSTATE_LOCK }; + BOOST_LOG(debug) << "is_user_session_locked: " << is_locked; + return is_locked; + } + } + + BOOST_LOG(warning) << "Failed to get session info in is_user_session_locked."; + } + else { + BOOST_LOG(error) << get_error_string(GetLastError()) << " failed while calling WTSQuerySessionInformationW!"; + } + + return false; + } + + bool + test_no_access_to_ccd_api() { + auto display_data { query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + BOOST_LOG(debug) << "test_no_access_to_ccd_api failed in query_display_config."; + return true; + } + + // Here we are supplying the retrieved display data back to SetDisplayConfig (with VALIDATE flag only, so that we make no actual changes). + // Unless something is really broken on Windows, this call should never fail under normal circumstances - the configuration is 100% correct, since it was + // provided by Windows. + const UINT32 flags { SDC_VALIDATE | SDC_USE_SUPPLIED_DISPLAY_CONFIG | 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) }; + + BOOST_LOG(debug) << "test_no_access_to_ccd_api result: " << get_error_string(result); + return result == ERROR_ACCESS_DENIED; + } + +} // 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..78843a8ad8c --- /dev/null +++ b/src/platform/windows/display_device/windows_utils.h @@ -0,0 +1,491 @@ +#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. + * + * 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. + * @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. + * @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. + * @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. + * @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); + + /** + * @brief Check whether the user session is locked. + * @returns True if it's definitely known that the session is locked, false otherwise. + * + * EXAMPLES: + * ```cpp + * const bool is_locked { is_user_session_locked() }; + * ``` + */ + bool + is_user_session_locked(); + + /** + * @brief Check whether it is already known that the CCD API will fail to set settings. + * @returns True if we already known we don't have access (for now), false otherwise. + * + * EXAMPLES: + * ```cpp + * const bool no_access { test_no_access_to_ccd_api() }; + * ``` + */ + bool + test_no_access_to_ccd_api(); + +} // namespace display_device::w_utils diff --git a/src/process.cpp b/src/process.cpp index 6ab6b31757b..89418f5fb00 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" @@ -330,16 +331,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 46887c3a0aa..695db38a388 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" @@ -1955,11 +1956,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 7758d68a9b5..22c59d64dae 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -18,6 +18,7 @@ extern "C" { #include "cbs.h" #include "config.h" +#include "display_device/display_device.h" #include "globals.h" #include "input.h" #include "logging.h" @@ -966,6 +967,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 @@ -984,7 +987,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 @@ -1004,7 +1007,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; } @@ -2307,7 +2310,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; } @@ -2437,7 +2441,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 8c2bce2b32f..932c7a677f6 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -51,6 +51,7 @@

{{ $t('config.configuration') }}

:platform="platform" :resolutions="resolutions" :fps="fps" + :display_mode_remapping="display_mode_remapping" > @@ -132,6 +133,7 @@

{{ $t('config.configuration') }}

resolutions: [], currentTab: "general", global_prep_cmd: [], + display_mode_remapping: [], tabs: [ // TODO: Move the options to each Component instead, encapsulate. { id: "general", @@ -174,6 +176,13 @@

{{ $t('config.configuration') }}

"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", + "display_mode_remapping": "[]", "resolutions": "[352x240,480x360,858x480,1280x720,1920x1080,2560x1080,2560x1440,3440x1440,1920x1200,3840x2160,3840x1600]", "fps": "[10,30,60,90,120]", "min_fps_factor": 1, @@ -332,6 +341,9 @@

{{ $t('config.configuration') }}

this.config.global_prep_cmd = this.config.global_prep_cmd || []; this.global_prep_cmd = JSON.parse(this.config.global_prep_cmd); + + this.config.display_mode_remapping = this.config.display_mode_remapping || []; + this.display_mode_remapping = JSON.parse(this.config.display_mode_remapping); }); }, methods: { @@ -350,6 +362,7 @@

{{ $t('config.configuration') }}

// remove quotes from values in fps this.config.fps = JSON.stringify(this.fps).replace(/"/g, ""); this.config.global_prep_cmd = JSON.stringify(this.global_prep_cmd); + this.config.display_mode_remapping = JSON.stringify(this.display_mode_remapping); }, save() { this.saved = false; @@ -364,7 +377,7 @@

{{ $t('config.configuration') }}

Object.keys(tab.options).forEach(optionKey => { let delete_value = false - if (["resolutions", "fps", "global_prep_cmd"].includes(optionKey)) { + if (["resolutions", "fps", "global_prep_cmd", "display_mode_remapping"].includes(optionKey)) { let config_value, default_value if (optionKey === "resolutions") { diff --git a/src_assets/common/assets/web/configs/tabs/AudioVideo.vue b/src_assets/common/assets/web/configs/tabs/AudioVideo.vue index 1a5b63c07ec..2a8337da086 100644 --- a/src_assets/common/assets/web/configs/tabs/AudioVideo.vue +++ b/src_assets/common/assets/web/configs/tabs/AudioVideo.vue @@ -14,6 +14,7 @@ const props = defineProps([ 'resolutions', 'fps', 'min_fps_factor', + 'display_mode_remapping', ]) const config = ref(props.config) @@ -73,11 +74,17 @@ const config = ref(props.config) :config="config" /> - + + diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index cc5a863a5e9..d348c0191db 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -150,6 +150,23 @@ "controller_desc": "Allows guests to control the host system with a gamepad / controller", "credentials_file": "Credentials File", "credentials_file_desc": "Store Username/Password separately from Sunshine's state file.", + "display_device_options_note_desc_windows": "Windows saves various display settings for each combination of currently active displays.\nSunshine then applies changes to a display(-s) belonging to such a display combination.\nIf you disconnect a device which was active when Sunshine applied the settings, the changes can not be\nreverted back unless the combination can be activated again by the time Sunshine tries to revert changes!", + "display_device_options_note_windows": "Note about how settings are applied", + "display_device_options_windows": "Display device options", + "display_device_prep_ensure_active_windows": "Activate the display automatically", + "display_device_prep_ensure_only_display_windows": "Deactivate other displays and activate only the specified display", + "display_device_prep_ensure_primary_windows": "Activate the display automatically and make it a primary display", + "display_device_prep_no_operation_windows": "Disabled", + "display_device_prep_windows": "Display preparation", + "display_mode_remapping_default_mode_desc_windows": "At least one \"received\" and one \"final\" value must be specified.\nEmpty field in \"received\" section means \"match any\". Empty field in \"final\" section means \"keep received value\".\nYou can match specific FPS value to specific resolution if you wish so...\n\nNote: if \"Optimize game settings\" option is not enabled on the Moonlight client, the rows containing resolution value(-s) are ignored.", + "display_mode_remapping_desc_windows": "Specify how a specific resolution and/or refresh rate should be remapped to other values.\nYou can stream at lower resolution, while rendering at higher resolution on host for a supersampling effect.\nOr you can stream at higher FPS while limiting the host to the lower refresh rate.\nMatching is performed top to bottom. Once the entry is matched, others are no longer checked, but still validated.", + "display_mode_remapping_final_refresh_rate_windows": "Final refresh rate", + "display_mode_remapping_final_resolution_windows": "Final resolution", + "display_mode_remapping_optional": "optional", + "display_mode_remapping_received_fps_windows": "Received FPS", + "display_mode_remapping_received_resolution_windows": "Received resolution", + "display_mode_remapping_resolution_only_mode_desc_windows": "Note: if \"Optimize game settings\" option is not enabled on the Moonlight client, the remapping is disabled.", + "display_mode_remapping_windows": "Remap display modes", "ds4_back_as_touchpad_click": "Map Back/Select to Touchpad Click", "ds4_back_as_touchpad_click_desc": "When forcing DS4 emulation, map Back/Select to Touchpad Click", "encoder": "Force a Specific Encoder", @@ -176,6 +193,9 @@ "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Command Preparations", "global_prep_cmd_desc": "Configure a list of commands to be executed before or after running any application. If any of the specified preparation commands fail, the application launch process will be aborted.", + "hdr_prep_automatic_windows": "Switch on/off the HDR mode as requested by the client", + "hdr_prep_no_operation_windows": "Disabled", + "hdr_prep_windows": "HDR state change", "hevc_mode": "HEVC Support", "hevc_mode_0": "Sunshine will advertise support for HEVC based on encoder capabilities (recommended)", "hevc_mode_1": "Sunshine will not advertise support for HEVC", @@ -253,9 +273,9 @@ "origin_web_ui_allowed_pc": "Only localhost may access Web UI", "origin_web_ui_allowed_wan": "Anyone may access Web UI", "output_name_desc_unix": "During Sunshine startup, you should see the list of detected displays. Note: You need to use the id value inside the parenthesis.", - "output_name_desc_windows": "Manually specify a display 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:", + "output_name_desc_windows": "Manually specify a display device id to use for capture. If unset, the primary display is captured.\nNote: If you specified a GPU above, this display must be connected to that GPU.\n\nDuring Sunshine startup, you should see the list of detected display devices and their ids, e.g.:", "output_name_unix": "Display number", - "output_name_windows": "Output Name", + "output_name_windows": "Display Device Id", "ping_timeout": "Ping Timeout", "ping_timeout_desc": "How long to wait in milliseconds for data from moonlight before shutting down the stream", "pkey": "Private Key", @@ -285,7 +305,18 @@ "qsv_preset_veryfast": "fastest (lowest quality)", "qsv_slow_hevc": "Allow Slow HEVC Encoding", "qsv_slow_hevc_desc": "This can enable HEVC encoding on older Intel GPUs, at the cost of higher GPU usage and worse performance.", + "refresh_rate_change_automatic_windows": "Use FPS value provided by the client", + "refresh_rate_change_manual_desc_windows": "Enter the refresh rate to be used", + "refresh_rate_change_manual_windows": "Use manually entered refresh rate", + "refresh_rate_change_no_operation_windows": "Disabled", + "refresh_rate_change_windows": "Resolution change", "res_fps_desc": "The display modes advertised by Sunshine. Some versions of Moonlight, such as Moonlight-nx (Switch), rely on these lists to ensure that the requested resolutions and fps are supported. This setting does not change how the screen stream is sent to Moonlight.", + "resolution_change_automatic_windows": "Use resolution provided by the client", + "resolution_change_manual_desc_windows": "\"Optimize game settings\" option must be enabled on the Moonlight client for this to work.", + "resolution_change_manual_windows": "Use manually entered resolution", + "resolution_change_no_operation_windows": "Disabled", + "resolution_change_ogs_desc_windows": "\"Optimize game settings\" option must be enabled on the Moonlight client for this to work.", + "resolution_change_windows": "Resolution change", "resolutions": "Advertised Resolutions", "restart_note": "Sunshine is restarting to apply changes.", "sunshine_name": "Sunshine Name", @@ -384,6 +415,10 @@ "logs": "Logs", "logs_desc": "See the logs uploaded by Sunshine", "logs_find": "Find...", + "reset_display_device_desc_windows": "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.\nThis could happen for various reasons: device is no longer available, has been plugged to a different port and so on.", + "reset_display_device_error_windows": "Error while resetting persistence!", + "reset_display_device_success_windows": "Success resetting persistence!", + "reset_display_device_windows": "Reset Persistent Display Device Settings", "restart_sunshine": "Restart Sunshine", "restart_sunshine_desc": "If Sunshine isn't working properly, you can try restarting it. This will terminate any running sessions.", "restart_sunshine_success": "Sunshine is restarting", diff --git a/src_assets/common/assets/web/troubleshooting.html b/src_assets/common/assets/web/troubleshooting.html index 2a41666054a..65663c1e2ec 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); + } + @@ -75,6 +75,25 @@

{{ $t('troubleshooting.restart_sunshine') }}

+ +
+
+

{{ $t('troubleshooting.reset_display_device_windows') }}

+
+

{{ $t('troubleshooting.reset_display_device_desc_windows') }}

+
+ {{ $t('troubleshooting.reset_display_device_success_windows') }} +
+
+ {{ $t('troubleshooting.reset_display_device_error_windows') }} +
+
+ +
+
+
@@ -144,6 +163,8 @@

{{ $t('troubleshooting.logs') }}

logs: 'Loading...', logFilter: null, logInterval: null, + resetDisplayDevicePressed: false, + resetDisplayDeviceStatus: null, restartPressed: false, showApplyMessage: false, unpairAllPressed: false, @@ -159,6 +180,12 @@

{{ $t('troubleshooting.logs') }}

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

{{ $t('troubleshooting.logs') }}

} }); }, + 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); + }); + }, clickedApplyBanner() { this.showApplyMessage = false; }, diff --git a/third-party/nlohmann_json b/third-party/nlohmann_json new file mode 160000 index 00000000000..199dea11b17 --- /dev/null +++ b/third-party/nlohmann_json @@ -0,0 +1 @@ +Subproject commit 199dea11b17c533721b26249e2dcaee6ca1d51d3