From c044842d24c77d069916733710d5fbf15278a139 Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Thu, 30 May 2024 22:33:04 +0300 Subject: [PATCH 1/2] feat: Implement API for changing display modes --- .../include/displaydevice/windows/types.h | 5 + .../displaydevice/windows/windisplaydevice.h | 16 +- .../windows/windisplaydeviceinterface.h | 43 +- src/windows/windisplaydevicemodes.cpp | 236 ++++++ src/windows/windisplaydevicetopology.cpp | 23 +- tests/unit/windows/test_winapiutils.cpp | 4 +- .../windows/test_windisplaydevicemodes.cpp | 796 ++++++++++++++++++ .../windows/test_windisplaydevicetopology.cpp | 45 +- tests/unit/windows/utils/comparison.cpp | 15 + tests/unit/windows/utils/comparison.h | 9 + tests/unit/windows/utils/guards.h | 10 +- tests/unit/windows/utils/helpers.cpp | 35 + tests/unit/windows/utils/helpers.h | 15 + tests/unit/windows/utils/mockwinapilayer.cpp | 14 +- 14 files changed, 1205 insertions(+), 61 deletions(-) create mode 100644 src/windows/windisplaydevicemodes.cpp create mode 100644 tests/unit/windows/test_windisplaydevicemodes.cpp create mode 100644 tests/unit/windows/utils/helpers.cpp create mode 100644 tests/unit/windows/utils/helpers.h diff --git a/src/windows/include/displaydevice/windows/types.h b/src/windows/include/displaydevice/windows/types.h index 20215a9..2d4da8e 100644 --- a/src/windows/include/displaydevice/windows/types.h +++ b/src/windows/include/displaydevice/windows/types.h @@ -86,4 +86,9 @@ namespace display_device { Rational m_refresh_rate; }; + /** + * @brief Ordered map of [DEVICE_ID -> DisplayMode]. + */ + using DeviceDisplayModeMap = std::map; + } // namespace display_device diff --git a/src/windows/include/displaydevice/windows/windisplaydevice.h b/src/windows/include/displaydevice/windows/windisplaydevice.h index 1cea632..c503f87 100644 --- a/src/windows/include/displaydevice/windows/windisplaydevice.h +++ b/src/windows/include/displaydevice/windows/windisplaydevice.h @@ -19,21 +19,29 @@ namespace display_device { */ explicit WinDisplayDevice(std::shared_ptr w_api); - /** For details @see WinDisplayDevice::getCurrentTopology */ + /** For details @see WinDisplayDeviceInterface::getCurrentTopology */ [[nodiscard]] ActiveTopology getCurrentTopology() const override; - /** For details @see WinDisplayDevice::isTopologyValid */ + /** For details @see WinDisplayDeviceInterface::isTopologyValid */ [[nodiscard]] bool isTopologyValid(const ActiveTopology &topology) const override; - /** For details @see WinDisplayDevice::getCurrentTopology */ + /** For details @see WinDisplayDeviceInterface::getCurrentTopology */ [[nodiscard]] bool isTopologyTheSame(const ActiveTopology &lhs, const ActiveTopology &rhs) const override; - /** For details @see WinDisplayDevice::setTopology */ + /** For details @see WinDisplayDeviceInterface::setTopology */ [[nodiscard]] bool setTopology(const ActiveTopology &new_topology) override; + + /** For details @see WinDisplayDeviceInterface::getCurrentDisplayModes */ + [[nodiscard]] DeviceDisplayModeMap + getCurrentDisplayModes(const std::set &device_ids) const override; + + /** For details @see WinDisplayDeviceInterface::setDisplayModes */ + [[nodiscard]] bool + setDisplayModes(const DeviceDisplayModeMap &modes) override; private: std::shared_ptr m_w_api; diff --git a/src/windows/include/displaydevice/windows/windisplaydeviceinterface.h b/src/windows/include/displaydevice/windows/windisplaydeviceinterface.h index 39bfc45..a8ac750 100644 --- a/src/windows/include/displaydevice/windows/windisplaydeviceinterface.h +++ b/src/windows/include/displaydevice/windows/windisplaydeviceinterface.h @@ -1,5 +1,8 @@ #pragma once +// system includes +#include + // local includes #include "displaydevice/windows/types.h" @@ -73,12 +76,48 @@ namespace display_device { * * EXAMPLES: * ```cpp - * auto current_topology { getCurrentTopology() }; + * const WinDisplayDeviceInterface* iface = getIface(...); + * auto current_topology { iface->getCurrentTopology() }; * // Modify the current_topology - * const bool success = setTopology(current_topology); + * const bool success = iface->setTopology(current_topology); * ``` */ [[nodiscard]] virtual bool setTopology(const ActiveTopology &new_topology) = 0; + + /** + * @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 WinDisplayDeviceInterface* iface = getIface(...); + * const std::set device_ids { "DEVICE_ID_1", "DEVICE_ID_2" }; + * const auto current_modes = iface->getCurrentDisplayModes(device_ids); + * ``` + */ + [[nodiscard]] virtual DeviceDisplayModeMap + getCurrentDisplayModes(const std::set &device_ids) const = 0; + + /** + * @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 WinDisplayDeviceInterface* iface = getIface(...); + * const std::string display_a { "MY_ID_1" }; + * const std::string display_b { "MY_ID_2" }; + * const auto success = iface->setDisplayModes({ { display_a, { { 1920, 1080 }, { 60, 1 } } }, + * { display_b, { { 1920, 1080 }, { 120, 1 } } } }); + * ``` + */ + [[nodiscard]] virtual bool + setDisplayModes(const DeviceDisplayModeMap &modes) = 0; }; } // namespace display_device diff --git a/src/windows/windisplaydevicemodes.cpp b/src/windows/windisplaydevicemodes.cpp new file mode 100644 index 0000000..41a4164 --- /dev/null +++ b/src/windows/windisplaydevicemodes.cpp @@ -0,0 +1,236 @@ +// class header include +#include "displaydevice/windows/windisplaydevice.h" + +// system includes +#include + +// local includes +#include "displaydevice/logging.h" +#include "displaydevice/windows/winapiutils.h" + +namespace display_device { + namespace { + + /** + * @brief Strategy to be used when changing display modes. + */ + enum class Strategy { + Relaxed, + Strict + }; + + /** + * @see set_display_modes for a description as this was split off to reduce cognitive complexity. + */ + bool + doSetModes(WinApiLayerInterface &w_api, const DeviceDisplayModeMap &modes, const Strategy strategy) { + auto display_data { w_api.queryDisplayConfig(QueryType::Active) }; + if (!display_data) { + // Error already logged + return false; + } + + bool changes_applied { false }; + for (const auto &[device_id, mode] : modes) { + const auto path { win_utils::getActivePath(w_api, device_id, display_data->m_paths) }; + if (!path) { + DD_LOG(error) << "Failed to find device for " << device_id << "!"; + return false; + } + + const auto source_mode { win_utils::getSourceMode(win_utils::getSourceIndex(*path, display_data->m_modes), display_data->m_modes) }; + if (!source_mode) { + DD_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.m_resolution.m_width || source_mode->height != mode.m_resolution.m_height }; + + bool refresh_rate_changed; + if (strategy == Strategy::Relaxed) { + refresh_rate_changed = !win_utils::fuzzyCompareRefreshRates(Rational { path->targetInfo.refreshRate.Numerator, path->targetInfo.refreshRate.Denominator }, mode.m_refresh_rate); + } + else { + // Since we are in strict mode, do not fuzzy compare it + refresh_rate_changed = path->targetInfo.refreshRate.Numerator != mode.m_refresh_rate.m_numerator || + path->targetInfo.refreshRate.Denominator != mode.m_refresh_rate.m_denominator; + } + + if (resolution_changed) { + source_mode->width = mode.m_resolution.m_width; + source_mode->height = mode.m_resolution.m_height; + new_changes = true; + } + + if (refresh_rate_changed) { + path->targetInfo.refreshRate = { mode.m_refresh_rate.m_numerator, mode.m_refresh_rate.m_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. + win_utils::setTargetIndex(*path, std::nullopt); + win_utils::setDesktopIndex(*path, std::nullopt); // Part of struct containing target index and so it needs to be cleared + } + + changes_applied = changes_applied || new_changes; + } + + if (!changes_applied) { + DD_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 (strategy == Strategy::Relaxed) { + // 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 { w_api.setDisplayConfig(display_data->m_paths, display_data->m_modes, flags) }; + if (result != ERROR_SUCCESS) { + DD_LOG(error) << w_api.getErrorString(result) << " failed to set display mode!"; + return false; + } + + return true; + } + } // namespace + + DeviceDisplayModeMap + WinDisplayDevice::getCurrentDisplayModes(const std::set &device_ids) const { + if (device_ids.empty()) { + DD_LOG(error) << "Device id set is empty!"; + return {}; + } + + const auto display_data { m_w_api->queryDisplayConfig(QueryType::Active) }; + if (!display_data) { + // Error already logged + return {}; + } + + DeviceDisplayModeMap current_modes; + for (const auto &device_id : device_ids) { + if (device_id.empty()) { + DD_LOG(error) << "Device id is empty!"; + return {}; + } + + const auto path { win_utils::getActivePath(*m_w_api, device_id, display_data->m_paths) }; + if (!path) { + DD_LOG(error) << "Failed to find device for " << device_id << "!"; + return {}; + } + + const auto source_mode { win_utils::getSourceMode(win_utils::getSourceIndex(*path, display_data->m_modes), display_data->m_modes) }; + if (!source_mode) { + DD_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] = DisplayMode { + { source_mode->width, source_mode->height }, + { target_refresh_rate.Numerator, target_refresh_rate.Denominator } + }; + } + + return current_modes; + } + + bool + WinDisplayDevice::setDisplayModes(const DeviceDisplayModeMap &modes) { + if (modes.empty()) { + DD_LOG(error) << "Modes map is empty!"; + 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 keys_view { std::ranges::views::keys(modes) }; + const std::set device_ids { std::begin(keys_view), std::end(keys_view) }; + const auto all_device_ids { win_utils::getAllDeviceIdsAndMatchingDuplicates(*m_w_api, device_ids) }; + if (all_device_ids.empty()) { + DD_LOG(error) << "Failed to get all duplicated devices!"; + return false; + } + + if (all_device_ids.size() != device_ids.size()) { + DD_LOG(error) << "Not all modes for duplicate displays were provided!"; + return false; + } + + const auto &original_data { m_w_api->queryDisplayConfig(QueryType::All) }; + if (!original_data) { + // Error already logged + return false; + } + + if (!doSetModes(*m_w_api, modes, Strategy::Relaxed)) { + // Error already logged + return false; + } + + const auto all_modes_match = [&modes](const DeviceDisplayModeMap ¤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 is a sanity check as `getCurrentDisplayModes` implicitly verifies this already. + return false; + } + + if (!win_utils::fuzzyCompareModes(mode_it->second, requested_mode)) { + return false; + } + } + + return true; + }; + + auto current_modes { getCurrentDisplayModes(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. + DD_LOG(info) << "Failed to change display modes using Windows recommended modes, trying to set modes more strictly!"; + if (doSetModes(*m_w_api, modes, Strategy::Strict)) { + current_modes = getCurrentDisplayModes(device_ids); + if (!current_modes.empty() && all_modes_match(current_modes)) { + return true; + } + } + } + + const UINT32 flags { SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_VIRTUAL_MODE_AWARE }; + static_cast(m_w_api->setDisplayConfig(original_data->m_paths, original_data->m_modes, flags)); // Return value does not matter as we are trying out best to undo + DD_LOG(error) << "Failed to set display mode(-s) completely!"; + return false; + } +} // namespace display_device diff --git a/src/windows/windisplaydevicetopology.cpp b/src/windows/windisplaydevicetopology.cpp index c12c203..418bff6 100644 --- a/src/windows/windisplaydevicetopology.cpp +++ b/src/windows/windisplaydevicetopology.cpp @@ -15,20 +15,14 @@ namespace display_device { * @see set_topology for a description as this was split off to reduce cognitive complexity. */ bool - doSetTopology(WinApiLayerInterface &w_api, const ActiveTopology &new_topology) { - auto display_data { w_api.queryDisplayConfig(QueryType::All) }; - if (!display_data) { - // Error already logged - return false; - } - - const auto path_data { win_utils::collectSourceDataForMatchingPaths(w_api, display_data->m_paths) }; + doSetTopology(WinApiLayerInterface &w_api, const ActiveTopology &new_topology, const PathAndModeData &display_data) { + const auto path_data { win_utils::collectSourceDataForMatchingPaths(w_api, display_data.m_paths) }; if (path_data.empty()) { // Error already logged return false; } - auto paths { win_utils::makePathsForNewTopology(new_topology, path_data, display_data->m_paths) }; + auto paths { win_utils::makePathsForNewTopology(new_topology, path_data, display_data.m_paths) }; if (paths.empty()) { // Error already logged return false; @@ -161,7 +155,13 @@ namespace display_device { return true; } - if (doSetTopology(*m_w_api, new_topology)) { + const auto &original_data { m_w_api->queryDisplayConfig(QueryType::All) }; + if (!original_data) { + // Error already logged + return false; + } + + if (doSetTopology(*m_w_api, new_topology, *original_data)) { const auto updated_topology { getCurrentTopology() }; if (isTopologyValid(updated_topology)) { if (isTopologyTheSame(new_topology, updated_topology)) { @@ -200,7 +200,8 @@ namespace display_device { } // Revert back to the original topology - doSetTopology(*m_w_api, current_topology); // Return value does not matter + const UINT32 flags { SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_VIRTUAL_MODE_AWARE }; + static_cast(m_w_api->setDisplayConfig(original_data->m_paths, original_data->m_modes, flags)); // Return value does not matter as we are trying out best to undo } return false; diff --git a/tests/unit/windows/test_winapiutils.cpp b/tests/unit/windows/test_winapiutils.cpp index 83bc064..500333f 100644 --- a/tests/unit/windows/test_winapiutils.cpp +++ b/tests/unit/windows/test_winapiutils.cpp @@ -777,12 +777,10 @@ TEST_F_S_MOCKED(MakePathsForNewTopology, EmptyList) { } TEST_F_S_MOCKED(GetAllDeviceIdsAndMatchingDuplicates) { - auto pam_no_modes { ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES }; - InSequence sequence; EXPECT_CALL(m_layer, queryDisplayConfig(display_device::QueryType::Active)) .Times(1) - .WillOnce(Return(pam_no_modes)); + .WillOnce(Return(ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES)); // DeviceId1 iterations { diff --git a/tests/unit/windows/test_windisplaydevicemodes.cpp b/tests/unit/windows/test_windisplaydevicemodes.cpp new file mode 100644 index 0000000..6768cdf --- /dev/null +++ b/tests/unit/windows/test_windisplaydevicemodes.cpp @@ -0,0 +1,796 @@ +// system includes +#include + +// local includes +#include "displaydevice/windows/winapilayer.h" +#include "displaydevice/windows/winapiutils.h" +#include "displaydevice/windows/windisplaydevice.h" +#include "fixtures.h" +#include "utils/comparison.h" +#include "utils/guards.h" +#include "utils/mockwinapilayer.h" + +namespace { + // Convenience keywords for GMock + using ::testing::_; + using ::testing::InSequence; + using ::testing::Return; + using ::testing::StrictMock; + + // Test fixture(s) for this file + class WinDisplayDeviceModes: public BaseTest { + public: + bool + isSystemTest() const override { + return true; + } + + std::shared_ptr m_layer { std::make_shared() }; + display_device::WinDisplayDevice m_win_dd { m_layer }; + }; + + class WinDisplayDeviceModesMocked: public BaseTest { + public: + void + setupExpectedGetActivePathCall(int id_number, InSequence & /* To ensure that sequence is created outside this scope */) { + for (int i = 1; i <= id_number; ++i) { + EXPECT_CALL(*m_layer, getMonitorDevicePath(_)) + .Times(1) + .WillOnce(Return("PathX")) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getDeviceId(_)) + .Times(1) + .WillOnce(Return("DeviceId" + std::to_string(i))) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getDisplayName(_)) + .Times(1) + .WillOnce(Return("DisplayNameX")) + .RetiresOnSaturation(); + } + } + + void + setupExpectedGetCurrentDisplayModesCall(InSequence &sequence /* To ensure that sequence is created outside this scope */, const std::optional &pam = ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES) { + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(pam)) + .RetiresOnSaturation(); + + for (int i = 1; i <= 4; ++i) { + setupExpectedGetActivePathCall(i, sequence); + } + } + + void + setupExpectedGetAllDeviceIdsCall(InSequence & /* To ensure that sequence is created outside this scope */, const std::set &entries = { 1, 2, 3, 4 }) { + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES)) + .RetiresOnSaturation(); + + for (const auto entry : entries) { + for (int i = 1; i <= entry; ++i) { + EXPECT_CALL(*m_layer, getMonitorDevicePath(_)) + .Times(1) + .WillOnce(Return("Path" + std::to_string(i))) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getDeviceId(_)) + .Times(1) + .WillOnce(Return("DeviceId" + std::to_string(i))) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getDisplayName(_)) + .Times(1) + .WillOnce(Return("DisplayName" + std::to_string(i))) + .RetiresOnSaturation(); + } + + for (int i = 1; i <= 4; ++i) { + EXPECT_CALL(*m_layer, getMonitorDevicePath(_)) + .Times(1) + .WillOnce(Return("Path" + std::to_string(i))) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getDeviceId(_)) + .Times(1) + .WillOnce(Return("DeviceId" + std::to_string(i))) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getDisplayName(_)) + .Times(1) + .WillOnce(Return("DisplayName" + std::to_string(i))) + .RetiresOnSaturation(); + } + } + } + + std::shared_ptr> m_layer { std::make_shared>() }; + display_device::WinDisplayDevice m_win_dd { m_layer }; + }; + + // Specialized TEST macro(s) for this test file +#define TEST_F_S(...) DD_MAKE_TEST(TEST_F, WinDisplayDeviceModes, __VA_ARGS__) +#define TEST_F_S_MOCKED(...) DD_MAKE_TEST(TEST_F, WinDisplayDeviceModesMocked, __VA_ARGS__) + + // Additional convenience global const(s) + const UINT32 RELAXED_FLAGS { SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_VIRTUAL_MODE_AWARE | SDC_ALLOW_CHANGES }; + const UINT32 STRICT_FLAGS { SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_VIRTUAL_MODE_AWARE }; + const UINT32 UNDO_FLAGS { SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_VIRTUAL_MODE_AWARE }; + + // Helper functions + std::optional + applyExpectedModesOntoInput(std::optional input, const display_device::DeviceDisplayModeMap &modes, const std::set &excluded_ids = {}) { + if (!input) { + return std::nullopt; + } + + for (const auto &[device_id, mode] : modes) { + if (excluded_ids.contains(device_id)) { + continue; + } + + auto path_index { std::stoi(device_id.substr(device_id.size() - 1, 1)) - 1 }; + input->m_paths.at(path_index).targetInfo.refreshRate = { mode.m_refresh_rate.m_numerator, mode.m_refresh_rate.m_denominator }; + input->m_modes.at(input->m_paths.at(path_index).sourceInfo.sourceModeInfoIdx).sourceMode.width = mode.m_resolution.m_width; + input->m_modes.at(input->m_paths.at(path_index).sourceInfo.sourceModeInfoIdx).sourceMode.height = mode.m_resolution.m_height; + + display_device::win_utils::setTargetIndex(input->m_paths.at(path_index), std::nullopt); + display_device::win_utils::setDesktopIndex(input->m_paths.at(path_index), std::nullopt); + } + + return input; + } + + display_device::DisplayMode + getTestMode(const int number) { + if (number == 1) { + return { 1024, 768, { 60, 1 } }; + } + + return { 1920, 1080, { 60, 1 } }; + } +} // namespace + +TEST_F_S(GetCurrentDisplayModes) { + const auto flattened_topology { flattenTopology(m_win_dd.getCurrentTopology()) }; + const auto current_modes { m_win_dd.getCurrentDisplayModes(flattened_topology) }; + + // Can't really compare anything else without knowing system specs + const auto mode_keys_view { std::ranges::views::keys(current_modes) }; + const std::set mode_keys { std::begin(mode_keys_view), std::end(mode_keys_view) }; + EXPECT_EQ(flattened_topology, mode_keys); +} + +TEST_F_S(SetCurrentDisplayModes, ExtendedTopology) { + const auto available_devices { getAvailableDevices(*m_layer, true) }; + ASSERT_TRUE(available_devices); + + if (available_devices->size() < 2) { + GTEST_SKIP_("Not enough devices are available in the system."); + } + + const auto topology_guard { makeTopologyGuard(m_win_dd) }; + ASSERT_TRUE(m_win_dd.setTopology({ { available_devices->at(0) }, { available_devices->at(1) } })); + + const display_device::DeviceDisplayModeMap mixed_modes_1 { + { available_devices->at(0), getTestMode(0) }, + { available_devices->at(1), getTestMode(1) } + }; + const display_device::DeviceDisplayModeMap mixed_modes_2 { + { available_devices->at(0), getTestMode(1) }, + { available_devices->at(1), getTestMode(0) } + }; + + const auto mode_guard { makeModeGuard(m_win_dd) }; + ASSERT_TRUE(m_win_dd.setDisplayModes(mixed_modes_1)); + ASSERT_TRUE(m_win_dd.setDisplayModes(mixed_modes_2)); +} + +TEST_F_S(SetCurrentDisplayModes, DuplicatedTopology) { + const auto available_devices { getAvailableDevices(*m_layer, true) }; + ASSERT_TRUE(available_devices); + + if (available_devices->size() < 2) { + GTEST_SKIP_("Not enough devices are available in the system."); + } + + const auto topology_guard { makeTopologyGuard(m_win_dd) }; + ASSERT_TRUE(m_win_dd.setTopology({ { available_devices->at(0), available_devices->at(1) } })); + + const display_device::DeviceDisplayModeMap same_modes_1 { + { available_devices->at(0), getTestMode(1) }, + { available_devices->at(1), getTestMode(1) } + }; + const display_device::DeviceDisplayModeMap same_modes_2 { + { available_devices->at(0), getTestMode(0) }, + { available_devices->at(1), getTestMode(0) } + }; + const display_device::DeviceDisplayModeMap mixed_modes { + { available_devices->at(0), getTestMode(1) }, + { available_devices->at(1), getTestMode(0) } + }; + + const auto mode_guard { makeModeGuard(m_win_dd) }; + ASSERT_TRUE(m_win_dd.setDisplayModes(same_modes_1)); + ASSERT_TRUE(m_win_dd.setDisplayModes(same_modes_2)); + ASSERT_FALSE(m_win_dd.setDisplayModes(mixed_modes)); +} + +TEST_F_S(SetCurrentDisplayModes, MixedTopology) { + const auto available_devices { getAvailableDevices(*m_layer, true) }; + ASSERT_TRUE(available_devices); + + if (available_devices->size() < 3) { + GTEST_SKIP_("Not enough devices are available in the system."); + } + + const auto topology_guard { makeTopologyGuard(m_win_dd) }; + ASSERT_TRUE(m_win_dd.setTopology({ { available_devices->at(0) }, { available_devices->at(1), available_devices->at(2) } })); + + const display_device::DeviceDisplayModeMap mixed_modes_1 { + { available_devices->at(0), getTestMode(0) }, + { available_devices->at(1), getTestMode(1) }, + { available_devices->at(2), getTestMode(1) } + }; + const display_device::DeviceDisplayModeMap mixed_modes_2 { + { available_devices->at(0), getTestMode(1) }, + { available_devices->at(1), getTestMode(0) }, + { available_devices->at(2), getTestMode(0) } + }; + + const auto mode_guard { makeModeGuard(m_win_dd) }; + ASSERT_TRUE(m_win_dd.setDisplayModes(mixed_modes_1)); + ASSERT_TRUE(m_win_dd.setDisplayModes(mixed_modes_2)); +} + +TEST_F_S_MOCKED(GetCurrentDisplayModes) { + InSequence sequence; + setupExpectedGetCurrentDisplayModesCall(sequence); + + const auto current_modes { m_win_dd.getCurrentDisplayModes({ "DeviceId1", "DeviceId2", "DeviceId3", "DeviceId4" }) }; + const display_device::DeviceDisplayModeMap expected_modes { + { "DeviceId1", { 1920, 1080, { 120, 1 } } }, + { "DeviceId2", { 1920, 2160, { 119995, 1000 } } }, + { "DeviceId3", { 1920, 2160, { 60, 1 } } }, + { "DeviceId4", { 3840, 2160, { 90, 1 } } }, + }; + EXPECT_EQ(current_modes, expected_modes); +} + +TEST_F_S_MOCKED(GetCurrentDisplayModes, EmptyIdList) { + const auto current_modes { m_win_dd.getCurrentDisplayModes({}) }; + EXPECT_TRUE(current_modes.empty()); +} + +TEST_F_S_MOCKED(GetCurrentDisplayModes, NoDisplayData) { + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_NULL)); + + const auto current_modes { m_win_dd.getCurrentDisplayModes({ "DeviceId1" }) }; + EXPECT_TRUE(current_modes.empty()); +} + +TEST_F_S_MOCKED(GetCurrentDisplayModes, EmptyDeviceId) { + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES)); + + const auto current_modes { m_win_dd.getCurrentDisplayModes({ "", "DeviceId2" }) }; + EXPECT_TRUE(current_modes.empty()); +} + +TEST_F_S_MOCKED(GetCurrentDisplayModes, FailedToGetActivePath) { + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_EMPTY)); + + const auto current_modes { m_win_dd.getCurrentDisplayModes({ "DeviceId1" }) }; + EXPECT_TRUE(current_modes.empty()); +} + +TEST_F_S_MOCKED(GetCurrentDisplayModes, FailedToGetSourceMode) { + auto pam_no_modes { ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES }; + pam_no_modes->m_modes.clear(); + + EXPECT_CALL(*m_layer, getMonitorDevicePath(_)) + .Times(1) + .WillRepeatedly(Return("PathX")); + EXPECT_CALL(*m_layer, getDisplayName(_)) + .Times(1) + .WillRepeatedly(Return("DisplayNameX")); + EXPECT_CALL(*m_layer, getDeviceId(_)) + .Times(1) + .WillOnce(Return("DeviceId1")); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(pam_no_modes)); + + const auto current_modes { m_win_dd.getCurrentDisplayModes({ "DeviceId1" }) }; + EXPECT_TRUE(current_modes.empty()); +} + +TEST_F_S_MOCKED(SetDisplayModes, Relaxed) { + const display_device::DeviceDisplayModeMap new_modes { + { "DeviceId1", { 1920, 1080, { 120, 1 } } }, + { "DeviceId2", { 1920, 1000, { 144, 1 } } }, + { "DeviceId3", { 1000, 1000, { 90, 1 } } }, + { "DeviceId4", { 1000, 2160, { 90, 10 } } }, + }; + + const auto pam_initial { ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES }; + const auto pam_submitted { applyExpectedModesOntoInput(pam_initial, new_modes, { "DeviceId1" }) }; + + InSequence sequence; + setupExpectedGetAllDeviceIdsCall(sequence); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + setupExpectedGetActivePathCall(1, sequence); + setupExpectedGetActivePathCall(2, sequence); + setupExpectedGetActivePathCall(3, sequence); + setupExpectedGetActivePathCall(4, sequence); + + EXPECT_CALL(*m_layer, setDisplayConfig(pam_submitted->m_paths, pam_submitted->m_modes, RELAXED_FLAGS)) + .Times(1) + .WillOnce(Return(ERROR_SUCCESS)) + .RetiresOnSaturation(); + setupExpectedGetCurrentDisplayModesCall(sequence, pam_submitted); + + EXPECT_TRUE(m_win_dd.setDisplayModes(new_modes)); +} + +TEST_F_S_MOCKED(SetDisplayModes, Strict) { + const display_device::DeviceDisplayModeMap new_modes { + { "DeviceId1", { 1920, 1080, { 120, 10 } } }, + { "DeviceId2", { 1000, 2160, { 119995, 100 } } }, + { "DeviceId3", { 1000, 1000, { 90, 1 } } }, + { "DeviceId4", { 3840, 2160, { 90, 1 } } }, + }; + + const auto pam_initial { ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES }; + const auto pam_submitted { applyExpectedModesOntoInput(pam_initial, new_modes, { "DeviceId4" }) }; + + InSequence sequence; + setupExpectedGetAllDeviceIdsCall(sequence); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + + // Relaxed try + { + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + setupExpectedGetActivePathCall(1, sequence); + setupExpectedGetActivePathCall(2, sequence); + setupExpectedGetActivePathCall(3, sequence); + setupExpectedGetActivePathCall(4, sequence); + + EXPECT_CALL(*m_layer, setDisplayConfig(pam_submitted->m_paths, pam_submitted->m_modes, RELAXED_FLAGS)) + .Times(1) + .WillOnce(Return(ERROR_SUCCESS)) + .RetiresOnSaturation(); + setupExpectedGetCurrentDisplayModesCall(sequence, pam_initial); + } + + // Strict try + { + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + setupExpectedGetActivePathCall(1, sequence); + setupExpectedGetActivePathCall(2, sequence); + setupExpectedGetActivePathCall(3, sequence); + setupExpectedGetActivePathCall(4, sequence); + + EXPECT_CALL(*m_layer, setDisplayConfig(pam_submitted->m_paths, pam_submitted->m_modes, STRICT_FLAGS)) + .Times(1) + .WillOnce(Return(ERROR_SUCCESS)) + .RetiresOnSaturation(); + setupExpectedGetCurrentDisplayModesCall(sequence, pam_submitted); + } + + EXPECT_TRUE(m_win_dd.setDisplayModes(new_modes)); +} + +TEST_F_S_MOCKED(SetDisplayModes, NoChanges) { + const display_device::DeviceDisplayModeMap new_modes { + { "DeviceId1", { 1920, 1080, { 120, 1 } } }, + { "DeviceId2", { 1920, 2160, { 119995, 1000 } } }, + { "DeviceId3", { 1920, 2160, { 60, 1 } } }, + { "DeviceId4", { 3840, 2160, { 90, 1 } } }, + }; + + InSequence sequence; + setupExpectedGetAllDeviceIdsCall(sequence); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES)) + .RetiresOnSaturation(); + setupExpectedGetActivePathCall(1, sequence); + setupExpectedGetActivePathCall(2, sequence); + setupExpectedGetActivePathCall(3, sequence); + setupExpectedGetActivePathCall(4, sequence); + setupExpectedGetCurrentDisplayModesCall(sequence, ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES); + + EXPECT_TRUE(m_win_dd.setDisplayModes(new_modes)); +} + +TEST_F_S_MOCKED(SetDisplayModes, EmptyModeMap) { + EXPECT_FALSE(m_win_dd.setDisplayModes({})); +} + +TEST_F_S_MOCKED(SetDisplayModes, FailedToGetDuplicateDevices) { + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_NULL)) + .RetiresOnSaturation(); + + EXPECT_FALSE(m_win_dd.setDisplayModes({ { "DeviceId1", {} } })); +} + +TEST_F_S_MOCKED(SetDisplayModes, MissingDuplicateDisplayModes) { + InSequence sequence; + setupExpectedGetAllDeviceIdsCall(sequence, { 2 }); + + EXPECT_FALSE(m_win_dd.setDisplayModes({ { "DeviceId2", {} } })); +} + +TEST_F_S_MOCKED(SetDisplayModes, FailedToGetOriginalData) { + InSequence sequence; + setupExpectedGetAllDeviceIdsCall(sequence, { 1 }); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_NULL)) + .RetiresOnSaturation(); + + EXPECT_FALSE(m_win_dd.setDisplayModes({ { "DeviceId1", {} } })); +} + +TEST_F_S_MOCKED(SetDisplayModes, DoSetModes, FailedToGetDisplayConfig) { + InSequence sequence; + setupExpectedGetAllDeviceIdsCall(sequence, { 1 }); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_NULL)) + .RetiresOnSaturation(); + + EXPECT_FALSE(m_win_dd.setDisplayModes({ { "DeviceId1", {} } })); +} + +TEST_F_S_MOCKED(SetDisplayModes, DoSetModes, EmptyListFromGetDisplayConfig) { + InSequence sequence; + setupExpectedGetAllDeviceIdsCall(sequence, { 1 }); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_EMPTY)) + .RetiresOnSaturation(); + + EXPECT_FALSE(m_win_dd.setDisplayModes({ { "DeviceId1", {} } })); +} + +TEST_F_S_MOCKED(SetDisplayModes, DoSetModes, FailedToGetActivePath) { + InSequence sequence; + setupExpectedGetAllDeviceIdsCall(sequence, { 1 }); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getMonitorDevicePath(_)) + .Times(4) + .WillOnce(Return("")) + .RetiresOnSaturation(); + + EXPECT_FALSE(m_win_dd.setDisplayModes({ { "DeviceId1", {} } })); +} + +TEST_F_S_MOCKED(SetDisplayModes, DoSetModes, FailedToGetSourceMode) { + auto pam_no_modes { ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES }; + pam_no_modes->m_modes.clear(); + + InSequence sequence; + setupExpectedGetAllDeviceIdsCall(sequence, { 1 }); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(pam_no_modes)) + .RetiresOnSaturation(); + setupExpectedGetActivePathCall(1, sequence); + + EXPECT_FALSE(m_win_dd.setDisplayModes({ { "DeviceId1", {} } })); +} + +TEST_F_S_MOCKED(SetDisplayModes, Relaxed, FailedToSetDisplayConfig) { + const display_device::DeviceDisplayModeMap new_modes { + { "DeviceId1", { 1920, 1080, { 120, 1 } } }, + { "DeviceId2", { 1920, 1000, { 144, 1 } } }, + { "DeviceId3", { 1000, 1000, { 90, 1 } } }, + { "DeviceId4", { 1000, 2160, { 90, 10 } } }, + }; + + const auto pam_initial { ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES }; + const auto pam_submitted { applyExpectedModesOntoInput(pam_initial, new_modes, { "DeviceId1" }) }; + + InSequence sequence; + setupExpectedGetAllDeviceIdsCall(sequence); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + setupExpectedGetActivePathCall(1, sequence); + setupExpectedGetActivePathCall(2, sequence); + setupExpectedGetActivePathCall(3, sequence); + setupExpectedGetActivePathCall(4, sequence); + + EXPECT_CALL(*m_layer, setDisplayConfig(pam_submitted->m_paths, pam_submitted->m_modes, RELAXED_FLAGS)) + .Times(1) + .WillOnce(Return(ERROR_ACCESS_DENIED)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getErrorString(ERROR_ACCESS_DENIED)) + .Times(1) + .WillRepeatedly(Return("ErrorDesc")) + .RetiresOnSaturation(); + + EXPECT_FALSE(m_win_dd.setDisplayModes(new_modes)); +} + +TEST_F_S_MOCKED(SetDisplayModes, Relaxed, FailedToGetCurrentDisplayModes) { + const display_device::DeviceDisplayModeMap new_modes { + { "DeviceId1", { 1920, 1080, { 120, 1 } } }, + { "DeviceId2", { 1920, 1000, { 144, 1 } } }, + { "DeviceId3", { 1000, 1000, { 90, 1 } } }, + { "DeviceId4", { 1000, 2160, { 90, 10 } } }, + }; + + const auto pam_initial { ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES }; + const auto pam_submitted { applyExpectedModesOntoInput(pam_initial, new_modes, { "DeviceId1" }) }; + + InSequence sequence; + setupExpectedGetAllDeviceIdsCall(sequence); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + setupExpectedGetActivePathCall(1, sequence); + setupExpectedGetActivePathCall(2, sequence); + setupExpectedGetActivePathCall(3, sequence); + setupExpectedGetActivePathCall(4, sequence); + + EXPECT_CALL(*m_layer, setDisplayConfig(pam_submitted->m_paths, pam_submitted->m_modes, RELAXED_FLAGS)) + .Times(1) + .WillOnce(Return(ERROR_SUCCESS)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_NULL)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, setDisplayConfig(pam_initial->m_paths, pam_initial->m_modes, UNDO_FLAGS)) + .Times(1) + .WillOnce(Return(ERROR_SUCCESS)) + .RetiresOnSaturation(); + + EXPECT_FALSE(m_win_dd.setDisplayModes(new_modes)); +} + +TEST_F_S_MOCKED(SetDisplayModes, Strict, FailedToSetDisplayConfig) { + const display_device::DeviceDisplayModeMap new_modes { + { "DeviceId1", { 1920, 1080, { 120, 10 } } }, + { "DeviceId2", { 1000, 2160, { 119995, 100 } } }, + { "DeviceId3", { 1000, 1000, { 90, 1 } } }, + { "DeviceId4", { 3840, 2160, { 90, 1 } } }, + }; + + const auto pam_initial { ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES }; + const auto pam_submitted { applyExpectedModesOntoInput(pam_initial, new_modes, { "DeviceId4" }) }; + + InSequence sequence; + setupExpectedGetAllDeviceIdsCall(sequence); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + + // Relaxed try + { + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + setupExpectedGetActivePathCall(1, sequence); + setupExpectedGetActivePathCall(2, sequence); + setupExpectedGetActivePathCall(3, sequence); + setupExpectedGetActivePathCall(4, sequence); + + EXPECT_CALL(*m_layer, setDisplayConfig(pam_submitted->m_paths, pam_submitted->m_modes, RELAXED_FLAGS)) + .Times(1) + .WillOnce(Return(ERROR_SUCCESS)) + .RetiresOnSaturation(); + setupExpectedGetCurrentDisplayModesCall(sequence, pam_initial); + } + + // Strict try + { + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + setupExpectedGetActivePathCall(1, sequence); + setupExpectedGetActivePathCall(2, sequence); + setupExpectedGetActivePathCall(3, sequence); + setupExpectedGetActivePathCall(4, sequence); + + EXPECT_CALL(*m_layer, setDisplayConfig(pam_submitted->m_paths, pam_submitted->m_modes, STRICT_FLAGS)) + .Times(1) + .WillOnce(Return(ERROR_ACCESS_DENIED)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getErrorString(ERROR_ACCESS_DENIED)) + .Times(1) + .WillRepeatedly(Return("ErrorDesc")) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, setDisplayConfig(pam_initial->m_paths, pam_initial->m_modes, UNDO_FLAGS)) + .Times(1) + .WillOnce(Return(ERROR_SUCCESS)) + .RetiresOnSaturation(); + } + + EXPECT_FALSE(m_win_dd.setDisplayModes(new_modes)); +} + +TEST_F_S_MOCKED(SetDisplayModes, Strict, FailedToGetCurrentDisplayModes) { + const display_device::DeviceDisplayModeMap new_modes { + { "DeviceId1", { 1920, 1080, { 120, 10 } } }, + { "DeviceId2", { 1000, 2160, { 119995, 100 } } }, + { "DeviceId3", { 1000, 1000, { 90, 1 } } }, + { "DeviceId4", { 3840, 2160, { 90, 1 } } }, + }; + + const auto pam_initial { ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES }; + const auto pam_submitted { applyExpectedModesOntoInput(pam_initial, new_modes, { "DeviceId4" }) }; + + InSequence sequence; + setupExpectedGetAllDeviceIdsCall(sequence); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + + // Relaxed try + { + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + setupExpectedGetActivePathCall(1, sequence); + setupExpectedGetActivePathCall(2, sequence); + setupExpectedGetActivePathCall(3, sequence); + setupExpectedGetActivePathCall(4, sequence); + + EXPECT_CALL(*m_layer, setDisplayConfig(pam_submitted->m_paths, pam_submitted->m_modes, RELAXED_FLAGS)) + .Times(1) + .WillOnce(Return(ERROR_SUCCESS)) + .RetiresOnSaturation(); + setupExpectedGetCurrentDisplayModesCall(sequence, pam_initial); + } + + // Strict try + { + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + setupExpectedGetActivePathCall(1, sequence); + setupExpectedGetActivePathCall(2, sequence); + setupExpectedGetActivePathCall(3, sequence); + setupExpectedGetActivePathCall(4, sequence); + + EXPECT_CALL(*m_layer, setDisplayConfig(pam_submitted->m_paths, pam_submitted->m_modes, STRICT_FLAGS)) + .Times(1) + .WillOnce(Return(ERROR_SUCCESS)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_NULL)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, setDisplayConfig(pam_initial->m_paths, pam_initial->m_modes, UNDO_FLAGS)) + .Times(1) + .WillOnce(Return(ERROR_SUCCESS)) + .RetiresOnSaturation(); + } + + EXPECT_FALSE(m_win_dd.setDisplayModes(new_modes)); +} + +TEST_F_S_MOCKED(SetDisplayModes, Strict, ModesDidNotChange) { + const display_device::DeviceDisplayModeMap new_modes { + { "DeviceId1", { 1920, 1080, { 120, 10 } } }, + { "DeviceId2", { 1000, 2160, { 119995, 100 } } }, + { "DeviceId3", { 1000, 1000, { 90, 1 } } }, + { "DeviceId4", { 3840, 2160, { 90, 1 } } }, + }; + + const auto pam_initial { ut_consts::PAM_4_ACTIVE_WITH_2_DUPLICATES }; + const auto pam_submitted { applyExpectedModesOntoInput(pam_initial, new_modes, { "DeviceId4" }) }; + + InSequence sequence; + setupExpectedGetAllDeviceIdsCall(sequence); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + + // Relaxed try + { + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + setupExpectedGetActivePathCall(1, sequence); + setupExpectedGetActivePathCall(2, sequence); + setupExpectedGetActivePathCall(3, sequence); + setupExpectedGetActivePathCall(4, sequence); + + EXPECT_CALL(*m_layer, setDisplayConfig(pam_submitted->m_paths, pam_submitted->m_modes, RELAXED_FLAGS)) + .Times(1) + .WillOnce(Return(ERROR_SUCCESS)) + .RetiresOnSaturation(); + setupExpectedGetCurrentDisplayModesCall(sequence, pam_initial); + } + + // Strict try + { + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(pam_initial)) + .RetiresOnSaturation(); + setupExpectedGetActivePathCall(1, sequence); + setupExpectedGetActivePathCall(2, sequence); + setupExpectedGetActivePathCall(3, sequence); + setupExpectedGetActivePathCall(4, sequence); + + EXPECT_CALL(*m_layer, setDisplayConfig(pam_submitted->m_paths, pam_submitted->m_modes, STRICT_FLAGS)) + .Times(1) + .WillOnce(Return(ERROR_SUCCESS)) + .RetiresOnSaturation(); + setupExpectedGetCurrentDisplayModesCall(sequence, pam_initial); + + EXPECT_CALL(*m_layer, setDisplayConfig(pam_initial->m_paths, pam_initial->m_modes, UNDO_FLAGS)) + .Times(1) + .WillOnce(Return(ERROR_SUCCESS)) + .RetiresOnSaturation(); + } + + EXPECT_FALSE(m_win_dd.setDisplayModes(new_modes)); +} diff --git a/tests/unit/windows/test_windisplaydevicetopology.cpp b/tests/unit/windows/test_windisplaydevicetopology.cpp index 8922ab7..df9cea2 100644 --- a/tests/unit/windows/test_windisplaydevicetopology.cpp +++ b/tests/unit/windows/test_windisplaydevicetopology.cpp @@ -22,24 +22,6 @@ namespace { return true; } - std::optional> - getAvailableDevices() { - const auto all_devices { m_layer->queryDisplayConfig(display_device::QueryType::All) }; - if (!all_devices) { - return std::nullopt; - } - - std::set device_ids; - for (const auto &path : all_devices->m_paths) { - const auto device_id { m_layer->getDeviceId(path) }; - if (!device_id.empty()) { - device_ids.insert(device_id); - } - } - - return std::vector { device_ids.begin(), device_ids.end() }; - } - std::shared_ptr m_layer { std::make_shared() }; display_device::WinDisplayDevice m_win_dd { m_layer }; }; @@ -108,20 +90,11 @@ TEST_F_S(GetCurrentTopology) { } // It is enough to check whether the topology contains expected ids - others test cases check the structure. - std::set flattened_topology; - const auto current_topology { m_win_dd.getCurrentTopology() }; - for (const auto &group : current_topology) { - for (const auto &device_id : group) { - EXPECT_FALSE(device_id.empty()); - EXPECT_TRUE(flattened_topology.insert(device_id).second); - } - } - - EXPECT_EQ(flattened_topology, expected_devices); + EXPECT_EQ(flattenTopology(m_win_dd.getCurrentTopology()), expected_devices); } TEST_F_S(SetCurrentTopology, ExtendedTopology) { - const auto available_devices { getAvailableDevices() }; + const auto available_devices { getAvailableDevices(*m_layer) }; ASSERT_TRUE(available_devices); if (available_devices->size() < 2) { @@ -143,7 +116,7 @@ TEST_F_S(SetCurrentTopology, ExtendedTopology) { } TEST_F_S(SetCurrentTopology, DuplicatedTopology) { - const auto available_devices { getAvailableDevices() }; + const auto available_devices { getAvailableDevices(*m_layer) }; ASSERT_TRUE(available_devices); if (available_devices->size() < 2) { @@ -161,7 +134,7 @@ TEST_F_S(SetCurrentTopology, DuplicatedTopology) { } TEST_F_S(SetCurrentTopology, MixedTopology) { - const auto available_devices { getAvailableDevices() }; + const auto available_devices { getAvailableDevices(*m_layer) }; ASSERT_TRUE(available_devices); if (available_devices->size() < 3) { @@ -440,9 +413,10 @@ TEST_F_S_MOCKED(SetCurrentTopology, TopologyWasSetAccordingToWinApi, CouldNotGet .WillOnce(Return(ut_consts::PAM_NULL)); // Called when doing the undo - EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + expected_flags = SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_VIRTUAL_MODE_AWARE; + EXPECT_CALL(*m_layer, setDisplayConfig(ut_consts::PAM_3_ACTIVE->m_paths, ut_consts::PAM_3_ACTIVE->m_modes, expected_flags)) .Times(1) - .WillOnce(Return(ut_consts::PAM_NULL)); + .WillOnce(Return(ERROR_SUCCESS)); EXPECT_FALSE(m_win_dd.setTopology({ { "DeviceId1" } })); } @@ -461,9 +435,10 @@ TEST_F_S_MOCKED(SetCurrentTopology, TopologyWasSetAccordingToWinApi, WinApiLied) setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::Active, sequence); // Called when doing the undo - EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + expected_flags = SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_VIRTUAL_MODE_AWARE; + EXPECT_CALL(*m_layer, setDisplayConfig(ut_consts::PAM_3_ACTIVE->m_paths, ut_consts::PAM_3_ACTIVE->m_modes, expected_flags)) .Times(1) - .WillOnce(Return(ut_consts::PAM_NULL)); + .WillOnce(Return(ERROR_SUCCESS)); EXPECT_FALSE(m_win_dd.setTopology({ { "DeviceId1" } })); } diff --git a/tests/unit/windows/utils/comparison.cpp b/tests/unit/windows/utils/comparison.cpp index d5e078f..e8af83e 100644 --- a/tests/unit/windows/utils/comparison.cpp +++ b/tests/unit/windows/utils/comparison.cpp @@ -111,4 +111,19 @@ namespace display_device { 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; } + + bool + operator==(const Rational &lhs, const Rational &rhs) { + return lhs.m_numerator == rhs.m_numerator && lhs.m_denominator == rhs.m_denominator; + } + + bool + operator==(const Resolution &lhs, const Resolution &rhs) { + return lhs.m_height == rhs.m_height && lhs.m_width == rhs.m_width; + } + + bool + operator==(const DisplayMode &lhs, const DisplayMode &rhs) { + return lhs.m_refresh_rate == rhs.m_refresh_rate && lhs.m_resolution == rhs.m_resolution; + } } // namespace display_device diff --git a/tests/unit/windows/utils/comparison.h b/tests/unit/windows/utils/comparison.h index fd8c9a8..e31cd44 100644 --- a/tests/unit/windows/utils/comparison.h +++ b/tests/unit/windows/utils/comparison.h @@ -46,4 +46,13 @@ operator==(const DISPLAYCONFIG_MODE_INFO &lhs, const DISPLAYCONFIG_MODE_INFO &rh namespace display_device { bool operator==(const PathSourceIndexData &lhs, const PathSourceIndexData &rhs); + + bool + operator==(const Rational &lhs, const Rational &rhs); + + bool + operator==(const Resolution &lhs, const Resolution &rhs); + + bool + operator==(const DisplayMode &lhs, const DisplayMode &rhs); } // namespace display_device diff --git a/tests/unit/windows/utils/guards.h b/tests/unit/windows/utils/guards.h index 8130166..53d0e00 100644 --- a/tests/unit/windows/utils/guards.h +++ b/tests/unit/windows/utils/guards.h @@ -5,11 +5,19 @@ // local includes #include "displaydevice/windows/windisplaydevice.h" +#include "helpers.h" // Helper functions to make guards for restoring previous state -auto +inline auto makeTopologyGuard(display_device::WinDisplayDevice &win_dd) { return boost::scope::make_scope_exit([&win_dd, topology = win_dd.getCurrentTopology()]() { static_cast(win_dd.setTopology(topology)); }); } + +inline auto +makeModeGuard(display_device::WinDisplayDevice &win_dd) { + return boost::scope::make_scope_exit([&win_dd, modes = win_dd.getCurrentDisplayModes(flattenTopology(win_dd.getCurrentTopology()))]() { + static_cast(win_dd.setDisplayModes(modes)); + }); +} diff --git a/tests/unit/windows/utils/helpers.cpp b/tests/unit/windows/utils/helpers.cpp new file mode 100644 index 0000000..02afd7b --- /dev/null +++ b/tests/unit/windows/utils/helpers.cpp @@ -0,0 +1,35 @@ +// local includes +#include "helpers.h" + +std::set +flattenTopology(const display_device::ActiveTopology &topology) { + std::set flattened_topology; + for (const auto &group : topology) { + for (const auto &device_id : group) { + flattened_topology.insert(device_id); + } + } + return flattened_topology; +} + +std::optional> +getAvailableDevices(display_device::WinApiLayer &layer, const bool only_valid_output) { + const auto all_devices { layer.queryDisplayConfig(display_device::QueryType::All) }; + if (!all_devices) { + return std::nullopt; + } + + std::set device_ids; + for (const auto &path : all_devices->m_paths) { + if (only_valid_output && path.targetInfo.outputTechnology == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_OTHER) { + continue; + } + + const auto device_id { layer.getDeviceId(path) }; + if (!device_id.empty()) { + device_ids.insert(device_id); + } + } + + return std::vector { device_ids.begin(), device_ids.end() }; +} diff --git a/tests/unit/windows/utils/helpers.h b/tests/unit/windows/utils/helpers.h new file mode 100644 index 0000000..13c7acc --- /dev/null +++ b/tests/unit/windows/utils/helpers.h @@ -0,0 +1,15 @@ +#pragma once + +// system includes +#include + +// local includes +#include "displaydevice/windows/types.h" +#include "displaydevice/windows/winapilayer.h" + +// Generic helper functions +std::set +flattenTopology(const display_device::ActiveTopology &topology); + +std::optional> +getAvailableDevices(display_device::WinApiLayer &layer, bool only_valid_output = false); diff --git a/tests/unit/windows/utils/mockwinapilayer.cpp b/tests/unit/windows/utils/mockwinapilayer.cpp index c6ced01..9f9198e 100644 --- a/tests/unit/windows/utils/mockwinapilayer.cpp +++ b/tests/unit/windows/utils/mockwinapilayer.cpp @@ -14,6 +14,7 @@ namespace { data.m_paths.back().sourceInfo.adapterId = { 1, 1 }; data.m_paths.back().sourceInfo.id = 0; data.m_paths.back().targetInfo.targetAvailable = TRUE; + data.m_paths.back().targetInfo.refreshRate = { 120, 1 }; data.m_modes.push_back({}); data.m_modes.back().infoType = DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE; @@ -31,13 +32,14 @@ namespace { data.m_paths.back().sourceInfo.adapterId = { 2, 2 }; data.m_paths.back().sourceInfo.id = 0; data.m_paths.back().targetInfo.targetAvailable = TRUE; + data.m_paths.back().targetInfo.refreshRate = { 119995, 1000 }; data.m_modes.push_back({}); data.m_modes.back().infoType = DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE; data.m_modes.back().sourceMode = {}; // Set the union - data.m_modes.back().sourceMode.position = { 1921, 0 }; // TODO + data.m_modes.back().sourceMode.position = { 1921, 0 }; data.m_modes.back().sourceMode.width = 1920; - data.m_modes.back().sourceMode.height = 1080; + data.m_modes.back().sourceMode.height = 2160; if (include_duplicate) { data.m_paths.push_back({}); @@ -46,13 +48,14 @@ namespace { data.m_paths.back().sourceInfo.adapterId = { 3, 3 }; data.m_paths.back().sourceInfo.id = 0; data.m_paths.back().targetInfo.targetAvailable = TRUE; + data.m_paths.back().targetInfo.refreshRate = { 60, 1 }; data.m_modes.push_back({}); data.m_modes.back().infoType = DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE; data.m_modes.back().sourceMode = {}; // Set the union data.m_modes.back().sourceMode.position = { 1921, 0 }; data.m_modes.back().sourceMode.width = 1920; - data.m_modes.back().sourceMode.height = 1080; + data.m_modes.back().sourceMode.height = 2160; } } @@ -64,13 +67,14 @@ namespace { data.m_paths.back().sourceInfo.adapterId = { 4, 4 }; data.m_paths.back().sourceInfo.id = 0; data.m_paths.back().targetInfo.targetAvailable = TRUE; + data.m_paths.back().targetInfo.refreshRate = { 90, 1 }; data.m_modes.push_back({}); data.m_modes.back().infoType = DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE; data.m_modes.back().sourceMode = {}; // Set the union data.m_modes.back().sourceMode.position = { 0, 1081 }; - data.m_modes.back().sourceMode.width = 1920; - data.m_modes.back().sourceMode.height = 1080; + data.m_modes.back().sourceMode.width = 3840; + data.m_modes.back().sourceMode.height = 2160; } return data; From 9ab594e27a91016a971567adb57f047a9be8b317 Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Fri, 31 May 2024 00:19:11 +0300 Subject: [PATCH 2/2] Fix lint --- src/windows/include/displaydevice/windows/windisplaydevice.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/windows/include/displaydevice/windows/windisplaydevice.h b/src/windows/include/displaydevice/windows/windisplaydevice.h index c503f87..9b7d32f 100644 --- a/src/windows/include/displaydevice/windows/windisplaydevice.h +++ b/src/windows/include/displaydevice/windows/windisplaydevice.h @@ -34,7 +34,7 @@ namespace display_device { /** For details @see WinDisplayDeviceInterface::setTopology */ [[nodiscard]] bool setTopology(const ActiveTopology &new_topology) override; - + /** For details @see WinDisplayDeviceInterface::getCurrentDisplayModes */ [[nodiscard]] DeviceDisplayModeMap getCurrentDisplayModes(const std::set &device_ids) const override;