diff --git a/src/windows/include/displaydevice/windows/types.h b/src/windows/include/displaydevice/windows/types.h index 096f883..369eacb 100644 --- a/src/windows/include/displaydevice/windows/types.h +++ b/src/windows/include/displaydevice/windows/types.h @@ -7,7 +7,6 @@ #include #include #include -#include #include namespace display_device { @@ -41,7 +40,7 @@ namespace display_device { * @brief Contains information about sources with identical adapter ids from matching paths. */ struct PathSourceIndexData { - std::unordered_map m_source_id_to_path_index; /**< Maps source ids to its index in the path list. */ + std::map m_source_id_to_path_index; /**< Maps source ids to its index in the path list. */ LUID m_adapter_id {}; /**< Adapter id shared by all source ids. */ std::optional m_active_source; /**< Currently active source id. */ }; diff --git a/src/windows/include/displaydevice/windows/winapiutils.h b/src/windows/include/displaydevice/windows/winapiutils.h index 2688250..b0be2db 100644 --- a/src/windows/include/displaydevice/windows/winapiutils.h +++ b/src/windows/include/displaydevice/windows/winapiutils.h @@ -220,4 +220,26 @@ namespace display_device::win_utils { */ [[nodiscard]] PathSourceIndexDataMap collectSourceDataForMatchingPaths(const WinApiLayerInterface &w_api, const std::vector &paths); + + /** + * @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 can simply ignore them (we will ask it 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_source_data Collected source data from paths. + * @param paths Display paths that were used for collecting source data. + * @return A list of path that will make up new topology, or an empty list if function fails. + */ + [[nodiscard]] std::vector + makePathsForNewTopology(const ActiveTopology &new_topology, const PathSourceIndexDataMap &path_source_data, const std::vector &paths); } // namespace display_device::win_utils diff --git a/src/windows/winapiutils.cpp b/src/windows/winapiutils.cpp index fccbba1..763205b 100644 --- a/src/windows/winapiutils.cpp +++ b/src/windows/winapiutils.cpp @@ -1,6 +1,9 @@ // header include #include "displaydevice/windows/winapiutils.h" +// system includes +#include + // local includes #include "displaydevice/logging.h" @@ -20,6 +23,21 @@ namespace { operator!=(const LUID &lhs, const LUID &rhs) { return lhs.HighPart != rhs.HighPart || lhs.LowPart != rhs.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 + toString(const LUID &id) { + return std::to_string(id.HighPart) + std::to_string(id.LowPart); + } } // namespace namespace display_device::win_utils { @@ -250,4 +268,109 @@ namespace display_device::win_utils { } return path_data; } + + std::vector + makePathsForNewTopology(const ActiveTopology &new_topology, const PathSourceIndexDataMap &path_source_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(toString(adapter_id)) }; + if (entry_it != std::end(used_source_ids_per_adapter)) { + return entry_it->second.contains(source_id); + } + + 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) -> std::optional { + auto entry_it { used_source_ids_per_adapter_per_group.find(toString(adapter_id)) }; + if (entry_it != std::end(used_source_ids_per_adapter_per_group)) { + return entry_it->second; + } + + return std::nullopt; + }; + + for (const std::string &device_id : group) { + auto path_source_data_it { path_source_data.find(device_id) }; + if (path_source_data_it == std::end(path_source_data)) { + DD_LOG(error) << "Device " << device_id << " does not exist in the available path source data!"; + return {}; + } + + std::size_t selected_path_index {}; + const auto &source_data { path_source_data_it->second }; + + const auto already_used_source_id { get_already_used_source_id_in_group(source_data.m_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_index_it { source_data.m_source_id_to_path_index.find(*already_used_source_id) }; + if (path_index_it == std::end(source_data.m_source_id_to_path_index)) { + DD_LOG(error) << "Device " << device_id << " does not have a path with a source id " << *already_used_source_id << "!"; + return {}; + } + + selected_path_index = path_index_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 should not 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. + std::optional path_index_candidate; + UINT32 used_source_id {}; + for (const auto [source_id, index] : source_data.m_source_id_to_path_index) { + if (is_source_id_already_used(source_data.m_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. + DD_LOG(error) << "Device " << device_id << " cannot be enabled as the adapter has no more free source ids (GPU limitation)!"; + return {}; + } + + selected_path_index = *path_index_candidate; + used_source_ids_per_adapter[toString(source_data.m_adapter_id)].insert(used_source_id); + used_source_ids_per_adapter_per_group[toString(source_data.m_adapter_id)] = used_source_id; + } + + if (selected_path_index >= paths.size()) { + DD_LOG(error) << "Selected path index " << selected_path_index << " is out of range! List size: " << paths.size(); + return {}; + } + + auto selected_path { paths[selected_path_index] }; + + // All the indexes must be cleared and only the group id specified + win_utils::setSourceIndex(selected_path, std::nullopt); + win_utils::setTargetIndex(selected_path, std::nullopt); + win_utils::setDesktopIndex(selected_path, std::nullopt); + win_utils::setCloneGroupId(selected_path, group_id); + win_utils::setActive(selected_path); // We also need to mark it as active... + + new_paths.push_back(selected_path); + } + + group_id++; + } + + return new_paths; + } } // namespace display_device::win_utils diff --git a/tests/unit/windows/test_winapiutils.cpp b/tests/unit/windows/test_winapiutils.cpp index 3722476..e65767a 100644 --- a/tests/unit/windows/test_winapiutils.cpp +++ b/tests/unit/windows/test_winapiutils.cpp @@ -19,27 +19,39 @@ namespace { #define TEST_F_S_MOCKED(...) DD_MAKE_TEST(TEST_F, WinApiUtilsMocked, __VA_ARGS__) // Additional convenience global const(s) - DISPLAYCONFIG_PATH_INFO AVAILABLE_AND_ACTIVE_PATH { + const DISPLAYCONFIG_PATH_INFO AVAILABLE_AND_ACTIVE_PATH { []() { DISPLAYCONFIG_PATH_INFO path; path.targetInfo.targetAvailable = TRUE; path.flags = DISPLAYCONFIG_PATH_ACTIVE; + // Some arbitrary "valid" values for comparison after they have been set/reset. + display_device::win_utils::setSourceIndex(path, 123); + display_device::win_utils::setTargetIndex(path, 456); + display_device::win_utils::setDesktopIndex(path, 789); + display_device::win_utils::setCloneGroupId(path, std::nullopt); + return path; }() }; - DISPLAYCONFIG_PATH_INFO AVAILABLE_AND_INACTIVE_PATH { + const DISPLAYCONFIG_PATH_INFO AVAILABLE_AND_INACTIVE_PATH { []() { DISPLAYCONFIG_PATH_INFO path; path.targetInfo.targetAvailable = TRUE; path.flags = ~DISPLAYCONFIG_PATH_ACTIVE; + // Some arbitrary "valid" values for comparison after they have been set/reset. + display_device::win_utils::setSourceIndex(path, std::nullopt); + display_device::win_utils::setTargetIndex(path, std::nullopt); + display_device::win_utils::setDesktopIndex(path, std::nullopt); + display_device::win_utils::setCloneGroupId(path, std::nullopt); + return path; }() }; - std::vector TARGET_AND_SOURCE_MODES { + const std::vector TARGET_AND_SOURCE_MODES { []() { std::vector modes; @@ -54,13 +66,13 @@ namespace { return modes; }() }; - std::vector PATHS_WITH_SOURCE_IDS { + const std::vector PATHS_WITH_SOURCE_IDS { []() { // Contains the following: - // - 2 paths for the same adapter (1 active and 1 inactive) + // - 4 paths for the same adapter (1 active and 3 inactive) // Note: source ids are out of order which is OK // - 1 active path for a different adapter - // - 1 inactive path for yet another different adapter + // - 2 inactive path for yet another different adapter std::vector paths; paths.push_back(AVAILABLE_AND_ACTIVE_PATH); @@ -79,19 +91,88 @@ namespace { paths.back().sourceInfo.adapterId = { 3, 3 }; paths.back().sourceInfo.id = 4; + paths.push_back(AVAILABLE_AND_INACTIVE_PATH); + paths.back().sourceInfo.adapterId = { 2, 2 }; + paths.back().sourceInfo.id = 1; + + paths.push_back(AVAILABLE_AND_INACTIVE_PATH); + paths.back().sourceInfo.adapterId = { 1, 1 }; + paths.back().sourceInfo.id = 0; + + paths.push_back(AVAILABLE_AND_INACTIVE_PATH); + paths.back().sourceInfo.adapterId = { 1, 1 }; + paths.back().sourceInfo.id = 1; + return paths; }() }; + const display_device::PathSourceIndexDataMap EXPECTED_SOURCE_INDEX_DATA { + // Contains the expected data if generated from PATHS_WITH_SOURCE_IDS and some + // sensibly chosen device paths and device ids. + { "DeviceId1", { { { 0, 2 }, { 1, 0 } }, { 1, 1 }, { 1 } } }, + { "DeviceId2", { { { 0, 1 }, { 1, 4 } }, { 2, 2 }, { 0 } } }, + { "DeviceId3", { { { 4, 3 } }, { 3, 3 }, std::nullopt } }, + { "DeviceId4", { { { 0, 5 }, { 1, 6 } }, { 1, 1 }, std::nullopt } } + }; + + // Helper functions + void + wipeIndexesAndActivatePaths(std::vector &paths) { + for (auto &path : paths) { + display_device::win_utils::setSourceIndex(path, std::nullopt); + display_device::win_utils::setTargetIndex(path, std::nullopt); + display_device::win_utils::setDesktopIndex(path, std::nullopt); + display_device::win_utils::setActive(path); + } + } } // namespace -namespace display_device { - // Helper comparison functions - bool - operator==(const LUID &lhs, const LUID &rhs) { - return lhs.HighPart == rhs.HighPart && lhs.LowPart == rhs.LowPart; - } +// Helper comparison operators +bool +operator==(const LUID &lhs, const LUID &rhs) { + return lhs.HighPart == rhs.HighPart && lhs.LowPart == rhs.LowPart; +} +bool +operator==(const DISPLAYCONFIG_RATIONAL &lhs, const DISPLAYCONFIG_RATIONAL &rhs) { + return lhs.Denominator == rhs.Denominator && lhs.Numerator == rhs.Numerator; +} + +bool +operator==(const DISPLAYCONFIG_PATH_SOURCE_INFO &lhs, const DISPLAYCONFIG_PATH_SOURCE_INFO &rhs) { + // clang-format off + return lhs.adapterId == rhs.adapterId && + lhs.id == rhs.id && + lhs.cloneGroupId == rhs.cloneGroupId && + lhs.sourceModeInfoIdx == rhs.sourceModeInfoIdx && + lhs.statusFlags == rhs.statusFlags; + // clang-format on +} + +bool +operator==(const DISPLAYCONFIG_PATH_TARGET_INFO &lhs, const DISPLAYCONFIG_PATH_TARGET_INFO &rhs) { + // clang-format off + return lhs.adapterId == rhs.adapterId && + lhs.id == rhs.id && + lhs.desktopModeInfoIdx == rhs.desktopModeInfoIdx && + lhs.targetModeInfoIdx == rhs.targetModeInfoIdx && + lhs.outputTechnology == rhs.outputTechnology && + lhs.rotation == rhs.rotation && + lhs.scaling == rhs.scaling && + lhs.refreshRate == rhs.refreshRate && + lhs.scanLineOrdering == rhs.scanLineOrdering && + lhs.targetAvailable == rhs.targetAvailable && + lhs.statusFlags == rhs.statusFlags; + // clang-format on +} + +bool +operator==(const DISPLAYCONFIG_PATH_INFO &lhs, const DISPLAYCONFIG_PATH_INFO &rhs) { + return lhs.sourceInfo == rhs.sourceInfo && lhs.targetInfo == rhs.targetInfo && lhs.flags == rhs.flags; +} + +namespace display_device { bool operator==(const PathSourceIndexData &lhs, const PathSourceIndexData &rhs) { return lhs.m_source_id_to_path_index == rhs.m_source_id_to_path_index && lhs.m_adapter_id == rhs.m_adapter_id && lhs.m_active_source == rhs.m_active_source; @@ -359,49 +440,55 @@ TEST_F_S_MOCKED(GetDeviceInfo, UnavailablePath, ActivePath) { TEST_F_S_MOCKED(CollectSourceDataForMatchingPaths) { EXPECT_CALL(m_layer, getMonitorDevicePath(_)) - .Times(4) + .Times(7) .WillOnce(Return("Path1")) .WillOnce(Return("Path2")) .WillOnce(Return("Path1")) - .WillOnce(Return("Path3")); + .WillOnce(Return("Path3")) + .WillOnce(Return("Path2")) + .WillOnce(Return("Path4")) + .WillOnce(Return("Path4")); EXPECT_CALL(m_layer, getDisplayName(_)) - .Times(4) + .Times(7) .WillRepeatedly(Return("DisplayNameX")); EXPECT_CALL(m_layer, getDeviceId(_)) - .Times(4) + .Times(7) .WillOnce(Return("DeviceId1")) .WillOnce(Return("DeviceId2")) .WillOnce(Return("DeviceId1")) - .WillOnce(Return("DeviceId3")); + .WillOnce(Return("DeviceId3")) + .WillOnce(Return("DeviceId2")) + .WillOnce(Return("DeviceId4")) + .WillOnce(Return("DeviceId4")); - const display_device::PathSourceIndexDataMap expected_data { - { "DeviceId1", { { { 0, 2 }, { 1, 0 } }, { 1, 1 }, { 1 } } }, - { "DeviceId2", { { { 0, 1 } }, { 2, 2 }, { 0 } } }, - { "DeviceId3", { { { 4, 3 } }, { 3, 3 }, std::nullopt } } - }; - EXPECT_EQ(display_device::win_utils::collectSourceDataForMatchingPaths(m_layer, PATHS_WITH_SOURCE_IDS), expected_data); + EXPECT_EQ(display_device::win_utils::collectSourceDataForMatchingPaths(m_layer, PATHS_WITH_SOURCE_IDS), EXPECTED_SOURCE_INDEX_DATA); } TEST_F_S_MOCKED(CollectSourceDataForMatchingPaths, TransientPathIssues) { EXPECT_CALL(m_layer, getMonitorDevicePath(_)) - .Times(4) + .Times(7) .WillOnce(Return("Path1")) - .WillOnce(Return("")) // Path is not available for some reason + .WillOnce(Return("Path2")) .WillOnce(Return("Path1")) - .WillOnce(Return("Path3")); + .WillOnce(Return("")) // Path is not available for some reason + .WillOnce(Return("Path2")) + .WillOnce(Return("Path4")) + .WillOnce(Return("Path4")); EXPECT_CALL(m_layer, getDisplayName(_)) - .Times(3) + .Times(6) .WillRepeatedly(Return("DisplayNameX")); EXPECT_CALL(m_layer, getDeviceId(_)) - .Times(3) + .Times(6) .WillOnce(Return("DeviceId1")) + .WillOnce(Return("DeviceId2")) .WillOnce(Return("DeviceId1")) - .WillOnce(Return("DeviceId3")); + .WillOnce(Return("DeviceId2")) + .WillOnce(Return("DeviceId4")) + .WillOnce(Return("DeviceId4")); + + display_device::PathSourceIndexDataMap expected_data { EXPECTED_SOURCE_INDEX_DATA }; + expected_data.erase(expected_data.find("DeviceId3")); - const display_device::PathSourceIndexDataMap expected_data { - { "DeviceId1", { { { 0, 2 }, { 1, 0 } }, { 1, 1 }, { 1 } } }, - { "DeviceId3", { { { 4, 3 } }, { 3, 3 }, std::nullopt } } - }; EXPECT_EQ(display_device::win_utils::collectSourceDataForMatchingPaths(m_layer, PATHS_WITH_SOURCE_IDS), expected_data); } @@ -542,3 +629,119 @@ TEST_F_S_MOCKED(CollectSourceDataForMatchingPaths, EmptyList) { const display_device::PathSourceIndexDataMap expected_data {}; EXPECT_EQ(display_device::win_utils::collectSourceDataForMatchingPaths(m_layer, paths), expected_data); } + +TEST_F_S_MOCKED(MakePathsForNewTopology) { + const display_device::ActiveTopology new_topology { { "DeviceId1" }, { "DeviceId2" }, { "DeviceId3", "DeviceId4" } }; + const std::vector paths { PATHS_WITH_SOURCE_IDS }; + + std::vector expected_paths { { paths.at(0), paths.at(1), paths.at(3), paths.at(5) } }; + wipeIndexesAndActivatePaths(expected_paths); + + display_device::win_utils::setCloneGroupId(expected_paths.at(0), 0); + display_device::win_utils::setCloneGroupId(expected_paths.at(1), 1); + display_device::win_utils::setCloneGroupId(expected_paths.at(2), 2); + display_device::win_utils::setCloneGroupId(expected_paths.at(3), 2); + + EXPECT_EQ(display_device::win_utils::makePathsForNewTopology(new_topology, EXPECTED_SOURCE_INDEX_DATA, paths), expected_paths); +} + +TEST_F_S_MOCKED(MakePathsForNewTopology, DevicesFromSameAdapterInAGroup) { + const display_device::ActiveTopology new_topology { { "DeviceId1", "DeviceId4" } }; + const std::vector paths { PATHS_WITH_SOURCE_IDS }; + + std::vector expected_paths { paths.at(0), paths.at(6) }; + wipeIndexesAndActivatePaths(expected_paths); + + display_device::win_utils::setCloneGroupId(expected_paths.at(0), 0); + display_device::win_utils::setCloneGroupId(expected_paths.at(1), 0); + + EXPECT_EQ(display_device::win_utils::makePathsForNewTopology(new_topology, EXPECTED_SOURCE_INDEX_DATA, paths), expected_paths); +} + +TEST_F_S_MOCKED(MakePathsForNewTopology, UnknownDeviceInNewTopology) { + const display_device::ActiveTopology new_topology { { "DeviceIdX", "DeviceId4" } }; + + const std::vector expected_paths {}; + EXPECT_EQ(display_device::win_utils::makePathsForNewTopology(new_topology, EXPECTED_SOURCE_INDEX_DATA, PATHS_WITH_SOURCE_IDS), expected_paths); +} + +TEST_F_S_MOCKED(MakePathsForNewTopology, MissingPathsForDuplicatedDisplays) { + // There must be N-1 (up to a GPU limit) amount of source ids (for each path/deviceId combination) available. + // For the same adapter, only devices with matching ids can be grouped (duplicated). + // In this case, have only 0 and 1 ids. You may also notice that 0 != 1, and thus we cannot group them. + const display_device::ActiveTopology new_topology { { "DeviceId1", "DeviceId2" } }; + std::vector paths {}; + + paths.push_back(AVAILABLE_AND_ACTIVE_PATH); + paths.back().sourceInfo.adapterId = { 1, 1 }; + paths.back().sourceInfo.id = 0; + + paths.push_back(AVAILABLE_AND_INACTIVE_PATH); + paths.back().sourceInfo.adapterId = { 1, 1 }; + paths.back().sourceInfo.id = 1; + + const display_device::PathSourceIndexDataMap path_source_data { + { "DeviceId1", { { { 0, 0 } }, { 1, 1 }, { 0 } } }, + { "DeviceId2", { { { 1, 1 } }, { 1, 1 }, std::nullopt } } + }; + + const std::vector expected_paths {}; + EXPECT_EQ(display_device::win_utils::makePathsForNewTopology(new_topology, path_source_data, paths), expected_paths); +} + +TEST_F_S_MOCKED(MakePathsForNewTopology, GpuLimit, DuplicatedDisplays) { + // We can only render 1 source, however since they are duplicated, source is reused + // and can be rendered to different devices. + const display_device::ActiveTopology new_topology { { "DeviceId1", "DeviceId2" } }; + std::vector paths {}; + + paths.push_back(AVAILABLE_AND_ACTIVE_PATH); + paths.back().sourceInfo.adapterId = { 1, 1 }; + paths.back().sourceInfo.id = 0; + + paths.push_back(AVAILABLE_AND_INACTIVE_PATH); + paths.back().sourceInfo.adapterId = { 1, 1 }; + paths.back().sourceInfo.id = 0; + + const display_device::PathSourceIndexDataMap path_source_data { + { "DeviceId1", { { { 0, 0 } }, { 1, 1 }, { 0 } } }, + { "DeviceId2", { { { 0, 1 } }, { 1, 1 }, std::nullopt } } + }; + + std::vector expected_paths { paths.at(0), paths.at(1) }; + wipeIndexesAndActivatePaths(expected_paths); + + display_device::win_utils::setCloneGroupId(expected_paths.at(0), 0); + display_device::win_utils::setCloneGroupId(expected_paths.at(1), 0); + + EXPECT_EQ(display_device::win_utils::makePathsForNewTopology(new_topology, path_source_data, paths), expected_paths); +} + +TEST_F_S_MOCKED(MakePathsForNewTopology, GpuLimit, ExtendedDisplays) { + // We can only render 1 source and since want extended displays, we must have 2 and that's impossible. + const display_device::ActiveTopology new_topology { { "DeviceId1" }, { "DeviceId2" } }; + std::vector paths {}; + + paths.push_back(AVAILABLE_AND_ACTIVE_PATH); + paths.back().sourceInfo.adapterId = { 1, 1 }; + paths.back().sourceInfo.id = 0; + + paths.push_back(AVAILABLE_AND_INACTIVE_PATH); + paths.back().sourceInfo.adapterId = { 1, 1 }; + paths.back().sourceInfo.id = 0; + + const display_device::PathSourceIndexDataMap path_source_data { + { "DeviceId1", { { { 0, 0 } }, { 1, 1 }, { 0 } } }, + { "DeviceId2", { { { 0, 1 } }, { 1, 1 }, std::nullopt } } + }; + + const std::vector expected_paths {}; + EXPECT_EQ(display_device::win_utils::makePathsForNewTopology(new_topology, path_source_data, paths), expected_paths); +} + +TEST_F_S_MOCKED(MakePathsForNewTopology, IndexOutOfRange) { + const display_device::ActiveTopology new_topology { { "DeviceId1", "DeviceId4" } }; + + const std::vector expected_paths {}; + EXPECT_EQ(display_device::win_utils::makePathsForNewTopology(new_topology, EXPECTED_SOURCE_INDEX_DATA, {}), expected_paths); +}