blob: 2f7df67657a28564262c83c6ba3f577ea0c00a31 [file] [log] [blame]
// Copyright 2024 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/linux/test/virtual_display_util_linux.h"
#include <algorithm>
#include <limits>
#include <vector>
#include "base/environment.h"
#include "base/nix/xdg_util.h"
#include "remoting/host/desktop_geometry.h"
#include "remoting/host/x11_desktop_resizer.h"
#include "ui/display/display.h"
#include "ui/display/display_list.h"
#include "ui/display/screen.h"
#include "ui/display/types/display_constants.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/x/randr.h"
namespace {
// Appends a new screen with `resolution` to the specified desktop
// `layout`. Arranges horizontally left to right.
void AppendScreen(remoting::DesktopLayoutSet& layout,
const remoting::DesktopResolution& resolution) {
// Find the rightmost screen layout.
const remoting::DesktopLayout* rightmost_layout = nullptr;
for (const auto& screen : layout.layouts) {
if (rightmost_layout == nullptr ||
screen.rect().right() > rightmost_layout->rect().right()) {
rightmost_layout = &screen;
}
}
layout.layouts.emplace_back(
std::nullopt,
gfx::Rect(rightmost_layout->rect().right() + 1,
rightmost_layout->position_y(), resolution.dimensions().width(),
resolution.dimensions().height()),
resolution.dpi());
}
} // namespace
namespace display::test {
VirtualDisplayUtilLinux::VirtualDisplayUtilLinux(Screen* screen)
: screen_(screen),
desktop_resizer_(std::make_unique<remoting::X11DesktopResizer>()),
initial_layout_(desktop_resizer_->GetLayout()),
current_layout_(initial_layout_) {
CHECK(screen_);
screen_->AddObserver(this);
}
VirtualDisplayUtilLinux::~VirtualDisplayUtilLinux() {
ResetDisplays();
screen_->RemoveObserver(this);
}
// static
bool VirtualDisplayUtilLinux::IsAPIAvailable() {
// Check if XRandR is running with a sufficient number of connected outputs.
// Skip base::nix::GetSessionType(...), which may return kTty instead of kX11
// in SSH sessions with virtualized X11 environments.
constexpr auto kConnected = static_cast<x11::RandR::RandRConnection>(0);
constexpr auto kDisabled = static_cast<x11::RandR::Crtc>(0);
x11::Connection* x11_connection = x11::Connection::Get();
if (!x11_connection) {
LOG(ERROR) << "X11 is not present.";
return false;
}
x11::RandR& xrandr = x11_connection->randr();
if (!xrandr.present()) {
LOG(ERROR) << "XRandR is not present.";
return false;
}
x11::Response<x11::RandR::GetScreenResourcesCurrentReply> screen_resources =
xrandr.GetScreenResourcesCurrent({x11_connection->default_screen().root})
.Sync();
if (!screen_resources.reply) {
LOG(ERROR) << "GetScreenResourcesCurrent failed.";
return false;
}
int connected_and_disabled_outputs = 0;
for (const auto& output : screen_resources.reply->outputs) {
std::unique_ptr<x11::RandR::GetOutputInfoReply> output_reply =
xrandr.GetOutputInfo(output, screen_resources.reply->config_timestamp)
.Sync()
.reply;
if (output_reply && output_reply->connection == kConnected &&
output_reply->crtc == kDisabled) {
connected_and_disabled_outputs++;
}
}
return connected_and_disabled_outputs >= kMaxDisplays;
}
int64_t VirtualDisplayUtilLinux::AddDisplay(
uint8_t id,
const DisplayParams& display_params) {
if (requested_ids_to_display_ids_.contains(id) ||
std::find(requested_ids_.begin(), requested_ids_.end(), id) !=
requested_ids_.end()) {
LOG(ERROR) << "Virtual display with id " << id
<< " already exists or requested.";
return kInvalidDisplayId;
}
if (current_layout_.layouts.size() - initial_layout_.layouts.size() >
kMaxDisplays) {
LOG(ERROR) << "Cannot exceed " << kMaxDisplays << " virtual displays.";
return kInvalidDisplayId;
}
CHECK(!current_layout_.layouts.empty());
last_requested_layout_ = current_layout_;
AppendScreen(last_requested_layout_,
remoting::DesktopResolution(display_params.resolution,
display_params.dpi));
requested_ids_.push_back(id);
desktop_resizer_->SetVideoLayout(last_requested_layout_);
detected_added_display_ids_.clear();
StartWaiting();
CHECK_EQ(detected_added_display_ids_.size(), 1u)
<< "Did not detect exactly one new display.";
// Reconcile the added resizer display ID to the detected display::Display id.
int64_t new_display_id = detected_added_display_ids_.front();
detected_added_display_ids_.pop_front();
remoting::DesktopLayoutSet prev_layout = current_layout_;
current_layout_ = desktop_resizer_->GetLayout();
for (const auto& layout : current_layout_.layouts) {
auto was_added =
std::find_if(prev_layout.layouts.begin(), prev_layout.layouts.end(),
[&](const remoting::DesktopLayout& prev) {
return prev.rect() == layout.rect();
});
if (was_added == prev_layout.layouts.end()) {
display_id_to_resizer_id_[new_display_id] = *layout.screen_id();
}
}
return new_display_id;
}
void VirtualDisplayUtilLinux::RemoveDisplay(int64_t display_id) {
if (!display_id_to_resizer_id_.contains(display_id)) {
LOG(ERROR) << "Invalid display_id. Missing mapping for " << display_id
<< " to resizer ID.";
return;
}
last_requested_layout_ = current_layout_;
std::erase_if(last_requested_layout_.layouts,
[&](const remoting::DesktopLayout& layout) {
return layout.screen_id() ==
display_id_to_resizer_id_[display_id];
});
desktop_resizer_->SetVideoLayout(last_requested_layout_);
StartWaiting();
}
void VirtualDisplayUtilLinux::ResetDisplays() {
last_requested_layout_ = initial_layout_;
desktop_resizer_->SetVideoLayout(last_requested_layout_);
StartWaiting();
current_layout_ = desktop_resizer_->GetLayout();
}
void VirtualDisplayUtilLinux::OnDisplayAdded(
const display::Display& new_display) {
// TODO(crbug.com/40257169): Support adding multiple displays at a time, or
// ignoring external display configuration changes.
CHECK_EQ(requested_ids_.size(), 1u)
<< "An extra display was detected that was either not requested by this "
"controller, or multiple displays were requested concurrently. This "
"is not supported.";
detected_added_display_ids_.push_back(new_display.id());
uint8_t requested_id = requested_ids_.front();
requested_ids_.pop_front();
requested_ids_to_display_ids_[requested_id] = new_display.id();
OnDisplayAddedOrRemoved(new_display.id());
}
void VirtualDisplayUtilLinux::OnDisplaysRemoved(
const display::Displays& removed_displays) {
for (const auto& display : removed_displays) {
base::EraseIf(requested_ids_to_display_ids_,
[&](std::pair<uint8_t, int64_t>& pair) {
return pair.second == display.id();
});
base::EraseIf(display_id_to_resizer_id_,
[&](std::pair<DisplayId, ResizerDisplayId>& pair) {
return pair.first == display.id();
});
base::EraseIf(detected_added_display_ids_,
[&](DisplayId& id) { return id == display.id(); });
OnDisplayAddedOrRemoved(display.id());
}
}
void VirtualDisplayUtilLinux::OnDisplayAddedOrRemoved(int64_t id) {
if (!RequestedLayoutIsSet()) {
return;
}
StopWaiting();
}
bool VirtualDisplayUtilLinux::RequestedLayoutIsSet() {
// Checks that the number of virtual displays (delta of last requested layout
// minus initial layout) is equal to the number of requested displays.
return last_requested_layout_.layouts.size() -
initial_layout_.layouts.size() ==
requested_ids_to_display_ids_.size();
}
void VirtualDisplayUtilLinux::StartWaiting() {
CHECK(!run_loop_);
if (RequestedLayoutIsSet()) {
return;
}
run_loop_ = std::make_unique<base::RunLoop>();
run_loop_->Run();
run_loop_.reset();
}
void VirtualDisplayUtilLinux::StopWaiting() {
CHECK(run_loop_);
run_loop_->Quit();
}
// static
std::unique_ptr<VirtualDisplayUtil> VirtualDisplayUtil::TryCreate(
Screen* screen) {
if (!VirtualDisplayUtilLinux::IsAPIAvailable()) {
return nullptr;
}
return std::make_unique<VirtualDisplayUtilLinux>(screen);
}
} // namespace display::test