Browse Source

Clamp nonfinite floating-point pixel conversions

pull/3187/head
James Jackson-South 3 weeks ago
parent
commit
8d48d34352
  1. 13
      src/ImageSharp/Common/Helpers/ColorNumerics.cs
  2. 178
      src/ImageSharp/Common/Helpers/Numerics.cs
  3. 201
      src/ImageSharp/PixelFormats/HalfTypeHelper.cs
  4. 2
      src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs
  5. 20
      src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs
  6. 63
      src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs
  7. 94
      src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs
  8. 7
      src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs
  9. 33
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  10. 109
      tests/ImageSharp.Tests/Helpers/NumericsTests.cs
  11. 183
      tests/ImageSharp.Tests/PixelFormats/FloatingPointPixelNormalizationTests.cs
  12. 55
      tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs

13
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). /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images).
/// </param> /// </param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetBT709Luminance(Vector4 vector, int luminanceLevels) public static int GetBT709Luminance(Vector4 vector, int luminanceLevels) => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (luminanceLevels - 1));
{
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));
}
/// <summary> /// <summary>
/// Gets the luminance from the rgb components using the formula /// Gets the luminance from the rgb components using the formula

178
src/ImageSharp/Common/Helpers/Numerics.cs

@ -263,63 +263,95 @@ internal static class Numerics
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="value">The value to clamp.</param> /// <param name="value">The value to clamp.</param>
/// <param name="min">The minimum inclusive value.</param> /// <param name="min">The minimum inclusive value.</param>
/// <param name="max">The maximum inclusive value.</param> /// <param name="max">The maximum inclusive value.</param>
/// <returns>The clamped <see cref="float"/>.</returns> /// <returns>The clamped <see cref="float"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float Clamp(float value, float min, float max) public static float Clamp(float value, float min, float max) => Clamp<float>(value, min, max);
{
if (value > max)
{
return max;
}
if (value < min)
{
return min;
}
return value;
}
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="value">The value to clamp.</param> /// <param name="value">The value to clamp.</param>
/// <param name="min">The minimum inclusive value.</param> /// <param name="min">The minimum inclusive value.</param>
/// <param name="max">The maximum inclusive value.</param> /// <param name="max">The maximum inclusive value.</param>
/// <returns>The clamped <see cref="double"/>.</returns> /// <returns>The clamped <see cref="double"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double Clamp(double value, double min, double max) public static double Clamp(double value, double min, double max) => Clamp<double>(value, min, max);
/// <summary>
/// Clamps components to the inclusive range of min and max, mapping NaN to min.
/// </summary>
/// <param name="value">The components to clamp.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped components.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector2 Clamp(Vector2 value, Vector2 min, Vector2 max) => Clamp(value.AsVector128(), min.AsVector128(), max.AsVector128()).AsVector2();
/// <summary>
/// Clamps components to the inclusive range of min and max, mapping NaN to min.
/// </summary>
/// <typeparam name="T">The component type.</typeparam>
/// <param name="value">The components to clamp.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped components.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<T> Clamp<T>(Vector128<T> value, Vector128<T> min, Vector128<T> max)
where T : struct, INumber<T>
{ {
if (value > max) // Ordered comparisons map NaN to min and preserve in-range signed zero on every runtime.
{ Vector128<T> lowerClamped = Vector128.ConditionalSelect(Vector128.GreaterThanOrEqual(value, min), value, min);
return max; return Vector128.ConditionalSelect(Vector128.GreaterThan(value, max), max, lowerClamped);
} }
if (value < min) /// <summary>
{ /// Clamps components to the inclusive range of min and max, mapping NaN to min.
return min; /// </summary>
} /// <typeparam name="T">The component type.</typeparam>
/// <param name="value">The components to clamp.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped components.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<T> Clamp<T>(Vector256<T> value, Vector256<T> min, Vector256<T> max)
where T : struct, INumber<T>
{
// Ordered comparisons map NaN to min and preserve in-range signed zero on every runtime.
Vector256<T> lowerClamped = Vector256.ConditionalSelect(Vector256.GreaterThanOrEqual(value, min), value, min);
return Vector256.ConditionalSelect(Vector256.GreaterThan(value, max), max, lowerClamped);
}
return value; /// <summary>
/// Clamps components to the inclusive range of min and max, mapping NaN to min.
/// </summary>
/// <typeparam name="T">The component type.</typeparam>
/// <param name="value">The components to clamp.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped components.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<T> Clamp<T>(Vector512<T> value, Vector512<T> min, Vector512<T> max)
where T : struct, INumber<T>
{
// Ordered comparisons map NaN to min and preserve in-range signed zero on every runtime.
Vector512<T> lowerClamped = Vector512.ConditionalSelect(Vector512.GreaterThanOrEqual(value, min), value, min);
return Vector512.ConditionalSelect(Vector512.GreaterThan(value, max), max, lowerClamped);
} }
/// <summary> /// <summary>
/// Returns the value clamped to the inclusive range of min and max. /// Clamps components to the inclusive range of min and max, mapping NaN to min.
/// 5x Faster than <see cref="Vector4.Clamp(Vector4, Vector4, Vector4)"/>
/// on platforms &lt; NET 5.
/// </summary> /// </summary>
/// <param name="value">The value to clamp.</param> /// <param name="value">The value to clamp.</param>
/// <param name="min">The minimum inclusive value.</param> /// <param name="min">The minimum inclusive value.</param>
/// <param name="max">The maximum inclusive value.</param> /// <param name="max">The maximum inclusive value.</param>
/// <returns>The clamped <see cref="Vector4"/>.</returns> /// <returns>The clamped <see cref="Vector4"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector4 Clamp(Vector4 value, Vector4 min, Vector4 max) public static Vector4 Clamp(Vector4 value, Vector4 min, Vector4 max) => Clamp(value.AsVector128(), min.AsVector128(), max.AsVector128()).AsVector4();
=> Vector4.Min(Vector4.Max(value, min), max);
/// <summary> /// <summary>
/// Clamps the span values to the inclusive range of min and max. /// 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); => TensorPrimitives_.Clamp(span, min, max, span);
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="span">The span containing the values to clamp.</param> /// <param name="span">The span containing the values to clamp.</param>
/// <param name="min">The minimum inclusive value.</param> /// <param name="min">The minimum inclusive value.</param>
/// <param name="max">The maximum inclusive value.</param> /// <param name="max">The maximum inclusive value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span<float> span, float min, float max) public static void Clamp(Span<float> span, float min, float max)
=> TensorPrimitives_.Clamp(span, min, max, span); => Clamp<float>(span, min, max);
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="span">The span containing the values to clamp.</param> /// <param name="span">The span containing the values to clamp.</param>
/// <param name="min">The minimum inclusive value.</param> /// <param name="min">The minimum inclusive value.</param>
/// <param name="max">The maximum inclusive value.</param> /// <param name="max">The maximum inclusive value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span<double> span, double min, double max) public static void Clamp(Span<double> span, double min, double max)
=> TensorPrimitives_.Clamp(span, min, max, span); => Clamp<double>(span, min, max);
/// <summary> /// <summary>
/// Pre-multiplies the "x", "y", "z" components of a vector by its "w" component leaving the "w" component intact. /// 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) public static void ClampRgbToAlpha(ref Vector4 source)
{ {
Vector4 alpha = PermuteW(source); Vector4 alpha = PermuteW(source);
source = WithW(Vector4.Min(Vector4.Max(source, Vector4.Zero), alpha), alpha); source = WithW(Clamp(source, Vector4.Zero, alpha), alpha);
} }
/// <summary> /// <summary>
@ -1071,4 +1103,78 @@ internal static class Numerics
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Normalize(Span<float> span, float sum) public static void Normalize(Span<float> span, float sum)
=> TensorPrimitives_.Divide(span, sum, span); => TensorPrimitives_.Divide(span, sum, span);
/// <summary>
/// Clamps a floating-point component while mapping NaN to the lower bound.
/// </summary>
/// <typeparam name="T">The component type.</typeparam>
/// <param name="value">The component to clamp.</param>
/// <param name="min">The inclusive lower bound.</param>
/// <param name="max">The inclusive upper bound.</param>
/// <returns>The clamped component.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T Clamp<T>(T value, T min, T max)
where T : struct, INumber<T>
{
// Ordered comparisons map NaN to min; in-range values retain their original bits, including signed zero.
return value > max ? max : value >= min ? value : min;
}
/// <summary>
/// Applies the scalar clamp contract to floating-point spans in place.
/// </summary>
/// <typeparam name="T">The component type.</typeparam>
/// <param name="span">The components to clamp.</param>
/// <param name="min">The inclusive lower bound.</param>
/// <param name="max">The inclusive upper bound.</param>
private static void Clamp<T>(Span<T> span, T min, T max)
where T : struct, INumber<T>
{
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<T> lower = Vector512.Create(min);
Vector512<T> upper = Vector512.Create(max);
for (; i <= span.Length - Vector512<T>.Count; i += Vector512<T>.Count)
{
Vector512<T> value = Vector512.LoadUnsafe(ref start, (nuint)i);
Clamp(value, lower, upper).StoreUnsafe(ref start, (nuint)i);
}
}
if (Vector256.IsHardwareAccelerated)
{
Vector256<T> lower = Vector256.Create(min);
Vector256<T> upper = Vector256.Create(max);
for (; i <= span.Length - Vector256<T>.Count; i += Vector256<T>.Count)
{
Vector256<T> value = Vector256.LoadUnsafe(ref start, (nuint)i);
Clamp(value, lower, upper).StoreUnsafe(ref start, (nuint)i);
}
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<T> lower = Vector128.Create(min);
Vector128<T> upper = Vector128.Create(max);
for (; i <= span.Length - Vector128<T>.Count; i += Vector128<T>.Count)
{
Vector128<T> 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);
}
}
} }

201
src/ImageSharp/PixelFormats/HalfTypeHelper.cs

@ -3,6 +3,7 @@
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.PixelFormats;
@ -50,52 +51,228 @@ internal static class HalfTypeHelper
internal static float Unpack(ushort value) => (float)BitConverter.UInt16BitsToHalf(value); internal static float Unpack(ushort value) => (float)BitConverter.UInt16BitsToHalf(value);
/// <summary> /// <summary>
/// Normalizes a finite binary16 value to the scaled pixel range. /// Normalizes a binary16 value to [0, 1], saturating infinities and mapping NaN to zero.
/// </summary> /// </summary>
/// <param name="value">The native binary16 value represented as a <see cref="float"/>.</param> /// <param name="value">The native binary16 value represented as a <see cref="float"/>.</param>
/// <returns>The normalized value.</returns> /// <returns>The normalized value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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);
}
/// <summary> /// <summary>
/// Normalizes finite binary16 values to the scaled pixel range. /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
/// </summary> /// </summary>
/// <param name="value">The native binary16 values.</param> /// <param name="value">The native binary16 values.</param>
/// <returns>The normalized values.</returns> /// <returns>The normalized values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector2 ToScaled(Vector2 value) => (value * InverseFiniteRange) + new Vector2(ScaledMidpoint); public static Vector2 ToScaled(Vector2 value) => ToScaled(value.AsVector128()).AsVector2();
/// <summary> /// <summary>
/// Normalizes finite binary16 values to the scaled pixel range. /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
/// </summary> /// </summary>
/// <param name="value">The native binary16 values.</param> /// <param name="value">The native binary16 values.</param>
/// <returns>The normalized values.</returns> /// <returns>The normalized values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector4 ToScaled(Vector4 value) => (value * InverseFiniteRange) + new Vector4(ScaledMidpoint); public static Vector4 ToScaled(Vector4 value) => ToScaled(value.AsVector128()).AsVector4();
/// <summary>
/// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
/// </summary>
/// <param name="value">The component values.</param>
/// <returns>The converted values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<float> ToScaled(Vector128<float> value)
{
Vector128<float> scaled = (value * Vector128.Create(InverseFiniteRange)) + Vector128.Create(ScaledMidpoint);
return Numerics.Clamp(scaled, Vector128<float>.Zero, Vector128<float>.One);
}
/// <summary>
/// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
/// </summary>
/// <param name="value">The component values.</param>
/// <returns>The converted values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> ToScaled(Vector256<float> value)
{
Vector256<float> scaled = (value * Vector256.Create(InverseFiniteRange)) + Vector256.Create(ScaledMidpoint);
return Numerics.Clamp(scaled, Vector256<float>.Zero, Vector256<float>.One);
}
/// <summary>
/// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
/// </summary>
/// <param name="value">The component values.</param>
/// <returns>The converted values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<float> ToScaled(Vector512<float> value)
{
Vector512<float> scaled = (value * Vector512.Create(InverseFiniteRange)) + Vector512.Create(ScaledMidpoint);
return Numerics.Clamp(scaled, Vector512<float>.Zero, Vector512<float>.One);
}
/// <summary>
/// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
/// </summary>
/// <param name="values">The component values to convert in place.</param>
public static void ToScaled(Span<Vector4> 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<float>.Count / Vector128<float>.Count;
for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister)
{
ref Vector512<float> vector = ref Unsafe.As<Vector4, Vector512<float>>(ref Unsafe.Add(ref source, (uint)i));
vector = ToScaled(vector);
}
}
if (Vector256.IsHardwareAccelerated)
{
int pixelsPerRegister = Vector256<float>.Count / Vector128<float>.Count;
for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister)
{
ref Vector256<float> vector = ref Unsafe.As<Vector4, Vector256<float>>(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);
}
}
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="value">The normalized value.</param> /// <param name="value">The normalized value.</param>
/// <returns>The native binary16 value represented as a <see cref="float"/>.</returns> /// <returns>The native binary16 value represented as a <see cref="float"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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;
}
/// <summary> /// <summary>
/// Expands normalized values to the finite binary16 range. /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
/// </summary> /// </summary>
/// <param name="value">The normalized values.</param> /// <param name="value">The normalized values.</param>
/// <returns>The native binary16 values.</returns> /// <returns>The native binary16 values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector2 FromScaled(Vector2 value) => (value * FiniteRange) + new Vector2(FiniteMinimum); public static Vector2 FromScaled(Vector2 value) => FromScaled(value.AsVector128()).AsVector2();
/// <summary> /// <summary>
/// Expands normalized values to the finite binary16 range. /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
/// </summary> /// </summary>
/// <param name="value">The normalized values.</param> /// <param name="value">The normalized values.</param>
/// <returns>The native binary16 values.</returns> /// <returns>The native binary16 values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector4 FromScaled(Vector4 value) => (value * FiniteRange) + new Vector4(FiniteMinimum); public static Vector4 FromScaled(Vector4 value) => FromScaled(value.AsVector128()).AsVector4();
/// <summary>
/// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
/// </summary>
/// <param name="value">The component values.</param>
/// <returns>The converted values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<float> FromScaled(Vector128<float> value)
{
Vector128<float> scaled = Numerics.Clamp(value, Vector128<float>.Zero, Vector128<float>.One);
return (scaled * Vector128.Create(FiniteRange)) + Vector128.Create(FiniteMinimum);
}
/// <summary>
/// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
/// </summary>
/// <param name="value">The component values.</param>
/// <returns>The converted values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> FromScaled(Vector256<float> value)
{
Vector256<float> scaled = Numerics.Clamp(value, Vector256<float>.Zero, Vector256<float>.One);
return (scaled * Vector256.Create(FiniteRange)) + Vector256.Create(FiniteMinimum);
}
/// <summary>
/// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
/// </summary>
/// <param name="value">The component values.</param>
/// <returns>The converted values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<float> FromScaled(Vector512<float> value)
{
Vector512<float> scaled = Numerics.Clamp(value, Vector512<float>.Zero, Vector512<float>.One);
return (scaled * Vector512.Create(FiniteRange)) + Vector512.Create(FiniteMinimum);
}
/// <summary>
/// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
/// </summary>
/// <param name="values">The component values to convert in place.</param>
public static void FromScaled(Span<Vector4> 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<float>.Count / Vector128<float>.Count;
for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister)
{
ref Vector512<float> vector = ref Unsafe.As<Vector4, Vector512<float>>(ref Unsafe.Add(ref source, (uint)i));
vector = FromScaled(vector);
}
}
if (Vector256.IsHardwareAccelerated)
{
int pixelsPerRegister = Vector256<float>.Count / Vector128<float>.Count;
for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister)
{
ref Vector256<float> vector = ref Unsafe.As<Vector4, Vector256<float>>(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);
}
}
/// <summary> /// <summary>
/// Unpacks eight binary16 values into two vectors of single-precision values. /// Unpacks eight binary16 values into two vectors of single-precision values.

2
src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs

@ -250,7 +250,7 @@ public partial struct HalfVector4P : IPixel<HalfVector4P>, IPackedVector<ulong>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float QuantizeScaledAlpha(float alpha) 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))); return HalfTypeHelper.ToScaled(HalfTypeHelper.Unpack(HalfTypeHelper.Pack(nativeAlpha)));
} }

20
src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs

@ -3,7 +3,6 @@
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats.Utils;
namespace SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.PixelFormats;
@ -17,11 +16,6 @@ public partial struct HalfVector4
/// </summary> /// </summary>
internal class PixelOperations : PixelOperations<HalfVector4> internal class PixelOperations : PixelOperations<HalfVector4>
{ {
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);
/// <inheritdoc /> /// <inheritdoc />
protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan<HalfVector4> source, Span<Vector4> destination) protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan<HalfVector4> source, Span<Vector4> destination)
{ {
@ -40,9 +34,9 @@ public partial struct HalfVector4
// Association uses normalized opacity, not the native binary16 alpha value. // Association uses normalized opacity, not the native binary16 alpha value.
RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(source), destination); RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(source), destination);
Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); HalfTypeHelper.ToScaled(destination);
Numerics.Premultiply(destination); Numerics.Premultiply(destination);
Vector4Converters.MultiplyThenAdd(destination, ScaledToNativeMultiplier, ScaledToNativeOffset); HalfTypeHelper.FromScaled(destination);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -52,7 +46,7 @@ public partial struct HalfVector4
destination = destination[..source.Length]; destination = destination[..source.Length];
RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(source), destination); RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(source), destination);
Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); HalfTypeHelper.ToScaled(destination);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -77,9 +71,9 @@ public partial struct HalfVector4
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// Restore normalized opacity before unassociating, then return the result to the native binary16 range. // 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); Numerics.UnPremultiply(source);
Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); HalfTypeHelper.FromScaled(source);
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(destination[..source.Length])); RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(destination[..source.Length]));
} }
@ -88,7 +82,7 @@ public partial struct HalfVector4
{ {
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); HalfTypeHelper.FromScaled(source);
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(destination[..source.Length])); RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(destination[..source.Length]));
} }
@ -98,7 +92,7 @@ public partial struct HalfVector4
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
Numerics.UnPremultiply(source); Numerics.UnPremultiply(source);
Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); HalfTypeHelper.FromScaled(source);
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(destination[..source.Length])); RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(destination[..source.Length]));
} }
} }

63
src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs

@ -20,16 +20,11 @@ public partial struct HalfVector4P
/// </summary> /// </summary>
internal class PixelOperations : AssociatedAlphaPixelOperations<HalfVector4P> internal class PixelOperations : AssociatedAlphaPixelOperations<HalfVector4P>
{ {
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);
/// <inheritdoc /> /// <inheritdoc />
protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan<HalfVector4P> source, Span<Vector4> destination) protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan<HalfVector4P> source, Span<Vector4> destination)
{ {
this.ToUnassociatedScaledVector4(configuration, source, destination); this.ToUnassociatedScaledVector4(configuration, source, destination);
Vector4Converters.MultiplyThenAdd(destination[..source.Length], ScaledToNativeMultiplier, ScaledToNativeOffset); HalfTypeHelper.FromScaled(destination[..source.Length]);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -54,7 +49,7 @@ public partial struct HalfVector4P
destination = destination[..source.Length]; destination = destination[..source.Length];
RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4P, RgbaHalfP>(source), destination); RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4P, RgbaHalfP>(source), destination);
Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); HalfTypeHelper.ToScaled(destination);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -62,7 +57,7 @@ public partial struct HalfVector4P
{ {
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset); HalfTypeHelper.ToScaled(source);
Associate(source); Associate(source);
PackAssociatedScaled(source, destination[..source.Length]); PackAssociatedScaled(source, destination[..source.Length]);
} }
@ -72,7 +67,7 @@ public partial struct HalfVector4P
{ {
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset); HalfTypeHelper.ToScaled(source);
Reassociate(source); Reassociate(source);
PackAssociatedScaled(source, destination[..source.Length]); PackAssociatedScaled(source, destination[..source.Length]);
} }
@ -250,7 +245,9 @@ public partial struct HalfVector4P
Vector128<float> storedAlpha = QuantizeScaledAlpha(alpha); Vector128<float> storedAlpha = QuantizeScaledAlpha(alpha);
Vector128<float> result = source * (storedAlpha / alpha); Vector128<float> result = source * (storedAlpha / alpha);
result = Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); 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); return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(alpha, zero), zero, result);
} }
@ -267,7 +264,9 @@ public partial struct HalfVector4P
Vector256<float> storedAlpha = QuantizeScaledAlpha(alpha); Vector256<float> storedAlpha = QuantizeScaledAlpha(alpha);
Vector256<float> result = source * (storedAlpha / alpha); Vector256<float> result = source * (storedAlpha / alpha);
result = Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); 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); return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(alpha, zero), zero, result);
} }
@ -285,7 +284,9 @@ public partial struct HalfVector4P
Vector512<float> result = source * (storedAlpha / alpha); Vector512<float> result = source * (storedAlpha / alpha);
Vector512<float> alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); Vector512<float> 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.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); return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(alpha, zero), zero, result);
} }
@ -297,8 +298,8 @@ public partial struct HalfVector4P
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> QuantizeScaledAlpha(Vector128<float> alpha) private static Vector128<float> QuantizeScaledAlpha(Vector128<float> alpha)
{ {
Vector128<float> native = (ClampUnit(alpha) * Vector128.Create(HalfTypeHelper.FiniteRange)) + Vector128.Create(HalfTypeHelper.FiniteMinimum); Vector128<float> native = HalfTypeHelper.FromScaled(alpha);
return (HalfTypeHelper.RoundToHalf(native) * Vector128.Create(HalfTypeHelper.InverseFiniteRange)) + Vector128.Create(HalfTypeHelper.ScaledMidpoint); return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native));
} }
/// <summary> /// <summary>
@ -309,8 +310,8 @@ public partial struct HalfVector4P
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> QuantizeScaledAlpha(Vector256<float> alpha) private static Vector256<float> QuantizeScaledAlpha(Vector256<float> alpha)
{ {
Vector256<float> native = (ClampUnit(alpha) * Vector256.Create(HalfTypeHelper.FiniteRange)) + Vector256.Create(HalfTypeHelper.FiniteMinimum); Vector256<float> native = HalfTypeHelper.FromScaled(alpha);
return (HalfTypeHelper.RoundToHalf(native) * Vector256.Create(HalfTypeHelper.InverseFiniteRange)) + Vector256.Create(HalfTypeHelper.ScaledMidpoint); return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native));
} }
/// <summary> /// <summary>
@ -321,45 +322,33 @@ public partial struct HalfVector4P
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> QuantizeScaledAlpha(Vector512<float> alpha) private static Vector512<float> QuantizeScaledAlpha(Vector512<float> alpha)
{ {
Vector512<float> native = (ClampUnit(alpha) * Vector512.Create(HalfTypeHelper.FiniteRange)) + Vector512.Create(HalfTypeHelper.FiniteMinimum); Vector512<float> native = HalfTypeHelper.FromScaled(alpha);
return (HalfTypeHelper.RoundToHalf(native) * Vector512.Create(HalfTypeHelper.InverseFiniteRange)) + Vector512.Create(HalfTypeHelper.ScaledMidpoint); return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native));
} }
/// <summary> /// <summary>
/// Clamps vectors to the scaled color range while preserving NaN lanes. /// Clamps vectors to the scaled color range, mapping NaN lanes to zero.
/// </summary> /// </summary>
/// <param name="source">The vectors to clamp.</param> /// <param name="source">The vectors to clamp.</param>
/// <returns>The clamped vectors.</returns> /// <returns>The clamped vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> ClampUnit(Vector128<float> source) private static Vector128<float> ClampUnit(Vector128<float> source) => Numerics.Clamp(source, Vector128<float>.Zero, Vector128<float>.One);
{
Vector128<float> clamped = Vector128.Min(Vector128.Max(source, Vector128<float>.Zero), Vector128<float>.One);
return Vector128.ConditionalSelect(Vector128.Equals(source, source), clamped, source);
}
/// <summary> /// <summary>
/// Clamps vectors to the scaled color range while preserving NaN lanes. /// Clamps vectors to the scaled color range, mapping NaN lanes to zero.
/// </summary> /// </summary>
/// <param name="source">The vectors to clamp.</param> /// <param name="source">The vectors to clamp.</param>
/// <returns>The clamped vectors.</returns> /// <returns>The clamped vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> ClampUnit(Vector256<float> source) private static Vector256<float> ClampUnit(Vector256<float> source) => Numerics.Clamp(source, Vector256<float>.Zero, Vector256<float>.One);
{
Vector256<float> clamped = Vector256.Min(Vector256.Max(source, Vector256<float>.Zero), Vector256<float>.One);
return Vector256.ConditionalSelect(Vector256.Equals(source, source), clamped, source);
}
/// <summary> /// <summary>
/// Clamps vectors to the scaled color range while preserving NaN lanes. /// Clamps vectors to the scaled color range, mapping NaN lanes to zero.
/// </summary> /// </summary>
/// <param name="source">The vectors to clamp.</param> /// <param name="source">The vectors to clamp.</param>
/// <returns>The clamped vectors.</returns> /// <returns>The clamped vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> ClampUnit(Vector512<float> source) private static Vector512<float> ClampUnit(Vector512<float> source) => Numerics.Clamp(source, Vector512<float>.Zero, Vector512<float>.One);
{
Vector512<float> clamped = Vector512.Min(Vector512.Max(source, Vector512<float>.Zero), Vector512<float>.One);
return Vector512.ConditionalSelect(Vector512.Equals(source, source), clamped, source);
}
/// <summary> /// <summary>
/// Maps associated scaled vectors to native components and packs them as binary16 values. /// Maps associated scaled vectors to native components and packs them as binary16 values.
@ -368,7 +357,7 @@ public partial struct HalfVector4P
/// <param name="destination">The destination pixels.</param> /// <param name="destination">The destination pixels.</param>
private static void PackAssociatedScaled(Span<Vector4> source, Span<HalfVector4P> destination) private static void PackAssociatedScaled(Span<Vector4> source, Span<HalfVector4P> destination)
{ {
Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); HalfTypeHelper.FromScaled(source);
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4P, RgbaHalfP>(destination)); RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4P, RgbaHalfP>(destination));
} }
} }

94
src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs

@ -277,8 +277,8 @@ public partial struct RgbaHalfP
{ {
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count) for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
{ {
Vector512<float> lower = ClampUnit(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); Vector512<float> lower = Numerics.Clamp(Vector512.LoadUnsafe(ref sourceBase, (nuint)i), Vector512<float>.Zero, Vector512<float>.One);
Vector512<float> upper = ClampUnit(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<float>.Count))); Vector512<float> upper = Numerics.Clamp(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<float>.Count)), Vector512<float>.Zero, Vector512<float>.One);
Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
} }
} }
@ -287,8 +287,8 @@ public partial struct RgbaHalfP
{ {
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count) for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
{ {
Vector256<float> lower = ClampUnit(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); Vector256<float> lower = Numerics.Clamp(Vector256.LoadUnsafe(ref sourceBase, (nuint)i), Vector256<float>.Zero, Vector256<float>.One);
Vector256<float> upper = ClampUnit(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<float>.Count))); Vector256<float> upper = Numerics.Clamp(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<float>.Count)), Vector256<float>.Zero, Vector256<float>.One);
Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
} }
} }
@ -297,15 +297,15 @@ public partial struct RgbaHalfP
{ {
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count) for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
{ {
Vector128<float> lower = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); Vector128<float> lower = Numerics.Clamp(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), Vector128<float>.Zero, Vector128<float>.One);
Vector128<float> upper = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<float>.Count))); Vector128<float> upper = Numerics.Clamp(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<float>.Count)), Vector128<float>.Zero, Vector128<float>.One);
Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
} }
if (i < componentCount) if (i < componentCount)
{ {
// Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel. // Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel.
Vector128<float> vector = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); Vector128<float> vector = Numerics.Clamp(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), Vector128<float>.Zero, Vector128<float>.One);
Vector128<ushort> packed = HalfTypeHelper.Pack(vector, vector); Vector128<ushort> packed = HalfTypeHelper.Pack(vector, vector);
Unsafe.WriteUnaligned(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); Unsafe.WriteUnaligned(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0));
} }
@ -401,8 +401,8 @@ public partial struct RgbaHalfP
{ {
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count) for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
{ {
Vector512<float> lower = ClampUnit(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i))); Vector512<float> lower = Numerics.Clamp(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)), Vector512<float>.Zero, Vector512<float>.One);
Vector512<float> upper = ClampUnit(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<float>.Count)))); Vector512<float> upper = Numerics.Clamp(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<float>.Count))), Vector512<float>.Zero, Vector512<float>.One);
Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
} }
} }
@ -411,8 +411,8 @@ public partial struct RgbaHalfP
{ {
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count) for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
{ {
Vector256<float> lower = ClampUnit(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i))); Vector256<float> lower = Numerics.Clamp(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)), Vector256<float>.Zero, Vector256<float>.One);
Vector256<float> upper = ClampUnit(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<float>.Count)))); Vector256<float> upper = Numerics.Clamp(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<float>.Count))), Vector256<float>.Zero, Vector256<float>.One);
Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
} }
} }
@ -421,15 +421,15 @@ public partial struct RgbaHalfP
{ {
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count) for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
{ {
Vector128<float> lower = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i))); Vector128<float> lower = Numerics.Clamp(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)), Vector128<float>.Zero, Vector128<float>.One);
Vector128<float> upper = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<float>.Count)))); Vector128<float> upper = Numerics.Clamp(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<float>.Count))), Vector128<float>.Zero, Vector128<float>.One);
Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
} }
if (i < componentCount) if (i < componentCount)
{ {
// Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel. // Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel.
Vector128<float> vector = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i))); Vector128<float> vector = Numerics.Clamp(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)), Vector128<float>.Zero, Vector128<float>.One);
Vector128<ushort> packed = HalfTypeHelper.Pack(vector, vector); Vector128<ushort> packed = HalfTypeHelper.Pack(vector, vector);
Unsafe.WriteUnaligned(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); Unsafe.WriteUnaligned(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0));
} }
@ -649,48 +649,6 @@ public partial struct RgbaHalfP
return Numerics.UnPremultiply(source, alpha); return Numerics.UnPremultiply(source, alpha);
} }
/// <summary>
/// Clamps vectors to the unit range represented by the pixel format.
/// </summary>
/// <param name="source">The vectors to clamp.</param>
/// <returns>The clamped vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> ClampUnit(Vector128<float> source)
{
Vector128<float> clamped = Vector128.Min(Vector128.Max(source, Vector128<float>.Zero), Vector128<float>.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);
}
/// <summary>
/// Clamps vectors to the unit range represented by the pixel format.
/// </summary>
/// <param name="source">The vectors to clamp.</param>
/// <returns>The clamped vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> ClampUnit(Vector256<float> source)
{
Vector256<float> clamped = Vector256.Min(Vector256.Max(source, Vector256<float>.Zero), Vector256<float>.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);
}
/// <summary>
/// Clamps vectors to the unit range represented by the pixel format.
/// </summary>
/// <param name="source">The vectors to clamp.</param>
/// <returns>The clamped vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> ClampUnit(Vector512<float> source)
{
Vector512<float> clamped = Vector512.Min(Vector512.Max(source, Vector512<float>.Zero), Vector512<float>.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);
}
/// <summary> /// <summary>
/// Associates unassociated vectors with the alpha value binary16 storage can reproduce. /// Associates unassociated vectors with the alpha value binary16 storage can reproduce.
/// </summary> /// </summary>
@ -699,7 +657,7 @@ public partial struct RgbaHalfP
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> AssociateForStorage(Vector128<float> source) private static Vector128<float> AssociateForStorage(Vector128<float> source)
{ {
source = ClampUnit(source); source = Numerics.Clamp(source, Vector128<float>.Zero, Vector128<float>.One);
Vector128<float> alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); Vector128<float> alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11);
Vector128<float> storedAlpha = HalfTypeHelper.RoundToHalf(alpha); Vector128<float> storedAlpha = HalfTypeHelper.RoundToHalf(alpha);
Vector128<float> result = source * storedAlpha; Vector128<float> result = source * storedAlpha;
@ -714,7 +672,7 @@ public partial struct RgbaHalfP
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> AssociateForStorage(Vector256<float> source) private static Vector256<float> AssociateForStorage(Vector256<float> source)
{ {
source = ClampUnit(source); source = Numerics.Clamp(source, Vector256<float>.Zero, Vector256<float>.One);
Vector256<float> alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); Vector256<float> alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11);
Vector256<float> storedAlpha = HalfTypeHelper.RoundToHalf(alpha); Vector256<float> storedAlpha = HalfTypeHelper.RoundToHalf(alpha);
Vector256<float> result = source * storedAlpha; Vector256<float> result = source * storedAlpha;
@ -729,7 +687,7 @@ public partial struct RgbaHalfP
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> AssociateForStorage(Vector512<float> source) private static Vector512<float> AssociateForStorage(Vector512<float> source)
{ {
source = ClampUnit(source); source = Numerics.Clamp(source, Vector512<float>.Zero, Vector512<float>.One);
Vector512<float> alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); Vector512<float> alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11);
Vector512<float> storedAlpha = HalfTypeHelper.RoundToHalf(alpha); Vector512<float> storedAlpha = HalfTypeHelper.RoundToHalf(alpha);
Vector512<float> result = source * storedAlpha; Vector512<float> result = source * storedAlpha;
@ -747,11 +705,13 @@ public partial struct RgbaHalfP
{ {
Vector128<float> zero = Vector128<float>.Zero; Vector128<float> zero = Vector128<float>.Zero;
Vector128<float> alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); Vector128<float> alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11);
Vector128<float> clampedAlpha = ClampUnit(alpha); Vector128<float> clampedAlpha = Numerics.Clamp(alpha, Vector128<float>.Zero, Vector128<float>.One);
Vector128<float> storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); Vector128<float> storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha);
Vector128<float> result = source * (storedAlpha / alpha); Vector128<float> result = source * (storedAlpha / alpha);
result = Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); 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); return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(alpha, zero), zero, result);
} }
@ -765,11 +725,13 @@ public partial struct RgbaHalfP
{ {
Vector256<float> zero = Vector256<float>.Zero; Vector256<float> zero = Vector256<float>.Zero;
Vector256<float> alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); Vector256<float> alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11);
Vector256<float> clampedAlpha = ClampUnit(alpha); Vector256<float> clampedAlpha = Numerics.Clamp(alpha, Vector256<float>.Zero, Vector256<float>.One);
Vector256<float> storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); Vector256<float> storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha);
Vector256<float> result = source * (storedAlpha / alpha); Vector256<float> result = source * (storedAlpha / alpha);
result = Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); 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); return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(alpha, zero), zero, result);
} }
@ -783,12 +745,14 @@ public partial struct RgbaHalfP
{ {
Vector512<float> zero = Vector512<float>.Zero; Vector512<float> zero = Vector512<float>.Zero;
Vector512<float> alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); Vector512<float> alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11);
Vector512<float> clampedAlpha = ClampUnit(alpha); Vector512<float> clampedAlpha = Numerics.Clamp(alpha, Vector512<float>.Zero, Vector512<float>.One);
Vector512<float> storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); Vector512<float> storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha);
Vector512<float> result = source * (storedAlpha / alpha); Vector512<float> result = source * (storedAlpha / alpha);
Vector512<float> alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); Vector512<float> 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.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); return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(alpha, zero), zero, result);
} }
} }

7
src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs

@ -71,12 +71,7 @@ internal static partial class Vector4Converters
/// <inheritdoc /> /// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 Invoke(Vector4 source) public Vector4 Invoke(Vector4 source) => this.Invoke(source.AsVector128()).AsVector4();
{
Vector128<float> result = (source.AsVector128() * this.multiplier.GetLower().GetLower()) + this.offset.GetLower().GetLower();
return result.AsVector4();
}
/// <inheritdoc /> /// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]

33
tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
using System.Numerics;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
@ -25,6 +26,38 @@ public class TiffDecoderTests : TiffDecoderBaseTester
{ {
public static readonly string[] MultiframeTestImages = Multiframes; public static readonly string[] MultiframeTestImages = Multiframes;
/// <summary>
/// Decoded floating-point components are normalized before they enter half-vector storage.
/// </summary>
/// <param name="hex">The encoded floating-point TIFF.</param>
/// <param name="intensity">The normalized intensity.</param>
[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<HalfVector4> image = Image.Load<HalfVector4>(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] [Theory]
[WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)]
[WithFile(Cmyk64BitDeflate, PixelTypes.Rgba32)] [WithFile(Cmyk64BitDeflate, PixelTypes.Rgba32)]

109
tests/ImageSharp.Tests/Helpers/NumericsTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics; using System.Numerics;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Tests.Helpers; namespace SixLabors.ImageSharp.Tests.Helpers;
@ -305,6 +306,114 @@ public class NumericsTests
(v, m1, m2) => Numerics.Clamp(v, m1, m2)); (v, m1, m2) => Numerics.Clamp(v, m1, m2));
} }
/// <summary>
/// Scalar, SIMD, and span clamps map nonfinite values to the requested bounds.
/// </summary>
/// <param name="min">The lower bound.</param>
/// <param name="max">The upper bound.</param>
[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<float> vector128 = Vector128.LoadUnsafe(ref values[0]);
Assert.Equal(Vector128.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector128, Vector128.Create(min), Vector128.Create(max)));
Vector256<float> vector256 = Vector256.LoadUnsafe(ref values[0]);
Assert.Equal(Vector256.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector256, Vector256.Create(min), Vector256.Create(max)));
Vector512<float> 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);
}
/// <summary>
/// Scalar, SIMD, and span clamps map nonfinite values to the requested bounds.
/// </summary>
/// <param name="min">The lower bound.</param>
/// <param name="max">The upper bound.</param>
[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<double> vector128 = Vector128.LoadUnsafe(ref values[0]);
Assert.Equal(Vector128.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector128, Vector128.Create(min), Vector128.Create(max)));
Vector256<double> vector256 = Vector256.LoadUnsafe(ref values[0]);
Assert.Equal(Vector256.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector256, Vector256.Create(min), Vector256.Create(max)));
Vector512<double> 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);
}
/// <summary>
/// Clamping an in-range zero preserves its sign in scalar and bulk conversions.
/// </summary>
[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<T>( private static void TestClampSpan<T>(
int length, int length,
T min, T min,

183
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
{
/// <summary>
/// HalfSingle normalizes scaled input identically in scalar and bulk conversions.
/// </summary>
[Fact]
public void HalfSingle_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<HalfSingle>();
/// <summary>
/// HalfVector2 normalizes scaled input identically in scalar and bulk conversions.
/// </summary>
[Fact]
public void HalfVector2_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<HalfVector2>();
/// <summary>
/// HalfVector4 normalizes scaled input identically in scalar and bulk conversions.
/// </summary>
[Fact]
public void HalfVector4_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<HalfVector4>();
/// <summary>
/// HalfVector4P normalizes scaled input identically in scalar and bulk conversions.
/// </summary>
[Fact]
public void HalfVector4P_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<HalfVector4P>();
/// <summary>
/// RgbaVector normalizes scaled input identically in scalar and bulk conversions.
/// </summary>
[Fact]
public void RgbaVector_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<RgbaVector>();
/// <summary>
/// RgbaHalf normalizes scaled input identically in scalar and bulk conversions.
/// </summary>
[Fact]
public void RgbaHalf_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<RgbaHalf>();
/// <summary>
/// RgbaHalfP normalizes scaled input identically in scalar and bulk conversions.
/// </summary>
[Fact]
public void RgbaHalfP_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<RgbaHalfP>();
/// <summary>
/// Raw half storage preserves IEEE special values while its scaled representation remains finite.
/// </summary>
[Fact]
public void HalfVector4_NativeSpecialValuesHaveNormalizedScaledOutput() => AssertNativeSpecialValuesHaveNormalizedScaledOutput();
/// <summary>
/// Associated half-vector conversion uses the stored alpha ratio before normalizing RGB.
/// </summary>
[Fact]
public void HalfVector4P_AssociatedScaledInputIsNormalized() => AssertAssociatedScaledInputIsNormalized<HalfVector4P>();
/// <summary>
/// Associated half-RGBA conversion uses the stored alpha ratio before normalizing RGB.
/// </summary>
[Fact]
public void RgbaHalfP_AssociatedScaledInputIsNormalized() => AssertAssociatedScaledInputIsNormalized<RgbaHalfP>();
/// <summary>
/// Checks saturation and NaN handling without deriving expectations from the invalid-input path.
/// </summary>
/// <typeparam name="TPixel">The destination pixel format.</typeparam>
private static void AssertScaledInputIsNormalized<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>.Instance.FromVector4Destructive(Configuration.Default, source, actual, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply);
Assert.Equal(expected, actual);
}
/// <summary>
/// Checks associated input against finite control values with the same represented color.
/// </summary>
/// <typeparam name="TPixel">The associated destination pixel format.</typeparam>
private static void AssertAssociatedScaledInputIsNormalized<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>.Instance.FromVector4Destructive(Configuration.Default, source, actual, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply);
Assert.Equal(expected, actual);
}
/// <summary>
/// Checks native storage and every scaled output lane independently of integer conversion semantics.
/// </summary>
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<HalfVector4>.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<HalfVector4>.Instance.ToVector4(Configuration.Default, source, actual, PixelConversionModifiers.Scale);
Assert.All(actual, value => Assert.Equal(expected, value));
}
}

55
tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs

@ -14,18 +14,6 @@ public class HistogramEqualizationTests
{ {
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F); private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F);
[Fact]
public void GlobalHistogramEqualization_ClampsDecodedFloatingPointSamples()
{
byte[] data = BuildFloatTiff(8, 2F);
using Image<HalfVector4> image = Image.Load<HalfVector4>(data);
image.Mutate(x => x.HistogramEqualization());
Assert.Equal(8, image.Width);
Assert.Equal(1, image.Height);
}
[Theory] [Theory]
[InlineData(256)] [InlineData(256)]
[InlineData(65536)] [InlineData(65536)]
@ -250,47 +238,4 @@ public class HistogramEqualizationTests
ValidatorComparer.VerifySimilarity(referenceResult, processed); 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);
}
}
} }

Loading…
Cancel
Save