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