Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add smart animated format conversion. #2588

Merged
merged 28 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
328e046
Wire up connectors and gif encoder
JimBobSquarePants Nov 15, 2023
a486558
Complete Webp and add tests
JimBobSquarePants Nov 17, 2023
7f4b457
Default loop count should be 1
JimBobSquarePants Nov 17, 2023
aea44fa
Merge branch 'main' into js/animation-synergy
JimBobSquarePants Nov 20, 2023
958c9c9
Deduper works
JimBobSquarePants Nov 21, 2023
a42f6b6
Enable dedup for png
JimBobSquarePants Nov 23, 2023
cc0727b
Add dedup to webp
JimBobSquarePants Nov 23, 2023
4b852e6
Update tests, fix issues
JimBobSquarePants Nov 24, 2023
4f8ea7f
Tweak bounds clamping
JimBobSquarePants Nov 24, 2023
78b1cad
Merge branch 'main' into js/animation-synergy
JimBobSquarePants Nov 25, 2023
c306c56
Use correct buffer dimensions
JimBobSquarePants Nov 25, 2023
6cda7b0
Fix scanline lengths
JimBobSquarePants Nov 26, 2023
90fa817
Update PngDecoderCore.cs
JimBobSquarePants Nov 26, 2023
4029b15
Remove duplicate condition check
JimBobSquarePants Nov 26, 2023
44b0311
Remove "new"
JimBobSquarePants Nov 26, 2023
55e69c7
Try disabling new high memory tests on failing platforms.
JimBobSquarePants Nov 26, 2023
a6f96f7
Optimize and fix deduper
JimBobSquarePants Nov 28, 2023
6bfabe9
Reduce memory pressure on gif decoder tests
JimBobSquarePants Nov 28, 2023
9677be0
More memory pressure reductions
JimBobSquarePants Nov 28, 2023
35d55b5
More memory pressure reduction
JimBobSquarePants Nov 28, 2023
e35e9a8
Reduce memory usage in pixel map
JimBobSquarePants Nov 29, 2023
5da17f3
Enable Sse2, simplify
JimBobSquarePants Nov 29, 2023
10cd3d5
Update AnimationUtilities.cs
JimBobSquarePants Nov 29, 2023
f4dc0fc
Disable Sse2 for now.
JimBobSquarePants Nov 29, 2023
43c8614
Fix Sse2 offset
JimBobSquarePants Nov 29, 2023
cf4106b
Add note
JimBobSquarePants Nov 29, 2023
3424efa
Merge branch 'main' into js/animation-synergy
JimBobSquarePants Nov 29, 2023
3b759a5
Merge branch 'main' into js/animation-synergy
JimBobSquarePants Nov 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
Expand Down Expand Up @@ -656,6 +657,36 @@ public static Vector128<byte> BlendVariable(Vector128<byte> left, Vector128<byte
return AdvSimd.BitwiseSelect(signedMask, right.AsInt16(), left.AsInt16()).AsByte();
}

/// <summary>
/// Blend packed 32-bit unsigned integers from <paramref name="left"/> and <paramref name="right"/> using <paramref name="mask"/>.
/// The high bit of each corresponding <paramref name="mask"/> byte determines the selection.
/// If the high bit is set the element of <paramref name="left"/> is selected.
/// The element of <paramref name="right"/> is selected otherwise.
/// </summary>
/// <param name="left">The left vector.</param>
/// <param name="right">The right vector.</param>
/// <param name="mask">The mask vector.</param>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<uint> BlendVariable(Vector128<uint> left, Vector128<uint> right, Vector128<uint> mask)
=> BlendVariable(left.AsByte(), right.AsByte(), mask.AsByte()).AsUInt32();

/// <summary>
/// Count the number of leading zero bits in a mask.
/// Similar in behavior to the x86 instruction LZCNT.
/// </summary>
/// <param name="value">The value.</param>
public static ushort LeadingZeroCount(ushort value)
=> (ushort)(BitOperations.LeadingZeroCount(value) - 16);

/// <summary>
/// Count the number of trailing zero bits in an integer value.
/// Similar in behavior to the x86 instruction TZCNT.
/// </summary>
/// <param name="value">The value.</param>
public static ushort TrailingZeroCount(ushort value)
=> (ushort)(BitOperations.TrailingZeroCount(value << 16) - 16);

/// <summary>
/// <see cref="ByteToNormalizedFloat"/> as many elements as possible, slicing them down (keeping the remainder).
/// </summary>
Expand Down
172 changes: 113 additions & 59 deletions src/ImageSharp/Formats/AnimationUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,31 @@ internal static class AnimationUtilities
/// <param name="configuration">The configuration.</param>
/// <param name="previousFrame">The previous frame if present.</param>
/// <param name="currentFrame">The current frame.</param>
/// <param name="nextFrame">The next frame if present.</param>
/// <param name="resultFrame">The resultant output.</param>
/// <param name="replacement">The value to use when replacing duplicate pixels.</param>
/// <param name="blend">Whether the resultant frame represents an animation blend.</param>
/// <param name="clampingMode">The clamping bound to apply when calculating difference bounds.</param>
/// <returns>The <see cref="ValueTuple{Boolean, Rectangle}"/> representing the operation result.</returns>
public static (bool Difference, Rectangle Bounds) DeDuplicatePixels<TPixel>(
Configuration configuration,
ImageFrame<TPixel>? previousFrame,
ImageFrame<TPixel> currentFrame,
ImageFrame<TPixel>? nextFrame,
ImageFrame<TPixel> resultFrame,
Vector4 replacement,
Color replacement,
bool blend,
ClampingMode clampingMode = ClampingMode.None)
where TPixel : unmanaged, IPixel<TPixel>
{
// TODO: This would be faster (but more complicated to find diff bounds) if we operated on Rgba32.
// If someone wants to do that, they have my unlimited thanks.
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
IMemoryOwner<Vector4> buffers = memoryAllocator.Allocate<Vector4>(currentFrame.Width * 3, AllocationOptions.Clean);
Span<Vector4> previous = buffers.GetSpan()[..currentFrame.Width];
Span<Vector4> current = buffers.GetSpan().Slice(currentFrame.Width, currentFrame.Width);
Span<Vector4> result = buffers.GetSpan()[(currentFrame.Width * 2)..];
using IMemoryOwner<Rgba32> buffers = memoryAllocator.Allocate<Rgba32>(currentFrame.Width * 4, AllocationOptions.Clean);
Span<Rgba32> previous = buffers.GetSpan()[..currentFrame.Width];
Span<Rgba32> current = buffers.GetSpan().Slice(currentFrame.Width, currentFrame.Width);
Span<Rgba32> next = buffers.GetSpan().Slice(currentFrame.Width * 2, currentFrame.Width);
Span<Rgba32> result = buffers.GetSpan()[(currentFrame.Width * 3)..];

Rgba32 bg = replacement;

int top = int.MinValue;
int bottom = int.MaxValue;
Expand All @@ -56,83 +61,145 @@ internal static class AnimationUtilities
{
if (previousFrame != null)
{
PixelOperations<TPixel>.Instance.ToVector4(configuration, previousFrame.DangerousGetPixelRowMemory(y).Span, previous, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.ToRgba32(configuration, previousFrame.DangerousGetPixelRowMemory(y).Span, previous);
}

PixelOperations<TPixel>.Instance.ToVector4(configuration, currentFrame.DangerousGetPixelRowMemory(y).Span, current, PixelConversionModifiers.Scale);

ref Vector256<float> previousBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(previous));
ref Vector256<float> currentBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(current));
ref Vector256<float> resultBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(result));
PixelOperations<TPixel>.Instance.ToRgba32(configuration, currentFrame.DangerousGetPixelRowMemory(y).Span, current);

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

int size = Unsafe.SizeOf<Vector4>();
ref Vector256<byte> previousBase256 = ref Unsafe.As<Rgba32, Vector256<byte>>(ref MemoryMarshal.GetReference(previous));
ref Vector256<byte> currentBase256 = ref Unsafe.As<Rgba32, Vector256<byte>>(ref MemoryMarshal.GetReference(current));
ref Vector256<byte> nextBase256 = ref Unsafe.As<Rgba32, Vector256<byte>>(ref MemoryMarshal.GetReference(next));
ref Vector256<byte> resultBase256 = ref Unsafe.As<Rgba32, Vector256<byte>>(ref MemoryMarshal.GetReference(result));

bool hasRowDiff = false;
int i = 0;
uint x = 0;
bool hasRowDiff = false;
int length = current.Length;
int remaining = current.Length;
if (Avx2.IsSupported && remaining >= 2)

if (Avx2.IsSupported && remaining >= 8)
{
while (remaining >= 2)
Vector256<uint> r256 = previousFrame != null ? Vector256.Create(bg.PackedValue) : Vector256<uint>.Zero;
Vector256<uint> vmb256 = Vector256<uint>.Zero;
if (blend)
{
Vector256<float> p = Unsafe.Add(ref previousBase, x);
Vector256<float> c = Unsafe.Add(ref currentBase, x);
vmb256 = Avx2.CompareEqual(vmb256, vmb256);
}

while (remaining >= 8)
{
Vector256<uint> p = Unsafe.Add(ref previousBase256, x).AsUInt32();
Vector256<uint> c = Unsafe.Add(ref currentBase256, x).AsUInt32();

// Compare the previous and current pixels
Vector256<int> mask = Avx2.CompareEqual(p.AsInt32(), c.AsInt32());
mask = Avx2.CompareEqual(mask.AsInt64(), Vector256<long>.AllBitsSet).AsInt32();
mask = Avx2.And(mask, Avx2.Shuffle(mask, 0b_01_00_11_10)).AsInt32();
Vector256<uint> eq = Avx2.CompareEqual(p, c);
Vector256<uint> r = Avx2.BlendVariable(c, r256, Avx2.And(eq, vmb256));

Vector256<int> neq = Avx2.Xor(mask.AsInt64(), Vector256<long>.AllBitsSet).AsInt32();
int m = Avx2.MoveMask(neq.AsByte());
if (m != 0)
if (nextFrame != null)
{
Vector256<int> n = Avx2.ShiftRightLogical(Unsafe.Add(ref nextBase256, x).AsUInt32(), 24).AsInt32();
eq = Avx2.AndNot(Avx2.CompareGreaterThan(Avx2.ShiftRightLogical(c, 24).AsInt32(), n).AsUInt32(), eq);
}

Unsafe.Add(ref resultBase256, x) = r.AsByte();

uint msk = (uint)Avx2.MoveMask(eq.AsByte());
msk = ~msk;

if (msk != 0)
{
// If is diff is found, the left side is marked by the min of previously found left side and the start position.
// The right is the max of the previously found right side and the end position.
int start = i + (BitOperations.TrailingZeroCount(m) / size);
int end = i + (2 - (BitOperations.LeadingZeroCount((uint)m) / size));
int start = i + (BitOperations.TrailingZeroCount(msk) / sizeof(uint));
int end = i + (8 - (BitOperations.LeadingZeroCount(msk) / sizeof(uint)));
left = Math.Min(left, start);
right = Math.Max(right, end);
hasRowDiff = true;
hasDiff = true;
}

// Replace the pixel value with the replacement if the full pixel is matched.
Vector256<float> r = Avx.BlendVariable(c, replacement256, mask.AsSingle());
Unsafe.Add(ref resultBase, x) = r;
x++;
i += 8;
remaining -= 8;
}
}

if (Sse2.IsSupported && remaining >= 4)
{
// Update offset since we may be operating on the remainder previously incremented by pixel steps of 8.
x *= 2;
Vector128<uint> r128 = previousFrame != null ? Vector128.Create(bg.PackedValue) : Vector128<uint>.Zero;
Vector128<uint> vmb128 = Vector128<uint>.Zero;
if (blend)
{
vmb128 = Sse2.CompareEqual(vmb128, vmb128);
}

while (remaining >= 4)
{
Vector128<uint> p = Unsafe.Add(ref Unsafe.As<Vector256<byte>, Vector128<uint>>(ref previousBase256), x);
Vector128<uint> c = Unsafe.Add(ref Unsafe.As<Vector256<byte>, Vector128<uint>>(ref currentBase256), x);

Vector128<uint> eq = Sse2.CompareEqual(p, c);
Vector128<uint> r = SimdUtils.HwIntrinsics.BlendVariable(c, r128, Sse2.And(eq, vmb128));

if (nextFrame != null)
{
Vector128<int> n = Sse2.ShiftRightLogical(Unsafe.Add(ref Unsafe.As<Vector256<byte>, Vector128<uint>>(ref nextBase256), x), 24).AsInt32();
eq = Sse2.AndNot(Sse2.CompareGreaterThan(Sse2.ShiftRightLogical(c, 24).AsInt32(), n).AsUInt32(), eq);
}

Unsafe.Add(ref Unsafe.As<Vector256<byte>, Vector128<uint>>(ref resultBase256), x) = r;

ushort msk = (ushort)(uint)Sse2.MoveMask(eq.AsByte());
msk = (ushort)~msk;
if (msk != 0)
{
// If is diff is found, the left side is marked by the min of previously found left side and the start position.
// The right is the max of the previously found right side and the end position.
int start = i + (SimdUtils.HwIntrinsics.TrailingZeroCount(msk) / sizeof(uint));
int end = i + (4 - (SimdUtils.HwIntrinsics.LeadingZeroCount(msk) / sizeof(uint)));
left = Math.Min(left, start);
right = Math.Max(right, end);
hasRowDiff = true;
hasDiff = true;
}

x++;
i += 2;
remaining -= 2;
i += 4;
remaining -= 4;
}
}

// TODO: v4 AdvSimd when we can use .NET 8
for (i = remaining; i > 0; i--)
{
x = (uint)(length - i);

Vector4 p = Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref previousBase), x);
Vector4 c = Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref currentBase), x);
ref Vector4 r = ref Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref resultBase), x);
Rgba32 p = Unsafe.Add(ref MemoryMarshal.GetReference(previous), x);
Rgba32 c = Unsafe.Add(ref MemoryMarshal.GetReference(current), x);
Rgba32 n = Unsafe.Add(ref MemoryMarshal.GetReference(next), x);
ref Rgba32 r = ref Unsafe.Add(ref MemoryMarshal.GetReference(result), x);

if (p != c)
{
r = c;
bool peq = c.Rgba == (previousFrame != null ? p.Rgba : bg.Rgba);
Rgba32 val = (blend & peq) ? replacement : c;

peq &= nextFrame == null || (n.Rgba >> 24 >= c.Rgba >> 24);
r = val;

if (!peq)
{
// If is diff is found, the left side is marked by the min of previously found left side and the diff position.
// The right is the max of the previously found right side and the diff position + 1.
left = Math.Min(left, (int)x);
right = Math.Max(right, (int)x + 1);
hasRowDiff = true;
hasDiff = true;
}
else
{
r = replacement;
}
}

if (hasRowDiff)
Expand All @@ -145,7 +212,7 @@ internal static class AnimationUtilities
bottom = y + 1;
}

PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.FromRgba32(configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span);
}

Rectangle bounds = Rectangle.FromLTRB(
Expand All @@ -163,20 +230,7 @@ internal static class AnimationUtilities
bounds.Y = Math.Max(0, bounds.Y - (bounds.Y & 1));
}

return new(hasDiff, bounds);
}

public static void CopySource<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Rectangle bounds)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2DRegion<TPixel> sourceBuffer = source.PixelBuffer.GetRegion(bounds);
Buffer2DRegion<TPixel> destBuffer = destination.PixelBuffer.GetRegion(bounds);
for (int y = 0; y < destBuffer.Height; y++)
{
Span<TPixel> sourceRow = sourceBuffer.DangerousGetRowSpan(y);
Span<TPixel> destRow = destBuffer.DangerousGetRowSpan(y);
sourceRow.CopyTo(destRow);
}
return (hasDiff, bounds);
}
}

Expand Down
17 changes: 14 additions & 3 deletions src/ImageSharp/Formats/Gif/GifEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,15 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken

stream.WriteByte(GifConstants.EndIntroducer);

quantized.Dispose();
quantized?.Dispose();
}

private static GifMetadata GetGifMetadata<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif))
{
return gif;
return (GifMetadata)gif.DeepClone();
}

if (image.Metadata.TryGetPngMetadata(out PngMetadata? png))
Expand Down Expand Up @@ -251,6 +251,7 @@ private static GifFrameMetadata GetGifFrameMetadata<TPixel>(ImageFrame<TPixel> f
{
// Gather the metadata for this frame.
ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
bool useLocal = this.colorTableMode == GifColorTableMode.Local || (gifMetadata.ColorTableMode == GifColorTableMode.Local);

Expand All @@ -268,6 +269,7 @@ private static GifFrameMetadata GetGifFrameMetadata<TPixel>(ImageFrame<TPixel> f
stream,
previousFrame,
currentFrame,
nextFrame,
encodingFrame,
useLocal,
gifMetadata,
Expand Down Expand Up @@ -311,6 +313,7 @@ private static GifFrameMetadata GetGifFrameMetadata<TPixel>(ImageFrame<TPixel> f
Stream stream,
ImageFrame<TPixel> previousFrame,
ImageFrame<TPixel> currentFrame,
ImageFrame<TPixel>? nextFrame,
ImageFrame<TPixel> encodingFrame,
bool useLocal,
GifFrameMetadata metadata,
Expand All @@ -325,7 +328,15 @@ private static GifFrameMetadata GetGifFrameMetadata<TPixel>(ImageFrame<TPixel> f
ImageFrame<TPixel>? previous = previousDisposal == GifDisposalMethod.RestoreToBackground ? null : previousFrame;

// Deduplicate and quantize the frame capturing only required parts.
(bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(this.configuration, previous, currentFrame, encodingFrame, Vector4.Zero);
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
this.configuration,
previous,
currentFrame,
nextFrame,
encodingFrame,
Color.Transparent,
true);

using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
encodingFrame,
Expand Down