// Copyright (c) Six Labors. // 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 { private static readonly int[] SpanLengthValues = [ 0, 1, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 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); /// /// Verifies that span-to-span operations require equal input lengths before accessing either input. /// [Fact] public void AddSpanSpanRejectsMismatchedInputLengths() { ArgumentException exception = Assert.Throws( () => TensorPrimitives_.Add(new int[4], new int[3], new int[4])); Assert.Null(exception.ParamName); } /// /// Verifies that every operation rejects a destination that cannot hold all input elements. /// [Fact] public void OperationsRejectShortDestinations() { int[] integers = new int[4]; float[] singles = new float[4]; Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Add(integers, integers, new int[3])).ParamName); Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Add(integers, 1, new int[3])).ParamName); Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Multiply(singles, 2F, new float[3])).ParamName); Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Divide(singles, 2F, new float[3])).ParamName); Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Max(singles, 2F, new float[3])).ParamName); Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Clamp(singles, 0F, 1F, new float[3])).ParamName); Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Negate(singles, new float[3])).ParamName); } /// /// Verifies that every operation rejects shifted input and destination overlap. /// [Fact] public void OperationsRejectShiftedOverlap() { int[] integers = new int[5]; int[] separateIntegers = new int[4]; float[] singles = new float[5]; // Both inputs need independent validation because either one may alias a shifted destination. Assert.Equal( "destination", Assert.Throws( () => TensorPrimitives_.Add(integers.AsSpan(0, 4), separateIntegers, integers.AsSpan(1, 4))).ParamName); Assert.Equal( "destination", Assert.Throws( () => TensorPrimitives_.Add(separateIntegers, integers.AsSpan(0, 4), integers.AsSpan(1, 4))).ParamName); Assert.Equal( "destination", Assert.Throws( () => TensorPrimitives_.Add(integers.AsSpan(0, 4), 1, integers.AsSpan(1, 4))).ParamName); Assert.Equal( "destination", Assert.Throws( () => TensorPrimitives_.Multiply(singles.AsSpan(0, 4), 2F, singles.AsSpan(1, 4))).ParamName); Assert.Equal( "destination", Assert.Throws( () => TensorPrimitives_.Divide(singles.AsSpan(0, 4), 2F, singles.AsSpan(1, 4))).ParamName); Assert.Equal( "destination", Assert.Throws( () => TensorPrimitives_.Max(singles.AsSpan(0, 4), 2F, singles.AsSpan(1, 4))).ParamName); Assert.Equal( "destination", Assert.Throws( () => TensorPrimitives_.Clamp(singles.AsSpan(0, 4), 0F, 1F, singles.AsSpan(1, 4))).ParamName); Assert.Equal( "destination", Assert.Throws( () => TensorPrimitives_.Negate(singles.AsSpan(0, 4), singles.AsSpan(1, 4))).ParamName); } /// /// 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. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void AddByteMatchesScalarFormula(int length) { byte[] x = new byte[length]; byte[] y = new byte[length]; byte[] expected = new byte[length]; for (int i = 0; i < length; i++) { x[i] = (byte)((i * 23) + 197); y[i] = (byte)((i * 41) + 113); expected[i] = unchecked((byte)(x[i] + y[i])); } byte[] destination = new byte[length]; TensorPrimitives_.Add(x, y, destination); Assert.Equal(expected, destination); byte[] xInPlace = (byte[])x.Clone(); TensorPrimitives_.Add(xInPlace, y, xInPlace); Assert.Equal(expected, xInPlace); byte[] yInPlace = (byte[])y.Clone(); TensorPrimitives_.Add(x, yInPlace, yInPlace); Assert.Equal(expected, yInPlace); } /// /// Verifies that unsigned integer addition preserves unchecked histogram accumulation semantics. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void AddUInt32MatchesScalarFormula(int length) { uint[] x = new uint[length]; uint[] y = new uint[length]; uint[] expected = new uint[length]; for (int i = 0; i < length; i++) { x[i] = ((uint)i * 1_234_567U) + 0xF0000000U; y[i] = ((uint)i * 7_654_321U) + 0x30000000U; expected[i] = unchecked(x[i] + y[i]); } TensorPrimitives_.Add(x, y, x); Assert.Equal(expected, x); } /// /// Verifies that scalar integer addition produces identical results for separate and in-place destinations. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void AddScalarInt32MatchesScalarFormula(int length) { int[] source = new int[length]; int[] expected = new int[length]; const int addend = 17; for (int i = 0; i < length; i++) { source[i] = (i * 37) - 200; expected[i] = source[i] + addend; } int[] destination = new int[length]; TensorPrimitives_.Add(source, addend, destination); Assert.Equal(expected, destination); int[] inPlace = (int[])source.Clone(); TensorPrimitives_.Add(inPlace, addend, inPlace); Assert.Equal(expected, inPlace); } /// /// Verifies that floating-point negation preserves the scalar operator's exact bit-level behavior. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void NegateSingleMatchesScalarFormula(int length) { float[] values = { float.NaN, -0F, 0F, -1F, 1F, float.NegativeInfinity, float.PositiveInfinity }; float[] source = new float[length]; float[] expected = new float[length]; for (int i = 0; i < source.Length; i++) { source[i] = values[i % values.Length]; expected[i] = -source[i]; } float[] destination = new float[length]; TensorPrimitives_.Negate(source, destination); AssertSingleBitsEqual(expected, destination); TensorPrimitives_.Negate(source, source); 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. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void ClampInt32MatchesScalarFormula(int length) { int[] source = new int[length]; int[] expected = new int[length]; for (int i = 0; i < source.Length; i++) { source[i] = ((i * 37) % 401) - 200; expected[i] = Math.Clamp(source[i], -73, 91); } int[] destination = new int[length]; TensorPrimitives_.Clamp(source, -73, 91, destination); Assert.Equal(expected, destination); int[] inPlace = (int[])source.Clone(); TensorPrimitives_.Clamp(inPlace, -73, 91, inPlace); Assert.Equal(expected, inPlace); } /// /// Verifies that floating-point clamping matches the runtime tensor formula for special values and unordered bounds. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void ClampSingleMatchesRuntimeFormula(int length) { float[] values = { float.NaN, -0F, 0F, -1F, 1F, float.NegativeInfinity, float.PositiveInfinity }; float[] source = new float[length]; float[] expected = new float[length]; for (int i = 0; i < source.Length; i++) { source[i] = values[i % values.Length]; // Runtime main follows Min(Max(x, min), max) for vectorizable types, including unordered bounds. expected[i] = float.Min(float.Max(source[i], 2F), -2F); } TensorPrimitives_.Clamp(source, 2F, -2F, source); AssertSingleBitsEqual(expected, source); } /// /// Verifies that single-precision clamping preserves the runtime's signed-zero and NaN behavior. /// [Fact] public void ClampSinglePreservesRuntimeSpecialValueSemantics() { float[] values = { float.NaN, float.NegativeInfinity, -0F, 0F, float.PositiveInfinity }; float[] actual = new float[129]; float[] expected = new float[actual.Length]; for (int i = 0; i < actual.Length; i++) { actual[i] = values[i % values.Length]; expected[i] = float.Min(float.Max(actual[i], -0F), 0F); } TensorPrimitives_.Clamp(actual, -0F, 0F, actual); AssertSingleBitsEqual(expected, actual); } /// /// Verifies that double-precision clamping preserves the runtime's signed-zero and NaN behavior. /// [Fact] public void ClampDoublePreservesRuntimeSpecialValueSemantics() { double[] values = { double.NaN, double.NegativeInfinity, -0D, 0D, double.PositiveInfinity }; double[] actual = new double[65]; double[] expected = new double[actual.Length]; for (int i = 0; i < actual.Length; i++) { actual[i] = values[i % values.Length]; expected[i] = double.Min(double.Max(actual[i], -0D), 0D); } TensorPrimitives_.Clamp(actual, -0D, 0D, actual); AssertDoubleBitsEqual(expected, actual); } /// /// Verifies that division produces identical results for separate and in-place destinations. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void DivideSingleMatchesScalarFormula(int length) { float[] source = new float[length]; float[] expected = new float[length]; for (int i = 0; i < source.Length; i++) { source[i] = (i - 65.25F) * 1.75F; expected[i] = source[i] / 3.25F; } float[] destination = new float[length]; TensorPrimitives_.Divide(source, 3.25F, destination); AssertSingleBitsEqual(expected, destination); float[] inPlace = (float[])source.Clone(); TensorPrimitives_.Divide(inPlace, 3.25F, inPlace); AssertSingleBitsEqual(expected, inPlace); } /// /// Verifies that maximum selection preserves the runtime's NaN and signed-zero semantics. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void MaxSingleMatchesRuntimeFormula(int length) { float[] values = { float.NaN, float.NegativeInfinity, -1F, -0F, 0F, 1F, float.PositiveInfinity }; float[] actual = new float[length]; float[] expected = new float[length]; for (int i = 0; i < length; i++) { actual[i] = values[i % values.Length]; expected[i] = float.Max(actual[i], -0F); } TensorPrimitives_.Max(actual, -0F, actual); AssertSingleBitsEqual(expected, actual); } /// /// Verifies that multiplication produces identical results for separate and in-place destinations. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void MultiplySingleMatchesScalarFormula(int length) { float[] source = new float[length]; float[] expected = new float[length]; for (int i = 0; i < source.Length; i++) { source[i] = (i - 65.25F) * 1.75F; expected[i] = source[i] * 0.375F; } float[] destination = new float[length]; TensorPrimitives_.Multiply(source, 0.375F, destination); AssertSingleBitsEqual(expected, destination); TensorPrimitives_.Multiply(source, 0.375F, source); AssertSingleBitsEqual(expected, source); } /// /// Verifies that the normalization compatibility call preserves its element-wise division contract. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void NormalizeMatchesScalarFormula(int length) { float[] actual = new float[length]; float[] expected = new float[length]; for (int i = 0; i < actual.Length; i++) { actual[i] = (i + 1) * 0.125F; expected[i] = actual[i] / 7.5F; } Numerics.Normalize(actual, 7.5F); AssertSingleBitsEqual(expected, actual); } /// /// Compares floating-point results while preserving signed-zero behavior. /// /// The expected values. /// The actual values. private static void AssertSingleBitsEqual(ReadOnlySpan expected, ReadOnlySpan actual) { Assert.Equal(expected.Length, actual.Length); for (int i = 0; i < expected.Length; i++) { if (float.IsNaN(expected[i])) { Assert.True(float.IsNaN(actual[i])); } else { Assert.Equal(BitConverter.SingleToInt32Bits(expected[i]), BitConverter.SingleToInt32Bits(actual[i])); } } } /// /// Compares double-precision results while preserving signed-zero behavior. /// /// The expected values. /// The actual values. private static void AssertDoubleBitsEqual(ReadOnlySpan expected, ReadOnlySpan actual) { Assert.Equal(expected.Length, actual.Length); for (int i = 0; i < expected.Length; i++) { if (double.IsNaN(expected[i])) { Assert.True(double.IsNaN(actual[i])); } else { Assert.Equal(BitConverter.DoubleToInt64Bits(expected[i]), BitConverter.DoubleToInt64Bits(actual[i])); } } } }