// Copyright (C) 2018 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qxcbconnection.h" #include "qxcbscreen.h" #include "qxcbintegration.h" #include #include #include #include void QXcbConnection::xrandrSelectEvents() { xcb_screen_iterator_t rootIter = xcb_setup_roots_iterator(setup()); for (; rootIter.rem; xcb_screen_next(&rootIter)) { xcb_randr_select_input(xcb_connection(), rootIter.data->root, XCB_RANDR_NOTIFY_MASK_SCREEN_CHANGE | XCB_RANDR_NOTIFY_MASK_OUTPUT_CHANGE | XCB_RANDR_NOTIFY_MASK_CRTC_CHANGE | XCB_RANDR_NOTIFY_MASK_OUTPUT_PROPERTY ); } } QXcbScreen* QXcbConnection::findScreenForCrtc(xcb_window_t rootWindow, xcb_randr_crtc_t crtc) const { for (QXcbScreen *screen : m_screens) { if (screen->root() == rootWindow) { if (screen->m_monitor) { if (screen->crtcs().contains(crtc)) return screen; } else { if (screen->crtc() == crtc) return screen; } } } return nullptr; } QXcbScreen* QXcbConnection::findScreenForOutput(xcb_window_t rootWindow, xcb_randr_output_t output) const { for (QXcbScreen *screen : m_screens) { if (screen->root() == rootWindow) { if (screen->m_monitor) { if (screen->outputs().contains(output)) return screen; } else { if (screen->output() == output) return screen; } } } return nullptr; } QXcbVirtualDesktop* QXcbConnection::virtualDesktopForRootWindow(xcb_window_t rootWindow) const { for (QXcbVirtualDesktop *virtualDesktop : m_virtualDesktops) { if (virtualDesktop->screen()->root == rootWindow) return virtualDesktop; } return nullptr; } /*! \brief Synchronizes the screen list, adds new screens, removes deleted ones */ void QXcbConnection::updateScreens(const xcb_randr_notify_event_t *event) { if (event->subCode == XCB_RANDR_NOTIFY_CRTC_CHANGE) { xcb_randr_crtc_change_t crtc = event->u.cc; QXcbVirtualDesktop *virtualDesktop = virtualDesktopForRootWindow(crtc.window); if (!virtualDesktop) // Not for us return; QXcbScreen *screen = findScreenForCrtc(crtc.window, crtc.crtc); qCDebug(lcQpaScreen) << "QXcbConnection: XCB_RANDR_NOTIFY_CRTC_CHANGE:" << crtc.crtc << "mode" << crtc.mode << "relevant screen" << screen; // Only update geometry when there's a valid mode on the CRTC // CRTC with node mode could mean that output has been disabled, and we'll // get RRNotifyOutputChange notification for that. if (screen && crtc.mode) { if (crtc.rotation == XCB_RANDR_ROTATION_ROTATE_90 || crtc.rotation == XCB_RANDR_ROTATION_ROTATE_270) std::swap(crtc.width, crtc.height); screen->updateGeometry(QRect(crtc.x, crtc.y, crtc.width, crtc.height), crtc.rotation); if (screen->mode() != crtc.mode) screen->updateRefreshRate(crtc.mode); } } else if (event->subCode == XCB_RANDR_NOTIFY_OUTPUT_CHANGE) { xcb_randr_output_change_t output = event->u.oc; QXcbVirtualDesktop *virtualDesktop = virtualDesktopForRootWindow(output.window); if (!virtualDesktop) // Not for us return; QXcbScreen *screen = findScreenForOutput(output.window, output.output); qCDebug(lcQpaScreen) << "QXcbConnection: XCB_RANDR_NOTIFY_OUTPUT_CHANGE:" << output.output; if (screen && output.connection == XCB_RANDR_CONNECTION_DISCONNECTED) { qCDebug(lcQpaScreen) << "screen" << screen->name() << "has been disconnected"; destroyScreen(screen); } else if (!screen && output.connection == XCB_RANDR_CONNECTION_CONNECTED) { // New XRandR output is available and it's enabled if (output.crtc != XCB_NONE && output.mode != XCB_NONE) { auto outputInfo = Q_XCB_REPLY(xcb_randr_get_output_info, xcb_connection(), output.output, output.config_timestamp); // Find a fake screen const auto scrs = virtualDesktop->screens(); for (QPlatformScreen *scr : scrs) { QXcbScreen *xcbScreen = static_cast(scr); if (xcbScreen->output() == XCB_NONE) { screen = xcbScreen; break; } } if (screen) { QString nameWas = screen->name(); // Transform the fake screen into a physical screen screen->setOutput(output.output, outputInfo.get()); updateScreen(screen, output); qCDebug(lcQpaScreen) << "output" << screen->name() << "is connected and enabled; was fake:" << nameWas; } else { screen = createScreen(virtualDesktop, output, outputInfo.get()); qCDebug(lcQpaScreen) << "output" << screen->name() << "is connected and enabled"; } } } else if (screen) { if (output.crtc == XCB_NONE && output.mode == XCB_NONE) { // Screen has been disabled auto outputInfo = Q_XCB_REPLY(xcb_randr_get_output_info, xcb_connection(), output.output, output.config_timestamp); if (outputInfo->crtc == XCB_NONE) { qCDebug(lcQpaScreen) << "output" << screen->name() << "has been disabled"; destroyScreen(screen); } else { qCDebug(lcQpaScreen) << "output" << screen->name() << "has been temporarily disabled for the mode switch"; // Reset crtc to skip RRCrtcChangeNotify events, // because they may be invalid in the middle of the mode switch screen->setCrtc(XCB_NONE); } } else { updateScreen(screen, output); qCDebug(lcQpaScreen) << "output has changed" << screen; } } qCDebug(lcQpaScreen) << "updateScreens: primary output is" << qAsConst(m_screens).first()->name(); } } bool QXcbConnection::checkOutputIsPrimary(xcb_window_t rootWindow, xcb_randr_output_t output) { auto primary = Q_XCB_REPLY(xcb_randr_get_output_primary, xcb_connection(), rootWindow); if (!primary) qWarning("failed to get the primary output of the screen"); const bool isPrimary = primary ? (primary->output == output) : false; return isPrimary; } void QXcbConnection::updateScreen(QXcbScreen *screen, const xcb_randr_output_change_t &outputChange) { screen->setCrtc(outputChange.crtc); // Set the new crtc, because it can be invalid screen->updateGeometry(outputChange.config_timestamp); if (screen->mode() != outputChange.mode) screen->updateRefreshRate(outputChange.mode); // Only screen which belongs to the primary virtual desktop can be a primary screen if (screen->screenNumber() == primaryScreenNumber()) { if (!screen->isPrimary() && checkOutputIsPrimary(outputChange.window, outputChange.output)) { screen->setPrimary(true); // If the screen became primary, reshuffle the order in QGuiApplicationPrivate const int idx = m_screens.indexOf(screen); if (idx > 0) { qAsConst(m_screens).first()->setPrimary(false); m_screens.swapItemsAt(0, idx); } screen->virtualDesktop()->setPrimaryScreen(screen); QWindowSystemInterface::handlePrimaryScreenChanged(screen); } } } QXcbScreen *QXcbConnection::createScreen(QXcbVirtualDesktop *virtualDesktop, const xcb_randr_output_change_t &outputChange, xcb_randr_get_output_info_reply_t *outputInfo) { QXcbScreen *screen = new QXcbScreen(this, virtualDesktop, outputChange.output, outputInfo); // Only screen which belongs to the primary virtual desktop can be a primary screen if (screen->screenNumber() == primaryScreenNumber()) screen->setPrimary(checkOutputIsPrimary(outputChange.window, outputChange.output)); if (screen->isPrimary()) { if (!m_screens.isEmpty()) qAsConst(m_screens).first()->setPrimary(false); m_screens.prepend(screen); } else { m_screens.append(screen); } virtualDesktop->addScreen(screen); QWindowSystemInterface::handleScreenAdded(screen, screen->isPrimary()); return screen; } void QXcbConnection::destroyScreen(QXcbScreen *screen) { QXcbVirtualDesktop *virtualDesktop = screen->virtualDesktop(); if (virtualDesktop->screens().count() == 1) { // If there are no other screens on the same virtual desktop, // then transform the physical screen into a fake screen. const QString nameWas = screen->name(); screen->setOutput(XCB_NONE, nullptr); qCDebug(lcQpaScreen) << "transformed" << nameWas << "to fake" << screen; } else { // There is more than one screen on the same virtual desktop, remove the screen m_screens.removeOne(screen); virtualDesktop->removeScreen(screen); // When primary screen is removed, set the new primary screen // which belongs to the primary virtual desktop. if (screen->isPrimary()) { QXcbScreen *newPrimary = static_cast(virtualDesktop->screens().at(0)); newPrimary->setPrimary(true); const int idx = m_screens.indexOf(newPrimary); if (idx > 0) m_screens.swapItemsAt(0, idx); QWindowSystemInterface::handlePrimaryScreenChanged(newPrimary); } qCDebug(lcQpaScreen) << "destroyScreen: destroy" << screen; QWindowSystemInterface::handleScreenRemoved(screen); } } void QXcbConnection::updateScreen_monitor(QXcbScreen *screen, xcb_randr_monitor_info_t *monitorInfo, xcb_timestamp_t timestamp) { screen->setMonitor(monitorInfo, timestamp); if (screen->isPrimary()) { const int idx = m_screens.indexOf(screen); if (idx > 0) { qAsConst(m_screens).first()->setPrimary(false); m_screens.swapItemsAt(0, idx); } screen->virtualDesktop()->setPrimaryScreen(screen); QWindowSystemInterface::handlePrimaryScreenChanged(screen); } qCDebug(lcQpaScreen) << "updateScreen_monitor: update" << screen << "(Primary:" << screen->isPrimary() << ")"; } QXcbScreen *QXcbConnection::createScreen_monitor(QXcbVirtualDesktop *virtualDesktop, xcb_randr_monitor_info_t *monitorInfo, xcb_timestamp_t timestamp) { QXcbScreen *screen = new QXcbScreen(this, virtualDesktop, monitorInfo, timestamp); if (screen->isPrimary()) { if (!m_screens.isEmpty()) qAsConst(m_screens).first()->setPrimary(false); m_screens.prepend(screen); } else { m_screens.append(screen); } qCDebug(lcQpaScreen) << "createScreen_monitor: adding" << screen << "(Primary:" << screen->isPrimary() << ")"; virtualDesktop->addScreen(screen); QWindowSystemInterface::handleScreenAdded(screen, screen->isPrimary()); return screen; } QXcbVirtualDesktop *QXcbConnection::virtualDesktopForNumber(int n) const { for (QXcbVirtualDesktop *virtualDesktop : m_virtualDesktops) { if (virtualDesktop->number() == n) return virtualDesktop; } return nullptr; } QXcbScreen *QXcbConnection::findScreenForMonitorInfo(const QList &screens, xcb_randr_monitor_info_t *monitorInfo) { for (int i = 0; i < screens.size(); ++i) { auto s = static_cast(screens[i]); if (monitorInfo) { QByteArray ba2 = atomName(monitorInfo->name); if (s->name().toLocal8Bit() == ba2) return s; } } return nullptr; } void QXcbConnection::initializeScreens(bool initialized) { xcb_screen_iterator_t it = xcb_setup_roots_iterator(setup()); int xcbScreenNumber = 0; // screen number in the xcb sense QXcbScreen *primaryScreen = nullptr; if (isAtLeastXRandR15() && initialized) m_screens.clear(); while (it.rem) { if (isAtLeastXRandR15()) initializeScreensFromMonitor(&it, xcbScreenNumber, &primaryScreen, initialized); else if (isAtLeastXRandR12()) initializeScreensFromOutput(&it, xcbScreenNumber, &primaryScreen); else { qWarning("There is no XRandR 1.2 and later version available. There will be only fake screen(s) to use."); initializeScreensWithoutXRandR(&it, xcbScreenNumber, &primaryScreen); } xcb_screen_next(&it); ++xcbScreenNumber; } for (QXcbVirtualDesktop *virtualDesktop : qAsConst(m_virtualDesktops)) virtualDesktop->subscribeToXFixesSelectionNotify(); if (m_virtualDesktops.isEmpty()) { qFatal("QXcbConnection: no screens available"); } else { // Ensure the primary screen is first on the list if (primaryScreen) { if (qAsConst(m_screens).first() != primaryScreen) { m_screens.removeOne(primaryScreen); m_screens.prepend(primaryScreen); } } // Push the screens to QGuiApplication if (!initialized) { for (QXcbScreen *screen : qAsConst(m_screens)) { qCDebug(lcQpaScreen) << "adding" << screen << "(Primary:" << screen->isPrimary() << ")"; QWindowSystemInterface::handleScreenAdded(screen, screen->isPrimary()); } } if (!m_screens.isEmpty()) qCDebug(lcQpaScreen) << "initializeScreens: primary output is" << qAsConst(m_screens).first()->name(); } } void QXcbConnection::initializeScreensWithoutXRandR(xcb_screen_iterator_t *it, int xcbScreenNumber, QXcbScreen **primaryScreen) { // XRandR extension is missing, then create a fake/legacy screen. xcb_screen_t *xcbScreen = it->data; QXcbVirtualDesktop *virtualDesktop = new QXcbVirtualDesktop(this, xcbScreen, xcbScreenNumber); m_virtualDesktops.append(virtualDesktop); QList siblings; QXcbScreen *screen = new QXcbScreen(this, virtualDesktop, XCB_NONE, nullptr); qCDebug(lcQpaScreen) << "created fake screen" << screen; m_screens << screen; if (primaryScreenNumber() == xcbScreenNumber) { *primaryScreen = screen; (*primaryScreen)->setPrimary(true); } siblings << screen; virtualDesktop->setScreens(std::move(siblings)); } void QXcbConnection::initializeScreensFromOutput(xcb_screen_iterator_t *it, int xcbScreenNumber, QXcbScreen **primaryScreen) { // Each "screen" in xcb terminology is a virtual desktop, // potentially a collection of separate juxtaposed monitors. // But we want a separate QScreen for each output (e.g. DVI-I-1, VGA-1, etc.) // which will become virtual siblings. xcb_screen_t *xcbScreen = it->data; QXcbVirtualDesktop *virtualDesktop = new QXcbVirtualDesktop(this, xcbScreen, xcbScreenNumber); m_virtualDesktops.append(virtualDesktop); QList siblings; if (isAtLeastXRandR12()) { // RRGetScreenResourcesCurrent is fast but it may return nothing if the // configuration is not initialized wrt to the hardware. We should call // RRGetScreenResources in this case. auto resources_current = Q_XCB_REPLY(xcb_randr_get_screen_resources_current, xcb_connection(), xcbScreen->root); decltype(Q_XCB_REPLY(xcb_randr_get_screen_resources, xcb_connection(), xcbScreen->root)) resources; if (!resources_current) { qWarning("failed to get the current screen resources"); } else { xcb_timestamp_t timestamp = 0; xcb_randr_output_t *outputs = nullptr; int outputCount = xcb_randr_get_screen_resources_current_outputs_length(resources_current.get()); if (outputCount) { timestamp = resources_current->config_timestamp; outputs = xcb_randr_get_screen_resources_current_outputs(resources_current.get()); } else { resources = Q_XCB_REPLY(xcb_randr_get_screen_resources, xcb_connection(), xcbScreen->root); if (!resources) { qWarning("failed to get the screen resources"); } else { timestamp = resources->config_timestamp; outputCount = xcb_randr_get_screen_resources_outputs_length(resources.get()); outputs = xcb_randr_get_screen_resources_outputs(resources.get()); } } if (outputCount) { auto primary = Q_XCB_REPLY(xcb_randr_get_output_primary, xcb_connection(), xcbScreen->root); if (!primary) { qWarning("failed to get the primary output of the screen"); } else { for (int i = 0; i < outputCount; i++) { auto output = Q_XCB_REPLY_UNCHECKED(xcb_randr_get_output_info, xcb_connection(), outputs[i], timestamp); // Invalid, disconnected or disabled output if (!output) continue; if (output->connection != XCB_RANDR_CONNECTION_CONNECTED) { qCDebug(lcQpaScreen, "Output %s is not connected", qPrintable( QString::fromUtf8((const char*)xcb_randr_get_output_info_name(output.get()), xcb_randr_get_output_info_name_length(output.get())))); continue; } if (output->crtc == XCB_NONE) { qCDebug(lcQpaScreen, "Output %s is not enabled", qPrintable( QString::fromUtf8((const char*)xcb_randr_get_output_info_name(output.get()), xcb_randr_get_output_info_name_length(output.get())))); continue; } QXcbScreen *screen = new QXcbScreen(this, virtualDesktop, outputs[i], output.get()); siblings << screen; m_screens << screen; // There can be multiple outputs per screen, use either // the first or an exact match. An exact match isn't // always available if primary->output is XCB_NONE // or currently disconnected output. if (primaryScreenNumber() == xcbScreenNumber) { if (!(*primaryScreen) || (primary && outputs[i] == primary->output)) { if (*primaryScreen) (*primaryScreen)->setPrimary(false); *primaryScreen = screen; (*primaryScreen)->setPrimary(true); siblings.prepend(siblings.takeLast()); } } } } } } } if (siblings.isEmpty()) { // If there are no XRandR outputs or XRandR extension is missing, // then create a fake/legacy screen. QXcbScreen *screen = new QXcbScreen(this, virtualDesktop, XCB_NONE, nullptr); qCDebug(lcQpaScreen) << "created fake screen" << screen; m_screens << screen; if (primaryScreenNumber() == xcbScreenNumber) { *primaryScreen = screen; (*primaryScreen)->setPrimary(true); } siblings << screen; } virtualDesktop->setScreens(std::move(siblings)); } void QXcbConnection::initializeScreensFromMonitor(xcb_screen_iterator_t *it, int xcbScreenNumber, QXcbScreen **primaryScreen, bool initialized) { // Each "screen" in xcb terminology is a virtual desktop, // potentially a collection of separate juxtaposed monitors. // But we want a separate QScreen for each output (e.g. DVI-I-1, VGA-1, etc.) // which will become virtual siblings. xcb_screen_t *xcbScreen = it->data; QXcbVirtualDesktop *virtualDesktop = nullptr; if (initialized) virtualDesktop = virtualDesktopForNumber(xcbScreenNumber); if (!virtualDesktop) { virtualDesktop = new QXcbVirtualDesktop(this, xcbScreen, xcbScreenNumber); m_virtualDesktops.append(virtualDesktop); } QList old = virtualDesktop->m_screens; QList siblings; xcb_randr_get_monitors_cookie_t monitors_c = xcb_randr_get_monitors(xcb_connection(), xcbScreen->root, 1); xcb_randr_get_monitors_reply_t *monitors_r = xcb_randr_get_monitors_reply(xcb_connection(), monitors_c, nullptr); if (!monitors_r) { qWarning("RANDR GetMonitors failed; this should not be possible"); return; } xcb_randr_monitor_info_iterator_t monitor_iter = xcb_randr_get_monitors_monitors_iterator(monitors_r); while (monitor_iter.rem) { xcb_randr_monitor_info_t *monitor_info = monitor_iter.data; QXcbScreen *screen = nullptr; if (!initialized) { screen = new QXcbScreen(this, virtualDesktop, monitor_info, monitors_r->timestamp); } else { screen = findScreenForMonitorInfo(old, monitor_info); if (!screen) { screen = createScreen_monitor(virtualDesktop, monitor_info, monitors_r->timestamp); QHighDpiScaling::updateHighDpiScaling(); } else { updateScreen_monitor(screen, monitor_info, monitors_r->timestamp); old.removeAll(screen); } } m_screens << screen; siblings << screen; // similar logic with QXcbConnection::initializeScreensFromOutput() if (primaryScreenNumber() == xcbScreenNumber) { if (!(*primaryScreen) || monitor_info->primary) { if (*primaryScreen) (*primaryScreen)->setPrimary(false); *primaryScreen = screen; (*primaryScreen)->setPrimary(true); siblings.prepend(siblings.takeLast()); } } xcb_randr_monitor_info_next(&monitor_iter); } free(monitors_r); if (siblings.isEmpty()) { QXcbScreen *screen = nullptr; if (initialized && !old.isEmpty()) { // If there are no other screens on the same virtual desktop, // then transform the physical screen into a fake screen. screen = static_cast(old.takeFirst()); const QString nameWas = screen->name(); screen->setMonitor(nullptr, XCB_NONE); qCDebug(lcQpaScreen) << "transformed" << nameWas << "to fake" << screen; } else { // If there are no XRandR outputs or XRandR extension is missing, // then create a fake/legacy screen. screen = new QXcbScreen(this, virtualDesktop, nullptr); qCDebug(lcQpaScreen) << "create a fake screen: " << screen; } if (primaryScreenNumber() == xcbScreenNumber) { *primaryScreen = screen; (*primaryScreen)->setPrimary(true); } siblings << screen; m_screens << screen; } if (initialized) { for (QPlatformScreen *ps : old) { virtualDesktop->removeScreen(ps); qCDebug(lcQpaScreen) << "destroy screen: " << ps; QWindowSystemInterface::handleScreenRemoved(ps); } } virtualDesktop->setScreens(std::move(siblings)); }