From 2ffa9fb07bd15364cd2c8e52f34666da225d5d85 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 17 Jul 2026 11:01:44 +1000 Subject: [PATCH] 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. --- src/ImageSharp/Advanced/AotCompilerTools.cs | 4 + src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 12 +- src/ImageSharp/PixelFormats/HalfTypeHelper.cs | 57 ++ .../PixelFormats/PixelImplementations/A8.cs | 7 +- .../PixelImplementations/Bgr565.cs | 8 +- .../PixelImplementations/Bgra4444.cs | 9 +- .../PixelImplementations/Bgra5551.cs | 11 +- .../PixelImplementations/Byte4.cs | 9 +- .../PixelImplementations/HalfSingle.cs | 26 +- .../PixelImplementations/HalfVector2.cs | 26 +- .../PixelImplementations/HalfVector4.cs | 36 +- .../PixelImplementations/HalfVector4P.cs | 73 +- .../PixelImplementations/NormalizedByte2.cs | 36 +- .../PixelImplementations/NormalizedByte4.cs | 39 +- .../PixelImplementations/NormalizedByte4P.cs | 44 +- .../PixelImplementations/NormalizedShort2.cs | 35 +- .../PixelImplementations/NormalizedShort4.cs | 38 +- .../HalfSingle.PixelOperations.cs | 12 - .../HalfVector2.PixelOperations.cs | 12 - .../HalfVector4.PixelOperations.cs | 48 +- .../HalfVector4P.PixelOperations.cs | 701 ++++----------- .../NormalizedByte4P.PixelOperations.cs | 66 +- .../RgbaHalf.PixelOperations.cs | 72 ++ .../RgbaHalfP.PixelOperations.cs | 795 ++++++++++++++++++ .../PixelOperations/Short2.PixelOperations.cs | 6 +- .../PixelOperations/Short4.PixelOperations.cs | 4 +- .../PixelFormats/PixelImplementations/Rg32.cs | 9 +- .../PixelImplementations/Rgba1010102.cs | 9 +- .../PixelImplementations/Rgba64.cs | 9 +- .../PixelImplementations/RgbaHalf.cs | 227 +++++ .../PixelImplementations/RgbaHalfP.cs | 242 ++++++ .../PixelImplementations/Short2.cs | 25 +- .../PixelImplementations/Short4.cs | 30 +- .../Utils/SignedShort4PixelOperations.cs | 57 +- ...tor4Converters.AssociatedRgbaCompatible.cs | 19 +- .../PixelFormats/AssociatedAlphaPixelTests.cs | 96 ++- .../PixelFormats/HalfSingleTests.cs | 54 +- .../PixelFormats/HalfVector2Tests.cs | 29 +- .../PixelFormats/HalfVector4Tests.cs | 36 +- .../PixelFormats/NormalizedByte2Tests.cs | 31 + .../PixelFormats/NormalizedByte4Tests.cs | 28 + .../PixelFormats/NormalizedShort2Tests.cs | 31 + .../PixelFormats/NormalizedShort4Tests.cs | 28 + .../PixelAlphaRepresentationTests.cs | 16 +- ...elOperationsTests.Specialized.Generated.cs | 30 + .../Generated/_Common.ttinclude | 6 +- .../PixelOperations/PixelOperationsTests.cs | 61 +- .../PixelFormats/RgbaHalfTests.cs | 283 +++++++ .../PixelFormats/Short2Tests.cs | 48 +- .../PixelFormats/Short4Tests.cs | 47 +- .../Processing/Filters/BrightnessTest.cs | 7 +- 51 files changed, 2656 insertions(+), 988 deletions(-) create mode 100644 src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalf.PixelOperations.cs create mode 100644 src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs create mode 100644 src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalf.cs create mode 100644 src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalfP.cs create mode 100644 tests/ImageSharp.Tests/PixelFormats/RgbaHalfTests.cs diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index bccbcc11a5..6d37fd5ce4 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -114,6 +114,8 @@ internal static class AotCompilerTools Seed(); Seed(); Seed(); + Seed(); + Seed(); Seed(); Seed(); Seed(); @@ -199,6 +201,8 @@ internal static class AotCompilerTools img.CloneAs(default); img.CloneAs(default); img.CloneAs(default); + img.CloneAs(default); + img.CloneAs(default); img.CloneAs(default); img.CloneAs(default); img.CloneAs(default); diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index a0bbdd30e9..2cb382de18 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/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++; diff --git a/src/ImageSharp/PixelFormats/HalfTypeHelper.cs b/src/ImageSharp/PixelFormats/HalfTypeHelper.cs index 473e2ed285..53f22eb09a 100644 --- a/src/ImageSharp/PixelFormats/HalfTypeHelper.cs +++ b/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; /// 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); + /// + /// Normalizes a finite binary16 value to the scaled pixel range. + /// + /// The native binary16 value represented as a . + /// The normalized value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static float ToScaled(float value) => (value * InverseFiniteRange) + ScaledMidpoint; + + /// + /// Normalizes finite binary16 values to the scaled pixel range. + /// + /// The native binary16 values. + /// The normalized values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector2 ToScaled(Vector2 value) => (value * InverseFiniteRange) + new Vector2(ScaledMidpoint); + + /// + /// Normalizes finite binary16 values to the scaled pixel range. + /// + /// The native binary16 values. + /// The normalized values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector4 ToScaled(Vector4 value) => (value * InverseFiniteRange) + new Vector4(ScaledMidpoint); + + /// + /// Expands a normalized value to the finite binary16 range. + /// + /// The normalized value. + /// The native binary16 value represented as a . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static float FromScaled(float value) => (value * FiniteRange) + FiniteMinimum; + + /// + /// Expands normalized values to the finite binary16 range. + /// + /// The normalized values. + /// The native binary16 values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector2 FromScaled(Vector2 value) => (value * FiniteRange) + new Vector2(FiniteMinimum); + + /// + /// Expands normalized values to the finite binary16 range. + /// + /// The normalized values. + /// The native binary16 values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector4 FromScaled(Vector4 value) => (value * FiniteRange) + new Vector4(FiniteMinimum); + /// /// Unpacks eight binary16 values into two vectors of single-precision values. /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/A8.cs b/src/ImageSharp/PixelFormats/PixelImplementations/A8.cs index 65f70867c0..8a17831a1c 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/A8.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/A8.cs @@ -8,10 +8,11 @@ namespace SixLabors.ImageSharp.PixelFormats; /// /// Packed pixel type containing a single 8-bit normalized alpha value. -/// -/// Ranges from [0, 0, 0, 0] to [0, 0, 0, 1] in vector form. -/// /// +/// +/// and scaled vector conversions return alpha in [0, 1] with zero color components. +/// The storage layout matches DXGI_FORMAT_A8_UNORM. +/// public partial struct A8 : IPixel, IPackedVector { /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs index a77174a715..338c55fe36 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs @@ -7,14 +7,12 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// 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. -/// -/// Ranges from [0, 0, 0, 1] to [1, 1, 1, 1] in vector form. -/// /// /// -/// Initializes a new instance of the struct. +/// and scaled vector conversions return color components in [0, 1] with an implicit +/// alpha of 1. The storage layout matches DXGI_FORMAT_B5G6R5_UNORM. /// /// /// The vector containing the components for the packed value. diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Bgra4444.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Bgra4444.cs index 97c2eabd01..17711835a1 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Bgra4444.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Bgra4444.cs @@ -7,11 +7,12 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing unsigned normalized values, ranging from 0 to 1, using 4 bits each for x, y, z, and w. -/// -/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. -/// +/// Packed pixel type containing four unsigned normalized values using 4 bits per component. /// +/// +/// and scaled vector conversions return all components in [0, 1]. The storage layout matches +/// DXGI_FORMAT_B4G4R4A4_UNORM. +/// public partial struct Bgra4444 : IPixel, IPackedVector { /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Bgra5551.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Bgra5551.cs index 799c2ce5c3..22a2c1f99f 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Bgra5551.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Bgra5551.cs @@ -7,12 +7,13 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// 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. -/// -/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. -/// +/// Packed pixel type containing four unsigned normalized values. +/// The x, y, and z components use 5 bits, and the w component uses 1 bit. /// +/// +/// and scaled vector conversions return all components in [0, 1]. The storage layout matches +/// DXGI_FORMAT_B5G5R5A1_UNORM. +/// public partial struct Bgra5551 : IPixel, IPackedVector { /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Byte4.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Byte4.cs index d8e9ca9ec4..c0372ab979 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Byte4.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Byte4.cs @@ -8,11 +8,12 @@ using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing four 8-bit unsigned integer values, ranging from 0 to 255. -/// -/// Ranges from [0, 0, 0, 0] to [255, 255, 255, 255] in vector form. -/// +/// Packed pixel type containing four 8-bit unsigned integer values. /// +/// +/// returns components in [0, 255]. Scaled vector conversions map that range to +/// [0, 1]. The storage layout matches DXGI_FORMAT_R8G8B8A8_UINT. +/// public partial struct Byte4 : IPixel, IPackedVector { private static readonly Vector4 MaxBytes = Vector128.Create(255f).AsVector4(); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs index c1204efed6..cbb0cf0866 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs @@ -7,11 +7,13 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing a single 16 bit floating point value. -/// -/// Ranges from [-1, 0, 0, 1] to [1, 0, 0, 1] in vector form. -/// +/// Packed pixel type containing a single IEEE 754 binary16 floating-point value. /// +/// +/// returns the stored IEEE 754 binary16 value directly. Scaled vector conversions normalize +/// the finite range [-65504, 65504] to [0, 1]. The packed representation is binary-compatible with +/// DXGI_FORMAT_R16_FLOAT. +/// public partial struct HalfSingle : IPixel, IPackedVector { /// @@ -53,9 +55,8 @@ public partial struct HalfSingle : IPixel, IPackedVector [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); } /// @@ -109,20 +110,15 @@ public partial struct HalfSingle : IPixel, IPackedVector [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); } /// [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)) }; /// [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs index 900ca07b42..ad6c7aefab 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs @@ -7,11 +7,13 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing two 16-bit floating-point values. -/// -/// Ranges from [-1, -1, 0, 1] to [1, 1, 0, 1] in vector form. -/// +/// Packed pixel type containing two IEEE 754 binary16 floating-point values. /// +/// +/// and return the stored IEEE 754 binary16 values directly. Scaled +/// vector conversions normalize the finite range [-65504, 65504] to [0, 1]. The packed representation is +/// binary-compatible with DXGI_FORMAT_R16G16_FLOAT. +/// public partial struct HalfVector2 : IPixel, IPackedVector { /// @@ -60,9 +62,7 @@ public partial struct HalfVector2 : IPixel, IPackedVector [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, IPackedVector [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, IPackedVector [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) }; } /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs index dec77f0480..42531937cb 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs @@ -7,11 +7,13 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing four 16-bit floating-point values. -/// -/// Ranges from [-1, -1, -1, -1] to [1, 1, 1, 1] in vector form. -/// +/// Packed pixel type containing four IEEE 754 binary16 floating-point values. /// +/// +/// returns the stored IEEE 754 binary16 values directly. Scaled vector conversions normalize +/// the finite range [-65504, 65504] to [0, 1]. The packed representation is binary-compatible with +/// DXGI_FORMAT_R16G16B16A16_FLOAT. +/// public partial struct HalfVector4 : IPixel, IPackedVector { /// @@ -63,13 +65,7 @@ public partial struct HalfVector4 : IPixel, IPackedVector /// [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()); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -112,10 +108,8 @@ public partial struct HalfVector4 : IPixel, IPackedVector { 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); } /// @@ -138,20 +132,14 @@ public partial struct HalfVector4 : IPixel, IPackedVector [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); } /// [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)); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs index 4a51c21afa..50fc272553 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs +++ b/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. /// /// -/// The native packed and vector representations use associated alpha. +/// returns the stored associated IEEE 754 binary16 values directly. Scaled vector conversions +/// normalize the finite range [-65504, 65504] to [0, 1] while preserving associated alpha. The packed +/// representation is binary-compatible with DXGI_FORMAT_R16G16B16A16_FLOAT. /// public partial struct HalfVector4P : IPixel, IPackedVector { @@ -58,13 +60,8 @@ public partial struct HalfVector4P : IPixel, IPackedVector } /// - 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()); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -90,11 +87,8 @@ public partial struct HalfVector4P : IPixel, IPackedVector [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); } /// @@ -121,9 +115,7 @@ public partial struct HalfVector4P : IPixel, IPackedVector [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, IPackedVector [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, IPackedVector [MethodImpl(MethodImplOptions.AggressiveInlining)] public static HalfVector4P FromAssociatedScaledVector4(Vector4 source) => PackAssociatedScaledVector4(Reassociate(source)); - /// - /// Packs an associated scaled vector into binary16-native storage. - /// - /// The associated scaled vector. - /// The packed pixel. - [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) }; - } - /// public static HalfVector4P FromAbgr32(Abgr32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); @@ -224,9 +200,8 @@ public partial struct HalfVector4P : IPixel, IPackedVector { 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, IPackedVector 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, IPackedVector return source; } + /// + /// Packs an associated scaled vector into the native binary16 representation. + /// + /// The associated scaled vector. + /// The packed pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static HalfVector4P PackAssociatedScaledVector4(Vector4 source) + { + source = HalfTypeHelper.FromScaled(source); + return new HalfVector4P { PackedValue = Pack(source) }; + } + + /// + /// Quantizes scaled alpha through the native binary16 representation. + /// + /// The scaled alpha value. + /// The scaled value represented by the stored binary16 component. + [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))); + } + /// /// Packs native half-precision components into a 64-bit value. /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs index 317e59574b..6039bd9d7c 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs @@ -7,15 +7,20 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing two 8-bit signed normalized values, ranging from −1 to 1. -/// -/// Ranges from [-1, -1, 0, 1] to [1, 1, 0, 1] in vector form. -/// +/// Packed pixel type containing two 8-bit signed normalized values. /// +/// +/// and return components in the native signed-normalized range +/// [-1, 1]. Scaled vector conversions return components in [0, 1]. +/// The packed two's-complement codes -128 and -127 both represent -1, +/// matching DXGI_FORMAT_R8G8_SNORM. +/// public partial struct NormalizedByte2 : IPixel, IPackedVector { 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); /// @@ -67,9 +72,14 @@ public partial struct NormalizedByte2 : IPixel, IPackedVector> 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, IPackedVector /// The . [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; + } /// public override readonly bool Equals(object? obj) => obj is NormalizedByte2 other && this.Equals(other); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4.cs b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4.cs index 09ddd0e91e..2f91718cf8 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4.cs @@ -8,16 +8,20 @@ using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing four 8-bit signed normalized values, ranging from −1 to 1. -/// -/// Ranges from [-1, -1, -1, -1] to [1, 1, 1, 1] in vector form. -/// +/// Packed pixel type containing four 8-bit signed normalized values. /// +/// +/// returns components in the native signed-normalized range [-1, 1]. +/// Scaled vector conversions return components in [0, 1]. +/// The packed two's-complement codes -128 and -127 both represent -1, +/// matching DXGI_FORMAT_R8G8B8A8_SNORM. +/// public partial struct NormalizedByte4 : IPixel, IPackedVector { 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(); /// @@ -73,21 +77,28 @@ public partial struct NormalizedByte4 : IPixel, IPackedVector> 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; } /// [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; + } /// public static PixelTypeInfo GetPixelTypeInfo() diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4P.cs b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4P.cs index 4f6df4c6fb..1172aafb95 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4P.cs +++ b/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. /// /// -/// The native packed and vector representations use associated alpha. +/// returns associated components in the native signed-normalized range [-1, 1]. +/// Scaled vector conversions return associated components in [0, 1]. +/// The packed two's-complement codes -128 and -127 both represent -1, +/// matching DXGI_FORMAT_R8G8B8A8_SNORM. /// public partial struct NormalizedByte4P : IPixel, IPackedVector { 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(); /// @@ -65,12 +69,13 @@ public partial struct NormalizedByte4P : IPixel, 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; } /// @@ -82,11 +87,17 @@ public partial struct NormalizedByte4P : IPixel, IPackedVector public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); /// - 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; + } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -244,10 +255,13 @@ public partial struct NormalizedByte4P : IPixel, 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) { diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs index dfc42594d1..3da0f7f0b9 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs @@ -7,15 +7,19 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing two 16-bit signed normalized values, ranging from −1 to 1. -/// -/// Ranges from [-1, -1, 0, 1] to [1, 1, 0, 1] in vector form. -/// +/// Packed pixel type containing two 16-bit signed normalized values. /// +/// +/// and return components in the native signed-normalized range +/// [-1, 1]. Scaled vector conversions return components in [0, 1]. +/// The packed two's-complement codes -32768 and -32767 both represent -1, +/// matching DXGI_FORMAT_R16G16_SNORM. +/// public partial struct NormalizedShort2 : IPixel, IPackedVector { // 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, 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, IPackedVector /// /// The . [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; + } /// public override readonly bool Equals(object? obj) => obj is NormalizedShort2 other && this.Equals(other); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort4.cs b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort4.cs index a5ab585df1..7acac0b7dd 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort4.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort4.cs @@ -8,11 +8,14 @@ using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing four 16-bit signed normalized values, ranging from −1 to 1. -/// -/// Ranges from [-1, -1, -1, -1] to [1, 1, 1, 1] in vector form. -/// +/// Packed pixel type containing four 16-bit signed normalized values. /// +/// +/// returns components in the native signed-normalized range [-1, 1]. +/// Scaled vector conversions return components in [0, 1]. +/// The packed two's-complement codes -32768 and -32767 both represent -1, +/// matching DXGI_FORMAT_R16G16B16A16_SNORM. +/// public partial struct NormalizedShort4 : IPixel, IPackedVector { // Largest two byte positive number 0xFFFF >> 1; @@ -74,21 +77,28 @@ public partial struct NormalizedShort4 : IPixel, 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; } /// [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; + } /// public static PixelTypeInfo GetPixelTypeInfo() diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfSingle.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfSingle.PixelOperations.cs index ec1fe3dd31..07428f4845 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfSingle.PixelOperations.cs +++ b/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 /// internal class PixelOperations : PixelOperations { - 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. /// @@ -26,13 +22,5 @@ public partial struct HalfSingle /// protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedScaledVector4(configuration, source, destination); - - /// - protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span 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); - } } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector2.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector2.PixelOperations.cs index 3f46d61f72..e167c4eb7b 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector2.PixelOperations.cs +++ b/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 /// internal class PixelOperations : PixelOperations { - 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. /// @@ -26,13 +22,5 @@ public partial struct HalfVector2 /// protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedScaledVector4(configuration, source, destination); - - /// - protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span 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); - } } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs index e290553661..9eae7d5cbc 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs @@ -17,14 +17,18 @@ public partial struct HalfVector4 /// internal class PixelOperations : PixelOperations { - 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); /// protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - HalfVector4P.PixelOperations.Unpack(MemoryMarshal.Cast(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(source), destination[..source.Length]); } /// @@ -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(source), destination, true); + // Association uses normalized opacity, not the native binary16 alpha value. + RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination); + Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); Numerics.Premultiply(destination); - Vector4Converters.MultiplyThenAdd(destination, NativeScale, -Vector4.One); + Vector4Converters.MultiplyThenAdd(destination, ScaledToNativeMultiplier, ScaledToNativeOffset); } /// @@ -46,17 +50,16 @@ public partial struct HalfVector4 { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - HalfVector4P.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination[..source.Length], true); + destination = destination[..source.Length]; + RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination); + Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); } /// protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) { - Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - - destination = destination[..source.Length]; - HalfVector4P.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination, true); - Numerics.Premultiply(destination); + this.ToUnassociatedScaledVector4(configuration, source, destination); + Numerics.Premultiply(destination[..source.Length]); } /// @@ -64,8 +67,8 @@ public partial struct HalfVector4 { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - destination = destination[..source.Length]; - HalfVector4P.PixelOperations.Pack(source, MemoryMarshal.Cast(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(destination[..source.Length])); } /// @@ -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(destination), true); + Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); + RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); } /// @@ -86,8 +88,8 @@ public partial struct HalfVector4 { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - destination = destination[..source.Length]; - HalfVector4P.PixelOperations.Pack(source, MemoryMarshal.Cast(destination), true); + Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); + RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); } /// @@ -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(destination), true); + Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); + RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); } } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs index 1646dd4d35..52b2dbf376 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs +++ b/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 /// internal class PixelOperations : AssociatedAlphaPixelOperations { + private static readonly Vector4 NativeToScaledMultiplier = new(HalfTypeHelper.InverseFiniteRange); + private static readonly Vector4 NativeToScaledOffset = new(HalfTypeHelper.ScaledMidpoint); + private static readonly Vector4 ScaledToNativeMultiplier = new(HalfTypeHelper.FiniteRange); + private static readonly Vector4 ScaledToNativeOffset = new(HalfTypeHelper.FiniteMinimum); + /// protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) { - Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - - UnpackUnassociated(source, destination[..source.Length], false); + this.ToUnassociatedScaledVector4(configuration, source, destination); + Vector4Converters.MultiplyThenAdd(destination[..source.Length], ScaledToNativeMultiplier, ScaledToNativeOffset); } /// @@ -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(source), destination[..source.Length]); } /// - protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span 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]); } /// - protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); destination = destination[..source.Length]; - PackAssociated(source, destination, false); + RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination); + Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); } /// - protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span 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]); } /// - protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span 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]); } /// @@ -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]); } /// @@ -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]); } /// - /// Unpacks native or scaled binary16 components into vectors. + /// Associates scaled vectors with the alpha values representable by native binary16 storage. /// - /// The packed source pixels. - /// The destination vectors. - /// Whether to produce scaled vectors. - internal static void Unpack(ReadOnlySpan source, Span destination, bool scaled) + /// The vectors to convert in place. + private static void Associate(Span source) { - ref ushort sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); - ref float destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); - int componentCount = source.Length * Vector128.Count; + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(source); int i = 0; if (Vector512.IsHardwareAccelerated) { - for (; i <= componentCount - Vector512.Count; i += Vector512.Count) - { - Vector512 packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); - (Vector512 lower, Vector512 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.Count)); - } - } - - if (Vector256.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector256.Count; i += Vector256.Count) - { - Vector256 packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); - (Vector256 lower, Vector256 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.Count)); - } - } - - if (Vector128.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector128.Count; i += Vector128.Count) - { - Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); - (Vector128 lower, Vector128 upper) = HalfTypeHelper.Unpack(packed); + int vectorsPerRegister = Vector512.Count / Vector128.Count; - if (scaled) - { - lower = ToScaled(lower); - upper = ToScaled(upper); - } - - Vector128.StoreUnsafe(lower, ref destinationBase, (nuint)i); - Vector128.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector128.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(ref Unsafe.As(ref Unsafe.Add(ref sourceBase, (uint)i))); - Vector128 packed = Vector128.CreateScalarUnsafe(remainder).AsUInt16(); - Vector128 vector = HalfTypeHelper.Unpack(packed).Lower; - - if (scaled) - { - vector = ToScaled(vector); - } - - Vector128.StoreUnsafe(vector, ref destinationBase, (nuint)i); - } - - return; - } - - ref HalfVector4P pixelBase = ref Unsafe.As(ref sourceBase); - ref Vector4 vectorBase = ref Unsafe.As(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(); - } - } - - /// - /// Unpacks associated binary16 storage directly into unassociated native or scaled vectors. - /// - /// The packed source pixels. - /// The destination vectors. - /// Whether to produce scaled vectors. - private static void UnpackUnassociated(ReadOnlySpan source, Span destination, bool scaled) - { - ref ushort sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); - ref float destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); - int componentCount = source.Length * Vector128.Count; - int i = 0; - - if (Vector512.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector512.Count; i += Vector512.Count) - { - Vector512 packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); - (Vector512 lower, Vector512 upper) = HalfTypeHelper.Unpack(packed); - Vector512.StoreUnsafe(ToUnassociated(lower, scaled), ref destinationBase, (nuint)i); - Vector512.StoreUnsafe(ToUnassociated(upper, scaled), ref destinationBase, (nuint)(i + Vector512.Count)); + ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); + vector = Associate(vector); } } if (Vector256.IsHardwareAccelerated) { - for (; i <= componentCount - Vector256.Count; i += Vector256.Count) - { - Vector256 packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); - (Vector256 lower, Vector256 upper) = HalfTypeHelper.Unpack(packed); - Vector256.StoreUnsafe(ToUnassociated(lower, scaled), ref destinationBase, (nuint)i); - Vector256.StoreUnsafe(ToUnassociated(upper, scaled), ref destinationBase, (nuint)(i + Vector256.Count)); - } - } - - if (Vector128.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector128.Count; i += Vector128.Count) - { - Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); - (Vector128 lower, Vector128 upper) = HalfTypeHelper.Unpack(packed); - Vector128.StoreUnsafe(ToUnassociated(lower, scaled), ref destinationBase, (nuint)i); - Vector128.StoreUnsafe(ToUnassociated(upper, scaled), ref destinationBase, (nuint)(i + Vector128.Count)); - } + int vectorsPerRegister = Vector256.Count / Vector128.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(ref Unsafe.As(ref Unsafe.Add(ref sourceBase, (uint)i))); - Vector128 packed = Vector128.CreateScalarUnsafe(remainder).AsUInt16(); - Vector128 vector = HalfTypeHelper.Unpack(packed).Lower; - Vector128.StoreUnsafe(ToUnassociated(vector, scaled), ref destinationBase, (nuint)i); - } - - return; - } - - ref HalfVector4P pixelBase = ref Unsafe.As(ref sourceBase); - ref Vector4 vectorBase = ref Unsafe.As(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(); - } - } - - /// - /// Packs native or scaled vectors into binary16 storage. - /// - /// The source vectors. - /// The destination pixels. - /// Whether the source contains scaled vectors. - internal static void Pack(Span source, Span destination, bool scaled) - { - ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); - ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); - int componentCount = source.Length * Vector128.Count; - int i = 0; - - if (Vector512.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector512.Count; i += Vector512.Count) - { - Vector512 lower = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); - Vector512 upper = Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count)); - Vector512 packed = scaled ? PackScaled(lower, upper) : HalfTypeHelper.Pack(lower, upper); - Vector512.StoreUnsafe(packed, ref destinationBase, (nuint)i); - } - } - - if (Vector256.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector256.Count; i += Vector256.Count) - { - Vector256 lower = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); - Vector256 upper = Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count)); - Vector256 packed = scaled ? PackScaled(lower, upper) : HalfTypeHelper.Pack(lower, upper); - Vector256.StoreUnsafe(packed, ref destinationBase, (nuint)i); + ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); + vector = Associate(vector); } } if (Vector128.IsHardwareAccelerated) { - for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + for (; i < source.Length; i++) { - Vector128 lower = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); - Vector128 upper = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count)); - Vector128 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 vector = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); - Vector128 packed = scaled ? PackScaled(vector, vector) : HalfTypeHelper.Pack(vector, vector); - Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); + ref Vector128 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); + vector = Associate(vector); } return; } - ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); - ref HalfVector4P pixelBase = ref Unsafe.As(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)); } } /// - /// 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. /// - /// The unassociated vectors. - /// The destination pixels. - /// Whether the source contains scaled vectors. - private static void PackUnassociated(Span source, Span destination, bool scaled) + /// The vectors to convert in place. + private static void Reassociate(Span source) { - ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); - ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); - int componentCount = source.Length * Vector128.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.Count; i += Vector512.Count) - { - Vector512 lower = Associate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i), scaled); - Vector512 upper = Associate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count)), scaled); - Vector512.StoreUnsafe(PackScaled(lower, upper), ref destinationBase, (nuint)i); - } - } - - if (Vector256.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector256.Count; i += Vector256.Count) - { - Vector256 lower = Associate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i), scaled); - Vector256 upper = Associate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count)), scaled); - Vector256.StoreUnsafe(PackScaled(lower, upper), ref destinationBase, (nuint)i); - } - } - - if (Vector128.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector128.Count; i += Vector128.Count) - { - Vector128 lower = Associate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), scaled); - Vector128 upper = Associate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.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 vector = Associate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), scaled); - Vector128 packed = PackScaled(vector, vector); - Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); - } - - return; - } - - ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); - ref HalfVector4P pixelBase = ref Unsafe.As(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.Count / Vector128.Count; - /// - /// Reassociates associated native or scaled vectors and packs them into binary16 storage in one pass. - /// - /// The associated vectors. - /// The destination pixels. - /// Whether the source contains scaled vectors. - private static void PackAssociated(Span source, Span destination, bool scaled) - { - ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); - ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); - int componentCount = source.Length * Vector128.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.Count; i += Vector512.Count) + for (; i <= source.Length - vectorsPerRegister; i += vectorsPerRegister) { - Vector512 lower = Reassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i), scaled); - Vector512 upper = Reassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count)), scaled); - Vector512.StoreUnsafe(PackScaled(lower, upper), ref destinationBase, (nuint)i); + ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); + vector = Reassociate(vector); } } if (Vector256.IsHardwareAccelerated) { - for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + int vectorsPerRegister = Vector256.Count / Vector128.Count; + + for (; i <= source.Length - vectorsPerRegister; i += vectorsPerRegister) { - Vector256 lower = Reassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i), scaled); - Vector256 upper = Reassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count)), scaled); - Vector256.StoreUnsafe(PackScaled(lower, upper), ref destinationBase, (nuint)i); + ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); + vector = Reassociate(vector); } } if (Vector128.IsHardwareAccelerated) { - for (; i <= componentCount - Vector128.Count; i += Vector128.Count) - { - Vector128 lower = Reassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), scaled); - Vector128 upper = Reassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.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 vector = Reassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), scaled); - Vector128 packed = PackScaled(vector, vector); - Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); + ref Vector128 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); + vector = Reassociate(vector); } return; } - ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); - ref HalfVector4P pixelBase = ref Unsafe.As(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)); } } /// - /// Converts native binary16 values to the normalized range used by pixel conversions. - /// - /// The native values. - /// The scaled values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 ToScaled(Vector128 source) => (source + Vector128.One) / Vector128.Create(2F); - - /// - /// Converts native binary16 values to the normalized range used by pixel conversions. - /// - /// The native values. - /// The scaled values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 ToScaled(Vector256 source) => (source + Vector256.One) / Vector256.Create(2F); - - /// - /// Converts native binary16 values to the normalized range used by pixel conversions. - /// - /// The native values. - /// The scaled values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 ToScaled(Vector512 source) => (source + Vector512.One) / Vector512.Create(2F); - - /// - /// Converts associated native binary16 values to unassociated native or scaled vectors. - /// - /// The associated native values. - /// Whether to produce scaled vectors. - /// The unassociated vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 ToUnassociated(Vector128 source, bool scaled) - { - source = ToScaled(source); - Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); - Vector128 result = Vector128.ConditionalSelect(Vector128.Equals(alpha, Vector128.Zero), source, source / alpha); - Vector128 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.One; - } - - /// - /// Converts associated native binary16 values to unassociated native or scaled vectors. - /// - /// The associated native values. - /// Whether to produce scaled vectors. - /// The unassociated vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 ToUnassociated(Vector256 source, bool scaled) - { - source = ToScaled(source); - Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); - Vector256 result = Vector256.ConditionalSelect(Vector256.Equals(alpha, Vector256.Zero), source, source / alpha); - Vector256 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.One; - } - - /// - /// Converts associated native binary16 values to unassociated native or scaled vectors. - /// - /// The associated native values. - /// Whether to produce scaled vectors. - /// The unassociated vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 ToUnassociated(Vector512 source, bool scaled) - { - source = ToScaled(source); - Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); - Vector512 result = Vector512.ConditionalSelect(Vector512.Equals(alpha, Vector512.Zero), source, source / alpha); - Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); - result = Vector512.ConditionalSelect(alphaMask, 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.One; - } - - /// - /// Converts unassociated native or scaled vectors to the destination's associated scaled representation. + /// Converts an unassociated scaled vector to associated scaled components. /// /// The unassociated vectors. - /// Whether the source contains scaled vectors. - /// The associated scaled vectors. + /// The associated vectors. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 Associate(Vector128 source, bool scaled) + private static Vector128 Associate(Vector128 source) { - Vector128 zero = Vector128.Zero; - Vector128 one = Vector128.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 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); - Vector128 storedAlpha = (HalfTypeHelper.RoundToHalf((alpha * Vector128.Create(2F)) - one) + one) / Vector128.Create(2F); + Vector128 storedAlpha = QuantizeScaledAlpha(alpha); Vector128 result = source * storedAlpha; - Vector128 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); } /// - /// Converts unassociated native or scaled vectors to the destination's associated scaled representation. + /// Converts unassociated scaled vectors to associated scaled components. /// /// The unassociated vectors. - /// Whether the source contains scaled vectors. - /// The associated scaled vectors. + /// The associated vectors. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 Associate(Vector256 source, bool scaled) + private static Vector256 Associate(Vector256 source) { - Vector256 zero = Vector256.Zero; - Vector256 one = Vector256.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 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); - Vector256 storedAlpha = (HalfTypeHelper.RoundToHalf((alpha * Vector256.Create(2F)) - one) + one) / Vector256.Create(2F); + Vector256 storedAlpha = QuantizeScaledAlpha(alpha); Vector256 result = source * storedAlpha; - Vector256 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); } /// - /// Converts unassociated native or scaled vectors to the destination's associated scaled representation. + /// Converts unassociated scaled vectors to associated scaled components. /// /// The unassociated vectors. - /// Whether the source contains scaled vectors. - /// The associated scaled vectors. + /// The associated vectors. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 Associate(Vector512 source, bool scaled) + private static Vector512 Associate(Vector512 source) { - Vector512 zero = Vector512.Zero; - Vector512 one = Vector512.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 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); - Vector512 storedAlpha = (HalfTypeHelper.RoundToHalf((alpha * Vector512.Create(2F)) - one) + one) / Vector512.Create(2F); + Vector512 storedAlpha = QuantizeScaledAlpha(alpha); Vector512 result = source * storedAlpha; Vector512 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); } /// - /// Reassociates native or scaled vectors with the scaled alpha values the destination stores. + /// Reassociates an associated scaled vector after alpha quantization. /// /// The associated vectors. - /// Whether the source contains scaled vectors. - /// The reassociated scaled vectors. + /// The reassociated vectors. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 Reassociate(Vector128 source, bool scaled) + private static Vector128 Reassociate(Vector128 source) { Vector128 zero = Vector128.Zero; - Vector128 one = Vector128.One; - - if (!scaled) - { - // Convert every component together so RGB and alpha enter reassociation in the same scaled coordinate system. - source = ToScaled(source); - } - Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); - Vector128 nativeAlpha = (alpha * Vector128.Create(2F)) - one; - Vector128 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 storedAlpha = (HalfTypeHelper.RoundToHalf(nativeAlpha) + one) / Vector128.Create(2F); + Vector128 storedAlpha = QuantizeScaledAlpha(alpha); Vector128 result = source * (storedAlpha / alpha); - Vector128 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); } /// - /// Reassociates native or scaled vectors with the scaled alpha values the destination stores. + /// Reassociates associated scaled vectors after alpha quantization. /// /// The associated vectors. - /// Whether the source contains scaled vectors. - /// The reassociated scaled vectors. + /// The reassociated vectors. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 Reassociate(Vector256 source, bool scaled) + private static Vector256 Reassociate(Vector256 source) { Vector256 zero = Vector256.Zero; - Vector256 one = Vector256.One; - - if (!scaled) - { - // Convert every component together so RGB and alpha enter reassociation in the same scaled coordinate system. - source = ToScaled(source); - } - Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); - Vector256 nativeAlpha = (alpha * Vector256.Create(2F)) - one; - Vector256 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 storedAlpha = (HalfTypeHelper.RoundToHalf(nativeAlpha) + one) / Vector256.Create(2F); + Vector256 storedAlpha = QuantizeScaledAlpha(alpha); Vector256 result = source * (storedAlpha / alpha); - Vector256 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); } /// - /// Reassociates native or scaled vectors with the scaled alpha values the destination stores. + /// Reassociates associated scaled vectors after alpha quantization. /// /// The associated vectors. - /// Whether the source contains scaled vectors. - /// The reassociated scaled vectors. + /// The reassociated vectors. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 Reassociate(Vector512 source, bool scaled) + private static Vector512 Reassociate(Vector512 source) { Vector512 zero = Vector512.Zero; - Vector512 one = Vector512.One; - - if (!scaled) - { - // Convert every component together so RGB and alpha enter reassociation in the same scaled coordinate system. - source = ToScaled(source); - } - Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); - Vector512 nativeAlpha = (alpha * Vector512.Create(2F)) - one; - Vector512 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 storedAlpha = (HalfTypeHelper.RoundToHalf(nativeAlpha) + one) / Vector512.Create(2F); + Vector512 storedAlpha = QuantizeScaledAlpha(alpha); Vector512 result = source * (storedAlpha / alpha); Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); result = Vector512.ConditionalSelect(alphaMask, storedAlpha, result); @@ -710,51 +290,86 @@ public partial struct HalfVector4P } /// - /// Converts two scaled vectors to binary16 storage while preserving scalar operation order. + /// Quantizes scaled alpha through the native binary16 representation. + /// + /// The scaled alpha lanes. + /// The scaled alpha values represented by binary16 storage. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 QuantizeScaledAlpha(Vector128 alpha) + { + Vector128 native = (ClampUnit(alpha) * Vector128.Create(HalfTypeHelper.FiniteRange)) + Vector128.Create(HalfTypeHelper.FiniteMinimum); + return (HalfTypeHelper.RoundToHalf(native) * Vector128.Create(HalfTypeHelper.InverseFiniteRange)) + Vector128.Create(HalfTypeHelper.ScaledMidpoint); + } + + /// + /// Quantizes scaled alpha through the native binary16 representation. + /// + /// The scaled alpha lanes. + /// The scaled alpha values represented by binary16 storage. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 QuantizeScaledAlpha(Vector256 alpha) + { + Vector256 native = (ClampUnit(alpha) * Vector256.Create(HalfTypeHelper.FiniteRange)) + Vector256.Create(HalfTypeHelper.FiniteMinimum); + return (HalfTypeHelper.RoundToHalf(native) * Vector256.Create(HalfTypeHelper.InverseFiniteRange)) + Vector256.Create(HalfTypeHelper.ScaledMidpoint); + } + + /// + /// Quantizes scaled alpha through the native binary16 representation. + /// + /// The scaled alpha lanes. + /// The scaled alpha values represented by binary16 storage. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 QuantizeScaledAlpha(Vector512 alpha) + { + Vector512 native = (ClampUnit(alpha) * Vector512.Create(HalfTypeHelper.FiniteRange)) + Vector512.Create(HalfTypeHelper.FiniteMinimum); + return (HalfTypeHelper.RoundToHalf(native) * Vector512.Create(HalfTypeHelper.InverseFiniteRange)) + Vector512.Create(HalfTypeHelper.ScaledMidpoint); + } + + /// + /// Clamps vectors to the scaled color range while preserving NaN lanes. /// - /// The lower scaled values. - /// The upper scaled values. - /// The packed binary16 values. + /// The vectors to clamp. + /// The clamped vectors. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 PackScaled(Vector128 lower, Vector128 upper) + private static Vector128 ClampUnit(Vector128 source) { - lower *= Vector128.Create(2F); - lower -= Vector128.One; - upper *= Vector128.Create(2F); - upper -= Vector128.One; - return HalfTypeHelper.Pack(lower, upper); + Vector128 clamped = Vector128.Min(Vector128.Max(source, Vector128.Zero), Vector128.One); + return Vector128.ConditionalSelect(Vector128.Equals(source, source), clamped, source); } /// - /// Converts two scaled vectors to binary16 storage while preserving scalar operation order. + /// Clamps vectors to the scaled color range while preserving NaN lanes. /// - /// The lower scaled values. - /// The upper scaled values. - /// The packed binary16 values. + /// The vectors to clamp. + /// The clamped vectors. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 PackScaled(Vector256 lower, Vector256 upper) + private static Vector256 ClampUnit(Vector256 source) { - lower *= Vector256.Create(2F); - lower -= Vector256.One; - upper *= Vector256.Create(2F); - upper -= Vector256.One; - return HalfTypeHelper.Pack(lower, upper); + Vector256 clamped = Vector256.Min(Vector256.Max(source, Vector256.Zero), Vector256.One); + return Vector256.ConditionalSelect(Vector256.Equals(source, source), clamped, source); } /// - /// Converts two scaled vectors to binary16 storage while preserving scalar operation order. + /// Clamps vectors to the scaled color range while preserving NaN lanes. /// - /// The lower scaled values. - /// The upper scaled values. - /// The packed binary16 values. + /// The vectors to clamp. + /// The clamped vectors. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 PackScaled(Vector512 lower, Vector512 upper) + private static Vector512 ClampUnit(Vector512 source) + { + Vector512 clamped = Vector512.Min(Vector512.Max(source, Vector512.Zero), Vector512.One); + return Vector512.ConditionalSelect(Vector512.Equals(source, source), clamped, source); + } + + /// + /// Maps associated scaled vectors to native components and packs them as binary16 values. + /// + /// The associated scaled vectors. + /// The destination pixels. + private static void PackAssociatedScaled(Span source, Span destination) { - lower *= Vector512.Create(2F); - lower -= Vector512.One; - upper *= Vector512.Create(2F); - upper -= Vector512.One; - return HalfTypeHelper.Pack(lower, upper); + Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); + RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination)); } } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4P.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4P.PixelOperations.cs index 509803f505..de2f8f17b7 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4P.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4P.PixelOperations.cs @@ -20,6 +20,8 @@ public partial struct NormalizedByte4P /// internal class PixelOperations : AssociatedAlphaPixelOperations { + private const byte RestorePackedPixelOrder = 0b_11_01_10_00; + /// protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) { @@ -107,27 +109,33 @@ public partial struct NormalizedByte4P int componentsPerPixel = Vector128.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.Count / Vector128.Count; for (; i <= source.Length - pixelsPerVector; i += pixelsPerVector) { Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i * componentsPerPixel)).AsSByte(); - Vector512 integers = Avx512F.ConvertToVector512Int32(packed); - Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToVector4(Avx512F.ConvertToVector512Single(integers), scaled); + Vector128 lowerShorts = Vector128.WidenLower(packed); + Vector128 upperShorts = Vector128.WidenUpper(packed); + Vector256 lowerIntegers = Vector256.Create(Vector128.WidenLower(lowerShorts), Vector128.WidenUpper(lowerShorts)); + Vector256 upperIntegers = Vector256.Create(Vector128.WidenLower(upperShorts), Vector128.WidenUpper(upperShorts)); + Vector512 integers = Vector512.Create(lowerIntegers, upperIntegers); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToVector4(Vector512.ConvertToSingle(integers), scaled); } } - if (Avx2.IsSupported) + if (Vector256.IsHardwareAccelerated) { int pixelsPerVector = Vector256.Count / Vector128.Count; for (; i <= source.Length - pixelsPerVector; i += pixelsPerVector) { ulong packed = Unsafe.ReadUnaligned(ref Unsafe.Add(ref sourceBase, (uint)(i * componentsPerPixel))); - Vector256 integers = Avx2.ConvertToVector256Int32(Vector128.CreateScalarUnsafe(packed).AsSByte()); - Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToVector4(Avx.ConvertToVector256Single(integers), scaled); + Vector128 shorts = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsSByte()); + Vector256 integers = Vector256.Create(Vector128.WidenLower(shorts), Vector128.WidenUpper(shorts)); + Unsafe.As>(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 ToVector4(Vector512 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 ToVector4(Vector256 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 ToVector4(Vector128 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.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.Count / Vector128.Count; for (; i <= source.Length - pixelsPerVector; i += pixelsPerVector) { Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i * componentsPerPixel)).AsSByte(); - Vector512 integers = Avx512F.ConvertToVector512Int32(packed); - Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Avx512F.ConvertToVector512Single(integers), scaled); + Vector128 lowerShorts = Vector128.WidenLower(packed); + Vector128 upperShorts = Vector128.WidenUpper(packed); + Vector256 lowerIntegers = Vector256.Create(Vector128.WidenLower(lowerShorts), Vector128.WidenUpper(lowerShorts)); + Vector256 upperIntegers = Vector256.Create(Vector128.WidenLower(upperShorts), Vector128.WidenUpper(upperShorts)); + Vector512 integers = Vector512.Create(lowerIntegers, upperIntegers); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Vector512.ConvertToSingle(integers), scaled); } } - if (Avx2.IsSupported) + if (Vector256.IsHardwareAccelerated) { int pixelsPerVector = Vector256.Count / Vector128.Count; for (; i <= source.Length - pixelsPerVector; i += pixelsPerVector) { ulong packed = Unsafe.ReadUnaligned(ref Unsafe.Add(ref sourceBase, (uint)(i * componentsPerPixel))); - Vector256 integers = Avx2.ConvertToVector256Int32(Vector128.CreateScalarUnsafe(packed).AsSByte()); - Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Avx.ConvertToVector256Single(integers), scaled); + Vector128 shorts = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsSByte()); + Vector256 integers = Vector256.Create(Vector128.WidenLower(shorts), Vector128.WidenUpper(shorts)); + Unsafe.As>(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 ToUnassociatedVector4(Vector512 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 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); Vector512 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 ToUnassociatedVector4(Vector256 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 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); Vector256 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 ToUnassociatedVector4(Vector128 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 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); Vector128 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.Count / Vector128.Count; for (; i <= source.Length - pixelsPerVector; i += pixelsPerVector) { Vector512 vector = Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); - Vector128 packed = Avx512F.ConvertToVector128SByteWithSaturation(ConvertToPackedInt32(vector, scaled)); + Vector512 integers = ConvertToPackedInt32(vector, scaled); + Vector256 shorts = Vector256_.PackSignedSaturate(integers.GetLower(), integers.GetUpper()); + Vector128 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>(ref Unsafe.Add(ref destinationBase, (uint)i)) = packed; } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalf.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalf.PixelOperations.cs new file mode 100644 index 0000000000..f3c01e082a --- /dev/null +++ b/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; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct RgbaHalf +{ + /// + /// Provides optimized bulk operations for . + /// + internal class PixelOperations : PixelOperations + { + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span 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(source), destination[..source.Length]); + } + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span 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(source), destination[..source.Length]); + } + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ToAssociatedVector4(configuration, source, destination); + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span 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(destination[..source.Length])); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span 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(destination[..source.Length])); + } + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + => this.FromUnassociatedVector4Destructive(configuration, source, destination); + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + => this.FromAssociatedVector4Destructive(configuration, source, destination); + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs new file mode 100644 index 0000000000..825ebeec92 --- /dev/null +++ b/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; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct RgbaHalfP +{ + /// + /// Provides optimized bulk operations for . + /// + internal class PixelOperations : AssociatedAlphaPixelOperations + { + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + UnpackUnassociated(source, destination[..source.Length]); + } + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + Unpack(source, destination[..source.Length]); + } + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ToAssociatedVector4(configuration, source, destination); + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + PackUnassociated(source, destination[..source.Length]); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + PackAssociated(source, destination[..source.Length]); + } + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + => this.FromUnassociatedVector4Destructive(configuration, source, destination); + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + => this.FromAssociatedVector4Destructive(configuration, source, destination); + + /// + /// Expands binary16 components without changing their unit-range representation. + /// + /// The packed source pixels. + /// The destination vectors. + internal static void Unpack(ReadOnlySpan source, Span destination) + { + ref ushort sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref float destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int componentCount = source.Length * Vector128.Count; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector512.Count; i += Vector512.Count) + { + Vector512 packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector512 lower, Vector512 upper) = HalfTypeHelper.Unpack(packed); + Vector512.StoreUnsafe(lower, ref destinationBase, (nuint)i); + Vector512.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector512.Count)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + { + Vector256 packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector256 lower, Vector256 upper) = HalfTypeHelper.Unpack(packed); + Vector256.StoreUnsafe(lower, ref destinationBase, (nuint)i); + Vector256.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector256.Count)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + { + Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector128 lower, Vector128 upper) = HalfTypeHelper.Unpack(packed); + Vector128.StoreUnsafe(lower, ref destinationBase, (nuint)i); + Vector128.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector128.Count)); + } + + if (i < componentCount) + { + // Four binary16 components form one pixel, so the only possible remainder is one complete pixel. + ulong remainder = Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref sourceBase, (uint)i))); + Vector128 packed = Vector128.CreateScalarUnsafe(remainder).AsUInt16(); + Vector128.StoreUnsafe(HalfTypeHelper.Unpack(packed).Lower, ref destinationBase, (nuint)i); + } + + return; + } + + ref RgbaHalfP pixelBase = ref Unsafe.As(ref sourceBase); + ref Vector4 vectorBase = ref Unsafe.As(ref destinationBase); + + for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) + { + Unsafe.Add(ref vectorBase, (uint)pixelIndex) = Unsafe.Add(ref pixelBase, (uint)pixelIndex).ToVector4(); + } + } + + /// + /// Expands unassociated binary16 components and associates RGB in the same pass. + /// + /// The packed source pixels. + /// The destination vectors. + internal static void UnpackAssociated(ReadOnlySpan source, Span destination) + { + ref ushort sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref float destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int componentCount = source.Length * Vector128.Count; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector512.Count; i += Vector512.Count) + { + Vector512 packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector512 lower, Vector512 upper) = HalfTypeHelper.Unpack(packed); + Vector512.StoreUnsafe(Associate(lower), ref destinationBase, (nuint)i); + Vector512.StoreUnsafe(Associate(upper), ref destinationBase, (nuint)(i + Vector512.Count)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + { + Vector256 packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector256 lower, Vector256 upper) = HalfTypeHelper.Unpack(packed); + Vector256.StoreUnsafe(Associate(lower), ref destinationBase, (nuint)i); + Vector256.StoreUnsafe(Associate(upper), ref destinationBase, (nuint)(i + Vector256.Count)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + { + Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector128 lower, Vector128 upper) = HalfTypeHelper.Unpack(packed); + Vector128.StoreUnsafe(Associate(lower), ref destinationBase, (nuint)i); + Vector128.StoreUnsafe(Associate(upper), ref destinationBase, (nuint)(i + Vector128.Count)); + } + + if (i < componentCount) + { + // Four binary16 components form one pixel, so the only possible remainder is one complete pixel. + ulong remainder = Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref sourceBase, (uint)i))); + Vector128 packed = Vector128.CreateScalarUnsafe(remainder).AsUInt16(); + Vector128.StoreUnsafe(Associate(HalfTypeHelper.Unpack(packed).Lower), ref destinationBase, (nuint)i); + } + + return; + } + + ref RgbaHalfP pixelBase = ref Unsafe.As(ref sourceBase); + ref Vector4 vectorBase = ref Unsafe.As(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; + } + } + + /// + /// Expands associated binary16 components and unassociates RGB in the same pass. + /// + /// The packed source pixels. + /// The destination vectors. + internal static void UnpackUnassociated(ReadOnlySpan source, Span destination) + { + ref ushort sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref float destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int componentCount = source.Length * Vector128.Count; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector512.Count; i += Vector512.Count) + { + Vector512 packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector512 lower, Vector512 upper) = HalfTypeHelper.Unpack(packed); + Vector512.StoreUnsafe(Unassociate(lower), ref destinationBase, (nuint)i); + Vector512.StoreUnsafe(Unassociate(upper), ref destinationBase, (nuint)(i + Vector512.Count)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + { + Vector256 packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector256 lower, Vector256 upper) = HalfTypeHelper.Unpack(packed); + Vector256.StoreUnsafe(Unassociate(lower), ref destinationBase, (nuint)i); + Vector256.StoreUnsafe(Unassociate(upper), ref destinationBase, (nuint)(i + Vector256.Count)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + { + Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector128 lower, Vector128 upper) = HalfTypeHelper.Unpack(packed); + Vector128.StoreUnsafe(Unassociate(lower), ref destinationBase, (nuint)i); + Vector128.StoreUnsafe(Unassociate(upper), ref destinationBase, (nuint)(i + Vector128.Count)); + } + + if (i < componentCount) + { + // Four binary16 components form one pixel, so the only possible remainder is one complete pixel. + ulong remainder = Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref sourceBase, (uint)i))); + Vector128 packed = Vector128.CreateScalarUnsafe(remainder).AsUInt16(); + Vector128.StoreUnsafe(Unassociate(HalfTypeHelper.Unpack(packed).Lower), ref destinationBase, (nuint)i); + } + + return; + } + + ref RgbaHalfP pixelBase = ref Unsafe.As(ref sourceBase); + ref Vector4 vectorBase = ref Unsafe.As(ref destinationBase); + + for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) + { + Unsafe.Add(ref vectorBase, (uint)pixelIndex) = Unsafe.Add(ref pixelBase, (uint)pixelIndex).ToUnassociatedVector4(); + } + } + + /// + /// Packs unassociated unit-range vectors directly into binary16 storage. + /// + /// The source vectors. + /// The destination pixels. + internal static void Pack(Span source, Span destination) + { + ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int componentCount = source.Length * Vector128.Count; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector512.Count; i += Vector512.Count) + { + Vector512 lower = ClampUnit(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector512 upper = ClampUnit(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))); + Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + { + Vector256 lower = ClampUnit(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector256 upper = ClampUnit(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))); + Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + { + Vector128 lower = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128 upper = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count))); + Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + + if (i < componentCount) + { + // Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel. + Vector128 vector = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128 packed = HalfTypeHelper.Pack(vector, vector); + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); + } + + return; + } + + ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); + ref RgbaHalfP pixelBase = ref Unsafe.As(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); + } + } + + /// + /// Packs vectors directly into IEEE 754 binary16 storage without applying unit-range color constraints. + /// + /// The source vectors. + /// The destination pixels. + internal static void PackUnclamped(Span source, Span destination) + { + ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int componentCount = source.Length * Vector128.Count; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector512.Count; i += Vector512.Count) + { + Vector512 lower = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); + Vector512 upper = Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count)); + Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + { + Vector256 lower = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); + Vector256 upper = Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count)); + Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + { + Vector128 lower = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); + Vector128 upper = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.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 vector = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); + Vector128 packed = HalfTypeHelper.Pack(vector, vector); + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); + } + + return; + } + + ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); + ref RgbaHalfP pixelBase = ref Unsafe.As(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); + } + } + + /// + /// Unassociates vectors and packs unassociated unit-range binary16 storage in the same pass. + /// + /// The associated source vectors. + /// The destination pixels. + internal static void PackFromAssociated(Span source, Span destination) + { + ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int componentCount = source.Length * Vector128.Count; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector512.Count; i += Vector512.Count) + { + Vector512 lower = ClampUnit(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i))); + Vector512 upper = ClampUnit(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count)))); + Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + { + Vector256 lower = ClampUnit(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i))); + Vector256 upper = ClampUnit(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count)))); + Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + { + Vector128 lower = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i))); + Vector128 upper = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count)))); + Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + + if (i < componentCount) + { + // Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel. + Vector128 vector = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i))); + Vector128 packed = HalfTypeHelper.Pack(vector, vector); + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); + } + + return; + } + + ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); + ref RgbaHalfP pixelBase = ref Unsafe.As(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); + } + } + + /// + /// Associates unassociated vectors with their stored binary16 alpha and packs them in one pass. + /// + /// The unassociated source vectors. + /// The destination pixels. + internal static void PackUnassociated(Span source, Span destination) + { + ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int componentCount = source.Length * Vector128.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.Count; i += Vector512.Count) + { + Vector512 lower = AssociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector512 upper = AssociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))); + Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + { + Vector256 lower = AssociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector256 upper = AssociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))); + Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + { + Vector128 lower = AssociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128 upper = AssociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.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 vector = AssociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128 packed = HalfTypeHelper.Pack(vector, vector); + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); + } + + return; + } + + ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); + ref RgbaHalfP pixelBase = ref Unsafe.As(ref destinationBase); + + for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) + { + Unsafe.Add(ref pixelBase, (uint)pixelIndex) = RgbaHalfP.FromUnassociatedVector4(Unsafe.Add(ref vectorBase, (uint)pixelIndex)); + } + } + + /// + /// Reassociates vectors with their stored binary16 alpha and packs them in one pass. + /// + /// The associated source vectors. + /// The destination pixels. + internal static void PackAssociated(Span source, Span destination) + { + ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int componentCount = source.Length * Vector128.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.Count; i += Vector512.Count) + { + Vector512 lower = ReassociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector512 upper = ReassociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))); + Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + { + Vector256 lower = ReassociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector256 upper = ReassociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))); + Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + { + Vector128 lower = ReassociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128 upper = ReassociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.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 vector = ReassociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128 packed = HalfTypeHelper.Pack(vector, vector); + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); + } + + return; + } + + ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); + ref RgbaHalfP pixelBase = ref Unsafe.As(ref destinationBase); + + for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) + { + Unsafe.Add(ref pixelBase, (uint)pixelIndex) = RgbaHalfP.FromAssociatedVector4(Unsafe.Add(ref vectorBase, (uint)pixelIndex)); + } + } + + /// + /// Associates RGB with alpha while preserving each alpha lane. + /// + /// The unassociated vectors. + /// The associated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Associate(Vector128 source) + { + Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); + Vector128 result = source * alpha; + return Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), alpha, result); + } + + /// + /// Associates RGB with alpha while preserving each alpha lane. + /// + /// The unassociated vectors. + /// The associated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Associate(Vector256 source) + { + Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); + Vector256 result = source * alpha; + return Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), alpha, result); + } + + /// + /// Associates RGB with alpha while preserving each alpha lane. + /// + /// The unassociated vectors. + /// The associated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Associate(Vector512 source) + { + Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); + Vector512 result = source * alpha; + Vector512 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); + } + + /// + /// Unassociates RGB while preserving each alpha lane. + /// + /// The associated vectors. + /// The unassociated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Unassociate(Vector128 source) + { + Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); + return Numerics.UnPremultiply(source, alpha); + } + + /// + /// Unassociates RGB while preserving each alpha lane. + /// + /// The associated vectors. + /// The unassociated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Unassociate(Vector256 source) + { + Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); + return Numerics.UnPremultiply(source, alpha); + } + + /// + /// Unassociates RGB while preserving each alpha lane. + /// + /// The associated vectors. + /// The unassociated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Unassociate(Vector512 source) + { + Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); + return Numerics.UnPremultiply(source, alpha); + } + + /// + /// Clamps vectors to the unit range represented by the pixel format. + /// + /// The vectors to clamp. + /// The clamped vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ClampUnit(Vector128 source) + { + Vector128 clamped = Vector128.Min(Vector128.Max(source, Vector128.Zero), Vector128.One); + + // Ordered comparison is false for NaN, restoring the source lane to match the scalar clamp contract. + return Vector128.ConditionalSelect(Vector128.Equals(source, source), clamped, source); + } + + /// + /// Clamps vectors to the unit range represented by the pixel format. + /// + /// The vectors to clamp. + /// The clamped vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ClampUnit(Vector256 source) + { + Vector256 clamped = Vector256.Min(Vector256.Max(source, Vector256.Zero), Vector256.One); + + // Ordered comparison is false for NaN, restoring the source lane to match the scalar clamp contract. + return Vector256.ConditionalSelect(Vector256.Equals(source, source), clamped, source); + } + + /// + /// Clamps vectors to the unit range represented by the pixel format. + /// + /// The vectors to clamp. + /// The clamped vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ClampUnit(Vector512 source) + { + Vector512 clamped = Vector512.Min(Vector512.Max(source, Vector512.Zero), Vector512.One); + + // Ordered comparison is false for NaN, restoring the source lane to match the scalar clamp contract. + return Vector512.ConditionalSelect(Vector512.Equals(source, source), clamped, source); + } + + /// + /// Associates unassociated vectors with the alpha value binary16 storage can reproduce. + /// + /// The unassociated vectors. + /// The associated vectors ready for packing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 AssociateForStorage(Vector128 source) + { + source = ClampUnit(source); + Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); + Vector128 storedAlpha = HalfTypeHelper.RoundToHalf(alpha); + Vector128 result = source * storedAlpha; + return Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); + } + + /// + /// Associates unassociated vectors with the alpha value binary16 storage can reproduce. + /// + /// The unassociated vectors. + /// The associated vectors ready for packing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 AssociateForStorage(Vector256 source) + { + source = ClampUnit(source); + Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); + Vector256 storedAlpha = HalfTypeHelper.RoundToHalf(alpha); + Vector256 result = source * storedAlpha; + return Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); + } + + /// + /// Associates unassociated vectors with the alpha value binary16 storage can reproduce. + /// + /// The unassociated vectors. + /// The associated vectors ready for packing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 AssociateForStorage(Vector512 source) + { + source = ClampUnit(source); + Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); + Vector512 storedAlpha = HalfTypeHelper.RoundToHalf(alpha); + Vector512 result = source * storedAlpha; + Vector512 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); + } + + /// + /// Reassociates vectors with the alpha value binary16 storage can reproduce. + /// + /// The associated vectors. + /// The associated vectors ready for packing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ReassociateForStorage(Vector128 source) + { + Vector128 zero = Vector128.Zero; + Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); + Vector128 clampedAlpha = ClampUnit(alpha); + Vector128 storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); + Vector128 result = source * (storedAlpha / alpha); + result = Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); + result = Vector128.Min(Vector128.Max(result, zero), storedAlpha); + return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(alpha, zero), zero, result); + } + + /// + /// Reassociates vectors with the alpha value binary16 storage can reproduce. + /// + /// The associated vectors. + /// The associated vectors ready for packing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ReassociateForStorage(Vector256 source) + { + Vector256 zero = Vector256.Zero; + Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); + Vector256 clampedAlpha = ClampUnit(alpha); + Vector256 storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); + Vector256 result = source * (storedAlpha / alpha); + result = Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); + result = Vector256.Min(Vector256.Max(result, zero), storedAlpha); + return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(alpha, zero), zero, result); + } + + /// + /// Reassociates vectors with the alpha value binary16 storage can reproduce. + /// + /// The associated vectors. + /// The associated vectors ready for packing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ReassociateForStorage(Vector512 source) + { + Vector512 zero = Vector512.Zero; + Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); + Vector512 clampedAlpha = ClampUnit(alpha); + Vector512 storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); + Vector512 result = source * (storedAlpha / alpha); + Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + result = Vector512.ConditionalSelect(alphaMask, storedAlpha, result); + result = Vector512.Min(Vector512.Max(result, zero), storedAlpha); + return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(alpha, zero), zero, result); + } + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short2.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short2.PixelOperations.cs index 7520b33e0b..4d6b066ad5 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short2.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short2.PixelOperations.cs @@ -16,8 +16,8 @@ public partial struct Short2 /// internal class PixelOperations : PixelOperations { - 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); } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short4.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short4.PixelOperations.cs index d9108be41d..83c3ce0531 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short4.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short4.PixelOperations.cs @@ -17,8 +17,8 @@ public partial struct Short4 /// internal class PixelOperations : PixelOperations { - 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); /// protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs index b84e0bf78e..a88823069d 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs @@ -7,11 +7,12 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing two 16-bit unsigned normalized values ranging from 0 to 1. -/// -/// Ranges from [0, 0, 0, 1] to [1, 1, 0, 1] in vector form. -/// +/// Packed pixel type containing two 16-bit unsigned normalized values. /// +/// +/// , , and scaled vector conversions return x and y in [0, 1], z +/// as 0, and implicit alpha as 1. The packed storage layout matches DXGI_FORMAT_R16G16_UNORM. +/// public partial struct Rg32 : IPixel, IPackedVector { private static readonly Vector2 Max = new(ushort.MaxValue); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba1010102.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba1010102.cs index 79913b499c..340e247772 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba1010102.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba1010102.cs @@ -7,12 +7,13 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// 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. -/// -/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. -/// /// +/// +/// and scaled vector conversions return all components in [0, 1]. The storage layout matches +/// DXGI_FORMAT_R10G10B10A2_UNORM. +/// public partial struct Rgba1010102 : IPixel, IPackedVector { private static readonly Vector4 Multiplier = new(1023F, 1023F, 1023F, 3F); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba64.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba64.cs index a9bc1e0627..300f2357fb 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba64.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba64.cs @@ -8,11 +8,12 @@ using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing four 16-bit unsigned normalized values ranging from 0 to 65535. -/// -/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. -/// +/// Packed pixel type containing four 16-bit unsigned normalized values. /// +/// +/// Component fields expose storage values in [0, 65535]. and scaled vector conversions +/// return them in [0, 1]. The storage layout matches DXGI_FORMAT_R16G16B16A16_UNORM. +/// [StructLayout(LayoutKind.Sequential)] public partial struct Rgba64 : IPixel, IPackedVector { diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalf.cs b/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalf.cs new file mode 100644 index 0000000000..573bfca55f --- /dev/null +++ b/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; + +/// +/// 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. +/// +/// +/// and scaled vector conversions return the same component values in the nominal color range +/// [0, 1]. The packed representation is binary-compatible with DXGI_FORMAT_R16G16B16A16_FLOAT. +/// +[StructLayout(LayoutKind.Sequential)] +public partial struct RgbaHalf : IPixel, IPackedVector +{ + /// + /// Gets or sets the red component. + /// + public Half R; + + /// + /// Gets or sets the green component. + /// + public Half G; + + /// + /// Gets or sets the blue component. + /// + public Half B; + + /// + /// Gets or sets the alpha component. + /// + public Half A; + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + public RgbaHalf(float r, float g, float b) + : this(r, g, b, 1F) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The alpha component. + 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; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector containing the component values. + public RgbaHalf(Vector4 vector) + : this(vector.X, vector.Y, vector.Z, vector.W) + { + } + + /// + public ulong PackedValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Unsafe.As(ref this) = value; + } + + /// + /// Compares two values for equality. + /// + /// The left value. + /// The right value. + /// when the values are equal. + public static bool operator ==(RgbaHalf left, RgbaHalf right) => left.Equals(right); + + /// + /// Compares two values for inequality. + /// + /// The left value. + /// The right value. + /// when the values are not equal. + public static bool operator !=(RgbaHalf left, RgbaHalf right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new((float)this.R, (float)this.G, (float)this.B, (float)this.A); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 16, 16, 16, 16), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalf FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalf FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalf FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalf FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalf FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalf FromVector4(Vector4 source) + { + source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); + return new RgbaHalf(source); + } + + /// + public static RgbaHalf FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is RgbaHalf other && this.Equals(other); + + /// + public readonly bool Equals(RgbaHalf other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() => FormattableString.Invariant($"RgbaHalf({(float)this.R:#0.##}, {(float)this.G:#0.##}, {(float)this.B:#0.##}, {(float)this.A:#0.##})"); +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalfP.cs b/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalfP.cs new file mode 100644 index 0000000000..75fe4907ce --- /dev/null +++ b/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; + +/// +/// 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. +/// +/// +/// and scaled vector conversions return the same associated component values in the nominal +/// color range [0, 1]. The packed representation is binary-compatible with +/// DXGI_FORMAT_R16G16B16A16_FLOAT. +/// +[StructLayout(LayoutKind.Sequential)] +public partial struct RgbaHalfP : IPixel, IPackedVector +{ + /// + /// Gets or sets the associated red component. + /// + public Half R; + + /// + /// Gets or sets the associated green component. + /// + public Half G; + + /// + /// Gets or sets the associated blue component. + /// + public Half B; + + /// + /// Gets or sets the alpha component. + /// + public Half A; + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + public RgbaHalfP(float r, float g, float b) + : this(r, g, b, 1F) + { + } + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + /// The alpha component. + 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; + } + + /// + /// Initializes a new instance of the struct from an associated vector. + /// + /// The associated vector. + public RgbaHalfP(Vector4 vector) + : this() => this = FromAssociatedScaledVector4(vector); + + /// + public ulong PackedValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Unsafe.As(ref this) = value; + } + + /// + /// Compares two values for equality. + /// + /// The left value. + /// The right value. + /// when the values are equal. + public static bool operator ==(RgbaHalfP left, RgbaHalfP right) => left.Equals(right); + + /// + /// Compares two values for inequality. + /// + /// The left value. + /// The right value. + /// when the values are not equal. + public static bool operator !=(RgbaHalfP left, RgbaHalfP right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToUnassociatedScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new((float)this.R, (float)this.G, (float)this.B, (float)this.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.UnPremultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToUnassociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 16, 16, 16, 16), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Associated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalfP FromScaledVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalfP FromVector4(Vector4 source) => FromAssociatedVector4(source); + + /// + [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); + } + + /// + [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); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalfP FromUnassociatedVector4(Vector4 source) => FromUnassociatedScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalfP FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + public static RgbaHalfP FromAbgr32(Abgr32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromArgb32(Argb32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromBgra5551(Bgra5551 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromBgr24(Bgr24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromBgra32(Bgra32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromL8(L8 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromL16(L16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromLa16(La16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromLa32(La32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromRgb24(Rgb24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromRgba32(Rgba32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromRgb48(Rgb48 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromRgba64(Rgba64 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is RgbaHalfP other && this.Equals(other); + + /// + public readonly bool Equals(RgbaHalfP other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() => FormattableString.Invariant($"RgbaHalfP({(float)this.R:#0.##}, {(float)this.G:#0.##}, {(float)this.B:#0.##}, {(float)this.A:#0.##})"); +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Short2.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Short2.cs index 59c6b4e0a0..d6ab2ac1a3 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Short2.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Short2.cs @@ -8,10 +8,12 @@ namespace SixLabors.ImageSharp.PixelFormats; /// /// Packed pixel type containing two 16-bit signed integer values. -/// -/// Ranges from [-32767, -32767, 0, 1] to [32767, 32767, 0, 1] in vector form. -/// /// +/// +/// and return stored components in [-32768, 32767]. +/// Scaled vector conversions map the full stored range to [0, 1]. The packed storage layout matches +/// DXGI_FORMAT_R16G16_SINT. +/// public partial struct Short2 : IPixel, IPackedVector { // Largest two byte positive number 0xFFFF >> 1; @@ -20,6 +22,9 @@ public partial struct Short2 : IPixel, IPackedVector // 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, IPackedVector 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, IPackedVector [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, IPackedVector [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) }; } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Short4.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Short4.cs index a128668fd0..fc3d3367df 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Short4.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Short4.cs @@ -8,10 +8,11 @@ namespace SixLabors.ImageSharp.PixelFormats; /// /// Packed pixel type containing four 16-bit signed integer values. -/// -/// Ranges from [-37267, -37267, -37267, -37267] to [37267, 37267, 37267, 37267] in vector form. -/// /// +/// +/// returns stored components in [-32768, 32767]. Scaled vector conversions map the full +/// stored range to [0, 1]. The packed storage layout matches DXGI_FORMAT_R16G16B16A16_SINT. +/// public partial struct Short4 : IPixel, IPackedVector { // Largest two byte positive number 0xFFFF >> 1; @@ -20,6 +21,9 @@ public partial struct Short4 : IPixel, IPackedVector // 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, IPackedVector 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, IPackedVector { 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, IPackedVector [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, IPackedVector [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Short4 FromScaledVector4(Vector4 source) { - source *= 65534F; - source -= new Vector4(32767F); + source *= Range; + source += Min; return FromVector4(source); } diff --git a/src/ImageSharp/PixelFormats/Utils/SignedShort4PixelOperations.cs b/src/ImageSharp/PixelFormats/Utils/SignedShort4PixelOperations.cs index 154d643894..4bf8c5e265 100644 --- a/src/ImageSharp/PixelFormats/Utils/SignedShort4PixelOperations.cs +++ b/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; /// /// 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)); diff --git a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AssociatedRgbaCompatible.cs b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AssociatedRgbaCompatible.cs index dd38e4118d..b47779df8d 100644 --- a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AssociatedRgbaCompatible.cs +++ b/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.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.Count / componentsPerPixel; for (; i <= destination.Length - pixelsPerVector; i += pixelsPerVector) { Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i * componentsPerPixel)); - Vector512 integers = Avx512F.ConvertToVector512Int32(packed); - Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Avx512F.ConvertToVector512Single(integers)); + Vector128 lowerShorts = Vector128.WidenLower(packed); + Vector128 upperShorts = Vector128.WidenUpper(packed); + Vector256 lowerIntegers = Vector256.Create(Vector128.WidenLower(lowerShorts), Vector128.WidenUpper(lowerShorts)); + Vector256 upperIntegers = Vector256.Create(Vector128.WidenLower(upperShorts), Vector128.WidenUpper(upperShorts)); + Vector512 integers = Vector512.Create(lowerIntegers, upperIntegers).AsInt32(); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Vector512.ConvertToSingle(integers)); } } - if (Avx2.IsSupported) + if (Vector256.IsHardwareAccelerated) { int pixelsPerVector = Vector256.Count / componentsPerPixel; for (; i <= destination.Length - pixelsPerVector; i += pixelsPerVector) { ulong packed = Unsafe.ReadUnaligned(ref Unsafe.Add(ref sourceBase, (uint)(i * componentsPerPixel))); - Vector256 integers = Avx2.ConvertToVector256Int32(Vector128.CreateScalarUnsafe(packed).AsByte()); - Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Avx.ConvertToVector256Single(integers)); + Vector128 shorts = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()); + Vector256 integers = Vector256.Create(Vector128.WidenLower(shorts), Vector128.WidenUpper(shorts)).AsInt32(); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Vector256.ConvertToSingle(integers)); } } diff --git a/tests/ImageSharp.Tests/PixelFormats/AssociatedAlphaPixelTests.cs b/tests/ImageSharp.Tests/PixelFormats/AssociatedAlphaPixelTests.cs index f63f853446..07a5c4dcc3 100644 --- a/tests/ImageSharp.Tests/PixelFormats/AssociatedAlphaPixelTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/AssociatedAlphaPixelTests.cs @@ -121,6 +121,17 @@ public class NormalizedByte4PTests : AssociatedAlphaPixelTests 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 [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 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 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 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.Instance.From(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(); @@ -1239,6 +1284,9 @@ public class AssociatedDestinationAlphaQuantizationTests [Fact] public void ColorToHalfVector4PUsesDestinationAlphaRepresentation() => AssertColorUsesDestinationAlphaRepresentation(); + [Fact] + public void ColorToRgbaHalfPUsesDestinationAlphaRepresentation() => AssertColorUsesDestinationAlphaRepresentation(); + /// /// Verifies the unsigned-byte destination grid through scalar and bulk conversion entry points. /// diff --git a/tests/ImageSharp.Tests/PixelFormats/HalfSingleTests.cs b/tests/ImageSharp.Tests/PixelFormats/HalfSingleTests.cs index 9366c51c98..f8f1276252 100644 --- a/tests/ImageSharp.Tests/PixelFormats/HalfSingleTests.cs +++ b/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.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale); + Assert.Equal(expectedVectors, actualVectors); + + Vector4[] destructiveSource = [.. expectedVectors]; + HalfSingle[] actualPixels = new HalfSingle[source.Length]; + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.Equal(source, actualPixels); } [Fact] diff --git a/tests/ImageSharp.Tests/PixelFormats/HalfVector2Tests.cs b/tests/ImageSharp.Tests/PixelFormats/HalfVector2Tests.cs index c5a89df1e9..8adc38bd36 100644 --- a/tests/ImageSharp.Tests/PixelFormats/HalfVector2Tests.cs +++ b/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.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale); + Assert.Equal(expectedVectors, actualVectors); + + Vector4[] destructiveSource = [.. expectedVectors]; + HalfVector2[] actualPixels = new HalfVector2[source.Length]; + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.Equal(source, actualPixels); + } + [Fact] public void HalfVector2_ToVector4() { diff --git a/tests/ImageSharp.Tests/PixelFormats/HalfVector4Tests.cs b/tests/ImageSharp.Tests/PixelFormats/HalfVector4Tests.cs index 16c78a23d3..257eceb9f6 100644 --- a/tests/ImageSharp.Tests/PixelFormats/HalfVector4Tests.cs +++ b/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.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale); + Assert.Equal(expectedVectors, actualVectors); + + Vector4[] destructiveSource = [.. expectedVectors]; + HalfVector4[] actualPixels = new HalfVector4[source.Length]; + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.Equal(source, actualPixels); + } + [Fact] public void HalfVector4_FromBgra5551() { diff --git a/tests/ImageSharp.Tests/PixelFormats/NormalizedByte2Tests.cs b/tests/ImageSharp.Tests/PixelFormats/NormalizedByte2Tests.cs index ffbddb1398..954d388c05 100644 --- a/tests/ImageSharp.Tests/PixelFormats/NormalizedByte2Tests.cs +++ b/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.Instance.ToVector4(Configuration.Default, source, native); + PixelOperations.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.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.All(actualPixels, actual => Assert.Equal((ushort)0x8181, actual.PackedValue)); + } + [Fact] public void NormalizedByte2_ToVector4() { diff --git a/tests/ImageSharp.Tests/PixelFormats/NormalizedByte4Tests.cs b/tests/ImageSharp.Tests/PixelFormats/NormalizedByte4Tests.cs index 5a025f6c4e..0fdb88caef 100644 --- a/tests/ImageSharp.Tests/PixelFormats/NormalizedByte4Tests.cs +++ b/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.Instance.ToVector4(Configuration.Default, source, native); + PixelOperations.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.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.All(actualPixels, actual => Assert.Equal(0x81818181U, actual.PackedValue)); + } + [Fact] public void NormalizedByte4_ToScaledVector4() { diff --git a/tests/ImageSharp.Tests/PixelFormats/NormalizedShort2Tests.cs b/tests/ImageSharp.Tests/PixelFormats/NormalizedShort2Tests.cs index be7b390527..e9f97d27f0 100644 --- a/tests/ImageSharp.Tests/PixelFormats/NormalizedShort2Tests.cs +++ b/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.Instance.ToVector4(Configuration.Default, source, native); + PixelOperations.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.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.All(actualPixels, actual => Assert.Equal(0x80018001U, actual.PackedValue)); + } + [Fact] public void NormalizedShort2_ToVector4() { diff --git a/tests/ImageSharp.Tests/PixelFormats/NormalizedShort4Tests.cs b/tests/ImageSharp.Tests/PixelFormats/NormalizedShort4Tests.cs index 281ae7ee52..9ed3456def 100644 --- a/tests/ImageSharp.Tests/PixelFormats/NormalizedShort4Tests.cs +++ b/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.Instance.ToVector4(Configuration.Default, source, native); + PixelOperations.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.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.All(actualPixels, actual => Assert.Equal(0x8001800180018001UL, actual.PackedValue)); + } + [Fact] public void NormalizedShort4_ToScaledVector4() { diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelAlphaRepresentationTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelAlphaRepresentationTests.cs index 036f1cd712..c6afe55b5c 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelAlphaRepresentationTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelAlphaRepresentationTests.cs @@ -308,7 +308,7 @@ public abstract class PixelAlphaRepresentationTests } /// -/// Verifies alpha conversion for formats whose native vector space differs from scaled color space. +/// Verifies alpha conversion for formats with distinct native vector contracts. /// [Trait("Category", "PixelFormats")] public class AffineNativeAlphaRepresentationTests @@ -321,7 +321,7 @@ public class AffineNativeAlphaRepresentationTests [Fact] public void Short4NativeConversionsUseScaledAlpha() - => AssertNativeConversions(static vector => (vector * 65534F) - new Vector4(32767F)); + => AssertNativeConversions(static vector => (vector * ushort.MaxValue) - new Vector4(32768F)); [Fact] public void NormalizedByte4NativeConversionsUseScaledAlpha() @@ -337,19 +337,19 @@ public class AffineNativeAlphaRepresentationTests [Fact] public void HalfVector4NativeConversionsUseScaledAlpha() - => AssertNativeConversions(static vector => (vector * 2F) - Vector4.One); + => AssertNativeConversions(static vector => (vector * 131008F) - new Vector4(65504F)); [Fact] public void HalfVector4PNativeConversionsUseScaledAlpha() - => AssertNativeConversions(static vector => (vector * 2F) - Vector4.One); + => AssertNativeConversions(static vector => (vector * 131008F) - new Vector4(65504F)); [Fact] public void HalfSingleFromAssociatedNativeVectorUsesScaledAlpha() - => AssertAlphaLessNativeFrom(static vector => new Vector4((vector.X * 2F) - 1F, 0F, 0F, vector.W)); + => AssertAlphaLessNativeFrom(static vector => new Vector4((vector.X * 131008F) - 65504F, 0F, 0F, vector.W)); [Fact] public void HalfVector2FromAssociatedNativeVectorUsesScaledAlpha() - => AssertAlphaLessNativeFrom(static vector => new Vector4((vector.X * 2F) - 1F, (vector.Y * 2F) - 1F, 0F, vector.W)); + => AssertAlphaLessNativeFrom(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(static vector => new Vector4((vector.X * 65534F) - 32767F, (vector.Y * 65534F) - 32767F, 0F, vector.W)); + => AssertAlphaLessNativeFrom(static vector => new Vector4((vector.X * ushort.MaxValue) - 32768F, (vector.Y * ushort.MaxValue) - 32768F, 0F, vector.W)); private static void AssertNativeConversions(Func encodeNative) where TPixel : unmanaged, IPixel @@ -486,6 +486,8 @@ public class Rgba1010102AlphaRepresentationTests : PixelAlphaRepresentationTests public class Rgba128AlphaRepresentationTests : PixelAlphaRepresentationTests { } public class Rgba32AlphaRepresentationTests : PixelAlphaRepresentationTests { } public class Rgba32PAlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class RgbaHalfAlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class RgbaHalfPAlphaRepresentationTests : PixelAlphaRepresentationTests { } public class Rgba64AlphaRepresentationTests : PixelAlphaRepresentationTests { } public class RgbaVectorAlphaRepresentationTests : PixelAlphaRepresentationTests { } public class Short2AlphaRepresentationTests : PixelAlphaRepresentationTests { } diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/PixelOperationsTests.Specialized.Generated.cs b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/PixelOperationsTests.Specialized.Generated.cs index f287a69958..3747b41eeb 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/PixelOperationsTests.Specialized.Generated.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/PixelOperationsTests.Specialized.Generated.cs @@ -492,6 +492,36 @@ public partial class PixelOperationsTests } } + public partial class RgbaHalf_OperationsTests : PixelOperationsTests + { + 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 + { + 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 { public RgbaVector_OperationsTests(ITestOutputHelper output) diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/_Common.ttinclude b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/_Common.ttinclude index 43dec7937a..e4a7e881e6 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/_Common.ttinclude +++ b/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" diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs index 7be839e72d..c569919f5f 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs @@ -102,6 +102,35 @@ public abstract class PixelOperationsTests : MeasureFixture return expected; } + /// + /// Creates the scalar oracle for companded scaled input. + /// + /// The source vectors. + /// Whether the source vectors use associated alpha. + /// The converted pixels. + 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 : 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 : 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 : MeasureFixture new TestPixel(), new TestPixel(), new TestPixel(), + new TestPixel(), + new TestPixel(), new TestPixel(), new TestPixel(), new TestPixel(), diff --git a/tests/ImageSharp.Tests/PixelFormats/RgbaHalfTests.cs b/tests/ImageSharp.Tests/PixelFormats/RgbaHalfTests.cs new file mode 100644 index 0000000000..9b22a9f8c8 --- /dev/null +++ b/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; + +/// +/// Tests the unit-range binary16 RGBA pixel formats. +/// +[Trait("Category", "PixelFormats")] +public class RgbaHalfTests +{ + /// + /// Verifies that the unassociated format has the native layout required by RGBA binary16 surfaces. + /// + [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()); + Assert.Equal(expected, pixel.PackedValue); + Assert.Equal(new Vector4(.25F, .5F, .75F, 1F), pixel.ToVector4()); + Assert.Equal(pixel.ToVector4(), pixel.ToScaledVector4()); + } + + /// + /// Verifies that zero-filled binary16 storage represents transparent black without affine remapping. + /// + [Fact] + public void RgbaHalfDefaultIsTransparentBlack() + { + RgbaHalf pixel = default; + + Assert.Equal(Vector4.Zero, pixel.ToVector4()); + Assert.Equal(Vector4.Zero, pixel.ToScaledVector4()); + } + + /// + /// Verifies that scaled input is clamped to the pixel format's unit color range. + /// + [Fact] + public void RgbaHalfFromScaledVector4ClampsToUnitRange() + { + RgbaHalf pixel = RgbaHalf.FromScaledVector4(new Vector4(-1F, .5F, 2F, 1F)); + + Assert.Equal(new Vector4(0F, .5F, 1F, 1F), pixel.ToScaledVector4()); + } + + /// + /// Verifies that the associated format stores associated binary16 components in the same RGBA order. + /// + [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()); + Assert.Equal(expected, pixel.PackedValue); + Assert.Equal(new Vector4(.125F, .25F, .375F, .5F), pixel.ToAssociatedScaledVector4()); + } + + /// + /// Verifies that zero-filled associated binary16 storage represents transparent black. + /// + [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()); + } + + /// + /// Verifies that association uses the alpha value that survives binary16 quantization. + /// + [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); + } + + /// + /// Verifies every bulk representation path against its scalar pixel contract at each SIMD width and tail boundary. + /// + [Fact] + public void RgbaHalfBulkConversionsMatchScalarAcrossHardwareWidths() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + static () => AssertBulkConversionsMatchScalar(), + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Verifies every associated bulk representation path against its scalar pixel contract at each SIMD width and tail boundary. + /// + [Fact] + public void RgbaHalfPBulkConversionsMatchScalarAcrossHardwareWidths() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + static () => AssertBulkConversionsMatchScalar(), + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Verifies the declared component precision and alpha representation for the unassociated format. + /// + [Fact] + public void RgbaHalfPixelInformationIsCorrect() => AssertPixelInformation(PixelAlphaRepresentation.Unassociated); + + /// + /// Verifies the declared component precision and alpha representation for the associated format. + /// + [Fact] + public void RgbaHalfPPixelInformationIsCorrect() => AssertPixelInformation(PixelAlphaRepresentation.Associated); + + /// + /// Compares bulk conversions with the corresponding scalar pixel operations for representative vector widths and remainders. + /// + /// The binary16 pixel format to test. + private static void AssertBulkConversionsMatchScalar() + where TPixel : unmanaged, IPixel + { + 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(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(sourceVectors[i], associated, scaled); + } + + PixelOperations.Instance.ToVector4(Configuration.Default, pixels, actualVectors, modifier); + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, sourceVectors.AsSpan(), actualPixels, modifier); + + Assert.Equal(expectedVectors, actualVectors); + Assert.Equal(expectedPixels, actualPixels); + } + } + } + + /// + /// Creates a deterministic unassociated unit-range vector for bulk conversion tests. + /// + /// The source index used to vary the component values. + /// The generated vector. + private static Vector4 CreateUnassociatedVector(int index) + => new( + ((index * 37) % 101) / 100F, + ((index * 53) % 101) / 100F, + ((index * 71) % 101) / 100F, + ((index * 89) % 101) / 100F); + + /// + /// Resolves whether a modifier combination requests associated output under the pixel format's native representation. + /// + /// The pixel format whose native representation participates in modifier resolution. + /// The conversion modifiers. + /// when the resulting vector representation is associated. + private static bool RequestsAssociated(PixelConversionModifiers modifiers) + where TPixel : unmanaged, IPixel + => modifiers.IsDefined(PixelConversionModifiers.Premultiply) + || (TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Associated + && !modifiers.IsDefined(PixelConversionModifiers.UnPremultiply)); + + /// + /// Converts one pixel through the scalar API selected by the requested representation and range. + /// + /// The source pixel format. + /// The source pixel. + /// Whether the result should use associated alpha. + /// Whether the result should use the scaled range. + /// The converted vector. + private static Vector4 ToScalarVector(TPixel pixel, bool associated, bool scaled) + where TPixel : unmanaged, IPixel + => (associated, scaled) switch + { + (true, true) => pixel.ToAssociatedScaledVector4(), + (true, false) => pixel.ToAssociatedVector4(), + (false, true) => pixel.ToUnassociatedScaledVector4(), + _ => pixel.ToUnassociatedVector4() + }; + + /// + /// Converts one vector through the scalar API selected by its representation and range. + /// + /// The destination pixel format. + /// The source vector. + /// Whether the source uses associated alpha. + /// Whether the source uses the scaled range. + /// The converted pixel. + private static TPixel FromScalarVector(Vector4 vector, bool associated, bool scaled) + where TPixel : unmanaged, IPixel + => (associated, scaled) switch + { + (true, true) => TPixel.FromAssociatedScaledVector4(vector), + (true, false) => TPixel.FromAssociatedVector4(vector), + (false, true) => TPixel.FromUnassociatedScaledVector4(vector), + _ => TPixel.FromUnassociatedVector4(vector) + }; + + /// + /// Verifies the component layout and alpha metadata exposed by a binary16 pixel format. + /// + /// The pixel format to inspect. + /// The expected alpha representation. + private static void AssertPixelInformation(PixelAlphaRepresentation alphaRepresentation) + where TPixel : unmanaged, IPixel + { + 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)); + } + } +} + +/// +/// Applies the shared associated-alpha contract to . +/// +public class RgbaHalfPAssociatedAlphaTests : AssociatedAlphaPixelTests +{ +} diff --git a/tests/ImageSharp.Tests/PixelFormats/Short2Tests.cs b/tests/ImageSharp.Tests/PixelFormats/Short2Tests.cs index f23da0c7a6..c0ee816f1a 100644 --- a/tests/ImageSharp.Tests/PixelFormats/Short2Tests.cs +++ b/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.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale); + Assert.Equal(expectedVectors, actualVectors); + + Vector4[] destructiveSource = [.. expectedVectors]; + Short2[] actualPixels = new Short2[length]; + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.Equal(source, actualPixels); } [Fact] diff --git a/tests/ImageSharp.Tests/PixelFormats/Short4Tests.cs b/tests/ImageSharp.Tests/PixelFormats/Short4Tests.cs index 819ff0e1e5..68f774f154 100644 --- a/tests/ImageSharp.Tests/PixelFormats/Short4Tests.cs +++ b/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.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale); + Assert.Equal(expectedVectors, actualVectors); + + Vector4[] destructiveSource = [.. expectedVectors]; + Short4[] actualPixels = new Short4[length]; + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.Equal(source, actualPixels); } [Fact] diff --git a/tests/ImageSharp.Tests/Processing/Filters/BrightnessTest.cs b/tests/ImageSharp.Tests/Processing/Filters/BrightnessTest.cs index 1352124004..d5f7f69d99 100644 --- a/tests/ImageSharp.Tests/Processing/Filters/BrightnessTest.cs +++ b/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 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 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(Configuration.Default, 100, 100, new HalfSingle(-0.5f)); + halfSingleImage = new Image(Configuration.Default, 100, 100, new HalfSingle(-32752F)); halfSingleImage.Mutate(x => x.ApplyProcessor(new BrightnessProcessor(2)));