File indexing completed on 2024-05-12 05:31:30

0001 /*
0002     KWin - the KDE window manager
0003     This file is part of the KDE project.
0004 
0005     SPDX-FileCopyrightText: 2019 David Edmundson <davidedmundson@kde.org>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include "effect/offscreenquickview.h"
0011 #include "effect/effecthandler.h"
0012 
0013 #include "logging_p.h"
0014 #include "opengl/glutils.h"
0015 
0016 #include <QGuiApplication>
0017 #include <QQmlComponent>
0018 #include <QQmlContext>
0019 #include <QQmlEngine>
0020 #include <QQuickItem>
0021 #include <QQuickRenderControl>
0022 #include <QQuickView>
0023 #include <QStyleHints>
0024 
0025 #include <QOffscreenSurface>
0026 #include <QOpenGLContext>
0027 #include <QOpenGLFramebufferObject>
0028 #include <QQuickGraphicsDevice>
0029 #include <QQuickOpenGLUtils>
0030 #include <QQuickRenderTarget>
0031 #include <QTimer>
0032 #include <private/qeventpoint_p.h> // for QMutableEventPoint
0033 
0034 namespace KWin
0035 {
0036 
0037 class Q_DECL_HIDDEN OffscreenQuickView::Private
0038 {
0039 public:
0040     std::unique_ptr<QQuickWindow> m_view;
0041     std::unique_ptr<QQuickRenderControl> m_renderControl;
0042     std::unique_ptr<QOffscreenSurface> m_offscreenSurface;
0043     std::unique_ptr<QOpenGLContext> m_glcontext;
0044     std::unique_ptr<QOpenGLFramebufferObject> m_fbo;
0045 
0046     std::unique_ptr<QTimer> m_repaintTimer;
0047     QImage m_image;
0048     std::unique_ptr<GLTexture> m_textureExport;
0049     // if we should capture a QImage after rendering into our BO.
0050     // Used for either software QtQuick rendering and nonGL kwin rendering
0051     bool m_useBlit = false;
0052     bool m_visible = true;
0053     bool m_hasAlphaChannel = true;
0054     bool m_automaticRepaint = true;
0055 
0056     QList<QEventPoint> touchPoints;
0057     QPointingDevice *touchDevice;
0058 
0059     ulong lastMousePressTime = 0;
0060     Qt::MouseButton lastMousePressButton = Qt::NoButton;
0061 
0062     void releaseResources();
0063 
0064     void updateTouchState(Qt::TouchPointState state, qint32 id, const QPointF &pos);
0065 };
0066 
0067 class Q_DECL_HIDDEN OffscreenQuickScene::Private
0068 {
0069 public:
0070     Private()
0071     {
0072     }
0073 
0074     std::unique_ptr<QQmlComponent> qmlComponent;
0075     std::unique_ptr<QQuickItem> quickItem;
0076 };
0077 
0078 OffscreenQuickView::OffscreenQuickView(ExportMode exportMode, bool alpha)
0079     : d(new OffscreenQuickView::Private)
0080 {
0081     d->m_renderControl = std::make_unique<QQuickRenderControl>();
0082 
0083     d->m_view = std::make_unique<QQuickWindow>(d->m_renderControl.get());
0084     Q_ASSERT(d->m_view->setProperty("_KWIN_WINDOW_IS_OFFSCREEN", true) || true);
0085     d->m_view->setFlags(Qt::FramelessWindowHint);
0086     d->m_view->setColor(Qt::transparent);
0087 
0088     d->m_hasAlphaChannel = alpha;
0089     if (exportMode == ExportMode::Image) {
0090         d->m_useBlit = true;
0091     }
0092 
0093     const bool usingGl = d->m_view->rendererInterface()->graphicsApi() == QSGRendererInterface::OpenGL;
0094 
0095     if (!usingGl) {
0096         qCDebug(LIBKWINEFFECTS) << "QtQuick Software rendering mode detected";
0097         d->m_useBlit = true;
0098         d->m_renderControl->initialize();
0099     } else {
0100         QSurfaceFormat format;
0101         format.setOption(QSurfaceFormat::ResetNotification);
0102         format.setDepthBufferSize(16);
0103         format.setStencilBufferSize(8);
0104         if (alpha) {
0105             format.setAlphaBufferSize(8);
0106         }
0107 
0108         d->m_view->setFormat(format);
0109 
0110         auto shareContext = QOpenGLContext::globalShareContext();
0111         d->m_glcontext = std::make_unique<QOpenGLContext>();
0112         d->m_glcontext->setShareContext(shareContext);
0113         d->m_glcontext->setFormat(format);
0114         d->m_glcontext->create();
0115 
0116         // and the offscreen surface
0117         d->m_offscreenSurface = std::make_unique<QOffscreenSurface>();
0118         d->m_offscreenSurface->setFormat(d->m_glcontext->format());
0119         d->m_offscreenSurface->create();
0120 
0121         d->m_glcontext->makeCurrent(d->m_offscreenSurface.get());
0122         d->m_view->setGraphicsDevice(QQuickGraphicsDevice::fromOpenGLContext(d->m_glcontext.get()));
0123         d->m_renderControl->initialize();
0124         d->m_glcontext->doneCurrent();
0125 
0126         // On Wayland, contexts are implicitly shared and QOpenGLContext::globalShareContext() is null.
0127         if (shareContext && !d->m_glcontext->shareContext()) {
0128             qCDebug(LIBKWINEFFECTS) << "Failed to create a shared context, falling back to raster rendering";
0129             // still render via GL, but blit for presentation
0130             d->m_useBlit = true;
0131         }
0132     }
0133 
0134     auto updateSize = [this]() {
0135         contentItem()->setSize(d->m_view->size());
0136     };
0137     updateSize();
0138     connect(d->m_view.get(), &QWindow::widthChanged, this, updateSize);
0139     connect(d->m_view.get(), &QWindow::heightChanged, this, updateSize);
0140 
0141     d->m_repaintTimer = std::make_unique<QTimer>();
0142     d->m_repaintTimer->setSingleShot(true);
0143     d->m_repaintTimer->setInterval(10);
0144 
0145     connect(d->m_repaintTimer.get(), &QTimer::timeout, this, &OffscreenQuickView::update);
0146     connect(d->m_renderControl.get(), &QQuickRenderControl::renderRequested, this, &OffscreenQuickView::handleRenderRequested);
0147     connect(d->m_renderControl.get(), &QQuickRenderControl::sceneChanged, this, &OffscreenQuickView::handleSceneChanged);
0148 
0149     d->touchDevice = new QPointingDevice(QStringLiteral("ForwardingTouchDevice"), {}, QInputDevice::DeviceType::TouchScreen, QPointingDevice::PointerType::Finger, QInputDevice::Capability::Position, 10, {});
0150 }
0151 
0152 OffscreenQuickView::~OffscreenQuickView()
0153 {
0154     disconnect(d->m_renderControl.get(), &QQuickRenderControl::renderRequested, this, &OffscreenQuickView::handleRenderRequested);
0155     disconnect(d->m_renderControl.get(), &QQuickRenderControl::sceneChanged, this, &OffscreenQuickView::handleSceneChanged);
0156 
0157     if (d->m_glcontext) {
0158         // close the view whilst we have an active GL context
0159         d->m_glcontext->makeCurrent(d->m_offscreenSurface.get());
0160     }
0161 
0162     d->m_view.reset();
0163     d->m_renderControl.reset();
0164 }
0165 
0166 bool OffscreenQuickView::automaticRepaint() const
0167 {
0168     return d->m_automaticRepaint;
0169 }
0170 
0171 void OffscreenQuickView::setAutomaticRepaint(bool set)
0172 {
0173     if (d->m_automaticRepaint != set) {
0174         d->m_automaticRepaint = set;
0175 
0176         // If there's an in-flight update, disable it.
0177         if (!d->m_automaticRepaint) {
0178             d->m_repaintTimer->stop();
0179         }
0180     }
0181 }
0182 
0183 void OffscreenQuickView::handleSceneChanged()
0184 {
0185     if (d->m_automaticRepaint) {
0186         d->m_repaintTimer->start();
0187     }
0188     Q_EMIT sceneChanged();
0189 }
0190 
0191 void OffscreenQuickView::handleRenderRequested()
0192 {
0193     if (d->m_automaticRepaint) {
0194         d->m_repaintTimer->start();
0195     }
0196     Q_EMIT renderRequested();
0197 }
0198 
0199 void OffscreenQuickView::update()
0200 {
0201     if (!d->m_visible) {
0202         return;
0203     }
0204     if (d->m_view->size().isEmpty()) {
0205         return;
0206     }
0207 
0208     bool usingGl = d->m_glcontext != nullptr;
0209 
0210     if (usingGl) {
0211         if (!d->m_glcontext->makeCurrent(d->m_offscreenSurface.get())) {
0212             // probably a context loss event, kwin is about to reset all the effects anyway
0213             return;
0214         }
0215 
0216         const QSize nativeSize = d->m_view->size() * d->m_view->devicePixelRatio();
0217         if (!d->m_fbo || d->m_fbo->size() != nativeSize) {
0218             d->m_textureExport.reset(nullptr);
0219 
0220             QOpenGLFramebufferObjectFormat fboFormat;
0221             fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
0222             fboFormat.setInternalTextureFormat(GL_RGBA8);
0223 
0224             d->m_fbo = std::make_unique<QOpenGLFramebufferObject>(nativeSize, fboFormat);
0225             if (!d->m_fbo->isValid()) {
0226                 d->m_fbo.reset();
0227                 d->m_glcontext->doneCurrent();
0228                 return;
0229             }
0230         }
0231 
0232         QQuickRenderTarget renderTarget = QQuickRenderTarget::fromOpenGLTexture(d->m_fbo->texture(), d->m_fbo->size());
0233         renderTarget.setDevicePixelRatio(d->m_view->devicePixelRatio());
0234 
0235         d->m_view->setRenderTarget(renderTarget);
0236     }
0237 
0238     d->m_renderControl->polishItems();
0239     d->m_renderControl->beginFrame();
0240     d->m_renderControl->sync();
0241     d->m_renderControl->render();
0242     d->m_renderControl->endFrame();
0243 
0244     if (usingGl) {
0245         QQuickOpenGLUtils::resetOpenGLState();
0246     }
0247 
0248     if (d->m_useBlit) {
0249         if (usingGl) {
0250             d->m_image = d->m_fbo->toImage();
0251             d->m_image.setDevicePixelRatio(d->m_view->devicePixelRatio());
0252         } else {
0253             d->m_image = d->m_view->grabWindow();
0254         }
0255     }
0256 
0257     if (usingGl) {
0258         QOpenGLFramebufferObject::bindDefault();
0259         d->m_glcontext->doneCurrent();
0260     }
0261     Q_EMIT repaintNeeded();
0262 }
0263 
0264 void OffscreenQuickView::forwardMouseEvent(QEvent *e)
0265 {
0266     if (!d->m_visible) {
0267         return;
0268     }
0269     switch (e->type()) {
0270     case QEvent::MouseMove:
0271     case QEvent::MouseButtonPress:
0272     case QEvent::MouseButtonRelease: {
0273         QMouseEvent *me = static_cast<QMouseEvent *>(e);
0274         const QPoint widgetPos = d->m_view->mapFromGlobal(me->pos());
0275         QMouseEvent cloneEvent(me->type(), widgetPos, me->pos(), me->button(), me->buttons(), me->modifiers());
0276         cloneEvent.setAccepted(false);
0277         QCoreApplication::sendEvent(d->m_view.get(), &cloneEvent);
0278         e->setAccepted(cloneEvent.isAccepted());
0279 
0280         if (e->type() == QEvent::MouseButtonPress) {
0281             const ulong doubleClickInterval = static_cast<ulong>(QGuiApplication::styleHints()->mouseDoubleClickInterval());
0282             const bool doubleClick = (me->timestamp() - d->lastMousePressTime < doubleClickInterval) && me->button() == d->lastMousePressButton;
0283             d->lastMousePressTime = me->timestamp();
0284             d->lastMousePressButton = me->button();
0285             if (doubleClick) {
0286                 d->lastMousePressButton = Qt::NoButton;
0287                 QMouseEvent doubleClickEvent(QEvent::MouseButtonDblClick, me->localPos(), me->windowPos(), me->screenPos(), me->button(), me->buttons(), me->modifiers());
0288                 QCoreApplication::sendEvent(d->m_view.get(), &doubleClickEvent);
0289             }
0290         }
0291 
0292         return;
0293     }
0294     case QEvent::HoverEnter:
0295     case QEvent::HoverLeave:
0296     case QEvent::HoverMove: {
0297         QHoverEvent *he = static_cast<QHoverEvent *>(e);
0298         const QPointF widgetPos = d->m_view->mapFromGlobal(he->pos());
0299         const QPointF oldWidgetPos = d->m_view->mapFromGlobal(he->oldPos());
0300         QHoverEvent cloneEvent(he->type(), widgetPos, oldWidgetPos, he->modifiers());
0301         cloneEvent.setAccepted(false);
0302         QCoreApplication::sendEvent(d->m_view.get(), &cloneEvent);
0303         e->setAccepted(cloneEvent.isAccepted());
0304         return;
0305     }
0306     case QEvent::Wheel: {
0307         QWheelEvent *we = static_cast<QWheelEvent *>(e);
0308         const QPointF widgetPos = d->m_view->mapFromGlobal(we->position().toPoint());
0309         QWheelEvent cloneEvent(widgetPos, we->globalPosition(), we->pixelDelta(), we->angleDelta(), we->buttons(),
0310                                we->modifiers(), we->phase(), we->inverted());
0311         cloneEvent.setAccepted(false);
0312         QCoreApplication::sendEvent(d->m_view.get(), &cloneEvent);
0313         e->setAccepted(cloneEvent.isAccepted());
0314         return;
0315     }
0316     default:
0317         return;
0318     }
0319 }
0320 
0321 void OffscreenQuickView::forwardKeyEvent(QKeyEvent *keyEvent)
0322 {
0323     if (!d->m_visible) {
0324         return;
0325     }
0326     QCoreApplication::sendEvent(d->m_view.get(), keyEvent);
0327 }
0328 
0329 bool OffscreenQuickView::forwardTouchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time)
0330 {
0331     d->updateTouchState(Qt::TouchPointPressed, id, pos);
0332 
0333     QTouchEvent event(QEvent::TouchBegin, d->touchDevice, Qt::NoModifier, d->touchPoints);
0334     event.setTimestamp(std::chrono::duration_cast<std::chrono::milliseconds>(time).count());
0335     event.setAccepted(false);
0336     QCoreApplication::sendEvent(d->m_view.get(), &event);
0337 
0338     return event.isAccepted();
0339 }
0340 
0341 bool OffscreenQuickView::forwardTouchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time)
0342 {
0343     d->updateTouchState(Qt::TouchPointMoved, id, pos);
0344 
0345     QTouchEvent event(QEvent::TouchUpdate, d->touchDevice, Qt::NoModifier, d->touchPoints);
0346     event.setTimestamp(std::chrono::duration_cast<std::chrono::milliseconds>(time).count());
0347     event.setAccepted(false);
0348     QCoreApplication::sendEvent(d->m_view.get(), &event);
0349 
0350     return event.isAccepted();
0351 }
0352 
0353 bool OffscreenQuickView::forwardTouchUp(qint32 id, std::chrono::microseconds time)
0354 {
0355     d->updateTouchState(Qt::TouchPointReleased, id, QPointF{});
0356 
0357     QTouchEvent event(QEvent::TouchEnd, d->touchDevice, Qt::NoModifier, d->touchPoints);
0358     event.setTimestamp(std::chrono::duration_cast<std::chrono::milliseconds>(time).count());
0359     event.setAccepted(false);
0360     QCoreApplication::sendEvent(d->m_view.get(), &event);
0361 
0362     return event.isAccepted();
0363 }
0364 
0365 QRect OffscreenQuickView::geometry() const
0366 {
0367     return d->m_view->geometry();
0368 }
0369 
0370 void OffscreenQuickView::setOpacity(qreal opacity)
0371 {
0372     d->m_view->setOpacity(opacity);
0373 }
0374 
0375 qreal OffscreenQuickView::opacity() const
0376 {
0377     return d->m_view->opacity();
0378 }
0379 
0380 bool OffscreenQuickView::hasAlphaChannel() const
0381 {
0382     return d->m_hasAlphaChannel;
0383 }
0384 
0385 QQuickItem *OffscreenQuickView::contentItem() const
0386 {
0387     return d->m_view->contentItem();
0388 }
0389 
0390 QQuickWindow *OffscreenQuickView::window() const
0391 {
0392     return d->m_view.get();
0393 }
0394 
0395 void OffscreenQuickView::setVisible(bool visible)
0396 {
0397     if (d->m_visible == visible) {
0398         return;
0399     }
0400     d->m_visible = visible;
0401 
0402     if (visible) {
0403         Q_EMIT d->m_renderControl->renderRequested();
0404     } else {
0405         // deferred to not change GL context
0406         QTimer::singleShot(0, this, [this]() {
0407             d->releaseResources();
0408         });
0409     }
0410 }
0411 
0412 bool OffscreenQuickView::isVisible() const
0413 {
0414     return d->m_visible;
0415 }
0416 
0417 void OffscreenQuickView::show()
0418 {
0419     setVisible(true);
0420 }
0421 
0422 void OffscreenQuickView::hide()
0423 {
0424     setVisible(false);
0425 }
0426 
0427 GLTexture *OffscreenQuickView::bufferAsTexture()
0428 {
0429     if (d->m_useBlit) {
0430         d->m_textureExport = GLTexture::upload(d->m_image);
0431     } else {
0432         if (!d->m_fbo) {
0433             return nullptr;
0434         }
0435         if (!d->m_textureExport) {
0436             d->m_textureExport = GLTexture::createNonOwningWrapper(d->m_fbo->texture(), d->m_fbo->format().internalTextureFormat(), d->m_fbo->size());
0437         }
0438     }
0439     return d->m_textureExport.get();
0440 }
0441 
0442 QImage OffscreenQuickView::bufferAsImage() const
0443 {
0444     return d->m_image;
0445 }
0446 
0447 QSize OffscreenQuickView::size() const
0448 {
0449     return d->m_view->geometry().size();
0450 }
0451 
0452 void OffscreenQuickView::setGeometry(const QRect &rect)
0453 {
0454     const QRect oldGeometry = d->m_view->geometry();
0455     d->m_view->setGeometry(rect);
0456     // QWindow::setGeometry() won't sync output if there's no platform window.
0457     d->m_view->setScreen(QGuiApplication::screenAt(rect.center()));
0458     Q_EMIT geometryChanged(oldGeometry, rect);
0459 }
0460 
0461 void OffscreenQuickView::Private::releaseResources()
0462 {
0463     if (m_glcontext) {
0464         m_glcontext->makeCurrent(m_offscreenSurface.get());
0465         m_view->releaseResources();
0466         m_glcontext->doneCurrent();
0467     } else {
0468         m_view->releaseResources();
0469     }
0470 }
0471 
0472 void OffscreenQuickView::Private::updateTouchState(Qt::TouchPointState state, qint32 id, const QPointF &pos)
0473 {
0474     // Remove the points that were previously in a released state, since they
0475     // are no longer relevant. Additionally, reset the state of all remaining
0476     // points to Stationary so we only have one touch point with a different
0477     // state.
0478     touchPoints.erase(std::remove_if(touchPoints.begin(), touchPoints.end(), [](QTouchEvent::TouchPoint &point) {
0479                           if (point.state() == QEventPoint::Released) {
0480                               return true;
0481                           }
0482                           QMutableEventPoint::setState(point, QEventPoint::Stationary);
0483                           return false;
0484                       }),
0485                       touchPoints.end());
0486 
0487     // QtQuick Pointer Handlers incorrectly consider a touch point with ID 0
0488     // to be an invalid touch point. This has been fixed in Qt 6 but could not
0489     // be fixed for Qt 5. Instead, we offset kwin's internal IDs with this
0490     // offset to trick QtQuick into treating them as valid points.
0491     static const qint32 idOffset = 111;
0492 
0493     // Find the touch point that has changed. This is separate from the above
0494     // loop because removing the released touch points invalidates iterators.
0495     auto changed = std::find_if(touchPoints.begin(), touchPoints.end(), [id](const QTouchEvent::TouchPoint &point) {
0496         return point.id() == id + idOffset;
0497     });
0498 
0499     switch (state) {
0500     case Qt::TouchPointPressed: {
0501         if (changed != touchPoints.end()) {
0502             return;
0503         }
0504 
0505         QTouchEvent::TouchPoint point;
0506         QMutableEventPoint::setState(point, QEventPoint::Pressed);
0507         QMutableEventPoint::setId(point, id + idOffset);
0508         QMutableEventPoint::setGlobalPosition(point, pos);
0509         QMutableEventPoint::setScenePosition(point, m_view->mapFromGlobal(pos.toPoint()));
0510         QMutableEventPoint::setPosition(point, m_view->mapFromGlobal(pos.toPoint()));
0511 
0512         touchPoints.append(point);
0513     } break;
0514     case Qt::TouchPointMoved: {
0515         if (changed == touchPoints.end()) {
0516             return;
0517         }
0518 
0519         auto &point = *changed;
0520         QMutableEventPoint::setGlobalLastPosition(point, point.globalPosition());
0521         QMutableEventPoint::setState(point, QEventPoint::Updated);
0522         QMutableEventPoint::setScenePosition(point, m_view->mapFromGlobal(pos.toPoint()));
0523         QMutableEventPoint::setPosition(point, m_view->mapFromGlobal(pos.toPoint()));
0524         QMutableEventPoint::setGlobalPosition(point, pos);
0525     } break;
0526     case Qt::TouchPointReleased: {
0527         if (changed == touchPoints.end()) {
0528             return;
0529         }
0530 
0531         auto &point = *changed;
0532         QMutableEventPoint::setGlobalLastPosition(point, point.globalPosition());
0533         QMutableEventPoint::setState(point, QEventPoint::Released);
0534     } break;
0535     default:
0536         break;
0537     }
0538 }
0539 
0540 OffscreenQuickScene::OffscreenQuickScene(OffscreenQuickView::ExportMode exportMode, bool alpha)
0541     : OffscreenQuickView(exportMode, alpha)
0542     , d(new OffscreenQuickScene::Private)
0543 {
0544 }
0545 
0546 OffscreenQuickScene::~OffscreenQuickScene() = default;
0547 
0548 void OffscreenQuickScene::setSource(const QUrl &source)
0549 {
0550     setSource(source, QVariantMap());
0551 }
0552 
0553 void OffscreenQuickScene::setSource(const QUrl &source, const QVariantMap &initialProperties)
0554 {
0555     if (!d->qmlComponent) {
0556         d->qmlComponent = std::make_unique<QQmlComponent>(effects->qmlEngine());
0557     }
0558 
0559     d->qmlComponent->loadUrl(source);
0560     if (d->qmlComponent->isError()) {
0561         qCWarning(LIBKWINEFFECTS).nospace() << "Failed to load effect quick view " << source << ": " << d->qmlComponent->errors();
0562         d->qmlComponent.reset();
0563         return;
0564     }
0565 
0566     d->quickItem.reset();
0567 
0568     std::unique_ptr<QObject> qmlObject(d->qmlComponent->createWithInitialProperties(initialProperties));
0569     QQuickItem *item = qobject_cast<QQuickItem *>(qmlObject.get());
0570     if (!item) {
0571         qCWarning(LIBKWINEFFECTS) << "Root object of effect quick view" << source << "is not a QQuickItem";
0572         return;
0573     }
0574 
0575     qmlObject.release();
0576     d->quickItem.reset(item);
0577 
0578     item->setParentItem(contentItem());
0579 
0580     auto updateSize = [item, this]() {
0581         item->setSize(contentItem()->size());
0582     };
0583     updateSize();
0584     connect(contentItem(), &QQuickItem::widthChanged, item, updateSize);
0585     connect(contentItem(), &QQuickItem::heightChanged, item, updateSize);
0586 }
0587 
0588 QQuickItem *OffscreenQuickScene::rootItem() const
0589 {
0590     return d->quickItem.get();
0591 }
0592 
0593 } // namespace KWin
0594 
0595 #include "moc_offscreenquickview.cpp"