From 03229db0bd1f5897a827efdfd502d56b75915000 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 23 Aug 2026 20:13:14 +0200 Subject: [PATCH 1/6] Fix cn and ss flags losing stars and clamp svg render sizes (#7168) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cn.svg and ss.svg defined their star polygons with coordinates around plus/minus 5e5 compensated by tiny scale transforms. Qt drops shapes whose device space bounds exceed its rasterizer coordinate limit, so both flags silently lost their stars when rendered wider than roughly 76px. That threshold was always exceeded because loadSvg with expandOnly renders at the declared 640x480 native size before scaling down to the icon size. Fold the scale transforms into the polygon coordinates so the geometry is unchanged while bounds stay small at every render size. Also cap expandOnly and usericon render canvases at four times the requested size to bound memory use and keep pathological theme svgs away from the rasterizer limit. Took 8 minutes Took 57 seconds Took 3 minutes Co-authored-by: Lukas Brübach --- cockatrice/resources/countries/cn.svg | 3 +- cockatrice/resources/countries/ss.svg | 2 +- .../src/interface/pixel_map_generator.cpp | 33 ++++++++++++++++++- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/cockatrice/resources/countries/cn.svg b/cockatrice/resources/countries/cn.svg index f510cf049..45b608127 100644 --- a/cockatrice/resources/countries/cn.svg +++ b/cockatrice/resources/countries/cn.svg @@ -52,8 +52,7 @@ id="defs8"> - + diff --git a/cockatrice/src/interface/pixel_map_generator.cpp b/cockatrice/src/interface/pixel_map_generator.cpp index d3b0252a6..5bfba1c8a 100644 --- a/cockatrice/src/interface/pixel_map_generator.cpp +++ b/cockatrice/src/interface/pixel_map_generator.cpp @@ -14,6 +14,32 @@ #define DEFAULT_COLOR_MODERATOR_RIGHT "#000000"; #define DEFAULT_COLOR_ADMIN "#ff2701"; +/** + * Clamps an svg render size so that rendering does not exceed a multiple of the requested size. + * + * Rendering at the full native size of an svg just to scale it down afterwards wastes memory, + * and canvases with extreme coordinates can exceed Qt's rasterizer coordinate limit which makes + * Qt silently drop shapes from the rendered image. + * + * @param renderSize The size the svg would be rendered at. + * @param requestedSize The size that was actually requested. + * + * @return A size with the aspect ratio of renderSize whose longest side is at most four times + * the longest side of requestedSize. + */ +static QSize capRenderSize(const QSize &renderSize, const QSize &requestedSize) +{ + const int longestRequestedSide = qMax(requestedSize.width(), requestedSize.height()); + if (longestRequestedSide <= 0) { + return renderSize; + } + + const int longestRenderSide = qMax(renderSize.width(), renderSize.height()); + const qreal scale = qMin(1.0, static_cast(longestRequestedSide * 4) / longestRenderSide); + return QSize(qMax(1, static_cast(renderSize.width() * scale)), + qMax(1, static_cast(renderSize.height() * scale))); +} + /** * Loads in an svg from file and scales it without affecting image quality. * @@ -35,6 +61,9 @@ static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnl // If expandOnly, make sure the pixmap is at least as large as the svg, so that we don't lose any detail. // QIcon.pixmap(size) will automatically scale down the image, but it won't scale it up. QSize pixmapSize = expandOnly ? svgRenderer.defaultSize().expandedTo(size) : size; + if (expandOnly) { + pixmapSize = capRenderSize(pixmapSize, size); + } QPixmap pix(pixmapSize); pix.fill(Qt::transparent); @@ -247,7 +276,9 @@ static QIcon loadAndColorSvg(const QString &iconPath, QSvgRenderer svgRenderer(doc.toByteArray()); - QPixmap pix(svgRenderer.defaultSize().expandedTo(QSize(minSize, minSize))); + const QSize pixmapSize = + capRenderSize(svgRenderer.defaultSize().expandedTo(QSize(minSize, minSize)), QSize(minSize, minSize)); + QPixmap pix(pixmapSize); pix.fill(Qt::transparent); QPainter pixPainter(&pix); From b80e6994abc9acacf8943d4dbe9732fb2306ea65 Mon Sep 17 00:00:00 2001 From: tooomm Date: Sun, 23 Aug 2026 20:40:28 +0200 Subject: [PATCH 2/6] [CI] Use modern `macos-26` across Mac builds (#7035) * use macos26 across * fix xcode versions * fix cross-arch compilation test * No cross-compile * reduce cache churn and race conditions between matrix runs better * Increase timeout to 300s * Update CMakeDMGSetup.script * switch intel target to faster `macos-15-intel` runner * Newest Xcode on macOS 15 runners is 26.3 * Revert timeout change * Qt bump, cache key updates, cleanup * more cleanup * more cleanup + separation * cache key --- .github/workflows/desktop-build.yml | 60 ++++++++++++++--------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 92695a9d1..744f9e70a 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -161,14 +161,11 @@ jobs: uses: actions/checkout@v7 - name: "Restore compiler cache (ccache)" - id: ccache_restore + id: restore_ccache uses: actions/cache/restore@v6 - env: - BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: - key: ccache-${{ matrix.distro }}${{ matrix.version }}-${{ env.BRANCH_NAME }} + key: ccache-${{ matrix.distro }}${{ matrix.version }} path: ${{ env.CACHE }} - restore-keys: ccache-${{ matrix.distro }}${{ matrix.version }}- - name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image" shell: bash @@ -203,10 +200,10 @@ jobs: # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 - name: "Delete remote compiler cache (ccache)" - if: github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit + if: github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit continue-on-error: true env: - CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }} + CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }} GH_TOKEN: ${{ github.token }} run: | if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then @@ -217,7 +214,7 @@ jobs: if: github.ref == 'refs/heads/master' uses: actions/cache/save@v6 with: - key: ${{ steps.ccache_restore.outputs.cache-primary-key }} + key: ${{ steps.restore_ccache.outputs.cache-primary-key }} path: ${{ env.CACHE }} - name: "Upload artifact" @@ -264,13 +261,14 @@ jobs: - os: macOS target: 13 # EOL 2025-09-15 runner: macos-15-intel # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md + # macos-26-intel is very slow and fails in CPack during DMG config if not increasing Finder timeout ccache_eviction_age: 7d cmake_generator: Ninja make_package: 1 override_target: 13 package_suffix: "-macOS13_Intel" - qt_version: 6.11.0 + qt_version: 6.11.1 qt_modules: qtimageformats qtmultimedia qtwebsockets soc: Intel type: Release @@ -279,47 +277,48 @@ jobs: - os: macOS target: 14 # EOL 2026-?? - runner: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md + runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md ccache_eviction_age: 7d cmake_generator: Ninja make_package: 1 override_target: 14 package_suffix: "-macOS14" - qt_version: 6.11.0 + qt_version: 6.11.1 qt_modules: qtimageformats qtmultimedia qtwebsockets soc: Apple type: Release use_ccache: 1 - xcode: "26.3" + xcode: "26.6" - os: macOS target: 15 - runner: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md + runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md ccache_eviction_age: 7d cmake_generator: Ninja make_package: 1 + override_target: 15 package_suffix: "-macOS15" - qt_version: 6.11.0 + qt_version: 6.11.1 qt_modules: qtimageformats qtmultimedia qtwebsockets soc: Apple type: Release use_ccache: 1 - xcode: "26.3" + xcode: "26.6" - os: macOS - target: 15 - runner: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md + target: 26 + runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md ccache_eviction_age: 7d cmake_generator: Ninja - qt_version: 6.11.0 + qt_version: 6.11.1 qt_modules: qtimageformats qtmultimedia qtwebsockets soc: Apple type: Debug use_ccache: 1 - xcode: "26.3" + xcode: "26.6" - os: Windows target: 10 @@ -329,7 +328,7 @@ jobs: cmake_generator_platform: x64 make_package: 1 package_suffix: "-Win10" - qt_version: 6.11.0 + qt_version: 6.11.1 qt_modules: qtimageformats qtmultimedia qtwebsockets type: Release @@ -349,7 +348,6 @@ jobs: - name: "[Windows] Add msbuild to PATH" if: matrix.os == 'Windows' - id: add-msbuild uses: microsoft/setup-msbuild@v3 with: msbuild-architecture: x64 @@ -360,14 +358,11 @@ jobs: - name: "[macOS] Restore compiler cache (ccache)" if: matrix.os == 'macOS' && matrix.use_ccache == 1 - id: ccache_restore + id: restore_ccache uses: actions/cache/restore@v6 - env: - BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: - key: ccache-${{ matrix.runner }}-${{ matrix.soc }}-${{ matrix.type }}-${{ env.BRANCH_NAME }} + key: ccache-${{ matrix.runner }}_${{ matrix.override_target }}-Xcode${{ matrix.xcode }} path: ${{ env.CCACHE_DIR }} - restore-keys: ccache-${{ matrix.runner }}-${{ matrix.soc }}-${{ matrix.type }}- - name: "Install aqtinstall" run: pipx install aqtinstall @@ -385,7 +380,7 @@ jobs: id: restore_qt uses: actions/cache/restore@v6 with: - key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} + key: Qt-${{ steps.resolve_qt_version.outputs.version }}-macOS-${{ matrix.soc }}-${{ matrix.qt_modules }}-thin path: ${{ github.workspace }}/Qt # Using jurplel/install-qt-action to install Qt without using brew @@ -407,7 +402,7 @@ jobs: if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' uses: actions/cache/save@v6 with: - key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} + key: ${{ steps.restore_qt.outputs.cache-primary-key }} path: ${{ github.workspace }}/Qt - name: "[Windows] Install Qt ${{ matrix.qt_version }}" @@ -417,6 +412,7 @@ jobs: # Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released aqtsource: git+https://github.com/miurahr/aqtinstall.git cache: true + cache-key-prefix: Qt modules: ${{ matrix.qt_modules }} version: ${{ steps.resolve_qt_version.outputs.version }} @@ -457,21 +453,21 @@ jobs: # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 - name: "[macOS] Delete remote compiler cache (ccache)" - if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit + if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit continue-on-error: true env: - CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }} + CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }} GH_TOKEN: ${{ github.token }} run: | if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then echo "Cache deleted successfully" fi - - name: "[macOS] Save updated compiler cache (ccache)" + - name: "[macOS] Cache updated compiler cache (ccache)" if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' uses: actions/cache/save@v6 with: - key: ${{ steps.ccache_restore.outputs.cache-primary-key }} + key: ${{ steps.restore_ccache.outputs.cache-primary-key }} path: ${{ env.CCACHE_DIR }} - name: "[macOS] Sign app bundle" From 21633eb0ec4c8ee4cc5170b349d23e69f5d7e698 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 23 Aug 2026 22:33:26 +0200 Subject: [PATCH 3/6] [UserList] Raise banner card art cache to 1024 entries (#7169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lukas Brübach --- .../src/interface/widgets/server/user/user_card_art_provider.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h index fb2f37812..2592237c4 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h +++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h @@ -26,7 +26,7 @@ public slots: private: bool dbReady = false; - static constexpr int MaxCacheEntries = 300; + static constexpr int MaxCacheEntries = 1024; QList cacheInsertionOrder; // FIFO eviction QMap cardArtCache; QSet pending; From daa0dcb2ea1a612eb2be655f726f115dd2cecc92 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 23 Aug 2026 22:45:49 +0200 Subject: [PATCH 4/6] [UserList] Keep banner art when a params-less user copy arrives (#7173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lukas Brübach --- .../interface/widgets/server/user/user_list_widget.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index 2534ee62c..e71eac23b 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -1372,9 +1372,13 @@ void UserListWidget::updateCardArtParams(const ServerInfo_User &user, const QStr params.zoom = cap.zoom(); cardArtParamsMap.insert(userName, params); cardArtProvider->requestCardArt(userName, params.cardName, params.cardProviderId); - } else { - cardArtParamsMap.remove(userName); // clear stale params on removal } + // Intentionally no removal branch: buddy/ignore list copies never carry + // card_art_params (the server omits the column), so a params-less copy here + // means "this snapshot doesn't include it", not "the banner was removed". + // Removing on such copies would wipe banners that the live online list set. + // The map is rebuilt from scratch (clear() + repopulate) on every rebuild, + // which is what actually drops stale entries. } void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online) From 648b472bfbafb3f2ece991a5c1f4b754d2a24e77 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 23 Aug 2026 22:46:07 +0200 Subject: [PATCH 5/6] [UserList] Prevent failed loads from poisoning the banner card art cache (#7170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lukas Brübach --- .../widgets/server/user/user_card_art_provider.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp index 67fb4f684..3a1876fa1 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp @@ -39,7 +39,10 @@ void UserCardArtProvider::requestCardArt(const QString &userName, const QString const QString key = makeKey(userName, cardName, providerId); - if (cardArtCache.contains(key) || pending.contains(key)) { + if (pending.contains(key)) { + return; + } + if (cardArtCache.contains(key) && !cardArtCache.value(key).isNull()) { return; } @@ -63,6 +66,10 @@ QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes) void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap) { + if (pixmap.isNull()) { + return; + } + if (!cardArtCache.contains(key)) { cacheInsertionOrder.append(key); while (cacheInsertionOrder.size() > MaxCacheEntries) { @@ -129,8 +136,6 @@ void UserCardArtProvider::processQueue() if (!fullRes.isNull()) { self->insertIntoCache(key, self->cropCardArt(fullRes)); - } else { - self->insertIntoCache(key, QPixmap()); } self->pending.remove(key); From a571a9aa04796915db172e2e60280ecbd5ec8926 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:00:08 +0200 Subject: [PATCH 6/6] [UserList] Restore accent-tinted rows for regular users in light mode (#7171) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lukas Brübach --- .../widgets/server/user/user_list_painter.cpp | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp index 82f2887c8..5c65b090d 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp @@ -101,23 +101,21 @@ void UserListPainter::drawBackground(QPainter *painter, bg.setColorAt(0, blend(style.cardStart, accentColor, selected ? 0.75 : 0.65)); bg.setColorAt(1, blend(style.cardEnd, accentColor, selected ? 0.18 : 0.10)); } else { - // Regular users are the light theme's neutral paper cards. A flat - // warm card fill (the normal row surface, slightly deepened) keeps - // every row clearly visible without borrowing a role color. Selection - // shifts the fill toward a soft slate so the highlight still reads. - const QColor paper = style.cardEnd.darker(108); - bg.setColorAt(0, blend(paper, accentColor, selected ? 0.35 : 0.0)); - bg.setColorAt(1, blend(paper, accentColor, selected ? 0.25 : 0.0)); + // Regular users keep a scaled-down accent tint so the banner card art + // stays legible over a colored backdrop (the pre-branch painter was + // always dark-styled) while the role hierarchy still reads. + bg.setColorAt(0, blend(style.cardStart, accentColor, (selected ? 0.75 : 0.65) * 0.7)); + bg.setColorAt(1, blend(style.cardEnd, accentColor, (selected ? 0.18 : 0.10) * 0.7)); } painter->setPen(Qt::NoPen); painter->setBrush(bg); painter->drawRoundedRect(cardRect, 6, 6); - if (style.dark || hasRole || selected) { - painter->setBrush(accentColor); - painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2); - } + // The 3px accent bar anchors every row so the banner card art reads as a + // consistent strip in either scheme (pre-branch parity). + painter->setBrush(accentColor); + painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2); } static QString makeKey(const QString &user, const QString &card, const QString &providerId)