diff --git a/src/libraries/System.Numerics.Vectors/tests/Vector2Tests.cs b/src/libraries/System.Numerics.Vectors/tests/Vector2Tests.cs index cc2b20ab5beb83..8040b219fcb6ae 100644 --- a/src/libraries/System.Numerics.Vectors/tests/Vector2Tests.cs +++ b/src/libraries/System.Numerics.Vectors/tests/Vector2Tests.cs @@ -1816,9 +1816,76 @@ public void ShuffleTest(float x, float y) [Theory] [InlineData(1.0f, 2.0f, 3.0f)] [InlineData(5.0f, 6.0f, 11.0f)] - public void SumTest(float x, float y, float expectedResult) - { - Assert.Equal(expectedResult, Vector2.Sum(Vector2.Create(x, y))); + [InlineData(-0.0f, -0.0f, -0.0f)] + [InlineData(1.0f, -1.0f, +0.0f)] + [InlineData(float.Epsilon, -float.Epsilon, +0.0f)] + public void SumAndDotTest(float x, float y, float expectedResult) + { + Vector2 value = Vector2.Create(x, y); + Assert.Equal(BitConverter.SingleToInt32Bits(expectedResult), BitConverter.SingleToInt32Bits(Vector2.Sum(value))); + Assert.Equal(BitConverter.SingleToInt32Bits(expectedResult), BitConverter.SingleToInt32Bits(Vector2.Dot(value, Vector2.One))); + + Vector2 sum = Vector2.Create(Vector2.Sum(value)); + Vector2 dot = Vector2.Create(Vector2.Dot(value, Vector2.One)); + for (int i = 0; i < ElementCount; i++) + { + Assert.Equal(BitConverter.SingleToInt32Bits(expectedResult), BitConverter.SingleToInt32Bits(sum[i])); + Assert.Equal(BitConverter.SingleToInt32Bits(expectedResult), BitConverter.SingleToInt32Bits(dot[i])); + } + } + + [Theory] + [InlineData(float.NaN, 1.0f, float.NaN)] + [InlineData(1.0f, float.NaN, float.NaN)] + [InlineData(float.PositiveInfinity, 1.0f, float.PositiveInfinity)] + [InlineData(1.0f, float.PositiveInfinity, float.PositiveInfinity)] + [InlineData(float.NegativeInfinity, 1.0f, float.NegativeInfinity)] + [InlineData(1.0f, float.NegativeInfinity, float.NegativeInfinity)] + [InlineData(float.PositiveInfinity, float.NegativeInfinity, float.NaN)] + [InlineData(float.NegativeInfinity, float.PositiveInfinity, float.NaN)] + public void ReductionsWithNonFiniteElementsTest(float x, float y, float expectedResult) + { + Vector2 value = Vector2.Create(x, y); + Assert.Equal(expectedResult, Vector2.Sum(value)); + Assert.Equal(expectedResult, Vector2.Dot(value, Vector2.One)); + + float lengthSquared = (x * x) + (y * y); + float length = float.Sqrt(lengthSquared); + Assert.Equal(length, value.Length()); + Assert.Equal(lengthSquared, value.LengthSquared()); + Assert.Equal(length, Vector2.Distance(value, Vector2.Zero)); + Assert.Equal(lengthSquared, Vector2.DistanceSquared(value, Vector2.Zero)); + + Vector2 reflected = Vector2.Reflect(value, Vector2.One); + float reflectionScale = -(expectedResult + expectedResult); + Vector2 normalized = Vector2.Normalize(value); + for (int i = 0; i < ElementCount; i++) + { + Assert.Equal(float.MultiplyAddEstimate(reflectionScale, 1.0f, value[i]), reflected[i]); + Assert.Equal(value[i] / length, normalized[i]); + } + } + + [Theory] + [InlineData(float.NaN)] + [InlineData(float.PositiveInfinity)] + [InlineData(float.NegativeInfinity)] + [InlineData(42.0f)] + public void ReductionsIgnoreUpperElementsTest(float upper) + { + // The JIT can retain the upper SIMD lanes through AsVector2 and inlined reductions. + // This exercises poisoned lanes when retained, but their preservation is not guaranteed. + Vector2 value = Vector128.Create(3.0f, 4.0f, upper, upper).AsVector2(); + Vector2 normal = Vector128.Create(1.0f, 0.0f, upper, upper).AsVector2(); + + Assert.Equal(7.0f, Vector2.Sum(value)); + Assert.Equal(25.0f, Vector2.Dot(value, value)); + Assert.Equal(5.0f, value.Length()); + Assert.Equal(25.0f, value.LengthSquared()); + Assert.Equal(5.0f, Vector2.Distance(value, Vector2.Zero)); + Assert.Equal(25.0f, Vector2.DistanceSquared(value, Vector2.Zero)); + Assert.Equal(Vector2.Create(3.0f / 5.0f, 4.0f / 5.0f), Vector2.Normalize(value)); + Assert.Equal(Vector2.Create(-3.0f, 4.0f), Vector2.Reflect(value, normal)); } [Theory] diff --git a/src/libraries/System.Numerics.Vectors/tests/Vector3Tests.cs b/src/libraries/System.Numerics.Vectors/tests/Vector3Tests.cs index 08399cd7b03c74..ee96308e06255a 100644 --- a/src/libraries/System.Numerics.Vectors/tests/Vector3Tests.cs +++ b/src/libraries/System.Numerics.Vectors/tests/Vector3Tests.cs @@ -1845,9 +1845,81 @@ public void ShuffleTest(float x, float y, float z) [Theory] [InlineData(1.0f, 2.0f, 3.0f, 6.0f)] [InlineData(5.0f, 6.0f, 7.0f, 18.0f)] - public void SumTest(float x, float y, float z, float expectedResult) + [InlineData(1.0f, -1.0f, -0.0f, +0.0f)] + [InlineData(float.Epsilon, -float.Epsilon, -0.0f, +0.0f)] + [InlineData(16777216.0f, -16777216.0f, 1.0f, 1.0f)] + public void SumAndDotTest(float x, float y, float z, float expectedResult) + { + Vector3 value = Vector3.Create(x, y, z); + Assert.Equal(BitConverter.SingleToInt32Bits(expectedResult), BitConverter.SingleToInt32Bits(Vector3.Sum(value))); + Assert.Equal(BitConverter.SingleToInt32Bits(expectedResult), BitConverter.SingleToInt32Bits(Vector3.Dot(value, Vector3.One))); + + Vector3 sum = Vector3.Create(Vector3.Sum(value)); + Vector3 dot = Vector3.Create(Vector3.Dot(value, Vector3.One)); + Vector3 reflected = Vector3.Reflect(value, Vector3.One); + float reflectionScale = -(expectedResult + expectedResult); + for (int i = 0; i < ElementCount; i++) + { + Assert.Equal(BitConverter.SingleToInt32Bits(expectedResult), BitConverter.SingleToInt32Bits(sum[i])); + Assert.Equal(BitConverter.SingleToInt32Bits(expectedResult), BitConverter.SingleToInt32Bits(dot[i])); + Assert.Equal( + BitConverter.SingleToInt32Bits(float.MultiplyAddEstimate(reflectionScale, 1.0f, value[i])), + BitConverter.SingleToInt32Bits(reflected[i])); + } + } + + [Fact] + [SkipOnMono("Mono's Vector3.Dot intrinsic can include an extra zero lane, changing negative zero to positive zero.")] + public void SumAndDotNegativeZeroTest() { - Assert.Equal(expectedResult, Vector3.Sum(Vector3.Create(x, y, z))); + SumAndDotTest(-0.0f, -0.0f, -0.0f, -0.0f); + } + + [Theory] + [InlineData(float.NaN, 1.0f, 2.0f, float.NaN)] + [InlineData(1.0f, float.NaN, 2.0f, float.NaN)] + [InlineData(1.0f, 2.0f, float.NaN, float.NaN)] + [InlineData(float.PositiveInfinity, 1.0f, 2.0f, float.PositiveInfinity)] + [InlineData(1.0f, 2.0f, float.NegativeInfinity, float.NegativeInfinity)] + [InlineData(float.PositiveInfinity, float.NegativeInfinity, 1.0f, float.NaN)] + [InlineData(1.0f, float.PositiveInfinity, float.NegativeInfinity, float.NaN)] + public void ReductionsWithNonFiniteElementsTest(float x, float y, float z, float expectedResult) + { + Vector3 value = Vector3.Create(x, y, z); + Assert.Equal(expectedResult, Vector3.Sum(value)); + Assert.Equal(expectedResult, Vector3.Dot(value, Vector3.One)); + + Vector3 reflected = Vector3.Reflect(value, Vector3.One); + float reflectionScale = -(expectedResult + expectedResult); + float length = float.Sqrt(((x * x) + (y * y)) + (z * z)); + Vector3 normalized = Vector3.Normalize(value); + for (int i = 0; i < ElementCount; i++) + { + Assert.Equal(float.MultiplyAddEstimate(reflectionScale, 1.0f, value[i]), reflected[i]); + Assert.Equal(value[i] / length, normalized[i]); + } + } + + [Theory] + [InlineData(float.NaN)] + [InlineData(float.PositiveInfinity)] + [InlineData(float.NegativeInfinity)] + [InlineData(42.0f)] + public void ReductionsIgnoreUpperElementsTest(float upper) + { + // The JIT can retain the upper SIMD lane through AsVector3 and inlined reductions. + // This exercises a poisoned lane when retained, but its preservation is not guaranteed. + Vector3 value = Vector128.Create(2.0f, 3.0f, 6.0f, upper).AsVector3(); + Vector3 normal = Vector128.Create(1.0f, 0.0f, 0.0f, upper).AsVector3(); + + Assert.Equal(11.0f, Vector3.Sum(value)); + Assert.Equal(49.0f, Vector3.Dot(value, value)); + Assert.Equal(7.0f, value.Length()); + Assert.Equal(49.0f, value.LengthSquared()); + Assert.Equal(7.0f, Vector3.Distance(value, Vector3.Zero)); + Assert.Equal(49.0f, Vector3.DistanceSquared(value, Vector3.Zero)); + Assert.Equal(Vector3.Create(2.0f / 7.0f, 3.0f / 7.0f, 6.0f / 7.0f), Vector3.Normalize(value)); + Assert.Equal(Vector3.Create(-2.0f, 3.0f, 6.0f), Vector3.Reflect(value, normal)); } [Theory] diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/Vector2.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/Vector2.cs index f2ac590846e2e1..fa3cf3506a169b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/Vector2.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/Vector2.cs @@ -481,7 +481,11 @@ public static float Cross(Vector2 value1, Vector2 value2) /// The distance. [Intrinsic] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Distance(Vector2 value1, Vector2 value2) => Vector128.Distance(value1.AsVector128(), value2.AsVector128()); + public static float Distance(Vector2 value1, Vector2 value2) + { + Vector128 difference = value1.AsVector128Unsafe() - value2.AsVector128Unsafe(); + return float.Sqrt(Sum((difference * difference).AsVector2())); + } /// Returns the Euclidean distance squared between two specified points. /// The first point. @@ -489,7 +493,11 @@ public static float Cross(Vector2 value1, Vector2 value2) /// The distance squared. [Intrinsic] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float DistanceSquared(Vector2 value1, Vector2 value2) => Vector128.DistanceSquared(value1.AsVector128(), value2.AsVector128()); + public static float DistanceSquared(Vector2 value1, Vector2 value2) + { + Vector128 difference = value1.AsVector128Unsafe() - value2.AsVector128Unsafe(); + return Sum((difference * difference).AsVector2()); + } /// Divides the first vector by the second. /// The first vector. @@ -513,7 +521,7 @@ public static float Cross(Vector2 value1, Vector2 value2) /// The dot product. [Intrinsic] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Dot(Vector2 value1, Vector2 value2) => Vector128.Dot(value1.AsVector128(), value2.AsVector128()); + public static float Dot(Vector2 value1, Vector2 value2) => Sum((value1.AsVector128Unsafe() * value2.AsVector128Unsafe()).AsVector2()); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -844,7 +852,11 @@ public static unsafe Vector2 LoadAligned(float* source) /// The normalized vector. [Intrinsic] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Normalize(Vector2 value) => Vector128.Normalize(value.AsVector128()).AsVector2(); + public static Vector2 Normalize(Vector2 value) + { + Vector128 vector = value.AsVector128Unsafe(); + return (vector / Vector128.Sqrt(SumAndBroadcast(vector * vector).AsVector128Unsafe())).AsVector2(); + } /// [Intrinsic] @@ -866,10 +878,10 @@ public static Vector2 Reflect(Vector2 vector, Vector2 normal) // This implementation is based on the DirectX Math Library XMVector2Reflect method // https://github.com/microsoft/DirectXMath/blob/master/Inc/DirectXMathVector.inl - Vector128 vVector = vector.AsVector128(); - Vector128 vNormal = normal.AsVector128(); + Vector128 vVector = vector.AsVector128Unsafe(); + Vector128 vNormal = normal.AsVector128Unsafe(); - Vector128 tmp = Vector128.Create(Vector128.Dot(vVector, vNormal)); + Vector128 tmp = SumAndBroadcast(vVector * vNormal).AsVector128Unsafe(); return Vector128.MultiplyAddEstimate(-(tmp + tmp), vNormal, vVector).AsVector2(); } @@ -926,7 +938,14 @@ public static (Vector2 Sin, Vector2 Cos) SinCos(Vector2 vector) /// [Intrinsic] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Sum(Vector2 value) => Vector128.Sum(value.AsVector128()); + public static float Sum(Vector2 value) => SumAndBroadcast(value.AsVector128Unsafe()).ToScalar(); + + [Intrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector2 SumAndBroadcast(Vector128 value) + { + return (value + Vector128.Shuffle(value, Vector128.Create(1, 0, 3, 2))).AsVector2(); + } /// Transforms a vector by a specified 3x2 matrix. /// The vector to transform. @@ -1104,7 +1123,11 @@ public readonly bool TryCopyTo(Span destination) /// [Intrinsic] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly float Length() => Vector128.Length(this.AsVector128()); + public readonly float Length() + { + Vector128 vector = this.AsVector128Unsafe(); + return float.Sqrt(Sum((vector * vector).AsVector2())); + } /// Returns the length of the vector squared. /// The vector's length squared. @@ -1112,7 +1135,11 @@ public readonly bool TryCopyTo(Span destination) /// [Intrinsic] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly float LengthSquared() => Vector128.LengthSquared(this.AsVector128()); + public readonly float LengthSquared() + { + Vector128 vector = this.AsVector128Unsafe(); + return Sum((vector * vector).AsVector2()); + } /// Returns the string representation of the current instance using default formatting. /// The string representation of the current instance. diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/Vector3.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/Vector3.cs index c090caec23a245..472e1b145c1aeb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/Vector3.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/Vector3.cs @@ -517,7 +517,11 @@ internal static Vector128 Cross(Vector128 vector1, Vector128The distance. [Intrinsic] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Distance(Vector3 value1, Vector3 value2) => Vector128.Distance(value1.AsVector128(), value2.AsVector128()); + public static float Distance(Vector3 value1, Vector3 value2) + { + Vector128 difference = value1.AsVector128Unsafe() - value2.AsVector128Unsafe(); + return float.Sqrt(Sum((difference * difference).AsVector3())); + } /// Returns the Euclidean distance squared between two specified points. /// The first point. @@ -525,7 +529,11 @@ internal static Vector128 Cross(Vector128 vector1, Vector128The distance squared. [Intrinsic] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float DistanceSquared(Vector3 value1, Vector3 value2) => Vector128.DistanceSquared(value1.AsVector128(), value2.AsVector128()); + public static float DistanceSquared(Vector3 value1, Vector3 value2) + { + Vector128 difference = value1.AsVector128Unsafe() - value2.AsVector128Unsafe(); + return Sum((difference * difference).AsVector3()); + } /// Divides the first vector by the second. /// The first vector. @@ -549,7 +557,7 @@ internal static Vector128 Cross(Vector128 vector1, Vector128The dot product. [Intrinsic] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Dot(Vector3 vector1, Vector3 vector2) => Vector128.Dot(vector1.AsVector128(), vector2.AsVector128()); + public static float Dot(Vector3 vector1, Vector3 vector2) => Sum((vector1.AsVector128Unsafe() * vector2.AsVector128Unsafe()).AsVector3()); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -881,7 +889,11 @@ public static unsafe Vector3 LoadAligned(float* source) /// The normalized vector. [Intrinsic] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 Normalize(Vector3 value) => Vector128.Normalize(value.AsVector128()).AsVector3(); + public static Vector3 Normalize(Vector3 value) + { + Vector128 vector = value.AsVector128Unsafe(); + return (vector / Vector128.Sqrt(SumAndBroadcast(vector * vector).AsVector128Unsafe())).AsVector3(); + } /// [Intrinsic] @@ -903,10 +915,10 @@ public static Vector3 Reflect(Vector3 vector, Vector3 normal) // This implementation is based on the DirectX Math Library XMVector3Reflect method // https://github.com/microsoft/DirectXMath/blob/master/Inc/DirectXMathVector.inl - Vector128 vVector = vector.AsVector128(); - Vector128 vNormal = normal.AsVector128(); + Vector128 vVector = vector.AsVector128Unsafe(); + Vector128 vNormal = normal.AsVector128Unsafe(); - Vector128 tmp = Vector128.Create(Vector128.Dot(vVector, vNormal)); + Vector128 tmp = SumAndBroadcast(vVector * vNormal).AsVector128Unsafe(); return Vector128.MultiplyAddEstimate(-(tmp + tmp), vNormal, vVector).AsVector3(); } @@ -964,7 +976,20 @@ public static (Vector3 Sin, Vector3 Cos) SinCos(Vector3 vector) /// [Intrinsic] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Sum(Vector3 value) => Vector128.Sum(value.AsVector128()); + public static float Sum(Vector3 value) + { + Vector128 vector = value.AsVector128Unsafe(); + Vector128 sum = Vector128.Shuffle(vector, Vector128.Create(1, 0, 3, 2)) + vector; + return (Vector128.Shuffle(vector, Vector128.Create(2)) + sum).ToScalar(); + } + + [Intrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector3 SumAndBroadcast(Vector128 value) + { + Vector128 sum = Vector128.Shuffle(value, Vector128.Create(1, 0, 3, 2)) + value; + return Vector128.Create((Vector128.Shuffle(value, Vector128.Create(2)) + sum).ToScalar()).AsVector3(); + } /// Transforms a vector by a specified 4x4 matrix. /// The vector to transform. @@ -1114,7 +1139,11 @@ public readonly bool TryCopyTo(Span destination) /// [Intrinsic] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly float Length() => Vector128.Length(this.AsVector128()); + public readonly float Length() + { + Vector128 vector = this.AsVector128Unsafe(); + return float.Sqrt(Sum((vector * vector).AsVector3())); + } /// Returns the length of the vector squared. /// The vector's length squared. @@ -1122,7 +1151,11 @@ public readonly bool TryCopyTo(Span destination) /// [Intrinsic] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly float LengthSquared() => Vector128.LengthSquared(this.AsVector128()); + public readonly float LengthSquared() + { + Vector128 vector = this.AsVector128Unsafe(); + return Sum((vector * vector).AsVector3()); + } /// Returns the string representation of the current instance using default formatting. /// The string representation of the current instance.