diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Add.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Add.cs index f2ef214cf..19f46c06f 100644 --- a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Add.cs +++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Add.cs @@ -16,6 +16,11 @@ internal static partial class TensorPrimitives_ /// The first addends. /// The second addends. /// The destination for the sums. + /// and do not have the same length. + /// is shorter than the input spans. + /// + /// An input and overlap without beginning at the same memory location. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Add(ReadOnlySpan x, ReadOnlySpan y, Span destination) where T : IAdditionOperators, IAdditiveIdentity @@ -28,6 +33,10 @@ internal static partial class TensorPrimitives_ /// The first addends. /// The scalar second addend. /// The destination for the sums. + /// is shorter than . + /// + /// and overlap without beginning at the same memory location. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Add(ReadOnlySpan x, T y, Span destination) where T : IAdditionOperators, IAdditiveIdentity diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Clamp.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Clamp.cs index e2cc9097e..b974163dc 100644 --- a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Clamp.cs +++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Clamp.cs @@ -18,6 +18,10 @@ internal static partial class TensorPrimitives_ /// The inclusive lower bound. /// The inclusive upper bound. /// The destination for the clamped values. + /// is shorter than . + /// + /// and overlap without beginning at the same memory location. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Clamp(ReadOnlySpan x, T min, T max, Span destination) where T : INumber diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Divide.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Divide.cs index e40a26cea..f4a5bd01a 100644 --- a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Divide.cs +++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Divide.cs @@ -16,6 +16,10 @@ internal static partial class TensorPrimitives_ /// The dividend values. /// The divisor. /// The destination for the quotient values. + /// is shorter than . + /// + /// and overlap without beginning at the same memory location. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Divide(ReadOnlySpan x, T y, Span destination) where T : IDivisionOperators diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Helpers.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Helpers.cs index 3ba1b61af..e80ca5411 100644 --- a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Helpers.cs +++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Helpers.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -109,6 +110,47 @@ internal static partial class TensorPrimitives_ public static abstract Vector512 Invoke(Vector512 x, Vector512 y, Vector512 z); } + /// + /// Validates that an input and destination are either disjoint or begin at the same memory location. + /// + /// The element type. + /// The input values. + /// The destination values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ValidateInputOutputSpanNonOverlapping(ReadOnlySpan input, Span destination) + { + // Runtime TensorPrimitives permits exact same-start overlap for in-place operation. A shifted overlap is + // rejected because forward SIMD stores could overwrite input elements before a later load consumes them. + if (!Unsafe.AreSame(ref MemoryMarshal.GetReference(input), ref MemoryMarshal.GetReference(destination)) + && input.Overlaps(destination)) + { + ThrowInputAndDestinationSpanMustNotOverlap(); + } + } + + /// + /// Throws when input spans do not have the same length. + /// + [DoesNotReturn] + private static void ThrowSpansMustHaveSameLength() + => throw new ArgumentException("Input span arguments must all have the same length."); + + /// + /// Throws when the destination cannot hold every result. + /// + [DoesNotReturn] + private static void ThrowDestinationTooShort() + => throw new ArgumentException("Destination is too short.", "destination"); + + /// + /// Throws when an input and destination overlap without beginning at the same memory location. + /// + [DoesNotReturn] + private static void ThrowInputAndDestinationSpanMustNotOverlap() + => throw new ArgumentException( + "The destination span may only overlap with an input span if the two spans start at the same memory location.", + "destination"); + /// /// Performs an element-wise binary operation between two spans. /// @@ -124,6 +166,19 @@ internal static partial class TensorPrimitives_ Span destination) where TOperator : struct, IBinaryOperator { + if (x.Length != y.Length) + { + ThrowSpansMustHaveSameLength(); + } + + if (x.Length > destination.Length) + { + ThrowDestinationTooShort(); + } + + ValidateInputOutputSpanNonOverlapping(x, destination); + ValidateInputOutputSpanNonOverlapping(y, destination); + ref T xRef = ref MemoryMarshal.GetReference(x); ref T yRef = ref MemoryMarshal.GetReference(y); ref T destinationRef = ref MemoryMarshal.GetReference(destination); @@ -173,6 +228,13 @@ internal static partial class TensorPrimitives_ Span destination) where TOperator : struct, IBinaryOperator { + if (x.Length > destination.Length) + { + ThrowDestinationTooShort(); + } + + ValidateInputOutputSpanNonOverlapping(x, destination); + ref T xRef = ref MemoryMarshal.GetReference(x); ref T destinationRef = ref MemoryMarshal.GetReference(destination); nuint length = (uint)x.Length; @@ -220,6 +282,13 @@ internal static partial class TensorPrimitives_ Span destination) where TOperator : struct, IBinaryOperator { + if (x.Length > destination.Length) + { + ThrowDestinationTooShort(); + } + + ValidateInputOutputSpanNonOverlapping(x, destination); + ref T xRef = ref MemoryMarshal.GetReference(x); ref T destinationRef = ref MemoryMarshal.GetReference(destination); nuint length = (uint)x.Length; @@ -282,6 +351,13 @@ internal static partial class TensorPrimitives_ Span destination) where TOperator : struct, ITernaryOperator { + if (x.Length > destination.Length) + { + ThrowDestinationTooShort(); + } + + ValidateInputOutputSpanNonOverlapping(x, destination); + ref T xRef = ref MemoryMarshal.GetReference(x); ref T destinationRef = ref MemoryMarshal.GetReference(destination); nuint length = (uint)x.Length; diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Max.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Max.cs index 31312dddf..fc621a122 100644 --- a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Max.cs +++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Max.cs @@ -16,6 +16,10 @@ internal static partial class TensorPrimitives_ /// The values to compare. /// The value to compare with each element. /// The destination for the maximum values. + /// is shorter than . + /// + /// and overlap without beginning at the same memory location. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Max(ReadOnlySpan x, T y, Span destination) where T : INumber diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Multiply.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Multiply.cs index deb1e922e..367a7ab0b 100644 --- a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Multiply.cs +++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Multiply.cs @@ -16,6 +16,10 @@ internal static partial class TensorPrimitives_ /// The multiplicands. /// The multiplier. /// The destination for the products. + /// is shorter than . + /// + /// and overlap without beginning at the same memory location. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Multiply(ReadOnlySpan x, T y, Span destination) where T : IMultiplyOperators, IMultiplicativeIdentity diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs index 99dc80184..05f93ac66 100644 --- a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs +++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs @@ -56,6 +56,10 @@ internal static partial class TensorPrimitives_ /// The element type. /// The values to negate. /// The destination for the negated values. + /// is shorter than . + /// + /// and overlap without beginning at the same memory location. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Negate(ReadOnlySpan x, Span destination) where T : IUnaryNegationOperators @@ -72,6 +76,13 @@ internal static partial class TensorPrimitives_ private static void InvokeSpanIntoSpan(ReadOnlySpan x, Span destination) where TOperator : struct, IUnaryOperator { + if (x.Length > destination.Length) + { + ThrowDestinationTooShort(); + } + + ValidateInputOutputSpanNonOverlapping(x, destination); + ref T xRef = ref MemoryMarshal.GetReference(x); ref T destinationRef = ref MemoryMarshal.GetReference(destination); nuint length = (uint)x.Length; diff --git a/tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs b/tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs index 5ce5401f8..b2f4dfe76 100644 --- a/tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs +++ b/tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs @@ -51,6 +51,88 @@ public class TensorPrimitivesTests | 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. ///