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); } } }