From ffef0a3daa3b56e481d87699f618f599b175844e Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Thu, 8 Feb 2024 23:25:50 +0200 Subject: [PATCH] Use more persistent device ids instead of just paths --- .../windows/display_device/windows_utils.cpp | 239 ++++++++++++++++-- src_assets/common/assets/web/config.html | 12 +- 2 files changed, 227 insertions(+), 24 deletions(-) diff --git a/src/platform/windows/display_device/windows_utils.cpp b/src/platform/windows/display_device/windows_utils.cpp index 864a452ce3b..16b6428bc91 100644 --- a/src/platform/windows/display_device/windows_utils.cpp +++ b/src/platform/windows/display_device/windows_utils.cpp @@ -1,3 +1,9 @@ +// lib includes +#include +#include +#include +#include + // standard includes #include #include @@ -6,6 +12,9 @@ #include "src/main.h" #include "windows_utils.h" +// Windows includes after "windows.h" +#include + namespace display_device { namespace w_utils { @@ -22,6 +31,117 @@ namespace display_device { return conv.to_bytes(str); } + 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_ccd_error_string(result) << " failed to get target device name!"; + return {}; + } + + return std::wstring { target_name.monitorDevicePath }; + } + + std::string + get_generic_error_string(const DWORD error_code) { + std::stringstream error; + error << "[code: "; + error << error_code; + error << ", message: " << std::system_category().message(static_cast(error_code)) << "]"; + return error.str(); + } + + 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_generic_error_string(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_generic_error_string(GetLastError()) << " \"SetupDiGetDeviceInterfaceDetailW\" failed."; + return false; + } + + dev_interface_path = std::wstring { detail_data->DevicePath }; + return !dev_interface_path.empty(); + } + + 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_generic_error_string(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_generic_error_string(GetLastError()) << " \"SetupDiGetDeviceInstanceIdW\" failed."; + return false; + } + + return !instance_id.empty(); + } + + 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_generic_error_string(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_generic_error_string(status) << " \"RegCloseKey\" failed."; + } + }) + }; + + DWORD required_size_in_bytes { 0 }; + auto status { RegQueryValueExW(reg_key, L"EDID", NULL, nullptr, nullptr, &required_size_in_bytes) }; + if (status != ERROR_SUCCESS) { + BOOST_LOG(error) << get_generic_error_string(status) << " \"RegQueryValueExW\" failed when getting size."; + return false; + } + + edid.resize(required_size_in_bytes); + + status = RegQueryValueExW(reg_key, L"EDID", NULL, nullptr, edid.data(), &required_size_in_bytes); + if (status != ERROR_SUCCESS) { + BOOST_LOG(error) << get_generic_error_string(status) << " \"RegQueryValueExW\" failed when getting size."; + return false; + } + + return !edid.empty(); + } + } // namespace std::string @@ -95,28 +215,111 @@ namespace display_device { std::string get_device_id(const DISPLAYCONFIG_PATH_INFO &path) { - // This is not the prettiest id there is, but it seems to be unique. - // The MONITOR ID that MultiMonitorTool uses is not always unique in some combinations, so we'll just go with the device path. - auto device_id { get_monitor_device_path(path) }; - std::replace(std::begin(device_id), std::end(device_id), '#', '-'); // Hashtags are not supported by Sunshine config - return device_id; - } + const auto device_path { get_monitor_device_path_wstr(path) }; + if (device_path.empty()) { + // Error already logged + return {}; + } - std::string - get_monitor_device_path(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); + 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_generic_error_string(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_generic_error_string(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 reinstalls. + // 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 list) - 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 - unstable + // + // 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 + auto index = instance_id.find_first_of(L'&', 0); + if (index != std::wstring::npos) { + index++; + index = index < instance_id.size() ? instance_id.find_first_of(L'&', index) : std::wstring::npos; + } + + if (index == std::wstring::npos) { + BOOST_LOG(error) << "failed to split the instance id string " << convert_to_string(instance_id); + break; + } + + BOOST_LOG(verbose) << "creating device id for path " << convert_to_string(device_path) << " from EDID and instance ID: " << convert_to_string({ std::begin(instance_id), std::begin(instance_id) + index }); + device_id_data.insert(std::end(device_id_data), + reinterpret_cast(instance_id.data()), + reinterpret_cast(instance_id.data() + index)); + break; + } + } - LONG result { DisplayConfigGetDeviceInfo(&target_name.header) }; - if (result != ERROR_SUCCESS) { - BOOST_LOG(error) << get_ccd_error_string(result) << " failed to get target device name!"; - return {}; + 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 " << convert_to_string(device_path); + device_id_data.insert(std::end(device_id_data), + reinterpret_cast(device_path.data()), + reinterpret_cast(device_path.data() + device_path.size())); } - return convert_to_string(target_name.monitorDevicePath); + 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 convert_to_string(get_monitor_device_path_wstr(path)); } std::string diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 99cd1c64420..b3369a478ce 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -471,14 +471,14 @@

Manually specify a display device id to use for capture. If unset, the primary display is captured.
Note: If you specified a GPU above, this display must be connected to that GPU.

During Sunshine startup, you should see the list of detected display devices and their ids, e.g.:
-     DEVICE ID: \\?\DISPLAY-ACI27EC-5&4fd2de4&2&UID4355-{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}
+     DEVICE ID: {de9bb7e2-186e-505b-9e93-f48793333810}
    DISPLAY NAME: \\.\DISPLAY1
    FRIENDLY NAME: ROG PG279Q
    DEVICE STATE: PRIMARY
@@ -522,10 +522,10 @@

Windows saves various display settings for each combination of currently active displays.
Sunshine applies changes to a display(-s) belonging to such a display combination.
- If you disconnect a device which was active when Sunshine applied the settings, the changes will not be reverted
- back unless the combination is active again by the time Sunshine tries to revert changes!
- The same is true if you connect a new device and Windows decides to activate it, which will
- change the active display combination. + If you disconnect a device which was active when Sunshine applied the settings, the changes will not be
+ reverted back unless the combination is active again by the time Sunshine tries to revert changes!
+ The same is true if you connect a new device and Windows decides to activate it, which will change
+ the active display combination.