blob: a1c4df5532e08966d5ccdcd565fca05aa62589bc [file] [log] [blame]
// Copyright (c) 2012 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 "remoting/host/desktop_resizer_x11.h"
#include <string.h>
#include "base/command_line.h"
#include "base/cxx17_backports.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "remoting/base/logging.h"
#include "remoting/host/linux/x11_util.h"
#include "ui/gfx/x/future.h"
#include "ui/gfx/x/randr.h"
#include "ui/gfx/x/scoped_ignore_errors.h"
// On Linux, we use the xrandr extension to change the desktop resolution. In
// curtain mode, we do exact resize where supported (currently only using a
// patched Xvfb server). Otherwise, we try to pick the best resolution from the
// existing modes.
//
// Xrandr has a number of restrictions that make exact resize more complex:
//
// 1. It's not possible to change the resolution of an existing mode. Instead,
// the mode must be deleted and recreated.
// 2. It's not possible to delete a mode that's in use.
// 3. Errors are communicated via Xlib's spectacularly unhelpful mechanism
// of terminating the process unless you install an error handler.
//
// The basic approach is as follows:
//
// 1. Create a new mode with the correct resolution;
// 2. Switch to the new mode;
// 3. Delete the old mode.
//
// Since the new mode must have a different name, and we want the current mode
// name to be consistent, we then additionally:
//
// 4. Recreate the old mode at the new resolution;
// 5. Switch to the old mode;
// 6. Delete the temporary mode.
//
// Name consistency will allow a future CL to disable resize-to-client if the
// user has changed the mode to something other than "Chrome Remote Desktop
// client resolution". It doesn't make the code significantly more complex.
namespace {
constexpr auto kInvalidMode = static_cast<x11::RandR::Mode>(0);
int PixelsToMillimeters(int pixels, int dpi) {
DCHECK(dpi != 0);
const double kMillimetersPerInch = 25.4;
// (pixels / dpi) is the length in inches. Multiplying by
// kMillimetersPerInch converts to mm. Multiplication is done first to
// avoid integer division.
return static_cast<int>(kMillimetersPerInch * pixels / dpi);
}
// TODO(jamiewalch): Use the correct DPI for the mode: http://crbug.com/172405.
const int kDefaultDPI = 96;
} // namespace
namespace remoting {
ScreenResources::ScreenResources() = default;
ScreenResources::~ScreenResources() = default;
bool ScreenResources::Refresh(x11::RandR* randr, x11::Window window) {
resources_ = nullptr;
if (auto response = randr->GetScreenResourcesCurrent({window}).Sync())
resources_ = std::move(response.reply);
return resources_ != nullptr;
}
x11::RandR::Mode ScreenResources::GetIdForMode(const std::string& name) {
CHECK(resources_);
const char* names = reinterpret_cast<const char*>(resources_->names.data());
for (const auto& mode_info : resources_->modes) {
std::string mode_name(names, mode_info.name_len);
names += mode_info.name_len;
if (name == mode_name)
return static_cast<x11::RandR::Mode>(mode_info.id);
}
return kInvalidMode;
}
x11::RandR::Output ScreenResources::GetOutput() {
CHECK(resources_);
return resources_->outputs[0];
}
x11::RandR::Crtc ScreenResources::GetCrtc() {
CHECK(resources_);
return resources_->crtcs[0];
}
x11::RandR::GetScreenResourcesCurrentReply* ScreenResources::get() {
return resources_.get();
}
DesktopResizerX11::DesktopResizerX11()
: connection_(x11::Connection::Get()),
randr_(&connection_->randr()),
screen_(&connection_->default_screen()),
root_(screen_->root),
exact_resize_(base::CommandLine::ForCurrentProcess()->HasSwitch(
"server-supports-exact-resize")) {
has_randr_ = randr_->present();
if (!has_randr_)
return;
// Let the server know the client version so it sends us data consistent with
// xcbproto's definitions. We don't care about the returned server version,
// so no need to sync.
randr_->QueryVersion({x11::RandR::major_version, x11::RandR::minor_version});
randr_->SelectInput({root_, x11::RandR::NotifyMask::ScreenChange});
}
DesktopResizerX11::~DesktopResizerX11() = default;
ScreenResolution DesktopResizerX11::GetCurrentResolution() {
// Process pending events so that the connection setup data is updated
// with the correct display metrics.
if (has_randr_)
connection_->DispatchAll();
ScreenResolution result(
webrtc::DesktopSize(connection_->default_screen().width_in_pixels,
connection_->default_screen().height_in_pixels),
webrtc::DesktopVector(kDefaultDPI, kDefaultDPI));
return result;
}
std::list<ScreenResolution> DesktopResizerX11::GetSupportedResolutions(
const ScreenResolution& preferred) {
std::list<ScreenResolution> result;
if (!has_randr_)
return result;
if (exact_resize_) {
// Clamp the specified size to something valid for the X server.
if (auto response = randr_->GetScreenSizeRange({root_}).Sync()) {
int width =
base::clamp(static_cast<uint16_t>(preferred.dimensions().width()),
response->min_width, response->max_width);
int height =
base::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.
ScreenResolution actual(
webrtc::DesktopSize(std::max(640, width), std::max(480, height)),
webrtc::DesktopVector(kDefaultDPI, kDefaultDPI));
result.push_back(actual);
}
} else {
// Retrieve supported resolutions with RandR
if (auto response = randr_->GetScreenInfo({root_}).Sync()) {
for (const auto& size : response->sizes) {
result.emplace_back(webrtc::DesktopSize(size.width, size.height),
webrtc::DesktopVector(kDefaultDPI, kDefaultDPI));
}
}
}
return result;
}
void DesktopResizerX11::SetResolution(const ScreenResolution& resolution) {
if (!has_randr_)
return;
// Ignore X errors encountered while resizing the display. We might hit an
// error, for example if xrandr has been used to add a mode with the same
// name as our temporary mode, or to remove the "client resolution" mode. We
// don't want to terminate the process if this happens.
x11::ScopedIgnoreErrors ignore_errors(connection_);
// Grab the X server while we're changing the display resolution. This ensures
// that the display configuration doesn't change under our feet.
ScopedXGrabServer grabber(connection_);
if (exact_resize_)
SetResolutionNewMode(resolution);
else
SetResolutionExistingMode(resolution);
}
void DesktopResizerX11::RestoreResolution(const ScreenResolution& original) {
SetResolution(original);
}
void DesktopResizerX11::SetResolutionNewMode(
const ScreenResolution& resolution) {
// The name of the mode representing the current client view resolution and
// the temporary mode used for the reasons described at the top of this file.
// The former should be localized if it's user-visible; the latter only
// exists briefly and does not need to localized.
const char* kModeName = "Chrome Remote Desktop client resolution";
const char* kTempModeName = "Chrome Remote Desktop temporary mode";
// Actually do the resize operation, preserving the current mode name. Note
// that we have to detach the output from any mode in order to resize it
// (strictly speaking, this is only required when reducing the size, but it
// seems safe to do it regardless).
HOST_LOG << "Changing desktop size to " << resolution.dimensions().width()
<< "x" << resolution.dimensions().height();
// TODO(lambroslambrou): Use the DPI from client size information.
uint32_t width_mm =
PixelsToMillimeters(resolution.dimensions().width(), kDefaultDPI);
uint32_t height_mm =
PixelsToMillimeters(resolution.dimensions().height(), kDefaultDPI);
CreateMode(kTempModeName, resolution.dimensions().width(),
resolution.dimensions().height());
SwitchToMode(nullptr);
randr_->SetScreenSize(
{root_, static_cast<uint16_t>(resolution.dimensions().width()),
static_cast<uint16_t>(resolution.dimensions().height()), width_mm,
height_mm});
SwitchToMode(kTempModeName);
DeleteMode(kModeName);
CreateMode(kModeName, resolution.dimensions().width(),
resolution.dimensions().height());
SwitchToMode(kModeName);
DeleteMode(kTempModeName);
}
void DesktopResizerX11::SetResolutionExistingMode(
const ScreenResolution& resolution) {
if (auto config = randr_->GetScreenInfo({root_}).Sync()) {
x11::RandR::Rotation current_rotation = config->rotation;
const std::vector<x11::RandR::ScreenSize>& sizes = config->sizes;
for (size_t i = 0; i < sizes.size(); ++i) {
if (sizes[i].width == resolution.dimensions().width() &&
sizes[i].height == resolution.dimensions().height()) {
randr_->SetScreenConfig({
.window = root_,
.timestamp = x11::Time::CurrentTime,
.config_timestamp = config->config_timestamp,
.sizeID = static_cast<uint16_t>(i),
.rotation = current_rotation,
.rate = 0,
});
break;
}
}
}
}
void DesktopResizerX11::CreateMode(const char* name, int width, int height) {
x11::RandR::ModeInfo mode;
mode.width = width;
mode.height = height;
mode.name_len = strlen(name);
randr_->CreateMode({root_, mode, name});
if (!resources_.Refresh(randr_, root_))
return;
x11::RandR::Mode mode_id = resources_.GetIdForMode(name);
if (mode_id == kInvalidMode)
return;
randr_->AddOutputMode({
resources_.GetOutput(),
mode_id,
});
}
void DesktopResizerX11::DeleteMode(const char* name) {
x11::RandR::Mode mode_id = resources_.GetIdForMode(name);
if (mode_id != kInvalidMode) {
randr_->DeleteOutputMode({resources_.GetOutput(), mode_id});
randr_->DestroyMode({mode_id});
resources_.Refresh(randr_, root_);
}
}
void DesktopResizerX11::SwitchToMode(const char* name) {
auto mode_id = kInvalidMode;
std::vector<x11::RandR::Output> outputs;
if (name) {
mode_id = resources_.GetIdForMode(name);
CHECK_NE(mode_id, kInvalidMode);
outputs = resources_.get()->outputs;
}
const auto* resources = resources_.get();
randr_->SetCrtcConfig({
.crtc = resources_.GetCrtc(),
.timestamp = x11::Time::CurrentTime,
.config_timestamp = resources->config_timestamp,
.x = 0,
.y = 0,
.mode = mode_id,
.rotation = x11::RandR::Rotation::Rotate_0,
.outputs = outputs,
});
}
} // namespace remoting