diff --git a/src/ImageSharp/Common/Helpers/ColorNumerics.cs b/src/ImageSharp/Common/Helpers/ColorNumerics.cs index d13eead453..8e99728314 100644 --- a/src/ImageSharp/Common/Helpers/ColorNumerics.cs +++ b/src/ImageSharp/Common/Helpers/ColorNumerics.cs @@ -26,18 +26,7 @@ internal static class ColorNumerics /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetBT709Luminance(Vector4 vector, int luminanceLevels) - { - float luminance = Vector4.Dot(vector, Bt709); - if (float.IsNaN(luminance)) - { - return 0; - } - - // Floating-point pixel formats can contain values outside their normalized range. - luminance = Math.Clamp(luminance, 0F, 1F); - return (int)MathF.Round(luminance * (luminanceLevels - 1)); - } + public static int GetBT709Luminance(Vector4 vector, int luminanceLevels) => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (luminanceLevels - 1)); /// /// Gets the luminance from the rgb components using the formula diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs index 8980d2b53e..4154cbf1f7 100644 --- a/src/ImageSharp/Common/Helpers/Numerics.cs +++ b/src/ImageSharp/Common/Helpers/Numerics.cs @@ -263,63 +263,95 @@ internal static class Numerics } /// - /// Returns the value clamped to the inclusive range of min and max. + /// Returns the value clamped to the inclusive range of min and max, mapping NaN to min. /// /// The value to clamp. /// The minimum inclusive value. /// The maximum inclusive value. /// The clamped . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Clamp(float value, float min, float max) - { - if (value > max) - { - return max; - } - - if (value < min) - { - return min; - } - - return value; - } + public static float Clamp(float value, float min, float max) => Clamp(value, min, max); /// - /// Returns the value clamped to the inclusive range of min and max. + /// Returns the value clamped to the inclusive range of min and max, mapping NaN to min. /// /// The value to clamp. /// The minimum inclusive value. /// The maximum inclusive value. /// The clamped . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double Clamp(double value, double min, double max) + public static double Clamp(double value, double min, double max) => Clamp(value, min, max); + + /// + /// Clamps components to the inclusive range of min and max, mapping NaN to min. + /// + /// The components to clamp. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Clamp(Vector2 value, Vector2 min, Vector2 max) => Clamp(value.AsVector128(), min.AsVector128(), max.AsVector128()).AsVector2(); + + /// + /// Clamps components to the inclusive range of min and max, mapping NaN to min. + /// + /// The component type. + /// The components to clamp. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Clamp(Vector128 value, Vector128 min, Vector128 max) + where T : struct, INumber { - if (value > max) - { - return max; - } + // Ordered comparisons map NaN to min and preserve in-range signed zero on every runtime. + Vector128 lowerClamped = Vector128.ConditionalSelect(Vector128.GreaterThanOrEqual(value, min), value, min); + return Vector128.ConditionalSelect(Vector128.GreaterThan(value, max), max, lowerClamped); + } - if (value < min) - { - return min; - } + /// + /// Clamps components to the inclusive range of min and max, mapping NaN to min. + /// + /// The component type. + /// The components to clamp. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Clamp(Vector256 value, Vector256 min, Vector256 max) + where T : struct, INumber + { + // Ordered comparisons map NaN to min and preserve in-range signed zero on every runtime. + Vector256 lowerClamped = Vector256.ConditionalSelect(Vector256.GreaterThanOrEqual(value, min), value, min); + return Vector256.ConditionalSelect(Vector256.GreaterThan(value, max), max, lowerClamped); + } - return value; + /// + /// Clamps components to the inclusive range of min and max, mapping NaN to min. + /// + /// The component type. + /// The components to clamp. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Clamp(Vector512 value, Vector512 min, Vector512 max) + where T : struct, INumber + { + // Ordered comparisons map NaN to min and preserve in-range signed zero on every runtime. + Vector512 lowerClamped = Vector512.ConditionalSelect(Vector512.GreaterThanOrEqual(value, min), value, min); + return Vector512.ConditionalSelect(Vector512.GreaterThan(value, max), max, lowerClamped); } /// - /// Returns the value clamped to the inclusive range of min and max. - /// 5x Faster than - /// on platforms < NET 5. + /// Clamps components to the inclusive range of min and max, mapping NaN to min. /// /// The value to clamp. /// The minimum inclusive value. /// The maximum inclusive value. /// The clamped . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector4 Clamp(Vector4 value, Vector4 min, Vector4 max) - => Vector4.Min(Vector4.Max(value, min), max); + public static Vector4 Clamp(Vector4 value, Vector4 min, Vector4 max) => Clamp(value.AsVector128(), min.AsVector128(), max.AsVector128()).AsVector4(); /// /// Clamps the span values to the inclusive range of min and max. @@ -352,24 +384,24 @@ internal static class Numerics => TensorPrimitives_.Clamp(span, min, max, span); /// - /// Clamps the span values to the inclusive range of min and max. + /// Clamps the span values to the inclusive range of min and max, mapping NaN to min. /// /// The span containing the values to clamp. /// The minimum inclusive value. /// The maximum inclusive value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Clamp(Span span, float min, float max) - => TensorPrimitives_.Clamp(span, min, max, span); + => Clamp(span, min, max); /// - /// Clamps the span values to the inclusive range of min and max. + /// Clamps the span values to the inclusive range of min and max, mapping NaN to min. /// /// The span containing the values to clamp. /// The minimum inclusive value. /// The maximum inclusive value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Clamp(Span span, double min, double max) - => TensorPrimitives_.Clamp(span, min, max, span); + => Clamp(span, min, max); /// /// Pre-multiplies the "x", "y", "z" components of a vector by its "w" component leaving the "w" component intact. @@ -392,7 +424,7 @@ internal static class Numerics public static void ClampRgbToAlpha(ref Vector4 source) { Vector4 alpha = PermuteW(source); - source = WithW(Vector4.Min(Vector4.Max(source, Vector4.Zero), alpha), alpha); + source = WithW(Clamp(source, Vector4.Zero, alpha), alpha); } /// @@ -1071,4 +1103,78 @@ internal static class Numerics [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Normalize(Span span, float sum) => TensorPrimitives_.Divide(span, sum, span); + + /// + /// Clamps a floating-point component while mapping NaN to the lower bound. + /// + /// The component type. + /// The component to clamp. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// The clamped component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T Clamp(T value, T min, T max) + where T : struct, INumber + { + // Ordered comparisons map NaN to min; in-range values retain their original bits, including signed zero. + return value > max ? max : value >= min ? value : min; + } + + /// + /// Applies the scalar clamp contract to floating-point spans in place. + /// + /// The component type. + /// The components to clamp. + /// The inclusive lower bound. + /// The inclusive upper bound. + private static void Clamp(Span span, T min, T max) + where T : struct, INumber + { + ref T start = ref MemoryMarshal.GetReference(span); + int i = 0; + + // Each register uses the same Clamp overload as individual vector callers. Descending widths consume + // the remainder without overlapping stores, and the final components use the scalar overload. + if (Vector512.IsHardwareAccelerated) + { + Vector512 lower = Vector512.Create(min); + Vector512 upper = Vector512.Create(max); + + for (; i <= span.Length - Vector512.Count; i += Vector512.Count) + { + Vector512 value = Vector512.LoadUnsafe(ref start, (nuint)i); + Clamp(value, lower, upper).StoreUnsafe(ref start, (nuint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 lower = Vector256.Create(min); + Vector256 upper = Vector256.Create(max); + + for (; i <= span.Length - Vector256.Count; i += Vector256.Count) + { + Vector256 value = Vector256.LoadUnsafe(ref start, (nuint)i); + Clamp(value, lower, upper).StoreUnsafe(ref start, (nuint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 lower = Vector128.Create(min); + Vector128 upper = Vector128.Create(max); + + for (; i <= span.Length - Vector128.Count; i += Vector128.Count) + { + Vector128 value = Vector128.LoadUnsafe(ref start, (nuint)i); + Clamp(value, lower, upper).StoreUnsafe(ref start, (nuint)i); + } + } + + for (; i < span.Length; i++) + { + ref T value = ref Unsafe.Add(ref start, (uint)i); + value = Clamp(value, min, max); + } + } } diff --git a/src/ImageSharp/PixelFormats/HalfTypeHelper.cs b/src/ImageSharp/PixelFormats/HalfTypeHelper.cs index 53f22eb09a..6de4b30b9e 100644 --- a/src/ImageSharp/PixelFormats/HalfTypeHelper.cs +++ b/src/ImageSharp/PixelFormats/HalfTypeHelper.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.PixelFormats; @@ -50,52 +51,228 @@ internal static class HalfTypeHelper internal static float Unpack(ushort value) => (float)BitConverter.UInt16BitsToHalf(value); /// - /// Normalizes a finite binary16 value to the scaled pixel range. + /// Normalizes a binary16 value to [0, 1], saturating infinities and mapping NaN to zero. /// /// The native binary16 value represented as a . /// The normalized value. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static float ToScaled(float value) => (value * InverseFiniteRange) + ScaledMidpoint; + public static float ToScaled(float value) + { + // Clamp after mapping so native infinities reach the scaled endpoints and NaN becomes zero. + return Numerics.Clamp((value * InverseFiniteRange) + ScaledMidpoint, 0F, 1F); + } /// - /// Normalizes finite binary16 values to the scaled pixel range. + /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. /// /// The native binary16 values. /// The normalized values. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Vector2 ToScaled(Vector2 value) => (value * InverseFiniteRange) + new Vector2(ScaledMidpoint); + public static Vector2 ToScaled(Vector2 value) => ToScaled(value.AsVector128()).AsVector2(); /// - /// Normalizes finite binary16 values to the scaled pixel range. + /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. /// /// The native binary16 values. /// The normalized values. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Vector4 ToScaled(Vector4 value) => (value * InverseFiniteRange) + new Vector4(ScaledMidpoint); + public static Vector4 ToScaled(Vector4 value) => ToScaled(value.AsVector128()).AsVector4(); + + /// + /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. + /// + /// The component values. + /// The converted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 ToScaled(Vector128 value) + { + Vector128 scaled = (value * Vector128.Create(InverseFiniteRange)) + Vector128.Create(ScaledMidpoint); + + return Numerics.Clamp(scaled, Vector128.Zero, Vector128.One); + } + + /// + /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. + /// + /// The component values. + /// The converted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ToScaled(Vector256 value) + { + Vector256 scaled = (value * Vector256.Create(InverseFiniteRange)) + Vector256.Create(ScaledMidpoint); + + return Numerics.Clamp(scaled, Vector256.Zero, Vector256.One); + } + + /// + /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. + /// + /// The component values. + /// The converted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ToScaled(Vector512 value) + { + Vector512 scaled = (value * Vector512.Create(InverseFiniteRange)) + Vector512.Create(ScaledMidpoint); + + return Numerics.Clamp(scaled, Vector512.Zero, Vector512.One); + } + + /// + /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. + /// + /// The component values to convert in place. + public static void ToScaled(Span values) + { + ref Vector4 source = ref MemoryMarshal.GetReference(values); + int i = 0; + + // Each register contains whole RGBA pixels. Convert wide groups first, then narrower + // remainders without revisiting any pixel: mapping the same pixel twice would change its value. + if (Vector512.IsHardwareAccelerated) + { + int pixelsPerRegister = Vector512.Count / Vector128.Count; + + for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister) + { + ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref source, (uint)i)); + + vector = ToScaled(vector); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int pixelsPerRegister = Vector256.Count / Vector128.Count; + + for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister) + { + ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref source, (uint)i)); + + vector = ToScaled(vector); + } + } + + // One Vector4 uses the same 128-bit conversion as an individual pixel, including the + // runtime's software fallback when SIMD is unavailable. No separate scalar mapping is needed. + for (; i < values.Length; i++) + { + ref Vector4 vector = ref Unsafe.Add(ref source, (uint)i); + + vector = ToScaled(vector); + } + } /// - /// Expands a normalized value to the finite binary16 range. + /// Normalizes a scaled value, mapping NaN to zero, and expands it to the finite binary16 range. /// /// The normalized value. /// The native binary16 value represented as a . [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static float FromScaled(float value) => (value * FiniteRange) + FiniteMinimum; + public static float FromScaled(float value) + { + // Clamp before expanding so nonfinite scaled input cannot become nonfinite half storage. + return (Numerics.Clamp(value, 0F, 1F) * FiniteRange) + FiniteMinimum; + } /// - /// Expands normalized values to the finite binary16 range. + /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range. /// /// The normalized values. /// The native binary16 values. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Vector2 FromScaled(Vector2 value) => (value * FiniteRange) + new Vector2(FiniteMinimum); + public static Vector2 FromScaled(Vector2 value) => FromScaled(value.AsVector128()).AsVector2(); /// - /// Expands normalized values to the finite binary16 range. + /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range. /// /// The normalized values. /// The native binary16 values. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Vector4 FromScaled(Vector4 value) => (value * FiniteRange) + new Vector4(FiniteMinimum); + public static Vector4 FromScaled(Vector4 value) => FromScaled(value.AsVector128()).AsVector4(); + + /// + /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range. + /// + /// The component values. + /// The converted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 FromScaled(Vector128 value) + { + Vector128 scaled = Numerics.Clamp(value, Vector128.Zero, Vector128.One); + + return (scaled * Vector128.Create(FiniteRange)) + Vector128.Create(FiniteMinimum); + } + + /// + /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range. + /// + /// The component values. + /// The converted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 FromScaled(Vector256 value) + { + Vector256 scaled = Numerics.Clamp(value, Vector256.Zero, Vector256.One); + + return (scaled * Vector256.Create(FiniteRange)) + Vector256.Create(FiniteMinimum); + } + + /// + /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range. + /// + /// The component values. + /// The converted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 FromScaled(Vector512 value) + { + Vector512 scaled = Numerics.Clamp(value, Vector512.Zero, Vector512.One); + + return (scaled * Vector512.Create(FiniteRange)) + Vector512.Create(FiniteMinimum); + } + + /// + /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range. + /// + /// The component values to convert in place. + public static void FromScaled(Span values) + { + ref Vector4 source = ref MemoryMarshal.GetReference(values); + int i = 0; + + // Each register contains whole RGBA pixels. Clamping and expansion happen together in + // the conversion overload, so each pixel is loaded and stored once without a clamp-only pass. + if (Vector512.IsHardwareAccelerated) + { + int pixelsPerRegister = Vector512.Count / Vector128.Count; + + for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister) + { + ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref source, (uint)i)); + + vector = FromScaled(vector); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int pixelsPerRegister = Vector256.Count / Vector128.Count; + + for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister) + { + ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref source, (uint)i)); + + vector = FromScaled(vector); + } + } + + // The remaining whole pixels use the same 128-bit conversion as individual pixels, + // or its software fallback. Narrowing the remainder never reprocesses a converted pixel. + for (; i < values.Length; i++) + { + ref Vector4 vector = ref Unsafe.Add(ref source, (uint)i); + + vector = FromScaled(vector); + } + } /// /// Unpacks eight binary16 values into two vectors of single-precision values. diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs index 50fc272553..ca90afcf75 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs @@ -250,7 +250,7 @@ public partial struct HalfVector4P : IPixel, IPackedVector [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float QuantizeScaledAlpha(float alpha) { - float nativeAlpha = HalfTypeHelper.FromScaled(Numerics.Clamp(alpha, 0F, 1F)); + float nativeAlpha = HalfTypeHelper.FromScaled(alpha); return HalfTypeHelper.ToScaled(HalfTypeHelper.Unpack(HalfTypeHelper.Pack(nativeAlpha))); } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs index 9eae7d5cbc..a921f99bae 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs @@ -3,7 +3,6 @@ using System.Numerics; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.PixelFormats.Utils; namespace SixLabors.ImageSharp.PixelFormats; @@ -17,11 +16,6 @@ public partial struct HalfVector4 /// internal class PixelOperations : PixelOperations { - private static readonly Vector4 NativeToScaledMultiplier = new(HalfTypeHelper.InverseFiniteRange); - private static readonly Vector4 NativeToScaledOffset = new(HalfTypeHelper.ScaledMidpoint); - private static readonly Vector4 ScaledToNativeMultiplier = new(HalfTypeHelper.FiniteRange); - private static readonly Vector4 ScaledToNativeOffset = new(HalfTypeHelper.FiniteMinimum); - /// protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) { @@ -40,9 +34,9 @@ public partial struct HalfVector4 // Association uses normalized opacity, not the native binary16 alpha value. RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination); - Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); + HalfTypeHelper.ToScaled(destination); Numerics.Premultiply(destination); - Vector4Converters.MultiplyThenAdd(destination, ScaledToNativeMultiplier, ScaledToNativeOffset); + HalfTypeHelper.FromScaled(destination); } /// @@ -52,7 +46,7 @@ public partial struct HalfVector4 destination = destination[..source.Length]; RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination); - Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); + HalfTypeHelper.ToScaled(destination); } /// @@ -77,9 +71,9 @@ public partial struct HalfVector4 Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); // Restore normalized opacity before unassociating, then return the result to the native binary16 range. - Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset); + HalfTypeHelper.ToScaled(source); Numerics.UnPremultiply(source); - Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); + HalfTypeHelper.FromScaled(source); RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); } @@ -88,7 +82,7 @@ public partial struct HalfVector4 { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); + HalfTypeHelper.FromScaled(source); RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); } @@ -98,7 +92,7 @@ public partial struct HalfVector4 Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); Numerics.UnPremultiply(source); - Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); + HalfTypeHelper.FromScaled(source); RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs index 52b2dbf376..172159a49c 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs @@ -20,16 +20,11 @@ public partial struct HalfVector4P /// internal class PixelOperations : AssociatedAlphaPixelOperations { - private static readonly Vector4 NativeToScaledMultiplier = new(HalfTypeHelper.InverseFiniteRange); - private static readonly Vector4 NativeToScaledOffset = new(HalfTypeHelper.ScaledMidpoint); - private static readonly Vector4 ScaledToNativeMultiplier = new(HalfTypeHelper.FiniteRange); - private static readonly Vector4 ScaledToNativeOffset = new(HalfTypeHelper.FiniteMinimum); - /// protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) { this.ToUnassociatedScaledVector4(configuration, source, destination); - Vector4Converters.MultiplyThenAdd(destination[..source.Length], ScaledToNativeMultiplier, ScaledToNativeOffset); + HalfTypeHelper.FromScaled(destination[..source.Length]); } /// @@ -54,7 +49,7 @@ public partial struct HalfVector4P destination = destination[..source.Length]; RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination); - Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); + HalfTypeHelper.ToScaled(destination); } /// @@ -62,7 +57,7 @@ public partial struct HalfVector4P { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset); + HalfTypeHelper.ToScaled(source); Associate(source); PackAssociatedScaled(source, destination[..source.Length]); } @@ -72,7 +67,7 @@ public partial struct HalfVector4P { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset); + HalfTypeHelper.ToScaled(source); Reassociate(source); PackAssociatedScaled(source, destination[..source.Length]); } @@ -250,7 +245,9 @@ public partial struct HalfVector4P Vector128 storedAlpha = QuantizeScaledAlpha(alpha); Vector128 result = source * (storedAlpha / alpha); result = Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); - result = Vector128.Min(Vector128.Max(result, zero), storedAlpha); + + // Clamp after the alpha ratio, matching the scalar conversion for nonfinite RGB. + result = Numerics.Clamp(result, zero, storedAlpha); return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(alpha, zero), zero, result); } @@ -267,7 +264,9 @@ public partial struct HalfVector4P Vector256 storedAlpha = QuantizeScaledAlpha(alpha); Vector256 result = source * (storedAlpha / alpha); result = Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); - result = Vector256.Min(Vector256.Max(result, zero), storedAlpha); + + // Clamp after the alpha ratio, matching the scalar conversion for nonfinite RGB. + result = Numerics.Clamp(result, zero, storedAlpha); return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(alpha, zero), zero, result); } @@ -285,7 +284,9 @@ public partial struct HalfVector4P Vector512 result = source * (storedAlpha / alpha); Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); result = Vector512.ConditionalSelect(alphaMask, storedAlpha, result); - result = Vector512.Min(Vector512.Max(result, zero), storedAlpha); + + // Clamp after the alpha ratio, matching the scalar conversion for nonfinite RGB. + result = Numerics.Clamp(result, zero, storedAlpha); return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(alpha, zero), zero, result); } @@ -297,8 +298,8 @@ public partial struct HalfVector4P [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector128 QuantizeScaledAlpha(Vector128 alpha) { - Vector128 native = (ClampUnit(alpha) * Vector128.Create(HalfTypeHelper.FiniteRange)) + Vector128.Create(HalfTypeHelper.FiniteMinimum); - return (HalfTypeHelper.RoundToHalf(native) * Vector128.Create(HalfTypeHelper.InverseFiniteRange)) + Vector128.Create(HalfTypeHelper.ScaledMidpoint); + Vector128 native = HalfTypeHelper.FromScaled(alpha); + return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native)); } /// @@ -309,8 +310,8 @@ public partial struct HalfVector4P [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector256 QuantizeScaledAlpha(Vector256 alpha) { - Vector256 native = (ClampUnit(alpha) * Vector256.Create(HalfTypeHelper.FiniteRange)) + Vector256.Create(HalfTypeHelper.FiniteMinimum); - return (HalfTypeHelper.RoundToHalf(native) * Vector256.Create(HalfTypeHelper.InverseFiniteRange)) + Vector256.Create(HalfTypeHelper.ScaledMidpoint); + Vector256 native = HalfTypeHelper.FromScaled(alpha); + return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native)); } /// @@ -321,45 +322,33 @@ public partial struct HalfVector4P [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector512 QuantizeScaledAlpha(Vector512 alpha) { - Vector512 native = (ClampUnit(alpha) * Vector512.Create(HalfTypeHelper.FiniteRange)) + Vector512.Create(HalfTypeHelper.FiniteMinimum); - return (HalfTypeHelper.RoundToHalf(native) * Vector512.Create(HalfTypeHelper.InverseFiniteRange)) + Vector512.Create(HalfTypeHelper.ScaledMidpoint); + Vector512 native = HalfTypeHelper.FromScaled(alpha); + return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native)); } /// - /// Clamps vectors to the scaled color range while preserving NaN lanes. + /// Clamps vectors to the scaled color range, mapping NaN lanes to zero. /// /// The vectors to clamp. /// The clamped vectors. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 ClampUnit(Vector128 source) - { - Vector128 clamped = Vector128.Min(Vector128.Max(source, Vector128.Zero), Vector128.One); - return Vector128.ConditionalSelect(Vector128.Equals(source, source), clamped, source); - } + private static Vector128 ClampUnit(Vector128 source) => Numerics.Clamp(source, Vector128.Zero, Vector128.One); /// - /// Clamps vectors to the scaled color range while preserving NaN lanes. + /// Clamps vectors to the scaled color range, mapping NaN lanes to zero. /// /// The vectors to clamp. /// The clamped vectors. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 ClampUnit(Vector256 source) - { - Vector256 clamped = Vector256.Min(Vector256.Max(source, Vector256.Zero), Vector256.One); - return Vector256.ConditionalSelect(Vector256.Equals(source, source), clamped, source); - } + private static Vector256 ClampUnit(Vector256 source) => Numerics.Clamp(source, Vector256.Zero, Vector256.One); /// - /// Clamps vectors to the scaled color range while preserving NaN lanes. + /// Clamps vectors to the scaled color range, mapping NaN lanes to zero. /// /// The vectors to clamp. /// The clamped vectors. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 ClampUnit(Vector512 source) - { - Vector512 clamped = Vector512.Min(Vector512.Max(source, Vector512.Zero), Vector512.One); - return Vector512.ConditionalSelect(Vector512.Equals(source, source), clamped, source); - } + private static Vector512 ClampUnit(Vector512 source) => Numerics.Clamp(source, Vector512.Zero, Vector512.One); /// /// Maps associated scaled vectors to native components and packs them as binary16 values. @@ -368,7 +357,7 @@ public partial struct HalfVector4P /// The destination pixels. private static void PackAssociatedScaled(Span source, Span destination) { - Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); + HalfTypeHelper.FromScaled(source); RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination)); } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs index 825ebeec92..a9f3081f1a 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs @@ -277,8 +277,8 @@ public partial struct RgbaHalfP { for (; i <= componentCount - Vector512.Count; i += Vector512.Count) { - Vector512 lower = ClampUnit(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); - Vector512 upper = ClampUnit(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))); + Vector512 lower = Numerics.Clamp(Vector512.LoadUnsafe(ref sourceBase, (nuint)i), Vector512.Zero, Vector512.One); + Vector512 upper = Numerics.Clamp(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count)), Vector512.Zero, Vector512.One); Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); } } @@ -287,8 +287,8 @@ public partial struct RgbaHalfP { for (; i <= componentCount - Vector256.Count; i += Vector256.Count) { - Vector256 lower = ClampUnit(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); - Vector256 upper = ClampUnit(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))); + Vector256 lower = Numerics.Clamp(Vector256.LoadUnsafe(ref sourceBase, (nuint)i), Vector256.Zero, Vector256.One); + Vector256 upper = Numerics.Clamp(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count)), Vector256.Zero, Vector256.One); Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); } } @@ -297,15 +297,15 @@ public partial struct RgbaHalfP { for (; i <= componentCount - Vector128.Count; i += Vector128.Count) { - Vector128 lower = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); - Vector128 upper = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count))); + Vector128 lower = Numerics.Clamp(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), Vector128.Zero, Vector128.One); + Vector128 upper = Numerics.Clamp(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count)), Vector128.Zero, Vector128.One); Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); } if (i < componentCount) { // Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel. - Vector128 vector = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128 vector = Numerics.Clamp(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), Vector128.Zero, Vector128.One); Vector128 packed = HalfTypeHelper.Pack(vector, vector); Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); } @@ -401,8 +401,8 @@ public partial struct RgbaHalfP { for (; i <= componentCount - Vector512.Count; i += Vector512.Count) { - Vector512 lower = ClampUnit(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i))); - Vector512 upper = ClampUnit(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count)))); + Vector512 lower = Numerics.Clamp(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)), Vector512.Zero, Vector512.One); + Vector512 upper = Numerics.Clamp(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))), Vector512.Zero, Vector512.One); Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); } } @@ -411,8 +411,8 @@ public partial struct RgbaHalfP { for (; i <= componentCount - Vector256.Count; i += Vector256.Count) { - Vector256 lower = ClampUnit(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i))); - Vector256 upper = ClampUnit(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count)))); + Vector256 lower = Numerics.Clamp(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)), Vector256.Zero, Vector256.One); + Vector256 upper = Numerics.Clamp(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))), Vector256.Zero, Vector256.One); Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); } } @@ -421,15 +421,15 @@ public partial struct RgbaHalfP { for (; i <= componentCount - Vector128.Count; i += Vector128.Count) { - Vector128 lower = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i))); - Vector128 upper = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count)))); + Vector128 lower = Numerics.Clamp(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)), Vector128.Zero, Vector128.One); + Vector128 upper = Numerics.Clamp(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count))), Vector128.Zero, Vector128.One); Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); } if (i < componentCount) { // Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel. - Vector128 vector = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i))); + Vector128 vector = Numerics.Clamp(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)), Vector128.Zero, Vector128.One); Vector128 packed = HalfTypeHelper.Pack(vector, vector); Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); } @@ -649,48 +649,6 @@ public partial struct RgbaHalfP return Numerics.UnPremultiply(source, alpha); } - /// - /// Clamps vectors to the unit range represented by the pixel format. - /// - /// The vectors to clamp. - /// The clamped vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 ClampUnit(Vector128 source) - { - Vector128 clamped = Vector128.Min(Vector128.Max(source, Vector128.Zero), Vector128.One); - - // Ordered comparison is false for NaN, restoring the source lane to match the scalar clamp contract. - return Vector128.ConditionalSelect(Vector128.Equals(source, source), clamped, source); - } - - /// - /// Clamps vectors to the unit range represented by the pixel format. - /// - /// The vectors to clamp. - /// The clamped vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 ClampUnit(Vector256 source) - { - Vector256 clamped = Vector256.Min(Vector256.Max(source, Vector256.Zero), Vector256.One); - - // Ordered comparison is false for NaN, restoring the source lane to match the scalar clamp contract. - return Vector256.ConditionalSelect(Vector256.Equals(source, source), clamped, source); - } - - /// - /// Clamps vectors to the unit range represented by the pixel format. - /// - /// The vectors to clamp. - /// The clamped vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 ClampUnit(Vector512 source) - { - Vector512 clamped = Vector512.Min(Vector512.Max(source, Vector512.Zero), Vector512.One); - - // Ordered comparison is false for NaN, restoring the source lane to match the scalar clamp contract. - return Vector512.ConditionalSelect(Vector512.Equals(source, source), clamped, source); - } - /// /// Associates unassociated vectors with the alpha value binary16 storage can reproduce. /// @@ -699,7 +657,7 @@ public partial struct RgbaHalfP [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector128 AssociateForStorage(Vector128 source) { - source = ClampUnit(source); + source = Numerics.Clamp(source, Vector128.Zero, Vector128.One); Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); Vector128 storedAlpha = HalfTypeHelper.RoundToHalf(alpha); Vector128 result = source * storedAlpha; @@ -714,7 +672,7 @@ public partial struct RgbaHalfP [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector256 AssociateForStorage(Vector256 source) { - source = ClampUnit(source); + source = Numerics.Clamp(source, Vector256.Zero, Vector256.One); Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); Vector256 storedAlpha = HalfTypeHelper.RoundToHalf(alpha); Vector256 result = source * storedAlpha; @@ -729,7 +687,7 @@ public partial struct RgbaHalfP [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector512 AssociateForStorage(Vector512 source) { - source = ClampUnit(source); + source = Numerics.Clamp(source, Vector512.Zero, Vector512.One); Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); Vector512 storedAlpha = HalfTypeHelper.RoundToHalf(alpha); Vector512 result = source * storedAlpha; @@ -747,11 +705,13 @@ public partial struct RgbaHalfP { Vector128 zero = Vector128.Zero; Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); - Vector128 clampedAlpha = ClampUnit(alpha); + Vector128 clampedAlpha = Numerics.Clamp(alpha, Vector128.Zero, Vector128.One); Vector128 storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); Vector128 result = source * (storedAlpha / alpha); result = Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); - result = Vector128.Min(Vector128.Max(result, zero), storedAlpha); + + // Clamp after the alpha ratio, matching the scalar conversion for nonfinite RGB. + result = Numerics.Clamp(result, zero, storedAlpha); return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(alpha, zero), zero, result); } @@ -765,11 +725,13 @@ public partial struct RgbaHalfP { Vector256 zero = Vector256.Zero; Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); - Vector256 clampedAlpha = ClampUnit(alpha); + Vector256 clampedAlpha = Numerics.Clamp(alpha, Vector256.Zero, Vector256.One); Vector256 storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); Vector256 result = source * (storedAlpha / alpha); result = Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); - result = Vector256.Min(Vector256.Max(result, zero), storedAlpha); + + // Clamp after the alpha ratio, matching the scalar conversion for nonfinite RGB. + result = Numerics.Clamp(result, zero, storedAlpha); return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(alpha, zero), zero, result); } @@ -783,12 +745,14 @@ public partial struct RgbaHalfP { Vector512 zero = Vector512.Zero; Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); - Vector512 clampedAlpha = ClampUnit(alpha); + Vector512 clampedAlpha = Numerics.Clamp(alpha, Vector512.Zero, Vector512.One); Vector512 storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); Vector512 result = source * (storedAlpha / alpha); Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); result = Vector512.ConditionalSelect(alphaMask, storedAlpha, result); - result = Vector512.Min(Vector512.Max(result, zero), storedAlpha); + + // Clamp after the alpha ratio, matching the scalar conversion for nonfinite RGB. + result = Numerics.Clamp(result, zero, storedAlpha); return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(alpha, zero), zero, result); } } diff --git a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs index 96d81617b2..32d6521dff 100644 --- a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs +++ b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs @@ -71,12 +71,7 @@ internal static partial class Vector4Converters /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector4 Invoke(Vector4 source) - { - Vector128 result = (source.AsVector128() * this.multiplier.GetLower().GetLower()) + this.offset.GetLower().GetLower(); - - return result.AsVector4(); - } + public Vector4 Invoke(Vector4 source) => this.Invoke(source.AsVector128()).AsVector4(); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index ec6113be30..d23eedcfee 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. // ReSharper disable InconsistentNaming +using System.Numerics; using System.Runtime.Intrinsics.X86; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Png; @@ -25,6 +26,38 @@ public class TiffDecoderTests : TiffDecoderBaseTester { public static readonly string[] MultiframeTestImages = Multiframes; + /// + /// Decoded floating-point components are normalized before they enter half-vector storage. + /// + /// The encoded floating-point TIFF. + /// The normalized intensity. + [Theory] + [InlineData("49492A00080000000A0000010400010000000800000001010400010000000100000002010300010000002000000003010300010000000100" + + "0000060103000100000001000000110104000100000086000000150103000100000001000000160104000100000001000000170104000100" + + "000020000000530103000100000003000000000000000000807F0000807F0000807F0000807F0000807F0000807F0000807F0000807F", 1F)] + [InlineData("49492A00080000000A0000010400010000000800000001010400010000000100000002010300010000002000000003010300010000000100" + + "0000060103000100000001000000110104000100000086000000150103000100000001000000160104000100000001000000170104000100" + + "000020000000530103000100000003000000000000000000C07F0000C07F0000C07F0000C07F0000C07F0000C07F0000C07F0000C07F", 0F)] + [InlineData("49492A00080000000A0000010400010000000800000001010400010000000100000002010300010000002000000003010300010000000100" + + "0000060103000100000001000000110104000100000086000000150103000100000001000000160104000100000001000000170104000100" + + "000020000000530103000100000003000000000000000000004000000040000000400000004000000040000000400000004000000040", 1F)] + [InlineData("49492A00080000000A0000010400010000000800000001010400010000000100000002010300010000002000000003010300010000000100" + + "0000060103000100000001000000110104000100000086000000150103000100000001000000160104000100000001000000170104000100" + + "000020000000530103000100000003000000000000000000003F0000003F0000003F0000003F0000003F0000003F0000003F0000003F", .5F)] + public void Decode_FloatingPointSamples_NormalizesHalfVector4(string hex, float intensity) + { + byte[] data = Convert.FromHexString(hex); + using Image image = Image.Load(data); + Assert.Equal(new Size(8, 1), image.Size); + + Vector4 expected = new(intensity, intensity, intensity, 1F); + + for (int x = 0; x < image.Width; x++) + { + Assert.Equal(expected, image[x, 0].ToScaledVector4()); + } + } + [Theory] [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] [WithFile(Cmyk64BitDeflate, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/Helpers/NumericsTests.cs b/tests/ImageSharp.Tests/Helpers/NumericsTests.cs index 35109d352f..77ecc447ee 100644 --- a/tests/ImageSharp.Tests/Helpers/NumericsTests.cs +++ b/tests/ImageSharp.Tests/Helpers/NumericsTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.Tests.Helpers; @@ -305,6 +306,114 @@ public class NumericsTests (v, m1, m2) => Numerics.Clamp(v, m1, m2)); } + /// + /// Scalar, SIMD, and span clamps map nonfinite values to the requested bounds. + /// + /// The lower bound. + /// The upper bound. + [Theory] + [InlineData(0F, 1F)] + [InlineData(-2F, 3F)] + [InlineData(.25F, .75F)] + public void ClampSingle_NormalizesNonfiniteValues(float min, float max) + { + float midpoint = (min + max) / 2; + float[] inputs = [float.NaN, float.PositiveInfinity, float.NegativeInfinity, midpoint]; + float[] normalized = [min, max, min, midpoint]; + float[] values = new float[65]; + float[] expected = new float[values.Length]; + + // The length includes complete registers and remainders for every supported SIMD width. + for (int i = 0; i < values.Length; i++) + { + values[i] = inputs[i % inputs.Length]; + expected[i] = normalized[i % normalized.Length]; + Assert.Equal(expected[i], Numerics.Clamp(values[i], min, max)); + } + + Vector4 input = new(inputs[0], inputs[1], inputs[2], inputs[3]); + Vector4 result = new(normalized[0], normalized[1], normalized[2], normalized[3]); + Assert.Equal(result, Numerics.Clamp(input, new Vector4(min), new Vector4(max))); + Assert.Equal(new Vector2(min, max), Numerics.Clamp(new Vector2(float.NaN, float.PositiveInfinity), new Vector2(min), new Vector2(max))); + + Vector128 vector128 = Vector128.LoadUnsafe(ref values[0]); + Assert.Equal(Vector128.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector128, Vector128.Create(min), Vector128.Create(max))); + + Vector256 vector256 = Vector256.LoadUnsafe(ref values[0]); + Assert.Equal(Vector256.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector256, Vector256.Create(min), Vector256.Create(max))); + + Vector512 vector512 = Vector512.LoadUnsafe(ref values[0]); + Assert.Equal(Vector512.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector512, Vector512.Create(min), Vector512.Create(max))); + + Numerics.Clamp(values, min, max); + Assert.Equal(expected, values); + } + + /// + /// Scalar, SIMD, and span clamps map nonfinite values to the requested bounds. + /// + /// The lower bound. + /// The upper bound. + [Theory] + [InlineData(0D, 1D)] + [InlineData(-2D, 3D)] + [InlineData(.25D, .75D)] + public void ClampDouble_NormalizesNonfiniteValues(double min, double max) + { + double midpoint = (min + max) / 2; + double[] inputs = [double.NaN, double.PositiveInfinity, double.NegativeInfinity, midpoint]; + double[] normalized = [min, max, min, midpoint]; + double[] values = new double[65]; + double[] expected = new double[values.Length]; + + // The length includes complete registers and remainders for every supported SIMD width. + for (int i = 0; i < values.Length; i++) + { + values[i] = inputs[i % inputs.Length]; + expected[i] = normalized[i % normalized.Length]; + Assert.Equal(expected[i], Numerics.Clamp(values[i], min, max)); + } + + Vector128 vector128 = Vector128.LoadUnsafe(ref values[0]); + Assert.Equal(Vector128.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector128, Vector128.Create(min), Vector128.Create(max))); + + Vector256 vector256 = Vector256.LoadUnsafe(ref values[0]); + Assert.Equal(Vector256.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector256, Vector256.Create(min), Vector256.Create(max))); + + Vector512 vector512 = Vector512.LoadUnsafe(ref values[0]); + Assert.Equal(Vector512.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector512, Vector512.Create(min), Vector512.Create(max))); + + Numerics.Clamp(values, min, max); + Assert.Equal(expected, values); + } + + /// + /// Clamping an in-range zero preserves its sign in scalar and bulk conversions. + /// + [Fact] + public void Clamp_PreservesInRangeSignedZero() + { + float[] singles = new float[65]; + double[] doubles = new double[65]; + + for (int i = 0; i < singles.Length; i++) + { + singles[i] = i % 2 == 0 ? -0F : 0F; + doubles[i] = i % 2 == 0 ? -0D : 0D; + Assert.Equal(BitConverter.SingleToInt32Bits(singles[i]), BitConverter.SingleToInt32Bits(Numerics.Clamp(singles[i], 0F, 1F))); + Assert.Equal(BitConverter.DoubleToInt64Bits(doubles[i]), BitConverter.DoubleToInt64Bits(Numerics.Clamp(doubles[i], 0D, 1D))); + } + + Numerics.Clamp(singles, 0F, 1F); + Numerics.Clamp(doubles, 0D, 1D); + + for (int i = 0; i < singles.Length; i++) + { + Assert.Equal(BitConverter.SingleToInt32Bits(i % 2 == 0 ? -0F : 0F), BitConverter.SingleToInt32Bits(singles[i])); + Assert.Equal(BitConverter.DoubleToInt64Bits(i % 2 == 0 ? -0D : 0D), BitConverter.DoubleToInt64Bits(doubles[i])); + } + } + private static void TestClampSpan( int length, T min, diff --git a/tests/ImageSharp.Tests/PixelFormats/FloatingPointPixelNormalizationTests.cs b/tests/ImageSharp.Tests/PixelFormats/FloatingPointPixelNormalizationTests.cs new file mode 100644 index 0000000000..5718da5b03 --- /dev/null +++ b/tests/ImageSharp.Tests/PixelFormats/FloatingPointPixelNormalizationTests.cs @@ -0,0 +1,183 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.PixelFormats; + +[Trait("Category", "PixelFormats")] +public class FloatingPointPixelNormalizationTests +{ + /// + /// HalfSingle normalizes scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void HalfSingle_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); + + /// + /// HalfVector2 normalizes scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void HalfVector2_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); + + /// + /// HalfVector4 normalizes scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void HalfVector4_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); + + /// + /// HalfVector4P normalizes scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void HalfVector4P_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); + + /// + /// RgbaVector normalizes scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void RgbaVector_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); + + /// + /// RgbaHalf normalizes scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void RgbaHalf_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); + + /// + /// RgbaHalfP normalizes scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void RgbaHalfP_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); + + /// + /// Raw half storage preserves IEEE special values while its scaled representation remains finite. + /// + [Fact] + public void HalfVector4_NativeSpecialValuesHaveNormalizedScaledOutput() => AssertNativeSpecialValuesHaveNormalizedScaledOutput(); + + /// + /// Associated half-vector conversion uses the stored alpha ratio before normalizing RGB. + /// + [Fact] + public void HalfVector4P_AssociatedScaledInputIsNormalized() => AssertAssociatedScaledInputIsNormalized(); + + /// + /// Associated half-RGBA conversion uses the stored alpha ratio before normalizing RGB. + /// + [Fact] + public void RgbaHalfP_AssociatedScaledInputIsNormalized() => AssertAssociatedScaledInputIsNormalized(); + + /// + /// Checks saturation and NaN handling without deriving expectations from the invalid-input path. + /// + /// The destination pixel format. + private static void AssertScaledInputIsNormalized() + where TPixel : unmanaged, IPixel + { + Vector4[] inputs = + [ + new(float.PositiveInfinity, float.NegativeInfinity, float.NaN, 1F), + new(2F, -2F, .5F, 1F), + new(.25F, .5F, .75F, .5F), + new(.25F, .5F, .75F, float.NaN), + new(.25F, .5F, .75F, float.PositiveInfinity) + ]; + + Vector4[] normalized = + [ + new(1F, 0F, 0F, 1F), + new(1F, 0F, .5F, 1F), + new(.25F, .5F, .75F, .5F), + new(.25F, .5F, .75F, 0F), + new(.25F, .5F, .75F, 1F) + ]; + + // Seventeen pixels exercise wide registers and the narrower remainder paths. + Vector4[] source = new Vector4[17]; + TPixel[] expected = new TPixel[source.Length]; + TPixel[] actual = new TPixel[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + int sample = i % inputs.Length; + source[i] = inputs[sample]; + expected[i] = TPixel.FromUnassociatedScaledVector4(normalized[sample]); + Assert.Equal(expected[i], TPixel.FromUnassociatedScaledVector4(source[i])); + } + + // Associated formats otherwise interpret the vectors using their native alpha representation. + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, source, actual, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + Assert.Equal(expected, actual); + } + + /// + /// Checks associated input against finite control values with the same represented color. + /// + /// The associated destination pixel format. + private static void AssertAssociatedScaledInputIsNormalized() + where TPixel : unmanaged, IPixel + { + Vector4[] inputs = + [ + new(float.PositiveInfinity, float.NegativeInfinity, float.NaN, 1F), + new(1F, .5F, 1.5F, 2F), + new(.125F, .25F, .375F, .5F), + new(.25F, .5F, .75F, float.NaN), + new(.25F, .5F, .75F, float.PositiveInfinity) + ]; + + Vector4[] normalized = + [ + new(1F, 0F, 0F, 1F), + new(.5F, .25F, .75F, 1F), + new(.125F, .25F, .375F, .5F), + Vector4.Zero, + new(0F, 0F, 0F, 1F) + ]; + + Vector4[] source = new Vector4[17]; + TPixel[] expected = new TPixel[source.Length]; + TPixel[] actual = new TPixel[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + int sample = i % inputs.Length; + source[i] = inputs[sample]; + expected[i] = TPixel.FromAssociatedScaledVector4(normalized[sample]); + Assert.Equal(expected[i], TPixel.FromAssociatedScaledVector4(source[i])); + } + + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, source, actual, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); + Assert.Equal(expected, actual); + } + + /// + /// Checks native storage and every scaled output lane independently of integer conversion semantics. + /// + private static void AssertNativeSpecialValuesHaveNormalizedScaledOutput() + { + Vector4 native = new(float.PositiveInfinity, float.NegativeInfinity, float.NaN, 65504F); + HalfVector4 pixel = HalfVector4.FromVector4(native); + Assert.True(float.IsPositiveInfinity(pixel.ToVector4().X)); + Assert.True(float.IsNegativeInfinity(pixel.ToVector4().Y)); + Assert.True(float.IsNaN(pixel.ToVector4().Z)); + + Vector4 expected = new(1F, 0F, 0F, 1F); + Assert.Equal(expected, pixel.ToScaledVector4()); + Assert.Equal(1F, new HalfSingle(float.PositiveInfinity).ToScaledVector4().X); + Assert.Equal(0F, new HalfSingle(float.NaN).ToScaledVector4().X); + Assert.Equal(new Vector4(1F, 0F, 0F, 1F), new HalfVector2(new Vector2(float.PositiveInfinity, float.NaN)).ToScaledVector4()); + + HalfVector4[] source = new HalfVector4[17]; + Vector4[] nativeSource = new Vector4[source.Length]; + Array.Fill(nativeSource, native); + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, nativeSource, source, PixelConversionModifiers.None); + Assert.All(source, value => Assert.Equal(pixel.PackedValue, value.PackedValue)); + + Vector4[] actual = new Vector4[source.Length]; + PixelOperations.Instance.ToVector4(Configuration.Default, source, actual, PixelConversionModifiers.Scale); + Assert.All(actual, value => Assert.Equal(expected, value)); + } +} diff --git a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs index 917d68d025..c39648d014 100644 --- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs +++ b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs @@ -14,18 +14,6 @@ public class HistogramEqualizationTests { private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F); - [Fact] - public void GlobalHistogramEqualization_ClampsDecodedFloatingPointSamples() - { - byte[] data = BuildFloatTiff(8, 2F); - - using Image image = Image.Load(data); - image.Mutate(x => x.HistogramEqualization()); - - Assert.Equal(8, image.Width); - Assert.Equal(1, image.Height); - } - [Theory] [InlineData(256)] [InlineData(65536)] @@ -250,47 +238,4 @@ public class HistogramEqualizationTests ValidatorComparer.VerifySimilarity(referenceResult, processed); } - - private static byte[] BuildFloatTiff(int width, float sample) - { - const int headerLength = 8; - const int entryCount = 10; - const int ifdLength = 2 + (entryCount * 12) + 4; - uint pixelOffset = headerLength + ifdLength; - - using MemoryStream stream = new(); - using BinaryWriter writer = new(stream); - - writer.Write((byte)0x49); - writer.Write((byte)0x49); - writer.Write((ushort)42); - writer.Write((uint)headerLength); - writer.Write((ushort)entryCount); - WriteEntry(writer, 256, 4, 1, (uint)width); - WriteEntry(writer, 257, 4, 1, 1); - WriteEntry(writer, 258, 3, 1, 32); - WriteEntry(writer, 259, 3, 1, 1); - WriteEntry(writer, 262, 3, 1, 1); - WriteEntry(writer, 273, 4, 1, pixelOffset); - WriteEntry(writer, 277, 3, 1, 1); - WriteEntry(writer, 278, 4, 1, 1); - WriteEntry(writer, 279, 4, 1, (uint)(width * sizeof(float))); - WriteEntry(writer, 339, 3, 1, 3); - writer.Write(0U); - - for (int i = 0; i < width; i++) - { - writer.Write(sample); - } - - return stream.ToArray(); - - static void WriteEntry(BinaryWriter writer, ushort tag, ushort type, uint count, uint value) - { - writer.Write(tag); - writer.Write(type); - writer.Write(count); - writer.Write(value); - } - } }