diff --git a/src/ImageSharp/Common/Extensions/SimdUtils.cs b/src/ImageSharp/Common/Extensions/SimdUtils.cs index d6b2fad09..0188bc03c 100644 --- a/src/ImageSharp/Common/Extensions/SimdUtils.cs +++ b/src/ImageSharp/Common/Extensions/SimdUtils.cs @@ -15,9 +15,9 @@ namespace SixLabors.ImageSharp internal static class SimdUtils { /// - /// Indicates AVX2 architecture where both float and integer registers are of size 256 byte. + /// Gets a value indicating whether the code is being executed on AVX2 CPU where both float and integer registers are of size 256 byte. /// - public static readonly bool IsAvx2CompatibleArchitecture = Vector.Count == 8 && Vector.Count == 8; + public static bool IsAvx2CompatibleArchitecture => Vector.Count == 8 && Vector.Count == 8; internal static void GuardAvx2(string operation) { diff --git a/src/ImageSharp/Common/Tuples/Tuple8.cs b/src/ImageSharp/Common/Tuples/Tuple8.cs new file mode 100644 index 000000000..3335e6e37 --- /dev/null +++ b/src/ImageSharp/Common/Tuples/Tuple8.cs @@ -0,0 +1,98 @@ +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Common.Tuples +{ + /// + /// Contains value type tuples of 8 elements. + /// TODO: We should T4 this stuff to be DRY + /// + internal static class Tuple8 + { + /// + /// Value type tuple of 8 -s + /// + [StructLayout(LayoutKind.Explicit, Size = 8 * sizeof(uint))] + public struct OfUInt32 + { + [FieldOffset(0 * sizeof(uint))] + public uint V0; + + [FieldOffset(1 * sizeof(uint))] + public uint V1; + + [FieldOffset(2 * sizeof(uint))] + public uint V2; + + [FieldOffset(3 * sizeof(uint))] + public uint V3; + + [FieldOffset(4 * sizeof(uint))] + public uint V4; + + [FieldOffset(5 * sizeof(uint))] + public uint V5; + + [FieldOffset(6 * sizeof(uint))] + public uint V6; + + [FieldOffset(7 * sizeof(uint))] + public uint V7; + + public override string ToString() + { + return $"[{this.V0},{this.V1},{this.V2},{this.V3},{this.V4},{this.V5},{this.V6},{this.V7}]"; + } + } + + /// + /// Value type tuple of 8 -s + /// + [StructLayout(LayoutKind.Explicit, Size = 8)] + public struct OfByte + { + [FieldOffset(0)] + public byte V0; + + [FieldOffset(1)] + public byte V1; + + [FieldOffset(2)] + public byte V2; + + [FieldOffset(3)] + public byte V3; + + [FieldOffset(4)] + public byte V4; + + [FieldOffset(5)] + public byte V5; + + [FieldOffset(6)] + public byte V6; + + [FieldOffset(7)] + public byte V7; + + public override string ToString() + { + return $"[{this.V0},{this.V1},{this.V2},{this.V3},{this.V4},{this.V5},{this.V6},{this.V7}]"; + } + + /// + /// Sets the values of this tuple by casting all elements of the given tuple to . + /// + 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/Common/Tuples/Vector4Pair.cs b/src/ImageSharp/Common/Tuples/Vector4Pair.cs new file mode 100644 index 000000000..4f43c9811 --- /dev/null +++ b/src/ImageSharp/Common/Tuples/Vector4Pair.cs @@ -0,0 +1,75 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Common.Tuples +{ + /// + /// Its faster to process multiple Vector4-s together, so let's pair them! + /// On AVX2 this pair should be convertible to of ! + /// + [StructLayout(LayoutKind.Sequential)] + internal struct Vector4Pair + { + public Vector4 A; + + public Vector4 B; + + private static readonly Vector4 Scale = new Vector4(1 / 255f); + + [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; + } + + /// + /// Downscale method, specific to Jpeg color conversion. Works only if Vector{float}.Count == 4! + /// TODO: Move it somewhere else. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void RoundAndDownscalePreAvx2() + { + ref Vector a = ref Unsafe.As>(ref this.A); + a = a.FastRound(); + + ref Vector b = ref Unsafe.As>(ref this.B); + b = b.FastRound(); + + // Downscale by 1/255 + this.A *= Scale; + this.B *= Scale; + } + + /// + /// AVX2-only Downscale method, specific to Jpeg color conversion. + /// TODO: Move it somewhere else. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void RoundAndDownscaleAvx2() + { + ref Vector self = ref Unsafe.As>(ref this); + Vector v = self; + v = v.FastRound(); + + // Downscale by 1/255 + v *= new Vector(1 / 255f); + self = v; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Common/Block8x8.cs b/src/ImageSharp/Formats/Jpeg/Common/Block8x8.cs index 3f4c69c3e..96ed0a8c9 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Block8x8.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Block8x8.cs @@ -176,17 +176,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common } /// - /// Convert into + /// Convert to /// public Block8x8F AsFloatBlock() { - // TODO: Optimize this var result = default(Block8x8F); - for (int i = 0; i < Size; i++) - { - result[i] = this[i]; - } - + result.LoadFrom(ref this); return result; } diff --git a/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.CopyTo.cs b/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.CopyTo.cs new file mode 100644 index 000000000..c1c5cfcde --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.CopyTo.cs @@ -0,0 +1,146 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Formats.Jpeg.Common +{ + internal partial struct Block8x8F + { + /// + /// Copy block data into the destination color buffer pixel area with the provided horizontal and vertical. + /// + public void CopyTo(BufferArea area, int horizontalScale, int verticalScale) + { + if (horizontalScale == 1 && verticalScale == 1) + { + 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[y8 + x]; + + for (int i = 0; i < verticalScale; i++) + { + for (int j = 0; j < horizontalScale; j++) + { + area[xx + j, yy + i] = value; + } + } + } + } + } + + // [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CopyTo(BufferArea area) + { + ref byte selfBase = ref Unsafe.As(ref this); + ref byte destBase = ref Unsafe.As(ref area.GetReferenceToOrigo()); + int destStride = area.Stride * sizeof(float); + + CopyRowImpl(ref selfBase, ref destBase, destStride, 0); + CopyRowImpl(ref selfBase, ref destBase, destStride, 1); + CopyRowImpl(ref selfBase, ref destBase, destStride, 2); + CopyRowImpl(ref selfBase, ref destBase, destStride, 3); + CopyRowImpl(ref selfBase, ref destBase, destStride, 4); + CopyRowImpl(ref selfBase, ref destBase, destStride, 5); + CopyRowImpl(ref selfBase, ref destBase, destStride, 6); + CopyRowImpl(ref selfBase, ref destBase, destStride, 7); + } + + [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)); + } + + private void CopyTo2x2(BufferArea area) + { + ref float destBase = ref area.GetReferenceToOrigo(); + int destStride = area.Stride; + + this.WidenCopyImpl2x2(ref destBase, 0, destStride); + this.WidenCopyImpl2x2(ref destBase, 1, destStride); + this.WidenCopyImpl2x2(ref destBase, 2, destStride); + this.WidenCopyImpl2x2(ref destBase, 3, destStride); + this.WidenCopyImpl2x2(ref destBase, 4, destStride); + this.WidenCopyImpl2x2(ref destBase, 5, destStride); + this.WidenCopyImpl2x2(ref destBase, 6, destStride); + this.WidenCopyImpl2x2(ref destBase, 7, destStride); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WidenCopyImpl2x2(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); + + Unsafe.Add(ref destLocalOrigo, 0) = selfLeft.X; + Unsafe.Add(ref destLocalOrigo, 1) = selfLeft.X; + Unsafe.Add(ref destLocalOrigo, 2) = selfLeft.Y; + Unsafe.Add(ref destLocalOrigo, 3) = selfLeft.Y; + Unsafe.Add(ref destLocalOrigo, 4) = selfLeft.Z; + Unsafe.Add(ref destLocalOrigo, 5) = selfLeft.Z; + Unsafe.Add(ref destLocalOrigo, 6) = selfLeft.W; + Unsafe.Add(ref destLocalOrigo, 7) = selfLeft.W; + + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, 8), 0) = selfRight.X; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, 8), 1) = selfRight.X; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, 8), 2) = selfRight.Y; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, 8), 3) = selfRight.Y; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, 8), 4) = selfRight.Z; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, 8), 5) = selfRight.Z; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, 8), 6) = selfRight.W; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, 8), 7) = selfRight.W; + + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride), 0) = selfLeft.X; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride), 1) = selfLeft.X; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride), 2) = selfLeft.Y; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride), 3) = selfLeft.Y; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride), 4) = selfLeft.Z; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride), 5) = selfLeft.Z; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride), 6) = selfLeft.W; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride), 7) = selfLeft.W; + + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride + 8), 0) = selfRight.X; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride + 8), 1) = selfRight.X; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride + 8), 2) = selfRight.Y; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride + 8), 3) = selfRight.Y; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride + 8), 4) = selfRight.Z; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride + 8), 5) = selfRight.Z; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride + 8), 6) = selfRight.W; + Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride + 8), 7) = selfRight.W; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WidenCopyImpl(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; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.Generated.cs b/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.Generated.cs index 4c1b4f4d1..93e9e0388 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.Generated.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.Generated.cs @@ -96,51 +96,139 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common /// /// Level shift by +128, clip to [0, 255] /// - /// The destination block - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void NormalizeColorsInto(ref Block8x8F d) + public void NormalizeColorsInplace() { - d.V0L = Vector4.Clamp(V0L + COff4, CMin4, CMax4); - d.V0R = Vector4.Clamp(V0R + COff4, CMin4, CMax4); - d.V1L = Vector4.Clamp(V1L + COff4, CMin4, CMax4); - d.V1R = Vector4.Clamp(V1R + COff4, CMin4, CMax4); - d.V2L = Vector4.Clamp(V2L + COff4, CMin4, CMax4); - d.V2R = Vector4.Clamp(V2R + COff4, CMin4, CMax4); - d.V3L = Vector4.Clamp(V3L + COff4, CMin4, CMax4); - d.V3R = Vector4.Clamp(V3R + COff4, CMin4, CMax4); - d.V4L = Vector4.Clamp(V4L + COff4, CMin4, CMax4); - d.V4R = Vector4.Clamp(V4R + COff4, CMin4, CMax4); - d.V5L = Vector4.Clamp(V5L + COff4, CMin4, CMax4); - d.V5R = Vector4.Clamp(V5R + COff4, CMin4, CMax4); - d.V6L = Vector4.Clamp(V6L + COff4, CMin4, CMax4); - d.V6R = Vector4.Clamp(V6R + COff4, CMin4, CMax4); - d.V7L = Vector4.Clamp(V7L + COff4, CMin4, CMax4); - d.V7R = Vector4.Clamp(V7R + COff4, CMin4, CMax4); + this.V0L = Vector4.Clamp(this.V0L + COff4, CMin4, CMax4); + this.V0R = Vector4.Clamp(this.V0R + COff4, CMin4, CMax4); + this.V1L = Vector4.Clamp(this.V1L + COff4, CMin4, CMax4); + this.V1R = Vector4.Clamp(this.V1R + COff4, CMin4, CMax4); + this.V2L = Vector4.Clamp(this.V2L + COff4, CMin4, CMax4); + this.V2R = Vector4.Clamp(this.V2R + COff4, CMin4, CMax4); + this.V3L = Vector4.Clamp(this.V3L + COff4, CMin4, CMax4); + this.V3R = Vector4.Clamp(this.V3R + COff4, CMin4, CMax4); + this.V4L = Vector4.Clamp(this.V4L + COff4, CMin4, CMax4); + this.V4R = Vector4.Clamp(this.V4R + COff4, CMin4, CMax4); + this.V5L = Vector4.Clamp(this.V5L + COff4, CMin4, CMax4); + this.V5R = Vector4.Clamp(this.V5R + COff4, CMin4, CMax4); + this.V6L = Vector4.Clamp(this.V6L + COff4, CMin4, CMax4); + this.V6R = Vector4.Clamp(this.V6R + COff4, CMin4, CMax4); + this.V7L = Vector4.Clamp(this.V7L + COff4, CMin4, CMax4); + this.V7R = Vector4.Clamp(this.V7R + COff4, CMin4, CMax4); } - /// - /// Level shift by +128, clip to [0, 255] + /// AVX2-only variant for executing and in one step. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void NormalizeColorsInplace() + public void NormalizeColorsAndRoundInplaceAvx2() + { + Vector off = new Vector(128f); + Vector max = new Vector(255F); + + ref Vector row0 = ref Unsafe.As>(ref this.V0L); + row0 = NormalizeAndRound(row0, off, max); + + ref Vector row1 = ref Unsafe.As>(ref this.V1L); + row1 = NormalizeAndRound(row1, off, max); + + ref Vector row2 = ref Unsafe.As>(ref this.V2L); + row2 = NormalizeAndRound(row2, off, max); + + ref Vector row3 = ref Unsafe.As>(ref this.V3L); + row3 = NormalizeAndRound(row3, off, max); + + ref Vector row4 = ref Unsafe.As>(ref this.V4L); + row4 = NormalizeAndRound(row4, off, max); + + ref Vector row5 = ref Unsafe.As>(ref this.V5L); + row5 = NormalizeAndRound(row5, off, max); + + ref Vector row6 = ref Unsafe.As>(ref this.V6L); + row6 = NormalizeAndRound(row6, off, max); + + ref Vector row7 = ref Unsafe.As>(ref this.V7L); + row7 = NormalizeAndRound(row7, off, max); + + } + + /// + /// Fill the block from 'source' doing short -> float conversion. + /// + public void LoadFrom(ref Block8x8 source) { - this.V0L = Vector4.Clamp(V0L + COff4, CMin4, CMax4); - this.V0R = Vector4.Clamp(V0R + COff4, CMin4, CMax4); - this.V1L = Vector4.Clamp(V1L + COff4, CMin4, CMax4); - this.V1R = Vector4.Clamp(V1R + COff4, CMin4, CMax4); - this.V2L = Vector4.Clamp(V2L + COff4, CMin4, CMax4); - this.V2R = Vector4.Clamp(V2R + COff4, CMin4, CMax4); - this.V3L = Vector4.Clamp(V3L + COff4, CMin4, CMax4); - this.V3R = Vector4.Clamp(V3R + COff4, CMin4, CMax4); - this.V4L = Vector4.Clamp(V4L + COff4, CMin4, CMax4); - this.V4R = Vector4.Clamp(V4R + COff4, CMin4, CMax4); - this.V5L = Vector4.Clamp(V5L + COff4, CMin4, CMax4); - this.V5R = Vector4.Clamp(V5R + COff4, CMin4, CMax4); - this.V6L = Vector4.Clamp(V6L + COff4, CMin4, CMax4); - this.V6R = Vector4.Clamp(V6R + COff4, CMin4, CMax4); - this.V7L = Vector4.Clamp(V7L + COff4, CMin4, CMax4); - this.V7R = Vector4.Clamp(V7R + COff4, CMin4, CMax4); + ref short selfRef = ref Unsafe.As(ref source); + + this.V0L.X = Unsafe.Add(ref selfRef, 0); + this.V0L.Y = Unsafe.Add(ref selfRef, 1); + this.V0L.Z = Unsafe.Add(ref selfRef, 2); + this.V0L.W = Unsafe.Add(ref selfRef, 3); + this.V0R.X = Unsafe.Add(ref selfRef, 4); + this.V0R.Y = Unsafe.Add(ref selfRef, 5); + this.V0R.Z = Unsafe.Add(ref selfRef, 6); + this.V0R.W = Unsafe.Add(ref selfRef, 7); + + this.V1L.X = Unsafe.Add(ref selfRef, 8); + this.V1L.Y = Unsafe.Add(ref selfRef, 9); + this.V1L.Z = Unsafe.Add(ref selfRef, 10); + this.V1L.W = Unsafe.Add(ref selfRef, 11); + this.V1R.X = Unsafe.Add(ref selfRef, 12); + this.V1R.Y = Unsafe.Add(ref selfRef, 13); + this.V1R.Z = Unsafe.Add(ref selfRef, 14); + this.V1R.W = Unsafe.Add(ref selfRef, 15); + + this.V2L.X = Unsafe.Add(ref selfRef, 16); + this.V2L.Y = Unsafe.Add(ref selfRef, 17); + this.V2L.Z = Unsafe.Add(ref selfRef, 18); + this.V2L.W = Unsafe.Add(ref selfRef, 19); + this.V2R.X = Unsafe.Add(ref selfRef, 20); + this.V2R.Y = Unsafe.Add(ref selfRef, 21); + this.V2R.Z = Unsafe.Add(ref selfRef, 22); + this.V2R.W = Unsafe.Add(ref selfRef, 23); + + this.V3L.X = Unsafe.Add(ref selfRef, 24); + this.V3L.Y = Unsafe.Add(ref selfRef, 25); + this.V3L.Z = Unsafe.Add(ref selfRef, 26); + this.V3L.W = Unsafe.Add(ref selfRef, 27); + this.V3R.X = Unsafe.Add(ref selfRef, 28); + this.V3R.Y = Unsafe.Add(ref selfRef, 29); + this.V3R.Z = Unsafe.Add(ref selfRef, 30); + this.V3R.W = Unsafe.Add(ref selfRef, 31); + + this.V4L.X = Unsafe.Add(ref selfRef, 32); + this.V4L.Y = Unsafe.Add(ref selfRef, 33); + this.V4L.Z = Unsafe.Add(ref selfRef, 34); + this.V4L.W = Unsafe.Add(ref selfRef, 35); + this.V4R.X = Unsafe.Add(ref selfRef, 36); + this.V4R.Y = Unsafe.Add(ref selfRef, 37); + this.V4R.Z = Unsafe.Add(ref selfRef, 38); + this.V4R.W = Unsafe.Add(ref selfRef, 39); + + this.V5L.X = Unsafe.Add(ref selfRef, 40); + this.V5L.Y = Unsafe.Add(ref selfRef, 41); + this.V5L.Z = Unsafe.Add(ref selfRef, 42); + this.V5L.W = Unsafe.Add(ref selfRef, 43); + this.V5R.X = Unsafe.Add(ref selfRef, 44); + this.V5R.Y = Unsafe.Add(ref selfRef, 45); + this.V5R.Z = Unsafe.Add(ref selfRef, 46); + this.V5R.W = Unsafe.Add(ref selfRef, 47); + + this.V6L.X = Unsafe.Add(ref selfRef, 48); + this.V6L.Y = Unsafe.Add(ref selfRef, 49); + this.V6L.Z = Unsafe.Add(ref selfRef, 50); + this.V6L.W = Unsafe.Add(ref selfRef, 51); + this.V6R.X = Unsafe.Add(ref selfRef, 52); + this.V6R.Y = Unsafe.Add(ref selfRef, 53); + this.V6R.Z = Unsafe.Add(ref selfRef, 54); + this.V6R.W = Unsafe.Add(ref selfRef, 55); + + this.V7L.X = Unsafe.Add(ref selfRef, 56); + this.V7L.Y = Unsafe.Add(ref selfRef, 57); + this.V7L.Z = Unsafe.Add(ref selfRef, 58); + this.V7L.W = Unsafe.Add(ref selfRef, 59); + this.V7R.X = Unsafe.Add(ref selfRef, 60); + this.V7R.Y = Unsafe.Add(ref selfRef, 61); + this.V7R.Z = Unsafe.Add(ref selfRef, 62); + this.V7R.W = Unsafe.Add(ref selfRef, 63); } - } + } } diff --git a/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.Generated.tt b/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.Generated.tt index bef3e4914..dc0996b65 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.Generated.tt +++ b/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.Generated.tt @@ -61,9 +61,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common /// /// Level shift by +128, clip to [0, 255] /// - /// The destination block - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void TransformByteConvetibleColorValuesInto(ref Block8x8F d) + public void NormalizeColorsInplace() { <# @@ -74,7 +72,61 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common for (int j = 0; j < 2; j++) { char side = j == 0 ? 'L' : 'R'; - Write($"d.V{i}{side} = Vector4.Clamp(V{i}{side} + COff4, CMin4, CMax4);\r\n"); + Write($"this.V{i}{side} = Vector4.Clamp(this.V{i}{side} + COff4, CMin4, CMax4);\r\n"); + } + } + PopIndent(); + #> + } + + /// + /// AVX2-only variant for executing and in one step. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void NormalizeColorsAndRoundInplaceAvx2() + { + Vector off = new Vector(128f); + Vector max = new Vector(255F); + <# + + for (int i = 0; i < 8; i++) + { + #> + + ref Vector row<#=i#> = ref Unsafe.As>(ref this.V<#=i#>L); + row<#=i#> = NormalizeAndRound(row<#=i#>, off, max); + <# + } + #> + + } + + /// + /// Fill the block from 'source' doing short -> float conversion. + /// + public void LoadFrom(ref Block8x8 source) + { + ref short selfRef = ref Unsafe.As(ref source); + + <# + PushIndent(" "); + for (int j = 0; j < 8; j++) + { + for (int i = 0; i < 8; i++) + { + char destCoord = coordz[i % 4]; + char destSide = (i / 4) % 2 == 0 ? 'L' : 'R'; + + if(j > 0 && i == 0){ + WriteLine(""); + } + + char srcCoord = coordz[j % 4]; + char srcSide = (j / 4) % 2 == 0 ? 'L' : 'R'; + + string expression = $"this.V{j}{destSide}.{destCoord} = Unsafe.Add(ref selfRef, {j*8+i});\r\n"; + Write(expression); + } } PopIndent(); diff --git a/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.cs index 191fa50ca..2dd42288c 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.cs @@ -7,7 +7,6 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; -using SixLabors.ImageSharp.Memory; // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Formats.Jpeg.Common @@ -17,16 +16,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common /// internal partial struct Block8x8F { - // Most of the static methods of this struct are instance methods by actual semantics: they use Block8x8F* as their first parameter. - // Example: GetScalarAt() and SetScalarAt() are really just other (optimized) versions of the indexer. - // It's much cleaner, easier and safer to work with the code, if the methods with same semantics are next to each other. -#pragma warning disable SA1204 // StaticElementsMustAppearBeforeInstanceElements - - /// - /// Vector count - /// - public const int VectorCount = 16; - /// /// A number of scalar coefficients in a /// @@ -157,36 +146,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common return result; } - /// - /// Pointer-based "Indexer" (getter part) - /// - /// Block pointer - /// Index - /// The scaleVec value at the specified index - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe float GetScalarAt(Block8x8F* blockPtr, int idx) - { - GuardBlockIndex(idx); - - float* fp = (float*)blockPtr; - return fp[idx]; - } - - /// - /// Pointer-based "Indexer" (setter part) - /// - /// Block pointer - /// Index - /// Value - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void SetScalarAt(Block8x8F* blockPtr, int idx, float value) - { - GuardBlockIndex(idx); - - float* fp = (float*)blockPtr; - fp[idx] = value; - } - /// /// Fill the block with defaults (zeroes) /// @@ -242,7 +201,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common /// /// Destination [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe void CopyTo(Span dest) + public void CopyTo(Span dest) { ref byte d = ref Unsafe.As(ref dest.DangerousGetPinnableReference()); ref byte s = ref Unsafe.As(ref this); @@ -306,109 +265,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common } } - // [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void CopyTo(BufferArea area) - { - ref byte selfBase = ref Unsafe.As(ref this); - ref byte destBase = ref Unsafe.As(ref area.GetReferenceToOrigo()); - int destStride = area.Stride * sizeof(float); - - CopyRowImpl(ref selfBase, ref destBase, destStride, 0); - CopyRowImpl(ref selfBase, ref destBase, destStride, 1); - CopyRowImpl(ref selfBase, ref destBase, destStride, 2); - CopyRowImpl(ref selfBase, ref destBase, destStride, 3); - CopyRowImpl(ref selfBase, ref destBase, destStride, 4); - CopyRowImpl(ref selfBase, ref destBase, destStride, 5); - CopyRowImpl(ref selfBase, ref destBase, destStride, 6); - CopyRowImpl(ref selfBase, ref destBase, destStride, 7); - } - - public void CopyTo(BufferArea area, int horizontalScale, int verticalScale) - { - if (horizontalScale == 1 && verticalScale == 1) - { - 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[y8 + x]; - - for (int i = 0; i < verticalScale; i++) - { - for (int j = 0; j < horizontalScale; j++) - { - area[xx + j, yy + i] = value; - } - } - } - } - } - - 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]; @@ -419,26 +275,50 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common /// /// Multiply all elements of the block. /// - /// Vector to multiply by + /// The value to multiply by [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void MultiplyAllInplace(float scaleVec) - { - this.V0L *= scaleVec; - this.V0R *= scaleVec; - this.V1L *= scaleVec; - this.V1R *= scaleVec; - this.V2L *= scaleVec; - this.V2R *= scaleVec; - this.V3L *= scaleVec; - this.V3R *= scaleVec; - this.V4L *= scaleVec; - this.V4R *= scaleVec; - this.V5L *= scaleVec; - this.V5R *= scaleVec; - this.V6L *= scaleVec; - this.V6R *= scaleVec; - this.V7L *= scaleVec; - this.V7R *= scaleVec; + public void MultiplyInplace(float value) + { + this.V0L *= value; + this.V0R *= value; + this.V1L *= value; + this.V1R *= value; + this.V2L *= value; + this.V2R *= value; + this.V3L *= value; + this.V3R *= value; + this.V4L *= value; + this.V4R *= value; + this.V5L *= value; + this.V5R *= value; + this.V6L *= value; + this.V6R *= value; + this.V7L *= value; + this.V7R *= value; + } + + /// + /// Multiply all elements of the block by the corresponding elements of 'other' + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MultiplyInplace(ref Block8x8F other) + { + this.V0L *= other.V0L; + this.V0R *= other.V0R; + this.V1L *= other.V1L; + this.V1R *= other.V1R; + this.V2L *= other.V2L; + this.V2R *= other.V2R; + this.V3L *= other.V3L; + this.V3R *= other.V3R; + this.V4L *= other.V4L; + this.V4R *= other.V4R; + this.V5L *= other.V5L; + this.V5R *= other.V5R; + this.V6L *= other.V6L; + this.V6R *= other.V6R; + this.V7L *= other.V7L; + this.V7R *= other.V7R; } /// @@ -472,56 +352,32 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common /// Block pointer /// Qt pointer /// Unzig pointer - [MethodImpl(MethodImplOptions.AggressiveInlining)] + // [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void DequantizeBlock(Block8x8F* blockPtr, Block8x8F* qtPtr, int* unzigPtr) { float* b = (float*)blockPtr; float* qtp = (float*)qtPtr; - for (int zig = 0; zig < Size; zig++) + for (int qtIndex = 0; qtIndex < Size; qtIndex++) { - float* unzigPos = b + unzigPtr[zig]; + int blockIndex = unzigPtr[qtIndex]; + float* unzigPos = b + blockIndex; + float val = *unzigPos; - val *= qtp[zig]; + val *= qtp[qtIndex]; *unzigPos = val; } } /// - /// Level shift by +128, clip to [0, 255], and write to buffer. - /// - /// Color buffer - /// Stride offset - /// Temp Block pointer - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe void CopyColorsTo(Span destinationBuffer, int stride, Block8x8F* tempBlockPtr) - { - this.NormalizeColorsInto(ref *tempBlockPtr); - ref byte d = ref destinationBuffer.DangerousGetPinnableReference(); - float* src = (float*)tempBlockPtr; - for (int i = 0; i < 8; i++) - { - ref byte dRow = ref Unsafe.Add(ref d, i * stride); - Unsafe.Add(ref dRow, 0) = (byte)src[0]; - Unsafe.Add(ref dRow, 1) = (byte)src[1]; - Unsafe.Add(ref dRow, 2) = (byte)src[2]; - Unsafe.Add(ref dRow, 3) = (byte)src[3]; - Unsafe.Add(ref dRow, 4) = (byte)src[4]; - Unsafe.Add(ref dRow, 5) = (byte)src[5]; - Unsafe.Add(ref dRow, 6) = (byte)src[6]; - Unsafe.Add(ref dRow, 7) = (byte)src[7]; - src += 8; - } - } - - /// + /// Quantize 'block' into 'dest' using the 'qt' quantization table: /// Unzig the elements of block into dest, while dividing them by elements of qt and "pre-rounding" the values. /// To finish the rounding it's enough to (int)-cast these values. /// /// Source block /// Destination block /// The quantization table - /// Pointer to elements of - public static unsafe void UnzigDivRound( + /// Pointer to elements of + public static unsafe void Quantize( Block8x8F* block, Block8x8F* dest, Block8x8F* qt, @@ -610,34 +466,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common return result; } - public void RoundInplace() + /// + /// Level shift by +128, clip to [0..255], and round all the values in the block. + /// + public void NormalizeColorsAndRoundInplace() { - if (Vector.Count == 8 && Vector.Count == 8) + if (SimdUtils.IsAvx2CompatibleArchitecture) { - ref Vector row0 = ref Unsafe.As>(ref this.V0L); - row0 = row0.FastRound(); - ref Vector row1 = ref Unsafe.As>(ref this.V1L); - row1 = row1.FastRound(); - ref Vector row2 = ref Unsafe.As>(ref this.V2L); - row2 = row2.FastRound(); - ref Vector row3 = ref Unsafe.As>(ref this.V3L); - row3 = row3.FastRound(); - ref Vector row4 = ref Unsafe.As>(ref this.V4L); - row4 = row4.FastRound(); - ref Vector row5 = ref Unsafe.As>(ref this.V5L); - row5 = row5.FastRound(); - ref Vector row6 = ref Unsafe.As>(ref this.V6L); - row6 = row6.FastRound(); - ref Vector row7 = ref Unsafe.As>(ref this.V7L); - row7 = row7.FastRound(); + this.NormalizeColorsAndRoundInplaceAvx2(); } else { - this.RoundInplaceSlow(); + this.NormalizeColorsInplace(); + this.RoundInplace(); } } - private void RoundInplaceSlow() + /// + /// Rounds all values in the block. + /// + public void RoundInplace() { for (int i = 0; i < Size; i++) { @@ -663,6 +511,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common return bld.ToString(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector NormalizeAndRound(Vector row, Vector off, Vector max) + { + row += off; + row = Vector.Max(row, Vector.Zero); + row = Vector.Min(row, max); + return row.FastRound(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector4 DivideRound(Vector4 dividend, Vector4 divisor) { @@ -679,10 +536,5 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common DebugGuard.MustBeLessThan(idx, Size, nameof(idx)); DebugGuard.MustBeGreaterThanOrEqualTo(idx, 0, nameof(idx)); } - - [StructLayout(LayoutKind.Explicit, Size = 8 * sizeof(float))] - private struct Row - { - } } } diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegBlockPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegBlockPostProcessor.cs index 4c4701753..574967b6b 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegBlockPostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegBlockPostProcessor.cs @@ -11,145 +11,72 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder /// Encapsulates the implementation of processing "raw" -s into Jpeg image channels. /// [StructLayout(LayoutKind.Sequential)] - internal unsafe struct JpegBlockPostProcessor + internal struct JpegBlockPostProcessor { /// - /// The + /// Source block /// - private ComputationData data; + public Block8x8F SourceBlock; /// - /// Pointers to elements of + /// Temporal block 1 to store intermediate and/or final computation results /// - private DataPointers pointers; + public Block8x8F WorkspaceBlock1; /// - /// Initialize the instance on the stack. + /// Temporal block 2 to store intermediate and/or final computation results /// - /// The instance - public static void Init(JpegBlockPostProcessor* postProcessor) - { - postProcessor->data = ComputationData.Create(); - postProcessor->pointers = new DataPointers(&postProcessor->data); - } - - public void QuantizeAndTransform(IRawJpegData decoder, IJpegComponent component, ref Block8x8 sourceBlock) - { - this.data.SourceBlock = sourceBlock.AsFloatBlock(); - int qtIndex = component.QuantizationTableIndex; - this.data.QuantiazationTable = decoder.QuantizationTables[qtIndex]; - - Block8x8F* b = this.pointers.SourceBlock; - - Block8x8F.DequantizeBlock(b, this.pointers.QuantiazationTable, this.pointers.Unzig); - - FastFloatingPointDCT.TransformIDCT(ref *b, ref this.data.WorkspaceBlock1, ref this.data.WorkspaceBlock2); - } + public Block8x8F WorkspaceBlock2; - public void ProcessBlockColorsInto( - IRawJpegData decoder, - IJpegComponent component, - ref Block8x8 sourceBlock, - BufferArea destArea) - { - this.QuantizeAndTransform(decoder, component, ref sourceBlock); - - this.data.WorkspaceBlock1.NormalizeColorsInplace(); - - // To conform better to libjpeg we actually NEED TO loose precision here. - // This is because they store blocks as Int16 between all the operations. - // Unfortunately, we need to emulate this to be "more accurate" :( - this.data.WorkspaceBlock1.RoundInplace(); + /// + /// The quantization table as + /// + public Block8x8F DequantiazationTable; - Size divs = component.SubSamplingDivisors; - this.data.WorkspaceBlock1.CopyTo(destArea, divs.Width, divs.Height); - } + /// + /// Defines the horizontal and vertical scale we need to apply to the 8x8 sized block. + /// + private Size subSamplingDivisors; /// - /// Holds the "large" data blocks needed for computations. + /// Initializes a new instance of the struct. /// - [StructLayout(LayoutKind.Sequential)] - public struct ComputationData + public JpegBlockPostProcessor(IRawJpegData decoder, IJpegComponent component) { - /// - /// Source block - /// - public Block8x8F SourceBlock; - - /// - /// Temporal block 1 to store intermediate and/or final computation results - /// - public Block8x8F WorkspaceBlock1; - - /// - /// Temporal block 2 to store intermediate and/or final computation results - /// - public Block8x8F WorkspaceBlock2; - - /// - /// The quantization table as - /// - public Block8x8F QuantiazationTable; - - /// - /// The jpeg unzig data - /// - public UnzigData Unzig; + int qtIndex = component.QuantizationTableIndex; + this.DequantiazationTable = ZigZag.CreateDequantizationTable(ref decoder.QuantizationTables[qtIndex]); + this.subSamplingDivisors = component.SubSamplingDivisors; - /// - /// Creates and initializes a new instance - /// - /// The - public static ComputationData Create() - { - var data = default(ComputationData); - data.Unzig = UnzigData.Create(); - return data; - } + this.SourceBlock = default(Block8x8F); + this.WorkspaceBlock1 = default(Block8x8F); + this.WorkspaceBlock2 = default(Block8x8F); } /// - /// Contains pointers to the memory regions of so they can be easily passed around to pointer based utility methods of + /// Processes 'sourceBlock' producing Jpeg color channel values from spectral values: + /// - Dequantize + /// - Applying IDCT + /// - Level shift by +128, clip to [0, 255] + /// - Copy the resultin color values into 'destArea' scaling up the block by amount defined in /// - public struct DataPointers + public void ProcessBlockColorsInto( + ref Block8x8 sourceBlock, + BufferArea destArea) { - /// - /// Pointer to - /// - public Block8x8F* SourceBlock; - - /// - /// Pointer to - /// - public Block8x8F* WorkspaceBlock1; + ref Block8x8F b = ref this.SourceBlock; + b.LoadFrom(ref sourceBlock); - /// - /// Pointer to - /// - public Block8x8F* WorkspaceBlock2; + // Dequantize: + b.MultiplyInplace(ref this.DequantiazationTable); - /// - /// Pointer to - /// - public Block8x8F* QuantiazationTable; + FastFloatingPointDCT.TransformIDCT(ref b, ref this.WorkspaceBlock1, ref this.WorkspaceBlock2); - /// - /// Pointer to as int* - /// - public int* Unzig; + // To conform better to libjpeg we actually NEED TO loose precision here. + // This is because they store blocks as Int16 between all the operations. + // To be "more accurate", we need to emulate this by rounding! + this.WorkspaceBlock1.NormalizeColorsAndRoundInplace(); - /// - /// Initializes a new instance of the struct. - /// - /// Pointer to - internal DataPointers(ComputationData* dataPtr) - { - this.SourceBlock = &dataPtr->SourceBlock; - this.WorkspaceBlock1 = &dataPtr->WorkspaceBlock1; - this.WorkspaceBlock2 = &dataPtr->WorkspaceBlock2; - this.QuantiazationTable = &dataPtr->QuantiazationTable; - this.Unzig = dataPtr->Unzig.Data; - } + this.WorkspaceBlock1.CopyTo(destArea, this.subSamplingDivisors.Width, this.subSamplingDivisors.Height); } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs index 059b2e89a..b988695e1 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs @@ -1,6 +1,7 @@ using System; using System.Numerics; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Tuples; namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder { @@ -121,9 +122,25 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder tmp.MultiplyInplace(1.772F); b.AddInplace(ref tmp); - r.RoundAndDownscale(); - g.RoundAndDownscale(); - b.RoundAndDownscale(); + if (Vector.Count == 4) + { + // TODO: Find a way to properly run & test this path on AVX2 PC-s! (Have I already mentioned that Vector is terrible?) + r.RoundAndDownscalePreAvx2(); + g.RoundAndDownscalePreAvx2(); + b.RoundAndDownscalePreAvx2(); + } + else if (SimdUtils.IsAvx2CompatibleArchitecture) + { + r.RoundAndDownscaleAvx2(); + g.RoundAndDownscaleAvx2(); + b.RoundAndDownscaleAvx2(); + } + else + { + // TODO: Run fallback scalar code here + // However, no issues expected before someone implements this: https://github.com/dotnet/coreclr/issues/12007 + throw new NotImplementedException("Your CPU architecture is too modern!"); + } // 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); @@ -131,53 +148,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder } } - /// - /// Its faster to process multiple Vector4-s together - /// - private struct Vector4Pair - { - public Vector4 A; - - public Vector4 B; - - private static readonly Vector4 Scale = new Vector4(1 / 255f); - - private static readonly Vector4 Half = new Vector4(0.5f); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void RoundAndDownscale() - { - // 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; - } - } - private struct Vector4Octet { #pragma warning disable SA1132 // Do not combine fields diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegComponentPostProcessor.cs index feb5164d7..87c1431e0 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegComponentPostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegComponentPostProcessor.cs @@ -66,10 +66,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder /// /// Invoke for block rows, copy the result into . /// - public unsafe void CopyBlocksToColorBuffer() + public void CopyBlocksToColorBuffer() { - var blockPp = default(JpegBlockPostProcessor); - JpegBlockPostProcessor.Init(&blockPp); + var blockPp = new JpegBlockPostProcessor(this.ImagePostProcessor.RawJpeg, this.Component); for (int y = 0; y < this.BlockRowsPerStep; y++) { @@ -95,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder this.blockAreaSize.Width, this.blockAreaSize.Height); - blockPp.ProcessBlockColorsInto(this.ImagePostProcessor.RawJpeg, this.Component, ref block, destArea); + blockPp.ProcessBlockColorsInto(ref block, destArea); } } diff --git a/src/ImageSharp/Formats/Jpeg/Common/FastFloatingPointDCT.cs b/src/ImageSharp/Formats/Jpeg/Common/FastFloatingPointDCT.cs index 8a4f56e3d..3ee6e72c5 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/FastFloatingPointDCT.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/FastFloatingPointDCT.cs @@ -50,7 +50,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common /// Temporary block provided by the caller public static void TransformIDCT(ref Block8x8F src, ref Block8x8F dest, ref Block8x8F temp) { + // TODO: Transpose is a bottleneck now. We need full AVX support to optimize it: + // https://github.com/dotnet/corefx/issues/22940 src.TransposeInto(ref temp); + IDCT8x4_LeftPart(ref temp, ref dest); IDCT8x4_RightPart(ref temp, ref dest); @@ -60,7 +63,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common IDCT8x4_RightPart(ref temp, ref dest); // TODO: What if we leave the blocks in a scaled-by-x8 state until final color packing? - dest.MultiplyAllInplace(C_0_125); + dest.MultiplyInplace(C_0_125); } /// @@ -334,7 +337,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common FDCT8x4_LeftPart(ref temp, ref dest); FDCT8x4_RightPart(ref temp, ref dest); - dest.MultiplyAllInplace(C_0_125); + dest.MultiplyInplace(C_0_125); } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Common/UnzigData.cs b/src/ImageSharp/Formats/Jpeg/Common/ZigZag.cs similarity index 70% rename from src/ImageSharp/Formats/Jpeg/Common/UnzigData.cs rename to src/ImageSharp/Formats/Jpeg/Common/ZigZag.cs index e243938e3..18270f5ba 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/UnzigData.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/ZigZag.cs @@ -6,13 +6,12 @@ using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Jpeg.Common { /// - /// TODO: This should be simply just a ! /// Holds the Jpeg UnZig array in a value/stack type. /// Unzig maps from the zigzag ordering to the natural ordering. For example, /// unzig[3] is the column and row of the fourth element in zigzag order. The /// value is 16, which means first column (16%8 == 0) and third row (16/8 == 2). /// - internal unsafe struct UnzigData + internal unsafe struct ZigZag { /// /// Copy of in a value type @@ -33,15 +32,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common }; /// - /// Creates and fills an instance of with Jpeg unzig indices + /// Creates and fills an instance of with Jpeg unzig indices /// /// The new instance - public static UnzigData Create() + public static ZigZag CreateUnzigTable() { - UnzigData result = default(UnzigData); + ZigZag result = default(ZigZag); int* unzigPtr = result.Data; Marshal.Copy(Unzig, 0, (IntPtr)unzigPtr, 64); return result; } + + /// + /// Apply Zigging to the given quantization table, so it will be sufficient to multiply blocks for dequantizing them. + /// + public static Block8x8F CreateDequantizationTable(ref Block8x8F qt) + { + Block8x8F result = default(Block8x8F); + + for (int i = 0; i < 64; i++) + { + result[Unzig[i]] = qt[i]; + } + + return result; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigJpegScanDecoder.ComputationData.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigJpegScanDecoder.ComputationData.cs index 6252d8209..d426ed4f1 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigJpegScanDecoder.ComputationData.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigJpegScanDecoder.ComputationData.cs @@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder /// /// The jpeg unzig data /// - public UnzigData Unzig; + public ZigZag Unzig; /// /// The buffer storing the -s for each component @@ -45,7 +45,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder public static ComputationData Create() { ComputationData data = default(ComputationData); - data.Unzig = UnzigData.Create(); + data.Unzig = ZigZag.CreateUnzigTable(); return data; } } diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/JpegEncoderCore.cs index 984fb828c..2deb3f62d 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/JpegEncoderCore.cs @@ -455,7 +455,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable; Block8x8F onStackChrominanceQuantTable = this.chrominanceQuantTable; - UnzigData unzig = UnzigData.Create(); + ZigZag unzig = ZigZag.CreateUnzigTable(); // ReSharper disable once InconsistentNaming int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; @@ -564,7 +564,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort { FastFloatingPointDCT.TransformFDCT(ref *src, ref *tempDest1, ref *tempDest2); - Block8x8F.UnzigDivRound(tempDest1, tempDest2, quant, unzigPtr); + Block8x8F.Quantize(tempDest1, tempDest2, quant, unzigPtr); float* unziggedDestPtr = (float*)tempDest2; int dc = (int)unziggedDestPtr[0]; @@ -912,7 +912,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable; Block8x8F onStackChrominanceQuantTable = this.chrominanceQuantTable; - UnzigData unzig = UnzigData.Create(); + ZigZag unzig = ZigZag.CreateUnzigTable(); // ReSharper disable once InconsistentNaming int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; diff --git a/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs b/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs index e5f2fd5e7..15a794144 100644 --- a/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs +++ b/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs @@ -8,6 +8,8 @@ namespace SixLabors.ImageSharp.Tests.Common using System.Linq; using System.Runtime.CompilerServices; + using SixLabors.ImageSharp.Common.Tuples; + using Xunit.Abstractions; using Xunit.Sdk; @@ -243,15 +245,15 @@ namespace SixLabors.ImageSharp.Tests.Common x = (x * scale) + magick; - SimdUtils.Octet.OfUInt32 ii = default(SimdUtils.Octet.OfUInt32); + Tuple8.OfUInt32 ii = default(Tuple8.OfUInt32); - ref Vector iiRef = ref Unsafe.As>(ref ii); + ref Vector iiRef = ref Unsafe.As>(ref ii); iiRef = x; - //SimdUtils.Octet.OfUInt32 ii = Unsafe.As, SimdUtils.Octet.OfUInt32>(ref x); + //Tuple8.OfUInt32 ii = Unsafe.As, Tuple8.OfUInt32>(ref x); - ref SimdUtils.Octet.OfByte d = ref dest.NonPortableCast()[0]; + ref Tuple8.OfByte d = ref dest.NonPortableCast()[0]; d.LoadFrom(ref ii); this.Output.WriteLine(ii.ToString()); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs index 52c38bee8..bf5507676 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs @@ -32,32 +32,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { } - [Fact] - public void Indexer() + private bool SkipOnNonAvx2Runner() { - float sum = 0; - this.Measure( - Times, - () => - { - var block = new Block8x8F(); - - for (int i = 0; i < Block8x8F.Size; i++) - { - block[i] = i; - } - - sum = 0; - for (int i = 0; i < Block8x8F.Size; i++) - { - sum += block[i]; - } - }); - Assert.Equal(sum, 64f * 63f * 0.5f); + if (!SimdUtils.IsAvx2CompatibleArchitecture) + { + this.Output.WriteLine("AVX2 not supported, skipping!"); + return true; + } + return false; } [Fact] - public unsafe void Indexer_GetScalarAt_SetScalarAt() + public void Indexer() { float sum = 0; this.Measure( @@ -68,18 +54,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg for (int i = 0; i < Block8x8F.Size; i++) { - Block8x8F.SetScalarAt(&block, i, i); + block[i] = i; } sum = 0; for (int i = 0; i < Block8x8F.Size; i++) { - sum += Block8x8F.GetScalarAt(&block, i); + sum += block[i]; } }); Assert.Equal(sum, 64f * 63f * 0.5f); } - + [Fact] public void Indexer_ReferenceBenchmarkWithArray() { @@ -222,33 +208,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg this.Output.WriteLine($"TranposeInto_PinningImpl_Benchmark finished in {sw.ElapsedMilliseconds} ms"); } - [Fact] - public unsafe void CopyColorsTo() - { - float[] data = Create8x8FloatData(); - var block = new Block8x8F(); - block.LoadFrom(data); - block.MultiplyAllInplace(5); - - int stride = 256; - int height = 42; - int offset = height * 10 + 20; - - byte[] colorsExpected = new byte[stride * height]; - byte[] colorsActual = new byte[stride * height]; - - var temp = new Block8x8F(); - - ReferenceImplementations.CopyColorsTo(ref block, new Span(colorsExpected, offset), stride); - - block.CopyColorsTo(new Span(colorsActual, offset), stride, &temp); - - // Output.WriteLine("******* EXPECTED: *********"); - // PrintLinearData(colorsExpected); - // Output.WriteLine("******** ACTUAL: **********"); - Assert.Equal(colorsExpected, colorsActual); - } - private static float[] Create8x8ColorCropTestData() { float[] result = new float[64]; @@ -263,30 +222,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg return result; } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void NormalizeColors(bool inplace) + [Fact] + public void NormalizeColors() { var block = default(Block8x8F); float[] input = Create8x8ColorCropTestData(); block.LoadFrom(input); this.Output.WriteLine("Input:"); this.PrintLinearData(input); - - var dest = default(Block8x8F); - if (inplace) - { - dest = block; - dest.NormalizeColorsInplace(); - } - else - { - block.NormalizeColorsInto(ref dest); - } + Block8x8F dest = block; + dest.NormalizeColorsInplace(); - float[] array = new float[64]; dest.CopyTo(array); this.Output.WriteLine("Result:"); @@ -297,11 +244,35 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg } } + [Theory] + [InlineData(1)] + [InlineData(2)] + public void NormalizeColorsAndRoundAvx2(int seed) + { + if (this.SkipOnNonAvx2Runner()) + { + return; + } + + Block8x8F source = CreateRandomFloatBlock(-200, 200, seed); + + Block8x8F expected = source; + expected.NormalizeColorsInplace(); + expected.RoundInplace(); + + Block8x8F actual = source; + actual.NormalizeColorsAndRoundInplaceAvx2(); + + this.Output.WriteLine(expected.ToString()); + this.Output.WriteLine(actual.ToString()); + this.CompareBlocks(expected, actual, 0); + } + [Theory] [InlineData(1)] [InlineData(2)] - public unsafe void UnzigDivRound(int seed) + public unsafe void Quantize(int seed) { var block = new Block8x8F(); block.LoadFrom(Create8x8RoundedRandomFloatData(-2000, 2000, seed)); @@ -309,14 +280,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg var qt = new Block8x8F(); qt.LoadFrom(Create8x8RoundedRandomFloatData(-2000, 2000, seed)); - var unzig = UnzigData.Create(); + var unzig = ZigZag.CreateUnzigTable(); int* expectedResults = stackalloc int[Block8x8F.Size]; - ReferenceImplementations.UnZigDivRoundRational(&block, expectedResults, &qt, unzig.Data); + ReferenceImplementations.QuantizeRational(&block, expectedResults, &qt, unzig.Data); var actualResults = default(Block8x8F); - Block8x8F.UnzigDivRound(&block, &actualResults, &qt, unzig.Data); + Block8x8F.Quantize(&block, &actualResults, &qt, unzig.Data); for (int i = 0; i < Block8x8F.Size; i++) { @@ -352,7 +323,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [InlineData(1)] [InlineData(2)] [InlineData(3)] - public void RoundInplace(int seed) + public void RoundInplaceSlow(int seed) { Block8x8F s = CreateRandomFloatBlock(-500, 500, seed); @@ -370,5 +341,77 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.Equal(expected, actual); } } + + [Fact] + public void MultiplyInplace_ByOtherBlock() + { + Block8x8F original = CreateRandomFloatBlock(-500, 500, 42); + Block8x8F m = CreateRandomFloatBlock(-500, 500, 42); + + Block8x8F actual = original; + + actual.MultiplyInplace(ref m); + + for (int i = 0; i < Block8x8F.Size; i++) + { + Assert.Equal(original[i]*m[i], actual[i]); + } + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public unsafe void DequantizeBlock(int seed) + { + Block8x8F original = CreateRandomFloatBlock(-500, 500, seed); + Block8x8F qt = CreateRandomFloatBlock(0, 10, seed + 42); + + var unzig = ZigZag.CreateUnzigTable(); + + Block8x8F expected = original; + Block8x8F actual = original; + + ReferenceImplementations.DequantizeBlock(&expected, &qt, unzig.Data); + Block8x8F.DequantizeBlock(&actual, &qt, unzig.Data); + + this.CompareBlocks(expected, actual, 0); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public unsafe void ZigZag_CreateDequantizationTable_MultiplicationShouldQuantize(int seed) + { + Block8x8F original = CreateRandomFloatBlock(-500, 500, seed); + Block8x8F qt = CreateRandomFloatBlock(0, 10, seed + 42); + + var unzig = ZigZag.CreateUnzigTable(); + Block8x8F zigQt = ZigZag.CreateDequantizationTable(ref qt); + + Block8x8F expected = original; + Block8x8F actual = original; + + ReferenceImplementations.DequantizeBlock(&expected, &qt, unzig.Data); + + actual.MultiplyInplace(ref zigQt); + + this.CompareBlocks(expected, actual, 0); + } + + [Fact] + public void MultiplyInplace_ByScalar() + { + Block8x8F original = CreateRandomFloatBlock(-500, 500); + + Block8x8F actual = original; + actual.MultiplyInplace(42f); + + for (int i = 0; i < 64; i++) + { + Assert.Equal(original[i]*42f, actual[i]); + } + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs index 706fa1e20..0197dd917 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { public class JpegColorConverterTests { - private const float Precision = 1/255f; + private const float Precision = 0.1f / 255; public static readonly TheoryData CommonConversionData = new TheoryData diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index d8d24224b..e26ab2cc9 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -9,6 +9,7 @@ using System; namespace SixLabors.ImageSharp.Tests.Formats.Jpg { + using System.Collections.Generic; using System.IO; using System.Linq; @@ -24,6 +25,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using Xunit; using Xunit.Abstractions; + // TODO: Scatter test cases into multiple test classes public class JpegDecoderTests { public static string[] BaselineTestJpegs = @@ -50,16 +52,43 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImages.Jpeg.Issues.MissingFF00ProgressiveGirl159, }; + private static readonly Dictionary CustomToleranceValues = new Dictionary + { + // Baseline: + [TestImages.Jpeg.Baseline.Calliphora] = 0.00002f / 100, + [TestImages.Jpeg.Baseline.Bad.ExifUndefType] = 0.011f / 100, + [TestImages.Jpeg.Baseline.Bad.BadEOF] = 0.38f / 100, + [TestImages.Jpeg.Baseline.Testorig420] = 0.38f / 100, + + // Progressive: + [TestImages.Jpeg.Issues.MissingFF00ProgressiveGirl159] = 0.34f / 100, + [TestImages.Jpeg.Issues.BadCoeffsProgressive178] = 0.38f / 100, + [TestImages.Jpeg.Progressive.Bad.BadEOF] = 0.3f / 100, + [TestImages.Jpeg.Progressive.Festzug] = 0.02f / 100, + [TestImages.Jpeg.Progressive.Fb] = 0.16f / 100, + [TestImages.Jpeg.Progressive.Progress] = 0.31f / 100, + }; + public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.RgbaVector; - // TODO: We should make this comparer less tolerant ... - private static readonly ImageComparer VeryTolerantJpegComparer = - ImageComparer.Tolerant(0.005f, perPixelManhattanThreshold: 4); + private const float BaselineTolerance_Orig = 0.001f / 100; + private const float BaselineTolerance_PdfJs = 0.005f; - // BUG: PDF.js output is wrong on spectral level! - private static readonly ImageComparer PdfJsProgressiveComparer = - ImageComparer.Tolerant(0.015f, perPixelManhattanThreshold: 4); + private const float ProgressiveTolerance_Orig = 0.2f / 100; + private const float ProgressiveTolerance_PdfJs = 1.5f / 100; // PDF.js Progressive output is wrong on spectral level! + private ImageComparer GetImageComparerForOrigDecoder(TestImageProvider provider) + where TPixel : struct, IPixel + { + string file = provider.SourceFileOrDescription; + + if (!CustomToleranceValues.TryGetValue(file, out float tolerance)) + { + tolerance = file.ToLower().Contains("baseline") ? BaselineTolerance_Orig : ProgressiveTolerance_Orig; + } + + return ImageComparer.Tolerant(tolerance); + } public JpegDecoderTests(ITestOutputHelper output) { @@ -71,7 +100,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg private static IImageDecoder OrigJpegDecoder => new OrigJpegDecoder(); private static IImageDecoder PdfJsJpegDecoder => new PdfJsJpegDecoder(); - + [Fact] public void ParseStream_BasicPropertiesAreCorrect1_PdfJs() { @@ -84,50 +113,56 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg VerifyJpeg.VerifyComponentSizes3(decoder.Frame.Components, 43, 61, 22, 31, 22, 31); } } - + public const string DecodeBaselineJpegOutputName = "DecodeBaselineJpeg"; + [Theory] - [WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)] - public void DecodeBaselineJpeg_PdfJs(TestImageProvider provider) + [WithFile(TestImages.Jpeg.Baseline.Calliphora, CommonNonDefaultPixelTypes, false)] + [WithFile(TestImages.Jpeg.Baseline.Calliphora, CommonNonDefaultPixelTypes, true)] + public void JpegDecoder_IsNotBoundToSinglePixelType(TestImageProvider provider, bool useOldDecoder) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(PdfJsJpegDecoder)) + IImageDecoder decoder = useOldDecoder ? OrigJpegDecoder : PdfJsJpegDecoder; + using (Image image = provider.GetImage(decoder)) { image.DebugSave(provider); provider.Utility.TestName = DecodeBaselineJpegOutputName; - image.CompareToReferenceOutput(provider, VeryTolerantJpegComparer, appendPixelTypeToFileName: false); + image.CompareToReferenceOutput(provider, ImageComparer.Tolerant(BaselineTolerance_PdfJs), appendPixelTypeToFileName: false); } } [Theory] - [WithFile(TestImages.Jpeg.Baseline.Calliphora, CommonNonDefaultPixelTypes, false)] - [WithFile(TestImages.Jpeg.Baseline.Calliphora, CommonNonDefaultPixelTypes, true)] - public void JpegDecoder_IsNotBoundToSinglePixelType(TestImageProvider provider, bool useOldDecoder) + [WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)] + public void DecodeBaselineJpeg_Orig(TestImageProvider provider) where TPixel : struct, IPixel { - IImageDecoder decoder = useOldDecoder ? OrigJpegDecoder : PdfJsJpegDecoder; - using (Image image = provider.GetImage(decoder)) + using (Image image = provider.GetImage(OrigJpegDecoder)) { image.DebugSave(provider); - provider.Utility.TestName = DecodeBaselineJpegOutputName; - image.CompareToReferenceOutput(provider, VeryTolerantJpegComparer, appendPixelTypeToFileName: false); + image.CompareToReferenceOutput( + provider, + this.GetImageComparerForOrigDecoder(provider), + appendPixelTypeToFileName: false); } } - + [Theory] [WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)] - public void DecodeBaselineJpeg_Orig(TestImageProvider provider) + public void DecodeBaselineJpeg_PdfJs(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(OrigJpegDecoder)) + using (Image image = provider.GetImage(PdfJsJpegDecoder)) { image.DebugSave(provider); provider.Utility.TestName = DecodeBaselineJpegOutputName; - image.CompareToReferenceOutput(provider, VeryTolerantJpegComparer, appendPixelTypeToFileName: false); + image.CompareToReferenceOutput( + provider, + ImageComparer.Tolerant(BaselineTolerance_PdfJs), + appendPixelTypeToFileName: false); } } @@ -144,37 +179,43 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Theory] [WithFileCollection(nameof(ProgressiveTestJpegs), PixelTypes.Rgba32)] - public void DecodeProgressiveJpeg_PdfJs(TestImageProvider provider) + public void DecodeProgressiveJpeg_Orig(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(PdfJsJpegDecoder)) + using (Image image = provider.GetImage(OrigJpegDecoder)) { image.DebugSave(provider); provider.Utility.TestName = DecodeProgressiveJpegOutputName; - image.CompareToReferenceOutput(provider, PdfJsProgressiveComparer, appendPixelTypeToFileName: false); + image.CompareToReferenceOutput( + provider, + this.GetImageComparerForOrigDecoder(provider), + appendPixelTypeToFileName: false); } } [Theory] [WithFileCollection(nameof(ProgressiveTestJpegs), PixelTypes.Rgba32)] - public void DecodeProgressiveJpeg_Orig(TestImageProvider provider) + public void DecodeProgressiveJpeg_PdfJs(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(OrigJpegDecoder)) + using (Image image = provider.GetImage(PdfJsJpegDecoder)) { image.DebugSave(provider); provider.Utility.TestName = DecodeProgressiveJpegOutputName; - image.CompareToReferenceOutput(provider, VeryTolerantJpegComparer, appendPixelTypeToFileName: false); + image.CompareToReferenceOutput( + provider, + ImageComparer.Tolerant(ProgressiveTolerance_PdfJs), + appendPixelTypeToFileName: false); } } - - private float GetDifferenceInPercents(Image image, TestImageProvider provider) + + private string GetDifferenceInPercentageString(Image image, TestImageProvider provider) where TPixel : struct, IPixel { var reportingComparer = ImageComparer.Tolerant(0, 0); - + ImageSimilarityReport report = image.GetReferenceOutputSimilarityReports( provider, reportingComparer, @@ -183,10 +224,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg if (report != null && report.TotalNormalizedDifference.HasValue) { - return report.TotalNormalizedDifference.Value * 100; + return report.DifferencePercentageString; } - return 0; + return "0%"; } private void CompareJpegDecodersImpl(TestImageProvider provider, string testName) @@ -202,14 +243,15 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using (Image image = provider.GetImage(OrigJpegDecoder)) { - double d = this.GetDifferenceInPercents(image, provider); - this.Output.WriteLine($"Difference using ORIGINAL decoder: {d:0.0000}%"); + string d = this.GetDifferenceInPercentageString(image, provider); + + this.Output.WriteLine($"Difference using ORIGINAL decoder: {d}"); } using (Image image = provider.GetImage(PdfJsJpegDecoder)) { - double d = this.GetDifferenceInPercents(image, provider); - this.Output.WriteLine($"Difference using PDFJS decoder: {d:0.0000}%"); + string d = this.GetDifferenceInPercentageString(image, provider); + this.Output.WriteLine($"Difference using PDFJS decoder: {d}"); } } @@ -228,7 +270,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { this.CompareJpegDecodersImpl(provider, DecodeProgressiveJpegOutputName); } - + [Theory] [WithSolidFilledImages(16, 16, 255, 0, 0, PixelTypes.Rgba32, JpegSubsample.Ratio420, 75)] [WithSolidFilledImages(16, 16, 255, 0, 0, PixelTypes.Rgba32, JpegSubsample.Ratio420, 100)] @@ -256,7 +298,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg var mirror = Image.Load(data, OrigJpegDecoder); mirror.DebugSave(provider, $"_{subsample}_Q{quality}"); } - + [Fact] public void Decoder_Reads_Correct_Resolution_From_Jfif() { @@ -314,7 +356,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // into "\tests\Images\ActualOutput\JpegDecoderTests\" //[Theory] //[WithFile(TestImages.Jpeg.Progressive.Progress, PixelTypes.Rgba32, "PdfJsOriginal_progress.png")] - public void ValidateProgressivePdfJsOutput(TestImageProvider provider, + public void ValidateProgressivePdfJsOutput(TestImageProvider provider, string pdfJsOriginalResultImage) where TPixel : struct, IPixel { @@ -335,7 +377,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { ImageSimilarityReport originalReport = comparer.CompareImagesOrFrames(expectedImage, pdfJsOriginalResult); ImageSimilarityReport portReport = comparer.CompareImagesOrFrames(expectedImage, pdfJsPortResult); - + this.Output.WriteLine($"Difference for PDF.js ORIGINAL: {originalReport.DifferencePercentageString}"); this.Output.WriteLine($"Difference for PORT: {portReport.DifferencePercentageString}"); } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs index a228bc236..baa31f674 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs @@ -34,8 +34,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImages.Jpeg.Baseline.Jpeg444, }; - // [Theory] // Benchmark, enable manually - // [MemberData(nameof(DecodeJpegData))] + //[Theory] // Benchmark, enable manually + //[MemberData(nameof(DecodeJpegData))] public void DecodeJpeg_Original(string fileName) { this.DecodeJpegBenchmarkImpl(fileName, new OrigJpegDecoder()); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/ReferenceImplementations.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/ReferenceImplementations.cs index c8240bf08..92ead8164 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/ReferenceImplementations.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/ReferenceImplementations.cs @@ -19,6 +19,21 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils /// internal static partial class ReferenceImplementations { + public static unsafe void DequantizeBlock(Block8x8F* blockPtr, Block8x8F* qtPtr, int* unzigPtr) + { + float* b = (float*)blockPtr; + float* qtp = (float*)qtPtr; + for (int qtIndex = 0; qtIndex < Block8x8F.Size; qtIndex++) + { + int i = unzigPtr[qtIndex]; + float* unzigPos = b + i; + + float val = *unzigPos; + val *= qtp[qtIndex]; + *unzigPos = val; + } + } + /// /// Transpose 8x8 block stored linearly in a (inplace) /// @@ -93,14 +108,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils } /// - /// Reference implementation to test . + /// Reference implementation to test . /// Rounding is done used an integer-based algorithm defined in . /// /// The input block /// The destination block of integers /// The quantization table - /// Pointer to - public static unsafe void UnZigDivRoundRational(Block8x8F* src, int* dest, Block8x8F* qt, int* unzigPtr) + /// Pointer to + public static unsafe void QuantizeRational(Block8x8F* src, int* dest, Block8x8F* qt, int* unzigPtr) { float* s = (float*)src; float* q = (float*)qt; diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 820ca8356..ed643c820 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { SizeF newSize = image.Size() * ratio; image.Mutate(x => x.Resize((Size)newSize, sampler, false)); - string details = $"{name}-{ratio}"; + string details = $"{name}-{ratio.ToString(System.Globalization.CultureInfo.InvariantCulture)}"; image.DebugSave(provider, details); image.CompareToReferenceOutput(provider, details); diff --git a/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs index 6b3f6ccd2..70d4df273 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs @@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests { float d = x - y; - return d > -this.Eps && d < this.Eps; + return d >= -this.Eps && d <= this.Eps; } public int GetHashCode(float obj) diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageSimilarityReport.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageSimilarityReport.cs index 78e390bbd..9501a6c88 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageSimilarityReport.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageSimilarityReport.cs @@ -26,9 +26,24 @@ // TODO: This should not be a nullable value! public float? TotalNormalizedDifference { get; } - public string DifferencePercentageString => this.TotalNormalizedDifference.HasValue - ? $"{this.TotalNormalizedDifference.Value * 100:0.0000}%" - : "?"; + public string DifferencePercentageString + { + get + { + if (!this.TotalNormalizedDifference.HasValue) + { + return "?"; + } + else if (this.TotalNormalizedDifference == 0) + { + return "0%"; + } + else + { + return $"{this.TotalNormalizedDifference.Value * 100:0.0000}%"; + } + } + } public PixelDifference[] Differences { get; } diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs index 8ff532b2f..92332eba0 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs @@ -105,15 +105,20 @@ namespace SixLabors.ImageSharp.Tests private static readonly ConcurrentDictionary> cache = new ConcurrentDictionary>(); - public FileProvider(string filePath) + // Needed for deserialization! + // ReSharper disable once UnusedMember.Local + public FileProvider() { - this.FilePath = filePath; } - public FileProvider() + public FileProvider(string filePath) { + this.FilePath = filePath; } - + + /// + /// Gets the file path relative to the "~/tests/images" folder + /// public string FilePath { get; private set; } public override string SourceFileOrDescription => this.FilePath; diff --git a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs index f4faa9456..7fd7a59a9 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs @@ -89,6 +89,8 @@ namespace SixLabors.ImageSharp.Tests return $"{this.GetTestOutputDir()}/{this.TestName}{pixName}{fn}{details}{extension}"; } + private static string Inv(FormattableString formattable) => System.FormattableString.Invariant(formattable); + /// /// Gets the recommended file name for the output of the test /// @@ -111,13 +113,17 @@ namespace SixLabors.ImageSharp.Tests TypeInfo info = type.GetTypeInfo(); if (info.IsPrimitive || info.IsEnum || type == typeof(decimal)) { - detailsString = testOutputDetails.ToString(); + detailsString = Inv($"{testOutputDetails}"); } else { IEnumerable properties = testOutputDetails.GetType().GetRuntimeProperties(); - detailsString = String.Join("_", properties.ToDictionary(x => x.Name, x => x.GetValue(testOutputDetails)).Select(x => $"{x.Key}-{x.Value}")); + detailsString = string.Join( + "_", + properties.ToDictionary(x => x.Name, x => x.GetValue(testOutputDetails)) + .Select(x => Inv($"{x.Key}-{x.Value}")) + ); } } return this.GetTestOutputFileNameImpl(extension, detailsString, appendPixelTypeToFileName);