Browse Source

Initial support for cross-testing against WPF (#15150)

* Initial support for cross-testing against WPF

* More cross-tests
pull/15277/head
Nikita Tsukanov 2 years ago
committed by GitHub
parent
commit
ba7405aead
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      Avalonia.Desktop.slnf
  2. 7
      Avalonia.sln
  3. 2
      tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj
  4. 23
      tests/Avalonia.RenderTests.WpfCompare/Avalonia.RenderTests.WpfCompare.csproj
  5. 13
      tests/Avalonia.RenderTests.WpfCompare/CrossFact.cs
  6. 53
      tests/Avalonia.RenderTests.WpfCompare/CrossTestBase.cs
  7. 188
      tests/Avalonia.RenderTests.WpfCompare/CrossUI.Wpf.cs
  8. 3
      tests/Avalonia.RenderTests.WpfCompare/Properties/AssemblyInfo.cs
  9. 54
      tests/Avalonia.RenderTests/CrossTests/Brushes/CrossTileBrushTests.cs
  10. 63
      tests/Avalonia.RenderTests/CrossTests/Brushes/RadialGradientBrushTests.cs
  11. 172
      tests/Avalonia.RenderTests/CrossUI/CrossUI.Avalonia.cs
  12. 203
      tests/Avalonia.RenderTests/CrossUI/CrossUI.cs
  13. 185
      tests/Avalonia.RenderTests/TestBase.cs
  14. 227
      tests/Avalonia.RenderTests/TestRenderHelper.cs
  15. 9
      tests/Avalonia.RenderTests/WpfCompareTestBase.cs
  16. 60
      tests/Avalonia.Skia.RenderTests/CrossTestBase.cs
  17. BIN
      tests/TestFiles/CrossTests/Media/RadialGradientBrush/Transform_Should_Work_As_Expected.wpf.png
  18. BIN
      tests/TestFiles/CrossTests/Media/TileBrushes/Simple_Checkboard_Pattern_Is_Rendered_Identically.wpf.png

1
Avalonia.Desktop.slnf

@ -62,6 +62,7 @@
"tests\\Avalonia.Markup.UnitTests\\Avalonia.Markup.UnitTests.csproj",
"tests\\Avalonia.Markup.Xaml.UnitTests\\Avalonia.Markup.Xaml.UnitTests.csproj",
"tests\\Avalonia.ReactiveUI.UnitTests\\Avalonia.ReactiveUI.UnitTests.csproj",
"tests\\Avalonia.RenderTests.WpfCompare\\Avalonia.RenderTests.WpfCompare.csproj",
"tests\\Avalonia.Skia.RenderTests\\Avalonia.Skia.RenderTests.csproj",
"tests\\Avalonia.Skia.UnitTests\\Avalonia.Skia.UnitTests.csproj",
"tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj",

7
Avalonia.sln

@ -291,6 +291,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildTasks", "BuildTasks",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PInvoke", "tests\TestFiles\BuildTasks\PInvoke\PInvoke.csproj", "{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.RenderTests.WpfCompare", "tests\Avalonia.RenderTests.WpfCompare\Avalonia.RenderTests.WpfCompare.csproj", "{9AE1B827-21AC-4063-AB22-C8804B7F931E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -675,6 +677,10 @@ Global
{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Release|Any CPU.Build.0 = Release|Any CPU
{9AE1B827-21AC-4063-AB22-C8804B7F931E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9AE1B827-21AC-4063-AB22-C8804B7F931E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9AE1B827-21AC-4063-AB22-C8804B7F931E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9AE1B827-21AC-4063-AB22-C8804B7F931E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -758,6 +764,7 @@ Global
{9D6AEF22-221F-4F4B-B335-A4BA510F002C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{5BF0C3B8-E595-4940-AB30-2DA206C2F085} = {9D6AEF22-221F-4F4B-B335-A4BA510F002C}
{0A948D71-99C5-43E9-BACB-B0BA59EA25B4} = {5BF0C3B8-E595-4940-AB30-2DA206C2F085}
{9AE1B827-21AC-4063-AB22-C8804B7F931E} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

2
tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj

@ -1,10 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
<DefineConstants>$(DefineConstants);AVALONIA_D2D</DefineConstants>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Avalonia.RenderTests\**\*.cs" />
<Compile Remove="..\Avalonia.RenderTests\CrossTests\**\*.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="..\Avalonia.RenderTests\**\*.ttf" />

23
tests/Avalonia.RenderTests.WpfCompare/Avalonia.RenderTests.WpfCompare.csproj

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(AvsCurrentWindowsTargetFramework)</TargetFramework>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<Nullable>enable</Nullable>
<UseWpf>true</UseWpf>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
<Compile Include="..\Avalonia.RenderTests\CrossUI\CrossUI.cs"/>
<Compile Include="..\Avalonia.RenderTests\CrossTests\**\*.cs"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Xunit.StaFact" Version="1.2.46-alpha" />
</ItemGroup>
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\ImageSharp.props" />
<Import Project="..\..\build\SkiaSharp.props" />
<Import Project="..\..\build\SharedVersion.props" />
</Project>

13
tests/Avalonia.RenderTests.WpfCompare/CrossFact.cs

@ -0,0 +1,13 @@
using Xunit;
namespace Avalonia.RenderTests.WpfCompare;
public class CrossFactAttribute : StaFactAttribute
{
}
public class CrossTheoryAttribute : StaTheoryAttribute
{
}

53
tests/Avalonia.RenderTests.WpfCompare/CrossTestBase.cs

@ -0,0 +1,53 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using CrossUI;
namespace Avalonia.RenderTests.WpfCompare;
public class CrossTestBase
{
private readonly string _groupName;
public CrossTestBase(string groupName)
{
_groupName = groupName;
}
protected void RenderAndCompare(CrossControl root, [CallerMemberName] string? testName = null, double dpi = 96)
{
var dir = Path.Combine(GetTestsDirectory(), "TestFiles", "CrossTests", _groupName);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
var path = Path.Combine(dir, testName + ".wpf.png");
var w = root.Width;
var h = root.Height;
var pw = (int)Math.Ceiling(w * dpi / 96);
var ph = (int)Math.Ceiling(h * dpi / 96);
var control = new WpfCrossControl(root);
control.Measure(new System.Windows.Size(w, h));
control.Arrange(new System.Windows.Rect(0, 0, w, h));
var bmp = new RenderTargetBitmap(pw, ph, dpi, dpi, PixelFormats.Default);
bmp.Render(control);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bmp));
using (var f = File.Create(path))
encoder.Save(f);
}
static string GetTestsDirectory()
{
var path = Directory.GetCurrentDirectory();
while (path.Length > 0 && Path.GetFileName(path) != "tests")
{
path = Path.GetDirectoryName(path);
}
return path;
}
}

188
tests/Avalonia.RenderTests.WpfCompare/CrossUI.Wpf.cs

@ -0,0 +1,188 @@
using System;
using System.Windows.Controls;
using CrossUI;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using AlignmentX = System.Windows.Media.AlignmentX;
using AlignmentY = System.Windows.Media.AlignmentY;
using Brush = System.Windows.Media.Brush;
using BrushMappingMode = System.Windows.Media.BrushMappingMode;
using Color = Avalonia.Media.Color;
using Drawing = System.Windows.Media.Drawing;
using DrawingBrush = System.Windows.Media.DrawingBrush;
using DrawingCollection = System.Windows.Media.DrawingCollection;
using DrawingContext = System.Windows.Media.DrawingContext;
using DrawingGroup = System.Windows.Media.DrawingGroup;
using DrawingImage = System.Windows.Media.DrawingImage;
using Geometry = System.Windows.Media.Geometry;
using GeometryDrawing = System.Windows.Media.GeometryDrawing;
using MatrixTransform = System.Windows.Media.MatrixTransform;
using Pen = System.Windows.Media.Pen;
using RectangleGeometry = System.Windows.Media.RectangleGeometry;
using SolidColorBrush = System.Windows.Media.SolidColorBrush;
using Stretch = System.Windows.Media.Stretch;
using TileBrush = System.Windows.Media.TileBrush;
using TileMode = System.Windows.Media.TileMode;
using Transform = System.Windows.Media.Transform;
using WPoint = System.Windows.Point;
using WSize = System.Windows.Size;
using WRect = System.Windows.Rect;
using WColor = System.Windows.Media.Color;
using WMatrix = System.Windows.Media.Matrix;
namespace Avalonia.RenderTests.WpfCompare;
internal static class WpfConvertExtensions
{
public static WPoint ToWpf(this Point pt) => new(pt.X, pt.Y);
public static WSize ToWpf(this Size size) => new(size.Width, size.Height);
public static WRect ToWpf(this Rect rect) => new(rect.Left, rect.Top, rect.Width, rect.Height);
public static WColor ToWpf(this Color color) => WColor.FromArgb(color.A, color.R, color.G, color.B);
public static WMatrix ToWpf(this Matrix m) => new WMatrix(m.M11, m.M12, m.M21, m.M22, m.M31, m.M32);
}
internal class WpfCrossControl : Panel
{
private readonly CrossControl _src;
private readonly Dictionary<CrossControl, WpfCrossControl> _children;
public WpfCrossControl(CrossControl src)
{
_src = src;
_children = src.Children.ToDictionary(x => x, x => new WpfCrossControl(x));
Width = src.Bounds.Width;
Height = src.Bounds.Height;
RenderTransform = new MatrixTransform(src.RenderTransform.ToWpf());
foreach (var ch in src.Children)
{
var c = _children[ch];
this.Children.Add(c);
}
}
protected override WSize MeasureOverride(WSize availableSize)
{
foreach (var ch in _children)
ch.Value.Measure(ch.Key.Bounds.Size.ToWpf());
return _src.Bounds.Size.ToWpf();
}
protected override WSize ArrangeOverride(WSize finalSize)
{
foreach (var ch in _children)
ch.Value.Arrange(ch.Key.Bounds.ToWpf());
return base.ArrangeOverride(finalSize);
}
protected override void OnRender(DrawingContext context)
{
_src.Render(new WpfCrossDrawingContext(context));
}
}
internal class WpfCrossDrawingContext : ICrossDrawingContext
{
private readonly DrawingContext _ctx;
public WpfCrossDrawingContext(DrawingContext ctx)
{
_ctx = ctx;
}
private static Transform? ConvertTransform(Matrix? m) => m == null ? null : new MatrixTransform(m.Value.ToWpf());
private static Geometry ConvertGeometry(CrossGeometry g)
{
if (g is CrossRectangleGeometry rg)
return new RectangleGeometry(rg.Rect.ToWpf());
else if (g is CrossSvgGeometry svg)
return Geometry.Parse(svg.Path);
else if (g is CrossEllipseGeometry ellipse)
return new EllipseGeometry(ellipse.Rect.ToWpf());
throw new NotSupportedException();
}
private static Drawing ConvertDrawing(CrossDrawing src)
{
if (src is CrossDrawingGroup g)
return new DrawingGroup() { Children = new DrawingCollection(g.Children.Select(ConvertDrawing)) };
if (src is CrossGeometryDrawing geo)
return new GeometryDrawing()
{
Geometry = ConvertGeometry(geo.Geometry), Brush = ConvertBrush(geo.Brush), Pen = ConvertPen(geo.Pen)
};
throw new NotSupportedException();
}
private static Brush? ConvertBrush(CrossBrush? brush)
{
if (brush == null)
return null;
static Brush Sync(Brush dst, CrossBrush src)
{
dst.Opacity = src.Opacity;
dst.Transform = ConvertTransform(src.Transform);
dst.RelativeTransform = ConvertTransform(src.RelativeTransform);
return dst;
}
static Brush SyncTile(TileBrush dst, CrossTileBrush src)
{
dst.Stretch = (Stretch)src.Stretch;
dst.AlignmentX = (AlignmentX)src.AlignmentX;
dst.AlignmentY = (AlignmentY)src.AlignmentY;
dst.TileMode = (TileMode)src.TileMode;
dst.Viewbox = src.Viewbox.ToWpf();
dst.ViewboxUnits = (BrushMappingMode)src.ViewboxUnits;
dst.Viewport = src.Viewport.ToWpf();
dst.ViewportUnits = (BrushMappingMode)src.ViewportUnits;
return Sync(dst, src);
}
static Brush SyncGradient(GradientBrush dst, CrossGradientBrush src)
{
dst.MappingMode = (BrushMappingMode)src.MappingMode;
dst.SpreadMethod = (GradientSpreadMethod)src.SpreadMethod;
dst.GradientStops =
new GradientStopCollection(src.GradientStops.Select(s => new GradientStop(s.Color.ToWpf(), s.Offset)));
return Sync(dst, src);
}
if (brush is CrossSolidColorBrush br)
return Sync(new SolidColorBrush(br.Color.ToWpf()), brush);
if (brush is CrossDrawingBrush db)
return SyncTile(new DrawingBrush(ConvertDrawing(db.Drawing)), db);
if (brush is CrossRadialGradientBrush radial)
return SyncGradient(new RadialGradientBrush()
{
RadiusX = radial.RadiusX,
RadiusY = radial.RadiusY,
Center = radial.Center.ToWpf(),
GradientOrigin = radial.GradientOrigin.ToWpf()
}, radial);
throw new NotSupportedException();
}
private static Pen? ConvertPen(CrossPen? pen)
{
if (pen == null)
return null;
return new Pen(ConvertBrush(pen.Brush), pen.Thickness);
}
private static ImageSource ConvertImage(CrossImage image)
{
if (image is CrossBitmapImage bi)
return new BitmapImage(new Uri(bi.Path, UriKind.Absolute));
if (image is CrossDrawingImage di)
return new DrawingImage(ConvertDrawing(di.Drawing));
throw new NotSupportedException();
}
public void DrawRectangle(CrossBrush? brush, CrossPen? pen, Rect rc) => _ctx.DrawRectangle(ConvertBrush(brush), ConvertPen(pen), rc.ToWpf());
public void DrawGeometry(CrossBrush? brush, CrossPen? pen, CrossGeometry geo) =>
_ctx.DrawGeometry(ConvertBrush(brush), ConvertPen(pen), ConvertGeometry(geo));
public void DrawImage(CrossImage image, Rect rc) => _ctx.DrawImage(ConvertImage(image), rc.ToWpf());
}

3
tests/Avalonia.RenderTests.WpfCompare/Properties/AssemblyInfo.cs

@ -0,0 +1,3 @@
using Xunit;
[assembly: CollectionBehavior(DisableTestParallelization = true)]

54
tests/Avalonia.RenderTests/CrossTests/Brushes/CrossTileBrushTests.cs

@ -0,0 +1,54 @@
using Avalonia.Media;
using CrossUI;
using Xunit;
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests;
#elif AVALONIA_D2D
namespace Avalonia.Direct2D1.RenderTests;
#else
namespace Avalonia.RenderTests.WpfCompare;
#endif
public class CrossTileBrushTests : CrossTestBase
{
public CrossTileBrushTests() : base("Media/TileBrushes")
{
}
[CrossFact]
public void Simple_Checkboard_Pattern_Is_Rendered_Identically()
{
RenderAndCompare(new CrossControl()
{
Width = 100,
Height = 100,
Background = new CrossDrawingBrush()
{
Drawing = new CrossDrawingGroup()
{
Children =
{
new CrossGeometryDrawing(new CrossRectangleGeometry(new(0, 0, 20, 20)))
{
Brush = new CrossSolidColorBrush(Colors.White)
},
new CrossGeometryDrawing(new CrossRectangleGeometry(new(0, 0, 10, 10)))
{
Brush = new CrossSolidColorBrush(Colors.Black)
},
new CrossGeometryDrawing(new CrossRectangleGeometry(new(10, 10, 10, 10)))
{
Brush = new CrossSolidColorBrush(Colors.Black)
},
}
},
Viewport = new Rect(0, 0, 10, 10),
ViewportUnits = BrushMappingMode.Absolute,
TileMode = TileMode.Tile
}
});
}
}

63
tests/Avalonia.RenderTests/CrossTests/Brushes/RadialGradientBrushTests.cs

@ -0,0 +1,63 @@
using Avalonia.Media;
using CrossUI;
using Xunit;
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests;
#elif AVALONIA_D2D
namespace Avalonia.Direct2D1.RenderTests;
#else
namespace Avalonia.RenderTests.WpfCompare;
#endif
public class CrossRadialGradientBrushTests : CrossTestBase
{
public CrossRadialGradientBrushTests() : base("Media/RadialGradientBrush")
{
}
[CrossFact]
public void Transform_Should_Work_As_Expected()
{
RenderAndCompare(
new CrossControl()
{
Children =
{
new CrossFuncControl(ctx =>
{
var geo = new CrossEllipseGeometry(new Rect(3.430200000000003, 29.019099999999998, 42.7692,
19.6732));
ctx.DrawGeometry(new CrossSolidColorBrush(Colors.Magenta), null, geo);
ctx.DrawGeometry(
new CrossRadialGradientBrush()
{
RadiusX = 12.289,
RadiusY = 12.289,
GradientOrigin = new Point(15.116, 63.965),
Center = new Point(15.116, 63.965),
MappingMode = BrushMappingMode.Absolute,
SpreadMethod = GradientSpreadMethod.Pad,
GradientStops =
{
new GradientStop(Colors.Black, 0), new GradientStop(Colors.Transparent, 1)
},
Transform = new Matrix(1.664, 0,
0, 0.75621371,
-0.06567275, -10.272)
}, null, geo);
})
{
Width = 48,
Height = 48,
RenderTransform = Matrix.CreateScale(4, 4)
}
},
Width = 256,
Height = 256,
Background = new CrossSolidColorBrush(Colors.White)
});
}
}

172
tests/Avalonia.RenderTests/CrossUI/CrossUI.Avalonia.cs

@ -0,0 +1,172 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable;
using CrossUI;
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests.CrossUI;
#else
namespace Avalonia.Direct2D1.RenderTests.CrossUI;
#endif
class AvaloniaCrossControl : Control
{
private readonly CrossControl _src;
private readonly Dictionary<CrossControl, AvaloniaCrossControl> _children;
public AvaloniaCrossControl(CrossControl src)
{
_src = src;
_children = src.Children.ToDictionary(x => x, x => new AvaloniaCrossControl(x));
Width = src.Bounds.Width;
Height = src.Bounds.Height;
RenderTransform = new MatrixTransform(src.RenderTransform);
RenderTransformOrigin = new RelativePoint(default, RelativeUnit.Relative);
foreach (var ch in src.Children)
{
var c = _children[ch];
VisualChildren.Add(c);
LogicalChildren.Add(c);
}
}
protected override Size MeasureOverride(Size availableSize)
{
foreach (var ch in _children)
ch.Value.Measure(ch.Key.Bounds.Size);
return _src.Bounds.Size;
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (var ch in _children)
ch.Value.Arrange(ch.Key.Bounds);
return finalSize;
}
public override void Render(DrawingContext context)
{
_src.Render(new AvaloniaCrossDrawingContext(context));
}
}
class AvaloniaCrossDrawingContext : ICrossDrawingContext
{
private readonly DrawingContext _ctx;
public AvaloniaCrossDrawingContext(DrawingContext ctx)
{
_ctx = ctx;
}
static Transform? ConvertTransform(Matrix? m) => m == null ? null : new MatrixTransform(m.Value);
static RelativeRect ConvertRect(Rect rc, BrushMappingMode mode)
=> new RelativeRect(rc,
mode == BrushMappingMode.RelativeToBoundingBox ? RelativeUnit.Relative : RelativeUnit.Absolute);
static RelativePoint ConvertPoint(Point pt, BrushMappingMode mode)
=> new(pt, mode == BrushMappingMode.RelativeToBoundingBox ? RelativeUnit.Relative : RelativeUnit.Absolute);
static RelativeScalar ConvertScalar(double scalar, BrushMappingMode mode)
=> new(scalar, mode == BrushMappingMode.RelativeToBoundingBox ? RelativeUnit.Relative : RelativeUnit.Absolute);
static Geometry ConvertGeometry(CrossGeometry g)
{
if (g is CrossRectangleGeometry rg)
return new RectangleGeometry(rg.Rect);
else if (g is CrossSvgGeometry svg)
return PathGeometry.Parse(svg.Path);
else if (g is CrossEllipseGeometry ellipse)
return new EllipseGeometry(ellipse.Rect);
throw new NotSupportedException();
}
static Drawing ConvertDrawing(CrossDrawing src)
{
if (src is CrossDrawingGroup g)
return new DrawingGroup() { Children = new DrawingCollection(g.Children.Select(ConvertDrawing)) };
if (src is CrossGeometryDrawing geo)
return new GeometryDrawing()
{
Geometry = ConvertGeometry(geo.Geometry), Brush = ConvertBrush(geo.Brush), Pen = ConvertPen(geo.Pen)
};
throw new NotSupportedException();
}
static IBrush? ConvertBrush(CrossBrush? brush)
{
if (brush == null)
return null;
static Brush Sync(Brush dst, CrossBrush src)
{
dst.Opacity = src.Opacity;
dst.Transform = ConvertTransform(src.Transform);
dst.TransformOrigin = new RelativePoint(default, RelativeUnit.Absolute);
if (src.RelativeTransform != null)
throw new PlatformNotSupportedException();
return dst;
}
static Brush SyncTile(TileBrush dst, CrossTileBrush src)
{
dst.Stretch = src.Stretch;
dst.AlignmentX = src.AlignmentX;
dst.AlignmentY = src.AlignmentY;
dst.TileMode = src.TileMode;
dst.SourceRect = ConvertRect(src.Viewbox, src.ViewboxUnits);
dst.DestinationRect = ConvertRect(src.Viewport, src.ViewportUnits);
return Sync(dst, src);
}
static Brush SyncGradient(GradientBrush dst, CrossGradientBrush src)
{
dst.GradientStops = new GradientStops();
dst.GradientStops.AddRange(src.GradientStops);
dst.SpreadMethod = src.SpreadMethod;
return Sync(dst, src);
}
if (brush is CrossSolidColorBrush br)
return Sync(new SolidColorBrush(br.Color), brush);
if (brush is CrossDrawingBrush db)
return SyncTile(new DrawingBrush(ConvertDrawing(db.Drawing)), db);
if (brush is CrossRadialGradientBrush radial)
return SyncGradient(
new RadialGradientBrush()
{
Center = ConvertPoint(radial.Center, radial.MappingMode),
GradientOrigin = ConvertPoint(radial.GradientOrigin, radial.MappingMode),
RadiusX = ConvertScalar(radial.RadiusX, radial.MappingMode),
RadiusY = ConvertScalar(radial.RadiusY, radial.MappingMode)
}, radial);
throw new NotSupportedException();
}
static IPen? ConvertPen(CrossPen? pen)
{
if (pen == null)
return null;
return new Pen(ConvertBrush(pen.Brush), pen.Thickness);
}
static IImage ConvertImage(CrossImage image)
{
if (image is CrossBitmapImage bi)
return new Bitmap(bi.Path);
if (image is CrossDrawingImage di)
return new DrawingImage(ConvertDrawing(di.Drawing));
throw new NotSupportedException();
}
public void DrawRectangle(CrossBrush? brush, CrossPen? pen, Rect rc) => _ctx.DrawRectangle(ConvertBrush(brush), ConvertPen(pen), rc);
public void DrawGeometry(CrossBrush? brush, CrossPen? pen, CrossGeometry geometry) =>
_ctx.DrawGeometry(ConvertBrush(brush), ConvertPen(pen), ConvertGeometry(geometry));
public void DrawImage(CrossImage image, Rect rc) => _ctx.DrawImage(ConvertImage(image), rc);
}

203
tests/Avalonia.RenderTests/CrossUI/CrossUI.cs

@ -0,0 +1,203 @@
// ReSharper disable RedundantNameQualifier
#nullable enable
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia;
namespace CrossUI;
public class CrossBrush
{
public double Opacity = 1;
public Avalonia.Matrix? Transform;
public Avalonia.Matrix? RelativeTransform;
}
public class CrossSolidColorBrush : CrossBrush
{
public Avalonia.Media.Color Color = Avalonia.Media.Colors.Black;
public CrossSolidColorBrush()
{
}
public CrossSolidColorBrush(Avalonia.Media.Color color)
{
Color = color;
}
}
public class CrossGradientBrush : CrossBrush
{
public List<GradientStop> GradientStops = new();
public Avalonia.Media.GradientSpreadMethod SpreadMethod;
public BrushMappingMode MappingMode;
}
public class CrossRadialGradientBrush : CrossGradientBrush
{
public Avalonia.Point Center;
public Avalonia.Point GradientOrigin;
public double RadiusX, RadiusY;
}
public class CrossTileBrush : CrossBrush
{
public Avalonia.Media.AlignmentX AlignmentX = AlignmentX.Center;
public Avalonia.Media.AlignmentY AlignmentY = AlignmentY.Center;
public Avalonia.Media.Stretch Stretch = Stretch.Fill;
public Avalonia.Media.TileMode TileMode = TileMode.None;
public Rect Viewbox = new Rect(0, 0, 1, 1);
public Avalonia.Media.BrushMappingMode ViewboxUnits = BrushMappingMode.RelativeToBoundingBox;
public Rect Viewport = new Rect(0, 0, 1, 1);
public Avalonia.Media.BrushMappingMode ViewportUnits = BrushMappingMode.RelativeToBoundingBox;
}
public abstract class CrossDrawing
{
}
public class CrossGeometryDrawing : CrossDrawing
{
public CrossGeometry Geometry;
public CrossBrush? Brush;
public CrossPen? Pen;
public CrossGeometryDrawing(CrossGeometry geometry)
{
Geometry = geometry;
}
}
public class CrossDrawingGroup : CrossDrawing
{
public List<CrossDrawing> Children = new();
}
public abstract class CrossGeometry
{
}
public class CrossSvgGeometry : CrossGeometry
{
public string Path;
public CrossSvgGeometry(string path)
{
Path = path;
}
}
public class CrossEllipseGeometry : CrossGeometry
{
public CrossEllipseGeometry(Rect rect)
{
Rect = rect;
}
public CrossEllipseGeometry()
{
}
public Rect Rect { get; set; }
}
public class CrossRectangleGeometry : CrossGeometry
{
public Rect Rect;
public CrossRectangleGeometry(Rect rect)
{
Rect = rect;
}
}
public class CrossDrawingBrush : CrossTileBrush
{
public CrossDrawing Drawing;
}
public class CrossPen
{
public CrossBrush Brush;
public double Thickness = 1;
}
public interface ICrossDrawingContext
{
void DrawRectangle(CrossBrush? brush, CrossPen? pen, Rect rc);
void DrawGeometry(CrossBrush? brush, CrossPen? pen, CrossGeometry geometry);
void DrawImage(CrossImage image, Rect rc);
}
public abstract class CrossImage
{
}
public class CrossBitmapImage : CrossImage
{
public string Path;
public CrossBitmapImage(string path)
{
Path = path;
}
}
public class CrossDrawingImage : CrossImage
{
public CrossDrawing Drawing;
}
public class CrossControl
{
public Rect Bounds => new Rect(Left, Top, Width, Height);
public double Left, Top, Width, Height;
public CrossBrush? Background;
public CrossPen? Outline;
public List<CrossControl> Children = new();
public Matrix RenderTransform = Matrix.Identity;
public virtual void Render(ICrossDrawingContext ctx)
{
var rc = new Rect(Bounds.Size);
if (Background != null || Outline != null)
ctx.DrawRectangle(Background, Outline, rc);
}
}
public class CrossFuncControl : CrossControl
{
private readonly Action<ICrossDrawingContext> _render;
public CrossFuncControl(Action<ICrossDrawingContext> render)
{
_render = render;
}
public override void Render(ICrossDrawingContext ctx)
{
base.Render(ctx);
_render(ctx);
}
}
public class CrossImageControl : CrossControl
{
public CrossImage Image;
public override void Render(ICrossDrawingContext ctx)
{
base.Render(ctx);
var rc = new Rect(Bounds.Size);
ctx.DrawImage(Image, rc);
}
}

185
tests/Avalonia.RenderTests/TestBase.cs

@ -42,27 +42,6 @@ namespace Avalonia.Direct2D1.RenderTests
#endif
public static FontFamily TestFontFamily = new FontFamily(s_fontUri);
private static readonly TestDispatcherImpl threadingInterface =
new TestDispatcherImpl();
private static readonly IAssetLoader assetLoader = new StandardAssetLoader();
static TestBase()
{
#if AVALONIA_SKIA
SkiaPlatform.Initialize();
#else
Direct2D1Platform.Initialize();
#endif
AvaloniaLocator.CurrentMutable
.Bind<IDispatcherImpl>()
.ToConstant(threadingInterface);
AvaloniaLocator.CurrentMutable
.Bind<IAssetLoader>()
.ToConstant(assetLoader);
}
public TestBase(string outputPath)
{
outputPath = outputPath.Replace('\\', Path.DirectorySeparatorChar);
@ -75,7 +54,7 @@ namespace Avalonia.Direct2D1.RenderTests
#endif
OutputPath = Path.Combine(testFiles, platform, outputPath);
threadingInterface.MainThread = Thread.CurrentThread;
TestRenderHelper.BeginTest();
}
public string OutputPath
@ -83,64 +62,17 @@ namespace Avalonia.Direct2D1.RenderTests
get;
}
protected Task RenderToFile(Control target, [CallerMemberName] string testName = "", double dpi = 96)
protected async Task RenderToFile(Control target, [CallerMemberName] string testName = "", double dpi = 96)
{
if (!Directory.Exists(OutputPath))
{
Directory.CreateDirectory(OutputPath);
}
var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png");
var compositedPath = Path.Combine(OutputPath, testName + ".composited.out.png");
var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
var pixelSize = new PixelSize((int)target.Width, (int)target.Height);
var size = new Size(target.Width, target.Height);
var dpiVector = new Vector(dpi, dpi);
using (RenderTargetBitmap bitmap = new RenderTargetBitmap(pixelSize, dpiVector))
{
target.Measure(size);
target.Arrange(new Rect(size));
bitmap.Render(target);
bitmap.Save(immediatePath);
}
var timer = new ManualRenderTimer();
var compositor = new Compositor(new RenderLoop(timer), null, true,
new DispatcherCompositorScheduler(), true, Dispatcher.UIThread);
using (var writableBitmap = factory.CreateWriteableBitmap(pixelSize, dpiVector, factory.DefaultPixelFormat, factory.DefaultAlphaFormat))
{
var root = new TestRenderRoot(dpiVector.X / 96, null!);
using (var renderer = new CompositingRenderer(root, compositor, () => new[]
{
new BitmapFramebufferSurface(writableBitmap)
}))
{
root.Initialize(renderer, target);
renderer.Start();
Dispatcher.UIThread.RunJobs();
renderer.Paint(new Rect(root.Bounds.Size), false);
}
writableBitmap.Save(compositedPath);
}
return Task.CompletedTask;
}
class BitmapFramebufferSurface : IFramebufferPlatformSurface
{
private readonly IWriteableBitmapImpl _bitmap;
public BitmapFramebufferSurface(IWriteableBitmapImpl bitmap)
{
_bitmap = bitmap;
}
public IFramebufferRenderTarget CreateFramebufferRenderTarget()
{
return new FuncFramebufferRenderTarget(() => _bitmap.Lock());
}
await TestRenderHelper.RenderToFile(target, immediatePath, true, dpi);
await TestRenderHelper.RenderToFile(target, compositedPath, false, dpi);
}
protected void CompareImages([CallerMemberName] string testName = "",
@ -156,7 +88,7 @@ namespace Avalonia.Direct2D1.RenderTests
{
if (!skipImmediate)
{
var immediateError = CompareImages(immediate!, expected);
var immediateError = TestRenderHelper.CompareImages(immediate!, expected);
if (immediateError > 0.022)
{
Assert.True(false, immediatePath + ": Error = " + immediateError);
@ -165,7 +97,7 @@ namespace Avalonia.Direct2D1.RenderTests
if (!skipCompositor)
{
var compositedError = CompareImages(composited!, expected);
var compositedError = TestRenderHelper.CompareImages(composited!, expected);
if (compositedError > 0.022)
{
Assert.True(false, compositedPath + ": Error = " + compositedError);
@ -178,108 +110,11 @@ namespace Avalonia.Direct2D1.RenderTests
{
var expectedPath = Path.Combine(OutputPath, (expectedName ?? testName) + ".expected.png");
var actualPath = Path.Combine(OutputPath, testName + ".out.png");
using (var expected = Image.Load<Rgba32>(expectedPath))
using (var actual = Image.Load<Rgba32>(actualPath))
{
double immediateError = CompareImages(actual, expected);
if (immediateError > 0.022)
{
Assert.True(false, actualPath + ": Error = " + immediateError);
}
}
TestRenderHelper.AssertCompareImages(actualPath, expectedPath);
}
/// <summary>
/// Calculates root mean square error for given two images.
/// Based roughly on ImageMagick implementation to ensure consistency.
/// </summary>
private static double CompareImages(Image<Rgba32> actual, Image<Rgba32> expected)
{
if (actual.Width != expected.Width || actual.Height != expected.Height)
{
throw new ArgumentException("Images have different resolutions");
}
var quantity = actual.Width * actual.Height;
double squaresError = 0;
const double scale = 1 / 255d;
for (var x = 0; x < actual.Width; x++)
{
double localError = 0;
for (var y = 0; y < actual.Height; y++)
{
var expectedAlpha = expected[x, y].A * scale;
var actualAlpha = actual[x, y].A * scale;
var r = scale * (expectedAlpha * expected[x, y].R - actualAlpha * actual[x, y].R);
var g = scale * (expectedAlpha * expected[x, y].G - actualAlpha * actual[x, y].G);
var b = scale * (expectedAlpha * expected[x, y].B - actualAlpha * actual[x, y].B);
var a = expectedAlpha - actualAlpha;
var error = r * r + g * g + b * b + a * a;
localError += error;
}
squaresError += localError;
}
var meanSquaresError = squaresError / quantity;
const int channelCount = 4;
meanSquaresError = meanSquaresError / channelCount;
return Math.Sqrt(meanSquaresError);
}
private static string GetTestsDirectory()
{
var path = Directory.GetCurrentDirectory();
private static string GetTestsDirectory() => TestRenderHelper.GetTestsDirectory();
while (path.Length > 0 && Path.GetFileName(path) != "tests")
{
path = Path.GetDirectoryName(path);
}
return path;
}
private class TestDispatcherImpl : IDispatcherImpl
{
public bool CurrentThreadIsLoopThread => MainThread.ManagedThreadId == Thread.CurrentThread.ManagedThreadId;
public Thread MainThread { get; set; }
#pragma warning disable 67
public event Action Signaled;
public event Action Timer;
#pragma warning restore 67
public void Signal()
{
// No-op
}
public long Now => 0;
public void UpdateTimer(long? dueTimeInMs)
{
// No-op
}
}
public void Dispose()
{
if (Dispatcher.UIThread.CheckAccess())
{
Dispatcher.UIThread.RunJobs();
}
}
public void Dispose() => TestRenderHelper.EndTest();
}
}

227
tests/Avalonia.RenderTests/TestRenderHelper.cs

@ -0,0 +1,227 @@
using System.IO;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using Avalonia.Rendering;
using SixLabors.ImageSharp;
using Xunit;
using Avalonia.Platform;
using System.Threading.Tasks;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Threading;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Media;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Avalonia.Utilities;
using SixLabors.ImageSharp.PixelFormats;
using Image = SixLabors.ImageSharp.Image;
#if AVALONIA_SKIA
using Avalonia.Skia;
#else
using Avalonia.Direct2D1;
#endif
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests;
#else
namespace Avalonia.Direct2D1.RenderTests;
#endif
static class TestRenderHelper
{
private static readonly TestDispatcherImpl s_dispatcherImpl =
new TestDispatcherImpl();
static TestRenderHelper()
{
#if AVALONIA_SKIA
SkiaPlatform.Initialize();
#else
Direct2D1Platform.Initialize();
#endif
AvaloniaLocator.CurrentMutable
.Bind<IDispatcherImpl>()
.ToConstant(s_dispatcherImpl);
AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(new StandardAssetLoader());
}
public static Task RenderToFile(Control target, string path, bool immediate, double dpi = 96)
{
var dir = Path.GetDirectoryName(path);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
var pixelSize = new PixelSize((int)target.Width, (int)target.Height);
var size = new Size(target.Width, target.Height);
var dpiVector = new Vector(dpi, dpi);
if (immediate)
{
using (RenderTargetBitmap bitmap = new RenderTargetBitmap(pixelSize, dpiVector))
{
target.Measure(size);
target.Arrange(new Rect(size));
bitmap.Render(target);
bitmap.Save(path);
}
}
else
{
var timer = new ManualRenderTimer();
var compositor = new Compositor(new RenderLoop(timer), null, true,
new DispatcherCompositorScheduler(), true, Dispatcher.UIThread);
using (var writableBitmap = factory.CreateWriteableBitmap(pixelSize, dpiVector, factory.DefaultPixelFormat,
factory.DefaultAlphaFormat))
{
var root = new TestRenderRoot(dpiVector.X / 96, null!);
using (var renderer = new CompositingRenderer(root, compositor,
() => new[] { new BitmapFramebufferSurface(writableBitmap) }))
{
root.Initialize(renderer, target);
renderer.Start();
Dispatcher.UIThread.RunJobs();
renderer.Paint(new Rect(root.Bounds.Size), false);
}
writableBitmap.Save(path);
}
}
return Task.CompletedTask;
}
class BitmapFramebufferSurface : IFramebufferPlatformSurface
{
private readonly IWriteableBitmapImpl _bitmap;
public BitmapFramebufferSurface(IWriteableBitmapImpl bitmap)
{
_bitmap = bitmap;
}
public IFramebufferRenderTarget CreateFramebufferRenderTarget()
{
return new FuncFramebufferRenderTarget(() => _bitmap.Lock());
}
}
public static void BeginTest()
{
s_dispatcherImpl.MainThread = Thread.CurrentThread;
}
public static void EndTest()
{
if (Dispatcher.UIThread.CheckAccess())
Dispatcher.UIThread.RunJobs();
}
public static string GetTestsDirectory()
{
var path = Directory.GetCurrentDirectory();
while (path.Length > 0 && Path.GetFileName(path) != "tests")
{
path = Path.GetDirectoryName(path);
}
return path;
}
private class TestDispatcherImpl : IDispatcherImpl
{
public bool CurrentThreadIsLoopThread => MainThread.ManagedThreadId == Thread.CurrentThread.ManagedThreadId;
public Thread MainThread { get; set; }
#pragma warning disable 67
public event Action Signaled;
public event Action Timer;
#pragma warning restore 67
public void Signal()
{
// No-op
}
public long Now => 0;
public void UpdateTimer(long? dueTimeInMs)
{
// No-op
}
}
public static void AssertCompareImages(string actualPath, string expectedPath)
{
using (var expected = Image.Load<Rgba32>(expectedPath))
using (var actual = Image.Load<Rgba32>(actualPath))
{
double immediateError = TestRenderHelper.CompareImages(actual, expected);
if (immediateError > 0.022)
{
Assert.True(false, actualPath + ": Error = " + immediateError);
}
}
}
/// <summary>
/// Calculates root mean square error for given two images.
/// Based roughly on ImageMagick implementation to ensure consistency.
/// </summary>
public static double CompareImages(Image<Rgba32> actual, Image<Rgba32> expected)
{
if (actual.Width != expected.Width || actual.Height != expected.Height)
{
throw new ArgumentException("Images have different resolutions");
}
var quantity = actual.Width * actual.Height;
double squaresError = 0;
const double scale = 1 / 255d;
for (var x = 0; x < actual.Width; x++)
{
double localError = 0;
for (var y = 0; y < actual.Height; y++)
{
var expectedAlpha = expected[x, y].A * scale;
var actualAlpha = actual[x, y].A * scale;
var r = scale * (expectedAlpha * expected[x, y].R - actualAlpha * actual[x, y].R);
var g = scale * (expectedAlpha * expected[x, y].G - actualAlpha * actual[x, y].G);
var b = scale * (expectedAlpha * expected[x, y].B - actualAlpha * actual[x, y].B);
var a = expectedAlpha - actualAlpha;
var error = r * r + g * g + b * b + a * a;
localError += error;
}
squaresError += localError;
}
var meanSquaresError = squaresError / quantity;
const int channelCount = 4;
meanSquaresError = meanSquaresError / channelCount;
return Math.Sqrt(meanSquaresError);
}
}

9
tests/Avalonia.RenderTests/WpfCompareTestBase.cs

@ -0,0 +1,9 @@
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests;
#else
namespace Avalonia.Direct2D1.RenderTests;
#endif
class WpfCompareTestBase
{
}

60
tests/Avalonia.Skia.RenderTests/CrossTestBase.cs

@ -0,0 +1,60 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using Avalonia.Skia.RenderTests;
using Avalonia.Skia.RenderTests.CrossUI;
using CrossUI;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests;
#else
namespace Avalonia.Direct2D1.RenderTests;
#endif
class CrossFactAttribute : FactAttribute
{
}
class CrossThreoryAttribute : TheoryAttribute
{
}
public class CrossTestBase : IDisposable
{
private readonly string _groupName;
public CrossTestBase(string groupName)
{
TestRenderHelper.BeginTest();
_groupName = groupName;
}
protected void RenderAndCompare(CrossControl root, [CallerMemberName] string? testName = null, double dpi = 96)
{
var dir = Path.Combine(TestRenderHelper.GetTestsDirectory(), "TestFiles", "CrossTests", _groupName);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
var flavor =
#if AVALONIA_SKIA
"skia";
#else
"d2d";
#endif
var pathBase = Path.Combine(dir, testName);
var renderPath = pathBase + "." + flavor + ".out.png";
var compareWith = pathBase + ".wpf.png";
var control = new AvaloniaCrossControl(root);
TestRenderHelper.RenderToFile(control, renderPath, false, dpi);
TestRenderHelper.AssertCompareImages(renderPath, compareWith);
}
public void Dispose()
{
TestRenderHelper.EndTest();
}
}

BIN
tests/TestFiles/CrossTests/Media/RadialGradientBrush/Transform_Should_Work_As_Expected.wpf.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
tests/TestFiles/CrossTests/Media/TileBrushes/Simple_Checkboard_Pattern_Is_Rendered_Identically.wpf.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

Loading…
Cancel
Save