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); + } + } +}