From f455dad3f1caf791c59bb8960dc399208956b1fd Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 7 Apr 2019 20:06:27 +0200 Subject: [PATCH 01/37] PixelConversionModifierExtensions.ApplyCompanding() + tests --- .../PixelConversionModifiersExtensions.cs | 18 +++++ .../Transforms/Resize/ResizeProcessor.cs | 7 +- ...PixelConversionModifiersExtensionsTests.cs | 65 +++++++++++++++++++ 3 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelConversionModifiersExtensionsTests.cs diff --git a/src/ImageSharp/PixelFormats/PixelConversionModifiersExtensions.cs b/src/ImageSharp/PixelFormats/PixelConversionModifiersExtensions.cs index bf77f8511..529041481 100644 --- a/src/ImageSharp/PixelFormats/PixelConversionModifiersExtensions.cs +++ b/src/ImageSharp/PixelFormats/PixelConversionModifiersExtensions.cs @@ -5,6 +5,9 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats { + /// + /// Extension and utility methods for . + /// internal static class PixelConversionModifiersExtensions { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -16,5 +19,20 @@ namespace SixLabors.ImageSharp.PixelFormats this PixelConversionModifiers modifiers, PixelConversionModifiers removeThis) => modifiers & ~removeThis; + + /// + /// Applies the union of and , + /// if is true, returns unmodified otherwise. + /// + /// + /// and + /// should be always used together! + /// + public static PixelConversionModifiers ApplyCompanding( + this PixelConversionModifiers originalModifiers, + bool compand) => + compand + ? originalModifiers | PixelConversionModifiers.Scale | PixelConversionModifiers.SRgbCompand + : originalModifiers; } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs index 2c186a276..198427d28 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -236,11 +236,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int sourceHeight = source.Height; - PixelConversionModifiers conversionModifiers = PixelConversionModifiers.Premultiply; - if (this.Compand) - { - conversionModifiers |= PixelConversionModifiers.Scale | PixelConversionModifiers.SRgbCompand; - } + PixelConversionModifiers conversionModifiers = + PixelConversionModifiers.Premultiply.ApplyCompanding(this.Compand); // Interpolate the image using the calculated weights. // A 2-pass 1D algorithm appears to be faster than splitting a 1-pass 2D algorithm diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelConversionModifiersExtensionsTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelConversionModifiersExtensionsTests.cs new file mode 100644 index 000000000..e98e14fc6 --- /dev/null +++ b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelConversionModifiersExtensionsTests.cs @@ -0,0 +1,65 @@ +// // Copyright (c) Six Labors and contributors. +// // Licensed under the Apache License, Version 2.0. +// // Copyright (c) Six Labors and contributors. +// // Licensed under the Apache License, Version 2.0. +// // Copyright (c) Six Labors and contributors. +// // Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations +{ + public class PixelConversionModifiersExtensionsTests + { + [Theory] + [InlineData(PixelConversionModifiers.None, PixelConversionModifiers.None, true)] + [InlineData(PixelConversionModifiers.None, PixelConversionModifiers.Premultiply, false)] + [InlineData(PixelConversionModifiers.SRgbCompand, PixelConversionModifiers.Premultiply, false)] + [InlineData( + PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale, + PixelConversionModifiers.Premultiply, + true)] + [InlineData( + PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale, + PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale, + true)] + [InlineData( + PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale, + PixelConversionModifiers.Scale, + true)] + internal void IsDefined( + PixelConversionModifiers baselineModifiers, + PixelConversionModifiers checkModifiers, + bool expected) + { + Assert.Equal(expected, baselineModifiers.IsDefined(checkModifiers)); + } + + [Theory] + [InlineData(PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale | PixelConversionModifiers.SRgbCompand, + PixelConversionModifiers.Scale, PixelConversionModifiers.Premultiply | PixelConversionModifiers.SRgbCompand)] + [InlineData(PixelConversionModifiers.None, PixelConversionModifiers.Premultiply, PixelConversionModifiers.None)] + internal void Remove( + PixelConversionModifiers baselineModifiers, + PixelConversionModifiers toRemove, + PixelConversionModifiers expected) + { + PixelConversionModifiers result = baselineModifiers.Remove(toRemove); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(PixelConversionModifiers.Premultiply, false, PixelConversionModifiers.Premultiply)] + [InlineData(PixelConversionModifiers.Premultiply, true, PixelConversionModifiers.Premultiply | PixelConversionModifiers.SRgbCompand | PixelConversionModifiers.Scale)] + internal void ApplyCompanding( + PixelConversionModifiers baselineModifiers, + bool compand, + PixelConversionModifiers expected) + { + PixelConversionModifiers result = baselineModifiers.ApplyCompanding(compand); + Assert.Equal(expected, result); + } + } +} \ No newline at end of file From 9861084a8def04f0635fe5edf18f73166fe9ca8c Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 7 Apr 2019 20:29:50 +0200 Subject: [PATCH 02/37] drop FileTestBase usage in ResizeTests --- .../Processing/Processors/Transforms/ResizeTests.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 034b66ae9..34c984248 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -15,7 +15,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { - public class ResizeTests : FileTestBase + public class ResizeTests { public static readonly string[] CommonTestImages = { TestImages.Png.CalliphoraPartial }; @@ -31,6 +31,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms nameof(KnownResamplers.Lanczos5), }; + private const PixelTypes DefaultPixelType = PixelTypes.Rgba32; + + private const PixelTypes CommonNonDefaultPixelTypes = + PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.RgbaVector; + [Theory] [WithFileCollection(nameof(CommonTestImages), nameof(AllResamplerNames), DefaultPixelType, 0.5f, null, null)] [WithFileCollection(nameof(CommonTestImages), nameof(SmokeTestResamplerNames), DefaultPixelType, 0.3f, null, null)] @@ -185,7 +190,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms image.Mutate(x => x.Resize(image.Width / 2, image.Height / 2, KnownResamplers.Bicubic)); // Comparer fights decoder with gif-s. Could not use CompareToReferenceOutput here :( - image.DebugSave(provider, extension: Extensions.Gif); + image.DebugSave(provider, extension: "gif"); } } From 985357cf843c6d2b66079d7f6d0791d68a8da864 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 7 Apr 2019 23:18:13 +0200 Subject: [PATCH 03/37] introduce [WithBasicTestPatternImages] --- .../Processors/Transforms/ResizeTests.cs | 17 +++++ .../WithBasicTestPatternImagesAttribute.cs | 37 +++++++++++ .../BasicTestPatternProvider.cs | 65 +++++++++++++++++++ .../ImageProviders/BlankProvider.cs | 3 + .../ImageProviders/SolidProvider.cs | 3 + .../ImageProviders/TestImageProvider.cs | 6 ++ .../ImageProviders/TestPatternProvider.cs | 5 +- .../Tests/TestImageProviderTests.cs | 14 ++++ 8 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 tests/ImageSharp.Tests/TestUtilities/Attributes/WithBasicTestPatternImagesAttribute.cs create mode 100644 tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 34c984248..7f1f5a26a 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -35,6 +35,23 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms private const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.RgbaVector; + + [Theory] + [WithBasicTestPatternImages(15, 12, PixelTypes.Rgba32)] + public void Resize_BasicSmall(TestImageProvider provider) + where TPixel : struct, IPixel + { + // Basic test case, very helpful for debugging + // resizing: (15, 12) -> (10, 6) + // kernel dimensions: (3, 4) + + using (Image image = provider.GetImage()) + { + var destSize = new Size(image.Width * 2 / 3, image.Height / 2); + image.Mutate(x => x.Resize(destSize, KnownResamplers.Bicubic, false)); + image.DebugSave(provider); + } + } [Theory] [WithFileCollection(nameof(CommonTestImages), nameof(AllResamplerNames), DefaultPixelType, 0.5f, null, null)] diff --git a/tests/ImageSharp.Tests/TestUtilities/Attributes/WithBasicTestPatternImagesAttribute.cs b/tests/ImageSharp.Tests/TestUtilities/Attributes/WithBasicTestPatternImagesAttribute.cs new file mode 100644 index 000000000..1e4324e04 --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/Attributes/WithBasicTestPatternImagesAttribute.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Reflection; + +namespace SixLabors.ImageSharp.Tests +{ + public class WithBasicTestPatternImagesAttribute : ImageDataAttributeBase + { + public WithBasicTestPatternImagesAttribute(int width, int height, PixelTypes pixelTypes, params object[] additionalParameters) + : this(null, width, height, pixelTypes, additionalParameters) + { + } + + public WithBasicTestPatternImagesAttribute(string memberData, int width, int height, PixelTypes pixelTypes, params object[] additionalParameters) + : base(memberData, pixelTypes, additionalParameters) + { + this.Width = width; + this.Height = height; + } + + /// + /// Gets the width + /// + public int Width { get; } + + /// + /// Gets the height + /// + public int Height { get; } + + protected override string GetFactoryMethodName(MethodInfo testMethod) => "BasicTestPattern"; + + protected override object[] GetFactoryMethodArgs(MethodInfo testMethod, Type factoryType) => new object[] { this.Width, this.Height }; + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs new file mode 100644 index 000000000..47bb22aeb --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests +{ + public abstract partial class TestImageProvider + { + private class BasicTestPatternProvider : BlankProvider + { + public BasicTestPatternProvider(int width, int height) + : base(width, height) + { + } + + /// + /// This parameterless constructor is needed for xUnit deserialization + /// + public BasicTestPatternProvider() + { + } + + public override string SourceFileOrDescription => TestUtils.AsInvariantString($"BasicTestPattern{this.Width}x{this.Height}"); + + public override Image GetImage() + { + var result = new Image(this.Width, this.Height); + + TPixel topLeftColor = NamedColors.Red; + TPixel topRightColor = NamedColors.Green; + TPixel bottomLeftColor = NamedColors.Blue; + + // Transparent purple: + TPixel bottomRightColor = default; + bottomRightColor.FromVector4(new Vector4(1f, 0f, 1f, 0.5f)); + + int midY = this.Height / 2; + int midX = this.Width / 2; + + for (int y = 0; y < midY; y++) + { + Span row = result.GetPixelRowSpan(y); + + row.Slice(0, midX).Fill(topLeftColor); + row.Slice(midX, this.Width-midX).Fill(topRightColor); + } + + for (int y = midY; y < this.Height; y++) + { + Span row = result.GetPixelRowSpan(y); + + row.Slice(0, midX).Fill(bottomLeftColor); + row.Slice(midX, this.Width-midX).Fill(bottomRightColor); + } + + return result; + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BlankProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BlankProvider.cs index 7821d0b51..1275e522f 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BlankProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BlankProvider.cs @@ -20,6 +20,9 @@ namespace SixLabors.ImageSharp.Tests this.Height = height; } + /// + /// This parameterless constructor is needed for xUnit deserialization + /// public BlankProvider() { this.Width = 100; diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/SolidProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/SolidProvider.cs index d68c37a76..1ff95f60d 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/SolidProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/SolidProvider.cs @@ -35,6 +35,9 @@ namespace SixLabors.ImageSharp.Tests this.a = a; } + /// + /// This parameterless constructor is needed for xUnit deserialization + /// public SolidProvider() : base() { diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs index 5b5e4740a..52f66a78b 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs @@ -44,6 +44,12 @@ namespace SixLabors.ImageSharp.Tests public string MethodName { get; private set; } public string OutputSubfolderName { get; private set; } + public static TestImageProvider BasicTestPattern(int width, + int height, + MethodInfo testMethod = null, + PixelTypes pixelTypeOverride = PixelTypes.Undefined) + => new BasicTestPatternProvider(width, height).Init(testMethod, pixelTypeOverride); + public static TestImageProvider TestPattern( int width, int height, diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs index 17e5369d4..336cdbf11 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Net.Mime; using System.Numerics; using SixLabors.ImageSharp.Memory; @@ -25,8 +26,10 @@ namespace SixLabors.ImageSharp.Tests { } + /// + /// This parameterless constructor is needed for xUnit deserialization + /// public TestPatternProvider() - : base() { } diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs index cac7828e9..1bee34f1a 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -325,6 +325,20 @@ namespace SixLabors.ImageSharp.Tests } } + [Theory] + [WithBasicTestPatternImages(50, 100, PixelTypes.Rgba32)] + [WithBasicTestPatternImages(49,17, PixelTypes.Rgba32)] + [WithBasicTestPatternImages(20, 10, PixelTypes.Rgba32)] + public void Use_WithBasicTestPatternImages(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image img = provider.GetImage()) + { + img.DebugSave(provider); + } + } + + public static readonly TheoryData BasicData = new TheoryData() { TestImageProvider.Blank(10, 20), From 8695c398f1faed367499053502406a3e0cb05524 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 8 Apr 2019 00:41:18 +0200 Subject: [PATCH 04/37] drop parallelism in ResizeProcessor for simplicity --- .../Transforms/Resize/ResizeProcessor.cs | 72 ++++++++----------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs index 198427d28..37d0ab413 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -244,63 +245,48 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // First process the columns. Since we are not using multiple threads startY and endY // are the upper and lower bounds of the source rectangle. using (Buffer2D firstPassPixelsTransposed = source.MemoryAllocator.Allocate2D(sourceHeight, width)) + using (IMemoryOwner tempBuffer = source.MemoryAllocator.Allocate(Math.Max(source.Width, width))) { firstPassPixelsTransposed.MemorySource.Clear(); - var processColsRect = new Rectangle(0, 0, source.Width, sourceRectangle.Bottom); + for (int y = 0; y < sourceRectangle.Bottom; y++) + { + Span sourceRow = source.GetPixelRowSpan(y).Slice(sourceX); + Span tempRowSpan = tempBuffer.GetSpan().Slice(sourceX, source.Width - sourceX); - ParallelHelper.IterateRowsWithTempBuffer( - processColsRect, - configuration, - (rows, tempRowBuffer) => - { - for (int y = rows.Min; y < rows.Max; y++) - { - Span sourceRow = source.GetPixelRowSpan(y).Slice(sourceX); - Span tempRowSpan = tempRowBuffer.Span.Slice(sourceX); - - PixelOperations.Instance.ToVector4(configuration, sourceRow, tempRowSpan, conversionModifiers); - - ref Vector4 firstPassBaseRef = ref firstPassPixelsTransposed.Span[y]; + PixelOperations.Instance.ToVector4(configuration, sourceRow, tempRowSpan, conversionModifiers); - for (int x = minX; x < maxX; x++) - { - ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - startX); - Unsafe.Add(ref firstPassBaseRef, x * sourceHeight) = kernel.Convolve(tempRowSpan); - } - } - }); + ref Vector4 firstPassBaseRef = ref firstPassPixelsTransposed.Span[y]; - var processRowsRect = Rectangle.FromLTRB(0, minY, width, maxY); + for (int x = minX; x < maxX; x++) + { + ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - startX); + Unsafe.Add(ref firstPassBaseRef, x * sourceHeight) = kernel.Convolve(tempRowSpan); + } + } // Now process the rows. - ParallelHelper.IterateRowsWithTempBuffer( - processRowsRect, - configuration, - (rows, tempRowBuffer) => - { - Span tempRowSpan = tempRowBuffer.Span; + Span tempColSpan = tempBuffer.GetSpan().Slice(0, width); - for (int y = rows.Min; y < rows.Max; y++) - { - // Ensure offsets are normalized for cropping and padding. - ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - startY); + for (int y = minY; y < maxY; y++) + { + // Ensure offsets are normalized for cropping and padding. + ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - startY); - ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempRowSpan); + ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempColSpan); - for (int x = 0; x < width; x++) - { - Span firstPassColumn = firstPassPixelsTransposed.GetRowSpan(x).Slice(sourceY); + for (int x = 0; x < width; x++) + { + Span firstPassColumn = firstPassPixelsTransposed.GetRowSpan(x).Slice(sourceY); - // Destination color components - Unsafe.Add(ref tempRowBase, x) = kernel.Convolve(firstPassColumn); - } + // Destination color components + Unsafe.Add(ref tempRowBase, x) = kernel.Convolve(firstPassColumn); + } - Span targetRowSpan = destination.GetPixelRowSpan(y); + Span targetRowSpan = destination.GetPixelRowSpan(y); - PixelOperations.Instance.FromVector4Destructive(configuration, tempRowSpan, targetRowSpan, conversionModifiers); - } - }); + PixelOperations.Instance.FromVector4Destructive(configuration, tempColSpan, targetRowSpan, conversionModifiers); + } } } From 701343a23e0c6aa45fa9025f06b93224f4b97d83 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 8 Apr 2019 00:59:24 +0200 Subject: [PATCH 05/37] ResizeRectangle -> TargetRectangle --- .../Transforms/Resize/ResizeProcessor.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs index 37d0ab413..cb1ab2d54 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -66,7 +66,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.Sampler = options.Sampler; this.Width = size.Width; this.Height = size.Height; - this.ResizeRectangle = rectangle; + this.TargetRectangle = rectangle; this.Compand = options.Compand; } @@ -89,11 +89,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The target width. /// The target height. /// The source image size - /// + /// /// The structure that specifies the portion of the target image object to draw to. /// /// Whether to compress or expand individual pixel color values on processing. - public ResizeProcessor(IResampler sampler, int width, int height, Size sourceSize, Rectangle resizeRectangle, bool compand) + public ResizeProcessor(IResampler sampler, int width, int height, Size sourceSize, Rectangle targetRectangle, bool compand) { Guard.NotNull(sampler, nameof(sampler)); @@ -104,13 +104,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms if (width == 0 && height > 0) { width = (int)MathF.Max(min, MathF.Round(sourceSize.Width * height / (float)sourceSize.Height)); - resizeRectangle.Width = width; + targetRectangle.Width = width; } if (height == 0 && width > 0) { height = (int)MathF.Max(min, MathF.Round(sourceSize.Height * width / (float)sourceSize.Width)); - resizeRectangle.Height = height; + targetRectangle.Height = height; } Guard.MustBeGreaterThan(width, 0, nameof(width)); @@ -119,7 +119,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.Sampler = sampler; this.Width = width; this.Height = height; - this.ResizeRectangle = resizeRectangle; + this.TargetRectangle = targetRectangle; this.Compand = compand; } @@ -141,7 +141,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// Gets the resize rectangle. /// - public Rectangle ResizeRectangle { get; } + public Rectangle TargetRectangle { get; } /// /// Gets a value indicating whether to compress or expand individual pixel color values on processing. @@ -167,13 +167,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms MemoryAllocator memoryAllocator = source.GetMemoryAllocator(); this.horizontalKernelMap = ResizeKernelMap.Calculate( this.Sampler, - this.ResizeRectangle.Width, + this.TargetRectangle.Width, sourceRectangle.Width, memoryAllocator); this.verticalKernelMap = ResizeKernelMap.Calculate( this.Sampler, - this.ResizeRectangle.Height, + this.TargetRectangle.Height, sourceRectangle.Height, memoryAllocator); } @@ -183,7 +183,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms protected override void OnFrameApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) { // Handle resize dimensions identical to the original - if (source.Width == destination.Width && source.Height == destination.Height && sourceRectangle == this.ResizeRectangle) + if (source.Width == destination.Width && source.Height == destination.Height && sourceRectangle == this.TargetRectangle) { // The cloned will be blank here copy all the pixel data over source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); @@ -194,10 +194,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int height = this.Height; int sourceX = sourceRectangle.X; int sourceY = sourceRectangle.Y; - int startY = this.ResizeRectangle.Y; - int endY = this.ResizeRectangle.Bottom; - int startX = this.ResizeRectangle.X; - int endX = this.ResizeRectangle.Right; + int startY = this.TargetRectangle.Y; + int endY = this.TargetRectangle.Bottom; + int startX = this.TargetRectangle.X; + int endX = this.TargetRectangle.Right; int minX = Math.Max(0, startX); int maxX = Math.Min(width, endX); @@ -209,8 +209,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); // Scaling factors - float widthFactor = sourceRectangle.Width / (float)this.ResizeRectangle.Width; - float heightFactor = sourceRectangle.Height / (float)this.ResizeRectangle.Height; + float widthFactor = sourceRectangle.Width / (float)this.TargetRectangle.Width; + float heightFactor = sourceRectangle.Height / (float)this.TargetRectangle.Height; ParallelHelper.IterateRows( workingRect, From 4d38d7c426f4b6135230c052ce20c0a22d79862a Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 14 Apr 2019 17:51:30 +0200 Subject: [PATCH 06/37] ResizeWindowOld --- .../Transforms/Resize/ResizeProcessor.cs | 122 +++++++++++++++--- .../Transforms/ResizeKernelMapTests.cs | 7 +- .../Processors/Transforms/ResizeTests.cs | 13 +- 3 files changed, 119 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs index 8c911cbfa..429b709ee 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -244,26 +244,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // A 2-pass 1D algorithm appears to be faster than splitting a 1-pass 2D algorithm // First process the columns. Since we are not using multiple threads startY and endY // are the upper and lower bounds of the source rectangle. - using (Buffer2D firstPassPixelsTransposed = source.MemoryAllocator.Allocate2D(sourceHeight, width)) + //using (Buffer2D firstPassPixelsTransposed = source.MemoryAllocator.Allocate2D(sourceHeight, width)) + + using (var resizeWindow = new ResizeWindowOld( + configuration, + sourceRectangle, + conversionModifiers, + this.horizontalKernelMap, + this.verticalKernelMap, + sourceHeight, + width, + minX, + maxX, + startX)) using (IMemoryOwner tempBuffer = source.MemoryAllocator.Allocate(Math.Max(source.Width, width))) { - firstPassPixelsTransposed.MemorySource.Clear(); + Span tempRowSpan = tempBuffer.GetSpan().Slice(sourceX, source.Width - sourceX); - for (int y = 0; y < sourceRectangle.Bottom; y++) - { - Span sourceRow = source.GetPixelRowSpan(y).Slice(sourceX); - Span tempRowSpan = tempBuffer.GetSpan().Slice(sourceX, source.Width - sourceX); - - PixelOperations.Instance.ToVector4(configuration, sourceRow, tempRowSpan, conversionModifiers); - - ref Vector4 firstPassBaseRef = ref firstPassPixelsTransposed.Span[y]; - - for (int x = minX; x < maxX; x++) - { - ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - startX); - Unsafe.Add(ref firstPassBaseRef, x * sourceHeight) = kernel.Convolve(tempRowSpan); - } - } + resizeWindow.Initialize(source.PixelBuffer, tempRowSpan); // Now process the rows. Span tempColSpan = tempBuffer.GetSpan().Slice(0, width); @@ -277,7 +275,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int x = 0; x < width; x++) { - Span firstPassColumn = firstPassPixelsTransposed.GetRowSpan(x).Slice(sourceY); + Span firstPassColumn = resizeWindow.GetColumnSpan(x); // Destination color components Unsafe.Add(ref tempRowBase, x) = kernel.Convolve(firstPassColumn); @@ -301,4 +299,92 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.verticalKernelMap = null; } } + + class ResizeWindowOld : IDisposable + { + private readonly Buffer2D buffer; + + private readonly Configuration configuration; + + private readonly Rectangle sourceRectangle; + + private readonly PixelConversionModifiers conversionModifiers; + + private readonly ResizeKernelMap horizontalKernelMap; + + private readonly ResizeKernelMap verticalKernelMap; + + private readonly int minX; + + private readonly int maxX; + + private readonly int startX; + + public ResizeWindowOld( + Configuration configuration, + Rectangle sourceRectangle, + PixelConversionModifiers conversionModifiers, + ResizeKernelMap horizontalKernelMap, + ResizeKernelMap verticalKernelMap, + int sourceHeight, + int destWidth, + int minX, + int maxX, + int startX) + { + this.configuration = configuration; + this.sourceRectangle = sourceRectangle; + this.conversionModifiers = conversionModifiers; + this.horizontalKernelMap = horizontalKernelMap; + this.verticalKernelMap = verticalKernelMap; + this.minX = minX; + this.maxX = maxX; + this.startX = startX; + this.buffer = configuration.MemoryAllocator.Allocate2D(sourceRectangle.Height, destWidth, AllocationOptions.Clean); + + this.Top = sourceRectangle.Top; + + this.Bottom = sourceRectangle.Bottom; + } + + public int Top { get; private set; } + + public int Bottom { get; private set; } + + public void Initialize( + Buffer2D source, + Span tempRowSpan) + where TPixel : struct, IPixel + { + for (int y = this.sourceRectangle.Top; y < this.sourceRectangle.Bottom; y++) + { + Span sourceRow = source.GetRowSpan(y).Slice(this.sourceRectangle.X); + + PixelOperations.Instance.ToVector4( + this.configuration, + sourceRow, + tempRowSpan, + this.conversionModifiers); + + ref Vector4 firstPassBaseRef = ref this.buffer.Span[y - this.sourceRectangle.Y]; + + for (int x = this.minX; x < this.maxX; x++) + { + ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - this.startX); + Unsafe.Add(ref firstPassBaseRef, x * this.sourceRectangle.Height) = kernel.Convolve(tempRowSpan); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetColumnSpan(int x) + { + return this.buffer.GetRowSpan(x); + } + + public void Dispose() + { + this.buffer.Dispose(); + } + } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs index 5d3790f07..9de866586 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs @@ -93,7 +93,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms GenerateImageResizeData(); - [Theory(Skip = "Only for debugging and development")] + [Theory( + Skip = "Only for debugging and development" + )] [MemberData(nameof(KernelMapData))] public void PrintNonNormalizedKernelMap(string resamplerName, int srcSize, int destSize) { @@ -130,7 +132,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms var referenceMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize); var kernelMap = ResizeKernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator); + + #if DEBUG + this.Output.WriteLine(kernelMap.Info); this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n"); this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); #endif diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 7f1f5a26a..5cea8ddae 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -5,6 +5,7 @@ using System; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; @@ -37,19 +38,23 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.RgbaVector; [Theory] - [WithBasicTestPatternImages(15, 12, PixelTypes.Rgba32)] - public void Resize_BasicSmall(TestImageProvider provider) + [WithBasicTestPatternImages(15, 12, PixelTypes.Rgba32, 2, 3, 1, 2)] + [WithBasicTestPatternImages(2, 256, PixelTypes.Rgba32, 1, 1, 1, 8)] + [WithBasicTestPatternImages(2, 32, PixelTypes.Rgba32, 1, 1, 1, 2)] + public void Resize_BasicSmall(TestImageProvider provider, int wN, int wD, int hN, int hD) where TPixel : struct, IPixel { // Basic test case, very helpful for debugging + // [WithBasicTestPatternImages(15, 12, PixelTypes.Rgba32, 2, 3, 1, 2)] means: // resizing: (15, 12) -> (10, 6) // kernel dimensions: (3, 4) using (Image image = provider.GetImage()) { - var destSize = new Size(image.Width * 2 / 3, image.Height / 2); + var destSize = new Size(image.Width * wN / wD, image.Height * hN / hD); image.Mutate(x => x.Resize(destSize, KnownResamplers.Bicubic, false)); - image.DebugSave(provider); + FormattableString outputInfo = $"({wN}÷{wD},{hN}÷{hD})"; + image.DebugSave(provider, outputInfo); } } From f81939e79177ba571dc429806ed96c4c11d49fbb Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 14 Apr 2019 18:37:16 +0200 Subject: [PATCH 07/37] ResizeWindow refactor 1 --- .../Transforms/Resize/ResizeProcessor.cs | 115 ++---------------- .../Transforms/Resize/ResizeWindow.cs | 97 +++++++++++++++ 2 files changed, 106 insertions(+), 106 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs index 429b709ee..707be1ec9 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -195,19 +195,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int sourceX = sourceRectangle.X; int sourceY = sourceRectangle.Y; int startY = this.TargetRectangle.Y; - int endY = this.TargetRectangle.Bottom; int startX = this.TargetRectangle.X; - int endX = this.TargetRectangle.Right; - int minX = Math.Max(0, startX); - int maxX = Math.Min(width, endX); - int minY = Math.Max(0, startY); - int maxY = Math.Min(height, endY); + var workingRect = Rectangle.Intersect( + this.TargetRectangle, + new Rectangle(0, 0, width, height)); if (this.Sampler is NearestNeighborResampler) { - var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); - // Scaling factors float widthFactor = sourceRectangle.Width / (float)this.TargetRectangle.Width; float heightFactor = sourceRectangle.Height / (float)this.TargetRectangle.Height; @@ -224,7 +219,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms source.GetPixelRowSpan((int)(((y - startY) * heightFactor) + sourceY)); Span targetRow = destination.GetPixelRowSpan(y); - for (int x = minX; x < maxX; x++) + for (int x = workingRect.Left; x < workingRect.Right; x++) { // X coordinates of source points targetRow[x] = sourceRow[(int)(((x - startX) * widthFactor) + sourceX)]; @@ -244,29 +239,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // A 2-pass 1D algorithm appears to be faster than splitting a 1-pass 2D algorithm // First process the columns. Since we are not using multiple threads startY and endY // are the upper and lower bounds of the source rectangle. - //using (Buffer2D firstPassPixelsTransposed = source.MemoryAllocator.Allocate2D(sourceHeight, width)) - - using (var resizeWindow = new ResizeWindowOld( + using (var resizeWindow = new ResizeWindow( configuration, sourceRectangle, conversionModifiers, this.horizontalKernelMap, this.verticalKernelMap, - sourceHeight, width, - minX, - maxX, + workingRect, startX)) - using (IMemoryOwner tempBuffer = source.MemoryAllocator.Allocate(Math.Max(source.Width, width))) + using (IMemoryOwner tempBuffer = source.MemoryAllocator.Allocate(Math.Max(sourceRectangle.Width, width))) { - Span tempRowSpan = tempBuffer.GetSpan().Slice(sourceX, source.Width - sourceX); + Span tempRowSpan = tempBuffer.GetSpan(); resizeWindow.Initialize(source.PixelBuffer, tempRowSpan); // Now process the rows. Span tempColSpan = tempBuffer.GetSpan().Slice(0, width); - for (int y = minY; y < maxY; y++) + for (int y = workingRect.Top; y < workingRect.Bottom; y++) { // Ensure offsets are normalized for cropping and padding. ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - startY); @@ -299,92 +290,4 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.verticalKernelMap = null; } } - - class ResizeWindowOld : IDisposable - { - private readonly Buffer2D buffer; - - private readonly Configuration configuration; - - private readonly Rectangle sourceRectangle; - - private readonly PixelConversionModifiers conversionModifiers; - - private readonly ResizeKernelMap horizontalKernelMap; - - private readonly ResizeKernelMap verticalKernelMap; - - private readonly int minX; - - private readonly int maxX; - - private readonly int startX; - - public ResizeWindowOld( - Configuration configuration, - Rectangle sourceRectangle, - PixelConversionModifiers conversionModifiers, - ResizeKernelMap horizontalKernelMap, - ResizeKernelMap verticalKernelMap, - int sourceHeight, - int destWidth, - int minX, - int maxX, - int startX) - { - this.configuration = configuration; - this.sourceRectangle = sourceRectangle; - this.conversionModifiers = conversionModifiers; - this.horizontalKernelMap = horizontalKernelMap; - this.verticalKernelMap = verticalKernelMap; - this.minX = minX; - this.maxX = maxX; - this.startX = startX; - this.buffer = configuration.MemoryAllocator.Allocate2D(sourceRectangle.Height, destWidth, AllocationOptions.Clean); - - this.Top = sourceRectangle.Top; - - this.Bottom = sourceRectangle.Bottom; - } - - public int Top { get; private set; } - - public int Bottom { get; private set; } - - public void Initialize( - Buffer2D source, - Span tempRowSpan) - where TPixel : struct, IPixel - { - for (int y = this.sourceRectangle.Top; y < this.sourceRectangle.Bottom; y++) - { - Span sourceRow = source.GetRowSpan(y).Slice(this.sourceRectangle.X); - - PixelOperations.Instance.ToVector4( - this.configuration, - sourceRow, - tempRowSpan, - this.conversionModifiers); - - ref Vector4 firstPassBaseRef = ref this.buffer.Span[y - this.sourceRectangle.Y]; - - for (int x = this.minX; x < this.maxX; x++) - { - ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - this.startX); - Unsafe.Add(ref firstPassBaseRef, x * this.sourceRectangle.Height) = kernel.Convolve(tempRowSpan); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span GetColumnSpan(int x) - { - return this.buffer.GetRowSpan(x); - } - - public void Dispose() - { - this.buffer.Dispose(); - } - } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs new file mode 100644 index 000000000..130dd93a4 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs @@ -0,0 +1,97 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Memory; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms +{ + class ResizeWindow : IDisposable + { + private readonly Buffer2D buffer; + + private readonly Configuration configuration; + + private readonly Rectangle sourceRectangle; + + private readonly PixelConversionModifiers conversionModifiers; + + private readonly ResizeKernelMap horizontalKernelMap; + + private readonly ResizeKernelMap verticalKernelMap; + + private readonly Rectangle workingRectangle; + + private readonly int startX; + + public ResizeWindow( + Configuration configuration, + Rectangle sourceRectangle, + PixelConversionModifiers conversionModifiers, + ResizeKernelMap horizontalKernelMap, + ResizeKernelMap verticalKernelMap, + int destWidth, + Rectangle workingRectangle, + int startX) + { + this.configuration = configuration; + this.sourceRectangle = sourceRectangle; + this.conversionModifiers = conversionModifiers; + this.horizontalKernelMap = horizontalKernelMap; + this.verticalKernelMap = verticalKernelMap; + this.workingRectangle = workingRectangle; + this.startX = startX; + this.buffer = configuration.MemoryAllocator.Allocate2D(sourceRectangle.Height, destWidth, AllocationOptions.Clean); + + this.Top = sourceRectangle.Top; + + this.Bottom = sourceRectangle.Bottom; + } + + public int Top { get; private set; } + + public int Bottom { get; private set; } + + public void Initialize( + Buffer2D source, + Span tempRowSpan) + where TPixel : struct, IPixel + { + for (int y = this.sourceRectangle.Top; y < this.sourceRectangle.Bottom; y++) + { + Span sourceRow = source.GetRowSpan(y).Slice(this.sourceRectangle.X); + + PixelOperations.Instance.ToVector4( + this.configuration, + sourceRow, + tempRowSpan, + this.conversionModifiers); + + ref Vector4 firstPassBaseRef = ref this.buffer.Span[y - this.sourceRectangle.Y]; + + for (int x = this.workingRectangle.Left; x < this.workingRectangle.Right; x++) + { + ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - this.startX); + Unsafe.Add(ref firstPassBaseRef, x * this.sourceRectangle.Height) = kernel.Convolve(tempRowSpan); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetColumnSpan(int x) + { + return this.buffer.GetRowSpan(x); + } + + public void Dispose() + { + this.buffer.Dispose(); + } + } +} \ No newline at end of file From 7079410229079411250a9cbeed9307dae5e3bad7 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 14 Apr 2019 21:19:43 +0200 Subject: [PATCH 08/37] ResizeWindow refactor 2 --- .../Transforms/Resize/ResizeProcessor.cs | 14 ++-- .../Transforms/Resize/ResizeWindow.cs | 67 +++++++++++-------- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs index 707be1ec9..b4b537e20 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -235,27 +235,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms PixelConversionModifiers conversionModifiers = PixelConversionModifiers.Premultiply.ApplyCompanding(this.Compand); + BufferArea sourceArea = source.PixelBuffer.GetArea(sourceRectangle); + // Interpolate the image using the calculated weights. // A 2-pass 1D algorithm appears to be faster than splitting a 1-pass 2D algorithm // First process the columns. Since we are not using multiple threads startY and endY // are the upper and lower bounds of the source rectangle. - using (var resizeWindow = new ResizeWindow( + using (var resizeWindow = new ResizeWindow( configuration, - sourceRectangle, + sourceArea, conversionModifiers, this.horizontalKernelMap, this.verticalKernelMap, width, workingRect, startX)) - using (IMemoryOwner tempBuffer = source.MemoryAllocator.Allocate(Math.Max(sourceRectangle.Width, width))) + using (IMemoryOwner tempBuffer = source.MemoryAllocator.Allocate(width)) { - Span tempRowSpan = tempBuffer.GetSpan(); - - resizeWindow.Initialize(source.PixelBuffer, tempRowSpan); + resizeWindow.Initialize(); // Now process the rows. - Span tempColSpan = tempBuffer.GetSpan().Slice(0, width); + Span tempColSpan = tempBuffer.GetSpan(); for (int y = workingRect.Top; y < workingRect.Bottom; y++) { diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs index 130dd93a4..bd68fdba7 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; @@ -12,27 +13,32 @@ using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Transforms { - class ResizeWindow : IDisposable + class ResizeWindow : IDisposable + where TPixel : struct, IPixel { private readonly Buffer2D buffer; private readonly Configuration configuration; - private readonly Rectangle sourceRectangle; - private readonly PixelConversionModifiers conversionModifiers; private readonly ResizeKernelMap horizontalKernelMap; - private readonly ResizeKernelMap verticalKernelMap; + private readonly BufferArea source; - private readonly Rectangle workingRectangle; + private readonly Rectangle sourceRectangle; private readonly int startX; + private readonly IMemoryOwner tempRowBuffer; + + private readonly ResizeKernelMap verticalKernelMap; + + private readonly Rectangle workingRectangle; + public ResizeWindow( Configuration configuration, - Rectangle sourceRectangle, + BufferArea source, PixelConversionModifiers conversionModifiers, ResizeKernelMap horizontalKernelMap, ResizeKernelMap verticalKernelMap, @@ -41,31 +47,45 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int startX) { this.configuration = configuration; - this.sourceRectangle = sourceRectangle; + this.source = source; + this.sourceRectangle = source.Rectangle; this.conversionModifiers = conversionModifiers; this.horizontalKernelMap = horizontalKernelMap; this.verticalKernelMap = verticalKernelMap; this.workingRectangle = workingRectangle; this.startX = startX; - this.buffer = configuration.MemoryAllocator.Allocate2D(sourceRectangle.Height, destWidth, AllocationOptions.Clean); + this.buffer = configuration.MemoryAllocator.Allocate2D( + this.sourceRectangle.Height, + destWidth, + AllocationOptions.Clean); + this.tempRowBuffer = configuration.MemoryAllocator.Allocate(this.sourceRectangle.Width); - this.Top = sourceRectangle.Top; + this.Top = this.sourceRectangle.Top; - this.Bottom = sourceRectangle.Bottom; + this.Bottom = this.sourceRectangle.Bottom; } + public int Bottom { get; private set; } + public int Top { get; private set; } - public int Bottom { get; private set; } + public void Dispose() + { + this.buffer.Dispose(); + } - public void Initialize( - Buffer2D source, - Span tempRowSpan) - where TPixel : struct, IPixel + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetColumnSpan(int x) { - for (int y = this.sourceRectangle.Top; y < this.sourceRectangle.Bottom; y++) + return this.buffer.GetRowSpan(x); + } + + public void Initialize() + { + Span tempRowSpan = this.tempRowBuffer.GetSpan(); + for (int y = 0; y < this.sourceRectangle.Height; y++) { - Span sourceRow = source.GetRowSpan(y).Slice(this.sourceRectangle.X); + Span sourceRow = this.source.GetRowSpan(y); PixelOperations.Instance.ToVector4( this.configuration, @@ -73,7 +93,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms tempRowSpan, this.conversionModifiers); - ref Vector4 firstPassBaseRef = ref this.buffer.Span[y - this.sourceRectangle.Y]; + ref Vector4 firstPassBaseRef = ref this.buffer.Span[y]; for (int x = this.workingRectangle.Left; x < this.workingRectangle.Right; x++) { @@ -82,16 +102,5 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } } } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span GetColumnSpan(int x) - { - return this.buffer.GetRowSpan(x); - } - - public void Dispose() - { - this.buffer.Dispose(); - } } } \ No newline at end of file From 545abf2d0614437ad7cdab827b89f4d67bcb00be Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 14 Apr 2019 21:59:06 +0200 Subject: [PATCH 09/37] ResizeWindow refactor 3 --- .../Transforms/Resize/ResizeKernelMap.cs | 11 +- .../Transforms/Resize/ResizeProcessor.cs | 5 + .../Transforms/Resize/ResizeWindow.cs | 12 +- .../Processors/Transforms/ResizeTests.cs | 433 +++++++++--------- 4 files changed, 245 insertions(+), 216 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs index 2ab574df2..9abbb66e3 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -54,11 +54,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.radius = radius; this.sourceLength = sourceLength; this.DestinationLength = destinationLength; - int maxWidth = (radius * 2) + 1; - this.data = memoryAllocator.Allocate2D(maxWidth, bufferHeight, AllocationOptions.Clean); + this.MaxDiameter = (radius * 2) + 1; + this.data = memoryAllocator.Allocate2D(this.MaxDiameter, bufferHeight, AllocationOptions.Clean); this.pinHandle = this.data.Memory.Pin(); this.kernels = new ResizeKernel[destinationLength]; - this.tempValues = new double[maxWidth]; + this.tempValues = new double[this.MaxDiameter]; } /// @@ -66,6 +66,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// public int DestinationLength { get; } + /// + /// Gets the maximum diameter of the kernels. + /// + public int MaxDiameter { get; } + /// /// Gets a string of information to help debugging /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs index b4b537e20..38419b5dd 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -262,6 +262,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // Ensure offsets are normalized for cropping and padding. ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - startY); + if (kernel.Left + kernel.Length > resizeWindow.Bottom) + { + resizeWindow.Slide(); + } + ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempColSpan); for (int x = 0; x < width; x++) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs index bd68fdba7..05015dee2 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs @@ -13,7 +13,7 @@ using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Transforms { - class ResizeWindow : IDisposable + internal class ResizeWindow : IDisposable where TPixel : struct, IPixel { private readonly Buffer2D buffer; @@ -36,6 +36,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly Rectangle workingRectangle; + private readonly int diameter; + public ResizeWindow( Configuration configuration, BufferArea source, @@ -54,6 +56,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.verticalKernelMap = verticalKernelMap; this.workingRectangle = workingRectangle; this.startX = startX; + + this.diameter = verticalKernelMap.MaxDiameter; + this.buffer = configuration.MemoryAllocator.Allocate2D( this.sourceRectangle.Height, destWidth, @@ -102,5 +107,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } } } + + public void Slide() + { + throw new InvalidOperationException("Shouldn't happen yet!"); + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 5cea8ddae..8c59fff26 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -5,38 +5,92 @@ using System; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; using Xunit; + // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { public class ResizeTests { - public static readonly string[] CommonTestImages = { TestImages.Png.CalliphoraPartial }; + private const PixelTypes CommonNonDefaultPixelTypes = + PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.RgbaVector; - private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.07F); + private const PixelTypes DefaultPixelType = PixelTypes.Rgba32; public static readonly string[] AllResamplerNames = TestUtils.GetAllResamplerNames(); + public static readonly string[] CommonTestImages = { TestImages.Png.CalliphoraPartial }; + public static readonly string[] SmokeTestResamplerNames = { - nameof(KnownResamplers.NearestNeighbor), - nameof(KnownResamplers.Bicubic), - nameof(KnownResamplers.Box), + nameof(KnownResamplers.NearestNeighbor), nameof(KnownResamplers.Bicubic), nameof(KnownResamplers.Box), nameof(KnownResamplers.Lanczos5), }; - private const PixelTypes DefaultPixelType = PixelTypes.Rgba32; + private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.07F); + + [Theory] + [InlineData(-2, 0)] + [InlineData(-1, 0)] + [InlineData(0, 1)] + [InlineData(1, 0)] + [InlineData(2, 0)] + public static void BicubicWindowOscillatesCorrectly(float x, float expected) + { + IResampler sampler = KnownResamplers.Bicubic; + float result = sampler.GetValue(x); + + Assert.Equal(result, expected); + } + + [Theory] + [InlineData(-2, 0)] + [InlineData(-1, 0)] + [InlineData(0, 1)] + [InlineData(1, 0)] + [InlineData(2, 0)] + public static void Lanczos3WindowOscillatesCorrectly(float x, float expected) + { + IResampler sampler = KnownResamplers.Lanczos3; + float result = sampler.GetValue(x); + + Assert.Equal(result, expected); + } + + [Theory] + [InlineData(-4, 0)] + [InlineData(-2, 0)] + [InlineData(0, 1)] + [InlineData(2, 0)] + [InlineData(4, 0)] + public static void Lanczos5WindowOscillatesCorrectly(float x, float expected) + { + IResampler sampler = KnownResamplers.Lanczos5; + float result = sampler.GetValue(x); + + Assert.Equal(result, expected); + } + + [Theory] + [InlineData(-2, 0)] + [InlineData(-1, 0)] + [InlineData(0, 1)] + [InlineData(1, 0)] + [InlineData(2, 0)] + public static void TriangleWindowOscillatesCorrectly(float x, float expected) + { + IResampler sampler = KnownResamplers.Triangle; + float result = sampler.GetValue(x); + + Assert.Equal(result, expected); + } - private const PixelTypes CommonNonDefaultPixelTypes = - PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.RgbaVector; - [Theory] [WithBasicTestPatternImages(15, 12, PixelTypes.Rgba32, 2, 3, 1, 2)] [WithBasicTestPatternImages(2, 256, PixelTypes.Rgba32, 1, 1, 1, 8)] @@ -54,14 +108,117 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms var destSize = new Size(image.Width * wN / wD, image.Height * hN / hD); image.Mutate(x => x.Resize(destSize, KnownResamplers.Bicubic, false)); FormattableString outputInfo = $"({wN}÷{wD},{hN}÷{hD})"; - image.DebugSave(provider, outputInfo); + image.DebugSave(provider, outputInfo, appendPixelTypeToFileName: false); + image.CompareToReferenceOutput(provider, outputInfo, appendPixelTypeToFileName: false); + } + } + + [Theory] + [WithTestPatternImages(100, 100, DefaultPixelType)] + public void Resize_Compand(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + image.Mutate(x => x.Resize(image.Size() / 2, true)); + + image.DebugSave(provider); + image.CompareToReferenceOutput(ValidatorComparer, provider); + } + } + + [Theory] + [WithFile(TestImages.Png.Kaboom, DefaultPixelType, false)] + [WithFile(TestImages.Png.Kaboom, DefaultPixelType, true)] + public void Resize_DoesNotBleedAlphaPixels(TestImageProvider provider, bool compand) + where TPixel : struct, IPixel + { + string details = compand ? "Compand" : ""; + + provider.RunValidatingProcessorTest( + x => x.Resize(x.GetCurrentSize() / 2, compand), + details, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithFile(TestImages.Gif.Giphy, DefaultPixelType)] + public void Resize_IsAppliedToAllFrames(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + image.Mutate(x => x.Resize(image.Width / 2, image.Height / 2, KnownResamplers.Bicubic)); + + // Comparer fights decoder with gif-s. Could not use CompareToReferenceOutput here :( + image.DebugSave(provider, extension: "gif"); + } + } + + [Theory] + [WithTestPatternImages(50, 50, CommonNonDefaultPixelTypes)] + public void Resize_IsNotBoundToSinglePixelType(TestImageProvider provider) + where TPixel : struct, IPixel + { + provider.RunValidatingProcessorTest(x => x.Resize(x.GetCurrentSize() / 2), comparer: ValidatorComparer); + } + + [Theory] + [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] + public void Resize_ThrowsForWrappedMemoryImage(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image0 = provider.GetImage()) + { + var mmg = TestMemoryManager.CreateAsCopyOf(image0.GetPixelSpan()); + + using (var image1 = Image.WrapMemory(mmg.Memory, image0.Width, image0.Height)) + { + Assert.ThrowsAny( + () => { image1.Mutate(x => x.Resize(image0.Width / 2, image0.Height / 2, true)); }); + } } } + [Theory] + [WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 1)] + [WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 4)] + [WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 8)] + [WithFileCollection(nameof(CommonTestImages), DefaultPixelType, -1)] + public void Resize_WorksWithAllParallelismLevels( + TestImageProvider provider, + int maxDegreeOfParallelism) + where TPixel : struct, IPixel + { + provider.Configuration.MaxDegreeOfParallelism = + maxDegreeOfParallelism > 0 ? maxDegreeOfParallelism : Environment.ProcessorCount; + + FormattableString details = $"MDP{maxDegreeOfParallelism}"; + + provider.RunValidatingProcessorTest( + x => x.Resize(x.GetCurrentSize() / 2), + details, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + [Theory] [WithFileCollection(nameof(CommonTestImages), nameof(AllResamplerNames), DefaultPixelType, 0.5f, null, null)] - [WithFileCollection(nameof(CommonTestImages), nameof(SmokeTestResamplerNames), DefaultPixelType, 0.3f, null, null)] - [WithFileCollection(nameof(CommonTestImages), nameof(SmokeTestResamplerNames), DefaultPixelType, 1.8f, null, null)] + [WithFileCollection( + nameof(CommonTestImages), + nameof(SmokeTestResamplerNames), + DefaultPixelType, + 0.3f, + null, + null)] + [WithFileCollection( + nameof(CommonTestImages), + nameof(SmokeTestResamplerNames), + DefaultPixelType, + 1.8f, + null, + null)] [WithTestPatternImages(nameof(SmokeTestResamplerNames), 100, 100, DefaultPixelType, 0.5f, null, null)] [WithTestPatternImages(nameof(SmokeTestResamplerNames), 100, 100, DefaultPixelType, 1f, null, null)] [WithTestPatternImages(nameof(SmokeTestResamplerNames), 50, 50, DefaultPixelType, 8f, null, null)] @@ -84,15 +241,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms // Resize_WorksWithAllResamplers_TestPattern301x1180_NearestNeighbor-300x480.png // TODO: Should we investigate this? bool allowHigherInaccuracy = !TestEnvironment.Is64BitProcess - && string.IsNullOrEmpty(TestEnvironment.NetCoreVersion) - && sampler is NearestNeighborResampler; + && string.IsNullOrEmpty(TestEnvironment.NetCoreVersion) + && sampler is NearestNeighborResampler; var comparer = ImageComparer.TolerantPercentage(allowHigherInaccuracy ? 0.3f : 0.017f); provider.RunValidatingProcessorTest( ctx => { - SizeF newSize; string destSizeInfo; if (ratio.HasValue) @@ -126,107 +282,41 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } [Theory] - [WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 1)] - [WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 4)] - [WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 8)] - [WithFileCollection(nameof(CommonTestImages), DefaultPixelType, -1)] - public void Resize_WorksWithAllParallelismLevels(TestImageProvider provider, int maxDegreeOfParallelism) - where TPixel : struct, IPixel - { - provider.Configuration.MaxDegreeOfParallelism = - maxDegreeOfParallelism > 0 ? maxDegreeOfParallelism : Environment.ProcessorCount; - - FormattableString details = $"MDP{maxDegreeOfParallelism}"; - - provider.RunValidatingProcessorTest( - x => x.Resize(x.GetCurrentSize() / 2), - details, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithTestPatternImages(100, 100, DefaultPixelType)] - public void Resize_Compand(TestImageProvider provider) + [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] + public void ResizeFromSourceRectangle(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) { - image.Mutate(x => x.Resize(image.Size() / 2, true)); + var sourceRectangle = new Rectangle( + image.Width / 8, + image.Height / 8, + image.Width / 4, + image.Height / 4); + var destRectangle = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); + + image.Mutate( + x => x.Resize( + image.Width, + image.Height, + KnownResamplers.Bicubic, + sourceRectangle, + destRectangle, + false)); image.DebugSave(provider); image.CompareToReferenceOutput(ValidatorComparer, provider); } } - [Theory] - [WithTestPatternImages(50, 50, CommonNonDefaultPixelTypes)] - public void Resize_IsNotBoundToSinglePixelType(TestImageProvider provider) - where TPixel : struct, IPixel - { - provider.RunValidatingProcessorTest(x => x.Resize(x.GetCurrentSize() / 2), comparer: ValidatorComparer); - } - - [Theory] - [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] - public void Resize_ThrowsForWrappedMemoryImage(TestImageProvider provider) - where TPixel : struct, IPixel - { - using (Image image0 = provider.GetImage()) - { - var mmg = TestMemoryManager.CreateAsCopyOf(image0.GetPixelSpan()); - - using (var image1 = Image.WrapMemory(mmg.Memory, image0.Width, image0.Height)) - { - Assert.ThrowsAny( - () => - { - image1.Mutate(x => x.Resize(image0.Width / 2, image0.Height / 2, true)); - }); - } - } - } - - [Theory] - [WithFile(TestImages.Png.Kaboom, DefaultPixelType, false)] - [WithFile(TestImages.Png.Kaboom, DefaultPixelType, true)] - public void Resize_DoesNotBleedAlphaPixels(TestImageProvider provider, bool compand) - where TPixel : struct, IPixel - { - string details = compand ? "Compand" : ""; - - provider.RunValidatingProcessorTest( - x => x.Resize(x.GetCurrentSize() / 2, compand), - details, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithFile(TestImages.Gif.Giphy, DefaultPixelType)] - public void Resize_IsAppliedToAllFrames(TestImageProvider provider) - where TPixel : struct, IPixel - { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Resize(image.Width / 2, image.Height / 2, KnownResamplers.Bicubic)); - - // Comparer fights decoder with gif-s. Could not use CompareToReferenceOutput here :( - image.DebugSave(provider, extension: "gif"); - } - } - [Theory] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] - public void ResizeFromSourceRectangle(TestImageProvider provider) + public void ResizeHeightAndKeepAspect(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) { - var sourceRectangle = new Rectangle(image.Width / 8, image.Height / 8, image.Width / 4, image.Height / 4); - var destRectangle = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); - - image.Mutate(x => x.Resize(image.Width, image.Height, KnownResamplers.Bicubic, sourceRectangle, destRectangle, false)); + image.Mutate(x => x.Resize(0, image.Height / 3, false)); image.DebugSave(provider); image.CompareToReferenceOutput(ValidatorComparer, provider); @@ -234,27 +324,26 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } [Theory] - [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] - public void ResizeWidthAndKeepAspect(TestImageProvider provider) + [WithTestPatternImages(10, 100, DefaultPixelType)] + public void ResizeHeightCannotKeepAspectKeepsOnePixel(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) { - image.Mutate(x => x.Resize(image.Width / 3, 0, false)); - - image.DebugSave(provider); - image.CompareToReferenceOutput(ValidatorComparer, provider); + image.Mutate(x => x.Resize(0, 5)); + Assert.Equal(1, image.Width); + Assert.Equal(5, image.Height); } } [Theory] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] - public void ResizeHeightAndKeepAspect(TestImageProvider provider) + public void ResizeWidthAndKeepAspect(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) { - image.Mutate(x => x.Resize(0, image.Height / 3, false)); + image.Mutate(x => x.Resize(image.Width / 3, 0, false)); image.DebugSave(provider); image.CompareToReferenceOutput(ValidatorComparer, provider); @@ -274,30 +363,17 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } } - [Theory] - [WithTestPatternImages(10, 100, DefaultPixelType)] - public void ResizeHeightCannotKeepAspectKeepsOnePixel(TestImageProvider provider) - where TPixel : struct, IPixel - { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Resize(0, 5)); - Assert.Equal(1, image.Width); - Assert.Equal(5, image.Height); - } - } - [Theory] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] - public void ResizeWithCropWidthMode(TestImageProvider provider) + public void ResizeWithBoxPadMode(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) { var options = new ResizeOptions - { - Size = new Size(image.Width / 2, image.Height) - }; + { + Size = new Size(image.Width + 200, image.Height + 200), Mode = ResizeMode.BoxPad + }; image.Mutate(x => x.Resize(options)); @@ -313,10 +389,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { using (Image image = provider.GetImage()) { - var options = new ResizeOptions - { - Size = new Size(image.Width, image.Height / 2) - }; + var options = new ResizeOptions { Size = new Size(image.Width, image.Height / 2) }; image.Mutate(x => x.Resize(options)); @@ -327,16 +400,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms [Theory] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] - public void ResizeWithPadMode(TestImageProvider provider) + public void ResizeWithCropWidthMode(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) { - var options = new ResizeOptions - { - Size = new Size(image.Width + 200, image.Height), - Mode = ResizeMode.Pad - }; + var options = new ResizeOptions { Size = new Size(image.Width / 2, image.Height) }; image.Mutate(x => x.Resize(options)); @@ -347,16 +416,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms [Theory] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] - public void ResizeWithBoxPadMode(TestImageProvider provider) + public void ResizeWithMaxMode(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) { - var options = new ResizeOptions - { - Size = new Size(image.Width + 200, image.Height + 200), - Mode = ResizeMode.BoxPad - }; + var options = new ResizeOptions { Size = new Size(300, 300), Mode = ResizeMode.Max }; image.Mutate(x => x.Resize(options)); @@ -367,16 +432,18 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms [Theory] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] - public void ResizeWithMaxMode(TestImageProvider provider) + public void ResizeWithMinMode(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) { var options = new ResizeOptions - { - Size = new Size(300, 300), - Mode = ResizeMode.Max - }; + { + Size = new Size( + (int)Math.Round(image.Width * .75F), + (int)Math.Round(image.Height * .95F)), + Mode = ResizeMode.Min + }; image.Mutate(x => x.Resize(options)); @@ -387,16 +454,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms [Theory] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] - public void ResizeWithMinMode(TestImageProvider provider) + public void ResizeWithPadMode(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) { var options = new ResizeOptions - { - Size = new Size((int)Math.Round(image.Width * .75F), (int)Math.Round(image.Height * .95F)), - Mode = ResizeMode.Min - }; + { + Size = new Size(image.Width + 200, image.Height), Mode = ResizeMode.Pad + }; image.Mutate(x => x.Resize(options)); @@ -413,10 +479,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms using (Image image = provider.GetImage()) { var options = new ResizeOptions - { - Size = new Size(image.Width / 2, image.Height), - Mode = ResizeMode.Stretch - }; + { + Size = new Size(image.Width / 2, image.Height), Mode = ResizeMode.Stretch + }; image.Mutate(x => x.Resize(options)); @@ -424,61 +489,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms image.CompareToReferenceOutput(ValidatorComparer, provider); } } - - [Theory] - [InlineData(-2, 0)] - [InlineData(-1, 0)] - [InlineData(0, 1)] - [InlineData(1, 0)] - [InlineData(2, 0)] - public static void BicubicWindowOscillatesCorrectly(float x, float expected) - { - IResampler sampler = KnownResamplers.Bicubic; - float result = sampler.GetValue(x); - - Assert.Equal(result, expected); - } - - [Theory] - [InlineData(-2, 0)] - [InlineData(-1, 0)] - [InlineData(0, 1)] - [InlineData(1, 0)] - [InlineData(2, 0)] - public static void TriangleWindowOscillatesCorrectly(float x, float expected) - { - IResampler sampler = KnownResamplers.Triangle; - float result = sampler.GetValue(x); - - Assert.Equal(result, expected); - } - - [Theory] - [InlineData(-2, 0)] - [InlineData(-1, 0)] - [InlineData(0, 1)] - [InlineData(1, 0)] - [InlineData(2, 0)] - public static void Lanczos3WindowOscillatesCorrectly(float x, float expected) - { - IResampler sampler = KnownResamplers.Lanczos3; - float result = sampler.GetValue(x); - - Assert.Equal(result, expected); - } - - [Theory] - [InlineData(-4, 0)] - [InlineData(-2, 0)] - [InlineData(0, 1)] - [InlineData(2, 0)] - [InlineData(4, 0)] - public static void Lanczos5WindowOscillatesCorrectly(float x, float expected) - { - IResampler sampler = KnownResamplers.Lanczos5; - float result = sampler.GetValue(x); - - Assert.Equal(result, expected); - } } } \ No newline at end of file From 20ddd11e5fe5c6ef09a26e675fc595af42d1fdb7 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 14 Apr 2019 22:13:51 +0200 Subject: [PATCH 10/37] ResizeWindow refactor 4 --- .../Transforms/Resize/ResizeKernel.cs | 29 +++++++++------- .../Transforms/Resize/ResizeProcessor.cs | 17 +++++----- .../Transforms/Resize/ResizeWindow.cs | 33 +++++++++++-------- ...ResizeKernelMapTests.ReferenceKernelMap.cs | 2 +- .../Transforms/ResizeKernelMapTests.cs | 4 +-- 5 files changed, 48 insertions(+), 37 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs index f349634ac..28b3d3a4d 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs @@ -19,27 +19,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// Initializes a new instance of the struct. /// [MethodImpl(InliningOptions.ShortMethod)] - internal ResizeKernel(int left, float* bufferPtr, int length) + internal ResizeKernel(int startIndex, float* bufferPtr, int length) { - this.Left = left; + this.StartIndex = startIndex; this.bufferPtr = bufferPtr; this.Length = length; } /// - /// Gets the left index for the destination row + /// Gets the start index for the destination row. /// - public int Left { get; } + public int StartIndex { get; } /// - /// Gets the the length of the kernel + /// Gets the the length of the kernel. /// public int Length { get; } /// - /// Gets the span representing the portion of the that this window covers + /// Gets the span representing the portion of the that this window covers. /// - /// The + /// The . /// public Span Values { @@ -55,17 +55,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms [MethodImpl(InliningOptions.ShortMethod)] public Vector4 Convolve(Span rowSpan) { - ref float horizontalValues = ref Unsafe.AsRef(this.bufferPtr); - int left = this.Left; - ref Vector4 vecPtr = ref Unsafe.Add(ref MemoryMarshal.GetReference(rowSpan), left); + ref Vector4 vecPtr = ref Unsafe.Add(ref MemoryMarshal.GetReference(rowSpan), this.StartIndex); + + return this.ConvolveCore(ref vecPtr); + } + [MethodImpl(InliningOptions.ShortMethod)] + public Vector4 ConvolveCore(ref Vector4 rowStartRef) + { + ref float horizontalValues = ref Unsafe.AsRef(this.bufferPtr); // Destination color components Vector4 result = Vector4.Zero; for (int i = 0; i < this.Length; i++) { float weight = Unsafe.Add(ref horizontalValues, i); - Vector4 v = Unsafe.Add(ref vecPtr, i); + Vector4 v = Unsafe.Add(ref rowStartRef, i); result += v * weight; } @@ -73,7 +78,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } /// - /// Copy the contents of altering + /// Copy the contents of altering /// to the value . /// internal ResizeKernel AlterLeftValue(int left) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs index 38419b5dd..73f9b4241 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -197,7 +197,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int startY = this.TargetRectangle.Y; int startX = this.TargetRectangle.X; - var workingRect = Rectangle.Intersect( + var destWorkingRect = Rectangle.Intersect( this.TargetRectangle, new Rectangle(0, 0, width, height)); @@ -208,7 +208,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms float heightFactor = sourceRectangle.Height / (float)this.TargetRectangle.Height; ParallelHelper.IterateRows( - workingRect, + destWorkingRect, configuration, rows => { @@ -219,7 +219,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms source.GetPixelRowSpan((int)(((y - startY) * heightFactor) + sourceY)); Span targetRow = destination.GetPixelRowSpan(y); - for (int x = workingRect.Left; x < workingRect.Right; x++) + for (int x = destWorkingRect.Left; x < destWorkingRect.Right; x++) { // X coordinates of source points targetRow[x] = sourceRow[(int)(((x - startX) * widthFactor) + sourceX)]; @@ -248,7 +248,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.horizontalKernelMap, this.verticalKernelMap, width, - workingRect, + destWorkingRect, startX)) using (IMemoryOwner tempBuffer = source.MemoryAllocator.Allocate(width)) { @@ -257,12 +257,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // Now process the rows. Span tempColSpan = tempBuffer.GetSpan(); - for (int y = workingRect.Top; y < workingRect.Bottom; y++) + for (int y = destWorkingRect.Top; y < destWorkingRect.Bottom; y++) { // Ensure offsets are normalized for cropping and padding. ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - startY); - if (kernel.Left + kernel.Length > resizeWindow.Bottom) + if (kernel.StartIndex + kernel.Length > resizeWindow.Bottom) { resizeWindow.Slide(); } @@ -271,10 +271,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int x = 0; x < width; x++) { - Span firstPassColumn = resizeWindow.GetColumnSpan(x); + Span firstPassColumn = resizeWindow.GetColumnSpan(x, kernel.StartIndex); + ref Vector4 rowStartReference = ref MemoryMarshal.GetReference(firstPassColumn); // Destination color components - Unsafe.Add(ref tempRowBase, x) = kernel.Convolve(firstPassColumn); + Unsafe.Add(ref tempRowBase, x) = kernel.ConvolveCore(ref rowStartReference); } Span targetRowSpan = destination.GetPixelRowSpan(y); diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs index 05015dee2..ba5f9a984 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs @@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly ResizeKernelMap verticalKernelMap; - private readonly Rectangle workingRectangle; + private readonly Rectangle destWorkingRect; private readonly int diameter; @@ -45,7 +45,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms ResizeKernelMap horizontalKernelMap, ResizeKernelMap verticalKernelMap, int destWidth, - Rectangle workingRectangle, + Rectangle destWorkingRect, int startX) { this.configuration = configuration; @@ -54,7 +54,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.conversionModifiers = conversionModifiers; this.horizontalKernelMap = horizontalKernelMap; this.verticalKernelMap = verticalKernelMap; - this.workingRectangle = workingRectangle; + this.destWorkingRect = destWorkingRect; this.startX = startX; this.diameter = verticalKernelMap.MaxDiameter; @@ -65,9 +65,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms AllocationOptions.Clean); this.tempRowBuffer = configuration.MemoryAllocator.Allocate(this.sourceRectangle.Width); - this.Top = this.sourceRectangle.Top; + this.Top = 0; - this.Bottom = this.sourceRectangle.Bottom; + this.Bottom = this.sourceRectangle.Height; } public int Bottom { get; private set; } @@ -80,15 +80,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span GetColumnSpan(int x) + public Span GetColumnSpan(int x, int startY) { - return this.buffer.GetRowSpan(x); + return this.buffer.GetRowSpan(x).Slice(startY); } public void Initialize() + { + this.Initialize(0, this.sourceRectangle.Height); + } + + public void Slide() + { + throw new InvalidOperationException("Shouldn't happen yet!"); + } + + private void Initialize(int top, int bottom) { Span tempRowSpan = this.tempRowBuffer.GetSpan(); - for (int y = 0; y < this.sourceRectangle.Height; y++) + for (int y = top; y < bottom; y++) { Span sourceRow = this.source.GetRowSpan(y); @@ -100,17 +110,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms ref Vector4 firstPassBaseRef = ref this.buffer.Span[y]; - for (int x = this.workingRectangle.Left; x < this.workingRectangle.Right; x++) + for (int x = this.destWorkingRect.Left; x < this.destWorkingRect.Right; x++) { ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - this.startX); Unsafe.Add(ref firstPassBaseRef, x * this.sourceRectangle.Height) = kernel.Convolve(tempRowSpan); } } } - - public void Slide() - { - throw new InvalidOperationException("Shouldn't happen yet!"); - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs index 7d842c4e1..2c5914253 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs @@ -104,7 +104,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms public static implicit operator ReferenceKernel(ResizeKernel orig) { - return new ReferenceKernel(orig.Left, orig.Values.ToArray()); + return new ReferenceKernel(orig.StartIndex, orig.Values.ToArray()); } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs index 9de866586..1bd909e7e 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs @@ -151,8 +151,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms referenceKernel.Length == kernel.Length, $"referenceKernel.Length != kernel.Length: {referenceKernel.Length} != {kernel.Length}"); Assert.True( - referenceKernel.Left == kernel.Left, - $"referenceKernel.Left != kernel.Left: {referenceKernel.Left} != {kernel.Left}"); + referenceKernel.Left == kernel.StartIndex, + $"referenceKernel.Left != kernel.Left: {referenceKernel.Left} != {kernel.StartIndex}"); float[] expectedValues = referenceKernel.Values; Span actualValues = kernel.Values; From 86cc83d7eaaccfde7d53002828a4856af4e368a4 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 14 Apr 2019 23:29:43 +0200 Subject: [PATCH 11/37] basic sliding window implementation (semi-stable state) --- .../Transforms/Resize/ResizeKernel.cs | 9 +++---- .../Transforms/Resize/ResizeProcessor.cs | 3 +-- .../Transforms/Resize/ResizeWindow.cs | 27 ++++++++++++------- .../Processors/Transforms/ResizeTests.cs | 1 + 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs index 28b3d3a4d..4606f482c 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs @@ -55,13 +55,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms [MethodImpl(InliningOptions.ShortMethod)] public Vector4 Convolve(Span rowSpan) { - ref Vector4 vecPtr = ref Unsafe.Add(ref MemoryMarshal.GetReference(rowSpan), this.StartIndex); - - return this.ConvolveCore(ref vecPtr); + return this.ConvolveCore(rowSpan.Slice(this.StartIndex)); } [MethodImpl(InliningOptions.ShortMethod)] - public Vector4 ConvolveCore(ref Vector4 rowStartRef) + public Vector4 ConvolveCore(Span offsetedRowSpan) { ref float horizontalValues = ref Unsafe.AsRef(this.bufferPtr); // Destination color components @@ -70,7 +68,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int i = 0; i < this.Length; i++) { float weight = Unsafe.Add(ref horizontalValues, i); - Vector4 v = Unsafe.Add(ref rowStartRef, i); + //Vector4 v = Unsafe.Add(ref rowStartRef, i); + Vector4 v = offsetedRowSpan[i]; result += v * weight; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs index 73f9b4241..abbe5908e 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -272,10 +272,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int x = 0; x < width; x++) { Span firstPassColumn = resizeWindow.GetColumnSpan(x, kernel.StartIndex); - ref Vector4 rowStartReference = ref MemoryMarshal.GetReference(firstPassColumn); // Destination color components - Unsafe.Add(ref tempRowBase, x) = kernel.ConvolveCore(ref rowStartReference); + Unsafe.Add(ref tempRowBase, x) = kernel.ConvolveCore(firstPassColumn); } Span targetRowSpan = destination.GetPixelRowSpan(y); diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs index ba5f9a984..8221a9cc4 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs @@ -38,6 +38,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly int diameter; + private readonly int windowHeight; + public ResizeWindow( Configuration configuration, BufferArea source, @@ -59,15 +61,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.diameter = verticalKernelMap.MaxDiameter; + this.windowHeight = Math.Min(this.sourceRectangle.Height, 2 * this.diameter); + this.buffer = configuration.MemoryAllocator.Allocate2D( - this.sourceRectangle.Height, + this.windowHeight, destWidth, AllocationOptions.Clean); this.tempRowBuffer = configuration.MemoryAllocator.Allocate(this.sourceRectangle.Width); - - this.Top = 0; - - this.Bottom = this.sourceRectangle.Height; } public int Bottom { get; private set; } @@ -82,21 +82,26 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms [MethodImpl(MethodImplOptions.AggressiveInlining)] public Span GetColumnSpan(int x, int startY) { - return this.buffer.GetRowSpan(x).Slice(startY); + return this.buffer.GetRowSpan(x).Slice(startY - this.Top); } public void Initialize() { - this.Initialize(0, this.sourceRectangle.Height); + this.Initialize(0, this.windowHeight); } public void Slide() { - throw new InvalidOperationException("Shouldn't happen yet!"); + int top = this.Top + this.diameter; + int bottom = Math.Min(this.Bottom + this.diameter, this.sourceRectangle.Height); + this.Initialize(top, bottom); } private void Initialize(int top, int bottom) { + this.Top = top; + this.Bottom = bottom; + Span tempRowSpan = this.tempRowBuffer.GetSpan(); for (int y = top; y < bottom; y++) { @@ -108,12 +113,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms tempRowSpan, this.conversionModifiers); - ref Vector4 firstPassBaseRef = ref this.buffer.Span[y]; + //ref Vector4 firstPassBaseRef = ref this.buffer.Span[y - top]; + Span firstPassSpan = this.buffer.Span.Slice(y - top); for (int x = this.destWorkingRect.Left; x < this.destWorkingRect.Right; x++) { ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - this.startX); - Unsafe.Add(ref firstPassBaseRef, x * this.sourceRectangle.Height) = kernel.Convolve(tempRowSpan); + firstPassSpan[x * this.windowHeight] = kernel.Convolve(tempRowSpan); + //Unsafe.Add(ref firstPassBaseRef, x * this.sourceRectangle.Height) = kernel.Convolve(tempRowSpan); } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 8c59fff26..3d6f08a15 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -102,6 +102,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms // [WithBasicTestPatternImages(15, 12, PixelTypes.Rgba32, 2, 3, 1, 2)] means: // resizing: (15, 12) -> (10, 6) // kernel dimensions: (3, 4) + using (Image image = provider.GetImage()) { From 56de415c949d6573170cb611cbaee6fc50e42e75 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 14 Apr 2019 23:51:08 +0200 Subject: [PATCH 12/37] fix ResizeWithCropHeightMode --- .../Processing/Processors/Transforms/Resize/ResizeProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs index abbe5908e..9508d7c00 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -262,7 +262,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // Ensure offsets are normalized for cropping and padding. ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - startY); - if (kernel.StartIndex + kernel.Length > resizeWindow.Bottom) + while (kernel.StartIndex + kernel.Length > resizeWindow.Bottom) { resizeWindow.Slide(); } From 8e607aeaf16a8c7f165a3ad5560715e1e3f8b79e Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 15 Apr 2019 01:24:22 +0200 Subject: [PATCH 13/37] reference output for Resize_BasicSmall --- tests/Images/External | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Images/External b/tests/Images/External index 94b5a8e11..c7333d2a8 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 94b5a8e11b33ba62c15d1d03f1b8b721468764f1 +Subproject commit c7333d2a81a74d1936bd202bcb6b16cbfe6bcdce From 682e5214590424d0e94c33874de1ac7f7e1888dd Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 15 Apr 2019 01:46:21 +0200 Subject: [PATCH 14/37] minor optimization --- .../Processors/Transforms/Resize/ResizeProcessor.cs | 4 +++- .../Processing/Processors/Transforms/Resize/ResizeWindow.cs | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs index 9508d7c00..d61cc5e89 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -269,9 +269,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempColSpan); + int top = kernel.StartIndex - resizeWindow.Top; + for (int x = 0; x < width; x++) { - Span firstPassColumn = resizeWindow.GetColumnSpan(x, kernel.StartIndex); + Span firstPassColumn = resizeWindow.GetColumnSpan(x).Slice(top); // Destination color components Unsafe.Add(ref tempRowBase, x) = kernel.ConvolveCore(firstPassColumn); diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs index 8221a9cc4..afa9dac2d 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs @@ -85,6 +85,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms return this.buffer.GetRowSpan(x).Slice(startY - this.Top); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetColumnSpan(int x) + { + return this.buffer.GetRowSpan(x); + } + public void Initialize() { this.Initialize(0, this.windowHeight); From 4a0839abd0ead5ea0a298e0d4a5b531787f91409 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Thu, 18 Apr 2019 23:43:44 +0200 Subject: [PATCH 15/37] refactor - ResizeWindow -> ResizeWorker - Most logic moved to ResizeWorker --- .../Transforms/Resize/ResizeProcessor.cs | 42 ++--------- .../{ResizeWindow.cs => ResizeWorker.cs} | 72 +++++++++++++++---- 2 files changed, 62 insertions(+), 52 deletions(-) rename src/ImageSharp/Processing/Processors/Transforms/Resize/{ResizeWindow.cs => ResizeWorker.cs} (61%) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs index d61cc5e89..63dd5efca 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -237,11 +237,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms BufferArea sourceArea = source.PixelBuffer.GetArea(sourceRectangle); - // Interpolate the image using the calculated weights. - // A 2-pass 1D algorithm appears to be faster than splitting a 1-pass 2D algorithm - // First process the columns. Since we are not using multiple threads startY and endY - // are the upper and lower bounds of the source rectangle. - using (var resizeWindow = new ResizeWindow( + // If we want to reintroduce processing: + // it's possible to launch multiple workers for different regions of the image + using (var worker = new ResizeWorker( configuration, sourceArea, conversionModifiers, @@ -250,39 +248,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms width, destWorkingRect, startX)) - using (IMemoryOwner tempBuffer = source.MemoryAllocator.Allocate(width)) { - resizeWindow.Initialize(); - - // Now process the rows. - Span tempColSpan = tempBuffer.GetSpan(); - - for (int y = destWorkingRect.Top; y < destWorkingRect.Bottom; y++) - { - // Ensure offsets are normalized for cropping and padding. - ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - startY); - - while (kernel.StartIndex + kernel.Length > resizeWindow.Bottom) - { - resizeWindow.Slide(); - } - - ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempColSpan); - - int top = kernel.StartIndex - resizeWindow.Top; - - for (int x = 0; x < width; x++) - { - Span firstPassColumn = resizeWindow.GetColumnSpan(x).Slice(top); - - // Destination color components - Unsafe.Add(ref tempRowBase, x) = kernel.ConvolveCore(firstPassColumn); - } - - Span targetRowSpan = destination.GetPixelRowSpan(y); - - PixelOperations.Instance.FromVector4Destructive(configuration, tempColSpan, targetRowSpan, conversionModifiers); - } + worker.Initialize(); + worker.FillDestinationPixels(destWorkingRect.Top, destWorkingRect.Bottom, startY, destination.PixelBuffer); } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs similarity index 61% rename from src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index afa9dac2d..11b49b863 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWindow.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -5,6 +5,7 @@ using System; using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -13,7 +14,7 @@ using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Transforms { - internal class ResizeWindow : IDisposable + internal class ResizeWorker : IDisposable where TPixel : struct, IPixel { private readonly Buffer2D buffer; @@ -32,15 +33,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly IMemoryOwner tempRowBuffer; + private readonly IMemoryOwner tempColumnBuffer; + private readonly ResizeKernelMap verticalKernelMap; + private readonly int destWidth; + private readonly Rectangle destWorkingRect; private readonly int diameter; private readonly int windowHeight; - public ResizeWindow( + public ResizeWorker( Configuration configuration, BufferArea source, PixelConversionModifiers conversionModifiers, @@ -56,6 +61,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.conversionModifiers = conversionModifiers; this.horizontalKernelMap = horizontalKernelMap; this.verticalKernelMap = verticalKernelMap; + this.destWidth = destWidth; this.destWorkingRect = destWorkingRect; this.startX = startX; @@ -67,22 +73,29 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.windowHeight, destWidth, AllocationOptions.Clean); + this.tempRowBuffer = configuration.MemoryAllocator.Allocate(this.sourceRectangle.Width); + this.tempColumnBuffer = configuration.MemoryAllocator.Allocate(destWidth); + + this.CurrentMinY = 0; + this.CurrentMaxY = this.windowHeight; } - public int Bottom { get; private set; } + public int CurrentMaxY { get; private set; } - public int Top { get; private set; } + public int CurrentMinY { get; private set; } public void Dispose() { this.buffer.Dispose(); + this.tempRowBuffer.Dispose(); + this.tempColumnBuffer.Dispose(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public Span GetColumnSpan(int x, int startY) { - return this.buffer.GetRowSpan(x).Slice(startY - this.Top); + return this.buffer.GetRowSpan(x).Slice(startY - this.CurrentMinY); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -93,23 +106,52 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms public void Initialize() { - this.Initialize(0, this.windowHeight); + this.CalculateFirstPassValues(0, this.windowHeight); } - public void Slide() + public void FillDestinationPixels(int minY, int maxY, int startY, Buffer2D destination) { - int top = this.Top + this.diameter; - int bottom = Math.Min(this.Bottom + this.diameter, this.sourceRectangle.Height); - this.Initialize(top, bottom); + Span tempColSpan = this.tempColumnBuffer.GetSpan(); + + for (int y = minY; y < maxY; y++) + { + // Ensure offsets are normalized for cropping and padding. + ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - startY); + + while (kernel.StartIndex + kernel.Length > this.CurrentMaxY) + { + this.Slide(); + } + + ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempColSpan); + + int top = kernel.StartIndex - this.CurrentMinY; + + for (int x = 0; x < this.destWidth; x++) + { + Span firstPassColumn = this.GetColumnSpan(x).Slice(top); + + // Destination color components + Unsafe.Add(ref tempRowBase, x) = kernel.ConvolveCore(firstPassColumn); + } + + Span targetRowSpan = destination.GetRowSpan(y); + + PixelOperations.Instance.FromVector4Destructive(configuration, tempColSpan, targetRowSpan, conversionModifiers); + } } - private void Initialize(int top, int bottom) + public void Slide() { - this.Top = top; - this.Bottom = bottom; + this.CurrentMinY = this.CurrentMinY + this.diameter; + this.CurrentMaxY = Math.Min(this.CurrentMaxY + this.diameter, this.sourceRectangle.Height); + this.CalculateFirstPassValues(this.CurrentMinY, this.CurrentMaxY); + } + private void CalculateFirstPassValues(int minY, int maxY) + { Span tempRowSpan = this.tempRowBuffer.GetSpan(); - for (int y = top; y < bottom; y++) + for (int y = minY; y < maxY; y++) { Span sourceRow = this.source.GetRowSpan(y); @@ -120,7 +162,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.conversionModifiers); //ref Vector4 firstPassBaseRef = ref this.buffer.Span[y - top]; - Span firstPassSpan = this.buffer.Span.Slice(y - top); + Span firstPassSpan = this.buffer.Span.Slice(y - minY); for (int x = this.destWorkingRect.Left; x < this.destWorkingRect.Right; x++) { From b3495e8f715017af5d22d24fec845b5961c96947 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Fri, 19 Apr 2019 00:23:06 +0200 Subject: [PATCH 16/37] refactor stuff + implement CalculateResizeWorkerWindowCount() --- src/ImageSharp/Configuration.cs | 13 +- .../Transforms/Resize}/ResizeHelper.cs | 339 ++++++++++-------- tests/ImageSharp.Tests/ConfigurationTests.cs | 17 +- .../Processors/Transforms/ResizeTests.cs | 19 + 4 files changed, 232 insertions(+), 156 deletions(-) rename src/ImageSharp/Processing/{ => Processors/Transforms/Resize}/ResizeHelper.cs (88%) diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index c0064d187..b1767101d 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.Memory; namespace SixLabors.ImageSharp @@ -102,6 +103,15 @@ namespace SixLabors.ImageSharp /// internal IFileSystem FileSystem { get; set; } = new LocalFileSystem(); + /// + /// Gets or sets the working buffer size hint for image processors. + /// The default value is 1MB. + /// + /// + /// Currently only used by . + /// + internal int WorkingBufferSizeHintInBytes { get; set; } = 1 * 1024 * 1024; + /// /// Gets or sets the image operations provider factory. /// @@ -130,7 +140,8 @@ namespace SixLabors.ImageSharp MemoryAllocator = this.MemoryAllocator, ImageOperationsProvider = this.ImageOperationsProvider, ReadOrigin = this.ReadOrigin, - FileSystem = this.FileSystem + FileSystem = this.FileSystem, + WorkingBufferSizeHintInBytes = this.WorkingBufferSizeHintInBytes, }; } diff --git a/src/ImageSharp/Processing/ResizeHelper.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs similarity index 88% rename from src/ImageSharp/Processing/ResizeHelper.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs index 3ae632162..0bc8cfda0 100644 --- a/src/ImageSharp/Processing/ResizeHelper.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs @@ -1,11 +1,13 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; using System.Linq; +using System.Numerics; + using SixLabors.Primitives; -namespace SixLabors.ImageSharp.Processing +namespace SixLabors.ImageSharp.Processing.Processors.Transforms { /// /// Provides methods to help calculate the target rectangle when resizing using the @@ -13,6 +15,16 @@ namespace SixLabors.ImageSharp.Processing /// internal static class ResizeHelper { + public static unsafe int CalculateResizeWorkerWindowCount( + int windowDiameter, + int width, + int sizeLimitHintInBytes) + { + int sizeLimitHint = sizeLimitHintInBytes / sizeof(Vector4); + int sizeOfOneWindow = windowDiameter * width; + return Math.Max(2, sizeLimitHint / sizeOfOneWindow); + } + /// /// Calculates the target location and bounds to perform the resize operation against. /// @@ -21,9 +33,13 @@ namespace SixLabors.ImageSharp.Processing /// The target width /// The target height /// - /// The . + /// The tuple representing the location and the bounds /// - public static (Size, Rectangle) CalculateTargetLocationAndBounds(Size sourceSize, ResizeOptions options, int width, int height) + public static (Size, Rectangle) CalculateTargetLocationAndBounds( + Size sourceSize, + ResizeOptions options, + int width, + int height) { switch (options.Mode) { @@ -44,7 +60,90 @@ namespace SixLabors.ImageSharp.Processing } } - private static (Size, Rectangle) CalculateCropRectangle(Size source, ResizeOptions options, int width, int height) + private static (Size, Rectangle) CalculateBoxPadRectangle( + Size source, + ResizeOptions options, + int width, + int height) + { + if (width <= 0 || height <= 0) + { + return (new Size(source.Width, source.Height), new Rectangle(0, 0, source.Width, source.Height)); + } + + int sourceWidth = source.Width; + int sourceHeight = source.Height; + + // Fractional variants for preserving aspect ratio. + float percentHeight = MathF.Abs(height / (float)sourceHeight); + float percentWidth = MathF.Abs(width / (float)sourceWidth); + + int boxPadHeight = height > 0 ? height : (int)MathF.Round(sourceHeight * percentWidth); + int boxPadWidth = width > 0 ? width : (int)MathF.Round(sourceWidth * percentHeight); + + // Only calculate if upscaling. + if (sourceWidth < boxPadWidth && sourceHeight < boxPadHeight) + { + int destinationX; + int destinationY; + int destinationWidth = sourceWidth; + int destinationHeight = sourceHeight; + width = boxPadWidth; + height = boxPadHeight; + + switch (options.Position) + { + case AnchorPositionMode.Left: + destinationY = (height - sourceHeight) / 2; + destinationX = 0; + break; + case AnchorPositionMode.Right: + destinationY = (height - sourceHeight) / 2; + destinationX = width - sourceWidth; + break; + case AnchorPositionMode.TopRight: + destinationY = 0; + destinationX = width - sourceWidth; + break; + case AnchorPositionMode.Top: + destinationY = 0; + destinationX = (width - sourceWidth) / 2; + break; + case AnchorPositionMode.TopLeft: + destinationY = 0; + destinationX = 0; + break; + case AnchorPositionMode.BottomRight: + destinationY = height - sourceHeight; + destinationX = width - sourceWidth; + break; + case AnchorPositionMode.Bottom: + destinationY = height - sourceHeight; + destinationX = (width - sourceWidth) / 2; + break; + case AnchorPositionMode.BottomLeft: + destinationY = height - sourceHeight; + destinationX = 0; + break; + default: + destinationY = (height - sourceHeight) / 2; + destinationX = (width - sourceWidth) / 2; + break; + } + + return (new Size(width, height), + new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight)); + } + + // Switch to pad mode to downscale and calculate from there. + return CalculatePadRectangle(source, options, width, height); + } + + private static (Size, Rectangle) CalculateCropRectangle( + Size source, + ResizeOptions options, + int width, + int height) { if (width <= 0 || height <= 0) { @@ -147,152 +246,15 @@ namespace SixLabors.ImageSharp.Processing destinationWidth = (int)MathF.Ceiling(sourceWidth * percentHeight); } - return (new Size(width, height), new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight)); - } - - private static (Size, Rectangle) CalculatePadRectangle(Size source, ResizeOptions options, int width, int height) - { - if (width <= 0 || height <= 0) - { - return (new Size(source.Width, source.Height), new Rectangle(0, 0, source.Width, source.Height)); - } - - float ratio; - int sourceWidth = source.Width; - int sourceHeight = source.Height; - - int destinationX = 0; - int destinationY = 0; - int destinationWidth = width; - int destinationHeight = height; - - // Fractional variants for preserving aspect ratio. - float percentHeight = MathF.Abs(height / (float)sourceHeight); - float percentWidth = MathF.Abs(width / (float)sourceWidth); - - if (percentHeight < percentWidth) - { - ratio = percentHeight; - destinationWidth = (int)MathF.Round(sourceWidth * percentHeight); - - switch (options.Position) - { - case AnchorPositionMode.Left: - case AnchorPositionMode.TopLeft: - case AnchorPositionMode.BottomLeft: - destinationX = 0; - break; - case AnchorPositionMode.Right: - case AnchorPositionMode.TopRight: - case AnchorPositionMode.BottomRight: - destinationX = (int)MathF.Round(width - (sourceWidth * ratio)); - break; - default: - destinationX = (int)MathF.Round((width - (sourceWidth * ratio)) / 2F); - break; - } - } - else - { - ratio = percentWidth; - destinationHeight = (int)MathF.Round(sourceHeight * percentWidth); - - switch (options.Position) - { - case AnchorPositionMode.Top: - case AnchorPositionMode.TopLeft: - case AnchorPositionMode.TopRight: - destinationY = 0; - break; - case AnchorPositionMode.Bottom: - case AnchorPositionMode.BottomLeft: - case AnchorPositionMode.BottomRight: - destinationY = (int)MathF.Round(height - (sourceHeight * ratio)); - break; - default: - destinationY = (int)MathF.Round((height - (sourceHeight * ratio)) / 2F); - break; - } - } - - return (new Size(width, height), new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight)); - } - - private static (Size, Rectangle) CalculateBoxPadRectangle(Size source, ResizeOptions options, int width, int height) - { - if (width <= 0 || height <= 0) - { - return (new Size(source.Width, source.Height), new Rectangle(0, 0, source.Width, source.Height)); - } - - int sourceWidth = source.Width; - int sourceHeight = source.Height; - - // Fractional variants for preserving aspect ratio. - float percentHeight = MathF.Abs(height / (float)sourceHeight); - float percentWidth = MathF.Abs(width / (float)sourceWidth); - - int boxPadHeight = height > 0 ? height : (int)MathF.Round(sourceHeight * percentWidth); - int boxPadWidth = width > 0 ? width : (int)MathF.Round(sourceWidth * percentHeight); - - // Only calculate if upscaling. - if (sourceWidth < boxPadWidth && sourceHeight < boxPadHeight) - { - int destinationX; - int destinationY; - int destinationWidth = sourceWidth; - int destinationHeight = sourceHeight; - width = boxPadWidth; - height = boxPadHeight; - - switch (options.Position) - { - case AnchorPositionMode.Left: - destinationY = (height - sourceHeight) / 2; - destinationX = 0; - break; - case AnchorPositionMode.Right: - destinationY = (height - sourceHeight) / 2; - destinationX = width - sourceWidth; - break; - case AnchorPositionMode.TopRight: - destinationY = 0; - destinationX = width - sourceWidth; - break; - case AnchorPositionMode.Top: - destinationY = 0; - destinationX = (width - sourceWidth) / 2; - break; - case AnchorPositionMode.TopLeft: - destinationY = 0; - destinationX = 0; - break; - case AnchorPositionMode.BottomRight: - destinationY = height - sourceHeight; - destinationX = width - sourceWidth; - break; - case AnchorPositionMode.Bottom: - destinationY = height - sourceHeight; - destinationX = (width - sourceWidth) / 2; - break; - case AnchorPositionMode.BottomLeft: - destinationY = height - sourceHeight; - destinationX = 0; - break; - default: - destinationY = (height - sourceHeight) / 2; - destinationX = (width - sourceWidth) / 2; - break; - } - - return (new Size(width, height), new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight)); - } - - // Switch to pad mode to downscale and calculate from there. - return CalculatePadRectangle(source, options, width, height); + return (new Size(width, height), + new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight)); } - private static (Size, Rectangle) CalculateMaxRectangle(Size source, ResizeOptions options, int width, int height) + private static (Size, Rectangle) CalculateMaxRectangle( + Size source, + ResizeOptions options, + int width, + int height) { int destinationWidth = width; int destinationHeight = height; @@ -320,7 +282,11 @@ namespace SixLabors.ImageSharp.Processing return (new Size(width, height), new Rectangle(0, 0, destinationWidth, destinationHeight)); } - private static (Size, Rectangle) CalculateMinRectangle(Size source, ResizeOptions options, int width, int height) + private static (Size, Rectangle) CalculateMinRectangle( + Size source, + ResizeOptions options, + int width, + int height) { int sourceWidth = source.Width; int sourceHeight = source.Height; @@ -372,5 +338,78 @@ namespace SixLabors.ImageSharp.Processing // Replace the size to match the rectangle. return (new Size(width, height), new Rectangle(0, 0, destinationWidth, destinationHeight)); } + + private static (Size, Rectangle) CalculatePadRectangle( + Size source, + ResizeOptions options, + int width, + int height) + { + if (width <= 0 || height <= 0) + { + return (new Size(source.Width, source.Height), new Rectangle(0, 0, source.Width, source.Height)); + } + + float ratio; + int sourceWidth = source.Width; + int sourceHeight = source.Height; + + int destinationX = 0; + int destinationY = 0; + int destinationWidth = width; + int destinationHeight = height; + + // Fractional variants for preserving aspect ratio. + float percentHeight = MathF.Abs(height / (float)sourceHeight); + float percentWidth = MathF.Abs(width / (float)sourceWidth); + + if (percentHeight < percentWidth) + { + ratio = percentHeight; + destinationWidth = (int)MathF.Round(sourceWidth * percentHeight); + + switch (options.Position) + { + case AnchorPositionMode.Left: + case AnchorPositionMode.TopLeft: + case AnchorPositionMode.BottomLeft: + destinationX = 0; + break; + case AnchorPositionMode.Right: + case AnchorPositionMode.TopRight: + case AnchorPositionMode.BottomRight: + destinationX = (int)MathF.Round(width - (sourceWidth * ratio)); + break; + default: + destinationX = (int)MathF.Round((width - (sourceWidth * ratio)) / 2F); + break; + } + } + else + { + ratio = percentWidth; + destinationHeight = (int)MathF.Round(sourceHeight * percentWidth); + + switch (options.Position) + { + case AnchorPositionMode.Top: + case AnchorPositionMode.TopLeft: + case AnchorPositionMode.TopRight: + destinationY = 0; + break; + case AnchorPositionMode.Bottom: + case AnchorPositionMode.BottomLeft: + case AnchorPositionMode.BottomRight: + destinationY = (int)MathF.Round(height - (sourceHeight * ratio)); + break; + default: + destinationY = (int)MathF.Round((height - (sourceHeight * ratio)) / 2F); + break; + } + } + + return (new Size(width, height), + new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight)); + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs index 208387e6d..6f68d0428 100644 --- a/tests/ImageSharp.Tests/ConfigurationTests.cs +++ b/tests/ImageSharp.Tests/ConfigurationTests.cs @@ -39,13 +39,13 @@ namespace SixLabors.ImageSharp.Tests /// Test that the default configuration is not null. /// [Fact] - public void TestDefaultConfigurationIsNotNull() => Assert.True(Configuration.Default != null); + public void TestDefaultConfigurationIsNotNull() => Assert.True(this.DefaultConfiguration != null); /// /// Test that the default configuration read origin options is set to begin. /// [Fact] - public void TestDefaultConfigurationReadOriginIsCurrent() => Assert.True(Configuration.Default.ReadOrigin == ReadOrigin.Current); + public void TestDefaultConfigurationReadOriginIsCurrent() => Assert.True(this.DefaultConfiguration.ReadOrigin == ReadOrigin.Current); /// /// Test that the default configuration parallel options max degrees of parallelism matches the @@ -54,7 +54,7 @@ namespace SixLabors.ImageSharp.Tests [Fact] public void TestDefaultConfigurationMaxDegreeOfParallelism() { - Assert.True(Configuration.Default.MaxDegreeOfParallelism == Environment.ProcessorCount); + Assert.True(this.DefaultConfiguration.MaxDegreeOfParallelism == Environment.ProcessorCount); var cfg = new Configuration(); Assert.True(cfg.MaxDegreeOfParallelism == Environment.ProcessorCount); @@ -93,7 +93,7 @@ namespace SixLabors.ImageSharp.Tests public void ConfigurationCannotAddDuplicates() { const int count = 4; - Configuration config = Configuration.Default; + Configuration config = this.DefaultConfiguration; Assert.Equal(count, config.ImageFormats.Count()); @@ -105,9 +105,16 @@ namespace SixLabors.ImageSharp.Tests [Fact] public void DefaultConfigurationHasCorrectFormatCount() { - Configuration config = Configuration.Default; + Configuration config = Configuration.CreateDefaultInstance(); Assert.Equal(4, config.ImageFormats.Count()); } + + [Fact] + public void WorkingBufferSizeHint_DefaultIsCorrect() + { + Configuration config = this.DefaultConfiguration; + Assert.True(config.WorkingBufferSizeHintInBytes > 1024); + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 3d6f08a15..ec0092f34 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -35,6 +35,25 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.07F); + [Theory] + [InlineData(20, 100, 1, 2)] + [InlineData(20, 100, 20*100*16, 2)] + [InlineData(20, 100, 40*100*16, 2)] + [InlineData(20, 100, 59*100*16, 2)] + [InlineData(20, 100, 60*100*16, 3)] + [InlineData(17, 63, 5*17*63*16, 5)] + [InlineData(17, 63, 5*17*63*16+1, 5)] + [InlineData(17, 63, 6*17*63*16-1, 5)] + public void CalculateResizeWorkerWindowCount( + int windowDiameter, + int width, + int sizeLimitHintInBytes, + int expectedCount) + { + int actualCount = ResizeHelper.CalculateResizeWorkerWindowCount(windowDiameter, width, sizeLimitHintInBytes); + Assert.Equal(expectedCount, actualCount); + } + [Theory] [InlineData(-2, 0)] [InlineData(-1, 0)] From 40aea16633ef084c5424e9c682e67485ea2f7676 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Fri, 19 Apr 2019 15:02:58 +0200 Subject: [PATCH 17/37] utilize CalculateResizeWorkerHeightInWindowBands() which has been renamed from CalculateResizeWorkerWindowCount() --- src/ImageSharp/Configuration.cs | 14 +++++++------- .../Processors/Transforms/Resize/ResizeHelper.cs | 6 +++--- .../Processors/Transforms/Resize/ResizeWorker.cs | 15 ++++++++++----- .../Processors/Transforms/ResizeKernelMapTests.cs | 1 + .../Processors/Transforms/ResizeTests.cs | 7 +++++-- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index b1767101d..7cb014563 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -128,9 +128,9 @@ namespace SixLabors.ImageSharp } /// - /// Creates a shallow copy of the + /// Creates a shallow copy of the . /// - /// A new configuration instance + /// A new configuration instance. public Configuration Clone() { return new Configuration @@ -147,12 +147,12 @@ namespace SixLabors.ImageSharp /// /// Creates the default instance with the following s preregistered: - /// - /// - /// - /// + /// + /// + /// + /// . /// - /// The default configuration of + /// The default configuration of . internal static Configuration CreateDefaultInstance() { return new Configuration( diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs index 0bc8cfda0..595a7e852 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs @@ -15,13 +15,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// internal static class ResizeHelper { - public static unsafe int CalculateResizeWorkerWindowCount( - int windowDiameter, + public static unsafe int CalculateResizeWorkerHeightInWindowBands( + int windowBandDiameter, int width, int sizeLimitHintInBytes) { int sizeLimitHint = sizeLimitHintInBytes / sizeof(Vector4); - int sizeOfOneWindow = windowDiameter * width; + int sizeOfOneWindow = windowBandDiameter * width; return Math.Max(2, sizeLimitHint / sizeOfOneWindow); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index 11b49b863..73dcce58b 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -41,7 +41,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly Rectangle destWorkingRect; - private readonly int diameter; + private readonly int windowBandDiameter; private readonly int windowHeight; @@ -65,9 +65,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.destWorkingRect = destWorkingRect; this.startX = startX; - this.diameter = verticalKernelMap.MaxDiameter; + this.windowBandDiameter = verticalKernelMap.MaxDiameter; - this.windowHeight = Math.Min(this.sourceRectangle.Height, 2 * this.diameter); + int numberOfWindowBands = ResizeHelper.CalculateResizeWorkerHeightInWindowBands( + this.windowBandDiameter, + destWidth, + configuration.WorkingBufferSizeHintInBytes); + + this.windowHeight = Math.Min(this.sourceRectangle.Height, numberOfWindowBands * this.windowBandDiameter); this.buffer = configuration.MemoryAllocator.Allocate2D( this.windowHeight, @@ -143,8 +148,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms public void Slide() { - this.CurrentMinY = this.CurrentMinY + this.diameter; - this.CurrentMaxY = Math.Min(this.CurrentMaxY + this.diameter, this.sourceRectangle.Height); + this.CurrentMinY = this.CurrentMinY + this.windowBandDiameter; + this.CurrentMaxY = Math.Min(this.CurrentMaxY + this.windowBandDiameter, this.sourceRectangle.Height); this.CalculateFirstPassValues(this.CurrentMinY, this.CurrentMaxY); } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs index 1bd909e7e..51680eee0 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs @@ -36,6 +36,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { nameof(KnownResamplers.Bicubic), 40, 50 }, { nameof(KnownResamplers.Bicubic), 500, 200 }, { nameof(KnownResamplers.Bicubic), 200, 500 }, + { nameof(KnownResamplers.Bicubic), 3032, 400 }, { nameof(KnownResamplers.Bicubic), 10, 25 }, diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index ec0092f34..532558166 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -44,13 +44,16 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms [InlineData(17, 63, 5*17*63*16, 5)] [InlineData(17, 63, 5*17*63*16+1, 5)] [InlineData(17, 63, 6*17*63*16-1, 5)] - public void CalculateResizeWorkerWindowCount( + [InlineData(33, 400, 1*1024*1024, 4)] + [InlineData(33, 400, 8*1024*1024, 39)] + [InlineData(50, 300, 1*1024*1024, 4)] + public void CalculateResizeWorkerHeightInWindowBands( int windowDiameter, int width, int sizeLimitHintInBytes, int expectedCount) { - int actualCount = ResizeHelper.CalculateResizeWorkerWindowCount(windowDiameter, width, sizeLimitHintInBytes); + int actualCount = ResizeHelper.CalculateResizeWorkerHeightInWindowBands(windowDiameter, width, sizeLimitHintInBytes); Assert.Equal(expectedCount, actualCount); } From 15b3b2a82d88ce0e44ffcc2b5a5e1a1ce72fe52f Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Fri, 19 Apr 2019 16:23:01 +0200 Subject: [PATCH 18/37] improve benchmark: ArrayCopy -> CopyBuffers --- .../Transforms/Resize/ResizeKernel.cs | 4 +- .../Transforms/Resize/ResizeWorker.cs | 7 +- .../General/ArrayCopy.cs | 103 ---------- .../General/CopyBuffers.cs | 184 ++++++++++++++++++ 4 files changed, 191 insertions(+), 107 deletions(-) delete mode 100644 tests/ImageSharp.Benchmarks/General/ArrayCopy.cs create mode 100644 tests/ImageSharp.Benchmarks/General/CopyBuffers.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs index 4606f482c..71cba9056 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs @@ -62,13 +62,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms public Vector4 ConvolveCore(Span offsetedRowSpan) { ref float horizontalValues = ref Unsafe.AsRef(this.bufferPtr); + // Destination color components Vector4 result = Vector4.Zero; for (int i = 0; i < this.Length; i++) { float weight = Unsafe.Add(ref horizontalValues, i); - //Vector4 v = Unsafe.Add(ref rowStartRef, i); + + // Vector4 v = Unsafe.Add(ref rowStartRef, i); Vector4 v = offsetedRowSpan[i]; result += v * weight; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index 73dcce58b..e9e39ef13 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -142,7 +142,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Span targetRowSpan = destination.GetRowSpan(y); - PixelOperations.Instance.FromVector4Destructive(configuration, tempColSpan, targetRowSpan, conversionModifiers); + PixelOperations.Instance.FromVector4Destructive(this.configuration, tempColSpan, targetRowSpan, this.conversionModifiers); } } @@ -166,14 +166,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms tempRowSpan, this.conversionModifiers); - //ref Vector4 firstPassBaseRef = ref this.buffer.Span[y - top]; + // ref Vector4 firstPassBaseRef = ref this.buffer.Span[y - top]; Span firstPassSpan = this.buffer.Span.Slice(y - minY); for (int x = this.destWorkingRect.Left; x < this.destWorkingRect.Right; x++) { ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - this.startX); firstPassSpan[x * this.windowHeight] = kernel.Convolve(tempRowSpan); - //Unsafe.Add(ref firstPassBaseRef, x * this.sourceRectangle.Height) = kernel.Convolve(tempRowSpan); + + // Unsafe.Add(ref firstPassBaseRef, x * this.sourceRectangle.Height) = kernel.Convolve(tempRowSpan); } } } diff --git a/tests/ImageSharp.Benchmarks/General/ArrayCopy.cs b/tests/ImageSharp.Benchmarks/General/ArrayCopy.cs deleted file mode 100644 index ac6b3f93c..000000000 --- a/tests/ImageSharp.Benchmarks/General/ArrayCopy.cs +++ /dev/null @@ -1,103 +0,0 @@ -// -// Copyright (c) James Jackson-South and contributors. -// Licensed under the Apache License, Version 2.0. -// -namespace SixLabors.ImageSharp.Benchmarks.General -{ - using System; - using System.Runtime.CompilerServices; - using System.Runtime.InteropServices; - - using BenchmarkDotNet.Attributes; - - [Config(typeof(Config.ShortClr))] - public class ArrayCopy - { - [Params(10, 100, 1000, 10000)] - public int Count { get; set; } - - byte[] source; - - byte[] destination; - - [GlobalSetup] - public void SetUp() - { - this.source = new byte[this.Count]; - this.destination = new byte[this.Count]; - } - - [Benchmark(Baseline = true, Description = "Copy using Array.Copy()")] - public void CopyArray() - { - Array.Copy(this.source, this.destination, this.Count); - } - - [Benchmark(Description = "Copy using Unsafe")] - public unsafe void CopyUnsafe() - { - fixed (byte* pinnedDestination = this.destination) - fixed (byte* pinnedSource = this.source) - { - Unsafe.CopyBlock(pinnedSource, pinnedDestination, (uint)this.Count); - } - } - - [Benchmark(Description = "Copy using Buffer.BlockCopy()")] - public void CopyUsingBufferBlockCopy() - { - Buffer.BlockCopy(this.source, 0, this.destination, 0, this.Count); - } - - [Benchmark(Description = "Copy using Buffer.MemoryCopy")] - public unsafe void CopyUsingBufferMemoryCopy() - { - fixed (byte* pinnedDestination = this.destination) - fixed (byte* pinnedSource = this.source) - { - Buffer.MemoryCopy(pinnedSource, pinnedDestination, this.Count, this.Count); - } - } - - [Benchmark(Description = "Copy using Marshal.Copy")] - public unsafe void CopyUsingMarshalCopy() - { - fixed (byte* pinnedDestination = this.destination) - { - Marshal.Copy(this.source, 0, (IntPtr)pinnedDestination, this.Count); - } - } - - /***************************************************************************************************************** - *************** RESULTS on i7-4810MQ 2.80GHz + Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1085.0 ******************** - ***************************************************************************************************************** - * - * Method | Count | Mean | StdErr | StdDev | Scaled | Scaled-StdDev | - * ---------------------------------- |------ |------------ |----------- |----------- |------- |-------------- | - * 'Copy using Array.Copy()' | 10 | 20.3074 ns | 0.1194 ns | 0.2068 ns | 1.00 | 0.00 | - * 'Copy using Unsafe' | 10 | 6.1002 ns | 0.1981 ns | 0.3432 ns | 0.30 | 0.01 | - * 'Copy using Buffer.BlockCopy()' | 10 | 10.7879 ns | 0.0984 ns | 0.1705 ns | 0.53 | 0.01 | - * 'Copy using Buffer.MemoryCopy' | 10 | 4.9625 ns | 0.0200 ns | 0.0347 ns | 0.24 | 0.00 | - * 'Copy using Marshal.Copy' | 10 | 16.1782 ns | 0.0919 ns | 0.1592 ns | 0.80 | 0.01 | - * - * 'Copy using Array.Copy()' | 100 | 31.5945 ns | 0.2908 ns | 0.5037 ns | 1.00 | 0.00 | - * 'Copy using Unsafe' | 100 | 10.2722 ns | 0.5202 ns | 0.9010 ns | 0.33 | 0.02 | - * 'Copy using Buffer.BlockCopy()' | 100 | 22.0322 ns | 0.0284 ns | 0.0493 ns | 0.70 | 0.01 | - * 'Copy using Buffer.MemoryCopy' | 100 | 10.2472 ns | 0.0359 ns | 0.0622 ns | 0.32 | 0.00 | - * 'Copy using Marshal.Copy' | 100 | 34.3820 ns | 1.1868 ns | 2.0555 ns | 1.09 | 0.05 | - * - * 'Copy using Array.Copy()' | 1000 | 40.9743 ns | 0.0521 ns | 0.0902 ns | 1.00 | 0.00 | - * 'Copy using Unsafe' | 1000 | 42.7840 ns | 2.0139 ns | 3.4882 ns | 1.04 | 0.07 | - * 'Copy using Buffer.BlockCopy()' | 1000 | 33.7361 ns | 0.0751 ns | 0.1300 ns | 0.82 | 0.00 | - * 'Copy using Buffer.MemoryCopy' | 1000 | 35.7541 ns | 0.0480 ns | 0.0832 ns | 0.87 | 0.00 | - * 'Copy using Marshal.Copy' | 1000 | 42.2028 ns | 0.2769 ns | 0.4795 ns | 1.03 | 0.01 | - * - * 'Copy using Array.Copy()' | 10000 | 200.0438 ns | 0.2251 ns | 0.3899 ns | 1.00 | 0.00 | - * 'Copy using Unsafe' | 10000 | 389.6957 ns | 13.2770 ns | 22.9964 ns | 1.95 | 0.09 | - * 'Copy using Buffer.BlockCopy()' | 10000 | 191.3478 ns | 0.1557 ns | 0.2697 ns | 0.96 | 0.00 | - * 'Copy using Buffer.MemoryCopy' | 10000 | 196.4679 ns | 0.2731 ns | 0.4730 ns | 0.98 | 0.00 | - * 'Copy using Marshal.Copy' | 10000 | 202.5392 ns | 0.5561 ns | 0.9631 ns | 1.01 | 0.00 | - * - */ - } -} diff --git a/tests/ImageSharp.Benchmarks/General/CopyBuffers.cs b/tests/ImageSharp.Benchmarks/General/CopyBuffers.cs new file mode 100644 index 000000000..117cdba41 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/General/CopyBuffers.cs @@ -0,0 +1,184 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using BenchmarkDotNet.Attributes; + +namespace SixLabors.ImageSharp.Benchmarks.General +{ + /// + /// Compare different methods for copying native and/or managed buffers. + /// Conclusions: + /// - Span.CopyTo() has terrible performance on classic .NET Framework + /// - Buffer.MemoryCopy() performance is good enough for all sizes (but needs pinning) + /// + [Config(typeof(Config.ShortClr))] + public class CopyBuffers + { + private byte[] destArray; + + private MemoryHandle destHandle; + + private Memory destMemory; + + private byte[] sourceArray; + + private MemoryHandle sourceHandle; + + private Memory sourceMemory; + + [Params(10, 50, 100, 1000, 10000)] + public int Count { get; set; } + + + [GlobalSetup] + public void Setup() + { + this.sourceArray = new byte[this.Count]; + this.sourceMemory = new Memory(this.sourceArray); + this.sourceHandle = this.sourceMemory.Pin(); + + this.destArray = new byte[this.Count]; + this.destMemory = new Memory(this.destArray); + this.destHandle = this.destMemory.Pin(); + } + + [GlobalCleanup] + public void Cleanup() + { + this.sourceHandle.Dispose(); + this.destHandle.Dispose(); + } + + [Benchmark(Baseline = true, Description = "Array.Copy()")] + public void ArrayCopy() + { + Array.Copy(this.sourceArray, this.destArray, this.Count); + } + + [Benchmark(Description = "Buffer.BlockCopy()")] + public void BufferBlockCopy() + { + Buffer.BlockCopy(this.sourceArray, 0, this.destArray, 0, this.Count); + } + + [Benchmark(Description = "Buffer.MemoryCopy()")] + public unsafe void BufferMemoryCopy() + { + void* pinnedDestination = this.destHandle.Pointer; + void* pinnedSource = this.sourceHandle.Pointer; + Buffer.MemoryCopy(pinnedSource, pinnedDestination, this.Count, this.Count); + } + + + [Benchmark(Description = "Marshal.Copy()")] + public unsafe void MarshalCopy() + { + void* pinnedDestination = this.destHandle.Pointer; + Marshal.Copy(this.sourceArray, 0, (IntPtr)pinnedDestination, this.Count); + } + + [Benchmark(Description = "Span.CopyTo()")] + public void SpanCopyTo() + { + this.sourceMemory.Span.CopyTo(this.destMemory.Span); + } + + [Benchmark(Description = "Unsafe.CopyBlock()")] + public unsafe void UnsafeCopyBlock() + { + void* pinnedDestination = this.destHandle.Pointer; + void* pinnedSource = this.sourceHandle.Pointer; + Unsafe.CopyBlock(pinnedSource, pinnedDestination, (uint)this.Count); + } + + // BenchmarkDotNet=v0.11.3, OS=Windows 10.0.17134.706 (1803/April2018Update/Redstone4) + // Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores + // Frequency=2742189 Hz, Resolution=364.6722 ns, Timer=TSC + // .NET Core SDK=2.2.202 + // [Host] : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT + // Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3394.0 + // Core : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT + // + // IterationCount=3 LaunchCount=1 WarmupCount=3 + // + // Method | Job | Runtime | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + // -------------------- |----- |-------- |------ |-----------:|-----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + // Array.Copy() | Clr | Clr | 10 | 23.579 ns | 1.6836 ns | 0.0923 ns | 1.00 | 0.00 | - | - | - | - | + // Buffer.BlockCopy() | Clr | Clr | 10 | 11.796 ns | 0.5280 ns | 0.0289 ns | 0.50 | 0.00 | - | - | - | - | + // Buffer.MemoryCopy() | Clr | Clr | 10 | 3.206 ns | 8.1741 ns | 0.4480 ns | 0.14 | 0.02 | - | - | - | - | + // Marshal.Copy() | Clr | Clr | 10 | 15.577 ns | 2.0937 ns | 0.1148 ns | 0.66 | 0.00 | - | - | - | - | + // Span.CopyTo() | Clr | Clr | 10 | 32.287 ns | 2.4107 ns | 0.1321 ns | 1.37 | 0.01 | - | - | - | - | + // Unsafe.CopyBlock() | Clr | Clr | 10 | 3.266 ns | 0.3848 ns | 0.0211 ns | 0.14 | 0.00 | - | - | - | - | + // | | | | | | | | | | | | | + // Array.Copy() | Core | Core | 10 | 19.713 ns | 7.3026 ns | 0.4003 ns | 1.00 | 0.00 | - | - | - | - | + // Buffer.BlockCopy() | Core | Core | 10 | 7.332 ns | 0.5465 ns | 0.0300 ns | 0.37 | 0.01 | - | - | - | - | + // Buffer.MemoryCopy() | Core | Core | 10 | 2.476 ns | 0.3476 ns | 0.0191 ns | 0.13 | 0.00 | - | - | - | - | + // Marshal.Copy() | Core | Core | 10 | 15.575 ns | 0.1335 ns | 0.0073 ns | 0.79 | 0.02 | - | - | - | - | + // Span.CopyTo() | Core | Core | 10 | 25.321 ns | 2.3556 ns | 0.1291 ns | 1.28 | 0.02 | - | - | - | - | + // Unsafe.CopyBlock() | Core | Core | 10 | 2.204 ns | 0.1836 ns | 0.0101 ns | 0.11 | 0.00 | - | - | - | - | + // | | | | | | | | | | | | | + // Array.Copy() | Clr | Clr | 50 | 35.217 ns | 2.7642 ns | 0.1515 ns | 1.00 | 0.00 | - | - | - | - | + // Buffer.BlockCopy() | Clr | Clr | 50 | 24.224 ns | 0.8737 ns | 0.0479 ns | 0.69 | 0.00 | - | - | - | - | + // Buffer.MemoryCopy() | Clr | Clr | 50 | 3.827 ns | 4.8733 ns | 0.2671 ns | 0.11 | 0.01 | - | - | - | - | + // Marshal.Copy() | Clr | Clr | 50 | 28.103 ns | 1.3570 ns | 0.0744 ns | 0.80 | 0.00 | - | - | - | - | + // Span.CopyTo() | Clr | Clr | 50 | 34.137 ns | 2.9274 ns | 0.1605 ns | 0.97 | 0.01 | - | - | - | - | + // Unsafe.CopyBlock() | Clr | Clr | 50 | 4.999 ns | 0.1778 ns | 0.0097 ns | 0.14 | 0.00 | - | - | - | - | + // | | | | | | | | | | | | | + // Array.Copy() | Core | Core | 50 | 20.925 ns | 1.0219 ns | 0.0560 ns | 1.00 | 0.00 | - | - | - | - | + // Buffer.BlockCopy() | Core | Core | 50 | 8.083 ns | 0.2158 ns | 0.0118 ns | 0.39 | 0.00 | - | - | - | - | + // Buffer.MemoryCopy() | Core | Core | 50 | 2.919 ns | 0.2878 ns | 0.0158 ns | 0.14 | 0.00 | - | - | - | - | + // Marshal.Copy() | Core | Core | 50 | 16.663 ns | 0.2505 ns | 0.0137 ns | 0.80 | 0.00 | - | - | - | - | + // Span.CopyTo() | Core | Core | 50 | 26.940 ns | 11.5855 ns | 0.6350 ns | 1.29 | 0.03 | - | - | - | - | + // Unsafe.CopyBlock() | Core | Core | 50 | 1.940 ns | 0.6327 ns | 0.0347 ns | 0.09 | 0.00 | - | - | - | - | + // | | | | | | | | | | | | | + // Array.Copy() | Clr | Clr | 100 | 39.284 ns | 0.5647 ns | 0.0310 ns | 1.00 | 0.00 | - | - | - | - | + // Buffer.BlockCopy() | Clr | Clr | 100 | 28.930 ns | 0.6774 ns | 0.0371 ns | 0.74 | 0.00 | - | - | - | - | + // Buffer.MemoryCopy() | Clr | Clr | 100 | 5.859 ns | 2.7931 ns | 0.1531 ns | 0.15 | 0.00 | - | - | - | - | + // Marshal.Copy() | Clr | Clr | 100 | 36.529 ns | 0.9886 ns | 0.0542 ns | 0.93 | 0.00 | - | - | - | - | + // Span.CopyTo() | Clr | Clr | 100 | 36.152 ns | 1.5109 ns | 0.0828 ns | 0.92 | 0.00 | - | - | - | - | + // Unsafe.CopyBlock() | Clr | Clr | 100 | 9.317 ns | 0.4342 ns | 0.0238 ns | 0.24 | 0.00 | - | - | - | - | + // | | | | | | | | | | | | | + // Array.Copy() | Core | Core | 100 | 22.899 ns | 8.4066 ns | 0.4608 ns | 1.00 | 0.00 | - | - | - | - | + // Buffer.BlockCopy() | Core | Core | 100 | 10.696 ns | 0.8106 ns | 0.0444 ns | 0.47 | 0.01 | - | - | - | - | + // Buffer.MemoryCopy() | Core | Core | 100 | 4.102 ns | 0.9040 ns | 0.0496 ns | 0.18 | 0.01 | - | - | - | - | + // Marshal.Copy() | Core | Core | 100 | 17.917 ns | 2.6490 ns | 0.1452 ns | 0.78 | 0.01 | - | - | - | - | + // Span.CopyTo() | Core | Core | 100 | 28.247 ns | 0.6375 ns | 0.0349 ns | 1.23 | 0.03 | - | - | - | - | + // Unsafe.CopyBlock() | Core | Core | 100 | 3.611 ns | 0.4792 ns | 0.0263 ns | 0.16 | 0.00 | - | - | - | - | + // | | | | | | | | | | | | | + // Array.Copy() | Clr | Clr | 1000 | 48.907 ns | 4.4228 ns | 0.2424 ns | 1.00 | 0.00 | - | - | - | - | + // Buffer.BlockCopy() | Clr | Clr | 1000 | 40.653 ns | 1.4055 ns | 0.0770 ns | 0.83 | 0.01 | - | - | - | - | + // Buffer.MemoryCopy() | Clr | Clr | 1000 | 24.720 ns | 1.2651 ns | 0.0693 ns | 0.51 | 0.00 | - | - | - | - | + // Marshal.Copy() | Clr | Clr | 1000 | 42.336 ns | 2.2466 ns | 0.1231 ns | 0.87 | 0.00 | - | - | - | - | + // Span.CopyTo() | Clr | Clr | 1000 | 70.735 ns | 2.6215 ns | 0.1437 ns | 1.45 | 0.01 | - | - | - | - | + // Unsafe.CopyBlock() | Clr | Clr | 1000 | 44.520 ns | 0.9641 ns | 0.0528 ns | 0.91 | 0.00 | - | - | - | - | + // | | | | | | | | | | | | | + // Array.Copy() | Core | Core | 1000 | 46.286 ns | 11.6373 ns | 0.6379 ns | 1.00 | 0.00 | - | - | - | - | + // Buffer.BlockCopy() | Core | Core | 1000 | 34.243 ns | 7.2264 ns | 0.3961 ns | 0.74 | 0.01 | - | - | - | - | + // Buffer.MemoryCopy() | Core | Core | 1000 | 23.135 ns | 0.3153 ns | 0.0173 ns | 0.50 | 0.01 | - | - | - | - | + // Marshal.Copy() | Core | Core | 1000 | 46.219 ns | 1.2869 ns | 0.0705 ns | 1.00 | 0.01 | - | - | - | - | + // Span.CopyTo() | Core | Core | 1000 | 45.371 ns | 3.3581 ns | 0.1841 ns | 0.98 | 0.02 | - | - | - | - | + // Unsafe.CopyBlock() | Core | Core | 1000 | 29.347 ns | 1.1349 ns | 0.0622 ns | 0.63 | 0.01 | - | - | - | - | + // | | | | | | | | | | | | | + // Array.Copy() | Clr | Clr | 10000 | 218.445 ns | 9.2567 ns | 0.5074 ns | 1.00 | 0.00 | - | - | - | - | + // Buffer.BlockCopy() | Clr | Clr | 10000 | 209.610 ns | 6.7447 ns | 0.3697 ns | 0.96 | 0.00 | - | - | - | - | + // Buffer.MemoryCopy() | Clr | Clr | 10000 | 213.061 ns | 66.6490 ns | 3.6533 ns | 0.98 | 0.02 | - | - | - | - | + // Marshal.Copy() | Clr | Clr | 10000 | 214.426 ns | 27.7722 ns | 1.5223 ns | 0.98 | 0.00 | - | - | - | - | + // Span.CopyTo() | Clr | Clr | 10000 | 486.728 ns | 12.1537 ns | 0.6662 ns | 2.23 | 0.00 | - | - | - | - | + // Unsafe.CopyBlock() | Clr | Clr | 10000 | 452.973 ns | 25.1490 ns | 1.3785 ns | 2.07 | 0.00 | - | - | - | - | + // | | | | | | | | | | | | | + // Array.Copy() | Core | Core | 10000 | 203.365 ns | 3.2200 ns | 0.1765 ns | 1.00 | 0.00 | - | - | - | - | + // Buffer.BlockCopy() | Core | Core | 10000 | 193.319 ns | 8.3370 ns | 0.4570 ns | 0.95 | 0.00 | - | - | - | - | + // Buffer.MemoryCopy() | Core | Core | 10000 | 196.541 ns | 37.8056 ns | 2.0723 ns | 0.97 | 0.01 | - | - | - | - | + // Marshal.Copy() | Core | Core | 10000 | 206.454 ns | 3.7652 ns | 0.2064 ns | 1.02 | 0.00 | - | - | - | - | + // Span.CopyTo() | Core | Core | 10000 | 214.799 ns | 3.0667 ns | 0.1681 ns | 1.06 | 0.00 | - | - | - | - | + // Unsafe.CopyBlock() | Core | Core | 10000 | 134.428 ns | 2.6024 ns | 0.1426 ns | 0.66 | 0.00 | - | - | - | - | + + } +} \ No newline at end of file From 6db07016952c25359759a20b06dc08fb8144637d Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Fri, 19 Apr 2019 16:47:01 +0200 Subject: [PATCH 19/37] moar RowInterval stuff --- src/ImageSharp/Memory/RowInterval.cs | 37 +++++++++++++- .../Transforms/Resize/ResizeWorker.cs | 43 ++++++++-------- .../Helpers/RowIntervalTests.cs | 49 +++++++++++++++++++ 3 files changed, 106 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Memory/RowInterval.cs b/src/ImageSharp/Memory/RowInterval.cs index 835e880e9..7e144e37c 100644 --- a/src/ImageSharp/Memory/RowInterval.cs +++ b/src/ImageSharp/Memory/RowInterval.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; + using SixLabors.Primitives; namespace SixLabors.ImageSharp.Memory @@ -8,7 +10,7 @@ namespace SixLabors.ImageSharp.Memory /// /// Represents an interval of rows in a and/or /// - internal readonly struct RowInterval + internal readonly struct RowInterval : IEquatable { /// /// Initializes a new instance of the struct. @@ -36,7 +38,40 @@ namespace SixLabors.ImageSharp.Memory /// public int Height => this.Max - this.Min; + + public static bool operator ==(RowInterval left, RowInterval right) + { + return left.Equals(right); + } + + public static bool operator !=(RowInterval left, RowInterval right) + { + return !left.Equals(right); + } + /// public override string ToString() => $"RowInterval [{this.Min}->{this.Max}]"; + + public RowInterval Slice(int start) => new RowInterval(this.Min + start, this.Max); + + public RowInterval Slice(int start, int length) => new RowInterval(this.Min + start, this.Min + start + length); + + public bool Equals(RowInterval other) + { + return this.Min == other.Min && this.Max == other.Max; + } + + public override bool Equals(object obj) + { + return !ReferenceEquals(null, obj) && obj is RowInterval other && this.Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return (this.Min * 397) ^ this.Max; + } + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index e9e39ef13..66a17e2d7 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -45,6 +45,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly int windowHeight; + private RowInterval currentWindow; + public ResizeWorker( Configuration configuration, BufferArea source, @@ -82,14 +84,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.tempRowBuffer = configuration.MemoryAllocator.Allocate(this.sourceRectangle.Width); this.tempColumnBuffer = configuration.MemoryAllocator.Allocate(destWidth); - this.CurrentMinY = 0; - this.CurrentMaxY = this.windowHeight; + this.currentWindow = new RowInterval(0, this.windowHeight); } - public int CurrentMaxY { get; private set; } - - public int CurrentMinY { get; private set; } - public void Dispose() { this.buffer.Dispose(); @@ -100,18 +97,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms [MethodImpl(MethodImplOptions.AggressiveInlining)] public Span GetColumnSpan(int x, int startY) { - return this.buffer.GetRowSpan(x).Slice(startY - this.CurrentMinY); + return this.buffer.GetRowSpan(x).Slice(startY - this.currentWindow.Min); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span GetColumnSpan(int x) - { - return this.buffer.GetRowSpan(x); - } public void Initialize() { - this.CalculateFirstPassValues(0, this.windowHeight); + this.CalculateFirstPassValues(this.currentWindow); } public void FillDestinationPixels(int minY, int maxY, int startY, Buffer2D destination) @@ -123,14 +115,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // Ensure offsets are normalized for cropping and padding. ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - startY); - while (kernel.StartIndex + kernel.Length > this.CurrentMaxY) + while (kernel.StartIndex + kernel.Length > this.currentWindow.Max) { this.Slide(); } ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempColSpan); - int top = kernel.StartIndex - this.CurrentMinY; + int top = kernel.StartIndex - this.currentWindow.Min; for (int x = 0; x < this.destWidth; x++) { @@ -146,17 +138,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } } - public void Slide() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Span GetColumnSpan(int x) + { + return this.buffer.GetRowSpan(x); + } + + private void Slide() { - this.CurrentMinY = this.CurrentMinY + this.windowBandDiameter; - this.CurrentMaxY = Math.Min(this.CurrentMaxY + this.windowBandDiameter, this.sourceRectangle.Height); - this.CalculateFirstPassValues(this.CurrentMinY, this.CurrentMaxY); + int minY = this.currentWindow.Min + this.windowBandDiameter; + int maxY = Math.Min(this.currentWindow.Max + this.windowBandDiameter, this.sourceRectangle.Height); + this.currentWindow = new RowInterval(minY, maxY); + this.CalculateFirstPassValues(this.currentWindow); } - private void CalculateFirstPassValues(int minY, int maxY) + private void CalculateFirstPassValues(RowInterval window) { Span tempRowSpan = this.tempRowBuffer.GetSpan(); - for (int y = minY; y < maxY; y++) + for (int y = window.Min; y < window.Max; y++) { Span sourceRow = this.source.GetRowSpan(y); @@ -167,7 +166,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.conversionModifiers); // ref Vector4 firstPassBaseRef = ref this.buffer.Span[y - top]; - Span firstPassSpan = this.buffer.Span.Slice(y - minY); + Span firstPassSpan = this.buffer.Span.Slice(y - window.Min); for (int x = this.destWorkingRect.Left; x < this.destWorkingRect.Right; x++) { diff --git a/tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs b/tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs index 629b3cdeb..3aead6aaa 100644 --- a/tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs +++ b/tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs @@ -34,5 +34,54 @@ namespace SixLabors.ImageSharp.Tests.Helpers Assert.True(Unsafe.AreSame(ref expected0, ref actual0)); } } + + [Fact] + public void Slice1() + { + RowInterval rowInterval = new RowInterval(10, 20); + RowInterval sliced = rowInterval.Slice(5); + + Assert.Equal(15, sliced.Min); + Assert.Equal(20, sliced.Max); + } + + [Fact] + public void Slice2() + { + RowInterval rowInterval = new RowInterval(10, 20); + RowInterval sliced = rowInterval.Slice(3, 5); + + Assert.Equal(13, sliced.Min); + Assert.Equal(18, sliced.Max); + } + + [Fact] + public void Equality_WhenTrue() + { + RowInterval a = new RowInterval(42, 123); + RowInterval b = new RowInterval(42, 123); + + Assert.True(a.Equals(b)); + Assert.True(a.Equals((object)b)); + Assert.True(a == b); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void Equality_WhenFalse() + { + RowInterval a = new RowInterval(42, 123); + RowInterval b = new RowInterval(42, 125); + RowInterval c = new RowInterval(40, 123); + + Assert.False(a.Equals(b)); + Assert.False(c.Equals(a)); + Assert.False(b.Equals(c)); + + Assert.False(a.Equals((object)b)); + Assert.False(a.Equals(null)); + Assert.False(a == b); + Assert.True(a != c); + } } } From 19766df11ad23998660f8e8ba6732af187703b62 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Fri, 19 Apr 2019 17:27:01 +0200 Subject: [PATCH 20/37] buffer.CopyColumns(...) --- src/ImageSharp/Memory/Buffer2DExtensions.cs | 144 +++++++++++------- .../ImageSharp.Tests/Memory/Buffer2DTests.cs | 28 ++++ .../TestUtilities/TestDataGenerator.cs | 13 +- 3 files changed, 130 insertions(+), 55 deletions(-) diff --git a/src/ImageSharp/Memory/Buffer2DExtensions.cs b/src/ImageSharp/Memory/Buffer2DExtensions.cs index 17ab6e252..c8bbf01c7 100644 --- a/src/ImageSharp/Memory/Buffer2DExtensions.cs +++ b/src/ImageSharp/Memory/Buffer2DExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Runtime.CompilerServices; using SixLabors.Primitives; @@ -14,41 +15,90 @@ namespace SixLabors.ImageSharp.Memory internal static class Buffer2DExtensions { /// - /// Gets a to the backing buffer of . + /// Copy columns of inplace, + /// from positions starting at to positions at . /// - internal static Span GetSpan(this Buffer2D buffer) + public static unsafe void CopyColumns( + this Buffer2D buffer, + int sourceIndex, + int destIndex, + int columnCount) where T : struct { - return buffer.MemorySource.GetSpan(); + DebugGuard.NotNull(buffer, nameof(buffer)); + DebugGuard.MustBeGreaterThanOrEqualTo(sourceIndex, 0, nameof(sourceIndex)); + DebugGuard.MustBeGreaterThanOrEqualTo(destIndex, sourceIndex + columnCount, nameof(destIndex)); + DebugGuard.MustBeLessThanOrEqualTo(destIndex, buffer.Width - columnCount, nameof(destIndex)); + + int elementSize = Unsafe.SizeOf(); + int width = buffer.Width * elementSize; + int sOffset = sourceIndex * elementSize; + int dOffset = destIndex * elementSize; + long count = columnCount * elementSize; + + using (MemoryHandle handle = buffer.Memory.Pin()) + { + byte* basePtr = (byte*)handle.Pointer; + for (int y = 0; y < buffer.Height; y++) + { + byte* sPtr = basePtr + sOffset; + byte* dPtr = basePtr + dOffset; + + Buffer.MemoryCopy(sPtr, dPtr, count, count); + + basePtr += width; + } + } } /// - /// Gets a to the row 'y' beginning from the pixel at 'x'. + /// Returns a representing the full area of the buffer. /// - /// The buffer - /// The x coordinate (position in the row) - /// The y (row) coordinate /// The element type - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Span GetRowSpan(this Buffer2D buffer, int x, int y) + /// The + /// The + public static Rectangle FullRectangle(this Buffer2D buffer) where T : struct { - return buffer.GetSpan().Slice((y * buffer.Width) + x, buffer.Width - x); + return new Rectangle(0, 0, buffer.Width, buffer.Height); } /// - /// Gets a to the row 'y' beginning from the pixel at the first pixel on that row. + /// Return a to the subarea represented by 'rectangle' /// - /// The buffer - /// The y (row) coordinate /// The element type - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Span GetRowSpan(this Buffer2D buffer, int y) + /// The + /// The rectangle subarea + /// The + public static BufferArea GetArea(this Buffer2D buffer, in Rectangle rectangle) + where T : struct => + new BufferArea(buffer, rectangle); + + public static BufferArea GetArea(this Buffer2D buffer, int x, int y, int width, int height) + where T : struct => + new BufferArea(buffer, new Rectangle(x, y, width, height)); + + /// + /// Return a to the whole area of 'buffer' + /// + /// The element type + /// The + /// The + public static BufferArea GetArea(this Buffer2D buffer) + where T : struct => + new BufferArea(buffer); + + public static BufferArea GetAreaBetweenRows(this Buffer2D buffer, int minY, int maxY) + where T : struct => + new BufferArea(buffer, new Rectangle(0, minY, buffer.Width, maxY - minY)); + + /// + /// Gets a span for all the pixels in defined by + /// + public static Span GetMultiRowSpan(this Buffer2D buffer, in RowInterval rows) where T : struct { - return buffer.GetSpan().Slice(y * buffer.Width, buffer.Width); + return buffer.Span.Slice(rows.Min * buffer.Width, rows.Height * buffer.Width); } /// @@ -66,61 +116,53 @@ namespace SixLabors.ImageSharp.Memory } /// - /// Returns the size of the buffer. + /// Gets a to the row 'y' beginning from the pixel at 'x'. /// + /// The buffer + /// The x coordinate (position in the row) + /// The y (row) coordinate /// The element type - /// The - /// The of the buffer - public static Size Size(this Buffer2D buffer) + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span GetRowSpan(this Buffer2D buffer, int x, int y) where T : struct { - return new Size(buffer.Width, buffer.Height); + return buffer.GetSpan().Slice((y * buffer.Width) + x, buffer.Width - x); } /// - /// Returns a representing the full area of the buffer. + /// Gets a to the row 'y' beginning from the pixel at the first pixel on that row. /// + /// The buffer + /// The y (row) coordinate /// The element type - /// The - /// The - public static Rectangle FullRectangle(this Buffer2D buffer) + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span GetRowSpan(this Buffer2D buffer, int y) where T : struct { - return new Rectangle(0, 0, buffer.Width, buffer.Height); + return buffer.GetSpan().Slice(y * buffer.Width, buffer.Width); } /// - /// Return a to the subarea represented by 'rectangle' - /// - /// The element type - /// The - /// The rectangle subarea - /// The - public static BufferArea GetArea(this Buffer2D buffer, in Rectangle rectangle) - where T : struct => new BufferArea(buffer, rectangle); - - public static BufferArea GetArea(this Buffer2D buffer, int x, int y, int width, int height) - where T : struct => new BufferArea(buffer, new Rectangle(x, y, width, height)); - - public static BufferArea GetAreaBetweenRows(this Buffer2D buffer, int minY, int maxY) - where T : struct => new BufferArea(buffer, new Rectangle(0, minY, buffer.Width, maxY - minY)); - - /// - /// Return a to the whole area of 'buffer' + /// Returns the size of the buffer. /// /// The element type /// The - /// The - public static BufferArea GetArea(this Buffer2D buffer) - where T : struct => new BufferArea(buffer); + /// The of the buffer + public static Size Size(this Buffer2D buffer) + where T : struct + { + return new Size(buffer.Width, buffer.Height); + } /// - /// Gets a span for all the pixels in defined by + /// Gets a to the backing buffer of . /// - public static Span GetMultiRowSpan(this Buffer2D buffer, in RowInterval rows) + internal static Span GetSpan(this Buffer2D buffer) where T : struct { - return buffer.Span.Slice(rows.Min * buffer.Width, rows.Height * buffer.Width); + return buffer.MemorySource.GetSpan(); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs b/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs index 19ec725f2..3d4087546 100644 --- a/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs +++ b/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs @@ -127,5 +127,33 @@ namespace SixLabors.ImageSharp.Tests.Memory Assert.Equal(new Size(10, 5), b.Size()); } } + + [Theory] + [InlineData(100, 20, 0, 90, 10)] + [InlineData(100, 3, 0, 50, 50)] + [InlineData(123, 23, 10, 80, 13)] + [InlineData(10, 1, 3, 6, 3)] + [InlineData(2, 2, 0, 1, 1)] + [InlineData(5, 1, 1, 3, 2)] + public void CopyColumns(int width, int height, int startIndex, int destIndex, int columnCount) + { + Random rnd = new Random(123); + using (Buffer2D b = this.MemoryAllocator.Allocate2D(width, height)) + { + rnd.RandomFill(b.Span, 0, 1); + + b.CopyColumns(startIndex, destIndex, columnCount); + + for (int y = 0; y < b.Height; y++) + { + Span row = b.GetRowSpan(y); + + Span s = row.Slice(startIndex, columnCount); + Span d = row.Slice(destIndex, columnCount); + + Xunit.Assert.True(s.SequenceEqual(d)); + } + } + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/TestDataGenerator.cs b/tests/ImageSharp.Tests/TestUtilities/TestDataGenerator.cs index e3d8bf380..4ccb38745 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestDataGenerator.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestDataGenerator.cs @@ -23,14 +23,19 @@ namespace SixLabors.ImageSharp.Tests { float[] values = new float[length]; - for (int i = 0; i < length; i++) - { - values[i] = GetRandomFloat(rnd, minVal, maxVal); - } + RandomFill(rnd, values, minVal, maxVal); return values; } + public static void RandomFill(this Random rnd, Span destination, float minVal, float maxVal) + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = GetRandomFloat(rnd, minVal, maxVal); + } + } + /// /// Creates an of the given length consisting of random values between the two ranges. /// From d9e4d6cdd5ed328d28d399755a6218d6307454b9 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Fri, 19 Apr 2019 17:47:35 +0200 Subject: [PATCH 21/37] simplify ResizeWorker logic --- src/ImageSharp/Memory/RowInterval.cs | 1 - .../Transforms/Resize/ResizeProcessor.cs | 18 +++++---- .../Transforms/Resize/ResizeWorker.cs | 37 +++++++++---------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/ImageSharp/Memory/RowInterval.cs b/src/ImageSharp/Memory/RowInterval.cs index 7e144e37c..3ee7ae774 100644 --- a/src/ImageSharp/Memory/RowInterval.cs +++ b/src/ImageSharp/Memory/RowInterval.cs @@ -38,7 +38,6 @@ namespace SixLabors.ImageSharp.Memory /// public int Height => this.Max - this.Min; - public static bool operator ==(RowInterval left, RowInterval right) { return left.Equals(right); diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs index 63dd5efca..e75f6014a 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -197,7 +197,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int startY = this.TargetRectangle.Y; int startX = this.TargetRectangle.X; - var destWorkingRect = Rectangle.Intersect( + var targetWorkingRect = Rectangle.Intersect( this.TargetRectangle, new Rectangle(0, 0, width, height)); @@ -208,7 +208,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms float heightFactor = sourceRectangle.Height / (float)this.TargetRectangle.Height; ParallelHelper.IterateRows( - destWorkingRect, + targetWorkingRect, configuration, rows => { @@ -219,7 +219,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms source.GetPixelRowSpan((int)(((y - startY) * heightFactor) + sourceY)); Span targetRow = destination.GetPixelRowSpan(y); - for (int x = destWorkingRect.Left; x < destWorkingRect.Right; x++) + for (int x = targetWorkingRect.Left; x < targetWorkingRect.Right; x++) { // X coordinates of source points targetRow[x] = sourceRow[(int)(((x - startX) * widthFactor) + sourceX)]; @@ -237,8 +237,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms BufferArea sourceArea = source.PixelBuffer.GetArea(sourceRectangle); - // If we want to reintroduce processing: - // it's possible to launch multiple workers for different regions of the image + // To reintroduce parallel processing, we to launch multiple workers + // for different row intervals of the image. using (var worker = new ResizeWorker( configuration, sourceArea, @@ -246,11 +246,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.horizontalKernelMap, this.verticalKernelMap, width, - destWorkingRect, - startX)) + targetWorkingRect, + this.TargetRectangle.Location)) { worker.Initialize(); - worker.FillDestinationPixels(destWorkingRect.Top, destWorkingRect.Bottom, startY, destination.PixelBuffer); + + var workingInterval = new RowInterval(targetWorkingRect.Top, targetWorkingRect.Bottom); + worker.FillDestinationPixels(workingInterval, destination.PixelBuffer); } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index 66a17e2d7..339e8a3b2 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms internal class ResizeWorker : IDisposable where TPixel : struct, IPixel { - private readonly Buffer2D buffer; + private readonly Buffer2D transposedFirstPassBuffer; private readonly Configuration configuration; @@ -29,8 +29,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly Rectangle sourceRectangle; - private readonly int startX; - private readonly IMemoryOwner tempRowBuffer; private readonly IMemoryOwner tempColumnBuffer; @@ -39,7 +37,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly int destWidth; - private readonly Rectangle destWorkingRect; + private readonly Rectangle targetWorkingRect; + + private readonly Point targetOrigin; private readonly int windowBandDiameter; @@ -54,8 +54,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms ResizeKernelMap horizontalKernelMap, ResizeKernelMap verticalKernelMap, int destWidth, - Rectangle destWorkingRect, - int startX) + Rectangle targetWorkingRect, + Point targetOrigin) { this.configuration = configuration; this.source = source; @@ -64,8 +64,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.horizontalKernelMap = horizontalKernelMap; this.verticalKernelMap = verticalKernelMap; this.destWidth = destWidth; - this.destWorkingRect = destWorkingRect; - this.startX = startX; + this.targetWorkingRect = targetWorkingRect; + this.targetOrigin = targetOrigin; this.windowBandDiameter = verticalKernelMap.MaxDiameter; @@ -76,7 +76,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.windowHeight = Math.Min(this.sourceRectangle.Height, numberOfWindowBands * this.windowBandDiameter); - this.buffer = configuration.MemoryAllocator.Allocate2D( + this.transposedFirstPassBuffer = configuration.MemoryAllocator.Allocate2D( this.windowHeight, destWidth, AllocationOptions.Clean); @@ -89,7 +89,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms public void Dispose() { - this.buffer.Dispose(); + this.transposedFirstPassBuffer.Dispose(); this.tempRowBuffer.Dispose(); this.tempColumnBuffer.Dispose(); } @@ -97,23 +97,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms [MethodImpl(MethodImplOptions.AggressiveInlining)] public Span GetColumnSpan(int x, int startY) { - return this.buffer.GetRowSpan(x).Slice(startY - this.currentWindow.Min); + return this.transposedFirstPassBuffer.GetRowSpan(x).Slice(startY - this.currentWindow.Min); } - public void Initialize() { this.CalculateFirstPassValues(this.currentWindow); } - public void FillDestinationPixels(int minY, int maxY, int startY, Buffer2D destination) + public void FillDestinationPixels(RowInterval rowInterval, Buffer2D destination) { Span tempColSpan = this.tempColumnBuffer.GetSpan(); - for (int y = minY; y < maxY; y++) + for (int y = rowInterval.Min; y < rowInterval.Max; y++) { // Ensure offsets are normalized for cropping and padding. - ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - startY); + ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - this.targetOrigin.Y); while (kernel.StartIndex + kernel.Length > this.currentWindow.Max) { @@ -141,7 +140,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms [MethodImpl(MethodImplOptions.AggressiveInlining)] private Span GetColumnSpan(int x) { - return this.buffer.GetRowSpan(x); + return this.transposedFirstPassBuffer.GetRowSpan(x); } private void Slide() @@ -166,11 +165,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.conversionModifiers); // ref Vector4 firstPassBaseRef = ref this.buffer.Span[y - top]; - Span firstPassSpan = this.buffer.Span.Slice(y - window.Min); + Span firstPassSpan = this.transposedFirstPassBuffer.Span.Slice(y - window.Min); - for (int x = this.destWorkingRect.Left; x < this.destWorkingRect.Right; x++) + for (int x = this.targetWorkingRect.Left; x < this.targetWorkingRect.Right; x++) { - ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - this.startX); + ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - this.targetOrigin.X); firstPassSpan[x * this.windowHeight] = kernel.Convolve(tempRowSpan); // Unsafe.Add(ref firstPassBaseRef, x * this.sourceRectangle.Height) = kernel.Convolve(tempRowSpan); From 51cc6593535d13116ee023a43626c92f1515a306 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Fri, 19 Apr 2019 21:00:39 +0200 Subject: [PATCH 22/37] WorkingBufferSizeHintInBytes_IsAppliedCorrectly --- .../Transforms/Resize/ResizeHelper.cs | 4 +- .../Transforms/Resize/ResizeWorker.cs | 20 +- .../Processors/Transforms/ResamplerTests.cs | 69 +++ .../Transforms/ResizeHelperTests.cs | 35 ++ .../Processors/Transforms/ResizeTests.cs | 143 +++--- .../BasicTestPatternProvider.cs | 2 +- .../ImageProviders/BlankProvider.cs | 2 +- .../ImageProviders/FileProvider.cs | 2 +- .../ImageProviders/TestImageProvider.cs | 2 +- .../ImageProviders/TestPatternProvider.cs | 3 +- .../TestUtilities/TestMemoryAllocator.cs | 39 +- .../Tests/TestImageProviderTests.cs | 458 ++++++++++-------- tests/Images/External | 2 +- 13 files changed, 474 insertions(+), 307 deletions(-) create mode 100644 tests/ImageSharp.Tests/Processing/Processors/Transforms/ResamplerTests.cs create mode 100644 tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeHelperTests.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs index 595a7e852..956e6b84e 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs @@ -16,12 +16,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms internal static class ResizeHelper { public static unsafe int CalculateResizeWorkerHeightInWindowBands( - int windowBandDiameter, + int windowBandHeight, int width, int sizeLimitHintInBytes) { int sizeLimitHint = sizeLimitHintInBytes / sizeof(Vector4); - int sizeOfOneWindow = windowBandDiameter * width; + int sizeOfOneWindow = windowBandHeight * width; return Math.Max(2, sizeLimitHint / sizeOfOneWindow); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index 339e8a3b2..ce28a00ee 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -41,9 +41,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly Point targetOrigin; - private readonly int windowBandDiameter; + private readonly int windowBandHeight; - private readonly int windowHeight; + private readonly int workerHeight; private RowInterval currentWindow; @@ -67,24 +67,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.targetWorkingRect = targetWorkingRect; this.targetOrigin = targetOrigin; - this.windowBandDiameter = verticalKernelMap.MaxDiameter; + this.windowBandHeight = verticalKernelMap.MaxDiameter; int numberOfWindowBands = ResizeHelper.CalculateResizeWorkerHeightInWindowBands( - this.windowBandDiameter, + this.windowBandHeight, destWidth, configuration.WorkingBufferSizeHintInBytes); - this.windowHeight = Math.Min(this.sourceRectangle.Height, numberOfWindowBands * this.windowBandDiameter); + this.workerHeight = Math.Min(this.sourceRectangle.Height, numberOfWindowBands * this.windowBandHeight); this.transposedFirstPassBuffer = configuration.MemoryAllocator.Allocate2D( - this.windowHeight, + this.workerHeight, destWidth, AllocationOptions.Clean); this.tempRowBuffer = configuration.MemoryAllocator.Allocate(this.sourceRectangle.Width); this.tempColumnBuffer = configuration.MemoryAllocator.Allocate(destWidth); - this.currentWindow = new RowInterval(0, this.windowHeight); + this.currentWindow = new RowInterval(0, this.workerHeight); } public void Dispose() @@ -145,8 +145,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private void Slide() { - int minY = this.currentWindow.Min + this.windowBandDiameter; - int maxY = Math.Min(this.currentWindow.Max + this.windowBandDiameter, this.sourceRectangle.Height); + int minY = this.currentWindow.Min + this.windowBandHeight; + int maxY = Math.Min(this.currentWindow.Max + this.windowBandHeight, this.sourceRectangle.Height); this.currentWindow = new RowInterval(minY, maxY); this.CalculateFirstPassValues(this.currentWindow); } @@ -170,7 +170,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int x = this.targetWorkingRect.Left; x < this.targetWorkingRect.Right; x++) { ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - this.targetOrigin.X); - firstPassSpan[x * this.windowHeight] = kernel.Convolve(tempRowSpan); + firstPassSpan[x * this.workerHeight] = kernel.Convolve(tempRowSpan); // Unsafe.Add(ref firstPassBaseRef, x * this.sourceRectangle.Height) = kernel.Convolve(tempRowSpan); } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResamplerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResamplerTests.cs new file mode 100644 index 000000000..b7b4597c7 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResamplerTests.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms +{ + public class ResamplerTests + { + [Theory] + [InlineData(-2, 0)] + [InlineData(-1, 0)] + [InlineData(0, 1)] + [InlineData(1, 0)] + [InlineData(2, 0)] + public static void BicubicWindowOscillatesCorrectly(float x, float expected) + { + IResampler sampler = KnownResamplers.Bicubic; + float result = sampler.GetValue(x); + + Assert.Equal(result, expected); + } + + [Theory] + [InlineData(-2, 0)] + [InlineData(-1, 0)] + [InlineData(0, 1)] + [InlineData(1, 0)] + [InlineData(2, 0)] + public static void Lanczos3WindowOscillatesCorrectly(float x, float expected) + { + IResampler sampler = KnownResamplers.Lanczos3; + float result = sampler.GetValue(x); + + Assert.Equal(result, expected); + } + + [Theory] + [InlineData(-4, 0)] + [InlineData(-2, 0)] + [InlineData(0, 1)] + [InlineData(2, 0)] + [InlineData(4, 0)] + public static void Lanczos5WindowOscillatesCorrectly(float x, float expected) + { + IResampler sampler = KnownResamplers.Lanczos5; + float result = sampler.GetValue(x); + + Assert.Equal(result, expected); + } + + [Theory] + [InlineData(-2, 0)] + [InlineData(-1, 0)] + [InlineData(0, 1)] + [InlineData(1, 0)] + [InlineData(2, 0)] + public static void TriangleWindowOscillatesCorrectly(float x, float expected) + { + IResampler sampler = KnownResamplers.Triangle; + float result = sampler.GetValue(x); + + Assert.Equal(result, expected); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeHelperTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeHelperTests.cs new file mode 100644 index 000000000..b0d8ef653 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeHelperTests.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms +{ + public class ResizeHelperTests + { + + [Theory] + [InlineData(20, 100, 1, 2)] + [InlineData(20, 100, 20*100*16, 2)] + [InlineData(20, 100, 40*100*16, 2)] + [InlineData(20, 100, 59*100*16, 2)] + [InlineData(20, 100, 60*100*16, 3)] + [InlineData(17, 63, 5*17*63*16, 5)] + [InlineData(17, 63, 5*17*63*16+1, 5)] + [InlineData(17, 63, 6*17*63*16-1, 5)] + [InlineData(33, 400, 1*1024*1024, 4)] + [InlineData(33, 400, 8*1024*1024, 39)] + [InlineData(50, 300, 1*1024*1024, 4)] + public void CalculateResizeWorkerHeightInWindowBands( + int windowDiameter, + int width, + int sizeLimitHintInBytes, + int expectedCount) + { + int actualCount = ResizeHelper.CalculateResizeWorkerHeightInWindowBands(windowDiameter, width, sizeLimitHintInBytes); + Assert.Equal(expectedCount, actualCount); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 532558166..2ffb42916 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -2,12 +2,17 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; +using SixLabors.ImageSharp.Tests.Memory; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using SixLabors.Memory; using SixLabors.Primitives; using Xunit; @@ -35,84 +40,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.07F); - [Theory] - [InlineData(20, 100, 1, 2)] - [InlineData(20, 100, 20*100*16, 2)] - [InlineData(20, 100, 40*100*16, 2)] - [InlineData(20, 100, 59*100*16, 2)] - [InlineData(20, 100, 60*100*16, 3)] - [InlineData(17, 63, 5*17*63*16, 5)] - [InlineData(17, 63, 5*17*63*16+1, 5)] - [InlineData(17, 63, 6*17*63*16-1, 5)] - [InlineData(33, 400, 1*1024*1024, 4)] - [InlineData(33, 400, 8*1024*1024, 39)] - [InlineData(50, 300, 1*1024*1024, 4)] - public void CalculateResizeWorkerHeightInWindowBands( - int windowDiameter, - int width, - int sizeLimitHintInBytes, - int expectedCount) - { - int actualCount = ResizeHelper.CalculateResizeWorkerHeightInWindowBands(windowDiameter, width, sizeLimitHintInBytes); - Assert.Equal(expectedCount, actualCount); - } - - [Theory] - [InlineData(-2, 0)] - [InlineData(-1, 0)] - [InlineData(0, 1)] - [InlineData(1, 0)] - [InlineData(2, 0)] - public static void BicubicWindowOscillatesCorrectly(float x, float expected) - { - IResampler sampler = KnownResamplers.Bicubic; - float result = sampler.GetValue(x); - - Assert.Equal(result, expected); - } - - [Theory] - [InlineData(-2, 0)] - [InlineData(-1, 0)] - [InlineData(0, 1)] - [InlineData(1, 0)] - [InlineData(2, 0)] - public static void Lanczos3WindowOscillatesCorrectly(float x, float expected) - { - IResampler sampler = KnownResamplers.Lanczos3; - float result = sampler.GetValue(x); - - Assert.Equal(result, expected); - } - - [Theory] - [InlineData(-4, 0)] - [InlineData(-2, 0)] - [InlineData(0, 1)] - [InlineData(2, 0)] - [InlineData(4, 0)] - public static void Lanczos5WindowOscillatesCorrectly(float x, float expected) - { - IResampler sampler = KnownResamplers.Lanczos5; - float result = sampler.GetValue(x); - - Assert.Equal(result, expected); - } - - [Theory] - [InlineData(-2, 0)] - [InlineData(-1, 0)] - [InlineData(0, 1)] - [InlineData(1, 0)] - [InlineData(2, 0)] - public static void TriangleWindowOscillatesCorrectly(float x, float expected) - { - IResampler sampler = KnownResamplers.Triangle; - float result = sampler.GetValue(x); - - Assert.Equal(result, expected); - } - [Theory] [WithBasicTestPatternImages(15, 12, PixelTypes.Rgba32, 2, 3, 1, 2)] [WithBasicTestPatternImages(2, 256, PixelTypes.Rgba32, 1, 1, 1, 8)] @@ -136,6 +63,66 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } } + private static readonly int SizeOfVector4 = Unsafe.SizeOf(); + + [Theory] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 50)] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 60)] + [WithTestPatternImages(100, 400, PixelTypes.Rgba32, 110)] + [WithTestPatternImages(79, 97, PixelTypes.Rgba32, 73)] + [WithTestPatternImages(79, 97, PixelTypes.Rgba32, 5)] + [WithTestPatternImages(47, 193, PixelTypes.Rgba32, 73)] + [WithTestPatternImages(23, 211, PixelTypes.Rgba32, 31)] + public void WorkingBufferSizeHintInBytes_IsAppliedCorrectly( + TestImageProvider provider, + int workingBufferLimitInRows) + where TPixel : struct, IPixel + { + using (Image image0 = provider.GetImage()) + { + Size destSize = image0.Size() / 4; + + Configuration configuration = Configuration.CreateDefaultInstance(); + + int workingBufferSizeHintInBytes = workingBufferLimitInRows * destSize.Width * SizeOfVector4; + TestMemoryAllocator allocator = new TestMemoryAllocator(); + configuration.MemoryAllocator = allocator; + configuration.WorkingBufferSizeHintInBytes = workingBufferSizeHintInBytes; + + var verticalKernelMap = ResizeKernelMap.Calculate( + KnownResamplers.Bicubic, + destSize.Height, + image0.Height, + Configuration.Default.MemoryAllocator); + int minimumWorkerAllocationInBytes = verticalKernelMap.MaxDiameter * 2 * destSize.Width * SizeOfVector4; + verticalKernelMap.Dispose(); + + using (Image image = image0.Clone(configuration)) + { + image.Mutate(x => x.Resize(destSize, KnownResamplers.Bicubic, false)); + + image.DebugSave( + provider, + testOutputDetails: workingBufferLimitInRows, + appendPixelTypeToFileName: false); + image.CompareToReferenceOutput( + provider, + testOutputDetails: workingBufferLimitInRows, + appendPixelTypeToFileName: false); + + Assert.NotEmpty(allocator.AllocationLog); + + var internalAllocations = allocator.AllocationLog.Where( + e => e.ElementType == typeof(Vector4)).ToArray(); + + int maxAllocationSize = internalAllocations.Max(e => e.LengthInBytes); + + + Assert.True(maxAllocationSize <= Math.Max(workingBufferSizeHintInBytes, minimumWorkerAllocationInBytes)); + } + } + } + [Theory] [WithTestPatternImages(100, 100, DefaultPixelType)] public void Resize_Compand(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs index 47bb22aeb..de203535c 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Tests public override Image GetImage() { - var result = new Image(this.Width, this.Height); + var result = new Image(this.Configuration, this.Width, this.Height); TPixel topLeftColor = NamedColors.Red; TPixel topRightColor = NamedColors.Green; diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BlankProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BlankProvider.cs index 1275e522f..dae2f0cfe 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BlankProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BlankProvider.cs @@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Tests protected int Width { get; private set; } - public override Image GetImage() => new Image(this.Width, this.Height); + public override Image GetImage() => new Image(this.Configuration, this.Width, this.Height); public override void Deserialize(IXunitSerializationInfo info) diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs index 3ed696c47..8c5b88b28 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs @@ -152,7 +152,7 @@ namespace SixLabors.ImageSharp.Tests Image cachedImage = cache.GetOrAdd(key, _ => this.LoadImage(decoder)); - return cachedImage.Clone(); + return cachedImage.Clone(this.Configuration); } public override void Deserialize(IXunitSerializationInfo info) diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs index 52f66a78b..15fab9b2b 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs @@ -106,7 +106,7 @@ namespace SixLabors.ImageSharp.Tests /// public Image GetImage(Action> operationsToApply) { - Image img = GetImage(); + Image img = this.GetImage(); img.Mutate(operationsToApply); return img; } diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs index 336cdbf11..6df8c8501 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs @@ -45,9 +45,8 @@ namespace SixLabors.ImageSharp.Tests DrawTestPattern(image); TestImages.Add(this.SourceFileOrDescription, image); } + return TestImages[this.SourceFileOrDescription].Clone(this.Configuration); } - - return TestImages[this.SourceFileOrDescription].Clone(); } /// diff --git a/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs b/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs index dc755e682..5613e7b68 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs @@ -1,5 +1,7 @@ using System; using System.Buffers; +using System.Collections.Generic; +using System.Numerics; using System.Runtime.InteropServices; using SixLabors.Memory; @@ -8,6 +10,8 @@ namespace SixLabors.ImageSharp.Tests.Memory { internal class TestMemoryAllocator : MemoryAllocator { + private List allocationLog = new List(); + public TestMemoryAllocator(byte dirtyValue = 42) { this.DirtyValue = dirtyValue; @@ -18,10 +22,11 @@ namespace SixLabors.ImageSharp.Tests.Memory /// public byte DirtyValue { get; } + public IList AllocationLog => this.allocationLog; + public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) - { + { T[] array = this.AllocateArray(length, options); - return new BasicArrayBuffer(array, length); } @@ -34,6 +39,7 @@ namespace SixLabors.ImageSharp.Tests.Memory private T[] AllocateArray(int length, AllocationOptions options) where T : struct { + this.allocationLog.Add(AllocationRequest.Create(options, length)); var array = new T[length + 42]; if (options == AllocationOptions.None) @@ -44,6 +50,35 @@ namespace SixLabors.ImageSharp.Tests.Memory return array; } + + public struct AllocationRequest + { + private AllocationRequest(Type elementType, AllocationOptions allocationOptions, int length, int lengthInBytes) + { + this.ElementType = elementType; + this.AllocationOptions = allocationOptions; + this.Length = length; + this.LengthInBytes = lengthInBytes; + + if (elementType == typeof(Vector4)) + { + + } + } + + public static AllocationRequest Create(AllocationOptions allocationOptions, int length) + { + Type type = typeof(T); + int elementSize = Marshal.SizeOf(type); + return new AllocationRequest(type, allocationOptions, length, length * elementSize); + } + + public Type ElementType { get; } + public AllocationOptions AllocationOptions { get; } + public int Length { get; } + public int LengthInBytes { get; } + } + /// /// Wraps an array as an instance. diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs index 1bee34f1a..4ef6a582c 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -4,112 +4,135 @@ using System; using System.Collections.Concurrent; using System.IO; + +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; + using Xunit; using Xunit.Abstractions; + // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests { public class TestImageProviderTests { + public static readonly TheoryData BasicData = new TheoryData() + { + TestImageProvider.Blank(10, 20), + TestImageProvider.Blank(10, 20), + }; + + public static readonly TheoryData FileData = new TheoryData() + { + TestImageProvider.File(TestImages.Bmp.Car), + TestImageProvider.File( + TestImages.Bmp.F) + }; + + public static string[] AllBmpFiles = { TestImages.Bmp.F, TestImages.Bmp.Bit8 }; + public TestImageProviderTests(ITestOutputHelper output) => this.Output = output; private ITestOutputHelper Output { get; } - [Theory] - [WithBlankImages(1, 1, PixelTypes.Rgba32)] - public void NoOutputSubfolderIsPresentByDefault(TestImageProvider provider) - where TPixel : struct, IPixel => Assert.Empty(provider.Utility.OutputSubfolderName); + /// + /// Need to us to create instance of when pixelType is StandardImageClass + /// + /// + /// + /// + public static Image CreateTestImage() + where TPixel : struct, IPixel => + new Image(3, 3); [Theory] - [WithBlankImages(42, 666, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.HalfSingle, "hello")] - public void Use_WithEmptyImageAttribute(TestImageProvider provider, string message) + [MemberData(nameof(BasicData))] + public void Blank_MemberData(TestImageProvider provider) where TPixel : struct, IPixel { Image img = provider.GetImage(); - Assert.Equal(42, img.Width); - Assert.Equal(666, img.Height); - Assert.Equal("hello", message); + Assert.True(img.Width * img.Height > 0); } [Theory] - [WithBlankImages(42, 666, PixelTypes.All, "hello")] - public void Use_WithBlankImagesAttribute_WithAllPixelTypes( - TestImageProvider provider, - string message) + [MemberData(nameof(FileData))] + public void File_MemberData(TestImageProvider provider) where TPixel : struct, IPixel { + this.Output.WriteLine("SRC: " + provider.Utility.SourceFileOrDescription); + this.Output.WriteLine("OUT: " + provider.Utility.GetTestOutputFileName()); + Image img = provider.GetImage(); - Assert.Equal(42, img.Width); - Assert.Equal(666, img.Height); - Assert.Equal("hello", message); + Assert.True(img.Width * img.Height > 0); } [Theory] - [WithBlankImages(1, 1, PixelTypes.Rgba32, PixelTypes.Rgba32)] - [WithBlankImages(1, 1, PixelTypes.Alpha8, PixelTypes.Alpha8)] - [WithBlankImages(1, 1, PixelTypes.Argb32, PixelTypes.Argb32)] - public void PixelType_PropertyValueIsCorrect(TestImageProvider provider, PixelTypes expected) - where TPixel : struct, IPixel => Assert.Equal(expected, provider.PixelType); - - [Theory] - [WithFile(TestImages.Bmp.Car, PixelTypes.All, 88)] - [WithFile(TestImages.Bmp.F, PixelTypes.All, 88)] - public void Use_WithFileAttribute(TestImageProvider provider, int yo) + [WithFile(TestImages.Bmp.F, PixelTypes.Rgba32)] + public void GetImage_WithCustomParameterlessDecoder_ShouldUtilizeCache( + TestImageProvider provider) where TPixel : struct, IPixel { + if (!TestEnvironment.Is64BitProcess) + { + // We don't cache with the 32 bit build. + return; + } + Assert.NotNull(provider.Utility.SourceFileOrDescription); - Image img = provider.GetImage(); - Assert.True(img.Width * img.Height > 0); - Assert.Equal(88, yo); + TestDecoder.DoTestThreadSafe( + () => + { + string testName = nameof(this.GetImage_WithCustomParameterlessDecoder_ShouldUtilizeCache); - string fn = provider.Utility.GetTestOutputFileName("jpg"); - this.Output.WriteLine(fn); - } + var decoder = new TestDecoder(); + decoder.InitCaller(testName); - private class TestDecoder : IImageDecoder - { - public Image Decode(Configuration configuration, Stream stream) - where TPixel : struct, IPixel - { - invocationCounts[this.callerName]++; - return new Image(42, 42); - } + provider.GetImage(decoder); + Assert.Equal(1, TestDecoder.GetInvocationCount(testName)); - // Couldn't make xUnit happy without this hackery: + provider.GetImage(decoder); + Assert.Equal(1, TestDecoder.GetInvocationCount(testName)); + }); + } - private static readonly ConcurrentDictionary invocationCounts = new ConcurrentDictionary(); + [Theory] + [WithFile(TestImages.Bmp.F, PixelTypes.Rgba32)] + public void GetImage_WithCustomParametricDecoder_ShouldNotUtilizeCache_WhenParametersAreNotEqual( + TestImageProvider provider) + where TPixel : struct, IPixel + { + Assert.NotNull(provider.Utility.SourceFileOrDescription); - private string callerName = null; + TestDecoderWithParameters.DoTestThreadSafe( + () => + { + string testName = nameof(this + .GetImage_WithCustomParametricDecoder_ShouldNotUtilizeCache_WhenParametersAreNotEqual); - internal void InitCaller(string name) - { - this.callerName = name; - invocationCounts[name] = 0; - } + var decoder1 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 42 }; + decoder1.InitCaller(testName); - internal static int GetInvocationCount(string callerName) => invocationCounts[callerName]; + var decoder2 = new TestDecoderWithParameters() { Param1 = "LoL", Param2 = 42 }; + decoder2.InitCaller(testName); - private static readonly object Monitor = new object(); + provider.GetImage(decoder1); + Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName)); - public static void DoTestThreadSafe(Action action) - { - lock (Monitor) - { - action(); - } - } + provider.GetImage(decoder2); + Assert.Equal(2, TestDecoderWithParameters.GetInvocationCount(testName)); + }); } [Theory] [WithFile(TestImages.Bmp.F, PixelTypes.Rgba32)] - public void GetImage_WithCustomParameterlessDecoder_ShouldUtilizeCache(TestImageProvider provider) + public void GetImage_WithCustomParametricDecoder_ShouldUtilizeCache_WhenParametersAreEqual( + TestImageProvider provider) where TPixel : struct, IPixel { if (!TestEnvironment.Is64BitProcess) @@ -120,121 +143,122 @@ namespace SixLabors.ImageSharp.Tests Assert.NotNull(provider.Utility.SourceFileOrDescription); - TestDecoder.DoTestThreadSafe(() => - { - string testName = nameof(this.GetImage_WithCustomParameterlessDecoder_ShouldUtilizeCache); - - var decoder = new TestDecoder(); - decoder.InitCaller(testName); - - provider.GetImage(decoder); - Assert.Equal(1, TestDecoder.GetInvocationCount(testName)); + TestDecoderWithParameters.DoTestThreadSafe( + () => + { + string testName = nameof(this + .GetImage_WithCustomParametricDecoder_ShouldUtilizeCache_WhenParametersAreEqual); - provider.GetImage(decoder); - Assert.Equal(1, TestDecoder.GetInvocationCount(testName)); - }); - } + var decoder1 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 666 }; + decoder1.InitCaller(testName); - private class TestDecoderWithParameters : IImageDecoder - { - public string Param1 { get; set; } + var decoder2 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 666 }; + decoder2.InitCaller(testName); - public int Param2 { get; set; } + provider.GetImage(decoder1); + Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName)); - public Image Decode(Configuration configuration, Stream stream) - where TPixel : struct, IPixel - { - invocationCounts[this.callerName]++; - return new Image(42, 42); - } + provider.GetImage(decoder2); + Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName)); + }); + } - private static readonly ConcurrentDictionary invocationCounts = new ConcurrentDictionary(); + [Theory] + [WithBlankImages(1, 1, PixelTypes.Rgba32)] + public void NoOutputSubfolderIsPresentByDefault(TestImageProvider provider) + where TPixel : struct, IPixel => + Assert.Empty(provider.Utility.OutputSubfolderName); - private string callerName = null; + [Theory] + [WithBlankImages(1, 1, PixelTypes.Rgba32, PixelTypes.Rgba32)] + [WithBlankImages(1, 1, PixelTypes.Alpha8, PixelTypes.Alpha8)] + [WithBlankImages(1, 1, PixelTypes.Argb32, PixelTypes.Argb32)] + public void PixelType_PropertyValueIsCorrect(TestImageProvider provider, PixelTypes expected) + where TPixel : struct, IPixel => + Assert.Equal(expected, provider.PixelType); - internal void InitCaller(string name) + [Theory] + [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)] + public void SaveTestOutputFileMultiFrame(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) { - this.callerName = name; - invocationCounts[name] = 0; - } - - internal static int GetInvocationCount(string callerName) => invocationCounts[callerName]; - - private static readonly object Monitor = new object(); + string[] files = provider.Utility.SaveTestOutputFileMultiFrame(image); - public static void DoTestThreadSafe(Action action) - { - lock (Monitor) + Assert.True(files.Length > 2); + foreach (string path in files) { - action(); + this.Output.WriteLine(path); + Assert.True(File.Exists(path)); } } } [Theory] - [WithFile(TestImages.Bmp.F, PixelTypes.Rgba32)] - public void GetImage_WithCustomParametricDecoder_ShouldUtilizeCache_WhenParametersAreEqual(TestImageProvider provider) + [WithBasicTestPatternImages(50, 100, PixelTypes.Rgba32)] + [WithBasicTestPatternImages(49, 17, PixelTypes.Rgba32)] + [WithBasicTestPatternImages(20, 10, PixelTypes.Rgba32)] + public void Use_WithBasicTestPatternImages(TestImageProvider provider) where TPixel : struct, IPixel { - if (!TestEnvironment.Is64BitProcess) + using (Image img = provider.GetImage()) { - // We don't cache with the 32 bit build. - return; + img.DebugSave(provider); } + } - Assert.NotNull(provider.Utility.SourceFileOrDescription); - - TestDecoderWithParameters.DoTestThreadSafe(() => - { - string testName = - nameof(this.GetImage_WithCustomParametricDecoder_ShouldUtilizeCache_WhenParametersAreEqual); - - var decoder1 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 666 }; - decoder1.InitCaller(testName); + [Theory] + [WithBlankImages(42, 666, PixelTypes.All, "hello")] + public void Use_WithBlankImagesAttribute_WithAllPixelTypes( + TestImageProvider provider, + string message) + where TPixel : struct, IPixel + { + Image img = provider.GetImage(); - var decoder2 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 666 }; - decoder2.InitCaller(testName); + Assert.Equal(42, img.Width); + Assert.Equal(666, img.Height); + Assert.Equal("hello", message); + } - provider.GetImage(decoder1); - Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName)); + [Theory] + [WithBlankImages(42, 666, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.HalfSingle, "hello")] + public void Use_WithEmptyImageAttribute(TestImageProvider provider, string message) + where TPixel : struct, IPixel + { + Image img = provider.GetImage(); - provider.GetImage(decoder2); - Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName)); - }); + Assert.Equal(42, img.Width); + Assert.Equal(666, img.Height); + Assert.Equal("hello", message); } [Theory] - [WithFile(TestImages.Bmp.F, PixelTypes.Rgba32)] - public void GetImage_WithCustomParametricDecoder_ShouldNotUtilizeCache_WhenParametersAreNotEqual(TestImageProvider provider) + [WithFile(TestImages.Bmp.Car, PixelTypes.All, 123)] + [WithFile(TestImages.Bmp.F, PixelTypes.All, 123)] + public void Use_WithFileAttribute(TestImageProvider provider, int yo) where TPixel : struct, IPixel { Assert.NotNull(provider.Utility.SourceFileOrDescription); - - TestDecoderWithParameters.DoTestThreadSafe(() => + using (Image img = provider.GetImage()) { - string testName = - nameof(this.GetImage_WithCustomParametricDecoder_ShouldNotUtilizeCache_WhenParametersAreNotEqual); - - var decoder1 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 42 }; - decoder1.InitCaller(testName); - - var decoder2 = new TestDecoderWithParameters() { Param1 = "LoL", Param2 = 42 }; - decoder2.InitCaller(testName); + Assert.True(img.Width * img.Height > 0); - provider.GetImage(decoder1); - Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName)); + Assert.Equal(123, yo); - provider.GetImage(decoder2); - Assert.Equal(2, TestDecoderWithParameters.GetInvocationCount(testName)); - }); + string fn = provider.Utility.GetTestOutputFileName("jpg"); + this.Output.WriteLine(fn); + } } - - public static string[] AllBmpFiles = - { - TestImages.Bmp.F, - TestImages.Bmp.Bit8 - }; + [Theory] + [WithFile(TestImages.Jpeg.Baseline.Testorig420, PixelTypes.Rgba32)] + public void Use_WithFileAttribute_CustomConfig(TestImageProvider provider) + where TPixel : struct, IPixel + { + EnsureCustomConfigurationIsApplied(provider); + } [Theory] [WithFileCollection(nameof(AllBmpFiles), PixelTypes.Rgba32 | PixelTypes.Argb32)] @@ -249,20 +273,15 @@ namespace SixLabors.ImageSharp.Tests } [Theory] - [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)] - public void SaveTestOutputFileMultiFrame(TestImageProvider provider) + [WithMemberFactory(nameof(CreateTestImage), PixelTypes.All)] + public void Use_WithMemberFactoryAttribute(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) + Image img = provider.GetImage(); + Assert.Equal(3, img.Width); + if (provider.PixelType == PixelTypes.Rgba32) { - string[] files = provider.Utility.SaveTestOutputFileMultiFrame(image); - - Assert.True(files.Length > 2); - foreach (string path in files) - { - this.Output.WriteLine(path); - Assert.True(File.Exists(path)); - } + Assert.IsType>(img); } } @@ -291,89 +310,112 @@ namespace SixLabors.ImageSharp.Tests } } - /// - /// Need to us to create instance of when pixelType is StandardImageClass - /// - /// - /// - /// - public static Image CreateTestImage() - where TPixel : struct, IPixel => new Image(3, 3); - [Theory] - [WithMemberFactory(nameof(CreateTestImage), PixelTypes.All)] - public void Use_WithMemberFactoryAttribute(TestImageProvider provider) + [WithTestPatternImages(49, 20, PixelTypes.Rgba32)] + public void Use_WithTestPatternImages(TestImageProvider provider) where TPixel : struct, IPixel { - Image img = provider.GetImage(); - Assert.Equal(3, img.Width); - if (provider.PixelType == PixelTypes.Rgba32) + using (Image img = provider.GetImage()) { - Assert.IsType>(img); + img.DebugSave(provider); } - } - + [Theory] - [WithTestPatternImages(49,20, PixelTypes.Rgba32)] - public void Use_WithTestPatternImages(TestImageProvider provider) + [WithTestPatternImages(20, 20, PixelTypes.Rgba32)] + public void Use_WithTestPatternImages_CustomConfiguration(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image img = provider.GetImage()) - { - img.DebugSave(provider); - } + EnsureCustomConfigurationIsApplied(provider); } - [Theory] - [WithBasicTestPatternImages(50, 100, PixelTypes.Rgba32)] - [WithBasicTestPatternImages(49,17, PixelTypes.Rgba32)] - [WithBasicTestPatternImages(20, 10, PixelTypes.Rgba32)] - public void Use_WithBasicTestPatternImages(TestImageProvider provider) + private static void EnsureCustomConfigurationIsApplied(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image img = provider.GetImage()) + using (var image1 = provider.GetImage()) { - img.DebugSave(provider); + var customConfiguration = Configuration.CreateDefaultInstance(); + provider.Configuration = customConfiguration; + + using (var image2 = provider.GetImage()) + using (var image3 = provider.GetImage()) + { + Assert.Same(customConfiguration, image2.GetConfiguration()); + Assert.Same(customConfiguration, image3.GetConfiguration()); + } } } - - public static readonly TheoryData BasicData = new TheoryData() + private class TestDecoder : IImageDecoder { - TestImageProvider.Blank(10, 20), - TestImageProvider.Blank( - 10, - 20), - }; + // Couldn't make xUnit happy without this hackery: - [Theory] - [MemberData(nameof(BasicData))] - public void Blank_MemberData(TestImageProvider provider) - where TPixel : struct, IPixel - { - Image img = provider.GetImage(); + private static readonly ConcurrentDictionary invocationCounts = + new ConcurrentDictionary(); - Assert.True(img.Width * img.Height > 0); + private static readonly object Monitor = new object(); + + private string callerName = null; + + public static void DoTestThreadSafe(Action action) + { + lock (Monitor) + { + action(); + } + } + + public Image Decode(Configuration configuration, Stream stream) + where TPixel : struct, IPixel + { + invocationCounts[this.callerName]++; + return new Image(42, 42); + } + + internal static int GetInvocationCount(string callerName) => invocationCounts[callerName]; + + internal void InitCaller(string name) + { + this.callerName = name; + invocationCounts[name] = 0; + } } - public static readonly TheoryData FileData = new TheoryData() + private class TestDecoderWithParameters : IImageDecoder { - TestImageProvider.File(TestImages.Bmp.Car), - TestImageProvider.File(TestImages.Bmp.F) - }; + private static readonly ConcurrentDictionary invocationCounts = + new ConcurrentDictionary(); - [Theory] - [MemberData(nameof(FileData))] - public void File_MemberData(TestImageProvider provider) - where TPixel : struct, IPixel - { - this.Output.WriteLine("SRC: " + provider.Utility.SourceFileOrDescription); - this.Output.WriteLine("OUT: " + provider.Utility.GetTestOutputFileName()); + private static readonly object Monitor = new object(); - Image img = provider.GetImage(); + private string callerName = null; - Assert.True(img.Width * img.Height > 0); + public string Param1 { get; set; } + + public int Param2 { get; set; } + + public static void DoTestThreadSafe(Action action) + { + lock (Monitor) + { + action(); + } + } + + public Image Decode(Configuration configuration, Stream stream) + where TPixel : struct, IPixel + { + invocationCounts[this.callerName]++; + return new Image(42, 42); + } + + internal static int GetInvocationCount(string callerName) => invocationCounts[callerName]; + + internal void InitCaller(string name) + { + this.callerName = name; + invocationCounts[name] = 0; + } } } -} +} \ No newline at end of file diff --git a/tests/Images/External b/tests/Images/External index c7333d2a8..6db7ed95d 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit c7333d2a81a74d1936bd202bcb6b16cbfe6bcdce +Subproject commit 6db7ed95dd8a2de4caa6d885952b6689d026ad27 From 3860fb6f5bcd23e3fdd1f2e907df44806f28dde1 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Fri, 19 Apr 2019 21:15:50 +0200 Subject: [PATCH 23/37] more robust tests --- .../Processing/Processors/Transforms/ResizeTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 2ffb42916..6d02b4b45 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -112,11 +112,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms Assert.NotEmpty(allocator.AllocationLog); - var internalAllocations = allocator.AllocationLog.Where( - e => e.ElementType == typeof(Vector4)).ToArray(); - - int maxAllocationSize = internalAllocations.Max(e => e.LengthInBytes); - + int maxAllocationSize = allocator.AllocationLog.Where( + e => e.ElementType == typeof(Vector4)).Max(e => e.LengthInBytes); Assert.True(maxAllocationSize <= Math.Max(workingBufferSizeHintInBytes, minimumWorkerAllocationInBytes)); } @@ -256,6 +253,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms var comparer = ImageComparer.TolerantPercentage(allowHigherInaccuracy ? 0.3f : 0.017f); + // Let's make the working buffer size non-default: + provider.Configuration.WorkingBufferSizeHintInBytes = 16 * 1024 * SizeOfVector4; + provider.RunValidatingProcessorTest( ctx => { From 1fd135f62bb833e5524e7a4740d04eca2b847777 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Fri, 19 Apr 2019 21:42:39 +0200 Subject: [PATCH 24/37] ResizeTests.LargeImage --- .../Transforms/Resize/ResizeWorker.cs | 7 ++++--- .../Processors/Transforms/ResizeTests.cs | 17 ++++++++++++++++- tests/Images/External | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index ce28a00ee..9282c2128 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -114,7 +114,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // Ensure offsets are normalized for cropping and padding. ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - this.targetOrigin.Y); - while (kernel.StartIndex + kernel.Length > this.currentWindow.Max) + if (kernel.StartIndex + kernel.Length > this.currentWindow.Max) { this.Slide(); } @@ -145,8 +145,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private void Slide() { - int minY = this.currentWindow.Min + this.windowBandHeight; - int maxY = Math.Min(this.currentWindow.Max + this.windowBandHeight, this.sourceRectangle.Height); + int minY = this.currentWindow.Max - this.windowBandHeight; + int maxY = Math.Min(minY + this.workerHeight, this.sourceRectangle.Height); + this.currentWindow = new RowInterval(minY, maxY); this.CalculateFirstPassValues(this.currentWindow); } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 6d02b4b45..f4098aaf7 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -34,12 +34,27 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms public static readonly string[] SmokeTestResamplerNames = { - nameof(KnownResamplers.NearestNeighbor), nameof(KnownResamplers.Bicubic), nameof(KnownResamplers.Box), + nameof(KnownResamplers.NearestNeighbor), + nameof(KnownResamplers.Bicubic), + nameof(KnownResamplers.Box), nameof(KnownResamplers.Lanczos5), }; private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.07F); + [Theory] + [WithTestPatternImages(4000, 4000, PixelTypes.Rgba32)] + public void LargeImage(TestImageProvider provider) + where TPixel : struct, IPixel + { + if (!TestEnvironment.Is64BitProcess) + { + return; + } + + provider.RunValidatingProcessorTest(x => x.Resize(300, 300), appendPixelTypeToFileName: false); + } + [Theory] [WithBasicTestPatternImages(15, 12, PixelTypes.Rgba32, 2, 3, 1, 2)] [WithBasicTestPatternImages(2, 256, PixelTypes.Rgba32, 1, 1, 1, 8)] diff --git a/tests/Images/External b/tests/Images/External index 6db7ed95d..8693e2fd4 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 6db7ed95dd8a2de4caa6d885952b6689d026ad27 +Subproject commit 8693e2fd4577a9ac1a749da8db564095b5a05389 From f6c778442d1210c7bdc93221eeb52ee804313275 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Fri, 19 Apr 2019 22:52:14 +0200 Subject: [PATCH 25/37] optimized sliding works! --- src/ImageSharp/Memory/Buffer2DExtensions.cs | 28 ++++++++++++++++--- .../Transforms/Resize/ResizeWorker.cs | 17 ++++++++--- .../ImageSharp.Tests/Memory/Buffer2DTests.cs | 23 +++++++++++++++ 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Memory/Buffer2DExtensions.cs b/src/ImageSharp/Memory/Buffer2DExtensions.cs index c8bbf01c7..61fcb99db 100644 --- a/src/ImageSharp/Memory/Buffer2DExtensions.cs +++ b/src/ImageSharp/Memory/Buffer2DExtensions.cs @@ -3,7 +3,9 @@ using System; using System.Buffers; +using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.Primitives; @@ -27,8 +29,8 @@ namespace SixLabors.ImageSharp.Memory { DebugGuard.NotNull(buffer, nameof(buffer)); DebugGuard.MustBeGreaterThanOrEqualTo(sourceIndex, 0, nameof(sourceIndex)); - DebugGuard.MustBeGreaterThanOrEqualTo(destIndex, sourceIndex + columnCount, nameof(destIndex)); - DebugGuard.MustBeLessThanOrEqualTo(destIndex, buffer.Width - columnCount, nameof(destIndex)); + DebugGuard.MustBeGreaterThanOrEqualTo(destIndex, 0, nameof(sourceIndex)); + CheckColumnRegionsDoNotOverlap(buffer, sourceIndex, destIndex, columnCount); int elementSize = Unsafe.SizeOf(); int width = buffer.Width * elementSize; @@ -36,9 +38,11 @@ namespace SixLabors.ImageSharp.Memory int dOffset = destIndex * elementSize; long count = columnCount * elementSize; - using (MemoryHandle handle = buffer.Memory.Pin()) + Span span = MemoryMarshal.AsBytes(buffer.Memory.Span); + + fixed (byte* ptr = span) { - byte* basePtr = (byte*)handle.Pointer; + byte* basePtr = (byte*)ptr; for (int y = 0; y < buffer.Height; y++) { byte* sPtr = basePtr + sOffset; @@ -164,5 +168,21 @@ namespace SixLabors.ImageSharp.Memory { return buffer.MemorySource.GetSpan(); } + + [Conditional("DEBUG")] + private static void CheckColumnRegionsDoNotOverlap( + Buffer2D buffer, + int sourceIndex, + int destIndex, + int columnCount) + where T : struct + { + int minIndex = Math.Min(sourceIndex, destIndex); + int maxIndex = Math.Max(sourceIndex, destIndex); + if (maxIndex < minIndex + columnCount || maxIndex > buffer.Width - columnCount) + { + throw new InvalidOperationException("Column regions should not overlap!"); + } + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index 9282c2128..ed505e221 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -148,14 +148,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int minY = this.currentWindow.Max - this.windowBandHeight; int maxY = Math.Min(minY + this.workerHeight, this.sourceRectangle.Height); + // Copy previous bottom band to the new top: + // (rows <--> columns, because the buffer is transposed) + this.transposedFirstPassBuffer.CopyColumns( + this.workerHeight - this.windowBandHeight, + 0, + this.windowBandHeight); + this.currentWindow = new RowInterval(minY, maxY); - this.CalculateFirstPassValues(this.currentWindow); + + // Calculate the remainder: + this.CalculateFirstPassValues(this.currentWindow.Slice(this.windowBandHeight)); } - private void CalculateFirstPassValues(RowInterval window) + private void CalculateFirstPassValues(RowInterval calculationInterval) { Span tempRowSpan = this.tempRowBuffer.GetSpan(); - for (int y = window.Min; y < window.Max; y++) + for (int y = calculationInterval.Min; y < calculationInterval.Max; y++) { Span sourceRow = this.source.GetRowSpan(y); @@ -166,7 +175,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.conversionModifiers); // ref Vector4 firstPassBaseRef = ref this.buffer.Span[y - top]; - Span firstPassSpan = this.transposedFirstPassBuffer.Span.Slice(y - window.Min); + Span firstPassSpan = this.transposedFirstPassBuffer.Span.Slice(y - this.currentWindow.Min); for (int x = this.targetWorkingRect.Left; x < this.targetWorkingRect.Right; x++) { diff --git a/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs b/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs index 3d4087546..4af3b81e2 100644 --- a/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs +++ b/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs @@ -155,5 +155,28 @@ namespace SixLabors.ImageSharp.Tests.Memory } } } + + [Fact] + public void CopyColumns_InvokeMultipleTimes() + { + Random rnd = new Random(123); + using (Buffer2D b = this.MemoryAllocator.Allocate2D(100, 100)) + { + rnd.RandomFill(b.Span, 0, 1); + + b.CopyColumns(0, 50, 22); + b.CopyColumns(0, 50, 22); + + for (int y = 0; y < b.Height; y++) + { + Span row = b.GetRowSpan(y); + + Span s = row.Slice(0, 22); + Span d = row.Slice(50, 22); + + Xunit.Assert.True(s.SequenceEqual(d)); + } + } + } } } \ No newline at end of file From f7e0a94e1d1a81c3c7494ed42d6df12dc4bc38ca Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Fri, 19 Apr 2019 23:02:10 +0200 Subject: [PATCH 26/37] reapply unsafe optimizations --- .../Processors/Transforms/Resize/ResizeKernel.cs | 8 ++++---- .../Processors/Transforms/Resize/ResizeWorker.cs | 13 +++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs index 71cba9056..dce4e70d6 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs @@ -55,11 +55,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms [MethodImpl(InliningOptions.ShortMethod)] public Vector4 Convolve(Span rowSpan) { - return this.ConvolveCore(rowSpan.Slice(this.StartIndex)); + return this.ConvolveCore(ref rowSpan[this.StartIndex]); } [MethodImpl(InliningOptions.ShortMethod)] - public Vector4 ConvolveCore(Span offsetedRowSpan) + public Vector4 ConvolveCore(ref Vector4 rowStartRef) { ref float horizontalValues = ref Unsafe.AsRef(this.bufferPtr); @@ -70,8 +70,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { float weight = Unsafe.Add(ref horizontalValues, i); - // Vector4 v = Unsafe.Add(ref rowStartRef, i); - Vector4 v = offsetedRowSpan[i]; + // Vector4 v = offsetedRowSpan[i]; + Vector4 v = Unsafe.Add(ref rowStartRef, i); result += v * weight; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index ed505e221..9b7d1d17c 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -125,10 +125,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int x = 0; x < this.destWidth; x++) { - Span firstPassColumn = this.GetColumnSpan(x).Slice(top); + // Span firstPassColumn = this.GetColumnSpan(x).Slice(top); + ref Vector4 firstPassColumnBase = ref this.GetColumnSpan(x)[top]; // Destination color components - Unsafe.Add(ref tempRowBase, x) = kernel.ConvolveCore(firstPassColumn); + Unsafe.Add(ref tempRowBase, x) = kernel.ConvolveCore(ref firstPassColumnBase); } Span targetRowSpan = destination.GetRowSpan(y); @@ -174,15 +175,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms tempRowSpan, this.conversionModifiers); - // ref Vector4 firstPassBaseRef = ref this.buffer.Span[y - top]; - Span firstPassSpan = this.transposedFirstPassBuffer.Span.Slice(y - this.currentWindow.Min); + // Span firstPassSpan = this.transposedFirstPassBuffer.Span.Slice(y - this.currentWindow.Min); + ref Vector4 firstPassBaseRef = ref this.transposedFirstPassBuffer.Span[y - this.currentWindow.Min]; for (int x = this.targetWorkingRect.Left; x < this.targetWorkingRect.Right; x++) { ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - this.targetOrigin.X); - firstPassSpan[x * this.workerHeight] = kernel.Convolve(tempRowSpan); - // Unsafe.Add(ref firstPassBaseRef, x * this.sourceRectangle.Height) = kernel.Convolve(tempRowSpan); + // firstPassSpan[x * this.workerHeight] = kernel.Convolve(tempRowSpan); + Unsafe.Add(ref firstPassBaseRef, x * this.workerHeight) = kernel.Convolve(tempRowSpan); } } } From 2168071e703a9b26f16161061e99eb7fa4cfc764 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Fri, 19 Apr 2019 23:07:32 +0200 Subject: [PATCH 27/37] moar unsafe optimization --- .../Processing/Processors/Transforms/Resize/ResizeWorker.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index 9b7d1d17c..7fe263880 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -123,10 +123,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int top = kernel.StartIndex - this.currentWindow.Min; + ref Vector4 fpBase = ref this.transposedFirstPassBuffer.Span[top]; + for (int x = 0; x < this.destWidth; x++) { // Span firstPassColumn = this.GetColumnSpan(x).Slice(top); - ref Vector4 firstPassColumnBase = ref this.GetColumnSpan(x)[top]; + // ref Vector4 firstPassColumnBase = ref this.GetColumnSpan(x)[top]; + ref Vector4 firstPassColumnBase = ref Unsafe.Add(ref fpBase, x * this.workerHeight); // Destination color components Unsafe.Add(ref tempRowBase, x) = kernel.ConvolveCore(ref firstPassColumnBase); From a7c5295107f66d36bc01a8b57127ad19ddb90a10 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sat, 20 Apr 2019 00:08:29 +0200 Subject: [PATCH 28/37] benchmark WorkingBufferSizeHint effects --- .../ImageSharp.Benchmarks/Samplers/Resize.cs | 61 +++++++++++++------ .../Processors/Transforms/ResizeTests.cs | 18 ++++-- 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/tests/ImageSharp.Benchmarks/Samplers/Resize.cs b/tests/ImageSharp.Benchmarks/Samplers/Resize.cs index 51f3a5653..cb2481dbf 100644 --- a/tests/ImageSharp.Benchmarks/Samplers/Resize.cs +++ b/tests/ImageSharp.Benchmarks/Samplers/Resize.cs @@ -4,6 +4,7 @@ using System; using System.Drawing; using System.Drawing.Drawing2D; +using System.Globalization; using BenchmarkDotNet.Attributes; @@ -23,15 +24,21 @@ namespace SixLabors.ImageSharp.Benchmarks private Bitmap sourceBitmap; - [Params(3032)] - public int SourceSize { get; set; } + [Params("3032-400")] + public virtual string SourceToDest { get; set; } + + protected int SourceSize { get; private set; } + + protected int DestSize { get; private set; } - [Params(400)] - public int DestSize { get; set; } [GlobalSetup] - public void Setup() + public virtual void Setup() { + string[] stuff = this.SourceToDest.Split('-'); + this.SourceSize = int.Parse(stuff[0], CultureInfo.InvariantCulture); + this.DestSize = int.Parse(stuff[1], CultureInfo.InvariantCulture); + this.sourceImage = new Image(this.Configuration, this.SourceSize, this.SourceSize); this.sourceBitmap = new Bitmap(this.SourceSize, this.SourceSize); } @@ -95,26 +102,44 @@ namespace SixLabors.ImageSharp.Benchmarks ctx.Resize(this.DestSize, this.DestSize, KnownResamplers.Bicubic); } - // RESULTS (2019 April): + // RESULTS - 2019 April - ResizeWorker: // - // BenchmarkDotNet=v0.11.3, OS=Windows 10.0.17134.648 (1803/April2018Update/Redstone4) + // BenchmarkDotNet=v0.11.3, OS=Windows 10.0.17134.706 (1803/April2018Update/Redstone4) // Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores - // Frequency=2742192 Hz, Resolution=364.6718 ns, Timer=TSC - // .NET Core SDK=2.1.602 + // Frequency=2742189 Hz, Resolution=364.6722 ns, Timer=TSC + // .NET Core SDK=2.2.202 // [Host] : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT - // Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3362.0 + // Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3394.0 // Core : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT // - // IterationCount=3 LaunchCount=1 WarmupCount=3 + // IterationCount=3 LaunchCount=1 WarmupCount=3 // - // Method | Job | Runtime | SourceSize | DestSize | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - // ----------------------------------------- |----- |-------- |----------- |--------- |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - // SystemDrawing | Clr | Clr | 3032 | 400 | 118.71 ms | 4.884 ms | 0.2677 ms | 1.00 | - | - | - | 2048 B | - // 'ImageSharp, MaxDegreeOfParallelism = 1' | Clr | Clr | 3032 | 400 | 94.55 ms | 16.160 ms | 0.8858 ms | 0.80 | - | - | - | 16384 B | - // | | | | | | | | | | | | | - // SystemDrawing | Core | Core | 3032 | 400 | 118.38 ms | 2.814 ms | 0.1542 ms | 1.00 | - | - | - | 96 B | - // 'ImageSharp, MaxDegreeOfParallelism = 1' | Core | Core | 3032 | 400 | 90.28 ms | 4.679 ms | 0.2565 ms | 0.76 | - | - | - | 15712 B | + // Method | Job | Runtime | SourceToDest | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + // ----------------------------------------- |----- |-------- |------------- |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + // SystemDrawing | Clr | Clr | 3032-400 | 120.11 ms | 1.435 ms | 0.0786 ms | 1.00 | 0.00 | - | - | - | 1638 B | + // 'ImageSharp, MaxDegreeOfParallelism = 1' | Clr | Clr | 3032-400 | 75.32 ms | 34.143 ms | 1.8715 ms | 0.63 | 0.02 | - | - | - | 16384 B | + // | | | | | | | | | | | | | + // SystemDrawing | Core | Core | 3032-400 | 120.33 ms | 6.669 ms | 0.3656 ms | 1.00 | 0.00 | - | - | - | 96 B | + // 'ImageSharp, MaxDegreeOfParallelism = 1' | Core | Core | 3032-400 | 88.56 ms | 1.864 ms | 0.1022 ms | 0.74 | 0.00 | - | - | - | 15568 B | + } + + /// + /// Is it worth to set a larger working buffer limit for resize? + /// Conclusion: It doesn't really have an effect. + /// + public class Resize_Bicubic_Rgba32_CompareWorkBufferSizes : Resize_Bicubic_Rgba32 + { + [Params(128, 512, 1024, 8 * 1024)] + public int WorkingBufferSizeHintInKilobytes { get; set; } + + [Params("3032-400", "4000-300")] + public override string SourceToDest { get; set; } + public override void Setup() + { + this.Configuration.WorkingBufferSizeHintInBytes = this.WorkingBufferSizeHintInKilobytes * 1024; + base.Setup(); + } } public class Resize_Bicubic_Bgra32 : ResizeBenchmarkBase diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index f4098aaf7..6bc2321de 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -42,9 +42,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.07F); - [Theory] - [WithTestPatternImages(4000, 4000, PixelTypes.Rgba32)] - public void LargeImage(TestImageProvider provider) + [Theory( + Skip = "Debug only, enable manually" + )] + [WithTestPatternImages(4000, 4000, PixelTypes.Rgba32, 300, 1024)] + [WithTestPatternImages(3032, 3032, PixelTypes.Rgba32, 400, 1024)] + [WithTestPatternImages(3032, 3032, PixelTypes.Rgba32, 400, 128)] + public void LargeImage(TestImageProvider provider, int destSize, int workingBufferSizeHintInKilobytes) where TPixel : struct, IPixel { if (!TestEnvironment.Is64BitProcess) @@ -52,7 +56,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms return; } - provider.RunValidatingProcessorTest(x => x.Resize(300, 300), appendPixelTypeToFileName: false); + provider.Configuration.WorkingBufferSizeHintInBytes = workingBufferSizeHintInKilobytes * 1024; + + using (var image = provider.GetImage()) + { + image.Mutate(x => x.Resize(destSize, destSize)); + image.DebugSave(provider, appendPixelTypeToFileName: false); + } } [Theory] From d882d911ef3b72176495633ee8925c10d66e1096 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sat, 20 Apr 2019 00:36:26 +0200 Subject: [PATCH 29/37] memory profiling with Sandbox46 --- tests/ImageSharp.Sandbox46/ImageSharp.Sandbox46.csproj | 2 +- tests/ImageSharp.Sandbox46/Program.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ImageSharp.Sandbox46/ImageSharp.Sandbox46.csproj b/tests/ImageSharp.Sandbox46/ImageSharp.Sandbox46.csproj index cb286cc28..6569dc002 100644 --- a/tests/ImageSharp.Sandbox46/ImageSharp.Sandbox46.csproj +++ b/tests/ImageSharp.Sandbox46/ImageSharp.Sandbox46.csproj @@ -1,7 +1,7 @@  Exe - net461 + net472 win7-x64 True false diff --git a/tests/ImageSharp.Sandbox46/Program.cs b/tests/ImageSharp.Sandbox46/Program.cs index 02d4f80c5..afe7eb04f 100644 --- a/tests/ImageSharp.Sandbox46/Program.cs +++ b/tests/ImageSharp.Sandbox46/Program.cs @@ -33,11 +33,11 @@ namespace SixLabors.ImageSharp.Sandbox46 /// public static void Main(string[] args) { - RunJpegColorProfilingTests(); + // RunJpegColorProfilingTests(); // RunDecodeJpegProfilingTests(); // RunToVector4ProfilingTest(); - // RunResizeProfilingTest(); + RunResizeProfilingTest(); Console.ReadLine(); } @@ -51,7 +51,7 @@ namespace SixLabors.ImageSharp.Sandbox46 private static void RunResizeProfilingTest() { var test = new ResizeProfilingBenchmarks(new ConsoleOutput()); - test.ResizeBicubic(2000, 2000); + test.ResizeBicubic(4000, 4000); } private static void RunToVector4ProfilingTest() From 0c684abc7e9187eaff9d52057d3a18bbfc46c274 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sat, 20 Apr 2019 01:21:15 +0200 Subject: [PATCH 30/37] add ResizeWorker.pptx --- .../Transforms/Resize/The ResizeWorker.pptx | Bin 0 -> 38674 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Transforms/Resize/The ResizeWorker.pptx diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/The ResizeWorker.pptx b/src/ImageSharp/Processing/Processors/Transforms/Resize/The ResizeWorker.pptx new file mode 100644 index 0000000000000000000000000000000000000000..063f72de84022037dcd5d44331b3fce6b12efe3c GIT binary patch literal 38674 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 Date: Sat, 20 Apr 2019 01:46:31 +0200 Subject: [PATCH 31/37] refine ResizeWorker.pptx --- ...he ResizeWorker.pptx => ResizeWorker.pptx} | Bin 38674 -> 45342 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename src/ImageSharp/Processing/Processors/Transforms/Resize/{The ResizeWorker.pptx => ResizeWorker.pptx} (59%) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/The ResizeWorker.pptx b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.pptx similarity index 59% rename from src/ImageSharp/Processing/Processors/Transforms/Resize/The ResizeWorker.pptx rename to src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.pptx index 063f72de84022037dcd5d44331b3fce6b12efe3c..66087ce5423736e5bcd615fd68cc75f0024778f8 100644 GIT binary patch delta 14372 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 M?{9MYkAF%32jo|;O;WGhM>VExE&;r;1(R_ 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 Date: Sat, 20 Apr 2019 02:15:38 +0200 Subject: [PATCH 32/37] xmldoc for ResizeWorker --- src/ImageSharp/Configuration.cs | 3 +-- .../Processors/Transforms/Resize/ResizeWorker.cs | 15 +++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 7cb014563..4e8284c2c 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -10,7 +10,6 @@ using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.Memory; namespace SixLabors.ImageSharp @@ -108,7 +107,7 @@ namespace SixLabors.ImageSharp /// The default value is 1MB. /// /// - /// Currently only used by . + /// Currently only used by Resize. /// internal int WorkingBufferSizeHintInBytes { get; set; } = 1 * 1024 * 1024; diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index 7fe263880..00a8cfbf3 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -14,6 +14,13 @@ using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Transforms { + ///

+ /// Implements the resize algorithm using a sliding window of size + /// maximized by . + /// The height of the window is a multiple of the vertical kernel's maximum diameter. + /// When sliding the window, the contents of the bottom window band are copied to the new top band. + /// For more details, and visual explanation, see "ResizeWorker.pptx". + /// internal class ResizeWorker : IDisposable where TPixel : struct, IPixel { @@ -127,8 +134,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int x = 0; x < this.destWidth; x++) { - // Span firstPassColumn = this.GetColumnSpan(x).Slice(top); - // ref Vector4 firstPassColumnBase = ref this.GetColumnSpan(x)[top]; ref Vector4 firstPassColumnBase = ref Unsafe.Add(ref fpBase, x * this.workerHeight); // Destination color components @@ -141,12 +146,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Span GetColumnSpan(int x) - { - return this.transposedFirstPassBuffer.GetRowSpan(x); - } - private void Slide() { int minY = this.currentWindow.Max - this.windowBandHeight; From d2e1a8d14c3e411c23f5a9cbec5ae0d968023b78 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sat, 20 Apr 2019 02:48:20 +0200 Subject: [PATCH 33/37] update ResizeWorker.pptx [skip CI] --- .../Transforms/Resize/ResizeWorker.pptx | Bin 45342 -> 45638 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.pptx b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.pptx index 66087ce5423736e5bcd615fd68cc75f0024778f8..248959170b01291490b7ab8c89dc646d3c2cb936 100644 GIT binary patch delta 5686 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 delta 5364 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 Date: Sat, 20 Apr 2019 13:17:54 +0200 Subject: [PATCH 34/37] fix tests --- .../Processing/Processors/Convolution/DetectEdgesTest.cs | 1 + .../Processing/Processors/Transforms/ResizeTests.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs index de72f6d09..edb24d6f1 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs @@ -42,6 +42,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution var bounds = new Rectangle(10, 10, size.Width / 2, size.Height / 2); ctx.DetectEdges(bounds); }, + comparer: ValidatorComparer, useReferenceOutputFrom: nameof(this.DetectEdges_InBox)); } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 6bc2321de..e3a43a652 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -131,6 +131,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms testOutputDetails: workingBufferLimitInRows, appendPixelTypeToFileName: false); image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.001f), provider, testOutputDetails: workingBufferLimitInRows, appendPixelTypeToFileName: false); From 026df1e4c88dec0512f8f0670fa2dee5ae59cd9e Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sat, 20 Apr 2019 13:21:15 +0200 Subject: [PATCH 35/37] fix license text in CopyBuffers benchmark --- tests/ImageSharp.Benchmarks/General/CopyBuffers.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/ImageSharp.Benchmarks/General/CopyBuffers.cs b/tests/ImageSharp.Benchmarks/General/CopyBuffers.cs index 117cdba41..4c6458c30 100644 --- a/tests/ImageSharp.Benchmarks/General/CopyBuffers.cs +++ b/tests/ImageSharp.Benchmarks/General/CopyBuffers.cs @@ -1,7 +1,5 @@ -// -// Copyright (c) James Jackson-South and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -// using System; using System.Buffers; From 143033660dc202e0764aedf13d0c71a82ea33fd3 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 22 Apr 2019 14:22:04 +0200 Subject: [PATCH 36/37] extend the CopyBuffers benchmark --- .../General/CopyBuffers.cs | 199 +++++++++++------- 1 file changed, 124 insertions(+), 75 deletions(-) diff --git a/tests/ImageSharp.Benchmarks/General/CopyBuffers.cs b/tests/ImageSharp.Benchmarks/General/CopyBuffers.cs index 4c6458c30..32f1d10c7 100644 --- a/tests/ImageSharp.Benchmarks/General/CopyBuffers.cs +++ b/tests/ImageSharp.Benchmarks/General/CopyBuffers.cs @@ -88,12 +88,32 @@ namespace SixLabors.ImageSharp.Benchmarks.General this.sourceMemory.Span.CopyTo(this.destMemory.Span); } - [Benchmark(Description = "Unsafe.CopyBlock()")] - public unsafe void UnsafeCopyBlock() + [Benchmark(Description = "Unsafe.CopyBlock(ref)")] + public unsafe void UnsafeCopyBlockReferences() + { + Unsafe.CopyBlock(ref this.destArray[0], ref this.sourceArray[0], (uint)this.Count); + } + + [Benchmark(Description = "Unsafe.CopyBlock(ptr)")] + public unsafe void UnsafeCopyBlockPointers() + { + void* pinnedDestination = this.destHandle.Pointer; + void* pinnedSource = this.sourceHandle.Pointer; + Unsafe.CopyBlock(pinnedDestination, pinnedSource, (uint)this.Count); + } + + [Benchmark(Description = "Unsafe.CopyBlockUnaligned(ref)")] + public unsafe void UnsafeCopyBlockUnalignedReferences() + { + Unsafe.CopyBlockUnaligned(ref this.destArray[0], ref this.sourceArray[0], (uint)this.Count); + } + + [Benchmark(Description = "Unsafe.CopyBlockUnaligned(ptr)")] + public unsafe void UnsafeCopyBlockUnalignedPointers() { void* pinnedDestination = this.destHandle.Pointer; void* pinnedSource = this.sourceHandle.Pointer; - Unsafe.CopyBlock(pinnedSource, pinnedDestination, (uint)this.Count); + Unsafe.CopyBlockUnaligned(pinnedDestination, pinnedSource, (uint)this.Count); } // BenchmarkDotNet=v0.11.3, OS=Windows 10.0.17134.706 (1803/April2018Update/Redstone4) @@ -106,77 +126,106 @@ namespace SixLabors.ImageSharp.Benchmarks.General // // IterationCount=3 LaunchCount=1 WarmupCount=3 // - // Method | Job | Runtime | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - // -------------------- |----- |-------- |------ |-----------:|-----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - // Array.Copy() | Clr | Clr | 10 | 23.579 ns | 1.6836 ns | 0.0923 ns | 1.00 | 0.00 | - | - | - | - | - // Buffer.BlockCopy() | Clr | Clr | 10 | 11.796 ns | 0.5280 ns | 0.0289 ns | 0.50 | 0.00 | - | - | - | - | - // Buffer.MemoryCopy() | Clr | Clr | 10 | 3.206 ns | 8.1741 ns | 0.4480 ns | 0.14 | 0.02 | - | - | - | - | - // Marshal.Copy() | Clr | Clr | 10 | 15.577 ns | 2.0937 ns | 0.1148 ns | 0.66 | 0.00 | - | - | - | - | - // Span.CopyTo() | Clr | Clr | 10 | 32.287 ns | 2.4107 ns | 0.1321 ns | 1.37 | 0.01 | - | - | - | - | - // Unsafe.CopyBlock() | Clr | Clr | 10 | 3.266 ns | 0.3848 ns | 0.0211 ns | 0.14 | 0.00 | - | - | - | - | - // | | | | | | | | | | | | | - // Array.Copy() | Core | Core | 10 | 19.713 ns | 7.3026 ns | 0.4003 ns | 1.00 | 0.00 | - | - | - | - | - // Buffer.BlockCopy() | Core | Core | 10 | 7.332 ns | 0.5465 ns | 0.0300 ns | 0.37 | 0.01 | - | - | - | - | - // Buffer.MemoryCopy() | Core | Core | 10 | 2.476 ns | 0.3476 ns | 0.0191 ns | 0.13 | 0.00 | - | - | - | - | - // Marshal.Copy() | Core | Core | 10 | 15.575 ns | 0.1335 ns | 0.0073 ns | 0.79 | 0.02 | - | - | - | - | - // Span.CopyTo() | Core | Core | 10 | 25.321 ns | 2.3556 ns | 0.1291 ns | 1.28 | 0.02 | - | - | - | - | - // Unsafe.CopyBlock() | Core | Core | 10 | 2.204 ns | 0.1836 ns | 0.0101 ns | 0.11 | 0.00 | - | - | - | - | - // | | | | | | | | | | | | | - // Array.Copy() | Clr | Clr | 50 | 35.217 ns | 2.7642 ns | 0.1515 ns | 1.00 | 0.00 | - | - | - | - | - // Buffer.BlockCopy() | Clr | Clr | 50 | 24.224 ns | 0.8737 ns | 0.0479 ns | 0.69 | 0.00 | - | - | - | - | - // Buffer.MemoryCopy() | Clr | Clr | 50 | 3.827 ns | 4.8733 ns | 0.2671 ns | 0.11 | 0.01 | - | - | - | - | - // Marshal.Copy() | Clr | Clr | 50 | 28.103 ns | 1.3570 ns | 0.0744 ns | 0.80 | 0.00 | - | - | - | - | - // Span.CopyTo() | Clr | Clr | 50 | 34.137 ns | 2.9274 ns | 0.1605 ns | 0.97 | 0.01 | - | - | - | - | - // Unsafe.CopyBlock() | Clr | Clr | 50 | 4.999 ns | 0.1778 ns | 0.0097 ns | 0.14 | 0.00 | - | - | - | - | - // | | | | | | | | | | | | | - // Array.Copy() | Core | Core | 50 | 20.925 ns | 1.0219 ns | 0.0560 ns | 1.00 | 0.00 | - | - | - | - | - // Buffer.BlockCopy() | Core | Core | 50 | 8.083 ns | 0.2158 ns | 0.0118 ns | 0.39 | 0.00 | - | - | - | - | - // Buffer.MemoryCopy() | Core | Core | 50 | 2.919 ns | 0.2878 ns | 0.0158 ns | 0.14 | 0.00 | - | - | - | - | - // Marshal.Copy() | Core | Core | 50 | 16.663 ns | 0.2505 ns | 0.0137 ns | 0.80 | 0.00 | - | - | - | - | - // Span.CopyTo() | Core | Core | 50 | 26.940 ns | 11.5855 ns | 0.6350 ns | 1.29 | 0.03 | - | - | - | - | - // Unsafe.CopyBlock() | Core | Core | 50 | 1.940 ns | 0.6327 ns | 0.0347 ns | 0.09 | 0.00 | - | - | - | - | - // | | | | | | | | | | | | | - // Array.Copy() | Clr | Clr | 100 | 39.284 ns | 0.5647 ns | 0.0310 ns | 1.00 | 0.00 | - | - | - | - | - // Buffer.BlockCopy() | Clr | Clr | 100 | 28.930 ns | 0.6774 ns | 0.0371 ns | 0.74 | 0.00 | - | - | - | - | - // Buffer.MemoryCopy() | Clr | Clr | 100 | 5.859 ns | 2.7931 ns | 0.1531 ns | 0.15 | 0.00 | - | - | - | - | - // Marshal.Copy() | Clr | Clr | 100 | 36.529 ns | 0.9886 ns | 0.0542 ns | 0.93 | 0.00 | - | - | - | - | - // Span.CopyTo() | Clr | Clr | 100 | 36.152 ns | 1.5109 ns | 0.0828 ns | 0.92 | 0.00 | - | - | - | - | - // Unsafe.CopyBlock() | Clr | Clr | 100 | 9.317 ns | 0.4342 ns | 0.0238 ns | 0.24 | 0.00 | - | - | - | - | - // | | | | | | | | | | | | | - // Array.Copy() | Core | Core | 100 | 22.899 ns | 8.4066 ns | 0.4608 ns | 1.00 | 0.00 | - | - | - | - | - // Buffer.BlockCopy() | Core | Core | 100 | 10.696 ns | 0.8106 ns | 0.0444 ns | 0.47 | 0.01 | - | - | - | - | - // Buffer.MemoryCopy() | Core | Core | 100 | 4.102 ns | 0.9040 ns | 0.0496 ns | 0.18 | 0.01 | - | - | - | - | - // Marshal.Copy() | Core | Core | 100 | 17.917 ns | 2.6490 ns | 0.1452 ns | 0.78 | 0.01 | - | - | - | - | - // Span.CopyTo() | Core | Core | 100 | 28.247 ns | 0.6375 ns | 0.0349 ns | 1.23 | 0.03 | - | - | - | - | - // Unsafe.CopyBlock() | Core | Core | 100 | 3.611 ns | 0.4792 ns | 0.0263 ns | 0.16 | 0.00 | - | - | - | - | - // | | | | | | | | | | | | | - // Array.Copy() | Clr | Clr | 1000 | 48.907 ns | 4.4228 ns | 0.2424 ns | 1.00 | 0.00 | - | - | - | - | - // Buffer.BlockCopy() | Clr | Clr | 1000 | 40.653 ns | 1.4055 ns | 0.0770 ns | 0.83 | 0.01 | - | - | - | - | - // Buffer.MemoryCopy() | Clr | Clr | 1000 | 24.720 ns | 1.2651 ns | 0.0693 ns | 0.51 | 0.00 | - | - | - | - | - // Marshal.Copy() | Clr | Clr | 1000 | 42.336 ns | 2.2466 ns | 0.1231 ns | 0.87 | 0.00 | - | - | - | - | - // Span.CopyTo() | Clr | Clr | 1000 | 70.735 ns | 2.6215 ns | 0.1437 ns | 1.45 | 0.01 | - | - | - | - | - // Unsafe.CopyBlock() | Clr | Clr | 1000 | 44.520 ns | 0.9641 ns | 0.0528 ns | 0.91 | 0.00 | - | - | - | - | - // | | | | | | | | | | | | | - // Array.Copy() | Core | Core | 1000 | 46.286 ns | 11.6373 ns | 0.6379 ns | 1.00 | 0.00 | - | - | - | - | - // Buffer.BlockCopy() | Core | Core | 1000 | 34.243 ns | 7.2264 ns | 0.3961 ns | 0.74 | 0.01 | - | - | - | - | - // Buffer.MemoryCopy() | Core | Core | 1000 | 23.135 ns | 0.3153 ns | 0.0173 ns | 0.50 | 0.01 | - | - | - | - | - // Marshal.Copy() | Core | Core | 1000 | 46.219 ns | 1.2869 ns | 0.0705 ns | 1.00 | 0.01 | - | - | - | - | - // Span.CopyTo() | Core | Core | 1000 | 45.371 ns | 3.3581 ns | 0.1841 ns | 0.98 | 0.02 | - | - | - | - | - // Unsafe.CopyBlock() | Core | Core | 1000 | 29.347 ns | 1.1349 ns | 0.0622 ns | 0.63 | 0.01 | - | - | - | - | - // | | | | | | | | | | | | | - // Array.Copy() | Clr | Clr | 10000 | 218.445 ns | 9.2567 ns | 0.5074 ns | 1.00 | 0.00 | - | - | - | - | - // Buffer.BlockCopy() | Clr | Clr | 10000 | 209.610 ns | 6.7447 ns | 0.3697 ns | 0.96 | 0.00 | - | - | - | - | - // Buffer.MemoryCopy() | Clr | Clr | 10000 | 213.061 ns | 66.6490 ns | 3.6533 ns | 0.98 | 0.02 | - | - | - | - | - // Marshal.Copy() | Clr | Clr | 10000 | 214.426 ns | 27.7722 ns | 1.5223 ns | 0.98 | 0.00 | - | - | - | - | - // Span.CopyTo() | Clr | Clr | 10000 | 486.728 ns | 12.1537 ns | 0.6662 ns | 2.23 | 0.00 | - | - | - | - | - // Unsafe.CopyBlock() | Clr | Clr | 10000 | 452.973 ns | 25.1490 ns | 1.3785 ns | 2.07 | 0.00 | - | - | - | - | - // | | | | | | | | | | | | | - // Array.Copy() | Core | Core | 10000 | 203.365 ns | 3.2200 ns | 0.1765 ns | 1.00 | 0.00 | - | - | - | - | - // Buffer.BlockCopy() | Core | Core | 10000 | 193.319 ns | 8.3370 ns | 0.4570 ns | 0.95 | 0.00 | - | - | - | - | - // Buffer.MemoryCopy() | Core | Core | 10000 | 196.541 ns | 37.8056 ns | 2.0723 ns | 0.97 | 0.01 | - | - | - | - | - // Marshal.Copy() | Core | Core | 10000 | 206.454 ns | 3.7652 ns | 0.2064 ns | 1.02 | 0.00 | - | - | - | - | - // Span.CopyTo() | Core | Core | 10000 | 214.799 ns | 3.0667 ns | 0.1681 ns | 1.06 | 0.00 | - | - | - | - | - // Unsafe.CopyBlock() | Core | Core | 10000 | 134.428 ns | 2.6024 ns | 0.1426 ns | 0.66 | 0.00 | - | - | - | - | - + // | Method | Job | Runtime | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + // |------------------------------- |----- |-------- |------ |-----------:|-----------:|----------:|------:|--------:|------:|------:|------:|----------:| + // | Array.Copy() | Clr | Clr | 10 | 23.636 ns | 2.5299 ns | 0.1387 ns | 1.00 | 0.00 | - | - | - | - | + // | Buffer.BlockCopy() | Clr | Clr | 10 | 11.420 ns | 2.3341 ns | 0.1279 ns | 0.48 | 0.01 | - | - | - | - | + // | Buffer.MemoryCopy() | Clr | Clr | 10 | 2.861 ns | 0.5059 ns | 0.0277 ns | 0.12 | 0.00 | - | - | - | - | + // | Marshal.Copy() | Clr | Clr | 10 | 14.870 ns | 2.4541 ns | 0.1345 ns | 0.63 | 0.01 | - | - | - | - | + // | Span.CopyTo() | Clr | Clr | 10 | 31.906 ns | 1.2213 ns | 0.0669 ns | 1.35 | 0.01 | - | - | - | - | + // | Unsafe.CopyBlock(ref) | Clr | Clr | 10 | 3.513 ns | 0.7392 ns | 0.0405 ns | 0.15 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlock(ptr) | Clr | Clr | 10 | 3.053 ns | 0.2010 ns | 0.0110 ns | 0.13 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ref) | Clr | Clr | 10 | 3.497 ns | 0.4911 ns | 0.0269 ns | 0.15 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ptr) | Clr | Clr | 10 | 3.109 ns | 0.5958 ns | 0.0327 ns | 0.13 | 0.00 | - | - | - | - | + // | | | | | | | | | | | | | | + // | Array.Copy() | Core | Core | 10 | 19.709 ns | 2.1867 ns | 0.1199 ns | 1.00 | 0.00 | - | - | - | - | + // | Buffer.BlockCopy() | Core | Core | 10 | 7.377 ns | 1.1582 ns | 0.0635 ns | 0.37 | 0.01 | - | - | - | - | + // | Buffer.MemoryCopy() | Core | Core | 10 | 2.581 ns | 1.1607 ns | 0.0636 ns | 0.13 | 0.00 | - | - | - | - | + // | Marshal.Copy() | Core | Core | 10 | 15.197 ns | 2.8446 ns | 0.1559 ns | 0.77 | 0.01 | - | - | - | - | + // | Span.CopyTo() | Core | Core | 10 | 25.394 ns | 0.9782 ns | 0.0536 ns | 1.29 | 0.01 | - | - | - | - | + // | Unsafe.CopyBlock(ref) | Core | Core | 10 | 2.254 ns | 0.1590 ns | 0.0087 ns | 0.11 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlock(ptr) | Core | Core | 10 | 1.878 ns | 0.1035 ns | 0.0057 ns | 0.10 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ref) | Core | Core | 10 | 2.263 ns | 0.1383 ns | 0.0076 ns | 0.11 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ptr) | Core | Core | 10 | 1.877 ns | 0.0602 ns | 0.0033 ns | 0.10 | 0.00 | - | - | - | - | + // | | | | | | | | | | | | | | + // | Array.Copy() | Clr | Clr | 50 | 35.068 ns | 5.9137 ns | 0.3242 ns | 1.00 | 0.00 | - | - | - | - | + // | Buffer.BlockCopy() | Clr | Clr | 50 | 23.299 ns | 2.3797 ns | 0.1304 ns | 0.66 | 0.01 | - | - | - | - | + // | Buffer.MemoryCopy() | Clr | Clr | 50 | 3.598 ns | 4.8536 ns | 0.2660 ns | 0.10 | 0.01 | - | - | - | - | + // | Marshal.Copy() | Clr | Clr | 50 | 27.720 ns | 4.6602 ns | 0.2554 ns | 0.79 | 0.01 | - | - | - | - | + // | Span.CopyTo() | Clr | Clr | 50 | 35.673 ns | 16.2972 ns | 0.8933 ns | 1.02 | 0.03 | - | - | - | - | + // | Unsafe.CopyBlock(ref) | Clr | Clr | 50 | 5.534 ns | 2.8119 ns | 0.1541 ns | 0.16 | 0.01 | - | - | - | - | + // | Unsafe.CopyBlock(ptr) | Clr | Clr | 50 | 4.511 ns | 0.9555 ns | 0.0524 ns | 0.13 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ref) | Clr | Clr | 50 | 5.613 ns | 1.6679 ns | 0.0914 ns | 0.16 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ptr) | Clr | Clr | 50 | 4.884 ns | 7.3153 ns | 0.4010 ns | 0.14 | 0.01 | - | - | - | - | + // | | | | | | | | | | | | | | + // | Array.Copy() | Core | Core | 50 | 20.232 ns | 1.5720 ns | 0.0862 ns | 1.00 | 0.00 | - | - | - | - | + // | Buffer.BlockCopy() | Core | Core | 50 | 8.142 ns | 0.7860 ns | 0.0431 ns | 0.40 | 0.00 | - | - | - | - | + // | Buffer.MemoryCopy() | Core | Core | 50 | 2.962 ns | 0.0611 ns | 0.0033 ns | 0.15 | 0.00 | - | - | - | - | + // | Marshal.Copy() | Core | Core | 50 | 16.802 ns | 2.9686 ns | 0.1627 ns | 0.83 | 0.00 | - | - | - | - | + // | Span.CopyTo() | Core | Core | 50 | 26.571 ns | 0.9228 ns | 0.0506 ns | 1.31 | 0.01 | - | - | - | - | + // | Unsafe.CopyBlock(ref) | Core | Core | 50 | 2.219 ns | 0.7191 ns | 0.0394 ns | 0.11 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlock(ptr) | Core | Core | 50 | 1.751 ns | 0.1884 ns | 0.0103 ns | 0.09 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ref) | Core | Core | 50 | 2.177 ns | 0.4489 ns | 0.0246 ns | 0.11 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ptr) | Core | Core | 50 | 1.806 ns | 0.1063 ns | 0.0058 ns | 0.09 | 0.00 | - | - | - | - | + // | | | | | | | | | | | | | | + // | Array.Copy() | Clr | Clr | 100 | 39.158 ns | 4.3068 ns | 0.2361 ns | 1.00 | 0.00 | - | - | - | - | + // | Buffer.BlockCopy() | Clr | Clr | 100 | 27.623 ns | 0.4611 ns | 0.0253 ns | 0.71 | 0.00 | - | - | - | - | + // | Buffer.MemoryCopy() | Clr | Clr | 100 | 5.018 ns | 0.5689 ns | 0.0312 ns | 0.13 | 0.00 | - | - | - | - | + // | Marshal.Copy() | Clr | Clr | 100 | 33.527 ns | 1.9019 ns | 0.1042 ns | 0.86 | 0.01 | - | - | - | - | + // | Span.CopyTo() | Clr | Clr | 100 | 35.604 ns | 2.7039 ns | 0.1482 ns | 0.91 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlock(ref) | Clr | Clr | 100 | 7.853 ns | 0.4925 ns | 0.0270 ns | 0.20 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlock(ptr) | Clr | Clr | 100 | 7.406 ns | 1.9733 ns | 0.1082 ns | 0.19 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ref) | Clr | Clr | 100 | 7.822 ns | 0.6837 ns | 0.0375 ns | 0.20 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ptr) | Clr | Clr | 100 | 7.392 ns | 1.2832 ns | 0.0703 ns | 0.19 | 0.00 | - | - | - | - | + // | | | | | | | | | | | | | | + // | Array.Copy() | Core | Core | 100 | 22.909 ns | 2.9754 ns | 0.1631 ns | 1.00 | 0.00 | - | - | - | - | + // | Buffer.BlockCopy() | Core | Core | 100 | 10.687 ns | 1.1262 ns | 0.0617 ns | 0.47 | 0.00 | - | - | - | - | + // | Buffer.MemoryCopy() | Core | Core | 100 | 4.063 ns | 0.1607 ns | 0.0088 ns | 0.18 | 0.00 | - | - | - | - | + // | Marshal.Copy() | Core | Core | 100 | 18.067 ns | 4.0557 ns | 0.2223 ns | 0.79 | 0.01 | - | - | - | - | + // | Span.CopyTo() | Core | Core | 100 | 28.352 ns | 1.2762 ns | 0.0700 ns | 1.24 | 0.01 | - | - | - | - | + // | Unsafe.CopyBlock(ref) | Core | Core | 100 | 4.130 ns | 0.2013 ns | 0.0110 ns | 0.18 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlock(ptr) | Core | Core | 100 | 4.096 ns | 0.2460 ns | 0.0135 ns | 0.18 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ref) | Core | Core | 100 | 4.160 ns | 0.3174 ns | 0.0174 ns | 0.18 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ptr) | Core | Core | 100 | 3.480 ns | 1.1683 ns | 0.0640 ns | 0.15 | 0.00 | - | - | - | - | + // | | | | | | | | | | | | | | + // | Array.Copy() | Clr | Clr | 1000 | 49.059 ns | 2.0729 ns | 0.1136 ns | 1.00 | 0.00 | - | - | - | - | + // | Buffer.BlockCopy() | Clr | Clr | 1000 | 38.270 ns | 23.6970 ns | 1.2989 ns | 0.78 | 0.03 | - | - | - | - | + // | Buffer.MemoryCopy() | Clr | Clr | 1000 | 27.599 ns | 6.8328 ns | 0.3745 ns | 0.56 | 0.01 | - | - | - | - | + // | Marshal.Copy() | Clr | Clr | 1000 | 42.752 ns | 5.1357 ns | 0.2815 ns | 0.87 | 0.01 | - | - | - | - | + // | Span.CopyTo() | Clr | Clr | 1000 | 69.983 ns | 2.1860 ns | 0.1198 ns | 1.43 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlock(ref) | Clr | Clr | 1000 | 44.822 ns | 0.1625 ns | 0.0089 ns | 0.91 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlock(ptr) | Clr | Clr | 1000 | 45.072 ns | 1.4053 ns | 0.0770 ns | 0.92 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ref) | Clr | Clr | 1000 | 45.306 ns | 5.2646 ns | 0.2886 ns | 0.92 | 0.01 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ptr) | Clr | Clr | 1000 | 44.813 ns | 0.9117 ns | 0.0500 ns | 0.91 | 0.00 | - | - | - | - | + // | | | | | | | | | | | | | | + // | Array.Copy() | Core | Core | 1000 | 51.907 ns | 3.1827 ns | 0.1745 ns | 1.00 | 0.00 | - | - | - | - | + // | Buffer.BlockCopy() | Core | Core | 1000 | 40.700 ns | 3.1488 ns | 0.1726 ns | 0.78 | 0.00 | - | - | - | - | + // | Buffer.MemoryCopy() | Core | Core | 1000 | 23.711 ns | 1.3004 ns | 0.0713 ns | 0.46 | 0.00 | - | - | - | - | + // | Marshal.Copy() | Core | Core | 1000 | 42.586 ns | 2.5390 ns | 0.1392 ns | 0.82 | 0.00 | - | - | - | - | + // | Span.CopyTo() | Core | Core | 1000 | 44.109 ns | 4.5604 ns | 0.2500 ns | 0.85 | 0.01 | - | - | - | - | + // | Unsafe.CopyBlock(ref) | Core | Core | 1000 | 33.926 ns | 5.1633 ns | 0.2830 ns | 0.65 | 0.01 | - | - | - | - | + // | Unsafe.CopyBlock(ptr) | Core | Core | 1000 | 33.267 ns | 0.2708 ns | 0.0148 ns | 0.64 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ref) | Core | Core | 1000 | 34.018 ns | 2.3238 ns | 0.1274 ns | 0.66 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ptr) | Core | Core | 1000 | 33.667 ns | 2.1983 ns | 0.1205 ns | 0.65 | 0.00 | - | - | - | - | + // | | | | | | | | | | | | | | + // | Array.Copy() | Clr | Clr | 10000 | 153.429 ns | 6.1735 ns | 0.3384 ns | 1.00 | 0.00 | - | - | - | - | + // | Buffer.BlockCopy() | Clr | Clr | 10000 | 201.373 ns | 4.3670 ns | 0.2394 ns | 1.31 | 0.00 | - | - | - | - | + // | Buffer.MemoryCopy() | Clr | Clr | 10000 | 211.768 ns | 71.3510 ns | 3.9110 ns | 1.38 | 0.02 | - | - | - | - | + // | Marshal.Copy() | Clr | Clr | 10000 | 215.299 ns | 17.2677 ns | 0.9465 ns | 1.40 | 0.01 | - | - | - | - | + // | Span.CopyTo() | Clr | Clr | 10000 | 486.325 ns | 32.4445 ns | 1.7784 ns | 3.17 | 0.01 | - | - | - | - | + // | Unsafe.CopyBlock(ref) | Clr | Clr | 10000 | 452.314 ns | 33.0593 ns | 1.8121 ns | 2.95 | 0.02 | - | - | - | - | + // | Unsafe.CopyBlock(ptr) | Clr | Clr | 10000 | 455.600 ns | 56.7534 ns | 3.1108 ns | 2.97 | 0.02 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ref) | Clr | Clr | 10000 | 452.279 ns | 8.6457 ns | 0.4739 ns | 2.95 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ptr) | Clr | Clr | 10000 | 453.146 ns | 12.3776 ns | 0.6785 ns | 2.95 | 0.00 | - | - | - | - | + // | | | | | | | | | | | | | | + // | Array.Copy() | Core | Core | 10000 | 204.508 ns | 3.1652 ns | 0.1735 ns | 1.00 | 0.00 | - | - | - | - | + // | Buffer.BlockCopy() | Core | Core | 10000 | 193.345 ns | 1.3742 ns | 0.0753 ns | 0.95 | 0.00 | - | - | - | - | + // | Buffer.MemoryCopy() | Core | Core | 10000 | 196.978 ns | 18.3279 ns | 1.0046 ns | 0.96 | 0.01 | - | - | - | - | + // | Marshal.Copy() | Core | Core | 10000 | 206.878 ns | 6.9938 ns | 0.3834 ns | 1.01 | 0.00 | - | - | - | - | + // | Span.CopyTo() | Core | Core | 10000 | 215.733 ns | 15.4824 ns | 0.8486 ns | 1.05 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlock(ref) | Core | Core | 10000 | 186.894 ns | 8.7617 ns | 0.4803 ns | 0.91 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlock(ptr) | Core | Core | 10000 | 186.662 ns | 10.6059 ns | 0.5813 ns | 0.91 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ref) | Core | Core | 10000 | 187.489 ns | 13.1527 ns | 0.7209 ns | 0.92 | 0.00 | - | - | - | - | + // | Unsafe.CopyBlockUnaligned(ptr) | Core | Core | 10000 | 186.586 ns | 4.6274 ns | 0.2536 ns | 0.91 | 0.00 | - | - | - | - | } } \ No newline at end of file From a359b195f2a6b4c5b0b33796153340a7e8a9eacb Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 22 Apr 2019 15:52:54 +0200 Subject: [PATCH 37/37] use HashCode.Combine() --- src/ImageSharp/Memory/RowInterval.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/ImageSharp/Memory/RowInterval.cs b/src/ImageSharp/Memory/RowInterval.cs index 3ee7ae774..815918754 100644 --- a/src/ImageSharp/Memory/RowInterval.cs +++ b/src/ImageSharp/Memory/RowInterval.cs @@ -65,12 +65,6 @@ namespace SixLabors.ImageSharp.Memory return !ReferenceEquals(null, obj) && obj is RowInterval other && this.Equals(other); } - public override int GetHashCode() - { - unchecked - { - return (this.Min * 397) ^ this.Max; - } - } + public override int GetHashCode() => HashCode.Combine(this.Min, this.Max); } } \ No newline at end of file