[vlc-commits] [Git][videolan/vlc][master] qt: optimize RoundImage generation

Steve Lhomme (@robUx4) gitlab at videolan.org
Wed May 31 10:00:18 UTC 2023



Steve Lhomme pushed to branch master at VideoLAN / VLC


Commits:
e1b09a5b by Pierre Lamot at 2023-05-31T09:35:34+00:00
qt: optimize RoundImage generation

RoundImage are generated asynchronously, so if a view request again the same
image before it generation ends and reach the cache, a new request is recreated
and the image is re-generated. This patch optimize the image generation by
exposing the request as a shared objects that will be shared amongst the
different image that makes the same request.

- - - - -


5 changed files:

- modules/gui/qt/Makefile.am
- modules/gui/qt/meson.build
- modules/gui/qt/widgets/native/roundimage.cpp
- modules/gui/qt/widgets/native/roundimage.hpp
- + modules/gui/qt/widgets/native/roundimage_p.hpp


Changes:

=====================================
modules/gui/qt/Makefile.am
=====================================
@@ -341,7 +341,7 @@ libqt_plugin_la_SOURCES = \
 	gui/qt/widgets/native/mlfolderseditor.hpp \
 	gui/qt/widgets/native/qvlcframe.cpp \
 	gui/qt/widgets/native/qvlcframe.hpp \
-	gui/qt/widgets/native/roundimage.cpp gui/qt/widgets/native/roundimage.hpp \
+	gui/qt/widgets/native/roundimage.cpp gui/qt/widgets/native/roundimage.hpp gui/qt/widgets/native/roundimage_p.hpp \
 	gui/qt/widgets/native/searchlineedit.cpp gui/qt/widgets/native/searchlineedit.hpp \
 	gui/qt/widgets/native/viewblockingrectangle.cpp gui/qt/widgets/native/viewblockingrectangle.hpp
 if HAVE_WIN32
@@ -521,6 +521,7 @@ nodist_libqt_plugin_la_SOURCES = \
 	gui/qt/widgets/native/navigation_attached.moc.cpp \
 	gui/qt/widgets/native/mlfolderseditor.moc.cpp \
 	gui/qt/widgets/native/roundimage.moc.cpp \
+	gui/qt/widgets/native/roundimage_p.moc.cpp \
 	gui/qt/widgets/native/searchlineedit.moc.cpp \
 	gui/qt/widgets/native/viewblockingrectangle.moc.cpp
 


=====================================
modules/gui/qt/meson.build
=====================================
@@ -145,6 +145,7 @@ moc_headers = files(
     'widgets/native/navigation_attached.hpp',
     'widgets/native/mlfolderseditor.hpp',
     'widgets/native/roundimage.hpp',
+    'widgets/native/roundimage_p.hpp',
     'widgets/native/searchlineedit.hpp',
     'widgets/native/viewblockingrectangle.hpp',
 )


=====================================
modules/gui/qt/widgets/native/roundimage.cpp
=====================================
@@ -20,25 +20,25 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
  *****************************************************************************/
 
-
 #ifdef HAVE_CONFIG_H
 # include "config.h"
 #endif
 
+#include "qt.hpp"
+
 #include "roundimage.hpp"
+#include "roundimage_p.hpp"
 #include "util/asynctask.hpp"
 #include "util/qsgroundedrectangularimagenode.hpp"
 
 #include <qhashfunctions.h>
 
 #include <QBuffer>
-#include <QCache>
 #include <QFile>
 #include <QImage>
 #include <QImageReader>
 #include <QPainter>
 #include <QPainterPath>
-#include <QQuickImageProvider>
 #include <QQuickWindow>
 #include <QGuiApplication>
 #include <QSGImageNode>
@@ -51,33 +51,24 @@
 #include <QNetworkRequest>
 #endif
 
-namespace
-{
-    struct ImageCacheKey
-    {
-        QUrl url;
-        QSize size;
-        qreal radius;
-    };
-
-    bool operator ==(const ImageCacheKey &lhs, const ImageCacheKey &rhs)
-    {
-        return lhs.radius == rhs.radius && lhs.size == rhs.size && lhs.url == rhs.url;
-    }
 
-    uint qHash(const ImageCacheKey &key, uint seed)
-    {
-        QtPrivate::QHashCombine hash;
-        seed = hash(seed, key.url);
-        seed = hash(seed, key.size.width());
-        seed = hash(seed, key.size.height());
-        seed = hash(seed, key.radius);
-        return seed;
-    }
+bool operator ==(const ImageCacheKey &lhs, const ImageCacheKey &rhs)
+{
+    return lhs.radius == rhs.radius && lhs.size == rhs.size && lhs.url == rhs.url;
+}
 
-    // images are cached (result of RoundImageGenerator) with the cost calculated from QImage::sizeInBytes
-    QCache<ImageCacheKey, QImage> imageCache(32 * 1024 * 1024); // 32 MiB
+uint qHash(const ImageCacheKey &key, uint seed)
+{
+    QtPrivate::QHashCombine hash;
+    seed = hash(seed, key.url);
+    seed = hash(seed, key.size.width());
+    seed = hash(seed, key.size.height());
+    seed = hash(seed, key.radius);
+    return seed;
+}
 
+namespace
+{
     QImage prepareImage(const QSize &targetSize, const qreal radius, const QImage sourceImage)
     {
         if (qFuzzyIsNull(radius))
@@ -319,44 +310,132 @@ namespace
         TaskHandle<RoundImageGenerator> generator;
     };
 
-    QQuickImageResponse *getAsyncImageResponse(const QUrl &url, const QSize &requestedSize, const qreal radius, QQmlEngine *engine)
+// global RoundImage cache
+RoundImageCache g_imageCache = {};
+
+}
+
+// RoundImageCache
+
+RoundImageCache::RoundImageCache()
+    : m_imageCache(32 * 1024 * 1024) // 32 MiB
+{}
+
+std::shared_ptr<RoundImageRequest> RoundImageCache::requestImage(const ImageCacheKey& key, qreal dpr, QQmlEngine *engine)
+{
+    //do we already have a pending request?
+    auto it = m_pendingRequests.find(key);
+    if (it != m_pendingRequests.end())
     {
-        if (url.scheme() == QStringLiteral("image"))
-        {
-            auto provider = engine->imageProvider(url.host());
-            if (!provider)
-                return nullptr;
+        std::shared_ptr<RoundImageRequest> request = it->second.lock();
+        if (request)
+            return request;
+    }
+    auto request = std::make_shared<RoundImageRequest>(key, dpr, engine);
+    if (request->getStatus() == RoundImage::Status::Error)
+        return {};
+    m_pendingRequests[key] = request;
+    return request;
+}
 
-            assert(provider->imageType() == QQmlImageProviderBase::ImageResponse);
+void RoundImageCache::removeRequest(const ImageCacheKey& key)
+{
+    m_pendingRequests.erase(key);
+}
 
-            const auto imageId = url.toString(QUrl::RemoveScheme | QUrl::RemoveAuthority).mid(1);
+// RoundImageRequest
 
-            if (provider->imageType() == QQmlImageProviderBase::ImageResponse)
-            {
-                auto rawImageResponse = static_cast<QQuickAsyncImageProvider *>(provider)->requestImageResponse(imageId, requestedSize);
-                return new ImageResponseRadiusAdaptor(rawImageResponse, radius);
-            }
+RoundImageRequest::RoundImageRequest(const ImageCacheKey& key, qreal dpr, QQmlEngine *engine)
+    : m_key(key)
+    , m_dpr(dpr)
+{
+    m_imageResponse = getAsyncImageResponse(key.url, key.size, key.radius, engine);
+    if (m_imageResponse)
+        connect(m_imageResponse, &QQuickImageResponse::finished, this, &RoundImageRequest::handleImageResponseFinished);
+    else
+        m_status = RoundImage::Error;
+}
+
+RoundImageRequest::~RoundImageRequest()
+{
+    if (m_imageResponse)
+    {
+        if (m_cancelOnDelete)
+            m_imageResponse->cancel();
+
+        m_imageResponse->deleteLater();
+    }
+    g_imageCache.removeRequest(m_key);
+}
+
+void RoundImageRequest::handleImageResponseFinished()
+{
+    m_cancelOnDelete = false;
+    g_imageCache.removeRequest(m_key);
+
+    const QString error = m_imageResponse->errorString();
+    QImage image;
+
+    if (auto textureFactory = m_imageResponse->textureFactory())
+    {
+        image = textureFactory->image();
+        delete textureFactory;
+    }
+
+    if (image.isNull())
+    {
+        qDebug() << "failed to get image, error" << error << m_key.url;
+        m_status = RoundImage::Status::Error;
+        emit requestCompleted(m_status, {});
+        return;
+    }
+
+    image.setDevicePixelRatio(m_dpr);
 
+    g_imageCache.insert(m_key, new QImage(image), image.sizeInBytes());
+    emit requestCompleted(RoundImage::Status::Ready, image);
+}
+
+
+QQuickImageResponse* RoundImageRequest::getAsyncImageResponse(const QUrl &url, const QSize &requestedSize, const qreal radius, QQmlEngine *engine)
+{
+    if (url.scheme() == QStringLiteral("image"))
+    {
+        auto provider = engine->imageProvider(url.host());
+        if (!provider)
             return nullptr;
-        }
-        else if (QQmlFile::isLocalFile(url))
+
+        assert(provider->imageType() == QQmlImageProviderBase::ImageResponse);
+
+        const auto imageId = url.toString(QUrl::RemoveScheme | QUrl::RemoveAuthority).mid(1);
+
+        if (provider->imageType() == QQmlImageProviderBase::ImageResponse)
         {
-            return new LocalImageResponse(QQmlFile::urlToLocalFileOrQrc(url), requestedSize, radius);
+            auto rawImageResponse = static_cast<QQuickAsyncImageProvider *>(provider)->requestImageResponse(imageId, requestedSize);
+            return new ImageResponseRadiusAdaptor(rawImageResponse, radius);
         }
+
+        return nullptr;
+    }
+    else if (QQmlFile::isLocalFile(url))
+    {
+        return new LocalImageResponse(QQmlFile::urlToLocalFileOrQrc(url), requestedSize, radius);
+    }
 #ifdef QT_NETWORK_LIB
-        else
-        {
-            QNetworkRequest request(url);
-            request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
-            auto reply = engine->networkAccessManager()->get(request);
-            return new NetworkImageResponse(reply, requestedSize, radius);
-        }
+    else
+    {
+        QNetworkRequest request(url);
+        request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
+        auto reply = engine->networkAccessManager()->get(request);
+        return new NetworkImageResponse(reply, requestedSize, radius);
+    }
 #else
-        return nullptr;
+    return nullptr;
 #endif
-    }
 }
 
+// RoundImage
+
 RoundImage::RoundImage(QQuickItem *parent) : QQuickItem {parent}
 {
     if (Q_LIKELY(qGuiApp))
@@ -377,7 +456,6 @@ RoundImage::RoundImage(QQuickItem *parent) : QQuickItem {parent}
 
 RoundImage::~RoundImage()
 {
-    resetImageResponse(true);
 }
 
 QSGNode *RoundImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
@@ -519,51 +597,6 @@ void RoundImage::setDPR(const qreal value)
     regenerateRoundImage();
 }
 
-void RoundImage::handleImageResponseFinished()
-{
-    const QString error = m_activeImageResponse->errorString();
-    QImage image;
-    if (auto textureFactory = m_activeImageResponse->textureFactory())
-    {
-        image = textureFactory->image();
-        delete textureFactory;
-    }
-
-    resetImageResponse(false);
-
-    if (image.isNull())
-    {
-        setStatus(Status::Error);
-        qDebug() << "failed to get image, error" << error << source();
-        return;
-    }
-
-    image.setDevicePixelRatio(m_dpr);
-    setRoundImage(image);
-    setStatus(Status::Ready);
-
-    // FIXME: These should be gathered from the generator:
-    const qreal scaledWidth = this->width() * m_dpr;
-    const qreal scaledHeight = this->height() * m_dpr;
-    const qreal scaledRadius = m_QSGCustomGeometry ? 0.0 : (this->radius() * m_dpr);
-
-    const ImageCacheKey key {source(), QSizeF {scaledWidth, scaledHeight}.toSize(), scaledRadius};
-    imageCache.insert(key, new QImage(image), image.sizeInBytes());
-}
-
-void RoundImage::resetImageResponse(bool cancel)
-{
-    if (!m_activeImageResponse)
-        return;
-
-    m_activeImageResponse->disconnect(this);
-
-    if (cancel)
-        m_activeImageResponse->cancel();
-
-    m_activeImageResponse = nullptr;
-}
-
 void RoundImage::load()
 {
     m_enqueuedGeneration = false;
@@ -577,21 +610,57 @@ void RoundImage::load()
     const qreal scaledRadius = m_QSGCustomGeometry ? 0.0 : (this->radius() * m_dpr);
 
     const ImageCacheKey key {source(), QSizeF {scaledWidth, scaledHeight}.toSize(), scaledRadius};
-    if (auto image = imageCache.object(key)) // should only by called in mainthread
+
+    if (auto image = g_imageCache.object(key)) // should only by called in mainthread
     {
-        setRoundImage(*image);
-        setStatus(Status::Ready);
+        onRequestCompleted(Status::Ready, *image);
         return;
     }
 
-    m_activeImageResponse = getAsyncImageResponse(source(), QSizeF {scaledWidth, scaledHeight}.toSize(), scaledRadius, engine);
+    m_activeImageResponse = g_imageCache.requestImage(key, m_dpr, engine);
+    if (!m_activeImageResponse)
+    {
+        onRequestCompleted(RoundImage::Error, {});
+        return;
+    }
 
-    connect(m_activeImageResponse, &QQuickImageResponse::finished, this, &RoundImage::handleImageResponseFinished);
-    connect(m_activeImageResponse, &QQuickImageResponse::finished, m_activeImageResponse, &QObject::deleteLater);
+    connect(m_activeImageResponse.get(), &RoundImageRequest::requestCompleted, this, &RoundImage::onRequestCompleted);
+    //at this point m_activeImageResponse is either in Loading or Error status
+    onRequestCompleted(RoundImage::Loading, {});
+}
+
+void RoundImage::onRequestCompleted(Status status, const QImage& image)
+{
+    setRoundImage(image);
+    switch (status)
+    {
+    case RoundImage::Error:
+        setStatus(Status::Error);
+        m_activeImageResponse.reset();
+        break;
+    case RoundImage::Ready:
+    {
+        if (image.isNull())
+            setStatus(Status::Error);
+        else
+            setStatus(Status::Ready);
+        m_activeImageResponse.reset();
+        break;
+    }
+    case RoundImage::Loading:
+        setStatus(Status::Loading);
+        break;
+    case RoundImage::Null:
+        //requests should not be yield this state
+        vlc_assert_unreachable();
+    }
 }
 
 void RoundImage::setRoundImage(QImage image)
 {
+    if (m_roundImage.isNull() && image.isNull())
+        return;
+
     m_dirty = true;
     m_roundImage = image;
 
@@ -600,7 +669,7 @@ void RoundImage::setRoundImage(QImage image)
     if (image.isNull())
         update();
 
-    setFlag(ItemHasContents, not image.isNull());
+    setFlag(ItemHasContents, !image.isNull());
     update();
 }
 
@@ -623,7 +692,7 @@ void RoundImage::regenerateRoundImage()
     // remove old contents
     setRoundImage({});
 
-    resetImageResponse(true);
+    m_activeImageResponse.reset();
 
     // use Qt::QueuedConnection to delay generation, so that dependent properties
     // subsequent updates can be merged, f.e when VLCStyle.scale changes


=====================================
modules/gui/qt/widgets/native/roundimage.hpp
=====================================
@@ -23,13 +23,13 @@
 #ifndef VLC_QT_ROUNDIMAGE_HPP
 #define VLC_QT_ROUNDIMAGE_HPP
 
-#include "qt.hpp"
-
 #include <QImage>
 #include <QQuickItem>
 #include <QUrl>
+#include <memory>
 
 class QQuickImageResponse;
+class RoundImageRequest;
 
 class RoundImage : public QQuickItem
 {
@@ -78,7 +78,6 @@ protected:
 private:
     void setDPR(qreal value);
     void handleImageResponseFinished();
-    void resetImageResponse(bool cancel);
     void load();
     void setRoundImage(QImage image);
     void setStatus(const Status status);
@@ -86,6 +85,7 @@ private:
 
 private slots:
     void adjustQSGCustomGeometry(const QQuickWindow* const window);
+    void onRequestCompleted(Status status, const QImage& image);
 
 private:
     QUrl m_source;
@@ -98,10 +98,9 @@ private:
     QImage m_roundImage;
     bool m_dirty = false;
 
-    QQuickImageResponse *m_activeImageResponse {};
+    std::shared_ptr<RoundImageRequest> m_activeImageResponse;
 
     bool m_enqueuedGeneration = false;
 };
 
 #endif
-


=====================================
modules/gui/qt/widgets/native/roundimage_p.hpp
=====================================
@@ -0,0 +1,161 @@
+/*****************************************************************************
+ * roundimage.cpp: Custom widgets
+ ****************************************************************************
+ * Copyright (C) 2023 the VideoLAN team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifndef VLC_ROUNDIMAGE_P_HPP
+#define VLC_ROUNDIMAGE_P_HPP
+
+#include <QObject>
+#include <QUrl>
+#include <QCache>
+#include <QQuickImageProvider>
+
+#include <unordered_map>
+#include <memory>
+
+#include "roundimage.hpp"
+
+struct ImageCacheKey
+{
+    ImageCacheKey(QUrl url, QSize size, qreal radius)
+        : url(url)
+        , size(size)
+        , radius(radius)
+    {}
+
+    QUrl url;
+    QSize size;
+    qreal radius;
+};
+
+bool operator ==(const ImageCacheKey &lhs, const ImageCacheKey &rhs);
+uint qHash(const ImageCacheKey &key, uint seed);
+
+//std hash version is required for unordered_map
+template<>
+struct std::hash<ImageCacheKey>
+{
+    std::size_t operator()(const ImageCacheKey& s) const noexcept
+    {
+        return qHash(s, 0); // or use boost::hash_combine
+    }
+};
+
+
+/**
+ * @brief The RoundImageRequest class represent a request for a RoundImage generation
+ * it will be shared amongst the diffent RoundImage instance that preforms the same request
+ * simultaneously, and will notify upon completion or failure through the requestCompleted signal
+ */
+class RoundImageRequest : public QObject
+{
+    Q_OBJECT
+public:
+    RoundImageRequest(const ImageCacheKey& key, qreal dpr, QQmlEngine *engine);
+
+    ~RoundImageRequest();
+
+    inline RoundImage::Status getStatus() const
+    {
+        return m_status;
+    }
+
+    void handleImageResponseFinished();
+
+signals:
+    void requestCompleted(RoundImage::Status status, QImage image);
+
+private:
+    QQuickImageResponse *getAsyncImageResponse(const QUrl &url, const QSize &requestedSize, const qreal radius, QQmlEngine *engine);
+
+    bool m_cancelOnDelete = true;
+    const ImageCacheKey m_key;
+    qreal m_dpr;
+    QQuickImageResponse* m_imageResponse = nullptr;
+    RoundImage::Status m_status = RoundImage::Status::Loading;
+};
+
+/**
+ * @brief The RoundImageCache class contains the cache of generated round images
+ *
+ * principle is the folowing:
+ *
+ * @startuml
+ * (*) --> if image already in cache then
+ *   --> [true] "use image from the cache"
+ *   --> "set image in Ready state"
+ *   --> (*)
+ * else
+ *   --> [false] "set image Loading state"
+ *   --> if pending requests already exists for the source
+ *       --> [true] reference pending request
+ *       --> "wait for request completion" as wait
+ *   else
+ *       --> [false] create a new request
+ *       --> wait
+ *   endif
+ *   --> if request succeed
+ *         --> [true] "store image result in cache"
+ *         --> "use request image result"
+ *         --> "set image in Ready state"
+ *         --> (*)
+ *       else
+ *         --> [false] "set the image in Error state"
+ *         --> (*)
+ *      endif
+ * endif
+ * @enduml
+ *
+ * notes that:
+ * - requests are refcounted, if no image reference a request anymore,
+ *   the request is canceled and destroyed
+ *
+ * - failed atempts are not stored, if the same resource is requested latter on,
+ *   a new request will be created and executed for this request
+ */
+class RoundImageCache
+{
+public:
+    RoundImageCache();
+
+    inline QImage* object(const ImageCacheKey& key) const
+    {
+        return m_imageCache.object(key);
+    }
+
+    inline void insert(const ImageCacheKey& key, QImage* image, int size)
+    {
+        m_imageCache.insert(key, image, size);
+    }
+
+    std::shared_ptr<RoundImageRequest> requestImage(const ImageCacheKey& key, qreal dpr, QQmlEngine *engine);
+
+    void removeRequest(const ImageCacheKey& key);
+
+private:
+    //images are cached (result of RoundImageGenerator) with the cost calculated from QImage::sizeInBytes
+    QCache<ImageCacheKey, QImage> m_imageCache;
+
+    //contains the pending request, we use a weak ptr here as the request may be canceled and destroyed
+    //when all RoundImage that requested the image drop the request. user should check for validity before
+    //accessing the RoundImageRequest
+    std::unordered_map<ImageCacheKey, std::weak_ptr<RoundImageRequest>> m_pendingRequests;
+};
+
+#endif // VLC_ROUNDIMAGE_P_HPP



View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/e1b09a5bf52e6606134cdbd3fe06122180502480

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/e1b09a5bf52e6606134cdbd3fe06122180502480
You're receiving this email because of your account on code.videolan.org.


VideoLAN code repository instance


More information about the vlc-commits mailing list