blob: 699ab623d9e1dbef37a34b3b1eacf1403da50301 [file] [log] [blame]
// 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 "remoting/host/linux/gnome_display_config.h"
#include <glib.h>
#include <algorithm>
#include "base/check_op.h"
#include "base/hash/hash.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/types/cxx23_to_underlying.h"
#include "remoting/base/logging.h"
#include "third_party/webrtc/modules/portal/scoped_glib.h"
#include "ui/gfx/geometry/rect.h"
namespace remoting {
// static
webrtc::ScreenId GnomeDisplayConfig::GetScreenId(
std::string_view monitor_name) {
// Mutter backend is hardcoded to return `Meta-$virtualId` as the monitor
// name, where $virtualId is sequentially numbered starting at 0 and recycled
// after the pipewire stream is destroyed.
// See:
// https://gitlab.gnome.org/GNOME/mutter/-/blob/51a3c7e8d3cce425a7617aee22c47b4e8c238871/src/backends/native/meta-output-virtual.c#L46
constexpr std::string_view kMetaPrefix = "Meta-";
if (monitor_name.starts_with(kMetaPrefix)) {
int64_t screen_id;
if (base::StringToInt64(monitor_name.substr(kMetaPrefix.length()),
&screen_id)) {
return screen_id;
}
}
// If in any case it doesn't match the pattern, we just use the hash of the
// monitor name as the screen ID. We add 1<<32 so that it doesn't conflict
// with the Meta- displays. On 64-bit machines, the hash value is 32-bit while
// the screen ID is 64 bit so there won't be overflow issues. On 32-bit
// machines, we can't do this trick, but the likelihood of a collision is
// still pretty low.
webrtc::ScreenId screen_id_adjustment = 0;
if constexpr (sizeof(webrtc::ScreenId) >= sizeof(int64_t)) {
screen_id_adjustment = 1ULL << 32;
}
return base::PersistentHash(monitor_name) + screen_id_adjustment;
}
////////////////////////////////////////////////////////////////////////////////
// GnomeDisplayConfig::MonitorInfo
GnomeDisplayConfig::MonitorMode::MonitorMode() = default;
GnomeDisplayConfig::MonitorMode::MonitorMode(const MonitorMode&) = default;
GnomeDisplayConfig::MonitorMode& GnomeDisplayConfig::MonitorMode::operator=(
const MonitorMode&) = default;
GnomeDisplayConfig::MonitorMode::~MonitorMode() = default;
bool GnomeDisplayConfig::MonitorMode::operator==(
const MonitorMode& other) const = default;
GnomeDisplayConfig::MonitorInfo::MonitorInfo() = default;
GnomeDisplayConfig::MonitorInfo::MonitorInfo(
const GnomeDisplayConfig::MonitorInfo&) = default;
GnomeDisplayConfig::MonitorInfo& GnomeDisplayConfig::MonitorInfo::operator=(
const GnomeDisplayConfig::MonitorInfo&) = default;
GnomeDisplayConfig::MonitorInfo::~MonitorInfo() = default;
bool GnomeDisplayConfig::MonitorInfo::operator==(
const GnomeDisplayConfig::MonitorInfo& other) const = default;
const GnomeDisplayConfig::MonitorMode*
GnomeDisplayConfig::MonitorInfo::GetCurrentMode() const {
for (auto& mode : modes) {
if (mode.is_current) {
return &mode;
}
}
return nullptr;
}
////////////////////////////////////////////////////////////////////////////////
// GnomeDisplayConfig
GnomeDisplayConfig::GnomeDisplayConfig() = default;
GnomeDisplayConfig::GnomeDisplayConfig(const GnomeDisplayConfig&) = default;
GnomeDisplayConfig& GnomeDisplayConfig::operator=(const GnomeDisplayConfig&) =
default;
GnomeDisplayConfig::~GnomeDisplayConfig() = default;
void GnomeDisplayConfig::AddMonitorFromVariant(GVariant* monitor) {
// With the Xorg "video_dummy" driver, the "Connector" value is the name of
// the X11 RANDR Output: "DUMMYnn".
webrtc::Scoped<char> connector;
webrtc::Scoped<GVariantIter> modes;
constexpr char kMonitorFormat[] = "((ssss)a(siiddada{sv})a{sv})";
if (!g_variant_check_format_string(monitor, kMonitorFormat,
/*copy_only=*/FALSE)) {
LOG(ERROR) << __func__ << " : monitor has incorrect type.";
return;
}
g_variant_get(monitor, kMonitorFormat, connector.receive(),
/*vendor=*/nullptr, /*product_name=*/nullptr,
/*product_serial=*/nullptr, modes.receive(),
/*properties=*/nullptr);
MonitorInfo info;
while (true) {
webrtc::Scoped<char> mode_id;
gint32 mode_width;
gint32 mode_height;
gdouble mode_refresh;
webrtc::Scoped<GVariantIter> supported_scales_iter;
webrtc::Scoped<GVariant> mode_properties;
if (!g_variant_iter_next(modes.get(), "(siiddad@a{sv})", mode_id.receive(),
&mode_width, &mode_height, &mode_refresh,
/*preferred_scale=*/nullptr,
supported_scales_iter.receive(),
mode_properties.receive())) {
break;
}
MonitorMode mode;
mode.name = mode_id.get();
mode.width = mode_width;
mode.height = mode_height;
gboolean is_current = FALSE;
g_variant_lookup(mode_properties.get(), "is-current", "b", &is_current);
mode.is_current = is_current;
gdouble scale;
while (g_variant_iter_next(supported_scales_iter.get(), "d", &scale)) {
mode.supported_scales.push_back(scale);
}
info.modes.push_back(std::move(mode));
}
// Each connector name should be unique, since it is used as an
// identifier by the ApplyMonitorsConfig DBus API. Since this information
// comes from an external API, it is better to cleanly overwrite any
// previous value, rather than risk duplicating some monitor-modes.
monitors[connector.get()] = info;
}
void GnomeDisplayConfig::AddLogicalMonitorFromVariant(
GVariant* logical_monitor) {
gint32 x;
gint32 y;
gdouble scale;
gboolean primary;
webrtc::Scoped<GVariantIter> monitors_iter;
constexpr char kLogicalMonitorFormat[] = "(iiduba(ssss)a{sv})";
if (!g_variant_check_format_string(logical_monitor, kLogicalMonitorFormat,
/*copy_only=*/FALSE)) {
LOG(ERROR) << __func__ << " : logical_monitor has incorrect type.";
return;
}
g_variant_get(logical_monitor, kLogicalMonitorFormat, &x, &y, &scale,
/*rotation=*/nullptr, &primary, monitors_iter.receive(),
/*properties=*/nullptr);
gsize num_monitors = g_variant_iter_n_children(monitors_iter.get());
if (num_monitors != 1) {
LOG(ERROR) << "Logical monitor has unexpected number of monitors: "
<< num_monitors;
return;
}
webrtc::Scoped<char> connector;
bool result = g_variant_iter_next(monitors_iter.get(), "(ssss)",
connector.receive(), /*vendor=*/nullptr,
/*product=*/nullptr, /*serial=*/nullptr);
if (!result) {
LOG(ERROR) << "Failed to read monitor properties.";
return;
}
MonitorInfo& info = monitors[connector.get()];
info.x = x;
info.y = y;
info.scale = scale;
info.is_primary = primary;
}
ScopedGVariant GnomeDisplayConfig::BuildMonitorsConfigParameters() const {
GVariantBuilder logical_monitors_builder;
g_variant_builder_init(&logical_monitors_builder,
G_VARIANT_TYPE("a(iiduba(ssa{sv}))"));
for (const auto& [id, monitor] : monitors) {
const auto* mode = monitor.GetCurrentMode();
if (!mode) {
// This monitor is disabled, and should be skipped. GNOME will disable
// monitors that are not provided in the ApplyMonitorConfig() request.
continue;
}
gint x = monitor.x;
gint y = monitor.y;
gdouble scale = monitor.scale;
guint transform = 0; // No rotation/reflection.
gboolean is_primary = monitor.is_primary;
GVariantBuilder monitor_list_builder;
g_variant_builder_init(&monitor_list_builder, G_VARIANT_TYPE("a(ssa{sv})"));
const gchar* connector = id.c_str();
const gchar* mode_id = mode->name.c_str();
g_variant_builder_add(&monitor_list_builder, "(ssa{sv})", connector,
mode_id, nullptr);
g_variant_builder_add(&logical_monitors_builder, "(iiduba(ssa{sv}))", x, y,
scale, transform, is_primary, &monitor_list_builder);
}
GVariantBuilder properties_builder;
g_variant_builder_init(&properties_builder, G_VARIANT_TYPE("a{sv}"));
g_variant_builder_add(&properties_builder, "{sv}", "layout-mode",
g_variant_new_uint32(base::to_underlying(layout_mode)));
return TakeGVariant(g_variant_new(
"(uua(iiduba(ssa{sv}))a{sv})", serial, base::to_underlying(method),
&logical_monitors_builder, &properties_builder));
}
std::map<std::string, GnomeDisplayConfig::MonitorInfo>::iterator
GnomeDisplayConfig::FindMonitor(webrtc::ScreenId screen_id) {
return std::ranges::find_if(monitors, [screen_id](const auto& kv) {
return GnomeDisplayConfig::GetScreenId(kv.first) == screen_id;
});
}
void GnomeDisplayConfig::SwitchLayoutMode(LayoutMode new_layout_mode) {
if (layout_mode != new_layout_mode) {
LayoutInfo info = GetLayoutInfo();
info.layout_mode = new_layout_mode;
Relayout(info);
}
}
void GnomeDisplayConfig::Relayout(const LayoutInfo& layout_info) {
if (monitors.size() <= 1) {
// No need to recalculate monitor offset since it's always (0, 0).
layout_mode = layout_info.layout_mode;
return;
}
if (layout_info.direction == LayoutDirection::kVertical) {
PackVertically(layout_info.layout_mode, layout_info.alignment);
} else {
Transpose();
PackVertically(layout_info.layout_mode, layout_info.alignment);
Transpose();
}
NormalizeMonitorOffsets();
}
void GnomeDisplayConfig::RemoveInvalidMonitors() {
std::erase_if(monitors,
[](const auto& kv) { return !kv.second.GetCurrentMode(); });
}
GnomeDisplayConfig::LayoutInfo GnomeDisplayConfig::GetLayoutInfo() const {
LayoutDirection direction = GetLayoutDirection();
return {layout_mode, direction, GetLayoutAlignment(direction)};
}
GnomeDisplayConfig::LayoutDirection GnomeDisplayConfig::GetLayoutDirection()
const {
if (monitors.size() <= 1) {
return LayoutDirection::kHorizontal;
}
const MonitorInfo& monitor1 = monitors.begin()->second;
const MonitorInfo& monitor2 = std::next(monitors.begin())->second;
// Determine the layout of the first two monitors, per the algorithm at
// //ui/gfx/x/x11_crtc_resizer.cc.
int left1 = monitor1.x;
int right1 = left1 + GetLayoutSize(monitor1, &MonitorMode::width);
int left2 = monitor2.x;
int right2 = left2 + GetLayoutSize(monitor2, &MonitorMode::width);
return (right1 > left2 && right2 > left1) ? LayoutDirection::kVertical
: LayoutDirection::kHorizontal;
}
GnomeDisplayConfig::LayoutAlignment GnomeDisplayConfig::GetLayoutAlignment(
LayoutDirection direction) const {
if (monitors.empty()) {
return LayoutAlignment::kUnknown;
}
if (direction == LayoutDirection::kHorizontal) {
const_cast<GnomeDisplayConfig*>(this)->Transpose();
}
bool is_left_aligned = true;
bool is_middle_aligned = true;
bool is_right_aligned = true;
const MonitorInfo& first_monitor = monitors.begin()->second;
auto first_monitor_left = first_monitor.x;
auto first_monitor_layout_width =
GetLayoutSize(first_monitor, &MonitorMode::width);
auto first_monitor_middle =
first_monitor_left + first_monitor_layout_width / 2;
auto first_monitor_right = first_monitor_left + first_monitor_layout_width;
for (const auto& [_, monitor_info] : monitors) {
auto monitor_left = monitor_info.x;
auto layout_width = GetLayoutSize(monitor_info, &MonitorMode::width);
auto monitor_middle = monitor_info.x + layout_width / 2;
auto monitor_right = monitor_info.x + layout_width;
if (monitor_left != first_monitor_left) {
is_left_aligned = false;
}
if (abs(monitor_middle - first_monitor_middle) > 1) {
is_middle_aligned = false;
}
if (monitor_right != first_monitor_right) {
is_right_aligned = false;
}
}
if (direction == LayoutDirection::kHorizontal) {
const_cast<GnomeDisplayConfig*>(this)->Transpose();
}
return is_left_aligned ? LayoutAlignment::kStart
: is_middle_aligned ? LayoutAlignment::kCenter
: is_right_aligned ? LayoutAlignment::kEnd
: LayoutAlignment::kUnknown;
}
void GnomeDisplayConfig::PackVertically(LayoutMode new_layout_mode,
LayoutAlignment alignment) {
DCHECK(!monitors.empty());
std::vector<MonitorInfo*> monitor_list;
monitor_list.reserve(monitors.size());
for (auto& kv : monitors) {
monitor_list.push_back(&kv.second);
}
// Sort vertically before packing.
std::ranges::sort(monitor_list,
[](MonitorInfo* a, MonitorInfo* b) { return a->y < b->y; });
// Pack the monitors by setting their y-offsets. If necessary, change the
// x-offset for right-alignment.
int current_y = 0;
layout_mode = new_layout_mode;
for (auto* monitor_info : monitor_list) {
monitor_info->y = current_y;
current_y += GetLayoutSize(*monitor_info, &MonitorMode::height);
// Place all monitors, respecting any alignment preference. If there are
// multiple possible alignments, prioritize left, then right, then middle.
// TODO: crbug.com/40225767 - Implement a more sophisticated algorithm that
// tries to preserve pairwise alignment. It is not enough to leave the
// x-offsets unchanged here - this tends to result in the monitors being
// arranged roughly diagonally, wasting lots of space. Some amount of
// horizontal compression is needed to prevent this from happening.
int layout_width = GetLayoutSize(*monitor_info, &MonitorMode::width);
switch (alignment) {
case LayoutAlignment::kStart:
monitor_info->x = 0;
break;
case LayoutAlignment::kCenter:
monitor_info->x = -layout_width / 2;
break;
case LayoutAlignment::kEnd:
monitor_info->x = -layout_width;
break;
default:
// The current implementation left-aligns the monitors if no other
// alignment is detected.
// TODO: crbug.com/40225767 - A future enhancement may be to detect and
// report one of {left, middle, right, none}. The "none" case (for
// vertical and horizontal layouts) could be treated as a
// client-controlled layout, where the host does not attempt any
// repositioning. In this case, the host could still support
// resize-to-fit, but in a simplified way - resize would be allowed
// whenever it creates no overlaps.
monitor_info->x = 0;
break;
}
}
}
void GnomeDisplayConfig::Transpose() {
for (auto& [_, monitor_info] : monitors) {
std::swap(monitor_info.x, monitor_info.y);
for (auto& mode : monitor_info.modes) {
std::swap(mode.width, mode.height);
}
}
}
void GnomeDisplayConfig::NormalizeMonitorOffsets() {
gfx::Rect bounding_box;
for (const auto& [_, monitor] : monitors) {
bounding_box.Union(gfx::Rect(monitor.x, monitor.y,
GetLayoutSize(monitor, &MonitorMode::width),
GetLayoutSize(monitor, &MonitorMode::height)));
}
gfx::Vector2d adjustment =
gfx::Vector2d(-bounding_box.origin().x(), -bounding_box.origin().y());
if (adjustment.IsZero()) {
return;
}
for (auto& [_, monitor] : monitors) {
monitor.x += adjustment.x();
monitor.y += adjustment.y();
}
}
int GnomeDisplayConfig::GetLayoutSize(
const MonitorInfo& monitor,
int MonitorMode::* width_or_height) const {
const MonitorMode* current_mode = monitor.GetCurrentMode();
if (!current_mode) {
LOG(WARNING) << "Cannot find current mode for monitor";
return 0;
}
return current_mode->*width_or_height /
(layout_mode == LayoutMode::kLogical ? monitor.scale : 1.0);
}
} // namespace remoting