From 747422cf6d0075f03b1d10b0d7b07a2063d7a0c9 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 22 Feb 2022 13:46:41 +0100 Subject: [PATCH 01/19] Add Sse2 version of Average png filter --- .../Formats/Png/Filters/AverageFilter.cs | 126 +++++++++++++++--- 1 file changed, 110 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index 83c6389348..94c4fb4d1a 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -20,9 +20,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters internal static class AverageFilter { /// - /// Decodes the scanline + /// Decodes a scanline, which was filtered with the average filter. /// - /// The scanline to decode + /// The scanline to decode. /// The previous scanline. /// The bytes per pixel. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -33,32 +33,126 @@ public static void Decode(Span scanline, Span previousScanline, int ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + // The Avg filter predicts each pixel as the (truncated) average of a and b: // Average(x) + floor((Raw(x-bpp)+Prior(x))/2) - int x = 1; - for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x) +#if SUPPORTS_RUNTIME_INTRINSICS + if (Sse2.IsSupported && bytesPerPixel is 3 or 4) { - ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); - byte above = Unsafe.Add(ref prevBaseRef, x); - scan = (byte)(scan + (above >> 1)); - } + if (bytesPerPixel is 3) + { + Vector128 a = Vector128.Zero; + Vector128 b = Vector128.Zero; + Vector128 d = Vector128.Zero; + var ones = Vector128.Create((byte)1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1); + + Span scratch = stackalloc byte[4]; + ref byte scratchRef = ref MemoryMarshal.GetReference(scratch); + int rb = scanline.Length; + int offset = 0; + while (rb >= 4) + { + a = d; + b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(); + d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref scanBaseRef, offset))).AsByte(); + + d = AverageSubtractAdd(a, b, d, ones); + + // Store the result. + int result = Sse2.ConvertToInt32(d.AsInt32()); + Unsafe.As(ref scratchRef) = result; + scratch.Slice(0, 3).CopyTo(scanline.Slice(offset, 3)); + + rb -= 3; + offset += 3; + } + + if (rb is 3) + { + a = d; + scratch[3] = 0; + previousScanline.Slice(offset, 3).CopyTo(scratch); + b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scratchRef)).AsByte(); + scanline.Slice(offset, 3).CopyTo(scratch); + d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scratchRef)).AsByte(); + + d = AverageSubtractAdd(a, b, d, ones); + + // Store the result. + int result = Sse2.ConvertToInt32(d.AsInt32()); + Unsafe.As(ref scratchRef) = result; + scratch.Slice(0, 3).CopyTo(scanline.Slice(offset, 3)); + } + } + else + { + Vector128 a = Vector128.Zero; + Vector128 b = Vector128.Zero; + Vector128 d = Vector128.Zero; + var ones = Vector128.Create((byte)1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1); + + Span scratch = stackalloc byte[4]; + ref byte scratchRef = ref MemoryMarshal.GetReference(scratch); + int rb = scanline.Length; + int offset = 0; + while (rb >= 4) + { + a = d; + b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(); + d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref scanBaseRef, offset))).AsByte(); - for (; x < scanline.Length; ++x) + d = AverageSubtractAdd(a, b, d, ones); + + // Store the result. + int result = Sse2.ConvertToInt32(d.AsInt32()); + Unsafe.As(ref scratchRef) = result; + scratch.CopyTo(scanline.Slice(offset, 4)); + + rb -= 4; + offset += 4; + } + } + } + else +#endif { - ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); - byte left = Unsafe.Add(ref scanBaseRef, x - bytesPerPixel); - byte above = Unsafe.Add(ref prevBaseRef, x); - scan = (byte)(scan + Average(left, above)); + int x = 1; + for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); + byte above = Unsafe.Add(ref prevBaseRef, x); + scan = (byte)(scan + (above >> 1)); + } + + for (; x < scanline.Length; ++x) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); + byte left = Unsafe.Add(ref scanBaseRef, x - bytesPerPixel); + byte above = Unsafe.Add(ref prevBaseRef, x); + scan = (byte)(scan + Average(left, above)); + } } } +#if SUPPORTS_RUNTIME_INTRINSICS + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 AverageSubtractAdd(Vector128 a, Vector128 b, Vector128 d, Vector128 ones) + { + // PNG requires a truncating average, so we can't just use _mm_avg_epu8. + // ...but we can fix it up by subtracting off 1 if it rounded up. + Vector128 avg = Sse2.Average(a, b); + avg = Sse2.Subtract(avg, Sse2.And(Sse2.Xor(a, b), ones)); + return Sse2.Add(d, avg); + } +#endif + /// - /// Encodes the scanline + /// Encodes a scanline with the average filter applied. /// - /// The scanline to encode + /// The scanline to encode. /// The previous scanline. /// The filtered scanline result. /// The bytes per pixel. - /// The sum of the total variance of the filtered row + /// The sum of the total variance of the filtered row. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, int bytesPerPixel, out int sum) { From 71520887acf61dd8d09716aee7f273d5135e5f59 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 22 Feb 2022 17:43:14 +0100 Subject: [PATCH 02/19] No need for the scratch buffer for 4 bytes per pixel --- .../Formats/Png/Filters/AverageFilter.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index 94c4fb4d1a..50058f01d2 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -51,16 +51,16 @@ public static void Decode(Span scanline, Span previousScanline, int int offset = 0; while (rb >= 4) { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); a = d; b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(); - d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref scanBaseRef, offset))).AsByte(); + d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(); d = AverageSubtractAdd(a, b, d, ones); // Store the result. int result = Sse2.ConvertToInt32(d.AsInt32()); - Unsafe.As(ref scratchRef) = result; - scratch.Slice(0, 3).CopyTo(scanline.Slice(offset, 3)); + Unsafe.As(ref scanRef) = result; rb -= 3; offset += 3; @@ -90,22 +90,20 @@ public static void Decode(Span scanline, Span previousScanline, int Vector128 d = Vector128.Zero; var ones = Vector128.Create((byte)1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1); - Span scratch = stackalloc byte[4]; - ref byte scratchRef = ref MemoryMarshal.GetReference(scratch); int rb = scanline.Length; int offset = 0; while (rb >= 4) { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); a = d; - b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(); + b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(); d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref scanBaseRef, offset))).AsByte(); d = AverageSubtractAdd(a, b, d, ones); // Store the result. int result = Sse2.ConvertToInt32(d.AsInt32()); - Unsafe.As(ref scratchRef) = result; - scratch.CopyTo(scanline.Slice(offset, 4)); + Unsafe.As(ref scanRef) = result; rb -= 4; offset += 4; From 18548f7eff6f815f9969ee6298810a5761e9499b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 22 Feb 2022 22:32:32 +0100 Subject: [PATCH 03/19] Dont use intrinsics for 3 bytes per pixel --- .../Formats/Png/Filters/AverageFilter.cs | 152 +++++++----------- 1 file changed, 56 insertions(+), 96 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index 50058f01d2..7747d14ea2 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -30,119 +30,79 @@ public static void Decode(Span scanline, Span previousScanline, int { DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); - ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); - ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); - // The Avg filter predicts each pixel as the (truncated) average of a and b: // Average(x) + floor((Raw(x-bpp)+Prior(x))/2) #if SUPPORTS_RUNTIME_INTRINSICS - if (Sse2.IsSupported && bytesPerPixel is 3 or 4) + if (Sse2.IsSupported && bytesPerPixel is 4) { - if (bytesPerPixel is 3) - { - Vector128 a = Vector128.Zero; - Vector128 b = Vector128.Zero; - Vector128 d = Vector128.Zero; - var ones = Vector128.Create((byte)1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1); - - Span scratch = stackalloc byte[4]; - ref byte scratchRef = ref MemoryMarshal.GetReference(scratch); - int rb = scanline.Length; - int offset = 0; - while (rb >= 4) - { - ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); - a = d; - b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(); - d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(); - - d = AverageSubtractAdd(a, b, d, ones); - - // Store the result. - int result = Sse2.ConvertToInt32(d.AsInt32()); - Unsafe.As(ref scanRef) = result; - - rb -= 3; - offset += 3; - } - - if (rb is 3) - { - a = d; - scratch[3] = 0; - previousScanline.Slice(offset, 3).CopyTo(scratch); - b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scratchRef)).AsByte(); - scanline.Slice(offset, 3).CopyTo(scratch); - d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scratchRef)).AsByte(); - - d = AverageSubtractAdd(a, b, d, ones); - - // Store the result. - int result = Sse2.ConvertToInt32(d.AsInt32()); - Unsafe.As(ref scratchRef) = result; - scratch.Slice(0, 3).CopyTo(scanline.Slice(offset, 3)); - } - } - else - { - Vector128 a = Vector128.Zero; - Vector128 b = Vector128.Zero; - Vector128 d = Vector128.Zero; - var ones = Vector128.Create((byte)1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1); - - int rb = scanline.Length; - int offset = 0; - while (rb >= 4) - { - ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); - a = d; - b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(); - d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref scanBaseRef, offset))).AsByte(); - - d = AverageSubtractAdd(a, b, d, ones); - - // Store the result. - int result = Sse2.ConvertToInt32(d.AsInt32()); - Unsafe.As(ref scanRef) = result; - - rb -= 4; - offset += 4; - } - } + DecodeSse2(scanline, previousScanline); } else #endif { - int x = 1; - for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x) - { - ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); - byte above = Unsafe.Add(ref prevBaseRef, x); - scan = (byte)(scan + (above >> 1)); - } - - for (; x < scanline.Length; ++x) - { - ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); - byte left = Unsafe.Add(ref scanBaseRef, x - bytesPerPixel); - byte above = Unsafe.Add(ref prevBaseRef, x); - scan = (byte)(scan + Average(left, above)); - } + DecodeScalar(scanline, previousScanline, bytesPerPixel); } } #if SUPPORTS_RUNTIME_INTRINSICS [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 AverageSubtractAdd(Vector128 a, Vector128 b, Vector128 d, Vector128 ones) + private static void DecodeSse2(Span scanline, Span previousScanline) { - // PNG requires a truncating average, so we can't just use _mm_avg_epu8. - // ...but we can fix it up by subtracting off 1 if it rounded up. - Vector128 avg = Sse2.Average(a, b); - avg = Sse2.Subtract(avg, Sse2.And(Sse2.Xor(a, b), ones)); - return Sse2.Add(d, avg); + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + Vector128 d = Vector128.Zero; + var ones = Vector128.Create((byte)1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1); + + int rb = scanline.Length; + int offset = 1; + while (rb >= 4) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + Vector128 a = d; + Vector128 b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(); + d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(); + + // PNG requires a truncating average, so we can't just use _mm_avg_epu8, + // but we can fix it up by subtracting off 1 if it rounded up. + Vector128 avg = Sse2.Average(a, b); + Vector128 xor = Sse2.Xor(a, b); + Vector128 and = Sse2.And(xor, ones); + avg = Sse2.Subtract(avg, and); + d = Sse2.Add(d, avg); + + // Store the result. + Unsafe.As(ref scanRef) = Sse2.ConvertToInt32(d.AsInt32()); + + rb -= 4; + offset += 4; + } } #endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeScalar(Span scanline, Span previousScanline, int bytesPerPixel) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + int x = 1; + for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); + byte above = Unsafe.Add(ref prevBaseRef, x); + scan = (byte)(scan + (above >> 1)); + } + + for (; x < scanline.Length; ++x) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); + byte left = Unsafe.Add(ref scanBaseRef, x - bytesPerPixel); + byte above = Unsafe.Add(ref prevBaseRef, x); + scan = (byte)(scan + Average(left, above)); + } + } + /// /// Encodes a scanline with the average filter applied. /// From 9eb71a20af6676afdb9c4cd07005d21cd022817d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 23 Feb 2022 18:21:21 +0100 Subject: [PATCH 04/19] Add average filter tests --- .../Formats/Png/PngDecoderFilterTests.cs | 62 +++++++++++++++++++ ...ilterTests.cs => PngEncoderFilterTests.cs} | 4 +- 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs rename tests/ImageSharp.Tests/Formats/Png/{PngFilterTests.cs => PngEncoderFilterTests.cs} (98%) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs new file mode 100644 index 0000000000..a901fc6cdf --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Png.Filters; +using SixLabors.ImageSharp.Tests.TestUtilities; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Png +{ + public class PngDecoderFilterTests + { + private static void RunAverageFilterTest() + { + // arrange + byte[] scanline = + { + 3, 39, 39, 39, 0, 4, 4, 4, 0, 1, 1, 1, 0, 1, 1, 1, 0, 2, 2, 2, 0, 2, 2, 2, 0, 2, 2, 2, 0, 4, 4, 4, + 0, 2, 2, 2, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 2, 2, 2, 0, + 1, 1, 1, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 3, 3, 3, 0, 254, 254, 254, + 0, 6, 6, 6, 14, 71, 71, 71, 157, 254, 254, 254, 28, 251, 251, 251, 0, 4, 4, 4, 0, 2, 2, 2, 0, 11, + 11, 11, 0, 226, 226, 226, 0, 255 + }; + + byte[] previousScanline = + { + 3, 74, 74, 74, 0, 73, 73, 73, 0, 73, 73, 73, 0, 74, 74, 74, 0, 74, 74, 74, 0, 73, 73, 73, 0, 72, 72, + 72, 0, 72, 72, 72, 0, 73, 73, 73, 0, 74, 74, 74, 0, 73, 73, 73, 0, 72, 72, 72, 0, 72, 72, 72, 0, 74, + 74, 74, 0, 72, 72, 72, 0, 73, 73, 73, 0, 75, 75, 75, 0, 73, 73, 73, 0, 74, 74, 74, 0, 72, 72, 72, 0, + 73, 73, 73, 0, 73, 73, 73, 0, 72, 72, 72, 0, 74, 74, 74, 0, 61, 61, 61, 0, 101, 101, 101, 78, 197, + 197, 197, 251, 152, 152, 152, 255, 155, 155, 155, 255, 162, 162, 162, 255, 175, 175, 175, 255, 160, + 160, 160, 255, 139 + }; + + byte[] expected = + { + 3, 76, 76, 76, 0, 78, 78, 78, 0, 76, 76, 76, 0, 76, 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 76, 76, + 76, 0, 78, 78, 78, 0, 77, 77, 77, 0, 78, 78, 78, 0, 76, 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 76, + 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 77, 77, 77, 0, 78, 78, 78, 0, 77, 77, 77, 0, 77, 77, 77, 0, + 76, 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 73, 73, 73, 0, 73, 73, 73, 14, 158, 158, 158, 203, 175, + 175, 175, 255, 158, 158, 158, 255, 160, 160, 160, 255, 163, 163, 163, 255, 180, 180, 180, 255, 140, + 140, 140, 255, 255 + }; + + // act + AverageFilter.Decode(scanline, previousScanline, 4); + + // assert + Assert.Equal(scanline, expected); + } + + [Fact] + public void AverageInverse_Works() => RunAverageFilterTest(); + +#if SUPPORTS_RUNTIME_INTRINSICS + [Fact] + public void AverageInverse_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.AllowAll); + + [Fact] + public void AverageInverse__WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.DisableSSE2); +#endif + } +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs similarity index 98% rename from tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs rename to tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs index 9b6119380a..11e3fbb230 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png { [Trait("Format", "Png")] - public partial class PngFilterTests : MeasureFixture + public class PngEncoderFilterTests : MeasureFixture { #if BENCHMARKING public const int Times = 1000000; @@ -21,7 +21,7 @@ public partial class PngFilterTests : MeasureFixture public const int Times = 1; #endif - public PngFilterTests(ITestOutputHelper output) + public PngEncoderFilterTests(ITestOutputHelper output) : base(output) { } From a921c57efc84a24b8d81843f61895b850a869077 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 23 Feb 2022 18:33:52 +0100 Subject: [PATCH 05/19] Add benchmark file for average filter with 4bpp --- .../Codecs/Png/DecodeFilteredPng.cs | 20 ++++++++++++------- tests/ImageSharp.Tests/TestImages.cs | 6 ++++-- tests/Images/Input/Png/AverageFilter4Bpp.png | 3 +++ 3 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 tests/Images/Input/Png/AverageFilter4Bpp.png diff --git a/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs b/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs index 6517bf3c48..373b563c82 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs @@ -16,7 +16,8 @@ public class DecodeFilteredPng private byte[] filter1; private byte[] filter2; private byte[] filter3; - private byte[] filter4; + private byte[] averageFilter3bpp; + private byte[] averageFilter4bpp; [GlobalSetup] public void ReadImages() @@ -24,8 +25,9 @@ public void ReadImages() this.filter0 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter0)); this.filter1 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter1)); this.filter2 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter2)); - this.filter3 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter3)); - this.filter4 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter4)); + this.filter3 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter4)); + this.averageFilter3bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter3bpp)); + this.averageFilter4bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter4bpp)); } [Benchmark(Baseline = true, Description = "None-filtered PNG file")] @@ -40,13 +42,17 @@ public Size PngFilter1() public Size PngFilter2() => LoadPng(this.filter2); - [Benchmark(Description = "Average-filtered PNG file")] - public Size PngFilter3() - => LoadPng(this.filter3); + [Benchmark(Description = "Average-filtered PNG file (3bpp)")] + public Size PngAvgFilter1() + => LoadPng(this.averageFilter3bpp); + + [Benchmark(Description = "Average-filtered PNG file (4bpp)")] + public Size PngAvgFilter2() + => LoadPng(this.averageFilter4bpp); [Benchmark(Description = "Paeth-filtered PNG file")] public Size PngFilter4() - => LoadPng(this.filter4); + => LoadPng(this.filter3); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Size LoadPng(byte[] bytes) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 5bce99ce12..2f3bb49f61 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -68,9 +68,11 @@ public static class Png public const string Filter0 = "Png/filter0.png"; public const string Filter1 = "Png/filter1.png"; public const string Filter2 = "Png/filter2.png"; - public const string Filter3 = "Png/filter3.png"; + public const string AverageFilter3bpp = "Png/filter3.png"; public const string Filter4 = "Png/filter4.png"; + public const string AverageFilter4bpp = "Png/AverageFilter4Bpp.png"; + // Paletted images also from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string PalettedTwoColor = "Png/basn3p01.png"; public const string PalettedFourColor = "Png/basn3p02.png"; @@ -159,7 +161,7 @@ public static class Bad { P1, Pd, Blur, Splash, Cross, Powerpoint, SplashInterlaced, Interlaced, - Filter0, Filter1, Filter2, Filter3, Filter4, + Filter0, Filter1, Filter2, AverageFilter3bpp, Filter4, FilterVar, VimImage1, VimImage2, VersioningImage1, VersioningImage2, Ratio4x1, Ratio1x4 }; diff --git a/tests/Images/Input/Png/AverageFilter4Bpp.png b/tests/Images/Input/Png/AverageFilter4Bpp.png new file mode 100644 index 0000000000..728b6cfaff --- /dev/null +++ b/tests/Images/Input/Png/AverageFilter4Bpp.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7add6fba794bc76ccea2ee3a311b4050cf17f4f78b69a50785f7739b8b35919e +size 181108 From 3ab1ba6bb304f52ef163c341ac8e22a82d62841a Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 23 Feb 2022 19:02:43 +0100 Subject: [PATCH 06/19] Use DisableHWIntrinsic in average filter test --- tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs index a901fc6cdf..ce6cd05266 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs @@ -56,7 +56,7 @@ private static void RunAverageFilterTest() public void AverageInverse_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.AllowAll); [Fact] - public void AverageInverse__WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.DisableSSE2); + public void AverageInverse_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.DisableHWIntrinsic); #endif } } From 9f322555a96d7b920740608ca736bc5f8bffc911 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 23 Feb 2022 19:17:44 +0100 Subject: [PATCH 07/19] Rename average filter tests --- tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs index ce6cd05266..a096c5eb6c 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs @@ -49,14 +49,14 @@ private static void RunAverageFilterTest() } [Fact] - public void AverageInverse_Works() => RunAverageFilterTest(); + public void AverageFilter_Works() => RunAverageFilterTest(); #if SUPPORTS_RUNTIME_INTRINSICS [Fact] - public void AverageInverse_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.AllowAll); + public void AverageFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.AllowAll); [Fact] - public void AverageInverse_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.DisableHWIntrinsic); + public void AverageFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.DisableHWIntrinsic); #endif } } From 4f6b807e389b7a2cdb20329065fe1d1d51654590 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 23 Feb 2022 19:50:55 +0100 Subject: [PATCH 08/19] Fix average test data --- .../ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs index a096c5eb6c..310f53c516 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs @@ -7,6 +7,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png { + [Trait("Format", "Png")] public class PngDecoderFilterTests { private static void RunAverageFilterTest() @@ -18,7 +19,7 @@ private static void RunAverageFilterTest() 0, 2, 2, 2, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 2, 2, 2, 0, 1, 1, 1, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 3, 3, 3, 0, 254, 254, 254, 0, 6, 6, 6, 14, 71, 71, 71, 157, 254, 254, 254, 28, 251, 251, 251, 0, 4, 4, 4, 0, 2, 2, 2, 0, 11, - 11, 11, 0, 226, 226, 226, 0, 255 + 11, 11, 0, 226, 226, 226, 0, 255, 128, 234 }; byte[] previousScanline = @@ -28,7 +29,7 @@ private static void RunAverageFilterTest() 74, 74, 0, 72, 72, 72, 0, 73, 73, 73, 0, 75, 75, 75, 0, 73, 73, 73, 0, 74, 74, 74, 0, 72, 72, 72, 0, 73, 73, 73, 0, 73, 73, 73, 0, 72, 72, 72, 0, 74, 74, 74, 0, 61, 61, 61, 0, 101, 101, 101, 78, 197, 197, 197, 251, 152, 152, 152, 255, 155, 155, 155, 255, 162, 162, 162, 255, 175, 175, 175, 255, 160, - 160, 160, 255, 139 + 160, 160, 255, 139, 128, 134 }; byte[] expected = @@ -38,7 +39,7 @@ private static void RunAverageFilterTest() 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 77, 77, 77, 0, 78, 78, 78, 0, 77, 77, 77, 0, 77, 77, 77, 0, 76, 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 73, 73, 73, 0, 73, 73, 73, 14, 158, 158, 158, 203, 175, 175, 175, 255, 158, 158, 158, 255, 160, 160, 160, 255, 163, 163, 163, 255, 180, 180, 180, 255, 140, - 140, 140, 255, 255 + 140, 140, 255, 138, 6, 115 }; // act From c01001fddb1eee22d316ce8648db295a37a7f26c Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 23 Feb 2022 20:07:09 +0100 Subject: [PATCH 09/19] Add comment about pixel layout --- src/ImageSharp/Formats/Png/Filters/AverageFilter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index 7747d14ea2..a99b93adb5 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -32,6 +32,9 @@ public static void Decode(Span scanline, Span previousScanline, int // The Avg filter predicts each pixel as the (truncated) average of a and b: // Average(x) + floor((Raw(x-bpp)+Prior(x))/2) + // With pixels positioned like this: + // prev: c b + // row: a d #if SUPPORTS_RUNTIME_INTRINSICS if (Sse2.IsSupported && bytesPerPixel is 4) { From e1f96f2647d6a9ccfef3a61c9d339381e7f3c4f9 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 23 Feb 2022 21:40:51 +0100 Subject: [PATCH 10/19] Additional png decoder tests for average filter --- .../Codecs/Png/DecodeFilteredPng.cs | 4 ++-- tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs | 11 +++++++++++ tests/ImageSharp.Tests/TestImages.cs | 6 +++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs b/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs index 373b563c82..92eb4af07e 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs @@ -26,8 +26,8 @@ public void ReadImages() this.filter1 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter1)); this.filter2 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter2)); this.filter3 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter4)); - this.averageFilter3bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter3bpp)); - this.averageFilter4bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter4bpp)); + this.averageFilter3bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter3BytesPerPixel)); + this.averageFilter4bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter4BytesPerPixel)); } [Benchmark(Baseline = true, Description = "None-filtered PNG file")] diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 0af5d99950..7d47087990 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -111,6 +111,17 @@ public void Decode(TestImageProvider provider) image.CompareToOriginal(provider, ImageComparer.Exact); } + [Theory] + [WithFile(TestImages.Png.AverageFilter3BytesPerPixel, PixelTypes.Rgba64)] + [WithFile(TestImages.Png.AverageFilter4BytesPerPixel, PixelTypes.Rgba64)] + public void Decode_WithAverageFilter(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact); + } + [Theory] [WithFile(TestImages.Png.GrayA8Bit, PixelTypes.Rgba32)] [WithFile(TestImages.Png.Gray1BitTrans, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 2f3bb49f61..ad50fc5c71 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -68,10 +68,10 @@ public static class Png public const string Filter0 = "Png/filter0.png"; public const string Filter1 = "Png/filter1.png"; public const string Filter2 = "Png/filter2.png"; - public const string AverageFilter3bpp = "Png/filter3.png"; + public const string AverageFilter3BytesPerPixel = "Png/filter3.png"; public const string Filter4 = "Png/filter4.png"; - public const string AverageFilter4bpp = "Png/AverageFilter4Bpp.png"; + public const string AverageFilter4BytesPerPixel = "Png/AverageFilter4Bpp.png"; // Paletted images also from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string PalettedTwoColor = "Png/basn3p01.png"; @@ -161,7 +161,7 @@ public static class Bad { P1, Pd, Blur, Splash, Cross, Powerpoint, SplashInterlaced, Interlaced, - Filter0, Filter1, Filter2, AverageFilter3bpp, Filter4, + Filter0, Filter1, Filter2, AverageFilter3BytesPerPixel, Filter4, FilterVar, VimImage1, VimImage2, VersioningImage1, VersioningImage2, Ratio4x1, Ratio1x4 }; From 8716c51d3044c40eaa46c6406f1f581afd699112 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 24 Feb 2022 11:53:50 +0100 Subject: [PATCH 11/19] Add SSE2 version of up filter --- .../Formats/Png/Filters/UpFilter.cs | 57 +++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs index 7e0286991b..2d2a442a19 100644 --- a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters internal static class UpFilter { /// - /// Decodes the scanline + /// Decodes a scanline, which was filtered with the up filter. /// /// The scanline to decode /// The previous scanline. @@ -30,6 +30,55 @@ public static void Decode(Span scanline, Span previousScanline) { DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); +#if SUPPORTS_RUNTIME_INTRINSICS + if (Sse2.IsSupported) + { + DecodeSse2(scanline, previousScanline); + } + else +#endif + { + DecodeScalar(scanline, previousScanline); + } + } + +#if SUPPORTS_RUNTIME_INTRINSICS + private static void DecodeSse2(Span scanline, Span previousScanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + // Up(x) + Prior(x) + int rb = scanline.Length; + int offset = 1; + const int bytesPerBatch = 16; + while (rb >= bytesPerBatch) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + Vector128 current = Unsafe.As>(ref scanRef); + Vector128 up = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, offset)); + + Vector128 sum = Sse2.Add(up, current); + Unsafe.As>(ref scanRef) = sum; + + offset += bytesPerBatch; + rb -= bytesPerBatch; + } + + // Handle left over. + for (int i = offset; i < scanline.Length; i++) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset); + byte above = Unsafe.Add(ref prevBaseRef, offset); + scan = (byte)(scan + above); + offset++; + } + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeScalar(Span scanline, Span previousScanline) + { ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); @@ -43,12 +92,12 @@ public static void Decode(Span scanline, Span previousScanline) } /// - /// Encodes the scanline + /// Encodes a scanline with the up filter applied. /// - /// The scanline to encode + /// The scanline to encode. /// The previous scanline. /// The filtered scanline result. - /// The sum of the total variance of the filtered row + /// The sum of the total variance of the filtered row. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, out int sum) { From 0c8dbeae8c6854e75ccefa3902df85f01160f2dc Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 24 Feb 2022 13:58:16 +0100 Subject: [PATCH 12/19] Add Avx version of up filter --- .../Formats/Png/Filters/UpFilter.cs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs index 2d2a442a19..20a828e520 100644 --- a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs @@ -31,7 +31,11 @@ public static void Decode(Span scanline, Span previousScanline) DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); #if SUPPORTS_RUNTIME_INTRINSICS - if (Sse2.IsSupported) + if (Avx2.IsSupported) + { + DecodeAvx2(scanline, previousScanline); + } + else if (Sse2.IsSupported) { DecodeSse2(scanline, previousScanline); } @@ -43,6 +47,38 @@ public static void Decode(Span scanline, Span previousScanline) } #if SUPPORTS_RUNTIME_INTRINSICS + private static void DecodeAvx2(Span scanline, Span previousScanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + // Up(x) + Prior(x) + int rb = scanline.Length; + int offset = 1; + const int bytesPerBatch = 32; + while (rb >= bytesPerBatch) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + Vector256 current = Unsafe.As>(ref scanRef); + Vector256 up = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, offset)); + + Vector256 sum = Avx2.Add(up, current); + Unsafe.As>(ref scanRef) = sum; + + offset += bytesPerBatch; + rb -= bytesPerBatch; + } + + // Handle left over. + for (int i = offset; i < scanline.Length; i++) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset); + byte above = Unsafe.Add(ref prevBaseRef, offset); + scan = (byte)(scan + above); + offset++; + } + } + private static void DecodeSse2(Span scanline, Span previousScanline) { ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); From 849e866221556474e4790e18ae6898e43de7e2f2 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 24 Feb 2022 16:22:29 +0100 Subject: [PATCH 13/19] Add SSE2 version of sub filter --- .../Formats/Png/Filters/SubFilter.cs | 50 +++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs index c28b877e41..7985977548 100644 --- a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs @@ -21,12 +21,52 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters internal static class SubFilter { /// - /// Decodes the scanline + /// Decodes a scanline, which was filtered with the sub filter. /// - /// The scanline to decode + /// The scanline to decode. /// The bytes per pixel. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Decode(Span scanline, int bytesPerPixel) + { + // The Sub filter predicts each pixel as the previous pixel. +#if SUPPORTS_RUNTIME_INTRINSICS + if (Sse2.IsSupported && bytesPerPixel is 4) + { + DecodeSse2(scanline); + } + else +#endif + { + DecodeScalar(scanline, bytesPerPixel); + } + } + +#if SUPPORTS_RUNTIME_INTRINSICS + private static void DecodeSse2(Span scanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + + Vector128 d = Vector128.Zero; + + int rb = scanline.Length; + int offset = 1; + while (rb >= 4) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + Vector128 a = d; + d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(); + + d = Sse2.Add(d, a); + + Unsafe.As(ref scanRef) = Sse2.ConvertToInt32(d.AsInt32()); + + rb -= 4; + offset += 4; + } + } +#endif + + private static void DecodeScalar(Span scanline, int bytesPerPixel) { ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); @@ -42,12 +82,12 @@ public static void Decode(Span scanline, int bytesPerPixel) } /// - /// Encodes the scanline + /// Encodes a scanline with the sup filter applied. /// - /// The scanline to encode + /// The scanline to encode. /// The filtered scanline result. /// The bytes per pixel. - /// The sum of the total variance of the filtered row + /// The sum of the total variance of the filtered row. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Encode(ReadOnlySpan scanline, ReadOnlySpan result, int bytesPerPixel, out int sum) { From 670105f17b5102e9d2e7a1370dc4efe0a04c69dd Mon Sep 17 00:00:00 2001 From: Brian Popow <38701097+brianpopow@users.noreply.github.com> Date: Thu, 24 Feb 2022 18:22:34 +0100 Subject: [PATCH 14/19] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günther Foidl --- src/ImageSharp/Formats/Png/Filters/AverageFilter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index a99b93adb5..5d5c4a79d1 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -55,7 +55,7 @@ private static void DecodeSse2(Span scanline, Span previousScanline) ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); Vector128 d = Vector128.Zero; - var ones = Vector128.Create((byte)1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1); + var ones = Vector128.Create((byte)1); int rb = scanline.Length; int offset = 1; @@ -89,7 +89,7 @@ private static void DecodeScalar(Span scanline, Span previousScanlin ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); - int x = 1; + nint x = 1; for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x) { ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); From 04219b5f74c39bd9134a10b12346f82a5b7affe8 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 24 Feb 2022 18:24:02 +0100 Subject: [PATCH 15/19] Use nint for offset --- src/ImageSharp/Formats/Png/Filters/AverageFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index 5d5c4a79d1..44a16f154e 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -58,7 +58,7 @@ private static void DecodeSse2(Span scanline, Span previousScanline) var ones = Vector128.Create((byte)1); int rb = scanline.Length; - int offset = 1; + nint offset = 1; while (rb >= 4) { ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); From 8432b9fa4d76cfd13fad3221e77ab43f67ecc6cb Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 24 Feb 2022 18:29:07 +0100 Subject: [PATCH 16/19] Use nint for offset --- src/ImageSharp/Formats/Png/Filters/SubFilter.cs | 4 ++-- src/ImageSharp/Formats/Png/Filters/UpFilter.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs index 7985977548..eaa4dc0344 100644 --- a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs @@ -49,7 +49,7 @@ private static void DecodeSse2(Span scanline) Vector128 d = Vector128.Zero; int rb = scanline.Length; - int offset = 1; + nint offset = 1; while (rb >= 4) { ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); @@ -71,7 +71,7 @@ private static void DecodeScalar(Span scanline, int bytesPerPixel) ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); // Sub(x) + Raw(x-bpp) - int x = bytesPerPixel + 1; + nint x = bytesPerPixel + 1; Unsafe.Add(ref scanBaseRef, x); for (; x < scanline.Length; ++x) { diff --git a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs index 20a828e520..0d24d9c5d5 100644 --- a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs @@ -54,7 +54,7 @@ private static void DecodeAvx2(Span scanline, Span previousScanline) // Up(x) + Prior(x) int rb = scanline.Length; - int offset = 1; + nint offset = 1; const int bytesPerBatch = 32; while (rb >= bytesPerBatch) { @@ -70,7 +70,7 @@ private static void DecodeAvx2(Span scanline, Span previousScanline) } // Handle left over. - for (int i = offset; i < scanline.Length; i++) + for (nint i = offset; i < scanline.Length; i++) { ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset); byte above = Unsafe.Add(ref prevBaseRef, offset); @@ -86,7 +86,7 @@ private static void DecodeSse2(Span scanline, Span previousScanline) // Up(x) + Prior(x) int rb = scanline.Length; - int offset = 1; + nint offset = 1; const int bytesPerBatch = 16; while (rb >= bytesPerBatch) { @@ -102,7 +102,7 @@ private static void DecodeSse2(Span scanline, Span previousScanline) } // Handle left over. - for (int i = offset; i < scanline.Length; i++) + for (nint i = offset; i < scanline.Length; i++) { ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset); byte above = Unsafe.Add(ref prevBaseRef, offset); From 06843f2a7d81a00da7be04d615ef26dc0f5389e2 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 24 Feb 2022 23:01:53 +0100 Subject: [PATCH 17/19] Add SSE version of paeth filter --- .../Formats/Png/Filters/PaethFilter.cs | 88 ++++++++++++++++++- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs index 6a89a1122a..0553eb46a9 100644 --- a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs @@ -22,9 +22,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters internal static class PaethFilter { /// - /// Decodes the scanline + /// Decodes a scanline, which was filtered with the paeth filter. /// - /// The scanline to decode + /// The scanline to decode. /// The previous scanline. /// The bytes per pixel. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -32,6 +32,86 @@ public static void Decode(Span scanline, Span previousScanline, int { DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); + // Paeth tries to predict pixel d using the pixel to the left of it, a, + // and two pixels from the previous row, b and c: + // prev: c b + // row: a d + // The Paeth function predicts d to be whichever of a, b, or c is nearest to + // p = a + b - c. +#if SUPPORTS_RUNTIME_INTRINSICS + if (Sse41.IsSupported && bytesPerPixel is 4) + { + DecodeSse41(scanline, previousScanline); + } + else +#endif + { + DecodeScalar(scanline, previousScanline, bytesPerPixel); + } + } + +#if SUPPORTS_RUNTIME_INTRINSICS + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeSse41(Span scanline, Span previousScanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + Vector128 b = Vector128.Zero; + Vector128 d = Vector128.Zero; + + int rb = scanline.Length; + nint offset = 1; + while (rb >= 4) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + + // It's easiest to do this math (particularly, deal with pc) with 16-bit intermediates. + Vector128 c = b; + Vector128 a = d; + b = Sse2.UnpackLow( + Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(), + Vector128.Zero); + d = Sse2.UnpackLow( + Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(), + Vector128.Zero); + + // (p-a) == (a+b-c - a) == (b-c) + Vector128 pa = Sse2.Subtract(b.AsInt16(), c.AsInt16()); + + // (p-b) == (a+b-c - b) == (a-c) + Vector128 pb = Sse2.Subtract(a.AsInt16(), c.AsInt16()); + + // (p-c) == (a+b-c - c) == (a+b-c-c) == (b-c)+(a-c) + Vector128 pc = Sse2.Add(pa.AsInt16(), pb.AsInt16()); + + pa = Ssse3.Abs(pa.AsInt16()).AsInt16(); /* |p-a| */ + pb = Ssse3.Abs(pb.AsInt16()).AsInt16(); /* |p-b| */ + pc = Ssse3.Abs(pc.AsInt16()).AsInt16(); /* |p-c| */ + + Vector128 smallest = Sse2.Min(pc, Sse2.Min(pa, pb)); + + // Paeth breaks ties favoring a over b over c. + Vector128 mask = Sse41.BlendVariable(c, b, Sse2.CompareEqual(smallest, pb).AsByte()); + Vector128 nearest = Sse41.BlendVariable(mask, a, Sse2.CompareEqual(smallest, pa).AsByte()); + + // Note `_epi8`: we need addition to wrap modulo 255. + d = Sse2.Add(d, nearest); + + // Store the result. + Unsafe.As(ref scanRef) = Sse2.ConvertToInt32(Sse2.PackUnsignedSaturate(d.AsInt16(), d.AsInt16()).AsInt32()); + + rb -= 4; + offset += 4; + } + } + +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeScalar(Span scanline, Span previousScanline, int bytesPerPixel) + { ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); @@ -56,13 +136,13 @@ public static void Decode(Span scanline, Span previousScanline, int } /// - /// Encodes the scanline + /// Encodes a scanline and applies the paeth filter. /// /// The scanline to encode /// The previous scanline. /// The filtered scanline result. /// The bytes per pixel. - /// The sum of the total variance of the filtered row + /// The sum of the total variance of the filtered row. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, int bytesPerPixel, out int sum) { From 88b75da62b2c74172f69930b555335bc59fdde9b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 25 Feb 2022 13:05:59 +0100 Subject: [PATCH 18/19] Add tests for png filters with and without intrinsics --- .../Formats/Png/PngDecoderFilterTests.cs | 118 +++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs index 310f53c516..edfff19a4b 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs @@ -46,18 +46,134 @@ private static void RunAverageFilterTest() AverageFilter.Decode(scanline, previousScanline, 4); // assert - Assert.Equal(scanline, expected); + Assert.Equal(expected, scanline); + } + + private static void RunUpFilterTest() + { + // arrange + byte[] scanline = + { + 62, 23, 186, 150, 174, 4, 205, 59, 153, 134, 158, 86, 240, 173, 191, 58, 111, 183, 77, 37, 85, 23, + 93, 204, 110, 139, 9, 20, 87, 154, 176, 54, 207, 214, 40, 11, 179, 199, 7, 219, 174, 242, 112, 220, + 149, 5, 9, 110, 103, 107, 231, 241, 13, 70, 216, 39, 186, 237, 39, 34, 251, 185, 228, 254 + }; + + byte[] previousScanline = + { + 214, 103, 135, 26, 133, 179, 134, 168, 175, 114, 118, 99, 167, 129, 55, 105, 129, 154, 173, 235, + 179, 191, 41, 137, 253, 0, 81, 198, 159, 228, 224, 245, 14, 113, 5, 45, 126, 239, 233, 179, 229, 62, + 66, 155, 207, 117, 128, 56, 181, 190, 160, 96, 11, 248, 74, 23, 62, 253, 29, 132, 98, 192, 9, 202 + }; + + byte[] expected = + { + 62, 126, 65, 176, 51, 183, 83, 227, 72, 248, 20, 185, 151, 46, 246, 163, 240, 81, 250, 16, 8, 214, + 134, 85, 107, 139, 90, 218, 246, 126, 144, 43, 221, 71, 45, 56, 49, 182, 240, 142, 147, 48, 178, + 119, 100, 122, 137, 166, 28, 41, 135, 81, 24, 62, 34, 62, 248, 234, 68, 166, 93, 121, 237, 200 + }; + + // act + UpFilter.Decode(scanline, previousScanline); + + // assert + Assert.Equal(expected, scanline); + } + + private static void RunSubFilterTest() + { + // arrange + byte[] scanline = + { + 62, 23, 186, 150, 174, 4, 205, 59, 153, 134, 158, 86, 240, 173, 191, 58, 111, 183, 77, 37, 85, 23, + 93, 204, 110, 139, 9, 20, 87, 154, 176, 54, 207, 214, 40, 11, 179, 199, 7, 219, 174, 242, 112, 220, + 149, 5, 9, 110, 103, 107, 231, 241, 13, 70, 216, 39, 186, 237, 39, 34, 251, 185, 228, 254 + }; + + byte[] expected = + { + 62, 23, 186, 150, 174, 27, 135, 209, 71, 161, 37, 39, 55, 78, 228, 97, 166, 5, 49, 134, 251, 28, + 142, 82, 105, 167, 151, 102, 192, 65, 71, 156, 143, 23, 111, 167, 66, 222, 118, 130, 240, 208, 230, + 94, 133, 213, 239, 204, 236, 64, 214, 189, 249, 134, 174, 228, 179, 115, 213, 6, 174, 44, 185, 4 + }; + + // act + SubFilter.Decode(scanline, 4); + + // assert + Assert.Equal(expected, scanline); + } + + private static void RunPaethFilterTest() + { + // arrange + byte[] scanline = + { + 62, 23, 186, 150, 174, 4, 205, 59, 153, 134, 158, 86, 240, 173, 191, 58, 111, 183, 77, 37, 85, 23, + 93, 204, 110, 139, 9, 20, 87, 154, 176, 54, 207, 214, 40, 11, 179, 199, 7, 219, 174, 242, 112, 220, + 149, 5, 9, 110, 103, 107, 231, 241, 13, 70, 216, 39, 186, 237, 39, 34, 251, 185, 228, 254 + }; + + byte[] previousScanline = + { + 214, 103, 135, 26, 133, 179, 134, 168, 175, 114, 118, 99, 167, 129, 55, 105, 129, 154, 173, 235, + 179, 191, 41, 137, 253, 0, 81, 198, 159, 228, 224, 245, 14, 113, 5, 45, 126, 239, 233, 179, 229, 62, + 66, 155, 207, 117, 128, 56, 181, 190, 160, 96, 11, 248, 74, 23, 62, 253, 29, 132, 98, 192, 9, 202 + }; + + byte[] expected = + { + 62, 126, 65, 176, 51, 183, 14, 235, 30, 248, 172, 254, 14, 165, 53, 56, 125, 92, 250, 16, 8, 177, + 10, 220, 118, 139, 50, 240, 205, 126, 144, 43, 221, 71, 45, 54, 144, 182, 240, 142, 147, 48, 178, + 106, 40, 122, 187, 166, 143, 41, 162, 151, 24, 111, 34, 135, 248, 92, 68, 169, 243, 21, 1, 200 + }; + + // act + PaethFilter.Decode(scanline, previousScanline, 4); + + // assert + Assert.Equal(expected, scanline); } [Fact] public void AverageFilter_Works() => RunAverageFilterTest(); + [Fact] + public void UpFilter_Works() => RunUpFilterTest(); + + [Fact] + public void SubFilter_Works() => RunSubFilterTest(); + + [Fact] + public void PaethFilter_Works() => RunPaethFilterTest(); + #if SUPPORTS_RUNTIME_INTRINSICS [Fact] public void AverageFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.AllowAll); [Fact] public void AverageFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.DisableHWIntrinsic); + + [Fact] + public void UpFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunUpFilterTest, HwIntrinsics.AllowAll); + + [Fact] + public void UpFilter_WithoutAVX2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunUpFilterTest, HwIntrinsics.DisableAVX2); + + [Fact] + public void UpFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunUpFilterTest, HwIntrinsics.DisableHWIntrinsic); + + [Fact] + public void SubFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSubFilterTest, HwIntrinsics.AllowAll); + + [Fact] + public void SubFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSubFilterTest, HwIntrinsics.DisableHWIntrinsic); + + [Fact] + public void PaethFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPaethFilterTest, HwIntrinsics.AllowAll); + + [Fact] + public void PaethFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPaethFilterTest, HwIntrinsics.DisableHWIntrinsic); #endif } } From 26a742eb92bdea04268069ca96bfe9db0b90f298 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 25 Feb 2022 13:41:38 +0100 Subject: [PATCH 19/19] Additional tests for decoding png's with filter --- .../Codecs/Png/DecodeFilteredPng.cs | 6 ++-- .../Formats/Png/PngDecoderTests.cs | 36 +++++++++++++++++-- tests/ImageSharp.Tests/TestImages.cs | 18 +++------- tests/Images/Input/Png/PaethFilter4Bpp.png | 3 ++ tests/Images/Input/Png/SubFilter4Bpp.png | 3 ++ 5 files changed, 48 insertions(+), 18 deletions(-) create mode 100644 tests/Images/Input/Png/PaethFilter4Bpp.png create mode 100644 tests/Images/Input/Png/SubFilter4Bpp.png diff --git a/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs b/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs index 92eb4af07e..5f91a050ee 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs @@ -23,9 +23,9 @@ public class DecodeFilteredPng public void ReadImages() { this.filter0 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter0)); - this.filter1 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter1)); - this.filter2 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter2)); - this.filter3 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter4)); + this.filter1 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.SubFilter3BytesPerPixel)); + this.filter2 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.UpFilter)); + this.filter3 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.PaethFilter3BytesPerPixel)); this.averageFilter3bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter3BytesPerPixel)); this.averageFilter4bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter4BytesPerPixel)); } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 7d47087990..752036126f 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -112,8 +112,8 @@ public void Decode(TestImageProvider provider) } [Theory] - [WithFile(TestImages.Png.AverageFilter3BytesPerPixel, PixelTypes.Rgba64)] - [WithFile(TestImages.Png.AverageFilter4BytesPerPixel, PixelTypes.Rgba64)] + [WithFile(TestImages.Png.AverageFilter3BytesPerPixel, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.AverageFilter4BytesPerPixel, PixelTypes.Rgba32)] public void Decode_WithAverageFilter(TestImageProvider provider) where TPixel : unmanaged, IPixel { @@ -122,6 +122,38 @@ public void Decode_WithAverageFilter(TestImageProvider provider) image.CompareToOriginal(provider, ImageComparer.Exact); } + [Theory] + [WithFile(TestImages.Png.SubFilter3BytesPerPixel, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.SubFilter4BytesPerPixel, PixelTypes.Rgba32)] + public void Decode_WithSubFilter(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact); + } + + [Theory] + [WithFile(TestImages.Png.UpFilter, PixelTypes.Rgba32)] + public void Decode_WithUpFilter(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact); + } + + [Theory] + [WithFile(TestImages.Png.PaethFilter3BytesPerPixel, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.PaethFilter4BytesPerPixel, PixelTypes.Rgba32)] + public void Decode_WithPaethFilter(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact); + } + [Theory] [WithFile(TestImages.Png.GrayA8Bit, PixelTypes.Rgba32)] [WithFile(TestImages.Png.Gray1BitTrans, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index ad50fc5c71..9ea3c09f16 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -66,12 +66,13 @@ public static class Png // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string Filter0 = "Png/filter0.png"; - public const string Filter1 = "Png/filter1.png"; - public const string Filter2 = "Png/filter2.png"; + public const string SubFilter3BytesPerPixel = "Png/filter1.png"; + public const string SubFilter4BytesPerPixel = "Png/SubFilter4Bpp.png"; + public const string UpFilter = "Png/filter2.png"; public const string AverageFilter3BytesPerPixel = "Png/filter3.png"; - public const string Filter4 = "Png/filter4.png"; - public const string AverageFilter4BytesPerPixel = "Png/AverageFilter4Bpp.png"; + public const string PaethFilter3BytesPerPixel = "Png/filter4.png"; + public const string PaethFilter4BytesPerPixel = "Png/PaethFilter4Bpp.png"; // Paletted images also from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string PalettedTwoColor = "Png/basn3p01.png"; @@ -156,15 +157,6 @@ public static class Bad public const string ColorTypeOne = "Png/xc1n0g08.png"; public const string ColorTypeNine = "Png/xc9n2c08.png"; } - - public static readonly string[] All = - { - P1, Pd, Blur, Splash, Cross, - Powerpoint, SplashInterlaced, Interlaced, - Filter0, Filter1, Filter2, AverageFilter3BytesPerPixel, Filter4, - FilterVar, VimImage1, VimImage2, VersioningImage1, - VersioningImage2, Ratio4x1, Ratio1x4 - }; } public static class Jpeg diff --git a/tests/Images/Input/Png/PaethFilter4Bpp.png b/tests/Images/Input/Png/PaethFilter4Bpp.png new file mode 100644 index 0000000000..64c9f96ec8 --- /dev/null +++ b/tests/Images/Input/Png/PaethFilter4Bpp.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b2b0a1190854577d5181fe40af61c421d615a1a2727cf9be5ebe727eaafd00d +size 8624 diff --git a/tests/Images/Input/Png/SubFilter4Bpp.png b/tests/Images/Input/Png/SubFilter4Bpp.png new file mode 100644 index 0000000000..d9f2c7fa25 --- /dev/null +++ b/tests/Images/Input/Png/SubFilter4Bpp.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:053fac72ff62c66dacb41a6251efa249d5b31567e0222efbf5b1bef912c0bf77 +size 13013