diff --git a/src/ImageSharp/Common/Extensions/SimdUtils.cs b/src/ImageSharp/Common/Extensions/SimdUtils.cs
index be9406367..c9acbc9fc 100644
--- a/src/ImageSharp/Common/Extensions/SimdUtils.cs
+++ b/src/ImageSharp/Common/Extensions/SimdUtils.cs
@@ -7,11 +7,27 @@ using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp
{
+ using System.Diagnostics;
+
///
/// Various extension and utility methods for and utilizing SIMD capabilities
///
internal static class SimdUtils
{
+ ///
+ /// Indicates AVX2 architecture where both float and integer registers are of size 256 byte.
+ ///
+ public static readonly bool IsAvx2 = Vector.Count == 8 && Vector.Count == 8;
+
+ [Conditional("DEBUG")]
+ internal static void GuardAvx2(string operation)
+ {
+ if (!IsAvx2)
+ {
+ throw new NotSupportedException($"{operation} is supported only on AVX2 CPU!");
+ }
+ }
+
///
/// Transform all scalars in 'v' in a way that converting them to would have rounding semantics.
///
@@ -41,5 +57,117 @@ namespace SixLabors.ImageSharp
Vector sub0 = Vector.Subtract(add0, or0);
return sub0;
}
+
+ ///
+ /// Convert 'source.Length' values normalized into [0..1] from 'source' into 'dest' buffer of values.
+ /// The values gonna be scaled up into [0-255] and rounded.
+ /// Based on:
+ ///
+ /// http://lolengine.net/blog/2011/3/20/understanding-fast-float-integer-conversions
+ ///
+ ///
+ internal static void BulkConvertNormalizedFloatToByte(ReadOnlySpan source, Span dest)
+ {
+ GuardAvx2(nameof(BulkConvertNormalizedFloatToByte));
+
+ DebugGuard.IsTrue((source.Length % Vector.Count) == 0, nameof(source), "source.Length should be divisable by Vector.Count!");
+
+ if (source.Length == 0)
+ {
+ return;
+ }
+
+ ref Vector srcBase = ref Unsafe.As>(ref source.DangerousGetPinnableReference());
+ ref Octet.OfByte destBase = ref Unsafe.As(ref dest.DangerousGetPinnableReference());
+
+ Vector magick = new Vector(32768.0f);
+ Vector scale = new Vector(255f) / new Vector(256f);
+
+ int n = source.Length;
+
+ for (int i = 0; i < n; i++)
+ {
+ // union { float f; uint32_t i; } u;
+ // u.f = 32768.0f + x * (255.0f / 256.0f);
+ // return (uint8_t)u.i;
+ Vector x = Unsafe.Add(ref srcBase, i);
+ x = (x * scale) + magick;
+
+ Vector u = Vector.AsVectorUInt32(x);
+
+ Octet.OfUInt32 ii = Unsafe.As, Octet.OfUInt32>(ref u);
+
+ ref Octet.OfByte d = ref Unsafe.Add(ref destBase, i);
+ d.LoadFrom(ref ii);
+ }
+ }
+
+ ///
+ /// Same as but clamps overflown values before conversion.
+ ///
+ internal static void BulkConvertNormalizedFloatToByteClampOverflows(ReadOnlySpan source, Span dest)
+ {
+ GuardAvx2(nameof(BulkConvertNormalizedFloatToByte));
+
+ DebugGuard.IsTrue((source.Length % Vector.Count) == 0, nameof(source), "source.Length should be divisable by Vector.Count!");
+
+ if (source.Length == 0)
+ {
+ return;
+ }
+
+ ref Vector srcBase = ref Unsafe.As>(ref source.DangerousGetPinnableReference());
+ ref Octet.OfByte destBase = ref Unsafe.As(ref dest.DangerousGetPinnableReference());
+
+ Vector magick = new Vector(32768.0f);
+ Vector scale = new Vector(255f) / new Vector(256f);
+
+ int n = source.Length;
+
+ for (int i = 0; i < n; i++)
+ {
+ // union { float f; uint32_t i; } u;
+ // u.f = 32768.0f + x * (255.0f / 256.0f);
+ // return (uint8_t)u.i;
+ Vector x = Unsafe.Add(ref srcBase, i);
+ x = Vector.Max(x, Vector.Zero);
+ x = Vector.Min(x, Vector.One);
+
+ x = (x * scale) + magick;
+
+ Vector u = Vector.AsVectorUInt32(x);
+
+ Octet.OfUInt32 ii = Unsafe.As, Octet.OfUInt32>(ref u);
+
+ ref Octet.OfByte d = ref Unsafe.Add(ref destBase, i);
+ d.LoadFrom(ref ii);
+ }
+ }
+
+#pragma warning disable SA1132 // Do not combine fields
+ private static class Octet
+ {
+ public struct OfUInt32
+ {
+ public uint V0, V1, V2, V3, V4, V5, V6, V7;
+ }
+
+ public struct OfByte
+ {
+ public byte V0, V1, V2, V3, V4, V5, V6, V7;
+
+ public void LoadFrom(ref OfUInt32 i)
+ {
+ this.V0 = (byte)i.V0;
+ this.V1 = (byte)i.V1;
+ this.V2 = (byte)i.V2;
+ this.V3 = (byte)i.V3;
+ this.V4 = (byte)i.V4;
+ this.V5 = (byte)i.V5;
+ this.V6 = (byte)i.V6;
+ this.V7 = (byte)i.V7;
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/ImageSharp/Memory/BufferArea{T}.cs b/src/ImageSharp/Memory/BufferArea{T}.cs
index 8ead22680..b5ed3566f 100644
--- a/src/ImageSharp/Memory/BufferArea{T}.cs
+++ b/src/ImageSharp/Memory/BufferArea{T}.cs
@@ -17,17 +17,19 @@ namespace SixLabors.ImageSharp.Memory
///
public readonly Rectangle Rectangle;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public BufferArea(IBuffer2D destinationBuffer, Rectangle rectangle)
{
- Guard.MustBeGreaterThanOrEqualTo(rectangle.X, 0, nameof(rectangle));
- Guard.MustBeGreaterThanOrEqualTo(rectangle.Y, 0, nameof(rectangle));
- Guard.MustBeLessThanOrEqualTo(rectangle.Width, destinationBuffer.Width, nameof(rectangle));
- Guard.MustBeLessThanOrEqualTo(rectangle.Height, destinationBuffer.Height, nameof(rectangle));
+ DebugGuard.MustBeGreaterThanOrEqualTo(rectangle.X, 0, nameof(rectangle));
+ DebugGuard.MustBeGreaterThanOrEqualTo(rectangle.Y, 0, nameof(rectangle));
+ DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, destinationBuffer.Width, nameof(rectangle));
+ DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, destinationBuffer.Height, nameof(rectangle));
this.DestinationBuffer = destinationBuffer;
this.Rectangle = rectangle;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public BufferArea(IBuffer2D destinationBuffer)
: this(destinationBuffer, destinationBuffer.FullRectangle())
{
diff --git a/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs b/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs
index 6f7dc09d1..cb2591999 100644
--- a/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs
+++ b/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs
@@ -5,7 +5,11 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Common
{
+ using System.Linq;
+ using System.Runtime.CompilerServices;
+
using Xunit.Abstractions;
+ using Xunit.Sdk;
public class SimdUtilsTests
{
@@ -64,14 +68,13 @@ namespace SixLabors.ImageSharp.Tests.Common
return new Vector(data);
}
- private static Vector CreateRandomTestVector(int seed, float scale)
+ private static Vector CreateRandomTestVector(int seed, float min, float max)
{
float[] data = new float[Vector.Count];
Random rnd = new Random();
for (int i = 0; i < Vector.Count; i++)
{
- float v = (float)rnd.NextDouble() - 0.5f;
- v *= 2 * scale;
+ float v = (float)rnd.NextDouble() * (max-min) + min;
data[i] = v;
}
return new Vector(data);
@@ -97,7 +100,7 @@ namespace SixLabors.ImageSharp.Tests.Common
[InlineData(42, 1000f)]
public void FastRound_RandomValues(int seed, float scale)
{
- Vector v = CreateRandomTestVector(seed, scale);
+ Vector v = CreateRandomTestVector(seed, -scale*0.5f, scale*0.5f);
Vector r = v.FastRound();
this.Output.WriteLine(v.ToString());
@@ -106,6 +109,86 @@ namespace SixLabors.ImageSharp.Tests.Common
AssertEvenRoundIsCorrect(r, v);
}
+ [Theory]
+ [InlineData(1, 0)]
+ [InlineData(1, 8)]
+ [InlineData(2, 16)]
+ [InlineData(3, 128)]
+ public void BulkConvertNormalizedFloatToByte_WithRoundedData(int seed, int count)
+ {
+ float[] orig = new Random(seed).GenerateRandomRoundedFloatArray(count, 0, 256);
+ float[] normalized = orig.Select(f => f / 255f).ToArray();
+
+ byte[] dest = new byte[count];
+
+ SimdUtils.BulkConvertNormalizedFloatToByte(normalized, dest);
+
+ byte[] expected = orig.Select(f => (byte)(f)).ToArray();
+
+ Assert.Equal(expected, dest);
+ }
+
+ [Theory]
+ [InlineData(1, 0)]
+ [InlineData(1, 8)]
+ [InlineData(2, 16)]
+ [InlineData(3, 128)]
+ public void BulkConvertNormalizedFloatToByte_WithNonRoundedData(int seed, int count)
+ {
+ float[] source = new Random(seed).GenerateRandomFloatArray(count, 0, 1f);
+
+ byte[] dest = new byte[count];
+
+ SimdUtils.BulkConvertNormalizedFloatToByte(source, dest);
+
+ byte[] expected = source.Select(f => (byte)Math.Round(f*255f)).ToArray();
+
+ Assert.Equal(expected, dest);
+ }
+
+ private static float Clamp255(float x) => MathF.Min(255f, MathF.Max(0f, x));
+
+ [Theory]
+ [InlineData(1, 0)]
+ [InlineData(1, 8)]
+ [InlineData(2, 16)]
+ [InlineData(3, 128)]
+ public void BulkConvertNormalizedFloatToByteClampOverflows(int seed, int count)
+ {
+ float[] orig = new Random(seed).GenerateRandomRoundedFloatArray(count, -50, 444);
+ float[] normalized = orig.Select(f => f / 255f).ToArray();
+
+ byte[] dest = new byte[count];
+
+ SimdUtils.BulkConvertNormalizedFloatToByteClampOverflows(normalized, dest);
+
+ byte[] expected = orig.Select(f => (byte)Clamp255(f)).ToArray();
+
+ Assert.Equal(expected, dest);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(7)]
+ [InlineData(42)]
+ [InlineData(255)]
+ [InlineData(256)]
+ [InlineData(257)]
+ private void MagicConvertToByte(float value)
+ {
+ byte actual = MagicConvert(value / 256f);
+ byte expected = (byte)value;
+
+ Assert.Equal(expected, actual);
+ }
+
+ private static byte MagicConvert(float x)
+ {
+ float f = 32768.0f + x;
+ uint i = Unsafe.As(ref f);
+ return (byte)i;
+ }
+
private static void AssertEvenRoundIsCorrect(Vector r, Vector v)
{
for (int i = 0; i < Vector.Count; i++)
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs
index 50746f683..706fa1e20 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs
@@ -228,6 +228,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
int componentCount,
int inputBufferLength,
int seed,
+ float minVal = 0f,
float maxVal = 255f)
{
var rnd = new Random(seed);
@@ -238,7 +239,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
for (int j = 0; j < inputBufferLength; j++)
{
- values[j] = (float)rnd.NextDouble() * maxVal;
+ values[j] = (float)rnd.NextDouble() * (maxVal-minVal)+minVal;
}
// no need to dispose when buffer is not array owner
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs
index 6ce16de2f..063f42c00 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
+// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
using System;
@@ -8,7 +9,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
using System.Linq;
using System.Numerics;
+ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Jpeg;
+ using SixLabors.ImageSharp.Formats.Jpeg.GolangPort;
+ using SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort;
using Xunit;
using Xunit.Abstractions;
@@ -30,9 +34,21 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
TestImages.Jpeg.Baseline.Jpeg444,
};
- //[Theory] // Benchmark, enable manually
- //[MemberData(nameof(DecodeJpegData))]
- public void DecodeJpeg(string fileName)
+ [Theory] // Benchmark, enable manually
+ [MemberData(nameof(DecodeJpegData))]
+ public void DecodeJpeg_Original(string fileName)
+ {
+ this.DecodeJpegBenchmarkImpl(fileName, new OrigJpegDecoder());
+ }
+
+ [Theory] // Benchmark, enable manually
+ [MemberData(nameof(DecodeJpegData))]
+ public void DecodeJpeg_PdfJs(string fileName)
+ {
+ this.DecodeJpegBenchmarkImpl(fileName, new PdfJsJpegDecoder());
+ }
+
+ private void DecodeJpegBenchmarkImpl(string fileName, IImageDecoder decoder)
{
const int ExecutionCount = 30;
@@ -48,11 +64,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
ExecutionCount,
() =>
{
- Image img = Image.Load(bytes);
+ Image img = Image.Load(bytes, decoder);
},
// ReSharper disable once ExplicitCallerInfoArgument
$"Decode {fileName}");
-
}
// Benchmark, enable manually!
diff --git a/tests/ImageSharp.Tests/TestUtilities/TestDataGenerator.cs b/tests/ImageSharp.Tests/TestUtilities/TestDataGenerator.cs
new file mode 100644
index 000000000..9eb051e7a
--- /dev/null
+++ b/tests/ImageSharp.Tests/TestUtilities/TestDataGenerator.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace SixLabors.ImageSharp.Tests
+{
+ internal static class TestDataGenerator
+ {
+ public static float[] GenerateRandomFloatArray(this Random rnd, int length, float minVal, float maxVal)
+ {
+ float[] values = new float[length];
+
+ for (int i = 0; i < length; i++)
+ {
+ values[i] = (float)rnd.NextDouble() * (maxVal - minVal) + minVal;
+ }
+
+ return values;
+ }
+
+ public static float[] GenerateRandomRoundedFloatArray(this Random rnd, int length, int minVal, int maxValExclusive)
+ {
+ float[] values = new float[length];
+
+ for (int i = 0; i < length; i++)
+ {
+ int val = rnd.Next(minVal, maxValExclusive);
+ values[i] = (float)val;
+ }
+
+ return values;
+ }
+ }
+}
\ No newline at end of file