diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs
index 622181dba0..a37fa6fd32 100644
--- a/src/Avalonia.Base/Media/DrawingContext.cs
+++ b/src/Avalonia.Base/Media/DrawingContext.cs
@@ -361,11 +361,12 @@ namespace Avalonia.Media
/// Pushes an opacity value.
///
/// The opacity.
+ /// The bounds.
/// A disposable used to undo the opacity.
- public PushedState PushOpacity(double opacity)
+ public PushedState PushOpacity(double opacity, Rect bounds)
//TODO: Eliminate platform-specific push opacity call
{
- PlatformImpl.PushOpacity(opacity);
+ PlatformImpl.PushOpacity(opacity, bounds);
return new PushedState(this, PushedState.PushedStateType.Opacity);
}
diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs
index b7abda2c61..7b02649b6c 100644
--- a/src/Avalonia.Base/Media/DrawingGroup.cs
+++ b/src/Avalonia.Base/Media/DrawingGroup.cs
@@ -74,10 +74,12 @@ namespace Avalonia.Media
public override void Draw(DrawingContext context)
{
+ var bounds = GetBounds();
+
using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity))
- using (context.PushOpacity(Opacity))
+ using (context.PushOpacity(Opacity, bounds))
using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default)
- using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default)
+ using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default)
{
foreach (var drawing in Children)
{
@@ -284,7 +286,7 @@ namespace Avalonia.Media
throw new NotImplementedException();
}
- public void PushOpacity(double opacity)
+ public void PushOpacity(double opacity, Rect bounds)
{
throw new NotImplementedException();
}
diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs
index 7d9534c414..2564d89bac 100644
--- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs
+++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs
@@ -281,11 +281,12 @@ namespace Avalonia.Media
/// Pushes an opacity value.
///
/// The opacity.
+ /// The bounds.
/// A disposable used to undo the opacity.
- public PushedState PushOpacity(double opacity)
+ public PushedState PushOpacity(double opacity, Rect bounds)
//TODO: Eliminate platform-specific push opacity call
{
- PlatformImpl.PushOpacity(opacity);
+ PlatformImpl.PushOpacity(opacity, bounds);
return new PushedState(this, PushedState.PushedStateType.Opacity);
}
diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs
index 8509067cd0..8962bc1586 100644
--- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs
+++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs
@@ -128,7 +128,7 @@ namespace Avalonia.Platform
/// Pushes an opacity value.
///
/// The opacity.
- void PushOpacity(double opacity);
+ void PushOpacity(double opacity, Rect bounds);
///
/// Pops the latest pushed opacity value.
diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
index b75d080cfd..6b380608fe 100644
--- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
@@ -313,13 +313,13 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
///
- public void PushOpacity(double opacity)
+ public void PushOpacity(double opacity, Rect bounds)
{
var next = NextDrawAs();
- if (next == null || !next.Item.Equals(opacity))
+ if (next == null || !next.Item.Equals(opacity, bounds))
{
- Add(new OpacityNode(opacity));
+ Add(new OpacityNode(opacity, bounds));
}
else
{
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
index 50df8bd32b..08e506536f 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
@@ -111,9 +111,9 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont
_impl.PopClip();
}
- public void PushOpacity(double opacity)
+ public void PushOpacity(double opacity, Rect bounds)
{
- _impl.PushOpacity(opacity);
+ _impl.PushOpacity(opacity, bounds);
}
public void PopOpacity()
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
index 98be861afa..f9492d0015 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
@@ -41,9 +41,9 @@ namespace Avalonia.Rendering.Composition.Server
return;
Root!.RenderedVisuals++;
-
- if (Opacity != 1)
- canvas.PushOpacity(Opacity);
+
+ var boundsRect = new Rect(new Size(Size.X, Size.Y));
+
if (AdornedVisual != null)
{
canvas.PostTransform = Matrix.Identity;
@@ -54,15 +54,16 @@ namespace Avalonia.Rendering.Composition.Server
var transform = GlobalTransformMatrix;
canvas.PostTransform = MatrixUtils.ToMatrix(transform);
canvas.Transform = Matrix.Identity;
-
- var boundsRect = new Rect(new Size(Size.X, Size.Y));
+
+ if (Opacity != 1)
+ canvas.PushOpacity(Opacity, boundsRect);
if (ClipToBounds && !HandlesClipToBounds)
canvas.PushClip(Root!.SnapToDevicePixels(boundsRect));
if (Clip != null)
canvas.PushGeometryClip(Clip);
if(OpacityMaskBrush != null)
canvas.PushOpacityMask(OpacityMaskBrush, boundsRect);
-
+
RenderCore(canvas, currentTransformedClip);
// Hack to force invalidation of SKMatrix
diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs
index 8e5dc38317..09d2d55ce3 100644
--- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs
+++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs
@@ -117,7 +117,7 @@ namespace Avalonia.Rendering
}
using (context.PushPostTransform(m))
- using (context.PushOpacity(opacity))
+ using (context.PushOpacity(opacity, bounds))
using (clipToBounds
#pragma warning disable CS0618 // Type or member is obsolete
? visual is IVisualWithRoundRectClip roundClipVisual
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs
index e41e639067..f76a055934 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs
+++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs
@@ -12,9 +12,11 @@ namespace Avalonia.Rendering.SceneGraph
/// opacity push.
///
/// The opacity to push.
- public OpacityNode(double opacity)
+ /// The bounds.
+ public OpacityNode(double opacity, Rect bounds)
{
Opacity = opacity;
+ Bounds = bounds;
}
///
@@ -26,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph
}
///
- public Rect Bounds => default;
+ public Rect Bounds { get; }
///
/// Gets the opacity to be pushed or null if the operation represents a pop.
@@ -40,19 +42,20 @@ namespace Avalonia.Rendering.SceneGraph
/// Determines if this draw operation equals another.
///
/// The opacity of the other draw operation.
+ /// The bounds of the other draw operation.
/// True if the draw operations are the same, otherwise false.
///
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
///
- public bool Equals(double? opacity) => Opacity == opacity;
+ public bool Equals(double? opacity, Rect bounds) => Opacity == opacity && Bounds == bounds;
///
public void Render(IDrawingContextImpl context)
{
if (Opacity.HasValue)
{
- context.PushOpacity(Opacity.Value);
+ context.PushOpacity(Opacity.Value, Bounds);
}
else
{
diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
index 68466fe381..5b84ceef7f 100644
--- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
@@ -392,7 +392,7 @@ namespace Avalonia.Headless
}
- public void PushOpacity(double opacity)
+ public void PushOpacity(double opacity, Rect rect)
{
}
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index 82d902cbd0..969f0b5e2a 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -40,6 +40,7 @@ namespace Avalonia.Skia
private static SKShader? s_acrylicNoiseShader;
private readonly ISkiaGpuRenderSession? _session;
private bool _leased;
+ private bool _useOpacitySaveLayer;
///
/// Context create info.
@@ -158,6 +159,13 @@ namespace Avalonia.Skia
}
Transform = Matrix.Identity;
+
+ var options = AvaloniaLocator.Current.GetService();
+
+ if(options != null)
+ {
+ _useOpacitySaveLayer = options.UseOpacitySaveLayer;
+ }
}
///
@@ -188,7 +196,7 @@ namespace Avalonia.Skia
var d = destRect.ToSKRect();
var paint = SKPaintCache.Shared.Get();
- paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity));
+ paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * (_useOpacitySaveLayer ? 1 : _currentOpacity)));
paint.FilterQuality = bitmapInterpolationMode.ToSKFilterQuality();
paint.BlendMode = _currentBlendingMode.ToSKBlendMode();
@@ -373,7 +381,7 @@ namespace Avalonia.Skia
{
if (!boxShadow.IsDefault && !boxShadow.IsInset)
{
- using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity))
+ using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity))
{
var spread = (float)boxShadow.Spread;
if (boxShadow.IsInset)
@@ -430,7 +438,7 @@ namespace Avalonia.Skia
{
if (!boxShadow.IsDefault && boxShadow.IsInset)
{
- using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity))
+ using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity))
{
var spread = (float)boxShadow.Spread;
var offsetX = (float)boxShadow.OffsetX;
@@ -568,18 +576,35 @@ namespace Avalonia.Skia
}
///
- public void PushOpacity(double opacity)
+ public void PushOpacity(double opacity, Rect bounds)
{
CheckLease();
- _opacityStack.Push(_currentOpacity);
- _currentOpacity *= opacity;
+
+ if(_useOpacitySaveLayer)
+ {
+ var rect = bounds.ToSKRect();
+ Canvas.SaveLayer(rect, new SKPaint { ColorF = new SKColorF(0, 0, 0, (float)opacity)});
+ }
+ else
+ {
+ _opacityStack.Push(_currentOpacity);
+ _currentOpacity *= opacity;
+ }
}
///
public void PopOpacity()
{
CheckLease();
- _currentOpacity = _opacityStack.Pop();
+
+ if(_useOpacitySaveLayer)
+ {
+ Canvas.Restore();
+ }
+ else
+ {
+ _currentOpacity = _opacityStack.Pop();
+ }
}
///
@@ -657,7 +682,7 @@ namespace Avalonia.Skia
var paint = SKPaintCache.Shared.Get();
- Canvas.SaveLayer(paint);
+ Canvas.SaveLayer(bounds.ToSKRect(), paint);
_maskStack.Push(CreatePaint(paint, mask, bounds.Size));
}
@@ -1021,8 +1046,6 @@ namespace Avalonia.Skia
paint.IsAntialias = true;
- double opacity = _currentOpacity;
-
var tintOpacity =
material.BackgroundSource == AcrylicBackgroundSource.Digger ?
material.TintOpacity : 1;
@@ -1071,7 +1094,7 @@ namespace Avalonia.Skia
paint.IsAntialias = true;
- double opacity = brush.Opacity * _currentOpacity;
+ double opacity = brush.Opacity * (_useOpacitySaveLayer ? 1 :_currentOpacity);
if (brush is ISolidColorBrush solid)
{
diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs
index b3c3056a58..84ad547d6c 100644
--- a/src/Skia/Avalonia.Skia/SkiaOptions.cs
+++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs
@@ -16,5 +16,13 @@ namespace Avalonia
/// Setting this to null will give you the default Skia value.
///
public long? MaxGpuResourceSizeBytes { get; set; } = 1024 * 600 * 4 * 12; // ~28mb 12x 1024 x 600 textures.
+
+ ///
+ /// Use Skia's SaveLayer API to handling opacity.
+ ///
+ ///
+ /// Enabling this might have performance implications.
+ ///
+ public bool UseOpacitySaveLayer { get; set; } = false;
}
}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
index 3506abc63b..0dd9c155bb 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
@@ -441,14 +441,15 @@ namespace Avalonia.Direct2D1.Media
/// Pushes an opacity value.
///
/// The opacity.
+ /// The bounds.
/// A disposable used to undo the opacity.
- public void PushOpacity(double opacity)
+ public void PushOpacity(double opacity, Rect bounds)
{
if (opacity < 1)
{
var parameters = new LayerParameters
{
- ContentBounds = PrimitiveExtensions.RectangleInfinite,
+ ContentBounds = bounds.ToDirect2D(),
MaskTransform = PrimitiveExtensions.Matrix3x2Identity,
Opacity = (float)opacity,
};
diff --git a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs
index e83b2d7598..40d504a0ac 100644
--- a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs
+++ b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs
@@ -65,7 +65,7 @@ namespace Avalonia.Benchmarks
{
}
- public void PushOpacity(double opacity)
+ public void PushOpacity(double opacity, Rect bounds)
{
}
diff --git a/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs b/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs
index 14f5b7c6c7..1199184d14 100644
--- a/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs
+++ b/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs
@@ -141,7 +141,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
new Rect(control.Bounds.Size),
4);
- using (context.PushOpacity(0.5))
+ using (context.PushOpacity(0.5, control.Bounds))
{
context.FillRectangle(
Brushes.Blue,
diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs
index c63a876d81..05e160dca8 100644
--- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs
+++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs
@@ -79,7 +79,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
using (var ctx = target.CreateDrawingContext(null))
{
ctx.Clear(Colors.Transparent);
- ctx.PushOpacity(0.8);
+ ctx.PushOpacity(0.8, new Rect(0, 0, 80, 80));
ctx.DrawRectangle(Brushes.Chartreuse, null, new Rect(0, 0, 20, 100));
ctx.DrawRectangle(Brushes.Crimson, null, new Rect(20, 0, 20, 100));
ctx.DrawRectangle(Brushes.Gold,null, new Rect(40, 0, 20, 100));