Browse Source

Implement SIMD-first AV1 color conversion

pull/2633/head
James Jackson-South 1 week ago
parent
commit
8bcb798b35
  1. 23
      HEIF_IMPLEMENTATION_PLAN.md
  2. 2
      src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs
  3. 844
      src/ImageSharp/Formats/Heif/Av1/Av1TransferFunctions.Simd.cs
  4. 2
      src/ImageSharp/Formats/Heif/Av1/Av1TransferFunctions.cs
  5. 623
      src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Operators.cs
  6. 156
      src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowOperation.cs
  7. 790
      src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Simd.cs
  8. 117
      src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs
  9. 56
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs
  10. 16
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
  11. 7
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolReader.cs
  12. 7
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs
  13. 9
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs
  14. 16
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs
  15. 17
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs
  16. 53
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs
  17. 6
      src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
  18. 47
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs
  19. 157
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransferFunctionsTests.cs
  20. 83
      tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs
  21. 9
      tests/ImageSharp.Tests/TestImages.cs
  22. 3
      tests/Images/Input/Heif/paris_icc_exif_xmp.avif
  23. 3
      tests/Images/Input/Heif/paris_icc_exif_xmp.png
  24. 3
      tests/Images/Input/Heif/perceptual_icc.avif

23
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.

2
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<TPixel>? resultFrame = null;

844
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;
/// <content>
/// Provides the SIMD implementations of the H.273 transfer characteristics used by AV1 color conversion.
/// </content>
internal static partial class Av1TransferFunctions
{
/// <summary>
/// Converts four nonlinear signal values to their H.273 linear-domain values.
/// </summary>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="value">The nonlinear signal values.</param>
/// <returns>The corresponding linear-domain values.</returns>
public static Vector128<float> ToLinear(ObuTransferCharacteristics transferCharacteristics, Vector128<float> value)
=> ToLinear<Vector128<float>, Vector128Operator>(transferCharacteristics, value);
/// <summary>
/// Converts eight nonlinear signal values to their H.273 linear-domain values.
/// </summary>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="value">The nonlinear signal values.</param>
/// <returns>The corresponding linear-domain values.</returns>
public static Vector256<float> ToLinear(ObuTransferCharacteristics transferCharacteristics, Vector256<float> value)
=> ToLinear<Vector256<float>, Vector256Operator>(transferCharacteristics, value);
/// <summary>
/// Converts sixteen nonlinear signal values to their H.273 linear-domain values.
/// </summary>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="value">The nonlinear signal values.</param>
/// <returns>The corresponding linear-domain values.</returns>
public static Vector512<float> ToLinear(ObuTransferCharacteristics transferCharacteristics, Vector512<float> value)
=> ToLinear<Vector512<float>, Vector512Operator>(transferCharacteristics, value);
/// <summary>
/// Converts four linear signal values to their H.273 nonlinear-domain values.
/// </summary>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="value">The linear signal values.</param>
/// <returns>The corresponding nonlinear-domain values.</returns>
public static Vector128<float> ToGamma(ObuTransferCharacteristics transferCharacteristics, Vector128<float> value)
=> ToGamma<Vector128<float>, Vector128Operator>(transferCharacteristics, value);
/// <summary>
/// Converts eight linear signal values to their H.273 nonlinear-domain values.
/// </summary>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="value">The linear signal values.</param>
/// <returns>The corresponding nonlinear-domain values.</returns>
public static Vector256<float> ToGamma(ObuTransferCharacteristics transferCharacteristics, Vector256<float> value)
=> ToGamma<Vector256<float>, Vector256Operator>(transferCharacteristics, value);
/// <summary>
/// Converts sixteen linear signal values to their H.273 nonlinear-domain values.
/// </summary>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="value">The linear signal values.</param>
/// <returns>The corresponding nonlinear-domain values.</returns>
public static Vector512<float> ToGamma(ObuTransferCharacteristics transferCharacteristics, Vector512<float> value)
=> ToGamma<Vector512<float>, Vector512Operator>(transferCharacteristics, value);
/// <summary>
/// Converts nonlinear signal values to their H.273 linear-domain values using the selected SIMD width.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="value">The nonlinear signal values.</param>
/// <returns>The corresponding linear-domain values.</returns>
private static TVector ToLinear<TVector, TOperator>(ObuTransferCharacteristics transferCharacteristics, TVector value)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
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<TVector, TOperator>(value);
case ObuTransferCharacteristics.Bt470M:
return Power<TVector, TOperator>(TOperator.Min(TOperator.Max(value, zero), one), 2.2F);
case ObuTransferCharacteristics.Bt470BG:
return Power<TVector, TOperator>(TOperator.Min(TOperator.Max(value, zero), one), 2.8F);
case ObuTransferCharacteristics.Smpte240:
return ToLinearSmpte240<TVector, TOperator>(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<TVector, TOperator>(value);
case ObuTransferCharacteristics.Bt1361:
return ToLinearBt1361<TVector, TOperator>(value);
case ObuTransferCharacteristics.Srgb:
return ToLinearSrgb<TVector, TOperator>(value);
case ObuTransferCharacteristics.Smpte2084:
return ToLinearPq<TVector, TOperator>(value);
case ObuTransferCharacteristics.Smpte428:
return TOperator.Divide(Power<TVector, TOperator>(TOperator.Max(value, zero), 2.6F), TOperator.Create(Smpte428Scale));
case ObuTransferCharacteristics.Hlg:
return ToLinearHlg<TVector, TOperator>(value);
default:
return ToLinearBt709<TVector, TOperator>(value);
}
}
/// <summary>
/// Converts linear signal values to their H.273 nonlinear-domain values using the selected SIMD width.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="value">The linear signal values.</param>
/// <returns>The corresponding nonlinear-domain values.</returns>
private static TVector ToGamma<TVector, TOperator>(ObuTransferCharacteristics transferCharacteristics, TVector value)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
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<TVector, TOperator>(value);
case ObuTransferCharacteristics.Bt470M:
return Power<TVector, TOperator>(TOperator.Min(TOperator.Max(value, zero), one), 1F / 2.2F);
case ObuTransferCharacteristics.Bt470BG:
return Power<TVector, TOperator>(TOperator.Min(TOperator.Max(value, zero), one), 1F / 2.8F);
case ObuTransferCharacteristics.Smpte240:
return ToGammaSmpte240<TVector, TOperator>(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<TVector, TOperator>(value);
case ObuTransferCharacteristics.Bt1361:
return ToGammaBt1361<TVector, TOperator>(value);
case ObuTransferCharacteristics.Srgb:
return ToGammaSrgb<TVector, TOperator>(value);
case ObuTransferCharacteristics.Smpte2084:
return ToGammaPq<TVector, TOperator>(value);
case ObuTransferCharacteristics.Smpte428:
return Power<TVector, TOperator>(TOperator.Multiply(TOperator.Create(Smpte428Scale), TOperator.Max(value, zero)), 1F / 2.6F);
case ObuTransferCharacteristics.Hlg:
return ToGammaHlg<TVector, TOperator>(value);
default:
return ToGammaBt709<TVector, TOperator>(value);
}
}
/// <summary>
/// Applies the inverse BT.709-family opto-electronic transfer function to a SIMD vector.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="value">The nonlinear signal values.</param>
/// <returns>The linear signal values.</returns>
private static TVector ToLinearBt709<TVector, TOperator>(TVector value)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
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<TVector, TOperator>(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);
}
/// <summary>
/// Applies the BT.709-family opto-electronic transfer function to a SIMD vector.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="value">The linear signal values.</param>
/// <returns>The nonlinear signal values.</returns>
private static TVector ToGammaBt709<TVector, TOperator>(TVector value)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
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<TVector, TOperator>(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);
}
/// <summary>
/// Applies the inverse SMPTE ST 240 opto-electronic transfer function to a SIMD vector.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="value">The nonlinear signal values.</param>
/// <returns>The linear signal values.</returns>
private static TVector ToLinearSmpte240<TVector, TOperator>(TVector value)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
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<TVector, TOperator>(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);
}
/// <summary>
/// Applies the SMPTE ST 240 opto-electronic transfer function to a SIMD vector.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="value">The linear signal values.</param>
/// <returns>The nonlinear signal values.</returns>
private static TVector ToGammaSmpte240<TVector, TOperator>(TVector value)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
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<TVector, TOperator>(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);
}
/// <summary>
/// Applies the inverse extended IEC 61966-2-4 transfer function to a SIMD vector.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="value">The nonlinear signal values.</param>
/// <returns>The linear signal values.</returns>
private static TVector ToLinearIec61966<TVector, TOperator>(TVector value)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
TVector negativeBase = TOperator.Divide(TOperator.Subtract(value, TOperator.Create(Bt709Alpha - 1F)), TOperator.Create(-Bt709Alpha));
TVector negative = TOperator.Negate(Power<TVector, TOperator>(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<TVector, TOperator>(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);
}
/// <summary>
/// Applies the extended IEC 61966-2-4 transfer function to a SIMD vector.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="value">The linear signal values.</param>
/// <returns>The nonlinear signal values.</returns>
private static TVector ToGammaIec61966<TVector, TOperator>(TVector value)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
TVector zero = TOperator.Create(0F);
TVector negative = TOperator.Add(
TOperator.Negate(TOperator.Multiply(
TOperator.Create(Bt709Alpha),
Power<TVector, TOperator>(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<TVector, TOperator>(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);
}
/// <summary>
/// Applies the inverse extended BT.1361 transfer function to a SIMD vector.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="value">The nonlinear signal values.</param>
/// <returns>The linear signal values.</returns>
private static TVector ToLinearBt1361<TVector, TOperator>(TVector value)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
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<TVector, TOperator>(TOperator.Max(negativeBase, zero), 1F / 0.45F), TOperator.Create(-4F));
TVector negativeOrPositive = TOperator.ConditionalSelect(TOperator.LessThan(value, zero), negative, ToLinearBt709<TVector, TOperator>(value));
return TOperator.ConditionalSelect(TOperator.LessThan(value, TOperator.Create(-0.25F)), TOperator.Create(-0.25F), negativeOrPositive);
}
/// <summary>
/// Applies the extended BT.1361 transfer function to a SIMD vector.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="value">The linear signal values.</param>
/// <returns>The nonlinear signal values.</returns>
private static TVector ToGammaBt1361<TVector, TOperator>(TVector value)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
TVector zero = TOperator.Create(0F);
TVector negativePower = Power<TVector, TOperator>(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<TVector, TOperator>(value));
return TOperator.ConditionalSelect(TOperator.LessThan(value, TOperator.Create(-0.25F)), TOperator.Create(-0.25F), negativeOrPositive);
}
/// <summary>
/// Applies the inverse extended IEC 61966-2-1 transfer function to a SIMD vector.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="value">The nonlinear signal values.</param>
/// <returns>The linear signal values.</returns>
private static TVector ToLinearSrgb<TVector, TOperator>(TVector value)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
TVector zero = TOperator.Create(0F);
TVector negativeBase = TOperator.Divide(TOperator.Subtract(value, TOperator.Create(SrgbAlpha - 1F)), TOperator.Create(-SrgbAlpha));
TVector negative = TOperator.Negate(Power<TVector, TOperator>(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<TVector, TOperator>(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);
}
/// <summary>
/// Applies the extended IEC 61966-2-1 transfer function to a SIMD vector.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="value">The linear signal values.</param>
/// <returns>The nonlinear signal values.</returns>
private static TVector ToGammaSrgb<TVector, TOperator>(TVector value)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
TVector zero = TOperator.Create(0F);
TVector negative = TOperator.Add(
TOperator.Negate(TOperator.Multiply(
TOperator.Create(SrgbAlpha),
Power<TVector, TOperator>(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<TVector, TOperator>(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);
}
/// <summary>
/// Applies the inverse SMPTE ST 2084 perceptual-quantizer transfer function to a SIMD vector.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="value">The nonlinear signal values.</param>
/// <returns>The normalized linear signal values.</returns>
private static TVector ToLinearPq<TVector, TOperator>(TVector value)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
TVector zero = TOperator.Create(0F);
TVector nonlinearPower = Power<TVector, TOperator>(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<TVector, TOperator>(TOperator.Divide(numerator, denominator), 1F / PqN), TOperator.Create(1F));
return TOperator.ConditionalSelect(TOperator.LessThanOrEqual(value, zero), zero, positive);
}
/// <summary>
/// Applies the SMPTE ST 2084 perceptual-quantizer transfer function to a SIMD vector.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="value">The normalized linear signal values.</param>
/// <returns>The nonlinear signal values.</returns>
private static TVector ToGammaPq<TVector, TOperator>(TVector value)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
TVector zero = TOperator.Create(0F);
TVector linearPower = Power<TVector, TOperator>(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<TVector, TOperator>(TOperator.Divide(numerator, denominator), PqM), TOperator.Create(1F));
return TOperator.ConditionalSelect(TOperator.LessThanOrEqual(value, zero), zero, positive);
}
/// <summary>
/// Applies the inverse HLG opto-electronic transfer function to a SIMD vector.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="value">The nonlinear signal values.</param>
/// <returns>The normalized linear signal values.</returns>
private static TVector ToLinearHlg<TVector, TOperator>(TVector value)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
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);
}
/// <summary>
/// Applies the HLG opto-electronic transfer function to a SIMD vector.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="value">The normalized linear signal values.</param>
/// <returns>The nonlinear signal values.</returns>
private static TVector ToGammaHlg<TVector, TOperator>(TVector value)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
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);
}
/// <summary>
/// Raises nonnegative SIMD values to a scalar exponent.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="value">The nonnegative base values.</param>
/// <param name="exponent">The exponent applied to every lane.</param>
/// <returns>The exponentiated values.</returns>
private static TVector Power<TVector, TOperator>(TVector value, float exponent)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
// 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)));
}
}
/// <summary>
/// Contains the stateless vector-width operators used by the shared H.273 transfer-function formulas.
/// </summary>
internal static class Av1TransferVectorOperators
{
/// <summary>
/// Defines the lane-wise operations required by the shared H.273 SIMD formulas.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
public interface ITransferVectorOperator<TVector>
where TVector : struct
{
/// <summary>
/// Creates a vector whose lanes contain the specified value.
/// </summary>
/// <param name="value">The value copied to every lane.</param>
/// <returns>The created vector.</returns>
public static abstract TVector Create(float value);
/// <summary>
/// Adds corresponding vector lanes.
/// </summary>
/// <param name="left">The left operand.</param>
/// <param name="right">The right operand.</param>
/// <returns>The lane-wise sum.</returns>
public static abstract TVector Add(TVector left, TVector right);
/// <summary>
/// Subtracts corresponding vector lanes.
/// </summary>
/// <param name="left">The left operand.</param>
/// <param name="right">The right operand.</param>
/// <returns>The lane-wise difference.</returns>
public static abstract TVector Subtract(TVector left, TVector right);
/// <summary>
/// Multiplies corresponding vector lanes.
/// </summary>
/// <param name="left">The left operand.</param>
/// <param name="right">The right operand.</param>
/// <returns>The lane-wise product.</returns>
public static abstract TVector Multiply(TVector left, TVector right);
/// <summary>
/// Multiplies corresponding vector lanes and adds an addend using the fastest supported estimate.
/// </summary>
/// <param name="left">The left multiplication operand.</param>
/// <param name="right">The right multiplication operand.</param>
/// <param name="addend">The value added to the product.</param>
/// <returns>The lane-wise multiply-add result.</returns>
public static abstract TVector MultiplyAddEstimate(TVector left, TVector right, TVector addend);
/// <summary>
/// Divides corresponding vector lanes.
/// </summary>
/// <param name="left">The dividend.</param>
/// <param name="right">The divisor.</param>
/// <returns>The lane-wise quotient.</returns>
public static abstract TVector Divide(TVector left, TVector right);
/// <summary>
/// Negates every vector lane.
/// </summary>
/// <param name="value">The input vector.</param>
/// <returns>The negated vector.</returns>
public static abstract TVector Negate(TVector value);
/// <summary>
/// Selects the smaller value in each pair of lanes.
/// </summary>
/// <param name="left">The left operand.</param>
/// <param name="right">The right operand.</param>
/// <returns>The lane-wise minimum.</returns>
public static abstract TVector Min(TVector left, TVector right);
/// <summary>
/// Selects the larger value in each pair of lanes.
/// </summary>
/// <param name="left">The left operand.</param>
/// <param name="right">The right operand.</param>
/// <returns>The lane-wise maximum.</returns>
public static abstract TVector Max(TVector left, TVector right);
/// <summary>
/// Compares whether each left lane is less than its right lane.
/// </summary>
/// <param name="left">The left operand.</param>
/// <param name="right">The right operand.</param>
/// <returns>The comparison mask.</returns>
public static abstract TVector LessThan(TVector left, TVector right);
/// <summary>
/// Compares whether each left lane is less than or equal to its right lane.
/// </summary>
/// <param name="left">The left operand.</param>
/// <param name="right">The right operand.</param>
/// <returns>The comparison mask.</returns>
public static abstract TVector LessThanOrEqual(TVector left, TVector right);
/// <summary>
/// Selects lanes from two vectors according to a comparison mask.
/// </summary>
/// <param name="condition">The comparison mask.</param>
/// <param name="left">The value selected for set mask lanes.</param>
/// <param name="right">The value selected for clear mask lanes.</param>
/// <returns>The selected lanes.</returns>
public static abstract TVector ConditionalSelect(TVector condition, TVector left, TVector right);
/// <summary>
/// Computes the natural exponential of every vector lane.
/// </summary>
/// <param name="value">The input vector.</param>
/// <returns>The lane-wise exponential.</returns>
public static abstract TVector Exp(TVector value);
/// <summary>
/// Computes the natural logarithm of every vector lane.
/// </summary>
/// <param name="value">The input vector.</param>
/// <returns>The lane-wise logarithm.</returns>
public static abstract TVector Log(TVector value);
/// <summary>
/// Computes the square root of every vector lane.
/// </summary>
/// <param name="value">The input vector.</param>
/// <returns>The lane-wise square root.</returns>
public static abstract TVector Sqrt(TVector value);
/// <summary>
/// Converts nonlinear signal lanes to their H.273 linear-domain values.
/// </summary>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="value">The nonlinear signal values.</param>
/// <returns>The corresponding linear-domain values.</returns>
public static abstract TVector ToLinear(ObuTransferCharacteristics transferCharacteristics, TVector value);
/// <summary>
/// Converts linear signal lanes to their H.273 nonlinear-domain values.
/// </summary>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="value">The linear signal values.</param>
/// <returns>The corresponding nonlinear-domain values.</returns>
public static abstract TVector ToGamma(ObuTransferCharacteristics transferCharacteristics, TVector value);
}
/// <summary>
/// Maps the shared transfer-function formulas to 128-bit vector operations.
/// </summary>
public readonly struct Vector128Operator : ITransferVectorOperator<Vector128<float>>
{
/// <inheritdoc/>
public static Vector128<float> Create(float value) => Vector128.Create(value);
/// <inheritdoc/>
public static Vector128<float> Add(Vector128<float> left, Vector128<float> right) => left + right;
/// <inheritdoc/>
public static Vector128<float> Subtract(Vector128<float> left, Vector128<float> right) => left - right;
/// <inheritdoc/>
public static Vector128<float> Multiply(Vector128<float> left, Vector128<float> right) => left * right;
/// <inheritdoc/>
public static Vector128<float> MultiplyAddEstimate(Vector128<float> left, Vector128<float> right, Vector128<float> addend)
=> Vector128.MultiplyAddEstimate(left, right, addend);
/// <inheritdoc/>
public static Vector128<float> Divide(Vector128<float> left, Vector128<float> right) => left / right;
/// <inheritdoc/>
public static Vector128<float> Negate(Vector128<float> value) => -value;
/// <inheritdoc/>
public static Vector128<float> Min(Vector128<float> left, Vector128<float> right) => Vector128.Min(left, right);
/// <inheritdoc/>
public static Vector128<float> Max(Vector128<float> left, Vector128<float> right) => Vector128.Max(left, right);
/// <inheritdoc/>
public static Vector128<float> LessThan(Vector128<float> left, Vector128<float> right) => Vector128.LessThan(left, right);
/// <inheritdoc/>
public static Vector128<float> LessThanOrEqual(Vector128<float> left, Vector128<float> right) => Vector128.LessThanOrEqual(left, right);
/// <inheritdoc/>
public static Vector128<float> ConditionalSelect(Vector128<float> condition, Vector128<float> left, Vector128<float> right)
=> Vector128.ConditionalSelect(condition, left, right);
/// <inheritdoc/>
public static Vector128<float> Exp(Vector128<float> value) => Vector128.Exp(value);
/// <inheritdoc/>
public static Vector128<float> Log(Vector128<float> value) => Vector128.Log(value);
/// <inheritdoc/>
public static Vector128<float> Sqrt(Vector128<float> value) => Vector128.Sqrt(value);
/// <inheritdoc/>
public static Vector128<float> ToLinear(ObuTransferCharacteristics transferCharacteristics, Vector128<float> value)
=> Av1TransferFunctions.ToLinear(transferCharacteristics, value);
/// <inheritdoc/>
public static Vector128<float> ToGamma(ObuTransferCharacteristics transferCharacteristics, Vector128<float> value)
=> Av1TransferFunctions.ToGamma(transferCharacteristics, value);
}
/// <summary>
/// Maps the shared transfer-function formulas to 256-bit vector operations.
/// </summary>
public readonly struct Vector256Operator : ITransferVectorOperator<Vector256<float>>
{
/// <inheritdoc/>
public static Vector256<float> Create(float value) => Vector256.Create(value);
/// <inheritdoc/>
public static Vector256<float> Add(Vector256<float> left, Vector256<float> right) => left + right;
/// <inheritdoc/>
public static Vector256<float> Subtract(Vector256<float> left, Vector256<float> right) => left - right;
/// <inheritdoc/>
public static Vector256<float> Multiply(Vector256<float> left, Vector256<float> right) => left * right;
/// <inheritdoc/>
public static Vector256<float> MultiplyAddEstimate(Vector256<float> left, Vector256<float> right, Vector256<float> addend)
=> Vector256.MultiplyAddEstimate(left, right, addend);
/// <inheritdoc/>
public static Vector256<float> Divide(Vector256<float> left, Vector256<float> right) => left / right;
/// <inheritdoc/>
public static Vector256<float> Negate(Vector256<float> value) => -value;
/// <inheritdoc/>
public static Vector256<float> Min(Vector256<float> left, Vector256<float> right) => Vector256.Min(left, right);
/// <inheritdoc/>
public static Vector256<float> Max(Vector256<float> left, Vector256<float> right) => Vector256.Max(left, right);
/// <inheritdoc/>
public static Vector256<float> LessThan(Vector256<float> left, Vector256<float> right) => Vector256.LessThan(left, right);
/// <inheritdoc/>
public static Vector256<float> LessThanOrEqual(Vector256<float> left, Vector256<float> right) => Vector256.LessThanOrEqual(left, right);
/// <inheritdoc/>
public static Vector256<float> ConditionalSelect(Vector256<float> condition, Vector256<float> left, Vector256<float> right)
=> Vector256.ConditionalSelect(condition, left, right);
/// <inheritdoc/>
public static Vector256<float> Exp(Vector256<float> value) => Vector256.Exp(value);
/// <inheritdoc/>
public static Vector256<float> Log(Vector256<float> value) => Vector256.Log(value);
/// <inheritdoc/>
public static Vector256<float> Sqrt(Vector256<float> value) => Vector256.Sqrt(value);
/// <inheritdoc/>
public static Vector256<float> ToLinear(ObuTransferCharacteristics transferCharacteristics, Vector256<float> value)
=> Av1TransferFunctions.ToLinear(transferCharacteristics, value);
/// <inheritdoc/>
public static Vector256<float> ToGamma(ObuTransferCharacteristics transferCharacteristics, Vector256<float> value)
=> Av1TransferFunctions.ToGamma(transferCharacteristics, value);
}
/// <summary>
/// Maps the shared transfer-function formulas to 512-bit vector operations.
/// </summary>
public readonly struct Vector512Operator : ITransferVectorOperator<Vector512<float>>
{
/// <inheritdoc/>
public static Vector512<float> Create(float value) => Vector512.Create(value);
/// <inheritdoc/>
public static Vector512<float> Add(Vector512<float> left, Vector512<float> right) => left + right;
/// <inheritdoc/>
public static Vector512<float> Subtract(Vector512<float> left, Vector512<float> right) => left - right;
/// <inheritdoc/>
public static Vector512<float> Multiply(Vector512<float> left, Vector512<float> right) => left * right;
/// <inheritdoc/>
public static Vector512<float> MultiplyAddEstimate(Vector512<float> left, Vector512<float> right, Vector512<float> addend)
=> Vector512.MultiplyAddEstimate(left, right, addend);
/// <inheritdoc/>
public static Vector512<float> Divide(Vector512<float> left, Vector512<float> right) => left / right;
/// <inheritdoc/>
public static Vector512<float> Negate(Vector512<float> value) => -value;
/// <inheritdoc/>
public static Vector512<float> Min(Vector512<float> left, Vector512<float> right) => Vector512.Min(left, right);
/// <inheritdoc/>
public static Vector512<float> Max(Vector512<float> left, Vector512<float> right) => Vector512.Max(left, right);
/// <inheritdoc/>
public static Vector512<float> LessThan(Vector512<float> left, Vector512<float> right) => Vector512.LessThan(left, right);
/// <inheritdoc/>
public static Vector512<float> LessThanOrEqual(Vector512<float> left, Vector512<float> right) => Vector512.LessThanOrEqual(left, right);
/// <inheritdoc/>
public static Vector512<float> ConditionalSelect(Vector512<float> condition, Vector512<float> left, Vector512<float> right)
=> Vector512.ConditionalSelect(condition, left, right);
/// <inheritdoc/>
public static Vector512<float> Exp(Vector512<float> value) => Vector512.Exp(value);
/// <inheritdoc/>
public static Vector512<float> Log(Vector512<float> value) => Vector512.Log(value);
/// <inheritdoc/>
public static Vector512<float> Sqrt(Vector512<float> value) => Vector512.Sqrt(value);
/// <inheritdoc/>
public static Vector512<float> ToLinear(ObuTransferCharacteristics transferCharacteristics, Vector512<float> value)
=> Av1TransferFunctions.ToLinear(transferCharacteristics, value);
/// <inheritdoc/>
public static Vector512<float> ToGamma(ObuTransferCharacteristics transferCharacteristics, Vector512<float> value)
=> Av1TransferFunctions.ToGamma(transferCharacteristics, value);
}
}

2
src/ImageSharp/Formats/Heif/Av1/Av1TransferFunctions.cs

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Applies the H.273 transfer characteristics used by AV1 color conversion.
/// </summary>
internal static class Av1TransferFunctions
internal static partial class Av1TransferFunctions
{
/// <summary>
/// The BT.709 and BT.2020 nonlinear scale factor.

623
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;
/// <content>
/// Provides the stateless scalar and SIMD operators used by AV1 YUV-to-RGB row conversion.
/// </content>
internal static partial class Av1YuvConverter
{
/// <summary>
/// Defines one H.273 YUV-to-RGB operation for scalar and SIMD traversal.
/// </summary>
private interface IYuvToRgbOperator
{
/// <summary>
/// Gets a value indicating whether the operator consumes chroma components.
/// </summary>
public static abstract bool UsesChroma { get; }
/// <summary>
/// Converts one normalized YUV sample to RGB.
/// </summary>
/// <param name="y">The normalized luma, replaced by red.</param>
/// <param name="cb">The normalized blue-difference component, replaced by green.</param>
/// <param name="cr">The normalized red-difference component, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static abstract void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters);
/// <summary>
/// Converts four normalized YUV samples to RGB.
/// </summary>
/// <param name="y">The normalized luma lanes, replaced by red.</param>
/// <param name="cb">The normalized blue-difference lanes, replaced by green.</param>
/// <param name="cr">The normalized red-difference lanes, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static abstract void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters);
/// <summary>
/// Converts eight normalized YUV samples to RGB.
/// </summary>
/// <param name="y">The normalized luma lanes, replaced by red.</param>
/// <param name="cb">The normalized blue-difference lanes, replaced by green.</param>
/// <param name="cr">The normalized red-difference lanes, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static abstract void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters);
/// <summary>
/// Converts sixteen normalized YUV samples to RGB.
/// </summary>
/// <param name="y">The normalized luma lanes, replaced by red.</param>
/// <param name="cb">The normalized blue-difference lanes, replaced by green.</param>
/// <param name="cr">The normalized red-difference lanes, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static abstract void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters);
}
/// <summary>
/// Stores the resolved H.273 values shared by every scalar and SIMD lane.
/// </summary>
private readonly struct YuvToRgbParameters
{
/// <summary>
/// Initializes a new instance of the <see cref="YuvToRgbParameters"/> struct.
/// </summary>
/// <param name="kr">The red luma coefficient.</param>
/// <param name="kg">The green luma coefficient.</param>
/// <param name="kb">The blue luma coefficient.</param>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="constantLuminanceScales">The constant-luminance chroma scales.</param>
/// <param name="lumaBias">The encoded luma bias.</param>
/// <param name="lumaScale">The encoded luma range.</param>
/// <param name="chromaBias">The encoded chroma midpoint.</param>
/// <param name="chromaScale">The encoded chroma range.</param>
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;
}
/// <summary>
/// Gets the red luma coefficient.
/// </summary>
public float Kr { get; }
/// <summary>
/// Gets the green luma coefficient.
/// </summary>
public float Kg { get; }
/// <summary>
/// Gets the blue luma coefficient.
/// </summary>
public float Kb { get; }
/// <summary>
/// Gets the signaled transfer characteristics.
/// </summary>
public ObuTransferCharacteristics TransferCharacteristics { get; }
/// <summary>
/// Gets the constant-luminance chroma scales.
/// </summary>
public ConstantLuminanceScales ConstantLuminanceScales { get; }
/// <summary>
/// Gets the encoded luma bias.
/// </summary>
public float LumaBias { get; }
/// <summary>
/// Gets the encoded luma range.
/// </summary>
public float LumaScale { get; }
/// <summary>
/// Gets the encoded chroma midpoint.
/// </summary>
public float ChromaBias { get; }
/// <summary>
/// Gets the encoded chroma range.
/// </summary>
public float ChromaScale { get; }
}
/// <summary>
/// Replicates luma into all three RGB components for monochrome input.
/// </summary>
private readonly struct MonochromeOperator : IYuvToRgbOperator
{
/// <inheritdoc/>
public static bool UsesChroma => false;
/// <inheritdoc/>
public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters)
{
cb = y;
cr = y;
}
/// <inheritdoc/>
public static void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters)
{
cb = y;
cr = y;
}
/// <inheritdoc/>
public static void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters)
{
cb = y;
cr = y;
}
/// <inheritdoc/>
public static void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters)
{
cb = y;
cr = y;
}
}
/// <summary>
/// Converts coefficient-based YCbCr signals to RGB.
/// </summary>
private readonly struct CoefficientsOperator : IYuvToRgbOperator
{
/// <inheritdoc/>
public static bool UsesChroma => true;
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public static void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertCoefficients<Vector128<float>, Vector128Operator>(ref y, ref cb, ref cr, in parameters);
/// <inheritdoc/>
public static void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertCoefficients<Vector256<float>, Vector256Operator>(ref y, ref cb, ref cr, in parameters);
/// <inheritdoc/>
public static void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertCoefficients<Vector512<float>, Vector512Operator>(ref y, ref cb, ref cr, in parameters);
}
/// <summary>
/// Maps identity-coded G, B, and R planes to RGB.
/// </summary>
private readonly struct IdentityOperator : IYuvToRgbOperator
{
/// <inheritdoc/>
public static bool UsesChroma => true;
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public static void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertIdentity<Vector128<float>, Vector128Operator>(ref y, ref cb, ref cr, in parameters);
/// <inheritdoc/>
public static void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertIdentity<Vector256<float>, Vector256Operator>(ref y, ref cb, ref cr, in parameters);
/// <inheritdoc/>
public static void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertIdentity<Vector512<float>, Vector512Operator>(ref y, ref cb, ref cr, in parameters);
}
/// <summary>
/// Converts YCgCo signals to RGB.
/// </summary>
private readonly struct YCgCoOperator : IYuvToRgbOperator
{
/// <inheritdoc/>
public static bool UsesChroma => true;
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public static void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertYCgCo<Vector128<float>, Vector128Operator>(ref y, ref cb, ref cr);
/// <inheritdoc/>
public static void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertYCgCo<Vector256<float>, Vector256Operator>(ref y, ref cb, ref cr);
/// <inheritdoc/>
public static void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertYCgCo<Vector512<float>, Vector512Operator>(ref y, ref cb, ref cr);
}
/// <summary>
/// Converts SMPTE ST 2085 YDzDx signals to RGB.
/// </summary>
private readonly struct Smpte2085Operator : IYuvToRgbOperator
{
/// <inheritdoc/>
public static bool UsesChroma => true;
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public static void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertSmpte2085<Vector128<float>, Vector128Operator>(ref y, ref cb, ref cr);
/// <inheritdoc/>
public static void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertSmpte2085<Vector256<float>, Vector256Operator>(ref y, ref cb, ref cr);
/// <inheritdoc/>
public static void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertSmpte2085<Vector512<float>, Vector512Operator>(ref y, ref cb, ref cr);
}
/// <summary>
/// Converts H.273 constant-luminance signals to RGB.
/// </summary>
private readonly struct ConstantLuminanceOperator : IYuvToRgbOperator
{
/// <inheritdoc/>
public static bool UsesChroma => true;
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public static void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertConstantLuminance<Vector128<float>, Vector128Operator>(ref y, ref cb, ref cr, in parameters);
/// <inheritdoc/>
public static void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertConstantLuminance<Vector256<float>, Vector256Operator>(ref y, ref cb, ref cr, in parameters);
/// <inheritdoc/>
public static void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertConstantLuminance<Vector512<float>, Vector512Operator>(ref y, ref cb, ref cr, in parameters);
}
/// <summary>
/// Converts the PQ-family ICtCp matrix to RGB.
/// </summary>
private readonly struct ICtCpOperator : IYuvToRgbOperator
{
/// <inheritdoc/>
public static bool UsesChroma => true;
/// <inheritdoc/>
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);
/// <inheritdoc/>
public static void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertICtCp<Vector128<float>, Vector128Operator>(ref y, ref cb, ref cr, in parameters, false);
/// <inheritdoc/>
public static void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertICtCp<Vector256<float>, Vector256Operator>(ref y, ref cb, ref cr, in parameters, false);
/// <inheritdoc/>
public static void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertICtCp<Vector512<float>, Vector512Operator>(ref y, ref cb, ref cr, in parameters, false);
}
/// <summary>
/// Converts the HLG-specific ICtCp matrix to RGB.
/// </summary>
private readonly struct ICtCpHlgOperator : IYuvToRgbOperator
{
/// <inheritdoc/>
public static bool UsesChroma => true;
/// <inheritdoc/>
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);
/// <inheritdoc/>
public static void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertICtCp<Vector128<float>, Vector128Operator>(ref y, ref cb, ref cr, in parameters, true);
/// <inheritdoc/>
public static void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertICtCp<Vector256<float>, Vector256Operator>(ref y, ref cb, ref cr, in parameters, true);
/// <inheritdoc/>
public static void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertICtCp<Vector512<float>, Vector512Operator>(ref y, ref cb, ref cr, in parameters, true);
}
/// <summary>
/// Contains the shared vector-width-independent arithmetic used by the color operators.
/// </summary>
private static class YuvToRgbMath
{
/// <summary>
/// Converts coefficient-based YCbCr SIMD lanes to RGB.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="y">The luma lanes, replaced by red.</param>
/// <param name="cb">The blue-difference lanes, replaced by green.</param>
/// <param name="cr">The red-difference lanes, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static void ConvertCoefficients<TVector, TOperator>(ref TVector y, ref TVector cb, ref TVector cr, in YuvToRgbParameters parameters)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
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;
}
/// <summary>
/// Maps identity-coded SIMD lanes to RGB.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="y">The green lanes, replaced by red.</param>
/// <param name="cb">The normalized blue plane, replaced by green.</param>
/// <param name="cr">The normalized red plane, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static void ConvertIdentity<TVector, TOperator>(ref TVector y, ref TVector cb, ref TVector cr, in YuvToRgbParameters parameters)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
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;
}
/// <summary>
/// Converts YCgCo SIMD lanes to RGB.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="y">The luma lanes, replaced by red.</param>
/// <param name="cb">The green-difference lanes, replaced by green.</param>
/// <param name="cr">The orange-difference lanes, replaced by blue.</param>
public static void ConvertYCgCo<TVector, TOperator>(ref TVector y, ref TVector cb, ref TVector cr)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
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;
}
/// <summary>
/// Converts SMPTE ST 2085 SIMD lanes to RGB.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="y">The luma lanes, replaced by red.</param>
/// <param name="cb">The blue-difference lanes, replaced by green.</param>
/// <param name="cr">The red-difference lanes, replaced by blue.</param>
public static void ConvertSmpte2085<TVector, TOperator>(ref TVector y, ref TVector cb, ref TVector cr)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
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;
}
/// <summary>
/// Converts constant-luminance SIMD lanes to RGB.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="y">The nonlinear luma lanes, replaced by red.</param>
/// <param name="cb">The blue-difference lanes, replaced by green.</param>
/// <param name="cr">The red-difference lanes, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static void ConvertConstantLuminance<TVector, TOperator>(ref TVector y, ref TVector cb, ref TVector cr, in YuvToRgbParameters parameters)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
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;
}
/// <summary>
/// Converts one ICtCp sample to RGB using the selected inverse matrix.
/// </summary>
/// <param name="y">The intensity, replaced by red.</param>
/// <param name="cb">The tritan-difference component, replaced by green.</param>
/// <param name="cr">The protan-difference component, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
/// <param name="isHlg">Whether to use the HLG-specific inverse ICtCp matrix.</param>
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);
}
/// <summary>
/// Converts ICtCp SIMD lanes to RGB using the selected inverse matrix.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="y">The intensity lanes, replaced by red.</param>
/// <param name="cb">The tritan-difference lanes, replaced by green.</param>
/// <param name="cr">The protan-difference lanes, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
/// <param name="isHlg">Whether to use the HLG-specific inverse ICtCp matrix.</param>
public static void ConvertICtCp<TVector, TOperator>(ref TVector y, ref TVector cb, ref TVector cr, in YuvToRgbParameters parameters, bool isHlg)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
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);
}
}
}

156
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;
/// <content>
/// Provides the pooled parallel row operation used by AV1 YUV-to-RGB decoding.
/// </content>
internal static partial class Av1YuvConverter
{
/// <summary>
/// Converts one reconstructed AV1 row using worker-owned pooled component, chroma, and packed-pixel storage.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <typeparam name="TSample">The reconstructed sample type.</typeparam>
/// <typeparam name="TLoader">The SIMD widening operations for the sample type.</typeparam>
private readonly struct YuvToRgbRowOperation<TPixel, TSample, TLoader> : IRowOperation<float>
where TPixel : unmanaged, IPixel<TPixel>
where TSample : unmanaged
where TLoader : struct, ISampleLoader<TSample>
{
private readonly Configuration configuration;
private readonly Av1FrameBuffer<byte> frameBuffer;
private readonly ImageFrame<TPixel> image;
private readonly Buffer2DRegion<byte> yPlane;
private readonly Buffer2DRegion<byte> uPlane;
private readonly Buffer2DRegion<byte> 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;
/// <summary>
/// Initializes a new instance of the <see cref="YuvToRgbRowOperation{TPixel, TSample, TLoader}"/> struct.
/// </summary>
/// <param name="configuration">The configuration used for packed-pixel conversion.</param>
/// <param name="frameBuffer">The reconstructed AV1 frame.</param>
/// <param name="image">The destination image frame.</param>
/// <param name="mode">The resolved H.273 conversion mode.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public YuvToRgbRowOperation(
Configuration configuration,
Av1FrameBuffer<byte> frameBuffer,
ImageFrame<TPixel> 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);
}
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
public void Invoke(int y, Span<float> span)
{
int width = this.image.Width;
Span<float> red = span[..width];
Span<float> green = span.Slice(width, width);
Span<float> blue = span.Slice(width * 2, width);
ReadOnlySpan<TSample> ySource;
if (typeof(TSample) == typeof(byte))
{
ySource = MemoryMarshal.Cast<byte, TSample>(this.yPlane.DangerousGetRowSpan(y));
}
else
{
ySource = MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, y, 0, 0));
}
ConvertSamplesToFloat<TSample, TLoader>(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<TSample> uRow0;
ReadOnlySpan<TSample> uRow1;
ReadOnlySpan<TSample> vRow0;
ReadOnlySpan<TSample> vRow1;
if (typeof(TSample) == typeof(byte))
{
uRow0 = MemoryMarshal.Cast<byte, TSample>(this.uPlane.DangerousGetRowSpan(y0));
uRow1 = MemoryMarshal.Cast<byte, TSample>(this.uPlane.DangerousGetRowSpan(y1));
vRow0 = MemoryMarshal.Cast<byte, TSample>(this.vPlane.DangerousGetRowSpan(y0));
vRow1 = MemoryMarshal.Cast<byte, TSample>(this.vPlane.DangerousGetRowSpan(y1));
}
else
{
uRow0 = MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y0, this.subX, this.subY));
uRow1 = MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y1, this.subX, this.subY));
vRow0 = MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y0, this.subX, this.subY));
vRow1 = MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y1, this.subX, this.subY));
}
Span<float> scratch0 = span.Slice(width * 3, width);
Span<float> scratch1 = span.Slice(width * 4, width);
bool isCenteredX = this.subX != 0 && (this.subY == 0 || this.chromaSamplePosition == ObuChromoSamplePosition.Unknown);
ReconstructChromaRow<TSample, TLoader>(uRow0, uRow1, y1Weight, this.subX, isCenteredX, green, scratch0, scratch1);
ReconstructChromaRow<TSample, TLoader>(vRow0, vRow1, y1Weight, this.subX, isCenteredX, blue, scratch0, scratch1);
packedOffset = width * 5;
}
ConvertYuvToRgbRow(red, green, blue, this.isMonochrome, this.mode, in this.parameters);
Span<float> packedStorage = span[packedOffset..];
Span<TPixel> destination = this.image.PixelBuffer.DangerousGetRowSpan(y);
if (typeof(TSample) == typeof(byte))
{
Span<Rgba32> packed = MemoryMarshal.Cast<float, Rgba32>(packedStorage)[..width];
PackRgba32(red, green, blue, packed);
PixelOperations<TPixel>.Instance.FromRgba32(this.configuration, packed, destination);
}
else
{
Span<Rgba64> packed = MemoryMarshal.Cast<float, Rgba64>(packedStorage)[..width];
PackRgba64(red, green, blue, packed);
PixelOperations<TPixel>.Instance.FromRgba64(this.configuration, packed, destination);
}
}
}
}

790
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;
/// <content>
/// Provides SIMD sample widening, chroma reconstruction, color traversal, and packed output for AV1 decoding.
/// </content>
internal static partial class Av1YuvConverter
{
/// <summary>
/// Defines the SIMD widening operations for one reconstructed AV1 sample type.
/// </summary>
/// <typeparam name="TSample">The reconstructed sample type.</typeparam>
private interface ISampleLoader<TSample>
where TSample : unmanaged
{
/// <summary>
/// Loads and widens four samples to single-precision lanes.
/// </summary>
/// <param name="source">The first source sample.</param>
/// <returns>The widened samples.</returns>
public static abstract Vector128<float> LoadVector128(ref TSample source);
/// <summary>
/// Loads and widens eight samples to single-precision lanes.
/// </summary>
/// <param name="source">The first source sample.</param>
/// <returns>The widened samples.</returns>
public static abstract Vector256<float> LoadVector256(ref TSample source);
/// <summary>
/// Loads and widens sixteen samples to single-precision lanes.
/// </summary>
/// <param name="source">The first source sample.</param>
/// <returns>The widened samples.</returns>
public static abstract Vector512<float> LoadVector512(ref TSample source);
}
/// <summary>
/// Widens reconstructed integer samples into a pooled float component row.
/// </summary>
/// <typeparam name="TSample">The reconstructed sample type.</typeparam>
/// <typeparam name="TLoader">The widening operations for the sample type.</typeparam>
/// <param name="source">The reconstructed samples.</param>
/// <param name="destination">The destination component row.</param>
private static void ConvertSamplesToFloat<TSample, TLoader>(ReadOnlySpan<TSample> source, Span<float> destination)
where TSample : unmanaged
where TLoader : struct, ISampleLoader<TSample>
{
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<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<float>.Count)
{
Vector512<float> samples = TLoader.LoadVector512(ref Unsafe.Add(ref sourceBase, i));
Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref destinationBase, i)) = samples;
}
}
if (Vector256.IsHardwareAccelerated)
{
int oneVectorFromEnd = length - Vector256<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{
Vector256<float> samples = TLoader.LoadVector256(ref Unsafe.Add(ref sourceBase, i));
Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref destinationBase, i)) = samples;
}
}
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = length - Vector128<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{
Vector128<float> samples = TLoader.LoadVector128(ref Unsafe.Add(ref sourceBase, i));
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref destinationBase, i)) = samples;
}
}
for (; i < length; i++)
{
Unsafe.Add(ref destinationBase, i) = GetSample(source, i);
}
}
/// <summary>
/// Reconstructs one full-width chroma row using the signaled vertical and horizontal sample positions.
/// </summary>
/// <typeparam name="TSample">The reconstructed sample type.</typeparam>
/// <typeparam name="TLoader">The widening operations for the sample type.</typeparam>
/// <param name="row0">The upper chroma row.</param>
/// <param name="row1">The lower chroma row.</param>
/// <param name="y1Weight">The lower-row weight with a denominator of four.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="isCenteredX">Whether horizontally subsampled chroma is centered between luma samples.</param>
/// <param name="destination">The reconstructed full-width chroma row.</param>
/// <param name="scratch0">The first pooled chroma scratch row.</param>
/// <param name="scratch1">The second pooled chroma scratch row.</param>
private static void ReconstructChromaRow<TSample, TLoader>(
ReadOnlySpan<TSample> row0,
ReadOnlySpan<TSample> row1,
int y1Weight,
int subX,
bool isCenteredX,
Span<float> destination,
Span<float> scratch0,
Span<float> scratch1)
where TSample : unmanaged
where TLoader : struct, ISampleLoader<TSample>
{
int sourceLength = subX == 0 ? destination.Length : (destination.Length + 1) >> 1;
Span<float> top = scratch0[..sourceLength];
ConvertSamplesToFloat<TSample, TLoader>(row0, top);
if (y1Weight != 0)
{
Span<float> bottom = scratch1[..sourceLength];
ConvertSamplesToFloat<TSample, TLoader>(row1, bottom);
InterpolateChromaRows(top, bottom, y1Weight);
}
if (subX == 0)
{
top.CopyTo(destination);
return;
}
UpsampleChromaHorizontal(top, destination, isCenteredX);
}
/// <summary>
/// Interpolates two chroma rows in place using AV1 quarter-sample weights.
/// </summary>
/// <param name="top">The upper row, replaced by the interpolated values.</param>
/// <param name="bottom">The lower row.</param>
/// <param name="bottomWeight">The lower-row weight with a denominator of four.</param>
private static void InterpolateChromaRows(Span<float> top, ReadOnlySpan<float> 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<float> topWeightVector = Vector512.Create(topWeight);
Vector512<float> bottomWeightVector = Vector512.Create((float)bottomWeight);
Vector512<float> scale = Vector512.Create(0.25F);
int oneVectorFromEnd = length - Vector512<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<float>.Count)
{
ref Vector512<float> topVector = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref topBase, i));
Vector512<float> bottomVector = Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref bottomBase, i));
topVector = Vector512.MultiplyAddEstimate(bottomWeightVector, bottomVector, topWeightVector * topVector) * scale;
}
}
if (Vector256.IsHardwareAccelerated)
{
Vector256<float> topWeightVector = Vector256.Create(topWeight);
Vector256<float> bottomWeightVector = Vector256.Create((float)bottomWeight);
Vector256<float> scale = Vector256.Create(0.25F);
int oneVectorFromEnd = length - Vector256<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{
ref Vector256<float> topVector = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref topBase, i));
Vector256<float> bottomVector = Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref bottomBase, i));
topVector = Vector256.MultiplyAddEstimate(bottomWeightVector, bottomVector, topWeightVector * topVector) * scale;
}
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<float> topWeightVector = Vector128.Create(topWeight);
Vector128<float> bottomWeightVector = Vector128.Create((float)bottomWeight);
Vector128<float> scale = Vector128.Create(0.25F);
int oneVectorFromEnd = length - Vector128<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{
ref Vector128<float> topVector = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref topBase, i));
Vector128<float> bottomVector = Unsafe.As<float, Vector128<float>>(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;
}
}
/// <summary>
/// Expands horizontally subsampled chroma to luma width using the AV1 sample-position rules.
/// </summary>
/// <param name="source">The subsampled chroma values.</param>
/// <param name="destination">The full-width chroma values.</param>
/// <param name="isCentered">Whether chroma lies between neighboring luma samples.</param>
private static void UpsampleChromaHorizontal(ReadOnlySpan<float> source, Span<float> 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<float>.Count - 1;
Vector512<float> quarter = Vector512.Create(0.25F);
Vector512<float> half = Vector512.Create(0.5F);
Vector512<float> three = Vector512.Create(3F);
for (; i <= oneVectorBeforeEnd; i += Vector512<float>.Count)
{
Vector512<float> center = Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref sourceBase, i));
Vector512<float> next = Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref sourceBase, i + 1));
Vector512<float> even;
Vector512<float> odd;
if (isCentered)
{
Vector512<float> previous = Unsafe.As<float, Vector512<float>>(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<float>.Count - 1;
Vector256<float> quarter = Vector256.Create(0.25F);
Vector256<float> half = Vector256.Create(0.5F);
Vector256<float> three = Vector256.Create(3F);
for (; i <= oneVectorBeforeEnd; i += Vector256<float>.Count)
{
Vector256<float> center = Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref sourceBase, i));
Vector256<float> next = Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref sourceBase, i + 1));
Vector256<float> even;
Vector256<float> odd;
if (isCentered)
{
Vector256<float> previous = Unsafe.As<float, Vector256<float>>(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<float>.Count - 1;
Vector128<float> quarter = Vector128.Create(0.25F);
Vector128<float> half = Vector128.Create(0.5F);
Vector128<float> three = Vector128.Create(3F);
for (; i <= oneVectorBeforeEnd; i += Vector128<float>.Count)
{
Vector128<float> center = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref sourceBase, i));
Vector128<float> next = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref sourceBase, i + 1));
Vector128<float> even;
Vector128<float> odd;
if (isCentered)
{
Vector128<float> previous = Unsafe.As<float, Vector128<float>>(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);
}
}
/// <summary>
/// Stores four even chroma lanes interleaved with their four odd lanes.
/// </summary>
/// <param name="even">The even luma-coordinate values.</param>
/// <param name="odd">The odd luma-coordinate values.</param>
/// <param name="destination">The first destination value.</param>
private static void StoreInterleavedChroma(Vector128<float> even, Vector128<float> odd, ref float destination)
{
Vector128<float> lower = Vector128_.UnpackLow(even.AsInt32(), odd.AsInt32()).AsSingle();
Vector128<float> upper = Vector128_.UnpackHigh(even.AsInt32(), odd.AsInt32()).AsSingle();
Unsafe.As<float, Vector128<float>>(ref destination) = lower;
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref destination, Vector128<float>.Count)) = upper;
}
/// <summary>
/// Stores one reconstructed chroma pair without writing beyond an odd-width destination row.
/// </summary>
/// <param name="destination">The first destination value.</param>
/// <param name="index">The even destination index.</param>
/// <param name="even">The even luma-coordinate value.</param>
/// <param name="odd">The odd luma-coordinate value.</param>
/// <param name="length">The destination length.</param>
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;
}
}
/// <summary>
/// Dispatches one normalized component row to its matrix-specific scalar and SIMD operator.
/// </summary>
/// <param name="red">The luma row, replaced by red.</param>
/// <param name="green">The blue-difference row, replaced by green.</param>
/// <param name="blue">The red-difference row, replaced by blue.</param>
/// <param name="isMonochrome">Whether the frame contains only luma samples.</param>
/// <param name="mode">The resolved H.273 conversion mode.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
private static void ConvertYuvToRgbRow(
Span<float> red,
Span<float> green,
Span<float> blue,
bool isMonochrome,
ConversionMode mode,
in YuvToRgbParameters parameters)
{
if (isMonochrome)
{
ConvertYuvToRgbRow<MonochromeOperator>(red, green, blue, in parameters);
return;
}
switch (mode)
{
case ConversionMode.Identity:
ConvertYuvToRgbRow<IdentityOperator>(red, green, blue, in parameters);
break;
case ConversionMode.YCgCo:
ConvertYuvToRgbRow<YCgCoOperator>(red, green, blue, in parameters);
break;
case ConversionMode.Smpte2085:
ConvertYuvToRgbRow<Smpte2085Operator>(red, green, blue, in parameters);
break;
case ConversionMode.ConstantLuminance:
ConvertYuvToRgbRow<ConstantLuminanceOperator>(red, green, blue, in parameters);
break;
case ConversionMode.ICtCp when parameters.TransferCharacteristics == OpenBitstreamUnit.ObuTransferCharacteristics.Hlg:
ConvertYuvToRgbRow<ICtCpHlgOperator>(red, green, blue, in parameters);
break;
case ConversionMode.ICtCp:
ConvertYuvToRgbRow<ICtCpOperator>(red, green, blue, in parameters);
break;
default:
ConvertYuvToRgbRow<CoefficientsOperator>(red, green, blue, in parameters);
break;
}
}
/// <summary>
/// Converts one component row using the selected static scalar and SIMD operator.
/// </summary>
/// <typeparam name="TOperator">The matrix-specific conversion operator.</typeparam>
/// <param name="red">The luma row, replaced by red.</param>
/// <param name="green">The blue-difference row, replaced by green.</param>
/// <param name="blue">The red-difference row, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
private static void ConvertYuvToRgbRow<TOperator>(Span<float> red, Span<float> green, Span<float> 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<float> lumaBias = Vector512.Create(parameters.LumaBias);
Vector512<float> inverseLumaScale = Vector512.Create(1F / parameters.LumaScale);
Vector512<float> chromaBias = Vector512.Create(parameters.ChromaBias);
Vector512<float> inverseChromaScale = Vector512.Create(1F / parameters.ChromaScale);
int oneVectorFromEnd = length - Vector512<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<float>.Count)
{
ref Vector512<float> redVector = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref redBase, i));
Vector512<float> y = (redVector - lumaBias) * inverseLumaScale;
Vector512<float> cb = default;
Vector512<float> cr = default;
if (TOperator.UsesChroma)
{
cb = (Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref greenBase, i)) - chromaBias) * inverseChromaScale;
cr = (Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref blueBase, i)) - chromaBias) * inverseChromaScale;
}
TOperator.Convert(ref y, ref cb, ref cr, in parameters);
redVector = y;
Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref greenBase, i)) = cb;
Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref blueBase, i)) = cr;
}
}
if (Vector256.IsHardwareAccelerated)
{
Vector256<float> lumaBias = Vector256.Create(parameters.LumaBias);
Vector256<float> inverseLumaScale = Vector256.Create(1F / parameters.LumaScale);
Vector256<float> chromaBias = Vector256.Create(parameters.ChromaBias);
Vector256<float> inverseChromaScale = Vector256.Create(1F / parameters.ChromaScale);
int oneVectorFromEnd = length - Vector256<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{
ref Vector256<float> redVector = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref redBase, i));
Vector256<float> y = (redVector - lumaBias) * inverseLumaScale;
Vector256<float> cb = default;
Vector256<float> cr = default;
if (TOperator.UsesChroma)
{
cb = (Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref greenBase, i)) - chromaBias) * inverseChromaScale;
cr = (Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref blueBase, i)) - chromaBias) * inverseChromaScale;
}
TOperator.Convert(ref y, ref cb, ref cr, in parameters);
redVector = y;
Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref greenBase, i)) = cb;
Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref blueBase, i)) = cr;
}
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<float> lumaBias = Vector128.Create(parameters.LumaBias);
Vector128<float> inverseLumaScale = Vector128.Create(1F / parameters.LumaScale);
Vector128<float> chromaBias = Vector128.Create(parameters.ChromaBias);
Vector128<float> inverseChromaScale = Vector128.Create(1F / parameters.ChromaScale);
int oneVectorFromEnd = length - Vector128<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{
ref Vector128<float> redVector = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref redBase, i));
Vector128<float> y = (redVector - lumaBias) * inverseLumaScale;
Vector128<float> cb = default;
Vector128<float> cr = default;
if (TOperator.UsesChroma)
{
cb = (Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref greenBase, i)) - chromaBias) * inverseChromaScale;
cr = (Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref blueBase, i)) - chromaBias) * inverseChromaScale;
}
TOperator.Convert(ref y, ref cb, ref cr, in parameters);
redVector = y;
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref greenBase, i)) = cb;
Unsafe.As<float, Vector128<float>>(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;
}
}
/// <summary>
/// Packs normalized RGB component rows into opaque eight-bit RGBA pixels.
/// </summary>
/// <param name="red">The normalized red components.</param>
/// <param name="green">The normalized green components.</param>
/// <param name="blue">The normalized blue components.</param>
/// <param name="destination">The destination pixels.</param>
private static void PackRgba32(ReadOnlySpan<float> red, ReadOnlySpan<float> green, ReadOnlySpan<float> blue, Span<Rgba32> 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<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<float>.Count)
{
Vector512<int> r = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref redBase, i)), ByteMaximum);
Vector512<int> g = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref greenBase, i)), ByteMaximum);
Vector512<int> b = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector512<float>>(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<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{
Vector256<int> r = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref redBase, i)), ByteMaximum);
Vector256<int> g = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref greenBase, i)), ByteMaximum);
Vector256<int> b = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector256<float>>(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<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{
Vector128<int> r = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref redBase, i)), ByteMaximum);
Vector128<int> g = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref greenBase, i)), ByteMaximum);
Vector128<int> b = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector128<float>>(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<byte>(Unsafe.Add(ref redBase, i) * ByteMaximum, ByteMaximum),
ToSample<byte>(Unsafe.Add(ref greenBase, i) * ByteMaximum, ByteMaximum),
ToSample<byte>(Unsafe.Add(ref blueBase, i) * ByteMaximum, ByteMaximum));
}
}
/// <summary>
/// Packs normalized RGB component rows into opaque 16-bit RGBA pixels.
/// </summary>
/// <param name="red">The normalized red components.</param>
/// <param name="green">The normalized green components.</param>
/// <param name="blue">The normalized blue components.</param>
/// <param name="destination">The destination pixels.</param>
private static void PackRgba64(ReadOnlySpan<float> red, ReadOnlySpan<float> green, ReadOnlySpan<float> blue, Span<Rgba64> 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<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<float>.Count)
{
Vector512<int> r = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref redBase, i)), UShortMaximum);
Vector512<int> g = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref greenBase, i)), UShortMaximum);
Vector512<int> b = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector512<float>>(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<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{
Vector256<int> r = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref redBase, i)), UShortMaximum);
Vector256<int> g = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref greenBase, i)), UShortMaximum);
Vector256<int> b = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector256<float>>(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<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{
Vector128<int> r = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref redBase, i)), UShortMaximum);
Vector128<int> g = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref greenBase, i)), UShortMaximum);
Vector128<int> b = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector128<float>>(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<ushort>(Unsafe.Add(ref redBase, i) * UShortMaximum, UShortMaximum),
ToSample<ushort>(Unsafe.Add(ref greenBase, i) * UShortMaximum, UShortMaximum),
ToSample<ushort>(Unsafe.Add(ref blueBase, i) * UShortMaximum, UShortMaximum),
ushort.MaxValue);
}
}
/// <summary>
/// Scales, rounds, and clamps four normalized components to integer storage values.
/// </summary>
/// <param name="value">The normalized component values.</param>
/// <param name="maximum">The largest storage value.</param>
/// <returns>The bounded integer values.</returns>
private static Vector128<int> ScaleRoundAndClampToInt32(Vector128<float> value, float maximum)
{
Vector128<float> scaled = value * Vector128.Create(maximum);
Vector128<float> bounded = Vector128.Min(Vector128.Max(scaled, Vector128<float>.Zero), Vector128.Create(maximum));
return Vector128.ConvertToInt32(Vector128.Round(bounded, MidpointRounding.AwayFromZero));
}
/// <summary>
/// Scales, rounds, and clamps eight normalized components to integer storage values.
/// </summary>
/// <param name="value">The normalized component values.</param>
/// <param name="maximum">The largest storage value.</param>
/// <returns>The bounded integer values.</returns>
private static Vector256<int> ScaleRoundAndClampToInt32(Vector256<float> value, float maximum)
{
Vector256<float> scaled = value * Vector256.Create(maximum);
Vector256<float> bounded = Vector256.Min(Vector256.Max(scaled, Vector256<float>.Zero), Vector256.Create(maximum));
return Vector256.ConvertToInt32(Vector256.Round(bounded, MidpointRounding.AwayFromZero));
}
/// <summary>
/// Scales, rounds, and clamps sixteen normalized components to integer storage values.
/// </summary>
/// <param name="value">The normalized component values.</param>
/// <param name="maximum">The largest storage value.</param>
/// <returns>The bounded integer values.</returns>
private static Vector512<int> ScaleRoundAndClampToInt32(Vector512<float> value, float maximum)
{
Vector512<float> scaled = value * Vector512.Create(maximum);
Vector512<float> bounded = Vector512.Min(Vector512.Max(scaled, Vector512<float>.Zero), Vector512.Create(maximum));
return Vector512.ConvertToInt32(Vector512.Round(bounded, MidpointRounding.AwayFromZero));
}
/// <summary>
/// Interleaves four red, green, and blue integer lanes into four opaque eight-bit RGBA pixels.
/// </summary>
/// <param name="red">The red component values.</param>
/// <param name="green">The green component values.</param>
/// <param name="blue">The blue component values.</param>
/// <param name="destination">The first destination pixel.</param>
private static void StoreRgba32Batch(Vector128<int> red, Vector128<int> green, Vector128<int> blue, ref Rgba32 destination)
{
Vector128<byte> red8 = Vector128.Narrow(Vector128.Narrow(red.AsUInt32(), Vector128<uint>.Zero), Vector128<ushort>.Zero);
Vector128<byte> green8 = Vector128.Narrow(Vector128.Narrow(green.AsUInt32(), Vector128<uint>.Zero), Vector128<ushort>.Zero);
Vector128<byte> blue8 = Vector128.Narrow(Vector128.Narrow(blue.AsUInt32(), Vector128<uint>.Zero), Vector128<ushort>.Zero);
Vector128<byte> alpha8 = Vector128.Create(byte.MaxValue);
Vector128<byte> redGreen = Vector128_.UnpackLow(red8, green8);
Vector128<byte> 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<byte> rgba = Vector128_.UnpackLow(redGreen.AsInt16(), blueAlpha.AsInt16()).AsByte();
Unsafe.As<Rgba32, Vector128<byte>>(ref destination) = rgba;
}
/// <summary>
/// Interleaves four red, green, and blue integer lanes into four opaque 16-bit RGBA pixels.
/// </summary>
/// <param name="red">The red component values.</param>
/// <param name="green">The green component values.</param>
/// <param name="blue">The blue component values.</param>
/// <param name="destination">The first destination pixel.</param>
private static void StoreRgba64Batch(Vector128<int> red, Vector128<int> green, Vector128<int> blue, ref Rgba64 destination)
{
Vector128<ushort> red16 = Vector128.Narrow(red.AsUInt32(), Vector128<uint>.Zero);
Vector128<ushort> green16 = Vector128.Narrow(green.AsUInt32(), Vector128<uint>.Zero);
Vector128<ushort> blue16 = Vector128.Narrow(blue.AsUInt32(), Vector128<uint>.Zero);
Vector128<ushort> alpha16 = Vector128.Create(ushort.MaxValue);
Vector128<ushort> redGreen = Vector128_.UnpackLow(red16.AsInt16(), green16.AsInt16()).AsUInt16();
Vector128<ushort> blueAlpha = Vector128_.UnpackLow(blue16.AsInt16(), alpha16.AsInt16()).AsUInt16();
Vector128<uint> lower = Vector128_.UnpackLow(redGreen.AsInt32(), blueAlpha.AsInt32()).AsUInt32();
Vector128<uint> upper = Vector128_.UnpackHigh(redGreen.AsInt32(), blueAlpha.AsInt32()).AsUInt32();
Unsafe.As<Rgba64, Vector128<uint>>(ref destination) = lower;
Unsafe.As<Rgba64, Vector128<uint>>(ref Unsafe.Add(ref destination, 2)) = upper;
}
/// <summary>
/// Widens reconstructed eight-bit samples using exact unsigned conversions.
/// </summary>
private readonly struct ByteSampleLoader : ISampleLoader<byte>
{
/// <inheritdoc/>
public static Vector128<float> LoadVector128(ref byte source)
{
uint packed = Unsafe.ReadUnaligned<uint>(ref source);
Vector128<ushort> samples16 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte());
return Vector128.ConvertToSingle(Vector128.WidenLower(samples16));
}
/// <inheritdoc/>
public static Vector256<float> LoadVector256(ref byte source)
{
ulong packed = Unsafe.ReadUnaligned<ulong>(ref source);
Vector128<ushort> samples16 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte());
Vector256<uint> samples32 = Vector256.Create(Vector128.WidenLower(samples16), Vector128.WidenUpper(samples16));
return Vector256.ConvertToSingle(samples32);
}
/// <inheritdoc/>
public static Vector512<float> LoadVector512(ref byte source)
{
Vector128<byte> packed = Unsafe.ReadUnaligned<Vector128<byte>>(ref source);
(Vector128<ushort> lower16, Vector128<ushort> upper16) = Vector128.Widen(packed);
Vector256<uint> lower32 = Vector256.Create(Vector128.WidenLower(lower16), Vector128.WidenUpper(lower16));
Vector256<uint> upper32 = Vector256.Create(Vector128.WidenLower(upper16), Vector128.WidenUpper(upper16));
return Vector512.ConvertToSingle(Vector512.Create(lower32, upper32));
}
}
/// <summary>
/// Widens reconstructed high-bit-depth samples using exact unsigned conversions.
/// </summary>
private readonly struct UShortSampleLoader : ISampleLoader<ushort>
{
/// <inheritdoc/>
public static Vector128<float> LoadVector128(ref ushort source)
{
ulong packed = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<ushort, byte>(ref source));
Vector128<ushort> samples16 = Vector128.CreateScalarUnsafe(packed).AsUInt16();
return Vector128.ConvertToSingle(Vector128.WidenLower(samples16));
}
/// <inheritdoc/>
public static Vector256<float> LoadVector256(ref ushort source)
{
Vector128<ushort> samples16 = Unsafe.ReadUnaligned<Vector128<ushort>>(ref Unsafe.As<ushort, byte>(ref source));
Vector256<uint> samples32 = Vector256.Create(Vector128.WidenLower(samples16), Vector128.WidenUpper(samples16));
return Vector256.ConvertToSingle(samples32);
}
/// <inheritdoc/>
public static Vector512<float> LoadVector512(ref ushort source)
{
Vector256<ushort> samples16 = Unsafe.ReadUnaligned<Vector256<ushort>>(ref Unsafe.As<ushort, byte>(ref source));
(Vector256<uint> lower32, Vector256<uint> upper32) = Vector256.Widen(samples16);
return Vector512.ConvertToSingle(Vector512.Create(lower32, upper32));
}
}
}

117
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;
/// <summary>
/// Converts between reconstructed AV1 YUV planes and packed ImageSharp pixels.
/// </summary>
internal static class Av1YuvConverter
internal static partial class Av1YuvConverter
{
/// <summary>
/// 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<byte> 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<byte> uPlane = isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.U, subX, subY);
Buffer2DRegion<byte> vPlane = isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.V, subX, subY);
bool isEightBit = frameBuffer.BitDepth == Av1BitDepth.EightBit;
using IMemoryOwner<Rgb24>? rowOwner = isEightBit
? configuration.MemoryAllocator.Allocate<Rgb24>(image.Width)
: null;
using IMemoryOwner<Rgb48>? highBitDepthRowOwner = isEightBit
? null
: configuration.MemoryAllocator.Allocate<Rgb48>(image.Width);
Span<Rgb24> rgbRow = rowOwner is null ? Span<Rgb24>.Empty : rowOwner.GetSpan()[..image.Width];
Span<Rgb48> highBitDepthRgbRow = highBitDepthRowOwner is null
? Span<Rgb48>.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<TPixel>.Instance.FromRgb24(
configuration,
rgbRow,
image.PixelBuffer.DangerousGetRowSpan(y));
}
else
{
PixelOperations<TPixel>.Instance.FromRgb48(
configuration,
highBitDepthRgbRow,
image.PixelBuffer.DangerousGetRowSpan(y));
}
YuvToRgbRowOperation<TPixel, byte, ByteSampleLoader> operation = new(configuration, frameBuffer, image, mode, in parameters);
ParallelRowIterator.IterateRows<YuvToRgbRowOperation<TPixel, byte, ByteSampleLoader>, float>(configuration, image.Bounds, in operation);
}
else
{
YuvToRgbRowOperation<TPixel, ushort, UShortSampleLoader> operation = new(configuration, frameBuffer, image, mode, in parameters);
ParallelRowIterator.IterateRows<YuvToRgbRowOperation<TPixel, ushort, UShortSampleLoader>, float>(configuration, image.Bounds, in operation);
}
}

56
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs

@ -460,29 +460,29 @@ internal ref struct Av1SymbolDecoder
}
/// <summary>
/// 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.
/// </summary>
/// <param name="blockSize">The current block size.</param>
/// <param name="context">The partition probability context.</param>
/// <returns><see cref="Av1PartitionType.Split"/> or <see cref="Av1PartitionType.Horizontal"/>.</returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="blockSize">The current block size.</param>
/// <param name="context">The partition probability context.</param>
/// <returns><see cref="Av1PartitionType.Split"/> or <see cref="Av1PartitionType.Vertical"/>.</returns>
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;
}
/// <summary>
@ -1259,24 +1259,24 @@ internal ref struct Av1SymbolDecoder
/// <param name="inputs">The full partition distributions.</param>
/// <param name="blockSize">The current block size.</param>
/// <param name="context">The partition probability context.</param>
/// <returns>The binary cumulative distribution for split versus the horizontal-like partition group.</returns>
internal static Av1Distribution GetSplitOrHorizontalDistribution(Av1Distribution[] inputs, Av1BlockSize blockSize, int context)
/// <returns>The Q15 probability of the split outcome.</returns>
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;
}
/// <summary>
@ -1285,24 +1285,24 @@ internal ref struct Av1SymbolDecoder
/// <param name="inputs">The full partition distributions.</param>
/// <param name="blockSize">The current block size.</param>
/// <param name="context">The partition probability context.</param>
/// <returns>The binary cumulative distribution for split versus the vertical-like partition group.</returns>
internal static Av1Distribution GetSplitOrVerticalDistribution(Av1Distribution[] inputs, Av1BlockSize blockSize, int context)
/// <returns>The Q15 probability of the split outcome.</returns>
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;
}
/// <summary>

16
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs

@ -199,31 +199,31 @@ internal class Av1SymbolEncoder : IDisposable
}
/// <summary>
/// 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.
/// </summary>
/// <param name="partitionType">The split or horizontal partition outcome.</param>
/// <param name="blockSize">The current block size.</param>
/// <param name="context">The partition probability context.</param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="partitionType">The split or vertical partition outcome.</param>
/// <param name="blockSize">The current block size.</param>
/// <param name="context">The partition probability context.</param>
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);
}
/// <summary>

7
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolReader.cs

@ -86,6 +86,13 @@ internal ref struct Av1SymbolReader
return value;
}
/// <summary>
/// Reads one non-adaptive binary symbol using the supplied Q15 probability for <see langword="true"/>.
/// </summary>
/// <param name="frequency">The probability that the symbol is <see langword="true"/>, scaled by 32768.</param>
/// <returns>The decoded binary symbol.</returns>
public bool ReadBoolean(uint frequency) => this.DecodeBoolQ15(frequency);
/// <summary>
/// Reads an unsigned literal in most-significant-bit-first order.
/// </summary>

7
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs

@ -95,6 +95,13 @@ internal class Av1SymbolWriter : IDisposable
}
}
/// <summary>
/// Writes one non-adaptive binary symbol using the supplied Q15 probability for <see langword="true"/>.
/// </summary>
/// <param name="value">The binary symbol.</param>
/// <param name="frequency">The probability that the symbol is <see langword="true"/>, scaled by 32768.</param>
public void WriteBoolean(bool value, uint frequency) => this.EncodeBoolQ15(value, frequency);
/// <summary>
/// Writes one equiprobable literal bit.
/// </summary>

9
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
/// <summary>
/// Reconstructs the coded blocks of one AV1 still-image frame into planar sample buffers.
/// </summary>
internal class Av1FrameDecoder : IAv1FrameDecoder
internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable
{
/// <summary>
/// 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);
}
/// <summary>
/// Releases the pooled block-reconstruction workspaces owned by this decoder.
/// </summary>
public void Dispose() => this.blockDecoder.Dispose();
/// <summary>
/// Reconstructs every coded tile and applies the implemented in-loop frame stages in normative order.
/// </summary>

16
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..]);

17
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
/// <summary>
/// Determines whether a frame-relative mode-information position lies inside the active tile.
/// </summary>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <param name="rowIndex">The frame-relative mode-information row.</param>
/// <param name="columnIndex">The frame-relative mode-information column.</param>
/// <returns><see langword="true"/> when the position lies within the active tile; otherwise, <see langword="false"/>.</returns>
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;
/// <summary>
/// Derives the partition entropy context from the current split bit of the above and left neighbors.

53
src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs

@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <summary>
/// Reconstructs AV1 transform blocks by combining prediction, inverse quantization, and inverse transforms.
/// </summary>
internal class Av1BlockDecoder
internal sealed class Av1BlockDecoder : IDisposable
{
/// <summary>
/// The sequence-level syntax that determines superblock size, plane layout, and sample depth.
@ -37,6 +37,21 @@ internal class Av1BlockDecoder
/// </summary>
private readonly Av1LoopFilterContext loopFilterContext;
/// <summary>
/// The frame-owned inverse quantizer carrying the active superblock delta-Q state.
/// </summary>
private readonly Av1InverseQuantizer inverseQuantizer;
/// <summary>
/// Owns the reusable raster-order inverse-quantization buffer.
/// </summary>
private readonly IMemoryOwner<int> inverseQuantizationOwner;
/// <summary>
/// Owns the reusable two-dimensional inverse-transform workspace.
/// </summary>
private readonly IMemoryOwner<int> transformWorkspaceOwner;
/// <summary>
/// Indicates whether transform traversal must also populate loop-filter parameters.
/// </summary>
@ -59,16 +74,19 @@ internal class Av1BlockDecoder
/// <param name="frameHeader">The decoded frame header.</param>
/// <param name="frameBuffer">The frame buffer receiving reconstructed samples.</param>
/// <param name="loopFilterContext">The transform-size map populated while reconstructing blocks.</param>
/// <param name="inverseQuantizer">The inverse quantizer carrying the active superblock delta-Q state.</param>
public Av1BlockDecoder(
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Av1FrameBuffer<byte> 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<int>(inverseQuantizationSize);
this.transformWorkspaceOwner = this.frameBuffer.MemoryAllocator.Allocate<int>(Av1TransformWorkspace.MaximumLength);
this.isLoopFilterEnabled = frameHeader.LoopFilterParameters.FilterLevel[0] != 0 ||
frameHeader.LoopFilterParameters.FilterLevel[1] != 0;
@ -88,7 +107,16 @@ internal class Av1BlockDecoder
/// <summary>
/// Gets the reusable raster-order coefficient buffer populated by inverse quantization.
/// </summary>
public int[] CurrentInverseQuantizationCoefficients { get; private set; }
public Span<int> CurrentInverseQuantizationCoefficients => this.inverseQuantizationOwner.Memory.Span;
/// <summary>
/// Releases the pooled reconstruction workspaces owned by this decoder.
/// </summary>
public void Dispose()
{
this.transformWorkspaceOwner.Dispose();
this.inverseQuantizationOwner.Dispose();
}
/// <summary>
/// Resets the per-plane packed coefficient cursors before reconstructing a superblock.
@ -113,8 +141,7 @@ internal class Av1BlockDecoder
/// <param name="tileInfo">The tile boundaries used to determine neighbor availability.</param>
public void DecodeBlock(Av1BlockModeInfo modeInfo, Point modeInfoPosition, Av1BlockSize blockSize, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo)
{
using IMemoryOwner<int> transformWorkspaceOwner = this.frameBuffer.MemoryAllocator.Allocate<int>(Av1TransformWorkspace.MaximumLength);
Span<int> transformWorkspace = transformWorkspaceOwner.Memory.Span;
Span<int> 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,

6
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

47
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
{
/// <summary>
/// Verifies the decoded block geometry and prediction modes against libaom inspection output for a real AVIF image item.
/// </summary>
[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<Rgba32> image = decoder.Decode<Rgba32>(content.AsSpan(codedItemOffset, codedItemLength));
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(decoder.FrameHeader);
Span<int> blockSizeCounts = stackalloc int[(int)Av1BlockSize.AllSizes];
Span<int> 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()
{

157
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;
/// <summary>
/// Verifies scalar and SIMD parity for every H.273 transfer characteristic consumed by AV1 color conversion.
/// </summary>
[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,
];
/// <summary>
/// Gets every defined AV1-signallable transfer characteristic, including the deterministic unspecified fallback.
/// </summary>
public static TheoryData<int> 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,
};
/// <summary>
/// Verifies that every SIMD width matches the scalar inverse transfer function at curve transitions, extrema, and extended-range values.
/// </summary>
/// <param name="transferCharacteristicsValue">The transfer-characteristic code point under test.</param>
[Theory]
[MemberData(nameof(TransferCharacteristics))]
public void ToLinearSimdMatchesScalar(int transferCharacteristicsValue)
{
ObuTransferCharacteristics transferCharacteristics = (ObuTransferCharacteristics)transferCharacteristicsValue;
float[] expected = SignalValues.Select(value => Av1TransferFunctions.ToLinear(transferCharacteristics, value)).ToArray();
Vector128<float> vector128 = Av1TransferFunctions.ToLinear(transferCharacteristics, Vector128.Create(SignalValues.AsSpan(0, Vector128<float>.Count)));
Vector256<float> vector256 = Av1TransferFunctions.ToLinear(transferCharacteristics, Vector256.Create(SignalValues.AsSpan(0, Vector256<float>.Count)));
Vector512<float> vector512 = Av1TransferFunctions.ToLinear(transferCharacteristics, Vector512.Create(SignalValues));
AssertVectorMatchesScalar(expected, vector128, transferCharacteristics);
AssertVectorMatchesScalar(expected, vector256, transferCharacteristics);
AssertVectorMatchesScalar(expected, vector512, transferCharacteristics);
}
/// <summary>
/// Verifies that every SIMD width matches the scalar forward transfer function at curve transitions, extrema, and extended-range values.
/// </summary>
/// <param name="transferCharacteristicsValue">The transfer-characteristic code point under test.</param>
[Theory]
[MemberData(nameof(TransferCharacteristics))]
public void ToGammaSimdMatchesScalar(int transferCharacteristicsValue)
{
ObuTransferCharacteristics transferCharacteristics = (ObuTransferCharacteristics)transferCharacteristicsValue;
float[] expected = SignalValues.Select(value => Av1TransferFunctions.ToGamma(transferCharacteristics, value)).ToArray();
Vector128<float> vector128 = Av1TransferFunctions.ToGamma(transferCharacteristics, Vector128.Create(SignalValues.AsSpan(0, Vector128<float>.Count)));
Vector256<float> vector256 = Av1TransferFunctions.ToGamma(transferCharacteristics, Vector256.Create(SignalValues.AsSpan(0, Vector256<float>.Count)));
Vector512<float> vector512 = Av1TransferFunctions.ToGamma(transferCharacteristics, Vector512.Create(SignalValues));
AssertVectorMatchesScalar(expected, vector128, transferCharacteristics);
AssertVectorMatchesScalar(expected, vector256, transferCharacteristics);
AssertVectorMatchesScalar(expected, vector512, transferCharacteristics);
}
/// <summary>
/// Compares four SIMD lanes with their scalar results.
/// </summary>
/// <param name="expected">The scalar results.</param>
/// <param name="actual">The SIMD results.</param>
/// <param name="transferCharacteristics">The transfer characteristic under test.</param>
private static void AssertVectorMatchesScalar(ReadOnlySpan<float> expected, Vector128<float> actual, ObuTransferCharacteristics transferCharacteristics)
{
for (int i = 0; i < Vector128<float>.Count; i++)
{
AssertClose(expected[i], actual.GetElement(i), transferCharacteristics, i, 128);
}
}
/// <summary>
/// Compares eight SIMD lanes with their scalar results.
/// </summary>
/// <param name="expected">The scalar results.</param>
/// <param name="actual">The SIMD results.</param>
/// <param name="transferCharacteristics">The transfer characteristic under test.</param>
private static void AssertVectorMatchesScalar(ReadOnlySpan<float> expected, Vector256<float> actual, ObuTransferCharacteristics transferCharacteristics)
{
for (int i = 0; i < Vector256<float>.Count; i++)
{
AssertClose(expected[i], actual.GetElement(i), transferCharacteristics, i, 256);
}
}
/// <summary>
/// Compares sixteen SIMD lanes with their scalar results.
/// </summary>
/// <param name="expected">The scalar results.</param>
/// <param name="actual">The SIMD results.</param>
/// <param name="transferCharacteristics">The transfer characteristic under test.</param>
private static void AssertVectorMatchesScalar(ReadOnlySpan<float> expected, Vector512<float> actual, ObuTransferCharacteristics transferCharacteristics)
{
for (int i = 0; i < Vector512<float>.Count; i++)
{
AssertClose(expected[i], actual.GetElement(i), transferCharacteristics, i, 512);
}
}
/// <summary>
/// Verifies one SIMD lane within the tolerance of the runtime vector exponential and logarithm kernels.
/// </summary>
/// <param name="expected">The scalar result.</param>
/// <param name="actual">The SIMD result.</param>
/// <param name="transferCharacteristics">The transfer characteristic under test.</param>
/// <param name="lane">The SIMD lane index.</param>
/// <param name="width">The SIMD register width.</param>
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}.");
}
}

83
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);
}
/// <summary>
/// Verifies that AVIF decoding preserves the exact embedded ICC profile bytes.
/// </summary>
[Theory]
[WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)]
public void DecodeAvifPreservesEmbeddedIccProfile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
using Image<TPixel> preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions);
using Image<TPixel> expectedPreserved = Image.Load<TPixel>(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());
}
/// <summary>
/// Verifies that AVIF decoding converts pixels from an embedded non-sRGB ICC profile to sRGB.
/// </summary>
[Theory]
[WithFile(TestImages.Heif.PerceptualIccAvif, PixelTypes.Rgba32)]
public void DecodeAvifConvertsEmbeddedNonSrgbIccProfile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert };
using Image<TPixel> preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions);
using Image<TPixel> converted = provider.GetImage(HeifDecoder.Instance, convertOptions);
using Image<TPixel> expected = Image.Load<TPixel>(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);
}
/// <summary>
/// Verifies that compact profile handling removes a canonical sRGB ICC profile without changing pixels.
/// </summary>
[Theory]
[WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)]
public void DecodeAvifCompactsCanonicalSrgbIccProfile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
DecoderOptions compactOptions = new() { ColorProfileHandling = ColorProfileHandling.Compact };
using Image<TPixel> preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions);
using Image<TPixel> compact = provider.GetImage(HeifDecoder.Instance, compactOptions);
Assert.NotNull(preserved.Metadata.IccProfile);
Assert.Null(compact.Metadata.IccProfile);
Assert.Empty(ImageComparer.Exact.CompareImages(preserved, compact));
}
/// <summary>
/// Verifies that metadata skipping omits the embedded AVIF ICC profile.
/// </summary>
[Theory]
[WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)]
public void DecodeAvifSkipsEmbeddedIccProfileWithMetadata<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
DecoderOptions options = new()
{
ColorProfileHandling = ColorProfileHandling.Preserve,
SkipMetadata = true
};
using Image<TPixel> image = provider.GetImage(HeifDecoder.Instance, options);
Assert.Null(image.Metadata.IccProfile);
}
[Fact]
public void DecodeIgnoresUnknownTopLevelBox()
{

9
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";

3
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

3
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

3
tests/Images/Input/Heif/perceptual_icc.avif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:438b5d2bc7b32206b62676d6d460fb0037b857aa51dd03049867f68755227b3b
size 205370
Loading…
Cancel
Save