Browse Source

Merge pull request #3102 from SixLabors/js/blend-perf-fixes

Add working-buffer blending and adjust row APIs
pull/3105/head
James Jackson-South 1 month ago
committed by GitHub
parent
commit
b10a7eda4b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs
  2. 8
      src/ImageSharp/Advanced/ParallelRowIterator.cs
  3. 13
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  4. 198
      src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs
  5. 16
      src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs

4
src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs

@ -15,12 +15,12 @@ public interface IRowOperation<TBuffer>
/// </summary>
/// <param name="bounds">The bounds of the operation.</param>
/// <returns>The required buffer length.</returns>
int GetRequiredBufferLength(Rectangle bounds);
public int GetRequiredBufferLength(Rectangle bounds);
/// <summary>
/// Invokes the method passing the row and a buffer.
/// </summary>
/// <param name="y">The row y coordinate.</param>
/// <param name="span">The contiguous region of memory.</param>
void Invoke(int y, Span<TBuffer> span);
public void Invoke(int y, Span<TBuffer> span);
}

8
src/ImageSharp/Advanced/ParallelRowIterator.cs

@ -68,7 +68,7 @@ public static partial class ParallelRowIterator
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -138,7 +138,7 @@ public static partial class ParallelRowIterator
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -195,7 +195,7 @@ public static partial class ParallelRowIterator
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowIntervalOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -262,7 +262,7 @@ public static partial class ParallelRowIterator
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowIntervalOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,

13
src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Numerics;
using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
@ -456,15 +457,21 @@ internal class WebpAnimationDecoder : IDisposable
// The destination frame has already been prepopulated with the pixel data from the previous frame
// so blending will leave the desired result which takes into consideration restoration to the
// background color within the restore area.
PixelBlender<TPixel> blender =
PixelOperations<TPixel>.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver);
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(
PixelColorBlendingMode.Normal,
PixelAlphaCompositionMode.SrcOver);
// By using a dedicated vector span we can avoid per-row pool allocations in PixelBlender.Blend
// We need 3 Vector4 values per pixel to store the background, foreground, and result pixels for blending.
using IMemoryOwner<Vector4> workingBufferOwner = imageFrame.Configuration.MemoryAllocator.Allocate<Vector4>(restoreArea.Width * 3);
Span<Vector4> workingBuffer = workingBufferOwner.GetSpan();
for (int y = 0; y < restoreArea.Height; y++)
{
Span<TPixel> framePixelRow = imageFramePixels.DangerousGetRowSpan(y);
Span<TPixel> decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width];
blender.Blend<TPixel>(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f);
blender.Blend<TPixel>(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f, workingBuffer);
}
return;

198
src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs

@ -3,7 +3,6 @@
using System.Buffers;
using System.Numerics;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.PixelFormats;
@ -52,16 +51,53 @@ public abstract class PixelBlender<TPixel>
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 3);
Span<Vector4> destinationVectors = buffer.Slice(0, maxLength);
Span<Vector4> backgroundVectors = buffer.Slice(maxLength, maxLength);
Span<Vector4> sourceVectors = buffer.Slice(maxLength * 2, maxLength);
this.Blend(
configuration,
destination,
background,
source,
amount,
buffer.Memory.Span[..(maxLength * 3)]);
}
/// <summary>
/// Blends 2 rows together using caller-provided temporary vector scratch.
/// </summary>
/// <typeparam name="TPixelSrc">the pixel format of the source span</typeparam>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source span</param>
/// <param name="amount">
/// A value between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
/// <param name="workingBuffer">Reusable temporary vector scratch with capacity for at least 3 rows.</param>
public void Blend<TPixelSrc>(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
ReadOnlySpan<TPixelSrc> source,
float amount,
Span<Vector4> workingBuffer)
where TPixelSrc : unmanaged, IPixel<TPixelSrc>
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length));
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 3, nameof(workingBuffer.Length));
Span<Vector4> destinationVectors = workingBuffer[..maxLength];
Span<Vector4> backgroundVectors = workingBuffer.Slice(maxLength, maxLength);
Span<Vector4> sourceVectors = workingBuffer.Slice(maxLength * 2, maxLength);
PixelOperations<TPixel>.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale);
PixelOperations<TPixelSrc>.Instance.ToVector4(configuration, source[..maxLength], sourceVectors, PixelConversionModifiers.Scale);
this.BlendFunction(destinationVectors, backgroundVectors, sourceVectors, amount);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale);
}
/// <summary>
@ -87,14 +123,48 @@ public abstract class PixelBlender<TPixel>
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 2);
Span<Vector4> destinationVectors = buffer.Slice(0, maxLength);
Span<Vector4> backgroundVectors = buffer.Slice(maxLength, maxLength);
this.Blend(
configuration,
destination,
background,
source,
amount,
buffer.Memory.Span[..(maxLength * 2)]);
}
/// <summary>
/// Blends a row against a constant source color using caller-provided temporary vector scratch.
/// </summary>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source color</param>
/// <param name="amount">
/// A value between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
/// <param name="workingBuffer">Reusable temporary vector scratch with capacity for at least 2 rows.</param>
public void Blend(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
TPixel source,
float amount,
Span<Vector4> workingBuffer)
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 2, nameof(workingBuffer.Length));
Span<Vector4> destinationVectors = workingBuffer[..maxLength];
Span<Vector4> backgroundVectors = workingBuffer.Slice(maxLength, maxLength);
PixelOperations<TPixel>.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale);
this.BlendFunction(destinationVectors, backgroundVectors, source.ToScaledVector4(), amount);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale);
}
/// <summary>
@ -116,6 +186,27 @@ public abstract class PixelBlender<TPixel>
ReadOnlySpan<float> amount)
=> this.Blend<TPixel>(configuration, destination, background, source, amount);
/// <summary>
/// Blends 2 rows together using caller-provided temporary vector scratch.
/// </summary>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source span</param>
/// <param name="amount">
/// A span with values between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
/// <param name="workingBuffer">Reusable temporary vector scratch with capacity for at least 3 rows.</param>
public void Blend(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
ReadOnlySpan<TPixel> source,
ReadOnlySpan<float> amount,
Span<Vector4> workingBuffer)
=> this.Blend<TPixel>(configuration, destination, background, source, amount, workingBuffer);
/// <summary>
/// Blends 2 rows together
/// </summary>
@ -142,20 +233,89 @@ public abstract class PixelBlender<TPixel>
Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 3);
Span<Vector4> destinationVectors = buffer.Slice(0, maxLength);
Span<Vector4> backgroundVectors = buffer.Slice(maxLength, maxLength);
Span<Vector4> sourceVectors = buffer.Slice(maxLength * 2, maxLength);
this.Blend(
configuration,
destination,
background,
source,
amount,
buffer.Memory.Span[..(maxLength * 3)]);
}
/// <summary>
/// Blends a row against a constant source color.
/// </summary>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source color</param>
/// <param name="amount">
/// A span with values between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
public void Blend(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
TPixel source,
ReadOnlySpan<float> amount)
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 2);
this.Blend(
configuration,
destination,
background,
source,
amount,
buffer.Memory.Span[..(maxLength * 2)]);
}
/// <summary>
/// Blends 2 rows together using caller-provided temporary vector scratch.
/// </summary>
/// <typeparam name="TPixelSrc">the pixel format of the source span</typeparam>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source span</param>
/// <param name="amount">
/// A span with values between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
/// <param name="workingBuffer">Reusable temporary vector scratch with capacity for at least 3 rows.</param>
public void Blend<TPixelSrc>(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
ReadOnlySpan<TPixelSrc> source,
ReadOnlySpan<float> amount,
Span<Vector4> workingBuffer)
where TPixelSrc : unmanaged, IPixel<TPixelSrc>
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length));
Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length));
Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 3, nameof(workingBuffer.Length));
Span<Vector4> destinationVectors = workingBuffer[..maxLength];
Span<Vector4> backgroundVectors = workingBuffer.Slice(maxLength, maxLength);
Span<Vector4> sourceVectors = workingBuffer.Slice(maxLength * 2, maxLength);
PixelOperations<TPixel>.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale);
PixelOperations<TPixelSrc>.Instance.ToVector4(configuration, source[..maxLength], sourceVectors, PixelConversionModifiers.Scale);
this.BlendFunction(destinationVectors, backgroundVectors, sourceVectors, amount);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale);
}
/// <summary>
/// Blends a row against a constant source color.
/// Blends a row against a constant source color using caller-provided temporary vector scratch.
/// </summary>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
@ -165,26 +325,28 @@ public abstract class PixelBlender<TPixel>
/// A span with values between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
/// <param name="workingBuffer">Reusable temporary vector scratch with capacity for at least 2 rows.</param>
public void Blend(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
TPixel source,
ReadOnlySpan<float> amount)
ReadOnlySpan<float> amount,
Span<Vector4> workingBuffer)
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length));
Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 2, nameof(workingBuffer.Length));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 2);
Span<Vector4> destinationVectors = buffer.Slice(0, maxLength);
Span<Vector4> backgroundVectors = buffer.Slice(maxLength, maxLength);
Span<Vector4> destinationVectors = workingBuffer[..maxLength];
Span<Vector4> backgroundVectors = workingBuffer.Slice(maxLength, maxLength);
PixelOperations<TPixel>.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale);
this.BlendFunction(destinationVectors, backgroundVectors, source.ToScaledVector4(), amount);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale);
}
/// <summary>

16
src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
@ -145,7 +146,7 @@ internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg>
this.Blender,
this.Opacity);
ParallelRowIterator.IterateRows(
ParallelRowIterator.IterateRows<RowOperation, Vector4>(
configuration,
new Rectangle(0, 0, foregroundRectangle.Width, foregroundRectangle.Height),
in operation);
@ -161,7 +162,7 @@ internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg>
/// <summary>
/// A <see langword="struct"/> implementing the draw logic for <see cref="DrawImageProcessor{TPixelBg,TPixelFg}"/>.
/// </summary>
private readonly struct RowOperation : IRowOperation
private readonly struct RowOperation : IRowOperation<Vector4>
{
private readonly Buffer2D<TPixelBg> background;
private readonly Buffer2D<TPixelFg> foreground;
@ -190,13 +191,20 @@ internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg>
this.opacity = opacity;
}
/// <inheritdoc/>
public int GetRequiredBufferLength(Rectangle bounds)
// By using a dedicated vector span we can avoid per-row pool allocations in PixelBlender.Blend
// We need 3 Vector4 values per pixel to store the background, foreground, and result pixels for blending.
=> 3 * bounds.Width;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
public void Invoke(int y, Span<Vector4> span)
{
Span<TPixelBg> background = this.background.DangerousGetRowSpan(y + this.backgroundRectangle.Top).Slice(this.backgroundRectangle.Left, this.backgroundRectangle.Width);
Span<TPixelFg> foreground = this.foreground.DangerousGetRowSpan(y + this.foregroundRectangle.Top).Slice(this.foregroundRectangle.Left, this.foregroundRectangle.Width);
this.blender.Blend<TPixelFg>(this.configuration, background, background, foreground, this.opacity);
this.blender.Blend<TPixelFg>(this.configuration, background, background, foreground, this.opacity, span);
}
}
}

Loading…
Cancel
Save