File indexing completed on 2024-11-10 04:56:45
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"