From dc968678346de1e6a675fc19efb5b10667c5f4a6 Mon Sep 17 00:00:00 2001 From: Petar Tasev Date: Sun, 18 Apr 2021 11:56:19 -0700 Subject: [PATCH 01/25] Added overloads to Image.WrapMemory for IMemoryOwner. --- src/ImageSharp/Image.WrapMemory.cs | 76 +++++++++++++++++++ src/ImageSharp/Memory/ByteMemoryOwner{T}.cs | 54 +++++++++++++ .../Image/ImageTests.WrapMemory.cs | 26 +++++++ 3 files changed, 156 insertions(+) create mode 100644 src/ImageSharp/Memory/ByteMemoryOwner{T}.cs diff --git a/src/ImageSharp/Image.WrapMemory.cs b/src/ImageSharp/Image.WrapMemory.cs index b0efdb60db..d7af873bed 100644 --- a/src/ImageSharp/Image.WrapMemory.cs +++ b/src/ImageSharp/Image.WrapMemory.cs @@ -303,6 +303,82 @@ namespace SixLabors.ImageSharp where TPixel : unmanaged, IPixel => WrapMemory(Configuration.Default, byteMemory, width, height); + /// + /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels, + /// allowing to view/manipulate it as an instance. + /// The ownership of the is being transferred to the new instance, + /// meaning that the caller is not allowed to dispose . + /// It will be disposed together with the result image. + /// + /// The pixel type + /// The + /// The that is being transferred to the image + /// The width of the memory image. + /// The height of the memory image. + /// The + /// The configuration is null. + /// The metadata is null. + /// An instance + public static Image WrapMemory( + Configuration configuration, + IMemoryOwner byteMemoryOwner, + int width, + int height, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + + var pixelMemoryOwner = new ByteMemoryOwner(byteMemoryOwner); + + Guard.IsTrue(pixelMemoryOwner.Memory.Length >= width * height, nameof(pixelMemoryOwner), "The length of the input memory is less than the specified image size"); + + var memorySource = MemoryGroup.Wrap(pixelMemoryOwner); + return new Image(configuration, memorySource, width, height, metadata); + } + + /// + /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels, + /// allowing to view/manipulate it as an instance. + /// The ownership of the is being transferred to the new instance, + /// meaning that the caller is not allowed to dispose . + /// It will be disposed together with the result image. + /// + /// The pixel type. + /// The + /// The that is being transferred to the image. + /// The width of the memory image. + /// The height of the memory image. + /// The configuration is null. + /// An instance + public static Image WrapMemory( + Configuration configuration, + IMemoryOwner byteMemoryOwner, + int width, + int height) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, byteMemoryOwner, width, height, new ImageMetadata()); + + /// + /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels, + /// allowing to view/manipulate it as an instance. + /// The ownership of the is being transferred to the new instance, + /// meaning that the caller is not allowed to dispose . + /// It will be disposed together with the result image. + /// + /// The pixel type + /// The that is being transferred to the image. + /// The width of the memory image. + /// The height of the memory image. + /// An instance. + public static Image WrapMemory( + IMemoryOwner byteMemoryOwner, + int width, + int height) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, byteMemoryOwner, width, height); + /// /// /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as diff --git a/src/ImageSharp/Memory/ByteMemoryOwner{T}.cs b/src/ImageSharp/Memory/ByteMemoryOwner{T}.cs new file mode 100644 index 0000000000..bcf8eabf12 --- /dev/null +++ b/src/ImageSharp/Memory/ByteMemoryOwner{T}.cs @@ -0,0 +1,54 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Text; + +namespace SixLabors.ImageSharp.Memory +{ + /// + /// A custom that can wrap of instances + /// and cast them to be for any arbitrary unmanaged value type. + /// + /// The value type to use when casting the wrapped instance. + internal sealed class ByteMemoryOwner : IMemoryOwner + where T : unmanaged + { + private readonly IMemoryOwner memoryOwner; + private readonly ByteMemoryManager memoryManager; + private bool disposedValue; + + /// + /// Initializes a new instance of the class. + /// + /// The of instance to wrap. + public ByteMemoryOwner(IMemoryOwner memoryOwner) + { + this.memoryOwner = memoryOwner; + this.memoryManager = new ByteMemoryManager(memoryOwner.Memory); + } + + /// + public Memory Memory => this.memoryManager.Memory; + + private void Dispose(bool disposing) + { + if (!this.disposedValue) + { + if (disposing) + { + this.memoryOwner.Dispose(); + } + + this.disposedValue = true; + } + } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs index 17c73cc834..1b40f43ab1 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs @@ -413,6 +413,32 @@ namespace SixLabors.ImageSharp.Tests Image.WrapMemory(memory, height, width); } + [Theory] + [InlineData(0, 5, 5)] + [InlineData(20, 5, 5)] + [InlineData(1023, 32, 32)] + public void WrapMemory_IMemoryOwnerOfByte_InvalidSize(int size, int height, int width) + { + var array = new byte[size * Unsafe.SizeOf()]; + var memory = new TestMemoryOwner { Memory = array }; + + Assert.Throws(() => Image.WrapMemory(memory, height, width)); + } + + [Theory] + [InlineData(25, 5, 5)] + [InlineData(26, 5, 5)] + [InlineData(2, 1, 1)] + [InlineData(1024, 32, 32)] + [InlineData(2048, 32, 32)] + public void WrapMemory_IMemoryOwnerOfByte_ValidSize(int size, int height, int width) + { + var array = new byte[size * Unsafe.SizeOf()]; + var memory = new TestMemoryOwner { Memory = array }; + + Image.WrapMemory(memory, height, width); + } + [Theory] [InlineData(0, 5, 5)] [InlineData(20, 5, 5)] From 402782acf5dff895897124e66b6429d810dd9e1e Mon Sep 17 00:00:00 2001 From: Petar Tasev Date: Sun, 18 Apr 2021 12:00:33 -0700 Subject: [PATCH 02/25] Removed unused using statements. --- src/ImageSharp/Memory/ByteMemoryOwner{T}.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ImageSharp/Memory/ByteMemoryOwner{T}.cs b/src/ImageSharp/Memory/ByteMemoryOwner{T}.cs index bcf8eabf12..a4761740ab 100644 --- a/src/ImageSharp/Memory/ByteMemoryOwner{T}.cs +++ b/src/ImageSharp/Memory/ByteMemoryOwner{T}.cs @@ -1,7 +1,5 @@ using System; using System.Buffers; -using System.Collections.Generic; -using System.Text; namespace SixLabors.ImageSharp.Memory { From ae30a49357ee3eeadc305c3aee8fe5c3cdce97fc Mon Sep 17 00:00:00 2001 From: Petar Tasev Date: Sun, 18 Apr 2021 12:03:16 -0700 Subject: [PATCH 03/25] Added file header. --- src/ImageSharp/Memory/ByteMemoryOwner{T}.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ImageSharp/Memory/ByteMemoryOwner{T}.cs b/src/ImageSharp/Memory/ByteMemoryOwner{T}.cs index a4761740ab..01262eb586 100644 --- a/src/ImageSharp/Memory/ByteMemoryOwner{T}.cs +++ b/src/ImageSharp/Memory/ByteMemoryOwner{T}.cs @@ -1,3 +1,6 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + using System; using System.Buffers; From 0871b85bdfd6df354d22f18456cb3390c567b8c6 Mon Sep 17 00:00:00 2001 From: Petar Tasev Date: Thu, 22 Apr 2021 19:34:48 -0700 Subject: [PATCH 04/25] Address code review comments. --- src/ImageSharp/Image.WrapMemory.cs | 2 +- src/ImageSharp/Memory/ByteMemoryOwner{T}.cs | 1 - .../Image/ImageTests.WrapMemory.cs | 29 +++++++++++++++---- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Image.WrapMemory.cs b/src/ImageSharp/Image.WrapMemory.cs index d7af873bed..115d51921c 100644 --- a/src/ImageSharp/Image.WrapMemory.cs +++ b/src/ImageSharp/Image.WrapMemory.cs @@ -332,7 +332,7 @@ namespace SixLabors.ImageSharp var pixelMemoryOwner = new ByteMemoryOwner(byteMemoryOwner); - Guard.IsTrue(pixelMemoryOwner.Memory.Length >= width * height, nameof(pixelMemoryOwner), "The length of the input memory is less than the specified image size"); + Guard.IsTrue(pixelMemoryOwner.Memory.Length >= (long)width * height, nameof(pixelMemoryOwner), "The length of the input memory is less than the specified image size"); var memorySource = MemoryGroup.Wrap(pixelMemoryOwner); return new Image(configuration, memorySource, width, height, metadata); diff --git a/src/ImageSharp/Memory/ByteMemoryOwner{T}.cs b/src/ImageSharp/Memory/ByteMemoryOwner{T}.cs index 01262eb586..3cf62bbc1f 100644 --- a/src/ImageSharp/Memory/ByteMemoryOwner{T}.cs +++ b/src/ImageSharp/Memory/ByteMemoryOwner{T}.cs @@ -49,7 +49,6 @@ namespace SixLabors.ImageSharp.Memory { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method this.Dispose(disposing: true); - GC.SuppressFinalize(this); } } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs index 1b40f43ab1..17be2fa2b0 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs @@ -380,11 +380,11 @@ namespace SixLabors.ImageSharp.Tests private class TestMemoryOwner : IMemoryOwner { + public bool Disposed { get; private set; } + public Memory Memory { get; set; } - public void Dispose() - { - } + public void Dispose() => this.Disposed = true; } [Theory] @@ -433,10 +433,29 @@ namespace SixLabors.ImageSharp.Tests [InlineData(2048, 32, 32)] public void WrapMemory_IMemoryOwnerOfByte_ValidSize(int size, int height, int width) { - var array = new byte[size * Unsafe.SizeOf()]; + var random = new Random(); + var pixelSize = Unsafe.SizeOf(); + var array = new byte[size * pixelSize]; var memory = new TestMemoryOwner { Memory = array }; - Image.WrapMemory(memory, height, width); + using (var img = Image.WrapMemory(memory, width, height)) + { + Assert.Equal(width, img.Width); + Assert.Equal(height, img.Height); + + for (int i = 0; i < height; ++i) + { + var arrayIndex = pixelSize * width * i; + var expected = (byte)random.Next(0, 256); + + Span rowSpan = img.GetPixelRowSpan(i); + array[arrayIndex] = expected; + + Assert.Equal(expected, rowSpan[0].R); + } + } + + Assert.True(memory.Disposed); } [Theory] From 70c6616fa96dbe0f426d52650ff21f8b188d7d02 Mon Sep 17 00:00:00 2001 From: Petar Tasev Date: Fri, 23 Apr 2021 08:49:22 -0700 Subject: [PATCH 05/25] Changed test to compare mem locations, and added same test to MemOwner of TPixel. --- .../Image/ImageTests.WrapMemory.cs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs index 17be2fa2b0..bb75578a4b 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs @@ -410,7 +410,24 @@ namespace SixLabors.ImageSharp.Tests var array = new Rgba32[size]; var memory = new TestMemoryOwner { Memory = array }; - Image.WrapMemory(memory, height, width); + using (var img = Image.WrapMemory(memory, width, height)) + { + Assert.Equal(width, img.Width); + Assert.Equal(height, img.Height); + + for (int i = 0; i < height; ++i) + { + var arrayIndex = width * i; + + Span rowSpan = img.GetPixelRowSpan(i); + ref Rgba32 r0 = ref rowSpan[0]; + ref Rgba32 r1 = ref array[arrayIndex]; + + Assert.True(Unsafe.AreSame(ref r0, ref r1)); + } + } + + Assert.True(memory.Disposed); } [Theory] @@ -433,7 +450,6 @@ namespace SixLabors.ImageSharp.Tests [InlineData(2048, 32, 32)] public void WrapMemory_IMemoryOwnerOfByte_ValidSize(int size, int height, int width) { - var random = new Random(); var pixelSize = Unsafe.SizeOf(); var array = new byte[size * pixelSize]; var memory = new TestMemoryOwner { Memory = array }; @@ -446,12 +462,12 @@ namespace SixLabors.ImageSharp.Tests for (int i = 0; i < height; ++i) { var arrayIndex = pixelSize * width * i; - var expected = (byte)random.Next(0, 256); Span rowSpan = img.GetPixelRowSpan(i); - array[arrayIndex] = expected; + ref Rgba32 r0 = ref rowSpan[0]; + ref Rgba32 r1 = ref Unsafe.As(ref array[arrayIndex]); - Assert.Equal(expected, rowSpan[0].R); + Assert.True(Unsafe.AreSame(ref r0, ref r1)); } } From 4db99dbbf552c2e761985536f797c6a01e2c2f2a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 23 Apr 2021 23:21:29 +0100 Subject: [PATCH 06/25] Update shared-infrastructure --- shared-infrastructure | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-infrastructure b/shared-infrastructure index 41fff7bf7d..0ea21d9e2a 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit 41fff7bf7ddb1d118898db1ddba43b95ba6ed0bb +Subproject commit 0ea21d9e2a76d307dae9cfb74e33234b259352b7 From 9ed5fe95cf3409eee0030568b583542d9dbd1140 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 24 Apr 2021 14:14:32 +0100 Subject: [PATCH 07/25] Update README.md --- README.md | 47 ++++++++++------------------------------------- 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 6cc8e53047..ab16bbb76a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

+

SixLabors.ImageSharp
@@ -26,9 +26,16 @@ Built against [.NET Standard 1.3](https://docs.microsoft.com/en-us/dotnet/standa ## License - ImageSharp is licensed under the [Apache License, Version 2.0](https://opensource.org/licenses/Apache-2.0) -- An alternative Commercial License can be purchased for projects and applications requiring support. +- An alternative Commercial Support License can be purchased **for projects and applications requiring support**. Please visit https://sixlabors.com/pricing for details. +## Support Six Labors + +Support the efforts of the development of the Six Labors projects. + - [Purchase a Commercial Support License :heart:](https://sixlabors.com/pricing/) + - [Become a sponsor via GitHub Sponsors :heart:]( https://github.com/sponsors/SixLabors) + - [Become a sponsor via Open Collective :heart:](https://opencollective.com/sixlabors) + ## Documentation - [Detailed documentation](https://sixlabors.github.io/docs/) for the ImageSharp API is available. This includes additional conceptual documentation to help you get started. @@ -57,7 +64,7 @@ If you prefer, you can compile ImageSharp yourself (please do and help!) - Using [Visual Studio 2019](https://visualstudio.microsoft.com/vs/) - Make sure you have the latest version installed - - Make sure you have [the .NET Core 3.1 SDK](https://www.microsoft.com/net/core#windows) installed + - Make sure you have [the .NET 5 SDK](https://www.microsoft.com/net/core#windows) installed Alternatively, you can work from command line and/or with a lightweight editor on **both Linux/Unix and Windows**: @@ -96,40 +103,6 @@ Please... Spread the word, contribute algorithms, submit performance improvement - [Scott Williams](https://github.com/tocsoft) - [Brian Popow](https://github.com/brianpopow) -## Sponsor Six Labors - -Support the efforts of the development of the Six Labors projects. [[Become a sponsor :heart:](https://opencollective.com/sixlabors#sponsor)] - -### Platinum Sponsors -Become a platinum sponsor with a monthly donation of $2000 (providing 32 hours of maintenance and development) and get 2 hours of dedicated support (remote support available through chat or screen-sharing) per month. - -In addition you get your logo (large) on our README on GitHub and the home page (large) of sixlabors.com - - - -### Gold Sponsors -Become a gold sponsor with a monthly donation of $1000 (providing 16 hours of maintenance and development) and get 1 hour of dedicated support (remote support available through chat or screen-sharing) per month. - -In addition you get your logo (large) on our README on GitHub and the home page (medium) of sixlabors.com - - - -### Silver Sponsors -Become a silver sponsor with a monthly donation of $500 (providing 8 hours of maintenance and development) and get your logo (medium) on our README on GitHub and the product pages of sixlabors.com - -### Bronze Sponsors -Become a bronze sponsor with a monthly donation of $100 and get your logo (small) on our README on GitHub. - - - - - - - - - - - From c05d4ddeb5eef4e6c2a0fa3c192c4aa0394473b8 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 4 May 2021 20:12:00 +0200 Subject: [PATCH 08/25] Add support for encoding 4 bit per pixel bitmaps --- src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs | 9 +- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 98 ++++++++++++++++--- .../Formats/Bmp/BmpEncoderTests.cs | 16 ++- 3 files changed, 104 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs index 6fdf8d6342..87f3b7e319 100644 --- a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs +++ b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.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.Bmp @@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp ///

public enum BmpBitsPerPixel : short { + /// + /// 4 bits per pixel. + /// + Pixel4 = 4, + /// /// 8 bits per pixel. Each pixel consists of 1 byte. /// @@ -28,4 +33,4 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// Pixel32 = 32 } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 7819b1ebdb..7733b718ae 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -51,6 +51,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// private const int ColorPaletteSize8Bit = 1024; + /// + /// The color palette for an 4 bit image will have 16 entry's with 4 bytes for each entry. + /// + private const int ColorPaletteSize4Bit = 64; + /// /// Used for allocating memory during processing operations. /// @@ -107,7 +112,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.configuration = image.GetConfiguration(); ImageMetadata metadata = image.Metadata; BmpMetadata bmpMetadata = metadata.GetBmpMetadata(); - this.bitsPerPixel = this.bitsPerPixel ?? bmpMetadata.BitsPerPixel; + this.bitsPerPixel ??= bmpMetadata.BitsPerPixel; short bpp = (short)this.bitsPerPixel; int bytesPerLine = 4 * (((image.Width * bpp) + 31) / 32); @@ -166,7 +171,15 @@ namespace SixLabors.ImageSharp.Formats.Bmp infoHeader.Compression = BmpCompression.BitFields; } - int colorPaletteSize = this.bitsPerPixel == BmpBitsPerPixel.Pixel8 ? ColorPaletteSize8Bit : 0; + int colorPaletteSize = 0; + if (this.bitsPerPixel == BmpBitsPerPixel.Pixel8) + { + colorPaletteSize = ColorPaletteSize8Bit; + } + else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel4) + { + colorPaletteSize = ColorPaletteSize4Bit; + } var fileHeader = new BmpFileHeader( type: BmpConstants.TypeMarkers.Bitmap, @@ -224,6 +237,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp case BmpBitsPerPixel.Pixel8: this.Write8Bit(stream, image); break; + + case BmpBitsPerPixel.Pixel4: + this.Write4BitColor(stream, image); + break; } } @@ -344,16 +361,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration); using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds()); - ReadOnlySpan quantizedColors = quantized.Palette.Span; - var quantizedColorBytes = quantizedColors.Length * 4; - PixelOperations.Instance.ToBgra32(this.configuration, quantizedColors, MemoryMarshal.Cast(colorPalette.Slice(0, quantizedColorBytes))); - Span colorPaletteAsUInt = MemoryMarshal.Cast(colorPalette); - for (int i = 0; i < colorPaletteAsUInt.Length; i++) - { - colorPaletteAsUInt[i] = colorPaletteAsUInt[i] & 0x00FFFFFF; // Padding byte, always 0. - } - - stream.Write(colorPalette); + ReadOnlySpan quantizedColorPalette = quantized.Palette.Span; + this.WriteColorPalette(stream, quantizedColorPalette, colorPalette); for (int y = image.Height - 1; y >= 0; y--) { @@ -404,5 +413,70 @@ namespace SixLabors.ImageSharp.Formats.Bmp } } } + + /// + /// Writes an 4 Bit color image with a color palette. The color palette has 16 entry's with 4 bytes for each entry. + /// + /// The type of the pixel. + /// The to write to. + /// The containing pixel data. + private void Write4BitColor(Stream stream, ImageFrame image) + where TPixel : unmanaged, IPixel + { + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration, new QuantizerOptions() + { + MaxColors = 16 + }); + using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds()); + using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize4Bit, AllocationOptions.Clean); + + Span colorPalette = colorPaletteBuffer.GetSpan(); + ReadOnlySpan quantizedColorPalette = quantized.Palette.Span; + this.WriteColorPalette(stream, quantizedColorPalette, colorPalette); + + ReadOnlySpan pixelRowSpan = quantized.GetPixelRowSpan(0); + int rowPadding = pixelRowSpan.Length % 2 != 0 ? this.padding - 1 : this.padding; + for (int y = image.Height - 1; y >= 0; y--) + { + pixelRowSpan = quantized.GetPixelRowSpan(y); + + int endIdx = pixelRowSpan.Length % 2 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 1; + for (int i = 0; i < endIdx; i += 2) + { + stream.WriteByte((byte)((pixelRowSpan[i] << 4) | pixelRowSpan[i + 1])); + } + + if (pixelRowSpan.Length % 2 != 0) + { + stream.WriteByte((byte)((pixelRowSpan[pixelRowSpan.Length - 1] << 4) | 0)); + } + + for (int i = 0; i < rowPadding; i++) + { + stream.WriteByte(0); + } + } + } + + /// + /// Writes the color palette to the stream. The color palette has 4 bytes for each entry. + /// + /// The type of the pixel. + /// The to write to. + /// The color palette from the quantized image. + /// A temporary byte span to write the color palette to. + private void WriteColorPalette(Stream stream, ReadOnlySpan quantizedColorPalette, Span colorPalette) + where TPixel : unmanaged, IPixel + { + int quantizedColorBytes = quantizedColorPalette.Length * 4; + PixelOperations.Instance.ToBgra32(this.configuration, quantizedColorPalette, MemoryMarshal.Cast(colorPalette.Slice(0, quantizedColorBytes))); + Span colorPaletteAsUInt = MemoryMarshal.Cast(colorPalette); + for (int i = 0; i < colorPaletteAsUInt.Length; i++) + { + colorPaletteAsUInt[i] = colorPaletteAsUInt[i] & 0x00FFFFFF; // Padding byte, always 0. + } + + stream.Write(colorPalette); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index fa63642bd2..35853413ea 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -13,7 +13,6 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; using Xunit; -using Xunit.Abstractions; using static SixLabors.ImageSharp.Tests.TestImages.Bmp; @@ -41,14 +40,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public static readonly TheoryData BmpBitsPerPixelFiles = new TheoryData { + { Rgb16, BmpBitsPerPixel.Pixel16 }, { Car, BmpBitsPerPixel.Pixel24 }, { Bit32Rgb, BmpBitsPerPixel.Pixel32 } }; - public BmpEncoderTests(ITestOutputHelper output) => this.Output = output; - - private ITestOutputHelper Output { get; } - [Theory] [MemberData(nameof(RatioFiles))] public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit) @@ -175,6 +171,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp bitsPerPixel, supportTransparency: false); + [Theory] + [WithFile(Bit4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel4)] + public void Encode_4Bit_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) + where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); + + [Theory] + [WithFile(Bit4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel4)] + public void Encode_4Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) + where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); + [Theory] [WithFile(Bit8Gs, PixelTypes.L8, BmpBitsPerPixel.Pixel8)] public void Encode_8BitGray_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) From 04e6b4135a120019885b1a0e88d0ca020d05cc08 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 4 May 2021 20:26:28 +0200 Subject: [PATCH 09/25] Make sure bitmap encoder preserves 4 bits per pixel, when the input is 4 bit --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 5 +++-- tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 0be0385725..17ba96312a 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -1304,8 +1304,9 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.bmpMetadata = this.metadata.GetBmpMetadata(); this.bmpMetadata.InfoHeaderType = infoHeaderType; - // We can only encode at these bit rates so far (1 bit and 4 bit are still missing). - if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel8) + // We can only encode at these bit rates so far (1 bit per pixel is still missing). + if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel4) + || bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel8) || bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel16) || bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel24) || bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel32)) diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index 35853413ea..82a93102d1 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -40,6 +40,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public static readonly TheoryData BmpBitsPerPixelFiles = new TheoryData { + { Bit4, BmpBitsPerPixel.Pixel4 }, + { Bit8, BmpBitsPerPixel.Pixel8 }, { Rgb16, BmpBitsPerPixel.Pixel16 }, { Car, BmpBitsPerPixel.Pixel24 }, { Bit32Rgb, BmpBitsPerPixel.Pixel32 } From 60bd3946dedd7fa293e4505d30a0166e0ee7be7b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 4 May 2021 20:38:58 +0200 Subject: [PATCH 10/25] Execute 4bit bitmap encoder tests only on windows --- .../Formats/Bmp/BmpEncoderTests.cs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index 82a93102d1..f439b3b199 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -175,13 +175,31 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp [Theory] [WithFile(Bit4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel4)] - public void Encode_4Bit_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) - where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); + public void Encode_4Bit_WithV3Header_Works( + TestImageProvider provider, + BmpBitsPerPixel bitsPerPixel) + where TPixel : unmanaged, IPixel + { + // The Magick Reference Decoder can not decode 4-Bit bitmaps, so only execute this on windows. + if (TestEnvironment.IsWindows) + { + TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); + } + } [Theory] [WithFile(Bit4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel4)] - public void Encode_4Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) - where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); + public void Encode_4Bit_WithV4Header_Works( + TestImageProvider provider, + BmpBitsPerPixel bitsPerPixel) + where TPixel : unmanaged, IPixel + { + // The Magick Reference Decoder can not decode 4-Bit bitmaps, so only execute this on windows. + if (TestEnvironment.IsWindows) + { + TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); + } + } [Theory] [WithFile(Bit8Gs, PixelTypes.L8, BmpBitsPerPixel.Pixel8)] From ea8bef4321325a2331436239f76ad3edb944745c Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 5 May 2021 12:28:38 +0200 Subject: [PATCH 11/25] Add support for encoding 1 bit per pixel bitmaps --- src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs | 5 ++ src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 11 +-- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 89 +++++++++++++++++-- .../Formats/Bmp/IBmpEncoderOptions.cs | 7 +- .../Formats/Bmp/BmpEncoderTests.cs | 47 +++++++++- 5 files changed, 139 insertions(+), 20 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs index 87f3b7e319..7801e48a91 100644 --- a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs +++ b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs @@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// public enum BmpBitsPerPixel : short { + /// + /// 1 bit per pixel. + /// + Pixel1 = 1, + /// /// 4 bits per pixel. /// diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 17ba96312a..f6fefda485 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -1303,16 +1303,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp short bitsPerPixel = this.infoHeader.BitsPerPixel; this.bmpMetadata = this.metadata.GetBmpMetadata(); this.bmpMetadata.InfoHeaderType = infoHeaderType; - - // We can only encode at these bit rates so far (1 bit per pixel is still missing). - if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel4) - || bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel8) - || bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel16) - || bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel24) - || bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel32)) - { - this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel; - } + this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel; } /// diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 7733b718ae..b407ad221f 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -56,6 +56,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// private const int ColorPaletteSize4Bit = 64; + /// + /// The color palette for an 1 bit image will have 2 entry's with 4 bytes for each entry. + /// + private const int ColorPaletteSize1Bit = 8; + /// /// Used for allocating memory during processing operations. /// @@ -79,7 +84,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp private readonly bool writeV4Header; /// - /// The quantizer for reducing the color count for 8-Bit images. + /// The quantizer for reducing the color count for 8-Bit, 4-Bit and 1-Bit images. /// private readonly IQuantizer quantizer; @@ -180,6 +185,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp { colorPaletteSize = ColorPaletteSize4Bit; } + else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel1) + { + colorPaletteSize = ColorPaletteSize1Bit; + } var fileHeader = new BmpFileHeader( type: BmpConstants.TypeMarkers.Bitmap, @@ -241,6 +250,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp case BmpBitsPerPixel.Pixel4: this.Write4BitColor(stream, image); break; + + case BmpBitsPerPixel.Pixel1: + this.Write1BitColor(stream, image); + break; } } @@ -325,7 +338,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Writes an 8 Bit image with a color palette. The color palette has 256 entry's with 4 bytes for each entry. + /// Writes an 8 bit image with a color palette. The color palette has 256 entry's with 4 bytes for each entry. /// /// The type of the pixel. /// The to write to. @@ -349,7 +362,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Writes an 8 Bit color image with a color palette. The color palette has 256 entry's with 4 bytes for each entry. + /// Writes an 8 bit color image with a color palette. The color palette has 256 entry's with 4 bytes for each entry. /// /// The type of the pixel. /// The to write to. @@ -377,7 +390,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Writes an 8 Bit gray image with a color palette. The color palette has 256 entry's with 4 bytes for each entry. + /// Writes an 8 bit gray image with a color palette. The color palette has 256 entry's with 4 bytes for each entry. /// /// The type of the pixel. /// The to write to. @@ -415,7 +428,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Writes an 4 Bit color image with a color palette. The color palette has 16 entry's with 4 bytes for each entry. + /// Writes an 4 bit color image with a color palette. The color palette has 16 entry's with 4 bytes for each entry. /// /// The type of the pixel. /// The to write to. @@ -458,6 +471,52 @@ namespace SixLabors.ImageSharp.Formats.Bmp } } + /// + /// Writes a 1 bit image with a color palette. The color palette has 2 entry's with 4 bytes for each entry. + /// + /// The type of the pixel. + /// The to write to. + /// The containing pixel data. + private void Write1BitColor(Stream stream, ImageFrame image) + where TPixel : unmanaged, IPixel + { + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration, new QuantizerOptions() + { + MaxColors = 2 + }); + using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds()); + using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize1Bit, AllocationOptions.Clean); + + Span colorPalette = colorPaletteBuffer.GetSpan(); + ReadOnlySpan quantizedColorPalette = quantized.Palette.Span; + this.WriteColorPalette(stream, quantizedColorPalette, colorPalette); + + ReadOnlySpan quantizedPixelRow = quantized.GetPixelRowSpan(0); + int rowPadding = quantizedPixelRow.Length % 8 != 0 ? this.padding - 1 : this.padding; + for (int y = image.Height - 1; y >= 0; y--) + { + quantizedPixelRow = quantized.GetPixelRowSpan(y); + + int endIdx = quantizedPixelRow.Length % 8 == 0 ? quantizedPixelRow.Length : quantizedPixelRow.Length - 8; + for (int i = 0; i < endIdx; i += 8) + { + Write1BitPalette(stream, i, i + 8, quantizedPixelRow); + } + + if (quantizedPixelRow.Length % 8 != 0) + { + int startIdx = quantizedPixelRow.Length - 7; + endIdx = quantizedPixelRow.Length; + Write1BitPalette(stream, startIdx, endIdx, quantizedPixelRow); + } + + for (int i = 0; i < rowPadding; i++) + { + stream.WriteByte(0); + } + } + } + /// /// Writes the color palette to the stream. The color palette has 4 bytes for each entry. /// @@ -478,5 +537,25 @@ namespace SixLabors.ImageSharp.Formats.Bmp stream.Write(colorPalette); } + + /// + /// Writes a 1-bit palette. + /// + /// The stream to write the palette to. + /// The start index. + /// The end index. + /// A quantized pixel row. + private static void Write1BitPalette(Stream stream, int startIdx, int endIdx, ReadOnlySpan quantizedPixelRow) + { + int shift = 7; + byte indices = 0; + for (int j = startIdx; j < endIdx; j++) + { + indices = (byte)(indices | ((byte)(quantizedPixelRow[j] & 1) << shift)); + shift--; + } + + stream.WriteByte(indices); + } } } diff --git a/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs b/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs index d4a22d66ea..ca1fbd0ded 100644 --- a/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs +++ b/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -24,8 +24,9 @@ namespace SixLabors.ImageSharp.Formats.Bmp bool SupportTransparency { get; } /// - /// Gets the quantizer for reducing the color count for 8-Bit images. + /// Gets the quantizer for reducing the color count for 8-Bit, 4-Bit, and 1-Bit images. + /// Defaults to the Octree Quantizer. /// IQuantizer Quantizer { get; } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index f439b3b199..31e66896d0 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -40,6 +40,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public static readonly TheoryData BmpBitsPerPixelFiles = new TheoryData { + { Bit1, BmpBitsPerPixel.Pixel1 }, { Bit4, BmpBitsPerPixel.Pixel4 }, { Bit8, BmpBitsPerPixel.Pixel8 }, { Rgb16, BmpBitsPerPixel.Pixel16 }, @@ -201,6 +202,42 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp } } + [Theory] + [WithFile(Bit1, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel1)] + public void Encode_1Bit_WithV3Header_Works( + TestImageProvider provider, + BmpBitsPerPixel bitsPerPixel) + where TPixel : unmanaged, IPixel + { + // The Magick Reference Decoder can not decode 1-Bit bitmaps, so only execute this on windows. + if (TestEnvironment.IsWindows) + { + TestBmpEncoderCore( + provider, + bitsPerPixel, + supportTransparency: false, + quantizer: KnownQuantizers.Wu); // using the wu quantizer, because the octree seems to have problems with just two colors. + } + } + + [Theory] + [WithFile(Bit1, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel1)] + public void Encode_1Bit_WithV4Header_Works( + TestImageProvider provider, + BmpBitsPerPixel bitsPerPixel) + where TPixel : unmanaged, IPixel + { + // The Magick Reference Decoder can not decode 1-Bit bitmaps, so only execute this on windows. + if (TestEnvironment.IsWindows) + { + TestBmpEncoderCore( + provider, + bitsPerPixel, + supportTransparency: true, + quantizer: KnownQuantizers.Wu); // using the wu quantizer, because the octree seems to have problems with just two colors. + } + } + [Theory] [WithFile(Bit8Gs, PixelTypes.L8, BmpBitsPerPixel.Pixel8)] public void Encode_8BitGray_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) @@ -297,7 +334,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp private static void TestBmpEncoderCore( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel, - bool supportTransparency = true, + bool supportTransparency = true, // if set to true, will write a V4 header, otherwise a V3 header. + IQuantizer quantizer = null, ImageComparer customComparer = null) where TPixel : unmanaged, IPixel { @@ -309,7 +347,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp image.Mutate(c => c.MakeOpaque()); } - var encoder = new BmpEncoder { BitsPerPixel = bitsPerPixel, SupportTransparency = supportTransparency }; + var encoder = new BmpEncoder + { + BitsPerPixel = bitsPerPixel, + SupportTransparency = supportTransparency, + Quantizer = quantizer ?? KnownQuantizers.Octree + }; // Does DebugSave & load reference CompareToReferenceInput(): image.VerifyEncoder(provider, "bmp", bitsPerPixel, encoder, customComparer); From 33192d396bf343c28faf26c8dcf7bd96aee0b3f0 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 5 May 2021 19:07:07 +0200 Subject: [PATCH 12/25] Switch default quantizer for the bitmap encoder to Wu-quantizer --- src/ImageSharp/Formats/Bmp/BmpEncoder.cs | 2 +- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 2 +- src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs | 1 - .../Formats/Bmp/BmpEncoderTests.cs | 14 +++----------- 4 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs index 2f5c4b7cf7..f256ed9f81 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs @@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Gets or sets the quantizer for reducing the color count for 8-Bit images. - /// Defaults to OctreeQuantizer. + /// Defaults to Wu Quantizer. /// public IQuantizer Quantizer { get; set; } diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index b407ad221f..5cf54388d3 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -98,7 +98,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.memoryAllocator = memoryAllocator; this.bitsPerPixel = options.BitsPerPixel; this.writeV4Header = options.SupportTransparency; - this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; + this.quantizer = options.Quantizer ?? KnownQuantizers.Wu; } /// diff --git a/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs b/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs index ca1fbd0ded..30aa70452e 100644 --- a/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs +++ b/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs @@ -25,7 +25,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Gets the quantizer for reducing the color count for 8-Bit, 4-Bit, and 1-Bit images. - /// Defaults to the Octree Quantizer. /// IQuantizer Quantizer { get; } } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index 31e66896d0..4eb3b900e1 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -212,11 +212,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp // The Magick Reference Decoder can not decode 1-Bit bitmaps, so only execute this on windows. if (TestEnvironment.IsWindows) { - TestBmpEncoderCore( - provider, - bitsPerPixel, - supportTransparency: false, - quantizer: KnownQuantizers.Wu); // using the wu quantizer, because the octree seems to have problems with just two colors. + TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); } } @@ -230,11 +226,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp // The Magick Reference Decoder can not decode 1-Bit bitmaps, so only execute this on windows. if (TestEnvironment.IsWindows) { - TestBmpEncoderCore( - provider, - bitsPerPixel, - supportTransparency: true, - quantizer: KnownQuantizers.Wu); // using the wu quantizer, because the octree seems to have problems with just two colors. + TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); } } @@ -351,7 +343,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp { BitsPerPixel = bitsPerPixel, SupportTransparency = supportTransparency, - Quantizer = quantizer ?? KnownQuantizers.Octree + Quantizer = quantizer ?? KnownQuantizers.Wu }; // Does DebugSave & load reference CompareToReferenceInput(): From 02d0a808c3d3ed406db7f7e7a7412e3567cce67b Mon Sep 17 00:00:00 2001 From: TechPizza Date: Sun, 9 May 2021 02:26:24 +0200 Subject: [PATCH 13/25] Vectorized PaethFilter --- .../Formats/Png/Filters/PaethFilter.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs index fab6788061..7562c47558 100644 --- a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -82,6 +83,43 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters sum += Numerics.Abs(unchecked((sbyte)res)); } +#if SUPPORTS_RUNTIME_INTRINSICS + if (Vector.IsHardwareAccelerated) + { + Vector sumAccumulator = Vector.Zero; + + for (int xLeft = x - bytesPerPixel; x + Vector.Count <= scanline.Length; xLeft += Vector.Count) + { + var scan = new Vector(scanline.Slice(x)); + var left = new Vector(scanline.Slice(xLeft)); + var above = new Vector(previousScanline.Slice(x)); + var upperLeft = new Vector(previousScanline.Slice(xLeft)); + + Vector res = scan - PaethPredictor(left, above, upperLeft); + res.CopyTo(result.Slice(x + 1)); // + 1 to skip filter type + x += Vector.Count; + + Vector.Widen( + Vector.Abs(Vector.AsVectorSByte(res)), + out Vector shortLow, + out Vector shortHigh); + + Vector.Widen(shortLow, out Vector intLow, out Vector intHigh); + sumAccumulator += intLow; + sumAccumulator += intHigh; + + Vector.Widen(shortHigh, out intLow, out intHigh); + sumAccumulator += intLow; + sumAccumulator += intHigh; + } + + for (int i = 0; i < Vector.Count; i++) + { + sum += sumAccumulator[i]; + } + } +#endif + for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) { byte scan = Unsafe.Add(ref scanBaseRef, x); @@ -127,5 +165,36 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters return upperLeft; } + + private static Vector PaethPredictor(Vector left, Vector above, Vector upperLeft) + { + Vector.Widen(left, out Vector a1, out Vector a2); + Vector.Widen(above, out Vector b1, out Vector b2); + Vector.Widen(upperLeft, out Vector c1, out Vector c2); + + Vector p1 = PaethPredictor(Vector.AsVectorInt16(a1), Vector.AsVectorInt16(b1), Vector.AsVectorInt16(c1)); + Vector p2 = PaethPredictor(Vector.AsVectorInt16(a2), Vector.AsVectorInt16(b2), Vector.AsVectorInt16(c2)); + return Vector.AsVectorByte(Vector.Narrow(p1, p2)); + } + + private static Vector PaethPredictor(Vector left, Vector above, Vector upperLeft) + { + Vector p = left + above - upperLeft; + var pa = Vector.Abs(p - left); + var pb = Vector.Abs(p - above); + var pc = Vector.Abs(p - upperLeft); + + var pa_pb = Vector.LessThanOrEqual(pa, pb); + var pa_pc = Vector.LessThanOrEqual(pa, pc); + var pb_pc = Vector.LessThanOrEqual(pb, pc); + + return Vector.ConditionalSelect( + condition: Vector.BitwiseAnd(pa_pb, pa_pc), + left: left, + right: Vector.ConditionalSelect( + condition: pb_pc, + left: above, + right: upperLeft)); + } } } From 5b280d3f5c43f74a17fb47f4b0e288a666d50ad4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 11 May 2021 18:25:15 +0100 Subject: [PATCH 14/25] Update branding [skip ci] --- shared-infrastructure | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-infrastructure b/shared-infrastructure index 0ea21d9e2a..48e73f455f 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit 0ea21d9e2a76d307dae9cfb74e33234b259352b7 +Subproject commit 48e73f455f15eafefbe3175efc7433e5f277e506 From 78b6d78058f78ab9d5a10cf0fcef8685aac7dc93 Mon Sep 17 00:00:00 2001 From: TechPizza Date: Sat, 15 May 2021 19:10:46 +0200 Subject: [PATCH 15/25] Moved Accumulate to Numerics --- src/ImageSharp/Common/Helpers/Numerics.cs | 14 ++++++++++++++ .../Formats/Png/Filters/PaethFilter.cs | 19 ++++--------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs index 6105422372..0147689117 100644 --- a/src/ImageSharp/Common/Helpers/Numerics.cs +++ b/src/ImageSharp/Common/Helpers/Numerics.cs @@ -748,5 +748,19 @@ namespace SixLabors.ImageSharp [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float Lerp(float value1, float value2, float amount) => ((value2 - value1) * amount) + value1; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Accumulate(ref Vector accumulator, Vector values) + { + Vector.Widen(values, out Vector shortLow, out Vector shortHigh); + + Vector.Widen(shortLow, out Vector intLow, out Vector intHigh); + accumulator += intLow; + accumulator += intHigh; + + Vector.Widen(shortHigh, out intLow, out intHigh); + accumulator += intLow; + accumulator += intHigh; + } } } diff --git a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs index 7562c47558..6e7bb8fb1f 100644 --- a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs @@ -86,7 +86,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters #if SUPPORTS_RUNTIME_INTRINSICS if (Vector.IsHardwareAccelerated) { - Vector sumAccumulator = Vector.Zero; + Vector sumAccumulator = Vector.Zero; for (int xLeft = x - bytesPerPixel; x + Vector.Count <= scanline.Length; xLeft += Vector.Count) { @@ -99,23 +99,12 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters res.CopyTo(result.Slice(x + 1)); // + 1 to skip filter type x += Vector.Count; - Vector.Widen( - Vector.Abs(Vector.AsVectorSByte(res)), - out Vector shortLow, - out Vector shortHigh); - - Vector.Widen(shortLow, out Vector intLow, out Vector intHigh); - sumAccumulator += intLow; - sumAccumulator += intHigh; - - Vector.Widen(shortHigh, out intLow, out intHigh); - sumAccumulator += intLow; - sumAccumulator += intHigh; + Numerics.Accumulate(ref sumAccumulator, Vector.AsVectorByte(Vector.Abs(Vector.AsVectorSByte(res)))); } - for (int i = 0; i < Vector.Count; i++) + for (int i = 0; i < Vector.Count; i++) { - sum += sumAccumulator[i]; + sum += (int)sumAccumulator[i]; } } #endif From c16af90f79c1d2f6dbf610006f0a674557fedb0b Mon Sep 17 00:00:00 2001 From: TechPizza Date: Sat, 15 May 2021 19:11:09 +0200 Subject: [PATCH 16/25] Vectorized AverageFilter --- .../Formats/Png/Filters/AverageFilter.cs | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index d1c214e3d6..57416a737b 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -5,6 +5,11 @@ using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +#if SUPPORTS_RUNTIME_INTRINSICS +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + namespace SixLabors.ImageSharp.Formats.Png.Filters { /// @@ -79,6 +84,89 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters sum += Numerics.Abs(unchecked((sbyte)res)); } +#if SUPPORTS_RUNTIME_INTRINSICS + if (Avx2.IsSupported) + { + Vector256 sumAccumulator = Vector256.Zero; + + for (int xLeft = x - bytesPerPixel; x + Vector256.Count <= scanline.Length; xLeft += Vector256.Count) + { + Vector256 scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); + Vector256 left = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); + Vector256 above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); + + Vector256 res = Avx2.Subtract(scan, Average(left, above)); + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type + x += Vector256.Count; + + Vector256 absRes = Avx2.Abs(res.AsSByte()).AsSByte(); + Vector256 loRes16 = Avx2.UnpackLow(absRes, Vector256.Zero).AsInt16(); + Vector256 hiRes16 = Avx2.UnpackHigh(absRes, Vector256.Zero).AsInt16(); + + Vector256 loRes32 = Avx2.UnpackLow(loRes16, Vector256.Zero).AsInt32(); + Vector256 hiRes32 = Avx2.UnpackHigh(loRes16, Vector256.Zero).AsInt32(); + sumAccumulator = Avx2.Add(sumAccumulator, loRes32); + sumAccumulator = Avx2.Add(sumAccumulator, hiRes32); + + loRes32 = Avx2.UnpackLow(hiRes16, Vector256.Zero).AsInt32(); + hiRes32 = Avx2.UnpackHigh(hiRes16, Vector256.Zero).AsInt32(); + sumAccumulator = Avx2.Add(sumAccumulator, loRes32); + sumAccumulator = Avx2.Add(sumAccumulator, hiRes32); + } + + for (int i = 0; i < Vector256.Count; i++) + { + sum += sumAccumulator.GetElement(i); + } + } + else if (Sse2.IsSupported) + { + var allBitsSet = Vector128.Create((sbyte)-1); + Vector128 sumAccumulator = Vector128.Zero; + + for (int xLeft = x - bytesPerPixel; x + Vector128.Count <= scanline.Length; xLeft += Vector128.Count) + { + Vector128 scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); + Vector128 left = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); + Vector128 above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); + + Vector128 res = Sse2.Subtract(scan, Average(left, above)); + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type + x += Vector128.Count; + + Vector128 absRes; + if (Ssse3.IsSupported) + { + absRes = Ssse3.Abs(res.AsSByte()).AsSByte(); + } + else + { + Vector128 mask = Sse2.CompareGreaterThan(res.AsSByte(), Vector128.Zero); + mask = Sse2.Xor(mask, allBitsSet); + absRes = Sse2.Xor(Sse2.Add(res.AsSByte(), mask), mask); + } + + Vector128 loRes16 = Sse2.UnpackLow(absRes, Vector128.Zero).AsInt16(); + Vector128 hiRes16 = Sse2.UnpackHigh(absRes, Vector128.Zero).AsInt16(); + + Vector128 loRes32 = Sse2.UnpackLow(loRes16, Vector128.Zero).AsInt32(); + Vector128 hiRes32 = Sse2.UnpackHigh(loRes16, Vector128.Zero).AsInt32(); + sumAccumulator = Sse2.Add(sumAccumulator, loRes32); + sumAccumulator = Sse2.Add(sumAccumulator, hiRes32); + + loRes32 = Sse2.UnpackLow(hiRes16, Vector128.Zero).AsInt32(); + hiRes32 = Sse2.UnpackHigh(hiRes16, Vector128.Zero).AsInt32(); + sumAccumulator = Sse2.Add(sumAccumulator, loRes32); + sumAccumulator = Sse2.Add(sumAccumulator, hiRes32); + } + + for (int i = 0; i < Vector128.Count; i++) + { + sum += sumAccumulator.GetElement(i); + } + } +#endif + for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) { byte scan = Unsafe.Add(ref scanBaseRef, x); @@ -101,5 +189,37 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Average(byte left, byte above) => (left + above) >> 1; + +#if SUPPORTS_RUNTIME_INTRINSICS + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Average(Vector128 left, Vector128 above) + { + Vector128 loLeft16 = Sse2.UnpackLow(left, Vector128.Zero).AsUInt16(); + Vector128 hiLeft16 = Sse2.UnpackHigh(left, Vector128.Zero).AsUInt16(); + + Vector128 loAbove16 = Sse2.UnpackLow(above, Vector128.Zero).AsUInt16(); + Vector128 hiAbove16 = Sse2.UnpackHigh(above, Vector128.Zero).AsUInt16(); + + Vector128 div1 = Sse2.ShiftRightLogical(Sse2.Add(loLeft16, loAbove16), 1); + Vector128 div2 = Sse2.ShiftRightLogical(Sse2.Add(hiLeft16, hiAbove16), 1); + + return Sse2.PackUnsignedSaturate(div1.AsInt16(), div2.AsInt16()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Average(Vector256 left, Vector256 above) + { + Vector256 loLeft16 = Avx2.UnpackLow(left, Vector256.Zero).AsUInt16(); + Vector256 hiLeft16 = Avx2.UnpackHigh(left, Vector256.Zero).AsUInt16(); + + Vector256 loAbove16 = Avx2.UnpackLow(above, Vector256.Zero).AsUInt16(); + Vector256 hiAbove16 = Avx2.UnpackHigh(above, Vector256.Zero).AsUInt16(); + + Vector256 div1 = Avx2.ShiftRightLogical(Avx2.Add(loLeft16, loAbove16), 1); + Vector256 div2 = Avx2.ShiftRightLogical(Avx2.Add(hiLeft16, hiAbove16), 1); + + return Avx2.PackUnsignedSaturate(div1.AsInt16(), div2.AsInt16()); + } +#endif } } From 514d23098276b48847e8658aeb57c5ffb195c1fc Mon Sep 17 00:00:00 2001 From: TechPizza Date: Sat, 15 May 2021 19:11:21 +0200 Subject: [PATCH 17/25] Vectorized SubFilter --- .../Formats/Png/Filters/SubFilter.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs index cb4cfb471f..31d65995a0 100644 --- a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -64,6 +65,30 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters sum += Numerics.Abs(unchecked((sbyte)res)); } +#if SUPPORTS_RUNTIME_INTRINSICS + if (Vector.IsHardwareAccelerated) + { + Vector sumAccumulator = Vector.Zero; + + for (int xLeft = x - bytesPerPixel; x + Vector.Count <= scanline.Length; xLeft += Vector.Count) + { + Vector scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); + Vector prev = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); + + Vector res = scan - prev; + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type + x += Vector.Count; + + Numerics.Accumulate(ref sumAccumulator, Vector.AsVectorByte(Vector.Abs(Vector.AsVectorSByte(res)))); + } + + for (int i = 0; i < Vector.Count; i++) + { + sum += (int)sumAccumulator[i]; + } + } +#endif + for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) { byte scan = Unsafe.Add(ref scanBaseRef, x); From 425e4876fa9a876ffebc8640d7cc75977a682c98 Mon Sep 17 00:00:00 2001 From: TechPizza Date: Sat, 15 May 2021 19:11:28 +0200 Subject: [PATCH 18/25] Vectorized UpFilter --- .../Formats/Png/Filters/UpFilter.cs | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs index cf553cbb68..f119c2fbae 100644 --- a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs @@ -1,7 +1,8 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -57,7 +58,33 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters // Up(x) = Raw(x) - Prior(x) resultBaseRef = 2; - for (int x = 0; x < scanline.Length; /* Note: ++x happens in the body to avoid one add operation */) + int x = 0; + +#if SUPPORTS_RUNTIME_INTRINSICS + if (Vector.IsHardwareAccelerated) + { + Vector sumAccumulator = Vector.Zero; + + for (; x + Vector.Count <= scanline.Length;) + { + Vector scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); + Vector above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); + + Vector res = scan - above; + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type + x += Vector.Count; + + Numerics.Accumulate(ref sumAccumulator, Vector.AsVectorByte(Vector.Abs(Vector.AsVectorSByte(res)))); + } + + for (int i = 0; i < Vector.Count; i++) + { + sum += (int)sumAccumulator[i]; + } + } +#endif + + for (; x < scanline.Length; /* Note: ++x happens in the body to avoid one add operation */) { byte scan = Unsafe.Add(ref scanBaseRef, x); byte above = Unsafe.Add(ref prevBaseRef, x); From 41b773ac0211fda9991a0d06ccc79b80d10d10f3 Mon Sep 17 00:00:00 2001 From: TechPizza Date: Sat, 15 May 2021 19:14:04 +0200 Subject: [PATCH 19/25] Made PaethFilter use unsafe loads --- src/ImageSharp/Formats/Png/Filters/PaethFilter.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs index 6e7bb8fb1f..05ecc74a7d 100644 --- a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs @@ -90,13 +90,13 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters for (int xLeft = x - bytesPerPixel; x + Vector.Count <= scanline.Length; xLeft += Vector.Count) { - var scan = new Vector(scanline.Slice(x)); - var left = new Vector(scanline.Slice(xLeft)); - var above = new Vector(previousScanline.Slice(x)); - var upperLeft = new Vector(previousScanline.Slice(xLeft)); + Vector scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); + Vector left = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); + Vector above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); + Vector upperLeft = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, xLeft)); Vector res = scan - PaethPredictor(left, above, upperLeft); - res.CopyTo(result.Slice(x + 1)); // + 1 to skip filter type + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type x += Vector.Count; Numerics.Accumulate(ref sumAccumulator, Vector.AsVectorByte(Vector.Abs(Vector.AsVectorSByte(res)))); From 29250ffbec1cd7b62857fae51a00658c5ac32652 Mon Sep 17 00:00:00 2001 From: TechPizza Date: Sun, 16 May 2021 16:50:16 +0200 Subject: [PATCH 20/25] Added Png filter tests --- .../Formats/Png/PngFilterTests.cs | 270 ++++++++++++++++++ .../Formats/Png/ReferenceImplementations.cs | 229 +++++++++++++++ 2 files changed, 499 insertions(+) create mode 100644 tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Png/ReferenceImplementations.cs diff --git a/tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs new file mode 100644 index 0000000000..dae8f25e58 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs @@ -0,0 +1,270 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// Uncomment this to turn unit tests into benchmarks: +// #define BENCHMARKING +using System; + +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Png.Filters; +using SixLabors.ImageSharp.Tests.Formats.Png.Utils; +using SixLabors.ImageSharp.Tests.TestUtilities; +using Xunit; +using Xunit.Abstractions; + +namespace SixLabors.ImageSharp.Tests.Formats.Png +{ + [Trait("Format", "Png")] + public partial class PngFilterTests : MeasureFixture + { +#if BENCHMARKING + public const int Times = 1000000; +#else + public const int Times = 1; +#endif + + public PngFilterTests(ITestOutputHelper output) + : base(output) + { + } + + public const int Size = 64; + + [Fact] + public void Average() + { + static void RunTest() + { + var data = new TestData(PngFilterMethod.Average, Size); + data.TestFilter(); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.DisableSIMD); + } + + [Fact] + public void AverageSse2() + { + static void RunTest() + { + var data = new TestData(PngFilterMethod.Average, Size); + data.TestFilter(); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSSE3); + } + + [Fact] + public void AverageSsse3() + { + static void RunTest() + { + var data = new TestData(PngFilterMethod.Average, Size); + data.TestFilter(); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + } + + [Fact] + public void AverageAvx2() + { + static void RunTest() + { + var data = new TestData(PngFilterMethod.Average, Size); + data.TestFilter(); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll); + } + + [Fact] + public void Paeth() + { + static void RunTest() + { + var data = new TestData(PngFilterMethod.Paeth, Size); + data.TestFilter(); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.DisableSIMD); + } + + [Fact] + public void PaethSimd() + { + static void RunTest() + { + var data = new TestData(PngFilterMethod.Paeth, Size); + data.TestFilter(); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll); + } + + [Fact] + public void Up() + { + static void RunTest() + { + var data = new TestData(PngFilterMethod.Up, Size); + data.TestFilter(); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.DisableSIMD); + } + + [Fact] + public void UpSimd() + { + static void RunTest() + { + var data = new TestData(PngFilterMethod.Up, Size); + data.TestFilter(); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll); + } + + [Fact] + public void Sub() + { + static void RunTest() + { + var data = new TestData(PngFilterMethod.Sub, Size); + data.TestFilter(); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.DisableSIMD); + } + + [Fact] + public void SubSimd() + { + static void RunTest() + { + var data = new TestData(PngFilterMethod.Sub, Size); + data.TestFilter(); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll); + } + + public class TestData + { + private readonly PngFilterMethod filter; + private readonly int bpp; + private readonly byte[] previousScanline; + private readonly byte[] scanline; + private readonly byte[] expectedResult; + private readonly int expectedSum; + private readonly byte[] resultBuffer; + + public TestData(PngFilterMethod filter, int size, int bpp = 4) + { + this.filter = filter; + this.bpp = bpp; + this.previousScanline = new byte[size * size * bpp]; + this.scanline = new byte[size * size * bpp]; + this.expectedResult = new byte[1 + (size * size * bpp)]; + this.resultBuffer = new byte[1 + (size * size * bpp)]; + + var rng = new Random(12345678); + byte[] tmp = new byte[6]; + for (int i = 0; i < this.previousScanline.Length; i += bpp) + { + rng.NextBytes(tmp); + + this.previousScanline[i + 0] = tmp[0]; + this.previousScanline[i + 1] = tmp[1]; + this.previousScanline[i + 2] = tmp[2]; + this.previousScanline[i + 3] = 255; + + this.scanline[i + 0] = tmp[3]; + this.scanline[i + 1] = tmp[4]; + this.scanline[i + 2] = tmp[5]; + this.scanline[i + 3] = 255; + } + + switch (this.filter) + { + case PngFilterMethod.Sub: + ReferenceImplementations.EncodeSubFilter( + this.scanline, this.expectedResult, this.bpp, out this.expectedSum); + break; + + case PngFilterMethod.Up: + ReferenceImplementations.EncodeUpFilter( + this.previousScanline, this.scanline, this.expectedResult, out this.expectedSum); + break; + + case PngFilterMethod.Average: + ReferenceImplementations.EncodeAverageFilter( + this.previousScanline, this.scanline, this.expectedResult, this.bpp, out this.expectedSum); + break; + + case PngFilterMethod.Paeth: + ReferenceImplementations.EncodePaethFilter( + this.previousScanline, this.scanline, this.expectedResult, this.bpp, out this.expectedSum); + break; + + case PngFilterMethod.None: + case PngFilterMethod.Adaptive: + default: + throw new InvalidOperationException(); + } + } + + public void TestFilter() + { + int sum; + switch (this.filter) + { + case PngFilterMethod.Sub: + SubFilter.Encode(this.scanline, this.resultBuffer, this.bpp, out sum); + break; + + case PngFilterMethod.Up: + UpFilter.Encode(this.previousScanline, this.scanline, this.resultBuffer, out sum); + break; + + case PngFilterMethod.Average: + AverageFilter.Encode(this.previousScanline, this.scanline, this.resultBuffer, this.bpp, out sum); + break; + + case PngFilterMethod.Paeth: + PaethFilter.Encode(this.previousScanline, this.scanline, this.resultBuffer, this.bpp, out sum); + break; + + case PngFilterMethod.None: + case PngFilterMethod.Adaptive: + default: + throw new InvalidOperationException(); + } + + Assert.Equal(this.expectedSum, sum); + Assert.Equal(this.expectedResult, this.resultBuffer); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Png/ReferenceImplementations.cs b/tests/ImageSharp.Tests/Formats/Png/ReferenceImplementations.cs new file mode 100644 index 0000000000..dd8ecc096d --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Png/ReferenceImplementations.cs @@ -0,0 +1,229 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Tests.Formats.Png.Utils +{ + /// + /// This class contains reference implementations to produce verification data for unit tests + /// + internal static partial class ReferenceImplementations + { + /// + /// Encodes the scanline + /// + /// 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 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EncodePaethFilter(Span scanline, Span previousScanline, Span result, int bytesPerPixel, out int sum) + { + DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); + DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); + + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); + sum = 0; + + // Paeth(x) = Raw(x) - PaethPredictor(Raw(x-bpp), Prior(x), Prior(x - bpp)) + resultBaseRef = 4; + + int x = 0; + for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) + { + byte scan = Unsafe.Add(ref scanBaseRef, x); + byte above = Unsafe.Add(ref prevBaseRef, x); + ++x; + ref byte res = ref Unsafe.Add(ref resultBaseRef, x); + res = (byte)(scan - PaethPredictor(0, above, 0)); + sum += Numerics.Abs(unchecked((sbyte)res)); + } + + for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) + { + byte scan = Unsafe.Add(ref scanBaseRef, x); + byte left = Unsafe.Add(ref scanBaseRef, xLeft); + byte above = Unsafe.Add(ref prevBaseRef, x); + byte upperLeft = Unsafe.Add(ref prevBaseRef, xLeft); + ++x; + ref byte res = ref Unsafe.Add(ref resultBaseRef, x); + res = (byte)(scan - PaethPredictor(left, above, upperLeft)); + sum += Numerics.Abs(unchecked((sbyte)res)); + } + + sum -= 4; + } + + /// + /// Encodes the scanline + /// + /// The scanline to encode + /// The filtered scanline result. + /// The bytes per pixel. + /// The sum of the total variance of the filtered row + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EncodeSubFilter(Span scanline, Span result, int bytesPerPixel, out int sum) + { + DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); + + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); + sum = 0; + + // Sub(x) = Raw(x) - Raw(x-bpp) + resultBaseRef = 1; + + int x = 0; + for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) + { + byte scan = Unsafe.Add(ref scanBaseRef, x); + ++x; + ref byte res = ref Unsafe.Add(ref resultBaseRef, x); + res = scan; + sum += Numerics.Abs(unchecked((sbyte)res)); + } + + for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) + { + byte scan = Unsafe.Add(ref scanBaseRef, x); + byte prev = Unsafe.Add(ref scanBaseRef, xLeft); + ++x; + ref byte res = ref Unsafe.Add(ref resultBaseRef, x); + res = (byte)(scan - prev); + sum += Numerics.Abs(unchecked((sbyte)res)); + } + + sum -= 1; + } + + /// + /// Encodes the scanline + /// + /// The scanline to encode + /// The previous scanline. + /// The filtered scanline result. + /// The sum of the total variance of the filtered row + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EncodeUpFilter(Span scanline, Span previousScanline, Span result, out int sum) + { + DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); + DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); + + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); + sum = 0; + + // Up(x) = Raw(x) - Prior(x) + resultBaseRef = 2; + + int x = 0; + + for (; x < scanline.Length; /* Note: ++x happens in the body to avoid one add operation */) + { + byte scan = Unsafe.Add(ref scanBaseRef, x); + byte above = Unsafe.Add(ref prevBaseRef, x); + ++x; + ref byte res = ref Unsafe.Add(ref resultBaseRef, x); + res = (byte)(scan - above); + sum += Numerics.Abs(unchecked((sbyte)res)); + } + + sum -= 2; + } + + /// + /// Encodes the scanline + /// + /// 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 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EncodeAverageFilter(Span scanline, Span previousScanline, Span result, int bytesPerPixel, out int sum) + { + DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); + DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); + + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); + sum = 0; + + // Average(x) = Raw(x) - floor((Raw(x-bpp)+Prior(x))/2) + resultBaseRef = 3; + + int x = 0; + for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) + { + byte scan = Unsafe.Add(ref scanBaseRef, x); + byte above = Unsafe.Add(ref prevBaseRef, x); + ++x; + ref byte res = ref Unsafe.Add(ref resultBaseRef, x); + res = (byte)(scan - (above >> 1)); + sum += Numerics.Abs(unchecked((sbyte)res)); + } + + for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) + { + byte scan = Unsafe.Add(ref scanBaseRef, x); + byte left = Unsafe.Add(ref scanBaseRef, xLeft); + byte above = Unsafe.Add(ref prevBaseRef, x); + ++x; + ref byte res = ref Unsafe.Add(ref resultBaseRef, x); + res = (byte)(scan - Average(left, above)); + sum += Numerics.Abs(unchecked((sbyte)res)); + } + + sum -= 3; + } + + /// + /// Calculates the average value of two bytes + /// + /// The left byte + /// The above byte + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Average(byte left, byte above) => (left + above) >> 1; + + /// + /// Computes a simple linear function of the three neighboring pixels (left, above, upper left), then chooses + /// as predictor the neighboring pixel closest to the computed value. + /// + /// The left neighbor pixel. + /// The above neighbor pixel. + /// The upper left neighbor pixel. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte PaethPredictor(byte left, byte above, byte upperLeft) + { + int p = left + above - upperLeft; + int pa = Numerics.Abs(p - left); + int pb = Numerics.Abs(p - above); + int pc = Numerics.Abs(p - upperLeft); + + if (pa <= pb && pa <= pc) + { + return left; + } + + if (pb <= pc) + { + return above; + } + + return upperLeft; + } + } +} From 64e082615a4ad3e1ca2a8b591b793e52e6e6b8f8 Mon Sep 17 00:00:00 2001 From: TechPizza Date: Tue, 18 May 2021 09:50:26 +0200 Subject: [PATCH 21/25] Optimized AverageFilter --- .../Formats/Png/Filters/AverageFilter.cs | 45 ++++--------------- 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index 57416a737b..b596643622 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -88,6 +88,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters if (Avx2.IsSupported) { Vector256 sumAccumulator = Vector256.Zero; + Vector256 allBitsSet = Avx2.CompareEqual(sumAccumulator, sumAccumulator).AsByte(); for (int xLeft = x - bytesPerPixel; x + Vector256.Count <= scanline.Length; xLeft += Vector256.Count) { @@ -95,7 +96,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters Vector256 left = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); Vector256 above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); - Vector256 res = Avx2.Subtract(scan, Average(left, above)); + Vector256 avg = Avx2.Xor(Avx2.Average(Avx2.Xor(left, allBitsSet), Avx2.Xor(above, allBitsSet)), allBitsSet); + Vector256 res = Avx2.Subtract(scan, avg); + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type x += Vector256.Count; @@ -121,8 +124,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters } else if (Sse2.IsSupported) { - var allBitsSet = Vector128.Create((sbyte)-1); Vector128 sumAccumulator = Vector128.Zero; + Vector128 allBitsSet = Sse2.CompareEqual(sumAccumulator, sumAccumulator).AsByte(); for (int xLeft = x - bytesPerPixel; x + Vector128.Count <= scanline.Length; xLeft += Vector128.Count) { @@ -130,7 +133,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters Vector128 left = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); Vector128 above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); - Vector128 res = Sse2.Subtract(scan, Average(left, above)); + Vector128 avg = Sse2.Xor(Sse2.Average(Sse2.Xor(left, allBitsSet), Sse2.Xor(above, allBitsSet)), allBitsSet); + Vector128 res = Sse2.Subtract(scan, avg); + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type x += Vector128.Count; @@ -142,7 +147,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters else { Vector128 mask = Sse2.CompareGreaterThan(res.AsSByte(), Vector128.Zero); - mask = Sse2.Xor(mask, allBitsSet); + mask = Sse2.Xor(mask, allBitsSet.AsSByte()); absRes = Sse2.Xor(Sse2.Add(res.AsSByte(), mask), mask); } @@ -189,37 +194,5 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Average(byte left, byte above) => (left + above) >> 1; - -#if SUPPORTS_RUNTIME_INTRINSICS - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 Average(Vector128 left, Vector128 above) - { - Vector128 loLeft16 = Sse2.UnpackLow(left, Vector128.Zero).AsUInt16(); - Vector128 hiLeft16 = Sse2.UnpackHigh(left, Vector128.Zero).AsUInt16(); - - Vector128 loAbove16 = Sse2.UnpackLow(above, Vector128.Zero).AsUInt16(); - Vector128 hiAbove16 = Sse2.UnpackHigh(above, Vector128.Zero).AsUInt16(); - - Vector128 div1 = Sse2.ShiftRightLogical(Sse2.Add(loLeft16, loAbove16), 1); - Vector128 div2 = Sse2.ShiftRightLogical(Sse2.Add(hiLeft16, hiAbove16), 1); - - return Sse2.PackUnsignedSaturate(div1.AsInt16(), div2.AsInt16()); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 Average(Vector256 left, Vector256 above) - { - Vector256 loLeft16 = Avx2.UnpackLow(left, Vector256.Zero).AsUInt16(); - Vector256 hiLeft16 = Avx2.UnpackHigh(left, Vector256.Zero).AsUInt16(); - - Vector256 loAbove16 = Avx2.UnpackLow(above, Vector256.Zero).AsUInt16(); - Vector256 hiAbove16 = Avx2.UnpackHigh(above, Vector256.Zero).AsUInt16(); - - Vector256 div1 = Avx2.ShiftRightLogical(Avx2.Add(loLeft16, loAbove16), 1); - Vector256 div2 = Avx2.ShiftRightLogical(Avx2.Add(hiLeft16, hiAbove16), 1); - - return Avx2.PackUnsignedSaturate(div1.AsInt16(), div2.AsInt16()); - } -#endif } } From d7f02bc23cdd5a0e6fb43ab89a074180d6aa8719 Mon Sep 17 00:00:00 2001 From: TechPizza Date: Tue, 18 May 2021 09:50:39 +0200 Subject: [PATCH 22/25] Greatly optimized PaethFilter --- .../Formats/Png/Filters/PaethFilter.cs | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs index 05ecc74a7d..7fa8a6b745 100644 --- a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs @@ -6,6 +6,11 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +#if SUPPORTS_RUNTIME_INTRINSICS +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + namespace SixLabors.ImageSharp.Formats.Png.Filters { /// @@ -84,7 +89,30 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters } #if SUPPORTS_RUNTIME_INTRINSICS - if (Vector.IsHardwareAccelerated) + if (Avx2.IsSupported) + { + Vector256 sumAccumulator = Vector256.Zero; + + for (int xLeft = x - bytesPerPixel; x + Vector256.Count <= scanline.Length; xLeft += Vector256.Count) + { + Vector256 scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); + Vector256 left = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); + Vector256 above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); + Vector256 upperLeft = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, xLeft)); + + Vector256 res = Avx2.Subtract(scan, PaethPredictor(left, above, upperLeft)); + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type + x += Vector256.Count; + + sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), Vector256.Zero).AsInt32()); + } + + for (int i = 0; i < Vector256.Count; i++) + { + sum += sumAccumulator.GetElement(i); + } + } + else if (Vector.IsHardwareAccelerated) { Vector sumAccumulator = Vector.Zero; @@ -155,6 +183,39 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters return upperLeft; } +#if SUPPORTS_RUNTIME_INTRINSICS + private static Vector256 PaethPredictor(Vector256 left, Vector256 above, Vector256 upleft) + { + Vector256 zero = Vector256.Zero; + + // Here, we refactor pa = abs(p - left) = abs(left + above - upleft - left) + // to pa = abs(above - upleft). Same deal for pb. + // Using saturated subtraction, if the result is negative, the output is zero. + // If we subtract in both directions and `or` the results, only one can be + // non-zero, so we end up with the absolute value. + Vector256 sac = Avx2.SubtractSaturate(above, upleft); + Vector256 sbc = Avx2.SubtractSaturate(left, upleft); + Vector256 pa = Avx2.Or(Avx2.SubtractSaturate(upleft, above), sac); + Vector256 pb = Avx2.Or(Avx2.SubtractSaturate(upleft, left), sbc); + + // pc = abs(left + above - upleft - upleft), or abs(left - upleft + above - upleft). + // We've already calculated left - upleft and above - upleft in `sac` and `sbc`. + // If they are both negative or both positive, the absolute value of their + // sum can't possibly be less than `pa` or `pb`, so we'll never use the value. + // We make a mask that sets the value to 255 if they either both got + // saturated to zero or both didn't. Then we calculate the absolute value + // of their difference using saturated subtract and `or`, same as before, + // keeping the value only where the mask isn't set. + Vector256 pm = Avx2.CompareEqual(Avx2.CompareEqual(sac, zero), Avx2.CompareEqual(sbc, zero)); + Vector256 pc = Avx2.Or(pm, Avx2.Or(Avx2.SubtractSaturate(pb, pa), Avx2.SubtractSaturate(pa, pb))); + + // Finally, blend the values together. We start with `upleft` and overwrite on + // tied values so that the `left`, `above`, `upleft` precedence is preserved. + Vector256 minbc = Avx2.Min(pc, pb); + Vector256 resbc = Avx2.BlendVariable(upleft, above, Avx2.CompareEqual(minbc, pb)); + return Avx2.BlendVariable(resbc, left, Avx2.CompareEqual(Avx2.Min(minbc, pa), pa)); + } + private static Vector PaethPredictor(Vector left, Vector above, Vector upperLeft) { Vector.Widen(left, out Vector a1, out Vector a2); @@ -185,5 +246,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters left: above, right: upperLeft)); } +#endif } } From 9d04ec8274f5cec35aa0c12d8e741652e7fbd341 Mon Sep 17 00:00:00 2001 From: TechPizza Date: Tue, 18 May 2021 10:02:28 +0200 Subject: [PATCH 23/25] Small intrinsics cleanup --- .../Formats/Png/Filters/AverageFilter.cs | 31 +++++++------------ .../Formats/Png/Filters/PaethFilter.cs | 3 +- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index b596643622..818119f331 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -87,6 +87,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters #if SUPPORTS_RUNTIME_INTRINSICS if (Avx2.IsSupported) { + Vector256 zero = Vector256.Zero; Vector256 sumAccumulator = Vector256.Zero; Vector256 allBitsSet = Avx2.CompareEqual(sumAccumulator, sumAccumulator).AsByte(); @@ -102,19 +103,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type x += Vector256.Count; - Vector256 absRes = Avx2.Abs(res.AsSByte()).AsSByte(); - Vector256 loRes16 = Avx2.UnpackLow(absRes, Vector256.Zero).AsInt16(); - Vector256 hiRes16 = Avx2.UnpackHigh(absRes, Vector256.Zero).AsInt16(); - - Vector256 loRes32 = Avx2.UnpackLow(loRes16, Vector256.Zero).AsInt32(); - Vector256 hiRes32 = Avx2.UnpackHigh(loRes16, Vector256.Zero).AsInt32(); - sumAccumulator = Avx2.Add(sumAccumulator, loRes32); - sumAccumulator = Avx2.Add(sumAccumulator, hiRes32); - - loRes32 = Avx2.UnpackLow(hiRes16, Vector256.Zero).AsInt32(); - hiRes32 = Avx2.UnpackHigh(hiRes16, Vector256.Zero).AsInt32(); - sumAccumulator = Avx2.Add(sumAccumulator, loRes32); - sumAccumulator = Avx2.Add(sumAccumulator, hiRes32); + sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32()); } for (int i = 0; i < Vector256.Count; i++) @@ -124,6 +113,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters } else if (Sse2.IsSupported) { + Vector128 zero8 = Vector128.Zero; + Vector128 zero16 = Vector128.Zero; Vector128 sumAccumulator = Vector128.Zero; Vector128 allBitsSet = Sse2.CompareEqual(sumAccumulator, sumAccumulator).AsByte(); @@ -146,21 +137,21 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters } else { - Vector128 mask = Sse2.CompareGreaterThan(res.AsSByte(), Vector128.Zero); + Vector128 mask = Sse2.CompareGreaterThan(res.AsSByte(), zero8); mask = Sse2.Xor(mask, allBitsSet.AsSByte()); absRes = Sse2.Xor(Sse2.Add(res.AsSByte(), mask), mask); } - Vector128 loRes16 = Sse2.UnpackLow(absRes, Vector128.Zero).AsInt16(); - Vector128 hiRes16 = Sse2.UnpackHigh(absRes, Vector128.Zero).AsInt16(); + Vector128 loRes16 = Sse2.UnpackLow(absRes, zero8).AsInt16(); + Vector128 hiRes16 = Sse2.UnpackHigh(absRes, zero8).AsInt16(); - Vector128 loRes32 = Sse2.UnpackLow(loRes16, Vector128.Zero).AsInt32(); - Vector128 hiRes32 = Sse2.UnpackHigh(loRes16, Vector128.Zero).AsInt32(); + Vector128 loRes32 = Sse2.UnpackLow(loRes16, zero16).AsInt32(); + Vector128 hiRes32 = Sse2.UnpackHigh(loRes16, zero16).AsInt32(); sumAccumulator = Sse2.Add(sumAccumulator, loRes32); sumAccumulator = Sse2.Add(sumAccumulator, hiRes32); - loRes32 = Sse2.UnpackLow(hiRes16, Vector128.Zero).AsInt32(); - hiRes32 = Sse2.UnpackHigh(hiRes16, Vector128.Zero).AsInt32(); + loRes32 = Sse2.UnpackLow(hiRes16, zero16).AsInt32(); + hiRes32 = Sse2.UnpackHigh(hiRes16, zero16).AsInt32(); sumAccumulator = Sse2.Add(sumAccumulator, loRes32); sumAccumulator = Sse2.Add(sumAccumulator, hiRes32); } diff --git a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs index 7fa8a6b745..f48010dba6 100644 --- a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs @@ -91,6 +91,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters #if SUPPORTS_RUNTIME_INTRINSICS if (Avx2.IsSupported) { + Vector256 zero = Vector256.Zero; Vector256 sumAccumulator = Vector256.Zero; for (int xLeft = x - bytesPerPixel; x + Vector256.Count <= scanline.Length; xLeft += Vector256.Count) @@ -104,7 +105,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type x += Vector256.Count; - sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), Vector256.Zero).AsInt32()); + sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32()); } for (int i = 0; i < Vector256.Count; i++) From 5c7f4a9ab37798a512f917f7df8d155dc180254c Mon Sep 17 00:00:00 2001 From: TechPizza Date: Tue, 18 May 2021 10:52:59 +0200 Subject: [PATCH 24/25] Added more specialized Png filter code Modified tests accordingly --- src/ImageSharp/Common/Helpers/Numerics.cs | 46 +++++++++++++++++ .../Formats/Png/Filters/AverageFilter.cs | 10 +--- .../Formats/Png/Filters/PaethFilter.cs | 5 +- .../Formats/Png/Filters/SubFilter.cs | 26 +++++++++- .../Formats/Png/Filters/UpFilter.cs | 26 +++++++++- .../Formats/Png/PngFilterTests.cs | 49 +++++++++++++++++-- 6 files changed, 145 insertions(+), 17 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs index 0147689117..f9969b27a5 100644 --- a/src/ImageSharp/Common/Helpers/Numerics.cs +++ b/src/ImageSharp/Common/Helpers/Numerics.cs @@ -749,6 +749,7 @@ namespace SixLabors.ImageSharp public static float Lerp(float value1, float value2, float amount) => ((value2 - value1) * amount) + value1; +#if SUPPORTS_RUNTIME_INTRINSICS [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Accumulate(ref Vector accumulator, Vector values) { @@ -762,5 +763,50 @@ namespace SixLabors.ImageSharp accumulator += intLow; accumulator += intHigh; } + + /// + /// Reduces elements of the vector into one sum. + /// + /// The accumulator to reduce. + /// The sum of all elements. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ReduceSum(Vector128 accumulator) + { + if (Ssse3.IsSupported) + { + Vector128 hadd = Ssse3.HorizontalAdd(accumulator, accumulator); + Vector128 swapped = Sse2.Shuffle(hadd, 0x1); + Vector128 tmp = Sse2.Add(hadd, swapped); + + // Vector128.ToScalar() isn't optimized pre-net5.0 https://github.com/dotnet/runtime/pull/37882 + return Sse2.ConvertToInt32(tmp); + } + else + { + int sum = 0; + for (int i = 0; i < Vector128.Count; i++) + { + sum += accumulator.GetElement(i); + } + + return sum; + } + } + + /// + /// Reduces even elements of the vector into one sum. + /// + /// The accumulator to reduce. + /// The sum of even elements. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int EvenReduceSum(Vector256 accumulator) + { + Vector128 vsum = Sse2.Add(accumulator.GetLower(), accumulator.GetUpper()); // add upper lane to lower lane + vsum = Sse2.Add(vsum, Sse2.Shuffle(vsum, 0b_11_10_11_10)); // add high to low + + // Vector128.ToScalar() isn't optimized pre-net5.0 https://github.com/dotnet/runtime/pull/37882 + return Sse2.ConvertToInt32(vsum); + } +#endif } } diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index 818119f331..0ab1413974 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -106,10 +106,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32()); } - for (int i = 0; i < Vector256.Count; i++) - { - sum += sumAccumulator.GetElement(i); - } + sum += Numerics.EvenReduceSum(sumAccumulator); } else if (Sse2.IsSupported) { @@ -156,10 +153,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters sumAccumulator = Sse2.Add(sumAccumulator, hiRes32); } - for (int i = 0; i < Vector128.Count; i++) - { - sum += sumAccumulator.GetElement(i); - } + sum += Numerics.ReduceSum(sumAccumulator); } #endif diff --git a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs index f48010dba6..e8e0aa7043 100644 --- a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs @@ -108,10 +108,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32()); } - for (int i = 0; i < Vector256.Count; i++) - { - sum += sumAccumulator.GetElement(i); - } + sum += Numerics.EvenReduceSum(sumAccumulator); } else if (Vector.IsHardwareAccelerated) { diff --git a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs index 31d65995a0..116154836e 100644 --- a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs @@ -6,6 +6,11 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +#if SUPPORTS_RUNTIME_INTRINSICS +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + namespace SixLabors.ImageSharp.Formats.Png.Filters { /// @@ -66,7 +71,26 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters } #if SUPPORTS_RUNTIME_INTRINSICS - if (Vector.IsHardwareAccelerated) + if (Avx2.IsSupported) + { + Vector256 zero = Vector256.Zero; + Vector256 sumAccumulator = Vector256.Zero; + + for (int xLeft = x - bytesPerPixel; x + Vector256.Count <= scanline.Length; xLeft += Vector256.Count) + { + Vector256 scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); + Vector256 prev = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); + + Vector256 res = Avx2.Subtract(scan, prev); + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type + x += Vector256.Count; + + sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32()); + } + + sum += Numerics.EvenReduceSum(sumAccumulator); + } + else if (Vector.IsHardwareAccelerated) { Vector sumAccumulator = Vector.Zero; diff --git a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs index f119c2fbae..e0f35293a4 100644 --- a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs @@ -6,6 +6,11 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +#if SUPPORTS_RUNTIME_INTRINSICS +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + namespace SixLabors.ImageSharp.Formats.Png.Filters { /// @@ -61,7 +66,26 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters int x = 0; #if SUPPORTS_RUNTIME_INTRINSICS - if (Vector.IsHardwareAccelerated) + if (Avx2.IsSupported) + { + Vector256 zero = Vector256.Zero; + Vector256 sumAccumulator = Vector256.Zero; + + for (; x + Vector256.Count <= scanline.Length;) + { + Vector256 scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); + Vector256 above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); + + Vector256 res = Avx2.Subtract(scan, above); + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type + x += Vector256.Count; + + sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32()); + } + + sum += Numerics.EvenReduceSum(sumAccumulator); + } + else if (Vector.IsHardwareAccelerated) { Vector sumAccumulator = Vector.Zero; diff --git a/tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs index dae8f25e58..5f7b4f8327 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs @@ -101,7 +101,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } [Fact] - public void PaethSimd() + public void PaethAvx2() { static void RunTest() { @@ -114,6 +114,20 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png HwIntrinsics.AllowAll); } + [Fact] + public void PaethVector() + { + static void RunTest() + { + var data = new TestData(PngFilterMethod.Paeth, Size); + data.TestFilter(); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + } + [Fact] public void Up() { @@ -128,8 +142,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png HwIntrinsics.DisableSIMD); } + [Fact] - public void UpSimd() + public void UpAvx2() { static void RunTest() { @@ -142,6 +157,20 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png HwIntrinsics.AllowAll); } + [Fact] + public void UpVector() + { + static void RunTest() + { + var data = new TestData(PngFilterMethod.Up, Size); + data.TestFilter(); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + } + [Fact] public void Sub() { @@ -157,7 +186,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } [Fact] - public void SubSimd() + public void SubAvx2() { static void RunTest() { @@ -170,6 +199,20 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png HwIntrinsics.AllowAll); } + [Fact] + public void SubVector() + { + static void RunTest() + { + var data = new TestData(PngFilterMethod.Sub, Size); + data.TestFilter(); + } + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + } + public class TestData { private readonly PngFilterMethod filter; From 4cfc7016ec711a6f2a16bf47a2f020ed65685b50 Mon Sep 17 00:00:00 2001 From: TechPizza Date: Tue, 18 May 2021 11:55:57 +0200 Subject: [PATCH 25/25] Added comment on Accumulate --- src/ImageSharp/Common/Helpers/Numerics.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs index f9969b27a5..0581993014 100644 --- a/src/ImageSharp/Common/Helpers/Numerics.cs +++ b/src/ImageSharp/Common/Helpers/Numerics.cs @@ -750,6 +750,23 @@ namespace SixLabors.ImageSharp => ((value2 - value1) * amount) + value1; #if SUPPORTS_RUNTIME_INTRINSICS + + /// + /// Accumulates 8-bit integers into by + /// widening them to 32-bit integers and performing four additions. + /// + /// + /// byte(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16) + /// is widened and added onto as such: + /// + /// accumulator += i32(1, 2, 3, 4); + /// accumulator += i32(5, 6, 7, 8); + /// accumulator += i32(9, 10, 11, 12); + /// accumulator += i32(13, 14, 15, 16); + /// + /// + /// The accumulator destination. + /// The values to accumulate. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Accumulate(ref Vector accumulator, Vector values) {