diff --git a/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.Parameters.cs b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.Parameters.cs new file mode 100644 index 000000000..42982f751 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.Parameters.cs @@ -0,0 +1,262 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Color; + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc.Color; + +/// +/// Provides fixed-point scalar and SIMD coefficient storage for eight-bit 4:2:0 conversion. +/// +internal static partial class HevcYuv420ToRgb8Converter +{ + /// + /// Stores every scalar and SIMD coefficient representation resolved once for an image. + /// + private readonly struct ConversionParameters + { + /// + /// The scalar fixed-point coefficients. + /// + public readonly FixedPointParameters Scalar; + + /// + /// The four-lane SIMD coefficients. + /// + public readonly Vector128Parameters FourLane; + + /// + /// The eight-lane SIMD coefficients. + /// + public readonly Vector256Parameters EightLane; + + /// + /// The sixteen-lane SIMD coefficients. + /// + public readonly Vector512Parameters SixteenLane; + + /// + /// Initializes a new instance of the struct. + /// + /// The shared floating-point conversion parameters. + public ConversionParameters(in HeifColorConversionParameters parameters) + { + FixedPointParameters scalar = new(in parameters); + this.Scalar = scalar; + this.FourLane = new(in scalar); + this.EightLane = new(in scalar); + this.SixteenLane = new(in scalar); + } + } + + /// + /// Stores the scalar fixed-point coefficients resolved for one image. + /// + private readonly struct FixedPointParameters + { + /// + /// Initializes a new instance of the struct. + /// + /// The shared floating-point conversion parameters. + public FixedPointParameters(in HeifColorConversionParameters parameters) + { + float scale = 1 << CoefficientShift; + + // Rounding each image-invariant coefficient once gives the integer kernel eight fractional bits. + // The signed green coefficients retain the exact addition and rounding order used by every SIMD lane. + this.RedCr = (int)MathF.Round(parameters.RedChromaScale * scale, MidpointRounding.AwayFromZero); + this.GreenCb = -(int)MathF.Round(parameters.GreenBlueChromaScale * scale, MidpointRounding.AwayFromZero); + this.GreenCr = -(int)MathF.Round(parameters.GreenRedChromaScale * scale, MidpointRounding.AwayFromZero); + this.BlueCb = (int)MathF.Round(parameters.BlueChromaScale * scale, MidpointRounding.AwayFromZero); + } + + /// + /// Gets the red contribution from centered Cr. + /// + public int RedCr { get; } + + /// + /// Gets the green contribution from centered Cb. + /// + public int GreenCb { get; } + + /// + /// Gets the green contribution from centered Cr. + /// + public int GreenCr { get; } + + /// + /// Gets the blue contribution from centered Cb. + /// + public int BlueCb { get; } + } + + /// + /// Broadcasts the fixed-point coefficients for four-lane conversion. + /// + private readonly struct Vector128Parameters + { + /// + /// Initializes a new instance of the struct. + /// + /// The scalar fixed-point coefficients. + public Vector128Parameters(in FixedPointParameters parameters) + { + this.ChromaMidpoint = Vector128.Create(HevcYuv420ToRgb8Converter.ChromaMidpoint); + this.RoundingBias = Vector128.Create(HevcYuv420ToRgb8Converter.RoundingBias); + this.Maximum = Vector128.Create((int)byte.MaxValue); + this.RedCr = Vector128.Create(parameters.RedCr); + this.GreenCb = Vector128.Create(parameters.GreenCb); + this.GreenCr = Vector128.Create(parameters.GreenCr); + this.BlueCb = Vector128.Create(parameters.BlueCb); + } + + /// + /// Gets the neutral chroma code-value lanes. + /// + public Vector128 ChromaMidpoint { get; } + + /// + /// Gets the fixed-point rounding-bias lanes. + /// + public Vector128 RoundingBias { get; } + + /// + /// Gets the maximum eight-bit sample lanes. + /// + public Vector128 Maximum { get; } + + /// + /// Gets the red Cr coefficient lanes. + /// + public Vector128 RedCr { get; } + + /// + /// Gets the green Cb coefficient lanes. + /// + public Vector128 GreenCb { get; } + + /// + /// Gets the green Cr coefficient lanes. + /// + public Vector128 GreenCr { get; } + + /// + /// Gets the blue Cb coefficient lanes. + /// + public Vector128 BlueCb { get; } + } + + /// + /// Broadcasts the fixed-point coefficients for eight-lane conversion. + /// + private readonly struct Vector256Parameters + { + /// + /// Initializes a new instance of the struct. + /// + /// The scalar fixed-point coefficients. + public Vector256Parameters(in FixedPointParameters parameters) + { + this.ChromaMidpoint = Vector256.Create(HevcYuv420ToRgb8Converter.ChromaMidpoint); + this.RoundingBias = Vector256.Create(HevcYuv420ToRgb8Converter.RoundingBias); + this.Maximum = Vector256.Create((int)byte.MaxValue); + this.RedCr = Vector256.Create(parameters.RedCr); + this.GreenCb = Vector256.Create(parameters.GreenCb); + this.GreenCr = Vector256.Create(parameters.GreenCr); + this.BlueCb = Vector256.Create(parameters.BlueCb); + } + + /// + /// Gets the neutral chroma code-value lanes. + /// + public Vector256 ChromaMidpoint { get; } + + /// + /// Gets the fixed-point rounding-bias lanes. + /// + public Vector256 RoundingBias { get; } + + /// + /// Gets the maximum eight-bit sample lanes. + /// + public Vector256 Maximum { get; } + + /// + /// Gets the red Cr coefficient lanes. + /// + public Vector256 RedCr { get; } + + /// + /// Gets the green Cb coefficient lanes. + /// + public Vector256 GreenCb { get; } + + /// + /// Gets the green Cr coefficient lanes. + /// + public Vector256 GreenCr { get; } + + /// + /// Gets the blue Cb coefficient lanes. + /// + public Vector256 BlueCb { get; } + } + + /// + /// Broadcasts the fixed-point coefficients for sixteen-lane conversion. + /// + private readonly struct Vector512Parameters + { + /// + /// Initializes a new instance of the struct. + /// + /// The scalar fixed-point coefficients. + public Vector512Parameters(in FixedPointParameters parameters) + { + this.ChromaMidpoint = Vector512.Create(HevcYuv420ToRgb8Converter.ChromaMidpoint); + this.RoundingBias = Vector512.Create(HevcYuv420ToRgb8Converter.RoundingBias); + this.Maximum = Vector512.Create((int)byte.MaxValue); + this.RedCr = Vector512.Create(parameters.RedCr); + this.GreenCb = Vector512.Create(parameters.GreenCb); + this.GreenCr = Vector512.Create(parameters.GreenCr); + this.BlueCb = Vector512.Create(parameters.BlueCb); + } + + /// + /// Gets the neutral chroma code-value lanes. + /// + public Vector512 ChromaMidpoint { get; } + + /// + /// Gets the fixed-point rounding-bias lanes. + /// + public Vector512 RoundingBias { get; } + + /// + /// Gets the maximum eight-bit sample lanes. + /// + public Vector512 Maximum { get; } + + /// + /// Gets the red Cr coefficient lanes. + /// + public Vector512 RedCr { get; } + + /// + /// Gets the green Cb coefficient lanes. + /// + public Vector512 GreenCb { get; } + + /// + /// Gets the green Cr coefficient lanes. + /// + public Vector512 GreenCr { get; } + + /// + /// Gets the blue Cb coefficient lanes. + /// + public Vector512 BlueCb { get; } + } +} diff --git a/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.Simd.cs b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.Simd.cs new file mode 100644 index 000000000..65223ea44 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.Simd.cs @@ -0,0 +1,293 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using static SixLabors.ImageSharp.Formats.Heif.Color.HeifColorConverterBase; + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc.Color; + +/// +/// Provides the fixed-point scalar and SIMD row kernels for eight-bit 4:2:0 conversion. +/// +internal static partial class HevcYuv420ToRgb8Converter +{ + /// + /// Converts one luma row and its nearest native chroma row to planar eight-bit RGB. + /// + /// The full-resolution luma samples. + /// The half-width blue-difference samples. + /// The half-width red-difference samples. + /// The destination red samples. + /// The destination green samples. + /// The destination blue samples. + /// The fixed-point matrix coefficients. + private static void ConvertRow( + ReadOnlySpan luma, + ReadOnlySpan chromaBlue, + ReadOnlySpan chromaRed, + Span red, + Span green, + Span blue, + in ConversionParameters parameters) + { + ref ushort lumaBase = ref MemoryMarshal.GetReference(luma); + ref ushort chromaBlueBase = ref MemoryMarshal.GetReference(chromaBlue); + ref ushort chromaRedBase = ref MemoryMarshal.GetReference(chromaRed); + ref byte redBase = ref MemoryMarshal.GetReference(red); + ref byte greenBase = ref MemoryMarshal.GetReference(green); + ref byte blueBase = ref MemoryMarshal.GetReference(blue); + int x = 0; + + // The shared offset lets the widest supported register consume the row first. Narrower widths then + // handle the complete remainder, leaving at most three pixels for the scalar fallback. + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = luma.Length - Vector512.Count; + + for (; x <= oneVectorFromEnd; x += Vector512.Count) + { + Vector512 y = LoadVector512(ref Unsafe.Add(ref lumaBase, x)); + Vector512 cb = LoadRepeatedVector512(ref Unsafe.Add(ref chromaBlueBase, x >> 1)); + Vector512 cr = LoadRepeatedVector512(ref Unsafe.Add(ref chromaRedBase, x >> 1)); + + Convert(y, cb, cr, in parameters.SixteenLane, out Vector512 r, out Vector512 g, out Vector512 b); + HeifByteSampleStorer.Store(r, ref Unsafe.Add(ref redBase, x)); + HeifByteSampleStorer.Store(g, ref Unsafe.Add(ref greenBase, x)); + HeifByteSampleStorer.Store(b, ref Unsafe.Add(ref blueBase, x)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = luma.Length - Vector256.Count; + + for (; x <= oneVectorFromEnd; x += Vector256.Count) + { + Vector256 y = LoadVector256(ref Unsafe.Add(ref lumaBase, x)); + Vector256 cb = LoadRepeatedVector256(ref Unsafe.Add(ref chromaBlueBase, x >> 1)); + Vector256 cr = LoadRepeatedVector256(ref Unsafe.Add(ref chromaRedBase, x >> 1)); + + Convert(y, cb, cr, in parameters.EightLane, out Vector256 r, out Vector256 g, out Vector256 b); + HeifByteSampleStorer.Store(r, ref Unsafe.Add(ref redBase, x)); + HeifByteSampleStorer.Store(g, ref Unsafe.Add(ref greenBase, x)); + HeifByteSampleStorer.Store(b, ref Unsafe.Add(ref blueBase, x)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = luma.Length - Vector128.Count; + + for (; x <= oneVectorFromEnd; x += Vector128.Count) + { + Vector128 y = LoadVector128(ref Unsafe.Add(ref lumaBase, x)); + Vector128 cb = LoadRepeatedVector128(ref Unsafe.Add(ref chromaBlueBase, x >> 1)); + Vector128 cr = LoadRepeatedVector128(ref Unsafe.Add(ref chromaRedBase, x >> 1)); + + Convert(y, cb, cr, in parameters.FourLane, out Vector128 r, out Vector128 g, out Vector128 b); + HeifByteSampleStorer.Store(r, ref Unsafe.Add(ref redBase, x)); + HeifByteSampleStorer.Store(g, ref Unsafe.Add(ref greenBase, x)); + HeifByteSampleStorer.Store(b, ref Unsafe.Add(ref blueBase, x)); + } + } + + for (; x < luma.Length; x++) + { + Convert( + Unsafe.Add(ref lumaBase, x), + Unsafe.Add(ref chromaBlueBase, x >> 1), + Unsafe.Add(ref chromaRedBase, x >> 1), + in parameters.Scalar, + out Unsafe.Add(ref redBase, x), + out Unsafe.Add(ref greenBase, x), + out Unsafe.Add(ref blueBase, x)); + } + } + + /// + /// Loads sixteen luma samples as signed 32-bit SIMD lanes. + /// + /// The first native luma sample. + /// The widened luma lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 LoadVector512(ref ushort source) + { + (Vector256 lower, Vector256 upper) = Vector256.Widen(Vector256.LoadUnsafe(ref source)); + return Vector512.Create(lower, upper).AsInt32(); + } + + /// + /// Loads eight luma samples as signed 32-bit SIMD lanes. + /// + /// The first native luma sample. + /// The widened luma lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 LoadVector256(ref ushort source) + { + Vector128 samples = Vector128.LoadUnsafe(ref source); + return Vector256.Create(Vector128.WidenLower(samples), Vector128.WidenUpper(samples)).AsInt32(); + } + + /// + /// Loads four luma samples as signed 32-bit SIMD lanes. + /// + /// The first native luma sample. + /// The widened luma lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 LoadVector128(ref ushort source) + { + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref source)); + return Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsUInt16()).AsInt32(); + } + + /// + /// Loads eight chroma samples and repeats each sample into two of sixteen 32-bit SIMD lanes. + /// + /// The first native chroma sample. + /// The horizontally replicated chroma lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 LoadRepeatedVector512(ref ushort source) + { + Vector128 samples = Vector128.LoadUnsafe(ref source); + Vector128 lower = Vector128_.UnpackLow(samples.AsInt16(), samples.AsInt16()).AsUInt16(); + Vector128 upper = Vector128_.UnpackHigh(samples.AsInt16(), samples.AsInt16()).AsUInt16(); + (Vector256 widenedLower, Vector256 widenedUpper) = Vector256.Widen(Vector256.Create(lower, upper)); + return Vector512.Create(widenedLower, widenedUpper).AsInt32(); + } + + /// + /// Loads four chroma samples and repeats each sample into two of eight 32-bit SIMD lanes. + /// + /// The first native chroma sample. + /// The horizontally replicated chroma lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 LoadRepeatedVector256(ref ushort source) + { + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref source)); + Vector128 samples = Vector128.CreateScalarUnsafe(packed).AsUInt16(); + Vector128 repeated = Vector128_.UnpackLow(samples.AsInt16(), samples.AsInt16()).AsUInt16(); + return Vector256.Create(Vector128.WidenLower(repeated), Vector128.WidenUpper(repeated)).AsInt32(); + } + + /// + /// Loads two chroma samples and repeats each sample into two of four 32-bit SIMD lanes. + /// + /// The first native chroma sample. + /// The horizontally replicated chroma lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 LoadRepeatedVector128(ref ushort source) + { + uint packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref source)); + Vector128 samples = Vector128.CreateScalarUnsafe(packed).AsUInt16(); + Vector128 repeated = Vector128_.UnpackLow(samples.AsInt16(), samples.AsInt16()).AsUInt16(); + return Vector128.WidenLower(repeated).AsInt32(); + } + + /// + /// Converts one coefficient-based H.273 YCbCr sample to eight-bit RGB. + /// + /// The luma sample. + /// The blue-difference sample. + /// The red-difference sample. + /// The fixed-point matrix coefficients. + /// The converted red sample. + /// The converted green sample. + /// The converted blue sample. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Convert(ushort y, ushort cb, ushort cr, in FixedPointParameters parameters, out byte r, out byte g, out byte b) + { + int centeredBlue = cb - ChromaMidpoint; + int centeredRed = cr - ChromaMidpoint; + int red = y + (((parameters.RedCr * centeredRed) + RoundingBias) >> CoefficientShift); + int green = y + (((parameters.GreenCb * centeredBlue) + (parameters.GreenCr * centeredRed) + RoundingBias) >> CoefficientShift); + int blue = y + (((parameters.BlueCb * centeredBlue) + RoundingBias) >> CoefficientShift); + + r = (byte)Numerics.Clamp(red, 0, byte.MaxValue); + g = (byte)Numerics.Clamp(green, 0, byte.MaxValue); + b = (byte)Numerics.Clamp(blue, 0, byte.MaxValue); + } + + /// + /// Converts four coefficient-based H.273 YCbCr samples to eight-bit RGB lanes. + /// + /// The luma lanes. + /// The blue-difference lanes. + /// The red-difference lanes. + /// The fixed-point matrix coefficient lanes. + /// The converted red lanes. + /// The converted green lanes. + /// The converted blue lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Convert( + Vector128 y, + Vector128 cb, + Vector128 cr, + in Vector128Parameters parameters, + out Vector128 r, + out Vector128 g, + out Vector128 b) + { + cb -= parameters.ChromaMidpoint; + cr -= parameters.ChromaMidpoint; + r = Vector128.Clamp(y + (((parameters.RedCr * cr) + parameters.RoundingBias) >> CoefficientShift), Vector128.Zero, parameters.Maximum); + g = Vector128.Clamp(y + (((parameters.GreenCb * cb) + (parameters.GreenCr * cr) + parameters.RoundingBias) >> CoefficientShift), Vector128.Zero, parameters.Maximum); + b = Vector128.Clamp(y + (((parameters.BlueCb * cb) + parameters.RoundingBias) >> CoefficientShift), Vector128.Zero, parameters.Maximum); + } + + /// + /// Converts eight coefficient-based H.273 YCbCr samples to eight-bit RGB lanes. + /// + /// The luma lanes. + /// The blue-difference lanes. + /// The red-difference lanes. + /// The fixed-point matrix coefficient lanes. + /// The converted red lanes. + /// The converted green lanes. + /// The converted blue lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Convert( + Vector256 y, + Vector256 cb, + Vector256 cr, + in Vector256Parameters parameters, + out Vector256 r, + out Vector256 g, + out Vector256 b) + { + cb -= parameters.ChromaMidpoint; + cr -= parameters.ChromaMidpoint; + r = Vector256.Clamp(y + (((parameters.RedCr * cr) + parameters.RoundingBias) >> CoefficientShift), Vector256.Zero, parameters.Maximum); + g = Vector256.Clamp(y + (((parameters.GreenCb * cb) + (parameters.GreenCr * cr) + parameters.RoundingBias) >> CoefficientShift), Vector256.Zero, parameters.Maximum); + b = Vector256.Clamp(y + (((parameters.BlueCb * cb) + parameters.RoundingBias) >> CoefficientShift), Vector256.Zero, parameters.Maximum); + } + + /// + /// Converts sixteen coefficient-based H.273 YCbCr samples to eight-bit RGB lanes. + /// + /// The luma lanes. + /// The blue-difference lanes. + /// The red-difference lanes. + /// The fixed-point matrix coefficient lanes. + /// The converted red lanes. + /// The converted green lanes. + /// The converted blue lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Convert( + Vector512 y, + Vector512 cb, + Vector512 cr, + in Vector512Parameters parameters, + out Vector512 r, + out Vector512 g, + out Vector512 b) + { + cb -= parameters.ChromaMidpoint; + cr -= parameters.ChromaMidpoint; + r = Vector512.Clamp(y + (((parameters.RedCr * cr) + parameters.RoundingBias) >> CoefficientShift), Vector512.Zero, parameters.Maximum); + g = Vector512.Clamp(y + (((parameters.GreenCb * cb) + (parameters.GreenCr * cr) + parameters.RoundingBias) >> CoefficientShift), Vector512.Zero, parameters.Maximum); + b = Vector512.Clamp(y + (((parameters.BlueCb * cb) + parameters.RoundingBias) >> CoefficientShift), Vector512.Zero, parameters.Maximum); + } +} diff --git a/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.cs b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.cs new file mode 100644 index 000000000..5fb3e0dd7 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.cs @@ -0,0 +1,92 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats.Heif.Color; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc.Color; + +/// +/// Converts full-range eight-bit HEVC 4:2:0 planes with an unspecified matrix to packed RGB pixels. +/// +internal static partial class HevcYuv420ToRgb8Converter +{ + /// + /// The fixed-point precision used for H.273 matrix coefficients. + /// + private const int CoefficientShift = 8; + + /// + /// The half-unit bias used before fixed-point coefficient results are shifted to integer samples. + /// + private const int RoundingBias = 1 << (CoefficientShift - 1); + + /// + /// The neutral code value for full-range eight-bit chroma. + /// + private const int ChromaMidpoint = 128; + + /// + /// Determines whether the specialized integer conversion supports the supplied picture and color description. + /// + /// The reconstructed HEVC picture. + /// The effective H.273 color description. + /// The resolved H.273 conversion operation. + /// when the picture can use this converter; otherwise, . + public static bool IsSupported(HevcPictureBuffer picture, CicpProfile colorProfile, HeifColorConversionMode mode) + => picture.ChromaFormat == 1 + && !picture.SeparateColorPlane + && picture.BitDepthLuma == 8 + && picture.BitDepthChroma == 8 + && colorProfile.FullRange + && colorProfile.MatrixCoefficients == CicpMatrixCoefficients.Unspecified + && mode == HeifColorConversionMode.Coefficients; + + /// + /// Converts a supported HEVC picture to packed pixels using integer SIMD with a scalar tail. + /// + /// The destination pixel type. + /// The configuration used for allocation and pixel conversion. + /// The reconstructed HEVC picture. + /// The destination image frame. + /// The resolved H.273 conversion parameters. + /// The horizontal luma-sample offset of the output window. + /// The vertical luma-sample offset of the output window. + public static void Convert( + Configuration configuration, + HevcPictureBuffer picture, + ImageFrame image, + in HeifColorConversionParameters parameters, + int sourceX, + int sourceY) + where TPixel : unmanaged, IPixel + { + ConversionParameters conversionParameters = new(in parameters); + using IMemoryOwner componentOwner = configuration.MemoryAllocator.Allocate(image.Width * 3); + Span components = componentOwner.GetSpan(); + Span red = components[..image.Width]; + Span green = components.Slice(image.Width, image.Width); + Span blue = components.Slice(image.Width * 2, image.Width); + + for (int y = 0; y < image.Height; y++) + { + int lumaY = sourceY + y; + + // HEVC expresses 4:2:0 conformance-window offsets in complete chroma sample units, so both source + // offsets are even here. The unspecified-matrix presentation replicates each native chroma sample + // across its 2x2 luma cell before applying the default BT.601 coefficients. + ReadOnlySpan luma = picture.GetRowSpan(HevcPlane.Y, lumaY).Slice(sourceX, image.Width); + ReadOnlySpan chromaBlue = picture.GetRowSpan(HevcPlane.Cb, lumaY >> 1).Slice(sourceX >> 1); + ReadOnlySpan chromaRed = picture.GetRowSpan(HevcPlane.Cr, lumaY >> 1).Slice(sourceX >> 1); + + ConvertRow(luma, chromaBlue, chromaRed, red, green, blue, in conversionParameters); + + Span destination = image.PixelBuffer.DangerousGetRowSpan(y); + PixelOperations.Instance.PackFromRgbPlanes(red, green, blue, destination); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcYuvConverter.cs b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuvConverter.cs similarity index 96% rename from src/ImageSharp/Formats/Heif/Hevc/HevcYuvConverter.cs rename to src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuvConverter.cs index 012ff858e..dfc2d0a6e 100644 --- a/src/ImageSharp/Formats/Heif/Hevc/HevcYuvConverter.cs +++ b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuvConverter.cs @@ -11,12 +11,12 @@ using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.PixelFormats; using static SixLabors.ImageSharp.Formats.Heif.Color.HeifColorConverterBase; -namespace SixLabors.ImageSharp.Formats.Heif.Hevc; +namespace SixLabors.ImageSharp.Formats.Heif.Hevc.Color; /// /// Converts between reconstructed HEVC component planes and packed ImageSharp pixels. /// -internal static class HevcYuvConverter +internal static partial class HevcYuvConverter { /// /// The largest value represented by an eight-bit packed RGB component. @@ -60,6 +60,15 @@ internal static class HevcYuvConverter where TPixel : unmanaged, IPixel { HeifColorConversionParameters parameters = GetConversionParameters(picture, colorProfile, out HeifColorConversionMode mode); + + // H.273 resolves an unspecified matrix to BT.601 coefficients. The common full-range eight-bit 4:2:0 + // presentation can therefore remain in the integer sample domain and avoid float staging and rounding. + if (HevcYuv420ToRgb8Converter.IsSupported(picture, colorProfile, mode)) + { + HevcYuv420ToRgb8Converter.Convert(configuration, picture, image, in parameters, sourceX, sourceY); + return; + } + HeifColorConverterBase colorConverter = HeifColorConverterBase.Create(mode, in parameters, picture.ChromaFormat == 0); YuvToRgbRowConverter converter = new(configuration, picture, image, colorConverter, chromaSampleLocation, sourceX, sourceY); using IMemoryOwner scratchOwner = configuration.MemoryAllocator.Allocate(converter.BufferLength); @@ -67,11 +76,9 @@ internal static class HevcYuvConverter if (converter.UsesBytePacking) { - using IMemoryOwner proxyOwner = configuration.MemoryAllocator.Allocate(image.Width + 3); - Span proxy = proxyOwner.GetSpan()[..(image.Width + 3)]; for (int y = 0; y < image.Height; y++) { - converter.Convert(y, scratch, proxy); + converter.Convert(y, scratch); } return; @@ -79,7 +86,7 @@ internal static class HevcYuvConverter for (int y = 0; y < image.Height; y++) { - converter.Convert(y, scratch, Span.Empty); + converter.Convert(y, scratch); } } @@ -317,8 +324,7 @@ internal static class HevcYuvConverter /// /// The zero-based luma row. /// The reusable pooled row buffer. - /// The padded byte-packing destination when the image row has insufficient padding. - public void Convert(int y, Span scratch, Span proxy) + public void Convert(int y, Span scratch) { int width = this.image.Width; Span red = scratch[..width]; @@ -391,15 +397,7 @@ internal static class HevcYuvConverter SimdUtils.NormalizedFloatToByteSaturate(green, greenBytes); SimdUtils.NormalizedFloatToByteSaturate(blue, blueBytes); - if (this.image.PixelBuffer.DangerousTryGetPaddedRowSpan(y, 3, out Span paddedDestination)) - { - PixelOperations.Instance.PackFromRgbPlanes(redBytes, greenBytes, blueBytes, paddedDestination); - } - else - { - PixelOperations.Instance.PackFromRgbPlanes(redBytes, greenBytes, blueBytes, proxy); - proxy[..width].CopyTo(destination); - } + PixelOperations.Instance.PackFromRgbPlanes(redBytes, greenBytes, blueBytes, destination); return; } diff --git a/src/ImageSharp/Formats/Heif/HevcHeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/HevcHeifItemDecoder.cs index 4ba80c86c..e6899994d 100644 --- a/src/ImageSharp/Formats/Heif/HevcHeifItemDecoder.cs +++ b/src/ImageSharp/Formats/Heif/HevcHeifItemDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Heif.Hevc; +using SixLabors.ImageSharp.Formats.Heif.Hevc.Color; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.PixelFormats; diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/HevcColorConversionBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/HevcColorConversionBenchmarks.cs index 11f6ee658..010d9498b 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Heif/HevcColorConversionBenchmarks.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Heif/HevcColorConversionBenchmarks.cs @@ -3,6 +3,7 @@ using BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.Formats.Heif.Hevc; +using SixLabors.ImageSharp.Formats.Heif.Hevc.Color; using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.PixelFormats; diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs index e66fc7cff..a8e1528bf 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs @@ -84,10 +84,7 @@ public class HeifDecoderTests HeifMetadata metadata = image.Metadata.GetHeifMetadata(); image.DebugSave(provider); - // The extracted native YUV tiles have byte-exact HM coverage. Pinned libheif 1.23.1 selects its cheaper fused - // nearest-neighbor RGB path by default, so the full-image oracle permits the bounded difference from our - // bilinear reconstruction while still covering grids, alpha composition, color conversion, and presentation. - image.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.6F), provider); + image.CompareToReferenceOutput(ImageComparer.Exact, provider); Assert.Equal(new Size(width, height), image.Size); Assert.Equal(HeifCompressionMethod.Hevc, metadata.CompressionMethod); diff --git a/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcYuvConverterTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcYuvConverterTests.cs index 1b66a49b1..b6ea5b42c 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcYuvConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcYuvConverterTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Heif.Hevc; +using SixLabors.ImageSharp.Formats.Heif.Hevc.Color; using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.PixelFormats;