[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