Browse Source

Make our TileBrush code a bit less byzantine (#17098)

* Use scalable rasterization for visual brush

* Make ConfigureSceneBrushContentWithPicture less byzantine

---------

Co-authored-by: Benedikt Stebner <Gillibald@users.noreply.github.com>
pull/17115/head
Nikita Tsukanov 2 years ago
committed by GitHub
parent
commit
04e76a9265
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      src/Avalonia.Base/Media/VisualBrush.cs
  2. 18
      src/Avalonia.Base/Rendering/Utilities/TileBrushCalculator.cs
  3. 170
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  4. 12
      src/Skia/Avalonia.Skia/PictureRenderTarget.cs
  5. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png
  6. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png
  7. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Is_Properly_Mapped_Relative.expected.png
  8. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Should_Be_Usable_As_Opacity_Mask.expected.png

4
src/Avalonia.Base/Media/VisualBrush.cs

@ -57,7 +57,7 @@ namespace Avalonia.Media
using var recorder = new RenderDataDrawingContext(null); using var recorder = new RenderDataDrawingContext(null);
ImmediateRenderer.Render(recorder, Visual, Visual.Bounds); ImmediateRenderer.Render(recorder, Visual, Visual.Bounds);
return recorder.GetImmediateSceneBrushContent(this, new(Visual.Bounds.Size), false); return recorder.GetImmediateSceneBrushContent(this, new(Visual.Bounds.Size), true);
} }
internal override Func<Compositor, ServerCompositionSimpleBrush> Factory => internal override Func<Compositor, ServerCompositionSimpleBrush> Factory =>
@ -99,7 +99,7 @@ namespace Avalonia.Media
} }
if (data != null) if (data != null)
content = new(data.Data.Server, data.Rect, false); content = new(data.Data.Server, data.Rect, true);
} }
writer.WriteObject(content); writer.WriteObject(content);

18
src/Avalonia.Base/Rendering/Utilities/TileBrushCalculator.cs

@ -130,29 +130,35 @@ namespace Avalonia.Rendering.Utilities
AlignmentY alignmentY, AlignmentY alignmentY,
Rect sourceRect, Rect sourceRect,
Rect destinationRect, Rect destinationRect,
Vector scale) Vector scale) => CalculateTranslate(alignmentX, alignmentY,
sourceRect.Size * scale, destinationRect.Size);
public static Vector CalculateTranslate(
AlignmentX alignmentX,
AlignmentY alignmentY,
Size sourceSize,
Size destinationSize)
{ {
var x = 0.0; var x = 0.0;
var y = 0.0; var y = 0.0;
var size = sourceRect.Size * scale;
switch (alignmentX) switch (alignmentX)
{ {
case AlignmentX.Center: case AlignmentX.Center:
x += (destinationRect.Width - size.Width) / 2; x += (destinationSize.Width - sourceSize.Width) / 2;
break; break;
case AlignmentX.Right: case AlignmentX.Right:
x += destinationRect.Width - size.Width; x += destinationSize.Width - sourceSize.Width;
break; break;
} }
switch (alignmentY) switch (alignmentY)
{ {
case AlignmentY.Center: case AlignmentY.Center:
y += (destinationRect.Height - size.Height) / 2; y += (destinationSize.Height - sourceSize.Height) / 2;
break; break;
case AlignmentY.Bottom: case AlignmentY.Bottom:
y += destinationRect.Height - size.Height; y += destinationSize.Height - sourceSize.Height;
break; break;
} }

170
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -1149,112 +1149,112 @@ namespace Avalonia.Skia
private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper, ISceneBrushContent content, private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper, ISceneBrushContent content,
Rect targetRect) Rect targetRect)
{ {
var tileBrush = content.Brush; // To understand what happens here, read
// https://learn.microsoft.com/en-us/dotnet/api/system.windows.media.tilebrush
var contentBounds = content.Rect; // and the rest of the docs
if (contentBounds.Size.Width <= 0 || contentBounds.Size.Height <= 0) // Avalonia follows WPF and WPF's brushes completely ignore whatever layout bounds visuals have,
// and instead are using content bounds, e. g.
// ╔════════════════════════════════════╗ <--- target control
// ║ ║ layout bounds
// ║ ╔═════╗───────────┐ <--- content ║
// ║ ║ ║<- content │ bounds ║
// ║ ╚═════╝ ╔══╗ ║
// ║ │ ^ content ╚══╝ ║
// ║ │ ╔═════╗content^ │ ║
// ║ └─╚═════╝─────────┘ ║
// ║ ║
// ╚════════════════════════════════════╝
//
// Source Rect (aka ViewBox) is relative to the content bounds, not to the visual/drawing
var contentRect = content.Rect;
var sourceRect = content.Brush.SourceRect.ToPixels(contentRect);
// Early escape
if (contentRect.Size.Width <= 0 || contentRect.Size.Height <= 0
|| sourceRect.Size.Width <= 0 || sourceRect.Size.Height <= 0)
{ {
paintWrapper.Paint.Color = SKColor.Empty; paintWrapper.Paint.Color = SKColor.Empty;
return; return;
} }
var brushTransform = Matrix.Identity; // We are moving the render area to make the top-left corner of the SourceRect (ViewBox) to be at (0,0)
// of the tile
var destinationRect = content.Brush.DestinationRect.ToPixels(targetRect.Size); var contentRenderTransform = Matrix.CreateTranslation(-sourceRect.X, -sourceRect.Y);
var sourceRect = tileBrush.SourceRect.ToPixels(contentBounds); // DestinationRect (aka Viewport) is specified relative to the target rect
var destinationRect = content.Brush.DestinationRect.ToPixels(targetRect);
brushTransform *= Matrix.CreateTranslation(-sourceRect.Position);
// Tile size matches the destination rect size
var scale = Vector.One; var tileSize = destinationRect.Size;
if (sourceRect.Size != destinationRect.Size) // Apply transforms to stretch content to match the tile
if (sourceRect.Size != tileSize)
{ {
//scale source to destination size // Stretch the content rect to match the tile size
scale = tileBrush.Stretch.CalculateScaling(destinationRect.Size, sourceRect.Size); var scale = content.Brush.Stretch.CalculateScaling(tileSize, sourceRect.Size);
var scaleTransform = Matrix.CreateScale(scale); // And move the resulting rect according to alignment rules
var alignmentTranslate = TileBrushCalculator.CalculateTranslate(
content.Brush.AlignmentX,
content.Brush.AlignmentY, sourceRect.Size * scale, tileSize);
brushTransform *= scaleTransform; contentRenderTransform = contentRenderTransform * Matrix.CreateScale(scale) *
Matrix.CreateTranslation(alignmentTranslate);
} }
var transform = Matrix.Identity; // Pre-rasterize the tile into SKPicture
using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _intermediateSurfaceDpi);
if (content.Transform is not null) using (var ctx = pictureTarget.CreateDrawingContext(tileSize, false))
{ {
var transformOrigin = content.TransformOrigin.ToPixels(targetRect); ctx.PushRenderOptions(RenderOptions);
var offset = Matrix.CreateTranslation(transformOrigin); content.Render(ctx, contentRenderTransform);
transform = -offset * content.Transform.Value * offset; ctx.PopRenderOptions();
if (tileBrush.TileMode == TileMode.None)
{
brushTransform *= transform;
destinationRect = destinationRect.TransformToAABB(transform);
destinationRect = new Rect(0, 0, destinationRect.Left + destinationRect.Width,
destinationRect.Top + destinationRect.Height);
}
} }
using var tile = pictureTarget.GetPicture();
if (tileBrush.Stretch != Stretch.Fill && transform == Matrix.Identity)
// If there is no BrushTransform and destinationRect is at (0,0) we don't need any transforms
Matrix shaderTransform = Matrix.Identity;
// Apply Brush.Transform to SKShader
if (content.Transform != null)
{ {
//align content
var alignmentOffset = TileBrushCalculator.CalculateTranslate(tileBrush.AlignmentX, tileBrush.AlignmentY, var transformOrigin = content.TransformOrigin.ToPixels(targetRect);
contentBounds, destinationRect, tileBrush.Stretch == Stretch.None ? Vector.One : scale); var offset = Matrix.CreateTranslation(transformOrigin);
shaderTransform = (-offset) * content.Transform.Value * (offset);
brushTransform *= Matrix.CreateTranslation(alignmentOffset);
} }
using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _intermediateSurfaceDpi); // Apply destinationRect position
using (var ctx = pictureTarget.CreateDrawingContext(destinationRect.Size)) if (destinationRect.Position != default)
shaderTransform *= Matrix.CreateTranslation(destinationRect.X, destinationRect.Y);
// Create shader
var (tileX, tileY) = GetTileModes(content.Brush.TileMode);
using(var shader = tile.ToShader(tileX, tileY, shaderTransform.ToSKMatrix(),
new SKRect(0, 0, tile.CullRect.Width, tile.CullRect.Height)))
{ {
ctx.PushRenderOptions(RenderOptions); paintWrapper.Paint.FilterQuality = SKFilterQuality.None;
content.Render(ctx, brushTransform); paintWrapper.Paint.Shader = shader;
ctx.PopRenderOptions();
} }
}
using var picture = pictureTarget.GetPicture(); (SKShaderTileMode x, SKShaderTileMode y) GetTileModes(TileMode mode)
{
var paintTransform = return (
tileBrush.TileMode != TileMode.None mode == TileMode.None
? SKMatrix.CreateTranslation(-(float)destinationRect.X, -(float)destinationRect.Y)
: SKMatrix.CreateIdentity();
SKShaderTileMode tileX =
tileBrush.TileMode == TileMode.None
? SKShaderTileMode.Decal ? SKShaderTileMode.Decal
: tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY : mode == TileMode.FlipX || mode == TileMode.FlipXY
? SKShaderTileMode.Mirror ? SKShaderTileMode.Mirror
: SKShaderTileMode.Repeat; : SKShaderTileMode.Repeat,
SKShaderTileMode tileY =
tileBrush.TileMode == TileMode.None mode == TileMode.None
? SKShaderTileMode.Decal ? SKShaderTileMode.Decal
: tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY : mode == TileMode.FlipY || mode == TileMode.FlipXY
? SKShaderTileMode.Mirror ? SKShaderTileMode.Mirror
: SKShaderTileMode.Repeat; : SKShaderTileMode.Repeat);
paintTransform = SKMatrix.Concat(paintTransform,
SKMatrix.CreateScale((float)(96.0 / _intermediateSurfaceDpi.X), (float)(96.0 / _intermediateSurfaceDpi.Y)));
if (tileBrush.DestinationRect.Unit == RelativeUnit.Relative)
paintTransform =
paintTransform.PreConcat(SKMatrix.CreateTranslation((float)targetRect.X, (float)targetRect.Y));
if (tileBrush.TileMode != TileMode.None)
{
paintTransform = paintTransform.PreConcat(transform.ToSKMatrix());
}
using (var shader = picture.ToShader(tileX, tileY, paintTransform,
new SKRect(0, 0, picture.CullRect.Width, picture.CullRect.Height)))
{
paintWrapper.Paint.FilterQuality = SKFilterQuality.None;
paintWrapper.Paint.Shader = shader;
}
} }
private static SKColorFilter CreateAlphaColorFilter(double opacity) private static SKColorFilter CreateAlphaColorFilter(double opacity)

12
src/Skia/Avalonia.Skia/PictureRenderTarget.cs

@ -25,12 +25,14 @@ internal class PictureRenderTarget : IDisposable
_picture = null; _picture = null;
return rv; return rv;
} }
public IDrawingContextImpl CreateDrawingContext(Size size) public IDrawingContextImpl CreateDrawingContext(Size size, bool scaleToDpi = true)
{ {
if (scaleToDpi)
size *= (_dpi / 96);
var recorder = new SKPictureRecorder(); var recorder = new SKPictureRecorder();
var canvas = recorder.BeginRecording(new SKRect(0, 0, (float)(size.Width * _dpi.X / 96), var canvas = recorder.BeginRecording(new SKRect(0, 0, (float)size.Width,
(float)(size.Height * _dpi.Y / 96))); (float)size.Height));
canvas.RestoreToCount(-1); canvas.RestoreToCount(-1);
canvas.ResetMatrix(); canvas.ResetMatrix();
@ -38,7 +40,7 @@ internal class PictureRenderTarget : IDisposable
var createInfo = new DrawingContextImpl.CreateInfo var createInfo = new DrawingContextImpl.CreateInfo
{ {
Canvas = canvas, Canvas = canvas,
ScaleDrawingToDpi = true, ScaleDrawingToDpi = scaleToDpi,
Dpi = _dpi, Dpi = _dpi,
DisableSubpixelTextRendering = true, DisableSubpixelTextRendering = true,
GrContext = _grContext, GrContext = _grContext,

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 B

After

Width:  |  Height:  |  Size: 182 B

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 B

After

Width:  |  Height:  |  Size: 395 B

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Is_Properly_Mapped_Relative.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Should_Be_Usable_As_Opacity_Mask.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Loading…
Cancel
Save