From d618413c5d3b69e5d5127580b4097dcc9dcde375 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 15 Apr 2026 02:42:34 +1000 Subject: [PATCH] Qt: Add AsyncPixmapLoader class --- src/duckstation-qt/CMakeLists.txt | 2 + src/duckstation-qt/asyncpixmaploader.cpp | 118 ++++++++++++++++++ src/duckstation-qt/asyncpixmaploader.h | 37 ++++++ src/duckstation-qt/duckstation-qt.vcxproj | 2 + .../duckstation-qt.vcxproj.filters | 2 + src/duckstation-qt/qtutils.cpp | 23 ++++ src/duckstation-qt/qtutils.h | 3 + 7 files changed, 187 insertions(+) create mode 100644 src/duckstation-qt/asyncpixmaploader.cpp create mode 100644 src/duckstation-qt/asyncpixmaploader.h diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index 7a2f5aa80..d2fba5141 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -17,6 +17,8 @@ set(SRCS advancedsettingswidget.cpp advancedsettingswidget.h advancedsettingswidget.ui + asyncpixmaploader.cpp + asyncpixmaploader.h audiosettingswidget.cpp audiosettingswidget.h audiosettingswidget.ui diff --git a/src/duckstation-qt/asyncpixmaploader.cpp b/src/duckstation-qt/asyncpixmaploader.cpp new file mode 100644 index 000000000..8e3453a00 --- /dev/null +++ b/src/duckstation-qt/asyncpixmaploader.cpp @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "asyncpixmaploader.h" +#include "qtutils.h" + +#include "core/host.h" + +#include "util/http_cache.h" +#include "util/object_archive.h" + +#include "common/error.h" +#include "common/heap_array.h" +#include "common/log.h" +#include "common/path.h" +#include "common/small_string.h" + +#include "moc_asyncpixmaploader.cpp" + +LOG_CHANNEL(Host); + +AsyncPixmapLoader::AsyncPixmapLoader(QObject* parent /*= nullptr*/) : QObject(parent) +{ +} + +AsyncPixmapLoader::~AsyncPixmapLoader() = default; + +bool AsyncPixmapLoader::isQueueNeeded(std::string_view url_or_path) +{ + if (!HTTPCache::IsHTTPURL(url_or_path)) + return false; + + // Don't try to async load when we don't have cache. + const auto cache = HTTPCache::GetCacheArchive(); + return (cache->IsOpen() && !cache->Contains(url_or_path)); +} + +static std::string_view GetExtensionFromURL(std::string_view url) +{ + return Path::GetExtension(HTTPCache::GetURLFilename(url)); +} + +static QPixmap LoadPixmapFromSpan(std::string_view extension, std::span data) +{ + QPixmap ret; + TinyString extension_cstr(extension); + if (!ret.loadFromData(data.data(), static_cast(data.size()), + extension_cstr.empty() ? nullptr : extension_cstr.c_str())) + { + ERROR_LOG("Failed to load pixmap from data for extension '{}'", extension); + } + + return ret; +} + +QPixmap AsyncPixmapLoader::load(std::string_view url_or_path) +{ + if (!HTTPCache::IsHTTPURL(url_or_path)) + return QPixmap(QtUtils::StringViewToQString(url_or_path)); + + Error error; + const HTTPCache::LookupResult result = HTTPCache::Lookup(url_or_path, &error); + if (result.status() != HTTPCache::LookupStatus::Hit) + { + ERROR_LOG("Failed to lookup pixmap for URL '{}': {}", url_or_path, error.GetDescription()); + return QPixmap(); + } + + return LoadPixmapFromSpan(GetExtensionFromURL(url_or_path), result->cspan()); +} + +void AsyncPixmapLoader::enqueue(std::string_view url_or_path) +{ + if (!HTTPCache::IsHTTPURL(url_or_path)) + { + QPixmap pm(QtUtils::StringViewToQString(url_or_path)); + emit pixmapLoaded(pm); + deleteLater(); + return; + } + + Error error; + HTTPCache::LookupResult result = HTTPCache::Lookup(url_or_path, &error); + + // gotta load it, this is the yuck part. + if (result.status() == HTTPCache::LookupStatus::Miss) + { + m_extension = GetExtensionFromURL(url_or_path); + result = HTTPCache::LookupOrFetch(url_or_path, &error, [this](std::span data) mutable { + if (!data.empty()) + m_data.assign(data); + QMetaObject::invokeMethod(this, &AsyncPixmapLoader::finishLoad, Qt::QueuedConnection); + }); + } + + if (result.status() == HTTPCache::LookupStatus::Hit) + { + QPixmap pm = LoadPixmapFromSpan(GetExtensionFromURL(url_or_path), result->cspan()); + emit pixmapLoaded(pm); + deleteLater(); + return; + } + else if (result.status() != HTTPCache::LookupStatus::Miss) + { + ERROR_LOG("Failed to lookup pixmap for URL '{}': {}", url_or_path, error.GetDescription()); + QPixmap pm; + emit pixmapLoaded(pm); + deleteLater(); + return; + } +} + +void AsyncPixmapLoader::finishLoad() +{ + QPixmap pm = LoadPixmapFromSpan(m_extension, m_data); + emit pixmapLoaded(pm); + deleteLater(); +} diff --git a/src/duckstation-qt/asyncpixmaploader.h b/src/duckstation-qt/asyncpixmaploader.h new file mode 100644 index 000000000..918cc52f9 --- /dev/null +++ b/src/duckstation-qt/asyncpixmaploader.h @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#pragma once + +#include "common/heap_array.h" + +#include +#include + +#include + +class QPixmap; + +class AsyncPixmapLoader final : public QObject +{ + Q_OBJECT + +public: + explicit AsyncPixmapLoader(QObject* parent = nullptr); + ~AsyncPixmapLoader() override; + + static bool isQueueNeeded(std::string_view url_or_path); + + static QPixmap load(std::string_view url_or_path); + + void enqueue(std::string_view url_or_path); + +Q_SIGNALS: + void pixmapLoaded(QPixmap& pixmap); + +private: + void finishLoad(); + + std::string m_extension; + DynamicHeapArray m_data; +}; diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj index c1d45b0fc..3c5918c20 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -6,6 +6,7 @@ + @@ -82,6 +83,7 @@ + diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters index 30e8cc6f5..d5345ad48 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj.filters +++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters @@ -52,6 +52,7 @@ + @@ -116,6 +117,7 @@ + diff --git a/src/duckstation-qt/qtutils.cpp b/src/duckstation-qt/qtutils.cpp index 316df5796..5f893a975 100644 --- a/src/duckstation-qt/qtutils.cpp +++ b/src/duckstation-qt/qtutils.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "qtutils.h" +#include "asyncpixmaploader.h" #include "qthost.h" #include "core/core.h" @@ -350,6 +351,28 @@ QMenu* QtUtils::NewPopupMenu(QWidget* parent, bool delete_on_close /*= true*/) return menu; } +void QtUtils::SetLabelPixmapPathOrURL(QLabel* label, std::string_view path_or_url, bool ignore_if_invalid) +{ + if (!AsyncPixmapLoader::isQueueNeeded(path_or_url)) + { + const QPixmap pm = AsyncPixmapLoader::load(path_or_url); + if (!pm.isNull() || !ignore_if_invalid) + label->setPixmap(pm); + + return; + } + + AsyncPixmapLoader* loader = new AsyncPixmapLoader(); + QObject::connect(loader, &AsyncPixmapLoader::pixmapLoaded, label, [label, ignore_if_invalid](QPixmap& pm) { + if (pm.isNull() && ignore_if_invalid) + return; + + pm.setDevicePixelRatio(label->devicePixelRatio()); + label->setPixmap(pm); + }); + loader->enqueue(path_or_url); +} + QMessageBox::StandardButton QtUtils::MessageBoxInformation(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) diff --git a/src/duckstation-qt/qtutils.h b/src/duckstation-qt/qtutils.h index 573133458..9b212c444 100644 --- a/src/duckstation-qt/qtutils.h +++ b/src/duckstation-qt/qtutils.h @@ -152,6 +152,9 @@ void StyleChildMenus(QWidget* widget); /// Creates a new popup menu, styled for the current theme. QMenu* NewPopupMenu(QWidget* parent, bool delete_on_close = true); +/// Helper function to load a QPixmap from a path or URL. +void SetLabelPixmapPathOrURL(QLabel* label, std::string_view path_or_url, bool ignore_if_invalid); + /// Returns icon for language. QIcon GetIconForTranslationLanguage(std::string_view language_name);