Browse Source

Normalize PNG filter encoding

pull/3161/head
James Jackson-South 3 weeks ago
parent
commit
9d1aa66f67
  1. 98
      src/ImageSharp/Formats/Png/Filters/AverageFilter.cs
  2. 524
      src/ImageSharp/Formats/Png/Filters/IPngFilterOperator.cs
  3. 153
      src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
  4. 228
      src/ImageSharp/Formats/Png/Filters/PngFilterEncoder.cs
  5. 81
      src/ImageSharp/Formats/Png/Filters/SubFilter.cs
  6. 75
      src/ImageSharp/Formats/Png/Filters/UpFilter.cs
  7. 157
      tests/ImageSharp.Benchmarks/Codecs/Png/PngFilterEncode.cs
  8. 104
      tests/ImageSharp.Benchmarks/Codecs/Png/PngFilterEncodeAssembly.cs
  9. 470
      tests/ImageSharp.Benchmarks/Codecs/Png/PngFilterEncodeBaseline.cs
  10. 134
      tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs

98
src/ImageSharp/Formats/Png/Filters/AverageFilter.cs

@ -140,98 +140,12 @@ internal static class AverageFilter
/// <param name="sum">The sum of the total variance of the filtered row.</param> /// <param name="sum">The sum of the total variance of the filtered row.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> previousScanline, Span<byte> result, uint bytesPerPixel, out int sum) public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> previousScanline, Span<byte> result, uint bytesPerPixel, out int sum)
{ => PngFilterEncoder.Encode<AverageFilterOperator>(
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); scanline,
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); previousScanline,
result,
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); bytesPerPixel,
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); out sum);
ref byte resultBaseRef = ref MemoryMarshal.GetReference(result);
sum = 0;
// Average(x) = Raw(x) - floor((Raw(x-bpp)+Prior(x))/2)
resultBaseRef = (byte)FilterType.Average;
nuint x = 0;
for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte above = Unsafe.Add(ref prevBaseRef, x);
++x;
ref byte res = ref Unsafe.Add(ref resultBaseRef, x);
res = (byte)(scan - (above >> 1));
sum += Numerics.Abs(unchecked((sbyte)res));
}
if (Avx2.IsSupported)
{
Vector256<byte> zero = Vector256<byte>.Zero;
Vector256<int> sumAccumulator = Vector256<int>.Zero;
Vector256<byte> allBitsSet = Avx2.CompareEqual(sumAccumulator, sumAccumulator).AsByte();
for (nuint xLeft = x - bytesPerPixel; (int)x <= scanline.Length - Vector256<byte>.Count; xLeft += (uint)Vector256<byte>.Count)
{
Vector256<byte> scan = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, x));
Vector256<byte> left = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft));
Vector256<byte> above = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref prevBaseRef, x));
Vector256<byte> avg = Avx2.Xor(Avx2.Average(Avx2.Xor(left, allBitsSet), Avx2.Xor(above, allBitsSet)), allBitsSet);
Vector256<byte> res = Avx2.Subtract(scan, avg);
Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type
x += (uint)Vector256<byte>.Count;
sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32());
}
sum += Numerics.EvenReduceSum(sumAccumulator);
}
else if (Sse2.IsSupported)
{
Vector128<byte> zero = Vector128<byte>.Zero;
Vector128<int> sumAccumulator = Vector128<int>.Zero;
Vector128<byte> allBitsSet = Sse2.CompareEqual(sumAccumulator, sumAccumulator).AsByte();
for (nuint xLeft = x - bytesPerPixel; (int)x <= scanline.Length - Vector128<byte>.Count; xLeft += (uint)Vector128<byte>.Count)
{
Vector128<byte> scan = Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref scanBaseRef, x));
Vector128<byte> left = Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft));
Vector128<byte> above = Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref prevBaseRef, x));
Vector128<byte> avg = Sse2.Xor(Sse2.Average(Sse2.Xor(left, allBitsSet), Sse2.Xor(above, allBitsSet)), allBitsSet);
Vector128<byte> res = Sse2.Subtract(scan, avg);
Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type
x += (uint)Vector128<byte>.Count;
Vector128<byte> absRes;
if (Ssse3.IsSupported)
{
absRes = Ssse3.Abs(res.AsSByte());
}
else
{
Vector128<sbyte> mask = Sse2.CompareGreaterThan(zero.AsSByte(), res.AsSByte());
absRes = Sse2.Xor(Sse2.Add(res.AsSByte(), mask), mask).AsByte();
}
sumAccumulator = Sse2.Add(sumAccumulator, Sse2.SumAbsoluteDifferences(absRes, zero).AsInt32());
}
sum += Numerics.EvenReduceSum(sumAccumulator);
}
for (nuint xLeft = x - bytesPerPixel; x < (uint)scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte left = Unsafe.Add(ref scanBaseRef, xLeft);
byte above = Unsafe.Add(ref prevBaseRef, x);
++x;
ref byte res = ref Unsafe.Add(ref resultBaseRef, x);
res = (byte)(scan - Average(left, above));
sum += Numerics.Abs(unchecked((sbyte)res));
}
}
/// <summary> /// <summary>
/// Calculates the average value of two bytes /// Calculates the average value of two bytes

524
src/ImageSharp/Formats/Png/Filters/IPngFilterOperator.cs

@ -0,0 +1,524 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Png.Filters;
/// <summary>
/// Defines the scalar and SIMD mappings used by the shared PNG filter traversal.
/// </summary>
internal interface IPngFilterOperator
{
/// <summary>
/// Gets the filter type written to the leading result byte.
/// </summary>
static abstract FilterType Type { get; }
/// <summary>
/// Gets a value indicating whether the predictor reads the left component.
/// </summary>
static abstract bool UsesLeft { get; }
/// <summary>
/// Gets a value indicating whether the predictor reads the above component.
/// </summary>
static abstract bool UsesAbove { get; }
/// <summary>
/// Gets a value indicating whether the predictor reads the upper-left component.
/// </summary>
static abstract bool UsesUpperLeft { get; }
/// <summary>
/// Filters one byte from its PNG neighborhood.
/// </summary>
/// <param name="scan">The component being filtered.</param>
/// <param name="left">The corresponding component in the preceding pixel.</param>
/// <param name="above">The corresponding component in the preceding scanline.</param>
/// <param name="upperLeft">The preceding component in the preceding scanline.</param>
/// <returns>The filtered residual.</returns>
static abstract byte Invoke(byte scan, byte left, byte above, byte upperLeft);
/// <summary>
/// Filters sixteen byte lanes from their PNG neighborhoods.
/// </summary>
/// <param name="scan">The components being filtered.</param>
/// <param name="left">The corresponding components in the preceding pixels.</param>
/// <param name="above">The corresponding components in the preceding scanline.</param>
/// <param name="upperLeft">The preceding components in the preceding scanline.</param>
/// <returns>The filtered residuals.</returns>
static abstract Vector128<byte> Invoke(
Vector128<byte> scan,
Vector128<byte> left,
Vector128<byte> above,
Vector128<byte> upperLeft);
/// <summary>
/// Filters thirty-two byte lanes from their PNG neighborhoods.
/// </summary>
/// <param name="scan">The components being filtered.</param>
/// <param name="left">The corresponding components in the preceding pixels.</param>
/// <param name="above">The corresponding components in the preceding scanline.</param>
/// <param name="upperLeft">The preceding components in the preceding scanline.</param>
/// <returns>The filtered residuals.</returns>
static abstract Vector256<byte> Invoke(
Vector256<byte> scan,
Vector256<byte> left,
Vector256<byte> above,
Vector256<byte> upperLeft);
/// <summary>
/// Filters sixty-four byte lanes from their PNG neighborhoods.
/// </summary>
/// <param name="scan">The components being filtered.</param>
/// <param name="left">The corresponding components in the preceding pixels.</param>
/// <param name="above">The corresponding components in the preceding scanline.</param>
/// <param name="upperLeft">The preceding components in the preceding scanline.</param>
/// <returns>The filtered residuals.</returns>
static abstract Vector512<byte> Invoke(
Vector512<byte> scan,
Vector512<byte> left,
Vector512<byte> above,
Vector512<byte> upperLeft);
}
/// <summary>
/// Maps each component to its difference from the corresponding component in the preceding pixel.
/// </summary>
internal readonly struct SubFilterOperator : IPngFilterOperator
{
/// <inheritdoc />
public static FilterType Type => FilterType.Sub;
/// <inheritdoc />
public static bool UsesLeft => true;
/// <inheritdoc />
public static bool UsesAbove => false;
/// <inheritdoc />
public static bool UsesUpperLeft => false;
/// <inheritdoc />
[MethodImpl(InliningOptions.AlwaysInline)]
public static byte Invoke(byte scan, byte left, byte above, byte upperLeft) => (byte)(scan - left);
/// <inheritdoc />
[MethodImpl(InliningOptions.AlwaysInline)]
public static Vector128<byte> Invoke(
Vector128<byte> scan,
Vector128<byte> left,
Vector128<byte> above,
Vector128<byte> upperLeft)
=> scan - left;
/// <inheritdoc />
[MethodImpl(InliningOptions.AlwaysInline)]
public static Vector256<byte> Invoke(
Vector256<byte> scan,
Vector256<byte> left,
Vector256<byte> above,
Vector256<byte> upperLeft)
=> scan - left;
/// <inheritdoc />
[MethodImpl(InliningOptions.AlwaysInline)]
public static Vector512<byte> Invoke(
Vector512<byte> scan,
Vector512<byte> left,
Vector512<byte> above,
Vector512<byte> upperLeft)
=> scan - left;
}
/// <summary>
/// Maps each component to its difference from the component directly above it.
/// </summary>
internal readonly struct UpFilterOperator : IPngFilterOperator
{
/// <inheritdoc />
public static FilterType Type => FilterType.Up;
/// <inheritdoc />
public static bool UsesLeft => false;
/// <inheritdoc />
public static bool UsesAbove => true;
/// <inheritdoc />
public static bool UsesUpperLeft => false;
/// <inheritdoc />
[MethodImpl(InliningOptions.AlwaysInline)]
public static byte Invoke(byte scan, byte left, byte above, byte upperLeft) => (byte)(scan - above);
/// <inheritdoc />
[MethodImpl(InliningOptions.AlwaysInline)]
public static Vector128<byte> Invoke(
Vector128<byte> scan,
Vector128<byte> left,
Vector128<byte> above,
Vector128<byte> upperLeft)
=> scan - above;
/// <inheritdoc />
[MethodImpl(InliningOptions.AlwaysInline)]
public static Vector256<byte> Invoke(
Vector256<byte> scan,
Vector256<byte> left,
Vector256<byte> above,
Vector256<byte> upperLeft)
=> scan - above;
/// <inheritdoc />
[MethodImpl(InliningOptions.AlwaysInline)]
public static Vector512<byte> Invoke(
Vector512<byte> scan,
Vector512<byte> left,
Vector512<byte> above,
Vector512<byte> upperLeft)
=> scan - above;
}
/// <summary>
/// Maps each component to its difference from the truncated average of its left and above neighbors.
/// </summary>
internal readonly struct AverageFilterOperator : IPngFilterOperator
{
/// <inheritdoc />
public static FilterType Type => FilterType.Average;
/// <inheritdoc />
public static bool UsesLeft => true;
/// <inheritdoc />
public static bool UsesAbove => true;
/// <inheritdoc />
public static bool UsesUpperLeft => false;
/// <inheritdoc />
[MethodImpl(InliningOptions.AlwaysInline)]
public static byte Invoke(byte scan, byte left, byte above, byte upperLeft)
=> (byte)(scan - ((left + above) >> 1));
/// <inheritdoc />
[MethodImpl(InliningOptions.AlwaysInline)]
public static Vector128<byte> Invoke(
Vector128<byte> scan,
Vector128<byte> left,
Vector128<byte> above,
Vector128<byte> upperLeft)
{
Vector128<byte> average;
if (Sse2.IsSupported)
{
// PAVG rounds upward. Complementing both inputs and the result converts
// that rounding into the floor((left + above) / 2) required by PNG.
average = ~Sse2.Average(~left, ~above);
}
else if (AdvSimd.IsSupported)
{
// ARM's halving add truncates directly and therefore needs no correction.
average = AdvSimd.FusedAddHalving(left, above);
}
else
{
// Portable 128-bit backends use the carry-free average identity. Shared
// bits supply the integer part while differing bits supply half the remainder.
average = (left & above) + Vector128.ShiftRightLogical(left ^ above, 1);
}
return scan - average;
}
/// <inheritdoc />
[MethodImpl(InliningOptions.AlwaysInline)]
public static Vector256<byte> Invoke(
Vector256<byte> scan,
Vector256<byte> left,
Vector256<byte> above,
Vector256<byte> upperLeft)
=> scan - ~Avx2.Average(~left, ~above);
/// <inheritdoc />
[MethodImpl(InliningOptions.AlwaysInline)]
public static Vector512<byte> Invoke(
Vector512<byte> scan,
Vector512<byte> left,
Vector512<byte> above,
Vector512<byte> upperLeft)
=> scan - ~Avx512BW.Average(~left, ~above);
}
/// <summary>
/// Maps each component to its difference from the nearest Paeth neighbor.
/// </summary>
internal readonly struct PaethFilterOperator : IPngFilterOperator
{
/// <inheritdoc />
public static FilterType Type => FilterType.Paeth;
/// <inheritdoc />
public static bool UsesLeft => true;
/// <inheritdoc />
public static bool UsesAbove => true;
/// <inheritdoc />
public static bool UsesUpperLeft => true;
/// <inheritdoc />
[MethodImpl(InliningOptions.AlwaysInline)]
public static byte Invoke(byte scan, byte left, byte above, byte upperLeft)
{
int p = left + above - upperLeft;
int distanceLeft = Numerics.Abs(p - left);
int distanceAbove = Numerics.Abs(p - above);
int distanceUpperLeft = Numerics.Abs(p - upperLeft);
// PNG resolves equal distances in left, above, upper-left order.
byte predictor = distanceLeft <= distanceAbove && distanceLeft <= distanceUpperLeft
? left
: distanceAbove <= distanceUpperLeft ? above : upperLeft;
return (byte)(scan - predictor);
}
/// <inheritdoc />
[MethodImpl(InliningOptions.AlwaysInline)]
public static Vector128<byte> Invoke(
Vector128<byte> scan,
Vector128<byte> left,
Vector128<byte> above,
Vector128<byte> upperLeft)
{
Vector128<byte> predictor = Predict(left, above, upperLeft);
return scan - predictor;
}
/// <inheritdoc />
[MethodImpl(InliningOptions.AlwaysInline)]
public static Vector256<byte> Invoke(
Vector256<byte> scan,
Vector256<byte> left,
Vector256<byte> above,
Vector256<byte> upperLeft)
{
Vector256<byte> predictor = Predict(left, above, upperLeft);
return scan - predictor;
}
/// <inheritdoc />
[MethodImpl(InliningOptions.AlwaysInline)]
public static Vector512<byte> Invoke(
Vector512<byte> scan,
Vector512<byte> left,
Vector512<byte> above,
Vector512<byte> upperLeft)
{
Vector512<byte> predictor = Predict(left, above, upperLeft);
return scan - predictor;
}
/// <summary>
/// Selects the nearest Paeth neighbor for sixteen independent byte lanes.
/// </summary>
[MethodImpl(InliningOptions.AlwaysInline)]
private static Vector128<byte> Predict(
Vector128<byte> left,
Vector128<byte> above,
Vector128<byte> upperLeft)
{
Vector128<byte> aboveMinusUpper = SubtractSaturate(above, upperLeft);
Vector128<byte> leftMinusUpper = SubtractSaturate(left, upperLeft);
Vector128<byte> distanceLeft = SubtractSaturate(upperLeft, above) | aboveMinusUpper;
Vector128<byte> distanceAbove = SubtractSaturate(upperLeft, left) | leftMinusUpper;
return SelectPredictor(
left,
above,
upperLeft,
aboveMinusUpper,
leftMinusUpper,
distanceLeft,
distanceAbove);
}
/// <summary>
/// Selects the nearest Paeth neighbor for thirty-two independent byte lanes.
/// </summary>
[MethodImpl(InliningOptions.AlwaysInline)]
private static Vector256<byte> Predict(
Vector256<byte> left,
Vector256<byte> above,
Vector256<byte> upperLeft)
{
Vector256<byte> aboveMinusUpper = Avx2.SubtractSaturate(above, upperLeft);
Vector256<byte> leftMinusUpper = Avx2.SubtractSaturate(left, upperLeft);
Vector256<byte> distanceLeft = Avx2.SubtractSaturate(upperLeft, above) | aboveMinusUpper;
Vector256<byte> distanceAbove = Avx2.SubtractSaturate(upperLeft, left) | leftMinusUpper;
return SelectPredictor(
left,
above,
upperLeft,
aboveMinusUpper,
leftMinusUpper,
distanceLeft,
distanceAbove);
}
/// <summary>
/// Selects the nearest Paeth neighbor for sixty-four independent byte lanes.
/// </summary>
[MethodImpl(InliningOptions.AlwaysInline)]
private static Vector512<byte> Predict(
Vector512<byte> left,
Vector512<byte> above,
Vector512<byte> upperLeft)
{
Vector512<byte> aboveMinusUpper = Avx512BW.SubtractSaturate(above, upperLeft);
Vector512<byte> leftMinusUpper = Avx512BW.SubtractSaturate(left, upperLeft);
Vector512<byte> distanceLeft = Avx512BW.SubtractSaturate(upperLeft, above) | aboveMinusUpper;
Vector512<byte> distanceAbove = Avx512BW.SubtractSaturate(upperLeft, left) | leftMinusUpper;
return SelectPredictor(
left,
above,
upperLeft,
aboveMinusUpper,
leftMinusUpper,
distanceLeft,
distanceAbove);
}
/// <summary>
/// Applies Paeth distance and tie-breaking rules to sixteen lanes.
/// </summary>
[MethodImpl(InliningOptions.AlwaysInline)]
private static Vector128<byte> SelectPredictor(
Vector128<byte> left,
Vector128<byte> above,
Vector128<byte> upperLeft,
Vector128<byte> aboveMinusUpper,
Vector128<byte> leftMinusUpper,
Vector128<byte> distanceLeft,
Vector128<byte> distanceAbove)
{
Vector128<byte> sameDirection = Vector128.Equals(
Vector128.Equals(aboveMinusUpper, Vector128<byte>.Zero),
Vector128.Equals(leftMinusUpper, Vector128<byte>.Zero));
Vector128<byte> distanceUpper = sameDirection
| SubtractSaturate(distanceAbove, distanceLeft)
| SubtractSaturate(distanceLeft, distanceAbove);
Vector128<byte> minimumAboveUpper = Vector128.Min(distanceUpper, distanceAbove);
Vector128<byte> aboveOrUpper = Vector128.ConditionalSelect(
Vector128.Equals(minimumAboveUpper, distanceAbove),
above,
upperLeft);
// Applying the left comparison last preserves PNG's left-first tie rule.
return Vector128.ConditionalSelect(
Vector128.Equals(Vector128.Min(minimumAboveUpper, distanceLeft), distanceLeft),
left,
aboveOrUpper);
}
/// <summary>
/// Applies Paeth distance and tie-breaking rules to thirty-two lanes.
/// </summary>
[MethodImpl(InliningOptions.AlwaysInline)]
private static Vector256<byte> SelectPredictor(
Vector256<byte> left,
Vector256<byte> above,
Vector256<byte> upperLeft,
Vector256<byte> aboveMinusUpper,
Vector256<byte> leftMinusUpper,
Vector256<byte> distanceLeft,
Vector256<byte> distanceAbove)
{
Vector256<byte> sameDirection = Vector256.Equals(
Vector256.Equals(aboveMinusUpper, Vector256<byte>.Zero),
Vector256.Equals(leftMinusUpper, Vector256<byte>.Zero));
Vector256<byte> distanceUpper = sameDirection
| Avx2.SubtractSaturate(distanceAbove, distanceLeft)
| Avx2.SubtractSaturate(distanceLeft, distanceAbove);
Vector256<byte> minimumAboveUpper = Vector256.Min(distanceUpper, distanceAbove);
Vector256<byte> aboveOrUpper = Vector256.ConditionalSelect(
Vector256.Equals(minimumAboveUpper, distanceAbove),
above,
upperLeft);
return Vector256.ConditionalSelect(
Vector256.Equals(Vector256.Min(minimumAboveUpper, distanceLeft), distanceLeft),
left,
aboveOrUpper);
}
/// <summary>
/// Applies Paeth distance and tie-breaking rules to sixty-four lanes.
/// </summary>
[MethodImpl(InliningOptions.AlwaysInline)]
private static Vector512<byte> SelectPredictor(
Vector512<byte> left,
Vector512<byte> above,
Vector512<byte> upperLeft,
Vector512<byte> aboveMinusUpper,
Vector512<byte> leftMinusUpper,
Vector512<byte> distanceLeft,
Vector512<byte> distanceAbove)
{
Vector512<byte> sameDirection = Vector512.Equals(
Vector512.Equals(aboveMinusUpper, Vector512<byte>.Zero),
Vector512.Equals(leftMinusUpper, Vector512<byte>.Zero));
Vector512<byte> distanceUpper = sameDirection
| Avx512BW.SubtractSaturate(distanceAbove, distanceLeft)
| Avx512BW.SubtractSaturate(distanceLeft, distanceAbove);
Vector512<byte> minimumAboveUpper = Vector512.Min(distanceUpper, distanceAbove);
Vector512<byte> aboveOrUpper = Vector512.ConditionalSelect(
Vector512.Equals(minimumAboveUpper, distanceAbove),
above,
upperLeft);
return Vector512.ConditionalSelect(
Vector512.Equals(Vector512.Min(minimumAboveUpper, distanceLeft), distanceLeft),
left,
aboveOrUpper);
}
/// <summary>
/// Performs an unsigned saturating subtraction using the active 128-bit instruction set.
/// </summary>
/// <param name="left">The minuend lanes.</param>
/// <param name="right">The subtrahend lanes.</param>
/// <returns>The saturated lane-wise differences.</returns>
[MethodImpl(InliningOptions.AlwaysInline)]
private static Vector128<byte> SubtractSaturate(Vector128<byte> left, Vector128<byte> right)
{
if (Sse2.IsSupported)
{
return Sse2.SubtractSaturate(left, right);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.SubtractSaturate(left, right);
}
// Subtracting the smaller operand produces max(left - right, 0) without
// requiring a backend-specific saturating-subtract instruction.
return left - Vector128.Min(left, right);
}
}

153
src/ImageSharp/Formats/Png/Filters/PaethFilter.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
@ -193,86 +192,12 @@ internal static class PaethFilter
/// <param name="sum">The sum of the total variance of the filtered row.</param> /// <param name="sum">The sum of the total variance of the filtered row.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> previousScanline, Span<byte> result, int bytesPerPixel, out int sum) public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> previousScanline, Span<byte> result, int bytesPerPixel, out int sum)
{ => PngFilterEncoder.Encode<PaethFilterOperator>(
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); scanline,
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); previousScanline,
result,
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); (uint)bytesPerPixel,
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); out sum);
ref byte resultBaseRef = ref MemoryMarshal.GetReference(result);
sum = 0;
// Paeth(x) = Raw(x) - PaethPredictor(Raw(x-bpp), Prior(x), Prior(x - bpp))
resultBaseRef = (byte)FilterType.Paeth;
nuint x = 0;
for (; x < (uint)bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte above = Unsafe.Add(ref prevBaseRef, x);
++x;
ref byte res = ref Unsafe.Add(ref resultBaseRef, x);
res = (byte)(scan - PaethPredictor(0, above, 0));
sum += Numerics.Abs(unchecked((sbyte)res));
}
if (Avx2.IsSupported)
{
Vector256<byte> zero = Vector256<byte>.Zero;
Vector256<int> sumAccumulator = Vector256<int>.Zero;
for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= scanline.Length - Vector256<byte>.Count; xLeft += (uint)Vector256<byte>.Count)
{
Vector256<byte> scan = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, x));
Vector256<byte> left = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft));
Vector256<byte> above = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref prevBaseRef, x));
Vector256<byte> upperLeft = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref prevBaseRef, xLeft));
Vector256<byte> res = Avx2.Subtract(scan, PaethPredictor(left, above, upperLeft));
Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type
x += (uint)Vector256<byte>.Count;
sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32());
}
sum += Numerics.EvenReduceSum(sumAccumulator);
}
else if (Vector.IsHardwareAccelerated)
{
Vector<uint> sumAccumulator = Vector<uint>.Zero;
for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= scanline.Length - Vector<byte>.Count; xLeft += (uint)Vector<byte>.Count)
{
Vector<byte> scan = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref scanBaseRef, x));
Vector<byte> left = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft));
Vector<byte> above = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref prevBaseRef, x));
Vector<byte> upperLeft = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref prevBaseRef, xLeft));
Vector<byte> res = scan - PaethPredictor(left, above, upperLeft);
Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type
x += (uint)Vector<byte>.Count;
Numerics.Accumulate(ref sumAccumulator, Vector.AsVectorByte(Vector.Abs(Vector.AsVectorSByte(res))));
}
for (int i = 0; i < Vector<uint>.Count; i++)
{
sum += (int)sumAccumulator[i];
}
}
for (nuint xLeft = x - (uint)bytesPerPixel; (int)x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte left = Unsafe.Add(ref scanBaseRef, xLeft);
byte above = Unsafe.Add(ref prevBaseRef, x);
byte upperLeft = Unsafe.Add(ref prevBaseRef, xLeft);
++x;
ref byte res = ref Unsafe.Add(ref resultBaseRef, x);
res = (byte)(scan - PaethPredictor(left, above, upperLeft));
sum += Numerics.Abs(unchecked((sbyte)res));
}
}
/// <summary> /// <summary>
/// Computes a simple linear function of the three neighboring pixels (left, above, upper left), then chooses /// Computes a simple linear function of the three neighboring pixels (left, above, upper left), then chooses
@ -304,70 +229,4 @@ internal static class PaethFilter
return upperLeft; return upperLeft;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<byte> PaethPredictor(Vector256<byte> left, Vector256<byte> above, Vector256<byte> upleft)
{
Vector256<byte> zero = Vector256<byte>.Zero;
// Here, we refactor pa = abs(p - left) = abs(left + above - upleft - left)
// to pa = abs(above - upleft). Same deal for pb.
// Using saturated subtraction, if the result is negative, the output is zero.
// If we subtract in both directions and `or` the results, only one can be
// non-zero, so we end up with the absolute value.
Vector256<byte> sac = Avx2.SubtractSaturate(above, upleft);
Vector256<byte> sbc = Avx2.SubtractSaturate(left, upleft);
Vector256<byte> pa = Avx2.Or(Avx2.SubtractSaturate(upleft, above), sac);
Vector256<byte> pb = Avx2.Or(Avx2.SubtractSaturate(upleft, left), sbc);
// pc = abs(left + above - upleft - upleft), or abs(left - upleft + above - upleft).
// We've already calculated left - upleft and above - upleft in `sac` and `sbc`.
// If they are both negative or both positive, the absolute value of their
// sum can't possibly be less than `pa` or `pb`, so we'll never use the value.
// We make a mask that sets the value to 255 if they either both got
// saturated to zero or both didn't. Then we calculate the absolute value
// of their difference using saturated subtract and `or`, same as before,
// keeping the value only where the mask isn't set.
Vector256<byte> pm = Avx2.CompareEqual(Avx2.CompareEqual(sac, zero), Avx2.CompareEqual(sbc, zero));
Vector256<byte> pc = Avx2.Or(pm, Avx2.Or(Avx2.SubtractSaturate(pb, pa), Avx2.SubtractSaturate(pa, pb)));
// Finally, blend the values together. We start with `upleft` and overwrite on
// tied values so that the `left`, `above`, `upleft` precedence is preserved.
Vector256<byte> minbc = Avx2.Min(pc, pb);
Vector256<byte> resbc = Avx2.BlendVariable(upleft, above, Avx2.CompareEqual(minbc, pb));
return Avx2.BlendVariable(resbc, left, Avx2.CompareEqual(Avx2.Min(minbc, pa), pa));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector<byte> PaethPredictor(Vector<byte> left, Vector<byte> above, Vector<byte> upperLeft)
{
Vector.Widen(left, out Vector<ushort> a1, out Vector<ushort> a2);
Vector.Widen(above, out Vector<ushort> b1, out Vector<ushort> b2);
Vector.Widen(upperLeft, out Vector<ushort> c1, out Vector<ushort> c2);
Vector<short> p1 = PaethPredictor(Vector.AsVectorInt16(a1), Vector.AsVectorInt16(b1), Vector.AsVectorInt16(c1));
Vector<short> p2 = PaethPredictor(Vector.AsVectorInt16(a2), Vector.AsVectorInt16(b2), Vector.AsVectorInt16(c2));
return Vector.AsVectorByte(Vector.Narrow(p1, p2));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector<short> PaethPredictor(Vector<short> left, Vector<short> above, Vector<short> upperLeft)
{
Vector<short> p = left + above - upperLeft;
Vector<short> pa = Vector.Abs(p - left);
Vector<short> pb = Vector.Abs(p - above);
Vector<short> pc = Vector.Abs(p - upperLeft);
Vector<short> pa_pb = Vector.LessThanOrEqual(pa, pb);
Vector<short> pa_pc = Vector.LessThanOrEqual(pa, pc);
Vector<short> pb_pc = Vector.LessThanOrEqual(pb, pc);
return Vector.ConditionalSelect(
condition: Vector.BitwiseAnd(pa_pb, pa_pc),
left: left,
right: Vector.ConditionalSelect(
condition: pb_pc,
left: above,
right: upperLeft));
}
} }

228
src/ImageSharp/Formats/Png/Filters/PngFilterEncoder.cs

@ -0,0 +1,228 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Png.Filters;
/// <summary>
/// Applies PNG filter operators while accumulating the absolute signed residuals used for filter selection.
/// </summary>
internal static class PngFilterEncoder
{
/// <summary>
/// Maps a scanline through a filter operator, writes the residuals, and reduces their total variance.
/// </summary>
/// <typeparam name="TOperator">The PNG predictor selected for this closed traversal.</typeparam>
/// <param name="scanline">The scanline to encode.</param>
/// <param name="previousScanline">The preceding scanline.</param>
/// <param name="result">The destination including its leading filter byte.</param>
/// <param name="bytesPerPixel">The distance to the corresponding component in the preceding pixel.</param>
/// <param name="sum">The sum of the absolute signed residuals.</param>
// Inlining closes every static interface call over TOperator. The JIT can then remove
// source loads ignored by simpler predictors and specialize the active register widths.
[MethodImpl(InliningOptions.AlwaysInline)]
public static void Encode<TOperator>(
ReadOnlySpan<byte> scanline,
ReadOnlySpan<byte> previousScanline,
Span<byte> result,
uint bytesPerPixel,
out int sum)
where TOperator : struct, IPngFilterOperator
{
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte previousBaseRef = ref MemoryMarshal.GetReference(previousScanline);
ref byte resultBaseRef = ref MemoryMarshal.GetReference(result);
resultBaseRef = (byte)TOperator.Type;
sum = 0;
nuint x = 0;
// Components in the first pixel have no left or upper-left neighbor. Supplying
// zeroes expresses the PNG boundary rule directly through the same predictor.
for (; x < bytesPerPixel; x++)
{
byte above = TOperator.UsesAbove ? Unsafe.Add(ref previousBaseRef, x) : (byte)0;
byte filtered = TOperator.Invoke(
Unsafe.Add(ref scanBaseRef, x),
0,
above,
0);
Unsafe.Add(ref resultBaseRef, x + 1) = filtered;
sum += Numerics.Abs(unchecked((sbyte)filtered));
}
Vector128<uint> sum128 = Vector128<uint>.Zero;
// A single 512-bit register does not amortize folding its SAD accumulator.
// Leave short rows to the narrower paths, which have lower fixed reduction cost.
if (Avx512BW.IsSupported && scanline.Length - (int)x >= Vector512<byte>.Count * 2)
{
Vector512<uint> sum512 = Vector512<uint>.Zero;
int oneRegisterFromEnd = scanline.Length - Vector512<byte>.Count;
for (nuint xLeft = x - bytesPerPixel; (int)x <= oneRegisterFromEnd; xLeft += (uint)Vector512<byte>.Count)
{
// Each byte lane represents one independently filtered component. The
// four input vectors retain the scan/left/above/upper-left PNG layout.
// Operator usage flags are constants after generic specialization, so
// unused predictors do not retain even fault-preserving probe loads.
Vector512<byte> left = TOperator.UsesLeft
? Unsafe.As<byte, Vector512<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft))
: default;
Vector512<byte> above = TOperator.UsesAbove
? Unsafe.As<byte, Vector512<byte>>(ref Unsafe.Add(ref previousBaseRef, x))
: default;
Vector512<byte> upperLeft = TOperator.UsesUpperLeft
? Unsafe.As<byte, Vector512<byte>>(ref Unsafe.Add(ref previousBaseRef, xLeft))
: default;
Vector512<byte> filtered = TOperator.Invoke(
Unsafe.As<byte, Vector512<byte>>(ref Unsafe.Add(ref scanBaseRef, x)),
left,
above,
upperLeft);
Unsafe.As<byte, Vector512<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = filtered;
x += (uint)Vector512<byte>.Count;
// PNG scores each residual as abs((sbyte)residual). VPSADBW sums eight
// byte lanes into every other 32-bit lane without widening each byte.
Vector512<byte> absolute = Avx512BW.Abs(filtered.AsSByte());
sum512 += Avx512BW.SumAbsoluteDifferences(absolute, Vector512<byte>.Zero).AsUInt32();
}
// Fold only widths that processed data. Short rows therefore avoid both
// wide accumulator initialization and an otherwise empty reduction.
Vector256<uint> folded512 = sum512.GetLower() + sum512.GetUpper();
sum128 += folded512.GetLower() + folded512.GetUpper();
}
if (Avx2.IsSupported)
{
Vector256<uint> sum256 = Vector256<uint>.Zero;
int oneRegisterFromEnd = scanline.Length - Vector256<byte>.Count;
for (nuint xLeft = x - bytesPerPixel; (int)x <= oneRegisterFromEnd; xLeft += (uint)Vector256<byte>.Count)
{
Vector256<byte> left = TOperator.UsesLeft
? Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft))
: default;
Vector256<byte> above = TOperator.UsesAbove
? Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref previousBaseRef, x))
: default;
Vector256<byte> upperLeft = TOperator.UsesUpperLeft
? Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref previousBaseRef, xLeft))
: default;
Vector256<byte> filtered = TOperator.Invoke(
Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, x)),
left,
above,
upperLeft);
Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = filtered;
x += (uint)Vector256<byte>.Count;
Vector256<byte> absolute = Avx2.Abs(filtered.AsSByte());
sum256 += Avx2.SumAbsoluteDifferences(absolute, Vector256<byte>.Zero).AsUInt32();
}
sum128 += sum256.GetLower() + sum256.GetUpper();
}
if (Vector128.IsHardwareAccelerated)
{
int oneRegisterFromEnd = scanline.Length - Vector128<byte>.Count;
for (nuint xLeft = x - bytesPerPixel; (int)x <= oneRegisterFromEnd; xLeft += (uint)Vector128<byte>.Count)
{
Vector128<byte> left = TOperator.UsesLeft
? Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft))
: default;
Vector128<byte> above = TOperator.UsesAbove
? Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref previousBaseRef, x))
: default;
Vector128<byte> upperLeft = TOperator.UsesUpperLeft
? Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref previousBaseRef, xLeft))
: default;
Vector128<byte> filtered = TOperator.Invoke(
Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref scanBaseRef, x)),
left,
above,
upperLeft);
Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = filtered;
x += (uint)Vector128<byte>.Count;
sum128 = AccumulateAbsolute(sum128, filtered);
}
}
sum += unchecked((int)Vector128.Sum(sum128));
for (nuint xLeft = x - bytesPerPixel; x < (uint)scanline.Length; xLeft++, x++)
{
byte left = TOperator.UsesLeft ? Unsafe.Add(ref scanBaseRef, xLeft) : (byte)0;
byte above = TOperator.UsesAbove ? Unsafe.Add(ref previousBaseRef, x) : (byte)0;
byte upperLeft = TOperator.UsesUpperLeft ? Unsafe.Add(ref previousBaseRef, xLeft) : (byte)0;
byte filtered = TOperator.Invoke(
Unsafe.Add(ref scanBaseRef, x),
left,
above,
upperLeft);
Unsafe.Add(ref resultBaseRef, x + 1) = filtered;
sum += Numerics.Abs(unchecked((sbyte)filtered));
}
}
/// <summary>
/// Accumulates the absolute signed values in a 128-bit residual vector.
/// </summary>
/// <param name="accumulator">The four-lane unsigned accumulator.</param>
/// <param name="residuals">The sixteen filtered byte residuals.</param>
/// <returns>The updated accumulator.</returns>
[MethodImpl(InliningOptions.AlwaysInline)]
private static Vector128<uint> AccumulateAbsolute(Vector128<uint> accumulator, Vector128<byte> residuals)
{
if (Sse2.IsSupported)
{
Vector128<byte> absolute;
if (Ssse3.IsSupported)
{
absolute = Ssse3.Abs(residuals.AsSByte());
}
else
{
// SSE2 has no packed signed-byte absolute instruction. The sign mask
// implements (value + mask) XOR mask, including -128 -> 128.
Vector128<sbyte> mask = Sse2.CompareGreaterThan(Vector128<sbyte>.Zero, residuals.AsSByte());
absolute = Sse2.Xor(Sse2.Add(residuals.AsSByte(), mask), mask).AsByte();
}
return accumulator + Sse2.SumAbsoluteDifferences(absolute, Vector128<byte>.Zero).AsUInt32();
}
Vector128<byte> absoluteArm = Vector128.Abs(residuals.AsSByte()).AsByte();
(Vector128<ushort> lower16, Vector128<ushort> upper16) = Vector128.Widen(absoluteArm);
(Vector128<uint> lower0, Vector128<uint> lower1) = Vector128.Widen(lower16);
(Vector128<uint> upper0, Vector128<uint> upper1) = Vector128.Widen(upper16);
// Four widening additions keep every byte contribution in a 32-bit lane,
// matching the x86 accumulator's overflow behavior without scalar reduction.
return accumulator + lower0 + lower1 + upper0 + upper1;
}
}

81
src/ImageSharp/Formats/Png/Filters/SubFilter.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
@ -110,77 +109,11 @@ internal static class SubFilter
/// <param name="bytesPerPixel">The bytes per pixel.</param> /// <param name="bytesPerPixel">The bytes per pixel.</param>
/// <param name="sum">The sum of the total variance of the filtered row.</param> /// <param name="sum">The sum of the total variance of the filtered row.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> result, int bytesPerPixel, out int sum) public static void Encode(ReadOnlySpan<byte> scanline, Span<byte> result, int bytesPerPixel, out int sum)
{ => PngFilterEncoder.Encode<SubFilterOperator>(
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); scanline,
scanline,
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); result,
ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); (uint)bytesPerPixel,
sum = 0; out sum);
// Sub(x) = Raw(x) - Raw(x-bpp)
resultBaseRef = (byte)FilterType.Sub;
nuint x = 0;
for (; x < (uint)bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
++x;
ref byte res = ref Unsafe.Add(ref resultBaseRef, x);
res = scan;
sum += Numerics.Abs(unchecked((sbyte)res));
}
if (Avx2.IsSupported)
{
Vector256<byte> zero = Vector256<byte>.Zero;
Vector256<int> sumAccumulator = Vector256<int>.Zero;
for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= (scanline.Length - Vector256<byte>.Count); xLeft += (uint)Vector256<byte>.Count)
{
Vector256<byte> scan = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, x));
Vector256<byte> prev = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft));
Vector256<byte> res = Avx2.Subtract(scan, prev);
Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type
x += (uint)Vector256<byte>.Count;
sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32());
}
sum += Numerics.EvenReduceSum(sumAccumulator);
}
else
if (Vector.IsHardwareAccelerated)
{
Vector<uint> sumAccumulator = Vector<uint>.Zero;
for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= (scanline.Length - Vector<byte>.Count); xLeft += (uint)Vector<byte>.Count)
{
Vector<byte> scan = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref scanBaseRef, x));
Vector<byte> prev = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft));
Vector<byte> res = scan - prev;
Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type
x += (uint)Vector<byte>.Count;
Numerics.Accumulate(ref sumAccumulator, Vector.AsVectorByte(Vector.Abs(Vector.AsVectorSByte(res))));
}
for (int i = 0; i < Vector<uint>.Count; i++)
{
sum += (int)sumAccumulator[i];
}
}
for (nuint xLeft = x - (uint)bytesPerPixel; x < (uint)scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte prev = Unsafe.Add(ref scanBaseRef, xLeft);
++x;
ref byte res = ref Unsafe.Add(ref resultBaseRef, x);
res = (byte)(scan - prev);
sum += Numerics.Abs(unchecked((sbyte)res));
}
}
} }

75
src/ImageSharp/Formats/Png/Filters/UpFilter.cs

@ -1,11 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Formats.Png.Filters; namespace SixLabors.ImageSharp.Formats.Png.Filters;
@ -40,69 +36,10 @@ internal static class UpFilter
/// <param name="sum">The sum of the total variance of the filtered row.</param> /// <param name="sum">The sum of the total variance of the filtered row.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> previousScanline, Span<byte> result, out int sum) public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> previousScanline, Span<byte> result, out int sum)
{ => PngFilterEncoder.Encode<UpFilterOperator>(
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); scanline,
DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); previousScanline,
result,
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); 0,
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); out sum);
ref byte resultBaseRef = ref MemoryMarshal.GetReference(result);
sum = 0;
// Up(x) = Raw(x) - Prior(x)
resultBaseRef = (byte)FilterType.Up;
nuint x = 0;
if (Avx2.IsSupported)
{
Vector256<byte> zero = Vector256<byte>.Zero;
Vector256<int> sumAccumulator = Vector256<int>.Zero;
for (; (int)x <= scanline.Length - Vector256<byte>.Count;)
{
Vector256<byte> scan = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, x));
Vector256<byte> above = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref prevBaseRef, x));
Vector256<byte> res = Avx2.Subtract(scan, above);
Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type
x += (uint)Vector256<byte>.Count;
sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32());
}
sum += Numerics.EvenReduceSum(sumAccumulator);
}
else if (Vector.IsHardwareAccelerated)
{
Vector<uint> sumAccumulator = Vector<uint>.Zero;
for (; (int)x <= scanline.Length - Vector<byte>.Count;)
{
Vector<byte> scan = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref scanBaseRef, x));
Vector<byte> above = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref prevBaseRef, x));
Vector<byte> res = scan - above;
Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type
x += (uint)Vector<byte>.Count;
Numerics.Accumulate(ref sumAccumulator, Vector.AsVectorByte(Vector.Abs(Vector.AsVectorSByte(res))));
}
for (int i = 0; i < Vector<uint>.Count; i++)
{
sum += (int)sumAccumulator[i];
}
}
for (; x < (uint)scanline.Length; /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte above = Unsafe.Add(ref prevBaseRef, x);
++x;
ref byte res = ref Unsafe.Add(ref resultBaseRef, x);
res = (byte)(scan - above);
sum += Numerics.Abs(unchecked((sbyte)res));
}
}
} }

157
tests/ImageSharp.Benchmarks/Codecs/Png/PngFilterEncode.cs

@ -0,0 +1,157 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Png.Filters;
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Png;
/// <summary>
/// Compares the shared PNG map/reduce traversal with the filter-specific traversals it replaces.
/// </summary>
[Config(typeof(Config.Short))]
public class PngFilterEncode
{
private const int BytesPerPixel = 4;
private byte[] scanline;
private byte[] previousScanline;
private byte[] currentResult;
private byte[] baselineResult;
/// <summary>
/// Gets or sets the filter evaluated by each invocation.
/// </summary>
[Params(PngFilterMethod.Sub, PngFilterMethod.Up, PngFilterMethod.Average, PngFilterMethod.Paeth)]
public PngFilterMethod Filter { get; set; }
/// <summary>
/// Gets or sets the number of scanline bytes.
/// </summary>
[Params(64, 1024, 16384)]
public int Count { get; set; }
/// <summary>
/// Creates deterministic non-uniform inputs and independent result buffers.
/// </summary>
[GlobalSetup]
public void Setup()
{
this.scanline = new byte[this.Count];
this.previousScanline = new byte[this.Count];
this.currentResult = new byte[this.Count + 1];
this.baselineResult = new byte[this.Count + 1];
Random random = new(12345678);
random.NextBytes(this.scanline);
random.NextBytes(this.previousScanline);
}
/// <summary>
/// Executes the operator-driven map/reduce traversal.
/// </summary>
/// <returns>The filter variance sum.</returns>
[Benchmark]
public int Current()
=> this.Filter switch
{
PngFilterMethod.Sub => this.EncodeSubCurrent(),
PngFilterMethod.Up => this.EncodeUpCurrent(),
PngFilterMethod.Average => this.EncodeAverageCurrent(),
PngFilterMethod.Paeth => this.EncodePaethCurrent(),
_ => throw new InvalidOperationException()
};
/// <summary>
/// Executes the filter-specific traversal being replaced.
/// </summary>
/// <returns>The filter variance sum.</returns>
[Benchmark(Baseline = true)]
public int Baseline()
{
int sum;
switch (this.Filter)
{
case PngFilterMethod.Sub:
PngFilterEncodeBaseline.EncodeSub(
this.scanline,
this.baselineResult,
BytesPerPixel,
out sum);
break;
case PngFilterMethod.Up:
PngFilterEncodeBaseline.EncodeUp(
this.scanline,
this.previousScanline,
this.baselineResult,
out sum);
break;
case PngFilterMethod.Average:
PngFilterEncodeBaseline.EncodeAverage(
this.scanline,
this.previousScanline,
this.baselineResult,
BytesPerPixel,
out sum);
break;
case PngFilterMethod.Paeth:
PngFilterEncodeBaseline.EncodePaeth(
this.scanline,
this.previousScanline,
this.baselineResult,
BytesPerPixel,
out sum);
break;
default:
throw new InvalidOperationException();
}
return sum;
}
/// <summary>
/// Executes the current Sub encoder.
/// </summary>
private int EncodeSubCurrent()
{
SubFilter.Encode(this.scanline, this.currentResult, BytesPerPixel, out int sum);
return sum;
}
/// <summary>
/// Executes the current Up encoder.
/// </summary>
private int EncodeUpCurrent()
{
UpFilter.Encode(this.scanline, this.previousScanline, this.currentResult, out int sum);
return sum;
}
/// <summary>
/// Executes the current Average encoder.
/// </summary>
private int EncodeAverageCurrent()
{
AverageFilter.Encode(this.scanline, this.previousScanline, this.currentResult, BytesPerPixel, out int sum);
return sum;
}
/// <summary>
/// Executes the current Paeth encoder.
/// </summary>
private int EncodePaethCurrent()
{
PaethFilter.Encode(this.scanline, this.previousScanline, this.currentResult, BytesPerPixel, out int sum);
return sum;
}
}

104
tests/ImageSharp.Benchmarks/Codecs/Png/PngFilterEncodeAssembly.cs

@ -0,0 +1,104 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Png.Filters;
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Png;
/// <summary>
/// Exposes every normalized PNG filter and retained baseline for assembly comparison.
/// </summary>
[Config(typeof(Config.Analysis))]
public class PngFilterEncodeAssembly
{
private const int BytesPerPixel = 4;
private const int Count = 180;
private byte[] scanline;
private byte[] previousScanline;
private byte[] currentResult;
private byte[] baselineResult;
/// <summary>
/// Creates inputs whose suffix exercises 512-, 256-, and 128-bit register widths.
/// </summary>
[GlobalSetup]
public void Setup()
{
this.scanline = new byte[Count];
this.previousScanline = new byte[Count];
this.currentResult = new byte[Count + 1];
this.baselineResult = new byte[Count + 1];
Random random = new(12345678);
random.NextBytes(this.scanline);
random.NextBytes(this.previousScanline);
}
/// <summary>
/// Executes the normalized Sub encoder.
/// </summary>
[Benchmark]
public void Sub()
=> SubFilter.Encode(this.scanline, this.currentResult, BytesPerPixel, out _);
/// <summary>
/// Executes the normalized Up encoder.
/// </summary>
[Benchmark]
public void Up()
=> UpFilter.Encode(this.scanline, this.previousScanline, this.currentResult, out _);
/// <summary>
/// Executes the normalized Average encoder.
/// </summary>
[Benchmark]
public void Average()
=> AverageFilter.Encode(this.scanline, this.previousScanline, this.currentResult, BytesPerPixel, out _);
/// <summary>
/// Executes the normalized Paeth encoder.
/// </summary>
[Benchmark]
public void Paeth()
=> PaethFilter.Encode(this.scanline, this.previousScanline, this.currentResult, BytesPerPixel, out _);
/// <summary>
/// Executes the retained Sub encoder.
/// </summary>
[Benchmark]
public void BaselineSub()
=> PngFilterEncodeBaseline.EncodeSub(this.scanline, this.baselineResult, BytesPerPixel, out _);
/// <summary>
/// Executes the retained Up encoder.
/// </summary>
[Benchmark]
public void BaselineUp()
=> PngFilterEncodeBaseline.EncodeUp(this.scanline, this.previousScanline, this.baselineResult, out _);
/// <summary>
/// Executes the retained Average encoder.
/// </summary>
[Benchmark]
public void BaselineAverage()
=> PngFilterEncodeBaseline.EncodeAverage(
this.scanline,
this.previousScanline,
this.baselineResult,
BytesPerPixel,
out _);
/// <summary>
/// Executes the retained Paeth encoder.
/// </summary>
[Benchmark]
public void BaselinePaeth()
=> PngFilterEncodeBaseline.EncodePaeth(
this.scanline,
this.previousScanline,
this.baselineResult,
BytesPerPixel,
out _);
}

470
tests/ImageSharp.Benchmarks/Codecs/Png/PngFilterEncodeBaseline.cs

@ -0,0 +1,470 @@
// 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;
using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Png;
/// <summary>
/// Retains the filter-specific PNG encode traversals for direct performance comparison.
/// </summary>
internal static class PngFilterEncodeBaseline
{
/// <summary>
/// Executes the filter-specific Sub traversal.
/// </summary>
public static void EncodeSub(
ReadOnlySpan<byte> scanline,
Span<byte> result,
int bytesPerPixel,
out int sum)
{
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte resultBaseRef = ref MemoryMarshal.GetReference(result);
sum = 0;
resultBaseRef = 1;
nuint x = 0;
for (; x < (uint)bytesPerPixel;)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
x++;
ref byte residual = ref Unsafe.Add(ref resultBaseRef, x);
residual = scan;
sum += Numerics.Abs(unchecked((sbyte)residual));
}
if (Avx2.IsSupported)
{
Vector256<byte> zero = Vector256<byte>.Zero;
Vector256<int> accumulator = Vector256<int>.Zero;
for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= scanline.Length - Vector256<byte>.Count; xLeft += (uint)Vector256<byte>.Count)
{
Vector256<byte> scan = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, x));
Vector256<byte> left = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft));
Vector256<byte> residual = Avx2.Subtract(scan, left);
Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = residual;
x += (uint)Vector256<byte>.Count;
accumulator = Avx2.Add(
accumulator,
Avx2.SumAbsoluteDifferences(Avx2.Abs(residual.AsSByte()), zero).AsInt32());
}
sum += Numerics.EvenReduceSum(accumulator);
}
else if (Vector.IsHardwareAccelerated)
{
Vector<uint> accumulator = Vector<uint>.Zero;
for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= scanline.Length - Vector<byte>.Count; xLeft += (uint)Vector<byte>.Count)
{
Vector<byte> scan = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref scanBaseRef, x));
Vector<byte> left = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft));
Vector<byte> residual = scan - left;
Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = residual;
x += (uint)Vector<byte>.Count;
Numerics.Accumulate(
ref accumulator,
Vector.AsVectorByte(Vector.Abs(Vector.AsVectorSByte(residual))));
}
for (int i = 0; i < Vector<uint>.Count; i++)
{
sum += (int)accumulator[i];
}
}
for (nuint xLeft = x - (uint)bytesPerPixel; x < (uint)scanline.Length; xLeft++)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte left = Unsafe.Add(ref scanBaseRef, xLeft);
x++;
ref byte residual = ref Unsafe.Add(ref resultBaseRef, x);
residual = (byte)(scan - left);
sum += Numerics.Abs(unchecked((sbyte)residual));
}
}
/// <summary>
/// Executes the filter-specific Up traversal.
/// </summary>
public static void EncodeUp(
ReadOnlySpan<byte> scanline,
ReadOnlySpan<byte> previousScanline,
Span<byte> result,
out int sum)
{
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte previousBaseRef = ref MemoryMarshal.GetReference(previousScanline);
ref byte resultBaseRef = ref MemoryMarshal.GetReference(result);
sum = 0;
resultBaseRef = 2;
nuint x = 0;
if (Avx2.IsSupported)
{
Vector256<byte> zero = Vector256<byte>.Zero;
Vector256<int> accumulator = Vector256<int>.Zero;
for (; (int)x <= scanline.Length - Vector256<byte>.Count;)
{
Vector256<byte> scan = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, x));
Vector256<byte> above = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref previousBaseRef, x));
Vector256<byte> residual = Avx2.Subtract(scan, above);
Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = residual;
x += (uint)Vector256<byte>.Count;
accumulator = Avx2.Add(
accumulator,
Avx2.SumAbsoluteDifferences(Avx2.Abs(residual.AsSByte()), zero).AsInt32());
}
sum += Numerics.EvenReduceSum(accumulator);
}
else if (Vector.IsHardwareAccelerated)
{
Vector<uint> accumulator = Vector<uint>.Zero;
for (; (int)x <= scanline.Length - Vector<byte>.Count;)
{
Vector<byte> scan = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref scanBaseRef, x));
Vector<byte> above = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref previousBaseRef, x));
Vector<byte> residual = scan - above;
Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = residual;
x += (uint)Vector<byte>.Count;
Numerics.Accumulate(
ref accumulator,
Vector.AsVectorByte(Vector.Abs(Vector.AsVectorSByte(residual))));
}
for (int i = 0; i < Vector<uint>.Count; i++)
{
sum += (int)accumulator[i];
}
}
for (; x < (uint)scanline.Length;)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte above = Unsafe.Add(ref previousBaseRef, x);
x++;
ref byte residual = ref Unsafe.Add(ref resultBaseRef, x);
residual = (byte)(scan - above);
sum += Numerics.Abs(unchecked((sbyte)residual));
}
}
/// <summary>
/// Executes the filter-specific Average traversal.
/// </summary>
public static void EncodeAverage(
ReadOnlySpan<byte> scanline,
ReadOnlySpan<byte> previousScanline,
Span<byte> result,
uint bytesPerPixel,
out int sum)
{
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte previousBaseRef = ref MemoryMarshal.GetReference(previousScanline);
ref byte resultBaseRef = ref MemoryMarshal.GetReference(result);
sum = 0;
resultBaseRef = 3;
nuint x = 0;
for (; x < bytesPerPixel;)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte above = Unsafe.Add(ref previousBaseRef, x);
x++;
ref byte residual = ref Unsafe.Add(ref resultBaseRef, x);
residual = (byte)(scan - (above >> 1));
sum += Numerics.Abs(unchecked((sbyte)residual));
}
if (Avx2.IsSupported)
{
Vector256<byte> zero = Vector256<byte>.Zero;
Vector256<int> accumulator = Vector256<int>.Zero;
Vector256<byte> allBitsSet = Avx2.CompareEqual(accumulator, accumulator).AsByte();
for (nuint xLeft = x - bytesPerPixel; (int)x <= scanline.Length - Vector256<byte>.Count; xLeft += (uint)Vector256<byte>.Count)
{
Vector256<byte> scan = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, x));
Vector256<byte> left = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft));
Vector256<byte> above = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref previousBaseRef, x));
Vector256<byte> average = Avx2.Xor(
Avx2.Average(Avx2.Xor(left, allBitsSet), Avx2.Xor(above, allBitsSet)),
allBitsSet);
Vector256<byte> residual = Avx2.Subtract(scan, average);
Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = residual;
x += (uint)Vector256<byte>.Count;
accumulator = Avx2.Add(
accumulator,
Avx2.SumAbsoluteDifferences(Avx2.Abs(residual.AsSByte()), zero).AsInt32());
}
sum += Numerics.EvenReduceSum(accumulator);
}
else if (Sse2.IsSupported)
{
Vector128<byte> zero = Vector128<byte>.Zero;
Vector128<int> accumulator = Vector128<int>.Zero;
Vector128<byte> allBitsSet = Sse2.CompareEqual(accumulator, accumulator).AsByte();
for (nuint xLeft = x - bytesPerPixel; (int)x <= scanline.Length - Vector128<byte>.Count; xLeft += (uint)Vector128<byte>.Count)
{
Vector128<byte> scan = Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref scanBaseRef, x));
Vector128<byte> left = Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft));
Vector128<byte> above = Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref previousBaseRef, x));
Vector128<byte> average = Sse2.Xor(
Sse2.Average(Sse2.Xor(left, allBitsSet), Sse2.Xor(above, allBitsSet)),
allBitsSet);
Vector128<byte> residual = Sse2.Subtract(scan, average);
Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = residual;
x += (uint)Vector128<byte>.Count;
Vector128<byte> absolute;
if (Ssse3.IsSupported)
{
absolute = Ssse3.Abs(residual.AsSByte());
}
else
{
Vector128<sbyte> mask = Sse2.CompareGreaterThan(zero.AsSByte(), residual.AsSByte());
absolute = Sse2.Xor(Sse2.Add(residual.AsSByte(), mask), mask).AsByte();
}
accumulator = Sse2.Add(
accumulator,
Sse2.SumAbsoluteDifferences(absolute, zero).AsInt32());
}
sum += Numerics.EvenReduceSum(accumulator);
}
for (nuint xLeft = x - bytesPerPixel; x < (uint)scanline.Length; xLeft++)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte left = Unsafe.Add(ref scanBaseRef, xLeft);
byte above = Unsafe.Add(ref previousBaseRef, x);
x++;
ref byte residual = ref Unsafe.Add(ref resultBaseRef, x);
residual = (byte)(scan - ((left + above) >> 1));
sum += Numerics.Abs(unchecked((sbyte)residual));
}
}
/// <summary>
/// Executes the filter-specific Paeth traversal.
/// </summary>
public static void EncodePaeth(
ReadOnlySpan<byte> scanline,
ReadOnlySpan<byte> previousScanline,
Span<byte> result,
int bytesPerPixel,
out int sum)
{
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte previousBaseRef = ref MemoryMarshal.GetReference(previousScanline);
ref byte resultBaseRef = ref MemoryMarshal.GetReference(result);
sum = 0;
resultBaseRef = 4;
nuint x = 0;
for (; x < (uint)bytesPerPixel;)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte above = Unsafe.Add(ref previousBaseRef, x);
x++;
ref byte residual = ref Unsafe.Add(ref resultBaseRef, x);
residual = (byte)(scan - above);
sum += Numerics.Abs(unchecked((sbyte)residual));
}
if (Avx2.IsSupported)
{
Vector256<byte> zero = Vector256<byte>.Zero;
Vector256<int> accumulator = Vector256<int>.Zero;
for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= scanline.Length - Vector256<byte>.Count; xLeft += (uint)Vector256<byte>.Count)
{
Vector256<byte> scan = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, x));
Vector256<byte> left = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft));
Vector256<byte> above = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref previousBaseRef, x));
Vector256<byte> upperLeft = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref previousBaseRef, xLeft));
Vector256<byte> residual = Avx2.Subtract(scan, PaethPredictor(left, above, upperLeft));
Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = residual;
x += (uint)Vector256<byte>.Count;
accumulator = Avx2.Add(
accumulator,
Avx2.SumAbsoluteDifferences(Avx2.Abs(residual.AsSByte()), zero).AsInt32());
}
sum += Numerics.EvenReduceSum(accumulator);
}
else if (Vector.IsHardwareAccelerated)
{
Vector<uint> accumulator = Vector<uint>.Zero;
for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= scanline.Length - Vector<byte>.Count; xLeft += (uint)Vector<byte>.Count)
{
Vector<byte> scan = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref scanBaseRef, x));
Vector<byte> left = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft));
Vector<byte> above = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref previousBaseRef, x));
Vector<byte> upperLeft = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref previousBaseRef, xLeft));
Vector<byte> residual = scan - PaethPredictor(left, above, upperLeft);
Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = residual;
x += (uint)Vector<byte>.Count;
Numerics.Accumulate(
ref accumulator,
Vector.AsVectorByte(Vector.Abs(Vector.AsVectorSByte(residual))));
}
for (int i = 0; i < Vector<uint>.Count; i++)
{
sum += (int)accumulator[i];
}
}
for (nuint xLeft = x - (uint)bytesPerPixel; x < (uint)scanline.Length; xLeft++)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte left = Unsafe.Add(ref scanBaseRef, xLeft);
byte above = Unsafe.Add(ref previousBaseRef, x);
byte upperLeft = Unsafe.Add(ref previousBaseRef, xLeft);
x++;
ref byte residual = ref Unsafe.Add(ref resultBaseRef, x);
residual = (byte)(scan - PaethPredictor(left, above, upperLeft));
sum += Numerics.Abs(unchecked((sbyte)residual));
}
}
/// <summary>
/// Selects the scalar Paeth predictor.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte PaethPredictor(byte left, byte above, byte upperLeft)
{
int p = left + above - upperLeft;
int distanceLeft = Numerics.Abs(p - left);
int distanceAbove = Numerics.Abs(p - above);
int distanceUpperLeft = Numerics.Abs(p - upperLeft);
if (distanceLeft <= distanceAbove && distanceLeft <= distanceUpperLeft)
{
return left;
}
return distanceAbove <= distanceUpperLeft ? above : upperLeft;
}
/// <summary>
/// Selects the AVX2 Paeth predictor.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<byte> PaethPredictor(
Vector256<byte> left,
Vector256<byte> above,
Vector256<byte> upperLeft)
{
Vector256<byte> zero = Vector256<byte>.Zero;
Vector256<byte> aboveMinusUpper = Avx2.SubtractSaturate(above, upperLeft);
Vector256<byte> leftMinusUpper = Avx2.SubtractSaturate(left, upperLeft);
Vector256<byte> distanceLeft =
Avx2.Or(Avx2.SubtractSaturate(upperLeft, above), aboveMinusUpper);
Vector256<byte> distanceAbove =
Avx2.Or(Avx2.SubtractSaturate(upperLeft, left), leftMinusUpper);
Vector256<byte> sameDirection = Avx2.CompareEqual(
Avx2.CompareEqual(aboveMinusUpper, zero),
Avx2.CompareEqual(leftMinusUpper, zero));
Vector256<byte> distanceUpper = Avx2.Or(
sameDirection,
Avx2.Or(
Avx2.SubtractSaturate(distanceAbove, distanceLeft),
Avx2.SubtractSaturate(distanceLeft, distanceAbove)));
Vector256<byte> minimumAboveUpper = Avx2.Min(distanceUpper, distanceAbove);
Vector256<byte> aboveOrUpper = Avx2.BlendVariable(
upperLeft,
above,
Avx2.CompareEqual(minimumAboveUpper, distanceAbove));
return Avx2.BlendVariable(
aboveOrUpper,
left,
Avx2.CompareEqual(Avx2.Min(minimumAboveUpper, distanceLeft), distanceLeft));
}
/// <summary>
/// Selects the portable vector Paeth predictor.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector<byte> PaethPredictor(
Vector<byte> left,
Vector<byte> above,
Vector<byte> upperLeft)
{
Vector.Widen(left, out Vector<ushort> leftLow, out Vector<ushort> leftHigh);
Vector.Widen(above, out Vector<ushort> aboveLow, out Vector<ushort> aboveHigh);
Vector.Widen(upperLeft, out Vector<ushort> upperLow, out Vector<ushort> upperHigh);
Vector<short> lower = PaethPredictor(
Vector.AsVectorInt16(leftLow),
Vector.AsVectorInt16(aboveLow),
Vector.AsVectorInt16(upperLow));
Vector<short> upper = PaethPredictor(
Vector.AsVectorInt16(leftHigh),
Vector.AsVectorInt16(aboveHigh),
Vector.AsVectorInt16(upperHigh));
return Vector.AsVectorByte(Vector.Narrow(lower, upper));
}
/// <summary>
/// Selects the portable widened Paeth predictor.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector<short> PaethPredictor(
Vector<short> left,
Vector<short> above,
Vector<short> upperLeft)
{
Vector<short> p = left + above - upperLeft;
Vector<short> distanceLeft = Vector.Abs(p - left);
Vector<short> distanceAbove = Vector.Abs(p - above);
Vector<short> distanceUpper = Vector.Abs(p - upperLeft);
Vector<short> chooseLeft = Vector.BitwiseAnd(
Vector.LessThanOrEqual(distanceLeft, distanceAbove),
Vector.LessThanOrEqual(distanceLeft, distanceUpper));
return Vector.ConditionalSelect(
chooseLeft,
left,
Vector.ConditionalSelect(
Vector.LessThanOrEqual(distanceAbove, distanceUpper),
above,
upperLeft));
}
}

134
tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs

@ -208,6 +208,140 @@ public class PngEncoderFilterTests : MeasureFixture
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2);
} }
/// <summary>
/// Verifies every encoder across pixel strides, register boundaries, and hardware widths.
/// </summary>
[Fact]
public void EncodeMatchesReferencesAcrossRegisterBoundaries()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(
AssertEncodersMatchReferences,
HwIntrinsics.AllowAll
| HwIntrinsics.DisableAVX512F
| HwIntrinsics.DisableAVX2
| HwIntrinsics.DisableHWIntrinsic);
/// <summary>
/// Compares every filter with its independent scalar reference across SIMD boundaries and pixel strides.
/// </summary>
private static void AssertEncodersMatchReferences()
{
int[] bytesPerPixels = [1, 2, 3, 4, 6, 8];
int[] lengths = [8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129, 131, 132, 133, 135, 136, 137, 257];
foreach (int bytesPerPixel in bytesPerPixels)
{
foreach (int length in lengths)
{
if (length < bytesPerPixel)
{
continue;
}
byte[] scanline = new byte[length];
byte[] previousScanline = new byte[length];
Random random = new((bytesPerPixel * 397) + length);
random.NextBytes(scanline);
random.NextBytes(previousScanline);
AssertFilterMatchesReference(
PngFilterMethod.Sub,
scanline,
previousScanline,
bytesPerPixel);
AssertFilterMatchesReference(
PngFilterMethod.Up,
scanline,
previousScanline,
bytesPerPixel);
AssertFilterMatchesReference(
PngFilterMethod.Average,
scanline,
previousScanline,
bytesPerPixel);
AssertFilterMatchesReference(
PngFilterMethod.Paeth,
scanline,
previousScanline,
bytesPerPixel);
}
}
}
/// <summary>
/// Compares one filter result and variance sum with its scalar reference.
/// </summary>
/// <param name="filter">The filter to evaluate.</param>
/// <param name="scanline">The current scanline.</param>
/// <param name="previousScanline">The preceding scanline.</param>
/// <param name="bytesPerPixel">The component distance between adjacent pixels.</param>
private static void AssertFilterMatchesReference(
PngFilterMethod filter,
byte[] scanline,
byte[] previousScanline,
int bytesPerPixel)
{
byte[] expected = new byte[scanline.Length + 1];
byte[] actual = new byte[scanline.Length + 1];
int expectedSum;
int actualSum;
switch (filter)
{
case PngFilterMethod.Sub:
ReferenceImplementations.EncodeSubFilter(scanline, expected, bytesPerPixel, out expectedSum);
SubFilter.Encode(scanline, actual, bytesPerPixel, out actualSum);
break;
case PngFilterMethod.Up:
ReferenceImplementations.EncodeUpFilter(scanline, previousScanline, expected, out expectedSum);
UpFilter.Encode(scanline, previousScanline, actual, out actualSum);
break;
case PngFilterMethod.Average:
ReferenceImplementations.EncodeAverageFilter(
scanline,
previousScanline,
expected,
bytesPerPixel,
out expectedSum);
AverageFilter.Encode(
scanline,
previousScanline,
actual,
(uint)bytesPerPixel,
out actualSum);
break;
case PngFilterMethod.Paeth:
ReferenceImplementations.EncodePaethFilter(
scanline,
previousScanline,
expected,
bytesPerPixel,
out expectedSum);
PaethFilter.Encode(
scanline,
previousScanline,
actual,
bytesPerPixel,
out actualSum);
break;
default:
throw new InvalidOperationException();
}
Assert.Equal(expectedSum, actualSum);
Assert.Equal(expected, actual);
}
public class TestData public class TestData
{ {
private readonly PngFilterMethod filter; private readonly PngFilterMethod filter;

Loading…
Cancel
Save