mirror of https://github.com/SixLabors/ImageSharp
3 changed files with 363 additions and 12 deletions
@ -0,0 +1,253 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Runtime.Intrinsics; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Metadata.Profiles.Icc; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts integer ICC lookup-table entries to their normalized single-precision representation.
|
||||
|
/// </summary>
|
||||
|
internal static class IccLutNormalizer |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines the scalar and SIMD conversion for an integer lookup-table element type.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The integer element type.</typeparam>
|
||||
|
private interface INormalizeOperator<T> |
||||
|
where T : unmanaged |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the divisor that maps the integer range to <c>[0, 1]</c>.
|
||||
|
/// </summary>
|
||||
|
public static abstract float Divisor { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts one scalar value.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The integer value.</param>
|
||||
|
/// <returns>The normalized value.</returns>
|
||||
|
public static abstract float Invoke(T source); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts one 128-bit input vector and stores the expanded single-precision results.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The packed integer values.</param>
|
||||
|
/// <param name="divisor">The normalization divisor.</param>
|
||||
|
/// <param name="destination">The first destination element.</param>
|
||||
|
public static abstract void Invoke(Vector128<T> source, Vector128<float> divisor, ref float destination); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts one 256-bit input vector and stores the expanded single-precision results.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The packed integer values.</param>
|
||||
|
/// <param name="divisor">The normalization divisor.</param>
|
||||
|
/// <param name="destination">The first destination element.</param>
|
||||
|
public static abstract void Invoke(Vector256<T> source, Vector256<float> divisor, ref float destination); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts one 512-bit input vector and stores the expanded single-precision results.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The packed integer values.</param>
|
||||
|
/// <param name="divisor">The normalization divisor.</param>
|
||||
|
/// <param name="destination">The first destination element.</param>
|
||||
|
public static abstract void Invoke(Vector512<T> source, Vector512<float> divisor, ref float destination); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts byte lookup-table entries to normalized single-precision values.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The integer lookup-table entries.</param>
|
||||
|
/// <param name="destination">The normalized destination values.</param>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public static void Normalize(ReadOnlySpan<byte> source, Span<float> destination) |
||||
|
=> Normalize<byte, ByteNormalizeOperator>(source, destination); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts unsigned-short lookup-table entries to normalized single-precision values.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The integer lookup-table entries.</param>
|
||||
|
/// <param name="destination">The normalized destination values.</param>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public static void Normalize(ReadOnlySpan<ushort> source, Span<float> destination) |
||||
|
=> Normalize<ushort, UInt16NormalizeOperator>(source, destination); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts an integer lookup table using the widest available portable SIMD width, followed by narrower
|
||||
|
/// widths and a scalar remainder.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The integer element type.</typeparam>
|
||||
|
/// <typeparam name="TOperator">The conversion implementation.</typeparam>
|
||||
|
/// <param name="source">The integer lookup-table entries.</param>
|
||||
|
/// <param name="destination">The normalized destination values.</param>
|
||||
|
private static void Normalize<T, TOperator>(ReadOnlySpan<T> source, Span<float> destination) |
||||
|
where T : unmanaged |
||||
|
where TOperator : struct, INormalizeOperator<T> |
||||
|
{ |
||||
|
ref T sourceRef = ref MemoryMarshal.GetReference(source); |
||||
|
ref float destinationRef = ref MemoryMarshal.GetReference(destination); |
||||
|
nuint length = (uint)source.Length; |
||||
|
nuint index = 0; |
||||
|
|
||||
|
if (Vector512.IsHardwareAccelerated) |
||||
|
{ |
||||
|
Vector512<float> divisor = Vector512.Create(TOperator.Divisor); |
||||
|
nuint count = (uint)Vector512<T>.Count; |
||||
|
|
||||
|
while (length - index >= count) |
||||
|
{ |
||||
|
ref float destinationStart = ref Unsafe.Add(ref destinationRef, index); |
||||
|
TOperator.Invoke(Vector512.LoadUnsafe(ref sourceRef, index), divisor, ref destinationStart); |
||||
|
index += count; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (Vector256.IsHardwareAccelerated) |
||||
|
{ |
||||
|
Vector256<float> divisor = Vector256.Create(TOperator.Divisor); |
||||
|
nuint count = (uint)Vector256<T>.Count; |
||||
|
|
||||
|
while (length - index >= count) |
||||
|
{ |
||||
|
ref float destinationStart = ref Unsafe.Add(ref destinationRef, index); |
||||
|
TOperator.Invoke(Vector256.LoadUnsafe(ref sourceRef, index), divisor, ref destinationStart); |
||||
|
index += count; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (Vector128.IsHardwareAccelerated) |
||||
|
{ |
||||
|
Vector128<float> divisor = Vector128.Create(TOperator.Divisor); |
||||
|
nuint count = (uint)Vector128<T>.Count; |
||||
|
|
||||
|
while (length - index >= count) |
||||
|
{ |
||||
|
ref float destinationStart = ref Unsafe.Add(ref destinationRef, index); |
||||
|
TOperator.Invoke(Vector128.LoadUnsafe(ref sourceRef, index), divisor, ref destinationStart); |
||||
|
index += count; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Preserve the scalar division expression for the final partial vector. Multiplication by a reciprocal
|
||||
|
// is not bit-equivalent for every input and would change the values stored in the ICC profile model.
|
||||
|
while (index < length) |
||||
|
{ |
||||
|
Unsafe.Add(ref destinationRef, index) = TOperator.Invoke(Unsafe.Add(ref sourceRef, index)); |
||||
|
index++; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts packed byte entries to normalized single-precision values.
|
||||
|
/// </summary>
|
||||
|
private readonly struct ByteNormalizeOperator : INormalizeOperator<byte> |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public static float Divisor => byte.MaxValue; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public static float Invoke(byte source) |
||||
|
=> source / (float)byte.MaxValue; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public static void Invoke(Vector128<byte> source, Vector128<float> divisor, ref float destination) |
||||
|
{ |
||||
|
// [b0..b15] becomes four ordered groups of four UInt32 values. Every widened value is at most
|
||||
|
// 255, so reinterpreting UInt32 as Int32 before conversion preserves its numeric value.
|
||||
|
(Vector128<ushort> lower16, Vector128<ushort> upper16) = Vector128.Widen(source); |
||||
|
(Vector128<uint> values0, Vector128<uint> values1) = Vector128.Widen(lower16); |
||||
|
(Vector128<uint> values2, Vector128<uint> values3) = Vector128.Widen(upper16); |
||||
|
|
||||
|
(Vector128.ConvertToSingle(values0.AsInt32()) / divisor).StoreUnsafe(ref destination); |
||||
|
(Vector128.ConvertToSingle(values1.AsInt32()) / divisor).StoreUnsafe(ref destination, 4); |
||||
|
(Vector128.ConvertToSingle(values2.AsInt32()) / divisor).StoreUnsafe(ref destination, 8); |
||||
|
(Vector128.ConvertToSingle(values3.AsInt32()) / divisor).StoreUnsafe(ref destination, 12); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public static void Invoke(Vector256<byte> source, Vector256<float> divisor, ref float destination) |
||||
|
{ |
||||
|
// [b0..b31] becomes four ordered groups of eight UInt32 values, matching four contiguous
|
||||
|
// Vector256<float> stores without shuffling the converted results.
|
||||
|
(Vector256<ushort> lower16, Vector256<ushort> upper16) = Vector256.Widen(source); |
||||
|
(Vector256<uint> values0, Vector256<uint> values1) = Vector256.Widen(lower16); |
||||
|
(Vector256<uint> values2, Vector256<uint> values3) = Vector256.Widen(upper16); |
||||
|
|
||||
|
(Vector256.ConvertToSingle(values0.AsInt32()) / divisor).StoreUnsafe(ref destination); |
||||
|
(Vector256.ConvertToSingle(values1.AsInt32()) / divisor).StoreUnsafe(ref destination, 8); |
||||
|
(Vector256.ConvertToSingle(values2.AsInt32()) / divisor).StoreUnsafe(ref destination, 16); |
||||
|
(Vector256.ConvertToSingle(values3.AsInt32()) / divisor).StoreUnsafe(ref destination, 24); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public static void Invoke(Vector512<byte> source, Vector512<float> divisor, ref float destination) |
||||
|
{ |
||||
|
// [b0..b63] becomes four ordered groups of sixteen UInt32 values, matching four contiguous
|
||||
|
// Vector512<float> stores. The portable widening APIs map to zero-extension instructions.
|
||||
|
(Vector512<ushort> lower16, Vector512<ushort> upper16) = Vector512.Widen(source); |
||||
|
(Vector512<uint> values0, Vector512<uint> values1) = Vector512.Widen(lower16); |
||||
|
(Vector512<uint> values2, Vector512<uint> values3) = Vector512.Widen(upper16); |
||||
|
|
||||
|
(Vector512.ConvertToSingle(values0.AsInt32()) / divisor).StoreUnsafe(ref destination); |
||||
|
(Vector512.ConvertToSingle(values1.AsInt32()) / divisor).StoreUnsafe(ref destination, 16); |
||||
|
(Vector512.ConvertToSingle(values2.AsInt32()) / divisor).StoreUnsafe(ref destination, 32); |
||||
|
(Vector512.ConvertToSingle(values3.AsInt32()) / divisor).StoreUnsafe(ref destination, 48); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts packed unsigned-short entries to normalized single-precision values.
|
||||
|
/// </summary>
|
||||
|
private readonly struct UInt16NormalizeOperator : INormalizeOperator<ushort> |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public static float Divisor => ushort.MaxValue; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public static float Invoke(ushort source) |
||||
|
=> source / (float)ushort.MaxValue; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public static void Invoke(Vector128<ushort> source, Vector128<float> divisor, ref float destination) |
||||
|
{ |
||||
|
// [u0..u7] becomes two ordered groups of four UInt32 values. Every value is at most 65535,
|
||||
|
// so signed conversion after reinterpretation is numerically identical to unsigned conversion.
|
||||
|
(Vector128<uint> lower, Vector128<uint> upper) = Vector128.Widen(source); |
||||
|
|
||||
|
(Vector128.ConvertToSingle(lower.AsInt32()) / divisor).StoreUnsafe(ref destination); |
||||
|
(Vector128.ConvertToSingle(upper.AsInt32()) / divisor).StoreUnsafe(ref destination, 4); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public static void Invoke(Vector256<ushort> source, Vector256<float> divisor, ref float destination) |
||||
|
{ |
||||
|
// [u0..u15] becomes two ordered groups of eight UInt32 values, matching two contiguous
|
||||
|
// Vector256<float> stores without a result shuffle.
|
||||
|
(Vector256<uint> lower, Vector256<uint> upper) = Vector256.Widen(source); |
||||
|
|
||||
|
(Vector256.ConvertToSingle(lower.AsInt32()) / divisor).StoreUnsafe(ref destination); |
||||
|
(Vector256.ConvertToSingle(upper.AsInt32()) / divisor).StoreUnsafe(ref destination, 8); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
public static void Invoke(Vector512<ushort> source, Vector512<float> divisor, ref float destination) |
||||
|
{ |
||||
|
// [u0..u31] becomes two ordered groups of sixteen UInt32 values, matching two contiguous
|
||||
|
// Vector512<float> stores. The portable widening APIs map to zero-extension instructions.
|
||||
|
(Vector512<uint> lower, Vector512<uint> upper) = Vector512.Widen(source); |
||||
|
|
||||
|
(Vector512.ConvertToSingle(lower.AsInt32()) / divisor).StoreUnsafe(ref destination); |
||||
|
(Vector512.ConvertToSingle(upper.AsInt32()) / divisor).StoreUnsafe(ref destination, 16); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,108 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.ICC.Various; |
||||
|
|
||||
|
[Trait("Profile", "Icc")] |
||||
|
public class IccLutTests |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets lengths that exercise scalar execution, every SIMD width, mixed-width remainders, and multiple vectors.
|
||||
|
/// </summary>
|
||||
|
public static TheoryData<int> LutLengths => new() |
||||
|
{ |
||||
|
0, |
||||
|
1, |
||||
|
7, |
||||
|
8, |
||||
|
9, |
||||
|
15, |
||||
|
16, |
||||
|
17, |
||||
|
31, |
||||
|
32, |
||||
|
33, |
||||
|
63, |
||||
|
64, |
||||
|
65, |
||||
|
255, |
||||
|
256, |
||||
|
257 |
||||
|
}; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that byte LUT construction preserves the scalar conversion result for every traversal shape.
|
||||
|
/// </summary>
|
||||
|
/// <param name="length">The number of LUT entries.</param>
|
||||
|
[Theory] |
||||
|
[MemberData(nameof(LutLengths))] |
||||
|
public void ByteConstructorMatchesScalarFormula(int length) |
||||
|
{ |
||||
|
byte[] values = new byte[length]; |
||||
|
|
||||
|
for (int i = 0; i < values.Length; i++) |
||||
|
{ |
||||
|
values[i] = (byte)((i * 73) + 19); |
||||
|
} |
||||
|
|
||||
|
IccLut actual = new(values); |
||||
|
|
||||
|
Assert.Equal(values.Length, actual.Values.Length); |
||||
|
|
||||
|
for (int i = 0; i < values.Length; i++) |
||||
|
{ |
||||
|
float expected = values[i] / (float)byte.MaxValue; |
||||
|
Assert.Equal(BitConverter.SingleToInt32Bits(expected), BitConverter.SingleToInt32Bits(actual.Values[i])); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that unsigned-short LUT construction preserves the scalar conversion result for every traversal shape.
|
||||
|
/// </summary>
|
||||
|
/// <param name="length">The number of LUT entries.</param>
|
||||
|
[Theory] |
||||
|
[MemberData(nameof(LutLengths))] |
||||
|
public void UInt16ConstructorMatchesScalarFormula(int length) |
||||
|
{ |
||||
|
ushort[] values = new ushort[length]; |
||||
|
|
||||
|
for (int i = 0; i < values.Length; i++) |
||||
|
{ |
||||
|
values[i] = (ushort)((i * 12_347) + 1_019); |
||||
|
} |
||||
|
|
||||
|
IccLut actual = new(values); |
||||
|
|
||||
|
Assert.Equal(values.Length, actual.Values.Length); |
||||
|
|
||||
|
for (int i = 0; i < values.Length; i++) |
||||
|
{ |
||||
|
float expected = values[i] / (float)ushort.MaxValue; |
||||
|
Assert.Equal(BitConverter.SingleToInt32Bits(expected), BitConverter.SingleToInt32Bits(actual.Values[i])); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies bit-exact normalization for every possible unsigned-short input.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void UInt16ConstructorMatchesScalarFormulaForEveryValue() |
||||
|
{ |
||||
|
ushort[] values = new ushort[ushort.MaxValue + 1]; |
||||
|
|
||||
|
for (int i = 0; i < values.Length; i++) |
||||
|
{ |
||||
|
values[i] = (ushort)i; |
||||
|
} |
||||
|
|
||||
|
IccLut actual = new(values); |
||||
|
|
||||
|
for (int i = 0; i < values.Length; i++) |
||||
|
{ |
||||
|
float expected = values[i] / (float)ushort.MaxValue; |
||||
|
Assert.Equal(BitConverter.SingleToInt32Bits(expected), BitConverter.SingleToInt32Bits(actual.Values[i])); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue