1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "remoting/host/desktop_resizer.h" 6#include "remoting/host/linux/x11_util.h" 7 8#include <string.h> 9#include <X11/extensions/Xrandr.h> 10#include <X11/Xlib.h> 11 12#include "base/command_line.h" 13#include "remoting/base/logging.h" 14 15// On Linux, we use the xrandr extension to change the desktop resolution. For 16// now, we only support resize-to-client for Xvfb-based servers that can match 17// the client resolution exactly. To support best-resolution matching, it would 18// be necessary to implement |GetSupportedResolutions|, but it's not considered 19// a priority now. 20// 21// Xrandr has a number of restrictions that make this code more complex: 22// 23// 1. It's not possible to change the resolution of an existing mode. Instead, 24// the mode must be deleted and recreated. 25// 2. It's not possible to delete a mode that's in use. 26// 3. Errors are communicated via Xlib's spectacularly unhelpful mechanism 27// of terminating the process unless you install an error handler. 28// 29// The basic approach is as follows: 30// 31// 1. Create a new mode with the correct resolution; 32// 2. Switch to the new mode; 33// 3. Delete the old mode. 34// 35// Since the new mode must have a different name, and we want the current mode 36// name to be consistent, we then additionally: 37// 38// 4. Recreate the old mode at the new resolution; 39// 5. Switch to the old mode; 40// 6. Delete the temporary mode. 41// 42// Name consistency will allow a future CL to disable resize-to-client if the 43// user has changed the mode to something other than "Chrome Remote Desktop 44// client resolution". It doesn't make the code significantly more complex. 45 46namespace { 47 48int PixelsToMillimeters(int pixels, int dpi) { 49 DCHECK(dpi != 0); 50 51 const double kMillimetersPerInch = 25.4; 52 53 // (pixels / dpi) is the length in inches. Multiplying by 54 // kMillimetersPerInch converts to mm. Multiplication is done first to 55 // avoid integer division. 56 return static_cast<int>(kMillimetersPerInch * pixels / dpi); 57} 58 59// TODO(jamiewalch): Use the correct DPI for the mode: http://crbug.com/172405. 60const int kDefaultDPI = 96; 61 62} // namespace 63 64namespace remoting { 65 66// Wrapper class for the XRRScreenResources struct. 67class ScreenResources { 68 public: 69 ScreenResources() : resources_(NULL) { 70 } 71 72 ~ScreenResources() { 73 Release(); 74 } 75 76 bool Refresh(Display* display, Window window) { 77 Release(); 78 resources_ = XRRGetScreenResources(display, window); 79 return resources_ != NULL; 80 } 81 82 void Release() { 83 if (resources_) { 84 XRRFreeScreenResources(resources_); 85 resources_ = NULL; 86 } 87 } 88 89 RRMode GetIdForMode(const char* name) { 90 CHECK(resources_); 91 for (int i = 0; i < resources_->nmode; ++i) { 92 const XRRModeInfo& mode = resources_->modes[i]; 93 if (strcmp(mode.name, name) == 0) { 94 return mode.id; 95 } 96 } 97 return 0; 98 } 99 100 // For now, assume we're only ever interested in the first output. 101 RROutput GetOutput() { 102 CHECK(resources_); 103 return resources_->outputs[0]; 104 } 105 106 // For now, assume we're only ever interested in the first crtc. 107 RRCrtc GetCrtc() { 108 CHECK(resources_); 109 return resources_->crtcs[0]; 110 } 111 112 XRROutputInfo* GetOutputInfo(Display* display, RROutput output_id) { 113 CHECK(resources_); 114 return XRRGetOutputInfo(display, resources_, output_id); 115 } 116 117 XRRScreenResources* get() { return resources_; } 118 119 private: 120 XRRScreenResources* resources_; 121}; 122 123 124class DesktopResizerLinux : public DesktopResizer { 125 public: 126 DesktopResizerLinux(); 127 virtual ~DesktopResizerLinux(); 128 129 // DesktopResizer interface 130 virtual ScreenResolution GetCurrentResolution() OVERRIDE; 131 virtual std::list<ScreenResolution> GetSupportedResolutions( 132 const ScreenResolution& preferred) OVERRIDE; 133 virtual void SetResolution(const ScreenResolution& resolution) OVERRIDE; 134 virtual void RestoreResolution(const ScreenResolution& original) OVERRIDE; 135 136 private: 137 // Create a mode, and attach it to the primary output. If the mode already 138 // exists, it is left unchanged. 139 void CreateMode(const char* name, int width, int height); 140 141 // Remove the specified mode from the primary output, and delete it. If the 142 // mode is in use, it is not deleted. 143 void DeleteMode(const char* name); 144 145 // Switch the primary output to the specified mode. If name is NULL, the 146 // primary output is disabled instead, which is required before changing 147 // its resolution. 148 void SwitchToMode(const char* name); 149 150 Display* display_; 151 int screen_; 152 Window root_; 153 ScreenResources resources_; 154 bool exact_resize_; 155 156 DISALLOW_COPY_AND_ASSIGN(DesktopResizerLinux); 157}; 158 159DesktopResizerLinux::DesktopResizerLinux() 160 : display_(XOpenDisplay(NULL)), 161 screen_(DefaultScreen(display_)), 162 root_(RootWindow(display_, screen_)), 163 exact_resize_(base::CommandLine::ForCurrentProcess()-> 164 HasSwitch("server-supports-exact-resize")) { 165 XRRSelectInput(display_, root_, RRScreenChangeNotifyMask); 166} 167 168DesktopResizerLinux::~DesktopResizerLinux() { 169 XCloseDisplay(display_); 170} 171 172ScreenResolution DesktopResizerLinux::GetCurrentResolution() { 173 if (!exact_resize_) { 174 // TODO(jamiewalch): Remove this early return if we decide to support 175 // non-Xvfb servers. 176 return ScreenResolution(); 177 } 178 179 // TODO(lambroslambrou): Xrandr requires that we process RRScreenChangeNotify 180 // events, otherwise DisplayWidth and DisplayHeight do not return the current 181 // values. Normally, this would be done via a central X event loop, but we 182 // don't have one, hence this horrible hack. 183 // 184 // Note that the WatchFileDescriptor approach taken in XServerClipboard 185 // doesn't work here because resize events have already been read from the 186 // X server socket by the time the resize function returns, hence the 187 // file descriptor is never seen as readable. 188 while (XEventsQueued(display_, QueuedAlready)) { 189 XEvent event; 190 XNextEvent(display_, &event); 191 XRRUpdateConfiguration(&event); 192 } 193 194 ScreenResolution result( 195 webrtc::DesktopSize( 196 DisplayWidth(display_, DefaultScreen(display_)), 197 DisplayHeight(display_, DefaultScreen(display_))), 198 webrtc::DesktopVector(kDefaultDPI, kDefaultDPI)); 199 return result; 200} 201 202std::list<ScreenResolution> DesktopResizerLinux::GetSupportedResolutions( 203 const ScreenResolution& preferred) { 204 std::list<ScreenResolution> result; 205 if (exact_resize_) { 206 // Clamp the specified size to something valid for the X server. 207 int min_width = 0, min_height = 0, max_width = 0, max_height = 0; 208 XRRGetScreenSizeRange(display_, root_, 209 &min_width, &min_height, 210 &max_width, &max_height); 211 int width = std::min(std::max(preferred.dimensions().width(), min_width), 212 max_width); 213 int height = std::min(std::max(preferred.dimensions().height(), min_height), 214 max_height); 215 // Additionally impose a minimum size of 640x480, since anything smaller 216 // doesn't seem very useful. 217 ScreenResolution actual( 218 webrtc::DesktopSize(std::max(640, width), std::max(480, height)), 219 webrtc::DesktopVector(kDefaultDPI, kDefaultDPI)); 220 result.push_back(actual); 221 } else { 222 // TODO(jamiewalch): Return the list of supported resolutions if we can't 223 // support exact-size matching. 224 } 225 return result; 226} 227 228void DesktopResizerLinux::SetResolution(const ScreenResolution& resolution) { 229 if (!exact_resize_) { 230 // TODO(jamiewalch): Remove this early return if we decide to support 231 // non-Xvfb servers. 232 return; 233 } 234 235 // Ignore X errors encountered while resizing the display. We might hit an 236 // error, for example if xrandr has been used to add a mode with the same 237 // name as our temporary mode, or to remove the "client resolution" mode. We 238 // don't want to terminate the process if this happens. 239 ScopedXErrorHandler handler(ScopedXErrorHandler::Ignore()); 240 241 // Grab the X server while we're changing the display resolution. This ensures 242 // that the display configuration doesn't change under our feet. 243 ScopedXGrabServer grabber(display_); 244 245 // The name of the mode representing the current client view resolution and 246 // the temporary mode used for the reasons described at the top of this file. 247 // The former should be localized if it's user-visible; the latter only 248 // exists briefly and does not need to localized. 249 const char* kModeName = "Chrome Remote Desktop client resolution"; 250 const char* kTempModeName = "Chrome Remote Desktop temporary mode"; 251 252 // Actually do the resize operation, preserving the current mode name. Note 253 // that we have to detach the output from any mode in order to resize it 254 // (strictly speaking, this is only required when reducing the size, but it 255 // seems safe to do it regardless). 256 HOST_LOG << "Changing desktop size to " << resolution.dimensions().width() 257 << "x" << resolution.dimensions().height(); 258 259 // TODO(lambroslambrou): Use the DPI from client size information. 260 int width_mm = PixelsToMillimeters(resolution.dimensions().width(), 261 kDefaultDPI); 262 int height_mm = PixelsToMillimeters(resolution.dimensions().height(), 263 kDefaultDPI); 264 CreateMode(kTempModeName, resolution.dimensions().width(), 265 resolution.dimensions().height()); 266 SwitchToMode(NULL); 267 XRRSetScreenSize(display_, root_, resolution.dimensions().width(), 268 resolution.dimensions().height(), width_mm, height_mm); 269 SwitchToMode(kTempModeName); 270 DeleteMode(kModeName); 271 CreateMode(kModeName, resolution.dimensions().width(), 272 resolution.dimensions().height()); 273 SwitchToMode(kModeName); 274 DeleteMode(kTempModeName); 275} 276 277void DesktopResizerLinux::RestoreResolution(const ScreenResolution& original) { 278 // Since the desktop is only visible via a remote connection, the original 279 // resolution of the desktop will never been seen and there's no point 280 // restoring it; if we did, we'd just risk messing up the user's window 281 // layout. 282} 283 284void DesktopResizerLinux::CreateMode(const char* name, int width, int height) { 285 XRRModeInfo mode; 286 memset(&mode, 0, sizeof(mode)); 287 mode.width = width; 288 mode.height = height; 289 mode.name = const_cast<char*>(name); 290 mode.nameLength = strlen(name); 291 XRRCreateMode(display_, root_, &mode); 292 293 if (!resources_.Refresh(display_, root_)) { 294 return; 295 } 296 RRMode mode_id = resources_.GetIdForMode(name); 297 if (!mode_id) { 298 return; 299 } 300 XRRAddOutputMode(display_, resources_.GetOutput(), mode_id); 301} 302 303void DesktopResizerLinux::DeleteMode(const char* name) { 304 RRMode mode_id = resources_.GetIdForMode(name); 305 if (mode_id) { 306 XRRDeleteOutputMode(display_, resources_.GetOutput(), mode_id); 307 XRRDestroyMode(display_, mode_id); 308 resources_.Refresh(display_, root_); 309 } 310} 311 312void DesktopResizerLinux::SwitchToMode(const char* name) { 313 RRMode mode_id = None; 314 RROutput* outputs = NULL; 315 int number_of_outputs = 0; 316 if (name) { 317 mode_id = resources_.GetIdForMode(name); 318 CHECK(mode_id); 319 outputs = resources_.get()->outputs; 320 number_of_outputs = resources_.get()->noutput; 321 } 322 XRRSetCrtcConfig(display_, resources_.get(), resources_.GetCrtc(), 323 CurrentTime, 0, 0, mode_id, 1, outputs, number_of_outputs); 324} 325 326scoped_ptr<DesktopResizer> DesktopResizer::Create() { 327 return scoped_ptr<DesktopResizer>(new DesktopResizerLinux); 328} 329 330} // namespace remoting 331