diff --git a/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs b/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs
new file mode 100644
index 0000000000..57ed85a182
--- /dev/null
+++ b/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs
@@ -0,0 +1,266 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Microsoft.DotNet.RemoteExecutor;
+using Xunit.Abstractions;
+
+namespace SixLabors.ImageSharp.Tests.TestUtilities
+{
+ ///
+ /// Allows the testing against specific feature sets.
+ ///
+ public static class FeatureTestRunner
+ {
+ private static readonly char[] SplitChars = new[] { ',', ' ' };
+
+ ///
+ /// 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 Deserialize(string value)
+ where T : IXunitSerializable
+ => BasicSerializer.Deserialize(value);
+
+ ///
+ /// 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())
+ {
+ var processStartInfo = new ProcessStartInfo();
+ if (intrinsic.Key != HwIntrinsics.AllowAll)
+ {
+ processStartInfo.Environment[$"COMPlus_Enable{intrinsic}"] = "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())
+ {
+ var processStartInfo = new ProcessStartInfo();
+ if (intrinsic.Key != HwIntrinsics.AllowAll)
+ {
+ processStartInfo.Environment[$"COMPlus_Enable{intrinsic}"] = "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 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())
+ {
+ var processStartInfo = new ProcessStartInfo();
+ if (intrinsic.Key != HwIntrinsics.AllowAll)
+ {
+ processStartInfo.Environment[$"COMPlus_Enable{intrinsic}"] = "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 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())
+ {
+ var processStartInfo = new ProcessStartInfo();
+ if (intrinsic.Key != HwIntrinsics.AllowAll)
+ {
+ processStartInfo.Environment[$"COMPlus_Enable{intrinsic}"] = "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());
+ }
+ }
+ }
+
+ internal static Dictionary ToFeatureKeyValueCollection(this HwIntrinsics intrinsics)
+ {
+ // Loop through and translate the given values into COMPlus equivaluents
+ var features = new Dictionary();
+ foreach (string intrinsic in intrinsics.ToString("G").Split(SplitChars, StringSplitOptions.RemoveEmptyEntries))
+ {
+ var key = (HwIntrinsics)Enum.Parse(typeof(HwIntrinsics), intrinsic);
+ switch (intrinsic)
+ {
+ case nameof(HwIntrinsics.DisableSIMD):
+ features.Add(key, "FeatureSIMD");
+ break;
+
+ 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.
+ DisableSIMD = 1 << 0,
+ DisableHWIntrinsic = 1 << 1,
+ DisableSSE = 1 << 2,
+ DisableSSE2 = 1 << 3,
+ DisableAES = 1 << 4,
+ DisablePCLMULQDQ = 1 << 5,
+ DisableSSE3 = 1 << 6,
+ DisableSSSE3 = 1 << 7,
+ DisableSSE41 = 1 << 8,
+ DisableSSE42 = 1 << 9,
+ DisablePOPCNT = 1 << 10,
+ DisableAVX = 1 << 11,
+ DisableFMA = 1 << 12,
+ DisableAVX2 = 1 << 13,
+ DisableBMI1 = 1 << 14,
+ DisableBMI2 = 1 << 15,
+ DisableLZCNT = 1 << 16,
+ AllowAll = 1 << 17
+ }
+}
diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs
new file mode 100644
index 0000000000..9852ba3478
--- /dev/null
+++ b/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs
@@ -0,0 +1,237 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+#if SUPPORTS_RUNTIME_INTRINSICS
+using System.Runtime.Intrinsics.X86;
+#endif
+using Xunit;
+using Xunit.Abstractions;
+
+namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests
+{
+ public class FeatureTestRunnerTests
+ {
+ public static TheoryData Intrinsics =>
+ new TheoryData
+ {
+ { HwIntrinsics.DisableAES | HwIntrinsics.AllowAll, new string[] { "EnableAES", "AllowAll" } },
+ { HwIntrinsics.DisableSIMD | HwIntrinsics.DisableHWIntrinsic, new string[] { "FeatureSIMD", "EnableHWIntrinsic" } },
+ { HwIntrinsics.DisableSSE42 | HwIntrinsics.DisableAVX, new string[] { "EnableSSE42", "EnableAVX" } }
+ };
+
+ [Theory]
+ [MemberData(nameof(Intrinsics))]
+ public void ToFeatureCollectionReturnsExpectedResult(HwIntrinsics expectedItrinsics, string[] expectedValues)
+ {
+ Dictionary features = expectedItrinsics.ToFeatureKeyValueCollection();
+ HwIntrinsics[] keys = features.Keys.ToArray();
+
+ HwIntrinsics actualIntrinsics = keys[0];
+ for (int i = 1; i < keys.Length; i++)
+ {
+ actualIntrinsics |= keys[i];
+ }
+
+ Assert.Equal(expectedItrinsics, actualIntrinsics);
+
+ IEnumerable actualValues = features.Select(x => x.Value);
+ Assert.Equal(expectedValues, actualValues);
+ }
+
+ [Fact]
+ public void AllowsAllHwIntrinsicFeatures()
+ {
+ FeatureTestRunner.RunWithHwIntrinsicsFeature(
+ () => Assert.True(Vector.IsHardwareAccelerated),
+ HwIntrinsics.AllowAll);
+ }
+
+ [Fact]
+ public void CanLimitHwIntrinsicFeatures()
+ {
+ FeatureTestRunner.RunWithHwIntrinsicsFeature(
+ () => Assert.False(Vector.IsHardwareAccelerated),
+ HwIntrinsics.DisableSIMD);
+ }
+
+ [Fact]
+ public void CanLimitHwIntrinsicFeaturesWithIntrinsicsParam()
+ {
+ static void AssertHwIntrinsicsFeatureDisabled(string intrinsic)
+ {
+ Assert.NotNull(intrinsic);
+
+ switch ((HwIntrinsics)Enum.Parse(typeof(HwIntrinsics), intrinsic))
+ {
+ case HwIntrinsics.DisableSIMD:
+ Assert.False(Vector.IsHardwareAccelerated);
+ break;
+#if SUPPORTS_RUNTIME_INTRINSICS
+ case HwIntrinsics.DisableHWIntrinsic:
+ Assert.False(Vector.IsHardwareAccelerated);
+ break;
+ case HwIntrinsics.DisableSSE:
+ Assert.False(Sse.IsSupported);
+ break;
+ case HwIntrinsics.DisableSSE2:
+ Assert.False(Sse2.IsSupported);
+ break;
+ case HwIntrinsics.DisableAES:
+ Assert.False(Aes.IsSupported);
+ break;
+ case HwIntrinsics.DisablePCLMULQDQ:
+ Assert.False(Pclmulqdq.IsSupported);
+ break;
+ case HwIntrinsics.DisableSSE3:
+ Assert.False(Sse3.IsSupported);
+ break;
+ case HwIntrinsics.DisableSSSE3:
+ Assert.False(Ssse3.IsSupported);
+ break;
+ case HwIntrinsics.DisableSSE41:
+ Assert.False(Sse41.IsSupported);
+ break;
+ case HwIntrinsics.DisableSSE42:
+ Assert.False(Sse42.IsSupported);
+ break;
+ case HwIntrinsics.DisablePOPCNT:
+ Assert.False(Popcnt.IsSupported);
+ break;
+ case HwIntrinsics.DisableAVX:
+ Assert.False(Avx.IsSupported);
+ break;
+ case HwIntrinsics.DisableFMA:
+ Assert.False(Fma.IsSupported);
+ break;
+ case HwIntrinsics.DisableAVX2:
+ Assert.False(Avx2.IsSupported);
+ break;
+ case HwIntrinsics.DisableBMI1:
+ Assert.False(Bmi1.IsSupported);
+ break;
+ case HwIntrinsics.DisableBMI2:
+ Assert.False(Bmi2.IsSupported);
+ break;
+ case HwIntrinsics.DisableLZCNT:
+ Assert.False(Lzcnt.IsSupported);
+ break;
+#endif
+ }
+ }
+
+ foreach (HwIntrinsics intrinsic in (HwIntrinsics[])Enum.GetValues(typeof(HwIntrinsics)))
+ {
+ FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertHwIntrinsicsFeatureDisabled, intrinsic);
+ }
+ }
+
+ [Fact]
+ public void CanLimitHwIntrinsicFeaturesWithSerializableParam()
+ {
+ static void AssertHwIntrinsicsFeatureDisabled(string serializable)
+ {
+ Assert.NotNull(serializable);
+ Assert.NotNull(FeatureTestRunner.Deserialize(serializable));
+
+#if SUPPORTS_RUNTIME_INTRINSICS
+ Assert.False(Sse.IsSupported);
+#endif
+ }
+
+ foreach (HwIntrinsics intrinsic in (HwIntrinsics[])Enum.GetValues(typeof(HwIntrinsics)))
+ {
+ FeatureTestRunner.RunWithHwIntrinsicsFeature(
+ AssertHwIntrinsicsFeatureDisabled,
+ HwIntrinsics.DisableSSE,
+ new FakeSerializable());
+ }
+ }
+
+ [Fact]
+ public void CanLimitHwIntrinsicFeaturesWithSerializableAndIntrinsicsParams()
+ {
+ static void AssertHwIntrinsicsFeatureDisabled(string serializable, string intrinsic)
+ {
+ Assert.NotNull(serializable);
+ Assert.NotNull(FeatureTestRunner.Deserialize(serializable));
+
+ switch ((HwIntrinsics)Enum.Parse(typeof(HwIntrinsics), intrinsic))
+ {
+ case HwIntrinsics.DisableSIMD:
+ Assert.False(Vector.IsHardwareAccelerated);
+ break;
+#if SUPPORTS_RUNTIME_INTRINSICS
+ case HwIntrinsics.DisableHWIntrinsic:
+ Assert.False(Vector.IsHardwareAccelerated);
+ break;
+ case HwIntrinsics.DisableSSE:
+ Assert.False(Sse.IsSupported);
+ break;
+ case HwIntrinsics.DisableSSE2:
+ Assert.False(Sse2.IsSupported);
+ break;
+ case HwIntrinsics.DisableAES:
+ Assert.False(Aes.IsSupported);
+ break;
+ case HwIntrinsics.DisablePCLMULQDQ:
+ Assert.False(Pclmulqdq.IsSupported);
+ break;
+ case HwIntrinsics.DisableSSE3:
+ Assert.False(Sse3.IsSupported);
+ break;
+ case HwIntrinsics.DisableSSSE3:
+ Assert.False(Ssse3.IsSupported);
+ break;
+ case HwIntrinsics.DisableSSE41:
+ Assert.False(Sse41.IsSupported);
+ break;
+ case HwIntrinsics.DisableSSE42:
+ Assert.False(Sse42.IsSupported);
+ break;
+ case HwIntrinsics.DisablePOPCNT:
+ Assert.False(Popcnt.IsSupported);
+ break;
+ case HwIntrinsics.DisableAVX:
+ Assert.False(Avx.IsSupported);
+ break;
+ case HwIntrinsics.DisableFMA:
+ Assert.False(Fma.IsSupported);
+ break;
+ case HwIntrinsics.DisableAVX2:
+ Assert.False(Avx2.IsSupported);
+ break;
+ case HwIntrinsics.DisableBMI1:
+ Assert.False(Bmi1.IsSupported);
+ break;
+ case HwIntrinsics.DisableBMI2:
+ Assert.False(Bmi2.IsSupported);
+ break;
+ case HwIntrinsics.DisableLZCNT:
+ Assert.False(Lzcnt.IsSupported);
+ break;
+#endif
+ }
+ }
+
+ foreach (HwIntrinsics intrinsic in (HwIntrinsics[])Enum.GetValues(typeof(HwIntrinsics)))
+ {
+ FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertHwIntrinsicsFeatureDisabled, intrinsic, new FakeSerializable());
+ }
+ }
+
+ public class FakeSerializable : IXunitSerializable
+ {
+ public void Deserialize(IXunitSerializationInfo info)
+ {
+ }
+
+ public void Serialize(IXunitSerializationInfo info)
+ {
+ }
+ }
+ }
+}