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);
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 =>
@ -99,7 +99,7 @@ namespace Avalonia.Media
}
if (data != null)
content = new(data.Data.Server, data.Rect, false);
content = new(data.Data.Server, data.Rect, true);
}
writer.WriteObject(content);

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

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

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

@ -1149,112 +1149,112 @@ namespace Avalonia.Skia
private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper, ISceneBrushContent content,
Rect targetRect)
{
var tileBrush = content.Brush;
var contentBounds = content.Rect;
if (contentBounds.Size.Width <= 0 || contentBounds.Size.Height <= 0)
// To understand what happens here, read
// https://learn.microsoft.com/en-us/dotnet/api/system.windows.media.tilebrush
// and the rest of the docs
// 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;
return;
}
var brushTransform = Matrix.Identity;
var destinationRect = content.Brush.DestinationRect.ToPixels(targetRect.Size);
var sourceRect = tileBrush.SourceRect.ToPixels(contentBounds);
brushTransform *= Matrix.CreateTranslation(-sourceRect.Position);
var scale = Vector.One;
if (sourceRect.Size != destinationRect.Size)
// 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 contentRenderTransform = Matrix.CreateTranslation(-sourceRect.X, -sourceRect.Y);
// DestinationRect (aka Viewport) is specified relative to the target rect
var destinationRect = content.Brush.DestinationRect.ToPixels(targetRect);
// Tile size matches the destination rect size
var tileSize = destinationRect.Size;
// Apply transforms to stretch content to match the tile
if (sourceRect.Size != tileSize)
{
//scale source to destination size
scale = tileBrush.Stretch.CalculateScaling(destinationRect.Size, sourceRect.Size);
// Stretch the content rect to match the tile 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;
if (content.Transform is not null)
// Pre-rasterize the tile into SKPicture
using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _intermediateSurfaceDpi);
using (var ctx = pictureTarget.CreateDrawingContext(tileSize, false))
{
var transformOrigin = content.TransformOrigin.ToPixels(targetRect);
var offset = Matrix.CreateTranslation(transformOrigin);
transform = -offset * content.Transform.Value * offset;
if (tileBrush.TileMode == TileMode.None)
{
brushTransform *= transform;
destinationRect = destinationRect.TransformToAABB(transform);
destinationRect = new Rect(0, 0, destinationRect.Left + destinationRect.Width,
destinationRect.Top + destinationRect.Height);
}
ctx.PushRenderOptions(RenderOptions);
content.Render(ctx, contentRenderTransform);
ctx.PopRenderOptions();
}
if (tileBrush.Stretch != Stretch.Fill && transform == Matrix.Identity)
using var tile = pictureTarget.GetPicture();
// 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,
contentBounds, destinationRect, tileBrush.Stretch == Stretch.None ? Vector.One : scale);
brushTransform *= Matrix.CreateTranslation(alignmentOffset);
var transformOrigin = content.TransformOrigin.ToPixels(targetRect);
var offset = Matrix.CreateTranslation(transformOrigin);
shaderTransform = (-offset) * content.Transform.Value * (offset);
}
using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _intermediateSurfaceDpi);
using (var ctx = pictureTarget.CreateDrawingContext(destinationRect.Size))
// Apply destinationRect position
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);
content.Render(ctx, brushTransform);
ctx.PopRenderOptions();
paintWrapper.Paint.FilterQuality = SKFilterQuality.None;
paintWrapper.Paint.Shader = shader;
}
}
using var picture = pictureTarget.GetPicture();
var paintTransform =
tileBrush.TileMode != TileMode.None
? SKMatrix.CreateTranslation(-(float)destinationRect.X, -(float)destinationRect.Y)
: SKMatrix.CreateIdentity();
SKShaderTileMode tileX =
tileBrush.TileMode == TileMode.None
(SKShaderTileMode x, SKShaderTileMode y) GetTileModes(TileMode mode)
{
return (
mode == TileMode.None
? SKShaderTileMode.Decal
: tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY
: mode == TileMode.FlipX || mode == TileMode.FlipXY
? SKShaderTileMode.Mirror
: SKShaderTileMode.Repeat;
: SKShaderTileMode.Repeat,
SKShaderTileMode tileY =
tileBrush.TileMode == TileMode.None
mode == TileMode.None
? SKShaderTileMode.Decal
: tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY
: mode == TileMode.FlipY || mode == TileMode.FlipXY
? SKShaderTileMode.Mirror
: 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;
}
: SKShaderTileMode.Repeat);
}
private static SKColorFilter CreateAlphaColorFilter(double opacity)

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

@ -25,12 +25,14 @@ internal class PictureRenderTarget : IDisposable
_picture = null;
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 canvas = recorder.BeginRecording(new SKRect(0, 0, (float)(size.Width * _dpi.X / 96),
(float)(size.Height * _dpi.Y / 96)));
var canvas = recorder.BeginRecording(new SKRect(0, 0, (float)size.Width,
(float)size.Height));
canvas.RestoreToCount(-1);
canvas.ResetMatrix();
@ -38,7 +40,7 @@ internal class PictureRenderTarget : IDisposable
var createInfo = new DrawingContextImpl.CreateInfo
{
Canvas = canvas,
ScaleDrawingToDpi = true,
ScaleDrawingToDpi = scaleToDpi,
Dpi = _dpi,
DisableSubpixelTextRendering = true,
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