diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs
index eb90a3903..99dc80184 100644
--- a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs
+++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs
@@ -264,12 +264,58 @@ internal static partial class TensorPrimitives_
public static T Invoke(T x) => -x;
///
- public static Vector128 Invoke(Vector128 x) => -x;
+ public static Vector128 Invoke(Vector128 x)
+ {
+ if (typeof(T) == typeof(float))
+ {
+ // IEEE-754 negation toggles the sign bit. Expressing that operation explicitly avoids the
+ // subtraction-based ARM64 code generated by .NET 8 for generic vector negation, which loses
+ // the sign when +0F is negated and therefore differs from both scalar and runtime-main behavior.
+ return x ^ Vector128.Create(-0F).As();
+ }
+
+ if (typeof(T) == typeof(double))
+ {
+ // Double-precision values use the same sign-bit representation, with the sign in bit 63.
+ return x ^ Vector128.Create(-0D).As();
+ }
+
+ return -x;
+ }
///
- public static Vector256 Invoke(Vector256 x) => -x;
+ public static Vector256 Invoke(Vector256 x)
+ {
+ if (typeof(T) == typeof(float))
+ {
+ // Keep the operation bitwise at every width so ARM64 preserves signed zero exactly.
+ return x ^ Vector256.Create(-0F).As();
+ }
+
+ if (typeof(T) == typeof(double))
+ {
+ return x ^ Vector256.Create(-0D).As();
+ }
+
+ return -x;
+ }
///
- public static Vector512 Invoke(Vector512 x) => -x;
+ public static Vector512 Invoke(Vector512 x)
+ {
+ if (typeof(T) == typeof(float))
+ {
+ // Vector512 can be hardware accelerated directly or decomposed by the runtime; the explicit
+ // bit operation provides identical IEEE-754 behavior in either case.
+ return x ^ Vector512.Create(-0F).As();
+ }
+
+ if (typeof(T) == typeof(double))
+ {
+ return x ^ Vector512.Create(-0D).As();
+ }
+
+ return -x;
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs b/tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs
index 1fe64893b..5ce5401f8 100644
--- a/tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs
+++ b/tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs
@@ -2,16 +2,14 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Common.Helpers;
+using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Common;
public class TensorPrimitivesTests
{
- ///
- /// Gets lengths that exercise scalar execution, every SIMD width, overlapping tails, and the unrolled loop.
- ///
- public static TheoryData SpanLengths => new()
- {
+ private static readonly int[] SpanLengthValues =
+ [
0,
1,
3,
@@ -33,7 +31,52 @@ public class TensorPrimitivesTests
128,
129,
2048
- };
+ ];
+
+ ///
+ /// Gets lengths that exercise scalar execution, every SIMD width, overlapping tails, and the unrolled loop.
+ ///
+ public static TheoryData SpanLengths => new(SpanLengthValues);
+
+ ///
+ /// Verifies every compatibility operation while forcing the supported SIMD feature tiers in isolated processes.
+ ///
+ [Fact]
+ public void OperationsMatchScalarFormulasAcrossHardwareIntrinsicFeatures()
+ => FeatureTestRunner.RunWithHwIntrinsicsFeature(
+ RunOperationsAcrossHardwareIntrinsicFeatures,
+ HwIntrinsics.AllowAll
+ | HwIntrinsics.DisableAVX512F
+ | HwIntrinsics.DisableAVX
+ | HwIntrinsics.DisableArm64Sve
+ | HwIntrinsics.DisableHWIntrinsic);
+
+ ///
+ /// Runs the TensorPrimitives compatibility assertions inside a process configured for one hardware-intrinsic tier.
+ ///
+ private static void RunOperationsAcrossHardwareIntrinsicFeatures()
+ {
+ TensorPrimitivesTests tests = new();
+
+ // Reuse the focused assertions so the remote feature matrix cannot drift from the normal test coverage.
+ foreach (int length in SpanLengthValues)
+ {
+ tests.AddByteMatchesScalarFormula(length);
+ tests.AddUInt32MatchesScalarFormula(length);
+ tests.AddScalarInt32MatchesScalarFormula(length);
+ tests.NegateSingleMatchesScalarFormula(length);
+ tests.NegateDoubleMatchesScalarFormula(length);
+ tests.ClampInt32MatchesScalarFormula(length);
+ tests.ClampSingleMatchesRuntimeFormula(length);
+ tests.DivideSingleMatchesScalarFormula(length);
+ tests.MaxSingleMatchesRuntimeFormula(length);
+ tests.MultiplySingleMatchesScalarFormula(length);
+ tests.NormalizeMatchesScalarFormula(length);
+ }
+
+ tests.ClampSinglePreservesRuntimeSpecialValueSemantics();
+ tests.ClampDoublePreservesRuntimeSpecialValueSemantics();
+ }
///
/// Verifies that byte addition wraps modulo 256 and supports either input as the in-place destination.
@@ -153,6 +196,42 @@ public class TensorPrimitivesTests
AssertSingleBitsEqual(expected, source);
}
+ ///
+ /// Verifies that double-precision negation preserves the scalar operator's exact bit-level behavior.
+ ///
+ /// The input length.
+ [Theory]
+ [MemberData(nameof(SpanLengths))]
+ public void NegateDoubleMatchesScalarFormula(int length)
+ {
+ double[] values =
+ {
+ double.NaN,
+ -0D,
+ 0D,
+ -1D,
+ 1D,
+ double.NegativeInfinity,
+ double.PositiveInfinity
+ };
+
+ double[] source = new double[length];
+ double[] expected = new double[length];
+
+ for (int i = 0; i < source.Length; i++)
+ {
+ source[i] = values[i % values.Length];
+ expected[i] = -source[i];
+ }
+
+ double[] destination = new double[length];
+ TensorPrimitives_.Negate(source, destination);
+ AssertDoubleBitsEqual(expected, destination);
+
+ TensorPrimitives_.Negate(source, source);
+ AssertDoubleBitsEqual(expected, source);
+ }
+
///
/// Verifies that integer clamping produces identical results for separate and in-place destinations.
///