Browse Source

Add binary16 pixel formats and scaling fixes

Introduces `RgbaHalf` and `RgbaHalfP` plus bulk pixel-operation support, updates AOT seeding, and routes EXR decoding through the new scaled conversions. Also fixes scaled/native range handling for half, SNORM, and signed integer pixel formats, and expands tests for the new layouts and edge cases.
pull/3154/head
James Jackson-South 1 day ago
parent
commit
2ffa9fb07b
  1. 4
      src/ImageSharp/Advanced/AotCompilerTools.cs
  2. 12
      src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
  3. 57
      src/ImageSharp/PixelFormats/HalfTypeHelper.cs
  4. 7
      src/ImageSharp/PixelFormats/PixelImplementations/A8.cs
  5. 8
      src/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs
  6. 9
      src/ImageSharp/PixelFormats/PixelImplementations/Bgra4444.cs
  7. 11
      src/ImageSharp/PixelFormats/PixelImplementations/Bgra5551.cs
  8. 9
      src/ImageSharp/PixelFormats/PixelImplementations/Byte4.cs
  9. 26
      src/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs
  10. 26
      src/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs
  11. 36
      src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs
  12. 73
      src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs
  13. 36
      src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs
  14. 39
      src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4.cs
  15. 44
      src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4P.cs
  16. 35
      src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs
  17. 38
      src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort4.cs
  18. 12
      src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfSingle.PixelOperations.cs
  19. 12
      src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector2.PixelOperations.cs
  20. 48
      src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs
  21. 701
      src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs
  22. 66
      src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4P.PixelOperations.cs
  23. 72
      src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalf.PixelOperations.cs
  24. 795
      src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs
  25. 6
      src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short2.PixelOperations.cs
  26. 4
      src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short4.PixelOperations.cs
  27. 9
      src/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs
  28. 9
      src/ImageSharp/PixelFormats/PixelImplementations/Rgba1010102.cs
  29. 9
      src/ImageSharp/PixelFormats/PixelImplementations/Rgba64.cs
  30. 227
      src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalf.cs
  31. 242
      src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalfP.cs
  32. 25
      src/ImageSharp/PixelFormats/PixelImplementations/Short2.cs
  33. 30
      src/ImageSharp/PixelFormats/PixelImplementations/Short4.cs
  34. 57
      src/ImageSharp/PixelFormats/Utils/SignedShort4PixelOperations.cs
  35. 19
      src/ImageSharp/PixelFormats/Utils/Vector4Converters.AssociatedRgbaCompatible.cs
  36. 96
      tests/ImageSharp.Tests/PixelFormats/AssociatedAlphaPixelTests.cs
  37. 54
      tests/ImageSharp.Tests/PixelFormats/HalfSingleTests.cs
  38. 29
      tests/ImageSharp.Tests/PixelFormats/HalfVector2Tests.cs
  39. 36
      tests/ImageSharp.Tests/PixelFormats/HalfVector4Tests.cs
  40. 31
      tests/ImageSharp.Tests/PixelFormats/NormalizedByte2Tests.cs
  41. 28
      tests/ImageSharp.Tests/PixelFormats/NormalizedByte4Tests.cs
  42. 31
      tests/ImageSharp.Tests/PixelFormats/NormalizedShort2Tests.cs
  43. 28
      tests/ImageSharp.Tests/PixelFormats/NormalizedShort4Tests.cs
  44. 16
      tests/ImageSharp.Tests/PixelFormats/PixelAlphaRepresentationTests.cs
  45. 30
      tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/PixelOperationsTests.Specialized.Generated.cs
  46. 6
      tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/_Common.ttinclude
  47. 61
      tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs
  48. 283
      tests/ImageSharp.Tests/PixelFormats/RgbaHalfTests.cs
  49. 48
      tests/ImageSharp.Tests/PixelFormats/Short2Tests.cs
  50. 47
      tests/ImageSharp.Tests/PixelFormats/Short4Tests.cs
  51. 7
      tests/ImageSharp.Tests/Processing/Filters/BrightnessTest.cs

4
src/ImageSharp/Advanced/AotCompilerTools.cs

@ -114,6 +114,8 @@ internal static class AotCompilerTools
Seed<Rgba32>();
Seed<Rgba32P>();
Seed<Rgba64>();
Seed<RgbaHalf>();
Seed<RgbaHalfP>();
Seed<RgbaVector>();
Seed<Short2>();
Seed<Short4>();
@ -199,6 +201,8 @@ internal static class AotCompilerTools
img.CloneAs<Rgba32>(default);
img.CloneAs<Rgba32P>(default);
img.CloneAs<Rgba64>(default);
img.CloneAs<RgbaHalf>(default);
img.CloneAs<RgbaHalfP>(default);
img.CloneAs<RgbaVector>(default);
img.CloneAs<Short2>(default);
img.CloneAs<Short4>(default);

12
src/ImageSharp/Formats/Exr/ExrDecoderCore.cs

@ -4,6 +4,7 @@
using System.Buffers;
using System.Buffers.Binary;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;
using SixLabors.ImageSharp.Formats.Exr.Compression;
@ -208,11 +209,10 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
for (int x = 0; x < width; x++)
{
HalfVector4 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : 1.0f);
Vector4 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : 1F);
// OpenEXR channel values are associated. The destination conversion retains that representation for associated
// pixels and unassociates only when the destination stores straight alpha.
pixelRow[x] = TPixel.FromAssociatedVector4(pixelValue.ToVector4());
// OpenEXR channels are associated color values, not values in the destination pixel format's native numeric range.
pixelRow[x] = TPixel.FromAssociatedScaledVector4(pixelValue);
}
decodedRows++;
@ -292,8 +292,8 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
{
Rgba128 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : uint.MaxValue);
// Preserve the file's associated-alpha convention while converting from the native unsigned channel domain.
pixelRow[x] = TPixel.FromAssociatedVector4(pixelValue.ToVector4());
// Rgba128 normalizes the unsigned channel values before the destination applies its own numeric representation.
pixelRow[x] = TPixel.FromAssociatedScaledVector4(pixelValue.ToVector4());
}
decodedRows++;

57
src/ImageSharp/PixelFormats/HalfTypeHelper.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
@ -11,6 +12,14 @@ namespace SixLabors.ImageSharp.PixelFormats;
/// </summary>
internal static class HalfTypeHelper
{
// IEEE 754 binary16 has a largest finite magnitude of 65504. Scaled pixel vectors map that complete finite
// interval to [0, 1], while native vectors continue to expose the stored floating-point value directly.
internal const float FiniteMinimum = -65504F;
internal const float FiniteMaximum = 65504F;
internal const float FiniteRange = FiniteMaximum - FiniteMinimum;
internal const float InverseFiniteRange = (float)(1D / FiniteRange);
internal const float ScaledMidpoint = .5F;
// These constants mirror the binary16 conversion used by System.Half. Keeping the vector conversion
// bit-for-bit equivalent to the scalar runtime conversion makes SIMD a pure throughput optimization.
private const uint HalfExponentMask = 0x7C00;
@ -40,6 +49,54 @@ internal static class HalfTypeHelper
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static float Unpack(ushort value) => (float)BitConverter.UInt16BitsToHalf(value);
/// <summary>
/// Normalizes a finite binary16 value to the scaled pixel range.
/// </summary>
/// <param name="value">The native binary16 value represented as a <see cref="float"/>.</param>
/// <returns>The normalized value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static float ToScaled(float value) => (value * InverseFiniteRange) + ScaledMidpoint;
/// <summary>
/// Normalizes finite binary16 values to the scaled pixel range.
/// </summary>
/// <param name="value">The native binary16 values.</param>
/// <returns>The normalized values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector2 ToScaled(Vector2 value) => (value * InverseFiniteRange) + new Vector2(ScaledMidpoint);
/// <summary>
/// Normalizes finite binary16 values to the scaled pixel range.
/// </summary>
/// <param name="value">The native binary16 values.</param>
/// <returns>The normalized values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector4 ToScaled(Vector4 value) => (value * InverseFiniteRange) + new Vector4(ScaledMidpoint);
/// <summary>
/// Expands a normalized value to the finite binary16 range.
/// </summary>
/// <param name="value">The normalized value.</param>
/// <returns>The native binary16 value represented as a <see cref="float"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static float FromScaled(float value) => (value * FiniteRange) + FiniteMinimum;
/// <summary>
/// Expands normalized values to the finite binary16 range.
/// </summary>
/// <param name="value">The normalized values.</param>
/// <returns>The native binary16 values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector2 FromScaled(Vector2 value) => (value * FiniteRange) + new Vector2(FiniteMinimum);
/// <summary>
/// Expands normalized values to the finite binary16 range.
/// </summary>
/// <param name="value">The normalized values.</param>
/// <returns>The native binary16 values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector4 FromScaled(Vector4 value) => (value * FiniteRange) + new Vector4(FiniteMinimum);
/// <summary>
/// Unpacks eight binary16 values into two vectors of single-precision values.
/// </summary>

7
src/ImageSharp/PixelFormats/PixelImplementations/A8.cs

@ -8,10 +8,11 @@ namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing a single 8-bit normalized alpha value.
/// <para>
/// Ranges from [0, 0, 0, 0] to [0, 0, 0, 1] in vector form.
/// </para>
/// </summary>
/// <remarks>
/// <see cref="ToVector4"/> and scaled vector conversions return alpha in <c>[0, 1]</c> with zero color components.
/// The storage layout matches <c>DXGI_FORMAT_A8_UNORM</c>.
/// </remarks>
public partial struct A8 : IPixel<A8>, IPackedVector<byte>
{
/// <summary>

8
src/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs

@ -7,14 +7,12 @@ using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing unsigned normalized values ranging from 0 to 1.
/// Packed pixel type containing three unsigned normalized values.
/// The x and z components use 5 bits, and the y component uses 6 bits.
/// <para>
/// Ranges from [0, 0, 0, 1] to [1, 1, 1, 1] in vector form.
/// </para>
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="Bgr565"/> struct.
/// <see cref="ToVector4"/> and scaled vector conversions return color components in <c>[0, 1]</c> with an implicit
/// alpha of <c>1</c>. The storage layout matches <c>DXGI_FORMAT_B5G6R5_UNORM</c>.
/// </remarks>
/// <param name="vector">
/// The vector containing the components for the packed value.

9
src/ImageSharp/PixelFormats/PixelImplementations/Bgra4444.cs

@ -7,11 +7,12 @@ using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing unsigned normalized values, ranging from 0 to 1, using 4 bits each for x, y, z, and w.
/// <para>
/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form.
/// </para>
/// Packed pixel type containing four unsigned normalized values using 4 bits per component.
/// </summary>
/// <remarks>
/// <see cref="ToVector4"/> and scaled vector conversions return all components in <c>[0, 1]</c>. The storage layout matches
/// <c>DXGI_FORMAT_B4G4R4A4_UNORM</c>.
/// </remarks>
public partial struct Bgra4444 : IPixel<Bgra4444>, IPackedVector<ushort>
{
/// <summary>

11
src/ImageSharp/PixelFormats/PixelImplementations/Bgra5551.cs

@ -7,12 +7,13 @@ using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing unsigned normalized values ranging from 0 to 1.
/// The x , y and z components use 5 bits, and the w component uses 1 bit.
/// <para>
/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form.
/// </para>
/// Packed pixel type containing four unsigned normalized values.
/// The x, y, and z components use 5 bits, and the w component uses 1 bit.
/// </summary>
/// <remarks>
/// <see cref="ToVector4"/> and scaled vector conversions return all components in <c>[0, 1]</c>. The storage layout matches
/// <c>DXGI_FORMAT_B5G5R5A1_UNORM</c>.
/// </remarks>
public partial struct Bgra5551 : IPixel<Bgra5551>, IPackedVector<ushort>
{
/// <summary>

9
src/ImageSharp/PixelFormats/PixelImplementations/Byte4.cs

@ -8,11 +8,12 @@ using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing four 8-bit unsigned integer values, ranging from 0 to 255.
/// <para>
/// Ranges from [0, 0, 0, 0] to [255, 255, 255, 255] in vector form.
/// </para>
/// Packed pixel type containing four 8-bit unsigned integer values.
/// </summary>
/// <remarks>
/// <see cref="ToVector4"/> returns components in <c>[0, 255]</c>. Scaled vector conversions map that range to
/// <c>[0, 1]</c>. The storage layout matches <c>DXGI_FORMAT_R8G8B8A8_UINT</c>.
/// </remarks>
public partial struct Byte4 : IPixel<Byte4>, IPackedVector<uint>
{
private static readonly Vector4 MaxBytes = Vector128.Create(255f).AsVector4();

26
src/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs

@ -7,11 +7,13 @@ using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing a single 16 bit floating point value.
/// <para>
/// Ranges from [-1, 0, 0, 1] to [1, 0, 0, 1] in vector form.
/// </para>
/// Packed pixel type containing a single IEEE 754 binary16 floating-point value.
/// </summary>
/// <remarks>
/// <see cref="ToVector4"/> returns the stored IEEE 754 binary16 value directly. Scaled vector conversions normalize
/// the finite range <c>[-65504, 65504]</c> to <c>[0, 1]</c>. The packed representation is binary-compatible with
/// <c>DXGI_FORMAT_R16_FLOAT</c>.
/// </remarks>
public partial struct HalfSingle : IPixel<HalfSingle>, IPackedVector<ushort>
{
/// <summary>
@ -53,9 +55,8 @@ public partial struct HalfSingle : IPixel<HalfSingle>, IPackedVector<ushort>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToScaledVector4()
{
float single = this.ToSingle() + 1F;
single /= 2F;
return new Vector4(single, 0, 0, 1F);
float scaled = HalfTypeHelper.ToScaled(this.ToSingle());
return new Vector4(scaled, 0, 0, 1F);
}
/// <inheritdoc />
@ -109,20 +110,15 @@ public partial struct HalfSingle : IPixel<HalfSingle>, IPackedVector<ushort>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static HalfSingle FromAssociatedVector4(Vector4 source)
{
// Only the stored color component uses the native [-1, 1] encoding; W remains the normalized source alpha.
source.X = (source.X + 1F) / 2F;
// This format has no native alpha component: X uses the binary16 finite range, while W remains the source opacity.
source.X = HalfTypeHelper.ToScaled(source.X);
return FromAssociatedScaledVector4(source);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static HalfSingle FromScaledVector4(Vector4 source)
{
float scaled = source.X;
scaled *= 2F;
scaled--;
return new HalfSingle { PackedValue = HalfTypeHelper.Pack(scaled) };
}
=> new() { PackedValue = HalfTypeHelper.Pack(HalfTypeHelper.FromScaled(source.X)) };
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]

26
src/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs

@ -7,11 +7,13 @@ using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing two 16-bit floating-point values.
/// <para>
/// Ranges from [-1, -1, 0, 1] to [1, 1, 0, 1] in vector form.
/// </para>
/// Packed pixel type containing two IEEE 754 binary16 floating-point values.
/// </summary>
/// <remarks>
/// <see cref="ToVector2"/> and <see cref="ToVector4"/> return the stored IEEE 754 binary16 values directly. Scaled
/// vector conversions normalize the finite range <c>[-65504, 65504]</c> to <c>[0, 1]</c>. The packed representation is
/// binary-compatible with <c>DXGI_FORMAT_R16G16_FLOAT</c>.
/// </remarks>
public partial struct HalfVector2 : IPixel<HalfVector2>, IPackedVector<uint>
{
/// <summary>
@ -60,9 +62,7 @@ public partial struct HalfVector2 : IPixel<HalfVector2>, IPackedVector<uint>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToScaledVector4()
{
Vector2 scaled = this.ToVector2();
scaled += Vector2.One;
scaled /= 2F;
Vector2 scaled = HalfTypeHelper.ToScaled(this.ToVector2());
return new Vector4(scaled, 0F, 1F);
}
@ -121,9 +121,10 @@ public partial struct HalfVector2 : IPixel<HalfVector2>, IPackedVector<uint>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static HalfVector2 FromAssociatedVector4(Vector4 source)
{
// Only the stored color components use the native [-1, 1] encoding; W remains the normalized source alpha.
source.X = (source.X + 1F) / 2F;
source.Y = (source.Y + 1F) / 2F;
// This format has no native alpha component: XY use the binary16 finite range, while W remains the source opacity.
Vector2 scaled = HalfTypeHelper.ToScaled(new Vector2(source.X, source.Y));
source.X = scaled.X;
source.Y = scaled.Y;
return FromAssociatedScaledVector4(source);
}
@ -131,9 +132,8 @@ public partial struct HalfVector2 : IPixel<HalfVector2>, IPackedVector<uint>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static HalfVector2 FromScaledVector4(Vector4 source)
{
Vector2 scaled = new Vector2(source.X, source.Y) * 2F;
scaled -= Vector2.One;
return new HalfVector2 { PackedValue = Pack(scaled.X, scaled.Y) };
Vector2 native = HalfTypeHelper.FromScaled(new Vector2(source.X, source.Y));
return new HalfVector2 { PackedValue = Pack(native.X, native.Y) };
}
/// <inheritdoc />

36
src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs

@ -7,11 +7,13 @@ using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing four 16-bit floating-point values.
/// <para>
/// Ranges from [-1, -1, -1, -1] to [1, 1, 1, 1] in vector form.
/// </para>
/// Packed pixel type containing four IEEE 754 binary16 floating-point values.
/// </summary>
/// <remarks>
/// <see cref="ToVector4"/> returns the stored IEEE 754 binary16 values directly. Scaled vector conversions normalize
/// the finite range <c>[-65504, 65504]</c> to <c>[0, 1]</c>. The packed representation is binary-compatible with
/// <c>DXGI_FORMAT_R16G16B16A16_FLOAT</c>.
/// </remarks>
public partial struct HalfVector4 : IPixel<HalfVector4>, IPackedVector<ulong>
{
/// <summary>
@ -63,13 +65,7 @@ public partial struct HalfVector4 : IPixel<HalfVector4>, IPackedVector<ulong>
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToScaledVector4()
{
Vector4 scaled = this.ToVector4();
scaled += Vector4.One;
scaled /= 2f;
return scaled;
}
public readonly Vector4 ToScaledVector4() => HalfTypeHelper.ToScaled(this.ToVector4());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -112,10 +108,8 @@ public partial struct HalfVector4 : IPixel<HalfVector4>, IPackedVector<ulong>
{
Vector4 vector = this.ToAssociatedScaledVector4();
// Native components use an affine [-1, 1] encoding, so direct multiplication would use the wrong zero point.
vector *= 2F;
vector -= Vector4.One;
return vector;
// Association is defined in scaled color space, so map the associated result back to the native binary16 range.
return HalfTypeHelper.FromScaled(vector);
}
/// <inheritdoc />
@ -138,20 +132,14 @@ public partial struct HalfVector4 : IPixel<HalfVector4>, IPackedVector<ulong>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static HalfVector4 FromAssociatedVector4(Vector4 source)
{
// Map the affine native encoding to logical [0, 1] space before unassociating.
source += Vector4.One;
source /= 2F;
// Restore scaled opacity before unassociating the color channels.
source = HalfTypeHelper.ToScaled(source);
return FromAssociatedScaledVector4(source);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static HalfVector4 FromScaledVector4(Vector4 source)
{
source *= 2f;
source -= Vector4.One;
return FromVector4(source);
}
public static HalfVector4 FromScaledVector4(Vector4 source) => FromVector4(HalfTypeHelper.FromScaled(source));
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]

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

@ -10,7 +10,9 @@ namespace SixLabors.ImageSharp.PixelFormats;
/// Packed pixel type containing four associated 16-bit floating-point values.
/// </summary>
/// <remarks>
/// The native packed and vector representations use associated alpha.
/// <see cref="ToVector4"/> returns the stored associated IEEE 754 binary16 values directly. Scaled vector conversions
/// normalize the finite range <c>[-65504, 65504]</c> to <c>[0, 1]</c> while preserving associated alpha. The packed
/// representation is binary-compatible with <c>DXGI_FORMAT_R16G16B16A16_FLOAT</c>.
/// </remarks>
public partial struct HalfVector4P : IPixel<HalfVector4P>, IPackedVector<ulong>
{
@ -58,13 +60,8 @@ public partial struct HalfVector4P : IPixel<HalfVector4P>, IPackedVector<ulong>
}
/// <inheritdoc />
public readonly Vector4 ToScaledVector4()
{
Vector4 scaled = this.ToVector4();
scaled += Vector4.One;
scaled /= 2F;
return scaled;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToScaledVector4() => HalfTypeHelper.ToScaled(this.ToVector4());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -90,11 +87,8 @@ public partial struct HalfVector4P : IPixel<HalfVector4P>, IPackedVector<ulong>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToUnassociatedVector4()
{
// Association is defined in the common scaled domain. Binary16-native W is an affine encoding of alpha, so it cannot be used as a divisor directly.
Vector4 vector = this.ToUnassociatedScaledVector4();
vector *= 2F;
vector -= Vector4.One;
return vector;
return HalfTypeHelper.FromScaled(vector);
}
/// <inheritdoc />
@ -121,9 +115,7 @@ public partial struct HalfVector4P : IPixel<HalfVector4P>, IPackedVector<ulong>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static HalfVector4P FromUnassociatedVector4(Vector4 source)
{
// Convert the binary16-native range to the common scaled domain before associating because native W is not opacity.
source += Vector4.One;
source /= 2F;
source = HalfTypeHelper.ToScaled(source);
return FromUnassociatedScaledVector4(source);
}
@ -131,9 +123,7 @@ public partial struct HalfVector4P : IPixel<HalfVector4P>, IPackedVector<ulong>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static HalfVector4P FromAssociatedVector4(Vector4 source)
{
// Reassociation must also operate in scaled space so RGB follows the quantized scaled alpha rather than the binary16-native W component.
source += Vector4.One;
source /= 2F;
source = HalfTypeHelper.ToScaled(source);
return FromAssociatedScaledVector4(source);
}
@ -145,20 +135,6 @@ public partial struct HalfVector4P : IPixel<HalfVector4P>, IPackedVector<ulong>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static HalfVector4P FromAssociatedScaledVector4(Vector4 source) => PackAssociatedScaledVector4(Reassociate(source));
/// <summary>
/// Packs an associated scaled vector into binary16-native storage.
/// </summary>
/// <param name="source">The associated scaled vector.</param>
/// <returns>The packed pixel.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static HalfVector4P PackAssociatedScaledVector4(Vector4 source)
{
// Binary16 storage uses the native [-1, 1] range even though association is defined in scaled opacity space.
source *= 2F;
source -= Vector4.One;
return new() { PackedValue = Pack(source) };
}
/// <inheritdoc />
public static HalfVector4P FromAbgr32(Abgr32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4());
@ -224,9 +200,8 @@ public partial struct HalfVector4P : IPixel<HalfVector4P>, IPackedVector<ulong>
{
source = Numerics.Clamp(source, Vector4.Zero, Vector4.One);
// Quantize alpha through the destination's native half representation before RGB is associated with it.
float nativeAlpha = (source.W * 2F) - 1F;
source.W = (HalfTypeHelper.Unpack(HalfTypeHelper.Pack(nativeAlpha)) + 1F) / 2F;
// RGB must use the scaled alpha that the binary16 representation can reproduce.
source.W = QuantizeScaledAlpha(source.W);
Numerics.Premultiply(ref source);
return source;
}
@ -246,9 +221,7 @@ public partial struct HalfVector4P : IPixel<HalfVector4P>, IPackedVector<ulong>
return Vector4.Zero;
}
// The binary16-native alpha range is [-1, 1]. Clamp before packing so out-of-range scaled alpha cannot escape the pixel's declared [0, 1] opacity range.
float nativeAlpha = Numerics.Clamp((alpha * 2F) - 1F, -1F, 1F);
float storedAlpha = (HalfTypeHelper.Unpack(HalfTypeHelper.Pack(nativeAlpha)) + 1F) / 2F;
float storedAlpha = QuantizeScaledAlpha(alpha);
// Associated RGB scales by the same ratio as alpha. Applying that ratio directly avoids the extra division and multiplication of an unpremultiply/premultiply round trip and preserves exact midpoints when alpha needs no quantization.
source *= storedAlpha / alpha;
@ -257,6 +230,30 @@ public partial struct HalfVector4P : IPixel<HalfVector4P>, IPackedVector<ulong>
return source;
}
/// <summary>
/// Packs an associated scaled vector into the native binary16 representation.
/// </summary>
/// <param name="source">The associated scaled vector.</param>
/// <returns>The packed pixel.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static HalfVector4P PackAssociatedScaledVector4(Vector4 source)
{
source = HalfTypeHelper.FromScaled(source);
return new HalfVector4P { PackedValue = Pack(source) };
}
/// <summary>
/// Quantizes scaled alpha through the native binary16 representation.
/// </summary>
/// <param name="alpha">The scaled alpha value.</param>
/// <returns>The scaled value represented by the stored binary16 component.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float QuantizeScaledAlpha(float alpha)
{
float nativeAlpha = HalfTypeHelper.FromScaled(Numerics.Clamp(alpha, 0F, 1F));
return HalfTypeHelper.ToScaled(HalfTypeHelper.Unpack(HalfTypeHelper.Pack(nativeAlpha)));
}
/// <summary>
/// Packs native half-precision components into a 64-bit value.
/// </summary>

36
src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs

@ -7,15 +7,20 @@ using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing two 8-bit signed normalized values, ranging from −1 to 1.
/// <para>
/// Ranges from [-1, -1, 0, 1] to [1, 1, 0, 1] in vector form.
/// </para>
/// Packed pixel type containing two 8-bit signed normalized values.
/// </summary>
/// <remarks>
/// <see cref="ToVector2"/> and <see cref="ToVector4"/> return components in the native signed-normalized range
/// <c>[-1, 1]</c>. Scaled vector conversions return components in <c>[0, 1]</c>.
/// The packed two's-complement codes <c>-128</c> and <c>-127</c> both represent <c>-1</c>,
/// matching <c>DXGI_FORMAT_R8G8_SNORM</c>.
/// </remarks>
public partial struct NormalizedByte2 : IPixel<NormalizedByte2>, IPackedVector<ushort>
{
private const float MaxPos = 127f;
private const float ScaledMagnitude = MaxPos * 2F;
private static readonly Vector2 Half = new(MaxPos);
private static readonly Vector2 Minimum = new(-MaxPos);
private static readonly Vector2 MinusOne = new(-1f);
/// <summary>
@ -67,9 +72,14 @@ public partial struct NormalizedByte2 : IPixel<NormalizedByte2>, IPackedVector<u
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToScaledVector4()
{
Vector2 scaled = this.ToVector2();
scaled += Vector2.One;
scaled /= 2f;
Vector2 scaled = new(
(sbyte)(this.PackedValue & 0xFF),
(sbyte)(this.PackedValue >> 8));
// SNORM reserves both minimum two's-complement codes for -1. Clamp before offsetting so raw -128 cannot escape the scaled range.
scaled = Vector2.Max(scaled, Minimum);
scaled += Half;
scaled /= ScaledMagnitude;
return new Vector4(scaled, 0f, 1f);
}
@ -201,9 +211,15 @@ public partial struct NormalizedByte2 : IPixel<NormalizedByte2>, IPackedVector<u
/// </summary>
/// <returns>The <see cref="Vector2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector2 ToVector2() => new(
(sbyte)((this.PackedValue >> 0) & 0xFF) / MaxPos,
(sbyte)((this.PackedValue >> 8) & 0xFF) / MaxPos);
public readonly Vector2 ToVector2()
{
Vector2 vector = new(
(sbyte)(this.PackedValue & 0xFF),
(sbyte)(this.PackedValue >> 8));
// DirectX SNORM maps both -128 and -127 to -1.
return Vector2.Max(vector, Minimum) / MaxPos;
}
/// <inheritdoc />
public override readonly bool Equals(object? obj) => obj is NormalizedByte2 other && this.Equals(other);

39
src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4.cs

@ -8,16 +8,20 @@ using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing four 8-bit signed normalized values, ranging from −1 to 1.
/// <para>
/// Ranges from [-1, -1, -1, -1] to [1, 1, 1, 1] in vector form.
/// </para>
/// Packed pixel type containing four 8-bit signed normalized values.
/// </summary>
/// <remarks>
/// <see cref="ToVector4"/> returns components in the native signed-normalized range <c>[-1, 1]</c>.
/// Scaled vector conversions return components in <c>[0, 1]</c>.
/// The packed two's-complement codes <c>-128</c> and <c>-127</c> both represent <c>-1</c>,
/// matching <c>DXGI_FORMAT_R8G8B8A8_SNORM</c>.
/// </remarks>
public partial struct NormalizedByte4 : IPixel<NormalizedByte4>, IPackedVector<uint>
{
private const float MaxPos = 127f;
private const float ScaledMagnitude = MaxPos * 2F;
private static readonly Vector4 Half = Vector128.Create(MaxPos).AsVector4();
private static readonly Vector4 Minimum = -Half;
private static readonly Vector4 MinusOne = Vector128.Create(-1f).AsVector4();
/// <summary>
@ -73,21 +77,28 @@ public partial struct NormalizedByte4 : IPixel<NormalizedByte4>, IPackedVector<u
{
// Offset the exact signed components before division. Mapping an already normalized value through (value + 1) / 2 loses precision near -1 through cancellation.
Vector4 scaled = new(
(sbyte)((this.PackedValue >> 0) & 0xFF) + MaxPos,
(sbyte)((this.PackedValue >> 8) & 0xFF) + MaxPos,
(sbyte)((this.PackedValue >> 16) & 0xFF) + MaxPos,
(sbyte)((this.PackedValue >> 24) & 0xFF) + MaxPos);
(sbyte)((this.PackedValue >> 0) & 0xFF),
(sbyte)((this.PackedValue >> 8) & 0xFF),
(sbyte)((this.PackedValue >> 16) & 0xFF),
(sbyte)((this.PackedValue >> 24) & 0xFF));
return scaled / ScaledMagnitude;
// SNORM reserves both minimum two's-complement codes for -1. Clamp before offsetting so raw -128 cannot escape the scaled range.
return (Vector4.Max(scaled, Minimum) + Half) / ScaledMagnitude;
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToVector4() => new(
(sbyte)((this.PackedValue >> 0) & 0xFF) / MaxPos,
(sbyte)((this.PackedValue >> 8) & 0xFF) / MaxPos,
(sbyte)((this.PackedValue >> 16) & 0xFF) / MaxPos,
(sbyte)((this.PackedValue >> 24) & 0xFF) / MaxPos);
public readonly Vector4 ToVector4()
{
Vector4 vector = new(
(sbyte)((this.PackedValue >> 0) & 0xFF),
(sbyte)((this.PackedValue >> 8) & 0xFF),
(sbyte)((this.PackedValue >> 16) & 0xFF),
(sbyte)((this.PackedValue >> 24) & 0xFF));
// DirectX SNORM maps both -128 and -127 to -1.
return Vector4.Max(vector, Minimum) / MaxPos;
}
/// <inheritdoc />
public static PixelTypeInfo GetPixelTypeInfo()

44
src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4P.cs

@ -11,13 +11,17 @@ namespace SixLabors.ImageSharp.PixelFormats;
/// Packed pixel type containing four associated 8-bit signed normalized values ranging from -1 to 1.
/// </summary>
/// <remarks>
/// The native packed and vector representations use associated alpha.
/// <see cref="ToVector4"/> returns associated components in the native signed-normalized range <c>[-1, 1]</c>.
/// Scaled vector conversions return associated components in <c>[0, 1]</c>.
/// The packed two's-complement codes <c>-128</c> and <c>-127</c> both represent <c>-1</c>,
/// matching <c>DXGI_FORMAT_R8G8B8A8_SNORM</c>.
/// </remarks>
public partial struct NormalizedByte4P : IPixel<NormalizedByte4P>, IPackedVector<uint>
{
private const float MaxPos = 127F;
private const float ScaledMagnitude = MaxPos * 2F;
private static readonly Vector4 Half = Vector128.Create(MaxPos).AsVector4();
private static readonly Vector4 Minimum = -Half;
private static readonly Vector4 MinusOne = Vector128.Create(-1F).AsVector4();
/// <summary>
@ -65,12 +69,13 @@ public partial struct NormalizedByte4P : IPixel<NormalizedByte4P>, IPackedVector
{
// Offset the exact signed components before division. Mapping an already normalized value through (value + 1) / 2 loses precision near -1 through cancellation.
Vector4 scaled = new(
(sbyte)((this.PackedValue >> 0) & 0xFF) + MaxPos,
(sbyte)((this.PackedValue >> 8) & 0xFF) + MaxPos,
(sbyte)((this.PackedValue >> 16) & 0xFF) + MaxPos,
(sbyte)((this.PackedValue >> 24) & 0xFF) + MaxPos);
(sbyte)((this.PackedValue >> 0) & 0xFF),
(sbyte)((this.PackedValue >> 8) & 0xFF),
(sbyte)((this.PackedValue >> 16) & 0xFF),
(sbyte)((this.PackedValue >> 24) & 0xFF));
return scaled / ScaledMagnitude;
// SNORM reserves both minimum two's-complement codes for -1. Clamp before offsetting so raw -128 cannot escape the scaled range.
return (Vector4.Max(scaled, Minimum) + Half) / ScaledMagnitude;
}
/// <inheritdoc />
@ -82,11 +87,17 @@ public partial struct NormalizedByte4P : IPixel<NormalizedByte4P>, IPackedVector
public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4();
/// <inheritdoc />
public readonly Vector4 ToVector4() => new(
(sbyte)((this.PackedValue >> 0) & 0xFF) / MaxPos,
(sbyte)((this.PackedValue >> 8) & 0xFF) / MaxPos,
(sbyte)((this.PackedValue >> 16) & 0xFF) / MaxPos,
(sbyte)((this.PackedValue >> 24) & 0xFF) / MaxPos);
public readonly Vector4 ToVector4()
{
Vector4 vector = new(
(sbyte)((this.PackedValue >> 0) & 0xFF),
(sbyte)((this.PackedValue >> 8) & 0xFF),
(sbyte)((this.PackedValue >> 16) & 0xFF),
(sbyte)((this.PackedValue >> 24) & 0xFF));
// DirectX SNORM maps both -128 and -127 to -1.
return Vector4.Max(vector, Minimum) / MaxPos;
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -244,10 +255,13 @@ public partial struct NormalizedByte4P : IPixel<NormalizedByte4P>, IPackedVector
{
// Offset signed storage into exact nonnegative byte magnitudes before division so the quotient retains the destination's byte-rounding midpoint.
Vector4 vector = new(
(sbyte)(source.PackedValue >> 0) + MaxPos,
(sbyte)(source.PackedValue >> 8) + MaxPos,
(sbyte)(source.PackedValue >> 16) + MaxPos,
(sbyte)(source.PackedValue >> 24) + MaxPos);
(sbyte)(source.PackedValue >> 0),
(sbyte)(source.PackedValue >> 8),
(sbyte)(source.PackedValue >> 16),
(sbyte)(source.PackedValue >> 24));
// Clamp the duplicate SNORM minimum encoding before converting it to the nonnegative associated domain.
vector = Vector4.Max(vector, Minimum) + Half;
if (vector.W == 0F)
{

35
src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs

@ -7,15 +7,19 @@ using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing two 16-bit signed normalized values, ranging from −1 to 1.
/// <para>
/// Ranges from [-1, -1, 0, 1] to [1, 1, 0, 1] in vector form.
/// </para>
/// Packed pixel type containing two 16-bit signed normalized values.
/// </summary>
/// <remarks>
/// <see cref="ToVector2"/> and <see cref="ToVector4"/> return components in the native signed-normalized range
/// <c>[-1, 1]</c>. Scaled vector conversions return components in <c>[0, 1]</c>.
/// The packed two's-complement codes <c>-32768</c> and <c>-32767</c> both represent <c>-1</c>,
/// matching <c>DXGI_FORMAT_R16G16_SNORM</c>.
/// </remarks>
public partial struct NormalizedShort2 : IPixel<NormalizedShort2>, IPackedVector<uint>
{
// Largest two byte positive number 0xFFFF >> 1;
private const float MaxPos = 0x7FFF;
private const float ScaledMagnitude = MaxPos * 2F;
private static readonly Vector2 Max = new(MaxPos);
private static readonly Vector2 Min = Vector2.Negate(Max);
@ -69,9 +73,14 @@ public partial struct NormalizedShort2 : IPixel<NormalizedShort2>, IPackedVector
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToScaledVector4()
{
Vector2 scaled = this.ToVector2();
scaled += Vector2.One;
scaled /= 2f;
Vector2 scaled = new(
(short)(this.PackedValue & 0xFFFF),
(short)(this.PackedValue >> 0x10));
// SNORM reserves both minimum two's-complement codes for -1. Clamp before offsetting so raw -32768 cannot escape the scaled range.
scaled = Vector2.Max(scaled, Min);
scaled += Max;
scaled /= ScaledMagnitude;
return new Vector4(scaled, 0f, 1f);
}
@ -203,9 +212,15 @@ public partial struct NormalizedShort2 : IPixel<NormalizedShort2>, IPackedVector
/// </summary>
/// <returns>The <see cref="Vector2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector2 ToVector2() => new(
(short)(this.PackedValue & 0xFFFF) / MaxPos,
(short)(this.PackedValue >> 0x10) / MaxPos);
public readonly Vector2 ToVector2()
{
Vector2 vector = new(
(short)(this.PackedValue & 0xFFFF),
(short)(this.PackedValue >> 0x10));
// DirectX SNORM maps both -32768 and -32767 to -1.
return Vector2.Max(vector, Min) / MaxPos;
}
/// <inheritdoc />
public override readonly bool Equals(object? obj) => obj is NormalizedShort2 other && this.Equals(other);

38
src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort4.cs

@ -8,11 +8,14 @@ using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing four 16-bit signed normalized values, ranging from −1 to 1.
/// <para>
/// Ranges from [-1, -1, -1, -1] to [1, 1, 1, 1] in vector form.
/// </para>
/// Packed pixel type containing four 16-bit signed normalized values.
/// </summary>
/// <remarks>
/// <see cref="ToVector4"/> returns components in the native signed-normalized range <c>[-1, 1]</c>.
/// Scaled vector conversions return components in <c>[0, 1]</c>.
/// The packed two's-complement codes <c>-32768</c> and <c>-32767</c> both represent <c>-1</c>,
/// matching <c>DXGI_FORMAT_R16G16B16A16_SNORM</c>.
/// </remarks>
public partial struct NormalizedShort4 : IPixel<NormalizedShort4>, IPackedVector<ulong>
{
// Largest two byte positive number 0xFFFF >> 1;
@ -74,21 +77,28 @@ public partial struct NormalizedShort4 : IPixel<NormalizedShort4>, IPackedVector
{
// Offset the exact signed components before division. Mapping an already normalized value through (value + 1) / 2 loses precision near -1 through cancellation.
Vector4 scaled = new(
(short)((this.PackedValue >> 0x00) & 0xFFFF) + MaxPos,
(short)((this.PackedValue >> 0x10) & 0xFFFF) + MaxPos,
(short)((this.PackedValue >> 0x20) & 0xFFFF) + MaxPos,
(short)((this.PackedValue >> 0x30) & 0xFFFF) + MaxPos);
(short)((this.PackedValue >> 0x00) & 0xFFFF),
(short)((this.PackedValue >> 0x10) & 0xFFFF),
(short)((this.PackedValue >> 0x20) & 0xFFFF),
(short)((this.PackedValue >> 0x30) & 0xFFFF));
return scaled / ScaledMagnitude;
// SNORM reserves both minimum two's-complement codes for -1. Clamp before offsetting so raw -32768 cannot escape the scaled range.
return (Vector4.Max(scaled, Min) + Max) / ScaledMagnitude;
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToVector4() => new(
(short)((this.PackedValue >> 0x00) & 0xFFFF) / MaxPos,
(short)((this.PackedValue >> 0x10) & 0xFFFF) / MaxPos,
(short)((this.PackedValue >> 0x20) & 0xFFFF) / MaxPos,
(short)((this.PackedValue >> 0x30) & 0xFFFF) / MaxPos);
public readonly Vector4 ToVector4()
{
Vector4 vector = new(
(short)((this.PackedValue >> 0x00) & 0xFFFF),
(short)((this.PackedValue >> 0x10) & 0xFFFF),
(short)((this.PackedValue >> 0x20) & 0xFFFF),
(short)((this.PackedValue >> 0x30) & 0xFFFF));
// DirectX SNORM maps both -32768 and -32767 to -1.
return Vector4.Max(vector, Min) / MaxPos;
}
/// <inheritdoc />
public static PixelTypeInfo GetPixelTypeInfo()

12
src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfSingle.PixelOperations.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats.Utils;
namespace SixLabors.ImageSharp.PixelFormats;
@ -16,9 +15,6 @@ public partial struct HalfSingle
/// </summary>
internal class PixelOperations : PixelOperations<HalfSingle>
{
private static readonly Vector4 NativeOffset = new(1F, 0F, 0F, 0F);
private static readonly Vector4 NativeDivisor = new(2F, 1F, 1F, 1F);
// Alpha is implicitly one, so both outward representations already contain associated color components.
/// <inheritdoc />
@ -26,13 +22,5 @@ public partial struct HalfSingle
/// <inheritdoc />
protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan<HalfSingle> source, Span<Vector4> destination) => this.ToUnassociatedScaledVector4(configuration, source, destination);
/// <inheritdoc />
protected override void FromAssociatedVector4Destructive(Configuration configuration, Span<Vector4> source, Span<HalfSingle> destination)
{
// Only X uses the affine native range; W must remain normalized so the scaled path can unassociate the source color.
Vector4Converters.AddThenDivide(source, NativeOffset, NativeDivisor);
this.FromAssociatedScaledVector4Destructive(configuration, source, destination);
}
}
}

12
src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector2.PixelOperations.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats.Utils;
namespace SixLabors.ImageSharp.PixelFormats;
@ -16,9 +15,6 @@ public partial struct HalfVector2
/// </summary>
internal class PixelOperations : PixelOperations<HalfVector2>
{
private static readonly Vector4 NativeOffset = new(1F, 1F, 0F, 0F);
private static readonly Vector4 NativeDivisor = new(2F, 2F, 1F, 1F);
// Alpha is implicitly one, so both outward representations already contain associated color components.
/// <inheritdoc />
@ -26,13 +22,5 @@ public partial struct HalfVector2
/// <inheritdoc />
protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan<HalfVector2> source, Span<Vector4> destination) => this.ToUnassociatedScaledVector4(configuration, source, destination);
/// <inheritdoc />
protected override void FromAssociatedVector4Destructive(Configuration configuration, Span<Vector4> source, Span<HalfVector2> destination)
{
// Only X and Y use affine native coordinates. W is the normalized alpha used by the scaled unassociation step.
Vector4Converters.AddThenDivide(source, NativeOffset, NativeDivisor);
this.FromAssociatedScaledVector4Destructive(configuration, source, destination);
}
}
}

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

@ -17,14 +17,18 @@ public partial struct HalfVector4
/// </summary>
internal class PixelOperations : PixelOperations<HalfVector4>
{
private static readonly Vector4 NativeScale = new(2F);
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 />
protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan<HalfVector4> source, Span<Vector4> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
HalfVector4P.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4, HalfVector4P>(source), destination[..source.Length], false);
// The half-vector layouts are identical, so the shared expansion kernel can process the source without copying it.
RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(source), destination[..source.Length]);
}
/// <inheritdoc />
@ -34,11 +38,11 @@ public partial struct HalfVector4
destination = destination[..source.Length];
// HalfVector4P has the same four-half layout, allowing both formats to share the SIMD half expansion. Association must
// still occur in scaled space because the native half values use an affine [-1, 1] color coordinate system.
HalfVector4P.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4, HalfVector4P>(source), destination, true);
// Association uses normalized opacity, not the native binary16 alpha value.
RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(source), destination);
Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset);
Numerics.Premultiply(destination);
Vector4Converters.MultiplyThenAdd(destination, NativeScale, -Vector4.One);
Vector4Converters.MultiplyThenAdd(destination, ScaledToNativeMultiplier, ScaledToNativeOffset);
}
/// <inheritdoc />
@ -46,17 +50,16 @@ public partial struct HalfVector4
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
HalfVector4P.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4, HalfVector4P>(source), destination[..source.Length], true);
destination = destination[..source.Length];
RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(source), destination);
Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset);
}
/// <inheritdoc />
protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan<HalfVector4> source, Span<Vector4> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
destination = destination[..source.Length];
HalfVector4P.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4, HalfVector4P>(source), destination, true);
Numerics.Premultiply(destination);
this.ToUnassociatedScaledVector4(configuration, source, destination);
Numerics.Premultiply(destination[..source.Length]);
}
/// <inheritdoc />
@ -64,8 +67,8 @@ public partial struct HalfVector4
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
destination = destination[..source.Length];
HalfVector4P.PixelOperations.Pack(source, MemoryMarshal.Cast<HalfVector4, HalfVector4P>(destination), false);
// DirectX half vectors are not normalized formats, so values outside the nominal color range must reach storage unchanged.
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(destination[..source.Length]));
}
/// <inheritdoc />
@ -73,12 +76,11 @@ public partial struct HalfVector4
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
destination = destination[..source.Length];
// Normalize native alpha with RGB before unassociation, then reuse the shared half packing kernel in scaled mode.
Vector4Converters.AddThenDivide(source, Vector4.One, NativeScale);
// Restore normalized opacity before unassociating, then return the result to the native binary16 range.
Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset);
Numerics.UnPremultiply(source);
HalfVector4P.PixelOperations.Pack(source, MemoryMarshal.Cast<HalfVector4, HalfVector4P>(destination), true);
Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset);
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(destination[..source.Length]));
}
/// <inheritdoc />
@ -86,8 +88,8 @@ public partial struct HalfVector4
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
destination = destination[..source.Length];
HalfVector4P.PixelOperations.Pack(source, MemoryMarshal.Cast<HalfVector4, HalfVector4P>(destination), true);
Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset);
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(destination[..source.Length]));
}
/// <inheritdoc />
@ -95,9 +97,9 @@ public partial struct HalfVector4
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
destination = destination[..source.Length];
Numerics.UnPremultiply(source);
HalfVector4P.PixelOperations.Pack(source, MemoryMarshal.Cast<HalfVector4, HalfVector4P>(destination), true);
Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset);
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(destination[..source.Length]));
}
}
}

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

@ -6,6 +6,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.PixelFormats.Utils;
namespace SixLabors.ImageSharp.PixelFormats;
@ -19,12 +20,16 @@ public partial struct HalfVector4P
/// </summary>
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 />
protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan<HalfVector4P> source, Span<Vector4> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
UnpackUnassociated(source, destination[..source.Length], false);
this.ToUnassociatedScaledVector4(configuration, source, destination);
Vector4Converters.MultiplyThenAdd(destination[..source.Length], ScaledToNativeMultiplier, ScaledToNativeOffset);
}
/// <inheritdoc />
@ -32,41 +37,44 @@ public partial struct HalfVector4P
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
Unpack(source, destination[..source.Length], false);
RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4P, RgbaHalfP>(source), destination[..source.Length]);
}
/// <inheritdoc />
protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span<Vector4> source, Span<HalfVector4P> destination)
protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan<HalfVector4P> source, Span<Vector4> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
destination = destination[..source.Length];
PackUnassociated(source, destination, false);
this.ToAssociatedScaledVector4(configuration, source, destination);
Numerics.UnPremultiply(destination[..source.Length]);
}
/// <inheritdoc />
protected override void FromAssociatedVector4Destructive(Configuration configuration, Span<Vector4> source, Span<HalfVector4P> destination)
protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan<HalfVector4P> source, Span<Vector4> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
destination = destination[..source.Length];
PackAssociated(source, destination, false);
RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4P, RgbaHalfP>(source), destination);
Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset);
}
/// <inheritdoc />
protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan<HalfVector4P> source, Span<Vector4> destination)
protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span<Vector4> source, Span<HalfVector4P> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
UnpackUnassociated(source, destination[..source.Length], true);
Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset);
Associate(source);
PackAssociatedScaled(source, destination[..source.Length]);
}
/// <inheritdoc />
protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan<HalfVector4P> source, Span<Vector4> destination)
protected override void FromAssociatedVector4Destructive(Configuration configuration, Span<Vector4> source, Span<HalfVector4P> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
Unpack(source, destination[..source.Length], true);
Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset);
Reassociate(source);
PackAssociatedScaled(source, destination[..source.Length]);
}
/// <inheritdoc />
@ -74,8 +82,8 @@ public partial struct HalfVector4P
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
destination = destination[..source.Length];
PackUnassociated(source, destination, true);
Associate(source);
PackAssociatedScaled(source, destination[..source.Length]);
}
/// <inheritdoc />
@ -83,625 +91,197 @@ public partial struct HalfVector4P
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
destination = destination[..source.Length];
PackAssociated(source, destination, true);
Reassociate(source);
PackAssociatedScaled(source, destination[..source.Length]);
}
/// <summary>
/// Unpacks native or scaled binary16 components into vectors.
/// Associates scaled vectors with the alpha values representable by native binary16 storage.
/// </summary>
/// <param name="source">The packed source pixels.</param>
/// <param name="destination">The destination vectors.</param>
/// <param name="scaled">Whether to produce scaled vectors.</param>
internal static void Unpack(ReadOnlySpan<HalfVector4P> source, Span<Vector4> destination, bool scaled)
/// <param name="source">The vectors to convert in place.</param>
private static void Associate(Span<Vector4> source)
{
ref ushort sourceBase = ref Unsafe.As<HalfVector4P, ushort>(ref MemoryMarshal.GetReference(source));
ref float destinationBase = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(destination));
int componentCount = source.Length * Vector128<float>.Count;
ref Vector4 sourceBase = ref MemoryMarshal.GetReference(source);
int i = 0;
if (Vector512.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
{
Vector512<ushort> packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i);
(Vector512<float> lower, Vector512<float> upper) = HalfTypeHelper.Unpack(packed);
if (scaled)
{
lower = ToScaled(lower);
upper = ToScaled(upper);
}
Vector512.StoreUnsafe(lower, ref destinationBase, (nuint)i);
Vector512.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector512<float>.Count));
}
}
if (Vector256.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
{
Vector256<ushort> packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i);
(Vector256<float> lower, Vector256<float> upper) = HalfTypeHelper.Unpack(packed);
if (scaled)
{
lower = ToScaled(lower);
upper = ToScaled(upper);
}
Vector256.StoreUnsafe(lower, ref destinationBase, (nuint)i);
Vector256.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector256<float>.Count));
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
{
Vector128<ushort> packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
(Vector128<float> lower, Vector128<float> upper) = HalfTypeHelper.Unpack(packed);
int vectorsPerRegister = Vector512<float>.Count / Vector128<float>.Count;
if (scaled)
{
lower = ToScaled(lower);
upper = ToScaled(upper);
}
Vector128.StoreUnsafe(lower, ref destinationBase, (nuint)i);
Vector128.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector128<float>.Count));
}
if (i < componentCount)
for (; i <= source.Length - vectorsPerRegister; i += vectorsPerRegister)
{
// A pixel contains four halves, so the only possible remainder is one complete pixel.
ulong remainder = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref sourceBase, (uint)i)));
Vector128<ushort> packed = Vector128.CreateScalarUnsafe(remainder).AsUInt16();
Vector128<float> vector = HalfTypeHelper.Unpack(packed).Lower;
if (scaled)
{
vector = ToScaled(vector);
}
Vector128.StoreUnsafe(vector, ref destinationBase, (nuint)i);
}
return;
}
ref HalfVector4P pixelBase = ref Unsafe.As<ushort, HalfVector4P>(ref sourceBase);
ref Vector4 vectorBase = ref Unsafe.As<float, Vector4>(ref destinationBase);
for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++)
{
HalfVector4P pixel = Unsafe.Add(ref pixelBase, (uint)pixelIndex);
Unsafe.Add(ref vectorBase, (uint)pixelIndex) = scaled ? pixel.ToScaledVector4() : pixel.ToVector4();
}
}
/// <summary>
/// Unpacks associated binary16 storage directly into unassociated native or scaled vectors.
/// </summary>
/// <param name="source">The packed source pixels.</param>
/// <param name="destination">The destination vectors.</param>
/// <param name="scaled">Whether to produce scaled vectors.</param>
private static void UnpackUnassociated(ReadOnlySpan<HalfVector4P> source, Span<Vector4> destination, bool scaled)
{
ref ushort sourceBase = ref Unsafe.As<HalfVector4P, ushort>(ref MemoryMarshal.GetReference(source));
ref float destinationBase = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(destination));
int componentCount = source.Length * Vector128<float>.Count;
int i = 0;
if (Vector512.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
{
Vector512<ushort> packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i);
(Vector512<float> lower, Vector512<float> upper) = HalfTypeHelper.Unpack(packed);
Vector512.StoreUnsafe(ToUnassociated(lower, scaled), ref destinationBase, (nuint)i);
Vector512.StoreUnsafe(ToUnassociated(upper, scaled), ref destinationBase, (nuint)(i + Vector512<float>.Count));
ref Vector512<float> vector = ref Unsafe.As<Vector4, Vector512<float>>(ref Unsafe.Add(ref sourceBase, (uint)i));
vector = Associate(vector);
}
}
if (Vector256.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
{
Vector256<ushort> packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i);
(Vector256<float> lower, Vector256<float> upper) = HalfTypeHelper.Unpack(packed);
Vector256.StoreUnsafe(ToUnassociated(lower, scaled), ref destinationBase, (nuint)i);
Vector256.StoreUnsafe(ToUnassociated(upper, scaled), ref destinationBase, (nuint)(i + Vector256<float>.Count));
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
{
Vector128<ushort> packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
(Vector128<float> lower, Vector128<float> upper) = HalfTypeHelper.Unpack(packed);
Vector128.StoreUnsafe(ToUnassociated(lower, scaled), ref destinationBase, (nuint)i);
Vector128.StoreUnsafe(ToUnassociated(upper, scaled), ref destinationBase, (nuint)(i + Vector128<float>.Count));
}
int vectorsPerRegister = Vector256<float>.Count / Vector128<float>.Count;
if (i < componentCount)
for (; i <= source.Length - vectorsPerRegister; i += vectorsPerRegister)
{
// A pixel contains four halves, so the only possible remainder is one complete pixel.
ulong remainder = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref sourceBase, (uint)i)));
Vector128<ushort> packed = Vector128.CreateScalarUnsafe(remainder).AsUInt16();
Vector128<float> vector = HalfTypeHelper.Unpack(packed).Lower;
Vector128.StoreUnsafe(ToUnassociated(vector, scaled), ref destinationBase, (nuint)i);
}
return;
}
ref HalfVector4P pixelBase = ref Unsafe.As<ushort, HalfVector4P>(ref sourceBase);
ref Vector4 vectorBase = ref Unsafe.As<float, Vector4>(ref destinationBase);
for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++)
{
HalfVector4P pixel = Unsafe.Add(ref pixelBase, (uint)pixelIndex);
Unsafe.Add(ref vectorBase, (uint)pixelIndex) = scaled ? pixel.ToUnassociatedScaledVector4() : pixel.ToUnassociatedVector4();
}
}
/// <summary>
/// Packs native or scaled vectors into binary16 storage.
/// </summary>
/// <param name="source">The source vectors.</param>
/// <param name="destination">The destination pixels.</param>
/// <param name="scaled">Whether the source contains scaled vectors.</param>
internal static void Pack(Span<Vector4> source, Span<HalfVector4P> destination, bool scaled)
{
ref float sourceBase = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(source));
ref ushort destinationBase = ref Unsafe.As<HalfVector4P, ushort>(ref MemoryMarshal.GetReference(destination));
int componentCount = source.Length * Vector128<float>.Count;
int i = 0;
if (Vector512.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
{
Vector512<float> lower = Vector512.LoadUnsafe(ref sourceBase, (nuint)i);
Vector512<float> upper = Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<float>.Count));
Vector512<ushort> packed = scaled ? PackScaled(lower, upper) : HalfTypeHelper.Pack(lower, upper);
Vector512.StoreUnsafe(packed, ref destinationBase, (nuint)i);
}
}
if (Vector256.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
{
Vector256<float> lower = Vector256.LoadUnsafe(ref sourceBase, (nuint)i);
Vector256<float> upper = Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<float>.Count));
Vector256<ushort> packed = scaled ? PackScaled(lower, upper) : HalfTypeHelper.Pack(lower, upper);
Vector256.StoreUnsafe(packed, ref destinationBase, (nuint)i);
ref Vector256<float> vector = ref Unsafe.As<Vector4, Vector256<float>>(ref Unsafe.Add(ref sourceBase, (uint)i));
vector = Associate(vector);
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
for (; i < source.Length; i++)
{
Vector128<float> lower = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
Vector128<float> upper = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<float>.Count));
Vector128<ushort> packed = scaled ? PackScaled(lower, upper) : HalfTypeHelper.Pack(lower, upper);
Vector128.StoreUnsafe(packed, ref destinationBase, (nuint)i);
}
if (i < componentCount)
{
// Duplicate the final vector to use the two-input narrowing primitive, then store only its lower pixel.
Vector128<float> vector = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
Vector128<ushort> packed = scaled ? PackScaled(vector, vector) : HalfTypeHelper.Pack(vector, vector);
Unsafe.WriteUnaligned(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0));
ref Vector128<float> vector = ref Unsafe.As<Vector4, Vector128<float>>(ref Unsafe.Add(ref sourceBase, (uint)i));
vector = Associate(vector);
}
return;
}
ref Vector4 vectorBase = ref Unsafe.As<float, Vector4>(ref sourceBase);
ref HalfVector4P pixelBase = ref Unsafe.As<ushort, HalfVector4P>(ref destinationBase);
for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++)
for (; i < source.Length; i++)
{
Vector4 vector = Unsafe.Add(ref vectorBase, (uint)pixelIndex);
Unsafe.Add(ref pixelBase, (uint)pixelIndex) = scaled ? FromScaledVector4(vector) : FromVector4(vector);
Unsafe.Add(ref sourceBase, (uint)i) = HalfVector4P.Associate(Unsafe.Add(ref sourceBase, (uint)i));
}
}
/// <summary>
/// Associates unassociated native or scaled vectors and packs them into binary16 storage in one pass.
/// Reassociates scaled vectors with the alpha values representable by native binary16 storage.
/// </summary>
/// <param name="source">The unassociated vectors.</param>
/// <param name="destination">The destination pixels.</param>
/// <param name="scaled">Whether the source contains scaled vectors.</param>
private static void PackUnassociated(Span<Vector4> source, Span<HalfVector4P> destination, bool scaled)
/// <param name="source">The vectors to convert in place.</param>
private static void Reassociate(Span<Vector4> source)
{
ref float sourceBase = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(source));
ref ushort destinationBase = ref Unsafe.As<HalfVector4P, ushort>(ref MemoryMarshal.GetReference(destination));
int componentCount = source.Length * Vector128<float>.Count;
ref Vector4 sourceBase = ref MemoryMarshal.GetReference(source);
int i = 0;
// Quantize alpha to the binary16 value the destination will store before associating RGB. The kernels replace W separately
// so alpha is stored unchanged instead of being multiplied by itself.
if (Vector512.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
{
Vector512<float> lower = Associate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i), scaled);
Vector512<float> upper = Associate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<float>.Count)), scaled);
Vector512.StoreUnsafe(PackScaled(lower, upper), ref destinationBase, (nuint)i);
}
}
if (Vector256.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
{
Vector256<float> lower = Associate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i), scaled);
Vector256<float> upper = Associate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<float>.Count)), scaled);
Vector256.StoreUnsafe(PackScaled(lower, upper), ref destinationBase, (nuint)i);
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
{
Vector128<float> lower = Associate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), scaled);
Vector128<float> upper = Associate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<float>.Count)), scaled);
Vector128.StoreUnsafe(PackScaled(lower, upper), ref destinationBase, (nuint)i);
}
if (i < componentCount)
{
// Duplicate the final vector to use the two-input narrowing primitive, then store only its lower pixel.
Vector128<float> vector = Associate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), scaled);
Vector128<ushort> packed = PackScaled(vector, vector);
Unsafe.WriteUnaligned(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0));
}
return;
}
ref Vector4 vectorBase = ref Unsafe.As<float, Vector4>(ref sourceBase);
ref HalfVector4P pixelBase = ref Unsafe.As<ushort, HalfVector4P>(ref destinationBase);
for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++)
{
Vector4 vector = Unsafe.Add(ref vectorBase, (uint)pixelIndex);
// Qualify the containing pixel type because this nested class also declares bulk overloads with these names.
Unsafe.Add(ref pixelBase, (uint)pixelIndex) = scaled ? HalfVector4P.FromUnassociatedScaledVector4(vector) : HalfVector4P.FromUnassociatedVector4(vector);
}
}
int vectorsPerRegister = Vector512<float>.Count / Vector128<float>.Count;
/// <summary>
/// Reassociates associated native or scaled vectors and packs them into binary16 storage in one pass.
/// </summary>
/// <param name="source">The associated vectors.</param>
/// <param name="destination">The destination pixels.</param>
/// <param name="scaled">Whether the source contains scaled vectors.</param>
private static void PackAssociated(Span<Vector4> source, Span<HalfVector4P> destination, bool scaled)
{
ref float sourceBase = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(source));
ref ushort destinationBase = ref Unsafe.As<HalfVector4P, ushort>(ref MemoryMarshal.GetReference(destination));
int componentCount = source.Length * Vector128<float>.Count;
int i = 0;
// Preserve straight color across binary16 alpha quantization by scaling RGB by storedAlpha / inputAlpha. The kernels replace
// W, clamp RGB to stored alpha, and clear zero-alpha vectors so the packed result remains valid associated color.
if (Vector512.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
for (; i <= source.Length - vectorsPerRegister; i += vectorsPerRegister)
{
Vector512<float> lower = Reassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i), scaled);
Vector512<float> upper = Reassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<float>.Count)), scaled);
Vector512.StoreUnsafe(PackScaled(lower, upper), ref destinationBase, (nuint)i);
ref Vector512<float> vector = ref Unsafe.As<Vector4, Vector512<float>>(ref Unsafe.Add(ref sourceBase, (uint)i));
vector = Reassociate(vector);
}
}
if (Vector256.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
int vectorsPerRegister = Vector256<float>.Count / Vector128<float>.Count;
for (; i <= source.Length - vectorsPerRegister; i += vectorsPerRegister)
{
Vector256<float> lower = Reassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i), scaled);
Vector256<float> upper = Reassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<float>.Count)), scaled);
Vector256.StoreUnsafe(PackScaled(lower, upper), ref destinationBase, (nuint)i);
ref Vector256<float> vector = ref Unsafe.As<Vector4, Vector256<float>>(ref Unsafe.Add(ref sourceBase, (uint)i));
vector = Reassociate(vector);
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
{
Vector128<float> lower = Reassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), scaled);
Vector128<float> upper = Reassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<float>.Count)), scaled);
Vector128.StoreUnsafe(PackScaled(lower, upper), ref destinationBase, (nuint)i);
}
if (i < componentCount)
for (; i < source.Length; i++)
{
// Duplicate the final vector to use the two-input narrowing primitive, then store only its lower pixel.
Vector128<float> vector = Reassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), scaled);
Vector128<ushort> packed = PackScaled(vector, vector);
Unsafe.WriteUnaligned(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0));
ref Vector128<float> vector = ref Unsafe.As<Vector4, Vector128<float>>(ref Unsafe.Add(ref sourceBase, (uint)i));
vector = Reassociate(vector);
}
return;
}
ref Vector4 vectorBase = ref Unsafe.As<float, Vector4>(ref sourceBase);
ref HalfVector4P pixelBase = ref Unsafe.As<ushort, HalfVector4P>(ref destinationBase);
for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++)
for (; i < source.Length; i++)
{
Vector4 vector = Unsafe.Add(ref vectorBase, (uint)pixelIndex);
// Qualify the containing pixel type because this nested class also declares bulk overloads with these names.
Unsafe.Add(ref pixelBase, (uint)pixelIndex) = scaled ? HalfVector4P.FromAssociatedScaledVector4(vector) : HalfVector4P.FromAssociatedVector4(vector);
Unsafe.Add(ref sourceBase, (uint)i) = HalfVector4P.Reassociate(Unsafe.Add(ref sourceBase, (uint)i));
}
}
/// <summary>
/// Converts native binary16 values to the normalized range used by pixel conversions.
/// </summary>
/// <param name="source">The native values.</param>
/// <returns>The scaled values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> ToScaled(Vector128<float> source) => (source + Vector128<float>.One) / Vector128.Create(2F);
/// <summary>
/// Converts native binary16 values to the normalized range used by pixel conversions.
/// </summary>
/// <param name="source">The native values.</param>
/// <returns>The scaled values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> ToScaled(Vector256<float> source) => (source + Vector256<float>.One) / Vector256.Create(2F);
/// <summary>
/// Converts native binary16 values to the normalized range used by pixel conversions.
/// </summary>
/// <param name="source">The native values.</param>
/// <returns>The scaled values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> ToScaled(Vector512<float> source) => (source + Vector512<float>.One) / Vector512.Create(2F);
/// <summary>
/// Converts associated native binary16 values to unassociated native or scaled vectors.
/// </summary>
/// <param name="source">The associated native values.</param>
/// <param name="scaled">Whether to produce scaled vectors.</param>
/// <returns>The unassociated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> ToUnassociated(Vector128<float> source, bool scaled)
{
source = ToScaled(source);
Vector128<float> alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11);
Vector128<float> result = Vector128.ConditionalSelect(Vector128.Equals(alpha, Vector128<float>.Zero), source, source / alpha);
Vector128<float> alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle();
result = Vector128.ConditionalSelect(alphaMask, alpha, result);
// Binary16-native W is an affine encoding rather than opacity, so map back only after unassociation in scaled space.
return scaled ? result : (result * Vector128.Create(2F)) - Vector128<float>.One;
}
/// <summary>
/// Converts associated native binary16 values to unassociated native or scaled vectors.
/// </summary>
/// <param name="source">The associated native values.</param>
/// <param name="scaled">Whether to produce scaled vectors.</param>
/// <returns>The unassociated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> ToUnassociated(Vector256<float> source, bool scaled)
{
source = ToScaled(source);
Vector256<float> alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11);
Vector256<float> result = Vector256.ConditionalSelect(Vector256.Equals(alpha, Vector256<float>.Zero), source, source / alpha);
Vector256<float> alphaMask = Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle();
result = Vector256.ConditionalSelect(alphaMask, alpha, result);
// Binary16-native W is an affine encoding rather than opacity, so map back only after unassociation in scaled space.
return scaled ? result : (result * Vector256.Create(2F)) - Vector256<float>.One;
}
/// <summary>
/// Converts associated native binary16 values to unassociated native or scaled vectors.
/// </summary>
/// <param name="source">The associated native values.</param>
/// <param name="scaled">Whether to produce scaled vectors.</param>
/// <returns>The unassociated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> ToUnassociated(Vector512<float> source, bool scaled)
{
source = ToScaled(source);
Vector512<float> alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11);
Vector512<float> result = Vector512.ConditionalSelect(Vector512.Equals(alpha, Vector512<float>.Zero), source, source / alpha);
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, alpha, result);
// Binary16-native W is an affine encoding rather than opacity, so map back only after unassociation in scaled space.
return scaled ? result : (result * Vector512.Create(2F)) - Vector512<float>.One;
}
/// <summary>
/// Converts unassociated native or scaled vectors to the destination's associated scaled representation.
/// Converts an unassociated scaled vector to associated scaled components.
/// </summary>
/// <param name="source">The unassociated vectors.</param>
/// <param name="scaled">Whether the source contains scaled vectors.</param>
/// <returns>The associated scaled vectors.</returns>
/// <returns>The associated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> Associate(Vector128<float> source, bool scaled)
private static Vector128<float> Associate(Vector128<float> source)
{
Vector128<float> zero = Vector128<float>.Zero;
Vector128<float> one = Vector128<float>.One;
if (!scaled)
{
// Binary16-native W is an affine encoding rather than opacity, so association must happen after mapping to scaled space.
source = ToScaled(source);
}
source = Vector128.Min(Vector128.Max(source, zero), one);
source = ClampUnit(source);
Vector128<float> alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11);
Vector128<float> storedAlpha = (HalfTypeHelper.RoundToHalf((alpha * Vector128.Create(2F)) - one) + one) / Vector128.Create(2F);
Vector128<float> storedAlpha = QuantizeScaledAlpha(alpha);
Vector128<float> result = source * storedAlpha;
Vector128<float> alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle();
return Vector128.ConditionalSelect(alphaMask, storedAlpha, result);
return Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result);
}
/// <summary>
/// Converts unassociated native or scaled vectors to the destination's associated scaled representation.
/// Converts unassociated scaled vectors to associated scaled components.
/// </summary>
/// <param name="source">The unassociated vectors.</param>
/// <param name="scaled">Whether the source contains scaled vectors.</param>
/// <returns>The associated scaled vectors.</returns>
/// <returns>The associated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> Associate(Vector256<float> source, bool scaled)
private static Vector256<float> Associate(Vector256<float> source)
{
Vector256<float> zero = Vector256<float>.Zero;
Vector256<float> one = Vector256<float>.One;
if (!scaled)
{
// Binary16-native W is an affine encoding rather than opacity, so association must happen after mapping to scaled space.
source = ToScaled(source);
}
source = Vector256.Min(Vector256.Max(source, zero), one);
source = ClampUnit(source);
Vector256<float> alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11);
Vector256<float> storedAlpha = (HalfTypeHelper.RoundToHalf((alpha * Vector256.Create(2F)) - one) + one) / Vector256.Create(2F);
Vector256<float> storedAlpha = QuantizeScaledAlpha(alpha);
Vector256<float> result = source * storedAlpha;
Vector256<float> alphaMask = Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle();
return Vector256.ConditionalSelect(alphaMask, storedAlpha, result);
return Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result);
}
/// <summary>
/// Converts unassociated native or scaled vectors to the destination's associated scaled representation.
/// Converts unassociated scaled vectors to associated scaled components.
/// </summary>
/// <param name="source">The unassociated vectors.</param>
/// <param name="scaled">Whether the source contains scaled vectors.</param>
/// <returns>The associated scaled vectors.</returns>
/// <returns>The associated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> Associate(Vector512<float> source, bool scaled)
private static Vector512<float> Associate(Vector512<float> source)
{
Vector512<float> zero = Vector512<float>.Zero;
Vector512<float> one = Vector512<float>.One;
if (!scaled)
{
// Binary16-native W is an affine encoding rather than opacity, so association must happen after mapping to scaled space.
source = ToScaled(source);
}
source = Vector512.Min(Vector512.Max(source, zero), one);
source = ClampUnit(source);
Vector512<float> alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11);
Vector512<float> storedAlpha = (HalfTypeHelper.RoundToHalf((alpha * Vector512.Create(2F)) - one) + one) / Vector512.Create(2F);
Vector512<float> storedAlpha = QuantizeScaledAlpha(alpha);
Vector512<float> result = source * storedAlpha;
Vector512<float> alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle();
return Vector512.ConditionalSelect(alphaMask, storedAlpha, result);
}
/// <summary>
/// Reassociates native or scaled vectors with the scaled alpha values the destination stores.
/// Reassociates an associated scaled vector after alpha quantization.
/// </summary>
/// <param name="source">The associated vectors.</param>
/// <param name="scaled">Whether the source contains scaled vectors.</param>
/// <returns>The reassociated scaled vectors.</returns>
/// <returns>The reassociated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> Reassociate(Vector128<float> source, bool scaled)
private static Vector128<float> Reassociate(Vector128<float> source)
{
Vector128<float> zero = Vector128<float>.Zero;
Vector128<float> one = Vector128<float>.One;
if (!scaled)
{
// Convert every component together so RGB and alpha enter reassociation in the same scaled coordinate system.
source = ToScaled(source);
}
Vector128<float> alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11);
Vector128<float> nativeAlpha = (alpha * Vector128.Create(2F)) - one;
Vector128<float> clampedNativeAlpha = Vector128.Min(Vector128.Max(nativeAlpha, -one), one);
// Hardware Min/Max replace NaN on .NET 8, whereas the scalar clamp preserves it. Restore those lanes before half quantization.
nativeAlpha = Vector128.ConditionalSelect(Vector128.Equals(nativeAlpha, nativeAlpha), clampedNativeAlpha, nativeAlpha);
// Clamp before binary16 quantization because the scaled alpha contract cannot represent opacity outside [0, 1].
Vector128<float> storedAlpha = (HalfTypeHelper.RoundToHalf(nativeAlpha) + one) / Vector128.Create(2F);
Vector128<float> storedAlpha = QuantizeScaledAlpha(alpha);
Vector128<float> result = source * (storedAlpha / alpha);
Vector128<float> alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle();
result = Vector128.ConditionalSelect(alphaMask, storedAlpha, result);
result = Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result);
result = Vector128.Min(Vector128.Max(result, zero), storedAlpha);
return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(alpha, zero), zero, result);
}
/// <summary>
/// Reassociates native or scaled vectors with the scaled alpha values the destination stores.
/// Reassociates associated scaled vectors after alpha quantization.
/// </summary>
/// <param name="source">The associated vectors.</param>
/// <param name="scaled">Whether the source contains scaled vectors.</param>
/// <returns>The reassociated scaled vectors.</returns>
/// <returns>The reassociated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> Reassociate(Vector256<float> source, bool scaled)
private static Vector256<float> Reassociate(Vector256<float> source)
{
Vector256<float> zero = Vector256<float>.Zero;
Vector256<float> one = Vector256<float>.One;
if (!scaled)
{
// Convert every component together so RGB and alpha enter reassociation in the same scaled coordinate system.
source = ToScaled(source);
}
Vector256<float> alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11);
Vector256<float> nativeAlpha = (alpha * Vector256.Create(2F)) - one;
Vector256<float> clampedNativeAlpha = Vector256.Min(Vector256.Max(nativeAlpha, -one), one);
// Hardware Min/Max replace NaN on .NET 8, whereas the scalar clamp preserves it. Restore those lanes before half quantization.
nativeAlpha = Vector256.ConditionalSelect(Vector256.Equals(nativeAlpha, nativeAlpha), clampedNativeAlpha, nativeAlpha);
// Clamp before binary16 quantization because the scaled alpha contract cannot represent opacity outside [0, 1].
Vector256<float> storedAlpha = (HalfTypeHelper.RoundToHalf(nativeAlpha) + one) / Vector256.Create(2F);
Vector256<float> storedAlpha = QuantizeScaledAlpha(alpha);
Vector256<float> result = source * (storedAlpha / alpha);
Vector256<float> alphaMask = Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle();
result = Vector256.ConditionalSelect(alphaMask, 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);
return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(alpha, zero), zero, result);
}
/// <summary>
/// Reassociates native or scaled vectors with the scaled alpha values the destination stores.
/// Reassociates associated scaled vectors after alpha quantization.
/// </summary>
/// <param name="source">The associated vectors.</param>
/// <param name="scaled">Whether the source contains scaled vectors.</param>
/// <returns>The reassociated scaled vectors.</returns>
/// <returns>The reassociated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> Reassociate(Vector512<float> source, bool scaled)
private static Vector512<float> Reassociate(Vector512<float> source)
{
Vector512<float> zero = Vector512<float>.Zero;
Vector512<float> one = Vector512<float>.One;
if (!scaled)
{
// Convert every component together so RGB and alpha enter reassociation in the same scaled coordinate system.
source = ToScaled(source);
}
Vector512<float> alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11);
Vector512<float> nativeAlpha = (alpha * Vector512.Create(2F)) - one;
Vector512<float> clampedNativeAlpha = Vector512.Min(Vector512.Max(nativeAlpha, -one), one);
// Hardware Min/Max replace NaN on .NET 8, whereas the scalar clamp preserves it. Restore those lanes before half quantization.
nativeAlpha = Vector512.ConditionalSelect(Vector512.Equals(nativeAlpha, nativeAlpha), clampedNativeAlpha, nativeAlpha);
// Clamp before binary16 quantization because the scaled alpha contract cannot represent opacity outside [0, 1].
Vector512<float> storedAlpha = (HalfTypeHelper.RoundToHalf(nativeAlpha) + one) / Vector512.Create(2F);
Vector512<float> storedAlpha = QuantizeScaledAlpha(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();
result = Vector512.ConditionalSelect(alphaMask, storedAlpha, result);
@ -710,51 +290,86 @@ public partial struct HalfVector4P
}
/// <summary>
/// Converts two scaled vectors to binary16 storage while preserving scalar operation order.
/// Quantizes scaled alpha through the native binary16 representation.
/// </summary>
/// <param name="alpha">The scaled alpha lanes.</param>
/// <returns>The scaled alpha values represented by binary16 storage.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> QuantizeScaledAlpha(Vector128<float> alpha)
{
Vector128<float> native = (ClampUnit(alpha) * Vector128.Create(HalfTypeHelper.FiniteRange)) + Vector128.Create(HalfTypeHelper.FiniteMinimum);
return (HalfTypeHelper.RoundToHalf(native) * Vector128.Create(HalfTypeHelper.InverseFiniteRange)) + Vector128.Create(HalfTypeHelper.ScaledMidpoint);
}
/// <summary>
/// Quantizes scaled alpha through the native binary16 representation.
/// </summary>
/// <param name="alpha">The scaled alpha lanes.</param>
/// <returns>The scaled alpha values represented by binary16 storage.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> QuantizeScaledAlpha(Vector256<float> alpha)
{
Vector256<float> native = (ClampUnit(alpha) * Vector256.Create(HalfTypeHelper.FiniteRange)) + Vector256.Create(HalfTypeHelper.FiniteMinimum);
return (HalfTypeHelper.RoundToHalf(native) * Vector256.Create(HalfTypeHelper.InverseFiniteRange)) + Vector256.Create(HalfTypeHelper.ScaledMidpoint);
}
/// <summary>
/// Quantizes scaled alpha through the native binary16 representation.
/// </summary>
/// <param name="alpha">The scaled alpha lanes.</param>
/// <returns>The scaled alpha values represented by binary16 storage.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> QuantizeScaledAlpha(Vector512<float> alpha)
{
Vector512<float> native = (ClampUnit(alpha) * Vector512.Create(HalfTypeHelper.FiniteRange)) + Vector512.Create(HalfTypeHelper.FiniteMinimum);
return (HalfTypeHelper.RoundToHalf(native) * Vector512.Create(HalfTypeHelper.InverseFiniteRange)) + Vector512.Create(HalfTypeHelper.ScaledMidpoint);
}
/// <summary>
/// Clamps vectors to the scaled color range while preserving NaN lanes.
/// </summary>
/// <param name="lower">The lower scaled values.</param>
/// <param name="upper">The upper scaled values.</param>
/// <returns>The packed binary16 values.</returns>
/// <param name="source">The vectors to clamp.</param>
/// <returns>The clamped vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<ushort> PackScaled(Vector128<float> lower, Vector128<float> upper)
private static Vector128<float> ClampUnit(Vector128<float> source)
{
lower *= Vector128.Create(2F);
lower -= Vector128<float>.One;
upper *= Vector128.Create(2F);
upper -= Vector128<float>.One;
return HalfTypeHelper.Pack(lower, upper);
Vector128<float> clamped = Vector128.Min(Vector128.Max(source, Vector128<float>.Zero), Vector128<float>.One);
return Vector128.ConditionalSelect(Vector128.Equals(source, source), clamped, source);
}
/// <summary>
/// Converts two scaled vectors to binary16 storage while preserving scalar operation order.
/// Clamps vectors to the scaled color range while preserving NaN lanes.
/// </summary>
/// <param name="lower">The lower scaled values.</param>
/// <param name="upper">The upper scaled values.</param>
/// <returns>The packed binary16 values.</returns>
/// <param name="source">The vectors to clamp.</param>
/// <returns>The clamped vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<ushort> PackScaled(Vector256<float> lower, Vector256<float> upper)
private static Vector256<float> ClampUnit(Vector256<float> source)
{
lower *= Vector256.Create(2F);
lower -= Vector256<float>.One;
upper *= Vector256.Create(2F);
upper -= Vector256<float>.One;
return HalfTypeHelper.Pack(lower, upper);
Vector256<float> clamped = Vector256.Min(Vector256.Max(source, Vector256<float>.Zero), Vector256<float>.One);
return Vector256.ConditionalSelect(Vector256.Equals(source, source), clamped, source);
}
/// <summary>
/// Converts two scaled vectors to binary16 storage while preserving scalar operation order.
/// Clamps vectors to the scaled color range while preserving NaN lanes.
/// </summary>
/// <param name="lower">The lower scaled values.</param>
/// <param name="upper">The upper scaled values.</param>
/// <returns>The packed binary16 values.</returns>
/// <param name="source">The vectors to clamp.</param>
/// <returns>The clamped vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<ushort> PackScaled(Vector512<float> lower, Vector512<float> upper)
private static Vector512<float> ClampUnit(Vector512<float> source)
{
Vector512<float> clamped = Vector512.Min(Vector512.Max(source, Vector512<float>.Zero), Vector512<float>.One);
return Vector512.ConditionalSelect(Vector512.Equals(source, source), clamped, source);
}
/// <summary>
/// Maps associated scaled vectors to native components and packs them as binary16 values.
/// </summary>
/// <param name="source">The associated scaled vectors.</param>
/// <param name="destination">The destination pixels.</param>
private static void PackAssociatedScaled(Span<Vector4> source, Span<HalfVector4P> destination)
{
lower *= Vector512.Create(2F);
lower -= Vector512<float>.One;
upper *= Vector512.Create(2F);
upper -= Vector512<float>.One;
return HalfTypeHelper.Pack(lower, upper);
Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset);
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4P, RgbaHalfP>(destination));
}
}
}

66
src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4P.PixelOperations.cs

@ -20,6 +20,8 @@ public partial struct NormalizedByte4P
/// </summary>
internal class PixelOperations : AssociatedAlphaPixelOperations<NormalizedByte4P>
{
private const byte RestorePackedPixelOrder = 0b_11_01_10_00;
/// <inheritdoc />
protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan<NormalizedByte4P> source, Span<Vector4> destination)
{
@ -107,27 +109,33 @@ public partial struct NormalizedByte4P
int componentsPerPixel = Vector128<float>.Count;
int i = 0;
if (Vector512.IsHardwareAccelerated && Avx512F.IsSupported)
// Portable widening advances one integer width at a time, so assemble the wider vectors from ordered 128-bit halves.
if (Vector512.IsHardwareAccelerated)
{
int pixelsPerVector = Vector512<float>.Count / Vector128<float>.Count;
for (; i <= source.Length - pixelsPerVector; i += pixelsPerVector)
{
Vector128<sbyte> packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i * componentsPerPixel)).AsSByte();
Vector512<int> integers = Avx512F.ConvertToVector512Int32(packed);
Unsafe.As<Vector4, Vector512<float>>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToVector4(Avx512F.ConvertToVector512Single(integers), scaled);
Vector128<short> lowerShorts = Vector128.WidenLower(packed);
Vector128<short> upperShorts = Vector128.WidenUpper(packed);
Vector256<int> lowerIntegers = Vector256.Create(Vector128.WidenLower(lowerShorts), Vector128.WidenUpper(lowerShorts));
Vector256<int> upperIntegers = Vector256.Create(Vector128.WidenLower(upperShorts), Vector128.WidenUpper(upperShorts));
Vector512<int> integers = Vector512.Create(lowerIntegers, upperIntegers);
Unsafe.As<Vector4, Vector512<float>>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToVector4(Vector512.ConvertToSingle(integers), scaled);
}
}
if (Avx2.IsSupported)
if (Vector256.IsHardwareAccelerated)
{
int pixelsPerVector = Vector256<float>.Count / Vector128<float>.Count;
for (; i <= source.Length - pixelsPerVector; i += pixelsPerVector)
{
ulong packed = Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref sourceBase, (uint)(i * componentsPerPixel)));
Vector256<int> integers = Avx2.ConvertToVector256Int32(Vector128.CreateScalarUnsafe(packed).AsSByte());
Unsafe.As<Vector4, Vector256<float>>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToVector4(Avx.ConvertToVector256Single(integers), scaled);
Vector128<short> shorts = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsSByte());
Vector256<int> integers = Vector256.Create(Vector128.WidenLower(shorts), Vector128.WidenUpper(shorts));
Unsafe.As<Vector4, Vector256<float>>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToVector4(Vector256.ConvertToSingle(integers), scaled);
}
}
@ -162,6 +170,9 @@ public partial struct NormalizedByte4P
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> ToVector4(Vector512<float> source, bool scaled)
{
// Both minimum two's-complement SNORM encodings represent -1.
source = Vector512.Max(source, Vector512.Create(-MaxPos));
if (scaled)
{
// Offset exact integer components before division to avoid cancellation near the signed-normalized lower bound.
@ -180,6 +191,9 @@ public partial struct NormalizedByte4P
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> ToVector4(Vector256<float> source, bool scaled)
{
// Both minimum two's-complement SNORM encodings represent -1.
source = Vector256.Max(source, Vector256.Create(-MaxPos));
if (scaled)
{
// Offset exact integer components before division to avoid cancellation near the signed-normalized lower bound.
@ -198,6 +212,9 @@ public partial struct NormalizedByte4P
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> ToVector4(Vector128<float> source, bool scaled)
{
// Both minimum two's-complement SNORM encodings represent -1.
source = Vector128.Max(source, Vector128.Create(-MaxPos));
if (scaled)
{
// Offset exact integer components before division to avoid cancellation near the signed-normalized lower bound.
@ -222,27 +239,33 @@ public partial struct NormalizedByte4P
int componentsPerPixel = Vector128<float>.Count;
int i = 0;
if (Vector512.IsHardwareAccelerated && Avx512F.IsSupported)
// Portable widening advances one integer width at a time, so assemble the wider vectors from ordered 128-bit halves.
if (Vector512.IsHardwareAccelerated)
{
int pixelsPerVector = Vector512<float>.Count / Vector128<float>.Count;
for (; i <= source.Length - pixelsPerVector; i += pixelsPerVector)
{
Vector128<sbyte> packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i * componentsPerPixel)).AsSByte();
Vector512<int> integers = Avx512F.ConvertToVector512Int32(packed);
Unsafe.As<Vector4, Vector512<float>>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Avx512F.ConvertToVector512Single(integers), scaled);
Vector128<short> lowerShorts = Vector128.WidenLower(packed);
Vector128<short> upperShorts = Vector128.WidenUpper(packed);
Vector256<int> lowerIntegers = Vector256.Create(Vector128.WidenLower(lowerShorts), Vector128.WidenUpper(lowerShorts));
Vector256<int> upperIntegers = Vector256.Create(Vector128.WidenLower(upperShorts), Vector128.WidenUpper(upperShorts));
Vector512<int> integers = Vector512.Create(lowerIntegers, upperIntegers);
Unsafe.As<Vector4, Vector512<float>>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Vector512.ConvertToSingle(integers), scaled);
}
}
if (Avx2.IsSupported)
if (Vector256.IsHardwareAccelerated)
{
int pixelsPerVector = Vector256<float>.Count / Vector128<float>.Count;
for (; i <= source.Length - pixelsPerVector; i += pixelsPerVector)
{
ulong packed = Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref sourceBase, (uint)(i * componentsPerPixel)));
Vector256<int> integers = Avx2.ConvertToVector256Int32(Vector128.CreateScalarUnsafe(packed).AsSByte());
Unsafe.As<Vector4, Vector256<float>>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Avx.ConvertToVector256Single(integers), scaled);
Vector128<short> shorts = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsSByte());
Vector256<int> integers = Vector256.Create(Vector128.WidenLower(shorts), Vector128.WidenUpper(shorts));
Unsafe.As<Vector4, Vector256<float>>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Vector256.ConvertToSingle(integers), scaled);
}
}
@ -277,6 +300,8 @@ public partial struct NormalizedByte4P
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> ToUnassociatedVector4(Vector512<float> source, bool scaled)
{
// Clamp the duplicate SNORM minimum encoding before converting it to the nonnegative associated domain.
source = Vector512.Max(source, Vector512.Create(-MaxPos));
source += Vector512.Create(MaxPos);
Vector512<float> alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11);
Vector512<float> alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle();
@ -297,6 +322,8 @@ public partial struct NormalizedByte4P
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> ToUnassociatedVector4(Vector256<float> source, bool scaled)
{
// Clamp the duplicate SNORM minimum encoding before converting it to the nonnegative associated domain.
source = Vector256.Max(source, Vector256.Create(-MaxPos));
source += Vector256.Create(MaxPos);
Vector256<float> alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11);
Vector256<float> alphaMask = Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle();
@ -317,6 +344,8 @@ public partial struct NormalizedByte4P
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> ToUnassociatedVector4(Vector128<float> source, bool scaled)
{
// Clamp the duplicate SNORM minimum encoding before converting it to the nonnegative associated domain.
source = Vector128.Max(source, Vector128.Create(-MaxPos));
source += Vector128.Create(MaxPos);
Vector128<float> alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11);
Vector128<float> alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle();
@ -663,14 +692,23 @@ public partial struct NormalizedByte4P
ref NormalizedByte4P destinationBase = ref MemoryMarshal.GetReference(destination);
int i = 0;
if (Vector512.IsHardwareAccelerated && Avx512F.IsSupported)
if (Vector512.IsHardwareAccelerated)
{
int pixelsPerVector = Vector512<float>.Count / Vector128<float>.Count;
for (; i <= source.Length - pixelsPerVector; i += pixelsPerVector)
{
Vector512<float> vector = Unsafe.As<Vector4, Vector512<float>>(ref Unsafe.Add(ref sourceBase, (uint)i));
Vector128<sbyte> packed = Avx512F.ConvertToVector128SByteWithSaturation(ConvertToPackedInt32(vector, scaled));
Vector512<int> integers = ConvertToPackedInt32(vector, scaled);
Vector256<short> shorts = Vector256_.PackSignedSaturate(integers.GetLower(), integers.GetUpper());
Vector128<sbyte> packed = Vector128_.PackSignedSaturate(shorts.GetLower(), shorts.GetUpper());
if (Avx2.IsSupported)
{
// AVX2 narrows each 128-bit lane independently, placing the middle two pixels out of order.
packed = Vector128_.ShuffleNative(packed.AsSingle(), RestorePackedPixelOrder).AsSByte();
}
Unsafe.As<NormalizedByte4P, Vector128<sbyte>>(ref Unsafe.Add(ref destinationBase, (uint)i)) = packed;
}
}

72
src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalf.PixelOperations.cs

@ -0,0 +1,72 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.PixelFormats;
/// <content>
/// Provides optimized overrides for bulk operations.
/// </content>
public partial struct RgbaHalf
{
/// <summary>
/// Provides optimized bulk operations for <see cref="RgbaHalf"/>.
/// </summary>
internal class PixelOperations : PixelOperations<RgbaHalf>
{
/// <inheritdoc />
protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan<RgbaHalf> source, Span<Vector4> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// RgbaHalf and RgbaHalfP have the same four-half layout. Sharing the binary16 expansion kernel keeps this path
// vectorized without changing the unassociated meaning of the source components.
RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<RgbaHalf, RgbaHalfP>(source), destination[..source.Length]);
}
/// <inheritdoc />
protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan<RgbaHalf> source, Span<Vector4> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// Association is fused with half expansion so the conversion reads and writes each span once.
RgbaHalfP.PixelOperations.UnpackAssociated(MemoryMarshal.Cast<RgbaHalf, RgbaHalfP>(source), destination[..source.Length]);
}
/// <inheritdoc />
protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan<RgbaHalf> source, Span<Vector4> destination)
=> this.ToUnassociatedVector4(configuration, source, destination);
/// <inheritdoc />
protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan<RgbaHalf> source, Span<Vector4> destination)
=> this.ToAssociatedVector4(configuration, source, destination);
/// <inheritdoc />
protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span<Vector4> source, Span<RgbaHalf> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// The layouts are identical, so the shared packer can write RgbaHalf storage without an intermediate buffer.
RgbaHalfP.PixelOperations.Pack(source, MemoryMarshal.Cast<RgbaHalf, RgbaHalfP>(destination[..source.Length]));
}
/// <inheritdoc />
protected override void FromAssociatedVector4Destructive(Configuration configuration, Span<Vector4> source, Span<RgbaHalf> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// Unassociation is fused with binary16 packing so processors do not pay for another pass over their vector buffer.
RgbaHalfP.PixelOperations.PackFromAssociated(source, MemoryMarshal.Cast<RgbaHalf, RgbaHalfP>(destination[..source.Length]));
}
/// <inheritdoc />
protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span<Vector4> source, Span<RgbaHalf> destination)
=> this.FromUnassociatedVector4Destructive(configuration, source, destination);
/// <inheritdoc />
protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span<Vector4> source, Span<RgbaHalf> destination)
=> this.FromAssociatedVector4Destructive(configuration, source, destination);
}
}

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

@ -0,0 +1,795 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.PixelFormats;
/// <content>
/// Provides optimized overrides for bulk operations.
/// </content>
public partial struct RgbaHalfP
{
/// <summary>
/// Provides optimized bulk operations for <see cref="RgbaHalfP"/>.
/// </summary>
internal class PixelOperations : AssociatedAlphaPixelOperations<RgbaHalfP>
{
/// <inheritdoc />
protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan<RgbaHalfP> source, Span<Vector4> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
UnpackUnassociated(source, destination[..source.Length]);
}
/// <inheritdoc />
protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan<RgbaHalfP> source, Span<Vector4> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
Unpack(source, destination[..source.Length]);
}
/// <inheritdoc />
protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan<RgbaHalfP> source, Span<Vector4> destination)
=> this.ToUnassociatedVector4(configuration, source, destination);
/// <inheritdoc />
protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan<RgbaHalfP> source, Span<Vector4> destination)
=> this.ToAssociatedVector4(configuration, source, destination);
/// <inheritdoc />
protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span<Vector4> source, Span<RgbaHalfP> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
PackUnassociated(source, destination[..source.Length]);
}
/// <inheritdoc />
protected override void FromAssociatedVector4Destructive(Configuration configuration, Span<Vector4> source, Span<RgbaHalfP> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
PackAssociated(source, destination[..source.Length]);
}
/// <inheritdoc />
protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span<Vector4> source, Span<RgbaHalfP> destination)
=> this.FromUnassociatedVector4Destructive(configuration, source, destination);
/// <inheritdoc />
protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span<Vector4> source, Span<RgbaHalfP> destination)
=> this.FromAssociatedVector4Destructive(configuration, source, destination);
/// <summary>
/// Expands binary16 components without changing their unit-range representation.
/// </summary>
/// <param name="source">The packed source pixels.</param>
/// <param name="destination">The destination vectors.</param>
internal static void Unpack(ReadOnlySpan<RgbaHalfP> source, Span<Vector4> destination)
{
ref ushort sourceBase = ref Unsafe.As<RgbaHalfP, ushort>(ref MemoryMarshal.GetReference(source));
ref float destinationBase = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(destination));
int componentCount = source.Length * Vector128<float>.Count;
int i = 0;
if (Vector512.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
{
Vector512<ushort> packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i);
(Vector512<float> lower, Vector512<float> upper) = HalfTypeHelper.Unpack(packed);
Vector512.StoreUnsafe(lower, ref destinationBase, (nuint)i);
Vector512.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector512<float>.Count));
}
}
if (Vector256.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
{
Vector256<ushort> packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i);
(Vector256<float> lower, Vector256<float> upper) = HalfTypeHelper.Unpack(packed);
Vector256.StoreUnsafe(lower, ref destinationBase, (nuint)i);
Vector256.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector256<float>.Count));
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
{
Vector128<ushort> packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
(Vector128<float> lower, Vector128<float> upper) = HalfTypeHelper.Unpack(packed);
Vector128.StoreUnsafe(lower, ref destinationBase, (nuint)i);
Vector128.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector128<float>.Count));
}
if (i < componentCount)
{
// Four binary16 components form one pixel, so the only possible remainder is one complete pixel.
ulong remainder = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref sourceBase, (uint)i)));
Vector128<ushort> packed = Vector128.CreateScalarUnsafe(remainder).AsUInt16();
Vector128.StoreUnsafe(HalfTypeHelper.Unpack(packed).Lower, ref destinationBase, (nuint)i);
}
return;
}
ref RgbaHalfP pixelBase = ref Unsafe.As<ushort, RgbaHalfP>(ref sourceBase);
ref Vector4 vectorBase = ref Unsafe.As<float, Vector4>(ref destinationBase);
for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++)
{
Unsafe.Add(ref vectorBase, (uint)pixelIndex) = Unsafe.Add(ref pixelBase, (uint)pixelIndex).ToVector4();
}
}
/// <summary>
/// Expands unassociated binary16 components and associates RGB in the same pass.
/// </summary>
/// <param name="source">The packed source pixels.</param>
/// <param name="destination">The destination vectors.</param>
internal static void UnpackAssociated(ReadOnlySpan<RgbaHalfP> source, Span<Vector4> destination)
{
ref ushort sourceBase = ref Unsafe.As<RgbaHalfP, ushort>(ref MemoryMarshal.GetReference(source));
ref float destinationBase = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(destination));
int componentCount = source.Length * Vector128<float>.Count;
int i = 0;
if (Vector512.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
{
Vector512<ushort> packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i);
(Vector512<float> lower, Vector512<float> upper) = HalfTypeHelper.Unpack(packed);
Vector512.StoreUnsafe(Associate(lower), ref destinationBase, (nuint)i);
Vector512.StoreUnsafe(Associate(upper), ref destinationBase, (nuint)(i + Vector512<float>.Count));
}
}
if (Vector256.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
{
Vector256<ushort> packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i);
(Vector256<float> lower, Vector256<float> upper) = HalfTypeHelper.Unpack(packed);
Vector256.StoreUnsafe(Associate(lower), ref destinationBase, (nuint)i);
Vector256.StoreUnsafe(Associate(upper), ref destinationBase, (nuint)(i + Vector256<float>.Count));
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
{
Vector128<ushort> packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
(Vector128<float> lower, Vector128<float> upper) = HalfTypeHelper.Unpack(packed);
Vector128.StoreUnsafe(Associate(lower), ref destinationBase, (nuint)i);
Vector128.StoreUnsafe(Associate(upper), ref destinationBase, (nuint)(i + Vector128<float>.Count));
}
if (i < componentCount)
{
// Four binary16 components form one pixel, so the only possible remainder is one complete pixel.
ulong remainder = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref sourceBase, (uint)i)));
Vector128<ushort> packed = Vector128.CreateScalarUnsafe(remainder).AsUInt16();
Vector128.StoreUnsafe(Associate(HalfTypeHelper.Unpack(packed).Lower), ref destinationBase, (nuint)i);
}
return;
}
ref RgbaHalfP pixelBase = ref Unsafe.As<ushort, RgbaHalfP>(ref sourceBase);
ref Vector4 vectorBase = ref Unsafe.As<float, Vector4>(ref destinationBase);
for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++)
{
Vector4 vector = Unsafe.Add(ref pixelBase, (uint)pixelIndex).ToVector4();
Numerics.Premultiply(ref vector);
Unsafe.Add(ref vectorBase, (uint)pixelIndex) = vector;
}
}
/// <summary>
/// Expands associated binary16 components and unassociates RGB in the same pass.
/// </summary>
/// <param name="source">The packed source pixels.</param>
/// <param name="destination">The destination vectors.</param>
internal static void UnpackUnassociated(ReadOnlySpan<RgbaHalfP> source, Span<Vector4> destination)
{
ref ushort sourceBase = ref Unsafe.As<RgbaHalfP, ushort>(ref MemoryMarshal.GetReference(source));
ref float destinationBase = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(destination));
int componentCount = source.Length * Vector128<float>.Count;
int i = 0;
if (Vector512.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
{
Vector512<ushort> packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i);
(Vector512<float> lower, Vector512<float> upper) = HalfTypeHelper.Unpack(packed);
Vector512.StoreUnsafe(Unassociate(lower), ref destinationBase, (nuint)i);
Vector512.StoreUnsafe(Unassociate(upper), ref destinationBase, (nuint)(i + Vector512<float>.Count));
}
}
if (Vector256.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
{
Vector256<ushort> packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i);
(Vector256<float> lower, Vector256<float> upper) = HalfTypeHelper.Unpack(packed);
Vector256.StoreUnsafe(Unassociate(lower), ref destinationBase, (nuint)i);
Vector256.StoreUnsafe(Unassociate(upper), ref destinationBase, (nuint)(i + Vector256<float>.Count));
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
{
Vector128<ushort> packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
(Vector128<float> lower, Vector128<float> upper) = HalfTypeHelper.Unpack(packed);
Vector128.StoreUnsafe(Unassociate(lower), ref destinationBase, (nuint)i);
Vector128.StoreUnsafe(Unassociate(upper), ref destinationBase, (nuint)(i + Vector128<float>.Count));
}
if (i < componentCount)
{
// Four binary16 components form one pixel, so the only possible remainder is one complete pixel.
ulong remainder = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref sourceBase, (uint)i)));
Vector128<ushort> packed = Vector128.CreateScalarUnsafe(remainder).AsUInt16();
Vector128.StoreUnsafe(Unassociate(HalfTypeHelper.Unpack(packed).Lower), ref destinationBase, (nuint)i);
}
return;
}
ref RgbaHalfP pixelBase = ref Unsafe.As<ushort, RgbaHalfP>(ref sourceBase);
ref Vector4 vectorBase = ref Unsafe.As<float, Vector4>(ref destinationBase);
for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++)
{
Unsafe.Add(ref vectorBase, (uint)pixelIndex) = Unsafe.Add(ref pixelBase, (uint)pixelIndex).ToUnassociatedVector4();
}
}
/// <summary>
/// Packs unassociated unit-range vectors directly into binary16 storage.
/// </summary>
/// <param name="source">The source vectors.</param>
/// <param name="destination">The destination pixels.</param>
internal static void Pack(Span<Vector4> source, Span<RgbaHalfP> destination)
{
ref float sourceBase = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(source));
ref ushort destinationBase = ref Unsafe.As<RgbaHalfP, ushort>(ref MemoryMarshal.GetReference(destination));
int componentCount = source.Length * Vector128<float>.Count;
int i = 0;
if (Vector512.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
{
Vector512<float> lower = ClampUnit(Vector512.LoadUnsafe(ref sourceBase, (nuint)i));
Vector512<float> upper = ClampUnit(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<float>.Count)));
Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
}
}
if (Vector256.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
{
Vector256<float> lower = ClampUnit(Vector256.LoadUnsafe(ref sourceBase, (nuint)i));
Vector256<float> upper = ClampUnit(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<float>.Count)));
Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
{
Vector128<float> lower = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)i));
Vector128<float> upper = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<float>.Count)));
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<float> vector = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)i));
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));
}
return;
}
ref Vector4 vectorBase = ref Unsafe.As<float, Vector4>(ref sourceBase);
ref RgbaHalfP pixelBase = ref Unsafe.As<ushort, RgbaHalfP>(ref destinationBase);
for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++)
{
Vector4 vector = Numerics.Clamp(Unsafe.Add(ref vectorBase, (uint)pixelIndex), Vector4.Zero, Vector4.One);
Unsafe.Add(ref pixelBase, (uint)pixelIndex) = new RgbaHalfP(vector.X, vector.Y, vector.Z, vector.W);
}
}
/// <summary>
/// Packs vectors directly into IEEE 754 binary16 storage without applying unit-range color constraints.
/// </summary>
/// <param name="source">The source vectors.</param>
/// <param name="destination">The destination pixels.</param>
internal static void PackUnclamped(Span<Vector4> source, Span<RgbaHalfP> destination)
{
ref float sourceBase = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(source));
ref ushort destinationBase = ref Unsafe.As<RgbaHalfP, ushort>(ref MemoryMarshal.GetReference(destination));
int componentCount = source.Length * Vector128<float>.Count;
int i = 0;
if (Vector512.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
{
Vector512<float> lower = Vector512.LoadUnsafe(ref sourceBase, (nuint)i);
Vector512<float> upper = Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<float>.Count));
Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
}
}
if (Vector256.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
{
Vector256<float> lower = Vector256.LoadUnsafe(ref sourceBase, (nuint)i);
Vector256<float> upper = Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<float>.Count));
Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
{
Vector128<float> lower = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
Vector128<float> upper = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<float>.Count));
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<float> vector = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
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));
}
return;
}
ref Vector4 vectorBase = ref Unsafe.As<float, Vector4>(ref sourceBase);
ref RgbaHalfP pixelBase = ref Unsafe.As<ushort, RgbaHalfP>(ref destinationBase);
for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++)
{
Vector4 vector = Unsafe.Add(ref vectorBase, (uint)pixelIndex);
Unsafe.Add(ref pixelBase, (uint)pixelIndex) = new RgbaHalfP(vector.X, vector.Y, vector.Z, vector.W);
}
}
/// <summary>
/// Unassociates vectors and packs unassociated unit-range binary16 storage in the same pass.
/// </summary>
/// <param name="source">The associated source vectors.</param>
/// <param name="destination">The destination pixels.</param>
internal static void PackFromAssociated(Span<Vector4> source, Span<RgbaHalfP> destination)
{
ref float sourceBase = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(source));
ref ushort destinationBase = ref Unsafe.As<RgbaHalfP, ushort>(ref MemoryMarshal.GetReference(destination));
int componentCount = source.Length * Vector128<float>.Count;
int i = 0;
if (Vector512.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
{
Vector512<float> lower = ClampUnit(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)));
Vector512<float> upper = ClampUnit(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<float>.Count))));
Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
}
}
if (Vector256.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
{
Vector256<float> lower = ClampUnit(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)));
Vector256<float> upper = ClampUnit(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<float>.Count))));
Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
{
Vector128<float> lower = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)));
Vector128<float> upper = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<float>.Count))));
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<float> vector = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)));
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));
}
return;
}
ref Vector4 vectorBase = ref Unsafe.As<float, Vector4>(ref sourceBase);
ref RgbaHalfP pixelBase = ref Unsafe.As<ushort, RgbaHalfP>(ref destinationBase);
for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++)
{
Vector4 vector = Unsafe.Add(ref vectorBase, (uint)pixelIndex);
Numerics.UnPremultiply(ref vector);
vector = Numerics.Clamp(vector, Vector4.Zero, Vector4.One);
Unsafe.Add(ref pixelBase, (uint)pixelIndex) = new RgbaHalfP(vector.X, vector.Y, vector.Z, vector.W);
}
}
/// <summary>
/// Associates unassociated vectors with their stored binary16 alpha and packs them in one pass.
/// </summary>
/// <param name="source">The unassociated source vectors.</param>
/// <param name="destination">The destination pixels.</param>
internal static void PackUnassociated(Span<Vector4> source, Span<RgbaHalfP> destination)
{
ref float sourceBase = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(source));
ref ushort destinationBase = ref Unsafe.As<RgbaHalfP, ushort>(ref MemoryMarshal.GetReference(destination));
int componentCount = source.Length * Vector128<float>.Count;
int i = 0;
// Alpha is rounded to the exact binary16 value that will be stored before RGB is associated with it.
if (Vector512.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
{
Vector512<float> lower = AssociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)i));
Vector512<float> upper = AssociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<float>.Count)));
Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
}
}
if (Vector256.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
{
Vector256<float> lower = AssociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)i));
Vector256<float> upper = AssociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<float>.Count)));
Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
{
Vector128<float> lower = AssociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i));
Vector128<float> upper = AssociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<float>.Count)));
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<float> vector = AssociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i));
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));
}
return;
}
ref Vector4 vectorBase = ref Unsafe.As<float, Vector4>(ref sourceBase);
ref RgbaHalfP pixelBase = ref Unsafe.As<ushort, RgbaHalfP>(ref destinationBase);
for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++)
{
Unsafe.Add(ref pixelBase, (uint)pixelIndex) = RgbaHalfP.FromUnassociatedVector4(Unsafe.Add(ref vectorBase, (uint)pixelIndex));
}
}
/// <summary>
/// Reassociates vectors with their stored binary16 alpha and packs them in one pass.
/// </summary>
/// <param name="source">The associated source vectors.</param>
/// <param name="destination">The destination pixels.</param>
internal static void PackAssociated(Span<Vector4> source, Span<RgbaHalfP> destination)
{
ref float sourceBase = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(source));
ref ushort destinationBase = ref Unsafe.As<RgbaHalfP, ushort>(ref MemoryMarshal.GetReference(destination));
int componentCount = source.Length * Vector128<float>.Count;
int i = 0;
// Scaling RGB by storedAlpha / inputAlpha preserves straight color when binary16 rounds the alpha channel.
if (Vector512.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
{
Vector512<float> lower = ReassociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)i));
Vector512<float> upper = ReassociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<float>.Count)));
Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
}
}
if (Vector256.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
{
Vector256<float> lower = ReassociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)i));
Vector256<float> upper = ReassociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<float>.Count)));
Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
{
Vector128<float> lower = ReassociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i));
Vector128<float> upper = ReassociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<float>.Count)));
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<float> vector = ReassociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i));
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));
}
return;
}
ref Vector4 vectorBase = ref Unsafe.As<float, Vector4>(ref sourceBase);
ref RgbaHalfP pixelBase = ref Unsafe.As<ushort, RgbaHalfP>(ref destinationBase);
for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++)
{
Unsafe.Add(ref pixelBase, (uint)pixelIndex) = RgbaHalfP.FromAssociatedVector4(Unsafe.Add(ref vectorBase, (uint)pixelIndex));
}
}
/// <summary>
/// Associates RGB with alpha while preserving each alpha lane.
/// </summary>
/// <param name="source">The unassociated vectors.</param>
/// <returns>The associated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> Associate(Vector128<float> source)
{
Vector128<float> alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11);
Vector128<float> result = source * alpha;
return Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), alpha, result);
}
/// <summary>
/// Associates RGB with alpha while preserving each alpha lane.
/// </summary>
/// <param name="source">The unassociated vectors.</param>
/// <returns>The associated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> Associate(Vector256<float> source)
{
Vector256<float> alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11);
Vector256<float> result = source * alpha;
return Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), alpha, result);
}
/// <summary>
/// Associates RGB with alpha while preserving each alpha lane.
/// </summary>
/// <param name="source">The unassociated vectors.</param>
/// <returns>The associated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> Associate(Vector512<float> source)
{
Vector512<float> alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11);
Vector512<float> result = source * alpha;
Vector512<float> alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle();
return Vector512.ConditionalSelect(alphaMask, alpha, result);
}
/// <summary>
/// Unassociates RGB while preserving each alpha lane.
/// </summary>
/// <param name="source">The associated vectors.</param>
/// <returns>The unassociated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> Unassociate(Vector128<float> source)
{
Vector128<float> alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11);
return Numerics.UnPremultiply(source, alpha);
}
/// <summary>
/// Unassociates RGB while preserving each alpha lane.
/// </summary>
/// <param name="source">The associated vectors.</param>
/// <returns>The unassociated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> Unassociate(Vector256<float> source)
{
Vector256<float> alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11);
return Numerics.UnPremultiply(source, alpha);
}
/// <summary>
/// Unassociates RGB while preserving each alpha lane.
/// </summary>
/// <param name="source">The associated vectors.</param>
/// <returns>The unassociated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> Unassociate(Vector512<float> source)
{
Vector512<float> alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11);
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>
/// Associates unassociated vectors with the alpha value binary16 storage can reproduce.
/// </summary>
/// <param name="source">The unassociated vectors.</param>
/// <returns>The associated vectors ready for packing.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> AssociateForStorage(Vector128<float> source)
{
source = ClampUnit(source);
Vector128<float> alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11);
Vector128<float> storedAlpha = HalfTypeHelper.RoundToHalf(alpha);
Vector128<float> result = source * storedAlpha;
return Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result);
}
/// <summary>
/// Associates unassociated vectors with the alpha value binary16 storage can reproduce.
/// </summary>
/// <param name="source">The unassociated vectors.</param>
/// <returns>The associated vectors ready for packing.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> AssociateForStorage(Vector256<float> source)
{
source = ClampUnit(source);
Vector256<float> alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11);
Vector256<float> storedAlpha = HalfTypeHelper.RoundToHalf(alpha);
Vector256<float> result = source * storedAlpha;
return Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result);
}
/// <summary>
/// Associates unassociated vectors with the alpha value binary16 storage can reproduce.
/// </summary>
/// <param name="source">The unassociated vectors.</param>
/// <returns>The associated vectors ready for packing.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> AssociateForStorage(Vector512<float> source)
{
source = ClampUnit(source);
Vector512<float> alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11);
Vector512<float> storedAlpha = HalfTypeHelper.RoundToHalf(alpha);
Vector512<float> result = source * storedAlpha;
Vector512<float> alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle();
return Vector512.ConditionalSelect(alphaMask, storedAlpha, result);
}
/// <summary>
/// Reassociates vectors with the alpha value binary16 storage can reproduce.
/// </summary>
/// <param name="source">The associated vectors.</param>
/// <returns>The associated vectors ready for packing.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> ReassociateForStorage(Vector128<float> source)
{
Vector128<float> zero = Vector128<float>.Zero;
Vector128<float> alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11);
Vector128<float> clampedAlpha = ClampUnit(alpha);
Vector128<float> storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha);
Vector128<float> result = source * (storedAlpha / alpha);
result = Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result);
result = Vector128.Min(Vector128.Max(result, zero), storedAlpha);
return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(alpha, zero), zero, result);
}
/// <summary>
/// Reassociates vectors with the alpha value binary16 storage can reproduce.
/// </summary>
/// <param name="source">The associated vectors.</param>
/// <returns>The associated vectors ready for packing.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> ReassociateForStorage(Vector256<float> source)
{
Vector256<float> zero = Vector256<float>.Zero;
Vector256<float> alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11);
Vector256<float> clampedAlpha = ClampUnit(alpha);
Vector256<float> storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha);
Vector256<float> 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);
return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(alpha, zero), zero, result);
}
/// <summary>
/// Reassociates vectors with the alpha value binary16 storage can reproduce.
/// </summary>
/// <param name="source">The associated vectors.</param>
/// <returns>The associated vectors ready for packing.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> ReassociateForStorage(Vector512<float> source)
{
Vector512<float> zero = Vector512<float>.Zero;
Vector512<float> alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11);
Vector512<float> clampedAlpha = ClampUnit(alpha);
Vector512<float> storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha);
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();
result = Vector512.ConditionalSelect(alphaMask, storedAlpha, result);
result = Vector512.Min(Vector512.Max(result, zero), storedAlpha);
return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(alpha, zero), zero, result);
}
}
}

6
src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short2.PixelOperations.cs

@ -16,8 +16,8 @@ public partial struct Short2
/// </summary>
internal class PixelOperations : PixelOperations<Short2>
{
private static readonly Vector4 NativeOffset = new(MaxPos, MaxPos, 0F, 0F);
private static readonly Vector4 NativeDivisor = new(MaxPos * 2F, MaxPos * 2F, 1F, 1F);
private static readonly Vector4 NativeOffset = new(-MinNeg, -MinNeg, 0F, 0F);
private static readonly Vector4 NativeRange = new(Range, Range, 1F, 1F);
// Alpha is implicitly one, so both outward representations already contain associated color components.
@ -32,7 +32,7 @@ public partial struct Short2
{
// X and Y use signed-native coordinates while incoming W remains normalized alpha. Map only the stored components before
// the scaled bulk path unassociates RGB; transforming W would change the opacity that must be removed.
Vector4Converters.AddThenDivide(source, NativeOffset, NativeDivisor);
Vector4Converters.AddThenDivide(source, NativeOffset, NativeRange);
this.FromAssociatedScaledVector4Destructive(configuration, source, destination);
}
}

4
src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short4.PixelOperations.cs

@ -17,8 +17,8 @@ public partial struct Short4
/// </summary>
internal class PixelOperations : PixelOperations<Short4>
{
private static readonly Vector4 NativeOffset = new(MaxPos);
private static readonly Vector4 NativeMagnitude = new(MaxPos * 2F);
private static readonly Vector4 NativeOffset = new(-MinNeg);
private static readonly Vector4 NativeMagnitude = new(Range);
/// <inheritdoc />
protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan<Short4> source, Span<Vector4> destination)

9
src/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs

@ -7,11 +7,12 @@ using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing two 16-bit unsigned normalized values ranging from 0 to 1.
/// <para>
/// Ranges from [0, 0, 0, 1] to [1, 1, 0, 1] in vector form.
/// </para>
/// Packed pixel type containing two 16-bit unsigned normalized values.
/// </summary>
/// <remarks>
/// <see cref="ToVector2"/>, <see cref="ToVector4"/>, and scaled vector conversions return x and y in <c>[0, 1]</c>, z
/// as <c>0</c>, and implicit alpha as <c>1</c>. The packed storage layout matches <c>DXGI_FORMAT_R16G16_UNORM</c>.
/// </remarks>
public partial struct Rg32 : IPixel<Rg32>, IPackedVector<uint>
{
private static readonly Vector2 Max = new(ushort.MaxValue);

9
src/ImageSharp/PixelFormats/PixelImplementations/Rgba1010102.cs

@ -7,12 +7,13 @@ using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed vector type containing 4 unsigned normalized values ranging from 0 to 1.
/// Packed pixel type containing four unsigned normalized values.
/// The x, y and z components use 10 bits, and the w component uses 2 bits.
/// <para>
/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form.
/// </para>
/// </summary>
/// <remarks>
/// <see cref="ToVector4"/> and scaled vector conversions return all components in <c>[0, 1]</c>. The storage layout matches
/// <c>DXGI_FORMAT_R10G10B10A2_UNORM</c>.
/// </remarks>
public partial struct Rgba1010102 : IPixel<Rgba1010102>, IPackedVector<uint>
{
private static readonly Vector4 Multiplier = new(1023F, 1023F, 1023F, 3F);

9
src/ImageSharp/PixelFormats/PixelImplementations/Rgba64.cs

@ -8,11 +8,12 @@ using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing four 16-bit unsigned normalized values ranging from 0 to 65535.
/// <para>
/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form.
/// </para>
/// Packed pixel type containing four 16-bit unsigned normalized values.
/// </summary>
/// <remarks>
/// Component fields expose storage values in <c>[0, 65535]</c>. <see cref="ToVector4"/> and scaled vector conversions
/// return them in <c>[0, 1]</c>. The storage layout matches <c>DXGI_FORMAT_R16G16B16A16_UNORM</c>.
/// </remarks>
[StructLayout(LayoutKind.Sequential)]
public partial struct Rgba64 : IPixel<Rgba64>, IPackedVector<ulong>
{

227
src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalf.cs

@ -0,0 +1,227 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing four 16-bit floating-point values typically ranging from 0 to 1.
/// The color components are stored in red, green, blue, and alpha order.
/// </summary>
/// <remarks>
/// <see cref="ToVector4"/> and scaled vector conversions return the same component values in the nominal color range
/// <c>[0, 1]</c>. The packed representation is binary-compatible with <c>DXGI_FORMAT_R16G16B16A16_FLOAT</c>.
/// </remarks>
[StructLayout(LayoutKind.Sequential)]
public partial struct RgbaHalf : IPixel<RgbaHalf>, IPackedVector<ulong>
{
/// <summary>
/// Gets or sets the red component.
/// </summary>
public Half R;
/// <summary>
/// Gets or sets the green component.
/// </summary>
public Half G;
/// <summary>
/// Gets or sets the blue component.
/// </summary>
public Half B;
/// <summary>
/// Gets or sets the alpha component.
/// </summary>
public Half A;
/// <summary>
/// Initializes a new instance of the <see cref="RgbaHalf"/> struct.
/// </summary>
/// <param name="r">The red component.</param>
/// <param name="g">The green component.</param>
/// <param name="b">The blue component.</param>
public RgbaHalf(float r, float g, float b)
: this(r, g, b, 1F)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RgbaHalf"/> struct.
/// </summary>
/// <param name="r">The red component.</param>
/// <param name="g">The green component.</param>
/// <param name="b">The blue component.</param>
/// <param name="a">The alpha component.</param>
public RgbaHalf(float r, float g, float b, float a)
{
this.R = (Half)r;
this.G = (Half)g;
this.B = (Half)b;
this.A = (Half)a;
}
/// <summary>
/// Initializes a new instance of the <see cref="RgbaHalf"/> struct.
/// </summary>
/// <param name="vector">The vector containing the component values.</param>
public RgbaHalf(Vector4 vector)
: this(vector.X, vector.Y, vector.Z, vector.W)
{
}
/// <inheritdoc />
public ulong PackedValue
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
readonly get => Unsafe.As<RgbaHalf, ulong>(ref Unsafe.AsRef(in this));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => Unsafe.As<RgbaHalf, ulong>(ref this) = value;
}
/// <summary>
/// Compares two <see cref="RgbaHalf"/> values for equality.
/// </summary>
/// <param name="left">The left value.</param>
/// <param name="right">The right value.</param>
/// <returns><see langword="true"/> when the values are equal.</returns>
public static bool operator ==(RgbaHalf left, RgbaHalf right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="RgbaHalf"/> values for inequality.
/// </summary>
/// <param name="left">The left value.</param>
/// <param name="right">The right value.</param>
/// <returns><see langword="true"/> when the values are not equal.</returns>
public static bool operator !=(RgbaHalf left, RgbaHalf right) => !left.Equals(right);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToScaledVector4() => this.ToVector4();
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToVector4() => new((float)this.R, (float)this.G, (float)this.B, (float)this.A);
/// <inheritdoc />
public static PixelTypeInfo GetPixelTypeInfo()
=> PixelTypeInfo.Create<RgbaHalf>(
PixelComponentInfo.Create<RgbaHalf>(4, 16, 16, 16, 16),
PixelColorType.RGB | PixelColorType.Alpha,
PixelAlphaRepresentation.Unassociated);
/// <inheritdoc />
public static PixelOperations<RgbaHalf> CreatePixelOperations() => new PixelOperations();
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4();
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToAssociatedScaledVector4()
{
Vector4 vector = this.ToScaledVector4();
Numerics.Premultiply(ref vector);
return vector;
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToUnassociatedVector4() => this.ToVector4();
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4();
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RgbaHalf FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RgbaHalf FromAssociatedScaledVector4(Vector4 source)
{
Numerics.UnPremultiply(ref source);
return FromScaledVector4(source);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RgbaHalf FromUnassociatedVector4(Vector4 source) => FromVector4(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RgbaHalf FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RgbaHalf FromScaledVector4(Vector4 source) => FromVector4(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RgbaHalf FromVector4(Vector4 source)
{
source = Numerics.Clamp(source, Vector4.Zero, Vector4.One);
return new RgbaHalf(source);
}
/// <inheritdoc />
public static RgbaHalf FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalf FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalf FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalf FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalf FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalf FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalf FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalf FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalf FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalf FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalf FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalf FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalf FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public override readonly bool Equals(object? obj) => obj is RgbaHalf other && this.Equals(other);
/// <inheritdoc />
public readonly bool Equals(RgbaHalf other) => this.PackedValue.Equals(other.PackedValue);
/// <inheritdoc />
public override readonly int GetHashCode() => this.PackedValue.GetHashCode();
/// <inheritdoc />
public override readonly string ToString() => FormattableString.Invariant($"RgbaHalf({(float)this.R:#0.##}, {(float)this.G:#0.##}, {(float)this.B:#0.##}, {(float)this.A:#0.##})");
}

242
src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalfP.cs

@ -0,0 +1,242 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing four associated 16-bit floating-point values typically ranging from 0 to 1.
/// The color components are stored in red, green, blue, and alpha order.
/// </summary>
/// <remarks>
/// <see cref="ToVector4"/> and scaled vector conversions return the same associated component values in the nominal
/// color range <c>[0, 1]</c>. The packed representation is binary-compatible with
/// <c>DXGI_FORMAT_R16G16B16A16_FLOAT</c>.
/// </remarks>
[StructLayout(LayoutKind.Sequential)]
public partial struct RgbaHalfP : IPixel<RgbaHalfP>, IPackedVector<ulong>
{
/// <summary>
/// Gets or sets the associated red component.
/// </summary>
public Half R;
/// <summary>
/// Gets or sets the associated green component.
/// </summary>
public Half G;
/// <summary>
/// Gets or sets the associated blue component.
/// </summary>
public Half B;
/// <summary>
/// Gets or sets the alpha component.
/// </summary>
public Half A;
/// <summary>
/// Initializes a new instance of the <see cref="RgbaHalfP"/> struct from associated components.
/// </summary>
/// <param name="r">The associated red component.</param>
/// <param name="g">The associated green component.</param>
/// <param name="b">The associated blue component.</param>
public RgbaHalfP(float r, float g, float b)
: this(r, g, b, 1F)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RgbaHalfP"/> struct from associated components.
/// </summary>
/// <param name="r">The associated red component.</param>
/// <param name="g">The associated green component.</param>
/// <param name="b">The associated blue component.</param>
/// <param name="a">The alpha component.</param>
public RgbaHalfP(float r, float g, float b, float a)
{
this.R = (Half)r;
this.G = (Half)g;
this.B = (Half)b;
this.A = (Half)a;
}
/// <summary>
/// Initializes a new instance of the <see cref="RgbaHalfP"/> struct from an associated vector.
/// </summary>
/// <param name="vector">The associated vector.</param>
public RgbaHalfP(Vector4 vector)
: this() => this = FromAssociatedScaledVector4(vector);
/// <inheritdoc />
public ulong PackedValue
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
readonly get => Unsafe.As<RgbaHalfP, ulong>(ref Unsafe.AsRef(in this));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => Unsafe.As<RgbaHalfP, ulong>(ref this) = value;
}
/// <summary>
/// Compares two <see cref="RgbaHalfP"/> values for equality.
/// </summary>
/// <param name="left">The left value.</param>
/// <param name="right">The right value.</param>
/// <returns><see langword="true"/> when the values are equal.</returns>
public static bool operator ==(RgbaHalfP left, RgbaHalfP right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="RgbaHalfP"/> values for inequality.
/// </summary>
/// <param name="left">The left value.</param>
/// <param name="right">The right value.</param>
/// <returns><see langword="true"/> when the values are not equal.</returns>
public static bool operator !=(RgbaHalfP left, RgbaHalfP right) => !left.Equals(right);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToUnassociatedScaledVector4());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToScaledVector4() => this.ToVector4();
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToVector4() => new((float)this.R, (float)this.G, (float)this.B, (float)this.A);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToUnassociatedScaledVector4()
{
Vector4 vector = this.ToScaledVector4();
Numerics.UnPremultiply(ref vector);
return vector;
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4();
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToUnassociatedVector4() => this.ToUnassociatedScaledVector4();
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToAssociatedVector4() => this.ToVector4();
/// <inheritdoc />
public static PixelTypeInfo GetPixelTypeInfo()
=> PixelTypeInfo.Create<RgbaHalfP>(
PixelComponentInfo.Create<RgbaHalfP>(4, 16, 16, 16, 16),
PixelColorType.RGB | PixelColorType.Alpha,
PixelAlphaRepresentation.Associated);
/// <inheritdoc />
public static PixelOperations<RgbaHalfP> CreatePixelOperations() => new PixelOperations();
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RgbaHalfP FromScaledVector4(Vector4 source) => FromAssociatedScaledVector4(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RgbaHalfP FromVector4(Vector4 source) => FromAssociatedVector4(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RgbaHalfP FromUnassociatedScaledVector4(Vector4 source)
{
source = Numerics.Clamp(source, Vector4.Zero, Vector4.One);
// RGB must be associated with the alpha value that binary16 storage can reproduce, not the higher-precision input alpha.
source.W = HalfTypeHelper.Unpack(HalfTypeHelper.Pack(source.W));
Numerics.Premultiply(ref source);
return new RgbaHalfP(source.X, source.Y, source.Z, source.W);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RgbaHalfP FromAssociatedScaledVector4(Vector4 source)
{
float alpha = source.W;
if (alpha <= 0F)
{
return default;
}
float storedAlpha = HalfTypeHelper.Unpack(HalfTypeHelper.Pack(Numerics.Clamp(alpha, 0F, 1F)));
// Preserve the represented straight color when binary16 rounds alpha, then restore the associated RGB <= alpha invariant.
source *= storedAlpha / alpha;
source.W = storedAlpha;
Numerics.ClampRgbToAlpha(ref source);
return new RgbaHalfP(source.X, source.Y, source.Z, source.W);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RgbaHalfP FromUnassociatedVector4(Vector4 source) => FromUnassociatedScaledVector4(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RgbaHalfP FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source);
/// <inheritdoc />
public static RgbaHalfP FromAbgr32(Abgr32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalfP FromArgb32(Argb32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalfP FromBgra5551(Bgra5551 source) => FromUnassociatedScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalfP FromBgr24(Bgr24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalfP FromBgra32(Bgra32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalfP FromL8(L8 source) => FromUnassociatedScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalfP FromL16(L16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalfP FromLa16(La16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalfP FromLa32(La32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalfP FromRgb24(Rgb24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalfP FromRgba32(Rgba32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalfP FromRgb48(Rgb48 source) => FromUnassociatedScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public static RgbaHalfP FromRgba64(Rgba64 source) => FromUnassociatedScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public override readonly bool Equals(object? obj) => obj is RgbaHalfP other && this.Equals(other);
/// <inheritdoc />
public readonly bool Equals(RgbaHalfP other) => this.PackedValue.Equals(other.PackedValue);
/// <inheritdoc />
public override readonly int GetHashCode() => this.PackedValue.GetHashCode();
/// <inheritdoc />
public override readonly string ToString() => FormattableString.Invariant($"RgbaHalfP({(float)this.R:#0.##}, {(float)this.G:#0.##}, {(float)this.B:#0.##}, {(float)this.A:#0.##})");
}

25
src/ImageSharp/PixelFormats/PixelImplementations/Short2.cs

@ -8,10 +8,12 @@ namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing two 16-bit signed integer values.
/// <para>
/// Ranges from [-32767, -32767, 0, 1] to [32767, 32767, 0, 1] in vector form.
/// </para>
/// </summary>
/// <remarks>
/// <see cref="ToVector2"/> and <see cref="ToVector4"/> return stored components in <c>[-32768, 32767]</c>.
/// Scaled vector conversions map the full stored range to <c>[0, 1]</c>. The packed storage layout matches
/// <c>DXGI_FORMAT_R16G16_SINT</c>.
/// </remarks>
public partial struct Short2 : IPixel<Short2>, IPackedVector<uint>
{
// Largest two byte positive number 0xFFFF >> 1;
@ -20,6 +22,9 @@ public partial struct Short2 : IPixel<Short2>, IPackedVector<uint>
// Two's complement
private const float MinNeg = ~(int)MaxPos;
// Scaled conversions cover every signed 16-bit code, including the asymmetric minimum value.
private const float Range = MaxPos - MinNeg;
private static readonly Vector2 Max = new(MaxPos);
private static readonly Vector2 Min = new(MinNeg);
@ -73,8 +78,8 @@ public partial struct Short2 : IPixel<Short2>, IPackedVector<uint>
public readonly Vector4 ToScaledVector4()
{
Vector2 scaled = this.ToVector2();
scaled += new Vector2(32767f);
scaled /= 65534F;
scaled -= Min;
scaled /= Range;
return new Vector4(scaled, 0f, 1f);
}
@ -129,9 +134,9 @@ public partial struct Short2 : IPixel<Short2>, IPackedVector<uint>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Short2 FromAssociatedVector4(Vector4 source)
{
// Only the stored color components use the signed native encoding; W remains the normalized source alpha.
source.X = (source.X + MaxPos) / (MaxPos * 2F);
source.Y = (source.Y + MaxPos) / (MaxPos * 2F);
// Only the stored color components use the signed native encoding; W remains the scaled source alpha.
source.X = (source.X - MinNeg) / Range;
source.Y = (source.Y - MinNeg) / Range;
return FromAssociatedScaledVector4(source);
}
@ -139,8 +144,8 @@ public partial struct Short2 : IPixel<Short2>, IPackedVector<uint>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Short2 FromScaledVector4(Vector4 source)
{
Vector2 scaled = new Vector2(source.X, source.Y) * 65534F;
scaled -= new Vector2(32767F);
Vector2 scaled = new Vector2(source.X, source.Y) * Range;
scaled += Min;
return new Short2 { PackedValue = Pack(scaled) };
}

30
src/ImageSharp/PixelFormats/PixelImplementations/Short4.cs

@ -8,10 +8,11 @@ namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing four 16-bit signed integer values.
/// <para>
/// Ranges from [-37267, -37267, -37267, -37267] to [37267, 37267, 37267, 37267] in vector form.
/// </para>
/// </summary>
/// <remarks>
/// <see cref="ToVector4"/> returns stored components in <c>[-32768, 32767]</c>. Scaled vector conversions map the full
/// stored range to <c>[0, 1]</c>. The packed storage layout matches <c>DXGI_FORMAT_R16G16B16A16_SINT</c>.
/// </remarks>
public partial struct Short4 : IPixel<Short4>, IPackedVector<ulong>
{
// Largest two byte positive number 0xFFFF >> 1;
@ -20,6 +21,9 @@ public partial struct Short4 : IPixel<Short4>, IPackedVector<ulong>
// Two's complement
private const float MinNeg = ~(int)MaxPos;
// Scaled conversions cover every signed 16-bit code, including the asymmetric minimum value.
private const float Range = MaxPos - MinNeg;
private static readonly Vector4 Max = new(MaxPos);
private static readonly Vector4 Min = new(MinNeg);
@ -75,8 +79,8 @@ public partial struct Short4 : IPixel<Short4>, IPackedVector<ulong>
public readonly Vector4 ToScaledVector4()
{
Vector4 scaled = this.ToVector4();
scaled += new Vector4(32767f);
scaled /= 65534f;
scaled -= Min;
scaled /= Range;
return scaled;
}
@ -122,9 +126,9 @@ public partial struct Short4 : IPixel<Short4>, IPackedVector<ulong>
{
Vector4 vector = this.ToAssociatedScaledVector4();
// Native components use an affine signed encoding, so direct multiplication would use the wrong zero point.
vector *= MaxPos * 2F;
vector -= new Vector4(MaxPos);
// Native components use the full asymmetric signed range, so scaled zero must map to -32768 rather than -32767.
vector *= Range;
vector += Min;
return vector;
}
@ -148,9 +152,9 @@ public partial struct Short4 : IPixel<Short4>, IPackedVector<ulong>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Short4 FromAssociatedVector4(Vector4 source)
{
// Map the affine signed native encoding to logical [0, 1] space before unassociating.
source += new Vector4(MaxPos);
source /= MaxPos * 2F;
// Map the full signed native encoding to scaled [0, 1] space before unassociating.
source -= Min;
source /= Range;
return FromAssociatedScaledVector4(source);
}
@ -158,8 +162,8 @@ public partial struct Short4 : IPixel<Short4>, IPackedVector<ulong>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Short4 FromScaledVector4(Vector4 source)
{
source *= 65534F;
source -= new Vector4(32767F);
source *= Range;
source += Min;
return FromVector4(source);
}

57
src/ImageSharp/PixelFormats/Utils/SignedShort4PixelOperations.cs

@ -17,7 +17,9 @@ internal static class SignedShort4PixelOperations
{
private const byte RestorePackedPixelOrder = 0b_11_01_10_00;
private const float ShortMaximum = short.MaxValue;
private const float ShortMagnitude = ShortMaximum * 2F;
private const float SignedNormalizedMagnitude = ShortMaximum * 2F;
private const float SignedIntegerMinimum = short.MinValue;
private const float SignedIntegerRange = ushort.MaxValue;
/// <summary>
/// Expands signed 16-bit components to native or scaled vectors.
@ -79,11 +81,14 @@ internal static class SignedShort4PixelOperations
if (normalized)
{
// Both minimum two's-complement SNORM encodings represent -1.
vector = Vector4.Max(vector, new Vector4(-ShortMaximum));
if (scaled)
{
// Offset exact integer components before division to avoid cancellation near the signed-normalized lower bound.
vector += new Vector4(ShortMaximum);
vector /= ShortMagnitude;
vector /= SignedNormalizedMagnitude;
}
else
{
@ -92,8 +97,9 @@ internal static class SignedShort4PixelOperations
}
else if (scaled)
{
vector += new Vector4(ShortMaximum);
vector /= ShortMagnitude;
// SINT uses every two's-complement code, unlike SNORM where both minimum encodings represent -1.
vector -= new Vector4(SignedIntegerMinimum);
vector /= SignedIntegerRange;
}
Unsafe.Add(ref destinationBase, (uint)index) = vector;
@ -182,8 +188,8 @@ internal static class SignedShort4PixelOperations
{
if (scaled)
{
vector *= ShortMagnitude;
vector -= new Vector4(ShortMaximum);
vector *= SignedIntegerRange;
vector += new Vector4(SignedIntegerMinimum);
}
vector = Numerics.Clamp(vector, new Vector4(short.MinValue), new Vector4(short.MaxValue));
@ -209,11 +215,14 @@ internal static class SignedShort4PixelOperations
{
if (normalized)
{
// Both minimum two's-complement SNORM encodings represent -1.
source = Vector512.Max(source, Vector512.Create(-ShortMaximum));
if (scaled)
{
// Offset exact integer components before division to avoid cancellation near the signed-normalized lower bound.
source += Vector512.Create(ShortMaximum);
source /= Vector512.Create(ShortMagnitude);
source /= Vector512.Create(SignedNormalizedMagnitude);
}
else
{
@ -222,8 +231,8 @@ internal static class SignedShort4PixelOperations
}
else if (scaled)
{
source += Vector512.Create(ShortMaximum);
source /= Vector512.Create(ShortMagnitude);
source -= Vector512.Create(SignedIntegerMinimum);
source /= Vector512.Create(SignedIntegerRange);
}
return source;
@ -241,11 +250,14 @@ internal static class SignedShort4PixelOperations
{
if (normalized)
{
// Both minimum two's-complement SNORM encodings represent -1.
source = Vector256.Max(source, Vector256.Create(-ShortMaximum));
if (scaled)
{
// Offset exact integer components before division to avoid cancellation near the signed-normalized lower bound.
source += Vector256.Create(ShortMaximum);
source /= Vector256.Create(ShortMagnitude);
source /= Vector256.Create(SignedNormalizedMagnitude);
}
else
{
@ -254,8 +266,8 @@ internal static class SignedShort4PixelOperations
}
else if (scaled)
{
source += Vector256.Create(ShortMaximum);
source /= Vector256.Create(ShortMagnitude);
source -= Vector256.Create(SignedIntegerMinimum);
source /= Vector256.Create(SignedIntegerRange);
}
return source;
@ -273,11 +285,14 @@ internal static class SignedShort4PixelOperations
{
if (normalized)
{
// Both minimum two's-complement SNORM encodings represent -1.
source = Vector128.Max(source, Vector128.Create(-ShortMaximum));
if (scaled)
{
// Offset exact integer components before division to avoid cancellation near the signed-normalized lower bound.
source += Vector128.Create(ShortMaximum);
source /= Vector128.Create(ShortMagnitude);
source /= Vector128.Create(SignedNormalizedMagnitude);
}
else
{
@ -286,8 +301,8 @@ internal static class SignedShort4PixelOperations
}
else if (scaled)
{
source += Vector128.Create(ShortMaximum);
source /= Vector128.Create(ShortMagnitude);
source -= Vector128.Create(SignedIntegerMinimum);
source /= Vector128.Create(SignedIntegerRange);
}
return source;
@ -318,8 +333,8 @@ internal static class SignedShort4PixelOperations
{
if (scaled)
{
source *= Vector512.Create(ShortMagnitude);
source -= Vector512.Create(ShortMaximum);
source *= Vector512.Create(SignedIntegerRange);
source += Vector512.Create(SignedIntegerMinimum);
}
source = Vector512.Min(Vector512.Max(source, Vector512.Create((float)short.MinValue)), Vector512.Create((float)short.MaxValue));
@ -353,8 +368,8 @@ internal static class SignedShort4PixelOperations
{
if (scaled)
{
source *= Vector256.Create(ShortMagnitude);
source -= Vector256.Create(ShortMaximum);
source *= Vector256.Create(SignedIntegerRange);
source += Vector256.Create(SignedIntegerMinimum);
}
source = Vector256.Min(Vector256.Max(source, Vector256.Create((float)short.MinValue)), Vector256.Create((float)short.MaxValue));
@ -388,8 +403,8 @@ internal static class SignedShort4PixelOperations
{
if (scaled)
{
source *= Vector128.Create(ShortMagnitude);
source -= Vector128.Create(ShortMaximum);
source *= Vector128.Create(SignedIntegerRange);
source += Vector128.Create(SignedIntegerMinimum);
}
source = Vector128.Min(Vector128.Max(source, Vector128.Create((float)short.MinValue)), Vector128.Create((float)short.MaxValue));

19
src/ImageSharp/PixelFormats/Utils/Vector4Converters.AssociatedRgbaCompatible.cs

@ -5,7 +5,6 @@ using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.PixelFormats.Utils;
@ -51,27 +50,33 @@ internal static partial class Vector4Converters
int componentsPerPixel = Vector128<float>.Count;
int i = 0;
if (Avx512F.IsSupported)
// Portable widening advances one integer width at a time, so assemble the wider vectors from ordered 128-bit halves.
if (Vector512.IsHardwareAccelerated)
{
int pixelsPerVector = Vector512<float>.Count / componentsPerPixel;
for (; i <= destination.Length - pixelsPerVector; i += pixelsPerVector)
{
Vector128<byte> packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i * componentsPerPixel));
Vector512<int> integers = Avx512F.ConvertToVector512Int32(packed);
Unsafe.As<Vector4, Vector512<float>>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Avx512F.ConvertToVector512Single(integers));
Vector128<ushort> lowerShorts = Vector128.WidenLower(packed);
Vector128<ushort> upperShorts = Vector128.WidenUpper(packed);
Vector256<uint> lowerIntegers = Vector256.Create(Vector128.WidenLower(lowerShorts), Vector128.WidenUpper(lowerShorts));
Vector256<uint> upperIntegers = Vector256.Create(Vector128.WidenLower(upperShorts), Vector128.WidenUpper(upperShorts));
Vector512<int> integers = Vector512.Create(lowerIntegers, upperIntegers).AsInt32();
Unsafe.As<Vector4, Vector512<float>>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Vector512.ConvertToSingle(integers));
}
}
if (Avx2.IsSupported)
if (Vector256.IsHardwareAccelerated)
{
int pixelsPerVector = Vector256<float>.Count / componentsPerPixel;
for (; i <= destination.Length - pixelsPerVector; i += pixelsPerVector)
{
ulong packed = Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref sourceBase, (uint)(i * componentsPerPixel)));
Vector256<int> integers = Avx2.ConvertToVector256Int32(Vector128.CreateScalarUnsafe(packed).AsByte());
Unsafe.As<Vector4, Vector256<float>>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Avx.ConvertToVector256Single(integers));
Vector128<ushort> shorts = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte());
Vector256<int> integers = Vector256.Create(Vector128.WidenLower(shorts), Vector128.WidenUpper(shorts)).AsInt32();
Unsafe.As<Vector4, Vector256<float>>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Vector256.ConvertToSingle(integers));
}
}

96
tests/ImageSharp.Tests/PixelFormats/AssociatedAlphaPixelTests.cs

@ -121,6 +121,17 @@ public class NormalizedByte4PTests : AssociatedAlphaPixelTests<NormalizedByte4P>
Assert.Equal(new NormalizedByte4(associated).PackedValue, new NormalizedByte4P(associated).PackedValue);
}
[Fact]
public void MinimumStorageCodeDecodesAsNegativeOne()
{
NormalizedByte4P pixel = new() { PackedValue = 0x80808080 };
Assert.Equal(-Vector4.One, pixel.ToVector4());
Assert.Equal(Vector4.Zero, pixel.ToScaledVector4());
Assert.Equal(-Vector4.One, pixel.ToUnassociatedVector4());
Assert.Equal(Vector4.Zero, pixel.ToUnassociatedScaledVector4());
}
[Fact]
public void AssociatedScaledVectorsMatchUnassociatedVectorsWithinTwoUlpsForEveryValidComponentAndAlpha()
{
@ -285,14 +296,7 @@ public class HalfVector4PTests : AssociatedAlphaPixelTests<HalfVector4P>
[Fact]
public void ColorRoundTripDoesNotIntroduceAdditionalAssociationLoss()
{
const ulong zeroComponent = 0xBC00;
// This is the first valid Half component/alpha pair for which an unassociate/reassociate round trip
// selects the adjacent component value instead of the direct scaled-vector conversion result.
HalfVector4P source = new()
{
PackedValue = 0x8BF5 | (zeroComponent << 16) | (zeroComponent << 32) | (0x05DFUL << 48)
};
HalfVector4P source = HalfVector4P.FromAssociatedScaledVector4(new Vector4(.125F, .25F, .375F, .5F));
HalfVector4P expected = HalfVector4P.FromScaledVector4(source.ToScaledVector4());
Color color = Color.FromPixel(source);
Color[] bulkColors = new Color[1];
@ -421,9 +425,8 @@ public class HalfVector4PTests : AssociatedAlphaPixelTests<HalfVector4P>
unassociatedScaled[i] = new Vector4(((i * 37) % 4093) / 4092F, ((i * 73) % 4093) / 4092F, ((i * 109) % 4093) / 4092F, alpha);
associatedScaled[i] = new Vector4(unassociatedScaled[i].X * alpha, unassociatedScaled[i].Y * alpha, unassociatedScaled[i].Z * alpha, alpha);
// HalfVector4's native domain is [-1, 1], so the native entry points need the affine encoding of the common scaled values.
unassociatedNative[i] = (unassociatedScaled[i] * 2F) - Vector4.One;
associatedNative[i] = (associatedScaled[i] * 2F) - Vector4.One;
unassociatedNative[i] = (unassociatedScaled[i] * 131008F) - new Vector4(65504F);
associatedNative[i] = (associatedScaled[i] * 131008F) - new Vector4(65504F);
expectedFromUnassociatedNative[i] = HalfVector4P.FromUnassociatedVector4(unassociatedNative[i]);
expectedFromAssociatedNative[i] = HalfVector4P.FromAssociatedVector4(associatedNative[i]);
expectedFromUnassociatedScaled[i] = HalfVector4P.FromUnassociatedScaledVector4(unassociatedScaled[i]);
@ -824,8 +827,10 @@ public class AssociatedToUnassociatedPackedPixelConversionTests
const int pairCount = 65536;
const int channelCount = 3;
// HalfVector4 maps scaled zero to native -1, whose IEEE 754 binary16 representation is 0xBC00.
const ulong zeroComponentBits = 0xBC00;
const float finiteMinimum = -65504F;
const float finiteRange = 131008F;
const float inverseFiniteRange = (float)(1D / finiteRange);
const ulong zeroComponentBits = 0xFBFF;
Rgba32[] source = new Rgba32[pairCount * channelCount];
HalfVector4P[] expected = new HalfVector4P[source.Length];
HalfVector4P[] actualBulk = new HalfVector4P[source.Length];
@ -833,18 +838,16 @@ public class AssociatedToUnassociatedPackedPixelConversionTests
for (int alpha = 0; alpha <= byte.MaxValue; alpha++)
{
// HalfVector4 stores normalized values over -1 through 1. Association must use the alpha value
// recovered from the destination half, rather than the higher-precision source alpha.
// Association must use the alpha value recovered from the destination half, rather than the higher-precision source alpha.
float normalizedAlpha = (float)(alpha / (double)byte.MaxValue);
float nativeAlpha = (normalizedAlpha * 2F) - 1F;
ushort alphaBits = BitConverter.HalfToUInt16Bits((Half)nativeAlpha);
float destinationAlpha = ((float)BitConverter.UInt16BitsToHalf(alphaBits) + 1F) * .5F;
ushort alphaBits = BitConverter.HalfToUInt16Bits((Half)((normalizedAlpha * finiteRange) + finiteMinimum));
float destinationAlpha = ((float)BitConverter.UInt16BitsToHalf(alphaBits) * inverseFiniteRange) + .5F;
for (int unassociated = 0; unassociated <= byte.MaxValue; unassociated++)
{
float normalizedComponent = (float)(unassociated / (double)byte.MaxValue);
float associated = normalizedComponent * destinationAlpha;
ushort associatedBits = BitConverter.HalfToUInt16Bits((Half)((associated * 2F) - 1F));
ushort associatedBits = BitConverter.HalfToUInt16Bits((Half)((associated * finiteRange) + finiteMinimum));
ulong alphaPacked = (ulong)alphaBits << 48;
source[index] = new Rgba32((byte)unassociated, 0, 0, (byte)alpha);
@ -1184,6 +1187,9 @@ public class AssociatedDestinationAlphaQuantizationTests
[Fact]
public void HalfVector4PQuantizesDestinationAlphaBeforeAssociation()
{
const float finiteMinimum = -65504F;
const float finiteRange = 131008F;
const float inverseFiniteRange = (float)(1D / finiteRange);
ReadOnlySpan<byte> components = [64, 127, 191];
Rgba64[] source = new Rgba64[(ushort.MaxValue + 1) * components.Length];
HalfVector4P[] actualBulk = new HalfVector4P[source.Length];
@ -1202,14 +1208,14 @@ public class AssociatedDestinationAlphaQuantizationTests
for (int alpha = 0; alpha <= ushort.MaxValue; alpha++)
{
float nativeAlpha = ((alpha / (float)ushort.MaxValue) * 2F) - 1F;
ushort expectedAlpha = BitConverter.HalfToUInt16Bits((Half)nativeAlpha);
float storedAlpha = ((float)BitConverter.UInt16BitsToHalf(expectedAlpha) + 1F) / 2F;
float normalizedAlpha = alpha / (float)ushort.MaxValue;
ushort expectedAlpha = BitConverter.HalfToUInt16Bits((Half)((normalizedAlpha * finiteRange) + finiteMinimum));
float storedAlpha = ((float)BitConverter.UInt16BitsToHalf(expectedAlpha) * inverseFiniteRange) + .5F;
foreach (byte component in components)
{
float associatedRed = ((component / (float)byte.MaxValue) * storedAlpha * 2F) - 1F;
ushort expectedRed = BitConverter.HalfToUInt16Bits((Half)associatedRed);
float associatedRed = (component / (float)byte.MaxValue) * storedAlpha;
ushort expectedRed = BitConverter.HalfToUInt16Bits((Half)((associatedRed * finiteRange) + finiteMinimum));
HalfVector4P actualScalar = HalfVector4P.FromRgba64(source[index]);
Assert.Equal(expectedRed, (ushort)actualScalar.PackedValue);
@ -1221,6 +1227,45 @@ public class AssociatedDestinationAlphaQuantizationTests
}
}
[Fact]
public void RgbaHalfPQuantizesDestinationAlphaBeforeAssociation()
{
ReadOnlySpan<byte> components = [64, 127, 191];
Rgba64[] source = new Rgba64[(ushort.MaxValue + 1) * components.Length];
RgbaHalfP[] actualBulk = new RgbaHalfP[source.Length];
int index = 0;
for (int alpha = 0; alpha <= ushort.MaxValue; alpha++)
{
foreach (byte component in components)
{
source[index++] = new Rgba64((ushort)(component * 257), 0, 0, (ushort)alpha);
}
}
PixelOperations<RgbaHalfP>.Instance.From<Rgba64>(Configuration.Default, source, actualBulk);
index = 0;
for (int alpha = 0; alpha <= ushort.MaxValue; alpha++)
{
float normalizedAlpha = alpha / (float)ushort.MaxValue;
Half expectedAlpha = (Half)normalizedAlpha;
float storedAlpha = (float)expectedAlpha;
foreach (byte component in components)
{
Half expectedRed = (Half)((component / (float)byte.MaxValue) * storedAlpha);
RgbaHalfP actualScalar = RgbaHalfP.FromRgba64(source[index]);
Assert.Equal(expectedRed, actualScalar.R);
Assert.Equal(expectedAlpha, actualScalar.A);
Assert.Equal(expectedRed, actualBulk[index].R);
Assert.Equal(expectedAlpha, actualBulk[index].A);
index++;
}
}
}
[Fact]
public void ColorToRgba32PUsesDestinationAlphaRepresentation() => AssertColorUsesDestinationAlphaRepresentation<Rgba32P>();
@ -1239,6 +1284,9 @@ public class AssociatedDestinationAlphaQuantizationTests
[Fact]
public void ColorToHalfVector4PUsesDestinationAlphaRepresentation() => AssertColorUsesDestinationAlphaRepresentation<HalfVector4P>();
[Fact]
public void ColorToRgbaHalfPUsesDestinationAlphaRepresentation() => AssertColorUsesDestinationAlphaRepresentation<RgbaHalfP>();
/// <summary>
/// Verifies the unsigned-byte destination grid through scalar and bulk conversion entry points.
/// </summary>

54
tests/ImageSharp.Tests/PixelFormats/HalfSingleTests.cs

@ -26,45 +26,57 @@ public class HalfSingleTests
public void HalfSingle_ToVector4()
{
// arrange
HalfSingle pixel = new(0.5f);
Vector4 expected = new(0.5f, 0, 0, 1);
HalfSingle pixel = new(-2F);
Vector4 expected = new(-2F, 0, 0, 1);
// act
Vector4 actual = pixel.ToVector4();
// assert
Assert.Equal(expected, actual);
Assert.Equal((float)Half.MinValue, new HalfSingle((float)Half.MinValue).ToSingle());
Assert.Equal((float)Half.MaxValue, new HalfSingle((float)Half.MaxValue).ToSingle());
}
[Fact]
public void HalfSingle_ToScaledVector4()
{
// arrange
HalfSingle pixel = new(-1F);
// act
Vector4 actual = pixel.ToScaledVector4();
// assert
Assert.Equal(0, actual.X);
Assert.Equal(0, actual.Y);
Assert.Equal(0, actual.Z);
Assert.Equal(1, actual.W);
Assert.Equal(new Vector4(0F, 0F, 0F, 1F), new HalfSingle((float)Half.MinValue).ToScaledVector4());
Assert.Equal(new Vector4(.5F, 0F, 0F, 1F), new HalfSingle(0F).ToScaledVector4());
Assert.Equal(new Vector4(1F, 0F, 0F, 1F), new HalfSingle((float)Half.MaxValue).ToScaledVector4());
}
[Fact]
public void HalfSingle_FromScaledVector4()
{
// arrange
Vector4 scaled = new HalfSingle(-1F).ToScaledVector4();
const int expected = 48128;
Assert.Equal((ushort)0xFBFF, HalfSingle.FromScaledVector4(new Vector4(0F, 0F, 0F, 1F)).PackedValue);
Assert.Equal((ushort)0, HalfSingle.FromScaledVector4(new Vector4(.5F, 0F, 0F, 1F)).PackedValue);
Assert.Equal((ushort)0x7BFF, HalfSingle.FromScaledVector4(new Vector4(1F, 0F, 0F, 1F)).PackedValue);
}
// act
HalfSingle pixel = HalfSingle.FromScaledVector4(scaled);
ushort actual = pixel.PackedValue;
[Fact]
public void HalfSingle_BulkScaledConversionsCoverFiniteRange()
{
ushort[] packedValues = [0xFBFF, 0, 0x7BFF];
Vector4[] scaledValues = [new(0F, 0F, 0F, 1F), new(.5F, 0F, 0F, 1F), new(1F, 0F, 0F, 1F)];
HalfSingle[] source = new HalfSingle[17];
Vector4[] expectedVectors = new Vector4[source.Length];
// assert
Assert.Equal(expected, actual);
for (int i = 0; i < source.Length; i++)
{
int valueIndex = i % packedValues.Length;
source[i].PackedValue = packedValues[valueIndex];
expectedVectors[i] = scaledValues[valueIndex];
}
Vector4[] actualVectors = new Vector4[source.Length];
PixelOperations<HalfSingle>.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale);
Assert.Equal(expectedVectors, actualVectors);
Vector4[] destructiveSource = [.. expectedVectors];
HalfSingle[] actualPixels = new HalfSingle[source.Length];
PixelOperations<HalfSingle>.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale);
Assert.Equal(source, actualPixels);
}
[Fact]

29
tests/ImageSharp.Tests/PixelFormats/HalfVector2Tests.cs

@ -25,19 +25,21 @@ public class HalfVector2Tests
Assert.Equal(Vector2.One, new HalfVector2(Vector2.One).ToVector2());
Assert.Equal(Vector2.UnitX, new HalfVector2(Vector2.UnitX).ToVector2());
Assert.Equal(Vector2.UnitY, new HalfVector2(Vector2.UnitY).ToVector2());
Assert.Equal(new Vector2(-2F, 4F), new HalfVector2(-2F, 4F).ToVector2());
Assert.Equal(new Vector2((float)Half.MinValue, (float)Half.MaxValue), new HalfVector2((float)Half.MinValue, (float)Half.MaxValue).ToVector2());
}
[Fact]
public void HalfVector2_ToScaledVector4()
{
// arrange
HalfVector2 halfVector = new(Vector2.One);
HalfVector2 halfVector = new((float)Half.MinValue, (float)Half.MaxValue);
// act
Vector4 actual = halfVector.ToScaledVector4();
// assert
Assert.Equal(1F, actual.X);
Assert.Equal(0F, actual.X);
Assert.Equal(1F, actual.Y);
Assert.Equal(0, actual.Z);
Assert.Equal(1, actual.W);
@ -47,8 +49,8 @@ public class HalfVector2Tests
public void HalfVector2_FromScaledVector4()
{
// arrange
Vector4 scaled = new HalfVector2(Vector2.One).ToScaledVector4();
const uint expected = 1006648320u;
Vector4 scaled = new(0F, 1F, 0F, 1F);
const uint expected = 0x7BFF_FBFF;
// act
HalfVector2 halfVector = HalfVector2.FromScaledVector4(scaled);
@ -58,6 +60,25 @@ public class HalfVector2Tests
Assert.Equal(expected, actual);
}
[Fact]
public void HalfVector2_BulkScaledConversionsCoverFiniteRange()
{
HalfVector2 pixel = new() { PackedValue = 0x7BFF_FBFF };
HalfVector2[] source = new HalfVector2[17];
Vector4[] expectedVectors = new Vector4[source.Length];
Array.Fill(source, pixel);
Array.Fill(expectedVectors, new Vector4(0F, 1F, 0F, 1F));
Vector4[] actualVectors = new Vector4[source.Length];
PixelOperations<HalfVector2>.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale);
Assert.Equal(expectedVectors, actualVectors);
Vector4[] destructiveSource = [.. expectedVectors];
HalfVector2[] actualPixels = new HalfVector2[source.Length];
PixelOperations<HalfVector2>.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale);
Assert.Equal(source, actualPixels);
}
[Fact]
public void HalfVector2_ToVector4()
{

36
tests/ImageSharp.Tests/PixelFormats/HalfVector4Tests.cs

@ -33,30 +33,32 @@ public class HalfVector4Tests
Assert.Equal(Vector4.UnitY, new HalfVector4(Vector4.UnitY).ToVector4());
Assert.Equal(Vector4.UnitZ, new HalfVector4(Vector4.UnitZ).ToVector4());
Assert.Equal(Vector4.UnitW, new HalfVector4(Vector4.UnitW).ToVector4());
Assert.Equal(new Vector4(-2F, 4F, .5F, 8F), new HalfVector4(-2F, 4F, .5F, 8F).ToVector4());
Assert.Equal(
new Vector4((float)Half.MinValue, (float)Half.MaxValue, (float)Half.MinValue, (float)Half.MaxValue),
new HalfVector4((float)Half.MinValue, (float)Half.MaxValue, (float)Half.MinValue, (float)Half.MaxValue).ToVector4());
}
[Fact]
public void HalfVector4_ToScaledVector4()
{
// arrange
HalfVector4 pixel = new(-Vector4.One);
Vector4 expected = new(0F, .5F, 1F, 0F);
HalfVector4 pixel = new((float)Half.MinValue, 0F, (float)Half.MaxValue, (float)Half.MinValue);
// act
Vector4 actual = pixel.ToScaledVector4();
// assert
Assert.Equal(0, actual.X);
Assert.Equal(0, actual.Y);
Assert.Equal(0, actual.Z);
Assert.Equal(0, actual.W);
Assert.Equal(expected, actual);
}
[Fact]
public void HalfVector4_FromScaledVector4()
{
// arrange
Vector4 scaled = new HalfVector4(-Vector4.One).ToScaledVector4();
const ulong expected = 13547034390470638592uL;
Vector4 scaled = new(0F, .25F, .5F, 1F);
ulong expected = new HalfVector4((float)Half.MinValue, -32752F, 0F, (float)Half.MaxValue).PackedValue;
// act
HalfVector4 pixel = HalfVector4.FromScaledVector4(scaled);
@ -66,6 +68,26 @@ public class HalfVector4Tests
Assert.Equal(expected, actual);
}
[Fact]
public void HalfVector4_BulkScaledConversionsCoverFiniteRange()
{
const ulong packedValue = 0xFBFF_7BFF_0000_FBFF;
HalfVector4 pixel = new() { PackedValue = packedValue };
HalfVector4[] source = new HalfVector4[17];
Vector4[] expectedVectors = new Vector4[source.Length];
Array.Fill(source, pixel);
Array.Fill(expectedVectors, new Vector4(0F, .5F, 1F, 0F));
Vector4[] actualVectors = new Vector4[source.Length];
PixelOperations<HalfVector4>.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale);
Assert.Equal(expectedVectors, actualVectors);
Vector4[] destructiveSource = [.. expectedVectors];
HalfVector4[] actualPixels = new HalfVector4[source.Length];
PixelOperations<HalfVector4>.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale);
Assert.Equal(source, actualPixels);
}
[Fact]
public void HalfVector4_FromBgra5551()
{

31
tests/ImageSharp.Tests/PixelFormats/NormalizedByte2Tests.cs

@ -29,6 +29,37 @@ public class NormalizedByte2Tests
Assert.Equal(-Vector2.One, new NormalizedByte2(Vector2.One * -1234.0f).ToVector2());
}
[Fact]
public void NormalizedByte2_MinimumStorageCodeDecodesAsNegativeOne()
{
NormalizedByte2 pixel = new() { PackedValue = 0x8080 };
Vector4 expectedNative = new(-1F, -1F, 0F, 1F);
Vector4 expectedScaled = new(0F, 0F, 0F, 1F);
Assert.Equal(expectedNative, pixel.ToVector4());
Assert.Equal(expectedScaled, pixel.ToScaledVector4());
NormalizedByte2[] source = new NormalizedByte2[17];
Vector4[] native = new Vector4[source.Length];
Vector4[] scaled = new Vector4[source.Length];
Array.Fill(source, pixel);
PixelOperations<NormalizedByte2>.Instance.ToVector4(Configuration.Default, source, native);
PixelOperations<NormalizedByte2>.Instance.ToVector4(Configuration.Default, source, scaled, PixelConversionModifiers.Scale);
for (int i = 0; i < source.Length; i++)
{
Assert.Equal(expectedNative, native[i]);
Assert.Equal(expectedScaled, scaled[i]);
}
Vector4[] destructiveSource = new Vector4[source.Length];
Array.Fill(destructiveSource, expectedScaled);
NormalizedByte2[] actualPixels = new NormalizedByte2[source.Length];
PixelOperations<NormalizedByte2>.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale);
Assert.All(actualPixels, actual => Assert.Equal((ushort)0x8181, actual.PackedValue));
}
[Fact]
public void NormalizedByte2_ToVector4()
{

28
tests/ImageSharp.Tests/PixelFormats/NormalizedByte4Tests.cs

@ -60,6 +60,34 @@ public class NormalizedByte4Tests
Assert.Equal(-Vector4.One, new NormalizedByte4(Vector4.One * -1234.0f).ToVector4());
}
[Fact]
public void NormalizedByte4_MinimumStorageCodeDecodesAsNegativeOne()
{
NormalizedByte4 pixel = new() { PackedValue = 0x80808080 };
Assert.Equal(-Vector4.One, pixel.ToVector4());
Assert.Equal(Vector4.Zero, pixel.ToScaledVector4());
NormalizedByte4[] source = new NormalizedByte4[17];
Vector4[] native = new Vector4[source.Length];
Vector4[] scaled = new Vector4[source.Length];
Array.Fill(source, pixel);
PixelOperations<NormalizedByte4>.Instance.ToVector4(Configuration.Default, source, native);
PixelOperations<NormalizedByte4>.Instance.ToVector4(Configuration.Default, source, scaled, PixelConversionModifiers.Scale);
for (int i = 0; i < source.Length; i++)
{
Assert.Equal(-Vector4.One, native[i]);
Assert.Equal(Vector4.Zero, scaled[i]);
}
Vector4[] destructiveSource = new Vector4[source.Length];
NormalizedByte4[] actualPixels = new NormalizedByte4[source.Length];
PixelOperations<NormalizedByte4>.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale);
Assert.All(actualPixels, actual => Assert.Equal(0x81818181U, actual.PackedValue));
}
[Fact]
public void NormalizedByte4_ToScaledVector4()
{

31
tests/ImageSharp.Tests/PixelFormats/NormalizedShort2Tests.cs

@ -33,6 +33,37 @@ public class NormalizedShort2Tests
Assert.Equal(-Vector2.One, new NormalizedShort2(Vector2.One * -1234.0f).ToVector2());
}
[Fact]
public void NormalizedShort2_MinimumStorageCodeDecodesAsNegativeOne()
{
NormalizedShort2 pixel = new() { PackedValue = 0x80008000 };
Vector4 expectedNative = new(-1F, -1F, 0F, 1F);
Vector4 expectedScaled = new(0F, 0F, 0F, 1F);
Assert.Equal(expectedNative, pixel.ToVector4());
Assert.Equal(expectedScaled, pixel.ToScaledVector4());
NormalizedShort2[] source = new NormalizedShort2[17];
Vector4[] native = new Vector4[source.Length];
Vector4[] scaled = new Vector4[source.Length];
Array.Fill(source, pixel);
PixelOperations<NormalizedShort2>.Instance.ToVector4(Configuration.Default, source, native);
PixelOperations<NormalizedShort2>.Instance.ToVector4(Configuration.Default, source, scaled, PixelConversionModifiers.Scale);
for (int i = 0; i < source.Length; i++)
{
Assert.Equal(expectedNative, native[i]);
Assert.Equal(expectedScaled, scaled[i]);
}
Vector4[] destructiveSource = new Vector4[source.Length];
Array.Fill(destructiveSource, expectedScaled);
NormalizedShort2[] actualPixels = new NormalizedShort2[source.Length];
PixelOperations<NormalizedShort2>.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale);
Assert.All(actualPixels, actual => Assert.Equal(0x80018001U, actual.PackedValue));
}
[Fact]
public void NormalizedShort2_ToVector4()
{

28
tests/ImageSharp.Tests/PixelFormats/NormalizedShort4Tests.cs

@ -61,6 +61,34 @@ public class NormalizedShort4Tests
Assert.Equal(-Vector4.One, new NormalizedShort4(Vector4.One * -1234.0f).ToVector4());
}
[Fact]
public void NormalizedShort4_MinimumStorageCodeDecodesAsNegativeOne()
{
NormalizedShort4 pixel = new() { PackedValue = 0x8000800080008000 };
Assert.Equal(-Vector4.One, pixel.ToVector4());
Assert.Equal(Vector4.Zero, pixel.ToScaledVector4());
NormalizedShort4[] source = new NormalizedShort4[17];
Vector4[] native = new Vector4[source.Length];
Vector4[] scaled = new Vector4[source.Length];
Array.Fill(source, pixel);
PixelOperations<NormalizedShort4>.Instance.ToVector4(Configuration.Default, source, native);
PixelOperations<NormalizedShort4>.Instance.ToVector4(Configuration.Default, source, scaled, PixelConversionModifiers.Scale);
for (int i = 0; i < source.Length; i++)
{
Assert.Equal(-Vector4.One, native[i]);
Assert.Equal(Vector4.Zero, scaled[i]);
}
Vector4[] destructiveSource = new Vector4[source.Length];
NormalizedShort4[] actualPixels = new NormalizedShort4[source.Length];
PixelOperations<NormalizedShort4>.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale);
Assert.All(actualPixels, actual => Assert.Equal(0x8001800180018001UL, actual.PackedValue));
}
[Fact]
public void NormalizedShort4_ToScaledVector4()
{

16
tests/ImageSharp.Tests/PixelFormats/PixelAlphaRepresentationTests.cs

@ -308,7 +308,7 @@ public abstract class PixelAlphaRepresentationTests<TPixel>
}
/// <summary>
/// Verifies alpha conversion for formats whose native vector space differs from scaled color space.
/// Verifies alpha conversion for formats with distinct native vector contracts.
/// </summary>
[Trait("Category", "PixelFormats")]
public class AffineNativeAlphaRepresentationTests
@ -321,7 +321,7 @@ public class AffineNativeAlphaRepresentationTests
[Fact]
public void Short4NativeConversionsUseScaledAlpha()
=> AssertNativeConversions<Short4>(static vector => (vector * 65534F) - new Vector4(32767F));
=> AssertNativeConversions<Short4>(static vector => (vector * ushort.MaxValue) - new Vector4(32768F));
[Fact]
public void NormalizedByte4NativeConversionsUseScaledAlpha()
@ -337,19 +337,19 @@ public class AffineNativeAlphaRepresentationTests
[Fact]
public void HalfVector4NativeConversionsUseScaledAlpha()
=> AssertNativeConversions<HalfVector4>(static vector => (vector * 2F) - Vector4.One);
=> AssertNativeConversions<HalfVector4>(static vector => (vector * 131008F) - new Vector4(65504F));
[Fact]
public void HalfVector4PNativeConversionsUseScaledAlpha()
=> AssertNativeConversions<HalfVector4P>(static vector => (vector * 2F) - Vector4.One);
=> AssertNativeConversions<HalfVector4P>(static vector => (vector * 131008F) - new Vector4(65504F));
[Fact]
public void HalfSingleFromAssociatedNativeVectorUsesScaledAlpha()
=> AssertAlphaLessNativeFrom<HalfSingle>(static vector => new Vector4((vector.X * 2F) - 1F, 0F, 0F, vector.W));
=> AssertAlphaLessNativeFrom<HalfSingle>(static vector => new Vector4((vector.X * 131008F) - 65504F, 0F, 0F, vector.W));
[Fact]
public void HalfVector2FromAssociatedNativeVectorUsesScaledAlpha()
=> AssertAlphaLessNativeFrom<HalfVector2>(static vector => new Vector4((vector.X * 2F) - 1F, (vector.Y * 2F) - 1F, 0F, vector.W));
=> AssertAlphaLessNativeFrom<HalfVector2>(static vector => new Vector4((vector.X * 131008F) - 65504F, (vector.Y * 131008F) - 65504F, 0F, vector.W));
[Fact]
public void NormalizedByte2FromAssociatedNativeVectorUsesScaledAlpha()
@ -361,7 +361,7 @@ public class AffineNativeAlphaRepresentationTests
[Fact]
public void Short2FromAssociatedNativeVectorUsesScaledAlpha()
=> AssertAlphaLessNativeFrom<Short2>(static vector => new Vector4((vector.X * 65534F) - 32767F, (vector.Y * 65534F) - 32767F, 0F, vector.W));
=> AssertAlphaLessNativeFrom<Short2>(static vector => new Vector4((vector.X * ushort.MaxValue) - 32768F, (vector.Y * ushort.MaxValue) - 32768F, 0F, vector.W));
private static void AssertNativeConversions<TPixel>(Func<Vector4, Vector4> encodeNative)
where TPixel : unmanaged, IPixel<TPixel>
@ -486,6 +486,8 @@ public class Rgba1010102AlphaRepresentationTests : PixelAlphaRepresentationTests
public class Rgba128AlphaRepresentationTests : PixelAlphaRepresentationTests<Rgba128> { }
public class Rgba32AlphaRepresentationTests : PixelAlphaRepresentationTests<Rgba32> { }
public class Rgba32PAlphaRepresentationTests : PixelAlphaRepresentationTests<Rgba32P> { }
public class RgbaHalfAlphaRepresentationTests : PixelAlphaRepresentationTests<RgbaHalf> { }
public class RgbaHalfPAlphaRepresentationTests : PixelAlphaRepresentationTests<RgbaHalfP> { }
public class Rgba64AlphaRepresentationTests : PixelAlphaRepresentationTests<Rgba64> { }
public class RgbaVectorAlphaRepresentationTests : PixelAlphaRepresentationTests<RgbaVector> { }
public class Short2AlphaRepresentationTests : PixelAlphaRepresentationTests<Short2> { }

30
tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/PixelOperationsTests.Specialized.Generated.cs

@ -492,6 +492,36 @@ public partial class PixelOperationsTests
}
}
public partial class RgbaHalf_OperationsTests : PixelOperationsTests<RgbaHalf>
{
public RgbaHalf_OperationsTests(ITestOutputHelper output)
: base(output)
{
}
[Fact]
public void PixelTypeInfoHasCorrectAlphaRepresentation()
{
var alphaRepresentation = RgbaHalf.GetPixelTypeInfo().AlphaRepresentation;
Assert.Equal(PixelAlphaRepresentation.Unassociated, alphaRepresentation);
}
}
public partial class RgbaHalfP_OperationsTests : PixelOperationsTests<RgbaHalfP>
{
public RgbaHalfP_OperationsTests(ITestOutputHelper output)
: base(output)
{
}
[Fact]
public void PixelTypeInfoHasCorrectAlphaRepresentation()
{
var alphaRepresentation = RgbaHalfP.GetPixelTypeInfo().AlphaRepresentation;
Assert.Equal(PixelAlphaRepresentation.Associated, alphaRepresentation);
}
}
public partial class RgbaVector_OperationsTests : PixelOperationsTests<RgbaVector>
{
public RgbaVector_OperationsTests(ITestOutputHelper output)

6
tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/_Common.ttinclude

@ -29,6 +29,7 @@ using Xunit.Abstractions;
"Rgba1010102",
"Rgba32",
"Rgba64",
"RgbaHalf",
"RgbaVector",
"Short4"
];
@ -40,7 +41,8 @@ using Xunit.Abstractions;
"Bgra32P",
"HalfVector4P",
"NormalizedByte4P",
"Rgba32P"
"Rgba32P",
"RgbaHalfP"
];
private static readonly string[] CommonPixelTypes =
@ -77,6 +79,8 @@ using Xunit.Abstractions;
"Rgba32",
"Rgba32P",
"Rgba64",
"RgbaHalf",
"RgbaHalfP",
"RgbaVector",
"Short2",
"Short4"

61
tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs

@ -102,6 +102,35 @@ public abstract class PixelOperationsTests<TPixel> : MeasureFixture
return expected;
}
/// <summary>
/// Creates the scalar oracle for companded scaled input.
/// </summary>
/// <param name="source">The source vectors.</param>
/// <param name="associated">Whether the source vectors use associated alpha.</param>
/// <returns>The converted pixels.</returns>
internal static TPixel[] CreateCompandedExpectedPixelData(Vector4[] source, bool associated)
{
TPixel[] expected = new TPixel[source.Length];
for (int i = 0; i < source.Length; i++)
{
Vector4 vector = source[i];
if (associated)
{
Numerics.UnPremultiply(ref vector);
}
vector = SRgbCompanding.Compress(vector);
// Transfer functions operate on straight RGB. Let the destination pixel perform any required association so its stored
// alpha quantization participates exactly once, matching the public bulk conversion contract.
expected[i] = TPixel.FromUnassociatedScaledVector4(vector);
}
return expected;
}
[Fact]
public void PixelTypeInfoHasCorrectBitsPerPixel()
{
@ -171,23 +200,8 @@ public abstract class PixelOperationsTests<TPixel> : MeasureFixture
}
}
void ExpectedAction(ref Vector4 v)
{
if (this.HasAssociatedAlpha)
{
Numerics.UnPremultiply(ref v);
}
v = SRgbCompanding.Compress(v);
if (this.HasAssociatedAlpha)
{
Numerics.Premultiply(ref v);
}
}
Vector4[] source = CreateVector4TestData(count, SourceAction);
TPixel[] expected = CreateScaledExpectedPixelData(source, ExpectedAction);
TPixel[] expected = CreateCompandedExpectedPixelData(source, this.HasAssociatedAlpha);
TestOperation(
source,
@ -274,19 +288,8 @@ public abstract class PixelOperationsTests<TPixel> : MeasureFixture
Numerics.Premultiply(ref v);
}
void ExpectedAction(ref Vector4 v)
{
Numerics.UnPremultiply(ref v);
v = SRgbCompanding.Compress(v);
if (this.HasAssociatedAlpha)
{
Numerics.Premultiply(ref v);
}
}
Vector4[] source = CreateVector4TestData(count, SourceAction);
TPixel[] expected = CreateScaledExpectedPixelData(source, ExpectedAction);
TPixel[] expected = CreateCompandedExpectedPixelData(source, true);
TestOperation(
source,
@ -349,6 +352,8 @@ public abstract class PixelOperationsTests<TPixel> : MeasureFixture
new TestPixel<Rgba32>(),
new TestPixel<Rgba32P>(),
new TestPixel<Rgba64>(),
new TestPixel<RgbaHalf>(),
new TestPixel<RgbaHalfP>(),
new TestPixel<RgbaVector>(),
new TestPixel<Short2>(),
new TestPixel<Short4>(),

283
tests/ImageSharp.Tests/PixelFormats/RgbaHalfTests.cs

@ -0,0 +1,283 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.PixelFormats;
/// <summary>
/// Tests the unit-range binary16 RGBA pixel formats.
/// </summary>
[Trait("Category", "PixelFormats")]
public class RgbaHalfTests
{
/// <summary>
/// Verifies that the unassociated format has the native layout required by RGBA binary16 surfaces.
/// </summary>
[Fact]
public void RgbaHalfHasRgbaBinary16Layout()
{
RgbaHalf pixel = new(.25F, .5F, .75F, 1F);
ulong expected = BitConverter.HalfToUInt16Bits((Half).25F)
| ((ulong)BitConverter.HalfToUInt16Bits((Half).5F) << 16)
| ((ulong)BitConverter.HalfToUInt16Bits((Half).75F) << 32)
| ((ulong)BitConverter.HalfToUInt16Bits((Half)1F) << 48);
Assert.Equal(8, Unsafe.SizeOf<RgbaHalf>());
Assert.Equal(expected, pixel.PackedValue);
Assert.Equal(new Vector4(.25F, .5F, .75F, 1F), pixel.ToVector4());
Assert.Equal(pixel.ToVector4(), pixel.ToScaledVector4());
}
/// <summary>
/// Verifies that zero-filled binary16 storage represents transparent black without affine remapping.
/// </summary>
[Fact]
public void RgbaHalfDefaultIsTransparentBlack()
{
RgbaHalf pixel = default;
Assert.Equal(Vector4.Zero, pixel.ToVector4());
Assert.Equal(Vector4.Zero, pixel.ToScaledVector4());
}
/// <summary>
/// Verifies that scaled input is clamped to the pixel format's unit color range.
/// </summary>
[Fact]
public void RgbaHalfFromScaledVector4ClampsToUnitRange()
{
RgbaHalf pixel = RgbaHalf.FromScaledVector4(new Vector4(-1F, .5F, 2F, 1F));
Assert.Equal(new Vector4(0F, .5F, 1F, 1F), pixel.ToScaledVector4());
}
/// <summary>
/// Verifies that the associated format stores associated binary16 components in the same RGBA order.
/// </summary>
[Fact]
public void RgbaHalfPHasAssociatedRgbaBinary16Layout()
{
RgbaHalfP pixel = new(.125F, .25F, .375F, .5F);
ulong expected = BitConverter.HalfToUInt16Bits((Half).125F)
| ((ulong)BitConverter.HalfToUInt16Bits((Half).25F) << 16)
| ((ulong)BitConverter.HalfToUInt16Bits((Half).375F) << 32)
| ((ulong)BitConverter.HalfToUInt16Bits((Half).5F) << 48);
Assert.Equal(8, Unsafe.SizeOf<RgbaHalfP>());
Assert.Equal(expected, pixel.PackedValue);
Assert.Equal(new Vector4(.125F, .25F, .375F, .5F), pixel.ToAssociatedScaledVector4());
}
/// <summary>
/// Verifies that zero-filled associated binary16 storage represents transparent black.
/// </summary>
[Fact]
public void RgbaHalfPDefaultIsTransparentBlack()
{
RgbaHalfP pixel = default;
Assert.Equal(Vector4.Zero, pixel.ToVector4());
Assert.Equal(Vector4.Zero, pixel.ToScaledVector4());
Assert.Equal(Vector4.Zero, pixel.ToUnassociatedScaledVector4());
}
/// <summary>
/// Verifies that association uses the alpha value that survives binary16 quantization.
/// </summary>
[Fact]
public void RgbaHalfPQuantizesAlphaBeforeAssociation()
{
Vector4 source = new(.75F, .5F, .25F, 1F / 3F);
float storedAlpha = (float)(Half)source.W;
RgbaHalfP pixel = RgbaHalfP.FromUnassociatedScaledVector4(source);
Assert.Equal((Half)(source.X * storedAlpha), pixel.R);
Assert.Equal((Half)(source.Y * storedAlpha), pixel.G);
Assert.Equal((Half)(source.Z * storedAlpha), pixel.B);
Assert.Equal((Half)storedAlpha, pixel.A);
}
/// <summary>
/// Verifies every bulk representation path against its scalar pixel contract at each SIMD width and tail boundary.
/// </summary>
[Fact]
public void RgbaHalfBulkConversionsMatchScalarAcrossHardwareWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(
static () => AssertBulkConversionsMatchScalar<RgbaHalf>(),
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
/// <summary>
/// Verifies every associated bulk representation path against its scalar pixel contract at each SIMD width and tail boundary.
/// </summary>
[Fact]
public void RgbaHalfPBulkConversionsMatchScalarAcrossHardwareWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(
static () => AssertBulkConversionsMatchScalar<RgbaHalfP>(),
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
/// <summary>
/// Verifies the declared component precision and alpha representation for the unassociated format.
/// </summary>
[Fact]
public void RgbaHalfPixelInformationIsCorrect() => AssertPixelInformation<RgbaHalf>(PixelAlphaRepresentation.Unassociated);
/// <summary>
/// Verifies the declared component precision and alpha representation for the associated format.
/// </summary>
[Fact]
public void RgbaHalfPPixelInformationIsCorrect() => AssertPixelInformation<RgbaHalfP>(PixelAlphaRepresentation.Associated);
/// <summary>
/// Compares bulk conversions with the corresponding scalar pixel operations for representative vector widths and remainders.
/// </summary>
/// <typeparam name="TPixel">The binary16 pixel format to test.</typeparam>
private static void AssertBulkConversionsMatchScalar<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
int[] lengths = [0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 259];
PixelConversionModifiers[] modifiers =
[
PixelConversionModifiers.None,
PixelConversionModifiers.Scale,
PixelConversionModifiers.Premultiply,
PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply,
PixelConversionModifiers.UnPremultiply,
PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply
];
foreach (int length in lengths)
{
TPixel[] pixels = new TPixel[length];
for (int i = 0; i < length; i++)
{
pixels[i] = TPixel.FromUnassociatedScaledVector4(CreateUnassociatedVector(i));
}
foreach (PixelConversionModifiers modifier in modifiers)
{
Vector4[] expectedVectors = new Vector4[length];
Vector4[] actualVectors = new Vector4[length];
Vector4[] sourceVectors = new Vector4[length];
TPixel[] expectedPixels = new TPixel[length];
TPixel[] actualPixels = new TPixel[length];
bool associated = RequestsAssociated<TPixel>(modifier);
bool scaled = modifier.IsDefined(PixelConversionModifiers.Scale);
for (int i = 0; i < length; i++)
{
expectedVectors[i] = ToScalarVector(pixels[i], associated, scaled);
sourceVectors[i] = CreateUnassociatedVector(i + 19);
if (associated)
{
Numerics.Premultiply(ref sourceVectors[i]);
}
expectedPixels[i] = FromScalarVector<TPixel>(sourceVectors[i], associated, scaled);
}
PixelOperations<TPixel>.Instance.ToVector4(Configuration.Default, pixels, actualVectors, modifier);
PixelOperations<TPixel>.Instance.FromVector4Destructive(Configuration.Default, sourceVectors.AsSpan(), actualPixels, modifier);
Assert.Equal(expectedVectors, actualVectors);
Assert.Equal(expectedPixels, actualPixels);
}
}
}
/// <summary>
/// Creates a deterministic unassociated unit-range vector for bulk conversion tests.
/// </summary>
/// <param name="index">The source index used to vary the component values.</param>
/// <returns>The generated vector.</returns>
private static Vector4 CreateUnassociatedVector(int index)
=> new(
((index * 37) % 101) / 100F,
((index * 53) % 101) / 100F,
((index * 71) % 101) / 100F,
((index * 89) % 101) / 100F);
/// <summary>
/// Resolves whether a modifier combination requests associated output under the pixel format's native representation.
/// </summary>
/// <typeparam name="TPixel">The pixel format whose native representation participates in modifier resolution.</typeparam>
/// <param name="modifiers">The conversion modifiers.</param>
/// <returns><see langword="true"/> when the resulting vector representation is associated.</returns>
private static bool RequestsAssociated<TPixel>(PixelConversionModifiers modifiers)
where TPixel : unmanaged, IPixel<TPixel>
=> modifiers.IsDefined(PixelConversionModifiers.Premultiply)
|| (TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Associated
&& !modifiers.IsDefined(PixelConversionModifiers.UnPremultiply));
/// <summary>
/// Converts one pixel through the scalar API selected by the requested representation and range.
/// </summary>
/// <typeparam name="TPixel">The source pixel format.</typeparam>
/// <param name="pixel">The source pixel.</param>
/// <param name="associated">Whether the result should use associated alpha.</param>
/// <param name="scaled">Whether the result should use the scaled range.</param>
/// <returns>The converted vector.</returns>
private static Vector4 ToScalarVector<TPixel>(TPixel pixel, bool associated, bool scaled)
where TPixel : unmanaged, IPixel<TPixel>
=> (associated, scaled) switch
{
(true, true) => pixel.ToAssociatedScaledVector4(),
(true, false) => pixel.ToAssociatedVector4(),
(false, true) => pixel.ToUnassociatedScaledVector4(),
_ => pixel.ToUnassociatedVector4()
};
/// <summary>
/// Converts one vector through the scalar API selected by its representation and range.
/// </summary>
/// <typeparam name="TPixel">The destination pixel format.</typeparam>
/// <param name="vector">The source vector.</param>
/// <param name="associated">Whether the source uses associated alpha.</param>
/// <param name="scaled">Whether the source uses the scaled range.</param>
/// <returns>The converted pixel.</returns>
private static TPixel FromScalarVector<TPixel>(Vector4 vector, bool associated, bool scaled)
where TPixel : unmanaged, IPixel<TPixel>
=> (associated, scaled) switch
{
(true, true) => TPixel.FromAssociatedScaledVector4(vector),
(true, false) => TPixel.FromAssociatedVector4(vector),
(false, true) => TPixel.FromUnassociatedScaledVector4(vector),
_ => TPixel.FromUnassociatedVector4(vector)
};
/// <summary>
/// Verifies the component layout and alpha metadata exposed by a binary16 pixel format.
/// </summary>
/// <typeparam name="TPixel">The pixel format to inspect.</typeparam>
/// <param name="alphaRepresentation">The expected alpha representation.</param>
private static void AssertPixelInformation<TPixel>(PixelAlphaRepresentation alphaRepresentation)
where TPixel : unmanaged, IPixel<TPixel>
{
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
PixelComponentInfo componentInfo = info.ComponentInfo.Value;
Assert.Equal(64, info.BitsPerPixel);
Assert.Equal(alphaRepresentation, info.AlphaRepresentation);
Assert.Equal(PixelColorType.RGB | PixelColorType.Alpha, info.ColorType);
Assert.Equal(4, componentInfo.ComponentCount);
Assert.Equal(0, componentInfo.Padding);
for (int i = 0; i < componentInfo.ComponentCount; i++)
{
Assert.Equal(16, componentInfo.GetComponentPrecision(i));
}
}
}
/// <summary>
/// Applies the shared associated-alpha contract to <see cref="RgbaHalfP"/>.
/// </summary>
public class RgbaHalfPAssociatedAlphaTests : AssociatedAlphaPixelTests<RgbaHalfP>
{
}

48
tests/ImageSharp.Tests/PixelFormats/Short2Tests.cs

@ -51,33 +51,39 @@ public class Short2Tests
[Fact]
public void Short2_ToScaledVector4()
{
// arrange
Short2 short2 = new(Vector2.One * 0x7FFF);
// act
Vector4 actual = short2.ToScaledVector4();
// assert
Assert.Equal(1, actual.X);
Assert.Equal(1, actual.Y);
Assert.Equal(0, actual.Z);
Assert.Equal(1, actual.W);
Assert.Equal(new Vector4(1F, 1F, 0F, 1F), new Short2(Vector2.One * short.MaxValue).ToScaledVector4());
Assert.Equal(new Vector4(0F, 0F, 0F, 1F), new Short2(Vector2.One * short.MinValue).ToScaledVector4());
}
[Fact]
public void Short2_FromScaledVector4()
{
// arrange
Short2 short2 = new(Vector2.One * 0x7FFF);
const ulong expected = 0x7FFF7FFF;
// act
Vector4 scaled = short2.ToScaledVector4();
Short2 pixel = Short2.FromScaledVector4(scaled);
uint actual = pixel.PackedValue;
Assert.Equal(0x80008000U, Short2.FromScaledVector4(new Vector4(0F, 0F, 0F, 1F)).PackedValue);
Assert.Equal(0x7FFF7FFFU, Short2.FromScaledVector4(Vector4.One).PackedValue);
}
// assert
Assert.Equal(expected, actual);
[Fact]
public void Short2_BulkScaledConversionsCoverFullSignedRange()
{
const int length = 17;
Short2[] source = new Short2[length];
Vector4[] expectedVectors = new Vector4[length];
for (int i = 0; i < length; i++)
{
bool minimum = (i & 1) == 0;
source[i].PackedValue = minimum ? 0x80008000U : 0x7FFF7FFFU;
expectedVectors[i] = minimum ? new Vector4(0F, 0F, 0F, 1F) : new Vector4(1F, 1F, 0F, 1F);
}
Vector4[] actualVectors = new Vector4[length];
PixelOperations<Short2>.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale);
Assert.Equal(expectedVectors, actualVectors);
Vector4[] destructiveSource = [.. expectedVectors];
Short2[] actualPixels = new Short2[length];
PixelOperations<Short2>.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale);
Assert.Equal(source, actualPixels);
}
[Fact]

47
tests/ImageSharp.Tests/PixelFormats/Short4Tests.cs

@ -38,32 +38,39 @@ public class Short4Tests
[Fact]
public void Short4_ToScaledVector4()
{
// arrange
Short4 short4 = new(Vector4.One * 0x7FFF);
// act
Vector4 actual = short4.ToScaledVector4();
// assert
Assert.Equal(1, actual.X);
Assert.Equal(1, actual.Y);
Assert.Equal(1, actual.Z);
Assert.Equal(1, actual.W);
Assert.Equal(Vector4.One, new Short4(Vector4.One * short.MaxValue).ToScaledVector4());
Assert.Equal(Vector4.Zero, new Short4(Vector4.One * short.MinValue).ToScaledVector4());
}
[Fact]
public void Short4_FromScaledVector4()
{
// arrange
Short4 short4 = new(Vector4.One * 0x7FFF);
Vector4 scaled = short4.ToScaledVector4();
const long expected = 0x7FFF7FFF7FFF7FFF;
// act
Short4 pixel = Short4.FromScaledVector4(scaled);
Assert.Equal(0x8000800080008000UL, Short4.FromScaledVector4(Vector4.Zero).PackedValue);
Assert.Equal(0x7FFF7FFF7FFF7FFFUL, Short4.FromScaledVector4(Vector4.One).PackedValue);
}
// assert
Assert.Equal((ulong)expected, pixel.PackedValue);
[Fact]
public void Short4_BulkScaledConversionsCoverFullSignedRange()
{
const int length = 17;
Short4[] source = new Short4[length];
Vector4[] expectedVectors = new Vector4[length];
for (int i = 0; i < length; i++)
{
bool minimum = (i & 1) == 0;
source[i].PackedValue = minimum ? 0x8000800080008000UL : 0x7FFF7FFF7FFF7FFFUL;
expectedVectors[i] = minimum ? Vector4.Zero : Vector4.One;
}
Vector4[] actualVectors = new Vector4[length];
PixelOperations<Short4>.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale);
Assert.Equal(expectedVectors, actualVectors);
Vector4[] destructiveSource = [.. expectedVectors];
Short4[] actualPixels = new Short4[length];
PixelOperations<Short4>.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale);
Assert.Equal(source, actualPixels);
}
[Fact]

7
tests/ImageSharp.Tests/Processing/Filters/BrightnessTest.cs

@ -43,13 +43,14 @@ public class BrightnessTest : BaseImageOperationsExtensionTest
Assert.Equal(new Rgb24(20, 20, 20), rgbImage[0, 0]);
Image<HalfSingle> halfSingleImage = new(Configuration.Default, 100, 100, new HalfSingle(-1));
// HalfSingle normalizes the complete finite binary16 interval, making -65504 logical zero and -32752 logical .25.
Image<HalfSingle> halfSingleImage = new(Configuration.Default, 100, 100, new HalfSingle((float)Half.MinValue));
halfSingleImage.Mutate(x => x.ApplyProcessor(new BrightnessProcessor(2)));
Assert.Equal(new HalfSingle(-1), halfSingleImage[0, 0]);
Assert.Equal(new HalfSingle((float)Half.MinValue), halfSingleImage[0, 0]);
halfSingleImage = new Image<HalfSingle>(Configuration.Default, 100, 100, new HalfSingle(-0.5f));
halfSingleImage = new Image<HalfSingle>(Configuration.Default, 100, 100, new HalfSingle(-32752F));
halfSingleImage.Mutate(x => x.ApplyProcessor(new BrightnessProcessor(2)));

Loading…
Cancel
Save