1 /*
2     SPDX-FileCopyrightText: 2011 Martin Gräßlin <mgraesslin@kde.org>
3     SPDX-FileCopyrightText: 2020 David Edmundson <davidedmundson@kde.org>
4     SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
5 
6     SPDX-License-Identifier: GPL-2.0-or-later
7 */
8 
9 #include "thumbnailitem.h"
10 #include "abstract_client.h"
11 #include "composite.h"
12 #include "effects.h"
13 #include "scene.h"
14 #include "screens.h"
15 #include "scripting_logging.h"
16 #include "virtualdesktops.h"
17 #include "workspace.h"
18 
19 #include <kwingltexture.h>
20 #include <kwinglutils.h>
21 
22 #include <QSGImageNode>
23 #include <QRunnable>
24 #include <QQuickWindow>
25 #include <QSGTextureProvider>
26 
27 namespace KWin
28 {
29 class ThumbnailTextureProvider : public QSGTextureProvider
30 {
31 public:
32     explicit ThumbnailTextureProvider(QQuickWindow *window);
33 
34     QSGTexture *texture() const override;
35     void setTexture(const QSharedPointer<GLTexture> &nativeTexture);
36     void setTexture(QSGTexture *texture);
37 
38 private:
39     QQuickWindow *m_window;
40     QSharedPointer<GLTexture> m_nativeTexture;
41     QScopedPointer<QSGTexture> m_texture;
42 };
43 
ThumbnailTextureProvider(QQuickWindow * window)44 ThumbnailTextureProvider::ThumbnailTextureProvider(QQuickWindow *window)
45     : m_window(window)
46 {
47 }
48 
texture() const49 QSGTexture *ThumbnailTextureProvider::texture() const
50 {
51     return m_texture.data();
52 }
53 
setTexture(const QSharedPointer<GLTexture> & nativeTexture)54 void ThumbnailTextureProvider::setTexture(const QSharedPointer<GLTexture> &nativeTexture)
55 {
56     if (m_nativeTexture != nativeTexture) {
57         const GLuint textureId = nativeTexture->texture();
58         m_nativeTexture = nativeTexture;
59         m_texture.reset(m_window->createTextureFromNativeObject(QQuickWindow::NativeObjectTexture,
60                                                                 &textureId, 0,
61                                                                 nativeTexture->size(),
62                                                                 QQuickWindow::TextureHasAlphaChannel));
63         m_texture->setFiltering(QSGTexture::Linear);
64         m_texture->setHorizontalWrapMode(QSGTexture::ClampToEdge);
65         m_texture->setVerticalWrapMode(QSGTexture::ClampToEdge);
66     }
67 
68     // The textureChanged signal must be emitted also if only texture data changes.
69     Q_EMIT textureChanged();
70 }
71 
setTexture(QSGTexture * texture)72 void ThumbnailTextureProvider::setTexture(QSGTexture *texture)
73 {
74     m_nativeTexture = nullptr;
75     m_texture.reset(texture);
76     Q_EMIT textureChanged();
77 }
78 
79 class ThumbnailTextureProviderCleanupJob : public QRunnable
80 {
81 public:
ThumbnailTextureProviderCleanupJob(ThumbnailTextureProvider * provider)82     explicit ThumbnailTextureProviderCleanupJob(ThumbnailTextureProvider *provider)
83         : m_provider(provider)
84     {
85     }
86 
run()87     void run() override
88     {
89         m_provider.reset();
90     }
91 
92 private:
93     QScopedPointer<ThumbnailTextureProvider> m_provider;
94 };
95 
ThumbnailItemBase(QQuickItem * parent)96 ThumbnailItemBase::ThumbnailItemBase(QQuickItem *parent)
97     : QQuickItem(parent)
98 {
99     setFlag(ItemHasContents);
100     updateFrameRenderingConnection();
101 
102     connect(Compositor::self(), &Compositor::aboutToToggleCompositing,
103             this, &ThumbnailItemBase::destroyOffscreenTexture);
104     connect(Compositor::self(), &Compositor::compositingToggled,
105             this, &ThumbnailItemBase::updateFrameRenderingConnection);
106     connect(this, &QQuickItem::windowChanged,
107             this, &ThumbnailItemBase::updateFrameRenderingConnection);
108 }
109 
~ThumbnailItemBase()110 ThumbnailItemBase::~ThumbnailItemBase()
111 {
112     destroyOffscreenTexture();
113 
114     if (m_provider) {
115         if (window()) {
116             window()->scheduleRenderJob(new ThumbnailTextureProviderCleanupJob(m_provider),
117                                         QQuickWindow::AfterSynchronizingStage);
118         } else {
119             qCCritical(KWIN_SCRIPTING) << "Can't destroy thumbnail texture provider because window is null";
120         }
121     }
122 }
123 
releaseResources()124 void ThumbnailItemBase::releaseResources()
125 {
126     if (m_provider) {
127         window()->scheduleRenderJob(new ThumbnailTextureProviderCleanupJob(m_provider),
128                                     QQuickWindow::AfterSynchronizingStage);
129         m_provider = nullptr;
130     }
131 }
132 
isTextureProvider() const133 bool ThumbnailItemBase::isTextureProvider() const
134 {
135     return true;
136 }
137 
textureProvider() const138 QSGTextureProvider *ThumbnailItemBase::textureProvider() const
139 {
140     if (QQuickItem::isTextureProvider()) {
141         return QQuickItem::textureProvider();
142     }
143     if (!m_provider) {
144         m_provider = new ThumbnailTextureProvider(window());
145     }
146     return m_provider;
147 }
148 
updateFrameRenderingConnection()149 void ThumbnailItemBase::updateFrameRenderingConnection()
150 {
151     disconnect(m_frameRenderingConnection);
152 
153     if (!Compositor::self()) {
154         return;
155     }
156     Scene *scene = Compositor::self()->scene();
157 
158     if (!window()) {
159         return;
160     }
161 
162     if (scene && scene->compositingType() == OpenGLCompositing) {
163         m_frameRenderingConnection = connect(scene, &Scene::frameRendered, this, &ThumbnailItemBase::updateOffscreenTexture);
164     }
165 }
166 
sourceSize() const167 QSize ThumbnailItemBase::sourceSize() const
168 {
169     return m_sourceSize;
170 }
171 
setSourceSize(const QSize & sourceSize)172 void ThumbnailItemBase::setSourceSize(const QSize &sourceSize)
173 {
174     if (m_sourceSize != sourceSize) {
175         m_sourceSize = sourceSize;
176         invalidateOffscreenTexture();
177         Q_EMIT sourceSizeChanged();
178     }
179 }
180 
destroyOffscreenTexture()181 void ThumbnailItemBase::destroyOffscreenTexture()
182 {
183     if (!Compositor::self()) {
184         return;
185     }
186     Scene *scene = Compositor::self()->scene();
187     if (!scene || scene->compositingType() != OpenGLCompositing) {
188         return;
189     }
190 
191     if (m_offscreenTexture) {
192         scene->makeOpenGLContextCurrent();
193         m_offscreenTarget.reset();
194         m_offscreenTexture.reset();
195 
196         if (m_acquireFence) {
197             glDeleteSync(m_acquireFence);
198             m_acquireFence = 0;
199         }
200         scene->doneOpenGLContextCurrent();
201     }
202 }
203 
updatePaintNode(QSGNode * oldNode,QQuickItem::UpdatePaintNodeData *)204 QSGNode *ThumbnailItemBase::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *)
205 {
206     if (Compositor::compositing() && !m_offscreenTexture) {
207         return oldNode;
208     }
209 
210     // Wait for rendering commands to the offscreen texture complete if there are any.
211     if (m_acquireFence) {
212         glClientWaitSync(m_acquireFence, GL_SYNC_FLUSH_COMMANDS_BIT, 5000);
213         glDeleteSync(m_acquireFence);
214         m_acquireFence = 0;
215     }
216 
217     if (!m_provider) {
218         m_provider = new ThumbnailTextureProvider(window());
219     }
220 
221     if (m_offscreenTexture) {
222         m_provider->setTexture(m_offscreenTexture);
223     } else {
224         const QImage placeholderImage = fallbackImage();
225         m_provider->setTexture(window()->createTextureFromImage(placeholderImage));
226         m_devicePixelRatio = placeholderImage.devicePixelRatio();
227     }
228 
229     QSGImageNode *node = static_cast<QSGImageNode *>(oldNode);
230     if (!node) {
231         node = window()->createImageNode();
232         node->setFiltering(QSGTexture::Linear);
233     }
234     node->setTexture(m_provider->texture());
235 
236     if (m_offscreenTexture && m_offscreenTexture->isYInverted()) {
237         node->setTextureCoordinatesTransform(QSGImageNode::MirrorVertically);
238     } else {
239         node->setTextureCoordinatesTransform(QSGImageNode::NoTransform);
240     }
241 
242     node->setRect(paintedRect());
243 
244     return node;
245 }
246 
setSaturation(qreal saturation)247 void ThumbnailItemBase::setSaturation(qreal saturation)
248 {
249     Q_UNUSED(saturation)
250     qCWarning(KWIN_SCRIPTING) << "ThumbnailItem.saturation is removed. Use a shader effect to change saturation";
251 }
252 
setBrightness(qreal brightness)253 void ThumbnailItemBase::setBrightness(qreal brightness)
254 {
255     Q_UNUSED(brightness)
256     qCWarning(KWIN_SCRIPTING) << "ThumbnailItem.brightness is removed. Use a shader effect to change brightness";
257 }
258 
setClipTo(QQuickItem * clip)259 void ThumbnailItemBase::setClipTo(QQuickItem *clip)
260 {
261     Q_UNUSED(clip)
262     qCWarning(KWIN_SCRIPTING) << "ThumbnailItem.clipTo is removed and it has no replacements";
263 }
264 
WindowThumbnailItem(QQuickItem * parent)265 WindowThumbnailItem::WindowThumbnailItem(QQuickItem *parent)
266     : ThumbnailItemBase(parent)
267 {
268 }
269 
wId() const270 QUuid WindowThumbnailItem::wId() const
271 {
272     return m_wId;
273 }
274 
setWId(const QUuid & wId)275 void WindowThumbnailItem::setWId(const QUuid &wId)
276 {
277     if (m_wId == wId) {
278         return;
279     }
280     m_wId = wId;
281     if (!m_wId.isNull()) {
282         setClient(workspace()->findAbstractClient(wId));
283     } else if (m_client) {
284         m_client = nullptr;
285         Q_EMIT clientChanged();
286     }
287     Q_EMIT wIdChanged();
288 }
289 
client() const290 AbstractClient *WindowThumbnailItem::client() const
291 {
292     return m_client;
293 }
294 
setClient(AbstractClient * client)295 void WindowThumbnailItem::setClient(AbstractClient *client)
296 {
297     if (m_client == client) {
298         return;
299     }
300     if (m_client) {
301         disconnect(m_client, &AbstractClient::frameGeometryChanged,
302                    this, &WindowThumbnailItem::invalidateOffscreenTexture);
303         disconnect(m_client, &AbstractClient::damaged,
304                    this, &WindowThumbnailItem::invalidateOffscreenTexture);
305     }
306     m_client = client;
307     if (m_client) {
308         connect(m_client, &AbstractClient::frameGeometryChanged,
309                 this, &WindowThumbnailItem::invalidateOffscreenTexture);
310         connect(m_client, &AbstractClient::damaged,
311                 this, &WindowThumbnailItem::invalidateOffscreenTexture);
312         setWId(m_client->internalId());
313     } else {
314         setWId(QUuid());
315     }
316     invalidateOffscreenTexture();
317     Q_EMIT clientChanged();
318 }
319 
fallbackImage() const320 QImage WindowThumbnailItem::fallbackImage() const
321 {
322     if (m_client) {
323         return m_client->icon().pixmap(window(), boundingRect().size().toSize()).toImage();
324     }
325     return QImage();
326 }
327 
centeredSize(const QRectF & boundingRect,const QSizeF & size)328 static QRectF centeredSize(const QRectF &boundingRect, const QSizeF &size)
329 {
330     const QSizeF scaled = size.scaled(boundingRect.size(), Qt::KeepAspectRatio);
331     const qreal x = boundingRect.x() + (boundingRect.width() - scaled.width()) / 2;
332     const qreal y = boundingRect.y() + (boundingRect.height() - scaled.height()) / 2;
333     return QRectF(QPointF(x, y), scaled);
334 }
335 
paintedRect() const336 QRectF WindowThumbnailItem::paintedRect() const
337 {
338     if (!m_client) {
339         return QRectF();
340     }
341     if (!m_offscreenTexture) {
342         const QSizeF iconSize = m_client->icon().actualSize(window(), boundingRect().size().toSize());
343         return centeredSize(boundingRect(), iconSize);
344     }
345 
346     const QRect visibleGeometry = m_client->visibleGeometry();
347     const QRect frameGeometry = m_client->frameGeometry();
348     const QSizeF scaled = QSizeF(frameGeometry.size()).scaled(boundingRect().size(), Qt::KeepAspectRatio);
349 
350     const qreal xScale = scaled.width() / frameGeometry.width();
351     const qreal yScale = scaled.height() / frameGeometry.height();
352 
353     QRectF paintedRect(boundingRect().x() + (boundingRect().width() - scaled.width()) / 2,
354                        boundingRect().y() + (boundingRect().height() - scaled.height()) / 2,
355                        visibleGeometry.width() * xScale,
356                        visibleGeometry.height() * yScale);
357 
358     paintedRect.moveLeft(paintedRect.x() + (visibleGeometry.x() - frameGeometry.x()) * xScale);
359     paintedRect.moveTop(paintedRect.y() + (visibleGeometry.y() - frameGeometry.y()) * yScale);
360 
361     return paintedRect;
362 }
363 
invalidateOffscreenTexture()364 void WindowThumbnailItem::invalidateOffscreenTexture()
365 {
366     m_dirty = true;
367     update();
368 }
369 
updateOffscreenTexture()370 void WindowThumbnailItem::updateOffscreenTexture()
371 {
372     if (m_acquireFence || !m_dirty || !m_client) {
373         return;
374     }
375     Q_ASSERT(window());
376 
377     const QRect geometry = m_client->visibleGeometry();
378     QSize textureSize = geometry.size();
379     if (sourceSize().width() > 0) {
380         textureSize.setWidth(sourceSize().width());
381     }
382     if (sourceSize().height() > 0) {
383         textureSize.setHeight(sourceSize().height());
384     }
385 
386     m_devicePixelRatio = window()->devicePixelRatio();
387     textureSize *= m_devicePixelRatio;
388 
389     if (!m_offscreenTexture || m_offscreenTexture->size() != textureSize) {
390         m_offscreenTexture.reset(new GLTexture(GL_RGBA8, textureSize));
391         m_offscreenTexture->setFilter(GL_LINEAR);
392         m_offscreenTexture->setWrapMode(GL_CLAMP_TO_EDGE);
393         m_offscreenTarget.reset(new GLRenderTarget(*m_offscreenTexture));
394     }
395 
396     GLRenderTarget::pushRenderTarget(m_offscreenTarget.data());
397     glClearColor(0.0, 0.0, 0.0, 0.0);
398     glClear(GL_COLOR_BUFFER_BIT);
399 
400     QMatrix4x4 projectionMatrix;
401     projectionMatrix.ortho(geometry.x(), geometry.x() + geometry.width(),
402                            geometry.y(), geometry.y() + geometry.height(), -1, 1);
403 
404     EffectWindowImpl *effectWindow = m_client->effectWindow();
405     WindowPaintData data(effectWindow);
406     data.setProjectionMatrix(projectionMatrix);
407 
408     // The thumbnail must be rendered using kwin's opengl context as VAOs are not
409     // shared across contexts. Unfortunately, this also introduces a latency of 1
410     // frame, which is not ideal, but it is acceptable for things such as thumbnails.
411     const int mask = Scene::PAINT_WINDOW_TRANSFORMED;
412     effectWindow->sceneWindow()->performPaint(mask, infiniteRegion(), data);
413     GLRenderTarget::popRenderTarget();
414 
415     // The fence is needed to avoid the case where qtquick renderer starts using
416     // the texture while all rendering commands to it haven't completed yet.
417     m_dirty = false;
418     m_acquireFence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
419 
420     // We know that the texture has changed, so schedule an item update.
421     update();
422 }
423 
DesktopThumbnailItem(QQuickItem * parent)424 DesktopThumbnailItem::DesktopThumbnailItem(QQuickItem *parent)
425     : ThumbnailItemBase(parent)
426 {
427 }
428 
desktop() const429 int DesktopThumbnailItem::desktop() const
430 {
431     return m_desktop;
432 }
433 
setDesktop(int desktop)434 void DesktopThumbnailItem::setDesktop(int desktop)
435 {
436     desktop = qBound<int>(1, desktop, VirtualDesktopManager::self()->count());
437     if (m_desktop != desktop) {
438         m_desktop = desktop;
439         invalidateOffscreenTexture();
440         Q_EMIT desktopChanged();
441     }
442 }
443 
fallbackImage() const444 QImage DesktopThumbnailItem::fallbackImage() const
445 {
446     return QImage();
447 }
448 
paintedRect() const449 QRectF DesktopThumbnailItem::paintedRect() const
450 {
451     return centeredSize(boundingRect(), screens()->size());
452 }
453 
invalidateOffscreenTexture()454 void DesktopThumbnailItem::invalidateOffscreenTexture()
455 {
456     update();
457 }
458 
updateOffscreenTexture()459 void DesktopThumbnailItem::updateOffscreenTexture()
460 {
461     if (m_acquireFence) {
462         return;
463     }
464 
465     const QRect geometry = screens()->geometry();
466     QSize textureSize = geometry.size();
467     if (sourceSize().width() > 0) {
468         textureSize.setWidth(sourceSize().width());
469     }
470     if (sourceSize().height() > 0) {
471         textureSize.setHeight(sourceSize().height());
472     }
473 
474     m_devicePixelRatio = window()->devicePixelRatio();
475     textureSize *= m_devicePixelRatio;
476 
477     if (!m_offscreenTexture || m_offscreenTexture->size() != textureSize) {
478         m_offscreenTexture.reset(new GLTexture(GL_RGBA8, textureSize));
479         m_offscreenTexture->setFilter(GL_LINEAR);
480         m_offscreenTexture->setWrapMode(GL_CLAMP_TO_EDGE);
481         m_offscreenTexture->setYInverted(true);
482         m_offscreenTarget.reset(new GLRenderTarget(*m_offscreenTexture));
483     }
484 
485     GLRenderTarget::pushRenderTarget(m_offscreenTarget.data());
486     glClearColor(0.0, 0.0, 0.0, 0.0);
487     glClear(GL_COLOR_BUFFER_BIT);
488 
489     QMatrix4x4 projectionMatrix;
490     projectionMatrix.ortho(geometry);
491     ScreenPaintData data(projectionMatrix);
492 
493     // The thumbnail must be rendered using kwin's opengl context as VAOs are not
494     // shared across contexts. Unfortunately, this also introduces a latency of 1
495     // frame, which is not ideal, but it is acceptable for things such as thumbnails.
496     const int mask = Scene::PAINT_WINDOW_TRANSFORMED | Scene::PAINT_SCREEN_TRANSFORMED;
497     Scene *scene = Compositor::self()->scene();
498     scene->paintDesktop(m_desktop, mask, infiniteRegion(), data);
499     GLRenderTarget::popRenderTarget();
500 
501     // The fence is needed to avoid the case where qtquick renderer starts using
502     // the texture while all rendering commands to it haven't completed yet.
503     m_acquireFence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
504 
505     // We know that the texture has changed, so schedule an item update.
506     update();
507 }
508 
509 } // namespace KWin
510