KWin
Loading...
Searching...
No Matches
offscreenquickview.cpp
Go to the documentation of this file.
1/*
2 KWin - the KDE window manager
3 This file is part of the KDE project.
4
5 SPDX-FileCopyrightText: 2019 David Edmundson <davidedmundson@kde.org>
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8*/
9
12
13#include "logging_p.h"
14#include "opengl/glutils.h"
15
16#include <QGuiApplication>
17#include <QQmlComponent>
18#include <QQmlContext>
19#include <QQmlEngine>
20#include <QQuickItem>
21#include <QQuickRenderControl>
22#include <QQuickView>
23#include <QStyleHints>
24
25#include <QOffscreenSurface>
26#include <QOpenGLContext>
27#include <QOpenGLFramebufferObject>
28#include <QQuickGraphicsDevice>
29#include <QQuickOpenGLUtils>
30#include <QQuickRenderTarget>
31#include <QTimer>
32#include <private/qeventpoint_p.h> // for QMutableEventPoint
33
34namespace KWin
35{
36
37class Q_DECL_HIDDEN OffscreenQuickView::Private
38{
39public:
40 std::unique_ptr<QQuickWindow> m_view;
41 std::unique_ptr<QQuickRenderControl> m_renderControl;
42 std::unique_ptr<QOffscreenSurface> m_offscreenSurface;
43 std::unique_ptr<QOpenGLContext> m_glcontext;
44 std::unique_ptr<QOpenGLFramebufferObject> m_fbo;
45
46 std::unique_ptr<QTimer> m_repaintTimer;
47 QImage m_image;
48 std::unique_ptr<GLTexture> m_textureExport;
49 // if we should capture a QImage after rendering into our BO.
50 // Used for either software QtQuick rendering and nonGL kwin rendering
51 bool m_useBlit = false;
52 bool m_visible = true;
53 bool m_hasAlphaChannel = true;
54 bool m_automaticRepaint = true;
55
56 QList<QEventPoint> touchPoints;
57 QPointingDevice *touchDevice;
58
59 ulong lastMousePressTime = 0;
60 Qt::MouseButton lastMousePressButton = Qt::NoButton;
61
63
64 void updateTouchState(Qt::TouchPointState state, qint32 id, const QPointF &pos);
65};
66
67class Q_DECL_HIDDEN OffscreenQuickScene::Private
68{
69public:
71 {
72 }
73
74 std::unique_ptr<QQmlComponent> qmlComponent;
75 std::unique_ptr<QQuickItem> quickItem;
76};
77
79 : d(new OffscreenQuickView::Private)
80{
81 d->m_renderControl = std::make_unique<QQuickRenderControl>();
82
83 d->m_view = std::make_unique<QQuickWindow>(d->m_renderControl.get());
84 Q_ASSERT(d->m_view->setProperty("_KWIN_WINDOW_IS_OFFSCREEN", true) || true);
85 d->m_view->setFlags(Qt::FramelessWindowHint);
86 d->m_view->setColor(Qt::transparent);
87
88 d->m_hasAlphaChannel = alpha;
89 if (exportMode == ExportMode::Image) {
90 d->m_useBlit = true;
91 }
92
93 const bool usingGl = d->m_view->rendererInterface()->graphicsApi() == QSGRendererInterface::OpenGL;
94
95 if (!usingGl) {
96 qCDebug(LIBKWINEFFECTS) << "QtQuick Software rendering mode detected";
97 d->m_useBlit = true;
98 d->m_renderControl->initialize();
99 } else {
100 QSurfaceFormat format;
101 format.setOption(QSurfaceFormat::ResetNotification);
102 format.setDepthBufferSize(16);
103 format.setStencilBufferSize(8);
104 if (alpha) {
105 format.setAlphaBufferSize(8);
106 }
107
108 d->m_view->setFormat(format);
109
110 auto shareContext = QOpenGLContext::globalShareContext();
111 d->m_glcontext = std::make_unique<QOpenGLContext>();
112 d->m_glcontext->setShareContext(shareContext);
113 d->m_glcontext->setFormat(format);
114 d->m_glcontext->create();
115
116 // and the offscreen surface
117 d->m_offscreenSurface = std::make_unique<QOffscreenSurface>();
118 d->m_offscreenSurface->setFormat(d->m_glcontext->format());
119 d->m_offscreenSurface->create();
120
121 d->m_glcontext->makeCurrent(d->m_offscreenSurface.get());
122 d->m_view->setGraphicsDevice(QQuickGraphicsDevice::fromOpenGLContext(d->m_glcontext.get()));
123 d->m_renderControl->initialize();
124 d->m_glcontext->doneCurrent();
125
126 // On Wayland, contexts are implicitly shared and QOpenGLContext::globalShareContext() is null.
127 if (shareContext && !d->m_glcontext->shareContext()) {
128 qCDebug(LIBKWINEFFECTS) << "Failed to create a shared context, falling back to raster rendering";
129 // still render via GL, but blit for presentation
130 d->m_useBlit = true;
131 }
132 }
133
134 auto updateSize = [this]() {
135 contentItem()->setSize(d->m_view->size());
136 };
137 updateSize();
138 connect(d->m_view.get(), &QWindow::widthChanged, this, updateSize);
139 connect(d->m_view.get(), &QWindow::heightChanged, this, updateSize);
140
141 d->m_repaintTimer = std::make_unique<QTimer>();
142 d->m_repaintTimer->setSingleShot(true);
143 d->m_repaintTimer->setInterval(10);
144
145 connect(d->m_repaintTimer.get(), &QTimer::timeout, this, &OffscreenQuickView::update);
146 connect(d->m_renderControl.get(), &QQuickRenderControl::renderRequested, this, &OffscreenQuickView::handleRenderRequested);
147 connect(d->m_renderControl.get(), &QQuickRenderControl::sceneChanged, this, &OffscreenQuickView::handleSceneChanged);
148
149 d->touchDevice = new QPointingDevice(QStringLiteral("ForwardingTouchDevice"), {}, QInputDevice::DeviceType::TouchScreen, QPointingDevice::PointerType::Finger, QInputDevice::Capability::Position, 10, {});
150}
151
153{
154 disconnect(d->m_renderControl.get(), &QQuickRenderControl::renderRequested, this, &OffscreenQuickView::handleRenderRequested);
155 disconnect(d->m_renderControl.get(), &QQuickRenderControl::sceneChanged, this, &OffscreenQuickView::handleSceneChanged);
156
157 if (d->m_glcontext) {
158 // close the view whilst we have an active GL context
159 d->m_glcontext->makeCurrent(d->m_offscreenSurface.get());
160 }
161
162 d->m_view.reset();
163 d->m_renderControl.reset();
164}
165
167{
168 return d->m_automaticRepaint;
169}
170
172{
173 if (d->m_automaticRepaint != set) {
174 d->m_automaticRepaint = set;
175
176 // If there's an in-flight update, disable it.
177 if (!d->m_automaticRepaint) {
178 d->m_repaintTimer->stop();
179 }
180 }
181}
182
183void OffscreenQuickView::handleSceneChanged()
184{
185 if (d->m_automaticRepaint) {
186 d->m_repaintTimer->start();
187 }
188 Q_EMIT sceneChanged();
189}
190
191void OffscreenQuickView::handleRenderRequested()
192{
193 if (d->m_automaticRepaint) {
194 d->m_repaintTimer->start();
195 }
196 Q_EMIT renderRequested();
197}
198
200{
201 if (!d->m_visible) {
202 return;
203 }
204 if (d->m_view->size().isEmpty()) {
205 return;
206 }
207
208 bool usingGl = d->m_glcontext != nullptr;
209
210 if (usingGl) {
211 if (!d->m_glcontext->makeCurrent(d->m_offscreenSurface.get())) {
212 // probably a context loss event, kwin is about to reset all the effects anyway
213 return;
214 }
215
216 const QSize nativeSize = d->m_view->size() * d->m_view->devicePixelRatio();
217 if (!d->m_fbo || d->m_fbo->size() != nativeSize) {
218 d->m_textureExport.reset(nullptr);
219
220 QOpenGLFramebufferObjectFormat fboFormat;
221 fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
222 fboFormat.setInternalTextureFormat(GL_RGBA8);
223
224 d->m_fbo = std::make_unique<QOpenGLFramebufferObject>(nativeSize, fboFormat);
225 if (!d->m_fbo->isValid()) {
226 d->m_fbo.reset();
227 d->m_glcontext->doneCurrent();
228 return;
229 }
230 }
231
232 QQuickRenderTarget renderTarget = QQuickRenderTarget::fromOpenGLTexture(d->m_fbo->texture(), d->m_fbo->size());
233 renderTarget.setDevicePixelRatio(d->m_view->devicePixelRatio());
234
235 d->m_view->setRenderTarget(renderTarget);
236 }
237
238 d->m_renderControl->polishItems();
239 d->m_renderControl->beginFrame();
240 d->m_renderControl->sync();
241 d->m_renderControl->render();
242 d->m_renderControl->endFrame();
243
244 if (usingGl) {
245 QQuickOpenGLUtils::resetOpenGLState();
246 }
247
248 if (d->m_useBlit) {
249 if (usingGl) {
250 d->m_image = d->m_fbo->toImage();
251 d->m_image.setDevicePixelRatio(d->m_view->devicePixelRatio());
252 } else {
253 d->m_image = d->m_view->grabWindow();
254 }
255 }
256
257 if (usingGl) {
258 QOpenGLFramebufferObject::bindDefault();
259 d->m_glcontext->doneCurrent();
260 }
261 Q_EMIT repaintNeeded();
262}
263
265{
266 if (!d->m_visible) {
267 return;
268 }
269 switch (e->type()) {
270 case QEvent::MouseMove:
271 case QEvent::MouseButtonPress:
272 case QEvent::MouseButtonRelease: {
273 QMouseEvent *me = static_cast<QMouseEvent *>(e);
274 const QPoint widgetPos = d->m_view->mapFromGlobal(me->pos());
275 QMouseEvent cloneEvent(me->type(), widgetPos, me->pos(), me->button(), me->buttons(), me->modifiers());
276 cloneEvent.setAccepted(false);
277 QCoreApplication::sendEvent(d->m_view.get(), &cloneEvent);
278 e->setAccepted(cloneEvent.isAccepted());
279
280 if (e->type() == QEvent::MouseButtonPress) {
281 const ulong doubleClickInterval = static_cast<ulong>(QGuiApplication::styleHints()->mouseDoubleClickInterval());
282 const bool doubleClick = (me->timestamp() - d->lastMousePressTime < doubleClickInterval) && me->button() == d->lastMousePressButton;
283 d->lastMousePressTime = me->timestamp();
284 d->lastMousePressButton = me->button();
285 if (doubleClick) {
286 d->lastMousePressButton = Qt::NoButton;
287 QMouseEvent doubleClickEvent(QEvent::MouseButtonDblClick, me->localPos(), me->windowPos(), me->screenPos(), me->button(), me->buttons(), me->modifiers());
288 QCoreApplication::sendEvent(d->m_view.get(), &doubleClickEvent);
289 }
290 }
291
292 return;
293 }
294 case QEvent::HoverEnter:
295 case QEvent::HoverLeave:
296 case QEvent::HoverMove: {
297 QHoverEvent *he = static_cast<QHoverEvent *>(e);
298 const QPointF widgetPos = d->m_view->mapFromGlobal(he->pos());
299 const QPointF oldWidgetPos = d->m_view->mapFromGlobal(he->oldPos());
300 QHoverEvent cloneEvent(he->type(), widgetPos, oldWidgetPos, he->modifiers());
301 cloneEvent.setAccepted(false);
302 QCoreApplication::sendEvent(d->m_view.get(), &cloneEvent);
303 e->setAccepted(cloneEvent.isAccepted());
304 return;
305 }
306 case QEvent::Wheel: {
307 QWheelEvent *we = static_cast<QWheelEvent *>(e);
308 const QPointF widgetPos = d->m_view->mapFromGlobal(we->position().toPoint());
309 QWheelEvent cloneEvent(widgetPos, we->globalPosition(), we->pixelDelta(), we->angleDelta(), we->buttons(),
310 we->modifiers(), we->phase(), we->inverted());
311 cloneEvent.setAccepted(false);
312 QCoreApplication::sendEvent(d->m_view.get(), &cloneEvent);
313 e->setAccepted(cloneEvent.isAccepted());
314 return;
315 }
316 default:
317 return;
318 }
319}
320
321void OffscreenQuickView::forwardKeyEvent(QKeyEvent *keyEvent)
322{
323 if (!d->m_visible) {
324 return;
325 }
326 QCoreApplication::sendEvent(d->m_view.get(), keyEvent);
327}
328
329bool OffscreenQuickView::forwardTouchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time)
330{
331 d->updateTouchState(Qt::TouchPointPressed, id, pos);
332
333 QTouchEvent event(QEvent::TouchBegin, d->touchDevice, Qt::NoModifier, d->touchPoints);
334 event.setTimestamp(std::chrono::duration_cast<std::chrono::milliseconds>(time).count());
335 event.setAccepted(false);
336 QCoreApplication::sendEvent(d->m_view.get(), &event);
337
338 return event.isAccepted();
339}
340
341bool OffscreenQuickView::forwardTouchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time)
342{
343 d->updateTouchState(Qt::TouchPointMoved, id, pos);
344
345 QTouchEvent event(QEvent::TouchUpdate, d->touchDevice, Qt::NoModifier, d->touchPoints);
346 event.setTimestamp(std::chrono::duration_cast<std::chrono::milliseconds>(time).count());
347 event.setAccepted(false);
348 QCoreApplication::sendEvent(d->m_view.get(), &event);
349
350 return event.isAccepted();
351}
352
353bool OffscreenQuickView::forwardTouchUp(qint32 id, std::chrono::microseconds time)
354{
355 d->updateTouchState(Qt::TouchPointReleased, id, QPointF{});
356
357 QTouchEvent event(QEvent::TouchEnd, d->touchDevice, Qt::NoModifier, d->touchPoints);
358 event.setTimestamp(std::chrono::duration_cast<std::chrono::milliseconds>(time).count());
359 event.setAccepted(false);
360 QCoreApplication::sendEvent(d->m_view.get(), &event);
361
362 return event.isAccepted();
363}
364
366{
367 return d->m_view->geometry();
368}
369
371{
372 d->m_view->setOpacity(opacity);
373}
374
376{
377 return d->m_view->opacity();
378}
379
381{
382 return d->m_hasAlphaChannel;
383}
384
386{
387 return d->m_view->contentItem();
388}
389
390QQuickWindow *OffscreenQuickView::window() const
391{
392 return d->m_view.get();
393}
394
396{
397 if (d->m_visible == visible) {
398 return;
399 }
400 d->m_visible = visible;
401
402 if (visible) {
403 Q_EMIT d->m_renderControl->renderRequested();
404 } else {
405 // deferred to not change GL context
406 QTimer::singleShot(0, this, [this]() {
407 d->releaseResources();
408 });
409 }
410}
411
413{
414 return d->m_visible;
415}
416
418{
419 setVisible(true);
420}
421
423{
424 setVisible(false);
425}
426
428{
429 if (d->m_useBlit) {
430 d->m_textureExport = GLTexture::upload(d->m_image);
431 } else {
432 if (!d->m_fbo) {
433 return nullptr;
434 }
435 if (!d->m_textureExport) {
436 d->m_textureExport = GLTexture::createNonOwningWrapper(d->m_fbo->texture(), d->m_fbo->format().internalTextureFormat(), d->m_fbo->size());
437 }
438 }
439 return d->m_textureExport.get();
440}
441
443{
444 return d->m_image;
445}
446
448{
449 return d->m_view->geometry().size();
450}
451
452void OffscreenQuickView::setGeometry(const QRect &rect)
453{
454 const QRect oldGeometry = d->m_view->geometry();
455 d->m_view->setGeometry(rect);
456 // QWindow::setGeometry() won't sync output if there's no platform window.
457 d->m_view->setScreen(QGuiApplication::screenAt(rect.center()));
458 Q_EMIT geometryChanged(oldGeometry, rect);
459}
460
461void OffscreenQuickView::Private::releaseResources()
462{
463 if (m_glcontext) {
464 m_glcontext->makeCurrent(m_offscreenSurface.get());
465 m_view->releaseResources();
466 m_glcontext->doneCurrent();
467 } else {
468 m_view->releaseResources();
469 }
470}
471
472void OffscreenQuickView::Private::updateTouchState(Qt::TouchPointState state, qint32 id, const QPointF &pos)
473{
474 // Remove the points that were previously in a released state, since they
475 // are no longer relevant. Additionally, reset the state of all remaining
476 // points to Stationary so we only have one touch point with a different
477 // state.
478 touchPoints.erase(std::remove_if(touchPoints.begin(), touchPoints.end(), [](QTouchEvent::TouchPoint &point) {
479 if (point.state() == QEventPoint::Released) {
480 return true;
481 }
482 QMutableEventPoint::setState(point, QEventPoint::Stationary);
483 return false;
484 }),
485 touchPoints.end());
486
487 // QtQuick Pointer Handlers incorrectly consider a touch point with ID 0
488 // to be an invalid touch point. This has been fixed in Qt 6 but could not
489 // be fixed for Qt 5. Instead, we offset kwin's internal IDs with this
490 // offset to trick QtQuick into treating them as valid points.
491 static const qint32 idOffset = 111;
492
493 // Find the touch point that has changed. This is separate from the above
494 // loop because removing the released touch points invalidates iterators.
495 auto changed = std::find_if(touchPoints.begin(), touchPoints.end(), [id](const QTouchEvent::TouchPoint &point) {
496 return point.id() == id + idOffset;
497 });
498
499 switch (state) {
500 case Qt::TouchPointPressed: {
501 if (changed != touchPoints.end()) {
502 return;
503 }
504
505 QTouchEvent::TouchPoint point;
506 QMutableEventPoint::setState(point, QEventPoint::Pressed);
507 QMutableEventPoint::setId(point, id + idOffset);
508 QMutableEventPoint::setGlobalPosition(point, pos);
509 QMutableEventPoint::setScenePosition(point, m_view->mapFromGlobal(pos.toPoint()));
510 QMutableEventPoint::setPosition(point, m_view->mapFromGlobal(pos.toPoint()));
511
512 touchPoints.append(point);
513 } break;
514 case Qt::TouchPointMoved: {
515 if (changed == touchPoints.end()) {
516 return;
517 }
518
519 auto &point = *changed;
520 QMutableEventPoint::setGlobalLastPosition(point, point.globalPosition());
521 QMutableEventPoint::setState(point, QEventPoint::Updated);
522 QMutableEventPoint::setScenePosition(point, m_view->mapFromGlobal(pos.toPoint()));
523 QMutableEventPoint::setPosition(point, m_view->mapFromGlobal(pos.toPoint()));
524 QMutableEventPoint::setGlobalPosition(point, pos);
525 } break;
526 case Qt::TouchPointReleased: {
527 if (changed == touchPoints.end()) {
528 return;
529 }
530
531 auto &point = *changed;
532 QMutableEventPoint::setGlobalLastPosition(point, point.globalPosition());
533 QMutableEventPoint::setState(point, QEventPoint::Released);
534 } break;
535 default:
536 break;
537 }
538}
539
540OffscreenQuickScene::OffscreenQuickScene(OffscreenQuickView::ExportMode exportMode, bool alpha)
541 : OffscreenQuickView(exportMode, alpha)
542 , d(new OffscreenQuickScene::Private)
543{
544}
545
547
548void OffscreenQuickScene::setSource(const QUrl &source)
549{
550 setSource(source, QVariantMap());
551}
552
553void OffscreenQuickScene::setSource(const QUrl &source, const QVariantMap &initialProperties)
554{
555 if (!d->qmlComponent) {
556 d->qmlComponent = std::make_unique<QQmlComponent>(effects->qmlEngine());
557 }
558
559 d->qmlComponent->loadUrl(source);
560 if (d->qmlComponent->isError()) {
561 qCWarning(LIBKWINEFFECTS).nospace() << "Failed to load effect quick view " << source << ": " << d->qmlComponent->errors();
562 d->qmlComponent.reset();
563 return;
564 }
565
566 d->quickItem.reset();
567
568 std::unique_ptr<QObject> qmlObject(d->qmlComponent->createWithInitialProperties(initialProperties));
569 QQuickItem *item = qobject_cast<QQuickItem *>(qmlObject.get());
570 if (!item) {
571 qCWarning(LIBKWINEFFECTS) << "Root object of effect quick view" << source << "is not a QQuickItem";
572 return;
573 }
574
575 qmlObject.release();
576 d->quickItem.reset(item);
577
578 item->setParentItem(contentItem());
579
580 auto updateSize = [item, this]() {
581 item->setSize(contentItem()->size());
582 };
583 updateSize();
584 connect(contentItem(), &QQuickItem::widthChanged, item, updateSize);
585 connect(contentItem(), &QQuickItem::heightChanged, item, updateSize);
586}
587
589{
590 return d->quickItem.get();
591}
592
593} // namespace KWin
594
595#include "moc_offscreenquickview.cpp"
QQmlEngine * qmlEngine() const
static std::unique_ptr< GLTexture > createNonOwningWrapper(GLuint textureId, GLenum internalFormat, const QSize &size)
static std::unique_ptr< GLTexture > upload(const QImage &image)
std::unique_ptr< QQmlComponent > qmlComponent
std::unique_ptr< QQuickItem > quickItem
void setSource(const QUrl &source)
The KwinQuickView class provides a convenient API for exporting QtQuick scenes as buffers that can be...
std::unique_ptr< QQuickRenderControl > m_renderControl
QQuickWindow * window() const
OffscreenQuickView(ExportMode exportMode=ExportMode::Texture, bool alpha=true)
QQuickItem * contentItem() const
void setVisible(bool visible)
Marks the window as visible/invisible This can be used to release resources used by the window The de...
std::unique_ptr< QOpenGLFramebufferObject > m_fbo
std::unique_ptr< QQuickWindow > m_view
std::unique_ptr< QOffscreenSurface > m_offscreenSurface
bool forwardTouchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time)
std::unique_ptr< QTimer > m_repaintTimer
std::unique_ptr< GLTexture > m_textureExport
void updateTouchState(Qt::TouchPointState state, qint32 id, const QPointF &pos)
void setGeometry(const QRect &rect)
bool forwardTouchUp(qint32 id, std::chrono::microseconds time)
bool forwardTouchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time)
std::unique_ptr< QOpenGLContext > m_glcontext
QList< QEventPoint > touchPoints
void forwardKeyEvent(QKeyEvent *keyEvent)
void forwardMouseEvent(QEvent *mouseEvent)
void geometryChanged(const QRect &oldGeometry, const QRect &newGeometry)
GLenum format
Definition gltexture.cpp:49
EffectsHandler * effects