diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanLut.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanLut.cs index bc6c8c6cc..ec77bf87d 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanLut.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanLut.cs @@ -5,8 +5,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { /// /// A compiled look-up table representation of a huffmanSpec. - /// Each value maps to a uint32 of which the 8 most significant bits hold the - /// codeword size in bits and the 24 least significant bits hold the codeword. + /// Each value maps to a int32 of which the 24 most significant bits hold the + /// codeword in bits and the 8 least significant bits hold the codeword size. /// The maximum codeword size is 16 bits. /// internal readonly struct HuffmanLut @@ -51,10 +51,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder for (int i = 0; i < spec.Count.Length; i++) { - int bits = (i + 1) << 24; + int len = i + 1; for (int j = 0; j < spec.Count[i]; j++) { - this.Values[spec.Values[k]] = bits | code; + this.Values[spec.Values[k]] = len | (code << 8); code++; k++; } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs index 860a9c323..331da275c 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs @@ -3,6 +3,10 @@ using System.IO; using System.Runtime.CompilerServices; +#if SUPPORTS_RUNTIME_INTRINSICS +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif using System.Threading; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -11,6 +15,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { internal class HuffmanScanEncoder { + /// + /// Compiled huffman tree to encode given values. + /// + /// Yields codewords by index consisting of [run length | bitsize]. + private HuffmanLut[] huffmanTables; + /// /// Number of bytes cached before being written to target stream via Stream.Write(byte[], offest, count). /// @@ -64,6 +74,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder public void Encode444(Image pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { + this.huffmanTables = HuffmanLut.TheHuffmanLut; + var unzig = ZigZag.CreateUnzigTable(); // ReSharper disable once InconsistentNaming @@ -122,6 +134,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder public void Encode420(Image pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { + this.huffmanTables = HuffmanLut.TheHuffmanLut; + var unzig = ZigZag.CreateUnzigTable(); // ReSharper disable once InconsistentNaming @@ -187,6 +201,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder public void EncodeGrayscale(Image pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { + this.huffmanTables = HuffmanLut.TheHuffmanLut; + var unzig = ZigZag.CreateUnzigTable(); // ReSharper disable once InconsistentNaming @@ -243,16 +259,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder Block8x8F.Quantize(ref refTemp1, ref refTemp2, ref quant, ref unZig); - int dc = (int)refTemp2[0]; - // Emit the DC delta. - this.EmitHuffRLE((2 * (int)index) + 0, 0, dc - prevDC); + int dc = (int)refTemp2[0]; + this.EmitDirectCurrentTerm(this.huffmanTables[2 * (int)index].Values, dc - prevDC); // Emit the AC components. - int h = (2 * (int)index) + 1; - int runLength = 0; + int[] acHuffTable = this.huffmanTables[(2 * (int)index) + 1].Values; - for (int zig = 1; zig < Block8x8F.Size; zig++) + int runLength = 0; + int lastValuableIndex = GetLastValuableElementIndex(ref refTemp2); + for (int zig = 1; zig <= lastValuableIndex; zig++) { int ac = (int)refTemp2[zig]; @@ -264,18 +280,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { while (runLength > 15) { - this.EmitHuff(h, 0xf0); + this.EmitHuff(acHuffTable, 0xf0); runLength -= 16; } - this.EmitHuffRLE(h, runLength, ac); + this.EmitHuffRLE(acHuffTable, runLength, ac); runLength = 0; } } - if (runLength > 0) + // if mcu block contains trailing zeros - we must write end of block (EOB) value indicating that current block is over + // this can be done for any number of trailing zeros, even when all 63 ac values are zero + // (Block8x8F.Size - 1) == 63 - last index of the mcu elements + if (lastValuableIndex != Block8x8F.Size - 1) { - this.EmitHuff(h, 0x00); + this.EmitHuff(acHuffTable, 0x00); } return dc; @@ -306,6 +325,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder { byte b = (byte)(bits >> 24); this.emitBuffer[this.emitLen++] = b; + + // Adding stuff byte + // This is because by JPEG standard scan data can contain JPEG markers (indicated by the 0xFF byte, followed by a non-zero byte) + // Considering this every 0xFF byte must be followed by 0x00 padding byte to signal that this is not a marker if (b == byte.MaxValue) { this.emitBuffer[this.emitLen++] = byte.MinValue; @@ -334,23 +357,43 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// /// Emits the given value with the given Huffman encoder. /// - /// The index of the Huffman encoder + /// Compiled Huffman spec values. /// The value to encode. [MethodImpl(InliningOptions.ShortMethod)] - private void EmitHuff(int index, int value) + private void EmitHuff(int[] table, int value) { - int x = HuffmanLut.TheHuffmanLut[index].Values[value]; - this.Emit(x & ((1 << 24) - 1), x >> 24); + int x = table[value]; + this.Emit(x >> 8, x & 0xff); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private void EmitDirectCurrentTerm(int[] table, int value) + { + int a = value; + int b = value; + if (a < 0) + { + a = -value; + b = value - 1; + } + + int bt = GetHuffmanEncodingLength((uint)a); + + this.EmitHuff(table, bt); + if (bt > 0) + { + this.Emit(b & ((1 << bt) - 1), bt); + } } /// /// Emits a run of runLength copies of value encoded with the given Huffman encoder. /// - /// The index of the Huffman encoder + /// Compiled Huffman spec values. /// The number of copies to encode. /// The value to encode. [MethodImpl(InliningOptions.ShortMethod)] - private void EmitHuffRLE(int index, int runLength, int value) + private void EmitHuffRLE(int[] table, int runLength, int value) { int a = value; int b = value; @@ -362,11 +405,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder int bt = GetHuffmanEncodingLength((uint)a); - this.EmitHuff(index, (runLength << 4) | bt); - if (bt > 0) - { - this.Emit(b & ((1 << bt) - 1), bt); - } + this.EmitHuff(table, (runLength << 4) | bt); + this.Emit(b & ((1 << bt) - 1), bt); } /// @@ -380,11 +420,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder if (padBitsCount != 0) { this.Emit((1 << padBitsCount) - 1, padBitsCount); - } - - // flush remaining bytes - if (this.emitLen != 0) - { this.target.Write(this.emitBuffer, 0, this.emitLen); } } @@ -393,11 +428,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// Calculates how many minimum bits needed to store given value for Huffman jpeg encoding. /// /// - /// This method returns 0 for input value 0. This is done specificaly for huffman encoding + /// This is an internal operation supposed to be used only in class for jpeg encoding. /// /// The value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetHuffmanEncodingLength(uint value) + [MethodImpl(InliningOptions.ShortMethod)] + internal static int GetHuffmanEncodingLength(uint value) { DebugGuard.IsTrue(value <= (1 << 16), "Huffman encoder is supposed to encode a value of 16bit size max"); #if SUPPORTS_BITOPERATIONS @@ -423,5 +458,67 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder return Numerics.Log2(value << 1); #endif } + + /// + /// Returns index of the last non-zero element in given mcu block. + /// If all values of the mcu block are zero, this method might return different results depending on the runtime and hardware support. + /// This is jpeg mcu specific code, mcu[0] stores a dc value which will be encoded outside of the loop. + /// This method is guaranteed to return either -1 or 0 if all elements are zero. + /// + /// + /// This is an internal operation supposed to be used only in class for jpeg encoding. + /// + /// Mcu block. + /// Index of the last non-zero element. + [MethodImpl(InliningOptions.ShortMethod)] + internal static int GetLastValuableElementIndex(ref Block8x8F mcu) + { +#if SUPPORTS_RUNTIME_INTRINSICS + if (Avx2.IsSupported) + { + const int equalityMask = unchecked((int)0b1111_1111_1111_1111_1111_1111_1111_1111); + + Vector256 zero8 = Vector256.Zero; + + ref Vector256 mcuStride = ref mcu.V0; + + for (int i = 7; i >= 0; i--) + { + int areEqual = Avx2.MoveMask(Avx2.CompareEqual(Avx.ConvertToVector256Int32(Unsafe.Add(ref mcuStride, i)), zero8).AsByte()); + + // we do not know for sure if this stride contain all non-zero elements or if it has some trailing zeros + if (areEqual != equalityMask) + { + // last index in the stride, we go from the end to the start of the stride + int startIndex = i * 8; + int index = startIndex + 7; + ref float elemRef = ref Unsafe.As(ref mcu); + while (index >= startIndex && (int)Unsafe.Add(ref elemRef, index) == 0) + { + index--; + } + + // this implementation will return -1 if all ac components are zero and dc are zero + return index; + } + } + + return -1; + } + else +#endif + { + int index = Block8x8F.Size - 1; + ref float elemRef = ref Unsafe.As(ref mcu); + + while (index > 0 && (int)Unsafe.Add(ref elemRef, index) == 0) + { + index--; + } + + // this implementation will return 0 if all ac components and dc are zero + return index; + } + } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs index f9c16c5be..51364e3c7 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder @@ -24,9 +24,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder 0, 0, 0 }, new byte[] - { - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 - }), + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 + }), + + // Luminance AC. new HuffmanSpec( new byte[] { @@ -60,6 +62,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa }), + + // Chrominance DC. new HuffmanSpec( new byte[] { @@ -132,4 +136,4 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder this.Values = values; } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs index d472791e4..87170e8d2 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs @@ -114,21 +114,21 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores .NET Core SDK=6.0.100-preview.3.21202.5 - [Host] : .NET Core 3.1.13 (CoreCLR 4.700.21.11102, CoreFX 4.700.21.11602), X64 RyuJIT [AttachedDebugger] + [Host] : .NET Core 3.1.13 (CoreCLR 4.700.21.11102, CoreFX 4.700.21.11602), X64 RyuJIT DefaultJob : .NET Core 3.1.13 (CoreCLR 4.700.21.11102, CoreFX 4.700.21.11602), X64 RyuJIT -| Method | Quality | Mean | Error | StdDev | Ratio | RatioSD | -|---------------------------- |-------- |---------:|---------:|---------:|------:|--------:| -| 'System.Drawing Jpeg 4:2:0' | 75 | 30.60 ms | 0.496 ms | 0.464 ms | 1.00 | 0.00 | -| 'ImageSharp Jpeg 4:2:0' | 75 | 29.86 ms | 0.350 ms | 0.311 ms | 0.98 | 0.02 | -| 'ImageSharp Jpeg 4:4:4' | 75 | 45.36 ms | 0.899 ms | 1.036 ms | 1.48 | 0.05 | -| | | | | | | | -| 'System.Drawing Jpeg 4:2:0' | 90 | 34.05 ms | 0.669 ms | 0.687 ms | 1.00 | 0.00 | -| 'ImageSharp Jpeg 4:2:0' | 90 | 37.26 ms | 0.706 ms | 0.660 ms | 1.10 | 0.03 | -| 'ImageSharp Jpeg 4:4:4' | 90 | 52.54 ms | 0.579 ms | 0.514 ms | 1.55 | 0.04 | -| | | | | | | | -| 'System.Drawing Jpeg 4:2:0' | 100 | 39.36 ms | 0.267 ms | 0.237 ms | 1.00 | 0.00 | -| 'ImageSharp Jpeg 4:2:0' | 100 | 42.44 ms | 0.410 ms | 0.383 ms | 1.08 | 0.01 | -| 'ImageSharp Jpeg 4:4:4' | 100 | 70.88 ms | 0.508 ms | 0.450 ms | 1.80 | 0.02 | +| Method | Quality | Mean | Error | StdDev | Ratio | +|---------------------------- |-------- |---------:|---------:|---------:|------:| +| 'System.Drawing Jpeg 4:2:0' | 75 | 29.41 ms | 0.108 ms | 0.096 ms | 1.00 | +| 'ImageSharp Jpeg 4:2:0' | 75 | 26.30 ms | 0.131 ms | 0.109 ms | 0.89 | +| 'ImageSharp Jpeg 4:4:4' | 75 | 36.70 ms | 0.303 ms | 0.269 ms | 1.25 | +| | | | | | | +| 'System.Drawing Jpeg 4:2:0' | 90 | 32.67 ms | 0.226 ms | 0.211 ms | 1.00 | +| 'ImageSharp Jpeg 4:2:0' | 90 | 33.56 ms | 0.237 ms | 0.222 ms | 1.03 | +| 'ImageSharp Jpeg 4:4:4' | 90 | 44.82 ms | 0.250 ms | 0.234 ms | 1.37 | +| | | | | | | +| 'System.Drawing Jpeg 4:2:0' | 100 | 39.06 ms | 0.233 ms | 0.218 ms | 1.00 | +| 'ImageSharp Jpeg 4:2:0' | 100 | 40.23 ms | 0.225 ms | 0.277 ms | 1.03 | +| 'ImageSharp Jpeg 4:4:4' | 100 | 63.35 ms | 0.486 ms | 0.431 ms | 1.62 | */ diff --git a/tests/ImageSharp.Tests/Formats/Jpg/HuffmanScanEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/HuffmanScanEncoderTests.cs new file mode 100644 index 000000000..b953e80b8 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/HuffmanScanEncoderTests.cs @@ -0,0 +1,241 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder; +using SixLabors.ImageSharp.Tests.TestUtilities; +using Xunit; +using Xunit.Abstractions; + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + [Trait("Format", "Jpg")] + public class HuffmanScanEncoderTests + { + private ITestOutputHelper Output { get; } + + public HuffmanScanEncoderTests(ITestOutputHelper output) + { + this.Output = output; + } + + private static int GetHuffmanEncodingLength_Reference(uint number) + { + int bits = 0; + if (number > 32767) + { + number >>= 16; + bits += 16; + } + + if (number > 127) + { + number >>= 8; + bits += 8; + } + + if (number > 7) + { + number >>= 4; + bits += 4; + } + + if (number > 1) + { + number >>= 2; + bits += 2; + } + + if (number > 0) + { + bits++; + } + + return bits; + } + + [Fact] + public void GetHuffmanEncodingLength_Zero() + { + int expected = 0; + + int actual = HuffmanScanEncoder.GetHuffmanEncodingLength(0); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + public void GetHuffmanEncodingLength_Random(int seed) + { + int maxNumber = 1 << 16; + + var rng = new Random(seed); + for (int i = 0; i < 1000; i++) + { + uint number = (uint)rng.Next(0, maxNumber); + + int expected = GetHuffmanEncodingLength_Reference(number); + + int actual = HuffmanScanEncoder.GetHuffmanEncodingLength(number); + + Assert.Equal(expected, actual); + } + } + + [Fact] + public void GetLastValuableElementIndex_AllZero() + { + static void RunTest() + { + Block8x8F data = default; + + int expectedLessThan = 1; + + int actual = HuffmanScanEncoder.GetLastValuableElementIndex(ref data); + + Assert.True(actual < expectedLessThan); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + } + + [Fact] + public void GetLastValuableElementIndex_AllNonZero() + { + static void RunTest() + { + Block8x8F data = default; + for (int i = 0; i < Block8x8F.Size; i++) + { + data[i] = 10; + } + + int expected = Block8x8F.Size - 1; + + int actual = HuffmanScanEncoder.GetLastValuableElementIndex(ref data); + + Assert.Equal(expected, actual); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + public void GetLastValuableElementIndex_RandomFilledSingle(int seed) + { + static void RunTest(string seedSerialized) + { + int seed = FeatureTestRunner.Deserialize(seedSerialized); + var rng = new Random(seed); + + for (int i = 0; i < 1000; i++) + { + Block8x8F data = default; + + int setIndex = rng.Next(1, Block8x8F.Size); + data[setIndex] = rng.Next(); + + int expected = setIndex; + + int actual = HuffmanScanEncoder.GetLastValuableElementIndex(ref data); + + Assert.Equal(expected, actual); + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + seed, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + public void GetLastValuableElementIndex_RandomFilledPartially(int seed) + { + static void RunTest(string seedSerialized) + { + int seed = FeatureTestRunner.Deserialize(seedSerialized); + var rng = new Random(seed); + + for (int i = 0; i < 1000; i++) + { + Block8x8F data = default; + + int lastIndex = rng.Next(1, Block8x8F.Size); + int fillValue = rng.Next(); + for (int dataIndex = 0; dataIndex <= lastIndex; dataIndex++) + { + data[dataIndex] = fillValue; + } + + int expected = lastIndex; + + int actual = HuffmanScanEncoder.GetLastValuableElementIndex(ref data); + + Assert.Equal(expected, actual); + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + seed, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + public void GetLastValuableElementIndex_RandomFilledFragmented(int seed) + { + static void RunTest(string seedSerialized) + { + int seed = FeatureTestRunner.Deserialize(seedSerialized); + var rng = new Random(seed); + + for (int i = 0; i < 1000; i++) + { + Block8x8F data = default; + + int fillValue = rng.Next(); + + // first filled chunk + int lastIndex1 = rng.Next(1, Block8x8F.Size / 2); + for (int dataIndex = 0; dataIndex <= lastIndex1; dataIndex++) + { + data[dataIndex] = fillValue; + } + + // second filled chunk, there might be a spot with zero(s) between first and second chunk + int lastIndex2 = rng.Next(lastIndex1 + 1, Block8x8F.Size); + for (int dataIndex = 0; dataIndex <= lastIndex2; dataIndex++) + { + data[dataIndex] = fillValue; + } + + int expected = lastIndex2; + + int actual = HuffmanScanEncoder.GetLastValuableElementIndex(ref data); + + Assert.Equal(expected, actual); + } + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + seed, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + } + } +}