Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add method for setting new topology #36

Merged
merged 5 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ jobs:
Set-Location -Path usbmmidd_v2/usbmmidd_v2
./deviceinstaller64 install usbmmidd.inf usbmmidd

# create up to 4 virtual displays
for ($i = 1; $i -le 4; $i++) {
# create 2 virtual displays
FrogTheFrog marked this conversation as resolved.
Show resolved Hide resolved
for ($i = 1; $i -le 2; $i++) {
./deviceinstaller64 enableidd 1
}

Expand Down
4 changes: 4 additions & 0 deletions src/windows/include/displaydevice/windows/winapilayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,9 @@ namespace display_device {
/** For details @see WinApiLayerInterface::getDisplayName */
[[nodiscard]] std::string
getDisplayName(const DISPLAYCONFIG_PATH_INFO &path) const override;

/** For details @see WinApiLayerInterface::setDisplayConfig */
[[nodiscard]] LONG
setDisplayConfig(std::vector<DISPLAYCONFIG_PATH_INFO> paths, std::vector<DISPLAYCONFIG_MODE_INFO> modes, UINT32 flags) override;
};
} // namespace display_device
20 changes: 20 additions & 0 deletions src/windows/include/displaydevice/windows/winapilayerinterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,5 +146,25 @@ namespace display_device {
*/
[[nodiscard]] virtual std::string
getDisplayName(const DISPLAYCONFIG_PATH_INFO &path) const = 0;

/**
* @brief Direct wrapper around the SetDisplayConfig WinAPI.
*
* It implements no additional logic, just a direct pass-trough.
*
* @param paths List of paths to pass.
* @param modes List of modes to pass.
* @param flags Flags to pass.
* @returns The return error code of the API.
*
* EXAMPLES:
* ```cpp
* std::vector<DISPLAYCONFIG_PATH_INFO> paths;
* const WinApiLayerInterface* iface = getIface(...);
* const auto result = iface->setDisplayConfig(paths, {}, 0);
* ```
*/
[[nodiscard]] virtual LONG
setDisplayConfig(std::vector<DISPLAYCONFIG_PATH_INFO> paths, std::vector<DISPLAYCONFIG_MODE_INFO> modes, UINT32 flags) = 0;
};
} // namespace display_device
4 changes: 4 additions & 0 deletions src/windows/include/displaydevice/windows/windisplaydevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ namespace display_device {
[[nodiscard]] bool
isTopologyTheSame(const ActiveTopology &lhs, const ActiveTopology &rhs) const override;

/** For details @see WinDisplayDevice::setTopology */
[[nodiscard]] bool
setTopology(const ActiveTopology &new_topology) override;

private:
std::shared_ptr<WinApiLayerInterface> m_w_api;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,20 @@ namespace display_device {
*/
[[nodiscard]] virtual bool
isTopologyTheSame(const ActiveTopology &lhs, const ActiveTopology &rhs) const = 0;

/**
* @brief Set a new active topology for the OS.
* @param new_topology New device topology to set.
* @returns True if the new topology has been set, false otherwise.
*
* EXAMPLES:
* ```cpp
* auto current_topology { getCurrentTopology() };
* // Modify the current_topology
* const bool success = setTopology(current_topology);
* ```
*/
[[nodiscard]] virtual bool
setTopology(const ActiveTopology &new_topology) = 0;
};
} // namespace display_device
13 changes: 12 additions & 1 deletion src/windows/winapilayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ namespace display_device {

status = RegQueryValueExW(reg_key, L"EDID", nullptr, nullptr, edid.data(), &required_size_in_bytes);
if (status != ERROR_SUCCESS) {
DD_LOG(error) << w_api.getErrorString(status) << " \"RegQueryValueExW\" failed when getting size.";
DD_LOG(error) << w_api.getErrorString(status) << " \"RegQueryValueExW\" failed when getting data.";
return false;
}

Expand Down Expand Up @@ -528,4 +528,15 @@ namespace display_device {

return toUtf8(*this, source_name.viewGdiDeviceName);
}

LONG
WinApiLayer::setDisplayConfig(std::vector<DISPLAYCONFIG_PATH_INFO> paths, std::vector<DISPLAYCONFIG_MODE_INFO> modes, UINT32 flags) {
// std::vector::data() "may or may not return a null pointer, if size() is 0", therefore we want to enforce nullptr...
return ::SetDisplayConfig(
paths.size(),
paths.empty() ? nullptr : paths.data(),
modes.size(),
modes.empty() ? nullptr : modes.data(),
flags);
}
} // namespace display_device
3 changes: 3 additions & 0 deletions src/windows/winapiutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,9 @@ namespace display_device::win_utils {
group_id++;
}

if (new_paths.empty()) {
DD_LOG(error) << "Failed to make paths for new topology!";
}
return new_paths;
}
} // namespace display_device::win_utils
108 changes: 108 additions & 0 deletions src/windows/windisplaydevicetopology.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,51 @@
#include "displaydevice/windows/winapiutils.h"

namespace display_device {
namespace {
/**
* @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) };
if (path_data.empty()) {
// Error already logged
return false;
}

auto paths { win_utils::makePathsForNewTopology(new_topology, path_data, display_data->m_paths) };
if (paths.empty()) {
// Error already logged
return false;
}

UINT32 flags { SDC_APPLY | SDC_TOPOLOGY_SUPPLIED | SDC_ALLOW_PATH_ORDER_CHANGES | SDC_VIRTUAL_MODE_AWARE };
LONG result { w_api.setDisplayConfig(paths, {}, flags) };
if (result == ERROR_GEN_FAILURE) {
DD_LOG(warning) << w_api.getErrorString(result) << " failed to change topology using the topology from Windows DB! Asking Windows to create the topology.";

flags = SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES /* This flag is probably not needed, but who knows really... (not MSDOCS at least) */ | SDC_VIRTUAL_MODE_AWARE | SDC_SAVE_TO_DATABASE;
result = w_api.setDisplayConfig(paths, {}, flags);
if (result != ERROR_SUCCESS) {
DD_LOG(error) << w_api.getErrorString(result) << " failed to create new topology configuration!";
return false;
}
}
else if (result != ERROR_SUCCESS) {
DD_LOG(error) << w_api.getErrorString(result) << " failed to change topology configuration!";
return false;
}

return true;
}
} // namespace

ActiveTopology
WinDisplayDevice::getCurrentTopology() const {
const auto display_data { m_w_api->queryDisplayConfig(QueryType::Active) };
Expand Down Expand Up @@ -97,4 +142,67 @@ namespace display_device {

return lhs_copy == rhs_copy;
}

bool
WinDisplayDevice::setTopology(const ActiveTopology &new_topology) {
if (!isTopologyValid(new_topology)) {
DD_LOG(error) << "Topology input is invalid!";
return false;
}

const auto current_topology { getCurrentTopology() };
if (!isTopologyValid(current_topology)) {
DD_LOG(error) << "Failed to get current topology!";
return false;
}

if (isTopologyTheSame(current_topology, new_topology)) {
DD_LOG(debug) << "Same topology provided.";
return true;
}

if (doSetTopology(*m_w_api, new_topology)) {
const auto updated_topology { getCurrentTopology() };
if (isTopologyValid(updated_topology)) {
if (isTopologyTheSame(new_topology, updated_topology)) {
return true;
}
else {
// There is an interesting bug in Windows when you have nearly
// identical devices, drivers or something. For example, imagine you have:
// AM - Actual Monitor
// IDD1 - Virtual display 1
// IDD2 - Virtual display 2
//
// You can have the following topology:
// [[AM, IDD1]]
// but not this:
// [[AM, IDD2]]
//
// Windows API will just default to:
// [[AM, IDD1]]
// even if you provide the second variant. Windows API will think
// it's OK and just return ERROR_SUCCESS in this case and there is
// nothing you can do. Even the Windows' settings app will not
// be able to set the desired topology.
//
// There seems to be a workaround - you need to make sure the IDD1
// device is used somewhere else in the topology, like:
// [[AM, IDD2], [IDD1]]
//
// However, since we have this bug an additional sanity check is needed
// regardless of what Windows report back to us.
DD_LOG(error) << "Failed to change topology due to Windows bug or because the display is in deep sleep!";
}
}
else {
DD_LOG(error) << "Failed to get updated topology!";
}

// Revert back to the original topology
doSetTopology(*m_w_api, current_topology); // Return value does not matter
}

return false;
}
} // namespace display_device
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ get_property(libraries GLOBAL PROPERTY DD_TEST_LIBRARIES)

add_executable(${TEST_BINARY} ${sources})
target_link_libraries(${TEST_BINARY}
PUBLIC
gmock_main # if we use this we don't need our own main function
libdisplaydevice # we are always testing at least the public API so it's safe to always link this
libfixtures # these are our fixtures/helpers for the tests
Expand Down
5 changes: 3 additions & 2 deletions tests/unit/windows/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Add the test files in this directory
add_dd_test_dir(
ADDITIONAL_LIBRARIES
Boost::scope
libwindows

ADDITIONAL_SOURCES
mocks/*.h
mocks/*.cpp
utils/*.h
utils/*.cpp
)
61 changes: 9 additions & 52 deletions tests/unit/windows/test_winapiutils.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// local includes
#include "displaydevice/windows/winapiutils.h"
#include "fixtures.h"
#include "mocks/mockwinapilayer.h"
#include "utils/comparison.h"
#include "utils/mockwinapilayer.h"

namespace {
// Convenience keywords for GMock
Expand Down Expand Up @@ -128,57 +129,6 @@ namespace {

} // namespace

// 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;
}
} // namespace display_device

TEST_F_S_MOCKED(IsAvailable) {
DISPLAYCONFIG_PATH_INFO available_path;
DISPLAYCONFIG_PATH_INFO unavailable_path;
Expand Down Expand Up @@ -745,3 +695,10 @@ TEST_F_S_MOCKED(MakePathsForNewTopology, IndexOutOfRange) {
const std::vector<DISPLAYCONFIG_PATH_INFO> expected_paths {};
EXPECT_EQ(display_device::win_utils::makePathsForNewTopology(new_topology, EXPECTED_SOURCE_INDEX_DATA, {}), expected_paths);
}

TEST_F_S_MOCKED(MakePathsForNewTopology, EmptyList) {
const display_device::ActiveTopology new_topology {};

const std::vector<DISPLAYCONFIG_PATH_INFO> expected_paths {};
EXPECT_EQ(display_device::win_utils::makePathsForNewTopology(new_topology, EXPECTED_SOURCE_INDEX_DATA, PATHS_WITH_SOURCE_IDS), expected_paths);
}
Loading
Loading