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