From 1f8a237f665ed638407676c062362b78918ab5c1 Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Sun, 5 May 2024 16:33:36 +0300 Subject: [PATCH 1/6] feat: add more methods to WinApiLayer --- .gitmodules | 12 +- CMakeLists.txt | 3 + cmake/Boost_DD.cmake | 25 ++ src/windows/CMakeLists.txt | 5 +- .../displaydevice/windows/winapilayer.h | 16 + .../windows/winapilayerinterface.h | 105 ++++++ src/windows/winapilayer.cpp | 312 ++++++++++++++++++ tests/unit/windows/test_winapilayer.cpp | 118 ++++++- third-party/boost | 1 + 9 files changed, 585 insertions(+), 12 deletions(-) create mode 100644 cmake/Boost_DD.cmake create mode 160000 third-party/boost diff --git a/.gitmodules b/.gitmodules index 79ca3b6..c0de96b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,8 +1,12 @@ -[submodule "third-party/googletest"] - path = third-party/googletest - url = https://github.com/google/googletest.git - branch = v1.14.x +[submodule "third-party/boost"] + path = third-party/boost + url = https://github.com/boostorg/boost.git + branch = master [submodule "third-party/json"] path = third-party/json url = https://github.com/nlohmann/json.git branch = master +[submodule "third-party/googletest"] + path = third-party/googletest + url = https://github.com/google/googletest.git + branch = v1.14.x diff --git a/CMakeLists.txt b/CMakeLists.txt index 8947e6e..0153e62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,9 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) endif() +# Add our custom CMake modules to the global path +list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) + # # Project optional configuration # diff --git a/cmake/Boost_DD.cmake b/cmake/Boost_DD.cmake new file mode 100644 index 0000000..54afcb2 --- /dev/null +++ b/cmake/Boost_DD.cmake @@ -0,0 +1,25 @@ +# +# Loads the boost library giving the priority to the system package first, with a fallback +# to the submodule. +# +include_guard(GLOBAL) + +find_package(Boost 1.85) +if(NOT Boost_FOUND) + message(STATUS "Boost v1.85.x package not found in system. Falling back to submodule.") + + # Limit boost to the required libraries only + set(BOOST_INCLUDE_LIBRARIES scope algorithm uuid) + add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../third-party/boost third-party/boost EXCLUDE_FROM_ALL) + + # Emulate the "find_package" target + add_library(Boost::boost INTERFACE IMPORTED) + + file(GLOB BOOST_LIBS CONFIGURE_DEPENDS "${CMAKE_CURRENT_LIST_DIR}/../third-party/boost/libs/*") + foreach(lib_path ${BOOST_LIBS}) + get_filename_component(lib_dir "${lib_path}" NAME) + if(TARGET "Boost::${lib_dir}") + target_link_libraries(Boost::boost INTERFACE "Boost::${lib_dir}") + endif() + endforeach() +endif() diff --git a/src/windows/CMakeLists.txt b/src/windows/CMakeLists.txt index 2af2f2f..b938d7e 100644 --- a/src/windows/CMakeLists.txt +++ b/src/windows/CMakeLists.txt @@ -11,5 +11,8 @@ add_library(${MODULE} ${HEADER_LIST} ${SOURCE_LIST}) # Provide the includes together with this library target_include_directories(${MODULE} PUBLIC include) +# Additional external libraries +include(Boost_DD) + # Link the additional libraries -target_link_libraries(${MODULE} PRIVATE libcommon) +target_link_libraries(${MODULE} PRIVATE libcommon Boost::boost setupapi) diff --git a/src/windows/include/displaydevice/windows/winapilayer.h b/src/windows/include/displaydevice/windows/winapilayer.h index e354e76..c850d0e 100644 --- a/src/windows/include/displaydevice/windows/winapilayer.h +++ b/src/windows/include/displaydevice/windows/winapilayer.h @@ -16,5 +16,21 @@ namespace display_device { /** For details @see WinApiLayerInterface::query_display_config */ [[nodiscard]] std::optional query_display_config(query_type_e type) const override; + + /** For details @see WinApiLayerInterface::get_device_id */ + [[nodiscard]] std::string + get_device_id(const DISPLAYCONFIG_PATH_INFO &path) const override; + + /** For details @see WinApiLayerInterface::get_monitor_device_path */ + [[nodiscard]] std::string + get_monitor_device_path(const DISPLAYCONFIG_PATH_INFO &path) const override; + + /** For details @see WinApiLayerInterface::get_friendly_name */ + [[nodiscard]] std::string + get_friendly_name(const DISPLAYCONFIG_PATH_INFO &path) const override; + + /** For details @see WinApiLayerInterface::get_display_name */ + [[nodiscard]] std::string + get_display_name(const DISPLAYCONFIG_PATH_INFO &path) const override; }; } // namespace display_device diff --git a/src/windows/include/displaydevice/windows/winapilayerinterface.h b/src/windows/include/displaydevice/windows/winapilayerinterface.h index 2c46fd4..dc89717 100644 --- a/src/windows/include/displaydevice/windows/winapilayerinterface.h +++ b/src/windows/include/displaydevice/windows/winapilayerinterface.h @@ -62,5 +62,110 @@ namespace display_device { */ [[nodiscard]] virtual std::optional query_display_config(query_type_e type) const = 0; + + /** + * @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 WinApiLayerInterface* iface = getIface(...); + * const std::string device_path = iface->get_device_id(path); + * ``` + */ + [[nodiscard]] virtual std::string + get_device_id(const DISPLAYCONFIG_PATH_INFO &path) const = 0; + + /** + * @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 WinApiLayerInterface* iface = getIface(...); + * const std::string device_path = iface->get_monitor_device_path(path); + * ``` + */ + [[nodiscard]] virtual std::string + get_monitor_device_path(const DISPLAYCONFIG_PATH_INFO &path) const = 0; + + /** + * @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 taken from EDID. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const WinApiLayerInterface* iface = getIface(...); + * const std::string friendly_name = iface->get_friendly_name(path); + * ``` + */ + [[nodiscard]] virtual std::string + get_friendly_name(const DISPLAYCONFIG_PATH_INFO &path) const = 0; + + /** + * @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 WinApiLayerInterface* iface = getIface(...); + * const std::string display_name = iface->get_display_name(path); + * ``` + */ + [[nodiscard]] virtual std::string + get_display_name(const DISPLAYCONFIG_PATH_INFO &path) const = 0; }; } // namespace display_device diff --git a/src/windows/winapilayer.cpp b/src/windows/winapilayer.cpp index dbc1da4..cdec847 100644 --- a/src/windows/winapilayer.cpp +++ b/src/windows/winapilayer.cpp @@ -2,11 +2,20 @@ #include "displaydevice/windows/winapilayer.h" // system includes +#include +#include +#include +#include +#include +#include #include // local includes #include "displaydevice/logging.h" +// Windows includes after "windows.h" +#include + namespace display_device { namespace { /** @brief Dumps the result of @see query_display_config into a string */ @@ -119,6 +128,159 @@ namespace display_device { return output.str(); } + + /** + * @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 WinApiLayerInterface &w_api, 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) { + DD_LOG(error) << w_api.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(const WinApiLayerInterface &w_api, 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)) { + DD_LOG(error) << "\"SetupDiGetDeviceInterfaceDetailW\" did not fail, what?!"; + return false; + } + else if (required_size_in_bytes <= 0) { + DD_LOG(error) << w_api.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)) { + DD_LOG(error) << w_api.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(const WinApiLayerInterface &w_api, 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)) { + DD_LOG(error) << "\"SetupDiGetDeviceInstanceIdW\" did not fail, what?!"; + return false; + } + else if (required_size_in_characters <= 0) { + DD_LOG(error) << w_api.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)) { + DD_LOG(error) << w_api.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(const WinApiLayerInterface &w_api, 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) { + DD_LOG(error) << w_api.get_error_string(static_cast(GetLastError())) << " \"SetupDiOpenDevRegKey\" failed."; + return false; + } + + const auto reg_key_cleanup { + boost::scope::scope_exit([&w_api, ®_key]() { + const auto status { RegCloseKey(reg_key) }; + if (status != ERROR_SUCCESS) { + DD_LOG(error) << w_api.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) { + DD_LOG(error) << w_api.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) { + DD_LOG(error) << w_api.get_error_string(status) << " \"RegQueryValueExW\" failed when getting size."; + return false; + } + + return !edid.empty(); + } + + /** + * @brief Converts a UTF-16 wide string into a UTF-8 string. + * @param w_api Reference to the WinApiLayer. + * @param value The UTF-16 wide string. + * @return The converted UTF-8 string. + */ + std::string + to_utf8(const WinApiLayerInterface &w_api, const std::wstring &value) { + // No conversion needed if the string is empty + if (value.empty()) { + return {}; + } + + // Get the output size required to store the string + auto output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), nullptr, 0, nullptr, nullptr); + if (output_size == 0) { + DD_LOG(error) << w_api.get_error_string(static_cast(GetLastError())) << " failed to get UTF-8 buffer size."; + return {}; + } + + // Perform the conversion + std::string output(output_size, '\0'); + output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), output.data(), static_cast(output.size()), nullptr, nullptr); + if (output_size == 0) { + DD_LOG(error) << w_api.get_error_string(static_cast(GetLastError())) << " failed to convert string to UTF-8."; + return {}; + } + + return output; + } } // namespace std::string @@ -195,4 +357,154 @@ namespace display_device { return path_and_mode_data_t { paths, modes }; } + std::string + WinApiLayer::get_device_id(const DISPLAYCONFIG_PATH_INFO &path) const { + const auto device_path { get_monitor_device_path_wstr(*this, 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 { + boost::scope::scope_exit([this, &dev_info_handle]() { + if (!SetupDiDestroyDeviceInfoList(dev_info_handle)) { + DD_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; + } + + DD_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(*this, 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(*this, dev_info_handle, dev_info_data, instance_id)) { + // Error already logged + break; + } + + if (!get_device_edid(*this, 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) { + DD_LOG(error) << "Failed to split off the stable part from instance id string " << to_utf8(*this, 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) { + DD_LOG(error) << "Failed to split off the semi-stable part from instance id string " << to_utf8(*this, instance_id); + break; + } + + DD_LOG(verbose) << "Creating device id for path " << to_utf8(*this, device_path) << " from EDID and instance ID: " << to_utf8(*this, { std::begin(instance_id), std::begin(instance_id) + unstable_part_index }) << to_utf8(*this, { std::begin(instance_id) + semi_stable_part_index, std::end(instance_id) }); // NOLINT(*-narrowing-conversions) + 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 + DD_LOG(verbose) << "Creating device id from path " << to_utf8(*this, 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 + WinApiLayer::get_monitor_device_path(const DISPLAYCONFIG_PATH_INFO &path) const { + return to_utf8(*this, get_monitor_device_path_wstr(*this, path)); + } + + std::string + WinApiLayer::get_friendly_name(const DISPLAYCONFIG_PATH_INFO &path) const { + 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) { + DD_LOG(error) << get_error_string(result) << " failed to get target device name!"; + return {}; + } + + return target_name.flags.friendlyNameFromEdid ? to_utf8(*this, target_name.monitorFriendlyDeviceName) : std::string {}; + } + + std::string + WinApiLayer::get_display_name(const DISPLAYCONFIG_PATH_INFO &path) const { + 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) { + DD_LOG(error) << get_error_string(result) << " failed to get display name! "; + return {}; + } + + return to_utf8(*this, source_name.viewGdiDeviceName); + } } // namespace display_device diff --git a/tests/unit/windows/test_winapilayer.cpp b/tests/unit/windows/test_winapilayer.cpp index bee9d91..869d98c 100644 --- a/tests/unit/windows/test_winapilayer.cpp +++ b/tests/unit/windows/test_winapilayer.cpp @@ -20,8 +20,8 @@ TEST(WinApiLayer, QueryDisplayConfigPathAndModeCount) { const auto active_devices { layer.query_display_config(display_device::WinApiLayer::query_type_e::Active) }; const auto all_devices { layer.query_display_config(display_device::WinApiLayer::query_type_e::All) }; - EXPECT_TRUE(active_devices); - EXPECT_TRUE(all_devices); + ASSERT_TRUE(active_devices); + ASSERT_TRUE(all_devices); // This test (and some others) is pointless without any paths. We should always have at least 1 active display device! EXPECT_TRUE(!active_devices->paths.empty()); @@ -33,7 +33,7 @@ TEST(WinApiLayer, QueryDisplayConfigPathActivePaths) { const display_device::WinApiLayer layer; const auto active_devices { layer.query_display_config(display_device::WinApiLayer::query_type_e::Active) }; - EXPECT_TRUE(active_devices); + ASSERT_TRUE(active_devices); for (const auto &path : active_devices->paths) { EXPECT_TRUE(static_cast(path.flags & DISPLAYCONFIG_PATH_ACTIVE)); @@ -53,7 +53,7 @@ TEST(WinApiLayer, QueryDisplayConfigModeIndexValidity) { const auto all_devices { layer.query_display_config(display_device::WinApiLayer::query_type_e::All) }; for (const auto &devices : { active_devices, all_devices }) { - EXPECT_TRUE(devices); + ASSERT_TRUE(devices); for (const auto &path : devices->paths) { const auto clone_group_id = path.sourceInfo.cloneGroupId; @@ -65,19 +65,123 @@ TEST(WinApiLayer, QueryDisplayConfigModeIndexValidity) { EXPECT_EQ(clone_group_id, DISPLAYCONFIG_PATH_CLONE_GROUP_INVALID); if (source_mode_index != DISPLAYCONFIG_PATH_SOURCE_MODE_IDX_INVALID) { - EXPECT_TRUE(source_mode_index < devices->modes.size()); + ASSERT_TRUE(source_mode_index < devices->modes.size()); EXPECT_EQ(devices->modes[source_mode_index].infoType, DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE); } if (target_mode_index != DISPLAYCONFIG_PATH_TARGET_MODE_IDX_INVALID) { - EXPECT_TRUE(target_mode_index < devices->modes.size()); + ASSERT_TRUE(target_mode_index < devices->modes.size()); EXPECT_EQ(devices->modes[target_mode_index].infoType, DISPLAYCONFIG_MODE_INFO_TYPE_TARGET); } if (desktop_mode_index != DISPLAYCONFIG_PATH_DESKTOP_IMAGE_IDX_INVALID) { - EXPECT_TRUE(desktop_mode_index < devices->modes.size()); + ASSERT_TRUE(desktop_mode_index < devices->modes.size()); EXPECT_EQ(devices->modes[desktop_mode_index].infoType, DISPLAYCONFIG_MODE_INFO_TYPE_DESKTOP_IMAGE); } } } } + +TEST(WinApiLayer, GetDeviceId) { + const display_device::WinApiLayer layer; + + const auto all_devices { layer.query_display_config(display_device::WinApiLayer::query_type_e::All) }; + ASSERT_TRUE(all_devices); + + std::map device_id_per_device_path; + for (const auto &path : all_devices->paths) { + const auto device_id { layer.get_device_id(path) }; + const auto device_id_2 { layer.get_device_id(path) }; + const auto device_path { layer.get_monitor_device_path(path) }; + + // Testing soft persistence - ids remain the same between calls + EXPECT_EQ(device_id, device_id_2); + + if (device_id.empty()) { + EXPECT_EQ(path.targetInfo.targetAvailable, FALSE); + } + else { + auto path_it { device_id_per_device_path.find(device_path) }; + if (path_it != std::end(device_id_per_device_path)) { + // Devices with the same paths must have the same device ids. + EXPECT_EQ(path_it->second, device_id); + } + else { + EXPECT_TRUE(device_id_per_device_path.insert({ device_path, device_id }).second); + } + } + } +} + +TEST(WinApiLayer, GetMonitorDevicePath) { + const display_device::WinApiLayer layer; + + const auto all_devices { layer.query_display_config(display_device::WinApiLayer::query_type_e::All) }; + ASSERT_TRUE(all_devices); + + std::set current_device_paths; + for (const auto &path : all_devices->paths) { + const auto device_path { layer.get_monitor_device_path(path) }; + const auto device_path_2 { layer.get_monitor_device_path(path) }; + + // Testing soft persistence - paths remain the same between calls + EXPECT_EQ(device_path, device_path_2); + + if (device_path.empty()) { + EXPECT_EQ(path.targetInfo.targetAvailable, FALSE); + } + else if (current_device_paths.contains(device_path)) { + // In case we have a duplicate device path, the path must be inactive, because + // active paths are always in the front of the list and therefore will be added + // first (only 1 active path is possible). + EXPECT_FALSE(static_cast(path.flags & DISPLAYCONFIG_PATH_ACTIVE)); + } + else { + EXPECT_TRUE(current_device_paths.insert(device_path).second); + } + } +} + +TEST(WinApiLayer, GetFriendlyName) { + const display_device::WinApiLayer layer; + + const auto all_devices { layer.query_display_config(display_device::WinApiLayer::query_type_e::All) }; + ASSERT_TRUE(all_devices); + + for (const auto &path : all_devices->paths) { + const auto friendly_name { layer.get_friendly_name(path) }; + const auto friendly_name_2 { layer.get_friendly_name(path) }; + + // Testing soft persistence - ids remain the same between calls + EXPECT_EQ(friendly_name, friendly_name_2); + + if (friendly_name.empty()) { + EXPECT_EQ(path.targetInfo.targetAvailable, FALSE); + } + else { + // Here we are making an assumption that the device will always have a friendly name, however it is not mandatory information, + // but these days almost every device should have something in the EDID... + } + + // We don't really have anything else to compare the friendly name against without going deep into EDID data. + } +} + +TEST(WinApiLayer, GetDisplayName) { + const display_device::WinApiLayer layer; + + const auto all_devices { layer.query_display_config(display_device::WinApiLayer::query_type_e::All) }; + ASSERT_TRUE(all_devices); + + for (const auto &path : all_devices->paths) { + const auto display_name { layer.get_display_name(path) }; + const auto display_name_2 { layer.get_display_name(path) }; + + // Testing soft persistence - ids remain the same between calls + EXPECT_EQ(display_name, display_name_2); + + // Display name is attached to the "source" mode (not a physical device) therefore a non-empty + // value is always expected. + EXPECT_TRUE(test_regex(display_name, R"(^\\\\.\\DISPLAY\d+$)")); + } +} diff --git a/third-party/boost b/third-party/boost new file mode 160000 index 0000000..2b5eb21 --- /dev/null +++ b/third-party/boost @@ -0,0 +1 @@ +Subproject commit 2b5eb21199724b11b43e859d4cecdddead7ea727 From c19b8f5c726872d9d26e5c6dffd240af7d57f077 Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Sun, 5 May 2024 16:37:02 +0300 Subject: [PATCH 2/6] lock boost to 1.85 --- third-party/boost | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/third-party/boost b/third-party/boost index 2b5eb21..ab7968a 160000 --- a/third-party/boost +++ b/third-party/boost @@ -1 +1 @@ -Subproject commit 2b5eb21199724b11b43e859d4cecdddead7ea727 +Subproject commit ab7968a0bbcf574a7859240d1d8443f58ed6f6cf From 4cd728cebef77b8c4dfebd48a00381c42c7bfeff Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Sun, 5 May 2024 16:52:03 +0300 Subject: [PATCH 3/6] fix UT --- tests/unit/windows/test_winapilayer.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/unit/windows/test_winapilayer.cpp b/tests/unit/windows/test_winapilayer.cpp index 869d98c..3c25f86 100644 --- a/tests/unit/windows/test_winapilayer.cpp +++ b/tests/unit/windows/test_winapilayer.cpp @@ -155,15 +155,8 @@ TEST(WinApiLayer, GetFriendlyName) { // Testing soft persistence - ids remain the same between calls EXPECT_EQ(friendly_name, friendly_name_2); - if (friendly_name.empty()) { - EXPECT_EQ(path.targetInfo.targetAvailable, FALSE); - } - else { - // Here we are making an assumption that the device will always have a friendly name, however it is not mandatory information, - // but these days almost every device should have something in the EDID... - } - // We don't really have anything else to compare the friendly name against without going deep into EDID data. + // Friendly name is also not mandatory in the EDID... } } From aa884865a7566433021bf177ee0a9efe0229ca3c Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Sun, 5 May 2024 17:48:48 +0300 Subject: [PATCH 4/6] Let's be REALLY verbose so that the log is actually useful... --- src/windows/winapilayer.cpp | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/windows/winapilayer.cpp b/src/windows/winapilayer.cpp index cdec847..09e822b 100644 --- a/src/windows/winapilayer.cpp +++ b/src/windows/winapilayer.cpp @@ -445,13 +445,31 @@ namespace display_device { break; } - DD_LOG(verbose) << "Creating device id for path " << to_utf8(*this, device_path) << " from EDID and instance ID: " << to_utf8(*this, { std::begin(instance_id), std::begin(instance_id) + unstable_part_index }) << to_utf8(*this, { std::begin(instance_id) + semi_stable_part_index, std::end(instance_id) }); // NOLINT(*-narrowing-conversions) 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())); + + static const auto dump_device_id_data { [](const auto &data) -> std::string { + if (data.empty()) { + return {}; + }; + + std::ostringstream output; + output << "["; + for (std::size_t i = 0; i < data.size(); ++i) { + output << "0x" << std::setw(2) << std::setfill('0') << std::hex << std::uppercase << static_cast(data[i]); + if (i + 1 < data.size()) { + output << " "; + } + } + output << "]"; + + return output.str(); + } }; + DD_LOG(verbose) << "Creating device id from EDID + instance ID: " << dump_device_id_data(device_id_data); break; } } @@ -466,7 +484,10 @@ namespace display_device { 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) + "}"; + const std::string device_id { "{" + boost::uuids::to_string(boost_uuid) + "}" }; + + DD_LOG(verbose) << "Created device id: " << to_utf8(*this, device_path) << " -> " << device_id; + return device_id; } std::string From d807199cb4ea89552cd6cc93098e72978688acfb Mon Sep 17 00:00:00 2001 From: Lukas Senionis Date: Sun, 5 May 2024 17:52:45 +0300 Subject: [PATCH 5/6] Update cmake/Boost_DD.cmake Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> --- cmake/Boost_DD.cmake | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmake/Boost_DD.cmake b/cmake/Boost_DD.cmake index 54afcb2..2ab942d 100644 --- a/cmake/Boost_DD.cmake +++ b/cmake/Boost_DD.cmake @@ -9,7 +9,10 @@ if(NOT Boost_FOUND) message(STATUS "Boost v1.85.x package not found in system. Falling back to submodule.") # Limit boost to the required libraries only - set(BOOST_INCLUDE_LIBRARIES scope algorithm uuid) + set(BOOST_INCLUDE_LIBRARIES + algorithm + scope + uuid) add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../third-party/boost third-party/boost EXCLUDE_FROM_ALL) # Emulate the "find_package" target From b1379be580d699791fe95de092989796599bbe0f Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Sun, 5 May 2024 17:54:20 +0300 Subject: [PATCH 6/6] ABC --- .gitmodules | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitmodules b/.gitmodules index c0de96b..9aa0741 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,11 +2,11 @@ path = third-party/boost url = https://github.com/boostorg/boost.git branch = master -[submodule "third-party/json"] - path = third-party/json - url = https://github.com/nlohmann/json.git - branch = master [submodule "third-party/googletest"] path = third-party/googletest url = https://github.com/google/googletest.git branch = v1.14.x +[submodule "third-party/json"] + path = third-party/json + url = https://github.com/nlohmann/json.git + branch = master