diff --git a/src/ImageSharp/Formats/Png/PngCgbiProcessor.cs b/src/ImageSharp/Formats/Png/PngCgbiProcessor.cs
new file mode 100644
index 0000000000..6478acced4
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/PngCgbiProcessor.cs
@@ -0,0 +1,325 @@
+// 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;
+using SixLabors.ImageSharp.Common.Helpers;
+using SixLabors.ImageSharp.PixelFormats;
+using static SixLabors.ImageSharp.SimdUtils;
+
+namespace SixLabors.ImageSharp.Formats.Png;
+
+///
+/// Reverses the pixel mangling applied by Apple's CgBI PNG variant. CgBI files
+/// (emitted by pngcrush -iphone) swap channel order from RGB(A) to BGR(A)
+/// and premultiply RGB samples by alpha. This converts a defiltered scanline back
+/// to standard PNG semantics in place so the existing scanline processors can
+/// consume it unchanged. CgBI is only emitted for 8-bit truecolor (with or
+/// without alpha); other color types are left alone.
+///
+///
+/// See https://theapplewiki.com/wiki/PNG_CgBI_Format
+///
+internal static class PngCgbiProcessor
+{
+ // Per-pixel byte indices that swap CgBI's BGRA layout to Rgba32's RGBA.
+ // MMShuffle3012 expands to [2, 1, 0, 3] per 4-byte pixel; the same 64-byte
+ // sequence seeds all three shuffle masks (V128/V256 ctors take the leading
+ // 16/32 bytes).
+ private static readonly Vector128 BgraToRgbaShuffle128 = Vector128.Create(BuildShuffleBytes());
+
+ private static readonly Vector256 BgraToRgbaShuffle256 = Vector256.Create(BuildShuffleBytes());
+
+ private static readonly Vector512 BgraToRgbaShuffle512 = Vector512.Create(BuildShuffleBytes());
+
+ ///
+ /// Applies the inverse of Apple's CgBI pixel mangling to a defiltered scanline in place.
+ ///
+ /// The configuration used by the Rgb24 R/B swap.
+ /// The defiltered pixel bytes (without the leading filter byte).
+ /// The PNG color type from IHDR.
+ public static void ApplyTransform(Configuration configuration, Span scanline, PngColorType colorType)
+ {
+ if (colorType == PngColorType.RgbWithAlpha)
+ {
+ Span pixels = MemoryMarshal.Cast(scanline);
+ int i = 0;
+
+ // Avx512BW is required for the per-byte vpshufb. Without it, ShuffleNative
+ // falls back to a software cross-lane shuffle that is slower than the V256
+ // path, so skip V512 entirely on Avx512F-only hosts.
+ if (Vector512.IsHardwareAccelerated && Avx512BW.IsSupported && pixels.Length >= 16)
+ {
+ i = ApplyTransformVector512(scanline, pixels.Length);
+ }
+
+ if (Vector256.IsHardwareAccelerated && Avx2.IsSupported && (pixels.Length - i) >= 8)
+ {
+ i = ApplyTransformVector256(scanline, i, pixels.Length);
+ }
+
+ if (Vector128.IsHardwareAccelerated && (pixels.Length - i) >= 4)
+ {
+ i = ApplyTransformVector128(scanline, i, pixels.Length);
+ }
+
+ for (; i < pixels.Length; i++)
+ {
+ ref Rgba32 pixel = ref pixels[i];
+ pixel = new Rgba32(pixel.B, pixel.G, pixel.R, pixel.A);
+ UndoPremultiplicationScalar(ref pixel);
+ }
+ }
+ else if (colorType == PngColorType.Rgb)
+ {
+ // No alpha channel, so just swap R and B using built in SIMD-optimized pixel operations.
+ Span target = MemoryMarshal.Cast(scanline);
+ PixelOperations.Instance.FromBgr24Bytes(configuration, scanline, target, target.Length);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void UndoPremultiplicationScalar(ref Rgba32 pixel)
+ {
+ byte a = pixel.A;
+ if (a is 0 or byte.MaxValue)
+ {
+ return;
+ }
+
+ // Reverse: c' = c * a / 255 => c = round(c' * 255 / a)
+ int half = a >> 1;
+ byte r = (byte)Math.Min(byte.MaxValue, ((pixel.R * byte.MaxValue) + half) / a);
+ byte g = (byte)Math.Min(byte.MaxValue, ((pixel.G * byte.MaxValue) + half) / a);
+ byte b = (byte)Math.Min(byte.MaxValue, ((pixel.B * byte.MaxValue) + half) / a);
+ pixel = new Rgba32(r, g, b, a);
+ }
+
+ internal static int ApplyTransformVector512(Span scanline, int pixelCount)
+ {
+ ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline);
+ int i = 0;
+
+ // The mask only swaps bytes inside each 4-byte pixel, so it is correct
+ // for the per-lane Avx512BW.Shuffle that ShuffleNative selects here.
+ Vector512 shuffleMask = BgraToRgbaShuffle512;
+
+ Vector512 zero = Vector512.Zero;
+ Vector512 one = Vector512.One;
+ Vector512 byteMask = Vector512.Create(0xFF);
+ Vector512 opaque = Vector512.Create(0xFF);
+ Vector512 byteMax = Vector512.Create((int)byte.MaxValue);
+
+ for (; i <= pixelCount - 16; i += 16)
+ {
+ ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf());
+ Vector512 bgra = Unsafe.ReadUnaligned>(ref blockRef);
+ Vector512 rgba = Vector512_.ShuffleNative(bgra, shuffleMask);
+ Vector512 packed = rgba.AsInt32();
+ Vector512 alpha = Vector512.ShiftRightLogical(packed, 24);
+
+ // Fully transparent and fully opaque pixels are identity cases for
+ // unpremultiplication. Masking them keeps the scalar behavior and lets
+ // safeAlpha avoid dividing by zero for alpha == 0.
+ Vector512 partialMask = ~(Vector512.Equals(alpha, zero) | Vector512.Equals(alpha, opaque));
+
+ Vector512 r = packed & byteMask;
+ Vector512 g = Vector512.ShiftRightLogical(packed, 8) & byteMask;
+ Vector512 b = Vector512.ShiftRightLogical(packed, 16) & byteMask;
+
+ Vector512 safeAlpha = Vector512.ConditionalSelect(partialMask, alpha, one);
+ Vector512 halfAlpha = Vector512.ShiftRightLogical(safeAlpha, 1);
+ Vector512 safeAlphaF = Vector512.ConvertToSingle(safeAlpha);
+
+ // The scalar path computes ((c * 255) + (a >> 1)) / a with integer
+ // division. Floor the positive quotient before converting so SIMD does
+ // not use the default round-to-nearest conversion and drift by one.
+ Vector512 unpremultipliedR = Vector512.Min(
+ byteMax,
+ Vector512.ConvertToInt32(Vector512.Floor(Vector512.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF)));
+
+ Vector512 unpremultipliedG = Vector512.Min(
+ byteMax,
+ Vector512.ConvertToInt32(Vector512.Floor(Vector512.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF)));
+
+ Vector512 unpremultipliedB = Vector512.Min(
+ byteMax,
+ Vector512.ConvertToInt32(Vector512.Floor(Vector512.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF)));
+
+ // ConditionalSelect applies the expensive unpremultiply only to pixels
+ // where alpha is between 1 and 254; alpha 0 and 255 lanes keep the
+ // shuffled channel values exactly as the scalar path does.
+ Vector512 finalR = Vector512.ConditionalSelect(partialMask, unpremultipliedR, r);
+ Vector512 finalG = Vector512.ConditionalSelect(partialMask, unpremultipliedG, g);
+ Vector512 finalB = Vector512.ConditionalSelect(partialMask, unpremultipliedB, b);
+
+ // Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so
+ // shifting the unpacked channels back to byte offsets 0, 1, 2, and 3
+ // recreates the in-memory RGBA bytes for the unaligned store.
+ Vector512 result =
+ finalR |
+ Vector512.ShiftLeft(finalG, 8) |
+ Vector512.ShiftLeft(finalB, 16) |
+ Vector512.ShiftLeft(alpha, 24);
+
+ Unsafe.WriteUnaligned(ref blockRef, result.AsByte());
+ }
+
+ return i;
+ }
+
+ internal static int ApplyTransformVector256(Span scanline, int startPixel, int pixelCount)
+ {
+ ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline);
+ int i = startPixel;
+
+ // vpshufb is 128-bit lane-local and uses only the low 4 bits of each
+ // index, so the same per-pixel [2,1,0,3] pattern in both lanes keeps
+ // every byte inside its own lane.
+ Vector256 shuffleMask = BgraToRgbaShuffle256;
+
+ Vector256 zero = Vector256.Zero;
+ Vector256 one = Vector256.One;
+ Vector256 byteMask = Vector256.Create(0xFF);
+ Vector256 opaque = Vector256.Create(0xFF);
+ Vector256 byteMax = Vector256.Create((int)byte.MaxValue);
+
+ for (; i <= pixelCount - 8; i += 8)
+ {
+ ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf());
+ Vector256 bgra = Unsafe.ReadUnaligned>(ref blockRef);
+ Vector256 rgba = Vector256_.ShufflePerLane(bgra, shuffleMask);
+ Vector256 packed = rgba.AsInt32();
+ Vector256 alpha = Vector256.ShiftRightLogical(packed, 24);
+
+ // Fully transparent and fully opaque pixels are identity cases for
+ // unpremultiplication. Masking them keeps the scalar behavior and lets
+ // safeAlpha avoid dividing by zero for alpha == 0.
+ Vector256 partialMask = ~(Vector256.Equals(alpha, zero) | Vector256.Equals(alpha, opaque));
+
+ Vector256 r = packed & byteMask;
+ Vector256 g = Vector256.ShiftRightLogical(packed, 8) & byteMask;
+ Vector256 b = Vector256.ShiftRightLogical(packed, 16) & byteMask;
+
+ Vector256 safeAlpha = Vector256.ConditionalSelect(partialMask, alpha, one);
+ Vector256 halfAlpha = Vector256.ShiftRightLogical(safeAlpha, 1);
+ Vector256 safeAlphaF = Vector256.ConvertToSingle(safeAlpha);
+
+ // The scalar path computes ((c * 255) + (a >> 1)) / a with integer
+ // division. Floor the positive quotient before converting so SIMD does
+ // not use the default round-to-nearest conversion and drift by one.
+ Vector256 unpremultipliedR = Vector256.Min(
+ byteMax,
+ Vector256.ConvertToInt32(Vector256.Floor(Vector256.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF)));
+
+ Vector256 unpremultipliedG = Vector256.Min(
+ byteMax,
+ Vector256.ConvertToInt32(Vector256.Floor(Vector256.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF)));
+
+ Vector256 unpremultipliedB = Vector256.Min(
+ byteMax,
+ Vector256.ConvertToInt32(Vector256.Floor(Vector256.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF)));
+
+ // ConditionalSelect applies the expensive unpremultiply only to pixels
+ // where alpha is between 1 and 254; alpha 0 and 255 lanes keep the
+ // shuffled channel values exactly as the scalar path does.
+ Vector256 finalR = Vector256.ConditionalSelect(partialMask, unpremultipliedR, r);
+ Vector256 finalG = Vector256.ConditionalSelect(partialMask, unpremultipliedG, g);
+ Vector256 finalB = Vector256.ConditionalSelect(partialMask, unpremultipliedB, b);
+
+ // Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so
+ // shifting the unpacked channels back to byte offsets 0, 1, 2, and 3
+ // recreates the in-memory RGBA bytes for the unaligned store.
+ Vector256 result =
+ finalR |
+ Vector256.ShiftLeft(finalG, 8) |
+ Vector256.ShiftLeft(finalB, 16) |
+ Vector256.ShiftLeft(alpha, 24);
+
+ Unsafe.WriteUnaligned(ref blockRef, result.AsByte());
+ }
+
+ return i;
+ }
+
+ internal static int ApplyTransformVector128(Span scanline, int startPixel, int pixelCount)
+ {
+ ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline);
+ int i = startPixel;
+
+ Vector128 shuffleMask = BgraToRgbaShuffle128;
+
+ Vector128 zero = Vector128.Zero;
+ Vector128 one = Vector128.One;
+ Vector128 byteMask = Vector128.Create(0xFF);
+ Vector128 opaque = Vector128.Create(0xFF);
+ Vector128 byteMax = Vector128.Create((int)byte.MaxValue);
+
+ for (; i <= pixelCount - 4; i += 4)
+ {
+ ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf());
+ Vector128 bgra = Unsafe.ReadUnaligned>(ref blockRef);
+ Vector128 rgba = Vector128_.ShuffleNative(bgra, shuffleMask);
+ Vector128 packed = rgba.AsInt32();
+ Vector128 alpha = Vector128.ShiftRightLogical(packed, 24);
+
+ // Fully transparent and fully opaque pixels are identity cases for
+ // unpremultiplication. Masking them keeps the scalar behavior and lets
+ // safeAlpha avoid dividing by zero for alpha == 0.
+ Vector128 partialMask = ~(Vector128.Equals(alpha, zero) | Vector128.Equals(alpha, opaque));
+
+ Vector128 r = packed & byteMask;
+ Vector128 g = Vector128.ShiftRightLogical(packed, 8) & byteMask;
+ Vector128 b = Vector128.ShiftRightLogical(packed, 16) & byteMask;
+
+ Vector128 safeAlpha = Vector128.ConditionalSelect(partialMask, alpha, one);
+ Vector128 halfAlpha = Vector128.ShiftRightLogical(safeAlpha, 1);
+ Vector128 safeAlphaF = Vector128.ConvertToSingle(safeAlpha);
+
+ // The scalar path computes ((c * 255) + (a >> 1)) / a with integer
+ // division. Floor the positive quotient before converting so SIMD does
+ // not use the default round-to-nearest conversion and drift by one.
+ Vector128 unpremultipliedR = Vector128.Min(
+ byteMax,
+ Vector128.ConvertToInt32(Vector128.Floor(Vector128.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF)));
+
+ Vector128 unpremultipliedG = Vector128.Min(
+ byteMax,
+ Vector128.ConvertToInt32(Vector128.Floor(Vector128.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF)));
+
+ Vector128 unpremultipliedB = Vector128.Min(
+ byteMax,
+ Vector128.ConvertToInt32(Vector128.Floor(Vector128.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF)));
+
+ // ConditionalSelect applies the expensive unpremultiply only to pixels
+ // where alpha is between 1 and 254; alpha 0 and 255 lanes keep the
+ // shuffled channel values exactly as the scalar path does.
+ Vector128 finalR = Vector128.ConditionalSelect(partialMask, unpremultipliedR, r);
+ Vector128 finalG = Vector128.ConditionalSelect(partialMask, unpremultipliedG, g);
+ Vector128 finalB = Vector128.ConditionalSelect(partialMask, unpremultipliedB, b);
+
+ // Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so
+ // shifting the unpacked channels back to byte offsets 0, 1, 2, and 3
+ // recreates the in-memory RGBA bytes for the unaligned store.
+ Vector128 result =
+ finalR |
+ Vector128.ShiftLeft(finalG, 8) |
+ Vector128.ShiftLeft(finalB, 16) |
+ Vector128.ShiftLeft(alpha, 24);
+
+ Unsafe.WriteUnaligned(ref blockRef, result.AsByte());
+ }
+
+ return i;
+ }
+
+ private static byte[] BuildShuffleBytes()
+ {
+ byte[] bytes = new byte[64];
+ Span span = bytes;
+ Shuffle.MMShuffleSpan(ref span, Shuffle.MMShuffle3012);
+ return bytes;
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 49b6fabc60..f3e2bbdbe0 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -9,8 +9,6 @@ using System.IO.Compression;
using System.IO.Hashing;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-using System.Runtime.Intrinsics;
-using System.Runtime.Intrinsics.X86;
using System.Text;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib;
@@ -922,7 +920,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
if (this.isCgbi)
{
- this.ApplyCgbiTransform(scanSpan[1..], this.pngColorType);
+ PngCgbiProcessor.ApplyTransform(this.configuration, scanSpan[1..], this.pngColorType);
}
this.ProcessDefilteredScanline(frameControl, currentRow, scanSpan, imageFrame, pngMetadata, blendRowBuffer);
@@ -1057,7 +1055,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
if (this.isCgbi)
{
- this.ApplyCgbiTransform(scanSpan[1..], this.pngColorType);
+ PngCgbiProcessor.ApplyTransform(this.configuration, scanSpan[1..], this.pngColorType);
}
Span rowSpan = imageBuffer.DangerousGetRowSpan(currentRow);
@@ -2529,303 +2527,4 @@ internal sealed class PngDecoderCore : ImageDecoderCore
private void SwapScanlineBuffers()
=> (this.scanline, this.previousScanline) = (this.previousScanline, this.scanline);
-
- ///
- /// Applies the inverse of Apple's CgBI pixel mangling to a defiltered scanline.
- /// CgBI PNGs are emitted by pngcrush -iphone with channel order swapped
- /// from RGB(A) to BGR(A) and RGB samples premultiplied by alpha. This converts
- /// the bytes back to standard PNG semantics in place so the existing scanline
- /// processors can consume them unchanged. CgBI is only emitted for 8-bit
- /// truecolor (with or without alpha); other color types are left alone.
- ///
- ///
- /// See https://theapplewiki.com/wiki/PNG_CgBI_Format
- ///
- /// The defiltered pixel bytes (without the leading filter byte).
- /// The PNG color type from IHDR.
- private void ApplyCgbiTransform(Span scanline, PngColorType colorType)
- {
- if (colorType == PngColorType.RgbWithAlpha)
- {
- Span pixels = MemoryMarshal.Cast(scanline);
- int i = 0;
-
- if (Vector512.IsHardwareAccelerated && pixels.Length >= 16)
- {
- i = ApplyCgbiTransformVector512(scanline, pixels.Length);
- }
-
- if (Vector256.IsHardwareAccelerated && Avx2.IsSupported && (pixels.Length - i) >= 8)
- {
- i = ApplyCgbiTransformVector256(scanline, i, pixels.Length);
- }
-
- if (Vector128.IsHardwareAccelerated && (pixels.Length - i) >= 4)
- {
- i = ApplyCgbiTransformVector128(scanline, i, pixels.Length);
- }
-
- for (; i < pixels.Length; i++)
- {
- ref Rgba32 pixel = ref pixels[i];
- pixel = new Rgba32(pixel.B, pixel.G, pixel.R, pixel.A);
- UndoCgbiPremultiplicationScalar(ref pixel);
- }
- }
- else if (colorType == PngColorType.Rgb)
- {
- // No alpha channel, so just swap R and B using built in SIMD-optimized pixel operations.
- Span target = MemoryMarshal.Cast(scanline);
- PixelOperations.Instance.FromBgr24Bytes(this.configuration, scanline, target, target.Length);
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void UndoCgbiPremultiplicationScalar(ref Rgba32 pixel)
- {
- byte a = pixel.A;
- if (a is 0 or byte.MaxValue)
- {
- return;
- }
-
- // Reverse: c' = c * a / 255 => c = round(c' * 255 / a)
- int half = a >> 1;
- byte r = (byte)Math.Min(byte.MaxValue, ((pixel.R * byte.MaxValue) + half) / a);
- byte g = (byte)Math.Min(byte.MaxValue, ((pixel.G * byte.MaxValue) + half) / a);
- byte b = (byte)Math.Min(byte.MaxValue, ((pixel.B * byte.MaxValue) + half) / a);
- pixel = new Rgba32(r, g, b, a);
- }
-
- private static int ApplyCgbiTransformVector512(Span scanline, int pixelCount)
- {
- ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline);
- int i = 0;
-
- Span temp = stackalloc byte[Vector512.Count];
- SimdUtils.Shuffle.MMShuffleSpan(ref temp, SimdUtils.Shuffle.MMShuffle3012);
-
- // MMShuffle3012 expands to [2, 1, 0, 3] for each 4-byte pixel, converting
- // CgBI's BGRA byte order to Rgba32's RGBA layout while keeping alpha in place.
- // The generated mask only swaps bytes inside each pixel, so it remains
- // correct for the optimized 512-bit byte shuffle helper.
- Vector512 shuffleMask = Unsafe.As>(ref MemoryMarshal.GetReference(temp));
-
- Vector512 zero = Vector512.Zero;
- Vector512 one = Vector512.One;
- Vector512 byteMask = Vector512.Create(0xFF);
- Vector512 opaque = Vector512.Create(0xFF);
- Vector512 byteMax = Vector512.Create((int)byte.MaxValue);
-
- for (; i <= pixelCount - 16; i += 16)
- {
- ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf());
- Vector512 bgra = Unsafe.ReadUnaligned>(ref blockRef);
- Vector512 rgba = Vector512_.ShuffleNative(bgra, shuffleMask);
- Vector512 packed = rgba.AsInt32();
- Vector512 alpha = Vector512.ShiftRightLogical(packed, 24);
-
- // Fully transparent and fully opaque pixels are identity cases for
- // unpremultiplication. Masking them keeps the scalar behavior and lets
- // safeAlpha avoid dividing by zero for alpha == 0.
- Vector512 partialMask = ~(Vector512.Equals(alpha, zero) | Vector512.Equals(alpha, opaque));
-
- Vector512 r = packed & byteMask;
- Vector512 g = Vector512.ShiftRightLogical(packed, 8) & byteMask;
- Vector512 b = Vector512.ShiftRightLogical(packed, 16) & byteMask;
-
- Vector512 safeAlpha = Vector512.ConditionalSelect(partialMask, alpha, one);
- Vector512 halfAlpha = Vector512.ShiftRightLogical(safeAlpha, 1);
- Vector512 safeAlphaF = Vector512.ConvertToSingle(safeAlpha);
-
- // The scalar path computes ((c * 255) + (a >> 1)) / a with integer
- // division. Floor the positive quotient before converting so SIMD does
- // not use the default round-to-nearest conversion and drift by one.
- Vector512 unpremultipliedR = Vector512.Min(
- byteMax,
- Vector512.ConvertToInt32(Vector512.Floor(Vector512.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF)));
-
- Vector512 unpremultipliedG = Vector512.Min(
- byteMax,
- Vector512.ConvertToInt32(Vector512.Floor(Vector512.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF)));
-
- Vector512 unpremultipliedB = Vector512.Min(
- byteMax,
- Vector512.ConvertToInt32(Vector512.Floor(Vector512.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF)));
-
- // ConditionalSelect applies the expensive unpremultiply only to pixels
- // where alpha is between 1 and 254; alpha 0 and 255 lanes keep the
- // shuffled channel values exactly as the scalar path does.
- Vector512 finalR = Vector512.ConditionalSelect(partialMask, unpremultipliedR, r);
- Vector512 finalG = Vector512.ConditionalSelect(partialMask, unpremultipliedG, g);
- Vector512 finalB = Vector512.ConditionalSelect(partialMask, unpremultipliedB, b);
-
- // Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so
- // shifting the unpacked channels back to byte offsets 0, 1, 2, and 3
- // recreates the in-memory RGBA bytes for the unaligned store.
- Vector512 result =
- finalR |
- Vector512.ShiftLeft(finalG, 8) |
- Vector512.ShiftLeft(finalB, 16) |
- Vector512.ShiftLeft(alpha, 24);
-
- Unsafe.WriteUnaligned(ref blockRef, result.AsByte());
- }
-
- return i;
- }
-
- private static int ApplyCgbiTransformVector256(Span scanline, int startPixel, int pixelCount)
- {
- ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline);
- int i = startPixel;
-
- Span temp = stackalloc byte[Vector512.Count];
- SimdUtils.Shuffle.MMShuffleSpan(ref temp, SimdUtils.Shuffle.MMShuffle3012);
-
- // MMShuffle3012 expands to [2, 1, 0, 3] for each 4-byte pixel, converting
- // CgBI's BGRA byte order to Rgba32's RGBA layout while keeping alpha in place.
- // Avx2.Shuffle is 128-bit lane-local, and the generated mask repeats inside
- // each lane, so no byte ever needs to cross the lane boundary.
- Vector256 shuffleMask = Unsafe.As>(ref MemoryMarshal.GetReference(temp));
-
- Vector256 zero = Vector256.Zero;
- Vector256 one = Vector256.One;
- Vector256 byteMask = Vector256.Create(0xFF);
- Vector256 opaque = Vector256.Create(0xFF);
- Vector256 byteMax = Vector256.Create((int)byte.MaxValue);
-
- for (; i <= pixelCount - 8; i += 8)
- {
- ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf());
- Vector256 bgra = Unsafe.ReadUnaligned>(ref blockRef);
- Vector256 rgba = Vector256_.ShufflePerLane(bgra, shuffleMask);
- Vector256 packed = rgba.AsInt32();
- Vector256 alpha = Vector256.ShiftRightLogical(packed, 24);
-
- // Fully transparent and fully opaque pixels are identity cases for
- // unpremultiplication. Masking them keeps the scalar behavior and lets
- // safeAlpha avoid dividing by zero for alpha == 0.
- Vector256 partialMask = ~(Vector256.Equals(alpha, zero) | Vector256.Equals(alpha, opaque));
-
- Vector256 r = packed & byteMask;
- Vector256 g = Vector256.ShiftRightLogical(packed, 8) & byteMask;
- Vector256 b = Vector256.ShiftRightLogical(packed, 16) & byteMask;
-
- Vector256 safeAlpha = Vector256.ConditionalSelect(partialMask, alpha, one);
- Vector256 halfAlpha = Vector256.ShiftRightLogical(safeAlpha, 1);
- Vector256 safeAlphaF = Vector256.ConvertToSingle(safeAlpha);
-
- // The scalar path computes ((c * 255) + (a >> 1)) / a with integer
- // division. Floor the positive quotient before converting so SIMD does
- // not use the default round-to-nearest conversion and drift by one.
- Vector256 unpremultipliedR = Vector256.Min(
- byteMax,
- Vector256.ConvertToInt32(Vector256.Floor(Vector256.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF)));
-
- Vector256 unpremultipliedG = Vector256.Min(
- byteMax,
- Vector256.ConvertToInt32(Vector256.Floor(Vector256.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF)));
-
- Vector256 unpremultipliedB = Vector256.Min(
- byteMax,
- Vector256.ConvertToInt32(Vector256.Floor(Vector256.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF)));
-
- // ConditionalSelect applies the expensive unpremultiply only to pixels
- // where alpha is between 1 and 254; alpha 0 and 255 lanes keep the
- // shuffled channel values exactly as the scalar path does.
- Vector256 finalR = Vector256.ConditionalSelect(partialMask, unpremultipliedR, r);
- Vector256 finalG = Vector256.ConditionalSelect(partialMask, unpremultipliedG, g);
- Vector256 finalB = Vector256.ConditionalSelect(partialMask, unpremultipliedB, b);
-
- // Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so
- // shifting the unpacked channels back to byte offsets 0, 1, 2, and 3
- // recreates the in-memory RGBA bytes for the unaligned store.
- Vector256 result =
- finalR |
- Vector256.ShiftLeft(finalG, 8) |
- Vector256.ShiftLeft(finalB, 16) |
- Vector256.ShiftLeft(alpha, 24);
-
- Unsafe.WriteUnaligned(ref blockRef, result.AsByte());
- }
-
- return i;
- }
-
- private static int ApplyCgbiTransformVector128(Span scanline, int startPixel, int pixelCount)
- {
- ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline);
- int i = startPixel;
-
- Span temp = stackalloc byte[Vector512.Count];
- SimdUtils.Shuffle.MMShuffleSpan(ref temp, SimdUtils.Shuffle.MMShuffle3012);
-
- // MMShuffle3012 expands to [2, 1, 0, 3] for each 4-byte pixel, converting
- // CgBI's BGRA byte order to Rgba32's RGBA layout while keeping alpha in place.
- Vector128 shuffleMask = Unsafe.As>(ref MemoryMarshal.GetReference(temp));
-
- Vector128 zero = Vector128.Zero;
- Vector128 one = Vector128.One;
- Vector128 byteMask = Vector128.Create(0xFF);
- Vector128 opaque = Vector128.Create(0xFF);
- Vector128 byteMax = Vector128.Create((int)byte.MaxValue);
-
- for (; i <= pixelCount - 4; i += 4)
- {
- ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf());
- Vector128 bgra = Unsafe.ReadUnaligned>(ref blockRef);
- Vector128 rgba = Vector128_.ShuffleNative(bgra, shuffleMask);
- Vector128 packed = rgba.AsInt32();
- Vector128 alpha = Vector128.ShiftRightLogical(packed, 24);
-
- // Fully transparent and fully opaque pixels are identity cases for
- // unpremultiplication. Masking them keeps the scalar behavior and lets
- // safeAlpha avoid dividing by zero for alpha == 0.
- Vector128 partialMask = ~(Vector128.Equals(alpha, zero) | Vector128.Equals(alpha, opaque));
-
- Vector128 r = packed & byteMask;
- Vector128 g = Vector128.ShiftRightLogical(packed, 8) & byteMask;
- Vector128 b = Vector128.ShiftRightLogical(packed, 16) & byteMask;
-
- Vector128 safeAlpha = Vector128.ConditionalSelect(partialMask, alpha, one);
- Vector128 halfAlpha = Vector128.ShiftRightLogical(safeAlpha, 1);
- Vector128 safeAlphaF = Vector128.ConvertToSingle(safeAlpha);
-
- // The scalar path computes ((c * 255) + (a >> 1)) / a with integer
- // division. Floor the positive quotient before converting so SIMD does
- // not use the default round-to-nearest conversion and drift by one.
- Vector128 unpremultipliedR = Vector128.Min(
- byteMax,
- Vector128.ConvertToInt32(Vector128.Floor(Vector128.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF)));
-
- Vector128 unpremultipliedG = Vector128.Min(
- byteMax,
- Vector128.ConvertToInt32(Vector128.Floor(Vector128.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF)));
-
- Vector128 unpremultipliedB = Vector128.Min(
- byteMax,
- Vector128.ConvertToInt32(Vector128.Floor(Vector128.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF)));
-
- // ConditionalSelect applies the expensive unpremultiply only to pixels
- // where alpha is between 1 and 254; alpha 0 and 255 lanes keep the
- // shuffled channel values exactly as the scalar path does.
- Vector128 finalR = Vector128.ConditionalSelect(partialMask, unpremultipliedR, r);
- Vector128 finalG = Vector128.ConditionalSelect(partialMask, unpremultipliedG, g);
- Vector128 finalB = Vector128.ConditionalSelect(partialMask, unpremultipliedB, b);
-
- // Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so
- // shifting the unpacked channels back to byte offsets 0, 1, 2, and 3
- // recreates the in-memory RGBA bytes for the unaligned store.
- Vector128 result =
- finalR |
- Vector128.ShiftLeft(finalG, 8) |
- Vector128.ShiftLeft(finalB, 16) |
- Vector128.ShiftLeft(alpha, 24);
-
- Unsafe.WriteUnaligned(ref blockRef, result.AsByte());
- }
-
- return i;
- }
}
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngCgbiProcessorTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngCgbiProcessorTests.cs
new file mode 100644
index 0000000000..426afb6d42
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Png/PngCgbiProcessorTests.cs
@@ -0,0 +1,174 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Png;
+
+[Trait("Format", "Png")]
+public class PngCgbiProcessorTests
+{
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(3)]
+ [InlineData(4)]
+ [InlineData(7)]
+ [InlineData(8)]
+ [InlineData(15)]
+ [InlineData(16)]
+ [InlineData(17)]
+ [InlineData(31)]
+ [InlineData(32)]
+ [InlineData(33)]
+ [InlineData(64)]
+ public void ApplyTransform_RgbWithAlpha_MatchesScalar(int pixelCount)
+ {
+ // Drives the full V512/V256/V128/scalar dispatch, so it covers each
+ // path that is hardware-accelerated on the host plus the scalar tail.
+ byte[] input = CreateBgraScanline(pixelCount);
+ byte[] processorOutput = (byte[])input.Clone();
+ byte[] scalarOutput = (byte[])input.Clone();
+
+ PngCgbiProcessor.ApplyTransform(Configuration.Default, processorOutput, PngColorType.RgbWithAlpha);
+ ApplyCgbiTransformScalarReference(scalarOutput);
+
+ Assert.Equal(scalarOutput, processorOutput);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(3)]
+ [InlineData(4)]
+ [InlineData(7)]
+ [InlineData(8)]
+ [InlineData(15)]
+ [InlineData(16)]
+ [InlineData(17)]
+ [InlineData(31)]
+ [InlineData(32)]
+ [InlineData(33)]
+ [InlineData(64)]
+ public void ApplyTransformVector512_MatchesScalar(int pixelCount) =>
+ // Vector512 uses Vector512_.ShuffleNative which falls back to the software
+ // Vector512.Shuffle when Avx512BW is unavailable, so the body runs regardless
+ // of whether Vector512 is hardware-accelerated on the host.
+ AssertVectorMatchesScalar(
+ pixelCount,
+ scanline => PngCgbiProcessor.ApplyTransformVector512(scanline, scanline.Length / 4),
+ blockSize: 16);
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(3)]
+ [InlineData(4)]
+ [InlineData(7)]
+ [InlineData(8)]
+ [InlineData(15)]
+ [InlineData(16)]
+ [InlineData(17)]
+ [InlineData(31)]
+ [InlineData(32)]
+ [InlineData(64)]
+ public void ApplyTransformVector256_MatchesScalar(int pixelCount) => AssertVectorMatchesScalar(
+ pixelCount,
+ scanline => PngCgbiProcessor.ApplyTransformVector256(scanline, 0, scanline.Length / 4),
+ blockSize: 8);
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(3)]
+ [InlineData(4)]
+ [InlineData(7)]
+ [InlineData(8)]
+ [InlineData(15)]
+ [InlineData(16)]
+ [InlineData(64)]
+ public void ApplyTransformVector128_MatchesScalar(int pixelCount) => AssertVectorMatchesScalar(
+ pixelCount,
+ scanline => PngCgbiProcessor.ApplyTransformVector128(scanline, 0, scanline.Length / 4),
+ blockSize: 4);
+
+ private static void AssertVectorMatchesScalar(int pixelCount, Func applyVector, int blockSize)
+ {
+ byte[] input = CreateBgraScanline(pixelCount);
+ byte[] vectorOutput = (byte[])input.Clone();
+ byte[] scalarOutput = (byte[])input.Clone();
+
+ int processed = applyVector(vectorOutput);
+
+ int expectedProcessed = (pixelCount / blockSize) * blockSize;
+ Assert.Equal(expectedProcessed, processed);
+
+ // The vector path is responsible for whole blocks only; remaining pixels are
+ // handled by the scalar tail in ApplyTransform. Run the scalar reference
+ // over every pixel and compare the prefix the vector path actually wrote.
+ ApplyCgbiTransformScalarReference(scalarOutput);
+
+ Span vectorProcessed = vectorOutput.AsSpan(0, processed * 4);
+ Span scalarProcessed = scalarOutput.AsSpan(0, processed * 4);
+ Assert.True(vectorProcessed.SequenceEqual(scalarProcessed), $"Mismatch at pixelCount={pixelCount}");
+
+ // Pixels past the vector's processed prefix must be untouched.
+ Span vectorTail = vectorOutput.AsSpan(processed * 4);
+ Span inputTail = input.AsSpan(processed * 4);
+ Assert.True(vectorTail.SequenceEqual(inputTail));
+ }
+
+ private static byte[] CreateBgraScanline(int pixelCount)
+ {
+ // Deterministic mix of edge cases (a=0, a=255, partial alpha) and varied channels.
+ byte[] bytes = new byte[pixelCount * 4];
+ for (int p = 0; p < pixelCount; p++)
+ {
+ byte a = (p % 7) switch
+ {
+ 0 => byte.MinValue,
+ 1 => byte.MaxValue,
+ _ => (byte)((((p * 37) + 23) & 0xFF) | 1) // never zero
+ };
+
+ // CgBI premultiplied BGRA: c' = c * a / 255
+ byte r = (byte)((p * 13) & 0xFF);
+ byte g = (byte)((p * 29) & 0xFF);
+ byte b = (byte)((p * 53) & 0xFF);
+ r = (byte)((r * a) / byte.MaxValue);
+ g = (byte)((g * a) / byte.MaxValue);
+ b = (byte)((b * a) / byte.MaxValue);
+
+ bytes[(p * 4) + 0] = b;
+ bytes[(p * 4) + 1] = g;
+ bytes[(p * 4) + 2] = r;
+ bytes[(p * 4) + 3] = a;
+ }
+
+ return bytes;
+ }
+
+ private static void ApplyCgbiTransformScalarReference(Span scanline)
+ {
+ Span pixels = MemoryMarshal.Cast(scanline);
+ for (int i = 0; i < pixels.Length; i++)
+ {
+ ref Rgba32 pixel = ref pixels[i];
+ pixel = new Rgba32(pixel.B, pixel.G, pixel.R, pixel.A);
+
+ byte a = pixel.A;
+ if (a is 0 or byte.MaxValue)
+ {
+ continue;
+ }
+
+ int half = a >> 1;
+ byte r = (byte)Math.Min(byte.MaxValue, ((pixel.R * byte.MaxValue) + half) / a);
+ byte g = (byte)Math.Min(byte.MaxValue, ((pixel.G * byte.MaxValue) + half) / a);
+ byte b = (byte)Math.Min(byte.MaxValue, ((pixel.B * byte.MaxValue) + half) / a);
+ pixel = new Rgba32(r, g, b, a);
+ }
+ }
+}