diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 29a47001f..aa8c30dcc 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -34,12 +34,19 @@ Checkboxes may be marked complete only when the implementation and the verificat - [x] Preserve the exact ICC payload from an independently encoded AVIF primary item. - [x] Prove that a genuine non-sRGB AVIF profile changes decoded pixels and matches the independently converted source image within the documented AV1 tolerance. - [x] Verify canonical-sRGB compaction and metadata skipping independently from ICC preservation and color conversion. + - [x] Replace the AV1 RGB/YUV arithmetic layer with JPEG's closed-generic static operator pattern in both directions: frame-scoped operator selection, in-place planar decode, RGB inputs with `out` component planes for encode, `Vector512`/`Vector256`/`Vector128` traversal, and one scalar tail. + - [x] Keep codec row scheduling sequential and reuse frame-scoped allocator-backed buffers. Reuse JPEG's optimized `UnpackIntoRgbPlanes` contract for 8-bit encoding and pooled `Rgb48` staging for 10/12-bit encoding so high-precision input is not truncated through an eight-bit or `Vector4` intermediate. + - [x] Implement SIMD range expansion, chroma reconstruction, matrix/transfer conversion, clamping, and RGB packing for coefficient YCbCr, identity GBR, YCgCo, constant luminance, SMPTE ST 2085, and PQ/HLG ICtCp, with focused SIMD-lane/scalar-tail parity coverage. + - [x] Implement SIMD 4:2:0 and 4:2:2 encoder downsampling with odd-width and odd-height tails, and verify the stored 8/12-bit chroma samples against independently encoded full-resolution planes. - [ ] Complete the SIMD YUV/CICP paths for every supported AV1 bit depth, chroma format, range, matrix, transfer function, color primary, and chroma position, with scalar fallback only when hardware vectorization is unavailable or the operation is inherently non-vectorizable. - [x] Apply ICC conversion only after the SIMD YUV/CICP stage, alpha composition, grid assembly, and presentation transforms have produced the presented RGB image; retain ImageSharp's shared ICC converter and optimize reusable bulk kernels rather than creating a HEIF-specific color-management implementation. - [ ] Verify ICC preservation, conversion, compaction, and metadata skipping for grids, alpha-composited images, every presented sequence frame, and the completed HEVC path. - [x] Independently encode AVIF primary, grid, alpha-auxiliary, and two-frame sequence vectors with libavif 1.4.2/libaom 3.14.1; verify exact profile preservation, non-sRGB conversion, canonical-sRGB compaction, non-sRGB compact preservation, metadata skipping, every presented frame, and unchanged composed alpha. - [ ] Repeat the same matrix for HEVC primary, grid, alpha-auxiliary, and sequence paths after HEVC reconstruction is complete. - [ ] Benchmark representative 8/10/12-bit end-to-end AVIF and HEIC color pipelines with and without ICC conversion, recording absolute timings and allocations before marking Phase 5 complete. + - [x] Add permanent frame-wide `Av1ColorConversionBenchmarks` coverage for sequential 1920x1080 YUV 4:2:0 conversion in both directions at 8, 10, and 12 bits, including managed-allocation reporting. + - [x] Record the .NET 10 short-run AV1 baseline on 2026-08-25. SIMD decode measured 4.025/2.803/2.742 ms and encode measured 3.750/2.555/2.450 ms at 8/10/12 bits with 488-744 B allocated per frame. Forced-scalar decode measured 31.15/29.47/28.40 ms and encode measured 19.19/15.77/15.14 ms, making the normal SIMD pipeline 7.7-10.5 times faster for decode and 5.1-6.2 times faster for encode on this machine. + - [ ] Add representative ICC-enabled AVIF cases and the equivalent HEVC cases after the HEVC reconstruction path is complete. - [x] **Completed:** remove production `ToArray()` materializations from the HEIF implementation and make every retained copy an explicit ownership decision. - [x] Store AV1 palette colors and HEVC short-term reference-picture state inline at their normative fixed bounds. - [x] Retain only the AV1 configuration sequence-header payload required for item/sample equivalence instead of every configuration OBU. @@ -458,9 +465,17 @@ Exit gate: Tasks: - [ ] Implement monochrome, 4:2:0, 4:2:2, and 4:4:4 plane access with every valid signaled chroma sample position. + - [x] Implement SIMD AV1 4:2:0 and 4:2:2 chroma reconstruction and downsampling, including centered/colocated reconstruction and odd image edges. + - [ ] Complete the HEVC plane paths and the exhaustive AV1/HEVC chroma-position matrix before closing this item. - [ ] Implement full- and limited-range expansion for 8, 10, and 12-bit samples across every supported plane layout. + - [x] Implement AV1 range expansion and quantization with SIMD-first byte/ushort loaders and storers and pooled planar rows. + - [ ] Complete the equivalent HEVC paths and independently verify the full cross-product. - [ ] Implement every non-reserved HEVC/AV1 color-primary, transfer-characteristic, and matrix-coefficient signaling path, including identity conversion, with correct fixed-point rounding and clipping. + - [x] Implement the AV1 H.273 matrix and transfer-function operators in both directions, including identity, derived coefficients, constant luminance, SMPTE ST 2085, and PQ/HLG ICtCp. + - [ ] Add independent conformance vectors for signaling combinations that libavif deliberately rejects, then complete and verify the HEVC operator set. - [ ] Make SIMD the default decode path for YUV range expansion, chroma reconstruction, H.273 matrix and transfer conversion, clamping, and 8/10/12-bit RGB packing. Use the established static operator pattern with `Vector512`, `Vector256`, and `Vector128` dispatch followed by one scalar tail that remains the behavioral oracle. + - [x] AV1 uses the JPEG-style static operator contract and descending SIMD-width traversal for decode and encode, with scalar execution limited to the remaining lanes or hardware without vector support. + - [ ] Complete the same default SIMD pipeline for HEVC and record end-to-end measurements for both codecs. - [ ] Apply embedded ICC profiles after codec YUV/CICP conversion and image composition through ImageSharp's existing color-profile converter. Cover preserve, convert, compact, and ignore behavior for primary items, grids, alpha-composited images, and every presented sequence frame; never substitute CICP or an assumed sRGB profile for an embedded ICC profile. - Primary AVIF verification passes four independently scoped cases: exact profile preservation, non-sRGB conversion with changed pixels and paired-source comparison, canonical-sRGB compaction without pixel changes, and metadata skipping. Grid, auxiliary-alpha, sequence, and HEVC cases remain open. - [ ] Decode alpha auxiliary items as monochrome planes, validate dimensions and bit depth, and compose them without losing precision. Define premultiplication behavior from AVIF signaling and ImageSharp's pixel contract. @@ -479,6 +494,8 @@ Delete the JPEG payload path from the production encoder. Keep the synchronous I Implement in vertical slices that always produce a decodable AV1 bitstream: - [ ] Convert RGB/RGBA to AV1 planes for every 8, 10, and 12-bit output, range, matrix, and monochrome/4:2:0/4:2:2/4:4:4 combination permitted by the selected AV1 profile. + - [x] Implement the pooled sequential RGB-to-planar pipeline, 8/10/12-bit quantization, every implemented H.273 operator, and SIMD 4:2:0/4:2:2 downsampling with odd-edge handling. + - [ ] Complete the exhaustive format/range/matrix/transfer/primary cross-product and validate encoded planes against an independent implementation before closing this item. - [ ] Write sequence, frame, tile-group, and metadata OBUs for a reduced still picture. - [ ] Implement a temporary smallest-valid intra-only vertical slice using existing partition, prediction, transform, quantization, coefficient, and entropy structures. - [ ] Complete block geometry, neighbor/context updates, transform selection and forward transforms, quantization, coefficient tokenization, and range coding. diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1ColorConversionParameters.cs b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConversionParameters.cs new file mode 100644 index 000000000..e2581248e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConversionParameters.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Stores the resolved H.273 values shared by every scalar and SIMD lane. +/// +internal readonly struct Av1ColorConversionParameters +{ + /// + /// Initializes a new instance of the struct. + /// + /// The red luma coefficient. + /// The green luma coefficient. + /// The blue luma coefficient. + /// The signaled transfer characteristics. + /// The constant-luminance chroma scales. + /// The encoded luma bias. + /// The encoded luma range. + /// The encoded chroma midpoint. + /// The encoded chroma range. + public Av1ColorConversionParameters( + float kr, + float kg, + float kb, + ObuTransferCharacteristics transferCharacteristics, + in Av1ConstantLuminanceScales constantLuminanceScales, + float lumaBias, + float lumaScale, + float chromaBias, + float chromaScale) + { + this.Kr = kr; + this.Kg = kg; + this.Kb = kb; + this.RedChromaScale = 2F * (1F - kr); + this.BlueChromaScale = 2F * (1F - kb); + this.GreenRedChromaScale = 2F * kr * (1F - kr) / kg; + this.GreenBlueChromaScale = 2F * kb * (1F - kb) / kg; + this.TransferCharacteristics = transferCharacteristics; + this.ConstantLuminanceScales = constantLuminanceScales; + this.LumaBias = lumaBias; + this.LumaScale = lumaScale; + this.ChromaBias = chromaBias; + this.ChromaScale = chromaScale; + } + + /// + /// Gets the red luma coefficient. + /// + public float Kr { get; } + + /// + /// Gets the green luma coefficient. + /// + public float Kg { get; } + + /// + /// Gets the blue luma coefficient. + /// + public float Kb { get; } + + /// + /// Gets the red contribution from the red-difference component. + /// + public float RedChromaScale { get; } + + /// + /// Gets the blue contribution from the blue-difference component. + /// + public float BlueChromaScale { get; } + + /// + /// Gets the red-difference subtraction from green. + /// + public float GreenRedChromaScale { get; } + + /// + /// Gets the blue-difference subtraction from green. + /// + public float GreenBlueChromaScale { get; } + + /// + /// Gets the signaled transfer characteristics. + /// + public ObuTransferCharacteristics TransferCharacteristics { get; } + + /// + /// Gets the constant-luminance chroma scales. + /// + public Av1ConstantLuminanceScales ConstantLuminanceScales { get; } + + /// + /// Gets the encoded luma bias. + /// + public float LumaBias { get; } + + /// + /// Gets the encoded luma range. + /// + public float LumaScale { get; } + + /// + /// Gets the encoded chroma midpoint. + /// + public float ChromaBias { get; } + + /// + /// Gets the encoded chroma range. + /// + public float ChromaScale { get; } +} + +/// +/// Stores the H.273 chroma normalization constants for constant-luminance conversion. +/// +internal readonly struct Av1ConstantLuminanceScales +{ + /// + /// Initializes a new instance of the struct. + /// + /// The signaled transfer characteristics. + /// The red luma coefficient. + /// The blue luma coefficient. + public Av1ConstantLuminanceScales(ObuTransferCharacteristics transferCharacteristics, float kr, float kb) + { + this.NegativeBlue = Av1TransferFunctions.ToGamma(transferCharacteristics, 1F - kb); + this.PositiveBlue = 1F - Av1TransferFunctions.ToGamma(transferCharacteristics, kb); + this.NegativeRed = Av1TransferFunctions.ToGamma(transferCharacteristics, 1F - kr); + this.PositiveRed = 1F - Av1TransferFunctions.ToGamma(transferCharacteristics, kr); + } + + /// + /// Gets the negative blue-difference scale. + /// + public float NegativeBlue { get; } + + /// + /// Gets the positive blue-difference scale. + /// + public float PositiveBlue { get; } + + /// + /// Gets the negative red-difference scale. + /// + public float NegativeRed { get; } + + /// + /// Gets the positive red-difference scale. + /// + public float PositiveRed { get; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.CoefficientOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.CoefficientOperator.cs new file mode 100644 index 000000000..8d92ee7ad --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.CoefficientOperator.cs @@ -0,0 +1,156 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +internal abstract partial class Av1ColorConverterBase +{ + /// + /// Implements coefficient-based YCbCr conversion for scalar and SIMD lanes. + /// + internal readonly struct Av1CoefficientColorOperator : IAv1ColorOperator + { + /// + public static bool ChromaUsesLumaRange => false; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float y, ref float cb, ref float cr, in Av1ColorConversionParameters parameters) + { + // The resolved coefficients are frame invariants. Keeping the operator in this direct matrix form + // lets every SIMD overload map the same three equations to fused multiply-add instructions. + float r = y + (parameters.RedChromaScale * cr); + float g = y - (parameters.GreenRedChromaScale * cr) - (parameters.GreenBlueChromaScale * cb); + float b = y + (parameters.BlueChromaScale * cb); + + y = r; + cb = g; + cr = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 y, ref Vector128 cb, ref Vector128 cr, in Av1ColorConversionParameters parameters) + { + Vector128 r = Vector128.MultiplyAddEstimate(Vector128.Create(parameters.RedChromaScale), cr, y); + Vector128 g = Vector128.MultiplyAddEstimate( + Vector128.Create(-parameters.GreenBlueChromaScale), + cb, + Vector128.MultiplyAddEstimate(Vector128.Create(-parameters.GreenRedChromaScale), cr, y)); + Vector128 b = Vector128.MultiplyAddEstimate(Vector128.Create(parameters.BlueChromaScale), cb, y); + + y = r; + cb = g; + cr = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 y, ref Vector256 cb, ref Vector256 cr, in Av1ColorConversionParameters parameters) + { + Vector256 r = Vector256.MultiplyAddEstimate(Vector256.Create(parameters.RedChromaScale), cr, y); + Vector256 g = Vector256.MultiplyAddEstimate( + Vector256.Create(-parameters.GreenBlueChromaScale), + cb, + Vector256.MultiplyAddEstimate(Vector256.Create(-parameters.GreenRedChromaScale), cr, y)); + Vector256 b = Vector256.MultiplyAddEstimate(Vector256.Create(parameters.BlueChromaScale), cb, y); + + y = r; + cb = g; + cr = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 y, ref Vector512 cb, ref Vector512 cr, in Av1ColorConversionParameters parameters) + { + Vector512 r = Vector512.MultiplyAddEstimate(Vector512.Create(parameters.RedChromaScale), cr, y); + Vector512 g = Vector512.MultiplyAddEstimate( + Vector512.Create(-parameters.GreenBlueChromaScale), + cb, + Vector512.MultiplyAddEstimate(Vector512.Create(-parameters.GreenRedChromaScale), cr, y)); + Vector512 b = Vector512.MultiplyAddEstimate(Vector512.Create(parameters.BlueChromaScale), cb, y); + + y = r; + cb = g; + cr = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float r, + float g, + float b, + in Av1ColorConversionParameters parameters, + out float y, + out float cb, + out float cr) + { + // Luma is shared by both chroma equations, so calculate it once before projecting blue and red. + y = (parameters.Kr * r) + (parameters.Kg * g) + (parameters.Kb * b); + cb = (b - y) / parameters.BlueChromaScale; + cr = (r - y) / parameters.RedChromaScale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 r, + Vector128 g, + Vector128 b, + in Av1ColorConversionParameters parameters, + out Vector128 y, + out Vector128 cb, + out Vector128 cr) + { + y = Vector128.MultiplyAddEstimate( + Vector128.Create(parameters.Kr), + r, + Vector128.MultiplyAddEstimate(Vector128.Create(parameters.Kg), g, Vector128.Create(parameters.Kb) * b)); + cb = (b - y) / Vector128.Create(parameters.BlueChromaScale); + cr = (r - y) / Vector128.Create(parameters.RedChromaScale); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 r, + Vector256 g, + Vector256 b, + in Av1ColorConversionParameters parameters, + out Vector256 y, + out Vector256 cb, + out Vector256 cr) + { + y = Vector256.MultiplyAddEstimate( + Vector256.Create(parameters.Kr), + r, + Vector256.MultiplyAddEstimate(Vector256.Create(parameters.Kg), g, Vector256.Create(parameters.Kb) * b)); + cb = (b - y) / Vector256.Create(parameters.BlueChromaScale); + cr = (r - y) / Vector256.Create(parameters.RedChromaScale); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 r, + Vector512 g, + Vector512 b, + in Av1ColorConversionParameters parameters, + out Vector512 y, + out Vector512 cb, + out Vector512 cr) + { + y = Vector512.MultiplyAddEstimate( + Vector512.Create(parameters.Kr), + r, + Vector512.MultiplyAddEstimate(Vector512.Create(parameters.Kg), g, Vector512.Create(parameters.Kb) * b)); + cb = (b - y) / Vector512.Create(parameters.BlueChromaScale); + cr = (r - y) / Vector512.Create(parameters.RedChromaScale); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.ConstantLuminanceOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.ConstantLuminanceOperator.cs new file mode 100644 index 000000000..990113ead --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.ConstantLuminanceOperator.cs @@ -0,0 +1,249 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +internal abstract partial class Av1ColorConverterBase +{ + /// + /// Implements H.273 constant-luminance conversion for scalar and SIMD lanes. + /// + internal readonly struct Av1ConstantLuminanceColorOperator : IAv1ColorOperator + { + /// + public static bool ChromaUsesLumaRange => false; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float y, ref float cb, ref float cr, in Av1ColorConversionParameters parameters) + { + Av1ConstantLuminanceScales scales = parameters.ConstantLuminanceScales; + + // Constant-luminance chroma has different positive and negative divisors. Reconstruct nonlinear + // red and blue first, then solve for green in linear light using the signaled transfer curve. + float nonlinearBlue = y + (2F * (cb <= 0F ? scales.NegativeBlue : scales.PositiveBlue) * cb); + float nonlinearRed = y + (2F * (cr <= 0F ? scales.NegativeRed : scales.PositiveRed) * cr); + float linearY = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, y); + float linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearBlue); + float linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearRed); + float linearGreen = (linearY - (parameters.Kr * linearRed) - (parameters.Kb * linearBlue)) / parameters.Kg; + y = nonlinearRed; + cb = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + cr = nonlinearBlue; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 y, ref Vector128 cb, ref Vector128 cr, in Av1ColorConversionParameters parameters) + { + Av1ConstantLuminanceScales scales = parameters.ConstantLuminanceScales; + Vector128 blueScale = Vector128.ConditionalSelect( + Vector128.LessThanOrEqual(cb, Vector128.Zero), + Vector128.Create(scales.NegativeBlue), + Vector128.Create(scales.PositiveBlue)); + Vector128 redScale = Vector128.ConditionalSelect( + Vector128.LessThanOrEqual(cr, Vector128.Zero), + Vector128.Create(scales.NegativeRed), + Vector128.Create(scales.PositiveRed)); + Vector128 nonlinearBlue = Vector128.MultiplyAddEstimate(Vector128.Create(2F) * blueScale, cb, y); + Vector128 nonlinearRed = Vector128.MultiplyAddEstimate(Vector128.Create(2F) * redScale, cr, y); + Vector128 linearY = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, y); + Vector128 linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearBlue); + Vector128 linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearRed); + Vector128 linearGreen = ( + linearY - (Vector128.Create(parameters.Kr) * linearRed) - (Vector128.Create(parameters.Kb) * linearBlue)) + / Vector128.Create(parameters.Kg); + + y = nonlinearRed; + cb = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + cr = nonlinearBlue; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 y, ref Vector256 cb, ref Vector256 cr, in Av1ColorConversionParameters parameters) + { + Av1ConstantLuminanceScales scales = parameters.ConstantLuminanceScales; + Vector256 blueScale = Vector256.ConditionalSelect( + Vector256.LessThanOrEqual(cb, Vector256.Zero), + Vector256.Create(scales.NegativeBlue), + Vector256.Create(scales.PositiveBlue)); + Vector256 redScale = Vector256.ConditionalSelect( + Vector256.LessThanOrEqual(cr, Vector256.Zero), + Vector256.Create(scales.NegativeRed), + Vector256.Create(scales.PositiveRed)); + Vector256 nonlinearBlue = Vector256.MultiplyAddEstimate(Vector256.Create(2F) * blueScale, cb, y); + Vector256 nonlinearRed = Vector256.MultiplyAddEstimate(Vector256.Create(2F) * redScale, cr, y); + Vector256 linearY = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, y); + Vector256 linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearBlue); + Vector256 linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearRed); + Vector256 linearGreen = ( + linearY - (Vector256.Create(parameters.Kr) * linearRed) - (Vector256.Create(parameters.Kb) * linearBlue)) + / Vector256.Create(parameters.Kg); + + y = nonlinearRed; + cb = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + cr = nonlinearBlue; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 y, ref Vector512 cb, ref Vector512 cr, in Av1ColorConversionParameters parameters) + { + Av1ConstantLuminanceScales scales = parameters.ConstantLuminanceScales; + Vector512 blueScale = Vector512.ConditionalSelect( + Vector512.LessThanOrEqual(cb, Vector512.Zero), + Vector512.Create(scales.NegativeBlue), + Vector512.Create(scales.PositiveBlue)); + Vector512 redScale = Vector512.ConditionalSelect( + Vector512.LessThanOrEqual(cr, Vector512.Zero), + Vector512.Create(scales.NegativeRed), + Vector512.Create(scales.PositiveRed)); + Vector512 nonlinearBlue = Vector512.MultiplyAddEstimate(Vector512.Create(2F) * blueScale, cb, y); + Vector512 nonlinearRed = Vector512.MultiplyAddEstimate(Vector512.Create(2F) * redScale, cr, y); + Vector512 linearY = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, y); + Vector512 linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearBlue); + Vector512 linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearRed); + Vector512 linearGreen = ( + linearY - (Vector512.Create(parameters.Kr) * linearRed) - (Vector512.Create(parameters.Kb) * linearBlue)) + / Vector512.Create(parameters.Kg); + + y = nonlinearRed; + cb = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + cr = nonlinearBlue; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float r, + float g, + float b, + in Av1ColorConversionParameters parameters, + out float y, + out float cb, + out float cr) + { + // Luma is formed in linear light. The nonlinear red and blue differences then choose the + // sign-dependent denominators that define constant-luminance Cb and Cr. + float linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, r); + float linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, g); + float linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, b); + float linearY = (parameters.Kr * linearRed) + (parameters.Kg * linearGreen) + (parameters.Kb * linearBlue); + y = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearY); + float blueDifference = b - y; + float redDifference = r - y; + Av1ConstantLuminanceScales scales = parameters.ConstantLuminanceScales; + cb = blueDifference / (2F * (blueDifference <= 0F ? scales.NegativeBlue : scales.PositiveBlue)); + cr = redDifference / (2F * (redDifference <= 0F ? scales.NegativeRed : scales.PositiveRed)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 r, + Vector128 g, + Vector128 b, + in Av1ColorConversionParameters parameters, + out Vector128 y, + out Vector128 cb, + out Vector128 cr) + { + Vector128 linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, r); + Vector128 linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, g); + Vector128 linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, b); + Vector128 linearY = Vector128.MultiplyAddEstimate( + Vector128.Create(parameters.Kr), + linearRed, + Vector128.MultiplyAddEstimate(Vector128.Create(parameters.Kg), linearGreen, Vector128.Create(parameters.Kb) * linearBlue)); + y = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearY); + Vector128 blueDifference = b - y; + Vector128 redDifference = r - y; + Av1ConstantLuminanceScales scales = parameters.ConstantLuminanceScales; + Vector128 blueScale = Vector128.ConditionalSelect( + Vector128.LessThanOrEqual(blueDifference, Vector128.Zero), + Vector128.Create(scales.NegativeBlue), + Vector128.Create(scales.PositiveBlue)); + Vector128 redScale = Vector128.ConditionalSelect( + Vector128.LessThanOrEqual(redDifference, Vector128.Zero), + Vector128.Create(scales.NegativeRed), + Vector128.Create(scales.PositiveRed)); + + cb = blueDifference / (Vector128.Create(2F) * blueScale); + cr = redDifference / (Vector128.Create(2F) * redScale); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 r, + Vector256 g, + Vector256 b, + in Av1ColorConversionParameters parameters, + out Vector256 y, + out Vector256 cb, + out Vector256 cr) + { + Vector256 linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, r); + Vector256 linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, g); + Vector256 linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, b); + Vector256 linearY = Vector256.MultiplyAddEstimate( + Vector256.Create(parameters.Kr), + linearRed, + Vector256.MultiplyAddEstimate(Vector256.Create(parameters.Kg), linearGreen, Vector256.Create(parameters.Kb) * linearBlue)); + y = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearY); + Vector256 blueDifference = b - y; + Vector256 redDifference = r - y; + Av1ConstantLuminanceScales scales = parameters.ConstantLuminanceScales; + Vector256 blueScale = Vector256.ConditionalSelect( + Vector256.LessThanOrEqual(blueDifference, Vector256.Zero), + Vector256.Create(scales.NegativeBlue), + Vector256.Create(scales.PositiveBlue)); + Vector256 redScale = Vector256.ConditionalSelect( + Vector256.LessThanOrEqual(redDifference, Vector256.Zero), + Vector256.Create(scales.NegativeRed), + Vector256.Create(scales.PositiveRed)); + + cb = blueDifference / (Vector256.Create(2F) * blueScale); + cr = redDifference / (Vector256.Create(2F) * redScale); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 r, + Vector512 g, + Vector512 b, + in Av1ColorConversionParameters parameters, + out Vector512 y, + out Vector512 cb, + out Vector512 cr) + { + Vector512 linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, r); + Vector512 linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, g); + Vector512 linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, b); + Vector512 linearY = Vector512.MultiplyAddEstimate( + Vector512.Create(parameters.Kr), + linearRed, + Vector512.MultiplyAddEstimate(Vector512.Create(parameters.Kg), linearGreen, Vector512.Create(parameters.Kb) * linearBlue)); + y = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearY); + Vector512 blueDifference = b - y; + Vector512 redDifference = r - y; + Av1ConstantLuminanceScales scales = parameters.ConstantLuminanceScales; + Vector512 blueScale = Vector512.ConditionalSelect( + Vector512.LessThanOrEqual(blueDifference, Vector512.Zero), + Vector512.Create(scales.NegativeBlue), + Vector512.Create(scales.PositiveBlue)); + Vector512 redScale = Vector512.ConditionalSelect( + Vector512.LessThanOrEqual(redDifference, Vector512.Zero), + Vector512.Create(scales.NegativeRed), + Vector512.Create(scales.PositiveRed)); + + cb = blueDifference / (Vector512.Create(2F) * blueScale); + cr = redDifference / (Vector512.Create(2F) * redScale); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.ICtCpOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.ICtCpOperator.cs new file mode 100644 index 000000000..146623057 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.ICtCpOperator.cs @@ -0,0 +1,526 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +internal abstract partial class Av1ColorConverterBase +{ + /// + /// Implements BT.2100 ICtCp conversion for scalar and SIMD lanes. + /// + internal readonly struct Av1ICtCpColorOperator : IAv1ColorOperator + { + /// + /// The PQ Ct contribution to nonlinear L. + /// + public const float PqCtToL = 0.008609037037932756F; + + /// + /// The PQ Cp contribution to nonlinear L. + /// + public const float PqCpToL = 0.11102962500302596F; + + /// + /// The PQ Ct contribution to nonlinear S. + /// + public const float PqCtToS = 0.5600313357106791F; + + /// + /// The PQ Cp contribution to nonlinear S. + /// + public const float PqCpToS = -0.32062717498731885F; + + /// + /// The HLG Ct contribution to nonlinear L. + /// + public const float HlgCtToL = 0.015718580108730413F; + + /// + /// The HLG Cp contribution to nonlinear L. + /// + public const float HlgCpToL = 0.2095810681164055F; + + /// + /// The HLG Ct contribution to nonlinear S. + /// + public const float HlgCtToS = 1.0212710798422342F; + + /// + /// The HLG Cp contribution to nonlinear S. + /// + public const float HlgCpToS = -0.6052744909924315F; + + /// + /// The linear L contribution to red. + /// + public const float LToRed = 3.4366066943330784F; + + /// + /// The linear M contribution to red. + /// + public const float MToRed = -2.50645211865627F; + + /// + /// The linear S contribution to red. + /// + public const float SToRed = 0.06984542432319148F; + + /// + /// The linear L contribution to green. + /// + public const float LToGreen = -0.7913295555989287F; + + /// + /// The linear M contribution to green. + /// + public const float MToGreen = 1.9836004517922907F; + + /// + /// The linear S contribution to green. + /// + public const float SToGreen = -0.192270896193362F; + + /// + /// The linear L contribution to blue. + /// + public const float LToBlue = -0.025949899690592672F; + + /// + /// The linear M contribution to blue. + /// + public const float MToBlue = -0.09891371471172644F; + + /// + /// The linear S contribution to blue. + /// + public const float SToBlue = 1.1248636144023192F; + + /// + /// The linear red contribution to L. + /// + public const float RedToL = 1688F / 4096F; + + /// + /// The linear green contribution to L. + /// + public const float GreenToL = 2146F / 4096F; + + /// + /// The linear blue contribution to L. + /// + public const float BlueToL = 262F / 4096F; + + /// + /// The linear red contribution to M. + /// + public const float RedToM = 683F / 4096F; + + /// + /// The linear green contribution to M. + /// + public const float GreenToM = 2951F / 4096F; + + /// + /// The linear blue contribution to M. + /// + public const float BlueToM = 462F / 4096F; + + /// + /// The linear red contribution to S. + /// + public const float RedToS = 99F / 4096F; + + /// + /// The linear green contribution to S. + /// + public const float GreenToS = 309F / 4096F; + + /// + /// The linear blue contribution to S. + /// + public const float BlueToS = 3688F / 4096F; + + /// + /// The PQ nonlinear L contribution to Ct. + /// + public const float PqLToCt = 6610F / 4096F; + + /// + /// The PQ nonlinear M contribution to Ct. + /// + public const float PqMToCt = -13613F / 4096F; + + /// + /// The PQ nonlinear S contribution to Ct. + /// + public const float PqSToCt = 7003F / 4096F; + + /// + /// The PQ nonlinear L contribution to Cp. + /// + public const float PqLToCp = 17933F / 4096F; + + /// + /// The PQ nonlinear M contribution to Cp. + /// + public const float PqMToCp = -17390F / 4096F; + + /// + /// The PQ nonlinear S contribution to Cp. + /// + public const float PqSToCp = -543F / 4096F; + + /// + /// The HLG nonlinear L contribution to Ct. + /// + public const float HlgLToCt = 3625F / 4096F; + + /// + /// The HLG nonlinear M contribution to Ct. + /// + public const float HlgMToCt = -7465F / 4096F; + + /// + /// The HLG nonlinear S contribution to Ct. + /// + public const float HlgSToCt = 3840F / 4096F; + + /// + /// The HLG nonlinear L contribution to Cp. + /// + public const float HlgLToCp = 9500F / 4096F; + + /// + /// The HLG nonlinear M contribution to Cp. + /// + public const float HlgMToCp = -9212F / 4096F; + + /// + /// The HLG nonlinear S contribution to Cp. + /// + public const float HlgSToCp = -288F / 4096F; + + /// + public static bool ChromaUsesLumaRange => false; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float intensity, ref float ct, ref float cp, in Av1ColorConversionParameters parameters) + { + bool isHlg = parameters.TransferCharacteristics == ObuTransferCharacteristics.Hlg; + float ctToL = isHlg ? HlgCtToL : PqCtToL; + float cpToL = isHlg ? HlgCpToL : PqCpToL; + + // The inverse ICtCp matrix first reconstructs nonlinear LMS. The transfer curve is then + // removed before the fixed LMS-to-RGB matrix and reapplied to the three output primaries. + float nonlinearL = intensity + (ctToL * ct) + (cpToL * cp); + float nonlinearM = intensity - (ctToL * ct) - (cpToL * cp); + float nonlinearS = intensity + ((isHlg ? HlgCtToS : PqCtToS) * ct) + ((isHlg ? HlgCpToS : PqCpToS) * cp); + float linearL = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL); + float linearM = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM); + float linearS = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS); + float linearRed = (LToRed * linearL) + (MToRed * linearM) + (SToRed * linearS); + float linearGreen = (LToGreen * linearL) + (MToGreen * linearM) + (SToGreen * linearS); + float linearBlue = (LToBlue * linearL) + (MToBlue * linearM) + (SToBlue * linearS); + + intensity = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed); + ct = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + cp = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector128 intensity, + ref Vector128 ct, + ref Vector128 cp, + in Av1ColorConversionParameters parameters) + { + bool isHlg = parameters.TransferCharacteristics == ObuTransferCharacteristics.Hlg; + Vector128 ctContribution = Vector128.Create(isHlg ? HlgCtToL : PqCtToL) * ct; + Vector128 cpContribution = Vector128.Create(isHlg ? HlgCpToL : PqCpToL) * cp; + Vector128 nonlinearL = intensity + ctContribution + cpContribution; + Vector128 nonlinearM = intensity - ctContribution - cpContribution; + Vector128 nonlinearS = Vector128.MultiplyAddEstimate( + Vector128.Create(isHlg ? HlgCpToS : PqCpToS), + cp, + Vector128.MultiplyAddEstimate(Vector128.Create(isHlg ? HlgCtToS : PqCtToS), ct, intensity)); + Vector128 linearL = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL); + Vector128 linearM = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM); + Vector128 linearS = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS); + Vector128 linearRed = Vector128.MultiplyAddEstimate( + Vector128.Create(SToRed), + linearS, + Vector128.MultiplyAddEstimate(Vector128.Create(MToRed), linearM, Vector128.Create(LToRed) * linearL)); + Vector128 linearGreen = Vector128.MultiplyAddEstimate( + Vector128.Create(SToGreen), + linearS, + Vector128.MultiplyAddEstimate(Vector128.Create(MToGreen), linearM, Vector128.Create(LToGreen) * linearL)); + Vector128 linearBlue = Vector128.MultiplyAddEstimate( + Vector128.Create(SToBlue), + linearS, + Vector128.MultiplyAddEstimate(Vector128.Create(MToBlue), linearM, Vector128.Create(LToBlue) * linearL)); + + intensity = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed); + ct = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + cp = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector256 intensity, + ref Vector256 ct, + ref Vector256 cp, + in Av1ColorConversionParameters parameters) + { + bool isHlg = parameters.TransferCharacteristics == ObuTransferCharacteristics.Hlg; + Vector256 ctContribution = Vector256.Create(isHlg ? HlgCtToL : PqCtToL) * ct; + Vector256 cpContribution = Vector256.Create(isHlg ? HlgCpToL : PqCpToL) * cp; + Vector256 nonlinearL = intensity + ctContribution + cpContribution; + Vector256 nonlinearM = intensity - ctContribution - cpContribution; + Vector256 nonlinearS = Vector256.MultiplyAddEstimate( + Vector256.Create(isHlg ? HlgCpToS : PqCpToS), + cp, + Vector256.MultiplyAddEstimate(Vector256.Create(isHlg ? HlgCtToS : PqCtToS), ct, intensity)); + Vector256 linearL = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL); + Vector256 linearM = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM); + Vector256 linearS = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS); + Vector256 linearRed = Vector256.MultiplyAddEstimate( + Vector256.Create(SToRed), + linearS, + Vector256.MultiplyAddEstimate(Vector256.Create(MToRed), linearM, Vector256.Create(LToRed) * linearL)); + Vector256 linearGreen = Vector256.MultiplyAddEstimate( + Vector256.Create(SToGreen), + linearS, + Vector256.MultiplyAddEstimate(Vector256.Create(MToGreen), linearM, Vector256.Create(LToGreen) * linearL)); + Vector256 linearBlue = Vector256.MultiplyAddEstimate( + Vector256.Create(SToBlue), + linearS, + Vector256.MultiplyAddEstimate(Vector256.Create(MToBlue), linearM, Vector256.Create(LToBlue) * linearL)); + + intensity = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed); + ct = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + cp = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector512 intensity, + ref Vector512 ct, + ref Vector512 cp, + in Av1ColorConversionParameters parameters) + { + bool isHlg = parameters.TransferCharacteristics == ObuTransferCharacteristics.Hlg; + Vector512 ctContribution = Vector512.Create(isHlg ? HlgCtToL : PqCtToL) * ct; + Vector512 cpContribution = Vector512.Create(isHlg ? HlgCpToL : PqCpToL) * cp; + Vector512 nonlinearL = intensity + ctContribution + cpContribution; + Vector512 nonlinearM = intensity - ctContribution - cpContribution; + Vector512 nonlinearS = Vector512.MultiplyAddEstimate( + Vector512.Create(isHlg ? HlgCpToS : PqCpToS), + cp, + Vector512.MultiplyAddEstimate(Vector512.Create(isHlg ? HlgCtToS : PqCtToS), ct, intensity)); + Vector512 linearL = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL); + Vector512 linearM = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM); + Vector512 linearS = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS); + Vector512 linearRed = Vector512.MultiplyAddEstimate( + Vector512.Create(SToRed), + linearS, + Vector512.MultiplyAddEstimate(Vector512.Create(MToRed), linearM, Vector512.Create(LToRed) * linearL)); + Vector512 linearGreen = Vector512.MultiplyAddEstimate( + Vector512.Create(SToGreen), + linearS, + Vector512.MultiplyAddEstimate(Vector512.Create(MToGreen), linearM, Vector512.Create(LToGreen) * linearL)); + Vector512 linearBlue = Vector512.MultiplyAddEstimate( + Vector512.Create(SToBlue), + linearS, + Vector512.MultiplyAddEstimate(Vector512.Create(MToBlue), linearM, Vector512.Create(LToBlue) * linearL)); + + intensity = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed); + ct = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + cp = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float red, + float green, + float blue, + in Av1ColorConversionParameters parameters, + out float intensity, + out float ct, + out float cp) + { + // ICtCp is defined in nonlinear LMS. Convert RGB to linear light, apply the LMS matrix, then + // apply the signaled PQ or HLG transfer curve before deriving intensity and the chroma axes. + float linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, red); + float linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, green); + float linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, blue); + float nonlinearL = Av1TransferFunctions.ToGamma( + parameters.TransferCharacteristics, + (RedToL * linearRed) + (GreenToL * linearGreen) + (BlueToL * linearBlue)); + float nonlinearM = Av1TransferFunctions.ToGamma( + parameters.TransferCharacteristics, + (RedToM * linearRed) + (GreenToM * linearGreen) + (BlueToM * linearBlue)); + float nonlinearS = Av1TransferFunctions.ToGamma( + parameters.TransferCharacteristics, + (RedToS * linearRed) + (GreenToS * linearGreen) + (BlueToS * linearBlue)); + intensity = 0.5F * (nonlinearL + nonlinearM); + bool isHlg = parameters.TransferCharacteristics == ObuTransferCharacteristics.Hlg; + ct = isHlg + ? (HlgLToCt * nonlinearL) + (HlgMToCt * nonlinearM) + (HlgSToCt * nonlinearS) + : (PqLToCt * nonlinearL) + (PqMToCt * nonlinearM) + (PqSToCt * nonlinearS); + + cp = isHlg + ? (HlgLToCp * nonlinearL) + (HlgMToCp * nonlinearM) + (HlgSToCp * nonlinearS) + : (PqLToCp * nonlinearL) + (PqMToCp * nonlinearM) + (PqSToCp * nonlinearS); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 red, + Vector128 green, + Vector128 blue, + in Av1ColorConversionParameters parameters, + out Vector128 intensity, + out Vector128 ct, + out Vector128 cp) + { + Vector128 linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, red); + Vector128 linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, green); + Vector128 linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, blue); + Vector128 linearL = Vector128.MultiplyAddEstimate( + Vector128.Create(RedToL), + linearRed, + Vector128.MultiplyAddEstimate(Vector128.Create(GreenToL), linearGreen, Vector128.Create(BlueToL) * linearBlue)); + Vector128 linearM = Vector128.MultiplyAddEstimate( + Vector128.Create(RedToM), + linearRed, + Vector128.MultiplyAddEstimate(Vector128.Create(GreenToM), linearGreen, Vector128.Create(BlueToM) * linearBlue)); + Vector128 linearS = Vector128.MultiplyAddEstimate( + Vector128.Create(RedToS), + linearRed, + Vector128.MultiplyAddEstimate(Vector128.Create(GreenToS), linearGreen, Vector128.Create(BlueToS) * linearBlue)); + Vector128 nonlinearL = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearL); + Vector128 nonlinearM = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearM); + Vector128 nonlinearS = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearS); + intensity = Vector128.Create(0.5F) * (nonlinearL + nonlinearM); + bool isHlg = parameters.TransferCharacteristics == ObuTransferCharacteristics.Hlg; + float lToCt = isHlg ? HlgLToCt : PqLToCt; + float mToCt = isHlg ? HlgMToCt : PqMToCt; + float sToCt = isHlg ? HlgSToCt : PqSToCt; + float lToCp = isHlg ? HlgLToCp : PqLToCp; + float mToCp = isHlg ? HlgMToCp : PqMToCp; + float sToCp = isHlg ? HlgSToCp : PqSToCp; + ct = Vector128.MultiplyAddEstimate( + Vector128.Create(lToCt), + nonlinearL, + Vector128.MultiplyAddEstimate(Vector128.Create(mToCt), nonlinearM, Vector128.Create(sToCt) * nonlinearS)); + cp = Vector128.MultiplyAddEstimate( + Vector128.Create(lToCp), + nonlinearL, + Vector128.MultiplyAddEstimate(Vector128.Create(mToCp), nonlinearM, Vector128.Create(sToCp) * nonlinearS)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 red, + Vector256 green, + Vector256 blue, + in Av1ColorConversionParameters parameters, + out Vector256 intensity, + out Vector256 ct, + out Vector256 cp) + { + Vector256 linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, red); + Vector256 linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, green); + Vector256 linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, blue); + Vector256 linearL = Vector256.MultiplyAddEstimate( + Vector256.Create(RedToL), + linearRed, + Vector256.MultiplyAddEstimate(Vector256.Create(GreenToL), linearGreen, Vector256.Create(BlueToL) * linearBlue)); + Vector256 linearM = Vector256.MultiplyAddEstimate( + Vector256.Create(RedToM), + linearRed, + Vector256.MultiplyAddEstimate(Vector256.Create(GreenToM), linearGreen, Vector256.Create(BlueToM) * linearBlue)); + Vector256 linearS = Vector256.MultiplyAddEstimate( + Vector256.Create(RedToS), + linearRed, + Vector256.MultiplyAddEstimate(Vector256.Create(GreenToS), linearGreen, Vector256.Create(BlueToS) * linearBlue)); + Vector256 nonlinearL = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearL); + Vector256 nonlinearM = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearM); + Vector256 nonlinearS = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearS); + intensity = Vector256.Create(0.5F) * (nonlinearL + nonlinearM); + bool isHlg = parameters.TransferCharacteristics == ObuTransferCharacteristics.Hlg; + float lToCt = isHlg ? HlgLToCt : PqLToCt; + float mToCt = isHlg ? HlgMToCt : PqMToCt; + float sToCt = isHlg ? HlgSToCt : PqSToCt; + float lToCp = isHlg ? HlgLToCp : PqLToCp; + float mToCp = isHlg ? HlgMToCp : PqMToCp; + float sToCp = isHlg ? HlgSToCp : PqSToCp; + ct = Vector256.MultiplyAddEstimate( + Vector256.Create(lToCt), + nonlinearL, + Vector256.MultiplyAddEstimate(Vector256.Create(mToCt), nonlinearM, Vector256.Create(sToCt) * nonlinearS)); + cp = Vector256.MultiplyAddEstimate( + Vector256.Create(lToCp), + nonlinearL, + Vector256.MultiplyAddEstimate(Vector256.Create(mToCp), nonlinearM, Vector256.Create(sToCp) * nonlinearS)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 red, + Vector512 green, + Vector512 blue, + in Av1ColorConversionParameters parameters, + out Vector512 intensity, + out Vector512 ct, + out Vector512 cp) + { + Vector512 linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, red); + Vector512 linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, green); + Vector512 linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, blue); + Vector512 linearL = Vector512.MultiplyAddEstimate( + Vector512.Create(RedToL), + linearRed, + Vector512.MultiplyAddEstimate(Vector512.Create(GreenToL), linearGreen, Vector512.Create(BlueToL) * linearBlue)); + Vector512 linearM = Vector512.MultiplyAddEstimate( + Vector512.Create(RedToM), + linearRed, + Vector512.MultiplyAddEstimate(Vector512.Create(GreenToM), linearGreen, Vector512.Create(BlueToM) * linearBlue)); + Vector512 linearS = Vector512.MultiplyAddEstimate( + Vector512.Create(RedToS), + linearRed, + Vector512.MultiplyAddEstimate(Vector512.Create(GreenToS), linearGreen, Vector512.Create(BlueToS) * linearBlue)); + Vector512 nonlinearL = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearL); + Vector512 nonlinearM = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearM); + Vector512 nonlinearS = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearS); + intensity = Vector512.Create(0.5F) * (nonlinearL + nonlinearM); + bool isHlg = parameters.TransferCharacteristics == ObuTransferCharacteristics.Hlg; + float lToCt = isHlg ? HlgLToCt : PqLToCt; + float mToCt = isHlg ? HlgMToCt : PqMToCt; + float sToCt = isHlg ? HlgSToCt : PqSToCt; + float lToCp = isHlg ? HlgLToCp : PqLToCp; + float mToCp = isHlg ? HlgMToCp : PqMToCp; + float sToCp = isHlg ? HlgSToCp : PqSToCp; + ct = Vector512.MultiplyAddEstimate( + Vector512.Create(lToCt), + nonlinearL, + Vector512.MultiplyAddEstimate(Vector512.Create(mToCt), nonlinearM, Vector512.Create(sToCt) * nonlinearS)); + cp = Vector512.MultiplyAddEstimate( + Vector512.Create(lToCp), + nonlinearL, + Vector512.MultiplyAddEstimate(Vector512.Create(mToCp), nonlinearM, Vector512.Create(sToCp) * nonlinearS)); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.IdentityOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.IdentityOperator.cs new file mode 100644 index 000000000..f3bf16cc0 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.IdentityOperator.cs @@ -0,0 +1,138 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +internal abstract partial class Av1ColorConverterBase +{ + /// + /// Implements direct G, B, and R plane mapping for scalar and SIMD lanes. + /// + internal readonly struct Av1IdentityColorOperator : IAv1ColorOperator + { + /// + public static bool ChromaUsesLumaRange => true; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float green, ref float blue, ref float red, in Av1ColorConversionParameters parameters) + { + // Identity-matrix AV1 stores the planes in G, B, R order. Rotate the three references in place + // so the shared traversal always leaves component0/component1/component2 as R, G, B. + float g = green; + green = red; + red = blue; + blue = g; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector128 green, + ref Vector128 blue, + ref Vector128 red, + in Av1ColorConversionParameters parameters) + { + Vector128 g = green; + green = red; + red = blue; + blue = g; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector256 green, + ref Vector256 blue, + ref Vector256 red, + in Av1ColorConversionParameters parameters) + { + Vector256 g = green; + green = red; + red = blue; + blue = g; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector512 green, + ref Vector512 blue, + ref Vector512 red, + in Av1ColorConversionParameters parameters) + { + Vector512 g = green; + green = red; + red = blue; + blue = g; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float red, + float green, + float blue, + in Av1ColorConversionParameters parameters, + out float component0, + out float component1, + out float component2) + { + // Identity-matrix AV1 stores RGB input as G, B, R without matrix arithmetic. + component0 = green; + component1 = blue; + component2 = red; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 red, + Vector128 green, + Vector128 blue, + in Av1ColorConversionParameters parameters, + out Vector128 component0, + out Vector128 component1, + out Vector128 component2) + { + component0 = green; + component1 = blue; + component2 = red; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 red, + Vector256 green, + Vector256 blue, + in Av1ColorConversionParameters parameters, + out Vector256 component0, + out Vector256 component1, + out Vector256 component2) + { + component0 = green; + component1 = blue; + component2 = red; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 red, + Vector512 green, + Vector512 blue, + in Av1ColorConversionParameters parameters, + out Vector512 component0, + out Vector512 component1, + out Vector512 component2) + { + component0 = green; + component1 = blue; + component2 = red; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.Operator.cs new file mode 100644 index 000000000..5b29b067d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.Operator.cs @@ -0,0 +1,427 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Provides the static operator contract and SIMD traversal used by AV1 color converters. +/// +internal abstract partial class Av1ColorConverterBase +{ + /// + /// Defines color-model arithmetic for scalar and SIMD lanes in both conversion directions. + /// + internal interface IAv1ColorOperator + { + /// + /// Gets a value indicating whether chroma uses the luma range rather than the centered chroma range. + /// + public static abstract bool ChromaUsesLumaRange { get; } + + /// + /// Converts one normalized AV1 sample to RGB. + /// + /// The first encoded component, replaced by red. + /// The second encoded component, replaced by green. + /// The third encoded component, replaced by blue. + /// The resolved H.273 conversion parameters. + public static abstract void ConvertToRgb( + ref float component0, + ref float component1, + ref float component2, + in Av1ColorConversionParameters parameters); + + /// + /// Converts four normalized AV1 samples to RGB. + /// + /// The first encoded component lanes, replaced by red. + /// The second encoded component lanes, replaced by green. + /// The third encoded component lanes, replaced by blue. + /// The resolved H.273 conversion parameters. + public static abstract void ConvertToRgb( + ref Vector128 component0, + ref Vector128 component1, + ref Vector128 component2, + in Av1ColorConversionParameters parameters); + + /// + /// Converts eight normalized AV1 samples to RGB. + /// + /// The first encoded component lanes, replaced by red. + /// The second encoded component lanes, replaced by green. + /// The third encoded component lanes, replaced by blue. + /// The resolved H.273 conversion parameters. + public static abstract void ConvertToRgb( + ref Vector256 component0, + ref Vector256 component1, + ref Vector256 component2, + in Av1ColorConversionParameters parameters); + + /// + /// Converts sixteen normalized AV1 samples to RGB. + /// + /// The first encoded component lanes, replaced by red. + /// The second encoded component lanes, replaced by green. + /// The third encoded component lanes, replaced by blue. + /// The resolved H.273 conversion parameters. + public static abstract void ConvertToRgb( + ref Vector512 component0, + ref Vector512 component1, + ref Vector512 component2, + in Av1ColorConversionParameters parameters); + + /// + /// Converts one normalized RGB sample to AV1 components. + /// + /// The normalized red component. + /// The normalized green component. + /// The normalized blue component. + /// The resolved H.273 conversion parameters. + /// The first converted component. + /// The second converted component. + /// The third converted component. + public static abstract void ConvertFromRgb( + float red, + float green, + float blue, + in Av1ColorConversionParameters parameters, + out float component0, + out float component1, + out float component2); + + /// + /// Converts four normalized RGB samples to AV1 components. + /// + /// The normalized red lanes. + /// The normalized green lanes. + /// The normalized blue lanes. + /// The resolved H.273 conversion parameters. + /// The first converted component lanes. + /// The second converted component lanes. + /// The third converted component lanes. + public static abstract void ConvertFromRgb( + Vector128 red, + Vector128 green, + Vector128 blue, + in Av1ColorConversionParameters parameters, + out Vector128 component0, + out Vector128 component1, + out Vector128 component2); + + /// + /// Converts eight normalized RGB samples to AV1 components. + /// + /// The normalized red lanes. + /// The normalized green lanes. + /// The normalized blue lanes. + /// The resolved H.273 conversion parameters. + /// The first converted component lanes. + /// The second converted component lanes. + /// The third converted component lanes. + public static abstract void ConvertFromRgb( + Vector256 red, + Vector256 green, + Vector256 blue, + in Av1ColorConversionParameters parameters, + out Vector256 component0, + out Vector256 component1, + out Vector256 component2); + + /// + /// Converts sixteen normalized RGB samples to AV1 components. + /// + /// The normalized red lanes. + /// The normalized green lanes. + /// The normalized blue lanes. + /// The resolved H.273 conversion parameters. + /// The first converted component lanes. + /// The second converted component lanes. + /// The third converted component lanes. + public static abstract void ConvertFromRgb( + Vector512 red, + Vector512 green, + Vector512 blue, + in Av1ColorConversionParameters parameters, + out Vector512 component0, + out Vector512 component1, + out Vector512 component2); + } + + /// + /// Converts an AV1 color model using one operator-driven traversal for all SIMD widths. + /// + /// The color-model-specific arithmetic. + internal sealed class Av1ColorConverter : Av1ColorConverterBase + where TOperator : struct, IAv1ColorOperator + { + /// + /// Initializes a new instance of the class. + /// + /// The resolved H.273 conversion parameters. + /// Whether the frame contains only luma samples. + public Av1ColorConverter(in Av1ColorConversionParameters parameters, bool isMonochrome) + : base(in parameters, isMonochrome) + { + } + + /// + public override float ChromaScale => TOperator.ChromaUsesLumaRange ? this.Parameters.LumaScale : this.Parameters.ChromaScale; + + /// + public override float ChromaBias => TOperator.ChromaUsesLumaRange ? this.Parameters.LumaBias : this.Parameters.ChromaBias; + + /// + public override void ConvertToRgbInPlace(Span component0, Span component1, Span component2) + { + Av1ColorConversionParameters parameters = this.Parameters; + + // Row reconstruction owns equally sized planar buffers. As in JPEG, first-element byrefs let each + // SIMD width share one offset while the closed operator type keeps color-model dispatch out of the loop. + ref float component0Base = ref MemoryMarshal.GetReference(component0); + ref float component1Base = ref MemoryMarshal.GetReference(component1); + ref float component2Base = ref MemoryMarshal.GetReference(component2); + int length = component0.Length; + int i = 0; + + if (this.IsMonochrome) + { + // Monochrome has no operator arithmetic: expanding the luma range once and copying each SIMD + // vector to all three planes is cheaper than routing it through a three-component operator. + if (Vector512.IsHardwareAccelerated && i <= length - Vector512.Count) + { + Vector512 bias = Vector512.Create(parameters.LumaBias); + Vector512 inverseScale = Vector512.Create(1F / parameters.LumaScale); + int oneVectorFromEnd = length - Vector512.Count; + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + Vector512 value = (Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) - bias) * inverseScale; + Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) = value; + Unsafe.As>(ref Unsafe.Add(ref component1Base, i)) = value; + Unsafe.As>(ref Unsafe.Add(ref component2Base, i)) = value; + } + } + + if (Vector256.IsHardwareAccelerated && i <= length - Vector256.Count) + { + Vector256 bias = Vector256.Create(parameters.LumaBias); + Vector256 inverseScale = Vector256.Create(1F / parameters.LumaScale); + int oneVectorFromEnd = length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 value = (Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) - bias) * inverseScale; + Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) = value; + Unsafe.As>(ref Unsafe.Add(ref component1Base, i)) = value; + Unsafe.As>(ref Unsafe.Add(ref component2Base, i)) = value; + } + } + + if (Vector128.IsHardwareAccelerated && i <= length - Vector128.Count) + { + Vector128 bias = Vector128.Create(parameters.LumaBias); + Vector128 inverseScale = Vector128.Create(1F / parameters.LumaScale); + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + Vector128 value = (Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) - bias) * inverseScale; + Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) = value; + Unsafe.As>(ref Unsafe.Add(ref component1Base, i)) = value; + Unsafe.As>(ref Unsafe.Add(ref component2Base, i)) = value; + } + } + + for (; i < length; i++) + { + float value = (Unsafe.Add(ref component0Base, i) - parameters.LumaBias) / parameters.LumaScale; + Unsafe.Add(ref component0Base, i) = value; + Unsafe.Add(ref component1Base, i) = value; + Unsafe.Add(ref component2Base, i) = value; + } + + return; + } + + float chromaBias = this.ChromaBias; + float chromaScale = this.ChromaScale; + + // Descending widths preserve vector execution for the remainder left by a wider register. The range + // expansion is folded into each load so operators receive normalized H.273 components directly. + if (Vector512.IsHardwareAccelerated && i <= length - Vector512.Count) + { + Vector512 lumaBias = Vector512.Create(parameters.LumaBias); + Vector512 inverseLumaScale = Vector512.Create(1F / parameters.LumaScale); + Vector512 chromaBiasVector = Vector512.Create(chromaBias); + Vector512 inverseChromaScale = Vector512.Create(1F / chromaScale); + int oneVectorFromEnd = length - Vector512.Count; + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + ref Vector512 c0 = ref Unsafe.As>(ref Unsafe.Add(ref component0Base, i)); + ref Vector512 c1 = ref Unsafe.As>(ref Unsafe.Add(ref component1Base, i)); + ref Vector512 c2 = ref Unsafe.As>(ref Unsafe.Add(ref component2Base, i)); + c0 = (c0 - lumaBias) * inverseLumaScale; + c1 = (c1 - chromaBiasVector) * inverseChromaScale; + c2 = (c2 - chromaBiasVector) * inverseChromaScale; + + TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters); + } + } + + if (Vector256.IsHardwareAccelerated && i <= length - Vector256.Count) + { + Vector256 lumaBias = Vector256.Create(parameters.LumaBias); + Vector256 inverseLumaScale = Vector256.Create(1F / parameters.LumaScale); + Vector256 chromaBiasVector = Vector256.Create(chromaBias); + Vector256 inverseChromaScale = Vector256.Create(1F / chromaScale); + int oneVectorFromEnd = length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + ref Vector256 c0 = ref Unsafe.As>(ref Unsafe.Add(ref component0Base, i)); + ref Vector256 c1 = ref Unsafe.As>(ref Unsafe.Add(ref component1Base, i)); + ref Vector256 c2 = ref Unsafe.As>(ref Unsafe.Add(ref component2Base, i)); + c0 = (c0 - lumaBias) * inverseLumaScale; + c1 = (c1 - chromaBiasVector) * inverseChromaScale; + c2 = (c2 - chromaBiasVector) * inverseChromaScale; + + TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters); + } + } + + if (Vector128.IsHardwareAccelerated && i <= length - Vector128.Count) + { + Vector128 lumaBias = Vector128.Create(parameters.LumaBias); + Vector128 inverseLumaScale = Vector128.Create(1F / parameters.LumaScale); + Vector128 chromaBiasVector = Vector128.Create(chromaBias); + Vector128 inverseChromaScale = Vector128.Create(1F / chromaScale); + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + ref Vector128 c0 = ref Unsafe.As>(ref Unsafe.Add(ref component0Base, i)); + ref Vector128 c1 = ref Unsafe.As>(ref Unsafe.Add(ref component1Base, i)); + ref Vector128 c2 = ref Unsafe.As>(ref Unsafe.Add(ref component2Base, i)); + c0 = (c0 - lumaBias) * inverseLumaScale; + c1 = (c1 - chromaBiasVector) * inverseChromaScale; + c2 = (c2 - chromaBiasVector) * inverseChromaScale; + + TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters); + } + } + + // Scalar conversion is reserved for the zero-to-three samples left after the SIMD cascade. + for (; i < length; i++) + { + float c0 = (Unsafe.Add(ref component0Base, i) - parameters.LumaBias) / parameters.LumaScale; + float c1 = (Unsafe.Add(ref component1Base, i) - chromaBias) / chromaScale; + float c2 = (Unsafe.Add(ref component2Base, i) - chromaBias) / chromaScale; + TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters); + Unsafe.Add(ref component0Base, i) = c0; + Unsafe.Add(ref component1Base, i) = c1; + Unsafe.Add(ref component2Base, i) = c2; + } + } + + /// + public override void ConvertFromRgbInPlace( + Span component0, + Span component1, + Span component2, + float maximumValue) + { + Av1ColorConversionParameters parameters = this.Parameters; + + // The unpacker supplies three planar RGB rows. These same buffers become the destination component + // rows after each operator call, so encoding retains JPEG's planar contract without another allocation. + ref float component0Base = ref MemoryMarshal.GetReference(component0); + ref float component1Base = ref MemoryMarshal.GetReference(component1); + ref float component2Base = ref MemoryMarshal.GetReference(component2); + int length = component0.Length; + int i = 0; + + // RGB normalization is part of the vector load, and each operator returns planar components through + // out parameters. This is the same input/output shape used by JPEG's encoder-side color operators. + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector512.Count; + + if (i <= oneVectorFromEnd) + { + Vector512 inverseMaximum = Vector512.Create(1F / maximumValue); + + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + Vector512 red = Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) * inverseMaximum; + Vector512 green = Unsafe.As>(ref Unsafe.Add(ref component1Base, i)) * inverseMaximum; + Vector512 blue = Unsafe.As>(ref Unsafe.Add(ref component2Base, i)) * inverseMaximum; + ref Vector512 c0 = ref Unsafe.As>(ref Unsafe.Add(ref component0Base, i)); + ref Vector512 c1 = ref Unsafe.As>(ref Unsafe.Add(ref component1Base, i)); + ref Vector512 c2 = ref Unsafe.As>(ref Unsafe.Add(ref component2Base, i)); + + TOperator.ConvertFromRgb(red, green, blue, in parameters, out c0, out c1, out c2); + } + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector256.Count; + + if (i <= oneVectorFromEnd) + { + Vector256 inverseMaximum = Vector256.Create(1F / maximumValue); + + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 red = Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) * inverseMaximum; + Vector256 green = Unsafe.As>(ref Unsafe.Add(ref component1Base, i)) * inverseMaximum; + Vector256 blue = Unsafe.As>(ref Unsafe.Add(ref component2Base, i)) * inverseMaximum; + ref Vector256 c0 = ref Unsafe.As>(ref Unsafe.Add(ref component0Base, i)); + ref Vector256 c1 = ref Unsafe.As>(ref Unsafe.Add(ref component1Base, i)); + ref Vector256 c2 = ref Unsafe.As>(ref Unsafe.Add(ref component2Base, i)); + + TOperator.ConvertFromRgb(red, green, blue, in parameters, out c0, out c1, out c2); + } + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector128.Count; + + if (i <= oneVectorFromEnd) + { + Vector128 inverseMaximum = Vector128.Create(1F / maximumValue); + + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + Vector128 red = Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) * inverseMaximum; + Vector128 green = Unsafe.As>(ref Unsafe.Add(ref component1Base, i)) * inverseMaximum; + Vector128 blue = Unsafe.As>(ref Unsafe.Add(ref component2Base, i)) * inverseMaximum; + ref Vector128 c0 = ref Unsafe.As>(ref Unsafe.Add(ref component0Base, i)); + ref Vector128 c1 = ref Unsafe.As>(ref Unsafe.Add(ref component1Base, i)); + ref Vector128 c2 = ref Unsafe.As>(ref Unsafe.Add(ref component2Base, i)); + + TOperator.ConvertFromRgb(red, green, blue, in parameters, out c0, out c1, out c2); + } + } + } + + float inverseMaximumScalar = 1F / maximumValue; + + // The shared offset leaves at most three samples for the scalar fallback on SIMD-capable systems. + for (; i < length; i++) + { + float red = Unsafe.Add(ref component0Base, i) * inverseMaximumScalar; + float green = Unsafe.Add(ref component1Base, i) * inverseMaximumScalar; + float blue = Unsafe.Add(ref component2Base, i) * inverseMaximumScalar; + + TOperator.ConvertFromRgb(red, green, blue, in parameters, out float c0, out float c1, out float c2); + + Unsafe.Add(ref component0Base, i) = c0; + Unsafe.Add(ref component1Base, i) = c1; + Unsafe.Add(ref component2Base, i) = c2; + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.Smpte2085Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.Smpte2085Operator.cs new file mode 100644 index 000000000..7facafd7f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.Smpte2085Operator.cs @@ -0,0 +1,143 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +internal abstract partial class Av1ColorConverterBase +{ + /// + /// Implements SMPTE ST 2085 YDzDx conversion for scalar and SIMD lanes. + /// + internal readonly struct Av1Smpte2085ColorOperator : IAv1ColorOperator + { + /// + /// The SMPTE ST 2085 blue primary normalization factor. + /// + public const float BlueNormalization = 0.986566F; + + /// + /// The SMPTE ST 2085 green contribution to the red primary. + /// + public const float RedGreenContribution = 0.991902F; + + /// + public static bool ChromaUsesLumaRange => false; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float y, ref float dz, ref float dx, in Av1ColorConversionParameters parameters) + { + // The encoded YDzDx planes carry green directly. The two difference planes restore blue and red. + float g = y; + float b = ((2F * dz) + y) / BlueNormalization; + float r = (2F * dx) + (RedGreenContribution * y); + y = r; + dz = g; + dx = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 y, ref Vector128 dz, ref Vector128 dx, in Av1ColorConversionParameters parameters) + { + Vector128 g = y; + Vector128 b = Vector128.MultiplyAddEstimate(Vector128.Create(2F), dz, y) / Vector128.Create(BlueNormalization); + Vector128 r = Vector128.MultiplyAddEstimate(Vector128.Create(2F), dx, Vector128.Create(RedGreenContribution) * y); + y = r; + dz = g; + dx = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 y, ref Vector256 dz, ref Vector256 dx, in Av1ColorConversionParameters parameters) + { + Vector256 g = y; + Vector256 b = Vector256.MultiplyAddEstimate(Vector256.Create(2F), dz, y) / Vector256.Create(BlueNormalization); + Vector256 r = Vector256.MultiplyAddEstimate(Vector256.Create(2F), dx, Vector256.Create(RedGreenContribution) * y); + y = r; + dz = g; + dx = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 y, ref Vector512 dz, ref Vector512 dx, in Av1ColorConversionParameters parameters) + { + Vector512 g = y; + Vector512 b = Vector512.MultiplyAddEstimate(Vector512.Create(2F), dz, y) / Vector512.Create(BlueNormalization); + Vector512 r = Vector512.MultiplyAddEstimate(Vector512.Create(2F), dx, Vector512.Create(RedGreenContribution) * y); + y = r; + dz = g; + dx = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float r, + float g, + float b, + in Av1ColorConversionParameters parameters, + out float y, + out float dz, + out float dx) + { + // Y is the green primary; Dz and Dx are half-scaled blue and red differences. + y = g; + dz = ((BlueNormalization * b) - y) * 0.5F; + dx = (r - (RedGreenContribution * y)) * 0.5F; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 r, + Vector128 g, + Vector128 b, + in Av1ColorConversionParameters parameters, + out Vector128 y, + out Vector128 dz, + out Vector128 dx) + { + y = g; + dz = Vector128.Create(0.5F) * Vector128.MultiplyAddEstimate(Vector128.Create(BlueNormalization), b, -y); + dx = Vector128.Create(0.5F) * Vector128.MultiplyAddEstimate(Vector128.Create(-RedGreenContribution), y, r); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 r, + Vector256 g, + Vector256 b, + in Av1ColorConversionParameters parameters, + out Vector256 y, + out Vector256 dz, + out Vector256 dx) + { + y = g; + dz = Vector256.Create(0.5F) * Vector256.MultiplyAddEstimate(Vector256.Create(BlueNormalization), b, -y); + dx = Vector256.Create(0.5F) * Vector256.MultiplyAddEstimate(Vector256.Create(-RedGreenContribution), y, r); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 r, + Vector512 g, + Vector512 b, + in Av1ColorConversionParameters parameters, + out Vector512 y, + out Vector512 dz, + out Vector512 dx) + { + y = g; + dz = Vector512.Create(0.5F) * Vector512.MultiplyAddEstimate(Vector512.Create(BlueNormalization), b, -y); + dx = Vector512.Create(0.5F) * Vector512.MultiplyAddEstimate(Vector512.Create(-RedGreenContribution), y, r); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.YCgCoOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.YCgCoOperator.cs new file mode 100644 index 000000000..8a62b72a7 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.YCgCoOperator.cs @@ -0,0 +1,141 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +internal abstract partial class Av1ColorConverterBase +{ + /// + /// Implements YCgCo conversion for scalar and SIMD lanes. + /// + internal readonly struct Av1YCgCoColorOperator : IAv1ColorOperator + { + /// + public static bool ChromaUsesLumaRange => false; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float y, ref float cg, ref float co, in Av1ColorConversionParameters parameters) + { + // Reusing Y - Cg for both outer primaries keeps the inverse transform to four additions. + float temporary = y - cg; + float r = temporary + co; + float g = y + cg; + float b = temporary - co; + y = r; + cg = g; + co = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 y, ref Vector128 cg, ref Vector128 co, in Av1ColorConversionParameters parameters) + { + Vector128 temporary = y - cg; + Vector128 r = temporary + co; + Vector128 g = y + cg; + Vector128 b = temporary - co; + y = r; + cg = g; + co = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 y, ref Vector256 cg, ref Vector256 co, in Av1ColorConversionParameters parameters) + { + Vector256 temporary = y - cg; + Vector256 r = temporary + co; + Vector256 g = y + cg; + Vector256 b = temporary - co; + y = r; + cg = g; + co = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 y, ref Vector512 cg, ref Vector512 co, in Av1ColorConversionParameters parameters) + { + Vector512 temporary = y - cg; + Vector512 r = temporary + co; + Vector512 g = y + cg; + Vector512 b = temporary - co; + y = r; + cg = g; + co = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float r, + float g, + float b, + in Av1ColorConversionParameters parameters, + out float y, + out float cg, + out float co) + { + // R + B is shared by Y and Cg, while Co is the half-scaled red/blue difference. + float sum = r + b; + y = (0.5F * g) + (0.25F * sum); + cg = (0.5F * g) - (0.25F * sum); + co = 0.5F * (r - b); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 r, + Vector128 g, + Vector128 b, + in Av1ColorConversionParameters parameters, + out Vector128 y, + out Vector128 cg, + out Vector128 co) + { + Vector128 sum = r + b; + y = Vector128.MultiplyAddEstimate(Vector128.Create(0.5F), g, Vector128.Create(0.25F) * sum); + cg = Vector128.MultiplyAddEstimate(Vector128.Create(0.5F), g, Vector128.Create(-0.25F) * sum); + co = Vector128.Create(0.5F) * (r - b); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 r, + Vector256 g, + Vector256 b, + in Av1ColorConversionParameters parameters, + out Vector256 y, + out Vector256 cg, + out Vector256 co) + { + Vector256 sum = r + b; + y = Vector256.MultiplyAddEstimate(Vector256.Create(0.5F), g, Vector256.Create(0.25F) * sum); + cg = Vector256.MultiplyAddEstimate(Vector256.Create(0.5F), g, Vector256.Create(-0.25F) * sum); + co = Vector256.Create(0.5F) * (r - b); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 r, + Vector512 g, + Vector512 b, + in Av1ColorConversionParameters parameters, + out Vector512 y, + out Vector512 cg, + out Vector512 co) + { + Vector512 sum = r + b; + y = Vector512.MultiplyAddEstimate(Vector512.Create(0.5F), g, Vector512.Create(0.25F) * sum); + cg = Vector512.MultiplyAddEstimate(Vector512.Create(0.5F), g, Vector512.Create(-0.25F) * sum); + co = Vector512.Create(0.5F) * (r - b); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.cs b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.cs new file mode 100644 index 000000000..970f830cf --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.cs @@ -0,0 +1,126 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Identifies the H.273 matrix operation used between encoded planes and RGB components. +/// +internal enum Av1ColorConversionMode +{ + /// + /// A coefficient-based YCbCr matrix conversion. + /// + Coefficients, + + /// + /// Direct G, B, and R component mapping from the Y, U, and V planes. + /// + Identity, + + /// + /// The reversible-style YCgCo color transform. + /// + YCgCo, + + /// + /// The SMPTE ST 2085 YDzDx color transform. + /// + Smpte2085, + + /// + /// A constant-luminance transform using the signaled transfer characteristics. + /// + ConstantLuminance, + + /// + /// The BT.2100 ICtCp color transform. + /// + ICtCp, +} + +/// +/// Converts normalized component planes between an AV1 color model and RGB. +/// +internal abstract partial class Av1ColorConverterBase +{ + /// + /// Initializes a new instance of the class. + /// + /// The resolved H.273 conversion parameters. + /// Whether the frame contains only luma samples. + protected Av1ColorConverterBase(in Av1ColorConversionParameters parameters, bool isMonochrome) + { + this.Parameters = parameters; + this.IsMonochrome = isMonochrome; + } + + /// + /// Gets the resolved H.273 conversion parameters. + /// + protected Av1ColorConversionParameters Parameters { get; } + + /// + /// Gets a value indicating whether the frame contains only luma samples. + /// + protected bool IsMonochrome { get; } + + /// + /// Gets the scale used to encode luma components. + /// + public float LumaScale => this.Parameters.LumaScale; + + /// + /// Gets the bias used to encode luma components. + /// + public float LumaBias => this.Parameters.LumaBias; + + /// + /// Gets the scale used to encode chroma components. + /// + public abstract float ChromaScale { get; } + + /// + /// Gets the bias used to encode chroma components. + /// + public abstract float ChromaBias { get; } + + /// + /// Converts normalized AV1 components to normalized RGB in place. + /// + /// The luma or first color component, replaced by red. + /// The first chroma or second color component, replaced by green. + /// The second chroma or third color component, replaced by blue. + public abstract void ConvertToRgbInPlace(Span component0, Span component1, Span component2); + + /// + /// Converts normalized RGB components to normalized AV1 components in place. + /// + /// The red component, replaced by luma or the first color component. + /// The green component, replaced by the first chroma or second color component. + /// The blue component, replaced by the second chroma or third color component. + /// The largest value in the RGB component planes. + public abstract void ConvertFromRgbInPlace( + Span component0, + Span component1, + Span component2, + float maximumValue); + + /// + /// Creates the converter selected by the resolved H.273 matrix and transfer characteristics. + /// + /// The resolved matrix conversion mode. + /// The resolved H.273 conversion parameters. + /// Whether the frame contains only luma samples. + /// The selected converter. + public static Av1ColorConverterBase Create(Av1ColorConversionMode mode, in Av1ColorConversionParameters parameters, bool isMonochrome) + => mode switch + { + Av1ColorConversionMode.Identity => new Av1ColorConverter(in parameters, isMonochrome), + Av1ColorConversionMode.YCgCo => new Av1ColorConverter(in parameters, isMonochrome), + Av1ColorConversionMode.Smpte2085 => new Av1ColorConverter(in parameters, isMonochrome), + Av1ColorConversionMode.ConstantLuminance => new Av1ColorConverter(in parameters, isMonochrome), + Av1ColorConversionMode.ICtCp => new Av1ColorConverter(in parameters, isMonochrome), + _ => new Av1ColorConverter(in parameters, isMonochrome), + }; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1TransferFunctions.Simd.cs b/src/ImageSharp/Formats/Heif/Av1/Av1TransferFunctions.Simd.cs index eaaa70a58..56b867e8a 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1TransferFunctions.Simd.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1TransferFunctions.Simd.cs @@ -641,22 +641,6 @@ internal static class Av1TransferVectorOperators /// The input vector. /// The lane-wise square root. public static abstract TVector Sqrt(TVector value); - - /// - /// Converts nonlinear signal lanes to their H.273 linear-domain values. - /// - /// The signaled transfer characteristics. - /// The nonlinear signal values. - /// The corresponding linear-domain values. - public static abstract TVector ToLinear(ObuTransferCharacteristics transferCharacteristics, TVector value); - - /// - /// Converts linear signal lanes to their H.273 nonlinear-domain values. - /// - /// The signaled transfer characteristics. - /// The linear signal values. - /// The corresponding nonlinear-domain values. - public static abstract TVector ToGamma(ObuTransferCharacteristics transferCharacteristics, TVector value); } /// @@ -710,14 +694,6 @@ internal static class Av1TransferVectorOperators /// public static Vector128 Sqrt(Vector128 value) => Vector128.Sqrt(value); - - /// - public static Vector128 ToLinear(ObuTransferCharacteristics transferCharacteristics, Vector128 value) - => Av1TransferFunctions.ToLinear(transferCharacteristics, value); - - /// - public static Vector128 ToGamma(ObuTransferCharacteristics transferCharacteristics, Vector128 value) - => Av1TransferFunctions.ToGamma(transferCharacteristics, value); } /// @@ -771,14 +747,6 @@ internal static class Av1TransferVectorOperators /// public static Vector256 Sqrt(Vector256 value) => Vector256.Sqrt(value); - - /// - public static Vector256 ToLinear(ObuTransferCharacteristics transferCharacteristics, Vector256 value) - => Av1TransferFunctions.ToLinear(transferCharacteristics, value); - - /// - public static Vector256 ToGamma(ObuTransferCharacteristics transferCharacteristics, Vector256 value) - => Av1TransferFunctions.ToGamma(transferCharacteristics, value); } /// @@ -832,13 +800,5 @@ internal static class Av1TransferVectorOperators /// public static Vector512 Sqrt(Vector512 value) => Vector512.Sqrt(value); - - /// - public static Vector512 ToLinear(ObuTransferCharacteristics transferCharacteristics, Vector512 value) - => Av1TransferFunctions.ToLinear(transferCharacteristics, value); - - /// - public static Vector512 ToGamma(ObuTransferCharacteristics transferCharacteristics, Vector512 value) - => Av1TransferFunctions.ToGamma(transferCharacteristics, value); } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Operators.cs b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Operators.cs deleted file mode 100644 index 45ad060cf..000000000 --- a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Operators.cs +++ /dev/null @@ -1,623 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; -using static SixLabors.ImageSharp.Formats.Heif.Av1.Av1TransferVectorOperators; - -namespace SixLabors.ImageSharp.Formats.Heif.Av1; - -/// -/// Provides the stateless scalar and SIMD operators used by AV1 YUV-to-RGB row conversion. -/// -internal static partial class Av1YuvConverter -{ - /// - /// Defines one H.273 YUV-to-RGB operation for scalar and SIMD traversal. - /// - private interface IYuvToRgbOperator - { - /// - /// Gets a value indicating whether the operator consumes chroma components. - /// - public static abstract bool UsesChroma { get; } - - /// - /// Converts one normalized YUV sample to RGB. - /// - /// The normalized luma, replaced by red. - /// The normalized blue-difference component, replaced by green. - /// The normalized red-difference component, replaced by blue. - /// The resolved H.273 conversion parameters. - public static abstract void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters); - - /// - /// Converts four normalized YUV samples to RGB. - /// - /// The normalized luma lanes, replaced by red. - /// The normalized blue-difference lanes, replaced by green. - /// The normalized red-difference lanes, replaced by blue. - /// The resolved H.273 conversion parameters. - public static abstract void Convert(ref Vector128 y, ref Vector128 cb, ref Vector128 cr, in YuvToRgbParameters parameters); - - /// - /// Converts eight normalized YUV samples to RGB. - /// - /// The normalized luma lanes, replaced by red. - /// The normalized blue-difference lanes, replaced by green. - /// The normalized red-difference lanes, replaced by blue. - /// The resolved H.273 conversion parameters. - public static abstract void Convert(ref Vector256 y, ref Vector256 cb, ref Vector256 cr, in YuvToRgbParameters parameters); - - /// - /// Converts sixteen normalized YUV samples to RGB. - /// - /// The normalized luma lanes, replaced by red. - /// The normalized blue-difference lanes, replaced by green. - /// The normalized red-difference lanes, replaced by blue. - /// The resolved H.273 conversion parameters. - public static abstract void Convert(ref Vector512 y, ref Vector512 cb, ref Vector512 cr, in YuvToRgbParameters parameters); - } - - /// - /// Stores the resolved H.273 values shared by every scalar and SIMD lane. - /// - private readonly struct YuvToRgbParameters - { - /// - /// Initializes a new instance of the struct. - /// - /// The red luma coefficient. - /// The green luma coefficient. - /// The blue luma coefficient. - /// The signaled transfer characteristics. - /// The constant-luminance chroma scales. - /// The encoded luma bias. - /// The encoded luma range. - /// The encoded chroma midpoint. - /// The encoded chroma range. - public YuvToRgbParameters( - float kr, - float kg, - float kb, - ObuTransferCharacteristics transferCharacteristics, - in ConstantLuminanceScales constantLuminanceScales, - float lumaBias, - float lumaScale, - float chromaBias, - float chromaScale) - { - this.Kr = kr; - this.Kg = kg; - this.Kb = kb; - this.TransferCharacteristics = transferCharacteristics; - this.ConstantLuminanceScales = constantLuminanceScales; - this.LumaBias = lumaBias; - this.LumaScale = lumaScale; - this.ChromaBias = chromaBias; - this.ChromaScale = chromaScale; - } - - /// - /// Gets the red luma coefficient. - /// - public float Kr { get; } - - /// - /// Gets the green luma coefficient. - /// - public float Kg { get; } - - /// - /// Gets the blue luma coefficient. - /// - public float Kb { get; } - - /// - /// Gets the signaled transfer characteristics. - /// - public ObuTransferCharacteristics TransferCharacteristics { get; } - - /// - /// Gets the constant-luminance chroma scales. - /// - public ConstantLuminanceScales ConstantLuminanceScales { get; } - - /// - /// Gets the encoded luma bias. - /// - public float LumaBias { get; } - - /// - /// Gets the encoded luma range. - /// - public float LumaScale { get; } - - /// - /// Gets the encoded chroma midpoint. - /// - public float ChromaBias { get; } - - /// - /// Gets the encoded chroma range. - /// - public float ChromaScale { get; } - } - - /// - /// Replicates luma into all three RGB components for monochrome input. - /// - private readonly struct MonochromeOperator : IYuvToRgbOperator - { - /// - public static bool UsesChroma => false; - - /// - public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters) - { - cb = y; - cr = y; - } - - /// - public static void Convert(ref Vector128 y, ref Vector128 cb, ref Vector128 cr, in YuvToRgbParameters parameters) - { - cb = y; - cr = y; - } - - /// - public static void Convert(ref Vector256 y, ref Vector256 cb, ref Vector256 cr, in YuvToRgbParameters parameters) - { - cb = y; - cr = y; - } - - /// - public static void Convert(ref Vector512 y, ref Vector512 cb, ref Vector512 cr, in YuvToRgbParameters parameters) - { - cb = y; - cr = y; - } - } - - /// - /// Converts coefficient-based YCbCr signals to RGB. - /// - private readonly struct CoefficientsOperator : IYuvToRgbOperator - { - /// - public static bool UsesChroma => true; - - /// - public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters) - { - float r = y + (2F * (1F - parameters.Kr) * cr); - float g = y - (2F * ((parameters.Kr * (1F - parameters.Kr) * cr) + (parameters.Kb * (1F - parameters.Kb) * cb)) / parameters.Kg); - float b = y + (2F * (1F - parameters.Kb) * cb); - y = r; - cb = g; - cr = b; - } - - /// - public static void Convert(ref Vector128 y, ref Vector128 cb, ref Vector128 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertCoefficients, Vector128Operator>(ref y, ref cb, ref cr, in parameters); - - /// - public static void Convert(ref Vector256 y, ref Vector256 cb, ref Vector256 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertCoefficients, Vector256Operator>(ref y, ref cb, ref cr, in parameters); - - /// - public static void Convert(ref Vector512 y, ref Vector512 cb, ref Vector512 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertCoefficients, Vector512Operator>(ref y, ref cb, ref cr, in parameters); - } - - /// - /// Maps identity-coded G, B, and R planes to RGB. - /// - private readonly struct IdentityOperator : IYuvToRgbOperator - { - /// - public static bool UsesChroma => true; - - /// - public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters) - { - float r = (((cr * parameters.ChromaScale) + parameters.ChromaBias) - parameters.LumaBias) / parameters.LumaScale; - float b = (((cb * parameters.ChromaScale) + parameters.ChromaBias) - parameters.LumaBias) / parameters.LumaScale; - cb = y; - y = r; - cr = b; - } - - /// - public static void Convert(ref Vector128 y, ref Vector128 cb, ref Vector128 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertIdentity, Vector128Operator>(ref y, ref cb, ref cr, in parameters); - - /// - public static void Convert(ref Vector256 y, ref Vector256 cb, ref Vector256 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertIdentity, Vector256Operator>(ref y, ref cb, ref cr, in parameters); - - /// - public static void Convert(ref Vector512 y, ref Vector512 cb, ref Vector512 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertIdentity, Vector512Operator>(ref y, ref cb, ref cr, in parameters); - } - - /// - /// Converts YCgCo signals to RGB. - /// - private readonly struct YCgCoOperator : IYuvToRgbOperator - { - /// - public static bool UsesChroma => true; - - /// - public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters) - { - float temporary = y - cb; - float r = temporary + cr; - float g = y + cb; - float b = temporary - cr; - y = r; - cb = g; - cr = b; - } - - /// - public static void Convert(ref Vector128 y, ref Vector128 cb, ref Vector128 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertYCgCo, Vector128Operator>(ref y, ref cb, ref cr); - - /// - public static void Convert(ref Vector256 y, ref Vector256 cb, ref Vector256 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertYCgCo, Vector256Operator>(ref y, ref cb, ref cr); - - /// - public static void Convert(ref Vector512 y, ref Vector512 cb, ref Vector512 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertYCgCo, Vector512Operator>(ref y, ref cb, ref cr); - } - - /// - /// Converts SMPTE ST 2085 YDzDx signals to RGB. - /// - private readonly struct Smpte2085Operator : IYuvToRgbOperator - { - /// - public static bool UsesChroma => true; - - /// - public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters) - { - float g = y; - float b = ((2F * cb) + y) / 0.986566F; - float r = (2F * cr) + (0.991902F * y); - y = r; - cb = g; - cr = b; - } - - /// - public static void Convert(ref Vector128 y, ref Vector128 cb, ref Vector128 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertSmpte2085, Vector128Operator>(ref y, ref cb, ref cr); - - /// - public static void Convert(ref Vector256 y, ref Vector256 cb, ref Vector256 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertSmpte2085, Vector256Operator>(ref y, ref cb, ref cr); - - /// - public static void Convert(ref Vector512 y, ref Vector512 cb, ref Vector512 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertSmpte2085, Vector512Operator>(ref y, ref cb, ref cr); - } - - /// - /// Converts H.273 constant-luminance signals to RGB. - /// - private readonly struct ConstantLuminanceOperator : IYuvToRgbOperator - { - /// - public static bool UsesChroma => true; - - /// - public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters) - { - ConstantLuminanceScales scales = parameters.ConstantLuminanceScales; - float nonlinearBlue = y + (2F * (cb <= 0F ? scales.NegativeBlue : scales.PositiveBlue) * cb); - float nonlinearRed = y + (2F * (cr <= 0F ? scales.NegativeRed : scales.PositiveRed) * cr); - float linearY = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, y); - float linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearBlue); - float linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearRed); - float linearGreen = (linearY - (parameters.Kr * linearRed) - (parameters.Kb * linearBlue)) / parameters.Kg; - y = nonlinearRed; - cb = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); - cr = nonlinearBlue; - } - - /// - public static void Convert(ref Vector128 y, ref Vector128 cb, ref Vector128 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertConstantLuminance, Vector128Operator>(ref y, ref cb, ref cr, in parameters); - - /// - public static void Convert(ref Vector256 y, ref Vector256 cb, ref Vector256 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertConstantLuminance, Vector256Operator>(ref y, ref cb, ref cr, in parameters); - - /// - public static void Convert(ref Vector512 y, ref Vector512 cb, ref Vector512 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertConstantLuminance, Vector512Operator>(ref y, ref cb, ref cr, in parameters); - } - - /// - /// Converts the PQ-family ICtCp matrix to RGB. - /// - private readonly struct ICtCpOperator : IYuvToRgbOperator - { - /// - public static bool UsesChroma => true; - - /// - public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertICtCpScalar(ref y, ref cb, ref cr, in parameters, false); - - /// - public static void Convert(ref Vector128 y, ref Vector128 cb, ref Vector128 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertICtCp, Vector128Operator>(ref y, ref cb, ref cr, in parameters, false); - - /// - public static void Convert(ref Vector256 y, ref Vector256 cb, ref Vector256 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertICtCp, Vector256Operator>(ref y, ref cb, ref cr, in parameters, false); - - /// - public static void Convert(ref Vector512 y, ref Vector512 cb, ref Vector512 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertICtCp, Vector512Operator>(ref y, ref cb, ref cr, in parameters, false); - } - - /// - /// Converts the HLG-specific ICtCp matrix to RGB. - /// - private readonly struct ICtCpHlgOperator : IYuvToRgbOperator - { - /// - public static bool UsesChroma => true; - - /// - public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertICtCpScalar(ref y, ref cb, ref cr, in parameters, true); - - /// - public static void Convert(ref Vector128 y, ref Vector128 cb, ref Vector128 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertICtCp, Vector128Operator>(ref y, ref cb, ref cr, in parameters, true); - - /// - public static void Convert(ref Vector256 y, ref Vector256 cb, ref Vector256 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertICtCp, Vector256Operator>(ref y, ref cb, ref cr, in parameters, true); - - /// - public static void Convert(ref Vector512 y, ref Vector512 cb, ref Vector512 cr, in YuvToRgbParameters parameters) - => YuvToRgbMath.ConvertICtCp, Vector512Operator>(ref y, ref cb, ref cr, in parameters, true); - } - - /// - /// Contains the shared vector-width-independent arithmetic used by the color operators. - /// - private static class YuvToRgbMath - { - /// - /// Converts coefficient-based YCbCr SIMD lanes to RGB. - /// - /// The SIMD vector type. - /// The operations for the SIMD vector type. - /// The luma lanes, replaced by red. - /// The blue-difference lanes, replaced by green. - /// The red-difference lanes, replaced by blue. - /// The resolved H.273 conversion parameters. - public static void ConvertCoefficients(ref TVector y, ref TVector cb, ref TVector cr, in YuvToRgbParameters parameters) - where TVector : struct - where TOperator : struct, ITransferVectorOperator - { - TVector r = TOperator.MultiplyAddEstimate(TOperator.Create(2F * (1F - parameters.Kr)), cr, y); - TVector greenDifference = TOperator.MultiplyAddEstimate( - TOperator.Create(parameters.Kr * (1F - parameters.Kr)), - cr, - TOperator.Multiply(TOperator.Create(parameters.Kb * (1F - parameters.Kb)), cb)); - - TVector g = TOperator.Subtract(y, TOperator.Multiply(TOperator.Create(2F / parameters.Kg), greenDifference)); - TVector b = TOperator.MultiplyAddEstimate(TOperator.Create(2F * (1F - parameters.Kb)), cb, y); - y = r; - cb = g; - cr = b; - } - - /// - /// Maps identity-coded SIMD lanes to RGB. - /// - /// The SIMD vector type. - /// The operations for the SIMD vector type. - /// The green lanes, replaced by red. - /// The normalized blue plane, replaced by green. - /// The normalized red plane, replaced by blue. - /// The resolved H.273 conversion parameters. - public static void ConvertIdentity(ref TVector y, ref TVector cb, ref TVector cr, in YuvToRgbParameters parameters) - where TVector : struct - where TOperator : struct, ITransferVectorOperator - { - TVector lumaBias = TOperator.Create(parameters.LumaBias); - TVector inverseLumaScale = TOperator.Create(1F / parameters.LumaScale); - TVector chromaScale = TOperator.Create(parameters.ChromaScale); - TVector chromaBias = TOperator.Create(parameters.ChromaBias); - TVector r = TOperator.Multiply(TOperator.Subtract(TOperator.MultiplyAddEstimate(cr, chromaScale, chromaBias), lumaBias), inverseLumaScale); - TVector b = TOperator.Multiply(TOperator.Subtract(TOperator.MultiplyAddEstimate(cb, chromaScale, chromaBias), lumaBias), inverseLumaScale); - cb = y; - y = r; - cr = b; - } - - /// - /// Converts YCgCo SIMD lanes to RGB. - /// - /// The SIMD vector type. - /// The operations for the SIMD vector type. - /// The luma lanes, replaced by red. - /// The green-difference lanes, replaced by green. - /// The orange-difference lanes, replaced by blue. - public static void ConvertYCgCo(ref TVector y, ref TVector cb, ref TVector cr) - where TVector : struct - where TOperator : struct, ITransferVectorOperator - { - TVector temporary = TOperator.Subtract(y, cb); - TVector r = TOperator.Add(temporary, cr); - TVector g = TOperator.Add(y, cb); - TVector b = TOperator.Subtract(temporary, cr); - y = r; - cb = g; - cr = b; - } - - /// - /// Converts SMPTE ST 2085 SIMD lanes to RGB. - /// - /// The SIMD vector type. - /// The operations for the SIMD vector type. - /// The luma lanes, replaced by red. - /// The blue-difference lanes, replaced by green. - /// The red-difference lanes, replaced by blue. - public static void ConvertSmpte2085(ref TVector y, ref TVector cb, ref TVector cr) - where TVector : struct - where TOperator : struct, ITransferVectorOperator - { - TVector g = y; - TVector b = TOperator.Multiply(TOperator.MultiplyAddEstimate(TOperator.Create(2F), cb, y), TOperator.Create(1F / 0.986566F)); - TVector r = TOperator.MultiplyAddEstimate(TOperator.Create(2F), cr, TOperator.Multiply(TOperator.Create(0.991902F), y)); - y = r; - cb = g; - cr = b; - } - - /// - /// Converts constant-luminance SIMD lanes to RGB. - /// - /// The SIMD vector type. - /// The operations for the SIMD vector type. - /// The nonlinear luma lanes, replaced by red. - /// The blue-difference lanes, replaced by green. - /// The red-difference lanes, replaced by blue. - /// The resolved H.273 conversion parameters. - public static void ConvertConstantLuminance(ref TVector y, ref TVector cb, ref TVector cr, in YuvToRgbParameters parameters) - where TVector : struct - where TOperator : struct, ITransferVectorOperator - { - TVector zero = TOperator.Create(0F); - ConstantLuminanceScales scales = parameters.ConstantLuminanceScales; - TVector blueScale = TOperator.ConditionalSelect( - TOperator.LessThanOrEqual(cb, zero), - TOperator.Create(scales.NegativeBlue), - TOperator.Create(scales.PositiveBlue)); - - TVector redScale = TOperator.ConditionalSelect( - TOperator.LessThanOrEqual(cr, zero), - TOperator.Create(scales.NegativeRed), - TOperator.Create(scales.PositiveRed)); - - TVector nonlinearBlue = TOperator.MultiplyAddEstimate(TOperator.Multiply(TOperator.Create(2F), blueScale), cb, y); - TVector nonlinearRed = TOperator.MultiplyAddEstimate(TOperator.Multiply(TOperator.Create(2F), redScale), cr, y); - TVector linearY = TOperator.ToLinear(parameters.TransferCharacteristics, y); - TVector linearBlue = TOperator.ToLinear(parameters.TransferCharacteristics, nonlinearBlue); - TVector linearRed = TOperator.ToLinear(parameters.TransferCharacteristics, nonlinearRed); - TVector redAndBlue = TOperator.MultiplyAddEstimate( - TOperator.Create(parameters.Kr), - linearRed, - TOperator.Multiply(TOperator.Create(parameters.Kb), linearBlue)); - - TVector linearGreen = TOperator.Divide(TOperator.Subtract(linearY, redAndBlue), TOperator.Create(parameters.Kg)); - y = nonlinearRed; - cb = TOperator.ToGamma(parameters.TransferCharacteristics, linearGreen); - cr = nonlinearBlue; - } - - /// - /// Converts one ICtCp sample to RGB using the selected inverse matrix. - /// - /// The intensity, replaced by red. - /// The tritan-difference component, replaced by green. - /// The protan-difference component, replaced by blue. - /// The resolved H.273 conversion parameters. - /// Whether to use the HLG-specific inverse ICtCp matrix. - public static void ConvertICtCpScalar(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters, bool isHlg) - { - float nonlinearL = isHlg - ? y + (0.015718580108730413F * cb) + (0.2095810681164055F * cr) - : y + (0.008609037037932756F * cb) + (0.11102962500302596F * cr); - - float nonlinearM = isHlg - ? y - (0.015718580108730413F * cb) - (0.2095810681164055F * cr) - : y - (0.008609037037932756F * cb) - (0.11102962500302596F * cr); - - float nonlinearS = isHlg - ? y + (1.0212710798422342F * cb) - (0.6052744909924315F * cr) - : y + (0.5600313357106791F * cb) - (0.32062717498731885F * cr); - - float linearL = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL); - float linearM = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM); - float linearS = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS); - float linearRed = (3.4366066943330784F * linearL) - (2.50645211865627F * linearM) + (0.06984542432319148F * linearS); - float linearGreen = (-0.7913295555989287F * linearL) + (1.9836004517922907F * linearM) - (0.192270896193362F * linearS); - float linearBlue = (-0.025949899690592672F * linearL) - (0.09891371471172644F * linearM) + (1.1248636144023192F * linearS); - y = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed); - cb = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); - cr = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue); - } - - /// - /// Converts ICtCp SIMD lanes to RGB using the selected inverse matrix. - /// - /// The SIMD vector type. - /// The operations for the SIMD vector type. - /// The intensity lanes, replaced by red. - /// The tritan-difference lanes, replaced by green. - /// The protan-difference lanes, replaced by blue. - /// The resolved H.273 conversion parameters. - /// Whether to use the HLG-specific inverse ICtCp matrix. - public static void ConvertICtCp(ref TVector y, ref TVector cb, ref TVector cr, in YuvToRgbParameters parameters, bool isHlg) - where TVector : struct - where TOperator : struct, ITransferVectorOperator - { - float cbToL = isHlg ? 0.015718580108730413F : 0.008609037037932756F; - float crToL = isHlg ? 0.2095810681164055F : 0.11102962500302596F; - float cbToS = isHlg ? 1.0212710798422342F : 0.5600313357106791F; - float crToS = isHlg ? -0.6052744909924315F : -0.32062717498731885F; - TVector cbContribution = TOperator.Multiply(TOperator.Create(cbToL), cb); - TVector crContribution = TOperator.Multiply(TOperator.Create(crToL), cr); - TVector nonlinearL = TOperator.Add(TOperator.Add(y, cbContribution), crContribution); - TVector nonlinearM = TOperator.Subtract(TOperator.Subtract(y, cbContribution), crContribution); - TVector nonlinearS = TOperator.MultiplyAddEstimate(TOperator.Create(crToS), cr, TOperator.MultiplyAddEstimate(TOperator.Create(cbToS), cb, y)); - TVector linearL = TOperator.ToLinear(parameters.TransferCharacteristics, nonlinearL); - TVector linearM = TOperator.ToLinear(parameters.TransferCharacteristics, nonlinearM); - TVector linearS = TOperator.ToLinear(parameters.TransferCharacteristics, nonlinearS); - TVector linearRed = TOperator.MultiplyAddEstimate( - TOperator.Create(0.06984542432319148F), - linearS, - TOperator.MultiplyAddEstimate( - TOperator.Create(-2.50645211865627F), - linearM, - TOperator.Multiply(TOperator.Create(3.4366066943330784F), linearL))); - - TVector linearGreen = TOperator.MultiplyAddEstimate( - TOperator.Create(-0.192270896193362F), - linearS, - TOperator.MultiplyAddEstimate( - TOperator.Create(1.9836004517922907F), - linearM, - TOperator.Multiply(TOperator.Create(-0.7913295555989287F), linearL))); - - TVector linearBlue = TOperator.MultiplyAddEstimate( - TOperator.Create(1.1248636144023192F), - linearS, - TOperator.MultiplyAddEstimate( - TOperator.Create(-0.09891371471172644F), - linearM, - TOperator.Multiply(TOperator.Create(-0.025949899690592672F), linearL))); - - y = TOperator.ToGamma(parameters.TransferCharacteristics, linearRed); - cb = TOperator.ToGamma(parameters.TransferCharacteristics, linearGreen); - cr = TOperator.ToGamma(parameters.TransferCharacteristics, linearBlue); - } - } -} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowConverters.cs b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowConverters.cs new file mode 100644 index 000000000..61d029c79 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowConverters.cs @@ -0,0 +1,426 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Provides the pooled row converters used by AV1 color conversion. +/// +internal static partial class Av1YuvConverter +{ + /// + /// Converts one reconstructed AV1 row using frame-scoped pooled component, chroma, and packed-pixel storage. + /// + /// The destination pixel type. + /// The reconstructed sample type. + /// The SIMD widening operations for the sample type. + private readonly struct YuvToRgbRowConverter + where TPixel : unmanaged, IPixel + where TSample : unmanaged + where TLoader : struct, ISampleLoader + { + /// + /// The configuration used by bulk pixel conversion. + /// + private readonly Configuration configuration; + + /// + /// The reconstructed AV1 frame containing the source planes. + /// + private readonly Av1FrameBuffer frameBuffer; + + /// + /// The destination image frame. + /// + private readonly ImageFrame image; + + /// + /// The full-resolution luma plane. + /// + private readonly Buffer2DRegion yPlane; + + /// + /// The blue-difference plane when the frame contains chroma. + /// + private readonly Buffer2DRegion uPlane; + + /// + /// The red-difference plane when the frame contains chroma. + /// + private readonly Buffer2DRegion vPlane; + + /// + /// The frame-scoped color-model converter. + /// + private readonly Av1ColorConverterBase colorConverter; + + /// + /// The signaled chroma sample position used for reconstruction. + /// + private readonly ObuChromoSamplePosition chromaSamplePosition; + + /// + /// Whether the source contains only a luma plane. + /// + private readonly bool isMonochrome; + + /// + /// The horizontal chroma subsampling shift. + /// + private readonly int subX; + + /// + /// The vertical chroma subsampling shift. + /// + private readonly int subY; + + /// + /// Initializes a new instance of the struct. + /// + /// The configuration used for packed-pixel conversion. + /// The reconstructed AV1 frame. + /// The destination image frame. + /// The selected H.273 color converter. + public YuvToRgbRowConverter( + Configuration configuration, + Av1FrameBuffer frameBuffer, + ImageFrame image, + Av1ColorConverterBase colorConverter) + { + this.configuration = configuration; + this.frameBuffer = frameBuffer; + this.image = image; + this.colorConverter = colorConverter; + this.isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400; + this.subX = frameBuffer.ColorConfig.SubSamplingX ? 1 : 0; + this.subY = frameBuffer.ColorConfig.SubSamplingY ? 1 : 0; + this.chromaSamplePosition = frameBuffer.ColorConfig.ChromaSamplePosition; + this.yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0); + this.uPlane = this.isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.U, this.subX, this.subY); + this.vPlane = this.isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.V, this.subX, this.subY); + } + + /// + /// Gets the number of float elements required by the reusable conversion buffer. + /// + public int BufferLength + { + get + { + // Three float component rows are converted in place. Color input adds two reusable chroma scratch + // rows, and the final one or two float-sized slots per pixel back an Rgba32 or Rgba64 staging row. + int rowCount = this.isMonochrome ? 3 : 5; + int packedRowCount = typeof(TSample) == typeof(byte) ? 1 : 2; + return this.image.Width * (rowCount + packedRowCount); + } + } + + /// + /// Converts one reconstructed AV1 row to packed pixels. + /// + /// The row index. + /// The reusable conversion buffer. + public void Convert(int y, Span span) + { + int width = this.image.Width; + Span red = span[..width]; + Span green = span.Slice(width, width); + Span blue = span.Slice(width * 2, width); + + ReadOnlySpan ySource; + if (typeof(TSample) == typeof(byte)) + { + ySource = MemoryMarshal.Cast(this.yPlane.DangerousGetRowSpan(y)); + } + else + { + ySource = MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, y, 0, 0)); + } + + ConvertSamplesToFloat(ySource, red); + + int packedOffset = width * 3; + if (!this.isMonochrome) + { + GetChromaCoordinates( + y, + this.subY, + this.subY != 0 && this.chromaSamplePosition != ObuChromoSamplePosition.Colocated, + this.uPlane.Height - 1, + out int y0, + out int y1, + out int y1Weight); + + ReadOnlySpan uRow0; + ReadOnlySpan uRow1; + ReadOnlySpan vRow0; + ReadOnlySpan vRow1; + if (typeof(TSample) == typeof(byte)) + { + uRow0 = MemoryMarshal.Cast(this.uPlane.DangerousGetRowSpan(y0)); + uRow1 = MemoryMarshal.Cast(this.uPlane.DangerousGetRowSpan(y1)); + vRow0 = MemoryMarshal.Cast(this.vPlane.DangerousGetRowSpan(y0)); + vRow1 = MemoryMarshal.Cast(this.vPlane.DangerousGetRowSpan(y1)); + } + else + { + uRow0 = MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y0, this.subX, this.subY)); + uRow1 = MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y1, this.subX, this.subY)); + vRow0 = MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y0, this.subX, this.subY)); + vRow1 = MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y1, this.subX, this.subY)); + } + + Span scratch0 = span.Slice(width * 3, width); + Span scratch1 = span.Slice(width * 4, width); + bool isCenteredX = this.subX != 0 && (this.subY == 0 || this.chromaSamplePosition == ObuChromoSamplePosition.Unknown); + ReconstructChromaRow(uRow0, uRow1, y1Weight, this.subX, isCenteredX, green, scratch0, scratch1); + ReconstructChromaRow(vRow0, vRow1, y1Weight, this.subX, isCenteredX, blue, scratch0, scratch1); + packedOffset = width * 5; + } + + this.colorConverter.ConvertToRgbInPlace(red, green, blue); + Span packedStorage = span[packedOffset..]; + Span destination = this.image.PixelBuffer.DangerousGetRowSpan(y); + if (typeof(TSample) == typeof(byte)) + { + Span packed = MemoryMarshal.Cast(packedStorage)[..width]; + PackRgba32(red, green, blue, packed); + PixelOperations.Instance.FromRgba32(this.configuration, packed, destination); + } + else + { + Span packed = MemoryMarshal.Cast(packedStorage)[..width]; + PackRgba64(red, green, blue, packed); + PixelOperations.Instance.FromRgba64(this.configuration, packed, destination); + } + } + } + + /// + /// Converts one source row, or one vertically subsampled row pair, using reusable pooled component storage. + /// + /// The source pixel type. + /// The encoded sample type. + /// The SIMD narrowing and storage operations for the sample type. + private readonly struct RgbToYuvRowConverter + where TPixel : unmanaged, IPixel + where TSample : unmanaged + where TStorer : struct, ISampleStorer + { + /// + /// The configuration used by bulk pixel conversion. + /// + private readonly Configuration configuration; + + /// + /// The destination AV1 frame. + /// + private readonly Av1FrameBuffer frameBuffer; + + /// + /// The source image frame. + /// + private readonly ImageFrame image; + + /// + /// The full-resolution luma plane. + /// + private readonly Buffer2DRegion yPlane; + + /// + /// The blue-difference plane when the frame contains chroma. + /// + private readonly Buffer2DRegion uPlane; + + /// + /// The red-difference plane when the frame contains chroma. + /// + private readonly Buffer2DRegion vPlane; + + /// + /// The frame-scoped color-model converter. + /// + private readonly Av1ColorConverterBase colorConverter; + + /// + /// The largest value represented by the encoded AV1 bit depth. + /// + private readonly float sampleMaximum; + + /// + /// Whether the destination contains only a luma plane. + /// + private readonly bool isMonochrome; + + /// + /// The horizontal chroma subsampling shift. + /// + private readonly int subX; + + /// + /// The vertical chroma subsampling shift. + /// + private readonly int rowShift; + + /// + /// Initializes a new instance of the struct. + /// + /// The configuration used for packed-pixel conversion. + /// The destination AV1 frame. + /// The source image frame. + /// The selected H.273 color converter. + /// The largest encoded sample value. + public RgbToYuvRowConverter( + Configuration configuration, + Av1FrameBuffer frameBuffer, + ImageFrame image, + Av1ColorConverterBase colorConverter, + float sampleMaximum) + { + this.configuration = configuration; + this.frameBuffer = frameBuffer; + this.image = image; + this.colorConverter = colorConverter; + this.sampleMaximum = sampleMaximum; + this.isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400; + this.subX = frameBuffer.ColorConfig.SubSamplingX ? 1 : 0; + this.rowShift = !this.isMonochrome && frameBuffer.ColorConfig.SubSamplingY ? 1 : 0; + this.yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0); + this.uPlane = this.isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.U, this.subX, this.rowShift); + this.vPlane = this.isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.V, this.subX, this.rowShift); + } + + /// + /// Gets the number of float elements required by the reusable conversion buffer. + /// + public int ComponentBufferLength + { + get + { + // Three planar rows hold the converted YUV values, and 4:2:0 keeps a second set until its + // chroma has been averaged with the first. + int componentRowCount = this.rowShift == 0 ? 3 : 6; + return this.image.Width * componentRowCount; + } + } + + /// + /// Converts one source row, or one vertically subsampled row pair, to AV1 planes. + /// + /// The conversion iteration index. + /// The reusable high-bit-depth RGB staging row. + /// The reusable planar component buffer. + public void Convert(int y, Span packed, Span components) + { + int width = this.image.Width; + int sourceY = y << this.rowShift; + Span yRow0 = components[..width]; + Span cbRow0 = components.Slice(width, width); + Span crRow0 = components.Slice(width * 2, width); + this.ConvertSourceRow(sourceY, packed, yRow0, cbRow0, crRow0); + WriteSamples( + yRow0, + this.GetPlaneRow(Av1Plane.Y, sourceY), + this.colorConverter.LumaScale, + this.colorConverter.LumaBias, + this.sampleMaximum); + + bool hasSecondSourceRow = this.rowShift != 0 && sourceY + 1 < this.image.Height; + Span yRow1 = Span.Empty; + Span cbRow1 = Span.Empty; + Span crRow1 = Span.Empty; + if (hasSecondSourceRow) + { + yRow1 = components.Slice(width * 3, width); + cbRow1 = components.Slice(width * 4, width); + crRow1 = components.Slice(width * 5, width); + this.ConvertSourceRow(sourceY + 1, packed, yRow1, cbRow1, crRow1); + WriteSamples( + yRow1, + this.GetPlaneRow(Av1Plane.Y, sourceY + 1), + this.colorConverter.LumaScale, + this.colorConverter.LumaBias, + this.sampleMaximum); + } + + if (this.isMonochrome) + { + return; + } + + Span uDestination = this.GetPlaneRow(Av1Plane.U, y); + Span vDestination = this.GetPlaneRow(Av1Plane.V, y); + float chromaScale = this.colorConverter.ChromaScale; + float chromaBias = this.colorConverter.ChromaBias; + if (this.subX == 0) + { + WriteSamples(cbRow0, uDestination, chromaScale, chromaBias, this.sampleMaximum); + WriteSamples(crRow0, vDestination, chromaScale, chromaBias, this.sampleMaximum); + } + else + { + WriteSubsampledSamples(cbRow0, cbRow1, uDestination, chromaScale, chromaBias, this.sampleMaximum); + WriteSubsampledSamples(crRow0, crRow1, vDestination, chromaScale, chromaBias, this.sampleMaximum); + } + } + + /// + /// Converts one source row into normalized planar AV1 components. + /// + /// The source row index. + /// The high-bit-depth RGB staging row. + /// The destination luma values. + /// The destination blue-difference values. + /// The destination red-difference values. + private void ConvertSourceRow(int y, Span packed, Span luma, Span chromaBlue, Span chromaRed) + { + ReadOnlySpan source = this.image.PixelBuffer.DangerousGetRowSpan(y); + if (typeof(TSample) == typeof(byte)) + { + // This is the same planar input contract used by JPEG encoding. Pixel types with optimized + // unpackers reach their existing SIMD path before the AV1 operator consumes the planes. + PixelOperations.Instance.UnpackIntoRgbPlanes(luma, chromaBlue, chromaRed, source); + this.colorConverter.ConvertFromRgbInPlace(luma, chromaBlue, chromaRed, ByteMaximum); + } + else + { + // JPEG's planar unpack contract is eight-bit. AV1 10/12-bit encoding stages Rgb48 instead + // so high-precision source pixels are not truncated before the color transform. + PixelOperations.Instance.ToRgb48(this.configuration, source, packed); + DeinterleaveRgb48(packed, luma, chromaBlue, chromaRed); + this.colorConverter.ConvertFromRgbInPlace(luma, chromaBlue, chromaRed, UShortMaximum); + } + } + + /// + /// Gets a writable row from an eight-bit or high-bit-depth AV1 plane. + /// + /// The requested plane. + /// The row index in the requested plane. + /// The writable sample row. + private Span GetPlaneRow(Av1Plane plane, int y) + { + if (typeof(TSample) == typeof(byte)) + { + Buffer2DRegion region = plane switch + { + Av1Plane.Y => this.yPlane, + Av1Plane.U => this.uPlane, + _ => this.vPlane, + }; + + return MemoryMarshal.Cast(region.DangerousGetRowSpan(y)); + } + + int subX = plane == Av1Plane.Y ? 0 : this.subX; + int subY = plane == Av1Plane.Y ? 0 : this.rowShift; + return MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(plane, y, subX, subY)); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowOperation.cs b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowOperation.cs deleted file mode 100644 index 243004338..000000000 --- a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowOperation.cs +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Formats.Heif.Av1; - -/// -/// Provides the pooled parallel row operation used by AV1 YUV-to-RGB decoding. -/// -internal static partial class Av1YuvConverter -{ - /// - /// Converts one reconstructed AV1 row using worker-owned pooled component, chroma, and packed-pixel storage. - /// - /// The destination pixel type. - /// The reconstructed sample type. - /// The SIMD widening operations for the sample type. - private readonly struct YuvToRgbRowOperation : IRowOperation - where TPixel : unmanaged, IPixel - where TSample : unmanaged - where TLoader : struct, ISampleLoader - { - private readonly Configuration configuration; - private readonly Av1FrameBuffer frameBuffer; - private readonly ImageFrame image; - private readonly Buffer2DRegion yPlane; - private readonly Buffer2DRegion uPlane; - private readonly Buffer2DRegion vPlane; - private readonly YuvToRgbParameters parameters; - private readonly ConversionMode mode; - private readonly ObuChromoSamplePosition chromaSamplePosition; - private readonly bool isMonochrome; - private readonly int subX; - private readonly int subY; - - /// - /// Initializes a new instance of the struct. - /// - /// The configuration used for packed-pixel conversion. - /// The reconstructed AV1 frame. - /// The destination image frame. - /// The resolved H.273 conversion mode. - /// The resolved H.273 conversion parameters. - public YuvToRgbRowOperation( - Configuration configuration, - Av1FrameBuffer frameBuffer, - ImageFrame image, - ConversionMode mode, - in YuvToRgbParameters parameters) - { - this.configuration = configuration; - this.frameBuffer = frameBuffer; - this.image = image; - this.mode = mode; - this.parameters = parameters; - this.isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400; - this.subX = frameBuffer.ColorConfig.SubSamplingX ? 1 : 0; - this.subY = frameBuffer.ColorConfig.SubSamplingY ? 1 : 0; - this.chromaSamplePosition = frameBuffer.ColorConfig.ChromaSamplePosition; - this.yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0); - this.uPlane = this.isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.U, this.subX, this.subY); - this.vPlane = this.isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.V, this.subX, this.subY); - } - - /// - public int GetRequiredBufferLength(Rectangle bounds) - { - // Three float component rows are converted in place. Color input adds two reusable chroma scratch - // rows, and the final one or two float-sized slots per pixel back an Rgba32 or Rgba64 staging row. - int rowCount = this.isMonochrome ? 3 : 5; - int packedRowCount = typeof(TSample) == typeof(byte) ? 1 : 2; - return bounds.Width * (rowCount + packedRowCount); - } - - /// - public void Invoke(int y, Span span) - { - int width = this.image.Width; - Span red = span[..width]; - Span green = span.Slice(width, width); - Span blue = span.Slice(width * 2, width); - - ReadOnlySpan ySource; - if (typeof(TSample) == typeof(byte)) - { - ySource = MemoryMarshal.Cast(this.yPlane.DangerousGetRowSpan(y)); - } - else - { - ySource = MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, y, 0, 0)); - } - - ConvertSamplesToFloat(ySource, red); - - int packedOffset = width * 3; - if (!this.isMonochrome) - { - GetChromaCoordinates( - y, - this.subY, - this.subY != 0 && this.chromaSamplePosition != ObuChromoSamplePosition.Colocated, - this.uPlane.Height - 1, - out int y0, - out int y1, - out int y1Weight); - - ReadOnlySpan uRow0; - ReadOnlySpan uRow1; - ReadOnlySpan vRow0; - ReadOnlySpan vRow1; - if (typeof(TSample) == typeof(byte)) - { - uRow0 = MemoryMarshal.Cast(this.uPlane.DangerousGetRowSpan(y0)); - uRow1 = MemoryMarshal.Cast(this.uPlane.DangerousGetRowSpan(y1)); - vRow0 = MemoryMarshal.Cast(this.vPlane.DangerousGetRowSpan(y0)); - vRow1 = MemoryMarshal.Cast(this.vPlane.DangerousGetRowSpan(y1)); - } - else - { - uRow0 = MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y0, this.subX, this.subY)); - uRow1 = MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y1, this.subX, this.subY)); - vRow0 = MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y0, this.subX, this.subY)); - vRow1 = MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y1, this.subX, this.subY)); - } - - Span scratch0 = span.Slice(width * 3, width); - Span scratch1 = span.Slice(width * 4, width); - bool isCenteredX = this.subX != 0 && (this.subY == 0 || this.chromaSamplePosition == ObuChromoSamplePosition.Unknown); - ReconstructChromaRow(uRow0, uRow1, y1Weight, this.subX, isCenteredX, green, scratch0, scratch1); - ReconstructChromaRow(vRow0, vRow1, y1Weight, this.subX, isCenteredX, blue, scratch0, scratch1); - packedOffset = width * 5; - } - - ConvertYuvToRgbRow(red, green, blue, this.isMonochrome, this.mode, in this.parameters); - Span packedStorage = span[packedOffset..]; - Span destination = this.image.PixelBuffer.DangerousGetRowSpan(y); - if (typeof(TSample) == typeof(byte)) - { - Span packed = MemoryMarshal.Cast(packedStorage)[..width]; - PackRgba32(red, green, blue, packed); - PixelOperations.Instance.FromRgba32(this.configuration, packed, destination); - } - else - { - Span packed = MemoryMarshal.Cast(packedStorage)[..width]; - PackRgba64(red, green, blue, packed); - PixelOperations.Instance.FromRgba64(this.configuration, packed, destination); - } - } - } -} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Simd.cs b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Simd.cs index 8de7d1967..df28df6ed 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Simd.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Simd.cs @@ -10,7 +10,7 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Heif.Av1; /// -/// Provides SIMD sample widening, chroma reconstruction, color traversal, and packed output for AV1 decoding. +/// Provides SIMD sample widening, chroma reconstruction, planar storage, and packed output for AV1 color conversion. /// internal static partial class Av1YuvConverter { @@ -43,6 +43,35 @@ internal static partial class Av1YuvConverter public static abstract Vector512 LoadVector512(ref TSample source); } + /// + /// Defines the SIMD narrowing and storage operations for one encoded AV1 sample type. + /// + /// The encoded sample type. + private interface ISampleStorer + where TSample : unmanaged + { + /// + /// Narrows and stores four integer samples. + /// + /// The integer samples. + /// The first destination sample. + public static abstract void Store(Vector128 source, ref TSample destination); + + /// + /// Narrows and stores eight integer samples. + /// + /// The integer samples. + /// The first destination sample. + public static abstract void Store(Vector256 source, ref TSample destination); + + /// + /// Narrows and stores sixteen integer samples. + /// + /// The integer samples. + /// The first destination sample. + public static abstract void Store(Vector512 source, ref TSample destination); + } + /// /// Widens reconstructed integer samples into a pooled float component row. /// @@ -357,158 +386,218 @@ internal static partial class Av1YuvConverter } /// - /// Dispatches one normalized component row to its matrix-specific scalar and SIMD operator. + /// Deinterleaves high-bit-depth RGB pixels into planar component rows. /// - /// The luma row, replaced by red. - /// The blue-difference row, replaced by green. - /// The red-difference row, replaced by blue. - /// Whether the frame contains only luma samples. - /// The resolved H.273 conversion mode. - /// The resolved H.273 conversion parameters. - private static void ConvertYuvToRgbRow( - Span red, - Span green, - Span blue, - bool isMonochrome, - ConversionMode mode, - in YuvToRgbParameters parameters) + /// The packed RGBA pixels. + /// The destination red components. + /// The destination green components. + /// The destination blue components. + private static void DeinterleaveRgb48(ReadOnlySpan source, Span red, Span green, Span blue) { - if (isMonochrome) + ref float redBase = ref MemoryMarshal.GetReference(red); + ref float greenBase = ref MemoryMarshal.GetReference(green); + ref float blueBase = ref MemoryMarshal.GetReference(blue); + ref Rgb48 sourceBase = ref MemoryMarshal.GetReference(source); + int length = source.Length; + int i = 0; + + if (Vector128.IsHardwareAccelerated) { - ConvertYuvToRgbRow(red, green, blue, in parameters); - return; + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + ref Rgb48 pixel0 = ref Unsafe.Add(ref sourceBase, i); + ref Rgb48 pixel1 = ref Unsafe.Add(ref sourceBase, i + 1); + ref Rgb48 pixel2 = ref Unsafe.Add(ref sourceBase, i + 2); + ref Rgb48 pixel3 = ref Unsafe.Add(ref sourceBase, i + 3); + + // Widen each UInt16 channel to a UInt32 lane before converting to Single. This avoids + // reinterpreting adjacent 16-bit samples as one unrelated 32-bit integer. + Vector128 redVector = Vector128.ConvertToSingle( + Vector128.Create((uint)pixel0.R, pixel1.R, pixel2.R, pixel3.R)); + Vector128 greenVector = Vector128.ConvertToSingle( + Vector128.Create((uint)pixel0.G, pixel1.G, pixel2.G, pixel3.G)); + Vector128 blueVector = Vector128.ConvertToSingle( + Vector128.Create((uint)pixel0.B, pixel1.B, pixel2.B, pixel3.B)); + + Unsafe.As>(ref Unsafe.Add(ref redBase, i)) = redVector; + Unsafe.As>(ref Unsafe.Add(ref greenBase, i)) = greenVector; + Unsafe.As>(ref Unsafe.Add(ref blueBase, i)) = blueVector; + } } - switch (mode) - { - case ConversionMode.Identity: - ConvertYuvToRgbRow(red, green, blue, in parameters); - break; - case ConversionMode.YCgCo: - ConvertYuvToRgbRow(red, green, blue, in parameters); - break; - case ConversionMode.Smpte2085: - ConvertYuvToRgbRow(red, green, blue, in parameters); - break; - case ConversionMode.ConstantLuminance: - ConvertYuvToRgbRow(red, green, blue, in parameters); - break; - case ConversionMode.ICtCp when parameters.TransferCharacteristics == OpenBitstreamUnit.ObuTransferCharacteristics.Hlg: - ConvertYuvToRgbRow(red, green, blue, in parameters); - break; - case ConversionMode.ICtCp: - ConvertYuvToRgbRow(red, green, blue, in parameters); - break; - default: - ConvertYuvToRgbRow(red, green, blue, in parameters); - break; + for (; i < length; i++) + { + Rgb48 pixel = Unsafe.Add(ref sourceBase, i); + Unsafe.Add(ref redBase, i) = pixel.R; + Unsafe.Add(ref greenBase, i) = pixel.G; + Unsafe.Add(ref blueBase, i) = pixel.B; } } /// - /// Converts one component row using the selected static scalar and SIMD operator. + /// Scales, quantizes, and stores one planar row using the widest available SIMD width. /// - /// The matrix-specific conversion operator. - /// The luma row, replaced by red. - /// The blue-difference row, replaced by green. - /// The red-difference row, replaced by blue. - /// The resolved H.273 conversion parameters. - private static void ConvertYuvToRgbRow(Span red, Span green, Span blue, in YuvToRgbParameters parameters) - where TOperator : struct, IYuvToRgbOperator + /// The encoded sample type. + /// The narrowing and storage operations for the sample type. + /// The normalized source values. + /// The encoded destination samples. + /// The encoded range scale. + /// The encoded range bias. + /// The largest encoded sample value. + private static void WriteSamples(ReadOnlySpan source, Span destination, float scale, float bias, float maximum) + where TSample : unmanaged + where TStorer : struct, ISampleStorer { - ref float redBase = ref MemoryMarshal.GetReference(red); - ref float greenBase = ref MemoryMarshal.GetReference(green); - ref float blueBase = ref MemoryMarshal.GetReference(blue); - int length = red.Length; + ref float sourceBase = ref MemoryMarshal.GetReference(source); + ref TSample destinationBase = ref MemoryMarshal.GetReference(destination); + int length = destination.Length; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector512.Count; + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + Vector512 values = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i)); + TStorer.Store(ScaleBiasRoundAndClampToInt32(values, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 values = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i)); + TStorer.Store(ScaleBiasRoundAndClampToInt32(values, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + Vector128 values = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i)); + TStorer.Store(ScaleBiasRoundAndClampToInt32(values, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i)); + } + } + + for (; i < length; i++) + { + Unsafe.Add(ref destinationBase, i) = ToSample((Unsafe.Add(ref sourceBase, i) * scale) + bias, maximum); + } + } + + /// + /// Averages one or two planar rows into horizontally subsampled encoded samples. + /// + /// The encoded sample type. + /// The narrowing and storage operations for the sample type. + /// The first normalized source row. + /// The optional second normalized source row. + /// The encoded subsampled destination row. + /// The encoded range scale. + /// The encoded range bias. + /// The largest encoded sample value. + private static void WriteSubsampledSamples( + ReadOnlySpan row0, + ReadOnlySpan row1, + Span destination, + float scale, + float bias, + float maximum) + where TSample : unmanaged + where TStorer : struct, ISampleStorer + { + ref float row0Base = ref MemoryMarshal.GetReference(row0); + ref float row1Base = ref MemoryMarshal.GetReference(row1); + ref TSample destinationBase = ref MemoryMarshal.GetReference(destination); + bool hasSecondRow = !row1.IsEmpty; + float averageScale = hasSecondRow ? 0.25F : 0.5F; + int length = row0.Length; int i = 0; if (Vector512.IsHardwareAccelerated) { - Vector512 lumaBias = Vector512.Create(parameters.LumaBias); - Vector512 inverseLumaScale = Vector512.Create(1F / parameters.LumaScale); - Vector512 chromaBias = Vector512.Create(parameters.ChromaBias); - Vector512 inverseChromaScale = Vector512.Create(1F / parameters.ChromaScale); + Vector512 evenOdd = Vector512.Create(0, 2, 4, 6, 8, 10, 12, 14, 1, 3, 5, 7, 9, 11, 13, 15); int oneVectorFromEnd = length - Vector512.Count; for (; i <= oneVectorFromEnd; i += Vector512.Count) { - ref Vector512 redVector = ref Unsafe.As>(ref Unsafe.Add(ref redBase, i)); - Vector512 y = (redVector - lumaBias) * inverseLumaScale; - Vector512 cb = default; - Vector512 cr = default; - if (TOperator.UsesChroma) + Vector512 sum = Unsafe.As>(ref Unsafe.Add(ref row0Base, i)); + if (hasSecondRow) { - cb = (Unsafe.As>(ref Unsafe.Add(ref greenBase, i)) - chromaBias) * inverseChromaScale; - cr = (Unsafe.As>(ref Unsafe.Add(ref blueBase, i)) - chromaBias) * inverseChromaScale; + sum += Unsafe.As>(ref Unsafe.Add(ref row1Base, i)); } - TOperator.Convert(ref y, ref cb, ref cr, in parameters); - redVector = y; - Unsafe.As>(ref Unsafe.Add(ref greenBase, i)) = cb; - Unsafe.As>(ref Unsafe.Add(ref blueBase, i)) = cr; + Vector512 shuffled = Vector512.Shuffle(sum, evenOdd); + Vector256 average = (shuffled.GetLower() + shuffled.GetUpper()) * Vector256.Create(averageScale); + TStorer.Store(ScaleBiasRoundAndClampToInt32(average, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i >> 1)); } } if (Vector256.IsHardwareAccelerated) { - Vector256 lumaBias = Vector256.Create(parameters.LumaBias); - Vector256 inverseLumaScale = Vector256.Create(1F / parameters.LumaScale); - Vector256 chromaBias = Vector256.Create(parameters.ChromaBias); - Vector256 inverseChromaScale = Vector256.Create(1F / parameters.ChromaScale); + Vector256 evenOdd = Vector256.Create(0, 2, 4, 6, 1, 3, 5, 7); int oneVectorFromEnd = length - Vector256.Count; for (; i <= oneVectorFromEnd; i += Vector256.Count) { - ref Vector256 redVector = ref Unsafe.As>(ref Unsafe.Add(ref redBase, i)); - Vector256 y = (redVector - lumaBias) * inverseLumaScale; - Vector256 cb = default; - Vector256 cr = default; - if (TOperator.UsesChroma) + Vector256 sum = Unsafe.As>(ref Unsafe.Add(ref row0Base, i)); + if (hasSecondRow) { - cb = (Unsafe.As>(ref Unsafe.Add(ref greenBase, i)) - chromaBias) * inverseChromaScale; - cr = (Unsafe.As>(ref Unsafe.Add(ref blueBase, i)) - chromaBias) * inverseChromaScale; + sum += Unsafe.As>(ref Unsafe.Add(ref row1Base, i)); } - TOperator.Convert(ref y, ref cb, ref cr, in parameters); - redVector = y; - Unsafe.As>(ref Unsafe.Add(ref greenBase, i)) = cb; - Unsafe.As>(ref Unsafe.Add(ref blueBase, i)) = cr; + Vector256 shuffled = Vector256.Shuffle(sum, evenOdd); + Vector128 average = (shuffled.GetLower() + shuffled.GetUpper()) * Vector128.Create(averageScale); + TStorer.Store(ScaleBiasRoundAndClampToInt32(average, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i >> 1)); } } if (Vector128.IsHardwareAccelerated) { - Vector128 lumaBias = Vector128.Create(parameters.LumaBias); - Vector128 inverseLumaScale = Vector128.Create(1F / parameters.LumaScale); - Vector128 chromaBias = Vector128.Create(parameters.ChromaBias); - Vector128 inverseChromaScale = Vector128.Create(1F / parameters.ChromaScale); - int oneVectorFromEnd = length - Vector128.Count; - for (; i <= oneVectorFromEnd; i += Vector128.Count) + Vector128 evenOdd = Vector128.Create(0, 2, 1, 3); + int twoVectorsFromEnd = length - (Vector128.Count * 2); + for (; i <= twoVectorsFromEnd; i += Vector128.Count * 2) { - ref Vector128 redVector = ref Unsafe.As>(ref Unsafe.Add(ref redBase, i)); - Vector128 y = (redVector - lumaBias) * inverseLumaScale; - Vector128 cb = default; - Vector128 cr = default; - if (TOperator.UsesChroma) + Vector128 sum0 = Unsafe.As>(ref Unsafe.Add(ref row0Base, i)); + Vector128 sum1 = Unsafe.As>(ref Unsafe.Add(ref row0Base, i + Vector128.Count)); + if (hasSecondRow) { - cb = (Unsafe.As>(ref Unsafe.Add(ref greenBase, i)) - chromaBias) * inverseChromaScale; - cr = (Unsafe.As>(ref Unsafe.Add(ref blueBase, i)) - chromaBias) * inverseChromaScale; + sum0 += Unsafe.As>(ref Unsafe.Add(ref row1Base, i)); + sum1 += Unsafe.As>(ref Unsafe.Add(ref row1Base, i + Vector128.Count)); } - TOperator.Convert(ref y, ref cb, ref cr, in parameters); - redVector = y; - Unsafe.As>(ref Unsafe.Add(ref greenBase, i)) = cb; - Unsafe.As>(ref Unsafe.Add(ref blueBase, i)) = cr; + Vector128 shuffled0 = Vector128.Shuffle(sum0, evenOdd); + Vector128 shuffled1 = Vector128.Shuffle(sum1, evenOdd); + Vector64 average0 = (shuffled0.GetLower() + shuffled0.GetUpper()) * Vector64.Create(averageScale); + Vector64 average1 = (shuffled1.GetLower() + shuffled1.GetUpper()) * Vector64.Create(averageScale); + Vector128 average = Vector128.Create(average0, average1); + TStorer.Store(ScaleBiasRoundAndClampToInt32(average, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i >> 1)); } } - for (; i < length; i++) + for (; i < length; i += 2) { - float y = (Unsafe.Add(ref redBase, i) - parameters.LumaBias) / parameters.LumaScale; - float cb = TOperator.UsesChroma ? (Unsafe.Add(ref greenBase, i) - parameters.ChromaBias) / parameters.ChromaScale : 0F; - float cr = TOperator.UsesChroma ? (Unsafe.Add(ref blueBase, i) - parameters.ChromaBias) / parameters.ChromaScale : 0F; - TOperator.Convert(ref y, ref cb, ref cr, in parameters); - Unsafe.Add(ref redBase, i) = y; - Unsafe.Add(ref greenBase, i) = cb; - Unsafe.Add(ref blueBase, i) = cr; + int columnCount = Math.Min(2, length - i); + float sum = Unsafe.Add(ref row0Base, i); + if (columnCount == 2) + { + sum += Unsafe.Add(ref row0Base, i + 1); + } + + if (hasSecondRow) + { + sum += Unsafe.Add(ref row1Base, i); + if (columnCount == 2) + { + sum += Unsafe.Add(ref row1Base, i + 1); + } + } + + float sampleCount = columnCount * (hasSecondRow ? 2 : 1); + Unsafe.Add(ref destinationBase, i >> 1) = ToSample(((sum / sampleCount) * scale) + bias, maximum); } } @@ -643,6 +732,51 @@ internal static partial class Av1YuvConverter } } + /// + /// Applies the encoded component range and converts four lanes to bounded integer samples. + /// + /// The normalized component values. + /// The encoded range scale. + /// The encoded range bias. + /// The largest encoded sample value. + /// The bounded integer samples. + private static Vector128 ScaleBiasRoundAndClampToInt32(Vector128 value, float scale, float bias, float maximum) + { + Vector128 encoded = (value * Vector128.Create(scale)) + Vector128.Create(bias); + Vector128 bounded = Vector128.Clamp(encoded, Vector128.Zero, Vector128.Create(maximum)); + return Vector128.ConvertToInt32(Vector128.Round(bounded, MidpointRounding.AwayFromZero)); + } + + /// + /// Applies the encoded component range and converts eight lanes to bounded integer samples. + /// + /// The normalized component values. + /// The encoded range scale. + /// The encoded range bias. + /// The largest encoded sample value. + /// The bounded integer samples. + private static Vector256 ScaleBiasRoundAndClampToInt32(Vector256 value, float scale, float bias, float maximum) + { + Vector256 encoded = (value * Vector256.Create(scale)) + Vector256.Create(bias); + Vector256 bounded = Vector256.Clamp(encoded, Vector256.Zero, Vector256.Create(maximum)); + return Vector256.ConvertToInt32(Vector256.Round(bounded, MidpointRounding.AwayFromZero)); + } + + /// + /// Applies the encoded component range and converts sixteen lanes to bounded integer samples. + /// + /// The normalized component values. + /// The encoded range scale. + /// The encoded range bias. + /// The largest encoded sample value. + /// The bounded integer samples. + private static Vector512 ScaleBiasRoundAndClampToInt32(Vector512 value, float scale, float bias, float maximum) + { + Vector512 encoded = (value * Vector512.Create(scale)) + Vector512.Create(bias); + Vector512 bounded = Vector512.Clamp(encoded, Vector512.Zero, Vector512.Create(maximum)); + return Vector512.ConvertToInt32(Vector512.Round(bounded, MidpointRounding.AwayFromZero)); + } + /// /// Scales, rounds, and clamps four normalized components to integer storage values. /// @@ -787,4 +921,63 @@ internal static partial class Av1YuvConverter return Vector512.ConvertToSingle(Vector512.Create(lower32, upper32)); } } + + /// + /// Narrows encoded integer lanes to eight-bit samples. + /// + private readonly struct ByteSampleStorer : ISampleStorer + { + /// + public static void Store(Vector128 source, ref byte destination) + { + Vector128 samples16 = Vector128.Narrow(source.AsUInt32(), Vector128.Zero); + Vector128 samples8 = Vector128.Narrow(samples16, Vector128.Zero); + + // The lower four bytes contain the four source lanes after the two narrowing stages. + Unsafe.WriteUnaligned(ref destination, samples8.AsUInt32().ToScalar()); + } + + /// + public static void Store(Vector256 source, ref byte destination) + { + Store(source.GetLower(), ref destination); + Store(source.GetUpper(), ref Unsafe.Add(ref destination, Vector128.Count)); + } + + /// + public static void Store(Vector512 source, ref byte destination) + { + Store(source.GetLower(), ref destination); + Store(source.GetUpper(), ref Unsafe.Add(ref destination, Vector256.Count)); + } + } + + /// + /// Narrows encoded integer lanes to unsigned 16-bit samples. + /// + private readonly struct UShortSampleStorer : ISampleStorer + { + /// + public static void Store(Vector128 source, ref ushort destination) + { + Vector128 samples = Vector128.Narrow(source.AsUInt32(), Vector128.Zero); + + // The lower four UInt16 values are contiguous and can be committed with one unaligned store. + Unsafe.WriteUnaligned(ref Unsafe.As(ref destination), samples.AsUInt64().ToScalar()); + } + + /// + public static void Store(Vector256 source, ref ushort destination) + { + Store(source.GetLower(), ref destination); + Store(source.GetUpper(), ref Unsafe.Add(ref destination, Vector128.Count)); + } + + /// + public static void Store(Vector512 source, ref ushort destination) + { + Store(source.GetLower(), ref destination); + Store(source.GetUpper(), ref Unsafe.Add(ref destination, Vector256.Count)); + } + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs index d2c994503..e5da2540a 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs @@ -26,42 +26,6 @@ internal static partial class Av1YuvConverter /// private const float UShortMaximum = ushort.MaxValue; - /// - /// Identifies the matrix operation used between encoded planes and RGB components. - /// - private enum ConversionMode - { - /// - /// A coefficient-based YCbCr matrix conversion. - /// - Coefficients, - - /// - /// Direct G, B, and R component mapping from the Y, U, and V planes. - /// - Identity, - - /// - /// The reversible-style YCgCo color transform. - /// - YCgCo, - - /// - /// The SMPTE ST 2085 YDzDx color transform. - /// - Smpte2085, - - /// - /// A constant-luminance transform using the signaled transfer characteristics. - /// - ConstantLuminance, - - /// - /// The BT.2100 ICtCp color transform. - /// - ICtCp, - } - /// /// Converts the reconstructed YUV planes to packed pixels. /// @@ -74,7 +38,7 @@ internal static partial class Av1YuvConverter { GetConversionParameters( frameBuffer, - out ConversionMode mode, + out Av1ColorConversionMode mode, out float kr, out float kg, out float kb, @@ -85,20 +49,41 @@ internal static partial class Av1YuvConverter out float sampleMaximum); ObuTransferCharacteristics transferCharacteristics = frameBuffer.ColorConfig.TransferCharacteristics; - ConstantLuminanceScales constantLuminanceScales = mode == ConversionMode.ConstantLuminance - ? new ConstantLuminanceScales(transferCharacteristics, kr, kb) + Av1ConstantLuminanceScales constantLuminanceScales = mode == Av1ColorConversionMode.ConstantLuminance + ? new Av1ConstantLuminanceScales(transferCharacteristics, kr, kb) : default; - YuvToRgbParameters parameters = new(kr, kg, kb, transferCharacteristics, in constantLuminanceScales, lumaBias, lumaScale, chromaBias, chromaScale); + Av1ColorConversionParameters parameters = new( + kr, + kg, + kb, + transferCharacteristics, + in constantLuminanceScales, + lumaBias, + lumaScale, + chromaBias, + chromaScale); + + Av1ColorConverterBase colorConverter = Av1ColorConverterBase.Create(mode, in parameters, frameBuffer.ColorFormat == Av1ColorFormat.Yuv400); if (frameBuffer.BitDepth == Av1BitDepth.EightBit) { - YuvToRgbRowOperation operation = new(configuration, frameBuffer, image, mode, in parameters); - ParallelRowIterator.IterateRows, float>(configuration, image.Bounds, in operation); + YuvToRgbRowConverter converter = new(configuration, frameBuffer, image, colorConverter); + using IMemoryOwner owner = configuration.MemoryAllocator.Allocate(converter.BufferLength); + Span scratch = owner.GetSpan(); + for (int y = 0; y < image.Height; y++) + { + converter.Convert(y, scratch); + } } else { - YuvToRgbRowOperation operation = new(configuration, frameBuffer, image, mode, in parameters); - ParallelRowIterator.IterateRows, float>(configuration, image.Bounds, in operation); + YuvToRgbRowConverter converter = new(configuration, frameBuffer, image, colorConverter); + using IMemoryOwner owner = configuration.MemoryAllocator.Allocate(converter.BufferLength); + Span scratch = owner.GetSpan(); + for (int y = 0; y < image.Height; y++) + { + converter.Convert(y, scratch); + } } } @@ -114,7 +99,7 @@ internal static partial class Av1YuvConverter { GetConversionParameters( frameBuffer, - out ConversionMode mode, + out Av1ColorConversionMode mode, out float kr, out float kg, out float kb, @@ -125,165 +110,45 @@ internal static partial class Av1YuvConverter out float sampleMaximum); ObuTransferCharacteristics transferCharacteristics = frameBuffer.ColorConfig.TransferCharacteristics; - ConstantLuminanceScales constantLuminanceScales = mode == ConversionMode.ConstantLuminance - ? new ConstantLuminanceScales(transferCharacteristics, kr, kb) + Av1ConstantLuminanceScales constantLuminanceScales = mode == Av1ColorConversionMode.ConstantLuminance + ? new Av1ConstantLuminanceScales(transferCharacteristics, kr, kb) : default; - bool isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400; - int subX = frameBuffer.ColorConfig.SubSamplingX ? 1 : 0; - int subY = frameBuffer.ColorConfig.SubSamplingY ? 1 : 0; - Buffer2DRegion yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0); - Buffer2DRegion uPlane = isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.U, subX, subY); - Buffer2DRegion vPlane = isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.V, subX, subY); - int sourceRowsPerIteration = !isMonochrome && subY != 0 ? 2 : 1; - bool isEightBit = frameBuffer.BitDepth == Av1BitDepth.EightBit; - int rowBufferLength = image.Width * sourceRowsPerIteration; - using IMemoryOwner? rowOwner = isEightBit - ? configuration.MemoryAllocator.Allocate(rowBufferLength) - : null; - - using IMemoryOwner? highBitDepthRowOwner = isEightBit - ? null - : configuration.MemoryAllocator.Allocate(rowBufferLength); + Av1ColorConversionParameters parameters = new( + kr, + kg, + kb, + transferCharacteristics, + in constantLuminanceScales, + lumaBias, + lumaScale, + chromaBias, + chromaScale); - Span rgbRow0 = rowOwner is null ? Span.Empty : rowOwner.GetSpan()[..image.Width]; - Span rgbRow1 = sourceRowsPerIteration == 2 && rowOwner is not null - ? rowOwner.GetSpan().Slice(image.Width, image.Width) - : Span.Empty; - - Span highBitDepthRgbRow0 = highBitDepthRowOwner is null - ? Span.Empty - : highBitDepthRowOwner.GetSpan()[..image.Width]; - - Span highBitDepthRgbRow1 = sourceRowsPerIteration == 2 && highBitDepthRowOwner is not null - ? highBitDepthRowOwner.GetSpan().Slice(image.Width, image.Width) - : Span.Empty; - - for (int y = 0; y < image.Height; y += sourceRowsPerIteration) + bool isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400; + Av1ColorConverterBase colorConverter = Av1ColorConverterBase.Create(mode, in parameters, isMonochrome); + int rowShift = !isMonochrome && frameBuffer.ColorConfig.SubSamplingY ? 1 : 0; + int iterationCount = (image.Height + rowShift) >> rowShift; + if (frameBuffer.BitDepth == Av1BitDepth.EightBit) { - if (isEightBit) + RgbToYuvRowConverter converter = new(configuration, frameBuffer, image, colorConverter, sampleMaximum); + using IMemoryOwner componentOwner = configuration.MemoryAllocator.Allocate(converter.ComponentBufferLength); + Span components = componentOwner.GetSpan(); + for (int y = 0; y < iterationCount; y++) { - PixelOperations.Instance.ToRgb24( - configuration, - image.PixelBuffer.DangerousGetRowSpan(y), - rgbRow0); + converter.Convert(y, Span.Empty, components); } - else - { - // Rgb48 retains source component precision before the values are quantized to the requested - // 10-bit or 12-bit AV1 sample range. - PixelOperations.Instance.ToRgb48( - configuration, - image.PixelBuffer.DangerousGetRowSpan(y), - highBitDepthRgbRow0); - } - - bool hasSecondSourceRow = sourceRowsPerIteration == 2 && y + 1 < image.Height; - if (hasSecondSourceRow) - { - if (isEightBit) - { - PixelOperations.Instance.ToRgb24( - configuration, - image.PixelBuffer.DangerousGetRowSpan(y + 1), - rgbRow1); - } - else - { - PixelOperations.Instance.ToRgb48( - configuration, - image.PixelBuffer.DangerousGetRowSpan(y + 1), - highBitDepthRgbRow1); - } - } - - if (isEightBit) - { - Span yRow0 = yPlane.DangerousGetRowSpan(y); - if (isMonochrome || subX == 0) - { - ConvertRgbToYuvRow( - rgbRow0, - yRow0, - isMonochrome ? Span.Empty : uPlane.DangerousGetRowSpan(y), - isMonochrome ? Span.Empty : vPlane.DangerousGetRowSpan(y), - mode, - kr, - kg, - kb, - transferCharacteristics, - in constantLuminanceScales, - lumaBias, - lumaScale, - chromaBias, - chromaScale, - sampleMaximum); - } - else - { - ConvertRgbToSubsampledYuvRows( - rgbRow0, - hasSecondSourceRow ? rgbRow1 : ReadOnlySpan.Empty, - yRow0, - hasSecondSourceRow ? yPlane.DangerousGetRowSpan(y + 1) : Span.Empty, - uPlane.DangerousGetRowSpan(y >> subY), - vPlane.DangerousGetRowSpan(y >> subY), - mode, - kr, - kg, - kb, - transferCharacteristics, - in constantLuminanceScales, - lumaBias, - lumaScale, - chromaBias, - chromaScale, - sampleMaximum); - } - } - else + } + else + { + RgbToYuvRowConverter converter = new(configuration, frameBuffer, image, colorConverter, sampleMaximum); + using IMemoryOwner packedOwner = configuration.MemoryAllocator.Allocate(image.Width); + using IMemoryOwner componentOwner = configuration.MemoryAllocator.Allocate(converter.ComponentBufferLength); + Span packed = packedOwner.GetSpan()[..image.Width]; + Span components = componentOwner.GetSpan(); + for (int y = 0; y < iterationCount; y++) { - Span yRow0 = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, y, 0, 0); - if (isMonochrome || subX == 0) - { - ConvertRgbToYuvRow( - highBitDepthRgbRow0, - yRow0, - isMonochrome ? Span.Empty : frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y, 0, 0), - isMonochrome ? Span.Empty : frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y, 0, 0), - mode, - kr, - kg, - kb, - transferCharacteristics, - in constantLuminanceScales, - lumaBias, - lumaScale, - chromaBias, - chromaScale, - sampleMaximum); - } - else - { - ConvertRgbToSubsampledYuvRows( - highBitDepthRgbRow0, - hasSecondSourceRow ? highBitDepthRgbRow1 : ReadOnlySpan.Empty, - yRow0, - hasSecondSourceRow ? frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, y + 1, 0, 0) : Span.Empty, - frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y >> subY, subX, subY), - frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y >> subY, subX, subY), - mode, - kr, - kg, - kb, - transferCharacteristics, - in constantLuminanceScales, - lumaBias, - lumaScale, - chromaBias, - chromaScale, - sampleMaximum); - } + converter.Convert(y, packed, components); } } } @@ -303,7 +168,7 @@ internal static partial class Av1YuvConverter /// The largest encoded sample value. private static void GetConversionParameters( Av1FrameBuffer frameBuffer, - out ConversionMode mode, + out Av1ColorConversionMode mode, out float kr, out float kg, out float kb, @@ -313,7 +178,7 @@ internal static partial class Av1YuvConverter out float chromaScale, out float sampleMaximum) { - mode = ConversionMode.Coefficients; + mode = Av1ColorConversionMode.Coefficients; kr = 0F; kb = 0F; @@ -321,7 +186,7 @@ internal static partial class Av1YuvConverter switch (frameBuffer.ColorConfig.MatrixCoefficients) { case ObuMatrixCoefficients.Identity: - mode = ConversionMode.Identity; + mode = Av1ColorConversionMode.Identity; break; case ObuMatrixCoefficients.Bt709: kr = 0.2126F; @@ -343,29 +208,29 @@ internal static partial class Av1YuvConverter kb = 0.087F; break; case ObuMatrixCoefficients.SmpteYCgCo: - mode = ConversionMode.YCgCo; + mode = Av1ColorConversionMode.YCgCo; break; case ObuMatrixCoefficients.Bt2020NonConstantLuminance: kr = 0.2627F; kb = 0.0593F; break; case ObuMatrixCoefficients.Bt2020ConstantLuminance: - mode = ConversionMode.ConstantLuminance; + mode = Av1ColorConversionMode.ConstantLuminance; kr = 0.2627F; kb = 0.0593F; break; case ObuMatrixCoefficients.Smpte2085: - mode = ConversionMode.Smpte2085; + mode = Av1ColorConversionMode.Smpte2085; break; case ObuMatrixCoefficients.ChromaticityDerivedNonConstantLuminance: GetChromaticityDerivedCoefficients(frameBuffer.ColorConfig.ColorPrimaries, out kr, out kb); break; case ObuMatrixCoefficients.ChromaticityDerivedConstantLuminance: - mode = ConversionMode.ConstantLuminance; + mode = Av1ColorConversionMode.ConstantLuminance; GetChromaticityDerivedCoefficients(frameBuffer.ColorConfig.ColorPrimaries, out kr, out kb); break; case ObuMatrixCoefficients.Bt2100ICtCp: - mode = ConversionMode.ICtCp; + mode = Av1ColorConversionMode.ICtCp; break; default: throw new NotSupportedException($"AV1 matrix coefficients '{frameBuffer.ColorConfig.MatrixCoefficients}' are not currently supported."); @@ -374,7 +239,7 @@ internal static partial class Av1YuvConverter kg = 1F - kr - kb; bool isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400; bool isFullRange = frameBuffer.ColorConfig.ColorRange; - if (mode == ConversionMode.Identity && !isMonochrome && frameBuffer.ColorFormat != Av1ColorFormat.Yuv444) + if (mode == Av1ColorConversionMode.Identity && !isMonochrome && frameBuffer.ColorFormat != Av1ColorFormat.Yuv444) { throw new InvalidImageContentException("AV1 identity matrix coefficients require YUV 4:4:4 sampling."); } @@ -393,7 +258,7 @@ internal static partial class Av1YuvConverter // H.273 limited-range YCgCo first maps R, G, and B through the 219-code luma range, so its // difference components inherit that scale instead of the 224-code scale used by YCbCr. - chromaScale = isFullRange || mode == ConversionMode.YCgCo + chromaScale = isFullRange || mode == Av1ColorConversionMode.YCgCo ? lumaScale : 224F * depthScale; } @@ -548,227 +413,6 @@ internal static partial class Av1YuvConverter (whiteZ * ((redX * greenY) - (greenX * redY))))) / denominator; } - /// - /// Converts one YUV row to packed RGB using the resolved H.273 conversion state. - /// - /// The encoded sample type. - /// The packed RGB staging type. - /// The luma samples. - /// The upper blue-difference chroma row. - /// The lower blue-difference chroma row. - /// The upper red-difference chroma row. - /// The lower red-difference chroma row. - /// The lower chroma-row weight with a denominator of four. - /// The destination RGB pixels. - /// Whether the frame contains only luma samples. - /// The horizontal chroma subsampling shift. - /// The vertical chroma subsampling shift. - /// The spatial position of subsampled chroma. - /// The conversion mode. - /// The red luma coefficient. - /// The green luma coefficient. - /// The blue luma coefficient. - /// The signaled transfer characteristics. - /// The constant-luminance chroma scales. - /// The encoded luma bias. - /// The encoded luma range. - /// The encoded chroma midpoint. - /// The encoded chroma range. - /// The largest encoded sample value. - private static void ConvertYuvToRgbRow( - ReadOnlySpan ySource, - ReadOnlySpan uRow0, - ReadOnlySpan uRow1, - ReadOnlySpan vRow0, - ReadOnlySpan vRow1, - int y1Weight, - Span destination, - bool isMonochrome, - int subX, - int subY, - ObuChromoSamplePosition chromaSamplePosition, - ConversionMode mode, - float kr, - float kg, - float kb, - ObuTransferCharacteristics transferCharacteristics, - in ConstantLuminanceScales constantLuminanceScales, - float lumaBias, - float lumaScale, - float chromaBias, - float chromaScale, - float sampleMaximum) - where TSample : unmanaged - where TRgb : unmanaged - { - for (int x = 0; x < destination.Length; x++) - { - float y = (GetSample(ySource, x) - lumaBias) / lumaScale; - float r; - float g; - float b; - - if (isMonochrome) - { - r = y; - g = y; - b = y; - } - else - { - float u = SampleChroma(uRow0, uRow1, x, subX, subY, chromaSamplePosition, y1Weight); - float v = SampleChroma(vRow0, vRow1, x, subX, subY, chromaSamplePosition, y1Weight); - float cb = (u - chromaBias) / chromaScale; - float cr = (v - chromaBias) / chromaScale; - - switch (mode) - { - case ConversionMode.Identity: - // H.273 identity coding stores the nonlinear G, B, and R signals in Y, U, and V order. - r = (v - lumaBias) / lumaScale; - g = y; - b = (u - lumaBias) / lumaScale; - break; - case ConversionMode.YCgCo: - float temporary = y - cb; - r = temporary + cr; - g = y + cb; - b = temporary - cr; - break; - case ConversionMode.Smpte2085: - // H.273 equations 76 to 78 store green as luma and use the ST 2085 scale factors for - // the blue and red difference components. - g = y; - b = ((2F * cb) + y) / 0.986566F; - r = (2F * cr) + (0.991902F * y); - break; - case ConversionMode.ConstantLuminance: - // H.273 equations 66 to 75 define luma in linear light, while the stored luma and - // difference signals remain nonlinear. Reconstruct red and blue before solving green. - float nonlinearBlue = y + - (2F * (cb <= 0F ? constantLuminanceScales.NegativeBlue : constantLuminanceScales.PositiveBlue) * cb); - - float nonlinearRed = y + - (2F * (cr <= 0F ? constantLuminanceScales.NegativeRed : constantLuminanceScales.PositiveRed) * cr); - - float linearY = Av1TransferFunctions.ToLinear(transferCharacteristics, y); - float linearBlue = Av1TransferFunctions.ToLinear(transferCharacteristics, nonlinearBlue); - float linearRed = Av1TransferFunctions.ToLinear(transferCharacteristics, nonlinearRed); - float linearGreen = (linearY - (kr * linearRed) - (kb * linearBlue)) / kg; - - r = nonlinearRed; - g = Av1TransferFunctions.ToGamma(transferCharacteristics, linearGreen); - b = nonlinearBlue; - break; - case ConversionMode.ICtCp: - float nonlinearL; - float nonlinearM; - float nonlinearS; - if (transferCharacteristics == ObuTransferCharacteristics.Hlg) - { - // This is the exact inverse of H.273 equations 82 to 84. The first column is one - // because intensity is defined as the average of the L and M components. - nonlinearL = y + (0.015718580108730413F * cb) + (0.2095810681164055F * cr); - nonlinearM = y - (0.015718580108730413F * cb) - (0.2095810681164055F * cr); - nonlinearS = y + (1.0212710798422342F * cb) - (0.6052744909924315F * cr); - } - else - { - // H.273 equations 79 to 81 are the ICtCp matrix selected for PQ and every transfer - // code other than HLG. These constants are the exact inverse of its integer matrix. - nonlinearL = y + (0.008609037037932756F * cb) + (0.11102962500302596F * cr); - nonlinearM = y - (0.008609037037932756F * cb) - (0.11102962500302596F * cr); - nonlinearS = y + (0.5600313357106791F * cb) - (0.32062717498731885F * cr); - } - - float linearL = Av1TransferFunctions.ToLinear(transferCharacteristics, nonlinearL); - float linearM = Av1TransferFunctions.ToLinear(transferCharacteristics, nonlinearM); - float linearS = Av1TransferFunctions.ToLinear(transferCharacteristics, nonlinearS); - - // This cofactor inverse of H.273 equations 14 to 16 recovers linear RGB from LMS. - // Applying the transfer curve last returns the nonlinear RGB values stored by ImageSharp. - float ictcpLinearRed = - (3.4366066943330784F * linearL) - - (2.50645211865627F * linearM) + - (0.06984542432319148F * linearS); - - float ictcpLinearGreen = - (-0.7913295555989287F * linearL) + - (1.9836004517922907F * linearM) - - (0.192270896193362F * linearS); - - float ictcpLinearBlue = - (-0.025949899690592672F * linearL) - - (0.09891371471172644F * linearM) + - (1.1248636144023192F * linearS); - - r = Av1TransferFunctions.ToGamma(transferCharacteristics, ictcpLinearRed); - g = Av1TransferFunctions.ToGamma(transferCharacteristics, ictcpLinearGreen); - b = Av1TransferFunctions.ToGamma(transferCharacteristics, ictcpLinearBlue); - break; - default: - r = y + (2F * (1F - kr) * cr); - g = y - (2F * ((kr * (1F - kr) * cr) + (kb * (1F - kb) * cb)) / kg); - b = y + (2F * (1F - kb) * cb); - break; - } - } - - // The generic staging type is controlled by the frame bit depth. The JIT removes the inactive branch, - // retaining direct component access without routing every pixel through Vector4 or interface dispatch. - if (typeof(TRgb) == typeof(Rgb24)) - { - Rgb24 pixel = new( - ToSample(r * ByteMaximum, ByteMaximum), - ToSample(g * ByteMaximum, ByteMaximum), - ToSample(b * ByteMaximum, ByteMaximum)); - - destination[x] = Unsafe.As(ref pixel); - } - else - { - Rgb48 pixel = new( - ToSample(r * UShortMaximum, UShortMaximum), - ToSample(g * UShortMaximum, UShortMaximum), - ToSample(b * UShortMaximum, UShortMaximum)); - - destination[x] = Unsafe.As(ref pixel); - } - } - } - - /// - /// Bilinearly reconstructs a chroma sample at a luma coordinate. - /// - /// The encoded sample type. - /// The upper chroma row. - /// The lower chroma row. - /// The luma column coordinate. - /// The horizontal chroma subsampling shift. - /// The vertical chroma subsampling shift. - /// The spatial position of subsampled chroma. - /// The lower chroma-row weight with a denominator of four. - /// The reconstructed encoded chroma sample. - private static float SampleChroma( - ReadOnlySpan row0, - ReadOnlySpan row1, - int x, - int subX, - int subY, - ObuChromoSamplePosition chromaSamplePosition, - int y1Weight) - where TSample : unmanaged - { - // Unknown 4:2:0 and all 4:2:2 input use the centered convention employed by libavif. - bool isCenteredX = subX != 0 && (subY == 0 || chromaSamplePosition == ObuChromoSamplePosition.Unknown); - GetChromaCoordinates(x, subX, isCenteredX, row0.Length - 1, out int x0, out int x1, out int x1Weight); - - float top = (GetSample(row0, x0) * (4 - x1Weight)) + (GetSample(row0, x1) * x1Weight); - float bottom = (GetSample(row1, x0) * (4 - x1Weight)) + (GetSample(row1, x1) * x1Weight); - - return ((top * (4 - y1Weight)) + (bottom * y1Weight)) / 16F; - } - /// /// Resolves the two chroma samples and quarter-sample weight surrounding a luma coordinate. /// @@ -811,287 +455,6 @@ internal static partial class Av1YuvConverter upperWeight = isOdd ? 2 : 0; } - /// - /// Converts one packed RGB row to luma and optional full-resolution chroma using the resolved H.273 conversion state. - /// - /// The encoded sample type. - /// The packed RGB staging type. - /// The source RGB pixels. - /// The destination luma samples. - /// The destination blue-difference chroma samples. - /// The destination red-difference chroma samples. - /// The conversion mode. - /// The red luma coefficient. - /// The green luma coefficient. - /// The blue luma coefficient. - /// The signaled transfer characteristics. - /// The constant-luminance chroma scales. - /// The encoded luma bias. - /// The encoded luma range. - /// The encoded chroma midpoint. - /// The encoded chroma range. - /// The largest encoded sample value. - private static void ConvertRgbToYuvRow( - ReadOnlySpan source, - Span yDestination, - Span uDestination, - Span vDestination, - ConversionMode mode, - float kr, - float kg, - float kb, - ObuTransferCharacteristics transferCharacteristics, - in ConstantLuminanceScales constantLuminanceScales, - float lumaBias, - float lumaScale, - float chromaBias, - float chromaScale, - float sampleMaximum) - where TSample : unmanaged - where TRgb : unmanaged - { - for (int x = 0; x < source.Length; x++) - { - ConvertRgbToYuv( - source[x], - mode, - kr, - kg, - kb, - transferCharacteristics, - in constantLuminanceScales, - out float y, - out float cb, - out float cr); - - yDestination[x] = ToSample((y * lumaScale) + lumaBias, sampleMaximum); - if (!uDestination.IsEmpty) - { - if (mode == ConversionMode.Identity) - { - uDestination[x] = ToSample((cb * lumaScale) + lumaBias, sampleMaximum); - vDestination[x] = ToSample((cr * lumaScale) + lumaBias, sampleMaximum); - } - else - { - uDestination[x] = ToSample((cb * chromaScale) + chromaBias, sampleMaximum); - vDestination[x] = ToSample((cr * chromaScale) + chromaBias, sampleMaximum); - } - } - } - } - - /// - /// Converts one or two packed RGB rows to luma and horizontally subsampled chroma. - /// - /// The encoded sample type. - /// The packed RGB staging type. - /// The first source row. - /// The optional second source row for 4:2:0 conversion. - /// The first destination luma row. - /// The optional second destination luma row. - /// The destination blue-difference chroma row. - /// The destination red-difference chroma row. - /// The conversion mode. - /// The red luma coefficient. - /// The green luma coefficient. - /// The blue luma coefficient. - /// The signaled transfer characteristics. - /// The constant-luminance chroma scales. - /// The encoded luma bias. - /// The encoded luma range. - /// The encoded chroma midpoint. - /// The encoded chroma range. - /// The largest encoded sample value. - private static void ConvertRgbToSubsampledYuvRows( - ReadOnlySpan sourceRow0, - ReadOnlySpan sourceRow1, - Span yDestination0, - Span yDestination1, - Span uDestination, - Span vDestination, - ConversionMode mode, - float kr, - float kg, - float kb, - ObuTransferCharacteristics transferCharacteristics, - in ConstantLuminanceScales constantLuminanceScales, - float lumaBias, - float lumaScale, - float chromaBias, - float chromaScale, - float sampleMaximum) - where TSample : unmanaged - where TRgb : unmanaged - { - int rowCount = sourceRow1.IsEmpty ? 1 : 2; - for (int x = 0; x < sourceRow0.Length; x += 2) - { - int columnCount = Math.Min(2, sourceRow0.Length - x); - float cbSum = 0F; - float crSum = 0F; - for (int row = 0; row < rowCount; row++) - { - ReadOnlySpan source = row == 0 ? sourceRow0 : sourceRow1; - Span yDestination = row == 0 ? yDestination0 : yDestination1; - for (int column = 0; column < columnCount; column++) - { - int sourceIndex = x + column; - ConvertRgbToYuv( - source[sourceIndex], - mode, - kr, - kg, - kb, - transferCharacteristics, - in constantLuminanceScales, - out float y, - out float cb, - out float cr); - - yDestination[sourceIndex] = ToSample((y * lumaScale) + lumaBias, sampleMaximum); - cbSum += cb; - crSum += cr; - } - } - - // libavif's scalar average path divides by the actual edge-block dimensions, so odd widths and heights - // do not replicate a missing RGB sample into the chroma average. - float sampleCount = columnCount * rowCount; - float cbAverage = cbSum / sampleCount; - float crAverage = crSum / sampleCount; - int chromaIndex = x >> 1; - uDestination[chromaIndex] = ToSample((cbAverage * chromaScale) + chromaBias, sampleMaximum); - vDestination[chromaIndex] = ToSample((crAverage * chromaScale) + chromaBias, sampleMaximum); - } - } - - /// - /// Converts one packed RGB pixel to normalized luma and chroma values. - /// - /// The packed RGB staging type. - /// The source RGB pixel. - /// The conversion mode. - /// The red luma coefficient. - /// The green luma coefficient. - /// The blue luma coefficient. - /// The signaled transfer characteristics. - /// The constant-luminance chroma scales. - /// The normalized luma result. - /// The normalized blue-difference chroma result. - /// The normalized red-difference chroma result. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ConvertRgbToYuv( - TRgb pixel, - ConversionMode mode, - float kr, - float kg, - float kb, - ObuTransferCharacteristics transferCharacteristics, - in ConstantLuminanceScales constantLuminanceScales, - out float y, - out float cb, - out float cr) - where TRgb : unmanaged - { - float r; - float g; - float b; - - // These are the only staging formats selected by the owning conversion methods. Keeping the format choice - // generic lets the JIT specialize the hot loop and preserves high-bit-depth input without boxing or copies. - if (typeof(TRgb) == typeof(Rgb24)) - { - Rgb24 rgb24 = Unsafe.As(ref pixel); - r = rgb24.R / ByteMaximum; - g = rgb24.G / ByteMaximum; - b = rgb24.B / ByteMaximum; - } - else - { - Rgb48 rgb48 = Unsafe.As(ref pixel); - r = rgb48.R / UShortMaximum; - g = rgb48.G / UShortMaximum; - b = rgb48.B / UShortMaximum; - } - - switch (mode) - { - case ConversionMode.Identity: - // H.273 identity coding stores the nonlinear G, B, and R signals in Y, U, and V order. - y = g; - cb = b; - cr = r; - break; - case ConversionMode.YCgCo: - y = (0.5F * g) + (0.25F * (r + b)); - cb = (0.5F * g) - (0.25F * (r + b)); - cr = 0.5F * (r - b); - break; - case ConversionMode.Smpte2085: - // ST 2085 uses green directly as luma, so this path must remain separate from Kr/Kb YCbCr. - y = g; - cb = ((0.986566F * b) - y) * 0.5F; - cr = (r - (0.991902F * y)) * 0.5F; - break; - case ConversionMode.ConstantLuminance: - // The packed RGB values are nonlinear signal components. H.273 constant luminance derives Y - // after applying the inverse transfer curve to each component. - float linearRed = Av1TransferFunctions.ToLinear(transferCharacteristics, r); - float linearGreen = Av1TransferFunctions.ToLinear(transferCharacteristics, g); - float linearBlue = Av1TransferFunctions.ToLinear(transferCharacteristics, b); - float linearY = (kr * linearRed) + (kg * linearGreen) + (kb * linearBlue); - y = Av1TransferFunctions.ToGamma(transferCharacteristics, linearY); - - float blueDifference = b - y; - float redDifference = r - y; - cb = blueDifference / - (2F * (blueDifference <= 0F ? constantLuminanceScales.NegativeBlue : constantLuminanceScales.PositiveBlue)); - - cr = redDifference / - (2F * (redDifference <= 0F ? constantLuminanceScales.NegativeRed : constantLuminanceScales.PositiveRed)); - - break; - case ConversionMode.ICtCp: - float ictcpLinearRed = Av1TransferFunctions.ToLinear(transferCharacteristics, r); - float ictcpLinearGreen = Av1TransferFunctions.ToLinear(transferCharacteristics, g); - float ictcpLinearBlue = Av1TransferFunctions.ToLinear(transferCharacteristics, b); - - // H.273 equations 14 to 16 convert linear BT.2100 RGB into the LMS cone-response domain - // before the signaled transfer curve is applied to each component. - float nonlinearL = Av1TransferFunctions.ToGamma( - transferCharacteristics, - ((1688F * ictcpLinearRed) + (2146F * ictcpLinearGreen) + (262F * ictcpLinearBlue)) / 4096F); - - float nonlinearM = Av1TransferFunctions.ToGamma( - transferCharacteristics, - ((683F * ictcpLinearRed) + (2951F * ictcpLinearGreen) + (462F * ictcpLinearBlue)) / 4096F); - - float nonlinearS = Av1TransferFunctions.ToGamma( - transferCharacteristics, - ((99F * ictcpLinearRed) + (309F * ictcpLinearGreen) + (3688F * ictcpLinearBlue)) / 4096F); - - y = 0.5F * (nonlinearL + nonlinearM); - if (transferCharacteristics == ObuTransferCharacteristics.Hlg) - { - cb = ((3625F * nonlinearL) - (7465F * nonlinearM) + (3840F * nonlinearS)) / 4096F; - cr = ((9500F * nonlinearL) - (9212F * nonlinearM) - (288F * nonlinearS)) / 4096F; - } - else - { - cb = ((6610F * nonlinearL) - (13613F * nonlinearM) + (7003F * nonlinearS)) / 4096F; - cr = ((17933F * nonlinearL) - (17390F * nonlinearM) - (543F * nonlinearS)) / 4096F; - } - - break; - default: - y = (kr * r) + (kg * g) + (kb * b); - cb = (b - y) / (2F * (1F - kb)); - cr = (r - y) / (2F * (1F - kr)); - break; - } - } - /// /// Reads an 8-bit or 16-bit unsigned sample without introducing a separate conversion buffer. /// @@ -1130,47 +493,4 @@ internal static partial class Av1YuvConverter ushort highBitDepthResult = (ushort)sample; return Unsafe.As(ref highBitDepthResult); } - - /// - /// Stores the H.273 chroma normalization constants for constant-luminance conversion. - /// - private readonly struct ConstantLuminanceScales - { - /// - /// Initializes a new instance of the struct. - /// - /// The signaled transfer characteristics. - /// The red luma coefficient. - /// The blue luma coefficient. - public ConstantLuminanceScales( - ObuTransferCharacteristics transferCharacteristics, - float kr, - float kb) - { - this.NegativeBlue = Av1TransferFunctions.ToGamma(transferCharacteristics, 1F - kb); - this.PositiveBlue = 1F - Av1TransferFunctions.ToGamma(transferCharacteristics, kb); - this.NegativeRed = Av1TransferFunctions.ToGamma(transferCharacteristics, 1F - kr); - this.PositiveRed = 1F - Av1TransferFunctions.ToGamma(transferCharacteristics, kr); - } - - /// - /// Gets the scale for a non-positive blue difference. - /// - public float NegativeBlue { get; } - - /// - /// Gets the scale for a positive blue difference. - /// - public float PositiveBlue { get; } - - /// - /// Gets the scale for a non-positive red difference. - /// - public float NegativeRed { get; } - - /// - /// Gets the scale for a positive red difference. - /// - public float PositiveRed { get; } - } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs new file mode 100644 index 000000000..54fafd1e5 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs @@ -0,0 +1,129 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; + +/// +/// Measures frame-wide AV1 YUV 4:2:0 color conversion in both directions. +/// +[MemoryDiagnoser(displayGenColumns: false)] +public class Av1ColorConversionBenchmarks +{ + /// + /// The benchmark frame width. + /// + private const int Width = 1920; + + /// + /// The benchmark frame height. + /// + private const int Height = 1080; + + /// + /// The source RGB image. + /// + private Image source = null!; + + /// + /// The destination RGB image. + /// + private Image destination = null!; + + /// + /// The reusable AV1 frame planes. + /// + private Av1FrameBuffer frameBuffer = null!; + + /// + /// Gets or sets the encoded AV1 bit depth. + /// + [Params(8, 10, 12)] + public int BitDepth { get; set; } + + /// + /// Allocates and populates deterministic full-HD RGB and YUV frames outside the measured operations. + /// + [GlobalSetup] + public void Setup() + { + this.source = new Image(Width, Height); + this.destination = new Image(Width, Height); + for (int y = 0; y < Height; y++) + { + Span row = this.source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + for (int x = 0; x < Width; x++) + { + // The relatively prime channel steps avoid uniform rows while remaining deterministic. + row[x] = new Rgb48( + (ushort)((x * 1879) + (y * 791)), + (ushort)((x * 977) + (y * 3251)), + (ushort)((x * 613) + (y * 4987))); + } + } + + ObuSequenceHeader sequenceHeader = new() + { + MaxFrameWidth = Width, + MaxFrameHeight = Height, + ColorConfig = new ObuColorConfig + { + BitDepth = this.BitDepth switch + { + 10 => Av1BitDepth.TenBit, + 12 => Av1BitDepth.TwelveBit, + _ => Av1BitDepth.EightBit, + }, + ColorPrimaries = ObuColorPrimaries.Bt709, + TransferCharacteristics = ObuTransferCharacteristics.Bt709, + MatrixCoefficients = ObuMatrixCoefficients.Bt709, + ColorRange = false, + SubSamplingX = true, + SubSamplingY = true, + ChromaSamplePosition = ObuChromoSamplePosition.Unknown, + }, + }; + + this.frameBuffer = new Av1FrameBuffer(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); + Av1YuvConverter.ConvertFromRgb(Configuration.Default, this.source.Frames.RootFrame, this.frameBuffer); + } + + /// + /// Releases the benchmark images and reconstructed planes. + /// + [GlobalCleanup] + public void Cleanup() + { + this.frameBuffer.Dispose(); + this.destination.Dispose(); + this.source.Dispose(); + } + + /// + /// Measures full-frame YUV-to-RGB conversion, including chroma reconstruction and packed-pixel conversion. + /// + /// A converted pixel that keeps the frame result observable. + [Benchmark] + public Rgb48 ConvertToRgb() + { + Av1YuvConverter.ConvertToRgb(Configuration.Default, this.frameBuffer, this.destination.Frames.RootFrame); + return this.destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(Height - 1)[Width - 1]; + } + + /// + /// Measures full-frame RGB-to-YUV conversion, including planar unpacking and chroma downsampling. + /// + /// An encoded luma sample that keeps the frame result observable. + [Benchmark] + public int ConvertFromRgb() + { + Av1YuvConverter.ConvertFromRgb(Configuration.Default, this.source.Frames.RootFrame, this.frameBuffer); + return this.BitDepth == 8 + ? this.frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(Height - 1)[Width - 1] + : this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, Height - 1, 0, 0)[Width - 1]; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs index db76d4d57..d749f39be 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs @@ -10,9 +10,23 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; +/// +/// Verifies AV1 color conversion, sample-range handling, chroma reconstruction, and encoder downsampling. +/// [Trait("Format", "Avif")] public class Av1YuvConverterTests { + /// + /// Verifies known RGB-to-YUV values across coefficient, identity, and YCgCo matrices and sample ranges. + /// + /// The source red component. + /// The source green component. + /// The source blue component. + /// The expected luma or first encoded component. + /// The expected first chroma or second encoded component. + /// The expected second chroma or third encoded component. + /// Whether the encoded samples use the full range. + /// The matrix coefficients used for conversion. [Theory] [InlineData(255, 255, 255, 255, 128, 128, true, ObuMatrixCoefficients.Bt709)] [InlineData(0, 0, 0, 0, 128, 128, true, ObuMatrixCoefficients.Bt709)] @@ -52,6 +66,17 @@ public class Av1YuvConverterTests Assert.Equal(v, actualV); } + /// + /// Verifies known YUV-to-RGB values across coefficient, identity, and YCgCo matrices and sample ranges. + /// + /// The expected red component. + /// The expected green component. + /// The expected blue component. + /// The source luma or first encoded component. + /// The source first chroma or second encoded component. + /// The source second chroma or third encoded component. + /// Whether the encoded samples use the full range. + /// The matrix coefficients used for conversion. [Theory] [InlineData(255, 255, 255, 255, 128, 128, true, ObuMatrixCoefficients.Bt709)] [InlineData(0, 0, 0, 0, 128, 128, true, ObuMatrixCoefficients.Bt709)] @@ -91,6 +116,9 @@ public class Av1YuvConverterTests Assert.Equal(b, actual.B, 1d); } + /// + /// Verifies that limited-range monochrome samples expand to the complete RGB output range. + /// [Fact] public void Yuv400ToRgbExpandsLimitedRangeLuma() { @@ -117,6 +145,48 @@ public class Av1YuvConverterTests Assert.Equal(new Rgb24(255, 255, 255), actual[1]); } + /// + /// Verifies RGB-to-monochrome conversion and range quantization for every supported AV1 bit depth. + /// + /// The encoded AV1 bit depth. + /// Whether the luma samples use the full range. + /// The expected encoded luma sample. + [Theory] + [InlineData(Av1BitDepth.EightBit, true, 107)] + [InlineData(Av1BitDepth.EightBit, false, 108)] + [InlineData(Av1BitDepth.TenBit, true, 429)] + [InlineData(Av1BitDepth.TenBit, false, 432)] + [InlineData(Av1BitDepth.TwelveBit, true, 1719)] + [InlineData(Av1BitDepth.TwelveBit, false, 1727)] + public void RgbToYuv400WritesQuantizedLuma(int bitDepth, bool fullRange, int expectedLuma) + { + // Rgb48 values scaled from eight-bit components exercise the precision-preserving high-bit-depth path. + using Image image = new(1, 1); + image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0] = new Rgb48(150 * 257, 100 * 257, 50 * 257); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + 1, + 1, + fullRange, + colorFormat: Av1ColorFormat.Yuv400, + bitDepth: (Av1BitDepth)bitDepth); + + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); + + Av1YuvConverter.ConvertFromRgb(Configuration.Default, image.Frames.RootFrame, frameBuffer); + + Assert.Equal(expectedLuma, GetPlaneSample(frameBuffer, Av1Plane.Y, 0, 0, 0, 0)); + Assert.Null(frameBuffer.BufferCb); + Assert.Null(frameBuffer.BufferCr); + } + + /// + /// Verifies full- and limited-range expansion for 10-bit and 12-bit reconstructed samples. + /// + /// The reconstructed AV1 bit depth. + /// Whether the samples use the full range. + /// The encoded black luma sample. + /// The encoded white luma sample. + /// The neutral encoded chroma sample. [Theory] [InlineData(Av1BitDepth.TenBit, true, 0, 1023, 512)] [InlineData(Av1BitDepth.TenBit, false, 64, 940, 512)] @@ -148,6 +218,10 @@ public class Av1YuvConverterTests Assert.Equal(new Rgb24(255, 255, 255), actual[1]); } + /// + /// Verifies that high-bit-depth frame strides and row access use 16-bit sample units consistently. + /// + /// The reconstructed AV1 bit depth. [Theory] [InlineData(Av1BitDepth.TenBit)] [InlineData(Av1BitDepth.TwelveBit)] @@ -175,6 +249,9 @@ public class Av1YuvConverterTests Assert.Equal(2, chromaRow.Length); } + /// + /// Verifies centered horizontal chroma reconstruction for a YUV 4:2:2 frame. + /// [Fact] public void Yuv422ToRgbBilinearlyUpsamplesCenteredChroma() { @@ -199,6 +276,12 @@ public class Av1YuvConverterTests Assert.Equal(new Rgb24(128, 116, 247), actual[3]); } + /// + /// Verifies vertical and horizontal YUV 4:2:0 reconstruction at every AV1 chroma sample position. + /// + /// The signaled AV1 chroma sample position. + /// The expected blue component in the top-row probe pixel. + /// The expected blue component in the left-column probe pixel. [Theory] [InlineData(ObuChromoSamplePosition.Unknown, 158, 98)] [InlineData(ObuChromoSamplePosition.Vertical, 187, 98)] @@ -237,6 +320,9 @@ public class Av1YuvConverterTests Assert.Equal(expectedLeftBlue, image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(1)[0].B); } + /// + /// Verifies that odd image dimensions retain the final YUV 4:2:0 chroma row and column. + /// [Fact] public void Yuv420UsesCeilingChromaPlaneDimensions() { @@ -253,6 +339,9 @@ public class Av1YuvConverterTests Assert.Equal(new Size(2, 2), vPlane.Size); } + /// + /// Compares SIMD-first RGB-to-YUV conversion with the independent scalar reference over randomized pixels. + /// [Fact] public void RgbToYuvCompareToReferenceRandomPixels() { @@ -290,6 +379,9 @@ public class Av1YuvConverterTests Compare(referenceOutput, actual, 3); } + /// + /// Compares SIMD-first YUV-to-RGB conversion with the independent scalar reference over randomized samples. + /// [Fact] public void YuvToRgbCompareToReferenceRandomPixels() { @@ -315,6 +407,12 @@ public class Av1YuvConverterTests Compare(referenceOutput, actual, 3); } + /// + /// Compares packed RGB rows within the permitted per-component tolerance. + /// + /// The independently converted reference pixels. + /// The pixels produced by the implementation under test. + /// The permitted absolute component difference. private static void Compare(Span referenceOutput, Span actual, int allowedDifference) { for (int i = 0; i < actual.Length; i++) @@ -328,6 +426,12 @@ public class Av1YuvConverterTests } } + /// + /// Fills one reconstructed plane with deterministic pseudo-random test samples. + /// + /// The deterministic random number generator. + /// The frame containing the destination plane. + /// The destination plane. private static void CreateTestData(Random rnd, Av1FrameBuffer frameBuffer, Av1Plane plane) { const int bitCount = 8; @@ -338,6 +442,12 @@ public class Av1YuvConverterTests } } + /// + /// Fills an eight-bit sample span with deterministic pseudo-random values. + /// + /// The deterministic random number generator. + /// The destination sample span. + /// The number of significant sample bits. private static void CreateTestData(Random rnd, Span span, int bitCount = 8) { int max = (1 << bitCount) - 1; @@ -348,6 +458,12 @@ public class Av1YuvConverterTests } } + /// + /// Fills a high-bit-depth sample span with deterministic pseudo-random values. + /// + /// The deterministic random number generator. + /// The destination sample span. + /// The number of significant sample bits. private static void CreateTestData(Random rnd, Span span, int bitCount) { int max = (1 << bitCount) - 1; @@ -358,6 +474,12 @@ public class Av1YuvConverterTests } } + /// + /// Verifies RGB-to-YUV-to-RGB conversion for representative single-pixel colors. + /// + /// The source red component. + /// The source green component. + /// The source blue component. [Theory] [InlineData(255, 255, 255)] [InlineData(0, 0, 0)] @@ -391,6 +513,11 @@ public class Av1YuvConverterTests Assert.Equal(b, actualPixel.B, 2d); } + /// + /// Verifies 10-bit and 12-bit round trips for coefficient, identity, and YCgCo matrices. + /// + /// The encoded AV1 bit depth. + /// The matrix coefficients used for conversion. [Theory] [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.Bt709)] [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.Identity)] @@ -429,6 +556,108 @@ public class Av1YuvConverterTests } } + /// + /// Verifies that every H.273 operator produces the same result in SIMD batches and the scalar row tail. + /// + /// The matrix coefficients selecting the color operator. + /// The transfer characteristics used by nonlinear operators. + [Theory] + [InlineData(ObuMatrixCoefficients.Bt709, ObuTransferCharacteristics.Bt709)] + [InlineData(ObuMatrixCoefficients.Identity, ObuTransferCharacteristics.Bt709)] + [InlineData(ObuMatrixCoefficients.SmpteYCgCo, ObuTransferCharacteristics.Bt709)] + [InlineData(ObuMatrixCoefficients.Bt2020ConstantLuminance, ObuTransferCharacteristics.Bt202010Bit)] + [InlineData(ObuMatrixCoefficients.Smpte2085, ObuTransferCharacteristics.Bt709)] + [InlineData(ObuMatrixCoefficients.ChromaticityDerivedNonConstantLuminance, ObuTransferCharacteristics.Bt709)] + [InlineData(ObuMatrixCoefficients.ChromaticityDerivedConstantLuminance, ObuTransferCharacteristics.Bt709)] + [InlineData(ObuMatrixCoefficients.Bt2100ICtCp, ObuTransferCharacteristics.Smpte2084)] + [InlineData(ObuMatrixCoefficients.Bt2100ICtCp, ObuTransferCharacteristics.Hlg)] + public void ColorOperatorSimdBatchesMatchScalarTail(int matrixCoefficients, int transferCharacteristics) + { + const int width = 31; + + // Thirty-one samples exercise Vector512, Vector256, Vector128, and scalar stages on AVX-512 hardware. + // The same row still reaches the widest available stages and scalar tail on narrower SIMD hardware. + using Image source = new(width, 1); + source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0).Fill(new Rgb48(39999, 27777, 12345)); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + width, + 1, + matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients, + bitDepth: Av1BitDepth.TwelveBit, + transferCharacteristics: (ObuTransferCharacteristics)transferCharacteristics, + colorPrimaries: ObuColorPrimaries.Bt2020); + + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + using Image destination = new(width, 1); + + Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer); + Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame); + + AssertPlaneContainsRepeatedSample(frameBuffer, Av1Plane.Y, 0, 0); + AssertPlaneContainsRepeatedSample(frameBuffer, Av1Plane.U, 0, 0); + AssertPlaneContainsRepeatedSample(frameBuffer, Av1Plane.V, 0, 0); + + Span pixels = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); + for (int x = 1; x < pixels.Length; x++) + { + Assert.Equal(pixels[0], pixels[x]); + } + } + + /// + /// Verifies horizontal and vertical chroma downsampling against full-resolution encoded components. + /// + /// The subsampled AV1 color format. + /// The encoded AV1 bit depth. + [Theory] + [InlineData(Av1ColorFormat.Yuv422, Av1BitDepth.EightBit)] + [InlineData(Av1ColorFormat.Yuv422, Av1BitDepth.TwelveBit)] + [InlineData(Av1ColorFormat.Yuv420, Av1BitDepth.EightBit)] + [InlineData(Av1ColorFormat.Yuv420, Av1BitDepth.TwelveBit)] + public void RgbToYuvSubsamplingAveragesFullResolutionChroma(int colorFormat, int bitDepth) + { + const int width = 35; + const int height = 3; + + using Image source = new(width, height); + for (int y = 0; y < height; y++) + { + Span row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + for (int x = 0; x < width; x++) + { + row[x] = new Rgb48( + (ushort)((x * 1879) + (y * 791)), + (ushort)((x * 977) + (y * 3251)), + (ushort)((x * 613) + (y * 4987))); + } + } + + ObuSequenceHeader fullResolutionHeader = CreateSequenceHeader( + width, + height, + colorFormat: Av1ColorFormat.Yuv444, + bitDepth: (Av1BitDepth)bitDepth); + + ObuSequenceHeader subsampledHeader = CreateSequenceHeader( + width, + height, + colorFormat: (Av1ColorFormat)colorFormat, + bitDepth: (Av1BitDepth)bitDepth); + + using Av1FrameBuffer fullResolution = new(Configuration.Default, fullResolutionHeader, Av1ColorFormat.Yuv444, false); + using Av1FrameBuffer subsampled = new(Configuration.Default, subsampledHeader, (Av1ColorFormat)colorFormat, false); + + Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, fullResolution); + Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, subsampled); + + AssertSubsampledPlaneMatchesAverage(fullResolution, subsampled, Av1Plane.U); + AssertSubsampledPlaneMatchesAverage(fullResolution, subsampled, Av1Plane.V); + } + + /// + /// Verifies an image-wide RGB-to-YUV-to-RGB conversion against the configured similarity tolerance. + /// + /// The source test-image provider. // [Theory] // [WithFile(TestImages.Jpeg.Baseline.Winter444_Interleaved, PixelTypes.Rgb24)] public void RoundTrip(TestImageProvider provider) @@ -448,6 +677,19 @@ public class Av1YuvConverterTests ImageComparer.Tolerant(0.002F).VerifySimilarity(image, actual); } + /// + /// Creates a sequence header containing the color signaling required by a conversion test. + /// + /// The frame width. + /// The frame height. + /// Whether encoded samples use the full range. + /// The matrix coefficients used for conversion. + /// The encoded plane layout. + /// The signaled chroma sample position. + /// The encoded sample bit depth. + /// The transfer characteristics used by nonlinear matrices. + /// The color primaries used by derived matrices. + /// The configured sequence header. private static ObuSequenceHeader CreateSequenceHeader( int width, int height, @@ -455,7 +697,9 @@ public class Av1YuvConverterTests ObuMatrixCoefficients matrixCoefficients = ObuMatrixCoefficients.Bt709, Av1ColorFormat colorFormat = Av1ColorFormat.Yuv444, ObuChromoSamplePosition chromaSamplePosition = ObuChromoSamplePosition.Unknown, - Av1BitDepth bitDepth = Av1BitDepth.EightBit) + Av1BitDepth bitDepth = Av1BitDepth.EightBit, + ObuTransferCharacteristics transferCharacteristics = ObuTransferCharacteristics.Bt709, + ObuColorPrimaries colorPrimaries = ObuColorPrimaries.Bt709) => new() { MaxFrameWidth = width, @@ -465,10 +709,83 @@ public class Av1YuvConverterTests IsMonochrome = colorFormat == Av1ColorFormat.Yuv400, BitDepth = bitDepth, MatrixCoefficients = matrixCoefficients, + TransferCharacteristics = transferCharacteristics, + ColorPrimaries = colorPrimaries, ColorRange = fullRange, SubSamplingX = colorFormat is Av1ColorFormat.Yuv400 or Av1ColorFormat.Yuv420 or Av1ColorFormat.Yuv422, SubSamplingY = colorFormat is Av1ColorFormat.Yuv400 or Av1ColorFormat.Yuv420, ChromaSamplePosition = chromaSamplePosition, }, }; + + /// + /// Verifies that every high-bit-depth sample in a plane matches its first sample. + /// + /// The encoded frame buffer. + /// The plane to inspect. + /// The horizontal subsampling shift. + /// The vertical subsampling shift. + private static void AssertPlaneContainsRepeatedSample(Av1FrameBuffer frameBuffer, Av1Plane plane, int subX, int subY) + { + Span samples = frameBuffer.GetHighBitDepthRowSpan(plane, 0, subX, subY); + for (int x = 1; x < samples.Length; x++) + { + Assert.Equal(samples[0], samples[x]); + } + } + + /// + /// Verifies that a subsampled plane contains the rounded mean of the corresponding full-resolution samples. + /// + /// The full-resolution encoded frame. + /// The subsampled encoded frame. + /// The chroma plane to compare. + private static void AssertSubsampledPlaneMatchesAverage( + Av1FrameBuffer fullResolution, + Av1FrameBuffer subsampled, + Av1Plane plane) + { + int subY = subsampled.ColorConfig.SubSamplingY ? 1 : 0; + int chromaHeight = (subsampled.Height + subY) >> subY; + int chromaWidth = (subsampled.Width + 1) >> 1; + for (int y = 0; y < chromaHeight; y++) + { + int sourceY = y << subY; + int rowCount = subY == 0 ? 1 : Math.Min(2, fullResolution.Height - sourceY); + for (int x = 0; x < chromaWidth; x++) + { + int sourceX = x << 1; + int columnCount = Math.Min(2, fullResolution.Width - sourceX); + int sum = 0; + for (int row = 0; row < rowCount; row++) + { + for (int column = 0; column < columnCount; column++) + { + sum += GetPlaneSample(fullResolution, plane, sourceX + column, sourceY + row, 0, 0); + } + } + + int expected = (int)MathF.Round((float)sum / (rowCount * columnCount), MidpointRounding.AwayFromZero); + int actual = GetPlaneSample(subsampled, plane, x, y, 1, subY); + Assert.True( + actual >= expected - 1 && actual <= expected + 1, + $"Plane {plane}, sample ({x}, {y}): expected {expected} +/- 1 from sum {sum} over {rowCount * columnCount} samples but found {actual}."); + } + } + } + + /// + /// Gets one encoded sample from an eight-bit or high-bit-depth frame plane. + /// + /// The encoded frame buffer. + /// The plane containing the sample. + /// The horizontal sample coordinate. + /// The vertical sample coordinate. + /// The horizontal subsampling shift. + /// The vertical subsampling shift. + /// The encoded sample value. + private static int GetPlaneSample(Av1FrameBuffer frameBuffer, Av1Plane plane, int x, int y, int subX, int subY) + => frameBuffer.BitDepth == Av1BitDepth.EightBit + ? frameBuffer.DeriveBlockPointer(plane, subX, subY).DangerousGetRowSpan(y)[x] + : frameBuffer.GetHighBitDepthRowSpan(plane, y, subX, subY)[x]; }