[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
This commit is contained in:
Lukas Brübach 2026-08-23 18:57:51 +02:00
parent b13c682a7a
commit 127d31cf4e
4 changed files with 110 additions and 26 deletions

View file

@ -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,23 +78,37 @@ void ColorIdentityWidget::toggleUnusedVisibility()
void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
QList<ManaSymbolWidget *> manaSymbols = findChildren<ManaSymbolWidget *>();
if (!manaSymbols.isEmpty()) {
int totalWidth = event->size().width();
int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
// 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);
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
QList<ManaSymbolWidget *> manaSymbols = findChildren<ManaSymbolWidget *>();
if (manaSymbols.isEmpty()) {
return;
}
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
if (iconSize == lastIconSize) {
return;
}
lastIconSize = iconSize;
for (ManaSymbolWidget *manaSymbol : manaSymbols) {
manaSymbol->setFixedSize(iconSize, iconSize);
}
}
}
QStringList ColorIdentityWidget::parseColorIdentity(const QString &manaString)
{

View file

@ -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

View file

@ -2,14 +2,56 @@
#include "../../../../client/settings/cache_settings.h"
#include <QHash>
#include <QImageReader>
#include <QResizeEvent>
#include <libcockatrice/settings/visual_deck_storage_settings.h>
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<QString, QPixmap> &scaledIconCache()
{
static QHash<QString, QPixmap> 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<QString, QPixmap> 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();
// 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;
}
void ManaSymbolWidget::loadManaIcon()
{
QString filename = "theme:icons/mana/";
if (symbol == "W" || symbol == "U" || symbol == "B" || symbol == "R" || symbol == "G") {
filename += symbol;
}
manaIcon = QPixmap(filename);
setPixmap(getCachedScaledIcon(symbol, newSize));
}

View file

@ -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;