0

Reland "Add VirtualDisplayUtilLinux with X11 support."

This is a reland of commit c20ada30ff
I've updated the build file conditionals to prevent an assertion
error on wayland ozone configuration on LaCros.

Original change's description:
> Add VirtualDisplayUtilLinux with X11 support.
>
> Adds VirtualDisplayUtilLinux class which uses
> remoting::X11DesktopResizer as the backing x11/XRandR logic.
> Adjusts some build rules to isolate some of the //remoting x11 utilities into its own component (to avoid layering/dependency violations).
>
> Bug: 40257169
> Change-Id: Ic7c3dd3cb7070acb6374ab2a161cff29ce8383bf
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5422325
> Reviewed-by: Lambros Lambrou <lambroslambrou@chromium.org>
> Auto-Submit: Brad Triebwasser <btriebw@chromium.org>
> Reviewed-by: Bruce Dawson <brucedawson@chromium.org>
> Reviewed-by: Mike Wasserman <msw@chromium.org>
> Reviewed-by: ccameron chromium <ccameron@chromium.org>
> Commit-Queue: Brad Triebwasser <btriebw@chromium.org>
> Reviewed-by: Ben Pastene <bpastene@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1304762}

Bug: 40257169
Change-Id: Iff5e306df1d5abedca600ea213378d9baa88d1fa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5560555
Reviewed-by: ccameron chromium <ccameron@chromium.org>
Reviewed-by: Lambros Lambrou <lambroslambrou@chromium.org>
Reviewed-by: Mike Wasserman <msw@chromium.org>
Reviewed-by: Bruce Dawson <brucedawson@chromium.org>
Commit-Queue: Brad Triebwasser <btriebw@chromium.org>
Reviewed-by: Ben Pastene <bpastene@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1305345}
This commit is contained in:
Brad Triebwasser
2024-05-23 22:24:09 +00:00
committed by Chromium LUCI CQ
parent 3ee67f50bc
commit d6ce652d6f
10 changed files with 555 additions and 63 deletions

@ -32,6 +32,7 @@ group("gtk") {
"//examples:peerconnection_client",
"//remoting/host:common",
"//remoting/host:remoting_me2me_host_static",
"//remoting/host:x11_display_utils",
"//remoting/host/file_transfer",
"//remoting/host/it2me:common",
"//remoting/host/it2me:main",

@ -11272,7 +11272,6 @@ if (!is_android && !is_chromeos_device) {
"//ui/base:test_support",
"//ui/base/clipboard:clipboard_test_support",
"//ui/compositor:test_support",
"//ui/display:display_interactive_ui_tests",
"//ui/display:test_support",
"//ui/events:events_interactive_ui_tests",
"//ui/events:gesture_detection",
@ -11698,6 +11697,7 @@ if (!is_android && !is_chromeos_device) {
"../browser/ui/views/accessibility/uia_accessibility_event_waiter.cc",
"../browser/ui/views/accessibility/uia_accessibility_event_waiter.h",
"../browser/ui/views/touch_events_interactive_uitest_win.cc",
"//ui/display/win/test/virtual_display_util_win_interactive_uitest.cc",
]
if (use_aura) {
@ -11740,6 +11740,7 @@ if (!is_android && !is_chromeos_device) {
"../browser/ui/find_bar/find_bar_platform_helper_mac_interactive_uitest.mm",
"../browser/ui/views/frame/immersive_mode_controller_mac_interactive_uitest.mm",
"../browser/webauthn/chrome_webauthn_autofill_mac_interactive_uitest.mm",
"//ui/display/mac/test/virtual_display_util_mac_interactive_uitest.mm",
]
sources -= [
@ -11794,6 +11795,9 @@ if (!is_android && !is_chromeos_device) {
}
if (is_linux) {
if (ozone_platform_x11) {
sources += [ "//ui/display/linux/test/virtual_display_util_linux_interactive_uitest.cc" ]
}
deps += [ "//ui/linux:linux_ui" ]
}

@ -297,10 +297,6 @@ static_library("common") {
"desktop_display_info_loader.h",
"desktop_display_info_monitor.cc",
"desktop_display_info_monitor.h",
"desktop_display_layout_util.cc",
"desktop_display_layout_util.h",
"desktop_geometry.cc",
"desktop_geometry.h",
"desktop_process.cc",
"desktop_process.h",
"desktop_resizer.h",
@ -437,6 +433,7 @@ static_library("common") {
deps = [
":client_session_control",
":clipboard",
":display_layout",
":host_main_headers",
":ipc_constants",
":resources",
@ -570,18 +567,12 @@ static_library("common") {
"linux/desktop_resizer_wayland.h",
"linux/ei_event_watcher_glib.cc",
"linux/ei_event_watcher_glib.h",
"linux/gnome_display_config.cc",
"linux/gnome_display_config.h",
"linux/gnome_display_config_dbus_client.cc",
"linux/gnome_display_config_dbus_client.h",
"linux/input_injector_wayland.cc",
"linux/input_injector_wayland.h",
"linux/remote_desktop_portal.cc",
"linux/remote_desktop_portal.h",
"linux/remote_desktop_portal_injector.cc",
"linux/remote_desktop_portal_injector.h",
"linux/scoped_glib.cc",
"linux/scoped_glib.h",
"linux/wayland_connection.cc",
"linux/wayland_connection.h",
"linux/wayland_desktop_capturer.cc",
@ -596,12 +587,6 @@ static_library("common") {
"linux/wayland_manager.h",
"linux/wayland_seat.cc",
"linux/wayland_seat.h",
"x11_crtc_resizer.cc",
"x11_crtc_resizer.h",
"x11_desktop_resizer.cc",
"x11_desktop_resizer.h",
"x11_display_util.cc",
"x11_display_util.h",
]
libs += [ "//third_party/libei/lib64/libei.a" ]
public_deps += [
@ -609,9 +594,8 @@ static_library("common") {
"//third_party/wayland-protocols:xdg_output_protocol",
]
deps += [
"//build/config/linux/gtk",
":x11_display_utils",
"//remoting/host/linux:wayland",
"//remoting/host/linux:x11",
"//ui/base/x",
"//ui/events/platform/x11",
"//ui/gfx/x",
@ -743,6 +727,45 @@ static_library("common") {
}
}
source_set("display_layout") {
sources = [
"desktop_display_layout_util.cc",
"desktop_display_layout_util.h",
"desktop_geometry.cc",
"desktop_geometry.h",
]
deps = [ "//ui/gfx" ]
}
if (is_linux && (ozone_platform_x11 || remoting_use_x11)) {
source_set("x11_display_utils") {
sources = [
"linux/gnome_display_config.cc",
"linux/gnome_display_config.h",
"linux/gnome_display_config_dbus_client.cc",
"linux/gnome_display_config_dbus_client.h",
"linux/scoped_glib.cc",
"linux/scoped_glib.h",
"x11_crtc_resizer.cc",
"x11_crtc_resizer.h",
"x11_desktop_resizer.cc",
"x11_desktop_resizer.h",
"x11_display_util.cc",
"x11_display_util.h",
]
deps = [
":display_layout",
"//build/config/linux/gtk",
"//remoting/base:logging",
"//remoting/host/linux:x11",
"//third_party/webrtc_overrides:webrtc_component",
"//ui/base",
"//ui/base/x",
"//ui/gfx/geometry",
]
}
}
static_library("test_support") {
testonly = true
@ -857,6 +880,7 @@ source_set("unit_tests") {
":chromoting_host_services_client",
":client_session_control",
":common",
":display_layout",
":resources",
":test_support",
"//build:branding_buildflags",
@ -891,8 +915,8 @@ source_set("unit_tests") {
"//skia",
"//testing/gmock",
"//testing/gtest",
"//ui/base:base",
"//ui/events:events",
"//ui/base",
"//ui/events",
]
# ChromotingHostServices currently only works on Linux and Windows.
@ -927,7 +951,10 @@ source_set("unit_tests") {
"x11_crtc_resizer_unittest.cc",
"x11_display_util_unittest.cc",
]
deps += [ "//ui/gfx/x" ]
deps += [
":x11_display_utils",
"//ui/gfx/x",
]
}
if (is_chromeos_ash) {

@ -171,6 +171,7 @@ source_set("unit_tests") {
deps = [
"//remoting/host:common",
"//remoting/host:test_support",
"//remoting/host:x11_display_utils",
"//remoting/host/it2me:common",
"//remoting/host/native_messaging",
"//remoting/host/security_key:unit_tests",

@ -28,7 +28,6 @@ DEFAULT_XVFB_WHD = '1280x800x24'
# pylint: disable=useless-object-inheritance
class _X11ProcessError(Exception):
"""Exception raised when Xvfb or Xorg cannot start."""
@ -168,20 +167,29 @@ def run_executable(cmd,
return test_env.run_executable(cmd, env, stdoutfile, cwd)
def _make_xorg_config(whd):
"""Generates an Xorg config file based on the specified WxHxD string and
returns the file path. See:
def _make_xorg_modeline(width, height, refresh):
"""Generates a tuple of a modeline (list of parameters) and label based off a
specified width, height and refresh rate.
See: https://www.x.org/archive/X11R7.0/doc/html/chips4.html"""
cvt_output = subprocess.check_output(
['cvt', str(width), str(height),
str(refresh)],
stderr=subprocess.STDOUT,
text=True)
re_matches = re.search('Modeline "(.*)"\s+(.*)', cvt_output, re.IGNORECASE)
modeline_label = re_matches.group(1)
modeline = re_matches.group(2)
# Split the modeline string on spaces, and filter out empty element (cvt adds
# double spaces between in some parts).
return (modeline_label, list(filter(lambda a: a != '', modeline.split(' '))))
def _make_xorg_config():
"""Generates an Xorg config file and returns the file path. See:
https://www.x.org/releases/current/doc/man/man5/xorg.conf.5.xhtml"""
(width, height, depth) = whd.split('x')
modeline = subprocess.check_output(['cvt', width, height, '60'],
stderr=subprocess.STDOUT,
text=True)
modeline_label = re.search('Modeline "(.*)"', modeline,
re.IGNORECASE).group(1)
config = f"""
config = """
Section "Monitor"
Identifier "Monitor0"
{modeline}
EndSection
Section "Device"
Identifier "Device0"
@ -190,14 +198,9 @@ Section "Device"
VideoRam 256000
EndSection
Section "Screen"
DefaultDepth {depth}
Identifier "Screen0"
Device "Device0"
Monitor "Monitor0"
SubSection "Display"
Depth {depth}
Modes "{modeline_label}"
EndSubSection
EndSection
"""
config_file = os.path.join(tempfile.gettempdir(), 'xorg.config')
@ -206,6 +209,49 @@ EndSection
return config_file
def _setup_xrandr(env, default_whd):
"""Configures xrandr dummy displays from xserver-xorg-video-dummy package."""
(width, height, _) = default_whd.split('x')
default_size = (int(width), int(height))
xrandr_sizes = [(800, 600), (1024, 768), (1920, 1080), (1600, 1200),
(3840, 2160)]
if (default_size not in xrandr_sizes):
xrandr_sizes.append(default_size)
xrandr_sizes = sorted(xrandr_sizes)
output_names = ['DUMMY0', 'DUMMY1', 'DUMMY2', 'DUMMY3', 'DUMMY4']
refresh_rate = 60
# Calls xrandr with the provided argument array
def call_xrandr(args):
subprocess.check_call(['xrandr'] + args,
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT)
for width, height in xrandr_sizes:
(modeline_label, modeline) = _make_xorg_modeline(width, height,
refresh_rate)
call_xrandr(['--newmode', modeline_label] + modeline)
for output_name in output_names:
call_xrandr(['--addmode', output_name, modeline_label])
(default_mode_label, _) = _make_xorg_modeline(*default_size, refresh_rate)
# Set the mode of all monitors to connect and activate them.
for i in range(0, len(output_names)):
args = ['--output', output_names[i], '--mode', default_mode_label]
if (i > 0):
args += ['--right-of', output_names[i - 1]]
call_xrandr(args)
# Sets the primary monitor (DUMMY0) to the default size and marks the rest as
# disabled.
call_xrandr(["-s", "%dx%d" % default_size])
# Set the DPI to something realistic (as required by some desktops).
call_xrandr(['--dpi', '96'])
def _run_with_x11(cmd, env, stdoutfile, use_openbox, use_xcompmgr, use_xorg,
xvfb_whd, cwd):
"""Runs with an X11 server. Uses Xvfb by default and Xorg when use_xorg is
@ -225,7 +271,7 @@ def _run_with_x11(cmd, env, stdoutfile, use_openbox, use_xcompmgr, use_xorg,
dbus_pid = None
x11_binary = 'Xorg' if use_xorg else 'Xvfb'
xorg_config_file = _make_xorg_config(xvfb_whd) if use_xorg else None
xorg_config_file = _make_xorg_config() if use_xorg else None
try:
signal.signal(signal.SIGTERM, raise_x11_error)
signal.signal(signal.SIGINT, raise_x11_error)
@ -317,6 +363,9 @@ def _run_with_x11(cmd, env, stdoutfile, use_openbox, use_xcompmgr, use_xorg,
stderr=subprocess.STDOUT,
env=env)
if use_xorg:
_setup_xrandr(env, xvfb_whd)
return test_env.run_executable(cmd, env, stdoutfile, cwd)
except OSError as e:
print('Failed to start %s or Openbox: %s\n' % (x11_binary, str(e)),

@ -253,6 +253,17 @@ static_library("test_support") {
]
}
if (is_linux && ozone_platform_x11) {
sources += [
"linux/test/virtual_display_util_linux.cc",
"linux/test/virtual_display_util_linux.h",
]
deps += [
"//remoting/host:display_layout",
"//remoting/host:x11_display_utils",
]
}
if (is_chromeos) {
sources += [ "test/display_test_util.cc" ]
}
@ -284,7 +295,7 @@ static_library("test_support") {
public_deps += [ "//ui/display:managed_display_info" ]
}
if (!is_mac && !is_win) {
if (!is_mac && !is_win && !(is_linux && ozone_platform_x11)) {
sources += [
# Virtual display util stub for unimplemented platforms.
"test/virtual_display_util_stub.cc",
@ -376,25 +387,3 @@ test("display_unittests") {
deps += [ "//mojo/core/test:run_all_unittests" ]
}
}
# This target is added as a dependency of browser interactive_ui_tests. It must
# be source_set, otherwise the linker will drop the tests as dead code.
source_set("display_interactive_ui_tests") {
testonly = true
if (is_mac) {
sources = [ "mac/test/virtual_display_util_mac_interactive_uitest.mm" ]
}
if (is_win) {
sources = [ "win/test/virtual_display_util_win_interactive_uitest.cc" ]
}
if (is_win || is_mac) {
deps = [
":display",
":test_support",
"//base/test:test_support",
"//testing/gtest",
]
}
}

@ -0,0 +1,5 @@
include_rules = [
"+ui/aura/screen_ozone.h",
"+remoting/host/x11_desktop_resizer.h",
"+remoting/host/desktop_geometry.h"
]

@ -0,0 +1,222 @@
// 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"
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 {
struct DisplayParams {
explicit DisplayParams(remoting::DesktopResolution resolution)
: resolution(resolution) {}
remoting::DesktopResolution resolution;
};
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() {
// Wayland is currently unimplemented. Note that this detection uses
// XDG_SESSION_TYPE environment variable. When running in a pure SSH / virtual
// X server, it may be necessary to manually set XDG_SESSION_TYPE=x11 in the
// command.
static base::nix::SessionType session_type =
base::nix::GetSessionType(*base::Environment::Create());
return session_type == base::nix::SessionType::kX11;
}
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_, display_params.resolution);
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
const DisplayParams VirtualDisplayUtilLinux::k1920x1080 = DisplayParams(
remoting::DesktopResolution(gfx::Size(1920, 1080), gfx::Vector2d(96, 96)));
const DisplayParams VirtualDisplayUtilLinux::k1024x768 = DisplayParams(
remoting::DesktopResolution(gfx::Size(1024, 768), gfx::Vector2d(96, 96)));
// VirtualDisplayUtil definitions:
const DisplayParams VirtualDisplayUtil::k1920x1080 =
VirtualDisplayUtilLinux::k1920x1080;
const DisplayParams VirtualDisplayUtil::k1024x768 =
VirtualDisplayUtilLinux::k1024x768;
// static
std::unique_ptr<VirtualDisplayUtil> VirtualDisplayUtil::TryCreate(
Screen* screen) {
if (!VirtualDisplayUtilLinux::IsAPIAvailable()) {
return nullptr;
}
return std::make_unique<VirtualDisplayUtilLinux>(screen);
}
} // namespace display::test

@ -0,0 +1,93 @@
// 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.
#ifndef UI_DISPLAY_LINUX_TEST_VIRTUAL_DISPLAY_UTIL_LINUX_H_
#define UI_DISPLAY_LINUX_TEST_VIRTUAL_DISPLAY_UTIL_LINUX_H_
#include "base/containers/circular_deque.h"
#include "base/containers/flat_map.h"
#include "base/run_loop.h"
#include "remoting/host/x11_desktop_resizer.h"
#include "ui/display/display_observer.h"
#include "ui/display/test/virtual_display_util.h"
namespace display {
class Display;
class Screen;
namespace test {
// Linux implementation of VirtualDisplayUtil. This uses remote desktop code
// (remoting::X11DesktopResizer) to do the X11/XRandR heavy lifting.
class VirtualDisplayUtilLinux : public display::DisplayObserver,
public VirtualDisplayUtil {
public:
explicit VirtualDisplayUtilLinux(Screen* screen);
~VirtualDisplayUtilLinux() override;
// Maximum number of displays that can be added through AddDisplay().
// It should be one less than the number of dummy monitors configured in
// //testing/xvfb.py
static constexpr int kMaxDisplays = 4;
// Check whether the related drivers are available on the current system.
static bool IsAPIAvailable();
// VirtualDisplayUtil overrides:
int64_t AddDisplay(uint8_t id, const DisplayParams& display_params) override;
void RemoveDisplay(int64_t display_id) override;
void ResetDisplays() override;
// These should be a subset of the resolutions configured in //testing/xvfb.py
static const DisplayParams k800x600;
static const DisplayParams k1024x768;
static const DisplayParams k1280x800;
static const DisplayParams k1920x1080;
static const DisplayParams k1600x1200;
static const DisplayParams k3840x2160;
private:
// display::DisplayObserver:
void OnDisplayAdded(const display::Display& new_display) override;
void OnDisplaysRemoved(const display::Displays& removed_displays) override;
void OnDisplayAddedOrRemoved(int64_t id);
bool RequestedLayoutIsSet();
// Start waiting for the detected displays to match `current_config_`.
void StartWaiting();
void StopWaiting();
std::unique_ptr<base::RunLoop> run_loop_;
raw_ptr<Screen> screen_;
std::unique_ptr<remoting::X11DesktopResizer> desktop_resizer_;
// Initial layout when this class was instantiated that should be restored.
remoting::DesktopLayoutSet initial_layout_;
// Current layout calculated by `desktop_resizer_` after an operation.
remoting::DesktopLayoutSet current_layout_;
// Last layout request sent to `desktop_resizer_`.
remoting::DesktopLayoutSet last_requested_layout_;
// There are lots of IDS to track here:
// 1. The user-requested ID set in AddDisplay().
// 2. The resizer (xrandr) display ID
// 3. The display ID detected by the display::Screen implementation.
using RequestedId = uint8_t;
using ResizerDisplayId = int64_t;
using DisplayId = int64_t;
// Queue of displays added via OnDisplayAdded. Removed as they are reconciled
// and moved to `display_id_to_resizer_id_`.
base::circular_deque<DisplayId> detected_added_display_ids_;
base::flat_map<DisplayId, ResizerDisplayId> display_id_to_resizer_id_;
// Tracks display IDs requested in AddDisplay(). The IDs don't do anything in
// this implementation, but they are tracked to prevent the user from
// specifying the same ID twice without deleting it first (to match other
// platform behavior);
base::circular_deque<RequestedId> requested_ids_;
base::flat_map<RequestedId, DisplayId> requested_ids_to_display_ids_;
};
} // namespace test
} // namespace display
#endif // UI_DISPLAY_LINUX_TEST_VIRTUAL_DISPLAY_UTIL_LINUX_H_

@ -0,0 +1,101 @@
// 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 <memory>
#include "base/command_line.h"
#include "base/test/task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/screen_ozone.h"
#include "ui/display/linux/test/virtual_display_util_linux.h"
#include "ui/display/screen.h"
#include "ui/display/types/display_constants.h"
class VirtualDisplayUtilLinuxInteractiveUitest : public testing::Test {
public:
VirtualDisplayUtilLinuxInteractiveUitest(
const VirtualDisplayUtilLinuxInteractiveUitest&) = delete;
VirtualDisplayUtilLinuxInteractiveUitest& operator=(
const VirtualDisplayUtilLinuxInteractiveUitest&) = delete;
protected:
VirtualDisplayUtilLinuxInteractiveUitest() = default;
~VirtualDisplayUtilLinuxInteractiveUitest() override = default;
void SetUp() override {
if (!display::test::VirtualDisplayUtilLinux::IsAPIAvailable()) {
GTEST_SKIP() << "Host does not support virtual displays.";
}
CHECK(!display::Screen::HasScreen());
screen_ = std::make_unique<aura::ScreenOzone>();
virtual_display_util_ =
std::make_unique<display::test::VirtualDisplayUtilLinux>(screen());
testing::Test::SetUp();
}
display::Screen* screen() { return screen_.get(); }
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::UI};
std::unique_ptr<aura::ScreenOzone> screen_;
std::unique_ptr<display::test::VirtualDisplayUtilLinux> virtual_display_util_;
};
TEST_F(VirtualDisplayUtilLinuxInteractiveUitest, IsAPIAvailable) {
EXPECT_TRUE(virtual_display_util_->IsAPIAvailable());
}
TEST_F(VirtualDisplayUtilLinuxInteractiveUitest, AddDisplay) {
int initial_display_count = screen()->GetNumDisplays();
int64_t display_id = virtual_display_util_->AddDisplay(
1, display::test::VirtualDisplayUtilLinux::k1920x1080);
EXPECT_NE(display_id, display::kInvalidDisplayId);
EXPECT_EQ(screen()->GetNumDisplays(), initial_display_count + 1);
display::Display d;
EXPECT_TRUE(screen()->GetDisplayWithDisplayId(display_id, &d));
EXPECT_EQ(d.size(), gfx::Size(1920, 1080));
// Expect failure when adding a duplicate index.
EXPECT_EQ(virtual_display_util_->AddDisplay(
1, display::test::VirtualDisplayUtilLinux::k1920x1080),
display::kInvalidDisplayId);
virtual_display_util_->ResetDisplays();
EXPECT_FALSE(screen()->GetDisplayWithDisplayId(display_id, &d));
EXPECT_EQ(screen()->GetNumDisplays(), initial_display_count);
}
TEST_F(VirtualDisplayUtilLinuxInteractiveUitest, AddRemove) {
int64_t display_id[3];
int initial_display_count = screen()->GetNumDisplays();
display_id[0] = virtual_display_util_->AddDisplay(
0, display::test::VirtualDisplayUtilLinux::k1920x1080);
EXPECT_NE(display_id[0], display::kInvalidDisplayId);
display::Display d;
EXPECT_TRUE(screen()->GetDisplayWithDisplayId(display_id[0], &d));
display_id[1] = virtual_display_util_->AddDisplay(
1, display::test::VirtualDisplayUtilLinux::k1024x768);
EXPECT_NE(display_id[1], display::kInvalidDisplayId);
EXPECT_TRUE(screen()->GetDisplayWithDisplayId(display_id[1], &d));
display_id[2] = virtual_display_util_->AddDisplay(
2, display::test::VirtualDisplayUtilLinux::k1920x1080);
EXPECT_NE(display_id[2], display::kInvalidDisplayId);
EXPECT_TRUE(screen()->GetDisplayWithDisplayId(display_id[2], &d));
EXPECT_EQ(screen()->GetNumDisplays(), initial_display_count + 3);
virtual_display_util_->RemoveDisplay(display_id[1]);
EXPECT_EQ(screen()->GetNumDisplays(), initial_display_count + 2);
// Only virtual display 2 should no longer exist.
EXPECT_TRUE(screen()->GetDisplayWithDisplayId(display_id[0], &d));
EXPECT_EQ(d.size(), gfx::Size(1920, 1080));
EXPECT_FALSE(screen()->GetDisplayWithDisplayId(display_id[1], &d));
EXPECT_TRUE(screen()->GetDisplayWithDisplayId(display_id[2], &d));
EXPECT_EQ(d.size(), gfx::Size(1920, 1080));
virtual_display_util_->ResetDisplays();
EXPECT_FALSE(screen()->GetDisplayWithDisplayId(display_id[0], &d));
EXPECT_FALSE(screen()->GetDisplayWithDisplayId(display_id[1], &d));
EXPECT_FALSE(screen()->GetDisplayWithDisplayId(display_id[2], &d));
EXPECT_EQ(screen()->GetNumDisplays(), initial_display_count);
}