From aa813ecc3e7c8ece6e384c7a498b403798231100 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Tue, 12 Sep 2017 01:39:05 +0200 Subject: [PATCH 1/8] better JpegColorConverterTests --- .../Formats/Jpg/JpegColorConverterTests.cs | 321 ++++++++++-------- 1 file changed, 177 insertions(+), 144 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs index 590cd322e..d24890e8f 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs @@ -1,26 +1,27 @@ -using System; -using System.Numerics; +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + using System; + using System.Numerics; -using SixLabors.ImageSharp.ColorSpaces; -using SixLabors.ImageSharp.ColorSpaces.Conversion; -using SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder; -using SixLabors.ImageSharp.Memory; + using SixLabors.ImageSharp.ColorSpaces; + using SixLabors.ImageSharp.ColorSpaces.Conversion; + using SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder; + using SixLabors.ImageSharp.Memory; -using Xunit; -using Xunit.Abstractions; + using Xunit; + using Xunit.Abstractions; -namespace SixLabors.ImageSharp.Tests.Formats.Jpg -{ public class JpegColorConverterTests { - private const float Precision = 0.1f; + private const float Precision = 0.01f; - private const int InputBufferLength = 42; - - // The result buffer could be shorter - private const int ResultBufferLength = 40; - - private readonly Vector4[] result = new Vector4[ResultBufferLength]; + public static readonly TheoryData CommonConversionData = + new TheoryData + { + { 40, 40, 1 }, + { 42, 40, 2 }, + { 42, 39, 3 } + }; private static readonly ColorSpaceConverter ColorSpaceConverter = new ColorSpaceConverter(); @@ -31,161 +32,193 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg private ITestOutputHelper Output { get; } - private static JpegColorConverter.ComponentValues CreateRandomValues(int componentCount, float maxVal = 255f) + [Theory] + [MemberData(nameof(CommonConversionData))] + public void ConvertFromYCbCr(int inputBufferLength, int resultBufferLength, int seed) { - var rnd = new Random(42); - Buffer2D[] buffers = new Buffer2D[componentCount]; - for (int i = 0; i < componentCount; i++) - { - float[] values = new float[InputBufferLength]; - - for (int j = 0; j < InputBufferLength; j++) - { - values[j] = (float)rnd.NextDouble() * maxVal; - } - - // no need to dispose when buffer is not array owner - buffers[i] = new Buffer2D(values, values.Length, 1); - } - return new JpegColorConverter.ComponentValues(buffers, 0); + ValidateConversion( + JpegColorSpace.YCbCr, + 3, + inputBufferLength, + resultBufferLength, + seed, + (values, result, i) => + { + float y = values.Component0[i]; + float cb = values.Component1[i]; + float cr = values.Component2[i]; + var ycbcr = new YCbCr(y, cb, cr); + + Vector4 rgba = result[i]; + var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); + var expected = ColorSpaceConverter.ToRgb(ycbcr); + + Assert.True(actual.AlmostEquals(expected, Precision)); + Assert.Equal(1, rgba.W); + }); } - [Fact] - public void ConvertFromYCbCr() + [Theory] + [MemberData(nameof(CommonConversionData))] + public void ConvertFromCmyk(int inputBufferLength, int resultBufferLength, int seed) { - var converter = JpegColorConverter.GetConverter(JpegColorSpace.YCbCr); - - JpegColorConverter.ComponentValues values = CreateRandomValues(3); - - converter.ConvertToRGBA(values, this.result); - - for (int i = 0; i < ResultBufferLength; i++) - { - float y = values.Component0[i]; - float cb = values.Component1[i]; - float cr = values.Component2[i]; - var ycbcr = new YCbCr(y, cb, cr); - - Vector4 rgba = this.result[i]; - var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); - var expected = ColorSpaceConverter.ToRgb(ycbcr); - - Assert.True(actual.AlmostEquals(expected, Precision)); - Assert.Equal(1, rgba.W); - } - } - - [Fact] - public void ConvertFromCmyk() - { - var converter = JpegColorConverter.GetConverter(JpegColorSpace.Cmyk); - - JpegColorConverter.ComponentValues values = CreateRandomValues(4); - - converter.ConvertToRGBA(values, this.result); - var v = new Vector4(0, 0, 0, 1F); var scale = new Vector4(1 / 255F, 1 / 255F, 1 / 255F, 1F); - for (int i = 0; i < ResultBufferLength; i++) - { - float c = values.Component0[i]; - float m = values.Component1[i]; - float y = values.Component2[i]; - float k = values.Component3[i] / 255F; - - v.X = c * k; - v.Y = m * k; - v.Z = y * k; - v.W = 1F; - - v *= scale; - - Vector4 rgba = this.result[i]; - var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); - var expected = new Rgb(v.X, v.Y, v.Z); - - Assert.True(actual.AlmostEquals(expected, Precision)); - Assert.Equal(1, rgba.W); - } + ValidateConversion( + JpegColorSpace.Cmyk, + 4, + inputBufferLength, + resultBufferLength, + seed, + (values, result, i) => + { + float c = values.Component0[i]; + float m = values.Component1[i]; + float y = values.Component2[i]; + float k = values.Component3[i] / 255F; + + v.X = c * k; + v.Y = m * k; + v.Z = y * k; + v.W = 1F; + + v *= scale; + + Vector4 rgba = result[i]; + var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); + var expected = new Rgb(v.X, v.Y, v.Z); + + Assert.True(actual.AlmostEquals(expected, Precision)); + Assert.Equal(1, rgba.W); + }); } - [Fact] - public void ConvertFromYcck() + [Theory] + [MemberData(nameof(CommonConversionData))] + public void ConvertFromGrayScale(int inputBufferLength, int resultBufferLength, int seed) { - var converter = JpegColorConverter.GetConverter(JpegColorSpace.Ycck); - - JpegColorConverter.ComponentValues values = CreateRandomValues(4); + ValidateConversion( + JpegColorSpace.GrayScale, + 1, + inputBufferLength, + resultBufferLength, + seed, + (values, result, i) => + { + float y = values.Component0[i]; + Vector4 rgba = result[i]; + var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); + var expected = new Rgb(y / 255F, y / 255F, y / 255F); + + Assert.True(actual.AlmostEquals(expected, Precision)); + Assert.Equal(1, rgba.W); + }); + } - converter.ConvertToRGBA(values, this.result); + [Theory] + [MemberData(nameof(CommonConversionData))] + public void ConvertFromRgb(int inputBufferLength, int resultBufferLength, int seed) + { + ValidateConversion( + JpegColorSpace.RGB, + 3, + inputBufferLength, + resultBufferLength, + seed, + (values, result, i) => + { + float r = values.Component0[i]; + float g = values.Component1[i]; + float b = values.Component2[i]; + Vector4 rgba = result[i]; + var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); + var expected = new Rgb(r / 255F, g / 255F, b / 255F); + + Assert.True(actual.AlmostEquals(expected, Precision)); + Assert.Equal(1, rgba.W); + }); + } + [Theory] + [MemberData(nameof(CommonConversionData))] + public void ConvertFromYcck(int inputBufferLength, int resultBufferLength, int seed) + { var v = new Vector4(0, 0, 0, 1F); var scale = new Vector4(1 / 255F, 1 / 255F, 1 / 255F, 1F); - for (int i = 0; i < ResultBufferLength; i++) - { - float y = values.Component0[i]; - float cb = values.Component1[i] - 128F; - float cr = values.Component2[i] - 128F; - float k = values.Component3[i] / 255F; - - v.X = (255F - MathF.Round(y + (1.402F * cr), MidpointRounding.AwayFromZero)) * k; - v.Y = (255F - MathF.Round(y - (0.344136F * cb) - (0.714136F * cr), MidpointRounding.AwayFromZero)) * k; - v.Z = (255F - MathF.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero)) * k; - v.W = 1F; - - v *= scale; - - Vector4 rgba = this.result[i]; - var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); - var expected = new Rgb(v.X, v.Y, v.Z); - - Assert.True(actual.AlmostEquals(expected, Precision)); - Assert.Equal(1, rgba.W); - } + ValidateConversion( + JpegColorSpace.Ycck, + 4, + inputBufferLength, + resultBufferLength, + seed, + (values, result, i) => + { + float y = values.Component0[i]; + float cb = values.Component1[i] - 128F; + float cr = values.Component2[i] - 128F; + float k = values.Component3[i] / 255F; + + v.X = (255F - MathF.Round(y + (1.402F * cr), MidpointRounding.AwayFromZero)) * k; + v.Y = (255F - MathF.Round( + y - (0.344136F * cb) - (0.714136F * cr), + MidpointRounding.AwayFromZero)) * k; + v.Z = (255F - MathF.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero)) * k; + v.W = 1F; + + v *= scale; + + Vector4 rgba = result[i]; + var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); + var expected = new Rgb(v.X, v.Y, v.Z); + + Assert.True(actual.AlmostEquals(expected, Precision)); + Assert.Equal(1, rgba.W); + }); } - [Fact] - public void ConvertFromGrayScale() + private static JpegColorConverter.ComponentValues CreateRandomValues( + int componentCount, + int inputBufferLength, + int seed, + float maxVal = 255f) { - var converter = JpegColorConverter.GetConverter(JpegColorSpace.GrayScale); - - JpegColorConverter.ComponentValues values = CreateRandomValues(1); - - converter.ConvertToRGBA(values, this.result); - - for (int i = 0; i < ResultBufferLength; i++) + var rnd = new Random(seed); + Buffer2D[] buffers = new Buffer2D[componentCount]; + for (int i = 0; i < componentCount; i++) { - float y = values.Component0[i]; - Vector4 rgba = this.result[i]; - var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); - var expected = new Rgb(y / 255F, y / 255F, y / 255F); + float[] values = new float[inputBufferLength]; - Assert.True(actual.AlmostEquals(expected, Precision)); - Assert.Equal(1, rgba.W); + for (int j = 0; j < inputBufferLength; j++) + { + values[j] = (float)rnd.NextDouble() * maxVal; + } + + // no need to dispose when buffer is not array owner + buffers[i] = new Buffer2D(values, values.Length, 1); } + return new JpegColorConverter.ComponentValues(buffers, 0); } - [Fact] - public void ConvertFromRgb() + private static void ValidateConversion( + JpegColorSpace colorSpace, + int componentCount, + int inputBufferLength, + int resultBufferLength, + int seed, + Action, int> doValidate) { - var converter = JpegColorConverter.GetConverter(JpegColorSpace.RGB); + var converter = JpegColorConverter.GetConverter(colorSpace); - JpegColorConverter.ComponentValues values = CreateRandomValues(3); + JpegColorConverter.ComponentValues values = CreateRandomValues(componentCount, inputBufferLength, seed); + Vector4[] result = new Vector4[resultBufferLength]; - converter.ConvertToRGBA(values, this.result); + converter.ConvertToRGBA(values, result); - for (int i = 0; i < ResultBufferLength; i++) + for (int i = 0; i < resultBufferLength; i++) { - float r = values.Component0[i]; - float g = values.Component1[i]; - float b = values.Component2[i]; - Vector4 rgba = this.result[i]; - var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); - var expected = new Rgb(r / 255F, g / 255F, b / 255F); - - Assert.True(actual.AlmostEquals(expected, Precision)); - Assert.Equal(1, rgba.W); + doValidate(values, result, i); } } } From e5197d78ef190c00011d3dba57fe1e0ffa500f36 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Tue, 12 Sep 2017 03:44:04 +0200 Subject: [PATCH 2/8] JpegColorConverter.FromYCbCrSimd256 conversion core method seems to work --- .../Common/Extensions/Vector4Extensions.cs | 11 ++ .../Decoder/JpegColorConverter.FromCmyk.cs | 2 +- .../JpegColorConverter.FromGrayScale.cs | 2 +- .../Decoder/JpegColorConverter.FromRgb.cs | 2 +- .../Decoder/JpegColorConverter.FromYCbCr.cs | 161 +++++++++++++++++- .../Decoder/JpegColorConverter.FromYccK.cs | 2 +- .../Jpeg/Common/Decoder/JpegColorConverter.cs | 2 +- .../Formats/Jpg/JpegColorConverterTests.cs | 114 +++++++++---- 8 files changed, 256 insertions(+), 40 deletions(-) diff --git a/src/ImageSharp/Common/Extensions/Vector4Extensions.cs b/src/ImageSharp/Common/Extensions/Vector4Extensions.cs index 5fbc3960a..1809dd329 100644 --- a/src/ImageSharp/Common/Extensions/Vector4Extensions.cs +++ b/src/ImageSharp/Common/Extensions/Vector4Extensions.cs @@ -79,5 +79,16 @@ namespace SixLabors.ImageSharp return MathF.Pow((signal + 0.055F) / 1.055F, 2.4F); } + + /// + /// Transform all scalars in 'v' in a way that converting them to would have rounding semantics. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector4 PseudoRound(this Vector4 v) + { + var sign = Vector4.Clamp(v, new Vector4(-1), new Vector4(1)); + + return v + (sign * 0.5f); + } } } diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromCmyk.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromCmyk.cs index 524cc76df..908ffb85e 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromCmyk.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromCmyk.cs @@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder { internal abstract partial class JpegColorConverter { - private class FromCmyk : JpegColorConverter + internal class FromCmyk : JpegColorConverter { public FromCmyk() : base(JpegColorSpace.Cmyk) diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromGrayScale.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromGrayScale.cs index 9ff263dcf..2b19eebac 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromGrayScale.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromGrayScale.cs @@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder { internal abstract partial class JpegColorConverter { - private class FromGrayScale : JpegColorConverter + internal class FromGrayScale : JpegColorConverter { public FromGrayScale() : base(JpegColorSpace.GrayScale) diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromRgb.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromRgb.cs index f4a702783..c4a30ea3e 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromRgb.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromRgb.cs @@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder { internal abstract partial class JpegColorConverter { - private class FromRgb : JpegColorConverter + internal class FromRgb : JpegColorConverter { public FromRgb() : base(JpegColorSpace.RGB) diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs index 24e8d753b..a516ceb5a 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs @@ -1,18 +1,26 @@ using System; using System.Numerics; +using System.Runtime.CompilerServices; + +// ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder { internal abstract partial class JpegColorConverter { - private class FromYCbCr : JpegColorConverter + internal class FromYCbCrBasic : JpegColorConverter { - public FromYCbCr() + public FromYCbCrBasic() : base(JpegColorSpace.YCbCr) { } public override void ConvertToRGBA(ComponentValues values, Span result) + { + ConvertCore(values, result); + } + + internal static void ConvertCore(ComponentValues values, Span result) { // TODO: We can optimize a lot here with Vector and SRCS.Unsafe()! ReadOnlySpan yVals = values.Component0; @@ -39,5 +47,154 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder } } } + + internal class FromYCbCrSimd256 : JpegColorConverter + { + public FromYCbCrSimd256() + : base(JpegColorSpace.YCbCr) + { + } + + public override void ConvertToRGBA(ComponentValues values, Span result) + { + } + + private struct Vector4Pair + { + public Vector4 A; + + public Vector4 B; + + private static readonly Vector4 Scale = new Vector4(1 / 255F); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RoundAndDownscale() + { + this.A = this.A.PseudoRound() * Scale; + this.B = this.B.PseudoRound() * Scale; + } + } + + private struct Vector4Octet + { +#pragma warning disable SA1132 // Do not combine fields + public Vector4 V0, V1, V2, V3, V4, V5, V6, V7; +#pragma warning restore SA1132 // Do not combine fields + + public static Vector4Octet CreateCollector() + { + var result = default(Vector4Octet); + result.V0.W = 1f; + result.V1.W = 1f; + result.V2.W = 1f; + result.V3.W = 1f; + result.V4.W = 1f; + result.V5.W = 1f; + result.V6.W = 1f; + result.V7.W = 1f; + return result; + } + + public void Collect(ref Vector4Pair rr, ref Vector4Pair gg, ref Vector4Pair bb) + { + this.V0.X = rr.A.X; + this.V0.Y = gg.A.X; + this.V0.Z = bb.A.X; + this.V0.W = 1f; + + this.V1.X = rr.A.Y; + this.V1.Y = gg.A.Y; + this.V1.Z = bb.A.Y; + this.V1.W = 1f; + + this.V2.X = rr.A.Z; + this.V2.Y = gg.A.Z; + this.V2.Z = bb.A.Z; + this.V2.W = 1f; + + this.V3.X = rr.A.W; + this.V3.Y = gg.A.W; + this.V3.Z = bb.A.W; + this.V3.W = 1f; + + this.V4.X = rr.B.X; + this.V4.Y = gg.B.X; + this.V4.Z = bb.B.X; + this.V4.W = 1f; + + this.V5.X = rr.B.Y; + this.V5.Y = gg.B.Y; + this.V5.Z = bb.B.Y; + this.V5.W = 1f; + + this.V6.X = rr.B.Z; + this.V6.Y = gg.B.Z; + this.V6.Z = bb.B.Z; + this.V6.W = 1f; + + this.V7.X = rr.B.W; + this.V7.Y = gg.B.W; + this.V7.Z = bb.B.W; + this.V7.W = 1f; + } + } + + internal static void ConvertAligned(ComponentValues values, Span result) + { + if (!IsAvailable) + { + throw new InvalidOperationException( + "JpegColorConverter.FromYCbCrSimd256 can be used only on architecture having 256 byte floating point SIMD registers!"); + } + + ref Vector yBase = + ref Unsafe.As>(ref values.Component0.DangerousGetPinnableReference()); + ref Vector cbBase = + ref Unsafe.As>(ref values.Component1.DangerousGetPinnableReference()); + ref Vector crBase = + ref Unsafe.As>(ref values.Component2.DangerousGetPinnableReference()); + + ref Vector4Octet resultBase = + ref Unsafe.As(ref result.DangerousGetPinnableReference()); + + var chromaOffset = new Vector(-128f); + + int n = result.Length / 8; + + for (int i = 0; i < n; i++) + { + // y = yVals[i]; + // cb = cbVals[i] - 128F; + // cr = crVals[i] - 128F; + Vector y = Unsafe.Add(ref yBase, i); + Vector cb = Unsafe.Add(ref cbBase, i) + chromaOffset; + Vector cr = Unsafe.Add(ref crBase, i) + chromaOffset; + + // r = y + (1.402F * cr); + // g = y - (0.344136F * cb) - (0.714136F * cr); + // b = y + (1.772F * cb); + // Adding & multiplying 8 elements at one time: + Vector r = y + (cr * new Vector(1.402F)); + Vector g = y - (cb * new Vector(0.344136F)) - (cr * new Vector(0.714136F)); + Vector b = y + (cb * new Vector(1.772F)); + + // Vector has no .Clamp(), need to switch to Vector4 for the next operation: + // TODO: Is it worth to use Vector at all? + Vector4Pair rr = Unsafe.As, Vector4Pair>(ref r); + Vector4Pair gg = Unsafe.As, Vector4Pair>(ref g); + Vector4Pair bb = Unsafe.As, Vector4Pair>(ref b); + + rr.RoundAndDownscale(); + gg.RoundAndDownscale(); + bb.RoundAndDownscale(); + + // Collect (r0,r1...r8) (g0,g1...g8) (b0,b1...b8) vector values in the expected (r0,g0,g1,1), (r1,g1,g2,1) ... order: + ref Vector4Octet destination = ref Unsafe.Add(ref resultBase, i); + destination.Collect(ref rr, ref gg, ref bb); + } + } + + public static bool IsAvailable => Vector.Count == 8; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYccK.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYccK.cs index 3449cc6b1..7e6edbdce 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYccK.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYccK.cs @@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder { internal abstract partial class JpegColorConverter { - private class FromYccK : JpegColorConverter + internal class FromYccK : JpegColorConverter { public FromYccK() : base(JpegColorSpace.Ycck) diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.cs index 567713422..0d0b6d3e9 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder /// /// The avalilable converters /// - private static readonly JpegColorConverter[] Converters = { new FromYCbCr(), new FromYccK(), new FromCmyk(), new FromGrayScale(), new FromRgb() }; + private static readonly JpegColorConverter[] Converters = { new FromYCbCrBasic(), new FromYccK(), new FromCmyk(), new FromGrayScale(), new FromRgb() }; /// /// Initializes a new instance of the class. diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs index d24890e8f..885a8f809 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs @@ -1,19 +1,20 @@ -namespace SixLabors.ImageSharp.Tests.Formats.Jpg -{ - using System; - using System.Numerics; +using System; +using System.Numerics; - using SixLabors.ImageSharp.ColorSpaces; - using SixLabors.ImageSharp.ColorSpaces.Conversion; - using SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder; - using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ColorSpaces; +using SixLabors.ImageSharp.ColorSpaces.Conversion; +using SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder; +using SixLabors.ImageSharp.Memory; - using Xunit; - using Xunit.Abstractions; +using Xunit; +using Xunit.Abstractions; +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ public class JpegColorConverterTests { - private const float Precision = 0.01f; + private const float Precision = 1/255f; public static readonly TheoryData CommonConversionData = new TheoryData @@ -32,30 +33,62 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg private ITestOutputHelper Output { get; } + private static int R(float f) => (int)MathF.Round(f, MidpointRounding.AwayFromZero); + + // TODO: Move this to a proper test class! + [Theory] + [InlineData(0.32, 54.5, -3.5, -4.1)] + [InlineData(5.3, 536.4, 4.5, 8.1)] + public void Vector4_PseudoRound(float x, float y, float z, float w) + { + var v = new Vector4(x, y, z, w); + + Vector4 actual = v.PseudoRound(); + + Assert.Equal( + R(v.X), + (int)actual.X + ); + Assert.Equal( + R(v.Y), + (int)actual.Y + ); + Assert.Equal( + R(v.Z), + (int)actual.Z + ); + Assert.Equal( + R(v.W), + (int)actual.W + ); + } + [Theory] [MemberData(nameof(CommonConversionData))] public void ConvertFromYCbCr(int inputBufferLength, int resultBufferLength, int seed) { - ValidateConversion( - JpegColorSpace.YCbCr, - 3, - inputBufferLength, - resultBufferLength, - seed, - (values, result, i) => - { - float y = values.Component0[i]; - float cb = values.Component1[i]; - float cr = values.Component2[i]; - var ycbcr = new YCbCr(y, cb, cr); + ValidateConversion(JpegColorSpace.YCbCr, 3, inputBufferLength, resultBufferLength, seed, ValidateYCbCr); + } - Vector4 rgba = result[i]; - var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); - var expected = ColorSpaceConverter.ToRgb(ycbcr); + private static void ValidateYCbCr(JpegColorConverter.ComponentValues values, Span result, int i) + { + float y = values.Component0[i]; + float cb = values.Component1[i]; + float cr = values.Component2[i]; + var ycbcr = new YCbCr(y, cb, cr); - Assert.True(actual.AlmostEquals(expected, Precision)); - Assert.Equal(1, rgba.W); - }); + Vector4 rgba = result[i]; + var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); + var expected = ColorSpaceConverter.ToRgb(ycbcr); + + Assert.True(actual.AlmostEquals(expected, Precision)); + Assert.Equal(1, rgba.W); + } + + [Fact] + public void ConvertFromYCbCr_SimdWithAlignedValues() + { + ValidateConversion(JpegColorConverter.FromYCbCrSimd256.ConvertAligned, 3, 64, 64, 1, ValidateYCbCr); } [Theory] @@ -207,18 +240,33 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg int inputBufferLength, int resultBufferLength, int seed, - Action, int> doValidate) + Action, int> validatePixelValue) { - var converter = JpegColorConverter.GetConverter(colorSpace); + ValidateConversion( + (v, r) => JpegColorConverter.GetConverter(colorSpace).ConvertToRGBA(v, r), + componentCount, + inputBufferLength, + resultBufferLength, + seed, + validatePixelValue); + } + private static void ValidateConversion( + Action> doConvert, + int componentCount, + int inputBufferLength, + int resultBufferLength, + int seed, + Action, int> validatePixelValue) + { JpegColorConverter.ComponentValues values = CreateRandomValues(componentCount, inputBufferLength, seed); Vector4[] result = new Vector4[resultBufferLength]; - converter.ConvertToRGBA(values, result); + doConvert(values, result); for (int i = 0; i < resultBufferLength; i++) { - doValidate(values, result, i); + validatePixelValue(values, result, i); } } } From 84944e2db9b74f4b5f226de33b3c8a5787dc8dc4 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Tue, 12 Sep 2017 13:11:38 +0200 Subject: [PATCH 3/8] full conversion + benchmarking --- .../Decoder/JpegColorConverter.FromYCbCr.cs | 147 +++++++++--------- .../Jpeg/Common/Decoder/JpegColorConverter.cs | 24 +++ tests/ImageSharp.Sandbox46/Program.cs | 13 +- .../Formats/Jpg/JpegColorConverterTests.cs | 73 ++++++++- .../TestUtilities/MeasureFixture.cs | 23 +++ 5 files changed, 195 insertions(+), 85 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs index a516ceb5a..c38a82b42 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs @@ -2,8 +2,6 @@ using System.Numerics; using System.Runtime.CompilerServices; -// ReSharper disable InconsistentNaming - namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder { internal abstract partial class JpegColorConverter @@ -55,8 +53,79 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder { } + public static bool IsAvailable => Vector.IsHardwareAccelerated && Vector.Count == 8; + public override void ConvertToRGBA(ComponentValues values, Span result) - { + { + int remainder = result.Length % 8; + int simdCount = result.Length - remainder; + if (simdCount > 0) + { + ConvertCore(values.Slice(0, simdCount), result.Slice(0, simdCount)); + } + + FromYCbCrBasic.ConvertCore(values.Slice(simdCount, remainder), result.Slice(simdCount, remainder)); + } + + /// + /// SIMD convert using buffers of sizes divisable by 8. + /// + internal static void ConvertCore(ComponentValues values, Span result) + { + // This implementation is actually AVX specific. + // An AVX register is capable of storing 8 float-s. + if (!IsAvailable) + { + throw new InvalidOperationException( + "JpegColorConverter.FromYCbCrSimd256 can be used only on architecture having 256 byte floating point SIMD registers!"); + } + + ref Vector yBase = + ref Unsafe.As>(ref values.Component0.DangerousGetPinnableReference()); + ref Vector cbBase = + ref Unsafe.As>(ref values.Component1.DangerousGetPinnableReference()); + ref Vector crBase = + ref Unsafe.As>(ref values.Component2.DangerousGetPinnableReference()); + + ref Vector4Octet resultBase = + ref Unsafe.As(ref result.DangerousGetPinnableReference()); + + var chromaOffset = new Vector(-128f); + + // Walking 8 elements at one step: + int n = result.Length / 8; + + for (int i = 0; i < n; i++) + { + // y = yVals[i]; + // cb = cbVals[i] - 128F; + // cr = crVals[i] - 128F; + Vector y = Unsafe.Add(ref yBase, i); + Vector cb = Unsafe.Add(ref cbBase, i) + chromaOffset; + Vector cr = Unsafe.Add(ref crBase, i) + chromaOffset; + + // r = y + (1.402F * cr); + // g = y - (0.344136F * cb) - (0.714136F * cr); + // b = y + (1.772F * cb); + // Adding & multiplying 8 elements at one time: + Vector r = y + (cr * new Vector(1.402F)); + Vector g = y - (cb * new Vector(0.344136F)) - (cr * new Vector(0.714136F)); + Vector b = y + (cb * new Vector(1.772F)); + + // Vector has no .Clamp(), need to switch to Vector4 for the next operation: + // TODO: Is it worth to use Vector at all? + Vector4Pair rr = Unsafe.As, Vector4Pair>(ref r); + Vector4Pair gg = Unsafe.As, Vector4Pair>(ref g); + Vector4Pair bb = Unsafe.As, Vector4Pair>(ref b); + + rr.RoundAndDownscale(); + gg.RoundAndDownscale(); + bb.RoundAndDownscale(); + + // Collect (r0,r1...r8) (g0,g1...g8) (b0,b1...b8) vector values in the expected (r0,g0,g1,1), (r1,g1,g2,1) ... order: + ref Vector4Octet destination = ref Unsafe.Add(ref resultBase, i); + destination.Collect(ref rr, ref gg, ref bb); + } } private struct Vector4Pair @@ -79,21 +148,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder { #pragma warning disable SA1132 // Do not combine fields public Vector4 V0, V1, V2, V3, V4, V5, V6, V7; -#pragma warning restore SA1132 // Do not combine fields - - public static Vector4Octet CreateCollector() - { - var result = default(Vector4Octet); - result.V0.W = 1f; - result.V1.W = 1f; - result.V2.W = 1f; - result.V3.W = 1f; - result.V4.W = 1f; - result.V5.W = 1f; - result.V6.W = 1f; - result.V7.W = 1f; - return result; - } public void Collect(ref Vector4Pair rr, ref Vector4Pair gg, ref Vector4Pair bb) { @@ -138,63 +192,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder this.V7.W = 1f; } } - - internal static void ConvertAligned(ComponentValues values, Span result) - { - if (!IsAvailable) - { - throw new InvalidOperationException( - "JpegColorConverter.FromYCbCrSimd256 can be used only on architecture having 256 byte floating point SIMD registers!"); - } - - ref Vector yBase = - ref Unsafe.As>(ref values.Component0.DangerousGetPinnableReference()); - ref Vector cbBase = - ref Unsafe.As>(ref values.Component1.DangerousGetPinnableReference()); - ref Vector crBase = - ref Unsafe.As>(ref values.Component2.DangerousGetPinnableReference()); - - ref Vector4Octet resultBase = - ref Unsafe.As(ref result.DangerousGetPinnableReference()); - - var chromaOffset = new Vector(-128f); - - int n = result.Length / 8; - - for (int i = 0; i < n; i++) - { - // y = yVals[i]; - // cb = cbVals[i] - 128F; - // cr = crVals[i] - 128F; - Vector y = Unsafe.Add(ref yBase, i); - Vector cb = Unsafe.Add(ref cbBase, i) + chromaOffset; - Vector cr = Unsafe.Add(ref crBase, i) + chromaOffset; - - // r = y + (1.402F * cr); - // g = y - (0.344136F * cb) - (0.714136F * cr); - // b = y + (1.772F * cb); - // Adding & multiplying 8 elements at one time: - Vector r = y + (cr * new Vector(1.402F)); - Vector g = y - (cb * new Vector(0.344136F)) - (cr * new Vector(0.714136F)); - Vector b = y + (cb * new Vector(1.772F)); - - // Vector has no .Clamp(), need to switch to Vector4 for the next operation: - // TODO: Is it worth to use Vector at all? - Vector4Pair rr = Unsafe.As, Vector4Pair>(ref r); - Vector4Pair gg = Unsafe.As, Vector4Pair>(ref g); - Vector4Pair bb = Unsafe.As, Vector4Pair>(ref b); - - rr.RoundAndDownscale(); - gg.RoundAndDownscale(); - bb.RoundAndDownscale(); - - // Collect (r0,r1...r8) (g0,g1...g8) (b0,b1...b8) vector values in the expected (r0,g0,g1,1), (r1,g1,g2,1) ... order: - ref Vector4Octet destination = ref Unsafe.Add(ref resultBase, i); - destination.Collect(ref rr, ref gg, ref bb); - } - } - - public static bool IsAvailable => Vector.Count == 8; } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.cs index 0d0b6d3e9..59d695d55 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.cs @@ -108,6 +108,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder } } } + + private ComponentValues( + int componentCount, + ReadOnlySpan c0, + ReadOnlySpan c1, + ReadOnlySpan c2, + ReadOnlySpan c3) + { + this.ComponentCount = componentCount; + this.Component0 = c0; + this.Component1 = c1; + this.Component2 = c2; + this.Component3 = c3; + } + + public ComponentValues Slice(int start, int length) + { + ReadOnlySpan c0 = this.Component0.Slice(start, length); + ReadOnlySpan c1 = this.ComponentCount > 1 ? this.Component1.Slice(start, length) : ReadOnlySpan.Empty; + ReadOnlySpan c2 = this.ComponentCount > 2 ? this.Component2.Slice(start, length) : ReadOnlySpan.Empty; + ReadOnlySpan c3 = this.ComponentCount > 3 ? this.Component3.Slice(start, length) : ReadOnlySpan.Empty; + + return new ComponentValues(this.ComponentCount, c0, c1, c2, c3); + } } } } \ No newline at end of file diff --git a/tests/ImageSharp.Sandbox46/Program.cs b/tests/ImageSharp.Sandbox46/Program.cs index 532bf9574..869a720df 100644 --- a/tests/ImageSharp.Sandbox46/Program.cs +++ b/tests/ImageSharp.Sandbox46/Program.cs @@ -41,14 +41,21 @@ namespace SixLabors.ImageSharp.Sandbox46 /// public static void Main(string[] args) { - RunDecodeJpegProfilingTests(); - // RunToVector4ProfilingTest(); + RunJpegColorProfilingTests(); - //RunResizeProfilingTest(); + // RunDecodeJpegProfilingTests(); + // RunToVector4ProfilingTest(); + // RunResizeProfilingTest(); Console.ReadLine(); } + private static void RunJpegColorProfilingTests() + { + new JpegColorConverterTests(new ConsoleOutput()).BenchmarkYCbCr(false); + new JpegColorConverterTests(new ConsoleOutput()).BenchmarkYCbCr(true); + } + private static void RunResizeProfilingTest() { ResizeProfilingBenchmarks test = new ResizeProfilingBenchmarks(new ConsoleOutput()); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs index 885a8f809..a414ee977 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs @@ -65,9 +65,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Theory] [MemberData(nameof(CommonConversionData))] - public void ConvertFromYCbCr(int inputBufferLength, int resultBufferLength, int seed) + public void ConvertFromYCbCrBasic(int inputBufferLength, int resultBufferLength, int seed) { - ValidateConversion(JpegColorSpace.YCbCr, 3, inputBufferLength, resultBufferLength, seed, ValidateYCbCr); + ValidateConversion(new JpegColorConverter.FromYCbCrBasic(), 3, inputBufferLength, resultBufferLength, seed, ValidateYCbCr); } private static void ValidateYCbCr(JpegColorConverter.ComponentValues values, Span result, int i) @@ -81,14 +81,56 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg var actual = new Rgb(rgba.X, rgba.Y, rgba.Z); var expected = ColorSpaceConverter.ToRgb(ycbcr); - Assert.True(actual.AlmostEquals(expected, Precision)); + Assert.True(actual.AlmostEquals(expected, Precision), $"{actual} != {expected}"); Assert.Equal(1, rgba.W); } - [Fact] - public void ConvertFromYCbCr_SimdWithAlignedValues() + [Theory] + [InlineData(64, 1)] + [InlineData(16, 2)] + [InlineData(8, 3)] + public void FromYCbCrSimd256_ConvertCore(int size, int seed) + { + ValidateConversion(JpegColorConverter.FromYCbCrSimd256.ConvertCore, 3, size, size, seed, ValidateYCbCr); + } + + [Theory] + [MemberData(nameof(CommonConversionData))] + public void FromYCbCrSimd256(int inputBufferLength, int resultBufferLength, int seed) + { + ValidateConversion(new JpegColorConverter.FromYCbCrSimd256(), 3, inputBufferLength, resultBufferLength, seed, ValidateYCbCr); + } + + [Theory] + [MemberData(nameof(CommonConversionData))] + public void ConvertFromYCbCr_WithDefaultConverter(int inputBufferLength, int resultBufferLength, int seed) { - ValidateConversion(JpegColorConverter.FromYCbCrSimd256.ConvertAligned, 3, 64, 64, 1, ValidateYCbCr); + ValidateConversion(JpegColorSpace.YCbCr, 3, inputBufferLength, resultBufferLength, seed, ValidateYCbCr); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void BenchmarkYCbCr(bool simd) + { + int count = 2053; + int times = 50000; + + JpegColorConverter.ComponentValues values = CreateRandomValues(3, count, 1); + Vector4[] result = new Vector4[count]; + + JpegColorConverter converter = simd ? (JpegColorConverter)new JpegColorConverter.FromYCbCrSimd256() : new JpegColorConverter.FromYCbCrBasic(); + + // Warm up: + converter.ConvertToRGBA(values, result); + + using (new MeasureGuard(this.Output, $"{converter.GetType().Name} x {times}")) + { + for (int i = 0; i < times; i++) + { + converter.ConvertToRGBA(values, result); + } + } } [Theory] @@ -243,7 +285,24 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Action, int> validatePixelValue) { ValidateConversion( - (v, r) => JpegColorConverter.GetConverter(colorSpace).ConvertToRGBA(v, r), + JpegColorConverter.GetConverter(colorSpace), + componentCount, + inputBufferLength, + resultBufferLength, + seed, + validatePixelValue); + } + + private static void ValidateConversion( + JpegColorConverter converter, + int componentCount, + int inputBufferLength, + int resultBufferLength, + int seed, + Action, int> validatePixelValue) + { + ValidateConversion( + converter.ConvertToRGBA, componentCount, inputBufferLength, resultBufferLength, diff --git a/tests/ImageSharp.Tests/TestUtilities/MeasureFixture.cs b/tests/ImageSharp.Tests/TestUtilities/MeasureFixture.cs index 7725994c4..c892c09de 100644 --- a/tests/ImageSharp.Tests/TestUtilities/MeasureFixture.cs +++ b/tests/ImageSharp.Tests/TestUtilities/MeasureFixture.cs @@ -56,4 +56,27 @@ namespace SixLabors.ImageSharp.Tests protected ITestOutputHelper Output { get; } } + + public class MeasureGuard : IDisposable + { + private readonly string operation; + + private readonly Stopwatch stopwatch = new Stopwatch(); + + public MeasureGuard(ITestOutputHelper output, string operation) + { + this.operation = operation; + this.Output = output; + this.Output.WriteLine(operation + " ..."); + this.stopwatch.Start(); + } + + private ITestOutputHelper Output { get; } + + public void Dispose() + { + this.stopwatch.Stop(); + this.Output.WriteLine($"{this.operation} completed in {this.stopwatch.ElapsedMilliseconds}ms"); + } + } } \ No newline at end of file From d1953ea79c0f70ed0ec749273c3abcf35162e126 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Tue, 12 Sep 2017 14:01:08 +0200 Subject: [PATCH 4/8] go home Vector, you are drunk --- .../Decoder/JpegColorConverter.FromYCbCr.cs | 108 ++++++++++++------ .../Formats/Jpg/JpegColorConverterTests.cs | 17 +-- 2 files changed, 79 insertions(+), 46 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs index c38a82b42..ab0886619 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs @@ -46,15 +46,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder } } - internal class FromYCbCrSimd256 : JpegColorConverter + internal class FromYCbCrSimd : JpegColorConverter { - public FromYCbCrSimd256() + public FromYCbCrSimd() : base(JpegColorSpace.YCbCr) { } - public static bool IsAvailable => Vector.IsHardwareAccelerated && Vector.Count == 8; - public override void ConvertToRGBA(ComponentValues values, Span result) { int remainder = result.Length % 8; @@ -72,25 +70,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder /// internal static void ConvertCore(ComponentValues values, Span result) { - // This implementation is actually AVX specific. - // An AVX register is capable of storing 8 float-s. - if (!IsAvailable) - { - throw new InvalidOperationException( - "JpegColorConverter.FromYCbCrSimd256 can be used only on architecture having 256 byte floating point SIMD registers!"); - } + DebugGuard.IsTrue(result.Length % 8 == 0, nameof(result), "result.Length should be divisable by 8!"); - ref Vector yBase = - ref Unsafe.As>(ref values.Component0.DangerousGetPinnableReference()); - ref Vector cbBase = - ref Unsafe.As>(ref values.Component1.DangerousGetPinnableReference()); - ref Vector crBase = - ref Unsafe.As>(ref values.Component2.DangerousGetPinnableReference()); + ref Vector4Pair yBase = + ref Unsafe.As(ref values.Component0.DangerousGetPinnableReference()); + ref Vector4Pair cbBase = + ref Unsafe.As(ref values.Component1.DangerousGetPinnableReference()); + ref Vector4Pair crBase = + ref Unsafe.As(ref values.Component2.DangerousGetPinnableReference()); ref Vector4Octet resultBase = ref Unsafe.As(ref result.DangerousGetPinnableReference()); - var chromaOffset = new Vector(-128f); + var chromaOffset = new Vector4(-128f); // Walking 8 elements at one step: int n = result.Length / 8; @@ -100,47 +92,87 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder // y = yVals[i]; // cb = cbVals[i] - 128F; // cr = crVals[i] - 128F; - Vector y = Unsafe.Add(ref yBase, i); - Vector cb = Unsafe.Add(ref cbBase, i) + chromaOffset; - Vector cr = Unsafe.Add(ref crBase, i) + chromaOffset; + Vector4Pair y = Unsafe.Add(ref yBase, i); + Vector4Pair cb = Unsafe.Add(ref cbBase, i); + Vector4Pair cr = Unsafe.Add(ref crBase, i); + cb.AddInplace(chromaOffset); + cr.AddInplace(chromaOffset); // r = y + (1.402F * cr); + Vector4Pair r = y; + Vector4Pair tmp = cr; + tmp.MultiplyInplace(1.402F); + r.AddInplace(ref tmp); + // g = y - (0.344136F * cb) - (0.714136F * cr); - // b = y + (1.772F * cb); - // Adding & multiplying 8 elements at one time: - Vector r = y + (cr * new Vector(1.402F)); - Vector g = y - (cb * new Vector(0.344136F)) - (cr * new Vector(0.714136F)); - Vector b = y + (cb * new Vector(1.772F)); + Vector4Pair g = y; + tmp = cb; + tmp.MultiplyInplace(-0.344136F); + g.AddInplace(ref tmp); + tmp = cr; + tmp.MultiplyInplace(-0.714136F); + g.AddInplace(ref tmp); - // Vector has no .Clamp(), need to switch to Vector4 for the next operation: - // TODO: Is it worth to use Vector at all? - Vector4Pair rr = Unsafe.As, Vector4Pair>(ref r); - Vector4Pair gg = Unsafe.As, Vector4Pair>(ref g); - Vector4Pair bb = Unsafe.As, Vector4Pair>(ref b); + // b = y + (1.772F * cb); + Vector4Pair b = y; + tmp = cb; + tmp.MultiplyInplace(1.772F); + b.AddInplace(ref tmp); - rr.RoundAndDownscale(); - gg.RoundAndDownscale(); - bb.RoundAndDownscale(); + r.RoundAndDownscale(); + g.RoundAndDownscale(); + b.RoundAndDownscale(); // Collect (r0,r1...r8) (g0,g1...g8) (b0,b1...b8) vector values in the expected (r0,g0,g1,1), (r1,g1,g2,1) ... order: ref Vector4Octet destination = ref Unsafe.Add(ref resultBase, i); - destination.Collect(ref rr, ref gg, ref bb); + destination.Collect(ref r, ref g, ref b); } } + /// + /// Its faster to process multiple Vector4-s + /// private struct Vector4Pair { public Vector4 A; public Vector4 B; - private static readonly Vector4 Scale = new Vector4(1 / 255F); + private static readonly Vector4 Scale = new Vector4(1 / 255f); + + private static readonly Vector4 Half = new Vector4(0.5f); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void RoundAndDownscale() { - this.A = this.A.PseudoRound() * Scale; - this.B = this.B.PseudoRound() * Scale; + // Emulate rounding: + this.A += Half; + this.B += Half; + + // Downscale by 1/255 + this.A *= Scale; + this.B *= Scale; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MultiplyInplace(float value) + { + this.A *= value; + this.B *= value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddInplace(Vector4 value) + { + this.A += value; + this.B += value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddInplace(ref Vector4Pair other) + { + this.A += other.A; + this.B += other.B; } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs index a414ee977..e9db65105 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs @@ -89,16 +89,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [InlineData(64, 1)] [InlineData(16, 2)] [InlineData(8, 3)] - public void FromYCbCrSimd256_ConvertCore(int size, int seed) + public void FromYCbCrSimd_ConvertCore(int size, int seed) { - ValidateConversion(JpegColorConverter.FromYCbCrSimd256.ConvertCore, 3, size, size, seed, ValidateYCbCr); + ValidateConversion(JpegColorConverter.FromYCbCrSimd.ConvertCore, 3, size, size, seed, ValidateYCbCr); } [Theory] [MemberData(nameof(CommonConversionData))] - public void FromYCbCrSimd256(int inputBufferLength, int resultBufferLength, int seed) + public void FromYCbCrSimd(int inputBufferLength, int resultBufferLength, int seed) { - ValidateConversion(new JpegColorConverter.FromYCbCrSimd256(), 3, inputBufferLength, resultBufferLength, seed, ValidateYCbCr); + ValidateConversion(new JpegColorConverter.FromYCbCrSimd(), 3, inputBufferLength, resultBufferLength, seed, ValidateYCbCr); } [Theory] @@ -108,9 +108,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg ValidateConversion(JpegColorSpace.YCbCr, 3, inputBufferLength, resultBufferLength, seed, ValidateYCbCr); } - [Theory] - [InlineData(false)] - [InlineData(true)] + // Becnhmark, for local execution only + //[Theory] + //[InlineData(false)] + //[InlineData(true)] public void BenchmarkYCbCr(bool simd) { int count = 2053; @@ -119,7 +120,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg JpegColorConverter.ComponentValues values = CreateRandomValues(3, count, 1); Vector4[] result = new Vector4[count]; - JpegColorConverter converter = simd ? (JpegColorConverter)new JpegColorConverter.FromYCbCrSimd256() : new JpegColorConverter.FromYCbCrBasic(); + JpegColorConverter converter = simd ? (JpegColorConverter)new JpegColorConverter.FromYCbCrSimd() : new JpegColorConverter.FromYCbCrBasic(); // Warm up: converter.ConvertToRGBA(values, result); From 343a8920d806d9d92efeb45c2f43f103fadfdbd4 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Tue, 12 Sep 2017 14:14:33 +0200 Subject: [PATCH 5/8] switched to FromYCbCrSimd --- .../Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs | 2 +- .../Formats/Jpeg/Common/Decoder/JpegColorConverter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs index ab0886619..e8755b286 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs @@ -130,7 +130,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder } /// - /// Its faster to process multiple Vector4-s + /// Its faster to process multiple Vector4-s together /// private struct Vector4Pair { diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.cs index 59d695d55..1c2dda01f 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder /// /// The avalilable converters /// - private static readonly JpegColorConverter[] Converters = { new FromYCbCrBasic(), new FromYccK(), new FromCmyk(), new FromGrayScale(), new FromRgb() }; + private static readonly JpegColorConverter[] Converters = { new FromYCbCrSimd(), new FromYccK(), new FromCmyk(), new FromGrayScale(), new FromRgb() }; /// /// Initializes a new instance of the class. From 82acd242415079b397be0baf96a71cdc99085d24 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Tue, 12 Sep 2017 14:27:37 +0200 Subject: [PATCH 6/8] minor cleanup --- .../Decoder/JpegColorConverter.FromYCbCr.cs | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs index e8755b286..059b2e89a 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs @@ -90,12 +90,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder for (int i = 0; i < n; i++) { // y = yVals[i]; - // cb = cbVals[i] - 128F; - // cr = crVals[i] - 128F; Vector4Pair y = Unsafe.Add(ref yBase, i); + + // cb = cbVals[i] - 128F; Vector4Pair cb = Unsafe.Add(ref cbBase, i); - Vector4Pair cr = Unsafe.Add(ref crBase, i); cb.AddInplace(chromaOffset); + + // cr = crVals[i] - 128F; + Vector4Pair cr = Unsafe.Add(ref crBase, i); cr.AddInplace(chromaOffset); // r = y + (1.402F * cr); @@ -181,46 +183,49 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder #pragma warning disable SA1132 // Do not combine fields public Vector4 V0, V1, V2, V3, V4, V5, V6, V7; - public void Collect(ref Vector4Pair rr, ref Vector4Pair gg, ref Vector4Pair bb) + /// + /// Collect (r0,r1...r8) (g0,g1...g8) (b0,b1...b8) vector values in the expected (r0,g0,g1,1), (r1,g1,g2,1) ... order. + /// + public void Collect(ref Vector4Pair r, ref Vector4Pair g, ref Vector4Pair b) { - this.V0.X = rr.A.X; - this.V0.Y = gg.A.X; - this.V0.Z = bb.A.X; + this.V0.X = r.A.X; + this.V0.Y = g.A.X; + this.V0.Z = b.A.X; this.V0.W = 1f; - this.V1.X = rr.A.Y; - this.V1.Y = gg.A.Y; - this.V1.Z = bb.A.Y; + this.V1.X = r.A.Y; + this.V1.Y = g.A.Y; + this.V1.Z = b.A.Y; this.V1.W = 1f; - this.V2.X = rr.A.Z; - this.V2.Y = gg.A.Z; - this.V2.Z = bb.A.Z; + this.V2.X = r.A.Z; + this.V2.Y = g.A.Z; + this.V2.Z = b.A.Z; this.V2.W = 1f; - this.V3.X = rr.A.W; - this.V3.Y = gg.A.W; - this.V3.Z = bb.A.W; + this.V3.X = r.A.W; + this.V3.Y = g.A.W; + this.V3.Z = b.A.W; this.V3.W = 1f; - this.V4.X = rr.B.X; - this.V4.Y = gg.B.X; - this.V4.Z = bb.B.X; + this.V4.X = r.B.X; + this.V4.Y = g.B.X; + this.V4.Z = b.B.X; this.V4.W = 1f; - this.V5.X = rr.B.Y; - this.V5.Y = gg.B.Y; - this.V5.Z = bb.B.Y; + this.V5.X = r.B.Y; + this.V5.Y = g.B.Y; + this.V5.Z = b.B.Y; this.V5.W = 1f; - this.V6.X = rr.B.Z; - this.V6.Y = gg.B.Z; - this.V6.Z = bb.B.Z; + this.V6.X = r.B.Z; + this.V6.Y = g.B.Z; + this.V6.Z = b.B.Z; this.V6.W = 1f; - this.V7.X = rr.B.W; - this.V7.Y = gg.B.W; - this.V7.Z = bb.B.W; + this.V7.X = r.B.W; + this.V7.Y = g.B.W; + this.V7.Z = b.B.W; this.V7.W = 1f; } } From a8f2e8fe358473801c36d00aface596df2c303e9 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Tue, 12 Sep 2017 19:34:23 +0200 Subject: [PATCH 7/8] Optimized Block8x8F.CopyTo() for the 4:2:0 case --- .../Formats/Jpeg/Common/Block8x8F.cs | 67 ++++++++++++++++--- src/ImageSharp/Memory/BufferArea{T}.cs | 1 + 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.cs index 4d0ec3393..474c75adf 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.cs @@ -305,14 +305,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CopyRowImpl(ref byte selfBase, ref byte destBase, int destStride, int row) - { - ref byte s = ref Unsafe.Add(ref selfBase, row * 8 * sizeof(float)); - ref byte d = ref Unsafe.Add(ref destBase, row * destStride); - Unsafe.CopyBlock(ref d, ref s, 8 * sizeof(float)); - } - + // [MethodImpl(MethodImplOptions.AggressiveInlining)] public void CopyTo(BufferArea area) { ref byte selfBase = ref Unsafe.As(ref this); @@ -336,17 +329,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common this.CopyTo(area); return; } + else if (horizontalScale == 2 && verticalScale == 2) + { + this.CopyTo2x2(area); + return; + } // TODO: Optimize: implement all the cases with loopless special code! (T4?) for (int y = 0; y < 8; y++) { int yy = y * verticalScale; + int y8 = y * 8; for (int x = 0; x < 8; x++) { int xx = x * horizontalScale; - float value = this[(y * 8) + x]; + float value = this[y8 + x]; for (int i = 0; i < verticalScale; i++) { @@ -359,6 +358,56 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common } } + private void CopyTo2x2(BufferArea area) + { + ref float destBase = ref area.GetReferenceToOrigo(); + int destStride = area.Stride; + + this.CopyRow2x2Impl(ref destBase, 0, destStride); + this.CopyRow2x2Impl(ref destBase, 1, destStride); + this.CopyRow2x2Impl(ref destBase, 2, destStride); + this.CopyRow2x2Impl(ref destBase, 3, destStride); + this.CopyRow2x2Impl(ref destBase, 4, destStride); + this.CopyRow2x2Impl(ref destBase, 5, destStride); + this.CopyRow2x2Impl(ref destBase, 6, destStride); + this.CopyRow2x2Impl(ref destBase, 7, destStride); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void CopyRowImpl(ref byte selfBase, ref byte destBase, int destStride, int row) + { + ref byte s = ref Unsafe.Add(ref selfBase, row * 8 * sizeof(float)); + ref byte d = ref Unsafe.Add(ref destBase, row * destStride); + Unsafe.CopyBlock(ref d, ref s, 8 * sizeof(float)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CopyRow2x2Impl(ref float destBase, int row, int destStride) + { + ref Vector4 selfLeft = ref Unsafe.Add(ref this.V0L, 2 * row); + ref Vector4 selfRight = ref Unsafe.Add(ref selfLeft, 1); + ref float destLocalOrigo = ref Unsafe.Add(ref destBase, row * 2 * destStride); + + Stride2VectorCopyImpl(ref selfLeft, ref destLocalOrigo); + Stride2VectorCopyImpl(ref selfRight, ref Unsafe.Add(ref destLocalOrigo, 8)); + + Stride2VectorCopyImpl(ref selfLeft, ref Unsafe.Add(ref destLocalOrigo, destStride)); + Stride2VectorCopyImpl(ref selfRight, ref Unsafe.Add(ref destLocalOrigo, destStride + 8)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Stride2VectorCopyImpl(ref Vector4 s, ref float destBase) + { + Unsafe.Add(ref destBase, 0) = s.X; + Unsafe.Add(ref destBase, 1) = s.X; + Unsafe.Add(ref destBase, 2) = s.Y; + Unsafe.Add(ref destBase, 3) = s.Y; + Unsafe.Add(ref destBase, 4) = s.Z; + Unsafe.Add(ref destBase, 5) = s.Z; + Unsafe.Add(ref destBase, 6) = s.W; + Unsafe.Add(ref destBase, 7) = s.W; + } + public float[] ToArray() { float[] result = new float[Size]; diff --git a/src/ImageSharp/Memory/BufferArea{T}.cs b/src/ImageSharp/Memory/BufferArea{T}.cs index 92e78e9c0..8ead22680 100644 --- a/src/ImageSharp/Memory/BufferArea{T}.cs +++ b/src/ImageSharp/Memory/BufferArea{T}.cs @@ -60,6 +60,7 @@ namespace SixLabors.ImageSharp.Memory /// Gets a reference to the [0,0] element. /// /// The reference to the [0,0] element + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ref T GetReferenceToOrigo() => ref this.DestinationBuffer.Span[(this.Rectangle.Y * this.DestinationBuffer.Width) + this.Rectangle.X]; From fac95ccc2bb60e870ec3c7fedfb9286484b182fe Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Tue, 12 Sep 2017 18:28:06 +0100 Subject: [PATCH 8/8] remove usage of Span from public api surface --- .../ImageSharp.Drawing.csproj | 4 +- src/ImageSharp.Drawing/Paths/ShapeRegion.cs | 8 +-- .../Processors/FillRegionProcessor.cs | 5 +- src/ImageSharp.Drawing/Region.cs | 5 +- src/ImageSharp/Advanced/ImageExtensions.cs | 2 +- src/ImageSharp/Image/Image.LoadPixelData.cs | 66 +++++++++++++++++-- src/ImageSharp/Image/ImageExtensions.cs | 2 +- .../Drawing/FillRegionProcessorTests.cs | 4 +- .../Drawing/Paths/ShapeRegionTests.cs | 12 ++-- 9 files changed, 83 insertions(+), 25 deletions(-) diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 4f8ab3462..dfef69b75 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -37,8 +37,8 @@ - - + + All diff --git a/src/ImageSharp.Drawing/Paths/ShapeRegion.cs b/src/ImageSharp.Drawing/Paths/ShapeRegion.cs index a303179e8..1965a7433 100644 --- a/src/ImageSharp.Drawing/Paths/ShapeRegion.cs +++ b/src/ImageSharp.Drawing/Paths/ShapeRegion.cs @@ -42,18 +42,18 @@ namespace SixLabors.ImageSharp.Drawing public override Rectangle Bounds { get; } /// - public override int Scan(float y, Span buffer) + public override int Scan(float y, float[] buffer, int offset) { var start = new PointF(this.Bounds.Left - 1, y); var end = new PointF(this.Bounds.Right + 1, y); using (var innerBuffer = new Buffer(buffer.Length)) { - var span = innerBuffer.Span; - int count = this.Shape.FindIntersections(start, end, span); + PointF[] array = innerBuffer.Array; + int count = this.Shape.FindIntersections(start, end, array, offset); for (int i = 0; i < count; i++) { - buffer[i] = span[i].X; + buffer[i] = array[i].X; } return count; diff --git a/src/ImageSharp.Drawing/Processors/FillRegionProcessor.cs b/src/ImageSharp.Drawing/Processors/FillRegionProcessor.cs index 3e0dedb3b..78231d29a 100644 --- a/src/ImageSharp.Drawing/Processors/FillRegionProcessor.cs +++ b/src/ImageSharp.Drawing/Processors/FillRegionProcessor.cs @@ -93,7 +93,6 @@ namespace SixLabors.ImageSharp.Drawing.Processors using (BrushApplicator applicator = this.Brush.CreateApplicator(source, rect, this.Options)) { float[] buffer = arrayPool.Rent(maxIntersections); - Span bufferSpan = buffer.AsSpan().Slice(0, maxIntersections); int scanlineWidth = maxX - minX; using (var scanline = new Buffer(scanlineWidth)) { @@ -117,14 +116,14 @@ namespace SixLabors.ImageSharp.Drawing.Processors float subpixelFractionPoint = subpixelFraction / subpixelCount; for (float subPixel = (float)y; subPixel < y + 1; subPixel += subpixelFraction) { - int pointsFound = region.Scan(subPixel, bufferSpan); + int pointsFound = region.Scan(subPixel, buffer, 0); if (pointsFound == 0) { // nothing on this line skip continue; } - QuickSort(bufferSpan.Slice(0, pointsFound)); + QuickSort(buffer.AsSpan().Slice(0, pointsFound)); for (int point = 0; point < pointsFound; point += 2) { diff --git a/src/ImageSharp.Drawing/Region.cs b/src/ImageSharp.Drawing/Region.cs index 5d4d471f1..c5e7c1cfd 100644 --- a/src/ImageSharp.Drawing/Region.cs +++ b/src/ImageSharp.Drawing/Region.cs @@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Drawing /// Gets the bounding box that entirely surrounds this region. /// /// - /// This should always contains all possible points returned from . + /// This should always contains all possible points returned from . /// public abstract Rectangle Bounds { get; } @@ -29,7 +29,8 @@ namespace SixLabors.ImageSharp.Drawing /// /// The position along the y axis to find intersections. /// The buffer. + /// The point in the buffer to start setting offset. /// The number of intersections found. - public abstract int Scan(float y, Span buffer); + public abstract int Scan(float y, float[] buffer, int offset); } } \ No newline at end of file diff --git a/src/ImageSharp/Advanced/ImageExtensions.cs b/src/ImageSharp/Advanced/ImageExtensions.cs index 7377e6ca0..ca2e522f5 100644 --- a/src/ImageSharp/Advanced/ImageExtensions.cs +++ b/src/ImageSharp/Advanced/ImageExtensions.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Advanced /// /// Extension methods over Image{TPixel} /// - public static partial class ImageExtensions + internal static partial class ImageExtensions { /// /// Gets the representation of the pixels as an area of contiguous memory in the given pixel format. diff --git a/src/ImageSharp/Image/Image.LoadPixelData.cs b/src/ImageSharp/Image/Image.LoadPixelData.cs index 6302eb66b..d6ed4fcde 100644 --- a/src/ImageSharp/Image/Image.LoadPixelData.cs +++ b/src/ImageSharp/Image/Image.LoadPixelData.cs @@ -25,7 +25,19 @@ namespace SixLabors.ImageSharp /// The height of the final image. /// The pixel format. /// A new . - public static Image LoadPixelData(Span data, int width, int height) + public static Image LoadPixelData(TPixel[] data, int width, int height) + where TPixel : struct, IPixel + => LoadPixelData(Configuration.Default, data, width, height); + + /// + /// Create a new instance of the class from the raw data. + /// + /// The byte array containing image data. + /// The width of the final image. + /// The height of the final image. + /// The pixel format. + /// A new . + private static Image LoadPixelData(Span data, int width, int height) where TPixel : struct, IPixel => LoadPixelData(Configuration.Default, data, width, height); @@ -37,7 +49,19 @@ namespace SixLabors.ImageSharp /// The height of the final image. /// The pixel format. /// A new . - public static Image LoadPixelData(Span data, int width, int height) + public static Image LoadPixelData(byte[] data, int width, int height) + where TPixel : struct, IPixel + => LoadPixelData(Configuration.Default, data, width, height); + + /// + /// Create a new instance of the class from the given byte array in format. + /// + /// The byte array containing image data. + /// The width of the final image. + /// The height of the final image. + /// The pixel format. + /// A new . + private static Image LoadPixelData(Span data, int width, int height) where TPixel : struct, IPixel => LoadPixelData(Configuration.Default, data, width, height); @@ -50,7 +74,20 @@ namespace SixLabors.ImageSharp /// The height of the final image. /// The pixel format. /// A new . - public static Image LoadPixelData(Configuration config, Span data, int width, int height) + public static Image LoadPixelData(Configuration config, byte[] data, int width, int height) + where TPixel : struct, IPixel + => LoadPixelData(config, new Span(data).NonPortableCast(), width, height); + + /// + /// Create a new instance of the class from the given byte array in format. + /// + /// The config for the decoder. + /// The byte array containing image data. + /// The width of the final image. + /// The height of the final image. + /// The pixel format. + /// A new . + private static Image LoadPixelData(Configuration config, Span data, int width, int height) where TPixel : struct, IPixel => LoadPixelData(config, data.NonPortableCast(), width, height); @@ -63,7 +100,28 @@ namespace SixLabors.ImageSharp /// The height of the final image. /// The pixel format. /// A new . - public static Image LoadPixelData(Configuration config, Span data, int width, int height) + public static Image LoadPixelData(Configuration config, TPixel[] data, int width, int height) + where TPixel : struct, IPixel + { + int count = width * height; + Guard.MustBeGreaterThanOrEqualTo(data.Length, count, nameof(data)); + + var image = new Image(config, width, height); + SpanHelper.Copy(data, image.GetPixelSpan(), count); + + return image; + } + + /// + /// Create a new instance of the class from the raw data. + /// + /// The config for the decoder. + /// The Span containing the image Pixel data. + /// The width of the final image. + /// The height of the final image. + /// The pixel format. + /// A new . + private static Image LoadPixelData(Configuration config, Span data, int width, int height) where TPixel : struct, IPixel { int count = width * height; diff --git a/src/ImageSharp/Image/ImageExtensions.cs b/src/ImageSharp/Image/ImageExtensions.cs index 6844bdc81..afd434381 100644 --- a/src/ImageSharp/Image/ImageExtensions.cs +++ b/src/ImageSharp/Image/ImageExtensions.cs @@ -138,7 +138,7 @@ namespace SixLabors.ImageSharp /// The source image /// The buffer to save the raw pixel data to. /// Thrown if the stream is null. - public static void SavePixelData(this Image source, Span buffer) + internal static void SavePixelData(this Image source, byte[] buffer) where TPixel : struct, IPixel { Span byteBuffer = source.GetPixelSpan().AsBytes(); diff --git a/tests/ImageSharp.Tests/Drawing/FillRegionProcessorTests.cs b/tests/ImageSharp.Tests/Drawing/FillRegionProcessorTests.cs index 9540ee81b..db6c1157c 100644 --- a/tests/ImageSharp.Tests/Drawing/FillRegionProcessorTests.cs +++ b/tests/ImageSharp.Tests/Drawing/FillRegionProcessorTests.cs @@ -39,7 +39,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing Image img = new Image(1, 1); processor.Apply(img, bounds); - region.Verify(x => x.Scan(It.IsAny(), It.IsAny>()), Times.Exactly(4)); + region.Verify(x => x.Scan(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(4)); } [Fact] @@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing region.Setup(x => x.Bounds).Returns(bounds); region.Setup(x => x.MaxIntersections).Returns(10); - region.Setup(x => x.Scan(It.IsAny(), It.IsAny>())) + region.Setup(x => x.Scan(It.IsAny(), It.IsAny(), It.IsAny())) .Returns>((y, span) => { if (y < 5) diff --git a/tests/ImageSharp.Tests/Drawing/Paths/ShapeRegionTests.cs b/tests/ImageSharp.Tests/Drawing/Paths/ShapeRegionTests.cs index 41fb5643f..f5950a6ef 100644 --- a/tests/ImageSharp.Tests/Drawing/Paths/ShapeRegionTests.cs +++ b/tests/ImageSharp.Tests/Drawing/Paths/ShapeRegionTests.cs @@ -76,7 +76,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths int yToScan = 10; ShapeRegion region = new ShapeRegion(pathMock.Object); - pathMock.Setup(x => x.FindIntersections(It.IsAny(), It.IsAny(), It.IsAny>())) + pathMock.Setup(x => x.FindIntersections(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback>((s, e, b) => { Assert.Equal(yToScan, s.Y); Assert.Equal(yToScan, e.Y); @@ -84,9 +84,9 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths Assert.True(e.X > bounds.Right); }).Returns(0); - int i = region.Scan(yToScan, new float[0]); + int i = region.Scan(yToScan, new float[0], 0); - pathMock.Verify(x => x.FindIntersections(It.IsAny(), It.IsAny(), It.IsAny>()), Times.Once); + pathMock.Verify(x => x.FindIntersections(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Fact] @@ -95,7 +95,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths int yToScan = 10; ShapeRegion region = new ShapeRegion(pathMock.Object); - pathMock.Setup(x => x.FindIntersections(It.IsAny(), It.IsAny(), It.IsAny>())) + pathMock.Setup(x => x.FindIntersections(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback>((s, e, b) => { Assert.Equal(yToScan, s.Y); Assert.Equal(yToScan, e.Y); @@ -103,9 +103,9 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Paths Assert.True(e.X > bounds.Right); }).Returns(0); - int i = region.Scan(yToScan, new float[0]); + int i = region.Scan(yToScan, new float[0], 0); - pathMock.Verify(x => x.FindIntersections(It.IsAny(), It.IsAny(), It.IsAny>()), Times.Once); + pathMock.Verify(x => x.FindIntersections(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Fact]