// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/display/win/test/virtual_display_util_win.h"
#include <algorithm>
#include <iterator>
#include "base/containers/flat_tree.h"
#include "base/logging.h"
#include "third_party/win_virtual_display/driver/public/properties.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/display/types/display_constants.h"
#include "ui/display/win/display_config_helper.h"
#include "ui/display/win/screen_win.h"
namespace display::test {
namespace {
// Comparer for gfx:Size for use in sorting algorithms.
struct SizeCompare {
bool operator()(const gfx::Size& a, const gfx::Size& b) const {
if (a.width() == b.width()) {
return a.height() < b.height();
}
return a.width() < b.width();
}
};
// Returns true if the current environment is detected to be headless.
bool IsHeadless() {
DISPLAY_DEVICE adapter{};
adapter.cb = sizeof(adapter);
for (DWORD i = 0;
EnumDisplayDevices(nullptr, i, &adapter, EDD_GET_DEVICE_INTERFACE_NAME);
i++) {
if (adapter.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) {
// If any driver is considered attached, then not headless.
// Note the default stub driver ("Microsoft Basic Display Driver") is
// never considered "attached".
return false;
}
}
return true;
}
// Sets the default display resolution to the specified size.
bool SetDisplayResolution(const gfx::Size& size) {
DEVMODE mode;
mode.dmSize = sizeof(DEVMODE);
mode.dmPelsWidth = size.width();
mode.dmPelsHeight = size.height();
mode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT;
LONG settings_result = ::ChangeDisplaySettings(&mode, 0);
LOG_IF(ERROR, settings_result != DISP_CHANGE_SUCCESSFUL)
<< "ChangeDisplaySettings failed with result: " << settings_result;
return settings_result == DISP_CHANGE_SUCCESSFUL;
}
// Creates a comma separated list of display strings (for debug /logs).
std::string JoinDisplayStrings(const std::vector<display::Display>& displays) {
std::vector<std::string> parts;
for (const auto& display : displays) {
parts.push_back(display.ToString());
}
return base::JoinString(parts, ", ");
}
} // namespace
struct DisplayParams {
explicit DisplayParams(MonitorConfig config) : monitor_config(config) {}
MonitorConfig monitor_config;
};
VirtualDisplayUtilWin::VirtualDisplayUtilWin(Screen* screen)
: screen_(screen), is_headless_(IsHeadless()) {
screen_->AddObserver(this);
initial_displays_ = screen_->GetAllDisplays();
if (IsAPIAvailable()) {
ResetDisplays();
}
}
VirtualDisplayUtilWin::~VirtualDisplayUtilWin() {
if (IsAPIAvailable()) {
driver_controller_.Reset();
if (virtual_displays_.size() > 0) {
current_config_ = DriverProperties();
StartWaiting();
}
}
screen_->RemoveObserver(this);
std::vector<display::Display> current_displays = screen_->GetAllDisplays();
// Restore the display to the initial size and wait for displays to update.
// This is necessary because Windows may change the resolution of the default
// display (Microsoft Basic Display) while virtual displays exists and does
// not automatically restore the original resolution.
if (is_headless_) {
CHECK_EQ(initial_displays_.size(), 1u);
CHECK_EQ(current_displays.size(), 1u);
const gfx::Size& initial_size = initial_displays_.front().size();
bool set_resolution_result = SetDisplayResolution(initial_size);
CHECK(set_resolution_result) << "SetDisplayResolution failed.";
DisplayConfigWaiter waiter(screen_);
waiter.WaitForDisplaySizes(std::vector<gfx::Size>{initial_size});
// Basic check to ensure that the # of displays and resolutions match what
// was in place when the utility was constructed. Helps prevent lingering
// changes that may impact other tests running on the bot (i.e.
// crbug.com/341931537).
CHECK_EQ(current_displays.size(), initial_displays_.size())
<< "# of displays mismatch after driver was closed.";
std::multiset<gfx::Size, SizeCompare> initial_sizes, current_sizes;
for (const auto& display : screen_->GetAllDisplays()) {
current_sizes.insert(display.size());
}
for (const auto& display : initial_displays_) {
initial_sizes.insert(display.size());
}
CHECK(initial_sizes == current_sizes)
<< "Display resolution does not match initial config. Initial: "
<< JoinDisplayStrings(initial_displays_)
<< ", current: " << JoinDisplayStrings(current_displays);
}
}
// static
bool VirtualDisplayUtilWin::IsAPIAvailable() {
return DisplayDriverController::IsDriverInstalled();
}
int64_t VirtualDisplayUtilWin::AddDisplay(uint8_t id,
const DisplayParams& display_params) {
if (virtual_displays_.find(id) != virtual_displays_.end()) {
LOG(ERROR) << "Duplicate virtual display ID added: " << id;
return kInvalidDisplayId;
}
std::vector<MonitorConfig> monitors;
monitors = current_config_.requested_configs();
MonitorConfig new_config = display_params.monitor_config;
new_config.set_product_code(id);
monitors.push_back(new_config);
if (!SetDriverProperties(DriverProperties(monitors))) {
return kInvalidDisplayId;
}
StartWaiting();
auto created_display = virtual_displays_.find(id);
CHECK(created_display != virtual_displays_.end());
return created_display->second;
}
void VirtualDisplayUtilWin::RemoveDisplay(int64_t display_id) {
auto it = std::find_if(
virtual_displays_.begin(), virtual_displays_.end(),
[&display_id](const std::pair<unsigned short, int64_t>& obj) {
return obj.second == display_id;
});
if (it == virtual_displays_.end()) {
LOG(WARNING) << "Display ID " << display_id << " is not a virtual display.";
return;
}
std::vector<MonitorConfig> monitors = current_config_.requested_configs();
std::erase_if(monitors, [&it](MonitorConfig& c) {
return c.product_code() == it->first;
});
if (SetDriverProperties(DriverProperties(monitors))) {
StartWaiting();
}
}
void VirtualDisplayUtilWin::ResetDisplays() {
// Internal virtual display ID used for replacing a headless stub display.
// This is arbitrarily chosen to be the max value that
// MonitorConfig::set_product_code() supports.
constexpr unsigned short kHeadlessDisplayId = 65535;
DriverProperties prev_config = current_config_;
DriverProperties new_config{};
if (is_headless_) {
LOG(INFO) << "Headless detected, setting base virtual display.";
// In a headless environment, when no working display adapter is attached,
// windows defaults to a pseudo/stub display. This causes the first call to
// AddDisplay() to not add a display, but *replace* the stub with our
// virtualized adapter. Therefore, we replace the default stub display with
// our own virtual one. See:
// https://learn.microsoft.com/en-us/windows-hardware/drivers/display/support-for-headless-systems
std::vector<MonitorConfig> configs{k1024x768.monitor_config};
configs[0].set_product_code(kHeadlessDisplayId);
new_config = DriverProperties(configs);
}
SetDriverProperties(new_config);
if (current_config_.requested_configs().size() !=
prev_config.requested_configs().size()) {
StartWaiting();
}
}
void VirtualDisplayUtilWin::OnDisplayAdded(
const display::Display& new_display) {
std::vector<MonitorConfig> requested = current_config_.requested_configs();
HMONITOR monitor = ::MonitorFromPoint(
win::ScreenWin::DIPToScreenPoint(new_display.work_area().CenterPoint())
.ToPOINT(),
MONITOR_DEFAULTTONEAREST);
std::optional<DISPLAYCONFIG_PATH_INFO> path_info =
::display::win::GetDisplayConfigPathInfo(monitor);
if (::display::win::GetDisplayManufacturerId(path_info) ==
kDriverMonitorManufacturer) {
UINT16 product_code = ::display::win::GetDisplayProductCode(path_info);
auto it = virtual_displays_.find(product_code);
// Should never detect multiple displays with the same product code.
CHECK(it == virtual_displays_.end())
<< "Detected duplicate virtual display product code:" << product_code;
virtual_displays_[product_code] = new_display.id();
}
OnDisplayAddedOrRemoved(new_display.id());
}
void VirtualDisplayUtilWin::OnDisplaysRemoved(
const display::Displays& removed_displays) {
for (const auto& display : removed_displays) {
base::EraseIf(virtual_displays_,
[&display](const std::pair<unsigned short, int64_t>& obj) {
return obj.second == display.id();
});
OnDisplayAddedOrRemoved(display.id());
}
}
bool VirtualDisplayUtilWin::SetDriverProperties(DriverProperties properties) {
if (!driver_controller_.SetDisplayConfig(properties)) {
LOG(ERROR) << "SetDisplayConfig failed: Failed to set display properties.";
return false;
}
current_config_ = properties;
return true;
}
void VirtualDisplayUtilWin::OnDisplayAddedOrRemoved(int64_t id) {
if (virtual_displays_.size() != current_config_.requested_configs().size()) {
return;
}
StopWaiting();
}
void VirtualDisplayUtilWin::StartWaiting() {
CHECK(!run_loop_);
run_loop_ = std::make_unique<base::RunLoop>();
if (virtual_displays_.size() != current_config_.requested_configs().size()) {
run_loop_->Run();
}
run_loop_.reset();
}
void VirtualDisplayUtilWin::StopWaiting() {
CHECK(run_loop_);
run_loop_->Quit();
}
const DisplayParams VirtualDisplayUtilWin::k1920x1080 =
DisplayParams(MonitorConfig::k1920x1080);
const DisplayParams VirtualDisplayUtilWin::k1024x768 =
DisplayParams(MonitorConfig::k1024x768);
// VirtualDisplayUtil definitions:
const DisplayParams VirtualDisplayUtil::k1920x1080 =
VirtualDisplayUtilWin::k1920x1080;
const DisplayParams VirtualDisplayUtil::k1024x768 =
VirtualDisplayUtilWin::k1024x768;
// static
std::unique_ptr<VirtualDisplayUtil> VirtualDisplayUtil::TryCreate(
Screen* screen) {
if (!VirtualDisplayUtilWin::IsAPIAvailable()) {
return nullptr;
}
return std::make_unique<VirtualDisplayUtilWin>(screen);
}
DisplayConfigWaiter::DisplayConfigWaiter(Screen* screen) : screen_(screen) {
screen_->AddObserver(this);
}
DisplayConfigWaiter::~DisplayConfigWaiter() {
screen_->RemoveObserver(this);
}
void DisplayConfigWaiter::WaitForDisplaySizes(std::vector<gfx::Size> sizes) {
wait_for_sizes_ = std::move(sizes);
CHECK(!run_loop_);
run_loop_ = std::make_unique<base::RunLoop>();
if (!IsWaitConditionMet()) {
run_loop_->Run();
}
run_loop_.reset();
}
void DisplayConfigWaiter::OnDisplayAdded(const display::Display& new_display) {
if (IsWaitConditionMet()) {
run_loop_->Quit();
}
}
void DisplayConfigWaiter::OnDisplaysRemoved(
const display::Displays& removed_displays) {
if (IsWaitConditionMet()) {
run_loop_->Quit();
}
}
bool DisplayConfigWaiter::IsWaitConditionMet() {
std::multiset<gfx::Size, SizeCompare> expected_sizes, current_sizes;
for (display::Display display : screen_->GetAllDisplays()) {
current_sizes.insert(display.size());
}
for (const auto& size : wait_for_sizes_) {
expected_sizes.insert(size);
}
return expected_sizes == current_sizes;
}
} // namespace display::test