Skip to content

Commit a6f96f7

Browse files
Optimize and fix deduper
1 parent 55e69c7 commit a6f96f7

6 files changed

Lines changed: 137 additions & 110 deletions

File tree

src/ImageSharp/Formats/AnimationUtilities.cs

Lines changed: 75 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,31 @@ internal static class AnimationUtilities
2525
/// <param name="configuration">The configuration.</param>
2626
/// <param name="previousFrame">The previous frame if present.</param>
2727
/// <param name="currentFrame">The current frame.</param>
28+
/// <param name="nextFrame">The next frame if present.</param>
2829
/// <param name="resultFrame">The resultant output.</param>
2930
/// <param name="replacement">The value to use when replacing duplicate pixels.</param>
31+
/// <param name="blend">Whether the resultant frame represents an animation blend.</param>
3032
/// <param name="clampingMode">The clamping bound to apply when calculating difference bounds.</param>
3133
/// <returns>The <see cref="ValueTuple{Boolean, Rectangle}"/> representing the operation result.</returns>
3234
public static (bool Difference, Rectangle Bounds) DeDuplicatePixels<TPixel>(
3335
Configuration configuration,
3436
ImageFrame<TPixel>? previousFrame,
3537
ImageFrame<TPixel> currentFrame,
38+
ImageFrame<TPixel>? nextFrame,
3639
ImageFrame<TPixel> resultFrame,
37-
Vector4 replacement,
40+
Color replacement,
41+
bool blend,
3842
ClampingMode clampingMode = ClampingMode.None)
3943
where TPixel : unmanaged, IPixel<TPixel>
4044
{
41-
// TODO: This would be faster (but more complicated to find diff bounds) if we operated on Rgba32.
42-
// If someone wants to do that, they have my unlimited thanks.
4345
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
44-
IMemoryOwner<Vector4> buffers = memoryAllocator.Allocate<Vector4>(currentFrame.Width * 3, AllocationOptions.Clean);
45-
Span<Vector4> previous = buffers.GetSpan()[..currentFrame.Width];
46-
Span<Vector4> current = buffers.GetSpan().Slice(currentFrame.Width, currentFrame.Width);
47-
Span<Vector4> result = buffers.GetSpan()[(currentFrame.Width * 2)..];
46+
IMemoryOwner<Rgba32> buffers = memoryAllocator.Allocate<Rgba32>(currentFrame.Width * 4, AllocationOptions.Clean);
47+
Span<Rgba32> previous = buffers.GetSpan()[..currentFrame.Width];
48+
Span<Rgba32> current = buffers.GetSpan().Slice(currentFrame.Width, currentFrame.Width);
49+
Span<Rgba32> next = buffers.GetSpan().Slice(currentFrame.Width * 2, currentFrame.Width);
50+
Span<Rgba32> result = buffers.GetSpan()[(currentFrame.Width * 3)..];
51+
52+
Rgba32 bg = replacement;
4853

4954
int top = int.MinValue;
5055
int bottom = int.MaxValue;
@@ -56,81 +61,97 @@ public static (bool Difference, Rectangle Bounds) DeDuplicatePixels<TPixel>(
5661
{
5762
if (previousFrame != null)
5863
{
59-
PixelOperations<TPixel>.Instance.ToVector4(configuration, previousFrame.DangerousGetPixelRowMemory(y).Span, previous, PixelConversionModifiers.Scale);
64+
PixelOperations<TPixel>.Instance.ToRgba32(configuration, previousFrame.DangerousGetPixelRowMemory(y).Span, previous);
6065
}
6166

62-
PixelOperations<TPixel>.Instance.ToVector4(configuration, currentFrame.DangerousGetPixelRowMemory(y).Span, current, PixelConversionModifiers.Scale);
63-
64-
ref Vector256<float> previousBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(previous));
65-
ref Vector256<float> currentBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(current));
66-
ref Vector256<float> resultBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(result));
67+
PixelOperations<TPixel>.Instance.ToRgba32(configuration, currentFrame.DangerousGetPixelRowMemory(y).Span, current);
6768

68-
Vector256<float> replacement256 = Vector256.Create(replacement.X, replacement.Y, replacement.Z, replacement.W, replacement.X, replacement.Y, replacement.Z, replacement.W);
69+
if (nextFrame != null)
70+
{
71+
PixelOperations<TPixel>.Instance.ToRgba32(configuration, nextFrame.DangerousGetPixelRowMemory(y).Span, next);
72+
}
6973

70-
int size = Unsafe.SizeOf<Vector4>();
74+
ref Vector256<byte> previousBase = ref Unsafe.As<Rgba32, Vector256<byte>>(ref MemoryMarshal.GetReference(previous));
75+
ref Vector256<byte> currentBase = ref Unsafe.As<Rgba32, Vector256<byte>>(ref MemoryMarshal.GetReference(current));
76+
ref Vector256<byte> nextBase = ref Unsafe.As<Rgba32, Vector256<byte>>(ref MemoryMarshal.GetReference(next));
77+
ref Vector256<byte> resultBase = ref Unsafe.As<Rgba32, Vector256<byte>>(ref MemoryMarshal.GetReference(result));
7178

72-
bool hasRowDiff = false;
7379
int i = 0;
7480
uint x = 0;
81+
bool hasRowDiff = false;
7582
int length = current.Length;
7683
int remaining = current.Length;
7784

78-
while (Avx2.IsSupported && remaining >= 2)
85+
if (Avx2.IsSupported && remaining >= 8)
7986
{
80-
Vector256<float> p = Unsafe.Add(ref previousBase, x);
81-
Vector256<float> c = Unsafe.Add(ref currentBase, x);
82-
83-
// Compare the previous and current pixels
84-
Vector256<int> mask = Avx2.CompareEqual(p.AsInt32(), c.AsInt32());
85-
mask = Avx2.CompareEqual(mask.AsInt64(), Vector256<long>.AllBitsSet).AsInt32();
86-
mask = Avx2.And(mask, Avx2.Shuffle(mask, 0b_01_00_11_10)).AsInt32();
87-
88-
Vector256<int> neq = Avx2.Xor(mask.AsInt64(), Vector256<long>.AllBitsSet).AsInt32();
89-
int m = Avx2.MoveMask(neq.AsByte());
90-
if (m != 0)
87+
Vector256<uint> r256 = previousFrame != null ? Vector256.Create(bg.PackedValue) : Vector256<uint>.Zero;
88+
Vector256<uint> vmb256 = Vector256<uint>.Zero;
89+
if (blend)
9190
{
92-
// If is diff is found, the left side is marked by the min of previously found left side and the start position.
93-
// The right is the max of the previously found right side and the end position.
94-
int start = i + (BitOperations.TrailingZeroCount(m) / size);
95-
int end = i + (2 - (BitOperations.LeadingZeroCount((uint)m) / size));
96-
left = Math.Min(left, start);
97-
right = Math.Max(right, end);
98-
hasRowDiff = true;
99-
hasDiff = true;
91+
vmb256 = Avx2.CompareEqual(vmb256, vmb256);
10092
}
10193

102-
// Replace the pixel value with the replacement if the full pixel is matched.
103-
Vector256<float> r = Avx.BlendVariable(c, replacement256, mask.AsSingle());
104-
Unsafe.Add(ref resultBase, x) = r;
105-
106-
x++;
107-
i += 2;
108-
remaining -= 2;
94+
while (remaining >= 8)
95+
{
96+
Vector256<uint> p = Unsafe.Add(ref previousBase, x).AsUInt32();
97+
Vector256<uint> c = Unsafe.Add(ref currentBase, x).AsUInt32();
98+
99+
Vector256<uint> eq = Avx2.CompareEqual(p, c);
100+
Vector256<uint> r = Avx2.BlendVariable(c, r256, Avx2.And(eq, vmb256));
101+
102+
if (nextFrame != null)
103+
{
104+
Vector256<int> n = Avx2.ShiftRightLogical(Unsafe.Add(ref nextBase, x).AsUInt32(), 24).AsInt32();
105+
eq = Avx2.AndNot(Avx2.CompareGreaterThan(Avx2.ShiftRightLogical(c, 24).AsInt32(), n).AsUInt32(), eq);
106+
}
107+
108+
Unsafe.Add(ref resultBase, x) = r.AsByte();
109+
110+
uint msk = (uint)Avx2.MoveMask(eq.AsByte());
111+
msk = ~msk;
112+
113+
if (msk != 0)
114+
{
115+
// If is diff is found, the left side is marked by the min of previously found left side and the start position.
116+
// The right is the max of the previously found right side and the end position.
117+
int start = i + (BitOperations.TrailingZeroCount(msk) / sizeof(uint));
118+
int end = i + (8 - (BitOperations.LeadingZeroCount(msk) / sizeof(uint)));
119+
left = Math.Min(left, start);
120+
right = Math.Max(right, end);
121+
hasRowDiff = true;
122+
hasDiff = true;
123+
}
124+
125+
x++;
126+
i += 8;
127+
remaining -= 8;
128+
}
109129
}
110130

111131
for (i = remaining; i > 0; i--)
112132
{
113133
x = (uint)(length - i);
114134

115-
Vector4 p = Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref previousBase), x);
116-
Vector4 c = Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref currentBase), x);
117-
ref Vector4 r = ref Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref resultBase), x);
135+
Rgba32 p = Unsafe.Add(ref MemoryMarshal.GetReference(previous), x);
136+
Rgba32 c = Unsafe.Add(ref MemoryMarshal.GetReference(current), x);
137+
Rgba32 n = Unsafe.Add(ref MemoryMarshal.GetReference(next), x);
138+
ref Rgba32 r = ref Unsafe.Add(ref MemoryMarshal.GetReference(result), x);
118139

119-
if (p != c)
120-
{
121-
r = c;
140+
bool peq = c.Rgba == (previousFrame != null ? p.Rgba : bg.Rgba);
141+
Rgba32 val = (blend & peq) ? replacement : c;
142+
143+
peq &= nextFrame == null || (n.Rgba >> 24 >= c.Rgba >> 24);
144+
r = val;
122145

146+
if (!peq)
147+
{
123148
// If is diff is found, the left side is marked by the min of previously found left side and the diff position.
124149
// The right is the max of the previously found right side and the diff position + 1.
125150
left = Math.Min(left, (int)x);
126151
right = Math.Max(right, (int)x + 1);
127152
hasRowDiff = true;
128153
hasDiff = true;
129154
}
130-
else
131-
{
132-
r = replacement;
133-
}
134155
}
135156

136157
if (hasRowDiff)
@@ -143,7 +164,7 @@ public static (bool Difference, Rectangle Bounds) DeDuplicatePixels<TPixel>(
143164
bottom = y + 1;
144165
}
145166

146-
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span, PixelConversionModifiers.Scale);
167+
PixelOperations<TPixel>.Instance.FromRgba32(configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span);
147168
}
148169

149170
Rectangle bounds = Rectangle.FromLTRB(
@@ -163,19 +184,6 @@ public static (bool Difference, Rectangle Bounds) DeDuplicatePixels<TPixel>(
163184

164185
return (hasDiff, bounds);
165186
}
166-
167-
public static void CopySource<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Rectangle bounds)
168-
where TPixel : unmanaged, IPixel<TPixel>
169-
{
170-
Buffer2DRegion<TPixel> sourceBuffer = source.PixelBuffer.GetRegion(bounds);
171-
Buffer2DRegion<TPixel> destBuffer = destination.PixelBuffer.GetRegion(bounds);
172-
for (int y = 0; y < destBuffer.Height; y++)
173-
{
174-
Span<TPixel> sourceRow = sourceBuffer.DangerousGetRowSpan(y);
175-
Span<TPixel> destRow = destBuffer.DangerousGetRowSpan(y);
176-
sourceRow.CopyTo(destRow);
177-
}
178-
}
179187
}
180188

181189
#pragma warning disable SA1201 // Elements should appear in the correct order

src/ImageSharp/Formats/Gif/GifEncoderCore.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,10 +264,13 @@ private void EncodeAdditionalFrames<TPixel>(
264264
hasPaletteQuantizer = true;
265265
}
266266

267+
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
268+
267269
this.EncodeAdditionalFrame(
268270
stream,
269271
previousFrame,
270272
currentFrame,
273+
nextFrame,
271274
encodingFrame,
272275
useLocal,
273276
gifMetadata,
@@ -311,6 +314,7 @@ private void EncodeAdditionalFrame<TPixel>(
311314
Stream stream,
312315
ImageFrame<TPixel> previousFrame,
313316
ImageFrame<TPixel> currentFrame,
317+
ImageFrame<TPixel>? nextFrame,
314318
ImageFrame<TPixel> encodingFrame,
315319
bool useLocal,
316320
GifFrameMetadata metadata,
@@ -325,7 +329,15 @@ private void EncodeAdditionalFrame<TPixel>(
325329
ImageFrame<TPixel>? previous = previousDisposal == GifDisposalMethod.RestoreToBackground ? null : previousFrame;
326330

327331
// Deduplicate and quantize the frame capturing only required parts.
328-
(bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(this.configuration, previous, currentFrame, encodingFrame, Vector4.Zero);
332+
(bool difference, Rectangle bounds) =
333+
AnimationUtilities.DeDuplicatePixels(
334+
this.configuration,
335+
previous,
336+
currentFrame,
337+
nextFrame,
338+
encodingFrame,
339+
Color.Transparent,
340+
true);
329341

330342
using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
331343
encodingFrame,

src/ImageSharp/Formats/Png/PngEncoderCore.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Buffers;
55
using System.Buffers.Binary;
6-
using System.Numerics;
76
using System.Runtime.CompilerServices;
87
using System.Runtime.InteropServices;
98
using SixLabors.ImageSharp.Common.Helpers;
@@ -208,21 +207,22 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
208207

209208
for (int i = 1; i < image.Frames.Count; i++)
210209
{
211-
currentFrame = image.Frames[i];
212-
frameMetadata = GetPngFrameMetadata(currentFrame);
213-
214210
ImageFrame<TPixel>? prev = previousDisposal == PngDisposalMethod.RestoreToBackground ? null : previousFrame;
215-
(bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(image.Configuration, prev, currentFrame, encodingFrame, Vector4.Zero);
211+
currentFrame = image.Frames[i];
212+
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
216213

217-
if (difference && previousDisposal != PngDisposalMethod.RestoreToBackground)
218-
{
219-
if (frameMetadata.BlendMethod == PngBlendMethod.Source)
220-
{
221-
// We've potentially introduced transparency within our area of interest
222-
// so we need to overwrite the changed area with the full data.
223-
AnimationUtilities.CopySource(currentFrame, encodingFrame, bounds);
224-
}
225-
}
214+
frameMetadata = GetPngFrameMetadata(currentFrame);
215+
bool blend = frameMetadata.BlendMethod == PngBlendMethod.Over;
216+
217+
(bool difference, Rectangle bounds) =
218+
AnimationUtilities.DeDuplicatePixels(
219+
image.Configuration,
220+
prev,
221+
currentFrame,
222+
nextFrame,
223+
encodingFrame,
224+
Color.Transparent,
225+
blend);
226226

227227
if (clearTransparency)
228228
{

src/ImageSharp/Formats/Webp/WebpEncoderCore.cs

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4-
using System.Numerics;
54
using SixLabors.ImageSharp.Formats.Webp.Lossless;
65
using SixLabors.ImageSharp.Formats.Webp.Lossy;
76
using SixLabors.ImageSharp.Memory;
@@ -161,22 +160,23 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
161160

162161
for (int i = 1; i < image.Frames.Count; i++)
163162
{
164-
ImageFrame<TPixel> currentFrame = image.Frames[i];
165-
frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame);
166-
167163
ImageFrame<TPixel>? prev = previousDisposal == WebpDisposalMethod.RestoreToBackground ? null : previousFrame;
164+
ImageFrame<TPixel> currentFrame = image.Frames[i];
165+
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
168166

169-
(bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(image.Configuration, prev, currentFrame, encodingFrame, Vector4.Zero, ClampingMode.Even);
170-
171-
if (difference && previousDisposal != WebpDisposalMethod.RestoreToBackground)
172-
{
173-
if (frameMetadata.BlendMethod == WebpBlendMethod.Source)
174-
{
175-
// We've potentially introduced transparency within our area of interest
176-
// so we need to overwrite the changed area with the full data.
177-
AnimationUtilities.CopySource(currentFrame, encodingFrame, bounds);
178-
}
179-
}
167+
frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame);
168+
bool blend = frameMetadata.BlendMethod == WebpBlendMethod.Over;
169+
170+
(bool difference, Rectangle bounds) =
171+
AnimationUtilities.DeDuplicatePixels(
172+
image.Configuration,
173+
prev,
174+
currentFrame,
175+
nextFrame,
176+
encodingFrame,
177+
Color.Transparent,
178+
blend,
179+
ClampingMode.Even);
180180

181181
using Vp8LEncoder animatedEncoder = new(
182182
this.memoryAllocator,
@@ -232,21 +232,23 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
232232

233233
for (int i = 1; i < image.Frames.Count; i++)
234234
{
235+
ImageFrame<TPixel>? prev = previousDisposal == WebpDisposalMethod.RestoreToBackground ? null : previousFrame;
235236
ImageFrame<TPixel> currentFrame = image.Frames[i];
236-
frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame);
237+
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
237238

238-
ImageFrame<TPixel>? prev = previousDisposal == WebpDisposalMethod.RestoreToBackground ? null : previousFrame;
239-
(bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(image.Configuration, prev, currentFrame, encodingFrame, Vector4.Zero, ClampingMode.Even);
240-
241-
if (difference && previousDisposal != WebpDisposalMethod.RestoreToBackground)
242-
{
243-
if (frameMetadata.BlendMethod == WebpBlendMethod.Source)
244-
{
245-
// We've potentially introduced transparency within our area of interest
246-
// so we need to overwrite the changed area with the full data.
247-
AnimationUtilities.CopySource(currentFrame, encodingFrame, bounds);
248-
}
249-
}
239+
frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame);
240+
bool blend = frameMetadata.BlendMethod == WebpBlendMethod.Over;
241+
242+
(bool difference, Rectangle bounds) =
243+
AnimationUtilities.DeDuplicatePixels(
244+
image.Configuration,
245+
prev,
246+
currentFrame,
247+
nextFrame,
248+
encodingFrame,
249+
Color.Transparent,
250+
blend,
251+
ClampingMode.Even);
250252

251253
using Vp8Encoder animatedEncoder = new(
252254
this.memoryAllocator,

0 commit comments

Comments
 (0)