From 8bcb798b35e22b0e98f1f47a6db20e14bbb9f8c7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 25 Aug 2026 20:31:02 +1000 Subject: [PATCH] Implement SIMD-first AV1 color conversion --- HEIF_IMPLEMENTATION_PLAN.md | 23 +- src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs | 2 +- .../Heif/Av1/Av1TransferFunctions.Simd.cs | 844 ++++++++++++++++++ .../Formats/Heif/Av1/Av1TransferFunctions.cs | 2 +- .../Heif/Av1/Av1YuvConverter.Operators.cs | 623 +++++++++++++ .../Heif/Av1/Av1YuvConverter.RowOperation.cs | 156 ++++ .../Formats/Heif/Av1/Av1YuvConverter.Simd.cs | 790 ++++++++++++++++ .../Formats/Heif/Av1/Av1YuvConverter.cs | 117 +-- .../Heif/Av1/Entropy/Av1SymbolDecoder.cs | 56 +- .../Heif/Av1/Entropy/Av1SymbolEncoder.cs | 16 +- .../Heif/Av1/Entropy/Av1SymbolReader.cs | 7 + .../Heif/Av1/Entropy/Av1SymbolWriter.cs | 7 + .../Heif/Av1/Pipeline/Av1FrameDecoder.cs | 9 +- .../Av1/Prediction/Av1PredictionDecoder.cs | 16 +- .../Formats/Heif/Av1/Tiling/Av1TileReader.cs | 17 +- .../Heif/Av1/Transform/Av1BlockDecoder.cs | 53 +- .../Formats/Heif/HeifDecoderCore.cs | 6 +- .../Formats/Heif/Av1/Av1TilingTests.cs | 47 + .../Heif/Av1/Av1TransferFunctionsTests.cs | 157 ++++ .../Formats/Heif/HeifDecoderTests.cs | 83 ++ tests/ImageSharp.Tests/TestImages.cs | 9 + .../Images/Input/Heif/paris_icc_exif_xmp.avif | 3 + .../Images/Input/Heif/paris_icc_exif_xmp.png | 3 + tests/Images/Input/Heif/perceptual_icc.avif | 3 + 24 files changed, 2871 insertions(+), 178 deletions(-) create mode 100644 src/ImageSharp/Formats/Heif/Av1/Av1TransferFunctions.Simd.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Operators.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowOperation.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Simd.cs create mode 100644 tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransferFunctionsTests.cs create mode 100644 tests/Images/Input/Heif/paris_icc_exif_xmp.avif create mode 100644 tests/Images/Input/Heif/paris_icc_exif_xmp.png create mode 100644 tests/Images/Input/Heif/perceptual_icc.avif diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 54b1ae4aa..7d7a92777 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -29,6 +29,15 @@ Checkboxes may be marked complete only when the implementation and the verificat ## Active and queued work +- [ ] **Active:** complete the SIMD-first shared HEIF color pipeline, including ICC color management after codec color conversion and image composition. + - [x] Correct AV1 clipped-edge partition entropy handling and verify every block-mode cell from a real libavif AVIF against libaom 3.14.1. + - [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. + - [ ] 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. + - [ ] 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. + - [ ] 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] **Completed:** eliminate per-call scratch allocation across every AV1 forward and inverse 1-D transform, replace shared mutable transform scratch with operation-owned workspace, and implement libaom-shaped bulk transform paths with scalar parity. - [x] Inventory the existing hot-path scratch use: 15 forward/inverse transformer types contain 30 `stackalloc int[...]` sites. - [x] Refactor every affected forward transform to reuse caller-owned workspace and its output buffer for staged ping-pong storage. @@ -42,7 +51,7 @@ Checkboxes may be marked complete only when the implementation and the verificat - [x] Add complete-block BenchmarkDotNet coverage for scalar, `Vector128`, `Vector256`, and runtime-dispatched 8x8 and 32x32 forward/inverse DCT paths, including managed-allocation reporting. - [x] Verify every scalar and hardware path across all transform types, sizes, bit depths, edge blocks, and bounded overflow cases, then record representative complete-block timings and allocations. - Verification covers all 159 normative size/type combinations at 8, 10, and 12 bits with padded input, prediction, and destination strides. Each of the 477 configurations compares scalar and `Vector128` output, and every configuration that contains a complete eight-lane tile also compares `Vector256` output. The 989-test focused suite passes with normal AVX2 dispatch, with AVX2 disabled, and with all hardware intrinsics disabled. -- [ ] **Active:** restore bounded animated HEIC and AVIF image-sequence scope, including the required image-level and per-frame metadata contracts, without introducing unrelated ISO BMFF surfaces. +- [ ] **Queued:** restore bounded animated HEIC and AVIF image-sequence scope, including the required image-level and per-frame metadata contracts, without introducing unrelated ISO BMFF surfaces. - [x] Reconcile the top-level still-image-only scope with the required animated HEIC and AVIF completion boundary before sequence implementation begins. - [x] Define the ImageSharp image-level sequence metadata and per-frame metadata contracts, including observable timing, repetition, color, alpha, orientation, and profile behavior. - `HeifMetadata` now carries repetition and root-frame animation behavior through `FormatConnectingMetadata`. `HeifFrameMetadata` carries frame duration through `FormatConnectingFrameMetadata`; frame-local color, alpha, orientation, and profile state remain owned by the existing `ImageFrameMetadata` contract. The focused Release suite passes all 40 encoder, image metadata, and frame metadata tests. @@ -243,7 +252,7 @@ This assessment is based on the current source after the upstream ImageSharp mer - Alpha auxiliary decoding now recognizes `auxC`, `auxl`, and `prem`, supports a direct auxiliary image/grid and the per-color-grid-tile form, preserves normalized alpha through `L16`/`Rgba64`, box-resamples differing plane sizes, and reports alpha presence. HEVC alpha remains blocked on the HEVC image-item decoder, while independent AVIF alpha fixtures are still required against the incomplete AV1 reconstruction pipeline. - Clean aperture, image rotation, and image mirror properties now validate their registered payloads, exact integer crop geometry, and essential associations; affect Identify dimensions; and reuse ImageSharp's optimized crop/rotate/flip processors after auxiliary alpha composition in the MIAF-defined order. Independent transform vectors must still verify every crop/rotation/mirror/alpha combination. - Decode now resolves `cdsc`-associated Exif and `application/rdf+xml` XMP items for the primary still image, validates the declared Exif TIFF-header offset, and attaches the payloads through ImageSharp's existing profile types before presentation transforms. Independent AVIF, HEIC, and HIF metadata fixtures and Identify-time profile reporting remain required. -- ICC and `nclx` CICP color properties are now associated with the presented color item instead of global parser state, validated, and exposed on Decode and Identify through the existing ImageSharp profiles. AV1 decode requires specified sequence-header fields and the range flag to match `nclx`, uses the property only for fields the bitstream leaves unspecified, and exposes the effective sequence-header CICP description when no property is present. Independent ICC/CICP fixtures, Identify-time bitstream fallback, ICC conversion coverage, and HEVC integration remain required. +- ICC and `nclx` CICP color properties are now associated with the presented color item instead of global parser state, validated, and exposed on Decode and Identify through the existing ImageSharp profiles. AV1 decode requires specified sequence-header fields and the range flag to match `nclx`, uses the property only for fields the bitstream leaves unspecified, and exposes the effective sequence-header CICP description when no property is present. Independent libavif fixtures now verify exact ICC byte preservation, observable non-sRGB conversion against the paired PNG, canonical-sRGB compaction, and metadata skipping for a primary AVIF item. Identify-time bitstream fallback, grid/alpha/sequence ICC coverage, independent CICP vectors, and HEVC integration remain required. - Pixel aspect ratio now preserves the complete unsigned spacing pair, affects Decode and Identify through ImageSharp's existing unitless resolution metadata, and follows quarter-turn presentation rotation. Independent grid, rotation, and maximum-spacing fixtures remain required. - AV1 codec configuration is now retained per `av01` image item rather than in decoder-global state. Decode requires the property, validates its fixed record against the item's sequence header and any associated `pixi` channel depths, and requires matching configurations across grid tiles. Identify now reports the configuration's 8/10/12-bit precision and monochrome shape. The optional `configOBUs` sequence is bounded and validated, including its mandatory size fields, first-position/at-most-one sequence-header rule, the image item's exactly-one sequence-header rule, and exact comparison of a repeated configuration header with the item header. Content light-level and mastering-display color-volume information are decoded from the bounded `clli` and `mdcv` image properties and matching AV1 HDR metadata OBUs, with the representations' distinct fixed-point precision, grid/property precedence, and `SkipMetadata` behavior preserved. The still-image `cclv`, `amve`, `reve`, and `ndwt` properties preserve content-volume, ambient-viewing, mastering-environment, and diffuse-white descriptions without importing their video-SEI or sample-entry state models. Independent fixtures and encoder preservation remain required. HEVC `hvcC` is now associated with each `hvc1` image item, structurally validated through its VPS/SPS/PPS arrays, and used to report encoded precision and monochrome shape; item-payload validation is implemented for the bounded decoder path, while pixel reconstruction remains incomplete. - Several image-item properties and relationships remain missing or parsed without fully affecting output. @@ -443,14 +452,17 @@ Tasks: - [ ] Implement monochrome, 4:2:0, 4:2:2, and 4:4:4 plane access with every valid signaled chroma sample position. - [ ] Implement full- and limited-range expansion for 8, 10, and 12-bit samples across every supported plane layout. - [ ] 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. +- [ ] 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. +- [ ] 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. - [ ] Validate grid tile type, dimensions, properties, order, and canvas coverage. Compose directly into the destination frame, cropping only the permitted right and bottom tile overlap. - [ ] Apply clean aperture, rotation, mirroring, and pixel aspect ratio according to the container property order and ImageSharp metadata/processing conventions. -- [ ] Preserve ICC, CICP, Exif, and XMP metadata using existing ImageSharp profile types. +- [ ] Preserve ICC, CICP, Exif, and XMP metadata using existing ImageSharp profile types. ICC preservation and ICC color conversion are separate contracts and must both be independently verified. Exit gate: -- [ ] The complete cross-product of valid HEVC/AV1 bit depths, subsampling modes, ranges, color signaling, alpha, grids, and transforms is covered by focused vectors and representative HEIC/AVIF integration files and matches independently decoded references within a documented conversion tolerance. Integer identity/lossless cases must match exactly. +- [ ] The complete cross-product of valid HEVC/AV1 bit depths, subsampling modes, ranges, color signaling, alpha, grids, and transforms is covered by focused vectors and representative HEIC/AVIF integration files and matches independently decoded references within a documented conversion tolerance. Tests must prove ICC conversion changes pixel values according to an independent profile transform instead of checking profile retention alone. Integer identity/lossless cases must match exactly. ### Phase 6: implement a real AV1 still-image encoder @@ -467,6 +479,7 @@ Implement in vertical slices that always produce a decodable AV1 bitstream: - [ ] Add in-loop filter decisions and signaling. A legal choice to disable an encoder feature is distinct from a decoder skipping a signaled feature. - [ ] Add alpha as an auxiliary AV1 item with independently controllable quality where the public option warrants it. - [ ] Write the AVIF item graph, extents, `av1C`, pixel information, color properties, metadata, and alpha relationships with correct brands. +- [ ] Write embedded ICC and CICP color properties independently, preserve the source profile selected by the encoder options, and verify both metadata round-trip and independently converted output pixels. - [ ] Make output deterministic for identical pixels, metadata, options, and configuration. - [ ] Complete the partition, prediction, transform, quantization, entropy, filter, and rate/distortion choices needed for quality and effort settings to provide a genuine compression tradeoff rather than selecting from a fixed coding subset. - [ ] Exercise every encoder bit-depth, plane-layout, range, color, alpha, lossless/lossy, quality, and effort combination through independent decode. @@ -486,6 +499,7 @@ Implement in vertical slices that always produce a HEVC bitstream accepted by an - [ ] Complete coding-tree partitioning, intra prediction selection, forward transforms, scaling/quantization, coefficient scanning, and CABAC encoding. - [ ] Add real rate/distortion mode decision, quality and effort controls, lossless mode, deblocking decisions, and sample-adaptive-offset decisions. - [ ] Add alpha auxiliary images, grids, metadata, color properties, image relationships, and correct HEIC brands. +- [ ] Write embedded ICC and CICP color properties independently, preserve the source profile selected by the encoder options, and verify both metadata round-trip and independently converted output pixels. - [ ] Exercise every encoder profile, bit depth, chroma format, range, color, alpha, lossless/lossy, quality, and effort combination through independent decode. Exit gate: @@ -503,6 +517,7 @@ Tasks: - [ ] Benchmark codec-specific costs for CABAC/range decode, inverse transforms, still-image prediction, deblocking, SAO, CDEF, restoration, chroma upsampling, color conversion, alpha packing, and grid copies. - [ ] Implement vector paths only for confirmed hot loops, using existing `Vector128`, `Vector256`, and `Vector512` helper and dispatch patterns where supported. - [ ] Prioritize shared color conversion and pixel packing, chroma upsampling, inverse-transform add-and-clip, intra predictors, HEVC deblock/SAO, AV1 loop filter/CDEF/restoration, and contiguous grid copies. +- [ ] Benchmark the complete decode color pipeline on representative 8/10/12-bit AVIF and HEIC images with and without embedded ICC profiles. Report absolute end-to-end timings and allocations in addition to the isolated YUV/CICP and ICC stage costs. - [ ] Port upstream SIMD algorithms only after mapping lane width, signedness, intermediate precision, rounding, saturation, edge extension, and high-bit-depth behavior to the scalar oracle. - [ ] Keep one scalar implementation as the specification-shaped reference. Vector paths must share tables and constants with it rather than duplicate codec policy. - [ ] Test scalar and each available hardware path with intrinsics explicitly enabled and disabled, including widths shorter than a vector, exact-vector widths, non-multiples, edges, maximum sample values, and high-bit-depth overflow cases. diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs index 4fc74ca9b..2baeaf373 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs @@ -171,7 +171,7 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable this.SequenceHeader.ColorConfig.GetColorFormat(), false); - Av1FrameDecoder frameDecoder = new(this.SequenceHeader, this.FrameHeader, this.FrameInfo, frameBuffer); + using Av1FrameDecoder frameDecoder = new(this.SequenceHeader, this.FrameHeader, this.FrameInfo, frameBuffer); frameDecoder.DecodeFrame(); ImageFrame? resultFrame = null; diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1TransferFunctions.Simd.cs b/src/ImageSharp/Formats/Heif/Av1/Av1TransferFunctions.Simd.cs new file mode 100644 index 000000000..eaaa70a58 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1TransferFunctions.Simd.cs @@ -0,0 +1,844 @@ +// 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 SIMD implementations of the H.273 transfer characteristics used by AV1 color conversion. +/// +internal static partial class Av1TransferFunctions +{ + /// + /// Converts four nonlinear signal values to their H.273 linear-domain values. + /// + /// The signaled transfer characteristics. + /// The nonlinear signal values. + /// The corresponding linear-domain values. + public static Vector128 ToLinear(ObuTransferCharacteristics transferCharacteristics, Vector128 value) + => ToLinear, Vector128Operator>(transferCharacteristics, value); + + /// + /// Converts eight nonlinear signal values to their H.273 linear-domain values. + /// + /// The signaled transfer characteristics. + /// The nonlinear signal values. + /// The corresponding linear-domain values. + public static Vector256 ToLinear(ObuTransferCharacteristics transferCharacteristics, Vector256 value) + => ToLinear, Vector256Operator>(transferCharacteristics, value); + + /// + /// Converts sixteen nonlinear signal values to their H.273 linear-domain values. + /// + /// The signaled transfer characteristics. + /// The nonlinear signal values. + /// The corresponding linear-domain values. + public static Vector512 ToLinear(ObuTransferCharacteristics transferCharacteristics, Vector512 value) + => ToLinear, Vector512Operator>(transferCharacteristics, value); + + /// + /// Converts four linear signal values to their H.273 nonlinear-domain values. + /// + /// The signaled transfer characteristics. + /// The linear signal values. + /// The corresponding nonlinear-domain values. + public static Vector128 ToGamma(ObuTransferCharacteristics transferCharacteristics, Vector128 value) + => ToGamma, Vector128Operator>(transferCharacteristics, value); + + /// + /// Converts eight linear signal values to their H.273 nonlinear-domain values. + /// + /// The signaled transfer characteristics. + /// The linear signal values. + /// The corresponding nonlinear-domain values. + public static Vector256 ToGamma(ObuTransferCharacteristics transferCharacteristics, Vector256 value) + => ToGamma, Vector256Operator>(transferCharacteristics, value); + + /// + /// Converts sixteen linear signal values to their H.273 nonlinear-domain values. + /// + /// The signaled transfer characteristics. + /// The linear signal values. + /// The corresponding nonlinear-domain values. + public static Vector512 ToGamma(ObuTransferCharacteristics transferCharacteristics, Vector512 value) + => ToGamma, Vector512Operator>(transferCharacteristics, value); + + /// + /// Converts nonlinear signal values to their H.273 linear-domain values using the selected SIMD width. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The signaled transfer characteristics. + /// The nonlinear signal values. + /// The corresponding linear-domain values. + private static TVector ToLinear(ObuTransferCharacteristics transferCharacteristics, TVector value) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + TVector zero = TOperator.Create(0F); + TVector one = TOperator.Create(1F); + + switch (transferCharacteristics) + { + case ObuTransferCharacteristics.Bt709: + case ObuTransferCharacteristics.Bt601: + case ObuTransferCharacteristics.Bt202010Bit: + case ObuTransferCharacteristics.Bt202012Bit: + return ToLinearBt709(value); + case ObuTransferCharacteristics.Bt470M: + return Power(TOperator.Min(TOperator.Max(value, zero), one), 2.2F); + case ObuTransferCharacteristics.Bt470BG: + return Power(TOperator.Min(TOperator.Max(value, zero), one), 2.8F); + case ObuTransferCharacteristics.Smpte240: + return ToLinearSmpte240(value); + case ObuTransferCharacteristics.Linear: + return TOperator.Min(TOperator.Max(value, zero), one); + case ObuTransferCharacteristics.Log100: + { + // H.273 assigns an interval to zero for logarithmic curves. The scalar midpoint convention is + // selected lane-wise after evaluating the positive branch, which keeps the hot path branchless. + TVector exponent = TOperator.Multiply(TOperator.Subtract(TOperator.Min(value, one), one), TOperator.Create(2F * 2.302585092994046F)); + TVector positive = TOperator.Exp(exponent); + return TOperator.ConditionalSelect(TOperator.LessThanOrEqual(value, zero), TOperator.Create(0.005F), positive); + } + + case ObuTransferCharacteristics.Log100Sqrt10: + { + TVector exponent = TOperator.Multiply(TOperator.Subtract(TOperator.Min(value, one), one), TOperator.Create(2.5F * 2.302585092994046F)); + TVector positive = TOperator.Exp(exponent); + return TOperator.ConditionalSelect(TOperator.LessThanOrEqual(value, zero), TOperator.Create(0.00158113883F), positive); + } + + case ObuTransferCharacteristics.Iec61966: + return ToLinearIec61966(value); + case ObuTransferCharacteristics.Bt1361: + return ToLinearBt1361(value); + case ObuTransferCharacteristics.Srgb: + return ToLinearSrgb(value); + case ObuTransferCharacteristics.Smpte2084: + return ToLinearPq(value); + case ObuTransferCharacteristics.Smpte428: + return TOperator.Divide(Power(TOperator.Max(value, zero), 2.6F), TOperator.Create(Smpte428Scale)); + case ObuTransferCharacteristics.Hlg: + return ToLinearHlg(value); + default: + return ToLinearBt709(value); + } + } + + /// + /// Converts linear signal values to their H.273 nonlinear-domain values using the selected SIMD width. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The signaled transfer characteristics. + /// The linear signal values. + /// The corresponding nonlinear-domain values. + private static TVector ToGamma(ObuTransferCharacteristics transferCharacteristics, TVector value) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + TVector zero = TOperator.Create(0F); + TVector one = TOperator.Create(1F); + + switch (transferCharacteristics) + { + case ObuTransferCharacteristics.Bt709: + case ObuTransferCharacteristics.Bt601: + case ObuTransferCharacteristics.Bt202010Bit: + case ObuTransferCharacteristics.Bt202012Bit: + return ToGammaBt709(value); + case ObuTransferCharacteristics.Bt470M: + return Power(TOperator.Min(TOperator.Max(value, zero), one), 1F / 2.2F); + case ObuTransferCharacteristics.Bt470BG: + return Power(TOperator.Min(TOperator.Max(value, zero), one), 1F / 2.8F); + case ObuTransferCharacteristics.Smpte240: + return ToGammaSmpte240(value); + case ObuTransferCharacteristics.Linear: + return TOperator.Min(TOperator.Max(value, zero), one); + case ObuTransferCharacteristics.Log100: + { + // Clamp inactive lanes to the threshold before Log. ConditionalSelect does not short-circuit, + // so this prevents negative input lanes from contaminating the vector operation with NaN values. + TVector threshold = TOperator.Create(0.01F); + TVector bounded = TOperator.Min(TOperator.Max(value, threshold), one); + TVector positive = TOperator.Add(one, TOperator.Divide(TOperator.Log(bounded), TOperator.Create(2F * 2.302585092994046F))); + return TOperator.ConditionalSelect(TOperator.LessThanOrEqual(value, threshold), zero, positive); + } + + case ObuTransferCharacteristics.Log100Sqrt10: + { + TVector threshold = TOperator.Create(0.00316227766F); + TVector bounded = TOperator.Min(TOperator.Max(value, threshold), one); + TVector positive = TOperator.Add(one, TOperator.Divide(TOperator.Log(bounded), TOperator.Create(2.5F * 2.302585092994046F))); + return TOperator.ConditionalSelect(TOperator.LessThanOrEqual(value, threshold), zero, positive); + } + + case ObuTransferCharacteristics.Iec61966: + return ToGammaIec61966(value); + case ObuTransferCharacteristics.Bt1361: + return ToGammaBt1361(value); + case ObuTransferCharacteristics.Srgb: + return ToGammaSrgb(value); + case ObuTransferCharacteristics.Smpte2084: + return ToGammaPq(value); + case ObuTransferCharacteristics.Smpte428: + return Power(TOperator.Multiply(TOperator.Create(Smpte428Scale), TOperator.Max(value, zero)), 1F / 2.6F); + case ObuTransferCharacteristics.Hlg: + return ToGammaHlg(value); + default: + return ToGammaBt709(value); + } + } + + /// + /// Applies the inverse BT.709-family opto-electronic transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The nonlinear signal values. + /// The linear signal values. + private static TVector ToLinearBt709(TVector value) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + TVector zero = TOperator.Create(0F); + TVector one = TOperator.Create(1F); + TVector linear = TOperator.Divide(value, TOperator.Create(4.5F)); + TVector baseValue = TOperator.Divide(TOperator.Add(value, TOperator.Create(Bt709Alpha - 1F)), TOperator.Create(Bt709Alpha)); + TVector nonlinear = Power(TOperator.Max(baseValue, zero), 1F / 0.45F); + TVector belowOne = TOperator.ConditionalSelect(TOperator.LessThan(value, TOperator.Create(4.5F * Bt709Beta)), linear, nonlinear); + + // The comparisons deliberately mirror the scalar ordering. This preserves the H.273 lower and upper + // saturation rules while allowing all lanes to execute without data-dependent branches. + TVector bounded = TOperator.ConditionalSelect(TOperator.LessThan(value, one), belowOne, one); + return TOperator.ConditionalSelect(TOperator.LessThan(value, zero), zero, bounded); + } + + /// + /// Applies the BT.709-family opto-electronic transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The linear signal values. + /// The nonlinear signal values. + private static TVector ToGammaBt709(TVector value) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + TVector zero = TOperator.Create(0F); + TVector one = TOperator.Create(1F); + TVector linear = TOperator.Multiply(value, TOperator.Create(4.5F)); + TVector nonlinear = TOperator.Subtract( + TOperator.Multiply(TOperator.Create(Bt709Alpha), Power(TOperator.Max(value, zero), 0.45F)), + TOperator.Create(Bt709Alpha - 1F)); + TVector belowOne = TOperator.ConditionalSelect(TOperator.LessThan(value, TOperator.Create(Bt709Beta)), linear, nonlinear); + TVector bounded = TOperator.ConditionalSelect(TOperator.LessThan(value, one), belowOne, one); + return TOperator.ConditionalSelect(TOperator.LessThan(value, zero), zero, bounded); + } + + /// + /// Applies the inverse SMPTE ST 240 opto-electronic transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The nonlinear signal values. + /// The linear signal values. + private static TVector ToLinearSmpte240(TVector value) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + TVector zero = TOperator.Create(0F); + TVector one = TOperator.Create(1F); + TVector linear = TOperator.Divide(value, TOperator.Create(4F)); + TVector baseValue = TOperator.Divide(TOperator.Add(value, TOperator.Create(Smpte240Alpha - 1F)), TOperator.Create(Smpte240Alpha)); + TVector nonlinear = Power(TOperator.Max(baseValue, zero), 1F / 0.45F); + TVector belowOne = TOperator.ConditionalSelect(TOperator.LessThan(value, TOperator.Create(4F * Smpte240Beta)), linear, nonlinear); + TVector bounded = TOperator.ConditionalSelect(TOperator.LessThan(value, one), belowOne, one); + return TOperator.ConditionalSelect(TOperator.LessThan(value, zero), zero, bounded); + } + + /// + /// Applies the SMPTE ST 240 opto-electronic transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The linear signal values. + /// The nonlinear signal values. + private static TVector ToGammaSmpte240(TVector value) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + TVector zero = TOperator.Create(0F); + TVector one = TOperator.Create(1F); + TVector linear = TOperator.Multiply(value, TOperator.Create(4F)); + TVector nonlinear = TOperator.Subtract( + TOperator.Multiply(TOperator.Create(Smpte240Alpha), Power(TOperator.Max(value, zero), 0.45F)), + TOperator.Create(Smpte240Alpha - 1F)); + TVector belowOne = TOperator.ConditionalSelect(TOperator.LessThan(value, TOperator.Create(Smpte240Beta)), linear, nonlinear); + TVector bounded = TOperator.ConditionalSelect(TOperator.LessThan(value, one), belowOne, one); + return TOperator.ConditionalSelect(TOperator.LessThan(value, zero), zero, bounded); + } + + /// + /// Applies the inverse extended IEC 61966-2-4 transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The nonlinear signal values. + /// The linear signal values. + private static TVector ToLinearIec61966(TVector value) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + TVector negativeBase = TOperator.Divide(TOperator.Subtract(value, TOperator.Create(Bt709Alpha - 1F)), TOperator.Create(-Bt709Alpha)); + TVector negative = TOperator.Negate(Power(TOperator.Max(negativeBase, TOperator.Create(0F)), 1F / 0.45F)); + TVector linear = TOperator.Divide(value, TOperator.Create(4.5F)); + TVector positiveBase = TOperator.Divide(TOperator.Add(value, TOperator.Create(Bt709Alpha - 1F)), TOperator.Create(Bt709Alpha)); + TVector positive = Power(TOperator.Max(positiveBase, TOperator.Create(0F)), 1F / 0.45F); + TVector centerOrPositive = TOperator.ConditionalSelect(TOperator.LessThan(value, TOperator.Create(4.5F * Bt709Beta)), linear, positive); + return TOperator.ConditionalSelect(TOperator.LessThan(value, TOperator.Create(-4.5F * Bt709Beta)), negative, centerOrPositive); + } + + /// + /// Applies the extended IEC 61966-2-4 transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The linear signal values. + /// The nonlinear signal values. + private static TVector ToGammaIec61966(TVector value) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + TVector zero = TOperator.Create(0F); + TVector negative = TOperator.Add( + TOperator.Negate(TOperator.Multiply( + TOperator.Create(Bt709Alpha), + Power(TOperator.Max(TOperator.Negate(value), zero), 0.45F))), + TOperator.Create(Bt709Alpha - 1F)); + + TVector linear = TOperator.Multiply(value, TOperator.Create(4.5F)); + TVector positive = TOperator.Subtract( + TOperator.Multiply(TOperator.Create(Bt709Alpha), Power(TOperator.Max(value, zero), 0.45F)), + TOperator.Create(Bt709Alpha - 1F)); + TVector centerOrPositive = TOperator.ConditionalSelect(TOperator.LessThan(value, TOperator.Create(Bt709Beta)), linear, positive); + return TOperator.ConditionalSelect(TOperator.LessThan(value, TOperator.Create(-Bt709Beta)), negative, centerOrPositive); + } + + /// + /// Applies the inverse extended BT.1361 transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The nonlinear signal values. + /// The linear signal values. + private static TVector ToLinearBt1361(TVector value) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + TVector zero = TOperator.Create(0F); + TVector negativeBase = TOperator.Divide(TOperator.Subtract(value, TOperator.Create(0.02482420670236F)), TOperator.Create(-0.27482420670236F)); + TVector negative = TOperator.Divide(Power(TOperator.Max(negativeBase, zero), 1F / 0.45F), TOperator.Create(-4F)); + TVector negativeOrPositive = TOperator.ConditionalSelect(TOperator.LessThan(value, zero), negative, ToLinearBt709(value)); + return TOperator.ConditionalSelect(TOperator.LessThan(value, TOperator.Create(-0.25F)), TOperator.Create(-0.25F), negativeOrPositive); + } + + /// + /// Applies the extended BT.1361 transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The linear signal values. + /// The nonlinear signal values. + private static TVector ToGammaBt1361(TVector value) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + TVector zero = TOperator.Create(0F); + TVector negativePower = Power(TOperator.Max(TOperator.Multiply(TOperator.Create(-4F), value), zero), 0.45F); + TVector negative = TOperator.Add(TOperator.Multiply(TOperator.Create(-0.27482420670236F), negativePower), TOperator.Create(0.02482420670236F)); + TVector negativeOrPositive = TOperator.ConditionalSelect(TOperator.LessThan(value, zero), negative, ToGammaBt709(value)); + return TOperator.ConditionalSelect(TOperator.LessThan(value, TOperator.Create(-0.25F)), TOperator.Create(-0.25F), negativeOrPositive); + } + + /// + /// Applies the inverse extended IEC 61966-2-1 transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The nonlinear signal values. + /// The linear signal values. + private static TVector ToLinearSrgb(TVector value) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + TVector zero = TOperator.Create(0F); + TVector negativeBase = TOperator.Divide(TOperator.Subtract(value, TOperator.Create(SrgbAlpha - 1F)), TOperator.Create(-SrgbAlpha)); + TVector negative = TOperator.Negate(Power(TOperator.Max(negativeBase, zero), 2.4F)); + TVector linear = TOperator.Divide(value, TOperator.Create(12.92F)); + TVector positiveBase = TOperator.Divide(TOperator.Add(value, TOperator.Create(SrgbAlpha - 1F)), TOperator.Create(SrgbAlpha)); + TVector positive = Power(TOperator.Max(positiveBase, zero), 2.4F); + TVector centerOrPositive = TOperator.ConditionalSelect(TOperator.LessThan(value, TOperator.Create(12.92F * SrgbBeta)), linear, positive); + return TOperator.ConditionalSelect(TOperator.LessThan(value, TOperator.Create(-12.92F * SrgbBeta)), negative, centerOrPositive); + } + + /// + /// Applies the extended IEC 61966-2-1 transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The linear signal values. + /// The nonlinear signal values. + private static TVector ToGammaSrgb(TVector value) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + TVector zero = TOperator.Create(0F); + TVector negative = TOperator.Add( + TOperator.Negate(TOperator.Multiply( + TOperator.Create(SrgbAlpha), + Power(TOperator.Max(TOperator.Negate(value), zero), 1F / 2.4F))), + TOperator.Create(SrgbAlpha - 1F)); + + TVector linear = TOperator.Multiply(value, TOperator.Create(12.92F)); + TVector positive = TOperator.Subtract( + TOperator.Multiply(TOperator.Create(SrgbAlpha), Power(TOperator.Max(value, zero), 1F / 2.4F)), + TOperator.Create(SrgbAlpha - 1F)); + TVector centerOrPositive = TOperator.ConditionalSelect(TOperator.LessThan(value, TOperator.Create(SrgbBeta)), linear, positive); + return TOperator.ConditionalSelect(TOperator.LessThan(value, TOperator.Create(-SrgbBeta)), negative, centerOrPositive); + } + + /// + /// Applies the inverse SMPTE ST 2084 perceptual-quantizer transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The nonlinear signal values. + /// The normalized linear signal values. + private static TVector ToLinearPq(TVector value) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + TVector zero = TOperator.Create(0F); + TVector nonlinearPower = Power(TOperator.Min(TOperator.Max(value, zero), TOperator.Create(1F)), 1F / PqM); + TVector numerator = TOperator.Max(TOperator.Subtract(nonlinearPower, TOperator.Create(PqC1)), zero); + TVector denominator = TOperator.Subtract(TOperator.Create(PqC2), TOperator.Multiply(TOperator.Create(PqC3), nonlinearPower)); + TVector positive = TOperator.Min(Power(TOperator.Divide(numerator, denominator), 1F / PqN), TOperator.Create(1F)); + return TOperator.ConditionalSelect(TOperator.LessThanOrEqual(value, zero), zero, positive); + } + + /// + /// Applies the SMPTE ST 2084 perceptual-quantizer transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The normalized linear signal values. + /// The nonlinear signal values. + private static TVector ToGammaPq(TVector value) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + TVector zero = TOperator.Create(0F); + TVector linearPower = Power(TOperator.Min(TOperator.Max(value, zero), TOperator.Create(1F)), PqN); + TVector numerator = TOperator.Add(TOperator.Create(PqC1), TOperator.Multiply(TOperator.Create(PqC2), linearPower)); + TVector denominator = TOperator.Add(TOperator.Create(1F), TOperator.Multiply(TOperator.Create(PqC3), linearPower)); + TVector positive = TOperator.Min(Power(TOperator.Divide(numerator, denominator), PqM), TOperator.Create(1F)); + return TOperator.ConditionalSelect(TOperator.LessThanOrEqual(value, zero), zero, positive); + } + + /// + /// Applies the inverse HLG opto-electronic transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The nonlinear signal values. + /// The normalized linear signal values. + private static TVector ToLinearHlg(TVector value) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + TVector zero = TOperator.Create(0F); + TVector bounded = TOperator.Min(TOperator.Max(value, zero), TOperator.Create(1F)); + TVector linear = TOperator.Divide(TOperator.Multiply(bounded, bounded), TOperator.Create(3F)); + TVector exponent = TOperator.Divide(TOperator.Subtract(bounded, TOperator.Create(HlgC)), TOperator.Create(HlgA)); + TVector logarithmic = TOperator.Divide(TOperator.Add(TOperator.Exp(exponent), TOperator.Create(HlgB)), TOperator.Create(12F)); + TVector positive = TOperator.ConditionalSelect(TOperator.LessThanOrEqual(value, TOperator.Create(0.5F)), linear, logarithmic); + return TOperator.ConditionalSelect(TOperator.LessThanOrEqual(value, zero), zero, positive); + } + + /// + /// Applies the HLG opto-electronic transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The normalized linear signal values. + /// The nonlinear signal values. + private static TVector ToGammaHlg(TVector value) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + TVector zero = TOperator.Create(0F); + TVector bounded = TOperator.Min(TOperator.Max(value, zero), TOperator.Create(1F)); + TVector linear = TOperator.Sqrt(TOperator.Multiply(TOperator.Create(3F), bounded)); + + // Clamp the logarithm input for inactive lanes. SIMD conditional selection evaluates both branches, + // while the scalar definition evaluates Log only above the 1/12 transition. + TVector logarithmInput = TOperator.Max( + TOperator.Subtract(TOperator.Multiply(TOperator.Create(12F), bounded), TOperator.Create(HlgB)), + TOperator.Create(float.Epsilon)); + TVector logarithmic = TOperator.Add(TOperator.Multiply(TOperator.Create(HlgA), TOperator.Log(logarithmInput)), TOperator.Create(HlgC)); + TVector positive = TOperator.ConditionalSelect(TOperator.LessThanOrEqual(bounded, TOperator.Create(1F / 12F)), linear, logarithmic); + return TOperator.ConditionalSelect(TOperator.LessThanOrEqual(value, zero), zero, positive); + } + + /// + /// Raises nonnegative SIMD values to a scalar exponent. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The nonnegative base values. + /// The exponent applied to every lane. + /// The exponentiated values. + private static TVector Power(TVector value, float exponent) + where TVector : struct + where TOperator : struct, ITransferVectorOperator + { + // System.Numerics.Tensors does not currently vectorize Pow. Expressing positive powers as Exp(Log(x) * y) + // uses the .NET 10 cross-platform vector math kernels and keeps all transfer-function lanes in SIMD. + return TOperator.Exp(TOperator.Multiply(TOperator.Log(value), TOperator.Create(exponent))); + } +} + +/// +/// Contains the stateless vector-width operators used by the shared H.273 transfer-function formulas. +/// +internal static class Av1TransferVectorOperators +{ + /// + /// Defines the lane-wise operations required by the shared H.273 SIMD formulas. + /// + /// The SIMD vector type. + public interface ITransferVectorOperator + where TVector : struct + { + /// + /// Creates a vector whose lanes contain the specified value. + /// + /// The value copied to every lane. + /// The created vector. + public static abstract TVector Create(float value); + + /// + /// Adds corresponding vector lanes. + /// + /// The left operand. + /// The right operand. + /// The lane-wise sum. + public static abstract TVector Add(TVector left, TVector right); + + /// + /// Subtracts corresponding vector lanes. + /// + /// The left operand. + /// The right operand. + /// The lane-wise difference. + public static abstract TVector Subtract(TVector left, TVector right); + + /// + /// Multiplies corresponding vector lanes. + /// + /// The left operand. + /// The right operand. + /// The lane-wise product. + public static abstract TVector Multiply(TVector left, TVector right); + + /// + /// Multiplies corresponding vector lanes and adds an addend using the fastest supported estimate. + /// + /// The left multiplication operand. + /// The right multiplication operand. + /// The value added to the product. + /// The lane-wise multiply-add result. + public static abstract TVector MultiplyAddEstimate(TVector left, TVector right, TVector addend); + + /// + /// Divides corresponding vector lanes. + /// + /// The dividend. + /// The divisor. + /// The lane-wise quotient. + public static abstract TVector Divide(TVector left, TVector right); + + /// + /// Negates every vector lane. + /// + /// The input vector. + /// The negated vector. + public static abstract TVector Negate(TVector value); + + /// + /// Selects the smaller value in each pair of lanes. + /// + /// The left operand. + /// The right operand. + /// The lane-wise minimum. + public static abstract TVector Min(TVector left, TVector right); + + /// + /// Selects the larger value in each pair of lanes. + /// + /// The left operand. + /// The right operand. + /// The lane-wise maximum. + public static abstract TVector Max(TVector left, TVector right); + + /// + /// Compares whether each left lane is less than its right lane. + /// + /// The left operand. + /// The right operand. + /// The comparison mask. + public static abstract TVector LessThan(TVector left, TVector right); + + /// + /// Compares whether each left lane is less than or equal to its right lane. + /// + /// The left operand. + /// The right operand. + /// The comparison mask. + public static abstract TVector LessThanOrEqual(TVector left, TVector right); + + /// + /// Selects lanes from two vectors according to a comparison mask. + /// + /// The comparison mask. + /// The value selected for set mask lanes. + /// The value selected for clear mask lanes. + /// The selected lanes. + public static abstract TVector ConditionalSelect(TVector condition, TVector left, TVector right); + + /// + /// Computes the natural exponential of every vector lane. + /// + /// The input vector. + /// The lane-wise exponential. + public static abstract TVector Exp(TVector value); + + /// + /// Computes the natural logarithm of every vector lane. + /// + /// The input vector. + /// The lane-wise logarithm. + public static abstract TVector Log(TVector value); + + /// + /// Computes the square root of every vector lane. + /// + /// 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); + } + + /// + /// Maps the shared transfer-function formulas to 128-bit vector operations. + /// + public readonly struct Vector128Operator : ITransferVectorOperator> + { + /// + public static Vector128 Create(float value) => Vector128.Create(value); + + /// + public static Vector128 Add(Vector128 left, Vector128 right) => left + right; + + /// + public static Vector128 Subtract(Vector128 left, Vector128 right) => left - right; + + /// + public static Vector128 Multiply(Vector128 left, Vector128 right) => left * right; + + /// + public static Vector128 MultiplyAddEstimate(Vector128 left, Vector128 right, Vector128 addend) + => Vector128.MultiplyAddEstimate(left, right, addend); + + /// + public static Vector128 Divide(Vector128 left, Vector128 right) => left / right; + + /// + public static Vector128 Negate(Vector128 value) => -value; + + /// + public static Vector128 Min(Vector128 left, Vector128 right) => Vector128.Min(left, right); + + /// + public static Vector128 Max(Vector128 left, Vector128 right) => Vector128.Max(left, right); + + /// + public static Vector128 LessThan(Vector128 left, Vector128 right) => Vector128.LessThan(left, right); + + /// + public static Vector128 LessThanOrEqual(Vector128 left, Vector128 right) => Vector128.LessThanOrEqual(left, right); + + /// + public static Vector128 ConditionalSelect(Vector128 condition, Vector128 left, Vector128 right) + => Vector128.ConditionalSelect(condition, left, right); + + /// + public static Vector128 Exp(Vector128 value) => Vector128.Exp(value); + + /// + public static Vector128 Log(Vector128 value) => Vector128.Log(value); + + /// + 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); + } + + /// + /// Maps the shared transfer-function formulas to 256-bit vector operations. + /// + public readonly struct Vector256Operator : ITransferVectorOperator> + { + /// + public static Vector256 Create(float value) => Vector256.Create(value); + + /// + public static Vector256 Add(Vector256 left, Vector256 right) => left + right; + + /// + public static Vector256 Subtract(Vector256 left, Vector256 right) => left - right; + + /// + public static Vector256 Multiply(Vector256 left, Vector256 right) => left * right; + + /// + public static Vector256 MultiplyAddEstimate(Vector256 left, Vector256 right, Vector256 addend) + => Vector256.MultiplyAddEstimate(left, right, addend); + + /// + public static Vector256 Divide(Vector256 left, Vector256 right) => left / right; + + /// + public static Vector256 Negate(Vector256 value) => -value; + + /// + public static Vector256 Min(Vector256 left, Vector256 right) => Vector256.Min(left, right); + + /// + public static Vector256 Max(Vector256 left, Vector256 right) => Vector256.Max(left, right); + + /// + public static Vector256 LessThan(Vector256 left, Vector256 right) => Vector256.LessThan(left, right); + + /// + public static Vector256 LessThanOrEqual(Vector256 left, Vector256 right) => Vector256.LessThanOrEqual(left, right); + + /// + public static Vector256 ConditionalSelect(Vector256 condition, Vector256 left, Vector256 right) + => Vector256.ConditionalSelect(condition, left, right); + + /// + public static Vector256 Exp(Vector256 value) => Vector256.Exp(value); + + /// + public static Vector256 Log(Vector256 value) => Vector256.Log(value); + + /// + 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); + } + + /// + /// Maps the shared transfer-function formulas to 512-bit vector operations. + /// + public readonly struct Vector512Operator : ITransferVectorOperator> + { + /// + public static Vector512 Create(float value) => Vector512.Create(value); + + /// + public static Vector512 Add(Vector512 left, Vector512 right) => left + right; + + /// + public static Vector512 Subtract(Vector512 left, Vector512 right) => left - right; + + /// + public static Vector512 Multiply(Vector512 left, Vector512 right) => left * right; + + /// + public static Vector512 MultiplyAddEstimate(Vector512 left, Vector512 right, Vector512 addend) + => Vector512.MultiplyAddEstimate(left, right, addend); + + /// + public static Vector512 Divide(Vector512 left, Vector512 right) => left / right; + + /// + public static Vector512 Negate(Vector512 value) => -value; + + /// + public static Vector512 Min(Vector512 left, Vector512 right) => Vector512.Min(left, right); + + /// + public static Vector512 Max(Vector512 left, Vector512 right) => Vector512.Max(left, right); + + /// + public static Vector512 LessThan(Vector512 left, Vector512 right) => Vector512.LessThan(left, right); + + /// + public static Vector512 LessThanOrEqual(Vector512 left, Vector512 right) => Vector512.LessThanOrEqual(left, right); + + /// + public static Vector512 ConditionalSelect(Vector512 condition, Vector512 left, Vector512 right) + => Vector512.ConditionalSelect(condition, left, right); + + /// + public static Vector512 Exp(Vector512 value) => Vector512.Exp(value); + + /// + public static Vector512 Log(Vector512 value) => Vector512.Log(value); + + /// + 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/Av1TransferFunctions.cs b/src/ImageSharp/Formats/Heif/Av1/Av1TransferFunctions.cs index add4ea9c1..923ee5ef9 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1TransferFunctions.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1TransferFunctions.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1; /// /// Applies the H.273 transfer characteristics used by AV1 color conversion. /// -internal static class Av1TransferFunctions +internal static partial class Av1TransferFunctions { /// /// The BT.709 and BT.2020 nonlinear scale factor. diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Operators.cs b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Operators.cs new file mode 100644 index 000000000..45ad060cf --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Operators.cs @@ -0,0 +1,623 @@ +// 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.RowOperation.cs b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowOperation.cs new file mode 100644 index 000000000..243004338 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowOperation.cs @@ -0,0 +1,156 @@ +// 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 new file mode 100644 index 000000000..8de7d1967 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Simd.cs @@ -0,0 +1,790 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Provides SIMD sample widening, chroma reconstruction, color traversal, and packed output for AV1 decoding. +/// +internal static partial class Av1YuvConverter +{ + /// + /// Defines the SIMD widening operations for one reconstructed AV1 sample type. + /// + /// The reconstructed sample type. + private interface ISampleLoader + where TSample : unmanaged + { + /// + /// Loads and widens four samples to single-precision lanes. + /// + /// The first source sample. + /// The widened samples. + public static abstract Vector128 LoadVector128(ref TSample source); + + /// + /// Loads and widens eight samples to single-precision lanes. + /// + /// The first source sample. + /// The widened samples. + public static abstract Vector256 LoadVector256(ref TSample source); + + /// + /// Loads and widens sixteen samples to single-precision lanes. + /// + /// The first source sample. + /// The widened samples. + public static abstract Vector512 LoadVector512(ref TSample source); + } + + /// + /// Widens reconstructed integer samples into a pooled float component row. + /// + /// The reconstructed sample type. + /// The widening operations for the sample type. + /// The reconstructed samples. + /// The destination component row. + private static void ConvertSamplesToFloat(ReadOnlySpan source, Span destination) + where TSample : unmanaged + where TLoader : struct, ISampleLoader + { + ref TSample sourceBase = ref MemoryMarshal.GetReference(source); + ref float destinationBase = ref MemoryMarshal.GetReference(destination); + int length = destination.Length; + int i = 0; + + // Descending vector widths match the JPEG color-converter traversal. A wide-capable CPU processes + // complete wide batches first while short and irregular rows continue through narrower SIMD tails. + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector512.Count; + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + Vector512 samples = TLoader.LoadVector512(ref Unsafe.Add(ref sourceBase, i)); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, i)) = samples; + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 samples = TLoader.LoadVector256(ref Unsafe.Add(ref sourceBase, i)); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, i)) = samples; + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + Vector128 samples = TLoader.LoadVector128(ref Unsafe.Add(ref sourceBase, i)); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, i)) = samples; + } + } + + for (; i < length; i++) + { + Unsafe.Add(ref destinationBase, i) = GetSample(source, i); + } + } + + /// + /// Reconstructs one full-width chroma row using the signaled vertical and horizontal sample positions. + /// + /// The reconstructed sample type. + /// The widening operations for the sample type. + /// The upper chroma row. + /// The lower chroma row. + /// The lower-row weight with a denominator of four. + /// The horizontal chroma subsampling shift. + /// Whether horizontally subsampled chroma is centered between luma samples. + /// The reconstructed full-width chroma row. + /// The first pooled chroma scratch row. + /// The second pooled chroma scratch row. + private static void ReconstructChromaRow( + ReadOnlySpan row0, + ReadOnlySpan row1, + int y1Weight, + int subX, + bool isCenteredX, + Span destination, + Span scratch0, + Span scratch1) + where TSample : unmanaged + where TLoader : struct, ISampleLoader + { + int sourceLength = subX == 0 ? destination.Length : (destination.Length + 1) >> 1; + Span top = scratch0[..sourceLength]; + ConvertSamplesToFloat(row0, top); + + if (y1Weight != 0) + { + Span bottom = scratch1[..sourceLength]; + ConvertSamplesToFloat(row1, bottom); + InterpolateChromaRows(top, bottom, y1Weight); + } + + if (subX == 0) + { + top.CopyTo(destination); + return; + } + + UpsampleChromaHorizontal(top, destination, isCenteredX); + } + + /// + /// Interpolates two chroma rows in place using AV1 quarter-sample weights. + /// + /// The upper row, replaced by the interpolated values. + /// The lower row. + /// The lower-row weight with a denominator of four. + private static void InterpolateChromaRows(Span top, ReadOnlySpan bottom, int bottomWeight) + { + ref float topBase = ref MemoryMarshal.GetReference(top); + ref float bottomBase = ref MemoryMarshal.GetReference(bottom); + int length = top.Length; + int i = 0; + float topWeight = 4 - bottomWeight; + + if (Vector512.IsHardwareAccelerated) + { + Vector512 topWeightVector = Vector512.Create(topWeight); + Vector512 bottomWeightVector = Vector512.Create((float)bottomWeight); + Vector512 scale = Vector512.Create(0.25F); + int oneVectorFromEnd = length - Vector512.Count; + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + ref Vector512 topVector = ref Unsafe.As>(ref Unsafe.Add(ref topBase, i)); + Vector512 bottomVector = Unsafe.As>(ref Unsafe.Add(ref bottomBase, i)); + topVector = Vector512.MultiplyAddEstimate(bottomWeightVector, bottomVector, topWeightVector * topVector) * scale; + } + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 topWeightVector = Vector256.Create(topWeight); + Vector256 bottomWeightVector = Vector256.Create((float)bottomWeight); + Vector256 scale = Vector256.Create(0.25F); + int oneVectorFromEnd = length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + ref Vector256 topVector = ref Unsafe.As>(ref Unsafe.Add(ref topBase, i)); + Vector256 bottomVector = Unsafe.As>(ref Unsafe.Add(ref bottomBase, i)); + topVector = Vector256.MultiplyAddEstimate(bottomWeightVector, bottomVector, topWeightVector * topVector) * scale; + } + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 topWeightVector = Vector128.Create(topWeight); + Vector128 bottomWeightVector = Vector128.Create((float)bottomWeight); + Vector128 scale = Vector128.Create(0.25F); + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + ref Vector128 topVector = ref Unsafe.As>(ref Unsafe.Add(ref topBase, i)); + Vector128 bottomVector = Unsafe.As>(ref Unsafe.Add(ref bottomBase, i)); + topVector = Vector128.MultiplyAddEstimate(bottomWeightVector, bottomVector, topWeightVector * topVector) * scale; + } + } + + for (; i < length; i++) + { + Unsafe.Add(ref topBase, i) = ((Unsafe.Add(ref topBase, i) * topWeight) + (Unsafe.Add(ref bottomBase, i) * bottomWeight)) * 0.25F; + } + } + + /// + /// Expands horizontally subsampled chroma to luma width using the AV1 sample-position rules. + /// + /// The subsampled chroma values. + /// The full-width chroma values. + /// Whether chroma lies between neighboring luma samples. + private static void UpsampleChromaHorizontal(ReadOnlySpan source, Span destination, bool isCentered) + { + ref float sourceBase = ref MemoryMarshal.GetReference(source); + ref float destinationBase = ref MemoryMarshal.GetReference(destination); + int sourceLength = source.Length; + int i = 0; + + if (isCentered) + { + // The first centered pair extends the left edge. Interior vectors can then read one real neighbor + // on each side and use the exact [1,3]/4 and [3,1]/4 AV1 interpolation weights. + StoreChromaPair(ref destinationBase, 0, source[0], ((3F * source[0]) + source[Math.Min(1, sourceLength - 1)]) * 0.25F, destination.Length); + i = 1; + } + + if (Vector512.IsHardwareAccelerated) + { + int oneVectorBeforeEnd = sourceLength - Vector512.Count - 1; + Vector512 quarter = Vector512.Create(0.25F); + Vector512 half = Vector512.Create(0.5F); + Vector512 three = Vector512.Create(3F); + for (; i <= oneVectorBeforeEnd; i += Vector512.Count) + { + Vector512 center = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i)); + Vector512 next = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i + 1)); + Vector512 even; + Vector512 odd; + if (isCentered) + { + Vector512 previous = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i - 1)); + even = Vector512.MultiplyAddEstimate(three, center, previous) * quarter; + odd = Vector512.MultiplyAddEstimate(three, center, next) * quarter; + } + else + { + even = center; + odd = (center + next) * half; + } + + // Vector512 has no cross-platform unpack helper. The interpolation remains 512-bit; four + // established Vector128 unpack operations only transpose the final even/odd lanes for storage. + StoreInterleavedChroma(even.GetLower().GetLower(), odd.GetLower().GetLower(), ref Unsafe.Add(ref destinationBase, i * 2)); + StoreInterleavedChroma(even.GetLower().GetUpper(), odd.GetLower().GetUpper(), ref Unsafe.Add(ref destinationBase, (i * 2) + 8)); + StoreInterleavedChroma(even.GetUpper().GetLower(), odd.GetUpper().GetLower(), ref Unsafe.Add(ref destinationBase, (i * 2) + 16)); + StoreInterleavedChroma(even.GetUpper().GetUpper(), odd.GetUpper().GetUpper(), ref Unsafe.Add(ref destinationBase, (i * 2) + 24)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorBeforeEnd = sourceLength - Vector256.Count - 1; + Vector256 quarter = Vector256.Create(0.25F); + Vector256 half = Vector256.Create(0.5F); + Vector256 three = Vector256.Create(3F); + for (; i <= oneVectorBeforeEnd; i += Vector256.Count) + { + Vector256 center = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i)); + Vector256 next = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i + 1)); + Vector256 even; + Vector256 odd; + if (isCentered) + { + Vector256 previous = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i - 1)); + even = Vector256.MultiplyAddEstimate(three, center, previous) * quarter; + odd = Vector256.MultiplyAddEstimate(three, center, next) * quarter; + } + else + { + even = center; + odd = (center + next) * half; + } + + StoreInterleavedChroma(even.GetLower(), odd.GetLower(), ref Unsafe.Add(ref destinationBase, i * 2)); + StoreInterleavedChroma(even.GetUpper(), odd.GetUpper(), ref Unsafe.Add(ref destinationBase, (i * 2) + 8)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorBeforeEnd = sourceLength - Vector128.Count - 1; + Vector128 quarter = Vector128.Create(0.25F); + Vector128 half = Vector128.Create(0.5F); + Vector128 three = Vector128.Create(3F); + for (; i <= oneVectorBeforeEnd; i += Vector128.Count) + { + Vector128 center = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i)); + Vector128 next = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i + 1)); + Vector128 even; + Vector128 odd; + if (isCentered) + { + Vector128 previous = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i - 1)); + even = Vector128.MultiplyAddEstimate(three, center, previous) * quarter; + odd = Vector128.MultiplyAddEstimate(three, center, next) * quarter; + } + else + { + even = center; + odd = (center + next) * half; + } + + StoreInterleavedChroma(even, odd, ref Unsafe.Add(ref destinationBase, i * 2)); + } + } + + for (; i < sourceLength; i++) + { + float center = source[i]; + float next = source[Math.Min(i + 1, sourceLength - 1)]; + float even = isCentered ? (source[Math.Max(i - 1, 0)] + (3F * center)) * 0.25F : center; + float odd = isCentered ? ((3F * center) + next) * 0.25F : (center + next) * 0.5F; + StoreChromaPair(ref destinationBase, i * 2, even, odd, destination.Length); + } + } + + /// + /// Stores four even chroma lanes interleaved with their four odd lanes. + /// + /// The even luma-coordinate values. + /// The odd luma-coordinate values. + /// The first destination value. + private static void StoreInterleavedChroma(Vector128 even, Vector128 odd, ref float destination) + { + Vector128 lower = Vector128_.UnpackLow(even.AsInt32(), odd.AsInt32()).AsSingle(); + Vector128 upper = Vector128_.UnpackHigh(even.AsInt32(), odd.AsInt32()).AsSingle(); + Unsafe.As>(ref destination) = lower; + Unsafe.As>(ref Unsafe.Add(ref destination, Vector128.Count)) = upper; + } + + /// + /// Stores one reconstructed chroma pair without writing beyond an odd-width destination row. + /// + /// The first destination value. + /// The even destination index. + /// The even luma-coordinate value. + /// The odd luma-coordinate value. + /// The destination length. + private static void StoreChromaPair(ref float destination, int index, float even, float odd, int length) + { + Unsafe.Add(ref destination, index) = even; + if (index + 1 < length) + { + Unsafe.Add(ref destination, index + 1) = odd; + } + } + + /// + /// Dispatches one normalized component row to its matrix-specific scalar and SIMD operator. + /// + /// 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) + { + if (isMonochrome) + { + ConvertYuvToRgbRow(red, green, blue, in parameters); + return; + } + + 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; + } + } + + /// + /// Converts one component row using the selected static scalar and SIMD operator. + /// + /// 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 + { + 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; + 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); + 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) + { + cb = (Unsafe.As>(ref Unsafe.Add(ref greenBase, i)) - chromaBias) * inverseChromaScale; + cr = (Unsafe.As>(ref Unsafe.Add(ref blueBase, i)) - chromaBias) * inverseChromaScale; + } + + 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; + } + } + + 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); + 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) + { + cb = (Unsafe.As>(ref Unsafe.Add(ref greenBase, i)) - chromaBias) * inverseChromaScale; + cr = (Unsafe.As>(ref Unsafe.Add(ref blueBase, i)) - chromaBias) * inverseChromaScale; + } + + 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; + } + } + + 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) + { + 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) + { + cb = (Unsafe.As>(ref Unsafe.Add(ref greenBase, i)) - chromaBias) * inverseChromaScale; + cr = (Unsafe.As>(ref Unsafe.Add(ref blueBase, i)) - chromaBias) * inverseChromaScale; + } + + 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; + } + } + + for (; i < length; i++) + { + 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; + } + } + + /// + /// Packs normalized RGB component rows into opaque eight-bit RGBA pixels. + /// + /// The normalized red components. + /// The normalized green components. + /// The normalized blue components. + /// The destination pixels. + private static void PackRgba32(ReadOnlySpan red, ReadOnlySpan green, ReadOnlySpan blue, Span destination) + { + ref float redBase = ref MemoryMarshal.GetReference(red); + ref float greenBase = ref MemoryMarshal.GetReference(green); + ref float blueBase = ref MemoryMarshal.GetReference(blue); + ref Rgba32 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 r = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref redBase, i)), ByteMaximum); + Vector512 g = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref greenBase, i)), ByteMaximum); + Vector512 b = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref blueBase, i)), ByteMaximum); + StoreRgba32Batch(r.GetLower().GetLower(), g.GetLower().GetLower(), b.GetLower().GetLower(), ref Unsafe.Add(ref destinationBase, i)); + StoreRgba32Batch(r.GetLower().GetUpper(), g.GetLower().GetUpper(), b.GetLower().GetUpper(), ref Unsafe.Add(ref destinationBase, i + 4)); + StoreRgba32Batch(r.GetUpper().GetLower(), g.GetUpper().GetLower(), b.GetUpper().GetLower(), ref Unsafe.Add(ref destinationBase, i + 8)); + StoreRgba32Batch(r.GetUpper().GetUpper(), g.GetUpper().GetUpper(), b.GetUpper().GetUpper(), ref Unsafe.Add(ref destinationBase, i + 12)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 r = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref redBase, i)), ByteMaximum); + Vector256 g = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref greenBase, i)), ByteMaximum); + Vector256 b = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref blueBase, i)), ByteMaximum); + StoreRgba32Batch(r.GetLower(), g.GetLower(), b.GetLower(), ref Unsafe.Add(ref destinationBase, i)); + StoreRgba32Batch(r.GetUpper(), g.GetUpper(), b.GetUpper(), ref Unsafe.Add(ref destinationBase, i + 4)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + Vector128 r = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref redBase, i)), ByteMaximum); + Vector128 g = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref greenBase, i)), ByteMaximum); + Vector128 b = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref blueBase, i)), ByteMaximum); + StoreRgba32Batch(r, g, b, ref Unsafe.Add(ref destinationBase, i)); + } + } + + for (; i < length; i++) + { + Unsafe.Add(ref destinationBase, i) = new Rgba32( + ToSample(Unsafe.Add(ref redBase, i) * ByteMaximum, ByteMaximum), + ToSample(Unsafe.Add(ref greenBase, i) * ByteMaximum, ByteMaximum), + ToSample(Unsafe.Add(ref blueBase, i) * ByteMaximum, ByteMaximum)); + } + } + + /// + /// Packs normalized RGB component rows into opaque 16-bit RGBA pixels. + /// + /// The normalized red components. + /// The normalized green components. + /// The normalized blue components. + /// The destination pixels. + private static void PackRgba64(ReadOnlySpan red, ReadOnlySpan green, ReadOnlySpan blue, Span destination) + { + ref float redBase = ref MemoryMarshal.GetReference(red); + ref float greenBase = ref MemoryMarshal.GetReference(green); + ref float blueBase = ref MemoryMarshal.GetReference(blue); + ref Rgba64 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 r = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref redBase, i)), UShortMaximum); + Vector512 g = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref greenBase, i)), UShortMaximum); + Vector512 b = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref blueBase, i)), UShortMaximum); + StoreRgba64Batch(r.GetLower().GetLower(), g.GetLower().GetLower(), b.GetLower().GetLower(), ref Unsafe.Add(ref destinationBase, i)); + StoreRgba64Batch(r.GetLower().GetUpper(), g.GetLower().GetUpper(), b.GetLower().GetUpper(), ref Unsafe.Add(ref destinationBase, i + 4)); + StoreRgba64Batch(r.GetUpper().GetLower(), g.GetUpper().GetLower(), b.GetUpper().GetLower(), ref Unsafe.Add(ref destinationBase, i + 8)); + StoreRgba64Batch(r.GetUpper().GetUpper(), g.GetUpper().GetUpper(), b.GetUpper().GetUpper(), ref Unsafe.Add(ref destinationBase, i + 12)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 r = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref redBase, i)), UShortMaximum); + Vector256 g = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref greenBase, i)), UShortMaximum); + Vector256 b = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref blueBase, i)), UShortMaximum); + StoreRgba64Batch(r.GetLower(), g.GetLower(), b.GetLower(), ref Unsafe.Add(ref destinationBase, i)); + StoreRgba64Batch(r.GetUpper(), g.GetUpper(), b.GetUpper(), ref Unsafe.Add(ref destinationBase, i + 4)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + Vector128 r = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref redBase, i)), UShortMaximum); + Vector128 g = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref greenBase, i)), UShortMaximum); + Vector128 b = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref blueBase, i)), UShortMaximum); + StoreRgba64Batch(r, g, b, ref Unsafe.Add(ref destinationBase, i)); + } + } + + for (; i < length; i++) + { + Unsafe.Add(ref destinationBase, i) = new Rgba64( + ToSample(Unsafe.Add(ref redBase, i) * UShortMaximum, UShortMaximum), + ToSample(Unsafe.Add(ref greenBase, i) * UShortMaximum, UShortMaximum), + ToSample(Unsafe.Add(ref blueBase, i) * UShortMaximum, UShortMaximum), + ushort.MaxValue); + } + } + + /// + /// Scales, rounds, and clamps four normalized components to integer storage values. + /// + /// The normalized component values. + /// The largest storage value. + /// The bounded integer values. + private static Vector128 ScaleRoundAndClampToInt32(Vector128 value, float maximum) + { + Vector128 scaled = value * Vector128.Create(maximum); + Vector128 bounded = Vector128.Min(Vector128.Max(scaled, Vector128.Zero), Vector128.Create(maximum)); + return Vector128.ConvertToInt32(Vector128.Round(bounded, MidpointRounding.AwayFromZero)); + } + + /// + /// Scales, rounds, and clamps eight normalized components to integer storage values. + /// + /// The normalized component values. + /// The largest storage value. + /// The bounded integer values. + private static Vector256 ScaleRoundAndClampToInt32(Vector256 value, float maximum) + { + Vector256 scaled = value * Vector256.Create(maximum); + Vector256 bounded = Vector256.Min(Vector256.Max(scaled, Vector256.Zero), Vector256.Create(maximum)); + return Vector256.ConvertToInt32(Vector256.Round(bounded, MidpointRounding.AwayFromZero)); + } + + /// + /// Scales, rounds, and clamps sixteen normalized components to integer storage values. + /// + /// The normalized component values. + /// The largest storage value. + /// The bounded integer values. + private static Vector512 ScaleRoundAndClampToInt32(Vector512 value, float maximum) + { + Vector512 scaled = value * Vector512.Create(maximum); + Vector512 bounded = Vector512.Min(Vector512.Max(scaled, Vector512.Zero), Vector512.Create(maximum)); + return Vector512.ConvertToInt32(Vector512.Round(bounded, MidpointRounding.AwayFromZero)); + } + + /// + /// Interleaves four red, green, and blue integer lanes into four opaque eight-bit RGBA pixels. + /// + /// The red component values. + /// The green component values. + /// The blue component values. + /// The first destination pixel. + private static void StoreRgba32Batch(Vector128 red, Vector128 green, Vector128 blue, ref Rgba32 destination) + { + Vector128 red8 = Vector128.Narrow(Vector128.Narrow(red.AsUInt32(), Vector128.Zero), Vector128.Zero); + Vector128 green8 = Vector128.Narrow(Vector128.Narrow(green.AsUInt32(), Vector128.Zero), Vector128.Zero); + Vector128 blue8 = Vector128.Narrow(Vector128.Narrow(blue.AsUInt32(), Vector128.Zero), Vector128.Zero); + Vector128 alpha8 = Vector128.Create(byte.MaxValue); + Vector128 redGreen = Vector128_.UnpackLow(red8, green8); + Vector128 blueAlpha = Vector128_.UnpackLow(blue8, alpha8); + + // Interleaving the byte pairs as 16-bit lanes produces RGBA memory order on every supported + // architecture without relying on the host integer endianness of Rgba32.PackedValue. + Vector128 rgba = Vector128_.UnpackLow(redGreen.AsInt16(), blueAlpha.AsInt16()).AsByte(); + Unsafe.As>(ref destination) = rgba; + } + + /// + /// Interleaves four red, green, and blue integer lanes into four opaque 16-bit RGBA pixels. + /// + /// The red component values. + /// The green component values. + /// The blue component values. + /// The first destination pixel. + private static void StoreRgba64Batch(Vector128 red, Vector128 green, Vector128 blue, ref Rgba64 destination) + { + Vector128 red16 = Vector128.Narrow(red.AsUInt32(), Vector128.Zero); + Vector128 green16 = Vector128.Narrow(green.AsUInt32(), Vector128.Zero); + Vector128 blue16 = Vector128.Narrow(blue.AsUInt32(), Vector128.Zero); + Vector128 alpha16 = Vector128.Create(ushort.MaxValue); + Vector128 redGreen = Vector128_.UnpackLow(red16.AsInt16(), green16.AsInt16()).AsUInt16(); + Vector128 blueAlpha = Vector128_.UnpackLow(blue16.AsInt16(), alpha16.AsInt16()).AsUInt16(); + Vector128 lower = Vector128_.UnpackLow(redGreen.AsInt32(), blueAlpha.AsInt32()).AsUInt32(); + Vector128 upper = Vector128_.UnpackHigh(redGreen.AsInt32(), blueAlpha.AsInt32()).AsUInt32(); + Unsafe.As>(ref destination) = lower; + Unsafe.As>(ref Unsafe.Add(ref destination, 2)) = upper; + } + + /// + /// Widens reconstructed eight-bit samples using exact unsigned conversions. + /// + private readonly struct ByteSampleLoader : ISampleLoader + { + /// + public static Vector128 LoadVector128(ref byte source) + { + uint packed = Unsafe.ReadUnaligned(ref source); + Vector128 samples16 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()); + return Vector128.ConvertToSingle(Vector128.WidenLower(samples16)); + } + + /// + public static Vector256 LoadVector256(ref byte source) + { + ulong packed = Unsafe.ReadUnaligned(ref source); + Vector128 samples16 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()); + Vector256 samples32 = Vector256.Create(Vector128.WidenLower(samples16), Vector128.WidenUpper(samples16)); + return Vector256.ConvertToSingle(samples32); + } + + /// + public static Vector512 LoadVector512(ref byte source) + { + Vector128 packed = Unsafe.ReadUnaligned>(ref source); + (Vector128 lower16, Vector128 upper16) = Vector128.Widen(packed); + Vector256 lower32 = Vector256.Create(Vector128.WidenLower(lower16), Vector128.WidenUpper(lower16)); + Vector256 upper32 = Vector256.Create(Vector128.WidenLower(upper16), Vector128.WidenUpper(upper16)); + return Vector512.ConvertToSingle(Vector512.Create(lower32, upper32)); + } + } + + /// + /// Widens reconstructed high-bit-depth samples using exact unsigned conversions. + /// + private readonly struct UShortSampleLoader : ISampleLoader + { + /// + public static Vector128 LoadVector128(ref ushort source) + { + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref source)); + Vector128 samples16 = Vector128.CreateScalarUnsafe(packed).AsUInt16(); + return Vector128.ConvertToSingle(Vector128.WidenLower(samples16)); + } + + /// + public static Vector256 LoadVector256(ref ushort source) + { + Vector128 samples16 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref source)); + Vector256 samples32 = Vector256.Create(Vector128.WidenLower(samples16), Vector128.WidenUpper(samples16)); + return Vector256.ConvertToSingle(samples32); + } + + /// + public static Vector512 LoadVector512(ref ushort source) + { + Vector256 samples16 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref source)); + (Vector256 lower32, Vector256 upper32) = Vector256.Widen(samples16); + return Vector512.ConvertToSingle(Vector512.Create(lower32, upper32)); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs index b0d680d13..d2c994503 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -13,7 +14,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1; /// /// Converts between reconstructed AV1 YUV planes and packed ImageSharp pixels. /// -internal static class Av1YuvConverter +internal static partial class Av1YuvConverter { /// /// The largest value represented by an eight-bit packed RGB component. @@ -88,112 +89,16 @@ internal static class Av1YuvConverter ? new ConstantLuminanceScales(transferCharacteristics, kr, kb) : default; - Buffer2DRegion yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0); - bool isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400; - int subX = frameBuffer.ColorConfig.SubSamplingX ? 1 : 0; - int subY = frameBuffer.ColorConfig.SubSamplingY ? 1 : 0; - Buffer2DRegion uPlane = isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.U, subX, subY); - Buffer2DRegion vPlane = isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.V, subX, subY); - bool isEightBit = frameBuffer.BitDepth == Av1BitDepth.EightBit; - using IMemoryOwner? rowOwner = isEightBit - ? configuration.MemoryAllocator.Allocate(image.Width) - : null; - - using IMemoryOwner? highBitDepthRowOwner = isEightBit - ? null - : configuration.MemoryAllocator.Allocate(image.Width); - - Span rgbRow = rowOwner is null ? Span.Empty : rowOwner.GetSpan()[..image.Width]; - Span highBitDepthRgbRow = highBitDepthRowOwner is null - ? Span.Empty - : highBitDepthRowOwner.GetSpan()[..image.Width]; - - for (int y = 0; y < image.Height; y++) + YuvToRgbParameters parameters = new(kr, kg, kb, transferCharacteristics, in constantLuminanceScales, lumaBias, lumaScale, chromaBias, chromaScale); + if (frameBuffer.BitDepth == Av1BitDepth.EightBit) { - int y0 = 0; - int y1 = 0; - int y1Weight = 0; - if (!isMonochrome) - { - GetChromaCoordinates( - y, - subY, - subY != 0 && frameBuffer.ColorConfig.ChromaSamplePosition != ObuChromoSamplePosition.Colocated, - uPlane.Height - 1, - out y0, - out y1, - out y1Weight); - } - - if (isEightBit) - { - ConvertYuvToRgbRow( - yPlane.DangerousGetRowSpan(y), - isMonochrome ? default : uPlane.DangerousGetRowSpan(y0), - isMonochrome ? default : uPlane.DangerousGetRowSpan(y1), - isMonochrome ? default : vPlane.DangerousGetRowSpan(y0), - isMonochrome ? default : vPlane.DangerousGetRowSpan(y1), - y1Weight, - rgbRow, - isMonochrome, - subX, - subY, - frameBuffer.ColorConfig.ChromaSamplePosition, - mode, - kr, - kg, - kb, - transferCharacteristics, - in constantLuminanceScales, - lumaBias, - lumaScale, - chromaBias, - chromaScale, - sampleMaximum); - } - else - { - // Staging high-bit-depth samples through Rgb48 preserves their precision while still using the - // optimized packed-pixel conversion paths shared by the rest of ImageSharp. - ConvertYuvToRgbRow( - frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, y, 0, 0), - isMonochrome ? default : frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y0, subX, subY), - isMonochrome ? default : frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y1, subX, subY), - isMonochrome ? default : frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y0, subX, subY), - isMonochrome ? default : frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y1, subX, subY), - y1Weight, - highBitDepthRgbRow, - isMonochrome, - subX, - subY, - frameBuffer.ColorConfig.ChromaSamplePosition, - mode, - kr, - kg, - kb, - transferCharacteristics, - in constantLuminanceScales, - lumaBias, - lumaScale, - chromaBias, - chromaScale, - sampleMaximum); - } - - if (isEightBit) - { - PixelOperations.Instance.FromRgb24( - configuration, - rgbRow, - image.PixelBuffer.DangerousGetRowSpan(y)); - } - else - { - PixelOperations.Instance.FromRgb48( - configuration, - highBitDepthRgbRow, - image.PixelBuffer.DangerousGetRowSpan(y)); - } + YuvToRgbRowOperation operation = new(configuration, frameBuffer, image, mode, in parameters); + ParallelRowIterator.IterateRows, float>(configuration, image.Bounds, in operation); + } + else + { + YuvToRgbRowOperation operation = new(configuration, frameBuffer, image, mode, in parameters); + ParallelRowIterator.IterateRows, float>(configuration, image.Bounds, in operation); } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs index a401f45f3..97513743d 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs @@ -460,29 +460,29 @@ internal ref struct Av1SymbolDecoder } /// - /// Reads the binary split-versus-horizontal decision used at a clipped right tile boundary. + /// Reads the binary split-versus-horizontal decision used at a clipped bottom tile boundary. /// /// The current block size. /// The partition probability context. /// or . public Av1PartitionType ReadSplitOrHorizontal(Av1BlockSize blockSize, int context) { - Av1Distribution distribution = GetSplitOrHorizontalDistribution(this.tilePartitionTypes, blockSize, context); + uint frequency = GetSplitOrHorizontalFrequency(this.tilePartitionTypes, blockSize, context); ref Av1SymbolReader r = ref this.reader; - return r.ReadSymbol(distribution) > 0 ? Av1PartitionType.Split : Av1PartitionType.Horizontal; + return r.ReadBoolean(frequency) ? Av1PartitionType.Split : Av1PartitionType.Horizontal; } /// - /// Reads the binary split-versus-vertical decision used at a clipped bottom tile boundary. + /// Reads the binary split-versus-vertical decision used at a clipped right tile boundary. /// /// The current block size. /// The partition probability context. /// or . public Av1PartitionType ReadSplitOrVertical(Av1BlockSize blockSize, int context) { - Av1Distribution distribution = GetSplitOrVerticalDistribution(this.tilePartitionTypes, blockSize, context); + uint frequency = GetSplitOrVerticalFrequency(this.tilePartitionTypes, blockSize, context); ref Av1SymbolReader r = ref this.reader; - return r.ReadSymbol(distribution) > 0 ? Av1PartitionType.Split : Av1PartitionType.Vertical; + return r.ReadBoolean(frequency) ? Av1PartitionType.Split : Av1PartitionType.Vertical; } /// @@ -1259,24 +1259,24 @@ internal ref struct Av1SymbolDecoder /// The full partition distributions. /// The current block size. /// The partition probability context. - /// The binary cumulative distribution for split versus the horizontal-like partition group. - internal static Av1Distribution GetSplitOrHorizontalDistribution(Av1Distribution[] inputs, Av1BlockSize blockSize, int context) + /// The Q15 probability of the split outcome. + internal static uint GetSplitOrHorizontalFrequency(Av1Distribution[] inputs, Av1BlockSize blockSize, int context) { Av1Distribution input = inputs[context]; - // At a clipped right edge, all syntax choices that advance horizontally collapse into one binary outcome. - uint p = Av1Distribution.ProbabilityTop; - p -= GetElementProbability(input, Av1PartitionType.Horizontal); - p -= GetElementProbability(input, Av1PartitionType.Split); - p -= GetElementProbability(input, Av1PartitionType.HorizontalA); - p -= GetElementProbability(input, Av1PartitionType.HorizontalB); - p -= GetElementProbability(input, Av1PartitionType.VerticalA); + // At the bottom edge, AV1 gathers every vertical-like partition mass into the split branch of the + // temporary binary CDF. Reading the frequency directly avoids allocating an adaptive distribution. + uint frequency = GetElementProbability(input, Av1PartitionType.Vertical); + frequency += GetElementProbability(input, Av1PartitionType.Split); + frequency += GetElementProbability(input, Av1PartitionType.HorizontalA); + frequency += GetElementProbability(input, Av1PartitionType.VerticalA); + frequency += GetElementProbability(input, Av1PartitionType.VerticalB); if (blockSize != Av1BlockSize.Block128x128) { - p -= GetElementProbability(input, Av1PartitionType.Horizontal4); + frequency += GetElementProbability(input, Av1PartitionType.Vertical4); } - return new(Av1Distribution.ProbabilityTop - p); + return frequency; } /// @@ -1285,24 +1285,24 @@ internal ref struct Av1SymbolDecoder /// The full partition distributions. /// The current block size. /// The partition probability context. - /// The binary cumulative distribution for split versus the vertical-like partition group. - internal static Av1Distribution GetSplitOrVerticalDistribution(Av1Distribution[] inputs, Av1BlockSize blockSize, int context) + /// The Q15 probability of the split outcome. + internal static uint GetSplitOrVerticalFrequency(Av1Distribution[] inputs, Av1BlockSize blockSize, int context) { Av1Distribution input = inputs[context]; - // At a clipped bottom edge, all syntax choices that advance vertically collapse into one binary outcome. - uint p = Av1Distribution.ProbabilityTop; - p -= GetElementProbability(input, Av1PartitionType.Vertical); - p -= GetElementProbability(input, Av1PartitionType.Split); - p -= GetElementProbability(input, Av1PartitionType.HorizontalA); - p -= GetElementProbability(input, Av1PartitionType.VerticalA); - p -= GetElementProbability(input, Av1PartitionType.VerticalB); + // At the right edge, AV1 gathers every horizontal-like partition mass into the split branch of the + // temporary binary CDF. Reading the frequency directly avoids allocating an adaptive distribution. + uint frequency = GetElementProbability(input, Av1PartitionType.Horizontal); + frequency += GetElementProbability(input, Av1PartitionType.Split); + frequency += GetElementProbability(input, Av1PartitionType.HorizontalA); + frequency += GetElementProbability(input, Av1PartitionType.HorizontalB); + frequency += GetElementProbability(input, Av1PartitionType.VerticalA); if (blockSize != Av1BlockSize.Block128x128) { - p -= GetElementProbability(input, Av1PartitionType.Vertical4); + frequency += GetElementProbability(input, Av1PartitionType.Horizontal4); } - return new(Av1Distribution.ProbabilityTop - p); + return frequency; } /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs index 666ae3d78..574255160 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs @@ -199,31 +199,31 @@ internal class Av1SymbolEncoder : IDisposable } /// - /// Writes the split-versus-horizontal boundary decision for a block clipped at the right tile edge. + /// Writes the split-versus-horizontal boundary decision for a block clipped at the bottom tile edge. /// /// The split or horizontal partition outcome. /// The current block size. /// The partition probability context. public void WriteSplitOrHorizontal(Av1PartitionType partitionType, Av1BlockSize blockSize, int context) { - Av1Distribution distribution = Av1SymbolDecoder.GetSplitOrHorizontalDistribution(this.tilePartitionTypes, blockSize, context); - int value = partitionType == Av1PartitionType.Split ? 1 : 0; + uint frequency = Av1SymbolDecoder.GetSplitOrHorizontalFrequency(this.tilePartitionTypes, blockSize, context); + bool value = partitionType == Av1PartitionType.Split; ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(value, distribution); + w.WriteBoolean(value, frequency); } /// - /// Writes the split-versus-vertical boundary decision for a block clipped at the bottom tile edge. + /// Writes the split-versus-vertical boundary decision for a block clipped at the right tile edge. /// /// The split or vertical partition outcome. /// The current block size. /// The partition probability context. public void WriteSplitOrVertical(Av1PartitionType partitionType, Av1BlockSize blockSize, int context) { - Av1Distribution distribution = Av1SymbolDecoder.GetSplitOrVerticalDistribution(this.tilePartitionTypes, blockSize, context); - int value = partitionType == Av1PartitionType.Split ? 1 : 0; + uint frequency = Av1SymbolDecoder.GetSplitOrVerticalFrequency(this.tilePartitionTypes, blockSize, context); + bool value = partitionType == Av1PartitionType.Split; ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(value, distribution); + w.WriteBoolean(value, frequency); } /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolReader.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolReader.cs index d690c5719..f86cbe764 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolReader.cs @@ -86,6 +86,13 @@ internal ref struct Av1SymbolReader return value; } + /// + /// Reads one non-adaptive binary symbol using the supplied Q15 probability for . + /// + /// The probability that the symbol is , scaled by 32768. + /// The decoded binary symbol. + public bool ReadBoolean(uint frequency) => this.DecodeBoolQ15(frequency); + /// /// Reads an unsigned literal in most-significant-bit-first order. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs index 6da6b857b..0249ca7c2 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs @@ -95,6 +95,13 @@ internal class Av1SymbolWriter : IDisposable } } + /// + /// Writes one non-adaptive binary symbol using the supplied Q15 probability for . + /// + /// The binary symbol. + /// The probability that the symbol is , scaled by 32768. + public void WriteBoolean(bool value, uint frequency) => this.EncodeBoolQ15(value, frequency); + /// /// Writes one equiprobable literal bit. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs index 8b187267d..30d59fe02 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; /// /// Reconstructs the coded blocks of one AV1 still-image frame into planar sample buffers. /// -internal class Av1FrameDecoder : IAv1FrameDecoder +internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable { /// /// The sequence-level superblock and color configuration. @@ -74,9 +74,14 @@ internal class Av1FrameDecoder : IAv1FrameDecoder this.inverseQuantizer = new(sequenceHeader, frameHeader); this.deQuants = new(sequenceHeader, frameHeader); this.loopFilterContext = new(sequenceHeader); - this.blockDecoder = new(this.sequenceHeader, this.frameHeader, this.frameBuffer, this.loopFilterContext); + this.blockDecoder = new(this.sequenceHeader, this.frameHeader, this.frameBuffer, this.loopFilterContext, this.inverseQuantizer); } + /// + /// Releases the pooled block-reconstruction workspaces owned by this decoder. + /// + public void Dispose() => this.blockDecoder.Dispose(); + /// /// Reconstructs every coded tile and applies the implemented in-loop frame stages in normative order. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs index 8d636d981..b181bd27d 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs @@ -597,12 +597,14 @@ internal class Av1PredictionDecoder // so the right half can consume references that already belong to the left half. if (blockSize.GetWidth() > 64 && blockModeInfoColumnOffset > 0) { - int planeBlockWidthInUnits64 = 64 >> subX; + int block64WidthInUnits = Av1BlockSize.Block64x64.Get4x4WideCount(); + int planeBlockWidthInUnits64 = block64WidthInUnits >> subX; int columnOffset64 = blockModeInfoColumnOffset % planeBlockWidthInUnits64; if (columnOffset64 == 0) { // We are at the left edge of top-right or bottom-right 64x* block. - int planeBlockHeightInUnits64 = 64 >> subY; + int block64HeightInUnits = Av1BlockSize.Block64x64.Get4x4HighCount(); + int planeBlockHeightInUnits64 = block64HeightInUnits >> subY; int rowOffset64 = blockModeInfoRowOffset % planeBlockHeightInUnits64; int planeBlockHeightInUnits = Math.Min(blockSize.Get4x4HighCount() >> subY, planeBlockHeightInUnits64); @@ -696,13 +698,15 @@ internal class Av1PredictionDecoder // Special case: For 128x128 blocks, the transform unit whose // top-right corner is at the center of the block does in fact have // pixels available at its top-right corner. - if (blockModeInfoRowOffset == 64 >> subY && - blockModeInfoColumnOffset + topRightUnitCount == 64 >> subX) + int block64WidthInUnits = Av1BlockSize.Block64x64.Get4x4WideCount(); + int block64HeightInUnits = Av1BlockSize.Block64x64.Get4x4HighCount(); + if (blockModeInfoRowOffset == block64HeightInUnits >> subY && + blockModeInfoColumnOffset + topRightUnitCount == block64WidthInUnits >> subX) { return true; } - int planeBlockWidthInUnits64 = 64 >> subX; + int planeBlockWidthInUnits64 = block64WidthInUnits >> subX; int blockModeInfoColumnOffset64 = blockModeInfoColumnOffset % planeBlockWidthInUnits64; return blockModeInfoColumnOffset64 + topRightUnitCount < planeBlockWidthInUnits64; } @@ -933,7 +937,7 @@ internal class Av1PredictionDecoder { aboveNeighbor[..topPixelCount].CopyTo(aboveRow); int i = topPixelCount; - if (needRight && topPixelCount > 0) + if (topRightPixelCount > 0) { Guard.IsTrue(topPixelCount == transformWidth, nameof(topPixelCount), string.Empty); aboveNeighbor.Slice(transformWidth, topRightPixelCount).CopyTo(aboveRow[transformWidth..]); diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs index 338a987b5..6e8b3fb9e 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs @@ -666,12 +666,12 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable { if (this.SequenceHeader.ColorConfig.SubSamplingY && block4x4Height == 1) { - partitionInfo.AvailableAboveForChroma = this.IsInside(rowIndex - 2, columnIndex); + partitionInfo.AvailableAboveForChroma = IsInside(tileInfo, rowIndex - 2, columnIndex); } if (this.SequenceHeader.ColorConfig.SubSamplingX && block4x4Width == 1) { - partitionInfo.AvailableLeftForChroma = this.IsInside(rowIndex, columnIndex - 2); + partitionInfo.AvailableLeftForChroma = IsInside(tileInfo, rowIndex, columnIndex - 2); } } @@ -1267,7 +1267,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable stepColumn = transformSizeUv.Get4x4WideCount(); stepRow = transformSizeUv.Get4x4HighCount(); - unitHeight = Av1Math.RoundPowerOf2(Math.Min(height + idx, maxBlockHigh), subY ? 1 : 0); + unitHeight = Av1Math.RoundPowerOf2(Math.Min(height + idy, maxBlockHigh), subY ? 1 : 0); unitWidth = Av1Math.RoundPowerOf2(Math.Min(width + idx, maxBlockWide), subX ? 1 : 0); for (int blockRow = idy; blockRow < unitHeight; blockRow += stepRow) { @@ -2246,14 +2246,15 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable /// /// Determines whether a frame-relative mode-information position lies inside the active tile. /// + /// The active tile boundaries. /// The frame-relative mode-information row. /// The frame-relative mode-information column. /// when the position lies within the active tile; otherwise, . - private bool IsInside(int rowIndex, int columnIndex) => - columnIndex >= this.FrameHeader.TilesInfo.TileColumnCount && - columnIndex < this.FrameHeader.TilesInfo.TileColumnCount && - rowIndex >= this.FrameHeader.TilesInfo.TileRowCount && - rowIndex < this.FrameHeader.TilesInfo.TileRowCount; + private static bool IsInside(Av1TileInfo tileInfo, int rowIndex, int columnIndex) => + columnIndex >= tileInfo.ModeInfoColumnStart && + columnIndex < tileInfo.ModeInfoColumnEnd && + rowIndex >= tileInfo.ModeInfoRowStart && + rowIndex < tileInfo.ModeInfoRowEnd; /// /// Derives the partition entropy context from the current split bit of the above and left neighbors. diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs index f9bd74a5e..708217894 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; /// /// Reconstructs AV1 transform blocks by combining prediction, inverse quantization, and inverse transforms. /// -internal class Av1BlockDecoder +internal sealed class Av1BlockDecoder : IDisposable { /// /// The sequence-level syntax that determines superblock size, plane layout, and sample depth. @@ -37,6 +37,21 @@ internal class Av1BlockDecoder /// private readonly Av1LoopFilterContext loopFilterContext; + /// + /// The frame-owned inverse quantizer carrying the active superblock delta-Q state. + /// + private readonly Av1InverseQuantizer inverseQuantizer; + + /// + /// Owns the reusable raster-order inverse-quantization buffer. + /// + private readonly IMemoryOwner inverseQuantizationOwner; + + /// + /// Owns the reusable two-dimensional inverse-transform workspace. + /// + private readonly IMemoryOwner transformWorkspaceOwner; + /// /// Indicates whether transform traversal must also populate loop-filter parameters. /// @@ -59,16 +74,19 @@ internal class Av1BlockDecoder /// The decoded frame header. /// The frame buffer receiving reconstructed samples. /// The transform-size map populated while reconstructing blocks. + /// The inverse quantizer carrying the active superblock delta-Q state. public Av1BlockDecoder( ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, Av1FrameBuffer frameBuffer, - Av1LoopFilterContext loopFilterContext) + Av1LoopFilterContext loopFilterContext, + Av1InverseQuantizer inverseQuantizer) { this.sequenceHeader = sequenceHeader; this.frameHeader = frameHeader; this.frameBuffer = frameBuffer; this.loopFilterContext = loopFilterContext; + this.inverseQuantizer = inverseQuantizer; int ySize = (1 << this.sequenceHeader.SuperblockSizeLog2) * (1 << this.sequenceHeader.SuperblockSizeLog2); // One scratch plane is reused for every transform unit. Its maximum size must cover a complete superblock @@ -77,7 +95,8 @@ internal class Av1BlockDecoder (this.sequenceHeader.ColorConfig.SubSamplingX ? ySize >> 2 : ySize) + (this.sequenceHeader.ColorConfig.SubSamplingY ? ySize >> 2 : ySize); - this.CurrentInverseQuantizationCoefficients = new int[inverseQuantizationSize]; + this.inverseQuantizationOwner = this.frameBuffer.MemoryAllocator.Allocate(inverseQuantizationSize); + this.transformWorkspaceOwner = this.frameBuffer.MemoryAllocator.Allocate(Av1TransformWorkspace.MaximumLength); this.isLoopFilterEnabled = frameHeader.LoopFilterParameters.FilterLevel[0] != 0 || frameHeader.LoopFilterParameters.FilterLevel[1] != 0; @@ -88,7 +107,16 @@ internal class Av1BlockDecoder /// /// Gets the reusable raster-order coefficient buffer populated by inverse quantization. /// - public int[] CurrentInverseQuantizationCoefficients { get; private set; } + public Span CurrentInverseQuantizationCoefficients => this.inverseQuantizationOwner.Memory.Span; + + /// + /// Releases the pooled reconstruction workspaces owned by this decoder. + /// + public void Dispose() + { + this.transformWorkspaceOwner.Dispose(); + this.inverseQuantizationOwner.Dispose(); + } /// /// Resets the per-plane packed coefficient cursors before reconstructing a superblock. @@ -113,8 +141,7 @@ internal class Av1BlockDecoder /// The tile boundaries used to determine neighbor availability. public void DecodeBlock(Av1BlockModeInfo modeInfo, Point modeInfoPosition, Av1BlockSize blockSize, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo) { - using IMemoryOwner transformWorkspaceOwner = this.frameBuffer.MemoryAllocator.Allocate(Av1TransformWorkspace.MaximumLength); - Span transformWorkspace = transformWorkspaceOwner.Memory.Span; + Span transformWorkspace = this.transformWorkspaceOwner.Memory.Span; ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig; Av1TransformType transformType; @@ -158,8 +185,6 @@ internal class Av1BlockDecoder bool highBitDepth = this.frameBuffer.BytesPerSample == 2; Av1PredictionDecoder predictionDecoder = new(this.sequenceHeader, this.frameHeader); - Av1InverseQuantizer inverseQuantizer = new(this.sequenceHeader, this.frameHeader); - for (int plane = 0; plane < colorConfig.PlaneCount; plane++) { int subX = (plane > 0) && colorConfig.SubSamplingX ? 1 : 0; @@ -287,7 +312,7 @@ internal class Av1BlockDecoder transformType = transformInfo[0].Type; // Inverse quantization writes raster coefficients into the reusable superblock scratch plane. - numberOfCoefficients = inverseQuantizer.InverseQuantize( + numberOfCoefficients = this.inverseQuantizer.InverseQuantize( modeInfo, coefficients, quantizationCoefficients, transformType, transformSize, (Av1Plane)plane); if (numberOfCoefficients != 0) { @@ -297,10 +322,11 @@ internal class Av1BlockDecoder if (highBitDepth) { - // AV1 high-bit-depth reconstruction stores unsigned samples in the existing signed 16-bit transform representation. + // Prediction receives a reference-prefixed span beginning on the previous row. Inverse + // reconstruction operates on the transform itself, so advance to the first destination row. Av1InverseTransformer.ReconstructHighBitDepth( quantizationCoefficients, - highBitDepthTransformBlockReconstructionBuffer, + highBitDepthTransformBlockReconstructionBuffer[reconstructionStride..], reconstructionStride, transformSize, transformType, @@ -312,10 +338,11 @@ internal class Av1BlockDecoder } else { - // The byte pipeline adds the inverse-transform residual directly to the prediction block. + // Keep the reference-prefix convention local to prediction; residuals are added at the + // first reconstructed row rather than the top-neighbor row. Av1InverseTransformer.Reconstruct8Bit( quantizationCoefficients, - transformBlockReconstructionBuffer, + transformBlockReconstructionBuffer[reconstructionStride..], reconstructionStride, transformSize, transformType, diff --git a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs index fe415b60e..dedc2e87c 100644 --- a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs +++ b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs @@ -2079,7 +2079,6 @@ internal sealed class HeifDecoderCore : ImageDecoderCore this.ApplyItemColorMetadata(image.Metadata, itemToDecode); this.ApplyItemHdrMetadata(image.Metadata, itemToDecode); this.ApplyAssociatedMetadata(image.Metadata, rootItem, buffers); - _ = this.TryConvertIccProfile(image); } // MIAF defines crop, rotation, and mirror as presentation operations in that order. Applying the @@ -2089,6 +2088,11 @@ internal sealed class HeifDecoderCore : ImageDecoderCore if (!this.Options.SkipMetadata) { this.ApplyItemPixelAspectRatioMetadata(image.Metadata, itemToDecode); + + // ICC conversion belongs to the presented RGB image. Running it after alpha, grid composition, crop, + // rotation, and mirroring keeps still images aligned with the sequence path and avoids converting + // pixels removed by a clean-aperture crop. + _ = this.TryConvertIccProfile(image); } // The decoder determines the compression of the pixels that were actually returned, including grid tiles diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs index 72f06e896..d2111811a 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs @@ -4,6 +4,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; using SixLabors.ImageSharp.Memory; @@ -14,6 +15,52 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; [Trait("Format", "Avif")] public class Av1TilingTests { + /// + /// Verifies the decoded block geometry and prediction modes against libaom inspection output for a real AVIF image item. + /// + [Fact] + public void ParsedRealAvifModeMapMatchesLibaom() + { + string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Heif.ParisIccExifXmpAvif); + byte[] content = File.ReadAllBytes(filePath); + + // The fixture's iloc box identifies item 1 as the AV1 payload at offset 0x17A8 with length 0x3AE4. + const int codedItemOffset = 0x17A8; + const int codedItemLength = 0x3AE4; + using Av1Decoder decoder = new(Configuration.Default); + using Image image = decoder.Decode(content.AsSpan(codedItemOffset, codedItemLength)); + Av1FrameInfo frameInfo = Assert.IsType(decoder.FrameInfo); + ObuFrameHeader frameHeader = Assert.IsType(decoder.FrameHeader); + Span blockSizeCounts = stackalloc int[(int)Av1BlockSize.AllSizes]; + Span modeCounts = stackalloc int[(int)Av1PredictionMode.IntraModes]; + + for (int row = 0; row < frameHeader.ModeInfoRowCount; row++) + { + for (int column = 0; column < frameHeader.ModeInfoColumnCount; column++) + { + Av1BlockModeInfo modeInfo = frameInfo.GetModeInfoAt(new Point(column, row)); + blockSizeCounts[(int)modeInfo.BlockSize]++; + modeCounts[(int)modeInfo.YMode]++; + } + } + + // These counts come from the 102 by 76 mode-info maps emitted by libaom 3.14.1's inspect tool. + int[] expectedBlockSizeCounts = new int[(int)Av1BlockSize.AllSizes]; + expectedBlockSizeCounts[(int)Av1BlockSize.Block8x8] = 3176; + expectedBlockSizeCounts[(int)Av1BlockSize.Block8x16] = 48; + expectedBlockSizeCounts[(int)Av1BlockSize.Block16x16] = 4080; + expectedBlockSizeCounts[(int)Av1BlockSize.Block32x32] = 448; + + int[] expectedModeCounts = new int[(int)Av1PredictionMode.IntraModes]; + expectedModeCounts[(int)Av1PredictionMode.DC] = 3020; + expectedModeCounts[(int)Av1PredictionMode.Vertical] = 228; + expectedModeCounts[(int)Av1PredictionMode.Horizontal] = 2360; + expectedModeCounts[(int)Av1PredictionMode.Smooth] = 2144; + + Assert.Equal(expectedBlockSizeCounts, blockSizeCounts.ToArray()); + Assert.Equal(expectedModeCounts, modeCounts.ToArray()); + } + [Fact] public void DecoderReadsFirstTile() { diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransferFunctionsTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransferFunctionsTests.cs new file mode 100644 index 000000000..8b6b0de31 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransferFunctionsTests.cs @@ -0,0 +1,157 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies scalar and SIMD parity for every H.273 transfer characteristic consumed by AV1 color conversion. +/// +[Trait("Format", "Avif")] +public class Av1TransferFunctionsTests +{ + private static readonly float[] SignalValues = + [ + -0.5F, + -0.25F, + -0.081247F, + -0.01F, + 0F, + 0.0031308F, + 0.01F, + 0.04045F, + 1F / 12F, + 0.18F, + 0.25F, + 0.5F, + 0.75F, + 1F, + 1.25F, + 2F, + ]; + + /// + /// Gets every defined AV1-signallable transfer characteristic, including the deterministic unspecified fallback. + /// + public static TheoryData TransferCharacteristics { get; } = new() + { + (int)ObuTransferCharacteristics.Bt709, + (int)ObuTransferCharacteristics.Unspecified, + (int)ObuTransferCharacteristics.Bt470M, + (int)ObuTransferCharacteristics.Bt470BG, + (int)ObuTransferCharacteristics.Bt601, + (int)ObuTransferCharacteristics.Smpte240, + (int)ObuTransferCharacteristics.Linear, + (int)ObuTransferCharacteristics.Log100, + (int)ObuTransferCharacteristics.Log100Sqrt10, + (int)ObuTransferCharacteristics.Iec61966, + (int)ObuTransferCharacteristics.Bt1361, + (int)ObuTransferCharacteristics.Srgb, + (int)ObuTransferCharacteristics.Bt202010Bit, + (int)ObuTransferCharacteristics.Bt202012Bit, + (int)ObuTransferCharacteristics.Smpte2084, + (int)ObuTransferCharacteristics.Smpte428, + (int)ObuTransferCharacteristics.Hlg, + }; + + /// + /// Verifies that every SIMD width matches the scalar inverse transfer function at curve transitions, extrema, and extended-range values. + /// + /// The transfer-characteristic code point under test. + [Theory] + [MemberData(nameof(TransferCharacteristics))] + public void ToLinearSimdMatchesScalar(int transferCharacteristicsValue) + { + ObuTransferCharacteristics transferCharacteristics = (ObuTransferCharacteristics)transferCharacteristicsValue; + float[] expected = SignalValues.Select(value => Av1TransferFunctions.ToLinear(transferCharacteristics, value)).ToArray(); + + Vector128 vector128 = Av1TransferFunctions.ToLinear(transferCharacteristics, Vector128.Create(SignalValues.AsSpan(0, Vector128.Count))); + Vector256 vector256 = Av1TransferFunctions.ToLinear(transferCharacteristics, Vector256.Create(SignalValues.AsSpan(0, Vector256.Count))); + Vector512 vector512 = Av1TransferFunctions.ToLinear(transferCharacteristics, Vector512.Create(SignalValues)); + + AssertVectorMatchesScalar(expected, vector128, transferCharacteristics); + AssertVectorMatchesScalar(expected, vector256, transferCharacteristics); + AssertVectorMatchesScalar(expected, vector512, transferCharacteristics); + } + + /// + /// Verifies that every SIMD width matches the scalar forward transfer function at curve transitions, extrema, and extended-range values. + /// + /// The transfer-characteristic code point under test. + [Theory] + [MemberData(nameof(TransferCharacteristics))] + public void ToGammaSimdMatchesScalar(int transferCharacteristicsValue) + { + ObuTransferCharacteristics transferCharacteristics = (ObuTransferCharacteristics)transferCharacteristicsValue; + float[] expected = SignalValues.Select(value => Av1TransferFunctions.ToGamma(transferCharacteristics, value)).ToArray(); + + Vector128 vector128 = Av1TransferFunctions.ToGamma(transferCharacteristics, Vector128.Create(SignalValues.AsSpan(0, Vector128.Count))); + Vector256 vector256 = Av1TransferFunctions.ToGamma(transferCharacteristics, Vector256.Create(SignalValues.AsSpan(0, Vector256.Count))); + Vector512 vector512 = Av1TransferFunctions.ToGamma(transferCharacteristics, Vector512.Create(SignalValues)); + + AssertVectorMatchesScalar(expected, vector128, transferCharacteristics); + AssertVectorMatchesScalar(expected, vector256, transferCharacteristics); + AssertVectorMatchesScalar(expected, vector512, transferCharacteristics); + } + + /// + /// Compares four SIMD lanes with their scalar results. + /// + /// The scalar results. + /// The SIMD results. + /// The transfer characteristic under test. + private static void AssertVectorMatchesScalar(ReadOnlySpan expected, Vector128 actual, ObuTransferCharacteristics transferCharacteristics) + { + for (int i = 0; i < Vector128.Count; i++) + { + AssertClose(expected[i], actual.GetElement(i), transferCharacteristics, i, 128); + } + } + + /// + /// Compares eight SIMD lanes with their scalar results. + /// + /// The scalar results. + /// The SIMD results. + /// The transfer characteristic under test. + private static void AssertVectorMatchesScalar(ReadOnlySpan expected, Vector256 actual, ObuTransferCharacteristics transferCharacteristics) + { + for (int i = 0; i < Vector256.Count; i++) + { + AssertClose(expected[i], actual.GetElement(i), transferCharacteristics, i, 256); + } + } + + /// + /// Compares sixteen SIMD lanes with their scalar results. + /// + /// The scalar results. + /// The SIMD results. + /// The transfer characteristic under test. + private static void AssertVectorMatchesScalar(ReadOnlySpan expected, Vector512 actual, ObuTransferCharacteristics transferCharacteristics) + { + for (int i = 0; i < Vector512.Count; i++) + { + AssertClose(expected[i], actual.GetElement(i), transferCharacteristics, i, 512); + } + } + + /// + /// Verifies one SIMD lane within the tolerance of the runtime vector exponential and logarithm kernels. + /// + /// The scalar result. + /// The SIMD result. + /// The transfer characteristic under test. + /// The SIMD lane index. + /// The SIMD register width. + private static void AssertClose(float expected, float actual, ObuTransferCharacteristics transferCharacteristics, int lane, int width) + { + float tolerance = MathF.Max(2E-5F, MathF.Abs(expected) * 2E-5F); + Assert.True( + MathF.Abs(expected - actual) <= tolerance, + $"{transferCharacteristics} at {width}-bit lane {lane}: expected {expected:R}, actual {actual:R}, tolerance {tolerance:R}."); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs index 8f12ab78e..541961647 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Heif; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; namespace SixLabors.ImageSharp.Tests.Formats.Heif; @@ -60,6 +61,88 @@ public class HeifDecoderTests Assert.Equal(HeifCompressionMethod.LegacyJpeg, heicMetadata.CompressionMethod); } + /// + /// Verifies that AVIF decoding preserves the exact embedded ICC profile bytes. + /// + [Theory] + [WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)] + public void DecodeAvifPreservesEmbeddedIccProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; + + using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); + using Image expectedPreserved = Image.Load(preserveOptions, TestFile.Create(TestImages.Heif.ParisIccExifXmpPng).Bytes); + + Assert.NotNull(preserved.Metadata.IccProfile); + Assert.NotNull(expectedPreserved.Metadata.IccProfile); + Assert.Equal(expectedPreserved.Metadata.IccProfile.ToByteArray(), preserved.Metadata.IccProfile.ToByteArray()); + } + + /// + /// Verifies that AVIF decoding converts pixels from an embedded non-sRGB ICC profile to sRGB. + /// + [Theory] + [WithFile(TestImages.Heif.PerceptualIccAvif, PixelTypes.Rgba32)] + public void DecodeAvifConvertsEmbeddedNonSrgbIccProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; + DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert }; + + using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); + using Image converted = provider.GetImage(HeifDecoder.Instance, convertOptions); + using Image expected = Image.Load(convertOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); + + Assert.NotNull(preserved.Metadata.IccProfile); + Assert.Null(converted.Metadata.IccProfile); + Assert.NotEmpty(ImageComparer.Exact.CompareImages(preserved, converted)); + + converted.DebugSave(provider, testOutputDetails: "IccConverted"); + + // The PNG is the independent RGB source used by libavif's avifenc. A tolerant comparison accounts for the + // AV1 loss while proving the AVIF ICC stage produces the same target-profile interpretation. + ImageComparer.TolerantPercentage(1F, 20).VerifySimilarity(expected, converted); + } + + /// + /// Verifies that compact profile handling removes a canonical sRGB ICC profile without changing pixels. + /// + [Theory] + [WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)] + public void DecodeAvifCompactsCanonicalSrgbIccProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; + DecoderOptions compactOptions = new() { ColorProfileHandling = ColorProfileHandling.Compact }; + + using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); + using Image compact = provider.GetImage(HeifDecoder.Instance, compactOptions); + + Assert.NotNull(preserved.Metadata.IccProfile); + Assert.Null(compact.Metadata.IccProfile); + Assert.Empty(ImageComparer.Exact.CompareImages(preserved, compact)); + } + + /// + /// Verifies that metadata skipping omits the embedded AVIF ICC profile. + /// + [Theory] + [WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)] + public void DecodeAvifSkipsEmbeddedIccProfileWithMetadata(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions options = new() + { + ColorProfileHandling = ColorProfileHandling.Preserve, + SkipMetadata = true + }; + + using Image image = provider.GetImage(HeifDecoder.Instance, options); + + Assert.Null(image.Metadata.IccProfile); + } + [Fact] public void DecodeIgnoresUnknownTopLevelBox() { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 2468d3722..a20ca719b 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1296,6 +1296,15 @@ public static class TestImages // Extremely small image, 4x4 pixels with a single solid color. public const string Orange4x4 = "Heif/Orange4x4.avif"; + // Sourced from libavif/tests/data under libavif's BSD-2-Clause license. The AVIF was independently + // encoded from the paired PNG with avifenc and carries the same ICC, Exif, and XMP metadata. + public const string ParisIccExifXmpAvif = "Heif/paris_icc_exif_xmp.avif"; + public const string ParisIccExifXmpPng = "Heif/paris_icc_exif_xmp.png"; + + // Independently encoded from Png.Icc.Perceptual with libavif 1.4.2 and libaom 3.14.1. Unlike the + // canonical-sRGB Paris profile, this perceptual profile requires an observable color conversion. + public const string PerceptualIccAvif = "Heif/perceptual_icc.avif"; + // Sourced from libavif/tests/data under libavif's BSD-2-Clause license. public const string Animated8Bit = "Heif/colors-animated-8bpc.avif"; public const string Animated8BitWithAudio = "Heif/colors-animated-8bpc-audio.avif"; diff --git a/tests/Images/Input/Heif/paris_icc_exif_xmp.avif b/tests/Images/Input/Heif/paris_icc_exif_xmp.avif new file mode 100644 index 000000000..d46a633cb --- /dev/null +++ b/tests/Images/Input/Heif/paris_icc_exif_xmp.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:961bc38b61e60b7651fa20efa24269ae2f35e4958822a81c908c9bbf9b3f66e1 +size 21132 diff --git a/tests/Images/Input/Heif/paris_icc_exif_xmp.png b/tests/Images/Input/Heif/paris_icc_exif_xmp.png new file mode 100644 index 000000000..2da25e38a --- /dev/null +++ b/tests/Images/Input/Heif/paris_icc_exif_xmp.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:308bb51c3338ed9519cdaaa79186b934a21d1748d94120ed0348eb4f193d0988 +size 153217 diff --git a/tests/Images/Input/Heif/perceptual_icc.avif b/tests/Images/Input/Heif/perceptual_icc.avif new file mode 100644 index 000000000..7258fb301 --- /dev/null +++ b/tests/Images/Input/Heif/perceptual_icc.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:438b5d2bc7b32206b62676d6d460fb0037b857aa51dd03049867f68755227b3b +size 205370