summaryrefslogtreecommitdiffstats
path: root/remoting/host/desktop_resizer_x11.cc
blob: 63fe54240fa1b08bfde2132e3d8c4c3b5b93bd00 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
// 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 <string.h>
#include <X11/extensions/Xrandr.h>
#include <X11/Xlib.h>

#include "base/command_line.h"
#include "base/macros.h"
#include "remoting/base/logging.h"
#include "remoting/host/linux/x11_util.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<int>(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_(nullptr) {
  }

  ~ScreenResources() {
    Release();
  }

  bool Refresh(Display* display, Window window) {
    Release();
    resources_ = XRRGetScreenResources(display, window);
    return resources_ != nullptr;
  }

  void Release() {
    if (resources_) {
      XRRFreeScreenResources(resources_);
      resources_ = nullptr;
    }
  }

  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 DesktopResizerX11 : public DesktopResizer {
 public:
  DesktopResizerX11();
  ~DesktopResizerX11() override;

  // DesktopResizer interface
  ScreenResolution GetCurrentResolution() override;
  std::list<ScreenResolution> 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 nullptr, 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(DesktopResizerX11);
};

DesktopResizerX11::DesktopResizerX11()
    : display_(XOpenDisplay(nullptr)),
      screen_(DefaultScreen(display_)),
      root_(RootWindow(display_, screen_)),
      exact_resize_(base::CommandLine::ForCurrentProcess()->
                    HasSwitch("server-supports-exact-resize")) {
  XRRSelectInput(display_, root_, RRScreenChangeNotifyMask);
}

DesktopResizerX11::~DesktopResizerX11() {
  XCloseDisplay(display_);
}

ScreenResolution DesktopResizerX11::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<ScreenResolution> DesktopResizerX11::GetSupportedResolutions(
    const ScreenResolution& preferred) {
  std::list<ScreenResolution> 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 DesktopResizerX11::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(nullptr);
  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 DesktopResizerX11::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 DesktopResizerX11::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<char*>(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 DesktopResizerX11::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 DesktopResizerX11::SwitchToMode(const char* name) {
  RRMode mode_id = None;
  RROutput* outputs = nullptr;
  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> DesktopResizer::Create() {
  return make_scoped_ptr(new DesktopResizerX11);
}

}  // namespace remoting