1 /****************************************************************************
2 **
3 ** Copyright (C) 2018 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the QtCore module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39 #include "qxcbconnection.h"
40 #include "qxcbscreen.h"
41 #include "qxcbintegration.h"
42 
43 #include <QtGui/private/qhighdpiscaling_p.h>
44 #include <QtCore/QString>
45 #include <QtCore/QList>
46 
47 #include <qpa/qwindowsysteminterface.h>
48 
49 #include <xcb/xinerama.h>
50 
xrandrSelectEvents()51 void QXcbConnection::xrandrSelectEvents()
52 {
53     xcb_screen_iterator_t rootIter = xcb_setup_roots_iterator(setup());
54     for (; rootIter.rem; xcb_screen_next(&rootIter)) {
55         xcb_randr_select_input(xcb_connection(),
56             rootIter.data->root,
57             XCB_RANDR_NOTIFY_MASK_SCREEN_CHANGE |
58             XCB_RANDR_NOTIFY_MASK_OUTPUT_CHANGE |
59             XCB_RANDR_NOTIFY_MASK_CRTC_CHANGE |
60             XCB_RANDR_NOTIFY_MASK_OUTPUT_PROPERTY
61         );
62     }
63 }
64 
findScreenForCrtc(xcb_window_t rootWindow,xcb_randr_crtc_t crtc) const65 QXcbScreen* QXcbConnection::findScreenForCrtc(xcb_window_t rootWindow, xcb_randr_crtc_t crtc) const
66 {
67     for (QXcbScreen *screen : m_screens) {
68         if (screen->root() == rootWindow && screen->crtc() == crtc)
69             return screen;
70     }
71 
72     return nullptr;
73 }
74 
findScreenForOutput(xcb_window_t rootWindow,xcb_randr_output_t output) const75 QXcbScreen* QXcbConnection::findScreenForOutput(xcb_window_t rootWindow, xcb_randr_output_t output) const
76 {
77     for (QXcbScreen *screen : m_screens) {
78         if (screen->root() == rootWindow && screen->output() == output)
79             return screen;
80     }
81 
82     return nullptr;
83 }
84 
virtualDesktopForRootWindow(xcb_window_t rootWindow) const85 QXcbVirtualDesktop* QXcbConnection::virtualDesktopForRootWindow(xcb_window_t rootWindow) const
86 {
87     for (QXcbVirtualDesktop *virtualDesktop : m_virtualDesktops) {
88         if (virtualDesktop->screen()->root == rootWindow)
89             return virtualDesktop;
90     }
91 
92     return nullptr;
93 }
94 
95 /*!
96     \brief Synchronizes the screen list, adds new screens, removes deleted ones
97 */
updateScreens(const xcb_randr_notify_event_t * event)98 void QXcbConnection::updateScreens(const xcb_randr_notify_event_t *event)
99 {
100     if (event->subCode == XCB_RANDR_NOTIFY_CRTC_CHANGE) {
101         xcb_randr_crtc_change_t crtc = event->u.cc;
102         QXcbVirtualDesktop *virtualDesktop = virtualDesktopForRootWindow(crtc.window);
103         if (!virtualDesktop)
104             // Not for us
105             return;
106 
107         QXcbScreen *screen = findScreenForCrtc(crtc.window, crtc.crtc);
108         qCDebug(lcQpaScreen) << "QXcbConnection: XCB_RANDR_NOTIFY_CRTC_CHANGE:" << crtc.crtc
109                              << "mode" << crtc.mode << "relevant screen" << screen;
110         // Only update geometry when there's a valid mode on the CRTC
111         // CRTC with node mode could mean that output has been disabled, and we'll
112         // get RRNotifyOutputChange notification for that.
113         if (screen && crtc.mode) {
114             if (crtc.rotation == XCB_RANDR_ROTATION_ROTATE_90 ||
115                 crtc.rotation == XCB_RANDR_ROTATION_ROTATE_270)
116                 std::swap(crtc.width, crtc.height);
117             screen->updateGeometry(QRect(crtc.x, crtc.y, crtc.width, crtc.height), crtc.rotation);
118             if (screen->mode() != crtc.mode)
119                 screen->updateRefreshRate(crtc.mode);
120         }
121 
122     } else if (event->subCode == XCB_RANDR_NOTIFY_OUTPUT_CHANGE) {
123         xcb_randr_output_change_t output = event->u.oc;
124         QXcbVirtualDesktop *virtualDesktop = virtualDesktopForRootWindow(output.window);
125         if (!virtualDesktop)
126             // Not for us
127             return;
128 
129         QXcbScreen *screen = findScreenForOutput(output.window, output.output);
130         qCDebug(lcQpaScreen) << "QXcbConnection: XCB_RANDR_NOTIFY_OUTPUT_CHANGE:" << output.output;
131 
132         if (screen && output.connection == XCB_RANDR_CONNECTION_DISCONNECTED) {
133             qCDebug(lcQpaScreen) << "screen" << screen->name() << "has been disconnected";
134             destroyScreen(screen);
135         } else if (!screen && output.connection == XCB_RANDR_CONNECTION_CONNECTED) {
136             // New XRandR output is available and it's enabled
137             if (output.crtc != XCB_NONE && output.mode != XCB_NONE) {
138                 auto outputInfo = Q_XCB_REPLY(xcb_randr_get_output_info, xcb_connection(),
139                                               output.output, output.config_timestamp);
140                 // Find a fake screen
141                 const auto scrs = virtualDesktop->screens();
142                 for (QPlatformScreen *scr : scrs) {
143                     QXcbScreen *xcbScreen = static_cast<QXcbScreen *>(scr);
144                     if (xcbScreen->output() == XCB_NONE) {
145                         screen = xcbScreen;
146                         break;
147                     }
148                 }
149 
150                 if (screen) {
151                     QString nameWas = screen->name();
152                     // Transform the fake screen into a physical screen
153                     screen->setOutput(output.output, outputInfo.get());
154                     updateScreen(screen, output);
155                     qCDebug(lcQpaScreen) << "output" << screen->name()
156                                          << "is connected and enabled; was fake:" << nameWas;
157                 } else {
158                     screen = createScreen(virtualDesktop, output, outputInfo.get());
159                     qCDebug(lcQpaScreen) << "output" << screen->name() << "is connected and enabled";
160                 }
161                 QHighDpiScaling::updateHighDpiScaling();
162             }
163         } else if (screen) {
164             if (output.crtc == XCB_NONE && output.mode == XCB_NONE) {
165                 // Screen has been disabled
166                 auto outputInfo = Q_XCB_REPLY(xcb_randr_get_output_info, xcb_connection(),
167                                               output.output, output.config_timestamp);
168                 if (outputInfo->crtc == XCB_NONE) {
169                     qCDebug(lcQpaScreen) << "output" << screen->name() << "has been disabled";
170                     destroyScreen(screen);
171                 } else {
172                     qCDebug(lcQpaScreen) << "output" << screen->name() << "has been temporarily disabled for the mode switch";
173                     // Reset crtc to skip RRCrtcChangeNotify events,
174                     // because they may be invalid in the middle of the mode switch
175                     screen->setCrtc(XCB_NONE);
176                 }
177             } else {
178                 updateScreen(screen, output);
179                 qCDebug(lcQpaScreen) << "output has changed" << screen;
180             }
181         }
182 
183         qCDebug(lcQpaScreen) << "primary output is" << qAsConst(m_screens).first()->name();
184     }
185 }
186 
checkOutputIsPrimary(xcb_window_t rootWindow,xcb_randr_output_t output)187 bool QXcbConnection::checkOutputIsPrimary(xcb_window_t rootWindow, xcb_randr_output_t output)
188 {
189     auto primary = Q_XCB_REPLY(xcb_randr_get_output_primary, xcb_connection(), rootWindow);
190     if (!primary)
191         qWarning("failed to get the primary output of the screen");
192 
193     const bool isPrimary = primary ? (primary->output == output) : false;
194 
195     return isPrimary;
196 }
197 
updateScreen(QXcbScreen * screen,const xcb_randr_output_change_t & outputChange)198 void QXcbConnection::updateScreen(QXcbScreen *screen, const xcb_randr_output_change_t &outputChange)
199 {
200     screen->setCrtc(outputChange.crtc); // Set the new crtc, because it can be invalid
201     screen->updateGeometry(outputChange.config_timestamp);
202     if (screen->mode() != outputChange.mode)
203         screen->updateRefreshRate(outputChange.mode);
204     // Only screen which belongs to the primary virtual desktop can be a primary screen
205     if (screen->screenNumber() == primaryScreenNumber()) {
206         if (!screen->isPrimary() && checkOutputIsPrimary(outputChange.window, outputChange.output)) {
207             screen->setPrimary(true);
208 
209             // If the screen became primary, reshuffle the order in QGuiApplicationPrivate
210             const int idx = m_screens.indexOf(screen);
211             if (idx > 0) {
212                 qAsConst(m_screens).first()->setPrimary(false);
213                 m_screens.swapItemsAt(0, idx);
214             }
215             screen->virtualDesktop()->setPrimaryScreen(screen);
216             QWindowSystemInterface::handlePrimaryScreenChanged(screen);
217         }
218     }
219 }
220 
createScreen(QXcbVirtualDesktop * virtualDesktop,const xcb_randr_output_change_t & outputChange,xcb_randr_get_output_info_reply_t * outputInfo)221 QXcbScreen *QXcbConnection::createScreen(QXcbVirtualDesktop *virtualDesktop,
222                                          const xcb_randr_output_change_t &outputChange,
223                                          xcb_randr_get_output_info_reply_t *outputInfo)
224 {
225     QXcbScreen *screen = new QXcbScreen(this, virtualDesktop, outputChange.output, outputInfo);
226     // Only screen which belongs to the primary virtual desktop can be a primary screen
227     if (screen->screenNumber() == primaryScreenNumber())
228         screen->setPrimary(checkOutputIsPrimary(outputChange.window, outputChange.output));
229 
230     if (screen->isPrimary()) {
231         if (!m_screens.isEmpty())
232             qAsConst(m_screens).first()->setPrimary(false);
233 
234         m_screens.prepend(screen);
235     } else {
236         m_screens.append(screen);
237     }
238     virtualDesktop->addScreen(screen);
239     QWindowSystemInterface::handleScreenAdded(screen, screen->isPrimary());
240 
241     return screen;
242 }
243 
destroyScreen(QXcbScreen * screen)244 void QXcbConnection::destroyScreen(QXcbScreen *screen)
245 {
246     QXcbVirtualDesktop *virtualDesktop = screen->virtualDesktop();
247     if (virtualDesktop->screens().count() == 1) {
248         // If there are no other screens on the same virtual desktop,
249         // then transform the physical screen into a fake screen.
250         const QString nameWas = screen->name();
251         screen->setOutput(XCB_NONE, nullptr);
252         qCDebug(lcQpaScreen) << "transformed" << nameWas << "to fake" << screen;
253     } else {
254         // There is more than one screen on the same virtual desktop, remove the screen
255         m_screens.removeOne(screen);
256         virtualDesktop->removeScreen(screen);
257 
258         // When primary screen is removed, set the new primary screen
259         // which belongs to the primary virtual desktop.
260         if (screen->isPrimary()) {
261             QXcbScreen *newPrimary = static_cast<QXcbScreen *>(virtualDesktop->screens().at(0));
262             newPrimary->setPrimary(true);
263             const int idx = m_screens.indexOf(newPrimary);
264             if (idx > 0)
265                 m_screens.swapItemsAt(0, idx);
266             QWindowSystemInterface::handlePrimaryScreenChanged(newPrimary);
267         }
268 
269         QWindowSystemInterface::handleScreenRemoved(screen);
270     }
271 }
272 
initializeScreens()273 void QXcbConnection::initializeScreens()
274 {
275     xcb_screen_iterator_t it = xcb_setup_roots_iterator(setup());
276     int xcbScreenNumber = 0;    // screen number in the xcb sense
277     QXcbScreen *primaryScreen = nullptr;
278     while (it.rem) {
279         // Each "screen" in xcb terminology is a virtual desktop,
280         // potentially a collection of separate juxtaposed monitors.
281         // But we want a separate QScreen for each output (e.g. DVI-I-1, VGA-1, etc.)
282         // which will become virtual siblings.
283         xcb_screen_t *xcbScreen = it.data;
284         QXcbVirtualDesktop *virtualDesktop = new QXcbVirtualDesktop(this, xcbScreen, xcbScreenNumber);
285         m_virtualDesktops.append(virtualDesktop);
286         QList<QPlatformScreen *> siblings;
287         if (hasXRandr()) {
288             // RRGetScreenResourcesCurrent is fast but it may return nothing if the
289             // configuration is not initialized wrt to the hardware. We should call
290             // RRGetScreenResources in this case.
291             auto resources_current = Q_XCB_REPLY(xcb_randr_get_screen_resources_current,
292                                                  xcb_connection(), xcbScreen->root);
293             decltype(Q_XCB_REPLY(xcb_randr_get_screen_resources,
294                                  xcb_connection(), xcbScreen->root)) resources;
295             if (!resources_current) {
296                 qWarning("failed to get the current screen resources");
297             } else {
298                 xcb_timestamp_t timestamp = 0;
299                 xcb_randr_output_t *outputs = nullptr;
300                 int outputCount = xcb_randr_get_screen_resources_current_outputs_length(resources_current.get());
301                 if (outputCount) {
302                     timestamp = resources_current->config_timestamp;
303                     outputs = xcb_randr_get_screen_resources_current_outputs(resources_current.get());
304                 } else {
305                     resources = Q_XCB_REPLY(xcb_randr_get_screen_resources,
306                                             xcb_connection(), xcbScreen->root);
307                     if (!resources) {
308                         qWarning("failed to get the screen resources");
309                     } else {
310                         timestamp = resources->config_timestamp;
311                         outputCount = xcb_randr_get_screen_resources_outputs_length(resources.get());
312                         outputs = xcb_randr_get_screen_resources_outputs(resources.get());
313                     }
314                 }
315 
316                 if (outputCount) {
317                     auto primary = Q_XCB_REPLY(xcb_randr_get_output_primary, xcb_connection(), xcbScreen->root);
318                     if (!primary) {
319                         qWarning("failed to get the primary output of the screen");
320                     } else {
321                         for (int i = 0; i < outputCount; i++) {
322                             auto output = Q_XCB_REPLY_UNCHECKED(xcb_randr_get_output_info,
323                                                                 xcb_connection(), outputs[i], timestamp);
324                             // Invalid, disconnected or disabled output
325                             if (!output)
326                                 continue;
327 
328                             if (output->connection != XCB_RANDR_CONNECTION_CONNECTED) {
329                                 qCDebug(lcQpaScreen, "Output %s is not connected", qPrintable(
330                                             QString::fromUtf8((const char*)xcb_randr_get_output_info_name(output.get()),
331                                                               xcb_randr_get_output_info_name_length(output.get()))));
332                                 continue;
333                             }
334 
335                             if (output->crtc == XCB_NONE) {
336                                 qCDebug(lcQpaScreen, "Output %s is not enabled", qPrintable(
337                                             QString::fromUtf8((const char*)xcb_randr_get_output_info_name(output.get()),
338                                                               xcb_randr_get_output_info_name_length(output.get()))));
339                                 continue;
340                             }
341 
342                             QXcbScreen *screen = new QXcbScreen(this, virtualDesktop, outputs[i], output.get());
343                             siblings << screen;
344                             m_screens << screen;
345 
346                             // There can be multiple outputs per screen, use either
347                             // the first or an exact match.  An exact match isn't
348                             // always available if primary->output is XCB_NONE
349                             // or currently disconnected output.
350                             if (primaryScreenNumber() == xcbScreenNumber) {
351                                 if (!primaryScreen || (primary && outputs[i] == primary->output)) {
352                                     if (primaryScreen)
353                                         primaryScreen->setPrimary(false);
354                                     primaryScreen = screen;
355                                     primaryScreen->setPrimary(true);
356                                     siblings.prepend(siblings.takeLast());
357                                 }
358                             }
359                         }
360                     }
361                 }
362             }
363         } else if (hasXinerama()) {
364             // Xinerama is available
365             auto screens = Q_XCB_REPLY(xcb_xinerama_query_screens, xcb_connection());
366             if (screens) {
367                 xcb_xinerama_screen_info_iterator_t it = xcb_xinerama_query_screens_screen_info_iterator(screens.get());
368                 while (it.rem) {
369                     xcb_xinerama_screen_info_t *screen_info = it.data;
370                     QXcbScreen *screen = new QXcbScreen(this, virtualDesktop,
371                                                         XCB_NONE, nullptr,
372                                                         screen_info, it.index);
373                     siblings << screen;
374                     m_screens << screen;
375                     xcb_xinerama_screen_info_next(&it);
376                 }
377             }
378         }
379         if (siblings.isEmpty()) {
380             // If there are no XRandR outputs or XRandR extension is missing,
381             // then create a fake/legacy screen.
382             QXcbScreen *screen = new QXcbScreen(this, virtualDesktop, XCB_NONE, nullptr);
383             qCDebug(lcQpaScreen) << "created fake screen" << screen;
384             m_screens << screen;
385             if (primaryScreenNumber() == xcbScreenNumber) {
386                 primaryScreen = screen;
387                 primaryScreen->setPrimary(true);
388             }
389             siblings << screen;
390         }
391         virtualDesktop->setScreens(std::move(siblings));
392         xcb_screen_next(&it);
393         ++xcbScreenNumber;
394     } // for each xcb screen
395 
396     for (QXcbVirtualDesktop *virtualDesktop : qAsConst(m_virtualDesktops))
397         virtualDesktop->subscribeToXFixesSelectionNotify();
398 
399     if (m_virtualDesktops.isEmpty()) {
400         qFatal("QXcbConnection: no screens available");
401     } else {
402         // Ensure the primary screen is first on the list
403         if (primaryScreen) {
404             if (qAsConst(m_screens).first() != primaryScreen) {
405                 m_screens.removeOne(primaryScreen);
406                 m_screens.prepend(primaryScreen);
407             }
408         }
409 
410         // Push the screens to QGuiApplication
411         for (QXcbScreen *screen : qAsConst(m_screens)) {
412             qCDebug(lcQpaScreen) << "adding" << screen << "(Primary:" << screen->isPrimary() << ")";
413             QWindowSystemInterface::handleScreenAdded(screen, screen->isPrimary());
414         }
415 
416         qCDebug(lcQpaScreen) << "primary output is" << qAsConst(m_screens).first()->name();
417     }
418 }
419