| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // 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 <bitset> |
| |
| #include "base/bits.h" |
| #include "base/compiler_specific.h" |
| #include "base/logging.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/matrix3_f.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/vector3d_f.h" |
| #include "ui/gfx/x/randr.h" |
| #include "ui/gfx/x/x11.h" |
| #include "ui/gfx/x/x11_atom_cache.h" |
| |
| namespace ui { |
| |
| namespace { |
| |
| constexpr int kMinVersionXrandr = 103; // Need at least xrandr version 1.3. |
| |
| constexpr const char kRandrEdidProperty[] = "EDID"; |
| |
| std::map<x11::RandR::Output, int> GetMonitors(int version, |
| x11::RandR* randr, |
| x11::Window window) { |
| std::map<x11::RandR::Output, int> output_to_monitor; |
| if (version >= 105) { |
| if (auto reply = randr->GetMonitors({window}).Sync()) { |
| 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; |
| } |
| |
| // 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, |
| int64_t primary_display_index, |
| float scale) { |
| x11::Window x_root_window = ui::GetX11RootWindow(); |
| |
| std::vector<int> value; |
| if (!ui::GetIntArrayProperty(x_root_window, "_NET_WORKAREA", &value) || |
| value.size() < 4) { |
| return; |
| } |
| gfx::Rect work_area = gfx::ScaleToEnclosingRect( |
| gfx::Rect(value[0], value[1], value[2], value[3]), 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::count_if(displays->begin(), displays->end(), |
| [&](const display::Display& display) { |
| return work_area.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. |
| auto found = std::find_if(displays->begin(), displays->end(), |
| [&](const display::Display& display) { |
| return display.bounds().Contains(work_area); |
| }); |
| |
| // 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; |
| |
| 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 (base::bits::IsPowerOfTwo(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::RandR* randr, |
| x11::RandR::Output output) { |
| auto future = randr->GetOutputProperty({ |
| .output = output, |
| .property = gfx::GetAtom(kRandrEdidProperty), |
| .long_length = 128, |
| }); |
| auto response = future.Sync(); |
| std::vector<uint8_t> edid; |
| if (response && response->format == 8 && response->type != x11::Atom::None) |
| edid = std::move(response->data); |
| return edid; |
| } |
| |
| } // namespace |
| |
| int GetXrandrVersion() { |
| auto impl = []() -> int { |
| auto future = x11::Connection::Get()->randr().QueryVersion( |
| {x11::RandR::major_version, x11::RandR::minor_version}); |
| if (auto response = future.Sync()) |
| return response->major_version * 100 + response->minor_version; |
| return 0; |
| }; |
| static int version = impl(); |
| return version; |
| } |
| |
| std::vector<display::Display> GetFallbackDisplayList(float scale) { |
| const auto& screen = x11::Connection::Get()->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.SetScaleAndBounds(scale, bounds_in_pixels); |
| gfx_display.set_work_area( |
| gfx::ScaleToEnclosingRect(bounds_in_pixels, 1.0f / scale)); |
| } 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}; |
| ClipWorkArea(&displays, 0, scale); |
| return displays; |
| } |
| |
| std::vector<display::Display> BuildDisplaysFromXRandRInfo( |
| int version, |
| float scale, |
| int64_t* primary_display_index_out) { |
| DCHECK(primary_display_index_out); |
| DCHECK_GE(version, kMinVersionXrandr); |
| auto* connection = x11::Connection::Get(); |
| auto& randr = connection->randr(); |
| auto x_root_window = static_cast<x11::Window>(ui::GetX11RootWindow()); |
| std::vector<display::Display> displays; |
| auto resources = randr.GetScreenResourcesCurrent({x_root_window}).Sync(); |
| if (!resources) { |
| LOG(ERROR) << "XRandR returned no displays; falling back to root window"; |
| return GetFallbackDisplayList(scale); |
| } |
| |
| const int depth = connection->default_screen().root_depth; |
| const int bits_per_component = DefaultBitsPerComponent(); |
| |
| std::map<x11::RandR::Output, int> output_to_monitor = |
| GetMonitors(version, &randr, x_root_window); |
| *primary_display_index_out = 0; |
| auto output_primary = randr.GetOutputPrimary({x_root_window}).Sync(); |
| if (!output_primary) |
| return GetFallbackDisplayList(scale); |
| x11::RandR::Output primary_display_id = output_primary->output; |
| |
| int explicit_primary_display_index = -1; |
| int monitor_order_primary_display_index = -1; |
| |
| // As per-display scale factor is not supported right now, |
| // the X11 root window's scale factor is always used. |
| for (size_t i = 0; i < resources->outputs.size(); i++) { |
| x11::RandR::Output output_id = resources->outputs[i]; |
| auto output_info = |
| randr.GetOutputInfo({output_id, resources->config_timestamp}).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 = |
| randr.GetCrtcInfo({output_info->crtc, resources->config_timestamp}) |
| .Sync(); |
| if (!crtc) |
| continue; |
| |
| display::EdidParser edid_parser( |
| GetEDIDProperty(&randr, static_cast<x11::RandR::Output>(output_id))); |
| auto output_32 = static_cast<uint32_t>(output_id); |
| int64_t display_id = |
| output_32 > 0xff ? 0 : edid_parser.GetDisplayId(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); |
| display::Display display(display_id, crtc_bounds); |
| |
| if (!display::Display::HasForceDeviceScaleFactor()) { |
| display.SetScaleAndBounds(scale, crtc_bounds); |
| display.set_work_area( |
| gfx::ScaleToEnclosingRect(crtc_bounds, 1.0f / scale)); |
| } |
| |
| 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 = displays.size(); |
| |
| 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 = displays.size(); |
| |
| if (!display::Display::HasForceDisplayColorProfile()) { |
| gfx::ICCProfile icc_profile = ui::GetICCProfileForMonitor( |
| monitor_iter == output_to_monitor.end() ? 0 : monitor_iter->second); |
| 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.set_color_spaces( |
| gfx::DisplayColorSpaces(color_space, gfx::BufferFormat::BGRA_8888)); |
| } |
| |
| display.set_color_depth(depth); |
| display.set_depth_per_component(bits_per_component); |
| |
| // Set monitor refresh rate |
| int refresh_rate = static_cast<int>( |
| GetRefreshRateFromXRRModeInfo(resources->modes, crtc->mode)); |
| display.set_display_frequency(refresh_rate); |
| |
| displays.push_back(display); |
| } |
| |
| 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; |
| |
| if (displays.empty()) |
| return GetFallbackDisplayList(scale); |
| |
| ClipWorkArea(&displays, *primary_display_index_out, scale); |
| return displays; |
| } |
| |
| base::TimeDelta GetPrimaryDisplayRefreshIntervalFromXrandr() { |
| constexpr base::TimeDelta kDefaultInterval = |
| base::TimeDelta::FromSecondsD(1. / 60); |
| x11::RandR randr = x11::Connection::Get()->randr(); |
| auto root = static_cast<x11::Window>(ui::GetX11RootWindow()); |
| auto resources = randr.GetScreenResourcesCurrent({root}).Sync(); |
| if (!resources) |
| return kDefaultInterval; |
| // TODO(crbug.com/726842): 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 output_primary = randr.GetOutputPrimary({root}).Sync(); |
| if (!output_primary) |
| return kDefaultInterval; |
| x11::RandR::Output primary_output = output_primary->output; |
| bool disconnected_primary = false; |
| for (size_t i = 0; i < resources->outputs.size(); i++) { |
| if (!disconnected_primary && resources->outputs[i] != primary_output) |
| continue; |
| |
| auto output_info = |
| randr.GetOutputInfo({primary_output, resources->config_timestamp}) |
| .Sync(); |
| if (!output_info) |
| continue; |
| |
| if (output_info->connection != x11::RandR::RandRConnection::Connected) { |
| // If the primary monitor is disconnected, then start over and choose the |
| // first connected monitor instead. |
| if (!disconnected_primary) { |
| disconnected_primary = true; |
| i = -1; |
| } |
| continue; |
| } |
| auto crtc = |
| randr.GetCrtcInfo({output_info->crtc, resources->config_timestamp}) |
| .Sync(); |
| if (!crtc) |
| continue; |
| float refresh_rate = |
| GetRefreshRateFromXRRModeInfo(resources->modes, crtc->mode); |
| if (refresh_rate == 0) |
| continue; |
| |
| return base::TimeDelta::FromSecondsD(1. / refresh_rate); |
| } |
| return kDefaultInterval; |
| } |
| |
| } // namespace ui |