| // Copyright 2012 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/desktop_resizer_x11.h" |
| |
| #include <gio/gio.h> |
| |
| #include <algorithm> |
| #include <cstdlib> |
| #include <iterator> |
| #include <memory> |
| #include <ranges> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/command_line.h" |
| #include "base/system/sys_info.h" |
| #include "base/types/cxx23_to_underlying.h" |
| #include "remoting/base/logging.h" |
| #include "remoting/host/desktop_geometry.h" |
| #include "remoting/host/linux/gvariant_ref.h" |
| #include "remoting/host/linux/x11_display_util.h" |
| #include "remoting/host/linux/x11_util.h" |
| #include "ui/base/glib/gsettings.h" |
| #include "ui/base/glib/scoped_gobject.h" |
| #include "ui/gfx/geometry/vector2d.h" |
| #include "ui/gfx/x/future.h" |
| #include "ui/gfx/x/randr.h" |
| #include "ui/gfx/x/x11_crtc_resizer.h" |
| |
| namespace remoting { |
| |
| namespace { |
| |
| // TODO(jamiewalch): Use the correct DPI for the mode: http://crbug.com/172405. |
| const int kDefaultDPI = 96; |
| constexpr base::TimeDelta kGnomeWaitTime = base::Seconds(1); |
| |
| uint32_t GetDotClockForModeInfo() { |
| static int proc_num = base::SysInfo::NumberOfProcessors(); |
| // Keep the proc_num logic in sync with linux_me2me_host.py |
| if (proc_num > 16) { |
| return 120 * 1e6; |
| } |
| return 60 * 1e6; |
| } |
| |
| } // namespace |
| |
| DesktopResizerX11::DesktopResizerX11() |
| : connection_(x11::Connection::Get()), |
| randr_output_manager_("CRD_", GetDotClockForModeInfo()), |
| is_virtual_session_(IsVirtualSession(connection_)) { |
| has_randr_ = RandR()->present(); |
| if (!has_randr_) { |
| return; |
| } |
| RandR()->SelectInput({RootWindow(), x11::RandR::NotifyMask::ScreenChange}); |
| |
| gnome_display_config_.Init(); |
| registry_ = ui::GSettingsNew("org.gnome.desktop.interface"); |
| } |
| |
| DesktopResizerX11::~DesktopResizerX11() = default; |
| |
| // DesktopResizer interface |
| ScreenResolution DesktopResizerX11::GetCurrentResolution( |
| webrtc::ScreenId screen_id) { |
| // Process pending events so that the connection setup data is updated |
| // with the correct display metrics. |
| if (has_randr_) { |
| connection_->DispatchAll(); |
| } |
| |
| // RANDR does not allow fetching information on a particular monitor. So |
| // fetch all of them and try to find the requested monitor. |
| auto reply = RandR()->GetMonitors({RootWindow()}).Sync(); |
| if (reply) { |
| for (const auto& monitor : reply->monitors) { |
| if (static_cast<x11::RandRMonitorConfig::ScreenId>(monitor.name) != |
| static_cast<x11::RandRMonitorConfig::ScreenId>(screen_id)) { |
| continue; |
| } |
| gfx::Vector2d dpi = GetMonitorDpi(monitor); |
| return ScreenResolution( |
| webrtc::DesktopSize(monitor.width, monitor.height), |
| webrtc::DesktopVector(dpi.x(), dpi.y())); |
| } |
| } |
| |
| LOG(ERROR) << "Cannot find current resolution for screen ID " << screen_id |
| << ". Resolution of the default screen will be returned."; |
| |
| return ScreenResolution( |
| webrtc::DesktopSize(connection_->default_screen().width_in_pixels, |
| connection_->default_screen().height_in_pixels), |
| webrtc::DesktopVector(kDefaultDPI, kDefaultDPI)); |
| } |
| std::list<ScreenResolution> DesktopResizerX11::GetSupportedResolutions( |
| const ScreenResolution& preferred, |
| webrtc::ScreenId screen_id) { |
| std::list<ScreenResolution> result; |
| if (!has_randr_ || !is_virtual_session_) { |
| return result; |
| } |
| |
| // Clamp the specified size to something valid for the X server. |
| if (auto response = RandR()->GetScreenSizeRange({RootWindow()}).Sync()) { |
| int width = |
| std::clamp(static_cast<uint16_t>(preferred.dimensions().width()), |
| response->min_width, response->max_width); |
| int height = |
| std::clamp(static_cast<uint16_t>(preferred.dimensions().height()), |
| response->min_height, response->max_height); |
| // Additionally impose a minimum size of 640x480, since anything smaller |
| // doesn't seem very useful. |
| result.emplace_back( |
| webrtc::DesktopSize(std::max(640, width), std::max(480, height)), |
| preferred.dpi()); |
| } |
| return result; |
| } |
| void DesktopResizerX11::SetResolution(const ScreenResolution& resolution, |
| webrtc::ScreenId screen_id) { |
| if (!has_randr_ || !is_virtual_session_) { |
| return; |
| } |
| |
| // Grab the X server while we're changing the display resolution. This |
| // ensures that the display configuration doesn't change under our feet. |
| x11::ScopedXGrabServer grabber(connection_); |
| |
| // RANDR does not allow fetching information on a particular monitor. So |
| // fetch all of them and try to find the requested monitor. |
| std::vector<x11::RandR::MonitorInfo> monitors; |
| if (!randr_output_manager_.TryGetCurrentMonitors(monitors)) { |
| return; |
| } |
| |
| for (const auto& monitor : monitors) { |
| if (static_cast<x11::RandRMonitorConfig::ScreenId>(monitor.name) != |
| static_cast<x11::RandRMonitorConfig::ScreenId>(screen_id)) { |
| continue; |
| } |
| |
| if (monitor.outputs.size() != 1) { |
| // This implementation only supports resizing a Monitor attached to a |
| // single output. The case where size() > 1 should never occur with |
| // Xorg+video-dummy. |
| // TODO(crbug.com/40225767): Maybe support resizing a Monitor not |
| // attached to any Output? |
| LOG(ERROR) << "Monitor " << screen_id |
| << " has unexpected #outputs: " << monitor.outputs.size(); |
| return; |
| } |
| |
| if (!monitor.automatic) { |
| // This implementation only supports resizing synthesized Monitors which |
| // automatically track their Outputs. |
| // TODO(crbug.com/40225767): Maybe support resizing manually-created |
| // Monitors? |
| LOG(ERROR) << "Not resizing Monitor " << screen_id |
| << " that was created manually."; |
| return; |
| } |
| |
| SetResolutionForOutput(monitor.outputs[0], resolution); |
| return; |
| } |
| LOG(ERROR) << "Monitor " << screen_id << " not found."; |
| } |
| |
| void DesktopResizerX11::RestoreResolution(const ScreenResolution& original, |
| webrtc::ScreenId screen_id) { |
| SetResolution(original, screen_id); |
| } |
| |
| void DesktopResizerX11::SetVideoLayout(const protocol::VideoLayout& layout) { |
| if (!has_randr_ || !is_virtual_session_) { |
| return; |
| } |
| x11::RandRMonitorLayout desktop_layouts; |
| if (layout.has_primary_screen_id()) { |
| desktop_layouts.primary_screen_id = layout.primary_screen_id(); |
| } |
| for (const auto& track : layout.video_track()) { |
| desktop_layouts.configs.emplace_back( |
| track.has_screen_id() ? std::make_optional(track.screen_id()) |
| : std::nullopt, |
| gfx::Rect(track.position_x(), track.position_y(), track.width(), |
| track.height()), |
| gfx::Vector2d(track.x_dpi(), track.y_dpi())); |
| } |
| randr_output_manager_.SetLayout(desktop_layouts); |
| } |
| |
| void DesktopResizerX11::SetResolutionForOutput( |
| x11::RandR::Output output, |
| const ScreenResolution& resolution) { |
| // Actually do the resize operation, preserving the current mode name. Note |
| // that we have to detach the output from the mode in order to delete the |
| // mode and re-create it with the new resolution. The output may also need to |
| // be detached from all modes in order to reduce the root window size. |
| HOST_LOG << "Resizing RANDR Output " << base::to_underlying(output) << " to " |
| << resolution.dimensions().width() << "x" |
| << resolution.dimensions().height(); |
| |
| randr_output_manager_.SetResolutionForOutput( |
| output, |
| gfx::Size(resolution.dimensions().width(), |
| resolution.dimensions().height()), |
| gfx::Vector2d(resolution.dpi().x(), resolution.dpi().y())); |
| |
| // Check to see if GNOME is using automatic-scaling. If the value is non-zero, |
| // the user prefers a particular scaling, so don't adjust the |
| // text-scaling-factor here. |
| if (registry_ && |
| g_settings_get_uint(registry_.get(), "scaling-factor") == 0U) { |
| // Start the timer to update the text-scaling-factor. Any previously |
| // started timer will be cancelled. |
| requested_dpi_ = resolution.dpi().x(); |
| gnome_delay_timer_.Start(FROM_HERE, kGnomeWaitTime, this, |
| &DesktopResizerX11::RequestGnomeDisplayConfig); |
| } |
| } |
| |
| void DesktopResizerX11::RequestGnomeDisplayConfig() { |
| // Unretained() is safe because `this` owns gnome_display_config_ which |
| // cancels callbacks on destruction. |
| gnome_display_config_.GetMonitorsConfig( |
| base::BindOnce(&DesktopResizerX11::OnGnomeDisplayConfigReceived, |
| base::Unretained(this))); |
| } |
| |
| void DesktopResizerX11::OnGnomeDisplayConfigReceived( |
| GnomeDisplayConfig config) { |
| // Look for an enabled monitor. Disabled monitors have no Mode set - a |
| // monitor can become disabled by being added then removed (using the website |
| // Display options). The Xorg xf86-video-dummy driver has a quirk that, once a |
| // monitor becomes "connected", it stays forever in the connected state, even |
| // if it is later disabled. All connected monitors (enabled or disabled) are |
| // included in the GNOME config. |
| |
| // For X11, the calculation of the text-scaling-factor does not depend on |
| // which enabled monitor is chosen here, because GNOME's X11 backend forces |
| // all monitors to have the same scale. However, it makes sense to select |
| // an enabled monitor, since a disabled monitor might not have a reliable |
| // "scale" property returned by GNOME. |
| auto monitor_iter = |
| std::ranges::find_if(config.monitors, [](const auto& entry) { |
| return entry.second.GetCurrentMode() != nullptr; |
| }); |
| if (monitor_iter == std::ranges::end(config.monitors)) { |
| LOG(ERROR) << "No enabled monitor found in GNOME config."; |
| return; |
| } |
| const auto& monitor = monitor_iter->second; |
| |
| if (monitor.scale == 0) { |
| // This should never happen - avoid division by 0. |
| return; |
| } |
| |
| // The GNOME scaling, multiplied by the GNOME text-scaling-factor, will be the |
| // rendered scaling of text. This should be the client's requested DPI divided |
| // by kDefaultDPI. |
| double text_scaling_factor = |
| static_cast<double>(requested_dpi_) / kDefaultDPI / monitor.scale; |
| HOST_LOG << "Target DPI = " << requested_dpi_ |
| << ", GNOME scale = " << monitor.scale |
| << ", calculated text-scaling = " << text_scaling_factor; |
| |
| if (!registry_ || |
| !g_settings_set_double(registry_.get(), "text-scaling-factor", |
| text_scaling_factor)) { |
| // Just log a warning - failure is expected if the value falls outside the |
| // interval [0.5, 3.0]. |
| LOG(WARNING) << "Failed to set text-scaling-factor."; |
| } |
| } |
| |
| bool DesktopResizerX11::supportsHighDpiResize() { |
| // High-DPI resize is supported only for Gnome. |
| ScopedGObject<GDBusConnection> connection = |
| TakeGObject(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr)); |
| if (!connection) { |
| return false; |
| } |
| ScopedGObject<GDBusProxy> dbus = TakeGObject(g_dbus_proxy_new_sync( |
| connection, G_DBUS_PROXY_FLAGS_NONE, nullptr, "org.freedesktop.DBus", |
| "/org/freedesktop/DBus", "org.freedesktop.DBus", nullptr, nullptr)); |
| if (!dbus) { |
| return false; |
| } |
| auto has_owner = GVariantRef<>::Take(g_dbus_proxy_call_sync( |
| dbus, "NameHasOwner", g_variant_new("(s)", "org.gnome.Shell"), |
| G_DBUS_CALL_FLAGS_NONE, -1, nullptr, nullptr)); |
| auto has_owner_bool = GVariantRef<"(b)">::TryFrom(has_owner); |
| if (!has_owner_bool.has_value()) { |
| return false; |
| } |
| return has_owner_bool->get<0>().Into<bool>(); |
| } |
| |
| } // namespace remoting |