From 127d31cf4e257be53f6966e600da8b8fa5cfc6d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sun, 23 Aug 2026 18:57:51 +0200 Subject: [PATCH] [VDS] Cache mana symbol renders and skip redundant resizes - Render each mana symbol once at a bounded master size and derive every requested size from the cached master, avoiding repeated full-size SVG rasterization on the GUI thread - Share scaled results through a process-wide cache keyed by symbol and size, so repeated widget creation and rescales don't redo the work - Skip redundant resize work in ColorIdentityWidget and ManaSymbolWidget when sizes did not change Took 8 minutes Took 50 seconds --- .../additional_info/color_identity_widget.cpp | 41 ++++++--- .../additional_info/color_identity_widget.h | 2 + .../additional_info/mana_symbol_widget.cpp | 83 ++++++++++++++++--- .../additional_info/mana_symbol_widget.h | 10 ++- 4 files changed, 110 insertions(+), 26 deletions(-) diff --git a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp index 3f0f30a27..a4cb86751 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp @@ -41,6 +41,11 @@ void ColorIdentityWidget::populateManaSymbolWidgets() // clear old layout QtUtils::clearLayoutRec(layout); + // The freshly created symbols haven't been sized yet, so force the next resize pass + // to apply the symbol size again. + lastIconSize = -1; + lastWidth = -1; + // populate mana symbols if (SettingsCache::instance().visualDeckStorage().getVisualDeckStorageDrawUnusedColorIdentities()) { for (const QString symbol : fullColorIdentity) { @@ -73,21 +78,35 @@ void ColorIdentityWidget::toggleUnusedVisibility() void ColorIdentityWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); + + // Layout passes resize this widget repeatedly with identical sizes, so bail out before + // touching the children when neither the width nor the resulting symbol size changed. + const int totalWidth = event->size().width(); + if (totalWidth == lastWidth && lastIconSize != -1) { + return; + } + lastWidth = totalWidth; + + const int totalHeight = totalWidth / 6; // Set height to 1/4 of the width + setFixedHeight(totalHeight); + QList manaSymbols = findChildren(); + if (manaSymbols.isEmpty()) { + return; + } - if (!manaSymbols.isEmpty()) { - int totalWidth = event->size().width(); - int totalHeight = totalWidth / 6; // Set height to 1/4 of the width - setFixedHeight(totalHeight); + const int spacing = layout->spacing(); + const int count = manaSymbols.size(); + const int availableWidth = totalWidth - (spacing * (count - 1)); + const int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height - int spacing = layout->spacing(); - int count = manaSymbols.size(); - int availableWidth = totalWidth - (spacing * (count - 1)); - int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height + if (iconSize == lastIconSize) { + return; + } + lastIconSize = iconSize; - for (ManaSymbolWidget *manaSymbol : manaSymbols) { - manaSymbol->setFixedSize(iconSize, iconSize); - } + for (ManaSymbolWidget *manaSymbol : manaSymbols) { + manaSymbol->setFixedSize(iconSize, iconSize); } } diff --git a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.h b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.h index f776d4c77..315ac07d6 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.h +++ b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.h @@ -30,6 +30,8 @@ public slots: private: QString colorIdentity; QHBoxLayout *layout; + int lastIconSize = -1; ///< The symbol size last applied, to skip redundant resize passes. + int lastWidth = -1; ///< The width last processed, to skip redundant resize passes. }; #endif // COLOR_IDENTITY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp b/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp index 51247da7a..973f532be 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp @@ -2,14 +2,56 @@ #include "../../../../client/settings/cache_settings.h" +#include +#include #include #include -ManaSymbolWidget::ManaSymbolWidget(QWidget *parent, QString _symbol, bool _isActive, bool _mayBeToggled) - : QLabel(parent), symbol(_symbol), isActive(_isActive), mayBeToggled(_mayBeToggled) +namespace { - loadManaIcon(); - setPixmap(manaIcon.scaled(50, 50, Qt::KeepAspectRatio, Qt::SmoothTransformation)); +/// Longest side mana symbols are rendered at before being scaled to their final size. +constexpr int MASTER_ICON_SIZE = 128; + +QHash &scaledIconCache() +{ + static QHash cache; + return cache; +} + +/** + * @brief Renders \a symbol once at a fixed moderate size, so repeated scalings never + * re-rasterize the source file (SVG sources can be very expensive to rasterize). + */ +const QPixmap &masterIcon(const QString &symbol) +{ + static QHash cache; + auto it = cache.constFind(symbol); + if (it != cache.constEnd()) { + return it.value(); + } + + QImageReader reader("theme:icons/mana/" + symbol); + QSize sourceSize = reader.size(); + if (!sourceSize.isEmpty()) { + sourceSize.scale(QSize(MASTER_ICON_SIZE, MASTER_ICON_SIZE), Qt::KeepAspectRatio); + reader.setScaledSize(sourceSize); + } + const QPixmap rendered = QPixmap::fromImageReader(&reader); + + return cache.insert(symbol, rendered).value(); +} + +QString cacheKey(const QString &symbol, const QSize &size) +{ + return symbol + QLatin1Char('|') + QString::number(size.width()) + QLatin1Char('x') + + QString::number(size.height()); +} +} // namespace + +ManaSymbolWidget::ManaSymbolWidget(QWidget *parent, QString _symbol, bool _isActive, bool _mayBeToggled) + : QLabel(parent), symbol(std::move(_symbol)), isActive(_isActive), mayBeToggled(_mayBeToggled) +{ + setPixmap(getCachedScaledIcon(symbol, QSize(50, 50))); setMaximumWidth(50); // Initialize opacity effect @@ -22,6 +64,24 @@ ManaSymbolWidget::ManaSymbolWidget(QWidget *parent, QString _symbol, bool _isAct &ManaSymbolWidget::updateOpacity); } +QPixmap ManaSymbolWidget::getCachedScaledIcon(const QString &symbol, const QSize &size) +{ + const QString key = cacheKey(symbol, size); + auto it = scaledIconCache().constFind(key); + if (it != scaledIconCache().constEnd()) { + return it.value(); + } + + const QPixmap &icon = masterIcon(symbol); + if (icon.isNull()) { + return {}; + } + + QPixmap scaled = icon.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); + scaledIconCache().insert(key, scaled); + return scaled; +} + void ManaSymbolWidget::toggleSymbol() { setColorActive(!isActive); @@ -64,16 +124,13 @@ void ManaSymbolWidget::mousePressEvent(QMouseEvent *event) void ManaSymbolWidget::resizeEvent(QResizeEvent *event) { QLabel::resizeEvent(event); - setPixmap(manaIcon.scaled(event->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); -} + const QSize newSize = event->size(); -void ManaSymbolWidget::loadManaIcon() -{ - QString filename = "theme:icons/mana/"; - - if (symbol == "W" || symbol == "U" || symbol == "B" || symbol == "R" || symbol == "G") { - filename += symbol; + // Skip the rescale when the size didn't actually change: layout passes resize these + // widgets repeatedly with identical sizes. + if (newSize.isEmpty() || pixmap().size() == newSize) { + return; } - manaIcon = QPixmap(filename); + setPixmap(getCachedScaledIcon(symbol, newSize)); } diff --git a/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.h b/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.h index 0f2d7acd1..dae13f7b9 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.h +++ b/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.h @@ -33,7 +33,14 @@ public: return symbol[0]; } - void loadManaIcon(); + /** + * @brief Returns a smooth-scaled rendering of the given mana symbol icon. + * + * Results are shared between all instances via a process-wide cache keyed by symbol + * and size, so scaling work is done once per distinct combination instead of once per + * widget creation or resize. + */ + static QPixmap getCachedScaledIcon(const QString &symbol, const QSize &size); public slots: void resizeEvent(QResizeEvent *event) override; @@ -44,7 +51,6 @@ signals: private: QString symbol; - QPixmap manaIcon; bool isActive; bool mayBeToggled; QGraphicsOpacityEffect *opacityEffect;