910e62b5创建于 1月15日历史提交
// 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