diff --git a/.gitattributes b/.gitattributes index 3b7ad7e196..01a3825f8c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -105,8 +105,11 @@ *.pvr binary *.snk binary *.tga binary +*.tif binary +*.tiff binary *.ttc binary *.ttf binary +*.wbmp binary *.webp binary *.woff binary *.woff2 binary diff --git a/src/ImageSharp/Common/Helpers/DenseMatrixUtils.cs b/src/ImageSharp/Common/Helpers/DenseMatrixUtils.cs deleted file mode 100644 index f265bdd517..0000000000 --- a/src/ImageSharp/Common/Helpers/DenseMatrixUtils.cs +++ /dev/null @@ -1,279 +0,0 @@ -// Copyright (c) Six Labors. -// 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; - -namespace SixLabors.ImageSharp -{ - /// - /// Extension methods for . - /// TODO: One day rewrite all this to use SIMD intrinsics. There's a lot of scope for improvement. - /// - internal static class DenseMatrixUtils - { - /// - /// Computes the sum of vectors in the span referenced by weighted by the two kernel weight values. - /// Using this method the convolution filter is not applied to alpha in addition to the color channels. - /// - /// The pixel format. - /// The vertical dense matrix. - /// The horizontal dense matrix. - /// The source frame. - /// The target row base reference. - /// The current row. - /// The current column. - /// The minimum working area row. - /// The maximum working area row. - /// The minimum working area column. - /// The maximum working area column. - [MethodImpl(InliningOptions.ShortMethod)] - public static void Convolve2D3( - in DenseMatrix matrixY, - in DenseMatrix matrixX, - Buffer2D sourcePixels, - ref Vector4 targetRowRef, - int row, - int column, - int minRow, - int maxRow, - int minColumn, - int maxColumn) - where TPixel : unmanaged, IPixel - { - Convolve2DImpl( - in matrixY, - in matrixX, - sourcePixels, - row, - column, - minRow, - maxRow, - minColumn, - maxColumn, - out Vector4 vector); - - ref Vector4 target = ref Unsafe.Add(ref targetRowRef, column); - vector.W = target.W; - - Numerics.UnPremultiply(ref vector); - target = vector; - } - - /// - /// Computes the sum of vectors in the span referenced by weighted by the two kernel weight values. - /// Using this method the convolution filter is applied to alpha in addition to the color channels. - /// - /// The pixel format. - /// The vertical dense matrix. - /// The horizontal dense matrix. - /// The source frame. - /// The target row base reference. - /// The current row. - /// The current column. - /// The minimum working area row. - /// The maximum working area row. - /// The minimum working area column. - /// The maximum working area column. - [MethodImpl(InliningOptions.ShortMethod)] - public static void Convolve2D4( - in DenseMatrix matrixY, - in DenseMatrix matrixX, - Buffer2D sourcePixels, - ref Vector4 targetRowRef, - int row, - int column, - int minRow, - int maxRow, - int minColumn, - int maxColumn) - where TPixel : unmanaged, IPixel - { - Convolve2DImpl( - in matrixY, - in matrixX, - sourcePixels, - row, - column, - minRow, - maxRow, - minColumn, - maxColumn, - out Vector4 vector); - - ref Vector4 target = ref Unsafe.Add(ref targetRowRef, column); - Numerics.UnPremultiply(ref vector); - target = vector; - } - - [MethodImpl(InliningOptions.ShortMethod)] - public static void Convolve2DImpl( - in DenseMatrix matrixY, - in DenseMatrix matrixX, - Buffer2D sourcePixels, - int row, - int column, - int minRow, - int maxRow, - int minColumn, - int maxColumn, - out Vector4 vector) - where TPixel : unmanaged, IPixel - { - Vector4 vectorY = default; - Vector4 vectorX = default; - int matrixHeight = matrixY.Rows; - int matrixWidth = matrixY.Columns; - int radiusY = matrixHeight >> 1; - int radiusX = matrixWidth >> 1; - int sourceOffsetColumnBase = column + minColumn; - - for (int y = 0; y < matrixHeight; y++) - { - int offsetY = Numerics.Clamp(row + y - radiusY, minRow, maxRow); - Span sourceRowSpan = sourcePixels.GetRowSpan(offsetY); - - for (int x = 0; x < matrixWidth; x++) - { - int offsetX = Numerics.Clamp(sourceOffsetColumnBase + x - radiusX, minColumn, maxColumn); - var currentColor = sourceRowSpan[offsetX].ToVector4(); - Numerics.Premultiply(ref currentColor); - - vectorX += matrixX[y, x] * currentColor; - vectorY += matrixY[y, x] * currentColor; - } - } - - vector = Vector4.SquareRoot((vectorX * vectorX) + (vectorY * vectorY)); - } - - /// - /// Computes the sum of vectors in the span referenced by weighted by the kernel weight values. - /// Using this method the convolution filter is not applied to alpha in addition to the color channels. - /// - /// The pixel format. - /// The dense matrix. - /// The source frame. - /// The target row base reference. - /// The current row. - /// The current column. - /// The minimum working area row. - /// The maximum working area row. - /// The minimum working area column. - /// The maximum working area column. - [MethodImpl(InliningOptions.ShortMethod)] - public static void Convolve3( - in DenseMatrix matrix, - Buffer2D sourcePixels, - ref Vector4 targetRowRef, - int row, - int column, - int minRow, - int maxRow, - int minColumn, - int maxColumn) - where TPixel : unmanaged, IPixel - { - Vector4 vector = default; - - ConvolveImpl( - in matrix, - sourcePixels, - row, - column, - minRow, - maxRow, - minColumn, - maxColumn, - ref vector); - - ref Vector4 target = ref Unsafe.Add(ref targetRowRef, column); - vector.W = target.W; - - Numerics.UnPremultiply(ref vector); - target = vector; - } - - /// - /// Computes the sum of vectors in the span referenced by weighted by the kernel weight values. - /// Using this method the convolution filter is applied to alpha in addition to the color channels. - /// - /// The pixel format. - /// The dense matrix. - /// The source frame. - /// The target row base reference. - /// The current row. - /// The current column. - /// The minimum working area row. - /// The maximum working area row. - /// The minimum working area column. - /// The maximum working area column. - [MethodImpl(InliningOptions.ShortMethod)] - public static void Convolve4( - in DenseMatrix matrix, - Buffer2D sourcePixels, - ref Vector4 targetRowRef, - int row, - int column, - int minRow, - int maxRow, - int minColumn, - int maxColumn) - where TPixel : unmanaged, IPixel - { - Vector4 vector = default; - - ConvolveImpl( - in matrix, - sourcePixels, - row, - column, - minRow, - maxRow, - minColumn, - maxColumn, - ref vector); - - ref Vector4 target = ref Unsafe.Add(ref targetRowRef, column); - Numerics.UnPremultiply(ref vector); - target = vector; - } - - [MethodImpl(InliningOptions.ShortMethod)] - private static void ConvolveImpl( - in DenseMatrix matrix, - Buffer2D sourcePixels, - int row, - int column, - int minRow, - int maxRow, - int minColumn, - int maxColumn, - ref Vector4 vector) - where TPixel : unmanaged, IPixel - { - int matrixHeight = matrix.Rows; - int matrixWidth = matrix.Columns; - int radiusY = matrixHeight >> 1; - int radiusX = matrixWidth >> 1; - int sourceOffsetColumnBase = column + minColumn; - - for (int y = 0; y < matrixHeight; y++) - { - int offsetY = Numerics.Clamp(row + y - radiusY, minRow, maxRow); - Span sourceRowSpan = sourcePixels.GetRowSpan(offsetY); - - for (int x = 0; x < matrixWidth; x++) - { - int offsetX = Numerics.Clamp(sourceOffsetColumnBase + x - radiusX, minColumn, maxColumn); - var currentColor = sourceRowSpan[offsetX].ToVector4(); - Numerics.Premultiply(ref currentColor); - vector += matrix[y, x] * currentColor; - } - } - } - } -} diff --git a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs index a7a51f77dd..0c35c88286 100644 --- a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs +++ b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs @@ -53,8 +53,13 @@ public override Span GetSpan() { ThrowObjectDisposedException(); } - +#if SUPPORTS_CREATESPAN + ref byte r0 = ref MemoryMarshal.GetReference(this.Data); + return MemoryMarshal.CreateSpan(ref Unsafe.As(ref r0), this.length); +#else return MemoryMarshal.Cast(this.Data.AsSpan()).Slice(0, this.length); +#endif + } /// diff --git a/src/ImageSharp/Primitives/DenseMatrix{T}.cs b/src/ImageSharp/Primitives/DenseMatrix{T}.cs index e312703368..60dadb617b 100644 --- a/src/ImageSharp/Primitives/DenseMatrix{T}.cs +++ b/src/ImageSharp/Primitives/DenseMatrix{T}.cs @@ -109,7 +109,7 @@ public DenseMatrix(T[,] data) /// The at the specified position. public ref T this[int row, int column] { - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { this.CheckCoordinates(row, column); @@ -124,7 +124,7 @@ public DenseMatrix(T[,] data) /// /// The representation on the source data. /// - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator DenseMatrix(T[,] data) => new DenseMatrix(data); /// @@ -134,7 +134,7 @@ public DenseMatrix(T[,] data) /// /// The representation on the source data. /// - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #pragma warning disable SA1008 // Opening parenthesis should be spaced correctly public static implicit operator T[,](in DenseMatrix data) #pragma warning restore SA1008 // Opening parenthesis should be spaced correctly @@ -175,7 +175,7 @@ public DenseMatrix(T[,] data) /// Transposes the rows and columns of the dense matrix. /// /// The . - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public DenseMatrix Transpose() { var result = new DenseMatrix(this.Rows, this.Columns); @@ -196,13 +196,13 @@ public DenseMatrix Transpose() /// Fills the matrix with the given value /// /// The value to fill each item with - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Fill(T value) => this.Span.Fill(value); /// /// Clears the matrix setting each value to the default value for the element type /// - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Clear() => this.Span.Clear(); /// @@ -232,14 +232,14 @@ public override bool Equals(object obj) => obj is DenseMatrix other && this.Equals(other); /// - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(DenseMatrix other) => this.Columns == other.Columns && this.Rows == other.Rows && this.Span.SequenceEqual(other.Span); /// - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override int GetHashCode() { HashCode code = default; diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs index 3a5f35cd14..bb559019b7 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs @@ -1,10 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System; using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -43,12 +40,12 @@ public Convolution2DProcessor( } /// - /// Gets the horizontal gradient operator. + /// Gets the horizontal convolution kernel. /// public DenseMatrix KernelX { get; } /// - /// Gets the vertical gradient operator. + /// Gets the vertical convolution kernel. /// public DenseMatrix KernelY { get; } @@ -60,102 +57,39 @@ public Convolution2DProcessor( /// protected override void OnFrameApply(ImageFrame source) { - using Buffer2D targetPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Width, source.Height); + MemoryAllocator allocator = this.Configuration.MemoryAllocator; + using Buffer2D targetPixels = allocator.Allocate2D(source.Width, source.Height); source.CopyTo(targetPixels); var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); - var operation = new RowOperation(interest, targetPixels, source.PixelBuffer, this.KernelY, this.KernelX, this.Configuration, this.PreserveAlpha); - ParallelRowIterator.IterateRows( - this.Configuration, - interest, - in operation); + // We use a rectangle 3x the interest width to allocate a buffer big enough + // for source and target bulk pixel conversion. + var operationBounds = new Rectangle(interest.X, interest.Y, interest.Width * 3, interest.Height); - Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); - } - - /// - /// A implementing the convolution logic for . - /// - private readonly struct RowOperation : IRowOperation - { - private readonly Rectangle bounds; - private readonly int maxY; - private readonly int maxX; - private readonly Buffer2D targetPixels; - private readonly Buffer2D sourcePixels; - private readonly DenseMatrix kernelY; - private readonly DenseMatrix kernelX; - private readonly Configuration configuration; - private readonly bool preserveAlpha; - - [MethodImpl(InliningOptions.ShortMethod)] - public RowOperation( - Rectangle bounds, - Buffer2D targetPixels, - Buffer2D sourcePixels, - DenseMatrix kernelY, - DenseMatrix kernelX, - Configuration configuration, - bool preserveAlpha) - { - this.bounds = bounds; - this.maxY = this.bounds.Bottom - 1; - this.maxX = this.bounds.Right - 1; - this.targetPixels = targetPixels; - this.sourcePixels = sourcePixels; - this.kernelY = kernelY; - this.kernelX = kernelX; - this.configuration = configuration; - this.preserveAlpha = preserveAlpha; - } - - /// - [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(int y, Span span) + using (var map = new KernelSamplingMap(allocator)) { - ref Vector4 spanRef = ref MemoryMarshal.GetReference(span); - Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); - PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span); + // Since the kernel sizes are identical we can use a single map. + map.BuildSamplingOffsetMap(this.KernelY, interest); - if (this.preserveAlpha) - { - for (int x = 0; x < this.bounds.Width; x++) - { - DenseMatrixUtils.Convolve2D3( - in this.kernelY, - in this.kernelX, - this.sourcePixels, - ref spanRef, - y, - x, - this.bounds.Y, - this.maxY, - this.bounds.X, - this.maxX); - } - } - else - { - for (int x = 0; x < this.bounds.Width; x++) - { - DenseMatrixUtils.Convolve2D4( - in this.kernelY, - in this.kernelX, - this.sourcePixels, - ref spanRef, - y, - x, - this.bounds.Y, - this.maxY, - this.bounds.X, - this.maxX); - } - } + var operation = new Convolution2DRowOperation( + interest, + targetPixels, + source.PixelBuffer, + map, + this.KernelY, + this.KernelX, + this.Configuration, + this.PreserveAlpha); - PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan); + ParallelRowIterator.IterateRows, Vector4>( + this.Configuration, + operationBounds, + in operation); } + + Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs new file mode 100644 index 0000000000..802d1809f2 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs @@ -0,0 +1,193 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// A implementing the logic for 2D convolution. + /// + internal readonly struct Convolution2DRowOperation : IRowOperation + where TPixel : unmanaged, IPixel + { + private readonly Rectangle bounds; + private readonly Buffer2D targetPixels; + private readonly Buffer2D sourcePixels; + private readonly KernelSamplingMap map; + private readonly DenseMatrix kernelMatrixY; + private readonly DenseMatrix kernelMatrixX; + private readonly Configuration configuration; + private readonly bool preserveAlpha; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Convolution2DRowOperation( + Rectangle bounds, + Buffer2D targetPixels, + Buffer2D sourcePixels, + KernelSamplingMap map, + DenseMatrix kernelMatrixY, + DenseMatrix kernelMatrixX, + Configuration configuration, + bool preserveAlpha) + { + this.bounds = bounds; + this.targetPixels = targetPixels; + this.sourcePixels = sourcePixels; + this.map = map; + this.kernelMatrixY = kernelMatrixY; + this.kernelMatrixX = kernelMatrixX; + this.configuration = configuration; + this.preserveAlpha = preserveAlpha; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke(int y, Span span) + { + if (this.preserveAlpha) + { + this.Convolve3(y, span); + } + else + { + this.Convolve4(y, span); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Convolve3(int y, Span span) + { + // Span is 3x bounds. + int boundsX = this.bounds.X; + int boundsWidth = this.bounds.Width; + Span sourceBuffer = span.Slice(0, boundsWidth); + Span targetYBuffer = span.Slice(boundsWidth, boundsWidth); + Span targetXBuffer = span.Slice(boundsWidth * 2, boundsWidth); + + var state = new Convolution2DState(in this.kernelMatrixY, in this.kernelMatrixX, this.map); + ref int sampleRowBase = ref state.GetSampleRow(y - this.bounds.Y); + + // Clear the target buffers for each row run. + targetYBuffer.Clear(); + targetXBuffer.Clear(); + ref Vector4 targetBaseY = ref MemoryMarshal.GetReference(targetYBuffer); + ref Vector4 targetBaseX = ref MemoryMarshal.GetReference(targetXBuffer); + + ReadOnlyKernel kernelY = state.KernelY; + ReadOnlyKernel kernelX = state.KernelX; + Span sourceRow; + for (int kY = 0; kY < kernelY.Rows; kY++) + { + // Get the precalculated source sample row for this kernel row and copy to our buffer. + int sampleY = Unsafe.Add(ref sampleRowBase, kY); + sourceRow = this.sourcePixels.GetRowSpan(sampleY).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); + + for (int x = 0; x < sourceBuffer.Length; x++) + { + ref int sampleColumnBase = ref state.GetSampleColumn(x); + ref Vector4 targetY = ref Unsafe.Add(ref targetBaseY, x); + ref Vector4 targetX = ref Unsafe.Add(ref targetBaseX, x); + + for (int kX = 0; kX < kernelY.Columns; kX++) + { + int sampleX = Unsafe.Add(ref sampleColumnBase, kX) - boundsX; + Vector4 sample = Unsafe.Add(ref sourceBase, sampleX); + targetY += kernelX[kY, kX] * sample; + targetX += kernelY[kY, kX] * sample; + } + } + } + + // Now we need to combine the values and copy the original alpha values + // from the source row. + sourceRow = this.sourcePixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + for (int x = 0; x < sourceRow.Length; x++) + { + ref Vector4 target = ref Unsafe.Add(ref targetBaseY, x); + Vector4 vectorY = target; + Vector4 vectorX = Unsafe.Add(ref targetBaseX, x); + + target = Vector4.SquareRoot((vectorX * vectorX) + (vectorY * vectorY)); + target.W = Unsafe.Add(ref MemoryMarshal.GetReference(sourceBuffer), x).W; + } + + Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetYBuffer, targetRowSpan); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Convolve4(int y, Span span) + { + // Span is 3x bounds. + int boundsX = this.bounds.X; + int boundsWidth = this.bounds.Width; + Span sourceBuffer = span.Slice(0, boundsWidth); + Span targetYBuffer = span.Slice(boundsWidth, boundsWidth); + Span targetXBuffer = span.Slice(boundsWidth * 2, boundsWidth); + + var state = new Convolution2DState(in this.kernelMatrixY, in this.kernelMatrixX, this.map); + ref int sampleRowBase = ref state.GetSampleRow(y - this.bounds.Y); + + // Clear the target buffers for each row run. + targetYBuffer.Clear(); + targetXBuffer.Clear(); + ref Vector4 targetBaseY = ref MemoryMarshal.GetReference(targetYBuffer); + ref Vector4 targetBaseX = ref MemoryMarshal.GetReference(targetXBuffer); + + ReadOnlyKernel kernelY = state.KernelY; + ReadOnlyKernel kernelX = state.KernelX; + for (int kY = 0; kY < kernelY.Rows; kY++) + { + // Get the precalculated source sample row for this kernel row and copy to our buffer. + int sampleY = Unsafe.Add(ref sampleRowBase, kY); + Span sourceRow = this.sourcePixels.GetRowSpan(sampleY).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + Numerics.Premultiply(sourceBuffer); + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); + + for (int x = 0; x < sourceBuffer.Length; x++) + { + ref int sampleColumnBase = ref state.GetSampleColumn(x); + ref Vector4 targetY = ref Unsafe.Add(ref targetBaseY, x); + ref Vector4 targetX = ref Unsafe.Add(ref targetBaseX, x); + + for (int kX = 0; kX < kernelY.Columns; kX++) + { + int sampleX = Unsafe.Add(ref sampleColumnBase, kX) - boundsX; + Vector4 sample = Unsafe.Add(ref sourceBase, sampleX); + targetY += kernelX[kY, kX] * sample; + targetX += kernelY[kY, kX] * sample; + } + } + } + + // Now we need to combine the values + for (int x = 0; x < targetYBuffer.Length; x++) + { + ref Vector4 target = ref Unsafe.Add(ref targetBaseY, x); + Vector4 vectorY = target; + Vector4 vectorX = Unsafe.Add(ref targetBaseX, x); + + target = Vector4.SquareRoot((vectorX * vectorX) + (vectorY * vectorY)); + } + + Numerics.UnPremultiply(targetYBuffer); + + Span targetRow = this.targetPixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetYBuffer, targetRow); + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DState.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DState.cs new file mode 100644 index 0000000000..218093ac4e --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DState.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// A stack only struct used for reducing reference indirection during 2D convolution operations. + /// + internal readonly ref struct Convolution2DState + { + private readonly Span rowOffsetMap; + private readonly Span columnOffsetMap; + private readonly int kernelHeight; + private readonly int kernelWidth; + + public Convolution2DState( + in DenseMatrix kernelY, + in DenseMatrix kernelX, + KernelSamplingMap map) + { + // We check the kernels are the same size upstream. + this.KernelY = new ReadOnlyKernel(kernelY); + this.KernelX = new ReadOnlyKernel(kernelX); + this.kernelHeight = kernelY.Rows; + this.kernelWidth = kernelY.Columns; + this.rowOffsetMap = map.GetRowOffsetSpan(); + this.columnOffsetMap = map.GetColumnOffsetSpan(); + } + + public readonly ReadOnlyKernel KernelY + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + public readonly ReadOnlyKernel KernelX + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly ref int GetSampleRow(int row) + => ref Unsafe.Add(ref MemoryMarshal.GetReference(this.rowOffsetMap), row * this.kernelHeight); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly ref int GetSampleColumn(int column) + => ref Unsafe.Add(ref MemoryMarshal.GetReference(this.columnOffsetMap), column * this.kernelWidth); + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs index b61690415a..151b0ffccc 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs @@ -42,12 +42,12 @@ public Convolution2PassProcessor( } /// - /// Gets the horizontal gradient operator. + /// Gets the horizontal convolution kernel. /// public DenseMatrix KernelX { get; } /// - /// Gets the vertical gradient operator. + /// Gets the vertical convolution kernel. /// public DenseMatrix KernelY { get; } @@ -63,96 +63,48 @@ protected override void OnFrameApply(ImageFrame source) var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); - // Horizontal convolution - var horizontalOperation = new RowOperation(interest, firstPassPixels, source.PixelBuffer, this.KernelX, this.Configuration, this.PreserveAlpha); - ParallelRowIterator.IterateRows( - this.Configuration, - interest, - in horizontalOperation); + // We use a rectangle 2x the interest width to allocate a buffer big enough + // for source and target bulk pixel conversion. + var operationBounds = new Rectangle(interest.X, interest.Y, interest.Width * 2, interest.Height); - // Vertical convolution - var verticalOperation = new RowOperation(interest, source.PixelBuffer, firstPassPixels, this.KernelY, this.Configuration, this.PreserveAlpha); - ParallelRowIterator.IterateRows( - this.Configuration, - interest, - in verticalOperation); - } - - /// - /// A implementing the convolution logic for . - /// - private readonly struct RowOperation : IRowOperation - { - private readonly Rectangle bounds; - private readonly Buffer2D targetPixels; - private readonly Buffer2D sourcePixels; - private readonly DenseMatrix kernel; - private readonly Configuration configuration; - private readonly bool preserveAlpha; - - [MethodImpl(InliningOptions.ShortMethod)] - public RowOperation( - Rectangle bounds, - Buffer2D targetPixels, - Buffer2D sourcePixels, - DenseMatrix kernel, - Configuration configuration, - bool preserveAlpha) + using (var mapX = new KernelSamplingMap(this.Configuration.MemoryAllocator)) { - this.bounds = bounds; - this.targetPixels = targetPixels; - this.sourcePixels = sourcePixels; - this.kernel = kernel; - this.configuration = configuration; - this.preserveAlpha = preserveAlpha; + mapX.BuildSamplingOffsetMap(this.KernelX, interest); + + // Horizontal convolution + var horizontalOperation = new ConvolutionRowOperation( + interest, + firstPassPixels, + source.PixelBuffer, + mapX, + this.KernelX, + this.Configuration, + this.PreserveAlpha); + + ParallelRowIterator.IterateRows, Vector4>( + this.Configuration, + operationBounds, + in horizontalOperation); } - /// - [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(int y, Span span) + using (var mapY = new KernelSamplingMap(this.Configuration.MemoryAllocator)) { - ref Vector4 spanRef = ref MemoryMarshal.GetReference(span); - - int maxY = this.bounds.Bottom - 1; - int maxX = this.bounds.Right - 1; - - Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); - PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span); - - if (this.preserveAlpha) - { - for (int x = 0; x < this.bounds.Width; x++) - { - DenseMatrixUtils.Convolve3( - in this.kernel, - this.sourcePixels, - ref spanRef, - y, - x, - this.bounds.Y, - maxY, - this.bounds.X, - maxX); - } - } - else - { - for (int x = 0; x < this.bounds.Width; x++) - { - DenseMatrixUtils.Convolve4( - in this.kernel, - this.sourcePixels, - ref spanRef, - y, - x, - this.bounds.Y, - maxY, - this.bounds.X, - maxX); - } - } - - PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan); + mapY.BuildSamplingOffsetMap(this.KernelY, interest); + + // Vertical convolution + var verticalOperation = new ConvolutionRowOperation( + interest, + source.PixelBuffer, + firstPassPixels, + mapY, + this.KernelY, + this.Configuration, + this.PreserveAlpha); + + ParallelRowIterator.IterateRows, Vector4>( + this.Configuration, + operationBounds, + in verticalOperation); } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs index 95fef15f62..924a1125bd 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs @@ -39,7 +39,7 @@ public ConvolutionProcessor( } /// - /// Gets the 2d gradient operator. + /// Gets the 2d convolution kernel. /// public DenseMatrix KernelXY { get; } @@ -51,16 +51,26 @@ public ConvolutionProcessor( /// protected override void OnFrameApply(ImageFrame source) { - using Buffer2D targetPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size()); + MemoryAllocator allocator = this.Configuration.MemoryAllocator; + using Buffer2D targetPixels = allocator.Allocate2D(source.Size()); source.CopyTo(targetPixels); var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); - var operation = new RowOperation(interest, targetPixels, source.PixelBuffer, this.KernelXY, this.Configuration, this.PreserveAlpha); - ParallelRowIterator.IterateRows( - this.Configuration, - interest, - in operation); + + // We use a rectangle 2x the interest width to allocate a buffer big enough + // for source and target bulk pixel conversion. + var operationBounds = new Rectangle(interest.X, interest.Y, interest.Width * 2, interest.Height); + using (var map = new KernelSamplingMap(allocator)) + { + map.BuildSamplingOffsetMap(this.KernelXY, interest); + + var operation = new RowOperation(interest, targetPixels, source.PixelBuffer, map, this.KernelXY, this.Configuration, this.PreserveAlpha); + ParallelRowIterator.IterateRows( + this.Configuration, + operationBounds, + in operation); + } Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); } @@ -71,10 +81,9 @@ protected override void OnFrameApply(ImageFrame source) private readonly struct RowOperation : IRowOperation { private readonly Rectangle bounds; - private readonly int maxY; - private readonly int maxX; private readonly Buffer2D targetPixels; private readonly Buffer2D sourcePixels; + private readonly KernelSamplingMap map; private readonly DenseMatrix kernel; private readonly Configuration configuration; private readonly bool preserveAlpha; @@ -84,15 +93,15 @@ public RowOperation( Rectangle bounds, Buffer2D targetPixels, Buffer2D sourcePixels, + KernelSamplingMap map, DenseMatrix kernel, Configuration configuration, bool preserveAlpha) { this.bounds = bounds; - this.maxY = this.bounds.Bottom - 1; - this.maxX = this.bounds.Right - 1; this.targetPixels = targetPixels; this.sourcePixels = sourcePixels; + this.map = map; this.kernel = kernel; this.configuration = configuration; this.preserveAlpha = preserveAlpha; @@ -102,45 +111,93 @@ public RowOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y, Span span) { - ref Vector4 spanRef = ref MemoryMarshal.GetReference(span); + // Span is 2x bounds. + int boundsX = this.bounds.X; + int boundsWidth = this.bounds.Width; + Span sourceBuffer = span.Slice(0, this.bounds.Width); + Span targetBuffer = span.Slice(this.bounds.Width); + + ref Vector4 targetRowRef = ref MemoryMarshal.GetReference(span); + Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(boundsX, boundsWidth); - Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); - PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span); + var state = new ConvolutionState(in this.kernel, this.map); + int row = y - this.bounds.Y; + ref int sampleRowBase = ref state.GetSampleRow(row); if (this.preserveAlpha) { - for (int x = 0; x < this.bounds.Width; x++) + // Clear the target buffer for each row run. + targetBuffer.Clear(); + ref Vector4 targetBase = ref MemoryMarshal.GetReference(targetBuffer); + + Span sourceRow; + for (int kY = 0; kY < state.Kernel.Rows; kY++) + { + // Get the precalculated source sample row for this kernel row and copy to our buffer. + int offsetY = Unsafe.Add(ref sampleRowBase, kY); + sourceRow = this.sourcePixels.GetRowSpan(offsetY).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); + + for (int x = 0; x < sourceBuffer.Length; x++) + { + ref int sampleColumnBase = ref state.GetSampleColumn(x); + ref Vector4 target = ref Unsafe.Add(ref targetBase, x); + + for (int kX = 0; kX < state.Kernel.Columns; kX++) + { + int offsetX = Unsafe.Add(ref sampleColumnBase, kX) - boundsX; + Vector4 sample = Unsafe.Add(ref sourceBase, offsetX); + target += state.Kernel[kY, kX] * sample; + } + } + } + + // Now we need to copy the original alpha values from the source row. + sourceRow = this.sourcePixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + for (int x = 0; x < sourceRow.Length; x++) { - DenseMatrixUtils.Convolve3( - in this.kernel, - this.sourcePixels, - ref spanRef, - y, - x, - this.bounds.Y, - this.maxY, - this.bounds.X, - this.maxX); + ref Vector4 target = ref Unsafe.Add(ref targetBase, x); + target.W = Unsafe.Add(ref MemoryMarshal.GetReference(sourceBuffer), x).W; } } else { - for (int x = 0; x < this.bounds.Width; x++) + // Clear the target buffer for each row run. + targetBuffer.Clear(); + ref Vector4 targetBase = ref MemoryMarshal.GetReference(targetBuffer); + + for (int kY = 0; kY < state.Kernel.Rows; kY++) { - DenseMatrixUtils.Convolve4( - in this.kernel, - this.sourcePixels, - ref spanRef, - y, - x, - this.bounds.Y, - this.maxY, - this.bounds.X, - this.maxX); + // Get the precalculated source sample row for this kernel row and copy to our buffer. + int offsetY = Unsafe.Add(ref sampleRowBase, kY); + Span sourceRow = this.sourcePixels.GetRowSpan(offsetY).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + Numerics.Premultiply(sourceBuffer); + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); + + for (int x = 0; x < sourceBuffer.Length; x++) + { + ref int sampleColumnBase = ref state.GetSampleColumn(x); + ref Vector4 target = ref Unsafe.Add(ref targetBase, x); + + for (int kX = 0; kX < state.Kernel.Columns; kX++) + { + int offsetX = Unsafe.Add(ref sampleColumnBase, kX) - boundsX; + Vector4 sample = Unsafe.Add(ref sourceBase, offsetX); + target += state.Kernel[kY, kX] * sample; + } + } } + + Numerics.UnPremultiply(targetBuffer); } - PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRowSpan); } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionRowOperation{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionRowOperation{TPixel}.cs new file mode 100644 index 0000000000..9876b2885b --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionRowOperation{TPixel}.cs @@ -0,0 +1,163 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// A implementing the logic for 1D convolution. + /// + internal readonly struct ConvolutionRowOperation : IRowOperation + where TPixel : unmanaged, IPixel + { + private readonly Rectangle bounds; + private readonly Buffer2D targetPixels; + private readonly Buffer2D sourcePixels; + private readonly KernelSamplingMap map; + private readonly DenseMatrix kernelMatrix; + private readonly Configuration configuration; + private readonly bool preserveAlpha; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ConvolutionRowOperation( + Rectangle bounds, + Buffer2D targetPixels, + Buffer2D sourcePixels, + KernelSamplingMap map, + DenseMatrix kernelMatrix, + Configuration configuration, + bool preserveAlpha) + { + this.bounds = bounds; + this.targetPixels = targetPixels; + this.sourcePixels = sourcePixels; + this.map = map; + this.kernelMatrix = kernelMatrix; + this.configuration = configuration; + this.preserveAlpha = preserveAlpha; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invoke(int y, Span span) + { + if (this.preserveAlpha) + { + this.Convolve3(y, span); + } + else + { + this.Convolve4(y, span); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Convolve3(int y, Span span) + { + // Span is 2x bounds. + int boundsX = this.bounds.X; + int boundsWidth = this.bounds.Width; + Span sourceBuffer = span.Slice(0, this.bounds.Width); + Span targetBuffer = span.Slice(this.bounds.Width); + + var state = new ConvolutionState(in this.kernelMatrix, this.map); + ref int sampleRowBase = ref state.GetSampleRow(y - this.bounds.Y); + + // Clear the target buffer for each row run. + targetBuffer.Clear(); + ref Vector4 targetBase = ref MemoryMarshal.GetReference(targetBuffer); + + ReadOnlyKernel kernel = state.Kernel; + Span sourceRow; + for (int kY = 0; kY < kernel.Rows; kY++) + { + // Get the precalculated source sample row for this kernel row and copy to our buffer. + int sampleY = Unsafe.Add(ref sampleRowBase, kY); + sourceRow = this.sourcePixels.GetRowSpan(sampleY).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); + + for (int x = 0; x < sourceBuffer.Length; x++) + { + ref int sampleColumnBase = ref state.GetSampleColumn(x); + ref Vector4 target = ref Unsafe.Add(ref targetBase, x); + + for (int kX = 0; kX < kernel.Columns; kX++) + { + int sampleX = Unsafe.Add(ref sampleColumnBase, kX) - boundsX; + Vector4 sample = Unsafe.Add(ref sourceBase, sampleX); + target += kernel[kY, kX] * sample; + } + } + } + + // Now we need to copy the original alpha values from the source row. + sourceRow = this.sourcePixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + for (int x = 0; x < sourceRow.Length; x++) + { + ref Vector4 target = ref Unsafe.Add(ref targetBase, x); + target.W = Unsafe.Add(ref MemoryMarshal.GetReference(sourceBuffer), x).W; + } + + Span targetRow = this.targetPixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Convolve4(int y, Span span) + { + // Span is 2x bounds. + int boundsX = this.bounds.X; + int boundsWidth = this.bounds.Width; + Span sourceBuffer = span.Slice(0, this.bounds.Width); + Span targetBuffer = span.Slice(this.bounds.Width); + + var state = new ConvolutionState(in this.kernelMatrix, this.map); + ref int sampleRowBase = ref state.GetSampleRow(y - this.bounds.Y); + + // Clear the target buffer for each row run. + targetBuffer.Clear(); + ref Vector4 targetBase = ref MemoryMarshal.GetReference(targetBuffer); + + ReadOnlyKernel kernel = state.Kernel; + for (int kY = 0; kY < kernel.Rows; kY++) + { + // Get the precalculated source sample row for this kernel row and copy to our buffer. + int sampleY = Unsafe.Add(ref sampleRowBase, kY); + Span sourceRow = this.sourcePixels.GetRowSpan(sampleY).Slice(boundsX, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + + Numerics.Premultiply(sourceBuffer); + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); + + for (int x = 0; x < sourceBuffer.Length; x++) + { + ref int sampleColumnBase = ref state.GetSampleColumn(x); + ref Vector4 target = ref Unsafe.Add(ref targetBase, x); + + for (int kX = 0; kX < kernel.Columns; kX++) + { + int sampleX = Unsafe.Add(ref sampleColumnBase, kX) - boundsX; + Vector4 sample = Unsafe.Add(ref sourceBase, sampleX); + target += kernel[kY, kX] * sample; + } + } + } + + Numerics.UnPremultiply(targetBuffer); + + Span targetRow = this.targetPixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow); + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionState.cs b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionState.cs new file mode 100644 index 0000000000..3f296c67df --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionState.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// A stack only struct used for reducing reference indirection during convolution operations. + /// + internal readonly ref struct ConvolutionState + { + private readonly Span rowOffsetMap; + private readonly Span columnOffsetMap; + private readonly int kernelHeight; + private readonly int kernelWidth; + + public ConvolutionState( + in DenseMatrix kernel, + KernelSamplingMap map) + { + this.Kernel = new ReadOnlyKernel(kernel); + this.kernelHeight = kernel.Rows; + this.kernelWidth = kernel.Columns; + this.rowOffsetMap = map.GetRowOffsetSpan(); + this.columnOffsetMap = map.GetColumnOffsetSpan(); + } + + public readonly ReadOnlyKernel Kernel + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly ref int GetSampleRow(int row) + => ref Unsafe.Add(ref MemoryMarshal.GetReference(this.rowOffsetMap), row * this.kernelHeight); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly ref int GetSampleColumn(int column) + => ref Unsafe.Add(ref MemoryMarshal.GetReference(this.columnOffsetMap), column * this.kernelWidth); + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs b/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs new file mode 100644 index 0000000000..e4b7dbea09 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// Provides a map of the convolution kernel sampling offsets. + /// + internal sealed class KernelSamplingMap : IDisposable + { + private readonly MemoryAllocator allocator; + private bool isDisposed; + private IMemoryOwner yOffsets; + private IMemoryOwner xOffsets; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + public KernelSamplingMap(MemoryAllocator allocator) => this.allocator = allocator; + + /// + /// Builds a map of the sampling offsets for the kernel clamped by the given bounds. + /// + /// The convolution kernel. + /// The source bounds. + public void BuildSamplingOffsetMap(DenseMatrix kernel, Rectangle bounds) + { + int kernelHeight = kernel.Rows; + int kernelWidth = kernel.Columns; + this.yOffsets = this.allocator.Allocate(bounds.Height * kernelHeight); + this.xOffsets = this.allocator.Allocate(bounds.Width * kernelWidth); + + int minY = bounds.Y; + int maxY = bounds.Bottom - 1; + int minX = bounds.X; + int maxX = bounds.Right - 1; + + int radiusY = kernelHeight >> 1; + int radiusX = kernelWidth >> 1; + + // Calculate the y and x sampling offsets clamped to the given rectangle. + // While this isn't a hotpath we still dip into unsafe to avoid the span bounds + // checks as the can potentially be looping over large arrays. + Span ySpan = this.yOffsets.GetSpan(); + ref int ySpanBase = ref MemoryMarshal.GetReference(ySpan); + for (int row = 0; row < bounds.Height; row++) + { + int rowBase = row * kernelHeight; + for (int y = 0; y < kernelHeight; y++) + { + Unsafe.Add(ref ySpanBase, rowBase + y) = row + y + minY - radiusY; + } + } + + if (kernelHeight > 1) + { + Numerics.Clamp(ySpan, minY, maxY); + } + + Span xSpan = this.xOffsets.GetSpan(); + ref int xSpanBase = ref MemoryMarshal.GetReference(xSpan); + for (int column = 0; column < bounds.Width; column++) + { + int columnBase = column * kernelWidth; + for (int x = 0; x < kernelWidth; x++) + { + Unsafe.Add(ref xSpanBase, columnBase + x) = column + x + minX - radiusX; + } + } + + if (kernelWidth > 1) + { + Numerics.Clamp(xSpan, minX, maxX); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetRowOffsetSpan() => this.yOffsets.GetSpan(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetColumnOffsetSpan() => this.xOffsets.GetSpan(); + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.yOffsets.Dispose(); + this.xOffsets.Dispose(); + + this.isDisposed = true; + } + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/ReadOnlyKernel.cs b/src/ImageSharp/Processing/Processors/Convolution/ReadOnlyKernel.cs new file mode 100644 index 0000000000..37e0060054 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/ReadOnlyKernel.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// A stack only, readonly, kernel matrix that can be indexed without + /// bounds checks when compiled in release mode. + /// + internal readonly ref struct ReadOnlyKernel + { + private readonly ReadOnlySpan values; + + public ReadOnlyKernel(DenseMatrix matrix) + { + this.Columns = matrix.Columns; + this.Rows = matrix.Rows; + this.values = matrix.Span; + } + + public int Columns + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + public int Rows + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + public float this[int row, int column] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + this.CheckCoordinates(row, column); + ref float vBase = ref MemoryMarshal.GetReference(this.values); + return Unsafe.Add(ref vBase, (row * this.Columns) + column); + } + } + + [Conditional("DEBUG")] + private void CheckCoordinates(int row, int column) + { + if (row < 0 || row >= this.Rows) + { + throw new ArgumentOutOfRangeException(nameof(row), row, $"{row} is outwith the matrix bounds."); + } + + if (column < 0 || column >= this.Columns) + { + throw new ArgumentOutOfRangeException(nameof(column), column, $"{column} is outwith the matrix bounds."); + } + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Config.cs b/tests/ImageSharp.Benchmarks/Config.cs index 4c9f6c06db..d08e2f2d66 100644 --- a/tests/ImageSharp.Benchmarks/Config.cs +++ b/tests/ImageSharp.Benchmarks/Config.cs @@ -27,6 +27,14 @@ public Config() } + public class MultiFramework : Config + { + public MultiFramework() => this.AddJob( + Job.Default.WithRuntime(ClrRuntime.Net472), + Job.Default.WithRuntime(CoreRuntime.Core21), + Job.Default.WithRuntime(CoreRuntime.Core31)); + } + public class ShortClr : Config { public ShortClr() => this.AddJob( diff --git a/tests/ImageSharp.Benchmarks/Samplers/GaussianBlur.cs b/tests/ImageSharp.Benchmarks/Samplers/GaussianBlur.cs index 62d5806037..8f009e58f1 100644 --- a/tests/ImageSharp.Benchmarks/Samplers/GaussianBlur.cs +++ b/tests/ImageSharp.Benchmarks/Samplers/GaussianBlur.cs @@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Samplers { - [Config(typeof(Config.ShortClr))] + [Config(typeof(Config.MultiFramework))] public class GaussianBlur { [Benchmark]