From a4022df686b2e11778f51a37e8e18aa1bb34832f Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 1 Jul 2024 15:35:15 +0100 Subject: [PATCH] Direct2D: Fix issue where contexts would not clear properly after pushing multiple clip layers This issue could be seen when calling setBufferedToImage on a component with a transparent background with a size different to the component's size. The details are unclear to me, but it seems like both calling Clear on the device context, and using the COPY blend mode, ignore alpha values and instead use a constant alpha of 1.0 when there is a geometric clipping layer active. As a workaround for this issue, when clearing a rectangle we now pop all active layers, fill their intersection using the COPY blend mode while there are no layers active, and then reinstate the layers. The new implementation is likely to be very slow, however I think this code path is unlikely to be used frequently in practice. The main use-case for rendering clear transparent areas is the rendering of buffered component images, but such cases normally use axis-aligned clipping regions, which should be able to use the faster path. --- .../juce_Direct2DGraphicsContext_windows.cpp | 142 ++++++++++++++++-- 1 file changed, 131 insertions(+), 11 deletions(-) diff --git a/modules/juce_graphics/native/juce_Direct2DGraphicsContext_windows.cpp b/modules/juce_graphics/native/juce_Direct2DGraphicsContext_windows.cpp index 733e079ef0..c2ead0ec73 100644 --- a/modules/juce_graphics/native/juce_Direct2DGraphicsContext_windows.cpp +++ b/modules/juce_graphics/native/juce_Direct2DGraphicsContext_windows.cpp @@ -39,6 +39,23 @@ namespace juce JUCE_IMPLEMENT_SINGLETON (Direct2DMetricsHub) #endif +struct ScopedBlendCopy +{ + explicit ScopedBlendCopy (ComSmartPtr c) + : ctx (c) + { + ctx->SetPrimitiveBlend (D2D1_PRIMITIVE_BLEND_COPY); + } + + ~ScopedBlendCopy() + { + ctx->SetPrimitiveBlend (blend); + } + + ComSmartPtr ctx; + D2D1_PRIMITIVE_BLEND blend = ctx->GetPrimitiveBlend(); +}; + class PushedLayers { public: @@ -142,10 +159,100 @@ public: return pushedLayers.empty(); } + void fillGeometryWithNoLayersActive (ComSmartPtr ctx, + ComSmartPtr geo, + ComSmartPtr brush) + { + ComSmartPtr factory; + ctx->GetFactory (factory.resetAndGetPointerAddress()); + + const auto hasGeoLayer = std::any_of (pushedLayers.begin(), + pushedLayers.end(), + [] (const auto& x) { return std::holds_alternative (x.var); }); + + const auto intersection = [&]() -> ComSmartPtr + { + if (! hasGeoLayer) + return {}; + + const auto contextSize = ctx->GetPixelSize(); + + ComSmartPtr rect; + factory->CreateRectangleGeometry (D2D1::RectF (0.0f, + 0.0f, + (float) contextSize.width, + (float) contextSize.height), + rect.resetAndGetPointerAddress()); + + ComSmartPtr clip = rect; + + for (const auto& layer : pushedLayers) + { + ScopedGeometryWithSink scope { factory, D2D1_FILL_MODE_WINDING }; + + if (auto* l = std::get_if (&layer.var)) + { + clip->CombineWithGeometry (l->geometry, + D2D1_COMBINE_MODE_INTERSECT, + l->params.maskTransform, + scope.sink); + } + else if (auto* r = std::get_if> (&layer.var)) + { + ComSmartPtr temporaryRect; + factory->CreateRectangleGeometry (D2DUtilities::toRECT_F (*r), + temporaryRect.resetAndGetPointerAddress()); + clip->CombineWithGeometry (temporaryRect, + D2D1_COMBINE_MODE_INTERSECT, + D2D1::Matrix3x2F::Identity(), + scope.sink); + } + + clip = scope.geometry; + } + + return clip; + }(); + + const auto clipWithGeo = [&]() -> ComSmartPtr + { + if (intersection == nullptr) + return geo; + + ScopedGeometryWithSink scope { factory, D2D1_FILL_MODE_WINDING }; + intersection->CombineWithGeometry (geo, + D2D1_COMBINE_MODE_INTERSECT, + D2D1::Matrix3x2F::Identity(), + scope.sink); + return scope.geometry; + }(); + + if (intersection != nullptr) + { + std::for_each (pushedLayers.rbegin(), + pushedLayers.rend(), + [&] (const auto& layer) { layer.pop (ctx); }); + } + + { + const ScopedBlendCopy scope { ctx }; + ctx->FillGeometry (clipWithGeo, brush); + } + + if (intersection != nullptr) + { + pushedLayers.clear(); + + auto newLayer = D2D1::LayerParameters1(); + newLayer.geometricMask = intersection; + push (ctx, newLayer); + } + } + private: struct OwningLayer { - explicit OwningLayer (D2D1_LAYER_PARAMETERS1 p) : params (p) {} + explicit OwningLayer (const D2D1_LAYER_PARAMETERS1& p) : params (p) {} D2D1_LAYER_PARAMETERS1 params; ComSmartPtr geometry = params.geometricMask != nullptr ? addComSmartPtrOwner (params.geometricMask) : nullptr; @@ -1276,17 +1383,30 @@ void Direct2DGraphicsContext::fillRect (const Rectangle& r, bool replaceExi return; if (replaceExistingContents) - clipToRectangle (r); - - const auto clearColour = currentState->fillType.colour; - - auto fill = [replaceExistingContents, clearColour] (Rectangle rect, - ComSmartPtr deviceContext, - ComSmartPtr brush) { - if (replaceExistingContents) - deviceContext->Clear (D2DUtilities::toCOLOR_F (clearColour)); - else if (brush != nullptr) + applyPendingClipList(); + + const auto asRectF = D2DUtilities::toRECT_F (getPimpl()->getFrameSize().toFloat()); + ComSmartPtr rectGeometry; + getPimpl()->getDirect2DFactory()->CreateRectangleGeometry (asRectF, + rectGeometry.resetAndGetPointerAddress()); + + const auto matrix = D2DUtilities::transformToMatrix (currentState->currentTransform.getTransform()); + ComSmartPtr geo; + getPimpl()->getDirect2DFactory()->CreateTransformedGeometry (rectGeometry, + matrix, + geo.resetAndGetPointerAddress()); + + const auto brush = currentState->fillType.isInvisible() ? currentState->currentBrush : currentState->getBrush(); + currentState->layers.fillGeometryWithNoLayersActive (getPimpl()->getDeviceContext(), geo, brush); + return; + } + + const auto fill = [] (Rectangle rect, + ComSmartPtr deviceContext, + ComSmartPtr brush) + { + if (brush != nullptr) deviceContext->FillRectangle (D2DUtilities::toRECT_F (rect), brush); };