Browse Source

[SKIA] UseOpacitySaveLayer feature switch (#9964)

* [SKIA] Introduce UseOpacitySaveLayer feature switch

* Only maintain _currentOpacity when OpacitySaveLayer is disabled
headless-tests
Benedikt Stebner 3 years ago
committed by GitHub
parent
commit
f5caa61051
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      src/Avalonia.Base/Media/DrawingContext.cs
  2. 8
      src/Avalonia.Base/Media/DrawingGroup.cs
  3. 5
      src/Avalonia.Base/Media/ImmediateDrawingContext.cs
  4. 2
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  5. 6
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
  6. 4
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  7. 13
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  8. 2
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  9. 11
      src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs
  10. 2
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  11. 45
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  12. 8
      src/Skia/Avalonia.Skia/SkiaOptions.cs
  13. 5
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  14. 2
      tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs
  15. 2
      tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs
  16. 2
      tests/Avalonia.RenderTests/Media/BitmapTests.cs

5
src/Avalonia.Base/Media/DrawingContext.cs

@ -361,11 +361,12 @@ namespace Avalonia.Media
/// Pushes an opacity value.
/// </summary>
/// <param name="opacity">The opacity.</param>
/// <param name="bounds">The bounds.</param>
/// <returns>A disposable used to undo the opacity.</returns>
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);
}

8
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();
}

5
src/Avalonia.Base/Media/ImmediateDrawingContext.cs

@ -281,11 +281,12 @@ namespace Avalonia.Media
/// Pushes an opacity value.
/// </summary>
/// <param name="opacity">The opacity.</param>
/// <param name="bounds">The bounds.</param>
/// <returns>A disposable used to undo the opacity.</returns>
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);
}

2
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@ -128,7 +128,7 @@ namespace Avalonia.Platform
/// Pushes an opacity value.
/// </summary>
/// <param name="opacity">The opacity.</param>
void PushOpacity(double opacity);
void PushOpacity(double opacity, Rect bounds);
/// <summary>
/// Pops the latest pushed opacity value.

6
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs

@ -313,13 +313,13 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
/// <inheritdoc/>
public void PushOpacity(double opacity)
public void PushOpacity(double opacity, Rect bounds)
{
var next = NextDrawAs<OpacityNode>();
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
{

4
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()

13
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

2
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

11
src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs

@ -12,9 +12,11 @@ namespace Avalonia.Rendering.SceneGraph
/// opacity push.
/// </summary>
/// <param name="opacity">The opacity to push.</param>
public OpacityNode(double opacity)
/// <param name="bounds">The bounds.</param>
public OpacityNode(double opacity, Rect bounds)
{
Opacity = opacity;
Bounds = bounds;
}
/// <summary>
@ -26,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public Rect Bounds => default;
public Rect Bounds { get; }
/// <summary>
/// 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.
/// </summary>
/// <param name="opacity">The opacity of the other draw operation.</param>
/// <param name="bounds">The bounds of the other draw operation.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(double? opacity) => Opacity == opacity;
public bool Equals(double? opacity, Rect bounds) => Opacity == opacity && Bounds == bounds;
/// <inheritdoc/>
public void Render(IDrawingContextImpl context)
{
if (Opacity.HasValue)
{
context.PushOpacity(Opacity.Value);
context.PushOpacity(Opacity.Value, Bounds);
}
else
{

2
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -392,7 +392,7 @@ namespace Avalonia.Headless
}
public void PushOpacity(double opacity)
public void PushOpacity(double opacity, Rect rect)
{
}

45
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;
/// <summary>
/// Context create info.
@ -158,6 +159,13 @@ namespace Avalonia.Skia
}
Transform = Matrix.Identity;
var options = AvaloniaLocator.Current.GetService<SkiaOptions>();
if(options != null)
{
_useOpacitySaveLayer = options.UseOpacitySaveLayer;
}
}
/// <summary>
@ -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
}
/// <inheritdoc />
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;
}
}
/// <inheritdoc />
public void PopOpacity()
{
CheckLease();
_currentOpacity = _opacityStack.Pop();
if(_useOpacitySaveLayer)
{
Canvas.Restore();
}
else
{
_currentOpacity = _opacityStack.Pop();
}
}
/// <inheritdoc />
@ -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)
{

8
src/Skia/Avalonia.Skia/SkiaOptions.cs

@ -16,5 +16,13 @@ namespace Avalonia
/// Setting this to null will give you the default Skia value.
/// </remarks>
public long? MaxGpuResourceSizeBytes { get; set; } = 1024 * 600 * 4 * 12; // ~28mb 12x 1024 x 600 textures.
/// <summary>
/// Use Skia's SaveLayer API to handling opacity.
/// </summary>
/// <remarks>
/// Enabling this might have performance implications.
/// </remarks>
public bool UseOpacitySaveLayer { get; set; } = false;
}
}

5
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -441,14 +441,15 @@ namespace Avalonia.Direct2D1.Media
/// Pushes an opacity value.
/// </summary>
/// <param name="opacity">The opacity.</param>
/// <param name="bounds">The bounds.</param>
/// <returns>A disposable used to undo the opacity.</returns>
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,
};

2
tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs

@ -65,7 +65,7 @@ namespace Avalonia.Benchmarks
{
}
public void PushOpacity(double opacity)
public void PushOpacity(double opacity, Rect bounds)
{
}

2
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,

2
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));

Loading…
Cancel
Save