diff --git a/.gitmodules b/.gitmodules index 4aa76d5206f..d246748ed1d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -18,6 +18,10 @@ path = third-party/inputtino url = https://github.com/games-on-whales/inputtino.git branch = stable +[submodule "third-party/libdisplaydevice"] + path = third-party/libdisplaydevice + url = https://github.com/LizardByte/libdisplaydevice.git + branch = master [submodule "third-party/moonlight-common-c"] path = third-party/moonlight-common-c url = https://github.com/moonlight-stream/moonlight-common-c.git diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake index f4a91a78ad0..edecb7eece5 100644 --- a/cmake/compile_definitions/common.cmake +++ b/cmake/compile_definitions/common.cmake @@ -61,6 +61,8 @@ set(SUNSHINE_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/uuid.h" "${CMAKE_SOURCE_DIR}/src/config.h" "${CMAKE_SOURCE_DIR}/src/config.cpp" + "${CMAKE_SOURCE_DIR}/src/display_device.h" + "${CMAKE_SOURCE_DIR}/src/display_device.cpp" "${CMAKE_SOURCE_DIR}/src/entry_handler.cpp" "${CMAKE_SOURCE_DIR}/src/entry_handler.h" "${CMAKE_SOURCE_DIR}/src/file_handler.cpp" @@ -137,4 +139,5 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${FFMPEG_LIBRARIES} ${Boost_LIBRARIES} ${OPENSSL_LIBRARIES} - ${PLATFORM_LIBRARIES}) + ${PLATFORM_LIBRARIES} + libdisplaydevice::display_device) diff --git a/cmake/dependencies/common.cmake b/cmake/dependencies/common.cmake index 01065b387e0..d53090baf50 100644 --- a/cmake/dependencies/common.cmake +++ b/cmake/dependencies/common.cmake @@ -12,6 +12,9 @@ add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/enet") # web server add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/Simple-Web-Server") +# libdisplaydevice +add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/libdisplaydevice") + # common dependencies find_package(OpenSSL REQUIRED) find_package(PkgConfig REQUIRED) diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst index 95025b91358..f58dbbbe04e 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,58 @@ 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 displays: - tools\dxgi-info.exe + .. code-block:: text + + Info: Currently available display devices: + [ + { + "device_id": "{64243705-4020-5895-b923-adc862c3457e}", + "display_name": "", + "friendly_name": "IDD HDR", + "info": null + }, + { + "device_id": "{77f67f3e-754f-5d31-af64-ee037e18100a}", + "display_name": "", + "friendly_name": "SunshineHDR", + "info": null + }, + { + "device_id": "{daeac860-f4db-5208-b1f5-cf59444fb768}", + "display_name": "\\\\.\\DISPLAY1", + "friendly_name": "ROG PG279Q", + "info": { + "hdr_state": null, + "origin_point": { + "x": 0, + "y": 0 + }, + "primary": true, + "refresh_rate": { + "type": "rational", + "value": { + "denominator": 1000, + "numerator": 119998 + } + }, + "resolution": { + "height": 1440, + "width": 2560 + }, + "resolution_scale": { + "type": "rational", + "value": { + "denominator": 100, + "numerator": 100 + } + } + } + } + ] + + You need to use the ``device_id`` value. **Default** Sunshine will select the default display. @@ -637,7 +686,7 @@ keybindings **Windows** .. code-block:: text - output_name = \\.\DISPLAY1 + output_name = {daeac860-f4db-5208-b1f5-cf59444fb768} `resolutions `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/display_device.cpp b/src/display_device.cpp new file mode 100644 index 00000000000..74f35459ce5 --- /dev/null +++ b/src/display_device.cpp @@ -0,0 +1,80 @@ +/** + * @file src/display_device.cpp + * @brief Definitions for display device handling. + */ +// header include +#include "display_device.h" + +// lib includes +#include +#include +#include + +// platform-specific includes +#ifdef _WIN32 + #include + #include + #include +#endif + +namespace display_device { + namespace { + std::unique_ptr + make_settings_manager() { +#ifdef _WIN32 + // TODO: In the upcoming PR, add audio context capture and settings persistence + return std::make_unique( + std::make_shared(std::make_shared()), + nullptr, + std::make_unique(nullptr)); +#else + return nullptr; +#endif + } + } // namespace + + session_t & + session_t::get() { + static session_t session; + return session; + } + + std::unique_ptr + session_t::init() { + // We can support re-init without any issues, however we should make sure to cleanup first! + get().impl = nullptr; + + // If we fail to create settings manager, this means platform is not supported and + // we will need to provided error-free passtrough in other methods + if (auto settings_manager { make_settings_manager() }) { + get().impl = std::make_unique>(std::move(settings_manager)); + + const auto available_devices { get().impl->execute([](auto &settings_iface) { return settings_iface.enumAvailableDevices(); }) }; + BOOST_LOG(info) << "Currently available display devices:\n" + << toJson(available_devices); + + // TODO: In the upcoming PR, schedule recovery here + } + + class deinit_t: public platf::deinit_t { + public: + ~deinit_t() override { + // TODO: In the upcoming PR, execute recovery once here + get().impl = nullptr; + } + }; + return std::make_unique(); + } + + std::string + session_t::map_output_name(const std::string &output_name) const { + if (impl) { + return impl->execute([&output_name](auto &settings_iface) { return settings_iface.getDisplayName(output_name); }); + } + + // Fallback to giving back the output name if the platform is not supported. + return output_name; + } + + session_t::session_t() = default; +} // namespace display_device diff --git a/src/display_device.h b/src/display_device.h new file mode 100644 index 00000000000..ac8bd185a19 --- /dev/null +++ b/src/display_device.h @@ -0,0 +1,87 @@ +/** + * @file src/display_device.h + * @brief Declarations for display device handling. + */ +#pragma once + +// local includes +#include "platform/common.h" + +// forward declarations +namespace display_device { + template + class RetryScheduler; + class SettingsManagerInterface; +} // namespace display_device + +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. + */ + class session_t { + public: + /** + * @brief Get the singleton instance. + * @returns Singleton instance for the class. + * + * EXAMPLES: + * ```cpp + * session_t& session { session_t::get() }; + * ``` + */ + [[nodiscard]] 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. + * + * EXAMPLES: + * ```cpp + * const auto session_guard { session_t::init() }; + * ``` + */ + [[nodiscard]] static std::unique_ptr + init(); + + /** + * @brief Map the output name to a specific display. + * @param output_name The user-configurable output name. + * @returns Mapped display name or empty string if the output name could not be mapped. + * + * EXAMPLES: + * ```cpp + * session_t& session { session_t::get() }; + * const auto mapped_name_config { session.get_display_name(config::video.output_name) }; + * const auto mapped_name_custom { session.get_display_name("{some-device-id}") }; + * ``` + */ + [[nodiscard]] std::string + map_output_name(const std::string &output_name) const; + + /** + * @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 private constructor to ensure the singleton pattern. + * @note Cannot be defaulted in declaration because of forward declared RetryScheduler. + */ + explicit session_t(); + + std::unique_ptr> impl; /**< Platform specific interface for managing settings (with retry functionality). */ + }; +} // namespace display_device diff --git a/src/logging.cpp b/src/logging.cpp index 82ca850c80a..f187455c0c1 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -13,6 +13,7 @@ #include #include #include +#include // local includes #include "logging.h" @@ -56,6 +57,7 @@ namespace logging { } setup_av_logging(min_log_level); + setup_libdisplaydevice_logging(min_log_level); sink = boost::make_shared(); @@ -142,6 +144,37 @@ namespace logging { }); } + void + setup_libdisplaydevice_logging(int min_log_level) { + constexpr int min_level { static_cast(display_device::Logger::LogLevel::verbose) }; + constexpr int max_level { static_cast(display_device::Logger::LogLevel::fatal) }; + const auto log_level { static_cast(std::min(std::max(min_level, min_log_level), max_level)) }; + + display_device::Logger::get().setLogLevel(log_level); + display_device::Logger::get().setCustomCallback([](const display_device::Logger::LogLevel level, const std::string &message) { + switch (level) { + case display_device::Logger::LogLevel::verbose: + BOOST_LOG(verbose) << message; + break; + case display_device::Logger::LogLevel::debug: + BOOST_LOG(debug) << message; + break; + case display_device::Logger::LogLevel::info: + BOOST_LOG(info) << message; + break; + case display_device::Logger::LogLevel::warning: + BOOST_LOG(warning) << message; + break; + case display_device::Logger::LogLevel::error: + BOOST_LOG(error) << message; + break; + case display_device::Logger::LogLevel::fatal: + BOOST_LOG(fatal) << message; + break; + } + }); + } + void log_flush() { if (sink) { diff --git a/src/logging.h b/src/logging.h index 6fe4645fc92..87775a8059c 100644 --- a/src/logging.h +++ b/src/logging.h @@ -60,6 +60,13 @@ namespace logging { void setup_av_logging(int min_log_level); + /** + * @brief Setup logging for libdisplaydevice. + * @param min_log_level The log level. + */ + void + setup_libdisplaydevice_logging(int min_log_level); + /** * @brief Flush the log. * @examples diff --git a/src/main.cpp b/src/main.cpp index ed584459e9b..1969760d743 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ // local includes #include "confighttp.h" +#include "display_device.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 guard here first as it also performs recovery after crash, + // otherwise people could theoretically end up without display output. + // It also should be destroyed before forced shutdown to expedite the cleanup. + 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 = nullptr; }); - 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 = nullptr; }); proc::refresh(config::stream.file_apps); diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp index 83c668eb20d..b231b9a0b2c 100644 --- a/src/platform/macos/input.cpp +++ b/src/platform/macos/input.cpp @@ -8,6 +8,7 @@ #include #include +#include "src/display_device.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" @@ -538,7 +539,7 @@ const KeyCodeMap kKeyCodesMap[] = { // Default to main display macos_input->display = CGMainDisplayID(); - auto output_name = config::video.output_name; + auto output_name = display_device::session_t::get().map_output_name(config::video.output_name); // If output_name is set, try to find the display with that display id if (!output_name.empty()) { uint32_t max_display = 32; diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 7d34529f27c..63f779e8dc3 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.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/video.h" @@ -1079,7 +1080,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_name { display_device::session_t::get().map_output_name(config::video.output_name) }; + if (!dxgi::probe_for_gpu_preference(output_name)) { BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv; } diff --git a/src/video.cpp b/src/video.cpp index 7758d68a9b5..724d936975f 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -18,6 +18,7 @@ extern "C" { #include "cbs.h" #include "config.h" +#include "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 name may be empty even if it wasn't before (device disconnected) or vice-versa + const auto output_name { display_device::session_t::get().map_output_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_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_name) { current_display_index = x; return; } @@ -2288,6 +2291,7 @@ namespace video { bool validate_encoder(encoder_t &encoder, bool expect_failure) { + const auto output_name { display_device::session_t::get().map_output_name(config::video.output_name) }; std::shared_ptr disp; BOOST_LOG(info) << "Trying encoder ["sv << encoder.name << ']'; @@ -2307,7 +2311,7 @@ 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); + reset_display(disp, encoder.platform_formats->dev_type, output_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_name, config); if (!disp) { return false; } @@ -2591,8 +2595,9 @@ namespace video { } if (chosen_encoder == nullptr) { + const auto output_name { display_device::session_t::get().map_output_name(config::video.output_name) }; BOOST_LOG(fatal) << "Unable to find display or encoder during startup."sv; - if (!config::video.adapter_name.empty() || !config::video.output_name.empty()) { + if (!config::video.adapter_name.empty() || !output_name.empty()) { BOOST_LOG(fatal) << "Please ensure your manually chosen GPU and monitor are connected and powered on."sv; } else { diff --git a/src_assets/common/assets/web/configs/tabs/AudioVideo.vue b/src_assets/common/assets/web/configs/tabs/AudioVideo.vue index 1a5b63c07ec..8a83cf9a43b 100644 --- a/src_assets/common/assets/web/configs/tabs/AudioVideo.vue +++ b/src_assets/common/assets/web/configs/tabs/AudioVideo.vue @@ -3,8 +3,7 @@ import {ref} from 'vue' import {$tp} from '../../platform-i18n' import PlatformLayout from '../../PlatformLayout.vue' import AdapterNameSelector from './audiovideo/AdapterNameSelector.vue' -import LegacyDisplayOutputSelector from './audiovideo/LegacyDisplayOutputSelector.vue' -import NewDisplayOutputSelector from './audiovideo/NewDisplayOutputSelector.vue' +import DisplayOutputSelector from './audiovideo/DisplayOutputSelector.vue' import DisplayDeviceOptions from "./audiovideo/DisplayDeviceOptions.vue"; import DisplayModesSettings from "./audiovideo/DisplayModesSettings.vue"; @@ -73,7 +72,7 @@ const config = ref(props.config) :config="config" /> - diff --git a/src_assets/common/assets/web/configs/tabs/audiovideo/LegacyDisplayOutputSelector.vue b/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayOutputSelector.vue similarity index 79% rename from src_assets/common/assets/web/configs/tabs/audiovideo/LegacyDisplayOutputSelector.vue rename to src_assets/common/assets/web/configs/tabs/audiovideo/DisplayOutputSelector.vue index 9dc3b21f609..ff3039b7ade 100644 --- a/src_assets/common/assets/web/configs/tabs/audiovideo/LegacyDisplayOutputSelector.vue +++ b/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayOutputSelector.vue @@ -9,7 +9,7 @@ const props = defineProps([ ]) const config = ref(props.config) -const outputNamePlaceholder = (props.platform === 'windows') ? '\\\\.\\DISPLAY1' : '0' +const outputNamePlaceholder = (props.platform === 'windows') ? '{de9bb7e2-186e-505b-9e93-f48793333810}' : '0'