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]