File indexing completed on 2024-05-12 05:32:12

0001 /*
0002     SPDX-FileCopyrightText: 2011 Martin Gräßlin <mgraesslin@kde.org>
0003     SPDX-FileCopyrightText: 2020 David Edmundson <davidedmundson@kde.org>
0004     SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "windowthumbnailitem.h"
0010 #include "compositor.h"
0011 #include "core/renderbackend.h"
0012 #include "core/rendertarget.h"
0013 #include "core/renderviewport.h"
0014 #include "effect/effect.h"
0015 #include "opengl/glframebuffer.h"
0016 #include "scene/itemrenderer.h"
0017 #include "scene/windowitem.h"
0018 #include "scene/workspacescene.h"
0019 #include "scripting_logging.h"
0020 #include "window.h"
0021 #include "workspace.h"
0022 
0023 #include "opengl/gltexture.h"
0024 
0025 #include <QOpenGLContext>
0026 #include <QQuickWindow>
0027 #include <QRunnable>
0028 #include <QSGImageNode>
0029 #include <QSGTextureProvider>
0030 
0031 namespace KWin
0032 {
0033 
0034 static bool useGlThumbnails()
0035 {
0036     static bool qtQuickIsSoftware = QStringList({QStringLiteral("software"), QStringLiteral("softwarecontext")}).contains(QQuickWindow::sceneGraphBackend());
0037     return Compositor::self()->backend() && Compositor::self()->backend()->compositingType() == OpenGLCompositing && !qtQuickIsSoftware;
0038 }
0039 
0040 WindowThumbnailSource::WindowThumbnailSource(QQuickWindow *view, Window *handle)
0041     : m_view(view)
0042     , m_handle(handle)
0043 {
0044     connect(handle, &Window::frameGeometryChanged, this, [this]() {
0045         m_dirty = true;
0046         Q_EMIT changed();
0047     });
0048     connect(handle, &Window::damaged, this, [this]() {
0049         m_dirty = true;
0050         Q_EMIT changed();
0051     });
0052 
0053     connect(Compositor::self()->scene(), &WorkspaceScene::preFrameRender, this, &WindowThumbnailSource::update);
0054 }
0055 
0056 WindowThumbnailSource::~WindowThumbnailSource()
0057 {
0058     if (!m_offscreenTexture) {
0059         return;
0060     }
0061     if (!QOpenGLContext::currentContext()) {
0062         Compositor::self()->scene()->makeOpenGLContextCurrent();
0063     }
0064     m_offscreenTarget.reset();
0065     m_offscreenTexture.reset();
0066 
0067     if (m_acquireFence) {
0068         glDeleteSync(m_acquireFence);
0069         m_acquireFence = 0;
0070     }
0071 }
0072 
0073 std::shared_ptr<WindowThumbnailSource> WindowThumbnailSource::getOrCreate(QQuickWindow *window, Window *handle)
0074 {
0075     using WindowThumbnailSourceKey = std::pair<QQuickWindow *, Window *>;
0076     const WindowThumbnailSourceKey key{window, handle};
0077 
0078     static std::map<WindowThumbnailSourceKey, std::weak_ptr<WindowThumbnailSource>> sources;
0079     auto &source = sources[key];
0080     if (!source.expired()) {
0081         return source.lock();
0082     }
0083 
0084     auto s = std::make_shared<WindowThumbnailSource>(window, handle);
0085     source = s;
0086 
0087     QObject::connect(handle, &Window::destroyed, [key]() {
0088         sources.erase(key);
0089     });
0090     QObject::connect(window, &QQuickWindow::destroyed, [key]() {
0091         sources.erase(key);
0092     });
0093     return s;
0094 }
0095 
0096 WindowThumbnailSource::Frame WindowThumbnailSource::acquire()
0097 {
0098     return Frame{
0099         .texture = m_offscreenTexture,
0100         .fence = std::exchange(m_acquireFence, nullptr),
0101     };
0102 }
0103 
0104 void WindowThumbnailSource::update()
0105 {
0106     if (m_acquireFence || !m_dirty || !m_handle) {
0107         return;
0108     }
0109     Q_ASSERT(m_view);
0110 
0111     const QRectF geometry = m_handle->visibleGeometry();
0112     const qreal devicePixelRatio = m_view->devicePixelRatio();
0113     const QSize textureSize = geometry.toAlignedRect().size() * devicePixelRatio;
0114 
0115     if (!m_offscreenTexture || m_offscreenTexture->size() != textureSize) {
0116         m_offscreenTexture = GLTexture::allocate(GL_RGBA8, textureSize);
0117         if (!m_offscreenTexture) {
0118             return;
0119         }
0120         m_offscreenTexture->setFilter(GL_LINEAR);
0121         m_offscreenTexture->setWrapMode(GL_CLAMP_TO_EDGE);
0122         m_offscreenTarget = std::make_unique<GLFramebuffer>(m_offscreenTexture.get());
0123     }
0124 
0125     RenderTarget offscreenRenderTarget(m_offscreenTarget.get());
0126     RenderViewport offscreenViewport(geometry, devicePixelRatio, offscreenRenderTarget);
0127     GLFramebuffer::pushFramebuffer(m_offscreenTarget.get());
0128     glClearColor(0.0, 0.0, 0.0, 0.0);
0129     glClear(GL_COLOR_BUFFER_BIT);
0130 
0131     QMatrix4x4 projectionMatrix;
0132     projectionMatrix.ortho(geometry.x() * devicePixelRatio, (geometry.x() + geometry.width()) * devicePixelRatio,
0133                            geometry.y() * devicePixelRatio, (geometry.y() + geometry.height()) * devicePixelRatio, -1, 1);
0134 
0135     WindowPaintData data;
0136     data.setProjectionMatrix(projectionMatrix);
0137 
0138     // The thumbnail must be rendered using kwin's opengl context as VAOs are not
0139     // shared across contexts. Unfortunately, this also introduces a latency of 1
0140     // frame, which is not ideal, but it is acceptable for things such as thumbnails.
0141     const int mask = Scene::PAINT_WINDOW_TRANSFORMED;
0142     Compositor::self()->scene()->renderer()->renderItem(offscreenRenderTarget, offscreenViewport, m_handle->windowItem(), mask, infiniteRegion(), data);
0143     GLFramebuffer::popFramebuffer();
0144 
0145     // The fence is needed to avoid the case where qtquick renderer starts using
0146     // the texture while all rendering commands to it haven't completed yet.
0147     m_dirty = false;
0148     m_acquireFence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
0149 
0150     Q_EMIT changed();
0151 }
0152 
0153 class ThumbnailTextureProvider : public QSGTextureProvider
0154 {
0155 public:
0156     explicit ThumbnailTextureProvider(QQuickWindow *window);
0157 
0158     QSGTexture *texture() const override;
0159     void setTexture(const std::shared_ptr<GLTexture> &nativeTexture);
0160     void setTexture(QSGTexture *texture);
0161 
0162 private:
0163     QQuickWindow *m_window;
0164     std::shared_ptr<GLTexture> m_nativeTexture;
0165     std::unique_ptr<QSGTexture> m_texture;
0166 };
0167 
0168 ThumbnailTextureProvider::ThumbnailTextureProvider(QQuickWindow *window)
0169     : m_window(window)
0170 {
0171 }
0172 
0173 QSGTexture *ThumbnailTextureProvider::texture() const
0174 {
0175     return m_texture.get();
0176 }
0177 
0178 void ThumbnailTextureProvider::setTexture(const std::shared_ptr<GLTexture> &nativeTexture)
0179 {
0180     if (m_nativeTexture != nativeTexture) {
0181         const GLuint textureId = nativeTexture->texture();
0182         m_nativeTexture = nativeTexture;
0183         m_texture.reset(QNativeInterface::QSGOpenGLTexture::fromNative(textureId, m_window,
0184                                                                        nativeTexture->size(),
0185                                                                        QQuickWindow::TextureHasAlphaChannel));
0186         m_texture->setFiltering(QSGTexture::Linear);
0187         m_texture->setHorizontalWrapMode(QSGTexture::ClampToEdge);
0188         m_texture->setVerticalWrapMode(QSGTexture::ClampToEdge);
0189     }
0190 
0191     // The textureChanged signal must be emitted also if only texture data changes.
0192     Q_EMIT textureChanged();
0193 }
0194 
0195 void ThumbnailTextureProvider::setTexture(QSGTexture *texture)
0196 {
0197     m_nativeTexture = nullptr;
0198     m_texture.reset(texture);
0199     Q_EMIT textureChanged();
0200 }
0201 
0202 class ThumbnailTextureProviderCleanupJob : public QRunnable
0203 {
0204 public:
0205     explicit ThumbnailTextureProviderCleanupJob(ThumbnailTextureProvider *provider)
0206         : m_provider(provider)
0207     {
0208     }
0209 
0210     void run() override
0211     {
0212         m_provider.reset();
0213     }
0214 
0215 private:
0216     std::unique_ptr<ThumbnailTextureProvider> m_provider;
0217 };
0218 
0219 WindowThumbnailItem::WindowThumbnailItem(QQuickItem *parent)
0220     : QQuickItem(parent)
0221 {
0222     setFlag(ItemHasContents);
0223 
0224     connect(Compositor::self(), &Compositor::aboutToToggleCompositing,
0225             this, &WindowThumbnailItem::resetSource);
0226     connect(Compositor::self(), &Compositor::compositingToggled,
0227             this, &WindowThumbnailItem::updateSource);
0228 }
0229 
0230 WindowThumbnailItem::~WindowThumbnailItem()
0231 {
0232     if (m_provider) {
0233         if (window()) {
0234             window()->scheduleRenderJob(new ThumbnailTextureProviderCleanupJob(m_provider),
0235                                         QQuickWindow::AfterSynchronizingStage);
0236         } else {
0237             qCCritical(KWIN_SCRIPTING) << "Can't destroy thumbnail texture provider because window is null";
0238         }
0239     }
0240 }
0241 
0242 void WindowThumbnailItem::releaseResources()
0243 {
0244     if (m_provider) {
0245         window()->scheduleRenderJob(new ThumbnailTextureProviderCleanupJob(m_provider),
0246                                     QQuickWindow::AfterSynchronizingStage);
0247         m_provider = nullptr;
0248     }
0249 }
0250 
0251 void WindowThumbnailItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value)
0252 {
0253     if (change == QQuickItem::ItemSceneChange) {
0254         updateSource();
0255     }
0256     QQuickItem::itemChange(change, value);
0257 }
0258 
0259 bool WindowThumbnailItem::isTextureProvider() const
0260 {
0261     return true;
0262 }
0263 
0264 QSGTextureProvider *WindowThumbnailItem::textureProvider() const
0265 {
0266     if (QQuickItem::isTextureProvider()) {
0267         return QQuickItem::textureProvider();
0268     }
0269     if (!m_provider) {
0270         m_provider = new ThumbnailTextureProvider(window());
0271     }
0272     return m_provider;
0273 }
0274 
0275 void WindowThumbnailItem::resetSource()
0276 {
0277     m_source.reset();
0278 }
0279 
0280 void WindowThumbnailItem::updateSource()
0281 {
0282     if (useGlThumbnails() && window() && m_client) {
0283         m_source = WindowThumbnailSource::getOrCreate(window(), m_client);
0284         connect(m_source.get(), &WindowThumbnailSource::changed, this, &WindowThumbnailItem::update);
0285     } else {
0286         m_source.reset();
0287     }
0288 }
0289 
0290 QSGNode *WindowThumbnailItem::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *)
0291 {
0292     if (Compositor::compositing()) {
0293         if (!m_source) {
0294             return oldNode;
0295         }
0296 
0297         auto [texture, acquireFence] = m_source->acquire();
0298         if (!texture) {
0299             return oldNode;
0300         }
0301 
0302         // Wait for rendering commands to the offscreen texture complete if there are any.
0303         if (acquireFence) {
0304             glWaitSync(acquireFence, 0, GL_TIMEOUT_IGNORED);
0305             glDeleteSync(acquireFence);
0306         }
0307 
0308         if (!m_provider) {
0309             m_provider = new ThumbnailTextureProvider(window());
0310         }
0311         m_provider->setTexture(texture);
0312     } else {
0313         if (!m_provider) {
0314             m_provider = new ThumbnailTextureProvider(window());
0315         }
0316 
0317         const QImage placeholderImage = fallbackImage();
0318         m_provider->setTexture(window()->createTextureFromImage(placeholderImage));
0319     }
0320 
0321     QSGImageNode *node = static_cast<QSGImageNode *>(oldNode);
0322     if (!node) {
0323         node = window()->createImageNode();
0324         node->setFiltering(QSGTexture::Linear);
0325     }
0326     node->setTexture(m_provider->texture());
0327     node->setTextureCoordinatesTransform(QSGImageNode::NoTransform);
0328     node->setRect(paintedRect());
0329 
0330     return node;
0331 }
0332 
0333 QUuid WindowThumbnailItem::wId() const
0334 {
0335     return m_wId;
0336 }
0337 
0338 void WindowThumbnailItem::setWId(const QUuid &wId)
0339 {
0340     if (m_wId == wId) {
0341         return;
0342     }
0343     m_wId = wId;
0344     if (!m_wId.isNull()) {
0345         setClient(workspace()->findWindow(wId));
0346     } else if (m_client) {
0347         m_client = nullptr;
0348         updateSource();
0349         updateImplicitSize();
0350         Q_EMIT clientChanged();
0351     }
0352 
0353     Q_EMIT wIdChanged();
0354 }
0355 
0356 Window *WindowThumbnailItem::client() const
0357 {
0358     return m_client;
0359 }
0360 
0361 void WindowThumbnailItem::setClient(Window *client)
0362 {
0363     if (m_client == client) {
0364         return;
0365     }
0366     if (m_client) {
0367         disconnect(m_client, &Window::frameGeometryChanged,
0368                    this, &WindowThumbnailItem::updateImplicitSize);
0369     }
0370     m_client = client;
0371     if (m_client) {
0372         connect(m_client, &Window::frameGeometryChanged,
0373                 this, &WindowThumbnailItem::updateImplicitSize);
0374         setWId(m_client->internalId());
0375     } else {
0376         setWId(QUuid());
0377     }
0378     updateSource();
0379     updateImplicitSize();
0380     Q_EMIT clientChanged();
0381 }
0382 
0383 void WindowThumbnailItem::updateImplicitSize()
0384 {
0385     QSize frameSize;
0386     if (m_client) {
0387         frameSize = m_client->frameGeometry().toAlignedRect().size();
0388     }
0389     setImplicitSize(frameSize.width(), frameSize.height());
0390 }
0391 
0392 QImage WindowThumbnailItem::fallbackImage() const
0393 {
0394     if (m_client) {
0395         return m_client->icon().pixmap(window(), boundingRect().size().toSize()).toImage();
0396     }
0397     return QImage();
0398 }
0399 
0400 static QRectF centeredSize(const QRectF &boundingRect, const QSizeF &size)
0401 {
0402     const QSizeF scaled = size.scaled(boundingRect.size(), Qt::KeepAspectRatio);
0403     const qreal x = boundingRect.x() + (boundingRect.width() - scaled.width()) / 2;
0404     const qreal y = boundingRect.y() + (boundingRect.height() - scaled.height()) / 2;
0405     return QRectF(QPointF(x, y), scaled);
0406 }
0407 
0408 QRectF WindowThumbnailItem::paintedRect() const
0409 {
0410     if (!m_client) {
0411         return QRectF();
0412     }
0413     if (!Compositor::compositing()) {
0414         const QSizeF iconSize = m_client->icon().actualSize(window(), boundingRect().size().toSize());
0415         return centeredSize(boundingRect(), iconSize);
0416     }
0417 
0418     const QRectF visibleGeometry = m_client->visibleGeometry();
0419     const QRectF frameGeometry = m_client->frameGeometry();
0420     const QSizeF scaled = QSizeF(frameGeometry.size()).scaled(boundingRect().size(), Qt::KeepAspectRatio);
0421 
0422     const qreal xScale = scaled.width() / frameGeometry.width();
0423     const qreal yScale = scaled.height() / frameGeometry.height();
0424 
0425     QRectF paintedRect(boundingRect().x() + (boundingRect().width() - scaled.width()) / 2,
0426                        boundingRect().y() + (boundingRect().height() - scaled.height()) / 2,
0427                        visibleGeometry.width() * xScale,
0428                        visibleGeometry.height() * yScale);
0429 
0430     paintedRect.moveLeft(paintedRect.x() + (visibleGeometry.x() - frameGeometry.x()) * xScale);
0431     paintedRect.moveTop(paintedRect.y() + (visibleGeometry.y() - frameGeometry.y()) * yScale);
0432 
0433     return paintedRect;
0434 }
0435 
0436 } // namespace KWin
0437 
0438 #include "moc_windowthumbnailitem.cpp"