// 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);
///
/// 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]));
}
}
}
}