diff --git a/src/ImageSharp/ColorSpaces/Companding/SRgbCompanding.cs b/src/ImageSharp/ColorSpaces/Companding/SRgbCompanding.cs index 9a8b5f0a84..dc6c960aa5 100644 --- a/src/ImageSharp/ColorSpaces/Companding/SRgbCompanding.cs +++ b/src/ImageSharp/ColorSpaces/Companding/SRgbCompanding.cs @@ -5,6 +5,10 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +#if SUPPORTS_RUNTIME_INTRINSICS +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif namespace SixLabors.ImageSharp.ColorSpaces.Companding { @@ -18,21 +22,83 @@ namespace SixLabors.ImageSharp.ColorSpaces.Companding /// public static class SRgbCompanding { + private const int Length = Scale + 2; // 256kb @ 16bit precision. + private const int Scale = (1 << 16) - 1; + + private static readonly Lazy LazyCompressTable = new Lazy( + () => + { + var result = new float[Length]; + + for (int i = 0; i < result.Length; i++) + { + double d = (double)i / Scale; + if (d <= (0.04045 / 12.92)) + { + d *= 12.92; + } + else + { + d = (1.055 * Math.Pow(d, 1.0 / 2.4)) - 0.055; + } + + result[i] = (float)d; + } + + return result; + }, + true); + + private static readonly Lazy LazyExpandTable = new Lazy( + () => + { + var result = new float[Length]; + + for (int i = 0; i < result.Length; i++) + { + double d = (double)i / Scale; + if (d <= 0.04045) + { + d /= 12.92; + } + else + { + d = Math.Pow((d + 0.055) / 1.055, 2.4); + } + + result[i] = (float)d; + } + + return result; + }, + true); + + private static float[] ExpandTable => LazyExpandTable.Value; + + private static float[] CompressTable => LazyCompressTable.Value; + /// /// Expands the companded vectors to their linear equivalents with respect to the energy. /// /// The span of vectors. - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Expand(Span vectors) { - ref Vector4 vectorsStart = ref MemoryMarshal.GetReference(vectors); - ref Vector4 vectorsEnd = ref Unsafe.Add(ref vectorsStart, vectors.Length); - - while (Unsafe.IsAddressLessThan(ref vectorsStart, ref vectorsEnd)) +#if SUPPORTS_RUNTIME_INTRINSICS + if (Avx2.IsSupported && vectors.Length >= 2) { - Expand(ref vectorsStart); + CompandAvx2(vectors, ExpandTable); - vectorsStart = ref Unsafe.Add(ref vectorsStart, 1); + if (Numerics.Modulo2(vectors.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + Expand(ref MemoryMarshal.GetReference(vectors.Slice(vectors.Length - 1))); + } + } + else +#endif + { + CompandScalar(vectors, ExpandTable); } } @@ -40,17 +106,24 @@ public static void Expand(Span vectors) /// Compresses the uncompanded vectors to their nonlinear equivalents with respect to the energy. /// /// The span of vectors. - [MethodImpl(InliningOptions.ShortMethod)] - public static void Compress(Span vectors) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void Compress(Span vectors) { - ref Vector4 vectorsStart = ref MemoryMarshal.GetReference(vectors); - ref Vector4 vectorsEnd = ref Unsafe.Add(ref vectorsStart, vectors.Length); - - while (Unsafe.IsAddressLessThan(ref vectorsStart, ref vectorsEnd)) +#if SUPPORTS_RUNTIME_INTRINSICS + if (Avx2.IsSupported && vectors.Length >= 2) { - Compress(ref vectorsStart); + CompandAvx2(vectors, CompressTable); - vectorsStart = ref Unsafe.Add(ref vectorsStart, 1); + if (Numerics.Modulo2(vectors.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + Compress(ref MemoryMarshal.GetReference(vectors.Slice(vectors.Length - 1))); + } + } + else +#endif + { + CompandScalar(vectors, CompressTable); } } @@ -58,9 +131,10 @@ public static void Compress(Span vectors) /// Expands a companded vector to its linear equivalent with respect to the energy. /// /// The vector. - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Expand(ref Vector4 vector) { + // Alpha is already a linear representation of opacity so we do not want to convert it. vector.X = Expand(vector.X); vector.Y = Expand(vector.Y); vector.Z = Expand(vector.Z); @@ -70,9 +144,10 @@ public static void Expand(ref Vector4 vector) /// Compresses an uncompanded vector (linear) to its nonlinear equivalent. /// /// The vector. - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Compress(ref Vector4 vector) { + // Alpha is already a linear representation of opacity so we do not want to convert it. vector.X = Compress(vector.X); vector.Y = Compress(vector.Y); vector.Z = Compress(vector.Z); @@ -83,15 +158,84 @@ public static void Compress(ref Vector4 vector) /// /// The channel value. /// The representing the linear channel value. - [MethodImpl(InliningOptions.ShortMethod)] - public static float Expand(float channel) => channel <= 0.04045F ? channel / 12.92F : MathF.Pow((channel + 0.055F) / 1.055F, 2.4F); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Expand(float channel) + => channel <= 0.04045F ? channel / 12.92F : MathF.Pow((channel + 0.055F) / 1.055F, 2.4F); /// /// Compresses an uncompanded channel (linear) to its nonlinear equivalent. /// /// The channel value. /// The representing the nonlinear channel value. - [MethodImpl(InliningOptions.ShortMethod)] - public static float Compress(float channel) => channel <= 0.0031308F ? 12.92F * channel : (1.055F * MathF.Pow(channel, 0.416666666666667F)) - 0.055F; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Compress(float channel) + => channel <= 0.0031308F ? 12.92F * channel : (1.055F * MathF.Pow(channel, 0.416666666666667F)) - 0.055F; + +#if SUPPORTS_RUNTIME_INTRINSICS + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe void CompandAvx2(Span vectors, float[] table) + { + fixed (float* tablePointer = &table[0]) + { + var scale = Vector256.Create((float)Scale); + Vector256 zero = Vector256.Zero; + var offset = Vector256.Create(1); + + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 vectorsBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vectors)); + ref Vector256 vectorsLast = ref Unsafe.Add(ref vectorsBase, (IntPtr)((uint)vectors.Length / 2u)); + + while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast)) + { + Vector256 multiplied = Avx.Multiply(scale, vectorsBase); + multiplied = Avx.Min(Avx.Max(zero, multiplied), scale); + + Vector256 truncated = Avx.ConvertToVector256Int32WithTruncation(multiplied); + Vector256 truncatedF = Avx.ConvertToVector256Single(truncated); + + Vector256 low = Avx2.GatherVector256(tablePointer, truncated, sizeof(float)); + Vector256 high = Avx2.GatherVector256(tablePointer, Avx2.Add(truncated, offset), sizeof(float)); + + // Alpha is already a linear representation of opacity so we do not want to convert it. + Vector256 companded = Numerics.Lerp(low, high, Avx.Subtract(multiplied, truncatedF)); + vectorsBase = Avx.Blend(companded, vectorsBase, Numerics.BlendAlphaControl); + vectorsBase = ref Unsafe.Add(ref vectorsBase, 1); + } + } + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe void CompandScalar(Span vectors, float[] table) + { + fixed (float* tablePointer = &table[0]) + { + Vector4 zero = Vector4.Zero; + var scale = new Vector4(Scale); + ref Vector4 vectorsBase = ref MemoryMarshal.GetReference(vectors); + ref Vector4 vectorsLast = ref Unsafe.Add(ref vectorsBase, vectors.Length); + + while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast)) + { + Vector4 multiplied = Numerics.Clamp(vectorsBase * Scale, zero, scale); + + float f0 = multiplied.X; + float f1 = multiplied.Y; + float f2 = multiplied.Z; + + uint i0 = (uint)f0; + uint i1 = (uint)f1; + uint i2 = (uint)f2; + + // Alpha is already a linear representation of opacity so we do not want to convert it. + vectorsBase.X = Numerics.Lerp(tablePointer[i0], tablePointer[i0 + 1], f0 - (int)i0); + vectorsBase.Y = Numerics.Lerp(tablePointer[i1], tablePointer[i1 + 1], f1 - (int)i1); + vectorsBase.Z = Numerics.Lerp(tablePointer[i2], tablePointer[i2 + 1], f2 - (int)i2); + + vectorsBase = ref Unsafe.Add(ref vectorsBase, 1); + } + } + } } } diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs index b9ccfafe0e..6105422372 100644 --- a/src/ImageSharp/Common/Helpers/Numerics.cs +++ b/src/ImageSharp/Common/Helpers/Numerics.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp internal static class Numerics { #if SUPPORTS_RUNTIME_INTRINSICS - private const int BlendAlphaControl = 0b_10_00_10_00; + public const int BlendAlphaControl = 0b_10_00_10_00; private const int ShuffleAlphaControl = 0b_11_11_11_11; #endif @@ -710,5 +710,43 @@ public static unsafe void CubeRootOnXYZ(Span vectors) } } } + +#if SUPPORTS_RUNTIME_INTRINSICS + + /// + /// Performs a linear interpolation between two values based on the given weighting. + /// + /// The first value. + /// The second value. + /// Values between 0 and 1 that indicates the weight of . + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Lerp( + in Vector256 value1, + in Vector256 value2, + in Vector256 amount) + { + Vector256 diff = Avx.Subtract(value2, value1); + if (Fma.IsSupported) + { + return Fma.MultiplyAdd(diff, amount, value1); + } + else + { + return Avx.Add(Avx.Multiply(diff, amount), value1); + } + } +#endif + + /// + /// Performs a linear interpolation between two values based on the given weighting. + /// + /// The first value. + /// The second value. + /// A value between 0 and 1 that indicates the weight of . + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Lerp(float value1, float value2, float amount) + => ((value2 - value1) * amount) + value1; } } diff --git a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.cs b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.cs index acc2725cec..7af2662765 100644 --- a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.cs +++ b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; @@ -45,4 +45,4 @@ internal static void ApplyBackwardConversionModifiers(Span vectors, Pix } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs index 35d1931d0e..d94aeffe69 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs @@ -61,9 +61,7 @@ public Span Values /// The weighted sum [MethodImpl(InliningOptions.ShortMethod)] public Vector4 Convolve(Span rowSpan) - { - return this.ConvolveCore(ref rowSpan[this.StartIndex]); - } + => this.ConvolveCore(ref rowSpan[this.StartIndex]); [MethodImpl(InliningOptions.ShortMethod)] public Vector4 ConvolveCore(ref Vector4 rowStartRef) @@ -91,9 +89,7 @@ public Vector4 ConvolveCore(ref Vector4 rowStartRef) /// [MethodImpl(InliningOptions.ShortMethod)] internal ResizeKernel AlterLeftValue(int left) - { - return new ResizeKernel(left, this.bufferPtr, this.Length); - } + => new ResizeKernel(left, this.bufferPtr, this.Length); internal void Fill(Span values) { diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index 764349884d..e7207c7e63 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -105,14 +105,10 @@ public void Dispose() [MethodImpl(InliningOptions.ShortMethod)] public Span GetColumnSpan(int x, int startY) - { - return this.transposedFirstPassBuffer.GetRowSpan(x).Slice(startY - this.currentWindow.Min); - } + => this.transposedFirstPassBuffer.GetRowSpan(x).Slice(startY - this.currentWindow.Min); public void Initialize() - { - this.CalculateFirstPassValues(this.currentWindow); - } + => this.CalculateFirstPassValues(this.currentWindow); public void FillDestinationPixels(RowInterval rowInterval, Buffer2D destination) { diff --git a/tests/ImageSharp.Benchmarks/Processing/Resize.cs b/tests/ImageSharp.Benchmarks/Processing/Resize.cs index 3823f7ec68..571c92cc87 100644 --- a/tests/ImageSharp.Benchmarks/Processing/Resize.cs +++ b/tests/ImageSharp.Benchmarks/Processing/Resize.cs @@ -4,10 +4,13 @@ using System.Drawing; using System.Drawing.Drawing2D; using System.Globalization; +using System.IO; using BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Tests; +using SDImage = System.Drawing.Image; namespace SixLabors.ImageSharp.Benchmarks { @@ -15,32 +18,35 @@ namespace SixLabors.ImageSharp.Benchmarks public abstract class Resize where TPixel : unmanaged, IPixel { + private byte[] bytes = null; + private Image sourceImage; - private Bitmap sourceBitmap; + private SDImage sourceBitmap; protected Configuration Configuration { get; } = new Configuration(new JpegConfigurationModule()); - [Params("3032-400")] - public virtual string SourceToDest { get; set; } - - protected int SourceSize { get; private set; } - protected int DestSize { get; private set; } [GlobalSetup] public virtual void Setup() { - string[] stuff = this.SourceToDest.Split('-'); - this.SourceSize = int.Parse(stuff[0], CultureInfo.InvariantCulture); - this.DestSize = int.Parse(stuff[1], CultureInfo.InvariantCulture); - this.sourceImage = new Image(this.Configuration, this.SourceSize, this.SourceSize); - this.sourceBitmap = new Bitmap(this.SourceSize, this.SourceSize); + if (this.bytes is null) + { + this.bytes = File.ReadAllBytes(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Snake)); + + this.sourceImage = Image.Load(this.bytes); + + var ms1 = new MemoryStream(this.bytes); + this.sourceBitmap = SDImage.FromStream(ms1); + this.DestSize = this.sourceBitmap.Width / 2; + } } [GlobalCleanup] public void Cleanup() { + this.bytes = null; this.sourceImage.Dispose(); this.sourceBitmap.Dispose(); } @@ -127,9 +133,6 @@ public class Resize_Bicubic_Rgba32_CompareWorkBufferSizes : Resize_Bicubic_Rgba3 [Params(128, 512, 1024, 8 * 1024)] public int WorkingBufferSizeHintInKilobytes { get; set; } - [Params("3032-400", "4000-300")] - public override string SourceToDest { get; set; } - public override void Setup() { this.Configuration.WorkingBufferSizeHintInBytes = this.WorkingBufferSizeHintInKilobytes * 1024; diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs index 39786a2177..cc7f32bef7 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs @@ -160,7 +160,7 @@ public void FromScaledVector4(int count) (s, d) => { Span destPixels = d.GetSpan(); - this.Operations.FromVector4Destructive(this.Configuration, (Span)s, destPixels, PixelConversionModifiers.Scale); + this.Operations.FromVector4Destructive(this.Configuration, s, destPixels, PixelConversionModifiers.Scale); }); } @@ -168,15 +168,9 @@ public void FromScaledVector4(int count) [MemberData(nameof(ArraySizesData))] public void FromCompandedScaledVector4(int count) { - void SourceAction(ref Vector4 v) - { - SRgbCompanding.Expand(ref v); - } + void SourceAction(ref Vector4 v) => SRgbCompanding.Expand(ref v); - void ExpectedAction(ref Vector4 v) - { - SRgbCompanding.Compress(ref v); - } + void ExpectedAction(ref Vector4 v) => SRgbCompanding.Compress(ref v); Vector4[] source = CreateVector4TestData(count, (ref Vector4 v) => SourceAction(ref v)); TPixel[] expected = CreateScaledExpectedPixelData(source, (ref Vector4 v) => ExpectedAction(ref v)); @@ -188,7 +182,8 @@ void ExpectedAction(ref Vector4 v) this.Configuration, s, d.GetSpan(), - PixelConversionModifiers.SRgbCompand | PixelConversionModifiers.Scale)); + PixelConversionModifiers.SRgbCompand | PixelConversionModifiers.Scale), + false); } [Theory] @@ -219,7 +214,8 @@ void ExpectedAction(ref Vector4 v) expected, (s, d) => { - PixelConversionModifiers modifiers = this.HasUnassociatedAlpha ? PixelConversionModifiers.Premultiply + PixelConversionModifiers modifiers = this.HasUnassociatedAlpha + ? PixelConversionModifiers.Premultiply : PixelConversionModifiers.None; this.Operations.FromVector4Destructive(this.Configuration, s, d.GetSpan(), modifiers); @@ -254,7 +250,8 @@ void ExpectedAction(ref Vector4 v) expected, (s, d) => { - PixelConversionModifiers modifiers = this.HasUnassociatedAlpha ? PixelConversionModifiers.Premultiply + PixelConversionModifiers modifiers = this.HasUnassociatedAlpha + ? PixelConversionModifiers.Premultiply : PixelConversionModifiers.None; this.Operations.FromVector4Destructive( @@ -297,7 +294,8 @@ void ExpectedAction(ref Vector4 v) expected, (s, d) => { - PixelConversionModifiers modifiers = this.HasUnassociatedAlpha ? PixelConversionModifiers.Premultiply + PixelConversionModifiers modifiers = this.HasUnassociatedAlpha + ? PixelConversionModifiers.Premultiply : PixelConversionModifiers.None; this.Operations.FromVector4Destructive( @@ -305,7 +303,8 @@ void ExpectedAction(ref Vector4 v) s, d.GetSpan(), modifiers | PixelConversionModifiers.SRgbCompand | PixelConversionModifiers.Scale); - }); + }, + false); } [Theory] @@ -343,7 +342,7 @@ public void Generic_To(TestPixel dummy) PixelConverterTests.ReferenceImplementations.To(this.Configuration, source, expected); - TestOperation(source, expected, (s, d) => this.Operations.To(this.Configuration, (ReadOnlySpan)s, d.GetSpan())); + TestOperation(source, expected, (s, d) => this.Operations.To(this.Configuration, s, d.GetSpan())); } [Theory] @@ -356,11 +355,11 @@ public void ToScaledVector4(int count) TestOperation( source, expected, - (s, d) => - { - Span destVectors = d.GetSpan(); - this.Operations.ToVector4(this.Configuration, (ReadOnlySpan)s, destVectors, PixelConversionModifiers.Scale); - }); + (s, d) => this.Operations.ToVector4( + this.Configuration, + s, + d.GetSpan(), + PixelConversionModifiers.Scale)); } [Theory] @@ -369,13 +368,9 @@ public void ToCompandedScaledVector4(int count) { void SourceAction(ref Vector4 v) { - SRgbCompanding.Compress(ref v); } - void ExpectedAction(ref Vector4 v) - { - SRgbCompanding.Expand(ref v); - } + void ExpectedAction(ref Vector4 v) => SRgbCompanding.Expand(ref v); TPixel[] source = CreateScaledPixelTestData(count, (ref Vector4 v) => SourceAction(ref v)); Vector4[] expected = CreateExpectedScaledVector4Data(source, (ref Vector4 v) => ExpectedAction(ref v)); @@ -396,13 +391,9 @@ public void ToPremultipliedVector4(int count) { void SourceAction(ref Vector4 v) { - Numerics.UnPremultiply(ref v); } - void ExpectedAction(ref Vector4 v) - { - Numerics.Premultiply(ref v); - } + void ExpectedAction(ref Vector4 v) => Numerics.Premultiply(ref v); TPixel[] source = CreatePixelTestData(count, (ref Vector4 v) => SourceAction(ref v)); Vector4[] expected = CreateExpectedVector4Data(source, (ref Vector4 v) => ExpectedAction(ref v)); @@ -419,13 +410,9 @@ public void ToPremultipliedScaledVector4(int count) { void SourceAction(ref Vector4 v) { - Numerics.UnPremultiply(ref v); } - void ExpectedAction(ref Vector4 v) - { - Numerics.Premultiply(ref v); - } + void ExpectedAction(ref Vector4 v) => Numerics.Premultiply(ref v); TPixel[] source = CreateScaledPixelTestData(count, (ref Vector4 v) => SourceAction(ref v)); Vector4[] expected = CreateExpectedScaledVector4Data(source, (ref Vector4 v) => ExpectedAction(ref v)); @@ -446,8 +433,6 @@ public void ToCompandedPremultipliedScaledVector4(int count) { void SourceAction(ref Vector4 v) { - Numerics.UnPremultiply(ref v); - SRgbCompanding.Compress(ref v); } void ExpectedAction(ref Vector4 v) @@ -1006,15 +991,9 @@ public void ToRgba64Bytes(int count) [Theory] [MemberData(nameof(ArraySizesData))] public void PackFromRgbPlanes(int count) - { - SimdUtilsTests.TestPackFromRgbPlanes( + => SimdUtilsTests.TestPackFromRgbPlanes( count, - ( - r, - g, - b, - actual) => PixelOperations.Instance.PackFromRgbPlanes(this.Configuration, r, g, b, actual)); - } + (r, g, b, actual) => PixelOperations.Instance.PackFromRgbPlanes(this.Configuration, r, g, b, actual)); public delegate void RefAction(ref T1 arg1); @@ -1053,11 +1032,12 @@ internal static Vector4[] CreateExpectedScaledVector4Data(TPixel[] source, RefAc internal static void TestOperation( TSource[] source, TDest[] expected, - Action> action) + Action> action, + bool preferExactComparison = true) where TSource : struct where TDest : struct { - using (var buffers = new TestBuffers(source, expected)) + using (var buffers = new TestBuffers(source, expected, preferExactComparison)) { action(buffers.SourceBuffer, buffers.ActualDestBuffer); buffers.Verify(); @@ -1071,7 +1051,7 @@ internal static Vector4[] CreateVector4TestData(int length, RefAction v for (int i = 0; i < result.Length; i++) { - Vector4 v = GetVector(rnd); + Vector4 v = GetScaledVector(rnd); vectorModifier?.Invoke(ref v); result[i] = v; @@ -1088,7 +1068,7 @@ internal static TPixel[] CreatePixelTestData(int length, RefAction vect for (int i = 0; i < result.Length; i++) { - Vector4 v = GetVector(rnd); + Vector4 v = GetScaledVector(rnd); vectorModifier?.Invoke(ref v); @@ -1106,7 +1086,7 @@ internal static TPixel[] CreateScaledPixelTestData(int length, RefAction new Vector4((float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble()); [StructLayout(LayoutKind.Sequential)] internal unsafe struct OctetBytes @@ -1160,11 +1138,14 @@ private class TestBuffers : IDisposable public TDest[] ExpectedDestBuffer { get; } - public TestBuffers(TSource[] source, TDest[] expectedDest) + public bool PreferExactComparison { get; } + + public TestBuffers(TSource[] source, TDest[] expectedDest, bool preferExactComparison = true) { this.SourceBuffer = source; this.ExpectedDestBuffer = expectedDest; this.ActualDestBuffer = Configuration.Default.MemoryAllocator.Allocate(expectedDest.Length); + this.PreferExactComparison = preferExactComparison; } public void Dispose() => this.ActualDestBuffer.Dispose(); @@ -1177,26 +1158,54 @@ public void Verify() { Span expected = MemoryMarshal.Cast(this.ExpectedDestBuffer.AsSpan()); Span actual = MemoryMarshal.Cast(this.ActualDestBuffer.GetSpan()); + var comparer = new ApproximateFloatComparer(TestEnvironment.Is64BitProcess ? 0.0001F : 0.001F); - var comparer = new ApproximateFloatComparer(0.001f); for (int i = 0; i < count; i++) { - // ReSharper disable PossibleNullReferenceException Assert.Equal(expected[i], actual[i], comparer); + } + } + else if (!this.PreferExactComparison && typeof(IPixel).IsAssignableFrom(typeof(TDest)) && IsComplexPixel()) + { + Span expected = this.ExpectedDestBuffer.AsSpan(); + Span actual = this.ActualDestBuffer.GetSpan(); + var comparer = new ApproximateFloatComparer(TestEnvironment.Is64BitProcess ? 0.0001F : 0.001F); - // ReSharper restore PossibleNullReferenceException + for (int i = 0; i < count; i++) + { + Assert.Equal((IPixel)expected[i], (IPixel)actual[i], comparer); } } else { Span expected = this.ExpectedDestBuffer.AsSpan(); Span actual = this.ActualDestBuffer.GetSpan(); + for (int i = 0; i < count; i++) { Assert.Equal(expected[i], actual[i]); } } } + + // TODO: We really need a PixelTypeInfo.BitsPerComponent property!! + private static bool IsComplexPixel() + { + switch (default(TDest)) + { + case HalfSingle _: + case HalfVector2 _: + case L16 _: + case La32 _: + case NormalizedShort2 _: + case Rg32 _: + case Short2 _: + return true; + + default: + return Unsafe.SizeOf() > sizeof(int); + } + } } } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs index b2f390dcdf..2a0ed19b48 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests { @@ -12,6 +13,7 @@ namespace SixLabors.ImageSharp.Tests internal readonly struct ApproximateFloatComparer : IEqualityComparer, IEqualityComparer, + IEqualityComparer, IEqualityComparer, IEqualityComparer { @@ -32,30 +34,42 @@ public bool Equals(float x, float y) } /// - public int GetHashCode(float obj) => obj.GetHashCode(); + public int GetHashCode(float obj) + => obj.GetHashCode(); /// - public bool Equals(Vector2 x, Vector2 y) => this.Equals(x.X, y.X) && this.Equals(x.Y, y.Y); + public bool Equals(Vector2 x, Vector2 y) + => this.Equals(x.X, y.X) && this.Equals(x.Y, y.Y); /// - public int GetHashCode(Vector2 obj) => obj.GetHashCode(); + public int GetHashCode(Vector2 obj) + => obj.GetHashCode(); /// - public bool Equals(Vector4 x, Vector4 y) => this.Equals(x.X, y.X) && this.Equals(x.Y, y.Y) && this.Equals(x.Z, y.Z) && this.Equals(x.W, y.W); + public bool Equals(IPixel x, IPixel y) + => this.Equals(x.ToScaledVector4(), y.ToScaledVector4()); + + public int GetHashCode(IPixel obj) + => obj.ToScaledVector4().GetHashCode(); + + /// + public bool Equals(Vector4 x, Vector4 y) + => this.Equals(x.X, y.X) + && this.Equals(x.Y, y.Y) + && this.Equals(x.Z, y.Z) + && this.Equals(x.W, y.W); /// - public int GetHashCode(Vector4 obj) => obj.GetHashCode(); + public int GetHashCode(Vector4 obj) + => obj.GetHashCode(); /// public bool Equals(ColorMatrix x, ColorMatrix y) - { - return - this.Equals(x.M11, y.M11) && this.Equals(x.M12, y.M12) && this.Equals(x.M13, y.M13) && this.Equals(x.M14, y.M14) + => this.Equals(x.M11, y.M11) && this.Equals(x.M12, y.M12) && this.Equals(x.M13, y.M13) && this.Equals(x.M14, y.M14) && this.Equals(x.M21, y.M21) && this.Equals(x.M22, y.M22) && this.Equals(x.M23, y.M23) && this.Equals(x.M24, y.M24) && this.Equals(x.M31, y.M31) && this.Equals(x.M32, y.M32) && this.Equals(x.M33, y.M33) && this.Equals(x.M34, y.M34) && this.Equals(x.M41, y.M41) && this.Equals(x.M42, y.M42) && this.Equals(x.M43, y.M43) && this.Equals(x.M44, y.M44) && this.Equals(x.M51, y.M51) && this.Equals(x.M52, y.M52) && this.Equals(x.M53, y.M53) && this.Equals(x.M54, y.M54); - } /// public int GetHashCode(ColorMatrix obj) => obj.GetHashCode();