diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperOffset.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperOffset.cs index 304cda41..1ebcc51c 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperOffset.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperOffset.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; - namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; /// @@ -10,8 +8,6 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; /// internal class ClipperOffset { - // To make the floating point polygons compatible with clipper we have to scale them. - private const float ScalingFactor = 1000F; private readonly PolygonOffsetter polygonClipperOffset; /// @@ -30,16 +26,16 @@ public ClipperOffset(float meterLimit = 2F, float arcTolerance = .25F) public ComplexPolygon Execute(float width) { PathsF solution = new(); - this.polygonClipperOffset.Execute(width * ScalingFactor, solution); + this.polygonClipperOffset.Execute(width, solution); - var polygons = new Polygon[solution.Count]; + Polygon[] polygons = new Polygon[solution.Count]; for (int i = 0; i < solution.Count; i++) { PathF pt = solution[i]; - var points = new PointF[pt.Count]; + PointF[] points = new PointF[pt.Count]; for (int j = 0; j < pt.Count; j++) { - points[j] = pt[j] / ScalingFactor; + points[j] = pt[j]; } polygons[i] = new Polygon(points); @@ -59,7 +55,7 @@ public void AddPath(ReadOnlySpan pathPoints, JointStyle jointStyle, EndC PathF points = new(pathPoints.Length); for (int i = 0; i < pathPoints.Length; i++) { - points.Add((Vector2)pathPoints[i] * ScalingFactor); + points.Add(pathPoints[i]); } this.polygonClipperOffset.AddPath(points, jointStyle, endCapStyle); diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs index 20803dcd..f1970f07 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs @@ -4,6 +4,10 @@ using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; @@ -42,17 +46,17 @@ private enum VertexCategory RightRight, } - internal static ScanEdgeCollection Create(TessellatedMultipolygon multipolygon, MemoryAllocator allocator, int subsampling) + internal static ScanEdgeCollection Create(TessellatedMultipolygon multiPolygon, MemoryAllocator allocator, int subsampling) { // We allocate more than we need, since we don't know how many horizontal edges do we have: - IMemoryOwner buffer = allocator.Allocate(multipolygon.TotalVertexCount); + IMemoryOwner buffer = allocator.Allocate(multiPolygon.TotalVertexCount); - RingWalker walker = new RingWalker(buffer.Memory.Span); + RingWalker walker = new(buffer.Memory.Span); - using IMemoryOwner roundedYBuffer = allocator.Allocate(multipolygon.Max(r => r.Vertices.Length)); + using IMemoryOwner roundedYBuffer = allocator.Allocate(multiPolygon.Max(r => r.Vertices.Length)); Span roundedY = roundedYBuffer.Memory.Span; - foreach (TessellatedMultipolygon.Ring ring in multipolygon) + foreach (TessellatedMultipolygon.Ring ring in multiPolygon) { if (ring.VertexCount < 3) { @@ -82,22 +86,140 @@ internal static ScanEdgeCollection Create(TessellatedMultipolygon multipolygon, static void RoundY(ReadOnlySpan vertices, Span destination, float subsamplingRatio) { - for (int i = 0; i < vertices.Length; i++) + int ri = 0; + if (Avx.IsSupported) { - // for future SIMD impl: - // https://www.ocf.berkeley.edu/~horie/rounding.html - // Avx.RoundToPositiveInfinity() - destination[i] = MathF.Round(vertices[i].Y * subsamplingRatio, MidpointRounding.AwayFromZero) / subsamplingRatio; + // If the length of the input buffer as a float array is a multiple of 16, we can use AVX instructions: + int verticesLengthInFloats = vertices.Length * 2; + int vector256FloatCount_x2 = Vector256.Count * 2; + int remainder = verticesLengthInFloats % vector256FloatCount_x2; + int verticesLength = verticesLengthInFloats - remainder; + + if (verticesLength > 0) + { + ri = vertices.Length - (remainder / 2); + float maxIterations = verticesLength / (Vector256.Count * 2); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vertices)); + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + Vector256 ssRatio = Vector256.Create(subsamplingRatio); + Vector256 inverseSsRatio = Vector256.Create(1F / subsamplingRatio); + Vector256 half = Vector256.Create(.5F); + + // For every 1 vector we add to the destination we read 2 from the vertices. + for (nint i = 0, j = 0; i < maxIterations; i++, j += 2) + { + // Load 8 PointF + Vector256 points1 = Unsafe.Add(ref sourceBase, j); + Vector256 points2 = Unsafe.Add(ref sourceBase, j + 1); + + // Shuffle the points to group the Y properties + Vector128 points1Y = Sse.Shuffle(points1.GetLower(), points1.GetUpper(), 0b11_01_11_01); + Vector128 points2Y = Sse.Shuffle(points2.GetLower(), points2.GetUpper(), 0b11_01_11_01); + Vector256 pointsY = Vector256.Create(points1Y, points2Y); + + // Multiply by the subsampling ratio, round, then multiply by the inverted subsampling ratio and assign. + // https://www.ocf.berkeley.edu/~horie/rounding.html + Vector256 rounded = Avx.RoundToPositiveInfinity(Avx.Subtract(Avx.Multiply(pointsY, ssRatio), half)); + Unsafe.Add(ref destinationBase, i) = Avx.Multiply(rounded, inverseSsRatio); + } + } + } + else if (Sse41.IsSupported) + { + // If the length of the input buffer as a float array is a multiple of 8, we can use Sse instructions: + int verticesLengthInFloats = vertices.Length * 2; + int vector128FloatCount_x2 = Vector128.Count * 2; + int remainder = verticesLengthInFloats % vector128FloatCount_x2; + int verticesLength = verticesLengthInFloats - remainder; + + if (verticesLength > 0) + { + ri = vertices.Length - (remainder / 2); + float maxIterations = verticesLength / (Vector128.Count * 2); + ref Vector128 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vertices)); + ref Vector128 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + Vector128 ssRatio = Vector128.Create(subsamplingRatio); + Vector128 inverseSsRatio = Vector128.Create(1F / subsamplingRatio); + Vector128 half = Vector128.Create(.5F); + + // For every 1 vector we add to the destination we read 2 from the vertices. + for (nint i = 0, j = 0; i < maxIterations; i++, j += 2) + { + // Load 4 PointF + Vector128 points1 = Unsafe.Add(ref sourceBase, j); + Vector128 points2 = Unsafe.Add(ref sourceBase, j + 1); + + // Shuffle the points to group the Y properties + Vector128 pointsY = Sse.Shuffle(points1, points2, 0b11_01_11_01); + + // Multiply by the subsampling ratio, round, then multiply by the inverted subsampling ratio and assign. + // https://www.ocf.berkeley.edu/~horie/rounding.html + Vector128 rounded = Sse41.RoundToPositiveInfinity(Sse.Subtract(Sse.Multiply(pointsY, ssRatio), half)); + Unsafe.Add(ref destinationBase, i) = Sse.Multiply(rounded, inverseSsRatio); + } + } + } + else if (AdvSimd.IsSupported) + { + // If the length of the input buffer as a float array is a multiple of 8, we can use AdvSimd instructions: + int verticesLengthInFloats = vertices.Length * 2; + int vector128FloatCount_x2 = Vector128.Count * 2; + int remainder = verticesLengthInFloats % vector128FloatCount_x2; + int verticesLength = verticesLengthInFloats - remainder; + + if (verticesLength > 0) + { + ri = vertices.Length - (remainder / 2); + float maxIterations = verticesLength / (Vector128.Count * 2); + ref Vector128 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vertices)); + ref Vector128 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + Vector128 ssRatio = Vector128.Create(subsamplingRatio); + Vector128 inverseSsRatio = Vector128.Create(1F / subsamplingRatio); + + // For every 1 vector we add to the destination we read 2 from the vertices. + for (nint i = 0, j = 0; i < maxIterations; i++, j += 2) + { + // Load 4 PointF + Vector128 points1 = Unsafe.Add(ref sourceBase, j); + Vector128 points2 = Unsafe.Add(ref sourceBase, j + 1); + + // Shuffle the points to group the Y properties + Vector128 pointsY = AdvSimdShuffle(points1, points2, 0b11_01_11_01); + + // Multiply by the subsampling ratio, round, then multiply by the inverted subsampling ratio and assign. + Vector128 rounded = AdvSimd.RoundAwayFromZero(AdvSimd.Multiply(pointsY, ssRatio)); + Unsafe.Add(ref destinationBase, i) = AdvSimd.Multiply(rounded, inverseSsRatio); + } + } + } + + for (; ri < vertices.Length; ri++) + { + destination[ri] = MathF.Round(vertices[ri].Y * subsamplingRatio, MidpointRounding.AwayFromZero) / subsamplingRatio; } } return new ScanEdgeCollection(buffer, walker.EdgeCounter); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 AdvSimdShuffle(Vector128 a, Vector128 b, byte control) + { + Vector128 result = Vector128.Create(AdvSimd.Extract(a, (byte)(control & 0x3))); + result = AdvSimd.Insert(result, 1, AdvSimd.Extract(a, (byte)((control >> 2) & 0x3))); + result = AdvSimd.Insert(result, 2, AdvSimd.Extract(b, (byte)((control >> 4) & 0x3))); + result = AdvSimd.Insert(result, 3, AdvSimd.Extract(b, (byte)((control >> 6) & 0x3))); + + return result; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static VertexCategory CreateVertexCategory(EdgeCategory previousCategory, EdgeCategory currentCategory) { - var value = (VertexCategory)(((int)previousCategory << 2) | (int)currentCategory); + VertexCategory value = (VertexCategory)(((int)previousCategory << 2) | (int)currentCategory); VerifyVertexCategory(value); return value; } @@ -106,7 +228,7 @@ private static VertexCategory CreateVertexCategory(EdgeCategory previousCategory private static void VerifyVertexCategory(VertexCategory vertexCategory) { int value = (int)vertexCategory; - if (value < 0 || value >= 16) + if (value is < 0 or >= 16) { throw new ArgumentOutOfRangeException(nameof(vertexCategory), "EdgeCategoryPair value shall be: 0 <= value < 16"); } @@ -151,7 +273,7 @@ public EdgeData(float startX, float endX, float startYRounded, float endYRounded public void EmitScanEdge(Span edges, ref int edgeCounter) { - if (this.EdgeCategory == EdgeCategory.Left || this.EdgeCategory == EdgeCategory.Right) + if (this.EdgeCategory is EdgeCategory.Left or EdgeCategory.Right) { return; } diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index 1f68dd1e..b0dc7c7b 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -19,7 +19,9 @@ - + + + diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/Rounding.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/Rounding.cs new file mode 100644 index 00000000..6d6bc327 --- /dev/null +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/Rounding.cs @@ -0,0 +1,143 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using BenchmarkDotNet.Attributes; + +namespace SixLabors.ImageSharp.Drawing.Benchmarks.Drawing; +public class Rounding +{ + private PointF[] vertices; + private float[] destination; + private float[] destinationSse41; + private float[] destinationAvx; + + [GlobalSetup] + public void Setup() + { + this.vertices = new PointF[1000]; + this.destination = new float[this.vertices.Length]; + this.destinationSse41 = new float[this.vertices.Length]; + this.destinationAvx = new float[this.vertices.Length]; + Random r = new(42); + for (int i = 0; i < this.vertices.Length; i++) + { + this.vertices[i] = new PointF((float)r.NextDouble(), (float)r.NextDouble()); + } + } + + [Benchmark] + public void RoundYAvx() => RoundYAvx(this.vertices, this.destinationAvx, 16); + + [Benchmark] + public void RoundYSse41() => RoundYSse41(this.vertices, this.destinationSse41, 16); + + [Benchmark(Baseline = true)] + public void RoundY() => RoundY(this.vertices, this.destination, 16); + + private static void RoundYAvx(ReadOnlySpan vertices, Span destination, float subsamplingRatio) + { + int ri = 0; + if (Avx.IsSupported) + { + // If the length of the input buffer as a float array is a multiple of 16, we can use AVX instructions: + int verticesLengthInFloats = vertices.Length * 2; + int vector256FloatCount_x2 = Vector256.Count * 2; + int remainder = verticesLengthInFloats % vector256FloatCount_x2; + int verticesLength = verticesLengthInFloats - remainder; + + if (verticesLength > 0) + { + ri = vertices.Length - (remainder / 2); + float maxIterations = verticesLength / (Vector256.Count * 2); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vertices)); + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + Vector256 ssRatio = Vector256.Create(subsamplingRatio); + Vector256 inverseSsRatio = Vector256.Create(1F / subsamplingRatio); + + // For every 1 vector we add to the destination we read 2 from the vertices. + for (nint i = 0, j = 0; i < maxIterations; i++, j += 2) + { + // Load 8 PointF + Vector256 points1 = Unsafe.Add(ref sourceBase, j); + Vector256 points2 = Unsafe.Add(ref sourceBase, j + 1); + + // Shuffle the points to group the Y properties + Vector128 points1Y = Sse.Shuffle(points1.GetLower(), points1.GetUpper(), 0b11_01_11_01); + Vector128 points2Y = Sse.Shuffle(points2.GetLower(), points2.GetUpper(), 0b11_01_11_01); + Vector256 pointsY = Vector256.Create(points1Y, points2Y); + + // Multiply by the subsampling ratio, round, then multiply by the inverted subsampling ratio and assign. + // https://www.ocf.berkeley.edu/~horie/rounding.html + Vector256 rounded = Avx.RoundToPositiveInfinity(Avx.Multiply(pointsY, ssRatio)); + Unsafe.Add(ref destinationBase, i) = Avx.Multiply(rounded, inverseSsRatio); + } + } + } + + for (; ri < vertices.Length; ri++) + { + destination[ri] = MathF.Round(vertices[ri].Y * subsamplingRatio, MidpointRounding.AwayFromZero) / subsamplingRatio; + } + } + + private static void RoundYSse41(ReadOnlySpan vertices, Span destination, float subsamplingRatio) + { + int ri = 0; + if (Sse41.IsSupported) + { + // If the length of the input buffer as a float array is a multiple of 8, we can use Sse instructions: + int verticesLengthInFloats = vertices.Length * 2; + int vector128FloatCount_x2 = Vector128.Count * 2; + int remainder = verticesLengthInFloats % vector128FloatCount_x2; + int verticesLength = verticesLengthInFloats - remainder; + + if (verticesLength > 0) + { + ri = vertices.Length - (remainder / 2); + float maxIterations = verticesLength / (Vector128.Count * 2); + ref Vector128 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vertices)); + ref Vector128 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + Vector128 ssRatio = Vector128.Create(subsamplingRatio); + Vector128 inverseSsRatio = Vector128.Create(1F / subsamplingRatio); + + // For every 1 vector we add to the destination we read 2 from the vertices. + for (nint i = 0, j = 0; i < maxIterations; i++, j += 2) + { + // Load 4 PointF + Vector128 points1 = Unsafe.Add(ref sourceBase, j); + Vector128 points2 = Unsafe.Add(ref sourceBase, j + 1); + + // Shuffle the points to group the Y properties + Vector128 points1Y = Sse.Shuffle(points1, points1, 0b11_01_11_01); + Vector128 points2Y = Sse.Shuffle(points2, points2, 0b11_01_11_01); + Vector128 pointsY = Vector128.Create(points1Y.GetLower(), points2Y.GetLower()); + + // Multiply by the subsampling ratio, round, then multiply by the inverted subsampling ratio and assign. + // https://www.ocf.berkeley.edu/~horie/rounding.html + Vector128 rounded = Sse41.RoundToPositiveInfinity(Sse.Multiply(pointsY, ssRatio)); + Unsafe.Add(ref destinationBase, i) = Sse.Multiply(rounded, inverseSsRatio); + } + } + } + + for (; ri < vertices.Length; ri++) + { + destination[ri] = MathF.Round(vertices[ri].Y * subsamplingRatio, MidpointRounding.AwayFromZero) / subsamplingRatio; + } + } + + private static void RoundY(ReadOnlySpan vertices, Span destination, float subsamplingRatio) + { + int ri = 0; + for (; ri < vertices.Length; ri++) + { + destination[ri] = MathF.Round(vertices[ri].Y * subsamplingRatio, MidpointRounding.AwayFromZero) / subsamplingRatio; + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs index 78fb6eb1..fbc04393 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs @@ -160,10 +160,8 @@ public void LargeGeoJson_Mississippi_Lines(TestImageProvider provider, i image.Mutate(c => c.DrawLine(Color.White, 1.0f, loop)); } - // Very strict tolerance, since the image is sparse (relaxed on .NET Framework) - ImageComparer comparer = TestEnvironment.IsFramework - ? ImageComparer.TolerantPercentage(1e-3f) - : ImageComparer.TolerantPercentage(1e-7f); + // Strict comparer, because the image is sparse: + ImageComparer comparer = ImageComparer.TolerantPercentage(0.0001F); string details = $"PixelOffset({pixelOffset})"; image.DebugSave(provider, details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs index cf807995..70004cff 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs @@ -196,7 +196,7 @@ public void FillPolygon_StarCircle_AllOperations(TestImageProvider provi provider.RunValidatingProcessorTest( c => c.Fill(Color.DeepPink, circle).Fill(Color.LightGray, star).Fill(Color.ForestGreen, shape), - comparer: ImageComparer.TolerantPercentage(0.01f), + comparer: ImageComparer.TolerantPercentage(0.01F), testOutputDetails: operation.ToString(), appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs index 96f8dd91..7927565c 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs @@ -23,7 +23,7 @@ public class DrawTextOnImageTests private static readonly ImageComparer TextDrawingComparer = ImageComparer.TolerantPercentage(1e-2f); - private static readonly ImageComparer OutlinedTextDrawingComparer = ImageComparer.TolerantPercentage(1e-3f); + private static readonly ImageComparer OutlinedTextDrawingComparer = ImageComparer.TolerantPercentage(0.0069F); public DrawTextOnImageTests(ITestOutputHelper output) => this.Output = output; @@ -278,7 +278,7 @@ public void FontShapesAreRenderedCorrectly_LargeText( } // Strict comparer, because the image is sparse: - var comparer = ImageComparer.TolerantPercentage(1e-6f); + var comparer = ImageComparer.TolerantPercentage(0.0001F); provider.VerifyOperation( comparer, diff --git a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj index e28c765d..5767ff84 100644 --- a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj +++ b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj @@ -6,7 +6,7 @@ latest True SixLabors.ImageSharp.Tests - AnyCPU;x64;x86 + AnyCPU;x64;x86;ARM64 SixLabors.ImageSharp.Drawing.Tests @@ -25,6 +25,8 @@ + + diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/ScanEdgeCollectionTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/ScanEdgeCollectionTests.cs index 3c118fe2..0a1dbd4d 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/ScanEdgeCollectionTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/ScanEdgeCollectionTests.cs @@ -11,9 +11,9 @@ public class ScanEdgeCollectionTests { private static MemoryAllocator MemoryAllocator => Configuration.Default.MemoryAllocator; - private static readonly DebugDraw DebugDraw = new DebugDraw(nameof(ScanEdgeCollectionTests)); + private static readonly DebugDraw DebugDraw = new(nameof(ScanEdgeCollectionTests)); - private void VerifyEdge( + private static void VerifyEdge( ScanEdgeCollection edges, float y0, float y1, @@ -46,60 +46,65 @@ private void VerifyEdge( [ValidateDisposedMemoryAllocations] public void SimplePolygon_AllEmitCases() { - // see: SimplePolygon_AllEmitCases.png - Polygon polygon = PolygonFactory.CreatePolygon( - (1, 2), - (2, 2), - (3, 1), - (4, 3), - (6, 1), - (7, 2), - (8, 2), - (9, 3), - (9, 4), - (10, 5), - (9, 6), - (8, 6), - (8, 7), - (9, 7), - (9, 8), - (7, 8), - (6, 7), - (5, 8), - (4, 7), - (3, 8), - (2, 8), - (2, 6), - (3, 5), - (2, 5), - (2, 4), - (1, 3)); - - DebugDraw.Polygon(polygon, 1, 100); + static void RunTest() + { + // see: SimplePolygon_AllEmitCases.png + Polygon polygon = PolygonFactory.CreatePolygon( + (1, 2), + (2, 2), + (3, 1), + (4, 3), + (6, 1), + (7, 2), + (8, 2), + (9, 3), + (9, 4), + (10, 5), + (9, 6), + (8, 6), + (8, 7), + (9, 7), + (9, 8), + (7, 8), + (6, 7), + (5, 8), + (4, 7), + (3, 8), + (2, 8), + (2, 6), + (3, 5), + (2, 5), + (2, 4), + (1, 3)); + + DebugDraw.Polygon(polygon, 1, 100); + + using ScanEdgeCollection edges = ScanEdgeCollection.Create(polygon, MemoryAllocator, 16); + + Assert.Equal(19, edges.Edges.Length); + + VerifyEdge(edges, 1f, 2f, (2.5f, 1.5f), 1, 2, true); + VerifyEdge(edges, 1f, 3f, (3.5f, 2f), 1, 1, false); + VerifyEdge(edges, 1f, 3f, (5f, 2f), 1, 1, true); + VerifyEdge(edges, 1f, 2f, (6.5f, 1.5f), 1, 2, false); + VerifyEdge(edges, 2f, 3f, (8.5f, 2.5f), 1, 0, false); + VerifyEdge(edges, 3f, 4f, (9f, 3.5f), 1, 0, false); + VerifyEdge(edges, 4f, 5f, (9.5f, 4.5f), 1, 0, false); + VerifyEdge(edges, 5f, 6f, (9.5f, 5.5f), 1, 1, false); + VerifyEdge(edges, 6f, 7f, (8f, 6.5f), 2, 2, false); + VerifyEdge(edges, 7f, 8f, (9f, 7.5f), 1, 1, false); + VerifyEdge(edges, 7f, 8f, (6.5f, 7.5f), 1, 1, true); + VerifyEdge(edges, 7f, 8f, (5.5f, 7.5f), 1, 1, false); + VerifyEdge(edges, 7f, 8f, (4.5f, 7.5f), 1, 1, true); + VerifyEdge(edges, 7f, 8f, (3.5f, 7.5f), 1, 1, false); + VerifyEdge(edges, 6f, 8f, (2f, 7f), 0, 1, true); + VerifyEdge(edges, 5f, 6f, (2.5f, 5.5f), 2, 1, true); + VerifyEdge(edges, 4f, 5f, (2f, 4.5f), 0, 1, true); + VerifyEdge(edges, 3f, 4f, (1.5f, 3.5f), 0, 1, true); + VerifyEdge(edges, 2f, 3f, (1f, 1.5f), 1, 1, true); + } - using var edges = ScanEdgeCollection.Create(polygon, MemoryAllocator, 16); - - Assert.Equal(19, edges.Edges.Length); - - this.VerifyEdge(edges, 1f, 2f, (2.5f, 1.5f), 1, 2, true); - this.VerifyEdge(edges, 1f, 3f, (3.5f, 2f), 1, 1, false); - this.VerifyEdge(edges, 1f, 3f, (5f, 2f), 1, 1, true); - this.VerifyEdge(edges, 1f, 2f, (6.5f, 1.5f), 1, 2, false); - this.VerifyEdge(edges, 2f, 3f, (8.5f, 2.5f), 1, 0, false); - this.VerifyEdge(edges, 3f, 4f, (9f, 3.5f), 1, 0, false); - this.VerifyEdge(edges, 4f, 5f, (9.5f, 4.5f), 1, 0, false); - this.VerifyEdge(edges, 5f, 6f, (9.5f, 5.5f), 1, 1, false); - this.VerifyEdge(edges, 6f, 7f, (8f, 6.5f), 2, 2, false); - this.VerifyEdge(edges, 7f, 8f, (9f, 7.5f), 1, 1, false); - this.VerifyEdge(edges, 7f, 8f, (6.5f, 7.5f), 1, 1, true); - this.VerifyEdge(edges, 7f, 8f, (5.5f, 7.5f), 1, 1, false); - this.VerifyEdge(edges, 7f, 8f, (4.5f, 7.5f), 1, 1, true); - this.VerifyEdge(edges, 7f, 8f, (3.5f, 7.5f), 1, 1, false); - this.VerifyEdge(edges, 6f, 8f, (2f, 7f), 0, 1, true); - this.VerifyEdge(edges, 5f, 6f, (2.5f, 5.5f), 2, 1, true); - this.VerifyEdge(edges, 4f, 5f, (2f, 4.5f), 0, 1, true); - this.VerifyEdge(edges, 3f, 4f, (1.5f, 3.5f), 0, 1, true); - this.VerifyEdge(edges, 2f, 3f, (1f, 1.5f), 1, 1, true); + FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTest, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableSSE41 | HwIntrinsics.DisableArm64AdvSimd); } [Fact] @@ -113,39 +118,39 @@ public void ComplexPolygon() IPath polygon = contour.Clip(hole); DebugDraw.Polygon(polygon, 1, 100); - using var edges = ScanEdgeCollection.Create(polygon, MemoryAllocator, 16); + using ScanEdgeCollection edges = ScanEdgeCollection.Create(polygon, MemoryAllocator, 16); Assert.Equal(8, edges.Count); - this.VerifyEdge(edges, 1, 4, (1, 2), 1, 1, true); - this.VerifyEdge(edges, 1, 2, (4, 1.5f), 1, 2, false); - this.VerifyEdge(edges, 4, 5, (2, 4.5f), 2, 1, true); - this.VerifyEdge(edges, 2, 5, (5, 3f), 1, 1, false); + VerifyEdge(edges, 1, 4, (1, 2), 1, 1, true); + VerifyEdge(edges, 1, 2, (4, 1.5f), 1, 2, false); + VerifyEdge(edges, 4, 5, (2, 4.5f), 2, 1, true); + VerifyEdge(edges, 2, 5, (5, 3f), 1, 1, false); - this.VerifyEdge(edges, 2, 3, (2, 2.5f), 2, 2, false); - this.VerifyEdge(edges, 2, 3, (3.5f, 2.5f), 2, 1, true); - this.VerifyEdge(edges, 3, 4, (3, 3.5f), 1, 2, false); - this.VerifyEdge(edges, 3, 4, (4, 3.5f), 0, 2, true); + VerifyEdge(edges, 2, 3, (2, 2.5f), 2, 2, false); + VerifyEdge(edges, 2, 3, (3.5f, 2.5f), 2, 1, true); + VerifyEdge(edges, 3, 4, (3, 3.5f), 1, 2, false); + VerifyEdge(edges, 3, 4, (4, 3.5f), 0, 2, true); } [Fact] public void NumericCornerCase_C() { - using var edges = ScanEdgeCollection.Create(NumericCornerCasePolygons.C, MemoryAllocator, 4); + using ScanEdgeCollection edges = ScanEdgeCollection.Create(NumericCornerCasePolygons.C, MemoryAllocator, 4); Assert.Equal(2, edges.Count); - this.VerifyEdge(edges, 3.5f, 4f, (2f, 3.75f), 1, 1, true); - this.VerifyEdge(edges, 3.5f, 4f, (8f, 3.75f), 1, 1, false); + VerifyEdge(edges, 3.5f, 4f, (2f, 3.75f), 1, 1, true); + VerifyEdge(edges, 3.5f, 4f, (8f, 3.75f), 1, 1, false); } [Fact] public void NumericCornerCase_D() { - using var edges = ScanEdgeCollection.Create(NumericCornerCasePolygons.D, MemoryAllocator, 4); + using ScanEdgeCollection edges = ScanEdgeCollection.Create(NumericCornerCasePolygons.D, MemoryAllocator, 4); Assert.Equal(5, edges.Count); - this.VerifyEdge(edges, 3.25f, 4f, (12f, 3.75f), 1, 1, true); - this.VerifyEdge(edges, 3.25f, 3.5f, (15f, 3.375f), 1, 0, false); - this.VerifyEdge(edges, 3.5f, 4f, (18f, 3.75f), 1, 1, false); + VerifyEdge(edges, 3.25f, 4f, (12f, 3.75f), 1, 1, true); + VerifyEdge(edges, 3.25f, 3.5f, (15f, 3.375f), 1, 0, false); + VerifyEdge(edges, 3.5f, 4f, (18f, 3.75f), 1, 1, false); // TODO: verify 2 more edges } @@ -153,15 +158,15 @@ public void NumericCornerCase_D() [Fact] public void NumericCornerCase_H_ShouldCollapseNearZeroEdge() { - using var edges = ScanEdgeCollection.Create(NumericCornerCasePolygons.H, MemoryAllocator, 4); + using ScanEdgeCollection edges = ScanEdgeCollection.Create(NumericCornerCasePolygons.H, MemoryAllocator, 4); Assert.Equal(3, edges.Count); - this.VerifyEdge(edges, 1.75f, 2f, (15f, 1.875f), 1, 1, true); - this.VerifyEdge(edges, 1.75f, 2.25f, (16f, 2f), 1, 1, false); + VerifyEdge(edges, 1.75f, 2f, (15f, 1.875f), 1, 1, true); + VerifyEdge(edges, 1.75f, 2.25f, (16f, 2f), 1, 1, false); // this places two dummy points: - this.VerifyEdge(edges, 2f, 2.25f, (15f, 2.125f), 2, 1, true); + VerifyEdge(edges, 2f, 2.25f, (15f, 2.125f), 2, 1, true); } - private static FuzzyFloat F(float value, float eps) => new FuzzyFloat(value, eps); + private static FuzzyFloat F(float value, float eps) => new(value, eps); } diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/BasicSerializer.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/BasicSerializer.cs new file mode 100644 index 00000000..2b61791f --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/BasicSerializer.cs @@ -0,0 +1,92 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.ComponentModel; +using Xunit.Abstractions; + +namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities; + +/// +/// RemoteExecutor can only execute static methods, which can only consume string arguments, +/// because data is being passed on command line interface. This utility allows serialization +/// of types to strings. +/// +internal class BasicSerializer : IXunitSerializationInfo +{ + private readonly Dictionary map = new(); + + public const char Separator = ':'; + + private string DumpToString(Type type) + { + using MemoryStream ms = new(); + using StreamWriter writer = new(ms); + writer.WriteLine(type.FullName); + foreach (KeyValuePair kv in this.map) + { + writer.WriteLine($"{kv.Key}{Separator}{kv.Value}"); + } + + writer.Flush(); + byte[] data = ms.ToArray(); + return Convert.ToBase64String(data); + } + + private Type LoadDump(string dump) + { + byte[] data = Convert.FromBase64String(dump); + + using MemoryStream ms = new(data); + using StreamReader reader = new(ms); + Type type = Type.GetType(reader.ReadLine()); + for (string s = reader.ReadLine(); s != null; s = reader.ReadLine()) + { + string[] kv = s.Split(Separator); + this.map[kv[0]] = kv[1]; + } + + return type; + } + + public static string Serialize(IXunitSerializable serializable) + { + BasicSerializer serializer = new(); + serializable.Serialize(serializer); + return serializer.DumpToString(serializable.GetType()); + } + + public static T Deserialize(string dump) + where T : IXunitSerializable + { + BasicSerializer serializer = new(); + Type type = serializer.LoadDump(dump); + + T result = (T)Activator.CreateInstance(type); + result.Deserialize(serializer); + return result; + } + + public void AddValue(string key, object value, Type type = null) + { + if (value == null) + { + return; + } + + type ??= value.GetType(); + + this.map[key] = TypeDescriptor.GetConverter(type).ConvertToInvariantString(value); + } + + public object GetValue(string key, Type type) + { + if (!this.map.TryGetValue(key, out string str)) + { + return type.IsValueType ? Activator.CreateInstance(type) : null; + } + + return TypeDescriptor.GetConverter(type).ConvertFromInvariantString(str); + } + + public T GetValue(string key) => (T)this.GetValue(key, typeof(T)); +} diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs new file mode 100644 index 00000000..4b2d9a0c --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs @@ -0,0 +1,465 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Globalization; +using Microsoft.DotNet.RemoteExecutor; +using Xunit.Abstractions; + +namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities; + +/// +/// Allows the testing against specific feature sets. +/// +public static class FeatureTestRunner +{ + private static readonly char[] SplitChars = { ',', ' ' }; + + /// + /// Allows the deserialization of parameters passed to the feature test. + /// + /// + /// This is required because does not allow + /// marshalling of fields so we cannot pass a wrapped + /// allowing automatic deserialization. + /// + /// + /// + /// The type to deserialize to. + /// The string value to deserialize. + /// The value. + public static T DeserializeForXUnit(string value) + where T : IXunitSerializable + => BasicSerializer.Deserialize(value); + + /// + /// Allows the deserialization of types implementing + /// passed to the feature test. + /// + /// The type of object to deserialize. + /// The string value to deserialize. + /// The value. + public static T Deserialize(string value) + where T : IConvertible + => (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture); + + /// + /// Runs the given test within an environment + /// where the given features. + /// + /// The test action to run. + /// The intrinsics features. + public static void RunWithHwIntrinsicsFeature( + Action action, + HwIntrinsics intrinsics) + { + if (!RemoteExecutor.IsSupported) + { + return; + } + + foreach (KeyValuePair intrinsic in intrinsics.ToFeatureKeyValueCollection()) + { + ProcessStartInfo processStartInfo = new(); + if (intrinsic.Key != HwIntrinsics.AllowAll) + { + processStartInfo.Environment[$"DOTNET_{intrinsic.Value}"] = "0"; + + RemoteExecutor.Invoke( + action, + new RemoteInvokeOptions + { + StartInfo = processStartInfo + }) + .Dispose(); + } + else + { + // Since we are running using the default architecture there is no + // point creating the overhead of running the action in a separate process. + action(); + } + } + } + + /// + /// Runs the given test within an environment + /// where the given features. + /// + /// + /// The test action to run. + /// The parameter passed will be a string representing the currently testing . + /// The intrinsics features. + public static void RunWithHwIntrinsicsFeature( + Action action, + HwIntrinsics intrinsics) + { + if (!RemoteExecutor.IsSupported) + { + return; + } + + foreach (KeyValuePair intrinsic in intrinsics.ToFeatureKeyValueCollection()) + { + ProcessStartInfo processStartInfo = new(); + if (intrinsic.Key != HwIntrinsics.AllowAll) + { + processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; + + RemoteExecutor.Invoke( + action, + intrinsic.Key.ToString(), + new RemoteInvokeOptions + { + StartInfo = processStartInfo + }) + .Dispose(); + } + else + { + // Since we are running using the default architecture there is no + // point creating the overhead of running the action in a separate process. + action(intrinsic.Key.ToString()); + } + } + } + + /// + /// Runs the given test within an environment + /// where the given features. + /// + /// The type to deserialize to. + /// The test action to run. + /// The intrinsics features. + /// The value to pass as a parameter to the test action. + public static void RunWithHwIntrinsicsFeature( + Action action, + HwIntrinsics intrinsics, + T serializable) + where T : IXunitSerializable + { + if (!RemoteExecutor.IsSupported) + { + return; + } + + foreach (KeyValuePair intrinsic in intrinsics.ToFeatureKeyValueCollection()) + { + ProcessStartInfo processStartInfo = new(); + if (intrinsic.Key != HwIntrinsics.AllowAll) + { + processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; + + RemoteExecutor.Invoke( + action, + BasicSerializer.Serialize(serializable), + new RemoteInvokeOptions + { + StartInfo = processStartInfo + }) + .Dispose(); + } + else + { + // Since we are running using the default architecture there is no + // point creating the overhead of running the action in a separate process. + action(BasicSerializer.Serialize(serializable)); + } + } + } + + /// + /// Runs the given test within an environment + /// where the given features. + /// + /// The type to deserialize to. + /// The test action to run. + /// The intrinsics features. + /// The value to pass as a parameter to the test action. + public static void RunWithHwIntrinsicsFeature( + Action action, + HwIntrinsics intrinsics, + T serializable) + where T : IXunitSerializable + { + if (!RemoteExecutor.IsSupported) + { + return; + } + + foreach (KeyValuePair intrinsic in intrinsics.ToFeatureKeyValueCollection()) + { + ProcessStartInfo processStartInfo = new(); + if (intrinsic.Key != HwIntrinsics.AllowAll) + { + processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; + + RemoteExecutor.Invoke( + action, + BasicSerializer.Serialize(serializable), + intrinsic.Key.ToString(), + new RemoteInvokeOptions + { + StartInfo = processStartInfo + }) + .Dispose(); + } + else + { + // Since we are running using the default architecture there is no + // point creating the overhead of running the action in a separate process. + action(BasicSerializer.Serialize(serializable), intrinsic.Key.ToString()); + } + } + } + + /// + /// Runs the given test within an environment + /// where the given features. + /// + /// The first type to deserialize to. + /// The second type to deserialize to. + /// The test action to run. + /// The intrinsics features. + /// The value to pass as a parameter to the test action. + /// The second value to pass as a parameter to the test action. + public static void RunWithHwIntrinsicsFeature( + Action action, + HwIntrinsics intrinsics, + T arg1, + T2 arg2) + where T : IXunitSerializable + where T2 : IXunitSerializable + { + if (!RemoteExecutor.IsSupported) + { + return; + } + + foreach (KeyValuePair intrinsic in intrinsics.ToFeatureKeyValueCollection()) + { + ProcessStartInfo processStartInfo = new(); + if (intrinsic.Key != HwIntrinsics.AllowAll) + { + processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; + + RemoteExecutor.Invoke( + action, + BasicSerializer.Serialize(arg1), + BasicSerializer.Serialize(arg2), + new RemoteInvokeOptions + { + StartInfo = processStartInfo + }) + .Dispose(); + } + else + { + // Since we are running using the default architecture there is no + // point creating the overhead of running the action in a separate process. + action(BasicSerializer.Serialize(arg1), BasicSerializer.Serialize(arg2)); + } + } + } + + /// + /// Runs the given test within an environment + /// where the given features. + /// + /// The type to deserialize to. + /// The test action to run. + /// The intrinsics features. + /// The value to pass as a parameter to the test action. + /// The second value to pass as a parameter to the test action. + public static void RunWithHwIntrinsicsFeature( + Action action, + HwIntrinsics intrinsics, + T arg1, + string arg2) + where T : IXunitSerializable + { + if (!RemoteExecutor.IsSupported) + { + return; + } + + foreach (KeyValuePair intrinsic in intrinsics.ToFeatureKeyValueCollection()) + { + ProcessStartInfo processStartInfo = new(); + if (intrinsic.Key != HwIntrinsics.AllowAll) + { + processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; + + RemoteExecutor.Invoke( + action, + BasicSerializer.Serialize(arg1), + arg2, + new RemoteInvokeOptions + { + StartInfo = processStartInfo + }) + .Dispose(); + } + else + { + // Since we are running using the default architecture there is no + // point creating the overhead of running the action in a separate process. + action(BasicSerializer.Serialize(arg1), arg2); + } + } + } + + /// + /// Runs the given test within an environment + /// where the given features. + /// + /// The type to deserialize to. + /// The test action to run. + /// The value to pass as a parameter to the test action. + /// The intrinsics features. + public static void RunWithHwIntrinsicsFeature( + Action action, + T serializable, + HwIntrinsics intrinsics) + where T : IConvertible + { + if (!RemoteExecutor.IsSupported) + { + return; + } + + foreach (KeyValuePair intrinsic in intrinsics.ToFeatureKeyValueCollection()) + { + ProcessStartInfo processStartInfo = new(); + if (intrinsic.Key != HwIntrinsics.AllowAll) + { + processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; + + RemoteExecutor.Invoke( + action, + serializable.ToString(), + new RemoteInvokeOptions + { + StartInfo = processStartInfo + }) + .Dispose(); + } + else + { + // Since we are running using the default architecture there is no + // point creating the overhead of running the action in a separate process. + action(serializable.ToString()); + } + } + } + + /// + /// Runs the given test within an environment + /// where the given features. + /// + /// The type to deserialize to. + /// The test action to run. + /// The value to pass as a parameter #0 to the test action. + /// The value to pass as a parameter #1 to the test action. + /// The intrinsics features. + public static void RunWithHwIntrinsicsFeature( + Action action, + T arg0, + T arg1, + HwIntrinsics intrinsics) + where T : IConvertible + { + if (!RemoteExecutor.IsSupported) + { + return; + } + + foreach (KeyValuePair intrinsic in intrinsics.ToFeatureKeyValueCollection()) + { + ProcessStartInfo processStartInfo = new(); + if (intrinsic.Key != HwIntrinsics.AllowAll) + { + processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; + + RemoteExecutor.Invoke( + action, + arg0.ToString(), + arg1.ToString(), + new RemoteInvokeOptions + { + StartInfo = processStartInfo + }) + .Dispose(); + } + else + { + // Since we are running using the default architecture there is no + // point creating the overhead of running the action in a separate process. + action(arg0.ToString(), arg1.ToString()); + } + } + } + + internal static Dictionary ToFeatureKeyValueCollection(this HwIntrinsics intrinsics) + { + // Loop through and translate the given values into COMPlus equivalents + Dictionary features = new(); + foreach (string intrinsic in intrinsics.ToString("G").Split(SplitChars, StringSplitOptions.RemoveEmptyEntries)) + { + HwIntrinsics key = (HwIntrinsics)Enum.Parse(typeof(HwIntrinsics), intrinsic); + switch (intrinsic) + { + case nameof(HwIntrinsics.AllowAll): + + // Not a COMPlus value. We filter in calling method. + features.Add(key, nameof(HwIntrinsics.AllowAll)); + break; + + default: + features.Add(key, intrinsic.Replace("Disable", "Enable")); + break; + } + } + + return features; + } +} + +/// +/// See +/// +/// ends up impacting all SIMD support(including System.Numerics) +/// but not things like , , and . +/// +/// +[Flags] +#pragma warning disable RCS1135 // Declare enum member with zero value (when enum has FlagsAttribute). +public enum HwIntrinsics +#pragma warning restore RCS1135 // Declare enum member with zero value (when enum has FlagsAttribute). +{ + // Use flags so we can pass multiple values without using params. + // Don't base on 0 or use inverse for All as that doesn't translate to string values. + DisableHWIntrinsic = 1 << 0, + DisableSSE = 1 << 1, + DisableSSE2 = 1 << 2, + DisableAES = 1 << 3, + DisablePCLMULQDQ = 1 << 4, + DisableSSE3 = 1 << 5, + DisableSSSE3 = 1 << 6, + DisableSSE41 = 1 << 7, + DisableSSE42 = 1 << 8, + DisablePOPCNT = 1 << 9, + DisableAVX = 1 << 10, + DisableFMA = 1 << 11, + DisableAVX2 = 1 << 12, + DisableBMI1 = 1 << 13, + DisableBMI2 = 1 << 14, + DisableLZCNT = 1 << 15, + DisableArm64AdvSimd = 1 << 16, + DisableArm64Crc32 = 1 << 17, + DisableArm64Dp = 1 << 18, + DisableArm64Aes = 1 << 19, + DisableArm64Sha1 = 1 << 20, + DisableArm64Sha256 = 1 << 21, + AllowAll = 1 << 22 +} diff --git a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Dashed.png b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Dashed.png index 546de665..844faa76 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Dashed.png and b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Dashed.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Overlap.png b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Overlap.png index 772c0376..30168925 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Overlap.png and b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Overlap.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png index afa346fd..a044cc56 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b5d50f6800d646d4a3049a5a3e49500857bcb7b4debc457c58f69b3cef3e0bf -size 5190 +oid sha256:fbf283cef01981b183b7ef8b8c07bdacbf39b9a24de3d86a087dc3a54a0cb958 +size 4868 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png index 71a0e7ab..a6081c36 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40bfcd17e14f0e65c9fab7242e4c36cdb57d9ba9392e49efaad3b95b1a913dbd -size 4427 +oid sha256:949b0e0af39b177c5214dd2f87355dc3e7a596ac6af05563e9deaa643037e5a1 +size 4377 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png index eec5b52d..adea8da1 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52b926fd8113abc40641fcb3523891dab877f723e0f60a92d5b4f3dc05b50937 -size 41045 +oid sha256:64de5c0902dc90f5122ce55bec6b83e520dc8f602c07e66438545e88b0e999a6 +size 40943 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_States_Fill.png b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_States_Fill.png index 1752fe91..0617cbf3 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_States_Fill.png and b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_States_Fill.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-20).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-20).png index fc9432fa..6fa17f8a 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-20).png and b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-20).png differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_0).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_0).png index f6fc6b35..f6b0dc61 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_0).png and b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_0).png differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-20).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-20).png index 2120ae01..77568a1a 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-20).png and b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-20).png differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_0).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_0).png index 2348c0e0..67cd84f7 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_0).png and b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_0).png differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(99_0).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(99_0).png index 7ffec942..2e2028ab 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(99_0).png and b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(99_0).png differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle.png index a549fcb6..1175a735 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle.png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(False)_IntersectionRule(Nonzero).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(False)_IntersectionRule(Nonzero).png index 4afb2b22..91eb354b 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(False)_IntersectionRule(Nonzero).png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(False)_IntersectionRule(Nonzero).png differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(True)_IntersectionRule(Nonzero).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(True)_IntersectionRule(Nonzero).png index 3eeaf4b3..c279180c 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(True)_IntersectionRule(Nonzero).png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(True)_IntersectionRule(Nonzero).png differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(7)_R(80)_Ang(-180).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(7)_R(80)_Ang(-180).png index 50ab7260..ebbb4aa5 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(7)_R(80)_Ang(-180).png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(7)_R(80)_Ang(-180).png differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/GradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/GradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png index e4d40263..747422fc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/GradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png and b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/GradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/FilledBezier_Rgba32_Blank500x500.png b/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/FilledBezier_Rgba32_Blank500x500.png index dfd8b968..5dd5b156 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/FilledBezier_Rgba32_Blank500x500.png and b/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/FilledBezier_Rgba32_Blank500x500.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/OverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png b/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/OverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png index e703adef..571db971 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/OverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png and b/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/OverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png index 901080a0..426d0065 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f0ac05a29dbf0235ba4cb7e96ac2164a432e2b6b1ee8b49de26f76c3d6a2e0c -size 4353 +oid sha256:95c2bb58b46703a0db1b4290e06383c528e1af87323bb3aa582f68710dd6da1b +size 4219 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png index 3b49924b..2b436caa 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a179fba9d30ba0c7cf21f07ab3b0e2c0260eeb2c62cd9bd581e6b09107819af6 -size 1036 +oid sha256:cb99b342371e50b3b20e7ae22ee80a13027df94b129a095c8e16120ae71877bd +size 710 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_LargeText.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_LargeText.png index 0c1d6b52..dc054247 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_LargeText.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_LargeText.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3367a65234258a6a26688eaadded51b97cd1efccba56534078a29cec80c868dd -size 110735 +oid sha256:041fe44d2a6a920e7deb699d9f089c7441a27533af9afb2152fbabfa5037f9d4 +size 110984