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