910e62b5创建于 1月15日历史提交
// Copyright 2018 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/base/x/x11_display_util.h"

#include <dlfcn.h>

#include <algorithm>
#include <bit>
#include <bitset>
#include <numeric>
#include <queue>
#include <unordered_set>

#include "base/bits.h"
#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/containers/flat_map.h"
#include "base/logging.h"
#include "base/notimplemented.h"
#include "base/numerics/clamped_math.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/x/x11_util.h"
#include "ui/display/util/display_util.h"
#include "ui/display/util/edid_parser.h"
#include "ui/gfx/color_space.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/switches.h"
#include "ui/gfx/x/atom_cache.h"
#include "ui/gfx/x/connection.h"
#include "ui/gfx/x/randr.h"
#include "ui/strings/grit/ui_strings.h"

namespace ui {

namespace {

// Need at least xrandr version 1.3
constexpr std::pair<uint32_t, uint32_t> kMinVersionXrandr{1, 3};

constexpr const char kRandrEdidProperty[] = "EDID";

std::map<x11::RandR::Output, size_t> GetMonitors(
    const x11::Response<x11::RandR::GetMonitorsReply>& reply) {
  std::map<x11::RandR::Output, size_t> output_to_monitor;
  if (!reply) {
    return output_to_monitor;
  }
  for (size_t monitor = 0; monitor < reply->monitors.size(); monitor++) {
    for (x11::RandR::Output output : reply->monitors[monitor].outputs) {
      output_to_monitor[output] = monitor;
    }
  }
  return output_to_monitor;
}

x11::Future<x11::GetPropertyReply> GetWorkAreaFuture(
    x11::Connection* connection) {
  return connection->GetProperty({
      .window = connection->default_root(),
      .property = connection->GetAtom("_NET_WORKAREA"),
      .long_length = 4,
  });
}

gfx::Rect GetWorkAreaSync(x11::Future<x11::GetPropertyReply> future) {
  auto response = future.Sync();
  if (!response || response->format != 32 || response->value_len != 4) {
    return gfx::Rect();
  }
  const uint32_t* value = response->value->cast_to<uint32_t>();
  return gfx::Rect(value[0], UNSAFE_TODO(value[1]), UNSAFE_TODO(value[2]),
                   UNSAFE_TODO(value[3]));
}

x11::Future<x11::GetPropertyReply> GetIccProfileFuture(
    x11::Connection* connection,
    size_t monitor) {
  std::string atom_name = monitor == 0
                              ? "_ICC_PROFILE"
                              : base::StringPrintf("_ICC_PROFILE_%zu", monitor);
  auto future = connection->GetProperty({
      .window = connection->default_root(),
      .property = x11::GetAtom(atom_name.c_str()),
      .long_length = std::numeric_limits<uint32_t>::max(),
  });
  future.IgnoreError();
  return future;
}

gfx::ICCProfile GetIccProfileSync(x11::Future<x11::GetPropertyReply> future) {
  auto response = future.Sync();
  if (!response || !response->value_len) {
    return gfx::ICCProfile();
  }
  return gfx::ICCProfile::FromData(response->value->bytes(),
                                   response->value_len * response->format / 8u);
}

x11::Future<x11::RandR::GetOutputPropertyReply> GetEdidFuture(
    x11::Connection* connection,
    x11::RandR::Output output) {
  auto future = connection->randr().GetOutputProperty({
      .output = output,
      .property = x11::GetAtom(kRandrEdidProperty),
      .long_length = 128,
  });
  future.IgnoreError();
  return future;
}

// Sets the work area on a list of displays.  The work area for each display
// must already be initialized to the display bounds.  At most one display out
// of |displays| will be affected.
void ClipWorkArea(std::vector<display::Display>* displays,
                  size_t primary_display_index,
                  const gfx::Rect& net_workarea) {
  if (net_workarea.IsEmpty()) {
    return;
  }

  auto get_work_area = [&](const display::Display& display) {
    float scale = display::Display::HasForceDeviceScaleFactor()
                      ? display::Display::GetForcedDeviceScaleFactor()
                      : display.device_scale_factor();
    return gfx::ScaleToEnclosingRect(net_workarea, 1.0f / scale);
  };

  // If the work area entirely contains exactly one display, assume it's meant
  // for that display (and so do nothing).
  if (std::ranges::count_if(*displays, [&](const display::Display& display) {
        return get_work_area(display).Contains(display.bounds());
      }) == 1) {
    return;
  }

  // If the work area is entirely contained within exactly one display, assume
  // it's meant for that display and intersect the work area with only that
  // display.
  const auto found =
      std::ranges::find_if(*displays, [&](const display::Display& display) {
        return display.bounds().Contains(get_work_area(display));
      });

  // If the work area spans multiple displays, intersect the work area with the
  // primary display, like GTK does.
  display::Display& primary =
      found == displays->end() ? (*displays)[primary_display_index] : *found;

  gfx::Rect work_area = get_work_area(primary);
  work_area.Intersect(primary.work_area());
  if (!work_area.IsEmpty()) {
    primary.set_work_area(work_area);
  }
}

float GetRefreshRateFromXRRModeInfo(
    const std::vector<x11::RandR::ModeInfo>& modes,
    x11::RandR::Mode current_mode_id) {
  for (const auto& mode_info : modes) {
    if (static_cast<x11::RandR::Mode>(mode_info.id) != current_mode_id) {
      continue;
    }
    if (!mode_info.htotal || !mode_info.vtotal) {
      return 0;
    }

    // Refresh Rate = Pixel Clock / (Horizontal Total * Vertical Total)
    return mode_info.dot_clock /
           static_cast<float>(mode_info.htotal * mode_info.vtotal);
  }
  return 0;
}

int DefaultBitsPerComponent() {
  auto* connection = x11::Connection::Get();
  const x11::VisualType& visual = connection->default_root_visual();

  // The mask fields are only valid for DirectColor and TrueColor classes.
  if (visual.c_class == x11::VisualClass::DirectColor ||
      visual.c_class == x11::VisualClass::TrueColor) {
    // RGB components are packed into fixed size integers for each visual.  The
    // layout of bits in the packing is given by
    // |visual.{red,green,blue}_mask|.  Count the number of bits to get the
    // number of bits per component.
    auto bits = [](auto mask) {
      return std::bitset<sizeof(mask) * 8>{mask}.count();
    };
    size_t red_bits = bits(visual.red_mask);
    size_t green_bits = bits(visual.green_mask);
    size_t blue_bits = bits(visual.blue_mask);
    if (red_bits == green_bits && red_bits == blue_bits) {
      return red_bits;
    }
  }

  // Next, try getting the number of colormap entries per subfield.  If it's a
  // power of 2, log2 is a possible guess for the number of bits per component.
  if (std::has_single_bit(visual.colormap_entries)) {
    return base::bits::Log2Ceiling(visual.colormap_entries);
  }

  // |bits_per_rgb| can sometimes be unreliable (may be 11 for 30bpp visuals),
  // so only use it as a last resort.
  return visual.bits_per_rgb_value;
}

// Get the EDID data from the `output` and stores to `edid`.
std::vector<uint8_t> GetEdidProperty(
    x11::Response<x11::RandR::GetOutputPropertyReply> response) {
  std::vector<uint8_t> edid;
  if (response && response->format == 8 && response->type != x11::Atom::None) {
    edid = std::move(response->data);
  }
  return edid;
}

float GetDisplayScale(const gfx::Rect& bounds,
                      const display::DisplayConfig& display_config) {
  constexpr auto kMaxDist = std::make_pair(INT_MAX, INT_MAX);
  auto min_dist_scale = std::make_pair(kMaxDist, display_config.primary_scale);
  for (const auto& geometry : display_config.display_geometries) {
    const auto dist_scale = std::make_pair(
        RectDistance(geometry.bounds_px, bounds), geometry.scale);
    min_dist_scale = std::min(min_dist_scale, dist_scale);
  }
  return min_dist_scale.second;
}

gfx::PointF DisplayOriginPxToDip(const display::Display& parent,
                                 const display::Display& child,
                                 const gfx::PointF& parent_origin_dip) {
  const gfx::Rect parent_px = parent.bounds();
  const gfx::Rect child_px = child.bounds();
  const float parent_scale = parent.device_scale_factor();
  const float child_scale = child.device_scale_factor();
  // Given a range [parent_l_px, parent_r_px) with scale factor `parent_scale`
  // and with `parent_l_px` mapping to `parent_l_dip`, and another range
  // [child_l_px, child_r_px) with scale factor `child_scale`, converts
  // `child_l_px` to DIPs in the child's coordinate system.
  auto map_coordinate = [&](int parent_l_px, int parent_r_px, int child_l_px,
                            int child_r_px, float parent_l_dip) {
    const base::ClampedNumeric<int> l = std::max(parent_l_px, child_l_px);
    const base::ClampedNumeric<int> r = std::min(parent_r_px, child_r_px);
    const float mid_px = std::midpoint<float>(float(l), float(r));
    const float mid_dip = (mid_px - parent_l_px) / parent_scale + parent_l_dip;
    return (child_l_px - mid_px) / child_scale + mid_dip;
  };
  const float x = map_coordinate(parent_px.x(), parent_px.right(), child_px.x(),
                                 child_px.right(), parent_origin_dip.x());
  const float y =
      map_coordinate(parent_px.y(), parent_px.bottom(), child_px.y(),
                     child_px.bottom(), parent_origin_dip.y());
  return {x, y};
}

}  // namespace

std::vector<display::Display> GetFallbackDisplayList(
    float scale,
    size_t* primary_display_index_out) {
  auto* connection = x11::Connection::Get();
  const auto& screen = connection->default_screen();
  gfx::Size physical_size(screen.width_in_millimeters,
                          screen.height_in_millimeters);

  int width = screen.width_in_pixels;
  int height = screen.height_in_pixels;
  gfx::Rect bounds_in_pixels(0, 0, width, height);
  display::Display gfx_display(0, bounds_in_pixels);

  if (!display::Display::HasForceDeviceScaleFactor() &&
      display::IsDisplaySizeValid(physical_size)) {
    DCHECK_LE(1.0f, scale);
    gfx_display.set_size_in_pixels(bounds_in_pixels.size());
    gfx_display.SetScale(scale);
    auto bounds_dip = gfx::ScaleToEnclosingRect(bounds_in_pixels, 1.0f / scale);
    gfx_display.set_bounds(bounds_dip);
    gfx_display.set_work_area(bounds_dip);
  } else {
    scale = 1;
  }

  gfx_display.set_color_depth(screen.root_depth);
  gfx_display.set_depth_per_component(DefaultBitsPerComponent());

  std::vector<display::Display> displays{gfx_display};
  *primary_display_index_out = 0;

  ClipWorkArea(&displays, *primary_display_index_out,
               GetWorkAreaSync(GetWorkAreaFuture(connection)));

  return displays;
}

std::vector<display::Display> BuildDisplaysFromXRandRInfo(
    const display::DisplayConfig& display_config,
    size_t* primary_display_index_out) {
  DCHECK(primary_display_index_out);
  auto* command_line = base::CommandLine::ForCurrentProcess();
  const float primary_scale = display_config.primary_scale;

  auto* connection = x11::Connection::Get();
  DCHECK(connection->randr_version() >= kMinVersionXrandr);
  auto& randr = connection->randr();
  auto x_root_window = ui::GetX11RootWindow();
  std::vector<display::Display> displays;

  auto resources_future = randr.GetScreenResourcesCurrent({x_root_window});
  auto output_primary_future = randr.GetOutputPrimary({x_root_window});
  x11::Future<x11::RandR::GetMonitorsReply> monitors_future;
  if (connection->randr_version() >= std::pair<uint32_t, uint32_t>{1, 5}) {
    monitors_future = randr.GetMonitors(x_root_window);
  }
  auto work_area_future = GetWorkAreaFuture(connection);
  connection->Flush();

  auto resources = resources_future.Sync();
  if (!resources) {
    LOG(ERROR) << "XRandR returned no displays; falling back to root window";
    return GetFallbackDisplayList(primary_scale, primary_display_index_out);
  }

  const int depth = connection->default_screen().root_depth;
  const int bits_per_component = DefaultBitsPerComponent();

  auto output_primary = output_primary_future.Sync();
  if (!output_primary) {
    return GetFallbackDisplayList(primary_scale, primary_display_index_out);
  }
  x11::RandR::Output primary_display_id = output_primary->output;

  const auto monitors_reply = monitors_future.Sync();
  const auto output_to_monitor = GetMonitors(monitors_reply);
  const size_t n_iccs =
      monitors_reply ? std::max<size_t>(1, monitors_reply->monitors.size()) : 1;

  int explicit_primary_display_index = -1;
  int monitor_order_primary_display_index = -1;

  std::vector<x11::Future<x11::RandR::GetCrtcInfoReply>> crtc_futures{};
  crtc_futures.reserve(resources->crtcs.size());
  for (auto crtc : resources->crtcs) {
    crtc_futures.push_back(
        randr.GetCrtcInfo({crtc, resources->config_timestamp}));
  }
  connection->Flush();

  std::vector<x11::Future<x11::GetPropertyReply>> icc_futures{n_iccs};
  if (!command_line->HasSwitch(switches::kHeadless)) {
    for (size_t monitor = 0; monitor < n_iccs; ++monitor) {
      icc_futures[monitor] = GetIccProfileFuture(connection, monitor);
    }
    connection->Flush();
  }

  std::vector<x11::Future<x11::RandR::GetOutputInfoReply>> output_futures{};
  output_futures.reserve(resources->outputs.size());
  for (auto output : resources->outputs) {
    output_futures.push_back(
        randr.GetOutputInfo({output, resources->config_timestamp}));
  }
  connection->Flush();

  std::vector<x11::Future<x11::RandR::GetOutputPropertyReply>> edid_futures{};
  edid_futures.reserve(resources->outputs.size());
  for (auto output : resources->outputs) {
    edid_futures.push_back(GetEdidFuture(connection, output));
  }
  connection->Flush();

  base::flat_map<x11::RandR::Crtc, x11::RandR::GetCrtcInfoResponse> crtcs;
  for (size_t i = 0; i < resources->crtcs.size(); ++i) {
    crtcs.emplace(resources->crtcs[i], crtc_futures[i].Sync());
  }

  std::vector<gfx::ICCProfile> iccs;
  iccs.reserve(n_iccs);
  for (auto& future : icc_futures) {
    iccs.push_back(GetIccProfileSync(std::move(future)));
  }

  for (size_t i = 0; i < resources->outputs.size(); i++) {
    x11::RandR::Output output_id = resources->outputs[i];
    auto output_info = output_futures[i].Sync();
    if (!output_info) {
      continue;
    }

    if (output_info->connection != x11::RandR::RandRConnection::Connected) {
      continue;
    }

    bool is_primary_display = (output_id == primary_display_id);

    if (output_info->crtc == static_cast<x11::RandR::Crtc>(0)) {
      continue;
    }

    auto crtc_it = crtcs.find(output_info->crtc);
    if (crtc_it == crtcs.end()) {
      continue;
    }
    const auto& crtc = crtc_it->second;
    if (!crtc) {
      continue;
    }

    display::EdidParser edid_parser(GetEdidProperty(edid_futures[i].Sync()));
    auto output_32 = static_cast<uint32_t>(output_id);
    int64_t display_id =
        output_32 > 0xff ? 0 : edid_parser.GetIndexBasedDisplayId(output_32);
    // It isn't ideal, but if we can't parse the EDID data, fall back on the
    // display number.
    if (!display_id) {
      display_id = i;
    }

    gfx::Rect crtc_bounds(crtc->x, crtc->y, crtc->width, crtc->height);
    const size_t display_index = displays.size();
    display::Display& display = displays.emplace_back(display_id, crtc_bounds);
    display.set_native_origin(crtc_bounds.origin());

    display.set_audio_formats(edid_parser.audio_formats());
    switch (crtc->rotation) {
      case x11::RandR::Rotation::Rotate_0:
        display.set_rotation(display::Display::ROTATE_0);
        break;
      case x11::RandR::Rotation::Rotate_90:
        display.set_rotation(display::Display::ROTATE_90);
        break;
      case x11::RandR::Rotation::Rotate_180:
        display.set_rotation(display::Display::ROTATE_180);
        break;
      case x11::RandR::Rotation::Rotate_270:
        display.set_rotation(display::Display::ROTATE_270);
        break;
      case x11::RandR::Rotation::Reflect_X:
      case x11::RandR::Rotation::Reflect_Y:
        NOTIMPLEMENTED();
    }

    if (is_primary_display) {
      explicit_primary_display_index = display_index;
    }

    const std::string name(output_info->name.begin(), output_info->name.end());
    auto process_type =
        command_line->GetSwitchValueASCII("type");
    if (name.starts_with("eDP") || name.starts_with("LVDS")) {
      display::SetInternalDisplayIds({display_id});
      // For browser process which has access to resource bundle,
      // use localized variant of "Built-in display" for internal displays.
      // This follows the ozone DRM behavior (i.e. ChromeOS).
      if (process_type.empty()) {
        display.set_label(l10n_util::GetStringUTF8(IDS_DISPLAY_NAME_INTERNAL));
      } else {
        display.set_label("Built-in display");
      }
    } else {
      display.set_label(edid_parser.display_name());
    }

    auto monitor_iter =
        output_to_monitor.find(static_cast<x11::RandR::Output>(output_id));
    if (monitor_iter != output_to_monitor.end() && monitor_iter->second == 0) {
      monitor_order_primary_display_index = display_index;
    }

    if (!display::HasForceDisplayColorProfile()) {
      const size_t monitor =
          monitor_iter == output_to_monitor.end() ? 0 : monitor_iter->second;
      const auto& icc_profile = iccs[monitor < iccs.size() ? monitor : 0];
      gfx::ColorSpace color_space = icc_profile.GetPrimariesOnlyColorSpace();

      // Most folks do not have an ICC profile set up, but we still want to
      // detect if a display has a wide color gamut so that HDR videos can be
      // enabled.  Only do this if |bits_per_component| > 8 or else SDR
      // screens may have washed out colors.
      if (bits_per_component > 8 && !color_space.IsValid()) {
        color_space = display::GetColorSpaceFromEdid(edid_parser);
      }

      display.SetColorSpaces(gfx::DisplayColorSpaces(
          color_space, viz::SinglePlaneFormat::kBGRA_8888));
    }

    display.set_color_depth(depth);
    display.set_depth_per_component(bits_per_component);

    // Set monitor refresh rate
    float refresh_rate =
        GetRefreshRateFromXRRModeInfo(resources->modes, crtc->mode);
    display.set_display_frequency(refresh_rate);
  }

  if (displays.empty()) {
    return GetFallbackDisplayList(primary_scale, primary_display_index_out);
  }

  if (explicit_primary_display_index != -1) {
    *primary_display_index_out = explicit_primary_display_index;
  } else if (monitor_order_primary_display_index != -1) {
    *primary_display_index_out = monitor_order_primary_display_index;
  } else {
    *primary_display_index_out = 0;
  }

  if (!display::Display::HasForceDeviceScaleFactor()) {
    for (auto& display : displays) {
      display.set_device_scale_factor(
          GetDisplayScale(display.bounds(), display_config));
    }

    ConvertDisplayBoundsToDips(&displays, *primary_display_index_out);
  }

  ClipWorkArea(&displays, *primary_display_index_out,
               GetWorkAreaSync(std::move(work_area_future)));
  return displays;
}

base::TimeDelta GetPrimaryDisplayRefreshIntervalFromXrandr() {
  constexpr base::TimeDelta kDefaultInterval = base::Seconds(1. / 60);

  size_t primary_display_index = 0;
  auto displays = BuildDisplaysFromXRandRInfo(display::DisplayConfig(),
                                              &primary_display_index);
  CHECK_LT(primary_display_index, displays.size());

  // TODO(crbug.com/41321728): It might make sense here to pick the output that
  // the window is on. On the other hand, if compositing is enabled, all drawing
  // might be synced to the primary output anyway. Needs investigation.
  auto frequency = displays[primary_display_index].display_frequency();
  return frequency > 0 ? base::Seconds(1. / frequency) : kDefaultInterval;
}

int RangeDistance(int min1, int max1, int min2, int max2) {
  base::ClampedNumeric<int> l1 = min1;
  base::ClampedNumeric<int> r1 = max1;
  base::ClampedNumeric<int> l2 = min2;
  base::ClampedNumeric<int> r2 = max2;
  return std::max(std::min(l2 - r1, r2 - l1), std::min(l1 - r2, r1 - l2));
}

std::pair<int, int> RectDistance(const gfx::Rect& p, const gfx::Rect& q) {
  const int dx = RangeDistance(p.x(), p.right(), q.x(), q.right());
  const int dy = RangeDistance(p.y(), p.bottom(), q.y(), q.bottom());
  return {std::max(dx, dy), std::min(dx, dy)};
}

void ConvertDisplayBoundsToDips(std::vector<display::Display>* displays,
                                size_t primary_display_index) {
  // Position displays starting with the primary display, which will have it's
  // origin directly converted from pixels to DIPs.
  std::vector<gfx::PointF> origins_dip(displays->size());
  const auto& primary_display = displays->at(primary_display_index);
  origins_dip[primary_display_index] =
      gfx::ScalePoint(gfx::PointF(primary_display.bounds().origin()),
                      1.0f / primary_display.device_scale_factor());

  // Construct a minimum spanning tree of displays using Prim's algorithm.  The
  // root of the tree is the primary display, and every other display will be
  // positioned relative to it's parent display.
  using EdgeDistance = std::tuple<std::pair<int, int>, size_t, size_t>;
  std::priority_queue<EdgeDistance, std::vector<EdgeDistance>, std::greater<>>
      queue;
  std::unordered_set<size_t> fringe;
  for (size_t i = 0; i < displays->size(); i++) {
    fringe.insert(i);
  }
  auto remove_from_fringe = [&](size_t parent) {
    fringe.erase(parent);
    for (size_t child : fringe) {
      const auto dist = RectDistance(displays->at(parent).bounds(),
                                     displays->at(child).bounds());
      queue.emplace(dist, parent, child);
    }
  };
  remove_from_fringe(primary_display_index);
  while (!queue.empty()) {
    auto [_, parent, child] = queue.top();
    queue.pop();
    if (fringe.contains(child)) {
      origins_dip[child] = DisplayOriginPxToDip(
          displays->at(parent), displays->at(child), origins_dip[parent]);
      remove_from_fringe(child);
    }
  }

  // Update the displays with the converted origins.
  for (size_t i = 0; i < displays->size(); i++) {
    auto& display = displays->at(i);
    gfx::SizeF size_dip = gfx::ScaleSize(gfx::SizeF(display.size()),
                                         1.0f / display.device_scale_factor());
    gfx::Rect bounds_dip =
        gfx::ToEnclosingRect(gfx::RectF(origins_dip[i], size_dip));
    display.set_bounds(bounds_dip);
    display.set_work_area(bounds_dip);
  }
}

}  // namespace ui