diff --git a/cockatrice/src/game_graphics/game_view.cpp b/cockatrice/src/game_graphics/game_view.cpp index b768c8317..4ab45272d 100644 --- a/cockatrice/src/game_graphics/game_view.cpp +++ b/cockatrice/src/game_graphics/game_view.cpp @@ -128,7 +128,16 @@ void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount) QPoint cursor = cursorPoint.toPoint(); QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized(); + + // Explicitly repaint the strip the band just vacated. When the band is a + // child widget, the viewport does not reliably receive the exposed region + // on all platforms (notably macOS), which would leave stale band pixels on + // screen until the next unrelated repaint. + const QRect previousGeometry = rubberBand->geometry(); rubberBand->setGeometry(rect); + if (viewport()) { + viewport()->update(previousGeometry.united(rect)); + } if (!SettingsCache::instance().userInterface().getShowDragSelectionCount()) { dragCountLabel->hide(); @@ -171,7 +180,14 @@ void GameView::stopRubberBand() return; } + // Same rationale as resizeRubberBand. Make sure the area under the band is + // repainted, since some platforms skip the exposed-region update of a + // hidden child widget. + const QRect bandGeometry = rubberBand->geometry(); rubberBand->hide(); + if (viewport()) { + viewport()->update(bandGeometry); + } dragCountLabel->hide(); } diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp index 7a88acdff..729df75be 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.cpp +++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp @@ -190,15 +190,40 @@ void PlayerGraphicsItem::paint(QPainter *painter, const QStyleOptionGraphicsItem const QRectF srcRect = computeArtSourceRect(playmatPixmap.size(), playmatParams); const QRectF dstRect = coverFitRect(combinedArea, srcRect.size()); + painter->save(); painter->setClipRect(combinedArea); - painter->drawPixmap(dstRect, playmatPixmap, srcRect); - painter->setClipping(false); + painter->setRenderHint(QPainter::SmoothPixmapTransform, true); + + // Render from a down-scaled copy of the art so the full-resolution source + // pixmap is never re-sampled at a tiny device size (also much cheaper than + // scaling it on every frame). + const QPixmap scaledPixmap = scaledPlaymatFor(srcRect, painter->worldTransform().mapRect(dstRect).size()); + painter->drawPixmap(dstRect, scaledPixmap, QRectF(scaledPixmap.rect())); + + painter->restore(); if (!playmatAttribution.isEmpty()) { paintArtAttribution(*painter, combinedArea, playmatAttribution, Qt::AlignRight | Qt::AlignBottom, 0.8); } } +QPixmap PlayerGraphicsItem::scaledPlaymatFor(const QRectF &srcRect, const QSizeF &deviceDstSize) +{ + // Bucket the render size so the source pixmap is re-scaled at most once per + // zoom step instead of once per frame. + constexpr int bucketSize = 32; + const QSize target = QSize(qMax(1, qRound(deviceDstSize.width() / bucketSize) * bucketSize), + qMax(1, qRound(deviceDstSize.height() / bucketSize) * bucketSize)) + .boundedTo(srcRect.toAlignedRect().size()); + + if (scaledPlaymatKey != target) { + const QPixmap crop = playmatPixmap.copy(srcRect.toAlignedRect()); + scaledPlaymatPixmap = crop.scaled(target, Qt::KeepAspectRatio, Qt::SmoothTransformation); + scaledPlaymatKey = target; + } + return scaledPlaymatPixmap; +} + void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth) { // Extend table (and hand, if horizontal) to accommodate the new player width. @@ -372,6 +397,7 @@ void PlayerGraphicsItem::updatePlaymat() } playmatParams = params; + scaledPlaymatKey = QSize(); // the art crop depends on the params, drop any cached scale ExactCard card = CardDatabaseManager::query()->getCard(playmatCard); if (!card) { @@ -409,6 +435,7 @@ void PlayerGraphicsItem::clearPlaymat() if (hasPlaymat) { hasPlaymat = false; playmatPixmap = QPixmap(); + scaledPlaymatKey = QSize(); emit playmatChanged(false); update(); } diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.h b/cockatrice/src/game_graphics/player/player_graphics_item.h index 55e1d1cf1..e5ae59a61 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.h +++ b/cockatrice/src/game_graphics/player/player_graphics_item.h @@ -149,6 +149,8 @@ private: bool handVisible = false; QPixmap playmatPixmap; + QPixmap scaledPlaymatPixmap; // down-scaled copy of playmatPixmap for the current render size + QSize scaledPlaymatKey; // size bucket scaledPlaymatPixmap was rendered for PlaymatParams playmatParams; QString playmatAttribution; bool hasPlaymat = false; @@ -160,6 +162,9 @@ private slots: void clearPlaymat(); void updatePlaymat(); void onPlaymatPixmapReady(); + +private: + QPixmap scaledPlaymatFor(const QRectF &srcRect, const QSizeF &deviceDstSize); }; #endif // COCKATRICE_PLAYER_GRAPHICS_ITEM_H diff --git a/cockatrice/src/game_graphics/zones/stack_zone.cpp b/cockatrice/src/game_graphics/zones/stack_zone.cpp index 812271042..e9b14f13d 100644 --- a/cockatrice/src/game_graphics/zones/stack_zone.cpp +++ b/cockatrice/src/game_graphics/zones/stack_zone.cpp @@ -43,6 +43,9 @@ void StackZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti void StackZone::onPlaymatChanged(bool active) { playmatActive = active; + // See TableZone::onPlaymatChanged for the rationale. Translucent overlay + // over a dynamic playmat should not be held in the device cache. + setCacheMode(active ? QGraphicsItem::NoCache : QGraphicsItem::DeviceCoordinateCache); update(); } diff --git a/cockatrice/src/game_graphics/zones/table_zone.cpp b/cockatrice/src/game_graphics/zones/table_zone.cpp index aba3dd1bf..88e9abe6c 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.cpp +++ b/cockatrice/src/game_graphics/zones/table_zone.cpp @@ -121,6 +121,11 @@ void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti void TableZone::onPlaymatChanged(bool active) { playmatActive = active; + // While a playmat is shown the zone paints a translucent overlay over the + // dynamic playmat behind it. Keep it out of the device cache so the cached + // pixels are never stale relative to the playmat (and to avoid compositing + // artifacts of cached translucent content on some platforms). + setCacheMode(active ? QGraphicsItem::NoCache : QGraphicsItem::DeviceCoordinateCache); update(); }