// 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.h" #include "remoting/host/linux/x11_util.h" #include #include #include #include "base/command_line.h" #include "remoting/base/logging.h" // On Linux, we use the xrandr extension to change the desktop resolution. For // now, we only support resize-to-client for Xvfb-based servers that can match // the client resolution exactly. To support best-resolution matching, it would // be necessary to implement |GetSupportedResolutions|, but it's not considered // a priority now. // // Xrandr has a number of restrictions that make this code 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 { 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(kMillimetersPerInch * pixels / dpi); } // TODO(jamiewalch): Use the correct DPI for the mode: http://crbug.com/172405. const int kDefaultDPI = 96; } // namespace namespace remoting { // Wrapper class for the XRRScreenResources struct. class ScreenResources { public: ScreenResources() : resources_(NULL) { } ~ScreenResources() { Release(); } bool Refresh(Display* display, Window window) { Release(); resources_ = XRRGetScreenResources(display, window); return resources_ != NULL; } void Release() { if (resources_) { XRRFreeScreenResources(resources_); resources_ = NULL; } } RRMode GetIdForMode(const char* name) { CHECK(resources_); for (int i = 0; i < resources_->nmode; ++i) { const XRRModeInfo& mode = resources_->modes[i]; if (strcmp(mode.name, name) == 0) { return mode.id; } } return 0; } // For now, assume we're only ever interested in the first output. RROutput GetOutput() { CHECK(resources_); return resources_->outputs[0]; } // For now, assume we're only ever interested in the first crtc. RRCrtc GetCrtc() { CHECK(resources_); return resources_->crtcs[0]; } XRROutputInfo* GetOutputInfo(Display* display, RROutput output_id) { CHECK(resources_); return XRRGetOutputInfo(display, resources_, output_id); } XRRScreenResources* get() { return resources_; } private: XRRScreenResources* resources_; }; class DesktopResizerLinux : public DesktopResizer { public: DesktopResizerLinux(); ~DesktopResizerLinux() override; // DesktopResizer interface ScreenResolution GetCurrentResolution() override; std::list GetSupportedResolutions( const ScreenResolution& preferred) override; void SetResolution(const ScreenResolution& resolution) override; void RestoreResolution(const ScreenResolution& original) override; private: // Create a mode, and attach it to the primary output. If the mode already // exists, it is left unchanged. void CreateMode(const char* name, int width, int height); // Remove the specified mode from the primary output, and delete it. If the // mode is in use, it is not deleted. void DeleteMode(const char* name); // Switch the primary output to the specified mode. If name is NULL, the // primary output is disabled instead, which is required before changing // its resolution. void SwitchToMode(const char* name); Display* display_; int screen_; Window root_; ScreenResources resources_; bool exact_resize_; DISALLOW_COPY_AND_ASSIGN(DesktopResizerLinux); }; DesktopResizerLinux::DesktopResizerLinux() : display_(XOpenDisplay(NULL)), screen_(DefaultScreen(display_)), root_(RootWindow(display_, screen_)), exact_resize_(base::CommandLine::ForCurrentProcess()-> HasSwitch("server-supports-exact-resize")) { XRRSelectInput(display_, root_, RRScreenChangeNotifyMask); } DesktopResizerLinux::~DesktopResizerLinux() { XCloseDisplay(display_); } ScreenResolution DesktopResizerLinux::GetCurrentResolution() { if (!exact_resize_) { // TODO(jamiewalch): Remove this early return if we decide to support // non-Xvfb servers. return ScreenResolution(); } // TODO(lambroslambrou): Xrandr requires that we process RRScreenChangeNotify // events, otherwise DisplayWidth and DisplayHeight do not return the current // values. Normally, this would be done via a central X event loop, but we // don't have one, hence this horrible hack. // // Note that the WatchFileDescriptor approach taken in XServerClipboard // doesn't work here because resize events have already been read from the // X server socket by the time the resize function returns, hence the // file descriptor is never seen as readable. while (XEventsQueued(display_, QueuedAlready)) { XEvent event; XNextEvent(display_, &event); XRRUpdateConfiguration(&event); } ScreenResolution result( webrtc::DesktopSize( DisplayWidth(display_, DefaultScreen(display_)), DisplayHeight(display_, DefaultScreen(display_))), webrtc::DesktopVector(kDefaultDPI, kDefaultDPI)); return result; } std::list DesktopResizerLinux::GetSupportedResolutions( const ScreenResolution& preferred) { std::list result; if (exact_resize_) { // Clamp the specified size to something valid for the X server. int min_width = 0, min_height = 0, max_width = 0, max_height = 0; XRRGetScreenSizeRange(display_, root_, &min_width, &min_height, &max_width, &max_height); int width = std::min(std::max(preferred.dimensions().width(), min_width), max_width); int height = std::min(std::max(preferred.dimensions().height(), min_height), 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 { // TODO(jamiewalch): Return the list of supported resolutions if we can't // support exact-size matching. } return result; } void DesktopResizerLinux::SetResolution(const ScreenResolution& resolution) { if (!exact_resize_) { // TODO(jamiewalch): Remove this early return if we decide to support // non-Xvfb servers. 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. ScopedXErrorHandler handler(ScopedXErrorHandler::Ignore()); // 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(display_); // 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. int width_mm = PixelsToMillimeters(resolution.dimensions().width(), kDefaultDPI); int height_mm = PixelsToMillimeters(resolution.dimensions().height(), kDefaultDPI); CreateMode(kTempModeName, resolution.dimensions().width(), resolution.dimensions().height()); SwitchToMode(NULL); XRRSetScreenSize(display_, root_, resolution.dimensions().width(), resolution.dimensions().height(), width_mm, height_mm); SwitchToMode(kTempModeName); DeleteMode(kModeName); CreateMode(kModeName, resolution.dimensions().width(), resolution.dimensions().height()); SwitchToMode(kModeName); DeleteMode(kTempModeName); } void DesktopResizerLinux::RestoreResolution(const ScreenResolution& original) { // Since the desktop is only visible via a remote connection, the original // resolution of the desktop will never been seen and there's no point // restoring it; if we did, we'd just risk messing up the user's window // layout. } void DesktopResizerLinux::CreateMode(const char* name, int width, int height) { XRRModeInfo mode; memset(&mode, 0, sizeof(mode)); mode.width = width; mode.height = height; mode.name = const_cast(name); mode.nameLength = strlen(name); XRRCreateMode(display_, root_, &mode); if (!resources_.Refresh(display_, root_)) { return; } RRMode mode_id = resources_.GetIdForMode(name); if (!mode_id) { return; } XRRAddOutputMode(display_, resources_.GetOutput(), mode_id); } void DesktopResizerLinux::DeleteMode(const char* name) { RRMode mode_id = resources_.GetIdForMode(name); if (mode_id) { XRRDeleteOutputMode(display_, resources_.GetOutput(), mode_id); XRRDestroyMode(display_, mode_id); resources_.Refresh(display_, root_); } } void DesktopResizerLinux::SwitchToMode(const char* name) { RRMode mode_id = None; RROutput* outputs = NULL; int number_of_outputs = 0; if (name) { mode_id = resources_.GetIdForMode(name); CHECK(mode_id); outputs = resources_.get()->outputs; number_of_outputs = resources_.get()->noutput; } XRRSetCrtcConfig(display_, resources_.get(), resources_.GetCrtc(), CurrentTime, 0, 0, mode_id, 1, outputs, number_of_outputs); } scoped_ptr DesktopResizer::Create() { return make_scoped_ptr(new DesktopResizerLinux); } } // namespace remoting