Browse Source

Merge c2cc544793 into adb982081a

pull/2633/merge
Ynse Hoornenborg 4 weeks ago
committed by GitHub
parent
commit
5265b84698
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      .gitattributes
  2. 3291
      HEIF_IMPLEMENTATION_PLAN.md
  3. 5
      ImageSharp.slnx
  4. 142
      THIRD-PARTY-NOTICES.TXT
  5. 2
      shared-infrastructure
  6. 5
      src/ImageSharp/ColorProfiles/ColorConversionOptions.cs
  7. 12
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
  8. 463
      src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs
  9. 21
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs
  10. 12
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs
  11. 44
      src/ImageSharp/ColorProfiles/Icc/IccConverterBase.Conversions.cs
  12. 5
      src/ImageSharp/ColorProfiles/Icc/IccConverterBase.cs
  13. 5
      src/ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs
  14. 5
      src/ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs
  15. 5
      src/ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs
  16. 5
      src/ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs
  17. 25
      src/ImageSharp/ColorProfiles/IccInterpolationMethod.cs
  18. 53
      src/ImageSharp/Common/Helpers/DisposableDictionary.cs
  19. 51
      src/ImageSharp/Common/Helpers/DisposableList.cs
  20. 40
      src/ImageSharp/Common/Helpers/Numerics.cs
  21. 185
      src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
  22. 472
      src/ImageSharp/Common/Helpers/SimdUtils.Pack.cs
  23. 81
      src/ImageSharp/Common/InlineArray.cs
  24. 2
      src/ImageSharp/Common/InlineArray.tt
  25. 3
      src/ImageSharp/Configuration.cs
  26. 25
      src/ImageSharp/Formats/Heif/Av1/Av1BitDepth.cs
  27. 19
      src/ImageSharp/Formats/Heif/Av1/Av1BitDepthExtensions.cs
  28. 314
      src/ImageSharp/Formats/Heif/Av1/Av1BitStreamReader.cs
  29. 309
      src/ImageSharp/Formats/Heif/Av1/Av1BitStreamWriter.cs
  30. 140
      src/ImageSharp/Formats/Heif/Av1/Av1BlockSize.cs
  31. 245
      src/ImageSharp/Formats/Heif/Av1/Av1BlockSizeExtensions.cs
  32. 829
      src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs
  33. 30
      src/ImageSharp/Formats/Heif/Av1/Av1ColorFormat.cs
  34. 353
      src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs
  35. 969
      src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs
  36. 817
      src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs
  37. 21
      src/ImageSharp/Formats/Heif/Av1/Av1LayerSelector.cs
  38. 97
      src/ImageSharp/Formats/Heif/Av1/Av1LayeredImageIndex.cs
  39. 309
      src/ImageSharp/Formats/Heif/Av1/Av1Math.cs
  40. 16
      src/ImageSharp/Formats/Heif/Av1/Av1OperatingPointSelector.cs
  41. 153
      src/ImageSharp/Formats/Heif/Av1/Av1PartitionType.cs
  42. 116
      src/ImageSharp/Formats/Heif/Av1/Av1PartitionTypeExtensions.cs
  43. 25
      src/ImageSharp/Formats/Heif/Av1/Av1Plane.cs
  44. 133
      src/ImageSharp/Formats/Heif/Av1/Color/Av1PlanarSampleBuffer.cs
  45. 780
      src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBuffer.cs
  46. 63
      src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBufferView.cs
  47. 299
      src/ImageSharp/Formats/Heif/Av1/Color/Av1YuvConverter.cs
  48. 2800
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs
  49. 424
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs
  50. 766
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContext.cs
  51. 140
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs
  52. 422
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorContext.cs
  53. 205
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorCosts.cs
  54. 501
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1NzMap.cs
  55. 84
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1ProbabilityCost.cs
  56. 319
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1RateDistortion.cs
  57. 62
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1RateDistortionStatistics.cs
  58. 1454
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolContextHelper.cs
  59. 1790
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs
  60. 2930
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
  61. 290
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolReader.cs
  62. 427
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs
  63. 22
      src/ImageSharp/Formats/Heif/Av1/IAv1TileReader.cs
  64. 19
      src/ImageSharp/Formats/Heif/Av1/IAv1TileWriter.cs
  65. 530
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionParameters.cs
  66. 30
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionType.cs
  67. 626
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopy.cs
  68. 1094
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopySearchIndex.cs
  69. 89
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.ByteOperator.cs
  70. 340
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.Fractional.cs
  71. 118
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.Operator.cs
  72. 651
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.SingleReference.cs
  73. 90
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.UInt16Operator.cs
  74. 674
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.cs
  75. 485
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchSettings.cs
  76. 189
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchSites.cs
  77. 283
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVariationCandidates.cs
  78. 284
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs
  79. 25
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVectorPrecision.cs
  80. 1598
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs
  81. 30
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuChromoSamplePosition.cs
  82. 101
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorConfig.cs
  83. 75
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorPrimaries.cs
  84. 40
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuConstraintDirectionalEnhancementFilterParameters.cs
  85. 32
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDecoderModelInfo.cs
  86. 25
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDeltaParameters.cs
  87. 251
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs
  88. 352
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs
  89. 101
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameReferenceState.cs
  90. 40
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameSize.cs
  91. 30
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameType.cs
  92. 45
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuHeader.cs
  93. 77
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopFilterParameters.cs
  94. 20
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationItem.cs
  95. 40
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationParameters.cs
  96. 95
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs
  97. 40
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMetadataType.cs
  98. 61
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOperatingPoint.cs
  99. 53
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOrderHintInfo.cs
  100. 65
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuQuantizationParameters.cs

6
.gitattributes

@ -138,6 +138,12 @@
*.heic filter=lfs diff=lfs merge=lfs -text
*.hif filter=lfs diff=lfs merge=lfs -text
*.avif filter=lfs diff=lfs merge=lfs -text
*.hvc1 filter=lfs diff=lfs merge=lfs -text
*.hvcc filter=lfs diff=lfs merge=lfs -text
*.ivf filter=lfs diff=lfs merge=lfs -text
*.y4m filter=lfs diff=lfs merge=lfs -text
*.yuv filter=lfs diff=lfs merge=lfs -text
*.bit filter=lfs diff=lfs merge=lfs -text
###############################################################################
# Handle ICC files by git lfs
###############################################################################

3291
HEIF_IMPLEMENTATION_PLAN.md

File diff suppressed because one or more lines are too long

5
ImageSharp.slnx

@ -114,6 +114,11 @@
<File Path="tests/Images/Input/Gif/issues/issue405_badappextlength252-2.gif" />
<File Path="tests/Images/Input/Gif/issues/issue405_badappextlength252.gif" />
</Folder>
<Folder Name="/tests/Images/Input/Heif/">
<File Path="tests/Images/Input/Heif/Irvine_CA.avif" />
<File Path="tests/Images/Input/Heif/jpeg444_xnconvert.avif" />
<File Path="tests/Images/Input/Heif/Orange4x4.avif" />
</Folder>
<Folder Name="/tests/Images/Input/Icon/">
<File Path="tests/Images/Input/Icon/aero_arrow.cur" />
<File Path="tests/Images/Input/Icon/flutter.ico" />

142
THIRD-PARTY-NOTICES.TXT

@ -30,3 +30,145 @@ See the LICENSE file in the CoreFX project root for more information.
https://github.com/dotnet/corefx/blob/17300169760c61a90cab8d913636c1058a30a8c1/LICENSE.TXT
https://github.com/dotnet/corefx/blob/17300169760c61a90cab8d913636c1058a30a8c1/src/Common/src/CoreLib/System/IO/Stream.cs#L742
https://github.com/dotnet/corefx/blob/17300169760c61a90cab8d913636c1058a30a8c1/src/Common/src/CoreLib/System/IO/Stream.cs#L775
License notice for the AOM AV1 Codec Library
-----
Parts of the AV1 implementation are adapted from the AOM AV1 Codec Library.
https://aomedia.googlesource.com/aom/
Copyright (c) 2016, Alliance for Open Media. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
Alliance for Open Media Patent License 1.0
1. License Terms.
1.1. Patent License. Subject to the terms and conditions of this License, each
Licensor, on behalf of itself and successors in interest and assigns,
grants Licensee a non-sublicensable, perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as expressly stated in this
License) patent license to its Necessary Claims to make, use, sell, offer
for sale, import or distribute any Implementation.
1.2. Conditions.
1.2.1. Availability. As a condition to the grant of rights to Licensee to make,
sell, offer for sale, import or distribute an Implementation under
Section 1.1, Licensee must make its Necessary Claims available under
this License, and must reproduce this License with any Implementation
as follows:
a. For distribution in source code, by including this License in the
root directory of the source code with its Implementation.
b. For distribution in any other form (including binary, object form,
and/or hardware description code (e.g., HDL, RTL, Gate Level Netlist,
GDSII, etc.)), by including this License in the documentation, legal
notices, and/or other written materials provided with the
Implementation.
1.2.2. Additional Conditions. This license is directly from Licensor to
Licensee. Licensee acknowledges as a condition of benefiting from it
that no rights from Licensor are received from suppliers, distributors,
or otherwise in connection with this License.
1.3. Defensive Termination. If any Licensee, its Affiliates, or its agents
initiates patent litigation or files, maintains, or voluntarily
participates in a lawsuit against another entity or any person asserting
that any Implementation infringes Necessary Claims, any patent licenses
granted under this License directly to the Licensee are immediately
terminated as of the date of the initiation of action unless 1) that suit
was in response to a corresponding suit regarding an Implementation first
brought against an initiating entity, or 2) that suit was brought to
enforce the terms of this License (including intervention in a third-party
action by a Licensee).
1.4. Disclaimers. The Reference Implementation and Specification are provided
"AS IS" and without warranty. The entire risk as to implementing or
otherwise using the Reference Implementation or Specification is assumed
by the implementer and user. Licensor expressly disclaims any warranties
(express, implied, or otherwise), including implied warranties of
merchantability, non-infringement, fitness for a particular purpose, or
title, related to the material. IN NO EVENT WILL LICENSOR BE LIABLE TO
ANY OTHER PARTY FOR LOST PROFITS OR ANY FORM OF INDIRECT, SPECIAL,
INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER FROM ANY CAUSES OF
ACTION OF ANY KIND WITH RESPECT TO THIS LICENSE, WHETHER BASED ON BREACH
OF CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, AND WHETHER OR
NOT THE OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2. Definitions.
2.1. Affiliate. "Affiliate" means an entity that directly or indirectly
Controls, is Controlled by, or is under common Control of that party.
2.2. Control. "Control" means direct or indirect control of more than 50% of
the voting power to elect directors of that corporation, or for any other
entity, the power to direct management of such entity.
2.3. Decoder. "Decoder" means any decoder that conforms fully with all
non-optional portions of the Specification.
2.4. Encoder. "Encoder" means any encoder that produces a bitstream that can
be decoded by a Decoder only to the extent it produces such a bitstream.
2.5. Final Deliverable. "Final Deliverable" means the final version of a
deliverable approved by the Alliance for Open Media as a Final
Deliverable.
2.6. Implementation. "Implementation" means any implementation, including the
Reference Implementation, that is an Encoder and/or a Decoder. An
Implementation also includes components of an Implementation only to the
extent they are used as part of an Implementation.
2.7. License. "License" means this license.
2.8. Licensee. "Licensee" means any person or entity who exercises patent
rights granted under this License.
2.9. Licensor. "Licensor" means (i) any Licensee that makes, sells, offers
for sale, imports or distributes any Implementation, or (ii) a person
or entity that has a licensing obligation to the Implementation as a
result of its membership and/or participation in the Alliance for Open
Media working group that developed the Specification.
2.10. Necessary Claims. "Necessary Claims" means all claims of patents or
patent applications, (a) that currently or at any time in the future,
are owned or controlled by the Licensor, and (b) (i) would be an
Essential Claim as defined by the W3C Policy as of February 5, 2004
(https://www.w3.org/Consortium/Patent-Policy-20040205/#def-essential)
as if the Specification was a W3C Recommendation; or (ii) are infringed
by the Reference Implementation.
2.11. Reference Implementation. "Reference Implementation" means an Encoder
and/or Decoder released by the Alliance for Open Media as a Final
Deliverable.
2.12. Specification. "Specification" means the specification designated by
the Alliance for Open Media as a Final Deliverable for which this
License was issued.

2
shared-infrastructure

@ -1 +1 @@
Subproject commit 52fc1c3e87a755b2b17ce77e8cd22ec4bea17bcb
Subproject commit a835a9d74e82b2d32b580a7902eb2699ebc47098

5
src/ImageSharp/ColorProfiles/ColorConversionOptions.cs

@ -73,6 +73,11 @@ public class ColorConversionOptions
/// </summary>
public IccProfile? TargetIccProfile { get; init; }
/// <summary>
/// Gets the interpolation method used for ICC color lookup tables. Defaults to <see cref="IccInterpolationMethod.Auto"/>.
/// </summary>
public IccInterpolationMethod IccInterpolationMethod { get; init; }
/// <summary>
/// Gets the transformation matrix used in conversion to perform chromatic adaptation.
/// <see cref="KnownChromaticAdaptationMatrices"/> for further information. Default is Bradford.

12
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs

@ -75,8 +75,8 @@ internal static class ColorProfileConverterExtensionsIcc
throw new InvalidOperationException("Target ICC profile is missing.");
}
ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true);
ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false);
ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true, converter.Options.IccInterpolationMethod);
ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false, converter.Options.IccInterpolationMethod);
ColorProfileConverter pcsConverter = new(new ColorConversionOptions
{
@ -142,8 +142,8 @@ internal static class ColorProfileConverterExtensionsIcc
Guard.MustBeGreaterThanOrEqualTo(source.Length, destination.Length, nameof(destination));
ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true);
ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false);
ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true, converter.Options.IccInterpolationMethod);
ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false, converter.Options.IccInterpolationMethod);
ColorProfileConverter pcsConverter = new(new ColorConversionOptions
{
@ -692,10 +692,10 @@ internal static class ColorProfileConverterExtensionsIcc
{
private readonly IccProfile profile;
internal ConversionParams(IccProfile profile, bool toPcs)
internal ConversionParams(IccProfile profile, bool toPcs, IccInterpolationMethod interpolationMethod)
{
this.profile = profile;
this.Converter = toPcs ? new IccDataToPcsConverter(profile) : new IccPcsToDataConverter(profile);
this.Converter = toPcs ? new IccDataToPcsConverter(profile, interpolationMethod) : new IccPcsToDataConverter(profile, interpolationMethod);
}
internal IccConverterBase Converter { get; }

463
src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs

@ -8,51 +8,39 @@ namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators;
/// <summary>
/// Implements interpolation methods for color profile lookup tables.
/// Adapted from ICC Reference implementation:
/// https://github.com/InternationalColorConsortium/DemoIccMAX/blob/79ecb74135ad47bac7d42692905a079839b7e105/IccProfLib/IccTagLut.cpp
/// </summary>
internal class ClutCalculator : IVector4Calculator
{
private readonly bool useTrilinearInterpolation;
private readonly int inputCount;
private readonly int outputCount;
private readonly float[] lut;
private readonly byte[] gridPointCount;
private readonly byte[] maxGridPoint;
private readonly int[] indexFactor;
private readonly int[] dimSize;
private readonly int nodeCount;
private readonly float[][] nodes;
private readonly float[] g;
private readonly uint[] ig;
private readonly float[] s;
private readonly float[] df;
private readonly uint[] nPower;
private int n000;
private int n001;
private int n010;
private int n011;
private int n100;
private int n101;
private int n110;
private int n111;
private int n1000;
public ClutCalculator(IccClut clut)
private const int LowerCorner = 0;
private readonly int n001;
private readonly int n010;
private readonly int n011;
/// <summary>
/// Initializes a new instance of the <see cref="ClutCalculator"/> class.
/// </summary>
/// <param name="clut">The table to evaluate.</param>
/// <param name="useTrilinearInterpolation">Whether tables use multilinear interpolation.</param>
public ClutCalculator(IccClut clut, bool useTrilinearInterpolation)
{
Guard.NotNull(clut, nameof(clut));
Guard.MustBeGreaterThan(clut.InputChannelCount, 0, nameof(clut.InputChannelCount));
Guard.MustBeGreaterThan(clut.OutputChannelCount, 0, nameof(clut.OutputChannelCount));
// This calculator consumes and produces Vector4 values. A table may describe
// more channels, but it cannot be evaluated through this four-channel contract.
Guard.MustBeBetweenOrEqualTo(clut.InputChannelCount, 1, 4, nameof(clut.InputChannelCount));
Guard.MustBeBetweenOrEqualTo(clut.OutputChannelCount, 1, 4, nameof(clut.OutputChannelCount));
this.useTrilinearInterpolation = useTrilinearInterpolation;
this.inputCount = clut.InputChannelCount;
this.outputCount = clut.OutputChannelCount;
this.g = new float[this.inputCount];
this.ig = new uint[this.inputCount];
this.s = new float[this.inputCount];
this.nPower = new uint[16];
this.lut = clut.Values;
this.nodeCount = (int)Math.Pow(2, clut.InputChannelCount);
this.df = new float[this.nodeCount];
this.nodes = new float[this.nodeCount][];
this.dimSize = new int[this.inputCount];
this.gridPointCount = clut.GridPointCount;
this.maxGridPoint = new byte[this.inputCount];
@ -67,9 +55,15 @@ internal class ClutCalculator : IVector4Calculator
this.dimSize[i] = this.dimSize[i + 1] * this.gridPointCount[i + 1];
}
this.indexFactor = this.CalculateIndexFactor();
this.n001 = this.dimSize[0];
if (this.inputCount == 2)
{
this.n010 = this.dimSize[1];
this.n011 = this.n001 + this.n010;
}
}
/// <inheritdoc/>
public unsafe Vector4 Calculate(Vector4 value)
{
Vector4 result = default;
@ -82,95 +76,30 @@ internal class ClutCalculator : IVector4Calculator
this.Interpolate2d((float*)&value, (float*)&result);
break;
case 3:
this.Interpolate3d((float*)&value, (float*)&result);
break;
case 4:
this.Interpolate4d((float*)&value, (float*)&result);
break;
default:
this.InterpolateNd((float*)&value, (float*)&result);
break;
}
return result;
}
if (this.useTrilinearInterpolation)
{
this.Interpolate3d((float*)&value, (float*)&result);
}
else
{
this.InterpolateTetrahedral((float*)&value, (float*)&result);
}
private int[] CalculateIndexFactor()
{
int[] factors = new int[16];
switch (this.inputCount)
{
case 1:
factors[0] = this.n000 = 0;
factors[1] = this.n001 = this.dimSize[0];
break;
case 2:
factors[0] = this.n000 = 0;
factors[1] = this.n001 = this.dimSize[0];
factors[2] = this.n010 = this.dimSize[1];
factors[3] = this.n011 = this.n001 + this.n010;
break;
case 3:
factors[0] = this.n000 = 0;
factors[1] = this.n001 = this.dimSize[0];
factors[2] = this.n010 = this.dimSize[1];
factors[3] = this.n011 = this.n001 + this.n010;
factors[4] = this.n100 = this.dimSize[2];
factors[5] = this.n101 = this.n100 + this.n001;
factors[6] = this.n110 = this.n100 + this.n010;
factors[7] = this.n111 = this.n110 + this.n001;
break;
case 4:
factors[0] = 0;
factors[1] = this.n001 = this.dimSize[0];
factors[2] = this.n010 = this.dimSize[1];
factors[3] = factors[2] + factors[1];
factors[4] = this.n100 = this.dimSize[2];
factors[5] = factors[4] + factors[1];
factors[6] = factors[4] + factors[2];
factors[7] = factors[4] + factors[3];
factors[8] = this.n1000 = this.dimSize[3];
factors[9] = factors[8] + factors[1];
factors[10] = factors[8] + factors[2];
factors[11] = factors[8] + factors[3];
factors[12] = factors[8] + factors[4];
factors[13] = factors[8] + factors[5];
factors[14] = factors[8] + factors[6];
factors[15] = factors[8] + factors[7];
break;
default:
// Initialize ND interpolation variables.
factors[0] = 0;
int count;
for (count = 0; count < this.inputCount; count++)
if (this.useTrilinearInterpolation)
{
this.nPower[count] = (uint)(1 << (this.inputCount - 1 - count));
this.Interpolate4d((float*)&value, (float*)&result);
}
uint[] nPower = [0, 1];
count = 0;
int nFlag = 1;
for (uint j = 1; j < this.nodeCount; j++)
else
{
if (j == nPower[1])
{
factors[j] = this.dimSize[count];
nPower[0] = (uint)(1 << count);
count++;
nPower[1] = (uint)(1 << count);
nFlag = 1;
}
else
{
factors[j] = factors[nPower[0]] + factors[nFlag];
nFlag++;
}
this.InterpolateTetrahedral((float*)&value, (float*)&result);
}
break;
}
return factors;
return result;
}
/// <summary>
@ -182,7 +111,7 @@ internal class ClutCalculator : IVector4Calculator
{
byte mx = this.maxGridPoint[0];
float x = UnitClip(srcPixel[0]) * mx;
float x = Numerics.Clamp(srcPixel[0], 0F, 1F) * mx;
uint ix = (uint)x;
@ -206,7 +135,7 @@ internal class ClutCalculator : IVector4Calculator
int offset = 0;
for (i = 0; i < this.outputCount; i++)
{
destPixel[i] = (float)((p[offset + this.n000] * dF0) + (p[offset + this.n001] * dF1));
destPixel[i] = (float)((p[offset + LowerCorner] * dF0) + (p[offset + this.n001] * dF1));
offset++;
}
}
@ -221,8 +150,8 @@ internal class ClutCalculator : IVector4Calculator
byte mx = this.maxGridPoint[0];
byte my = this.maxGridPoint[1];
float x = UnitClip(srcPixel[0]) * mx;
float y = UnitClip(srcPixel[1]) * my;
float x = Numerics.Clamp(srcPixel[0], 0F, 1F) * mx;
float y = Numerics.Clamp(srcPixel[1], 0F, 1F) * my;
uint ix = (uint)x;
uint iy = (uint)y;
@ -257,25 +186,29 @@ internal class ClutCalculator : IVector4Calculator
int offset = 0;
for (i = 0; i < this.outputCount; i++)
{
destPixel[i] = (float)((p[offset + this.n000] * dF0) + (p[offset + this.n001] * dF1) + (p[offset + this.n010] * dF2) + (p[offset + this.n011] * dF3));
destPixel[i] = (float)((p[offset + LowerCorner] * dF0) + (p[offset + this.n001] * dF1) + (p[offset + this.n010] * dF2) + (p[offset + this.n011] * dF3));
offset++;
}
}
/// <summary>
/// Three dimensional interpolation function.
/// Interpolates a three-channel table independently along each axis.
/// </summary>
/// <param name="srcPixel">The input pixel values, which will be interpolated.</param>
/// <param name="destPixel">The interpolated output pixels.</param>
private unsafe void Interpolate3d(float* srcPixel, float* destPixel)
{
int xStride = this.dimSize[0];
int yStride = this.dimSize[1];
int zStride = this.dimSize[2];
byte mx = this.maxGridPoint[0];
byte my = this.maxGridPoint[1];
byte mz = this.maxGridPoint[2];
float x = UnitClip(srcPixel[0]) * mx;
float y = UnitClip(srcPixel[1]) * my;
float z = UnitClip(srcPixel[2]) * mz;
float x = Numerics.Clamp(srcPixel[0], 0F, 1F) * mx;
float y = Numerics.Clamp(srcPixel[1], 0F, 1F) * my;
float z = Numerics.Clamp(srcPixel[2], 0F, 1F) * mz;
uint ix = (uint)x;
uint iy = (uint)y;
@ -307,9 +240,10 @@ internal class ClutCalculator : IVector4Calculator
float nt = (float)(1.0 - t);
float nu = (float)(1.0 - u);
Span<float> p = this.lut.AsSpan((int)((ix * this.n001) + (iy * this.n010) + (iz * this.n100)));
Span<float> p = this.lut.AsSpan((int)((ix * xStride) + (iy * yStride) + (iz * zStride)));
// Normalize grid units
// The eight corner weights are products of the independent axis fractions.
// This tensor-product blend is used for Lab-indexed output tables.
float dF0 = ns * nt * nu;
float dF1 = ns * nt * u;
float dF2 = ns * t * nu;
@ -322,51 +256,61 @@ internal class ClutCalculator : IVector4Calculator
int offset = 0;
for (int i = 0; i < this.outputCount; i++)
{
destPixel[i] = (float)((p[offset + this.n000] * dF0) +
(p[offset + this.n001] * dF1) +
(p[offset + this.n010] * dF2) +
(p[offset + this.n011] * dF3) +
(p[offset + this.n100] * dF4) +
(p[offset + this.n101] * dF5) +
(p[offset + this.n110] * dF6) +
(p[offset + this.n111] * dF7));
destPixel[i] = (float)((p[offset + 0] * dF0) +
(p[offset + xStride] * dF1) +
(p[offset + yStride] * dF2) +
(p[offset + (xStride + yStride)] * dF3) +
(p[offset + zStride] * dF4) +
(p[offset + (xStride + zStride)] * dF5) +
(p[offset + (yStride + zStride)] * dF6) +
(p[offset + (xStride + yStride + zStride)] * dF7));
offset++;
}
}
/// <summary>
/// Four dimensional interpolation function.
/// Interpolates three-channel tables or blends tetrahedral slices of four-channel tables.
/// </summary>
/// <param name="srcPixel">The input pixel values, which will be interpolated.</param>
/// <param name="destPixel">The interpolated output pixels.</param>
private unsafe void Interpolate4d(float* srcPixel, float* destPixel)
private unsafe void InterpolateTetrahedral(float* srcPixel, float* destPixel)
{
byte mw = this.maxGridPoint[0];
byte mx = this.maxGridPoint[1];
byte my = this.maxGridPoint[2];
byte mz = this.maxGridPoint[3];
int dimension = this.inputCount - 3;
int tableOffset = 0;
int sliceStride = 0;
float fraction = 0F;
if (this.inputCount == 4)
{
float position = Numerics.Clamp(srcPixel[0], 0F, 1F) * this.maxGridPoint[0];
int lowerSlice = (int)position;
fraction = position - lowerSlice;
tableOffset = lowerSlice * this.dimSize[0];
sliceStride = lowerSlice == this.maxGridPoint[0] ? 0 : this.dimSize[0];
srcPixel++;
}
float w = UnitClip(srcPixel[0]) * mw;
float x = UnitClip(srcPixel[1]) * mx;
float y = UnitClip(srcPixel[2]) * my;
float z = UnitClip(srcPixel[3]) * mz;
// Adjacent slices have the same grid and input coordinates. Compute their cell
// and tetrahedron once; only the first-axis offset differs between the slices.
int xStride = this.dimSize[dimension];
int yStride = this.dimSize[dimension + 1];
int zStride = this.dimSize[dimension + 2];
byte mx = this.maxGridPoint[dimension];
byte my = this.maxGridPoint[dimension + 1];
byte mz = this.maxGridPoint[dimension + 2];
float x = Numerics.Clamp(srcPixel[0], 0F, 1F) * mx;
float y = Numerics.Clamp(srcPixel[1], 0F, 1F) * my;
float z = Numerics.Clamp(srcPixel[2], 0F, 1F) * mz;
uint iw = (uint)w;
uint ix = (uint)x;
uint iy = (uint)y;
uint iz = (uint)z;
float v = w - iw;
float u = x - ix;
float t = y - iy;
float s = z - iz;
if (iw == mw)
{
iw--;
v = 1.0f;
}
if (ix == mx)
{
ix--;
@ -385,122 +329,161 @@ internal class ClutCalculator : IVector4Calculator
s = 1.0f;
}
float ns = (float)(1.0 - s);
float nt = (float)(1.0 - t);
float nu = (float)(1.0 - u);
float nv = (float)(1.0 - v);
Span<float> p = this.lut.AsSpan((int)((iw * this.n001) + (ix * this.n010) + (iy * this.n100) + (iz * this.n1000)));
// Normalize grid units.
float[] dF =
[
ns * nt * nu * nv,
ns * nt * nu * v,
ns * nt * u * nv,
ns * nt * u * v,
ns * t * nu * nv,
ns * t * nu * v,
ns * t * u * nv,
ns * t * u * v,
s * nt * nu * nv,
s * nt * nu * v,
s * nt * u * nv,
s * nt * u * v,
s * t * nu * nv,
s * t * nu * v,
s * t * u * nv,
s * t * u * v,
];
// The fractional coordinates select one of six tetrahedra sharing the cell's
// lower and upper corners. Walking the axes from largest fraction to smallest
// identifies the two intermediate vertices. Choose once for all output channels.
int firstVertex;
int secondVertex;
float firstWeight;
float secondWeight;
float thirdWeight;
int offset = 0;
for (int i = 0; i < this.outputCount; i++)
if (u >= t)
{
float pv = 0.0f;
for (int j = 0; j < 16; j++)
if (t >= s)
{
pv += p[offset + this.indexFactor[j]] * dF[j];
firstVertex = xStride;
secondVertex = xStride + yStride;
firstWeight = u;
secondWeight = t;
thirdWeight = s;
}
destPixel[i] = pv;
offset++;
}
}
/// <summary>
/// Generic N-dimensional interpolation function.
/// </summary>
/// <param name="srcPixel">The input pixel values, which will be interpolated.</param>
/// <param name="destPixel">The interpolated output pixels.</param>
private unsafe void InterpolateNd(float* srcPixel, float* destPixel)
{
int index = 0;
for (int i = 0; i < this.inputCount; i++)
{
this.g[i] = UnitClip(srcPixel[i]) * this.maxGridPoint[i];
this.ig[i] = (uint)this.g[i];
this.s[this.inputCount - 1 - i] = this.g[i] - this.ig[i];
if (this.ig[i] == this.maxGridPoint[i])
else if (u >= s)
{
this.ig[i]--;
this.s[this.inputCount - 1 - i] = 1.0f;
firstVertex = xStride;
secondVertex = xStride + zStride;
firstWeight = u;
secondWeight = s;
thirdWeight = t;
}
else
{
firstVertex = zStride;
secondVertex = xStride + zStride;
firstWeight = s;
secondWeight = u;
thirdWeight = t;
}
index += (int)this.ig[i] * this.dimSize[i];
}
Span<float> p = this.lut.AsSpan(index);
float[] temp = new float[2];
bool nFlag = false;
for (int i = 0; i < this.nodeCount; i++)
else if (u >= s)
{
firstVertex = yStride;
secondVertex = xStride + yStride;
firstWeight = t;
secondWeight = u;
thirdWeight = s;
}
else if (t >= s)
{
this.df[i] = 1.0f;
firstVertex = yStride;
secondVertex = yStride + zStride;
firstWeight = t;
secondWeight = s;
thirdWeight = u;
}
else
{
firstVertex = zStride;
secondVertex = yStride + zStride;
firstWeight = s;
secondWeight = t;
thirdWeight = u;
}
for (int i = 0; i < this.inputCount; i++)
ReadOnlySpan<float> cell = this.lut.AsSpan(tableOffset + (int)((ix * xStride) + (iy * yStride) + (iz * zStride)));
// Interpolate along the tetrahedron's three edges. Sorted fractions give vertex
// weights 1-first, first-second, second-third, and third, which sum to one.
// An input at the upper boundary uses the preceding cell with fraction one;
// equal fractions give a shared face or edge the same value from either side.
int upperVertex = xStride + yStride + zStride;
if (this.inputCount == 3)
{
temp[0] = 1.0f - this.s[i];
temp[1] = this.s[i];
index = (int)this.nPower[i];
for (int j = 0; j < this.nodeCount; j++)
for (int i = 0; i < this.outputCount; i++)
{
this.df[j] *= temp[nFlag ? 1 : 0];
if ((j + 1) % index == 0)
{
nFlag = !nFlag;
}
float lower = cell[i];
float first = cell[i + firstVertex];
float second = cell[i + secondVertex];
float upper = cell[i + upperVertex];
destPixel[i] = lower
+ ((first - lower) * firstWeight)
+ ((second - first) * secondWeight)
+ ((upper - second) * thirdWeight);
}
nFlag = false;
}
int offset = 0;
for (int i = 0; i < this.outputCount; i++)
else
{
float pv = 0;
for (int j = 0; j < this.nodeCount; j++)
// Evaluate corresponding vertices in both slices and immediately blend the
// channel results. At the upper boundary both slices address the same cell.
ReadOnlySpan<float> upperCell = cell[sliceStride..];
for (int i = 0; i < this.outputCount; i++)
{
pv += p[offset + this.indexFactor[j]] * this.df[j];
float lower = cell[i];
float first = cell[i + firstVertex];
float second = cell[i + secondVertex];
float upper = cell[i + upperVertex];
float lowerValue = lower
+ ((first - lower) * firstWeight)
+ ((second - first) * secondWeight)
+ ((upper - second) * thirdWeight);
lower = upperCell[i];
first = upperCell[i + firstVertex];
second = upperCell[i + secondVertex];
upper = upperCell[i + upperVertex];
float upperValue = lower
+ ((first - lower) * firstWeight)
+ ((second - first) * secondWeight)
+ ((upper - second) * thirdWeight);
destPixel[i] = lowerValue + ((upperValue - lowerValue) * fraction);
}
destPixel[i] = pv;
offset++;
}
}
private static float UnitClip(float v)
/// <summary>
/// Interpolates the sixteen corners surrounding a four-channel input.
/// </summary>
/// <param name="srcPixel">The normalized input channels.</param>
/// <param name="destPixel">The interpolated output channels, initially zero.</param>
private unsafe void Interpolate4d(float* srcPixel, float* destPixel)
{
if (v < 0)
{
return 0;
}
if (v > 1.0)
// Each lane holds one input axis. At the upper boundary, the lower and upper
// corner share an index, so a zero stride keeps every lookup inside the table.
Vector4 position = Numerics.Clamp(new Vector4(srcPixel[0], srcPixel[1], srcPixel[2], srcPixel[3]), Vector4.Zero, Vector4.One)
* new Vector4(this.maxGridPoint[0], this.maxGridPoint[1], this.maxGridPoint[2], this.maxGridPoint[3]);
int w = (int)position.X;
int x = (int)position.Y;
int y = (int)position.Z;
int z = (int)position.W;
Vector4 fraction = position - new Vector4(w, x, y, z);
Vector4 inverse = Vector4.One - fraction;
int offset = (w * this.dimSize[0]) + (x * this.dimSize[1]) + (y * this.dimSize[2]) + (z * this.dimSize[3]);
int dw = w == this.maxGridPoint[0] ? 0 : this.dimSize[0];
int dx = x == this.maxGridPoint[1] ? 0 : this.dimSize[1];
int dy = y == this.maxGridPoint[2] ? 0 : this.dimSize[2];
int dz = z == this.maxGridPoint[3] ? 0 : this.dimSize[3];
// The low bit selects the first axis. Multiply weights from the last axis
// to the first, and reuse each corner's weight across all output channels.
for (int corner = 0; corner < 16; corner++)
{
return 1.0f;
float weight = ((corner & 8) == 0 ? inverse.W : fraction.W)
* ((corner & 4) == 0 ? inverse.Z : fraction.Z)
* ((corner & 2) == 0 ? inverse.Y : fraction.Y)
* ((corner & 1) == 0 ? inverse.X : fraction.X);
int index = offset
+ ((corner & 1) == 0 ? 0 : dw)
+ ((corner & 2) == 0 ? 0 : dx)
+ ((corner & 4) == 0 ? 0 : dy)
+ ((corner & 8) == 0 ? 0 : dz);
for (int channel = 0; channel < this.outputCount; channel++)
{
destPixel[channel] += this.lut[index + channel] * weight;
}
}
return v;
}
}

21
src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs

@ -21,10 +21,11 @@ internal partial class LutABCalculator : IVector4Calculator
/// Initializes a new instance of the <see cref="LutABCalculator"/> class for an ICC <c>mAB</c> transform.
/// </summary>
/// <param name="entry">The parsed A-to-B LUT entry.</param>
public LutABCalculator(IccLutAToBTagDataEntry entry)
/// <param name="useTrilinearInterpolation">Whether a three-channel table uses trilinear interpolation.</param>
public LutABCalculator(IccLutAToBTagDataEntry entry, bool useTrilinearInterpolation)
{
Guard.NotNull(entry, nameof(entry));
this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues);
this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues, useTrilinearInterpolation);
this.type = CalculationType.AtoB;
}
@ -32,10 +33,11 @@ internal partial class LutABCalculator : IVector4Calculator
/// Initializes a new instance of the <see cref="LutABCalculator"/> class for an ICC <c>mBA</c> transform.
/// </summary>
/// <param name="entry">The parsed B-to-A LUT entry.</param>
public LutABCalculator(IccLutBToATagDataEntry entry)
/// <param name="useTrilinearInterpolation">Whether a three-channel table uses trilinear interpolation.</param>
public LutABCalculator(IccLutBToATagDataEntry entry, bool useTrilinearInterpolation)
{
Guard.NotNull(entry, nameof(entry));
this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues);
this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues, useTrilinearInterpolation);
this.type = CalculationType.BtoA;
}
@ -117,7 +119,14 @@ internal partial class LutABCalculator : IVector4Calculator
/// <remarks>
/// The tag entry classes already validate channel continuity, so this method only materializes the available stages.
/// </remarks>
private void Init(IccTagDataEntry[] curveA, IccTagDataEntry[] curveB, IccTagDataEntry[] curveM, Vector3? matrix3x1, Matrix4x4? matrix3x3, IccClut clut)
private void Init(
IccTagDataEntry[] curveA,
IccTagDataEntry[] curveB,
IccTagDataEntry[] curveM,
Vector3? matrix3x1,
Matrix4x4? matrix3x3,
IccClut clut,
bool useTrilinearInterpolation)
{
bool hasACurve = curveA != null;
bool hasBCurve = curveB != null;
@ -152,7 +161,7 @@ internal partial class LutABCalculator : IVector4Calculator
if (hasClut)
{
this.clutCalculator = new ClutCalculator(clut);
this.clutCalculator = new ClutCalculator(clut, useTrilinearInterpolation);
}
}
}

12
src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs

@ -16,17 +16,17 @@ internal class LutEntryCalculator : IVector4Calculator
private Matrix4x4 matrix;
private bool doTransform;
public LutEntryCalculator(IccLut8TagDataEntry lut)
public LutEntryCalculator(IccLut8TagDataEntry lut, bool useTrilinearInterpolation)
{
Guard.NotNull(lut, nameof(lut));
this.Init(lut.InputValues, lut.OutputValues, lut.ClutValues, lut.Matrix);
this.Init(lut.InputValues, lut.OutputValues, lut.ClutValues, lut.Matrix, useTrilinearInterpolation);
this.Is16Bit = false;
}
public LutEntryCalculator(IccLut16TagDataEntry lut)
public LutEntryCalculator(IccLut16TagDataEntry lut, bool useTrilinearInterpolation)
{
Guard.NotNull(lut, nameof(lut));
this.Init(lut.InputValues, lut.OutputValues, lut.ClutValues, lut.Matrix);
this.Init(lut.InputValues, lut.OutputValues, lut.ClutValues, lut.Matrix, useTrilinearInterpolation);
this.Is16Bit = true;
}
@ -57,11 +57,11 @@ internal class LutEntryCalculator : IVector4Calculator
return value;
}
private void Init(IccLut[] inputCurve, IccLut[] outputCurve, IccClut clut, Matrix4x4 matrix)
private void Init(IccLut[] inputCurve, IccLut[] outputCurve, IccClut clut, Matrix4x4 matrix, bool useTrilinearInterpolation)
{
this.inputCurve = InitLut(inputCurve);
this.outputCurve = InitLut(outputCurve);
this.clutCalculator = new ClutCalculator(clut);
this.clutCalculator = new ClutCalculator(clut, useTrilinearInterpolation);
this.matrix = matrix;
this.doTransform = !matrix.IsIdentity && inputCurve.Length == 3;

44
src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs → src/ImageSharp/ColorProfiles/Icc/IccConverterBase.Conversions.cs

@ -24,7 +24,8 @@ internal abstract partial class IccConverterBase
/// <param name="toPcs">True if the conversion is to the Profile Connection Space.</param>
/// <param name="renderingIntent">The wanted rendering intent. Can be ignored if not available.</param>
/// <exception cref="InvalidIccProfileException">Invalid conversion method.</exception>
protected void Init(IccProfile profile, bool toPcs, IccRenderingIntent renderingIntent)
/// <param name="interpolationMethod">The interpolation method used for color lookup tables.</param>
protected void Init(IccProfile profile, bool toPcs, IccRenderingIntent renderingIntent, IccInterpolationMethod interpolationMethod)
=> this.calculator = GetConversionMethod(profile, renderingIntent) switch
{
ConversionMethod.D0 => toPcs ?
@ -40,28 +41,45 @@ internal abstract partial class IccConverterBase
InitD(profile, IccProfileTag.DToB3) :
InitD(profile, IccProfileTag.BToD3),
ConversionMethod.A0 => toPcs ?
InitA(profile, IccProfileTag.AToB0) :
InitA(profile, IccProfileTag.BToA0),
InitA(profile, IccProfileTag.AToB0, interpolationMethod) :
InitA(profile, IccProfileTag.BToA0, interpolationMethod),
ConversionMethod.A1 => toPcs ?
InitA(profile, IccProfileTag.AToB1) :
InitA(profile, IccProfileTag.BToA1),
InitA(profile, IccProfileTag.AToB1, interpolationMethod) :
InitA(profile, IccProfileTag.BToA1, interpolationMethod),
ConversionMethod.A2 => toPcs ?
InitA(profile, IccProfileTag.AToB2) :
InitA(profile, IccProfileTag.BToA2),
InitA(profile, IccProfileTag.AToB2, interpolationMethod) :
InitA(profile, IccProfileTag.BToA2, interpolationMethod),
ConversionMethod.ColorTrc => InitColorTrc(profile, toPcs),
ConversionMethod.GrayTrc => InitGrayTrc(profile, toPcs),
_ => throw new InvalidIccProfileException("Invalid conversion method."),
};
private static IVector4Calculator InitA(IccProfile profile, IccProfileTag tag)
=> GetTag(profile, tag) switch
/// <summary>
/// Creates a LUT calculator with interpolation selected for its input color space and direction.
/// </summary>
/// <param name="profile">The profile containing the table.</param>
/// <param name="tag">The transform tag to evaluate.</param>
/// <returns>The configured table calculator.</returns>
/// <param name="interpolationMethod">The requested interpolation method.</param>
private static IVector4Calculator InitA(IccProfile profile, IccProfileTag tag, IccInterpolationMethod interpolationMethod)
{
// Lab-indexed output and linking tables use independent-axis interpolation.
// Device-to-PCS tables use tetrahedra in their final three input dimensions.
bool useTrilinearInterpolation = interpolationMethod == IccInterpolationMethod.Trilinear
|| (interpolationMethod == IccInterpolationMethod.Auto
&& profile.Header.ProfileConnectionSpace == IccColorSpaceType.CieLab
&& (tag is IccProfileTag.BToA0 or IccProfileTag.BToA1 or IccProfileTag.BToA2
|| profile.Header.Class is IccProfileClass.DeviceLink or IccProfileClass.Abstract));
return GetTag(profile, tag) switch
{
IccLut8TagDataEntry lut8 => new LutEntryCalculator(lut8),
IccLut16TagDataEntry lut16 => new LutEntryCalculator(lut16),
IccLutAToBTagDataEntry lutAtoB => new LutABCalculator(lutAtoB),
IccLutBToATagDataEntry lutBtoA => new LutABCalculator(lutBtoA),
IccLut8TagDataEntry lut8 => new LutEntryCalculator(lut8, useTrilinearInterpolation),
IccLut16TagDataEntry lut16 => new LutEntryCalculator(lut16, useTrilinearInterpolation),
IccLutAToBTagDataEntry lutAtoB => new LutABCalculator(lutAtoB, useTrilinearInterpolation),
IccLutBToATagDataEntry lutBtoA => new LutABCalculator(lutBtoA, useTrilinearInterpolation),
_ => throw new InvalidIccProfileException($"Invalid entry {tag}."),
};
}
private static IVector4Calculator InitD(IccProfile profile, IccProfileTag tag)
{

5
src/ImageSharp/ColorProfiles/Icc/IccConverterbase.cs → src/ImageSharp/ColorProfiles/Icc/IccConverterBase.cs

@ -18,10 +18,11 @@ internal abstract partial class IccConverterBase
/// </summary>
/// <param name="profile">The ICC profile to use for the conversions</param>
/// <param name="toPcs">True if the conversion is to the profile connection space (PCS); False if the conversion is to the data space</param>
protected IccConverterBase(IccProfile profile, bool toPcs)
/// <param name="interpolationMethod">The interpolation method used for color lookup tables.</param>
protected IccConverterBase(IccProfile profile, bool toPcs, IccInterpolationMethod interpolationMethod)
{
Guard.NotNull(profile, nameof(profile));
this.Init(profile, toPcs, profile.Header.RenderingIntent);
this.Init(profile, toPcs, profile.Header.RenderingIntent, interpolationMethod);
}
/// <summary>

5
src/ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs

@ -15,8 +15,9 @@ internal class IccDataToDataConverter : IccConverterBase
/// Initializes a new instance of the <see cref="IccDataToDataConverter"/> class.
/// </summary>
/// <param name="profile">The ICC profile to use for the conversions</param>
public IccDataToDataConverter(IccProfile profile)
: base(profile, true) // toPCS is true because in this case the PCS space is also a data space
/// <param name="interpolationMethod">The interpolation method used for color lookup tables.</param>
public IccDataToDataConverter(IccProfile profile, IccInterpolationMethod interpolationMethod)
: base(profile, true, interpolationMethod) // toPCS is true because in this case the PCS space is also a data space
{
}
}

5
src/ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs

@ -15,8 +15,9 @@ internal class IccDataToPcsConverter : IccConverterBase
/// Initializes a new instance of the <see cref="IccDataToPcsConverter"/> class.
/// </summary>
/// <param name="profile">The ICC profile to use for the conversions</param>
public IccDataToPcsConverter(IccProfile profile)
: base(profile, true)
/// <param name="interpolationMethod">The interpolation method used for color lookup tables.</param>
public IccDataToPcsConverter(IccProfile profile, IccInterpolationMethod interpolationMethod)
: base(profile, true, interpolationMethod)
{
}
}

5
src/ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs

@ -15,8 +15,9 @@ internal class IccPcsToDataConverter : IccConverterBase
/// Initializes a new instance of the <see cref="IccPcsToDataConverter"/> class.
/// </summary>
/// <param name="profile">The ICC profile to use for the conversions</param>
public IccPcsToDataConverter(IccProfile profile)
: base(profile, false)
/// <param name="interpolationMethod">The interpolation method used for color lookup tables.</param>
public IccPcsToDataConverter(IccProfile profile, IccInterpolationMethod interpolationMethod)
: base(profile, false, interpolationMethod)
{
}
}

5
src/ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs

@ -15,8 +15,9 @@ internal class IccPcsToPcsConverter : IccConverterBase
/// Initializes a new instance of the <see cref="IccPcsToPcsConverter"/> class.
/// </summary>
/// <param name="profile">The ICC profile to use for the conversions</param>
public IccPcsToPcsConverter(IccProfile profile)
: base(profile, true)
/// <param name="interpolationMethod">The interpolation method used for color lookup tables.</param>
public IccPcsToPcsConverter(IccProfile profile, IccInterpolationMethod interpolationMethod)
: base(profile, true, interpolationMethod)
{
}
}

25
src/ImageSharp/ColorProfiles/IccInterpolationMethod.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Defines the interpolation method for ICC color lookup tables.
/// </summary>
public enum IccInterpolationMethod
{
/// <summary>
/// Selects trilinear interpolation for Lab output and Lab device-link or abstract profiles, and tetrahedral interpolation otherwise.
/// </summary>
Auto,
/// <summary>
/// Uses trilinear interpolation for three input channels and multilinear interpolation for four input channels.
/// </summary>
Trilinear,
/// <summary>
/// Uses tetrahedral interpolation for three input channels and linearly blends tetrahedral results for four input channels.
/// </summary>
Tetrahedral
}

53
src/ImageSharp/Common/Helpers/DisposableDictionary.cs

@ -0,0 +1,53 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Common.Helpers;
/// <summary>
/// Dictionary of <see cref="IDisposable"/> objects, which is itself <see cref="IDisposable"/>.
/// </summary>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <typeparam name="TValue">Tye type of value, needs to implement <see cref="IDisposable"/>.</typeparam>
public sealed class DisposableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IDisposable
where TKey : notnull
where TValue : IDisposable
{
private bool disposedValue;
/// <inheritdoc />
public DisposableDictionary()
: base()
{
}
/// <inheritdoc />
public DisposableDictionary(int capacity)
: base(capacity)
{
}
/// <inheritdoc />
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
this.Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!this.disposedValue)
{
if (disposing)
{
foreach (KeyValuePair<TKey, TValue> pair in this)
{
pair.Value?.Dispose();
}
}
this.Clear();
this.disposedValue = true;
}
}
}

51
src/ImageSharp/Common/Helpers/DisposableList.cs

@ -0,0 +1,51 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Common.Helpers;
/// <summary>
/// List of <see cref="IDisposable"/> objects, which is itself <see cref="IDisposable"/>.
/// </summary>
/// <typeparam name="TValue">Tye type of value, needs to implement <see cref="IDisposable"/>.</typeparam>
public sealed class DisposableList<TValue> : List<TValue>, IDisposable
where TValue : IDisposable
{
private bool disposedValue;
/// <inheritdoc />
public DisposableList()
: base()
{
}
/// <inheritdoc />
public DisposableList(int capacity)
: base(capacity)
{
}
/// <inheritdoc />
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
this.Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!this.disposedValue)
{
if (disposing)
{
foreach (TValue item in this)
{
item?.Dispose();
}
}
this.Clear();
this.disposedValue = true;
}
}
}

40
src/ImageSharp/Common/Helpers/Numerics.cs

@ -1024,6 +1024,46 @@ internal static class Numerics
where TVector : struct
=> (uint)span.Length / (uint)Vector512<TVector>.Count;
/// <summary>
/// Gets the count of vectors that safely fit into a span whose element type matches the vector lane type.
/// </summary>
/// <typeparam name="TVector">The type of the span elements and vector lanes.</typeparam>
/// <param name="span">The given span.</param>
/// <returns>Count of vectors that safely fit into the span.</returns>
public static nuint Vector128Count<TVector>(this ReadOnlySpan<TVector> span)
where TVector : struct
=> (uint)span.Length / (uint)Vector128<TVector>.Count;
/// <summary>
/// Gets the count of vectors that safely fit into a span whose element type matches the vector lane type.
/// </summary>
/// <typeparam name="TVector">The type of the span elements and vector lanes.</typeparam>
/// <param name="span">The given span.</param>
/// <returns>Count of vectors that safely fit into the span.</returns>
public static nuint Vector256Count<TVector>(this ReadOnlySpan<TVector> span)
where TVector : struct
=> (uint)span.Length / (uint)Vector256<TVector>.Count;
/// <summary>
/// Gets the count of vectors that safely fit into a span whose element type matches the vector lane type.
/// </summary>
/// <typeparam name="TVector">The type of the span elements and vector lanes.</typeparam>
/// <param name="span">The given span.</param>
/// <returns>Count of vectors that safely fit into the span.</returns>
public static nuint Vector512Count<TVector>(this ReadOnlySpan<TVector> span)
where TVector : struct
=> (uint)span.Length / (uint)Vector512<TVector>.Count;
/// <summary>
/// Gets the count of vectors that safely fit into the given length.
/// </summary>
/// <typeparam name="TVector">The type of the vector.</typeparam>
/// <param name="length">The given length.</param>
/// <returns>Count of vectors that safely fit into the length.</returns>
public static nuint Vector128Count<TVector>(int length)
where TVector : struct
=> (uint)length / (uint)Vector128<TVector>.Count;
/// <summary>
/// Gets the count of vectors that safely fit into length.
/// </summary>

185
src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs

@ -9,13 +9,12 @@ using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp;
internal static partial class SimdUtils
{
public static class HwIntrinsics
public static partial class HwIntrinsics
{
#pragma warning disable SA1117 // Parameters should be on same line or separate lines
#pragma warning disable SA1137 // Elements should have the same indentation
@ -25,31 +24,15 @@ internal static partial class SimdUtils
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<int> PermuteMaskDeinterleave16x32() => Vector512.Create(0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<uint> PermuteMaskEvenOdd8x32() => Vector256.Create(0, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0).AsUInt32();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<uint> PermuteMaskSwitchInnerDWords8x32() => Vector256.Create(0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0).AsUInt32();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<uint> MoveFirst24BytesToSeparateLanes() => Vector256.Create(0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 6, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0).AsUInt32();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector256<byte> ExtractRgb() => Vector256.Create(0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, 0xFF, 0xFF, 0xFF, 0xFF, 0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, 0xFF, 0xFF, 0xFF, 0xFF);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> ShuffleMaskPad4Nx16() => Vector128.Create(0, 1, 2, 0x80, 3, 4, 5, 0x80, 6, 7, 8, 0x80, 9, 10, 11, 0x80);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> ShuffleMaskSlice4Nx16() => Vector128.Create(0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 0x80, 0x80, 0x80, 0x80);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<byte> ShuffleMaskShiftAlpha() => Vector256.Create(
(byte)0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 3, 7, 11, 15,
0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 3, 7, 11, 15);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<uint> PermuteMaskShiftAlpha8x32() => Vector256.Create(0u, 1, 2, 4, 5, 6, 3, 7);
#pragma warning restore SA1137 // Elements should have the same indentation
#pragma warning restore SA1117 // Parameters should be on same line or separate lines
@ -1038,171 +1021,5 @@ internal static partial class SimdUtils
}
}
}
internal static void PackFromRgbPlanesAvx2Reduce(
ref ReadOnlySpan<byte> redChannel,
ref ReadOnlySpan<byte> greenChannel,
ref ReadOnlySpan<byte> blueChannel,
ref Span<Rgb24> destination)
{
ref Vector256<byte> rBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(redChannel));
ref Vector256<byte> gBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(greenChannel));
ref Vector256<byte> bBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(blueChannel));
ref byte dBase = ref Unsafe.As<Rgb24, byte>(ref MemoryMarshal.GetReference(destination));
nuint count = redChannel.Vector256Count<byte>();
Vector256<uint> control1 = PermuteMaskEvenOdd8x32();
Vector256<uint> control2 = PermuteMaskShiftAlpha8x32();
Vector256<byte> a = Vector256.Create((byte)255);
Vector256<byte> shuffleAlpha = ShuffleMaskShiftAlpha();
for (nuint i = 0; i < count; i++)
{
Vector256<byte> r0 = Unsafe.Add(ref rBase, i);
Vector256<byte> g0 = Unsafe.Add(ref gBase, i);
Vector256<byte> b0 = Unsafe.Add(ref bBase, i);
r0 = Avx2.PermuteVar8x32(r0.AsUInt32(), control1).AsByte();
g0 = Avx2.PermuteVar8x32(g0.AsUInt32(), control1).AsByte();
b0 = Avx2.PermuteVar8x32(b0.AsUInt32(), control1).AsByte();
Vector256<byte> rg = Avx2.UnpackLow(r0, g0);
Vector256<byte> b1 = Avx2.UnpackLow(b0, a);
Vector256<byte> rgb1 = Avx2.UnpackLow(rg.AsUInt16(), b1.AsUInt16()).AsByte();
Vector256<byte> rgb2 = Avx2.UnpackHigh(rg.AsUInt16(), b1.AsUInt16()).AsByte();
rg = Avx2.UnpackHigh(r0, g0);
b1 = Avx2.UnpackHigh(b0, a);
Vector256<byte> rgb3 = Avx2.UnpackLow(rg.AsUInt16(), b1.AsUInt16()).AsByte();
Vector256<byte> rgb4 = Avx2.UnpackHigh(rg.AsUInt16(), b1.AsUInt16()).AsByte();
rgb1 = Avx2.Shuffle(rgb1, shuffleAlpha);
rgb2 = Avx2.Shuffle(rgb2, shuffleAlpha);
rgb3 = Avx2.Shuffle(rgb3, shuffleAlpha);
rgb4 = Avx2.Shuffle(rgb4, shuffleAlpha);
rgb1 = Avx2.PermuteVar8x32(rgb1.AsUInt32(), control2).AsByte();
rgb2 = Avx2.PermuteVar8x32(rgb2.AsUInt32(), control2).AsByte();
rgb3 = Avx2.PermuteVar8x32(rgb3.AsUInt32(), control2).AsByte();
rgb4 = Avx2.PermuteVar8x32(rgb4.AsUInt32(), control2).AsByte();
ref byte d1 = ref Unsafe.Add(ref dBase, 24 * 4 * i);
ref byte d2 = ref Unsafe.Add(ref d1, 24);
ref byte d3 = ref Unsafe.Add(ref d2, 24);
ref byte d4 = ref Unsafe.Add(ref d3, 24);
Unsafe.As<byte, Vector256<byte>>(ref d1) = rgb1;
Unsafe.As<byte, Vector256<byte>>(ref d2) = rgb2;
Unsafe.As<byte, Vector256<byte>>(ref d3) = rgb3;
Unsafe.As<byte, Vector256<byte>>(ref d4) = rgb4;
}
int slice = (int)count * Vector256<byte>.Count;
redChannel = redChannel[slice..];
greenChannel = greenChannel[slice..];
blueChannel = blueChannel[slice..];
destination = destination[slice..];
}
internal static void PackFromRgbPlanesAvx2Reduce(
ref ReadOnlySpan<byte> redChannel,
ref ReadOnlySpan<byte> greenChannel,
ref ReadOnlySpan<byte> blueChannel,
ref Span<Rgba32> destination)
{
ref Vector256<byte> rBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(redChannel));
ref Vector256<byte> gBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(greenChannel));
ref Vector256<byte> bBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(blueChannel));
ref Vector256<byte> dBase = ref Unsafe.As<Rgba32, Vector256<byte>>(ref MemoryMarshal.GetReference(destination));
nuint count = redChannel.Vector256Count<byte>();
Vector256<uint> control1 = PermuteMaskEvenOdd8x32();
Vector256<byte> a = Vector256.Create((byte)255);
for (nuint i = 0; i < count; i++)
{
Vector256<byte> r0 = Unsafe.Add(ref rBase, i);
Vector256<byte> g0 = Unsafe.Add(ref gBase, i);
Vector256<byte> b0 = Unsafe.Add(ref bBase, i);
r0 = Avx2.PermuteVar8x32(r0.AsUInt32(), control1).AsByte();
g0 = Avx2.PermuteVar8x32(g0.AsUInt32(), control1).AsByte();
b0 = Avx2.PermuteVar8x32(b0.AsUInt32(), control1).AsByte();
Vector256<byte> rg = Avx2.UnpackLow(r0, g0);
Vector256<byte> b1 = Avx2.UnpackLow(b0, a);
Vector256<byte> rgb1 = Avx2.UnpackLow(rg.AsUInt16(), b1.AsUInt16()).AsByte();
Vector256<byte> rgb2 = Avx2.UnpackHigh(rg.AsUInt16(), b1.AsUInt16()).AsByte();
rg = Avx2.UnpackHigh(r0, g0);
b1 = Avx2.UnpackHigh(b0, a);
Vector256<byte> rgb3 = Avx2.UnpackLow(rg.AsUInt16(), b1.AsUInt16()).AsByte();
Vector256<byte> rgb4 = Avx2.UnpackHigh(rg.AsUInt16(), b1.AsUInt16()).AsByte();
ref Vector256<byte> d0 = ref Unsafe.Add(ref dBase, i * 4);
d0 = rgb1;
Unsafe.Add(ref d0, 1) = rgb2;
Unsafe.Add(ref d0, 2) = rgb3;
Unsafe.Add(ref d0, 3) = rgb4;
}
int slice = (int)count * Vector256<byte>.Count;
redChannel = redChannel[slice..];
greenChannel = greenChannel[slice..];
blueChannel = blueChannel[slice..];
destination = destination[slice..];
}
internal static void UnpackToRgbPlanesAvx2Reduce(
ref Span<float> redChannel,
ref Span<float> greenChannel,
ref Span<float> blueChannel,
ref ReadOnlySpan<Rgb24> source)
{
ref Vector256<byte> rgbByteSpan = ref Unsafe.As<Rgb24, Vector256<byte>>(ref MemoryMarshal.GetReference(source));
ref Vector256<float> destRRef = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(redChannel));
ref Vector256<float> destGRef = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(greenChannel));
ref Vector256<float> destBRef = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(blueChannel));
Vector256<uint> extractToLanesMask = MoveFirst24BytesToSeparateLanes();
Vector256<byte> extractRgbMask = ExtractRgb();
Vector256<byte> rgb, rg, bx;
Vector256<float> r, g, b;
// Each iteration consumes 8 Rgb24 pixels (24 bytes) but starts with a 32-byte load,
// so we need 3 extra pixels of addressable slack beyond the vectorized chunk.
const int bytesPerRgbStride = 24;
nuint count = source.Length > 3 ? (uint)(source.Length - 3) / 8 : 0;
for (nuint i = 0; i < count; i++)
{
rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref rgbByteSpan, (uint)(bytesPerRgbStride * i)).AsUInt32(), extractToLanesMask).AsByte();
rgb = Avx2.Shuffle(rgb, extractRgbMask);
rg = Avx2.UnpackLow(rgb, Vector256<byte>.Zero);
bx = Avx2.UnpackHigh(rgb, Vector256<byte>.Zero);
r = Avx.ConvertToVector256Single(Avx2.UnpackLow(rg, Vector256<byte>.Zero).AsInt32());
g = Avx.ConvertToVector256Single(Avx2.UnpackHigh(rg, Vector256<byte>.Zero).AsInt32());
b = Avx.ConvertToVector256Single(Avx2.UnpackLow(bx, Vector256<byte>.Zero).AsInt32());
Unsafe.Add(ref destRRef, i) = r;
Unsafe.Add(ref destGRef, i) = g;
Unsafe.Add(ref destBRef, i) = b;
}
int sliceCount = (int)(count * 8);
redChannel = redChannel[sliceCount..];
greenChannel = greenChannel[sliceCount..];
blueChannel = blueChannel[sliceCount..];
source = source[sliceCount..];
}
}
}

472
src/ImageSharp/Common/Helpers/SimdUtils.Pack.cs

@ -3,7 +3,9 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp;
@ -19,11 +21,16 @@ internal static partial class SimdUtils
{
DebugGuard.IsTrue(greenChannel.Length == redChannel.Length, nameof(greenChannel), "Channels must be of same size!");
DebugGuard.IsTrue(blueChannel.Length == redChannel.Length, nameof(blueChannel), "Channels must be of same size!");
DebugGuard.IsTrue(destination.Length > redChannel.Length + 2, nameof(destination), "'destination' must contain a padding of 3 elements!");
DebugGuard.IsTrue(destination.Length >= redChannel.Length, nameof(destination), "'destination' span should not be shorter than the source channels!");
if (Avx2.IsSupported)
{
HwIntrinsics.PackFromRgbPlanesAvx2Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination);
HwIntrinsics.PackFromRgbPlanesReduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination);
}
if (Vector128.IsHardwareAccelerated)
{
PackFromRgbPlanesVector128Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination);
}
else
{
@ -42,11 +49,16 @@ internal static partial class SimdUtils
{
DebugGuard.IsTrue(greenChannel.Length == redChannel.Length, nameof(greenChannel), "Channels must be of same size!");
DebugGuard.IsTrue(blueChannel.Length == redChannel.Length, nameof(blueChannel), "Channels must be of same size!");
DebugGuard.IsTrue(destination.Length > redChannel.Length, nameof(destination), "'destination' span should not be shorter than the source channels!");
DebugGuard.IsTrue(destination.Length >= redChannel.Length, nameof(destination), "'destination' span should not be shorter than the source channels!");
if (Avx2.IsSupported)
{
HwIntrinsics.PackFromRgbPlanesAvx2Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination);
HwIntrinsics.PackFromRgbPlanesReduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination);
}
if (Vector128.IsHardwareAccelerated)
{
PackFromRgbPlanesVector128Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination);
}
else
{
@ -69,12 +81,143 @@ internal static partial class SimdUtils
if (Avx2.IsSupported)
{
HwIntrinsics.UnpackToRgbPlanesAvx2Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref source);
HwIntrinsics.UnpackToRgbPlanesReduce(ref redChannel, ref greenChannel, ref blueChannel, ref source);
}
UnpackToRgbPlanesScalar(redChannel, greenChannel, blueChannel, source);
}
/// <summary>
/// Packs complete sixteen-pixel batches into exact-length <see cref="Rgb24"/> storage using portable 128-bit SIMD.
/// </summary>
/// <param name="redChannel">The red source span, advanced past the converted batches.</param>
/// <param name="greenChannel">The green source span, advanced past the converted batches.</param>
/// <param name="blueChannel">The blue source span, advanced past the converted batches.</param>
/// <param name="destination">The destination span, advanced past the converted batches.</param>
private static void PackFromRgbPlanesVector128Reduce(
ref ReadOnlySpan<byte> redChannel,
ref ReadOnlySpan<byte> greenChannel,
ref ReadOnlySpan<byte> blueChannel,
ref Span<Rgb24> destination)
{
ref byte redBase = ref MemoryMarshal.GetReference(redChannel);
ref byte greenBase = ref MemoryMarshal.GetReference(greenChannel);
ref byte blueBase = ref MemoryMarshal.GetReference(blueChannel);
ref byte destinationBase = ref Unsafe.As<Rgb24, byte>(ref MemoryMarshal.GetReference(destination));
Vector128<byte> opaqueAlpha = Vector128.Create(byte.MaxValue);
Vector128<byte> removeAlpha = Vector128.Create((byte)0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);
nuint batchCount = (nuint)(uint)redChannel.Length / (uint)Vector128<byte>.Count;
for (nuint i = 0; i < batchCount; i++)
{
nuint sourceOffset = i * (uint)Vector128<byte>.Count;
Vector128<byte> red = Vector128.LoadUnsafe(ref redBase, sourceOffset);
Vector128<byte> green = Vector128.LoadUnsafe(ref greenBase, sourceOffset);
Vector128<byte> blue = Vector128.LoadUnsafe(ref blueBase, sourceOffset);
InterleaveRgbPlanes(red, green, blue, opaqueAlpha, out Vector128<byte> rgba0, out Vector128<byte> rgba1, out Vector128<byte> rgba2, out Vector128<byte> rgba3);
// The native byte shuffle removes alpha from four pixels at a time. Each result owns twelve bytes, so
// exact stores avoid coupling the SIMD path to padding beyond the row or the next memory-group segment.
ref byte destination0 = ref Unsafe.Add(ref destinationBase, i * 48);
StoreRgb24Batch(Vector128.ShuffleNative(rgba0, removeAlpha), ref destination0);
StoreRgb24Batch(Vector128.ShuffleNative(rgba1, removeAlpha), ref Unsafe.Add(ref destination0, 12));
StoreRgb24Batch(Vector128.ShuffleNative(rgba2, removeAlpha), ref Unsafe.Add(ref destination0, 24));
StoreRgb24Batch(Vector128.ShuffleNative(rgba3, removeAlpha), ref Unsafe.Add(ref destination0, 36));
}
int convertedCount = (int)(batchCount * (uint)Vector128<byte>.Count);
redChannel = redChannel[convertedCount..];
greenChannel = greenChannel[convertedCount..];
blueChannel = blueChannel[convertedCount..];
destination = destination[convertedCount..];
}
/// <summary>
/// Packs complete sixteen-pixel batches into exact-length <see cref="Rgba32"/> storage using portable 128-bit SIMD.
/// </summary>
/// <param name="redChannel">The red source span, advanced past the converted batches.</param>
/// <param name="greenChannel">The green source span, advanced past the converted batches.</param>
/// <param name="blueChannel">The blue source span, advanced past the converted batches.</param>
/// <param name="destination">The destination span, advanced past the converted batches.</param>
private static void PackFromRgbPlanesVector128Reduce(
ref ReadOnlySpan<byte> redChannel,
ref ReadOnlySpan<byte> greenChannel,
ref ReadOnlySpan<byte> blueChannel,
ref Span<Rgba32> destination)
{
ref byte redBase = ref MemoryMarshal.GetReference(redChannel);
ref byte greenBase = ref MemoryMarshal.GetReference(greenChannel);
ref byte blueBase = ref MemoryMarshal.GetReference(blueChannel);
ref Vector128<byte> destinationBase = ref Unsafe.As<Rgba32, Vector128<byte>>(ref MemoryMarshal.GetReference(destination));
Vector128<byte> opaqueAlpha = Vector128.Create(byte.MaxValue);
nuint batchCount = (nuint)(uint)redChannel.Length / (uint)Vector128<byte>.Count;
for (nuint i = 0; i < batchCount; i++)
{
nuint sourceOffset = i * (uint)Vector128<byte>.Count;
Vector128<byte> red = Vector128.LoadUnsafe(ref redBase, sourceOffset);
Vector128<byte> green = Vector128.LoadUnsafe(ref greenBase, sourceOffset);
Vector128<byte> blue = Vector128.LoadUnsafe(ref blueBase, sourceOffset);
InterleaveRgbPlanes(red, green, blue, opaqueAlpha, out Vector128<byte> rgba0, out Vector128<byte> rgba1, out Vector128<byte> rgba2, out Vector128<byte> rgba3);
ref Vector128<byte> destination0 = ref Unsafe.Add(ref destinationBase, i * 4);
destination0 = rgba0;
Unsafe.Add(ref destination0, 1) = rgba1;
Unsafe.Add(ref destination0, 2) = rgba2;
Unsafe.Add(ref destination0, 3) = rgba3;
}
int convertedCount = (int)(batchCount * (uint)Vector128<byte>.Count);
redChannel = redChannel[convertedCount..];
greenChannel = greenChannel[convertedCount..];
blueChannel = blueChannel[convertedCount..];
destination = destination[convertedCount..];
}
/// <summary>
/// Interleaves sixteen planar RGB samples into four groups of four opaque RGBA pixels.
/// </summary>
/// <param name="red">The red component lanes.</param>
/// <param name="green">The green component lanes.</param>
/// <param name="blue">The blue component lanes.</param>
/// <param name="alpha">The opaque alpha lanes.</param>
/// <param name="rgba0">The first four interleaved pixels.</param>
/// <param name="rgba1">The second four interleaved pixels.</param>
/// <param name="rgba2">The third four interleaved pixels.</param>
/// <param name="rgba3">The fourth four interleaved pixels.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InterleaveRgbPlanes(
Vector128<byte> red,
Vector128<byte> green,
Vector128<byte> blue,
Vector128<byte> alpha,
out Vector128<byte> rgba0,
out Vector128<byte> rgba1,
out Vector128<byte> rgba2,
out Vector128<byte> rgba3)
{
Vector128<byte> redGreenLow = Vector128_.UnpackLow(red, green);
Vector128<byte> redGreenHigh = Vector128_.UnpackHigh(red, green);
Vector128<byte> blueAlphaLow = Vector128_.UnpackLow(blue, alpha);
Vector128<byte> blueAlphaHigh = Vector128_.UnpackHigh(blue, alpha);
rgba0 = Vector128_.UnpackLow(redGreenLow.AsInt16(), blueAlphaLow.AsInt16()).AsByte();
rgba1 = Vector128_.UnpackHigh(redGreenLow.AsInt16(), blueAlphaLow.AsInt16()).AsByte();
rgba2 = Vector128_.UnpackLow(redGreenHigh.AsInt16(), blueAlphaHigh.AsInt16()).AsByte();
rgba3 = Vector128_.UnpackHigh(redGreenHigh.AsInt16(), blueAlphaHigh.AsInt16()).AsByte();
}
/// <summary>
/// Stores the twelve packed RGB bytes in one shuffled SIMD value without writing its unused lanes.
/// </summary>
/// <param name="value">The packed RGB bytes in the first twelve lanes.</param>
/// <param name="destination">The first destination byte.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void StoreRgb24Batch(Vector128<byte> value, ref byte destination)
{
Unsafe.WriteUnaligned(ref destination, value.AsUInt64().ToScalar());
Unsafe.WriteUnaligned(ref Unsafe.Add(ref destination, 8), value.AsUInt32().GetElement(2));
}
private static void PackFromRgbPlanesScalarBatchedReduce(
ref ReadOnlySpan<byte> redChannel,
ref ReadOnlySpan<byte> greenChannel,
@ -86,8 +229,8 @@ internal static partial class SimdUtils
ref ByteTuple4 b = ref Unsafe.As<byte, ByteTuple4>(ref MemoryMarshal.GetReference(blueChannel));
ref Rgb24 rgb = ref MemoryMarshal.GetReference(destination);
nuint count = (uint)redChannel.Length / 4;
for (nuint i = 0; i < count; i++)
nuint batchCount = (uint)redChannel.Length / 4;
for (nuint i = 0; i < batchCount; i++)
{
ref Rgb24 d0 = ref Unsafe.Add(ref rgb, i * 4);
ref Rgb24 d1 = ref Unsafe.Add(ref d0, 1);
@ -115,11 +258,11 @@ internal static partial class SimdUtils
d3.B = bb.V3;
}
int finished = (int)(count * 4);
redChannel = redChannel[finished..];
greenChannel = greenChannel[finished..];
blueChannel = blueChannel[finished..];
destination = destination[finished..];
int convertedCount = (int)(batchCount * 4);
redChannel = redChannel[convertedCount..];
greenChannel = greenChannel[convertedCount..];
blueChannel = blueChannel[convertedCount..];
destination = destination[convertedCount..];
}
private static void PackFromRgbPlanesScalarBatchedReduce(
@ -133,9 +276,9 @@ internal static partial class SimdUtils
ref ByteTuple4 b = ref Unsafe.As<byte, ByteTuple4>(ref MemoryMarshal.GetReference(blueChannel));
ref Rgba32 rgb = ref MemoryMarshal.GetReference(destination);
nuint count = (uint)redChannel.Length / 4;
nuint batchCount = (uint)redChannel.Length / 4;
destination.Fill(new Rgba32(0, 0, 0, 255));
for (nuint i = 0; i < count; i++)
for (nuint i = 0; i < batchCount; i++)
{
ref Rgba32 d0 = ref Unsafe.Add(ref rgb, i * 4);
ref Rgba32 d1 = ref Unsafe.Add(ref d0, 1);
@ -163,11 +306,11 @@ internal static partial class SimdUtils
d3.B = bb.V3;
}
int finished = (int)(count * 4);
redChannel = redChannel[finished..];
greenChannel = greenChannel[finished..];
blueChannel = blueChannel[finished..];
destination = destination[finished..];
int convertedCount = (int)(batchCount * 4);
redChannel = redChannel[convertedCount..];
greenChannel = greenChannel[convertedCount..];
blueChannel = blueChannel[convertedCount..];
destination = destination[convertedCount..];
}
private static void PackFromRgbPlanesRemainder(
@ -181,7 +324,7 @@ internal static partial class SimdUtils
ref byte b = ref MemoryMarshal.GetReference(blueChannel);
ref Rgb24 rgb = ref MemoryMarshal.GetReference(destination);
for (nuint i = 0; i < (uint)destination.Length; i++)
for (nuint i = 0; i < (uint)redChannel.Length; i++)
{
ref Rgb24 d = ref Unsafe.Add(ref rgb, i);
d.R = Unsafe.Add(ref r, i);
@ -201,7 +344,7 @@ internal static partial class SimdUtils
ref byte b = ref MemoryMarshal.GetReference(blueChannel);
ref Rgba32 rgba = ref MemoryMarshal.GetReference(destination);
for (nuint i = 0; i < (uint)destination.Length; i++)
for (nuint i = 0; i < (uint)redChannel.Length; i++)
{
ref Rgba32 d = ref Unsafe.Add(ref rgba, i);
d.R = Unsafe.Add(ref r, i);
@ -234,4 +377,291 @@ internal static partial class SimdUtils
Unsafe.Add(ref b, i) = src.B;
}
}
/// <summary>
/// Provides the hardware-intrinsic reducers used by the planar RGB packing pipeline.
/// </summary>
public static partial class HwIntrinsics
{
/// <summary>
/// Creates the AVX2 lane order used before interleaving planar RGB components.
/// </summary>
/// <returns>The source lane permutation.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<uint> PermuteMaskEvenOdd8x32() => Vector256.Create(0u, 2, 4, 6, 1, 3, 5, 7);
/// <summary>
/// Packs complete AVX2 batches into <see cref="Rgb24"/> pixels and retains the unconverted remainder.
/// </summary>
/// <param name="redChannel">The red source span.</param>
/// <param name="greenChannel">The green source span.</param>
/// <param name="blueChannel">The blue source span.</param>
/// <param name="destination">The destination pixel span.</param>
internal static void PackFromRgbPlanesReduce(
ref ReadOnlySpan<byte> redChannel,
ref ReadOnlySpan<byte> greenChannel,
ref ReadOnlySpan<byte> blueChannel,
ref Span<Rgb24> destination)
{
ref Vector256<byte> redBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(redChannel));
ref Vector256<byte> greenBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(greenChannel));
ref Vector256<byte> blueBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(blueChannel));
ref byte destinationBase = ref Unsafe.As<Rgb24, byte>(ref MemoryMarshal.GetReference(destination));
nuint batchCount = redChannel.Vector256Count<byte>();
Vector256<uint> sourceOrder = PermuteMaskEvenOdd8x32();
Vector256<uint> packedOrder = Vector256.Create(0u, 1, 2, 4, 5, 6, 3, 7);
Vector256<byte> opaqueAlpha = Vector256.Create(byte.MaxValue);
Vector128<byte> removeAlphaLower = Vector128.Create((byte)0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 3, 7, 11, 15);
Vector128<byte> removeAlphaUpper = Vector128.Create((byte)16, 17, 18, 20, 21, 22, 24, 25, 26, 28, 29, 30, 19, 23, 27, 31);
Vector256<byte> removeAlpha = Vector256.Create(removeAlphaLower, removeAlphaUpper);
bool hasWritablePadding = destination.Length >= redChannel.Length + 3;
nuint i = 0;
// Non-final batches retain the original four overlapping wide stores. Splitting the final batch keeps
// the exact-row decision out of the hot loop and limits the narrower stores to the only bytes that can
// cross the destination boundary.
for (; i + 1 < batchCount; i++)
{
PackRgb24Batch(
Unsafe.Add(ref redBase, i),
Unsafe.Add(ref greenBase, i),
Unsafe.Add(ref blueBase, i),
opaqueAlpha,
sourceOrder,
packedOrder,
removeAlpha,
out Vector256<byte> rgb0,
out Vector256<byte> rgb1,
out Vector256<byte> rgb2,
out Vector256<byte> rgb3);
ref byte destination0 = ref Unsafe.Add(ref destinationBase, 96 * i);
ref byte destination1 = ref Unsafe.Add(ref destination0, 24);
ref byte destination2 = ref Unsafe.Add(ref destination1, 24);
ref byte destination3 = ref Unsafe.Add(ref destination2, 24);
Unsafe.As<byte, Vector256<byte>>(ref destination0) = rgb0;
Unsafe.As<byte, Vector256<byte>>(ref destination1) = rgb1;
Unsafe.As<byte, Vector256<byte>>(ref destination2) = rgb2;
Unsafe.As<byte, Vector256<byte>>(ref destination3) = rgb3;
}
if (i < batchCount)
{
PackRgb24Batch(
Unsafe.Add(ref redBase, i),
Unsafe.Add(ref greenBase, i),
Unsafe.Add(ref blueBase, i),
opaqueAlpha,
sourceOrder,
packedOrder,
removeAlpha,
out Vector256<byte> rgb0,
out Vector256<byte> rgb1,
out Vector256<byte> rgb2,
out Vector256<byte> rgb3);
ref byte destination0 = ref Unsafe.Add(ref destinationBase, 96 * i);
ref byte destination1 = ref Unsafe.Add(ref destination0, 24);
ref byte destination2 = ref Unsafe.Add(ref destination1, 24);
ref byte destination3 = ref Unsafe.Add(ref destination2, 24);
Unsafe.As<byte, Vector256<byte>>(ref destination0) = rgb0;
Unsafe.As<byte, Vector256<byte>>(ref destination1) = rgb1;
Unsafe.As<byte, Vector256<byte>>(ref destination2) = rgb2;
if (hasWritablePadding)
{
Unsafe.As<byte, Vector256<byte>>(ref destination3) = rgb3;
}
else
{
// The final compacted vector contains 24 RGB bytes followed by eight unused bytes. Exact stores
// retain all useful bytes without writing beyond an unpadded destination row.
Unsafe.As<byte, Vector128<byte>>(ref destination3) = rgb3.GetLower();
Unsafe.As<byte, ulong>(ref Unsafe.Add(ref destination3, 16)) = rgb3.GetUpper().AsUInt64().ToScalar();
}
}
int convertedCount = (int)batchCount * Vector256<byte>.Count;
redChannel = redChannel[convertedCount..];
greenChannel = greenChannel[convertedCount..];
blueChannel = blueChannel[convertedCount..];
destination = destination[convertedCount..];
}
/// <summary>
/// Packs complete AVX2 batches into <see cref="Rgba32"/> pixels and retains the unconverted remainder.
/// </summary>
/// <param name="redChannel">The red source span.</param>
/// <param name="greenChannel">The green source span.</param>
/// <param name="blueChannel">The blue source span.</param>
/// <param name="destination">The destination pixel span.</param>
internal static void PackFromRgbPlanesReduce(
ref ReadOnlySpan<byte> redChannel,
ref ReadOnlySpan<byte> greenChannel,
ref ReadOnlySpan<byte> blueChannel,
ref Span<Rgba32> destination)
{
ref Vector256<byte> redBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(redChannel));
ref Vector256<byte> greenBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(greenChannel));
ref Vector256<byte> blueBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(blueChannel));
ref Vector256<byte> destinationBase = ref Unsafe.As<Rgba32, Vector256<byte>>(ref MemoryMarshal.GetReference(destination));
nuint batchCount = redChannel.Vector256Count<byte>();
Vector256<uint> sourceOrder = PermuteMaskEvenOdd8x32();
Vector256<byte> opaqueAlpha = Vector256.Create(byte.MaxValue);
for (nuint i = 0; i < batchCount; i++)
{
InterleaveRgbPlanes(
Unsafe.Add(ref redBase, i),
Unsafe.Add(ref greenBase, i),
Unsafe.Add(ref blueBase, i),
opaqueAlpha,
sourceOrder,
out Vector256<byte> rgba0,
out Vector256<byte> rgba1,
out Vector256<byte> rgba2,
out Vector256<byte> rgba3);
ref Vector256<byte> destination0 = ref Unsafe.Add(ref destinationBase, i * 4);
destination0 = rgba0;
Unsafe.Add(ref destination0, 1) = rgba1;
Unsafe.Add(ref destination0, 2) = rgba2;
Unsafe.Add(ref destination0, 3) = rgba3;
}
int convertedCount = (int)batchCount * Vector256<byte>.Count;
redChannel = redChannel[convertedCount..];
greenChannel = greenChannel[convertedCount..];
blueChannel = blueChannel[convertedCount..];
destination = destination[convertedCount..];
}
/// <summary>
/// Unpacks complete AVX2 batches from <see cref="Rgb24"/> pixels and retains the unconverted remainder.
/// </summary>
/// <param name="redChannel">The red destination span.</param>
/// <param name="greenChannel">The green destination span.</param>
/// <param name="blueChannel">The blue destination span.</param>
/// <param name="source">The source pixel span.</param>
internal static void UnpackToRgbPlanesReduce(
ref Span<float> redChannel,
ref Span<float> greenChannel,
ref Span<float> blueChannel,
ref ReadOnlySpan<Rgb24> source)
{
ref Vector256<byte> sourceBase = ref Unsafe.As<Rgb24, Vector256<byte>>(ref MemoryMarshal.GetReference(source));
ref Vector256<float> redBase = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(redChannel));
ref Vector256<float> greenBase = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(greenChannel));
ref Vector256<float> blueBase = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(blueChannel));
Vector256<uint> separateLanes = Vector256.Create(0u, 1, 2, 6, 3, 4, 5, 7);
Vector128<byte> extractRgbLower = Vector128.Create((byte)0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);
Vector128<byte> extractRgbUpper = Vector128.Create((byte)16, 19, 22, 25, 17, 20, 23, 26, 18, 21, 24, 27, byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);
Vector256<byte> extractRgb = Vector256.Create(extractRgbLower, extractRgbUpper);
// Each iteration consumes eight Rgb24 pixels, or 24 bytes, but starts with a 32-byte load. Three extra
// source pixels must therefore remain addressable beyond every vectorized batch.
const int bytesPerBatch = 24;
nuint batchCount = source.Length > 3 ? (uint)(source.Length - 3) / 8 : 0;
for (nuint i = 0; i < batchCount; i++)
{
Vector256<byte> packed = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref sourceBase, (uint)(bytesPerBatch * i)).AsUInt32(), separateLanes).AsByte();
packed = Vector256.ShuffleNative(packed, extractRgb);
Vector256<byte> redGreen = Avx2.UnpackLow(packed, Vector256<byte>.Zero);
Vector256<byte> blue = Avx2.UnpackHigh(packed, Vector256<byte>.Zero);
Vector256<float> red = Avx.ConvertToVector256Single(Avx2.UnpackLow(redGreen, Vector256<byte>.Zero).AsInt32());
Vector256<float> green = Avx.ConvertToVector256Single(Avx2.UnpackHigh(redGreen, Vector256<byte>.Zero).AsInt32());
Vector256<float> blueValues = Avx.ConvertToVector256Single(Avx2.UnpackLow(blue, Vector256<byte>.Zero).AsInt32());
Unsafe.Add(ref redBase, i) = red;
Unsafe.Add(ref greenBase, i) = green;
Unsafe.Add(ref blueBase, i) = blueValues;
}
int convertedCount = (int)(batchCount * 8);
redChannel = redChannel[convertedCount..];
greenChannel = greenChannel[convertedCount..];
blueChannel = blueChannel[convertedCount..];
source = source[convertedCount..];
}
/// <summary>
/// Interleaves and compacts one AVX2 batch into four groups of eight <see cref="Rgb24"/> pixels.
/// </summary>
/// <param name="red">The red component lanes.</param>
/// <param name="green">The green component lanes.</param>
/// <param name="blue">The blue component lanes.</param>
/// <param name="alpha">The opaque alpha lanes used during interleaving.</param>
/// <param name="sourceOrder">The cross-lane source permutation.</param>
/// <param name="packedOrder">The cross-lane packed RGB permutation.</param>
/// <param name="removeAlpha">The native byte-shuffle indices that compact RGBA to RGB.</param>
/// <param name="rgb0">The first eight packed pixels.</param>
/// <param name="rgb1">The second eight packed pixels.</param>
/// <param name="rgb2">The third eight packed pixels.</param>
/// <param name="rgb3">The fourth eight packed pixels.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void PackRgb24Batch(
Vector256<byte> red,
Vector256<byte> green,
Vector256<byte> blue,
Vector256<byte> alpha,
Vector256<uint> sourceOrder,
Vector256<uint> packedOrder,
Vector256<byte> removeAlpha,
out Vector256<byte> rgb0,
out Vector256<byte> rgb1,
out Vector256<byte> rgb2,
out Vector256<byte> rgb3)
{
InterleaveRgbPlanes(red, green, blue, alpha, sourceOrder, out Vector256<byte> rgba0, out Vector256<byte> rgba1, out Vector256<byte> rgba2, out Vector256<byte> rgba3);
rgb0 = Avx2.PermuteVar8x32(Vector256.ShuffleNative(rgba0, removeAlpha).AsUInt32(), packedOrder).AsByte();
rgb1 = Avx2.PermuteVar8x32(Vector256.ShuffleNative(rgba1, removeAlpha).AsUInt32(), packedOrder).AsByte();
rgb2 = Avx2.PermuteVar8x32(Vector256.ShuffleNative(rgba2, removeAlpha).AsUInt32(), packedOrder).AsByte();
rgb3 = Avx2.PermuteVar8x32(Vector256.ShuffleNative(rgba3, removeAlpha).AsUInt32(), packedOrder).AsByte();
}
/// <summary>
/// Interleaves 32 planar RGB samples into four groups of eight opaque RGBA pixels.
/// </summary>
/// <param name="red">The red component lanes.</param>
/// <param name="green">The green component lanes.</param>
/// <param name="blue">The blue component lanes.</param>
/// <param name="alpha">The opaque alpha lanes.</param>
/// <param name="sourceOrder">The cross-lane source permutation.</param>
/// <param name="rgba0">The first eight interleaved pixels.</param>
/// <param name="rgba1">The second eight interleaved pixels.</param>
/// <param name="rgba2">The third eight interleaved pixels.</param>
/// <param name="rgba3">The fourth eight interleaved pixels.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InterleaveRgbPlanes(
Vector256<byte> red,
Vector256<byte> green,
Vector256<byte> blue,
Vector256<byte> alpha,
Vector256<uint> sourceOrder,
out Vector256<byte> rgba0,
out Vector256<byte> rgba1,
out Vector256<byte> rgba2,
out Vector256<byte> rgba3)
{
red = Avx2.PermuteVar8x32(red.AsUInt32(), sourceOrder).AsByte();
green = Avx2.PermuteVar8x32(green.AsUInt32(), sourceOrder).AsByte();
blue = Avx2.PermuteVar8x32(blue.AsUInt32(), sourceOrder).AsByte();
Vector256<byte> redGreenLow = Avx2.UnpackLow(red, green);
Vector256<byte> redGreenHigh = Avx2.UnpackHigh(red, green);
Vector256<byte> blueAlphaLow = Avx2.UnpackLow(blue, alpha);
Vector256<byte> blueAlphaHigh = Avx2.UnpackHigh(blue, alpha);
rgba0 = Avx2.UnpackLow(redGreenLow.AsUInt16(), blueAlphaLow.AsUInt16()).AsByte();
rgba1 = Avx2.UnpackHigh(redGreenLow.AsUInt16(), blueAlphaLow.AsUInt16()).AsByte();
rgba2 = Avx2.UnpackLow(redGreenHigh.AsUInt16(), blueAlphaHigh.AsUInt16()).AsByte();
rgba3 = Avx2.UnpackHigh(redGreenHigh.AsUInt16(), blueAlphaHigh.AsUInt16()).AsByte();
}
}
}

81
src/ImageSharp/Common/InlineArray.cs

@ -8,6 +8,24 @@ using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp;
/// <summary>
/// Represents a safe, fixed sized buffer of 2 elements.
/// </summary>
[InlineArray(2)]
internal struct InlineArray2<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 3 elements.
/// </summary>
[InlineArray(3)]
internal struct InlineArray3<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 4 elements.
/// </summary>
@ -17,6 +35,24 @@ internal struct InlineArray4<T>
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 6 elements.
/// </summary>
[InlineArray(6)]
internal struct InlineArray6<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 7 elements.
/// </summary>
[InlineArray(7)]
internal struct InlineArray7<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 8 elements.
/// </summary>
@ -26,6 +62,24 @@ internal struct InlineArray8<T>
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 10 elements.
/// </summary>
[InlineArray(10)]
internal struct InlineArray10<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 12 elements.
/// </summary>
[InlineArray(12)]
internal struct InlineArray12<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 14 elements.
/// </summary>
@ -62,6 +116,24 @@ internal struct InlineArray19<T>
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 24 elements.
/// </summary>
[InlineArray(24)]
internal struct InlineArray24<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 25 elements.
/// </summary>
[InlineArray(25)]
internal struct InlineArray25<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 26 elements.
/// </summary>
@ -80,6 +152,15 @@ internal struct InlineArray36<T>
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 65 elements.
/// </summary>
[InlineArray(65)]
internal struct InlineArray65<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 256 elements.
/// </summary>

2
src/ImageSharp/Common/InlineArray.tt

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp;
<#GenerateInlineArrays();#>
<#+
private static int[] Lengths = [4, 8, 14, 16, 18, 19, 26, 36, 256];
private static int[] Lengths = [2, 3, 4, 6, 7, 8, 10, 12, 14, 16, 18, 19, 24, 25, 26, 36, 65, 256];
void GenerateInlineArrays()
{

3
src/ImageSharp/Configuration.cs

@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Pbm;
@ -225,6 +226,7 @@ public sealed class Configuration
/// <see cref="WebpConfigurationModule"/>.
/// <see cref="ExrConfigurationModule"/>.
/// <see cref="QoiConfigurationModule"/>.
/// <see cref="HeifConfigurationModule"/>.
/// <see cref="AniConfigurationModule"/>.
/// </summary>
/// <returns>The default configuration of <see cref="Configuration"/>.</returns>
@ -239,6 +241,7 @@ public sealed class Configuration
new WebpConfigurationModule(),
new ExrConfigurationModule(),
new QoiConfigurationModule(),
new HeifConfigurationModule(),
new IcoConfigurationModule(),
new CurConfigurationModule(),
new AniConfigurationModule());

25
src/ImageSharp/Formats/Heif/Av1/Av1BitDepth.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Identifies the sample precision of an AV1 sequence.
/// </summary>
internal enum Av1BitDepth : int
{
/// <summary>
/// Eight bits per sample.
/// </summary>
EightBit = 0,
/// <summary>
/// Ten bits per sample.
/// </summary>
TenBit = 1,
/// <summary>
/// Twelve bits per sample.
/// </summary>
TwelveBit = 2,
}

19
src/ImageSharp/Formats/Heif/Av1/Av1BitDepthExtensions.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Provides sample-precision conversions for AV1 bit-depth values.
/// </summary>
internal static class Av1BitDepthExtensions
{
/// <summary>
/// Gets the number of bits represented by an AV1 bit-depth value.
/// </summary>
/// <param name="bitDepth">The AV1 bit-depth value.</param>
/// <returns>Eight, ten, or twelve.</returns>
public static int GetBitCount(this Av1BitDepth bitDepth) => 8 + ((int)bitDepth << 1);
}

314
src/ImageSharp/Formats/Heif/Av1/Av1BitStreamReader.cs

@ -0,0 +1,314 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Reads AV1 fixed-width and variable-length syntax from a most-significant-bit-first byte span.
/// </summary>
internal ref struct Av1BitStreamReader
{
/// <summary>
/// The complete encoded byte span.
/// </summary>
private readonly Span<byte> data;
/// <summary>
/// Initializes a new instance of the <see cref="Av1BitStreamReader"/> struct.
/// </summary>
/// <param name="data">The encoded AV1 data.</param>
public Av1BitStreamReader(Span<byte> data) => this.data = data;
/// <summary>
/// Gets the zero-based position of the next bit to read.
/// </summary>
public int BitPosition { get; private set; } = 0;
/// <summary>
/// Gets the number of bytes in the reader's buffer.
/// </summary>
public readonly int Length => this.data.Length;
/// <summary>
/// Moves the next read position to the beginning of the buffer.
/// </summary>
public void Reset() => this.BitPosition = 0;
/// <summary>
/// Advances the read position without interpreting the skipped bits.
/// </summary>
/// <param name="bitCount">The number of bits to skip.</param>
public void Skip(int bitCount) => this.BitPosition += bitCount;
/// <summary>
/// Reads an unsigned fixed-width value in most-significant-bit-first order.
/// </summary>
/// <param name="bitCount">The number of bits to read.</param>
/// <returns>The decoded unsigned value.</returns>
public uint ReadLiteral(int bitCount)
{
DebugGuard.MustBeBetweenOrEqualTo(bitCount, 0, 32, nameof(bitCount));
uint literal = 0;
for (int bit = bitCount - 1; bit >= 0; bit--)
{
literal |= this.ReadBit() << bit;
}
return literal;
}
/// <summary>
/// Reads the next encoded bit.
/// </summary>
/// <returns>Zero or one.</returns>
public uint ReadBit()
{
int byteOffset = Av1Math.DivideBy8Floor(this.BitPosition);
byte shift = (byte)(7 - Av1Math.Modulus8(this.BitPosition));
this.BitPosition++;
return (uint)((this.data[byteOffset] >> shift) & 0x01);
}
/// <summary>
/// Reads the next encoded bit as a Boolean value.
/// </summary>
/// <returns><see langword="true"/> for one; otherwise, <see langword="false"/>.</returns>
public bool ReadBoolean() => this.ReadLiteral(1) > 0;
/// <summary>
/// Reads an AV1 little-endian base-128 value from a byte-aligned position.
/// </summary>
/// <param name="length">Receives the number of encoded bytes consumed.</param>
/// <returns>The decoded unsigned value.</returns>
public ulong ReadLittleEndianBytes128(out int length)
{
DebugGuard.IsTrue((this.BitPosition & 0x07) == 0, $"Reading of Little Endian 128 value only allowed on byte alignment (offset {this.BitPosition}).");
ulong value = 0;
length = 0;
for (int shift = 0; shift < 56; shift += 7)
{
uint leb128Byte = this.ReadLiteral(8);
value |= (leb128Byte & 0x7FUL) << shift;
length++;
if ((leb128Byte & 0x80U) == 0)
{
return value;
}
}
// AV1 limits unsigned LEB128 fields to eight bytes. A continuation bit in the eighth byte does not describe
// another value byte; accepting it would move the following OBU header into the declared size field.
throw new InvalidImageContentException("The AV1 LEB128 value is not terminated within eight bytes.");
}
/// <summary>
/// Reads the AV1 unsigned-variable-length code.
/// </summary>
/// <returns>The decoded unsigned value.</returns>
public uint ReadUnsignedVariableLength()
{
int leadingZerosCount = 0;
while (leadingZerosCount < 32)
{
uint bit = this.ReadLiteral(1);
if (bit == 1)
{
break;
}
leadingZerosCount++;
}
if (leadingZerosCount == 32)
{
return uint.MaxValue;
}
if (leadingZerosCount != 0)
{
uint basis = (1U << leadingZerosCount) - 1U;
uint value = this.ReadLiteral(leadingZerosCount);
return basis + value;
}
return 0;
}
/// <summary>
/// Reads a value from an alphabet whose size is not a power of two.
/// </summary>
/// <param name="n">The number of symbols in the alphabet.</param>
/// <returns>A decoded symbol in the range zero through <paramref name="n"/> minus one.</returns>
public uint ReadNonSymmetric(uint n)
{
if (n <= 1)
{
return 0;
}
int w = (int)(Av1Math.FloorLog2(n) + 1);
uint m = (uint)((1 << w) - n);
uint v = this.ReadLiteral(w - 1);
if (v < m)
{
return v;
}
return (v << 1) - m + this.ReadLiteral(1);
}
/// <summary>
/// Reads a finite subexponential value recentered around a signed reference value.
/// </summary>
/// <param name="valueMagnitude">One greater than the maximum absolute value in the signed domain.</param>
/// <param name="groupBitCount">The bit width of the first subexponential group.</param>
/// <param name="reference">The signed reference value around which smaller codewords are concentrated.</param>
/// <returns>A decoded value in the inclusive range from minus <paramref name="valueMagnitude"/> plus one through
/// <paramref name="valueMagnitude"/> minus one.</returns>
public int ReadSignedReferenceSubexponential(int valueMagnitude, int groupBitCount, int reference)
{
int shiftedReference = reference + valueMagnitude - 1;
int scaledValueCount = (valueMagnitude << 1) - 1;
return this.ReadReferenceSubexponential(scaledValueCount, groupBitCount, shiftedReference) - valueMagnitude + 1;
}
/// <summary>
/// Reads a fixed-width two's-complement signed integer.
/// </summary>
/// <param name="n">The encoded bit width.</param>
/// <returns>The sign-extended integer.</returns>
public int ReadSignedFromUnsigned(int n)
{
int signedValue;
uint value = this.ReadLiteral(n);
uint signMask = 1U << (n - 1);
if ((value & signMask) == signMask)
{
// The subtraction represents sign extension; widening first preserves the n=32 case.
signedValue = (int)((long)value - (signMask << 1));
}
else
{
signedValue = (int)value;
}
return signedValue;
}
/// <summary>
/// Reads a byte-aligned unsigned integer whose least-significant byte is encoded first.
/// </summary>
/// <param name="n">The number of bytes to read.</param>
/// <returns>The decoded unsigned integer.</returns>
public uint ReadLittleEndian(int n)
{
DebugGuard.IsTrue(Av1Math.Modulus8(this.BitPosition) == 0, "Reading of Little Endian value only allowed on byte alignment");
uint t = 0;
for (int i = 0; i < 8 * n; i += 8)
{
t += this.ReadLiteral(8) << i;
}
return t;
}
/// <summary>
/// Gets a byte-aligned tile payload for entropy decoding and advances past it.
/// </summary>
/// <param name="tileDataSize">The tile payload length in bytes.</param>
/// <returns>The tile payload span.</returns>
public Span<byte> GetSymbolReader(int tileDataSize)
=> this.ReadBytes(tileDataSize);
/// <summary>
/// Gets the next byte-aligned portion of the encoded data and advances past it.
/// </summary>
/// <param name="byteCount">The number of bytes to read.</param>
/// <returns>The requested bytes.</returns>
public Span<byte> ReadBytes(int byteCount)
{
DebugGuard.IsTrue(Av1Math.Modulus8(this.BitPosition) == 0, "Byte spans must start on a byte boundary.");
int byteOffset = Av1Math.DivideBy8Floor(this.BitPosition);
if ((uint)byteOffset > (uint)this.data.Length || (uint)byteCount > (uint)(this.data.Length - byteOffset))
{
throw new InvalidImageContentException("The AV1 payload exceeds its declared data boundary.");
}
Span<byte> payload = this.data.Slice(byteOffset, byteCount);
this.Skip(byteCount << 3);
return payload;
}
/// <summary>
/// Reads a finite subexponential value and inverse-recenters it around an unsigned reference value.
/// </summary>
/// <param name="valueCount">The number of values in the finite domain.</param>
/// <param name="groupBitCount">The bit width of the first subexponential group.</param>
/// <param name="reference">The reference value within the finite domain.</param>
/// <returns>The decoded value in the range zero through <paramref name="valueCount"/> minus one.</returns>
private int ReadReferenceSubexponential(int valueCount, int groupBitCount, int reference)
{
int value = this.ReadSubexponential(valueCount, groupBitCount);
// Recentering enumerates values by increasing distance from the reference. References in the upper half use
// the mirrored domain so the shorter side of the finite range always participates in the alternating mapping.
if ((reference << 1) <= valueCount)
{
return InverseRecenter(reference, value);
}
return valueCount - 1 - InverseRecenter(valueCount - 1 - reference, value);
}
/// <summary>
/// Reads one value from a finite subexponential code.
/// </summary>
/// <param name="valueCount">The number of values in the finite domain.</param>
/// <param name="groupBitCount">The bit width of the first subexponential group.</param>
/// <returns>The decoded zero-based value.</returns>
private int ReadSubexponential(int valueCount, int groupBitCount)
{
int groupIndex = 0;
int groupStart = 0;
while (true)
{
// AV1 keeps the first two groups at width k and then doubles each following group. Once fewer than three
// groups remain, the non-symmetric code consumes the exact finite tail without introducing unused values.
int bitCount = groupIndex == 0 ? groupBitCount : groupBitCount + groupIndex - 1;
int groupSize = 1 << bitCount;
if (valueCount <= groupStart + (3 * groupSize))
{
return (int)this.ReadNonSymmetric((uint)(valueCount - groupStart)) + groupStart;
}
if (!this.ReadBoolean())
{
return (int)this.ReadLiteral(bitCount) + groupStart;
}
groupIndex++;
groupStart += groupSize;
}
}
/// <summary>
/// Maps a nonnegative code value around a nonnegative reference value.
/// </summary>
/// <param name="reference">The recentering reference.</param>
/// <param name="value">The coded nonnegative value.</param>
/// <returns>The inverse-recentered value.</returns>
private static int InverseRecenter(int reference, int value)
{
// Codes within twice the reference alternate above and below it: even values select the upper side and odd
// values select the lower side. Larger codes lie beyond the lower-side range and map directly to the tail.
if (value > (reference << 1))
{
return value;
}
return (value & 1) == 0 ? (value >> 1) + reference : reference - ((value + 1) >> 1);
}
}

309
src/ImageSharp/Formats/Heif/Av1/Av1BitStreamWriter.cs

@ -0,0 +1,309 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Writes AV1 fixed-width and variable-length syntax to a caller-provided buffer.
/// </summary>
internal ref struct Av1BitStreamWriter
{
/// <summary>
/// The number of bits in one output byte.
/// </summary>
private const int WordSize = 8;
/// <summary>
/// The writable output buffer.
/// </summary>
private readonly Span<byte> span;
/// <summary>
/// The partially assembled output byte.
/// </summary>
private byte buffer = 0;
/// <summary>
/// Initializes a new instance of the <see cref="Av1BitStreamWriter"/> struct.
/// </summary>
/// <param name="span">The preallocated output buffer.</param>
public Av1BitStreamWriter(Span<byte> span)
{
this.span = span;
}
/// <summary>
/// Gets the zero-based position of the next output bit.
/// </summary>
public int BitPosition { get; private set; } = 0;
/// <summary>
/// Gets the current output capacity in bytes.
/// </summary>
public readonly int Capacity => this.span.Length;
/// <summary>
/// Encodes an unsigned 32-bit value using little-endian base-128 bytes.
/// </summary>
/// <param name="value">The value to encode.</param>
/// <param name="span">The destination receiving up to five bytes.</param>
/// <returns>The number of bytes written.</returns>
public static int GetLittleEndianBytes128(uint value, Span<byte> span)
{
int length = 0;
do
{
byte encodedByte = (byte)(value & 0x7fU);
value >>= 7;
if (value != 0)
{
encodedByte |= 0x80;
}
span[length++] = encodedByte;
}
while (value != 0);
return length;
}
/// <summary>
/// Advances the output position, emitting the current byte whenever the skip crosses a byte boundary.
/// </summary>
/// <param name="bitCount">The number of bits to skip.</param>
public void Skip(int bitCount)
{
this.BitPosition += bitCount;
while (this.BitPosition >= WordSize)
{
this.BitPosition -= WordSize;
this.WriteBuffer();
}
}
/// <summary>
/// Writes a partially assembled byte and resets the position for output-memory reuse.
/// </summary>
public void Flush()
{
if (Av1Math.Modulus8(this.BitPosition) != 0)
{
// Flush a partial byte also.
this.WriteBuffer();
}
this.BitPosition = 0;
}
/// <summary>
/// Writes an unsigned fixed-width value in most-significant-bit-first order.
/// </summary>
/// <param name="value">The value to write.</param>
/// <param name="bitCount">The number of low-order bits to write.</param>
public void WriteLiteral(uint value, int bitCount)
{
for (int bit = bitCount - 1; bit >= 0; bit--)
{
this.WriteBit((byte)((value >> bit) & 0x1));
}
}
/// <summary>
/// Writes one Boolean bit.
/// </summary>
/// <param name="value">The Boolean value.</param>
public void WriteBoolean(bool value)
{
byte boolByte = value ? (byte)1 : (byte)0;
this.WriteBit(boolByte);
}
/// <summary>
/// Writes a fixed-width signed integer in two's-complement form.
/// </summary>
/// <param name="signedValue">The signed value.</param>
/// <param name="n">The encoded bit width.</param>
public void WriteSignedFromUnsigned(int signedValue, int n)
{
ulong value = (ulong)signedValue;
if (signedValue < 0)
{
value += 1UL << n;
}
this.WriteLiteral((uint)value, n);
}
/// <summary>
/// Writes an unsigned 32-bit value using little-endian base-128 bytes.
/// </summary>
/// <param name="value">The value to write.</param>
public void WriteLittleEndianBytes128(uint value)
{
int wordPosition = this.BitPosition >> 3;
int bytesWritten = GetLittleEndianBytes128(value, this.span[wordPosition..]);
this.BitPosition += bytesWritten << 3;
}
/// <summary>
/// Writes a value from an alphabet whose size is not a power of two.
/// </summary>
/// <param name="value">The symbol value.</param>
/// <param name="numberOfSymbols">The number of symbols in the alphabet.</param>
public void WriteNonSymmetric(uint value, uint numberOfSymbols)
{
if (numberOfSymbols <= 1)
{
return;
}
int w = (int)(Av1Math.FloorLog2(numberOfSymbols) + 1);
uint m = (uint)((1 << w) - numberOfSymbols);
if (value < m)
{
this.WriteLiteral(value, w - 1);
}
else
{
// libaom partitions the upper values into a shorter prefix followed by the low bit of the offset from m.
uint offset = value - m;
uint k = m + (offset >> 1);
this.WriteLiteral(k, w - 1);
this.WriteLiteral(offset & 1, 1);
}
}
/// <summary>
/// Writes a finite subexponential value recentered around a signed reference value.
/// </summary>
/// <param name="value">The signed value to write.</param>
/// <param name="valueMagnitude">One greater than the maximum absolute value in the signed domain.</param>
/// <param name="groupBitCount">The bit width of the first subexponential group.</param>
/// <param name="reference">The signed reference value around which smaller codewords are concentrated.</param>
public void WriteSignedReferenceSubexponential(int value, int valueMagnitude, int groupBitCount, int reference)
{
int shiftedReference = reference + valueMagnitude - 1;
int shiftedValue = value + valueMagnitude - 1;
int scaledValueCount = (valueMagnitude << 1) - 1;
int recenteredValue = RecenterFiniteNonNegative(scaledValueCount, shiftedReference, shiftedValue);
this.WriteSubexponential(recenteredValue, scaledValueCount, groupBitCount);
}
/// <summary>
/// Writes one value using a finite sequence of exponentially growing code groups.
/// </summary>
private void WriteSubexponential(int value, int valueCount, int groupBitCount)
{
int groupIndex = 0;
int groupStart = 0;
while (true)
{
// The first two groups retain the initial width. Later groups grow one bit at a time until the
// finite tail is small enough for the exact non-symmetric alphabet.
int bitCount = groupIndex == 0 ? groupBitCount : groupBitCount + groupIndex - 1;
int groupSize = 1 << bitCount;
if (valueCount <= groupStart + (3 * groupSize))
{
this.WriteNonSymmetric((uint)(value - groupStart), (uint)(valueCount - groupStart));
return;
}
bool useLaterGroup = value >= groupStart + groupSize;
this.WriteBoolean(useLaterGroup);
if (!useLaterGroup)
{
this.WriteLiteral((uint)(value - groupStart), bitCount);
return;
}
groupIndex++;
groupStart += groupSize;
}
}
/// <summary>
/// Maps an unsigned value to increasing distance from a reference inside a finite domain.
/// </summary>
private static int RecenterFiniteNonNegative(int valueCount, int reference, int value)
{
if ((reference << 1) <= valueCount)
{
return RecenterNonNegative(reference, value);
}
return RecenterNonNegative(valueCount - 1 - reference, valueCount - 1 - value);
}
/// <summary>
/// Maps an unsigned value to alternating positions around a nonnegative reference.
/// </summary>
private static int RecenterNonNegative(int reference, int value)
{
if (value > (reference << 1))
{
return value;
}
return value >= reference
? (value - reference) << 1
: ((reference - value) << 1) - 1;
}
/// <summary>
/// Appends one bit to the partially assembled output byte.
/// </summary>
/// <param name="value">Zero or one.</param>
private void WriteBit(byte value)
{
int bit = this.BitPosition & 0x07;
this.buffer = (byte)(((value << (7 - bit)) & 0xff) | this.buffer);
if (bit == 7)
{
this.WriteBuffer();
}
this.BitPosition++;
}
/// <summary>
/// Writes an unsigned integer with its least-significant byte first.
/// </summary>
/// <param name="value">The value to write.</param>
/// <param name="n">The number of bytes to write.</param>
public void WriteLittleEndian(uint value, int n)
{
DebugGuard.IsTrue(Av1Math.Modulus8(this.BitPosition) == 0, "Writing of Little Endian value only allowed on byte alignment");
uint t = value;
for (int i = 0; i < n; i++)
{
this.WriteLiteral(t & 0xff, 8);
t >>= 8;
}
}
/// <summary>
/// Writes a byte-aligned entropy-coded tile payload.
/// </summary>
/// <param name="tileData">The tile payload.</param>
public void WriteBlob(ReadOnlySpan<byte> tileData)
{
DebugGuard.IsTrue(Av1Math.Modulus8(this.BitPosition) == 0, "Writing of Tile Data only allowed on byte alignment");
int wordPosition = this.BitPosition >> 3;
tileData.CopyTo(this.span[wordPosition..]);
this.BitPosition += tileData.Length << 3;
}
/// <summary>
/// Stores the current output byte.
/// </summary>
private void WriteBuffer()
{
int wordPosition = Av1Math.DivideBy8Floor(this.BitPosition);
this.span[wordPosition] = this.buffer;
this.buffer = 0;
}
}

140
src/ImageSharp/Formats/Heif/Av1/Av1BlockSize.cs

@ -0,0 +1,140 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Identifies every luma block size defined by AV1.
/// </summary>
internal enum Av1BlockSize : byte
{
/// <summary>
/// A block of samples, 4 samples wide and 4 samples high.
/// </summary>
Block4x4 = 0,
/// <summary>
/// A block of samples, 4 samples wide and 8 samples high.
/// </summary>
Block4x8 = 1,
/// <summary>
/// A block of samples, 8 samples wide and 4 samples high.
/// </summary>
Block8x4 = 2,
/// <summary>
/// A block of samples, 8 samples wide and 8 samples high.
/// </summary>
Block8x8 = 3,
/// <summary>
/// A block of samples, 8 samples wide and 16 samples high.
/// </summary>
Block8x16 = 4,
/// <summary>
/// A block of samples, 16 samples wide and 8 samples high.
/// </summary>
Block16x8 = 5,
/// <summary>
/// A block of samples, 16 samples wide and 16 samples high.
/// </summary>
Block16x16 = 6,
/// <summary>
/// A block of samples, 16 samples wide and 32 samples high.
/// </summary>
Block16x32 = 7,
/// <summary>
/// A block of samples, 32 samples wide and 16 samples high.
/// </summary>
Block32x16 = 8,
/// <summary>
/// A block of samples, 32 samples wide and 32 samples high.
/// </summary>
Block32x32 = 9,
/// <summary>
/// A block of samples, 32 samples wide and 64 samples high.
/// </summary>
Block32x64 = 10,
/// <summary>
/// A block of samples, 64 samples wide and 32 samples high.
/// </summary>
Block64x32 = 11,
/// <summary>
/// A block of samples, 64 samples wide and 64 samples high.
/// </summary>
Block64x64 = 12,
/// <summary>
/// A block of samples, 64 samples wide and 128 samples high.
/// </summary>
Block64x128 = 13,
/// <summary>
/// A block of samples, 128 samples wide and 64 samples high.
/// </summary>
Block128x64 = 14,
/// <summary>
/// A block of samples, 128 samples wide and 128 samples high.
/// </summary>
Block128x128 = 15,
/// <summary>
/// A block of samples, 4 samples wide and 16 samples high.
/// </summary>
Block4x16 = 16,
/// <summary>
/// A block of samples, 16 samples wide and 4 samples high.
/// </summary>
Block16x4 = 17,
/// <summary>
/// A block of samples, 8 samples wide and 32 samples high.
/// </summary>
Block8x32 = 18,
/// <summary>
/// A block of samples, 32 samples wide and 8 samples high.
/// </summary>
Block32x8 = 19,
/// <summary>
/// A block of samples, 16 samples wide and 64 samples high.
/// </summary>
Block16x64 = 20,
/// <summary>
/// A block of samples, 64 samples wide and 16 samples high.
/// </summary>
Block64x16 = 21,
/// <summary>
/// The number of concrete block-size values.
/// </summary>
AllSizes = 22,
/// <summary>
/// The first extended rectangular block size following the primary size set.
/// </summary>
SizeS = Block4x16,
/// <summary>
/// A sentinel representing an invalid block size.
/// </summary>
Invalid = 255,
/// <summary>
/// The final value in the primary block-size set.
/// </summary>
Largest = SizeS - 1,
}

245
src/ImageSharp/Formats/Heif/Av1/Av1BlockSizeExtensions.cs

@ -0,0 +1,245 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Provides dimensions, chroma subsampling, and transform limits for AV1 block sizes.
/// </summary>
internal static class Av1BlockSizeExtensions
{
/// <summary>
/// The width of each block size in units of four samples.
/// </summary>
private static readonly int[] SizeWide = [1, 1, 2, 2, 2, 4, 4, 4, 8, 8, 8, 16, 16, 16, 32, 32, 1, 4, 2, 8, 4, 16];
/// <summary>
/// The height of each block size in units of four samples.
/// </summary>
private static readonly int[] SizeHigh = [1, 2, 1, 2, 4, 2, 4, 8, 4, 8, 16, 8, 16, 32, 16, 32, 4, 1, 8, 2, 16, 4];
/// <summary>
/// Maps each luma block size and pair of chroma subsampling shifts to its residual-plane block size.
/// </summary>
private static readonly Av1BlockSize[][][] SubSampled =
[
// ss_x == 0 ss_x == 0 ss_x == 1 ss_x == 1
// ss_y == 0 ss_y == 1 ss_y == 0 ss_y == 1
[[Av1BlockSize.Block4x4, Av1BlockSize.Block4x4], [Av1BlockSize.Block4x4, Av1BlockSize.Block4x4]],
[[Av1BlockSize.Block4x8, Av1BlockSize.Block4x4], [Av1BlockSize.Invalid, Av1BlockSize.Block4x4]],
[[Av1BlockSize.Block8x4, Av1BlockSize.Invalid], [Av1BlockSize.Block4x4, Av1BlockSize.Block4x4]],
[[Av1BlockSize.Block8x8, Av1BlockSize.Block8x4], [Av1BlockSize.Block4x8, Av1BlockSize.Block4x4]],
[[Av1BlockSize.Block8x16, Av1BlockSize.Block8x8], [Av1BlockSize.Invalid, Av1BlockSize.Block4x8]],
[[Av1BlockSize.Block16x8, Av1BlockSize.Invalid], [Av1BlockSize.Block8x8, Av1BlockSize.Block8x4]],
[[Av1BlockSize.Block16x16, Av1BlockSize.Block16x8], [Av1BlockSize.Block8x16, Av1BlockSize.Block8x8]],
[[Av1BlockSize.Block16x32, Av1BlockSize.Block16x16], [Av1BlockSize.Invalid, Av1BlockSize.Block8x16]],
[[Av1BlockSize.Block32x16, Av1BlockSize.Invalid], [Av1BlockSize.Block16x16, Av1BlockSize.Block16x8]],
[[Av1BlockSize.Block32x32, Av1BlockSize.Block32x16], [Av1BlockSize.Block16x32, Av1BlockSize.Block16x16]],
[[Av1BlockSize.Block32x64, Av1BlockSize.Block32x32], [Av1BlockSize.Invalid, Av1BlockSize.Block16x32]],
[[Av1BlockSize.Block64x32, Av1BlockSize.Invalid], [Av1BlockSize.Block32x32, Av1BlockSize.Block32x16]],
[[Av1BlockSize.Block64x64, Av1BlockSize.Block64x32], [Av1BlockSize.Block32x64, Av1BlockSize.Block32x32]],
[[Av1BlockSize.Block64x128, Av1BlockSize.Block64x64], [Av1BlockSize.Invalid, Av1BlockSize.Block32x64]],
[[Av1BlockSize.Block128x64, Av1BlockSize.Invalid], [Av1BlockSize.Block64x64, Av1BlockSize.Block64x32]],
[[Av1BlockSize.Block128x128, Av1BlockSize.Block128x64], [Av1BlockSize.Block64x128, Av1BlockSize.Block64x64]],
[[Av1BlockSize.Block4x16, Av1BlockSize.Block4x8], [Av1BlockSize.Invalid, Av1BlockSize.Block4x8]],
[[Av1BlockSize.Block16x4, Av1BlockSize.Invalid], [Av1BlockSize.Block8x4, Av1BlockSize.Block8x4]],
[[Av1BlockSize.Block8x32, Av1BlockSize.Block8x16], [Av1BlockSize.Invalid, Av1BlockSize.Block4x16]],
[[Av1BlockSize.Block32x8, Av1BlockSize.Invalid], [Av1BlockSize.Block16x8, Av1BlockSize.Block16x4]],
[[Av1BlockSize.Block16x64, Av1BlockSize.Block16x32], [Av1BlockSize.Invalid, Av1BlockSize.Block8x32]],
[[Av1BlockSize.Block64x16, Av1BlockSize.Invalid], [Av1BlockSize.Block32x16, Av1BlockSize.Block32x8]]
];
/// <summary>
/// Maps each block size to its largest permitted transform size.
/// </summary>
private static readonly Av1TransformSize[] MaxTransformSize = [
Av1TransformSize.Size4x4, Av1TransformSize.Size4x8, Av1TransformSize.Size8x4, Av1TransformSize.Size8x8,
Av1TransformSize.Size8x16, Av1TransformSize.Size16x8, Av1TransformSize.Size16x16, Av1TransformSize.Size16x32,
Av1TransformSize.Size32x16, Av1TransformSize.Size32x32, Av1TransformSize.Size32x64, Av1TransformSize.Size64x32,
Av1TransformSize.Size64x64, Av1TransformSize.Size64x64, Av1TransformSize.Size64x64, Av1TransformSize.Size64x64,
Av1TransformSize.Size4x16, Av1TransformSize.Size16x4, Av1TransformSize.Size8x32, Av1TransformSize.Size32x8,
Av1TransformSize.Size16x64, Av1TransformSize.Size64x16
];
/// <summary>
/// Contains the base-two logarithm of the sample count for each block size.
/// </summary>
private static readonly int[] PelsLog2Count =
[4, 5, 5, 6, 7, 7, 8, 9, 9, 10, 11, 11, 12, 13, 13, 14, 6, 6, 8, 8, 10, 10];
/// <summary>
/// Maps geometry dimension logarithms to an AV1 block size using the mode-decision scan's transposed axis convention.
/// </summary>
private static readonly Av1BlockSize[][] HeightWidthToSize = [
[Av1BlockSize.Block4x4, Av1BlockSize.Block4x8, Av1BlockSize.Block4x16, Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid],
[Av1BlockSize.Block8x4, Av1BlockSize.Block8x8, Av1BlockSize.Block8x16, Av1BlockSize.Block8x32, Av1BlockSize.Invalid, Av1BlockSize.Invalid],
[Av1BlockSize.Block16x4, Av1BlockSize.Block16x8, Av1BlockSize.Block16x16, Av1BlockSize.Block16x32, Av1BlockSize.Block16x64, Av1BlockSize.Invalid],
[Av1BlockSize.Invalid, Av1BlockSize.Block32x8, Av1BlockSize.Block32x16, Av1BlockSize.Block32x32, Av1BlockSize.Block32x64, Av1BlockSize.Invalid],
[Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x16, Av1BlockSize.Block64x32, Av1BlockSize.Block64x64, Av1BlockSize.Block64x128],
[Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block128x64, Av1BlockSize.Block128x128]
];
/// <summary>
/// Gets the block width in units of four samples.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The number of four-sample columns.</returns>
public static int Get4x4WideCount(this Av1BlockSize blockSize) => SizeWide[(int)blockSize];
/// <summary>
/// Gets the block height in units of four samples.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The number of four-sample rows.</returns>
public static int Get4x4HighCount(this Av1BlockSize blockSize) => SizeHigh[(int)blockSize];
/// <summary>
/// Gets the block size from mode-decision geometry dimension logarithms, where zero represents four samples.
/// </summary>
/// <param name="widthLog2">The base-two width logarithm minus two.</param>
/// <param name="heightLog2">The base-two height logarithm minus two.</param>
/// <returns>The matching block size, or <see cref="Av1BlockSize.Invalid"/> for unsupported dimensions.</returns>
public static Av1BlockSize FromWidthAndHeight(uint widthLog2, uint heightLog2)
{
// Mode-decision geometry is ported with its source axis order, so its size lookup is indexed height first.
return HeightWidthToSize[heightLog2][widthLog2];
}
/// <summary>
/// Gets the block width in samples.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The block width in samples.</returns>
public static int GetWidth(this Av1BlockSize blockSize)
=> Get4x4WideCount(blockSize) << 2;
/// <summary>
/// Gets the block height in samples.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The block height in samples.</returns>
public static int GetHeight(this Av1BlockSize blockSize)
=> Get4x4HighCount(blockSize) << 2;
/// <summary>
/// Gets the base-two logarithm of the block width in units of four samples.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The base-two logarithm of the four-sample column count.</returns>
public static int Get4x4WidthLog2(this Av1BlockSize blockSize)
=> Av1Math.Log2(Get4x4WideCount(blockSize));
/// <summary>
/// Gets the base-two logarithm of the block height in units of four samples.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The base-two logarithm of the four-sample row count.</returns>
public static int Get4x4HeightLog2(this Av1BlockSize blockSize)
=> Av1Math.Log2(Get4x4HighCount(blockSize));
/// <summary>
/// Gets the entropy context group associated with the block size.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The zero-based size group in the inclusive range zero through three.</returns>
public static int GetSizeGroup(this Av1BlockSize blockSize)
{
// AV1 section 9.3 groups a block by its smaller dimension in 4x4 units and caps that logarithm at three.
// Deriving the value from the existing geometry tables exactly matches the reference decoder's size_group_lookup table.
return Math.Min(3, Math.Min(blockSize.Get4x4WidthLog2(), blockSize.Get4x4HeightLog2()));
}
/// <summary>
/// Gets the residual-plane block size for Boolean chroma subsampling flags.
/// </summary>
/// <param name="blockSize">The luma block size.</param>
/// <param name="subX">Indicates horizontal chroma subsampling.</param>
/// <param name="subY">Indicates vertical chroma subsampling.</param>
/// <returns>The corresponding residual-plane block size.</returns>
public static Av1BlockSize GetSubsampled(this Av1BlockSize blockSize, bool subX, bool subY)
=> GetSubsampled(blockSize, subX ? 1 : 0, subY ? 1 : 0);
/// <summary>
/// Gets the residual-plane block size for chroma subsampling shifts.
/// </summary>
/// <param name="blockSize">The luma block size.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <returns>The corresponding residual-plane block size, or <see cref="Av1BlockSize.Invalid"/> when unavailable.</returns>
public static Av1BlockSize GetSubsampled(this Av1BlockSize blockSize, int subX, int subY)
{
if (blockSize == Av1BlockSize.Invalid)
{
return Av1BlockSize.Invalid;
}
return SubSampled[(int)blockSize][subX][subY];
}
/// <summary>
/// Determines whether a luma block permits chroma-from-luma prediction.
/// </summary>
/// <param name="blockSize">The luma block size.</param>
/// <param name="isLossless">Indicates whether the block belongs to a lossless segment.</param>
/// <param name="subX">Indicates horizontal chroma subsampling.</param>
/// <param name="subY">Indicates vertical chroma subsampling.</param>
/// <returns><see langword="true"/> when chroma-from-luma prediction is permitted; otherwise, <see langword="false"/>.</returns>
public static bool AllowsChromaFromLuma(
this Av1BlockSize blockSize,
bool isLossless,
bool subX,
bool subY)
{
if (isLossless)
{
// Lossless coding fixes the transform to 4x4, so the subsampled chroma block must have the same dimensions.
return blockSize.GetSubsampled(subX, subY) == Av1BlockSize.Block4x4;
}
return blockSize.GetWidth() <= 32 && blockSize.GetHeight() <= 32;
}
/// <summary>
/// Gets the maximum chroma transform size after applying plane subsampling and AV1 chroma transform limits.
/// </summary>
/// <param name="blockSize">The luma block size.</param>
/// <param name="subX">Indicates horizontal chroma subsampling.</param>
/// <param name="subY">Indicates vertical chroma subsampling.</param>
/// <returns>The maximum chroma transform size, or <see cref="Av1TransformSize.Invalid"/> when the plane block size is invalid.</returns>
public static Av1TransformSize GetMaxUvTransformSize(this Av1BlockSize blockSize, bool subX, bool subY)
{
Av1BlockSize planeBlockSize = blockSize.GetSubsampled(subX, subY);
Av1TransformSize uvTransformSize = Av1TransformSize.Invalid;
if (planeBlockSize < Av1BlockSize.AllSizes)
{
uvTransformSize = planeBlockSize.GetMaximumTransformSize();
}
return uvTransformSize switch
{
Av1TransformSize.Size64x64 or Av1TransformSize.Size64x32 or Av1TransformSize.Size32x64 => Av1TransformSize.Size32x32,
Av1TransformSize.Size64x16 => Av1TransformSize.Size32x16,
Av1TransformSize.Size16x64 => Av1TransformSize.Size16x32,
_ => uvTransformSize,
};
}
/// <summary>
/// Gets the largest square or rectangular transform size permitted for a block.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The maximum transform size.</returns>
public static Av1TransformSize GetMaximumTransformSize(this Av1BlockSize blockSize)
=> MaxTransformSize[(int)blockSize];
/// <summary>
/// Gets the base-two logarithm of the block's sample count.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The base-two logarithm of width multiplied by height.</returns>
public static int GetPelsLog2Count(this Av1BlockSize blockSize)
=> PelsLog2Count[(int)blockSize];
}

829
src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs

@ -0,0 +1,829 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Contains the image-description fields stored in an AV1 codec-configuration item property.
/// </summary>
internal sealed class Av1CodecConfiguration
{
/// <summary>
/// The number of bytes in the fixed AV1 codec-configuration record.
/// </summary>
public const int FixedHeaderSize = 4;
/// <summary>
/// The optional sequence-header payload retained from the configuration open bitstream units.
/// </summary>
private readonly byte[] configSequenceHeader;
/// <summary>
/// The sequence-header OBU extension byte, or <c>-1</c> when its header has no extension.
/// </summary>
private readonly int configSequenceHeaderExtension;
/// <summary>
/// The content light-level metadata carried by the configuration OBUs, or <see langword="null"/> when absent.
/// </summary>
private readonly HeifContentLightLevel? configContentLightLevel;
/// <summary>
/// The mastering-display color volume carried by the configuration OBUs, or <see langword="null"/> when absent.
/// </summary>
private readonly HeifMasteringDisplayColorVolume? configMasteringDisplayColorVolume;
/// <summary>
/// Initializes a new instance of the <see cref="Av1CodecConfiguration"/> class from an AV1 codec-configuration
/// item-property payload.
/// </summary>
/// <param name="boxBuffer">The configuration payload beginning with the marker and version fields.</param>
/// <param name="options">The general options governing metadata validation.</param>
public Av1CodecConfiguration(Span<byte> boxBuffer, DecoderOptions options)
{
if (boxBuffer.Length < FixedHeaderSize)
{
throw new InvalidImageContentException("The AV1 codec configuration is truncated.");
}
Av1BitStreamReader reader = new(boxBuffer);
uint marker = reader.ReadLiteral(1);
uint version = reader.ReadLiteral(7);
if (marker != 1 || version != 1)
{
throw new InvalidImageContentException("The AV1 codec configuration has an invalid marker or version.");
}
this.SequenceProfile = (byte)reader.ReadLiteral(3);
this.SequenceLevelIndex = (byte)reader.ReadLiteral(5);
this.SequenceTier = reader.ReadLiteral(1) == 1;
this.HighBitDepth = reader.ReadLiteral(1) == 1;
this.TwelveBit = reader.ReadLiteral(1) == 1;
this.IsMonochrome = reader.ReadLiteral(1) == 1;
this.ChromaSubsamplingX = reader.ReadLiteral(1) == 1;
this.ChromaSubsamplingY = reader.ReadLiteral(1) == 1;
this.ChromaSamplePosition = (byte)reader.ReadLiteral(2);
if (this.SequenceProfile > (byte)ObuSequenceProfile.Professional
|| (this.TwelveBit && !this.HighBitDepth)
|| this.ChromaSamplePosition == (byte)ObuChromoSamplePosition.Reserved)
{
throw new InvalidImageContentException("The AV1 codec configuration contains invalid image-description fields.");
}
if (reader.ReadLiteral(3) != 0)
{
throw new InvalidImageContentException("The AV1 codec configuration has nonzero reserved bits.");
}
bool hasInitialPresentationDelay = reader.ReadLiteral(1) == 1;
uint delayOrReserved = reader.ReadLiteral(4);
if (!hasInitialPresentationDelay && delayOrReserved != 0)
{
throw new InvalidImageContentException("The AV1 codec configuration has a nonzero reserved delay field.");
}
// The delay syntax is consumed to validate the fixed record, but it describes sample presentation and has
// no meaning for the independently presented image item supported by this bounded container implementation.
ReadOnlySpan<byte> configObus = boxBuffer[FixedHeaderSize..];
int sequenceHeaderCount = ScanObus(
configObus,
true,
true,
"AV1 codec configuration",
options,
out int configSequenceHeaderOffset,
out int configSequenceHeaderLength,
out this.configSequenceHeaderExtension,
out this.configContentLightLevel,
out this.configMasteringDisplayColorVolume);
if (sequenceHeaderCount > 1)
{
throw new InvalidImageContentException("The AV1 codec configuration contains more than one sequence header OBU.");
}
if (configSequenceHeaderOffset < 0)
{
this.configSequenceHeader = [];
}
else
{
// The property-reader span is pooled and reused. Retain only the sequence-header bytes required for
// item/sample equivalence instead of materializing every optional configuration OBU.
this.configSequenceHeader = new byte[configSequenceHeaderLength];
configObus.Slice(configSequenceHeaderOffset, configSequenceHeaderLength).CopyTo(this.configSequenceHeader);
}
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1CodecConfiguration"/> class from an encoded sequence header.
/// </summary>
/// <param name="sequenceHeader">The sequence header describing the encoded image item.</param>
public Av1CodecConfiguration(ObuSequenceHeader sequenceHeader)
{
ObuOperatingPoint operatingPoint = sequenceHeader.OperatingPoint[0];
ObuColorConfig colorConfig = sequenceHeader.ColorConfig;
this.SequenceProfile = (byte)sequenceHeader.SequenceProfile;
this.SequenceLevelIndex = (byte)operatingPoint.SequenceLevelIndex;
this.SequenceTier = operatingPoint.SequenceTier != 0;
this.HighBitDepth = colorConfig.BitDepth is Av1BitDepth.TenBit or Av1BitDepth.TwelveBit;
this.TwelveBit = colorConfig.BitDepth == Av1BitDepth.TwelveBit;
this.IsMonochrome = colorConfig.IsMonochrome;
this.ChromaSubsamplingX = colorConfig.SubSamplingX;
this.ChromaSubsamplingY = colorConfig.SubSamplingY;
this.ChromaSamplePosition = (byte)colorConfig.ChromaSamplePosition;
this.configSequenceHeader = [];
this.configSequenceHeaderExtension = -1;
this.configContentLightLevel = null;
this.configMasteringDisplayColorVolume = null;
}
/// <summary>
/// Gets the sequence profile declared for the coded image.
/// </summary>
public byte SequenceProfile { get; }
/// <summary>
/// Gets the first operating point's sequence-level index.
/// </summary>
public byte SequenceLevelIndex { get; }
/// <summary>
/// Gets a value indicating whether the first operating point uses the high tier.
/// </summary>
public bool SequenceTier { get; }
/// <summary>
/// Gets a value indicating whether the coded image uses more than eight bits per sample.
/// </summary>
public bool HighBitDepth { get; }
/// <summary>
/// Gets a value indicating whether the coded image uses twelve bits per sample.
/// </summary>
public bool TwelveBit { get; }
/// <summary>
/// Gets the coded image sample precision in bits.
/// </summary>
public HeifBitDepth BitDepth => this.TwelveBit ? HeifBitDepth.Bit12 : this.HighBitDepth ? HeifBitDepth.Bit10 : HeifBitDepth.Bit8;
/// <summary>
/// Gets a value indicating whether the coded image contains only a luma plane.
/// </summary>
public bool IsMonochrome { get; }
/// <summary>
/// Gets a value indicating whether the coded image's chroma planes are horizontally subsampled.
/// </summary>
public bool ChromaSubsamplingX { get; }
/// <summary>
/// Gets a value indicating whether the coded image's chroma planes are vertically subsampled.
/// </summary>
public bool ChromaSubsamplingY { get; }
/// <summary>
/// Gets the position of vertically subsampled chroma samples relative to luma samples.
/// </summary>
public byte ChromaSamplePosition { get; }
/// <summary>
/// Writes the fixed AV1 codec-configuration record without optional configuration OBUs.
/// </summary>
/// <param name="destination">The destination receiving the four-byte record.</param>
public void WriteFixedHeader(Span<byte> destination)
{
// The image item payload already begins with its required sequence header. Keeping configOBUs empty avoids
// retaining or copying the same OBU into the configuration property.
destination[0] = 0x81;
destination[1] = (byte)((this.SequenceProfile << 5) | this.SequenceLevelIndex);
destination[2] = (byte)(
(this.SequenceTier ? 1 << 7 : 0)
| (this.HighBitDepth ? 1 << 6 : 0)
| (this.TwelveBit ? 1 << 5 : 0)
| (this.IsMonochrome ? 1 << 4 : 0)
| (this.ChromaSubsamplingX ? 1 << 3 : 0)
| (this.ChromaSubsamplingY ? 1 << 2 : 0)
| this.ChromaSamplePosition);
destination[3] = 0;
}
/// <summary>
/// Validates the AV1 image item OBU layout and metadata against its item properties and configuration record.
/// </summary>
/// <param name="itemData">The complete AV1 image item payload.</param>
/// <param name="itemContentLightLevel">
/// The content light-level property associated with the image item, or <see langword="null"/> when absent.
/// </param>
/// <param name="itemMasteringDisplayColorVolume">
/// The mastering-display property associated with the image item, or <see langword="null"/> when absent.
/// </param>
/// <param name="options">The general options governing metadata validation.</param>
/// <param name="contentLightLevel">
/// Receives the content light-level metadata carried by the combined configuration and item OBUs.
/// </param>
/// <param name="masteringDisplayColorVolume">
/// Receives the mastering-display metadata carried by the combined configuration and item OBUs.
/// </param>
public void ValidateItemData(
ReadOnlySpan<byte> itemData,
HeifContentLightLevel? itemContentLightLevel,
HeifMasteringDisplayColorVolume? itemMasteringDisplayColorVolume,
DecoderOptions options,
out HeifContentLightLevel? contentLightLevel,
out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume)
=> this.ValidateData(
itemData,
true,
"AV1 image item",
itemContentLightLevel,
itemMasteringDisplayColorVolume,
options,
out contentLightLevel,
out masteringDisplayColorVolume);
/// <summary>
/// Validates one AV1 track sample against its sync-sample declaration, sample-entry metadata, and configuration record.
/// </summary>
/// <param name="sampleData">The complete AV1 sample payload.</param>
/// <param name="isSyncSample">Indicates that the sample is declared as a random-access point.</param>
/// <param name="sampleContentLightLevel">
/// The content light-level property associated with the sample entry, or <see langword="null"/> when absent.
/// </param>
/// <param name="sampleMasteringDisplayColorVolume">
/// The mastering-display property associated with the sample entry, or <see langword="null"/> when absent.
/// </param>
/// <param name="options">The general options governing metadata validation.</param>
/// <param name="contentLightLevel">
/// Receives the content light-level metadata carried by the combined configuration and sample OBUs.
/// </param>
/// <param name="masteringDisplayColorVolume">
/// Receives the mastering-display metadata carried by the combined configuration and sample OBUs.
/// </param>
public void ValidateSampleData(
ReadOnlySpan<byte> sampleData,
bool isSyncSample,
HeifContentLightLevel? sampleContentLightLevel,
HeifMasteringDisplayColorVolume? sampleMasteringDisplayColorVolume,
DecoderOptions options,
out HeifContentLightLevel? contentLightLevel,
out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume)
=> this.ValidateData(
sampleData,
isSyncSample,
"AV1 track sample",
sampleContentLightLevel,
sampleMasteringDisplayColorVolume,
options,
out contentLightLevel,
out masteringDisplayColorVolume);
/// <summary>
/// Validates one bounded AV1 payload while applying the item or track sequence-header requirement.
/// </summary>
/// <param name="data">The complete bounded AV1 payload.</param>
/// <param name="sequenceHeaderRequired">Indicates that exactly one sequence header is required.</param>
/// <param name="sourceName">The source description used by invalid-content errors.</param>
/// <param name="containerContentLightLevel">The content light-level property associated with the payload.</param>
/// <param name="containerMasteringDisplayColorVolume">The mastering-display property associated with the payload.</param>
/// <param name="options">The general options governing metadata validation.</param>
/// <param name="contentLightLevel">Receives validated OBU content light-level metadata.</param>
/// <param name="masteringDisplayColorVolume">Receives validated OBU mastering-display metadata.</param>
private void ValidateData(
ReadOnlySpan<byte> data,
bool sequenceHeaderRequired,
string sourceName,
HeifContentLightLevel? containerContentLightLevel,
HeifMasteringDisplayColorVolume? containerMasteringDisplayColorVolume,
DecoderOptions options,
out HeifContentLightLevel? contentLightLevel,
out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume)
{
int sequenceHeaderCount = ScanObus(
data,
false,
false,
sourceName,
options,
out int dataSequenceHeaderOffset,
out int dataSequenceHeaderLength,
out int dataSequenceHeaderExtension,
out HeifContentLightLevel? dataObuContentLightLevel,
out HeifMasteringDisplayColorVolume? dataObuMasteringDisplayColorVolume);
if (sequenceHeaderCount > 1 || (sequenceHeaderRequired && sequenceHeaderCount != 1))
{
string requirement = sequenceHeaderRequired ? "exactly one" : "at most one";
throw new InvalidImageContentException($"The {sourceName} contains {sequenceHeaderCount} sequence header OBUs instead of {requirement}.");
}
if (this.configSequenceHeader.Length != 0 && dataSequenceHeaderOffset >= 0)
{
ReadOnlySpan<byte> dataSequenceHeader = data.Slice(
dataSequenceHeaderOffset,
dataSequenceHeaderLength);
// Compare the extension and payload rather than the encoded OBU size. Configuration OBUs must carry a
// size field while a payload's final OBU may omit one, and different legal LEB128 widths do not alter
// the Sequence Header OBU being repeated.
if (this.configSequenceHeaderExtension != dataSequenceHeaderExtension
|| !this.configSequenceHeader.AsSpan().SequenceEqual(dataSequenceHeader))
{
throw new InvalidImageContentException(
$"The AV1 codec configuration sequence header does not match the {sourceName} sequence header.");
}
}
contentLightLevel = null;
masteringDisplayColorVolume = null;
if (options.SkipMetadata)
{
return;
}
try
{
ValidateContentLightLevel(this.configContentLightLevel, containerContentLightLevel, "AV1 codec configuration");
ValidateContentLightLevel(dataObuContentLightLevel, containerContentLightLevel, sourceName);
ValidateMasteringDisplayColorVolume(
this.configMasteringDisplayColorVolume,
containerMasteringDisplayColorVolume,
"AV1 codec configuration");
ValidateMasteringDisplayColorVolume(
dataObuMasteringDisplayColorVolume,
containerMasteringDisplayColorVolume,
sourceName);
if (this.configContentLightLevel is not null
&& dataObuContentLightLevel is not null
&& !ContentLightLevelsMatch(this.configContentLightLevel.Value, dataObuContentLightLevel.Value))
{
throw new InvalidImageContentException(
$"The AV1 codec configuration and {sourceName} contain conflicting content light-level metadata.");
}
if (this.configMasteringDisplayColorVolume is not null
&& dataObuMasteringDisplayColorVolume is not null
&& this.configMasteringDisplayColorVolume.Value != dataObuMasteringDisplayColorVolume.Value)
{
throw new InvalidImageContentException(
$"The AV1 codec configuration and {sourceName} contain conflicting mastering-display metadata.");
}
// Configuration OBUs precede the payload OBUs, so a payload OBU supplies the effective value when both
// sequences repeat the same metadata type.
contentLightLevel = dataObuContentLightLevel ?? this.configContentLightLevel;
masteringDisplayColorVolume = dataObuMasteringDisplayColorVolume ?? this.configMasteringDisplayColorVolume;
}
catch (Exception ex) when (HeifDecoderCore.ShouldIgnoreAncillarySegmentError(options, ex))
{
// Conflicting optional OBU metadata is discarded without weakening OBU framing or sequence-header checks.
}
}
/// <summary>
/// Determines whether another item configuration describes the same coded-image sample layout.
/// </summary>
/// <param name="other">The configuration to compare.</param>
/// <returns><see langword="true"/> when every fixed image-description field is equal.</returns>
public bool HasMatchingImageConfiguration(Av1CodecConfiguration other)
=> this.SequenceProfile == other.SequenceProfile
&& this.SequenceLevelIndex == other.SequenceLevelIndex
&& this.SequenceTier == other.SequenceTier
&& this.HighBitDepth == other.HighBitDepth
&& this.TwelveBit == other.TwelveBit
&& this.IsMonochrome == other.IsMonochrome
&& this.ChromaSubsamplingX == other.ChromaSubsamplingX
&& this.ChromaSubsamplingY == other.ChromaSubsamplingY
&& this.ChromaSamplePosition == other.ChromaSamplePosition;
/// <summary>
/// Validates the configuration fields against the sequence header that describes the coded image item.
/// </summary>
/// <param name="sequenceHeader">The decoded AV1 sequence header.</param>
public void Validate(ObuSequenceHeader sequenceHeader)
{
ObuOperatingPoint operatingPoint = sequenceHeader.OperatingPoint[0];
ObuColorConfig colorConfig = sequenceHeader.ColorConfig;
bool highBitDepth = colorConfig.BitDepth is Av1BitDepth.TenBit or Av1BitDepth.TwelveBit;
bool twelveBit = colorConfig.BitDepth == Av1BitDepth.TwelveBit;
// Chroma sample position is signaled only for 4:2:0. Other layouts have no corresponding
// sequence-header field, so their container value cannot be compared with the parser default.
bool hasChromaSamplePosition = !colorConfig.IsMonochrome && colorConfig.SubSamplingX && colorConfig.SubSamplingY;
if (this.SequenceProfile != (byte)sequenceHeader.SequenceProfile
|| this.SequenceLevelIndex != operatingPoint.SequenceLevelIndex
|| this.SequenceTier != (operatingPoint.SequenceTier != 0)
|| this.HighBitDepth != highBitDepth
|| this.TwelveBit != twelveBit
|| this.IsMonochrome != colorConfig.IsMonochrome
|| this.ChromaSubsamplingX != colorConfig.SubSamplingX
|| this.ChromaSubsamplingY != colorConfig.SubSamplingY
|| (hasChromaSamplePosition && this.ChromaSamplePosition != (byte)colorConfig.ChromaSamplePosition))
{
throw new InvalidImageContentException("The AV1 item configuration does not match its sequence header.");
}
}
/// <summary>
/// Scans a low-overhead AV1 OBU sequence and locates its still-image description metadata.
/// </summary>
/// <param name="data">The complete bounded OBU sequence.</param>
/// <param name="requireSizeFields">Indicates that every OBU must carry its registered payload-size field.</param>
/// <param name="sequenceHeaderMustBeFirst">
/// Indicates that a sequence-header OBU, when present, must be the first OBU in the sequence.
/// </param>
/// <param name="sourceName">The source description used by invalid-content errors.</param>
/// <param name="options">The general options governing metadata validation.</param>
/// <param name="sequenceHeaderOffset">Receives the first sequence-header payload offset, or <c>-1</c>.</param>
/// <param name="sequenceHeaderLength">Receives the first sequence-header payload length.</param>
/// <param name="sequenceHeaderExtension">Receives the first sequence-header extension byte, or <c>-1</c>.</param>
/// <param name="contentLightLevel">
/// Receives the content light-level metadata carried by the sequence, or <see langword="null"/> when absent.
/// </param>
/// <param name="masteringDisplayColorVolume">
/// Receives the mastering-display metadata carried by the sequence, or <see langword="null"/> when absent.
/// </param>
/// <returns>The number of sequence-header OBUs in the sequence.</returns>
private static int ScanObus(
ReadOnlySpan<byte> data,
bool requireSizeFields,
bool sequenceHeaderMustBeFirst,
string sourceName,
DecoderOptions options,
out int sequenceHeaderOffset,
out int sequenceHeaderLength,
out int sequenceHeaderExtension,
out HeifContentLightLevel? contentLightLevel,
out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume)
{
sequenceHeaderOffset = -1;
sequenceHeaderLength = 0;
sequenceHeaderExtension = -1;
contentLightLevel = null;
masteringDisplayColorVolume = null;
int sequenceHeaderCount = 0;
int obuIndex = 0;
int offset = 0;
while (offset < data.Length)
{
byte header = data[offset++];
if ((header & 0x80) != 0)
{
throw new InvalidImageContentException($"The {sourceName} contains an OBU with a set forbidden header bit.");
}
// The reference decoder deliberately ignores obu_reserved_1bit. The bit does not alter the OBU boundary or
// decoded syntax, so the bounded container scan must not reject data that the production parser accepts.
ObuType type = (ObuType)((header >> 3) & 0x0F);
bool hasExtension = (header & 0x04) != 0;
bool hasSizeField = (header & 0x02) != 0;
int extension = -1;
if (hasExtension)
{
if (offset >= data.Length)
{
throw new InvalidImageContentException($"The {sourceName} contains a truncated OBU extension header.");
}
// extension_header_reserved_3bits is also consumed but ignored by the reference decoder.
extension = data[offset++];
}
if (requireSizeFields && !hasSizeField)
{
throw new InvalidImageContentException($"The {sourceName} contains an OBU without its required payload-size field.");
}
int payloadLength;
if (hasSizeField)
{
payloadLength = ReadObuPayloadLength(data, ref offset, sourceName);
}
else
{
// Low-overhead image item syntax permits only the final OBU to omit its size, in which case the
// remaining item bytes are that OBU's payload and cannot contain another independently parsed OBU.
payloadLength = data.Length - offset;
}
if (payloadLength > data.Length - offset)
{
throw new InvalidImageContentException($"The {sourceName} contains an OBU payload that exceeds its data boundary.");
}
if (type == ObuType.SequenceHeader)
{
if (sequenceHeaderMustBeFirst && obuIndex != 0)
{
throw new InvalidImageContentException($"The {sourceName} contains a sequence header OBU after another OBU.");
}
sequenceHeaderCount++;
if (sequenceHeaderOffset < 0)
{
sequenceHeaderOffset = offset;
sequenceHeaderLength = payloadLength;
sequenceHeaderExtension = extension;
}
}
else if (type == ObuType.Metadata && !options.SkipMetadata)
{
try
{
ReadHdrMetadata(
data.Slice(offset, payloadLength),
sourceName,
out HeifContentLightLevel? obuContentLightLevel,
out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume);
if (obuContentLightLevel is not null)
{
if (contentLightLevel is not null
&& !ContentLightLevelsMatch(contentLightLevel.Value, obuContentLightLevel.Value))
{
throw new InvalidImageContentException($"The {sourceName} contains conflicting content light-level metadata OBUs.");
}
contentLightLevel = obuContentLightLevel;
}
if (obuMasteringDisplayColorVolume is not null)
{
if (masteringDisplayColorVolume is not null
&& masteringDisplayColorVolume.Value != obuMasteringDisplayColorVolume.Value)
{
throw new InvalidImageContentException($"The {sourceName} contains conflicting mastering-display metadata OBUs.");
}
masteringDisplayColorVolume = obuMasteringDisplayColorVolume;
}
}
catch (Exception ex) when (HeifDecoderCore.ShouldIgnoreAncillarySegmentError(options, ex))
{
// The OBU payload remains bounded by the image-data scan; only its invalid optional metadata is discarded.
}
}
offset += payloadLength;
obuIndex++;
}
return sequenceHeaderCount;
}
/// <summary>
/// Reads a bounded AV1 little-endian base-128 OBU payload length.
/// </summary>
/// <param name="data">The complete bounded OBU sequence.</param>
/// <param name="offset">The current byte offset, advanced past the encoded length.</param>
/// <param name="sourceName">The source description used by invalid-content errors.</param>
/// <returns>The payload length representable by the current item buffer.</returns>
private static int ReadObuPayloadLength(ReadOnlySpan<byte> data, ref int offset, string sourceName)
{
ulong value = ReadLeb128(data, ref offset, sourceName, "OBU payload length");
if (value > int.MaxValue)
{
throw new InvalidImageContentException($"The {sourceName} contains an OBU payload too large to buffer.");
}
return (int)value;
}
/// <summary>
/// Reads still-image high-dynamic-range data from an AV1 metadata OBU payload.
/// </summary>
/// <param name="payload">The bounded metadata OBU payload.</param>
/// <param name="sourceName">The source description used by invalid-content errors.</param>
/// <param name="contentLightLevel">Receives decoded content light-level metadata when present.</param>
/// <param name="masteringDisplayColorVolume">Receives decoded mastering-display metadata when present.</param>
public static void ReadHdrMetadata(
ReadOnlySpan<byte> payload,
string sourceName,
out HeifContentLightLevel? contentLightLevel,
out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume)
{
contentLightLevel = null;
masteringDisplayColorVolume = null;
int offset = 0;
ulong metadataType = ReadLeb128(payload, ref offset, sourceName, "metadata type");
if (metadataType != (ulong)ObuMetadataType.HdrCll
&& metadataType != (ulong)ObuMetadataType.HdrMdcv)
{
return;
}
int metadataLength = metadataType == (ulong)ObuMetadataType.HdrCll ? 4 : 24;
if (payload.Length - offset <= metadataLength)
{
throw new InvalidImageContentException($"The {sourceName} contains truncated HDR metadata or no trailing bits.");
}
ReadOnlySpan<byte> metadataData = payload.Slice(offset, metadataLength);
ValidateByteAlignedMetadataTrailingBits(payload[(offset + metadataLength)..], sourceName);
if (metadataType == (ulong)ObuMetadataType.HdrCll)
{
contentLightLevel = new HeifContentLightLevel(
BinaryPrimitives.ReadUInt16BigEndian(metadataData),
BinaryPrimitives.ReadUInt16BigEndian(metadataData[2..]));
return;
}
const float chromaticityScale = 1F / 65536F;
const double maximumLuminanceScale = 1D / 256D;
const double minimumLuminanceScale = 1D / 16384D;
// AV1 stores the primaries in R, G, B order and uses codec-specific fixed-point units that differ from the
// ISOBMFF mdcv property. Decode both representations to the same observable ImageSharp color coordinates.
CieXyChromaticityCoordinates redPrimary = new(
BinaryPrimitives.ReadUInt16BigEndian(metadataData) * chromaticityScale,
BinaryPrimitives.ReadUInt16BigEndian(metadataData[2..]) * chromaticityScale);
CieXyChromaticityCoordinates greenPrimary = new(
BinaryPrimitives.ReadUInt16BigEndian(metadataData[4..]) * chromaticityScale,
BinaryPrimitives.ReadUInt16BigEndian(metadataData[6..]) * chromaticityScale);
CieXyChromaticityCoordinates bluePrimary = new(
BinaryPrimitives.ReadUInt16BigEndian(metadataData[8..]) * chromaticityScale,
BinaryPrimitives.ReadUInt16BigEndian(metadataData[10..]) * chromaticityScale);
masteringDisplayColorVolume = new HeifMasteringDisplayColorVolume(
new RgbPrimariesChromaticityCoordinates(redPrimary, greenPrimary, bluePrimary),
new CieXyChromaticityCoordinates(
BinaryPrimitives.ReadUInt16BigEndian(metadataData[12..]) * chromaticityScale,
BinaryPrimitives.ReadUInt16BigEndian(metadataData[14..]) * chromaticityScale),
BinaryPrimitives.ReadUInt32BigEndian(metadataData[16..]) * maximumLuminanceScale,
BinaryPrimitives.ReadUInt32BigEndian(metadataData[20..]) * minimumLuminanceScale);
}
/// <summary>
/// Validates the trailing bits of byte-aligned fixed-length AV1 metadata.
/// </summary>
/// <param name="trailingData">The metadata payload bytes following its fixed fields.</param>
/// <param name="sourceName">The source description used by invalid-content errors.</param>
private static void ValidateByteAlignedMetadataTrailingBits(ReadOnlySpan<byte> trailingData, string sourceName)
{
byte lastNonzeroByte = 0;
for (int i = trailingData.Length - 1; i >= 0; i--)
{
if (trailingData[i] != 0)
{
lastNonzeroByte = trailingData[i];
break;
}
}
// Both fixed HDR structures end on a byte boundary. Zero padding after the required 0x80 byte is accepted,
// so locate the last nonzero byte rather than assuming the OBU payload ends immediately after trailing_bits().
if (lastNonzeroByte != 0x80)
{
throw new InvalidImageContentException($"The {sourceName} HDR metadata has invalid trailing bits.");
}
}
/// <summary>
/// Reads a bounded AV1 little-endian base-128 value.
/// </summary>
/// <param name="data">The complete bounded byte sequence.</param>
/// <param name="offset">The current byte offset, advanced past the encoded value.</param>
/// <param name="sourceName">The source description used by invalid-content errors.</param>
/// <param name="valueName">The value description used by invalid-content errors.</param>
/// <returns>The decoded unsigned value.</returns>
private static ulong ReadLeb128(
ReadOnlySpan<byte> data,
ref int offset,
string sourceName,
string valueName)
{
ulong value = 0;
for (int byteIndex = 0; byteIndex < 8; byteIndex++)
{
if (offset >= data.Length)
{
throw new InvalidImageContentException($"The {sourceName} contains a truncated {valueName}.");
}
byte current = data[offset++];
value |= (ulong)(current & 0x7F) << (byteIndex * 7);
if ((current & 0x80) == 0)
{
if (value > uint.MaxValue)
{
throw new InvalidImageContentException($"The {sourceName} contains a {valueName} larger than the AV1 32-bit limit.");
}
return value;
}
}
throw new InvalidImageContentException($"The {sourceName} contains an unterminated {valueName}.");
}
/// <summary>
/// Validates content light-level metadata against the corresponding image-item property when both are present.
/// </summary>
/// <param name="obuContentLightLevel">The value carried by an AV1 metadata OBU.</param>
/// <param name="itemContentLightLevel">The value carried by the associated image-item property.</param>
/// <param name="sourceName">The OBU source description used by invalid-content errors.</param>
private static void ValidateContentLightLevel(
HeifContentLightLevel? obuContentLightLevel,
HeifContentLightLevel? itemContentLightLevel,
string sourceName)
{
if (obuContentLightLevel is not null
&& itemContentLightLevel is not null
&& !ContentLightLevelsMatch(obuContentLightLevel.Value, itemContentLightLevel.Value))
{
throw new InvalidImageContentException($"The {sourceName} content light-level metadata does not match the image-item property.");
}
}
/// <summary>
/// Determines whether two content light-level descriptions carry the same observable values.
/// </summary>
/// <param name="left">The first content light-level description.</param>
/// <param name="right">The second content light-level description.</param>
/// <returns><see langword="true"/> when both light-level fields are equal.</returns>
private static bool ContentLightLevelsMatch(HeifContentLightLevel left, HeifContentLightLevel right)
{
return left.MaximumContentLightLevel == right.MaximumContentLightLevel
&& left.MaximumPictureAverageLightLevel == right.MaximumPictureAverageLightLevel;
}
/// <summary>
/// Validates mastering-display metadata against the corresponding image-item property when both are present.
/// </summary>
/// <param name="obuColorVolume">The value carried by an AV1 metadata OBU.</param>
/// <param name="itemColorVolume">The value carried by the associated image-item property.</param>
/// <param name="sourceName">The OBU source description used by invalid-content errors.</param>
private static void ValidateMasteringDisplayColorVolume(
HeifMasteringDisplayColorVolume? obuColorVolume,
HeifMasteringDisplayColorVolume? itemColorVolume,
string sourceName)
{
if (obuColorVolume is not null
&& itemColorVolume is not null
&& !MasteringDisplayColorVolumesMatch(obuColorVolume.Value, itemColorVolume.Value))
{
throw new InvalidImageContentException($"The {sourceName} mastering-display metadata does not match the image-item property.");
}
}
/// <summary>
/// Determines whether AV1 and ISOBMFF mastering-display values agree within their fixed-point precision.
/// </summary>
/// <param name="obuColorVolume">The mastering-display values decoded from the AV1 representation.</param>
/// <param name="itemColorVolume">The mastering-display values decoded from the ISOBMFF representation.</param>
/// <returns><see langword="true"/> when all decoded values agree within their combined quantization error.</returns>
private static bool MasteringDisplayColorVolumesMatch(
HeifMasteringDisplayColorVolume obuColorVolume,
HeifMasteringDisplayColorVolume itemColorVolume)
{
const float chromaticityTolerance = ((1F / 65536F) + (1F / 50000F)) / 2F;
const double maximumLuminanceTolerance = ((1D / 256D) + (1D / 10000D)) / 2D;
const double minimumLuminanceTolerance = ((1D / 16384D) + (1D / 10000D)) / 2D;
return ChromaticitiesMatch(obuColorVolume.Primaries.R, itemColorVolume.Primaries.R, chromaticityTolerance)
&& ChromaticitiesMatch(obuColorVolume.Primaries.G, itemColorVolume.Primaries.G, chromaticityTolerance)
&& ChromaticitiesMatch(obuColorVolume.Primaries.B, itemColorVolume.Primaries.B, chromaticityTolerance)
&& ChromaticitiesMatch(obuColorVolume.WhitePoint, itemColorVolume.WhitePoint, chromaticityTolerance)
&& ValuesMatch(obuColorVolume.MaximumLuminance, itemColorVolume.MaximumLuminance, maximumLuminanceTolerance)
&& ValuesMatch(obuColorVolume.MinimumLuminance, itemColorVolume.MinimumLuminance, minimumLuminanceTolerance);
}
/// <summary>
/// Determines whether two chromaticity-coordinate pairs agree within the supplied fixed-point tolerance.
/// </summary>
/// <param name="left">The first chromaticity-coordinate pair.</param>
/// <param name="right">The second chromaticity-coordinate pair.</param>
/// <param name="tolerance">The maximum permitted difference on either coordinate axis.</param>
/// <returns><see langword="true"/> when both coordinate differences are within the tolerance.</returns>
private static bool ChromaticitiesMatch(
CieXyChromaticityCoordinates left,
CieXyChromaticityCoordinates right,
float tolerance)
{
return ValuesMatch(left.X, right.X, tolerance)
&& ValuesMatch(left.Y, right.Y, tolerance);
}
/// <summary>
/// Determines whether two decoded fixed-point values agree within the supplied tolerance.
/// </summary>
/// <param name="left">The first decoded value.</param>
/// <param name="right">The second decoded value.</param>
/// <param name="tolerance">The maximum permitted absolute difference.</param>
/// <returns><see langword="true"/> when the absolute difference does not exceed the tolerance.</returns>
private static bool ValuesMatch(double left, double right, double tolerance)
=> Math.Abs(left - right) <= tolerance;
}

30
src/ImageSharp/Formats/Heif/Av1/Av1ColorFormat.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Identifies the AV1 luma and chroma plane sampling layout.
/// </summary>
internal enum Av1ColorFormat
{
/// <summary>
/// Monochrome luma samples without chroma planes.
/// </summary>
Yuv400,
/// <summary>
/// Chroma samples subsampled by two horizontally and vertically.
/// </summary>
Yuv420,
/// <summary>
/// Chroma samples subsampled by two horizontally.
/// </summary>
Yuv422,
/// <summary>
/// Full-resolution luma and chroma samples.
/// </summary>
Yuv444,
}

353
src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs

@ -0,0 +1,353 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Defines shared AV1 syntax, geometry, entropy, and transform limits.
/// </summary>
internal static class Av1Constants
{
/// <summary>
/// The highest sequence profile defined by AV1.
/// </summary>
public const ObuSequenceProfile MaxSequenceProfile = ObuSequenceProfile.Professional;
/// <summary>
/// The number of bits used for an operating-point level index.
/// </summary>
public const int LevelBits = 5;
/// <summary>
/// The number of bits used for the zero-based operating-point count.
/// </summary>
public const int OperatingPointCountBits = 5;
/// <summary>
/// The number of bits used for an operating-point layer-selection mask.
/// </summary>
public const int OperatingPointIdcBits = 12;
/// <summary>
/// The number of bits used for a frame type.
/// </summary>
public const int FrameTypeBits = 2;
/// <summary>
/// The first sequence-level index that carries an explicit tier bit.
/// </summary>
public const int SequenceTierMinimumLevelIndex = 8;
/// <summary>
/// The number of bits used to select a frame from the eight-slot reference map.
/// </summary>
public const int ReferenceFrameIndexBits = 3;
/// <summary>
/// The sequence-header value that lets each frame choose whether to use screen-content tools.
/// </summary>
public const int SelectScreenContentTools = 2;
/// <summary>
/// The sequence-header value that lets each applicable frame choose whether to require integer motion vectors.
/// </summary>
public const int SelectIntegerMotionVector = 2;
/// <summary>
/// The maximum number of operating points declared by one AV1 sequence header.
/// </summary>
public const int MaxOperatingPointCount = 32;
/// <summary>
/// The maximum number of spatial layers identified by an AV1 OBU extension header.
/// </summary>
public const int MaxSpatialLayerCount = 4;
/// <summary>
/// The number of bits used to signal a super-resolution denominator offset.
/// </summary>
public const int SuperResolutionScaleBits = 3;
/// <summary>
/// The fixed numerator of the AV1 super-resolution scaling ratio.
/// </summary>
public const int ScaleNumerator = 8;
/// <summary>
/// The number of reference frames that can be used for inter prediction.
/// </summary>
public const int ReferencesPerFrame = 7;
/// <summary>
/// The largest frame width or height representable by the 16-bit AV1 dimension syntax.
/// </summary>
public const int MaxFrameDimension = 1 << 16;
/// <summary>
/// The maximum area of a tile in units of luma samples.
/// </summary>
public const int MaxTileArea = 4096 * 2304;
/// <summary>
/// The maximum width of a tile in units of luma samples.
/// </summary>
public const int MaxTileWidth = 4096;
/// <summary>
/// The maximum number of tile columns.
/// </summary>
public const int MaxTileColumnCount = 64;
/// <summary>
/// The maximum number of tile rows.
/// </summary>
public const int MaxTileRowCount = 64;
/// <summary>
/// The number of 64x64 CDEF filter units in a 128x128 superblock.
/// </summary>
public const int CdefUnitsPerSuperblock = 4;
/// <summary>
/// The number of frames that can be stored for future reference.
/// </summary>
public const int ReferenceFrameCount = 8;
/// <summary>
/// The primary-reference-frame value indicating that no primary reference is selected.
/// </summary>
public const uint PrimaryReferenceFrameNone = 7;
/// <summary>
/// The number of bits used to signal a primary reference frame.
/// </summary>
public const int PrimaryReferenceBits = 3;
/// <summary>
/// The number of segments allowed in a segmentation map.
/// </summary>
public const int MaxSegmentCount = 8;
/// <summary>
/// The smallest signaled denominator for an active super-resolution ratio.
/// </summary>
public const int SuperResolutionScaleDenominatorMinimum = 9;
/// <summary>
/// The base-two logarithm of the maximum superblock size in luma samples.
/// </summary>
public const int MaxSuperBlockSizeLog2 = 7;
/// <summary>
/// The base-two logarithm of the smallest mode-info block size in luma samples.
/// </summary>
public const int ModeInfoSizeLog2 = 2;
/// <summary>
/// The maximum quantizer index.
/// </summary>
public const int MaxQ = 255;
/// <summary>
/// The number of segmentation features.
/// </summary>
public const int SegmentationLevelMax = 8;
/// <summary>
/// The maximum loop-restoration tile size in samples.
/// </summary>
public const int RestorationMaxTileSize = 256;
/// <summary>
/// The number of independent Wiener filter coefficients per direction.
/// </summary>
public const int WienerCoefficientCount = 3;
/// <summary>
/// The number of luma and chroma frame loop-filter levels.
/// </summary>
public const int FrameLoopFilterCount = 4;
/// <summary>
/// The first quantizer-delta magnitude encoded through the escape path.
/// </summary>
public const int DeltaQuantizerSmall = 3;
/// <summary>
/// The first loop-filter-delta magnitude encoded through the escape path.
/// </summary>
public const int DeltaLoopFilterSmall = 3;
/// <summary>
/// The maximum loop-filter strength.
/// </summary>
public const int MaxLoopFilter = 63;
/// <summary>
/// The maximum directional-prediction angle-delta magnitude.
/// </summary>
public const int MaxAngleDelta = 3;
/// <summary>
/// The maximum number of color planes.
/// </summary>
public const int MaxPlanes = 3;
/// <summary>
/// The number of reference-frame types, including the intra type.
/// </summary>
public const int TotalReferencesPerFrame = 8;
/// <summary>
/// The maximum palette size.
/// </summary>
public const int PaletteMaxSize = 8;
/// <summary>
/// The number of transform-size probability categories.
/// </summary>
public const int MaxTransformCategories = 4;
/// <summary>
/// The number of cumulative coefficient-level magnitude contexts.
/// </summary>
public const int CoefficientContextCount = 6;
/// <summary>
/// The number of coefficient magnitudes represented by base symbols before base-range coding.
/// </summary>
public const int BaseLevelsCount = 2;
/// <summary>
/// The maximum coefficient magnitude increment represented by base-range symbols.
/// </summary>
public const int CoefficientBaseRange = 12;
/// <summary>
/// The maximum transform dimension in samples.
/// </summary>
public const int MaxTransformSize = 1 << 6;
/// <summary>
/// The maximum transform dimension in units of four samples.
/// </summary>
public const int MaxTransformSizeUnit = MaxTransformSize >> 2;
/// <summary>
/// The number of low-order bits reserved for a cumulative coefficient-level context.
/// </summary>
public const int CoefficientContextBitCount = 3;
/// <summary>
/// The mask selecting the cumulative coefficient-level magnitude bits.
/// </summary>
public const int CoefficientContextMask = (1 << CoefficientContextBitCount) - 1;
/// <summary>
/// The base-two logarithm of the horizontal coefficient-context padding.
/// </summary>
public const int TransformPadHorizontalLog2 = 2;
/// <summary>
/// The horizontal coefficient-context padding in elements.
/// </summary>
public const int TransformPadHorizontal = 1 << TransformPadHorizontalLog2;
/// <summary>
/// The total vertical coefficient-context padding in rows.
/// </summary>
public const int TransformPadVertical = 6;
/// <summary>
/// The trailing coefficient-context padding in elements.
/// </summary>
public const int TransformPadEnd = 16;
/// <summary>
/// The maximum padded two-dimensional coefficient-context allocation size.
/// </summary>
public const int TransformPad2d = ((MaxTransformSize + TransformPadHorizontal) * (MaxTransformSize + TransformPadVertical)) + TransformPadEnd;
/// <summary>
/// The coefficient-context padding above a transform.
/// </summary>
public const int TransformPadTop = 2;
/// <summary>
/// The coefficient-context padding below a transform.
/// </summary>
public const int TransformPadBottom = 4;
/// <summary>
/// The largest symbol in a coefficient base-range distribution.
/// </summary>
public const int BaseRangeSizeMinus1 = 3;
/// <summary>
/// The largest coefficient magnitude represented before Golomb coding.
/// </summary>
public const int MaxBaseRange = 15;
/// <summary>
/// The base-two logarithm of the chroma-from-luma alpha alphabet size.
/// </summary>
public const int ChromaFromLumaAlphabetSizeLog2 = 4;
/// <summary>
/// The number of quantization-matrix levels.
/// </summary>
public const int QuantificationMatrixLevelCount = 1 << 4;
/// <summary>
/// The fixed-point precision of each quantization-matrix element.
/// </summary>
public const int QuantizationMatrixElementBitCount = 5;
/// <summary>
/// The directional intra-prediction angle increment in degrees.
/// </summary>
public const int AngleStep = 3;
/// <summary>
/// The maximum number of stages in a one-dimensional transform function.
/// </summary>
public const int MaxTransformStageNumber = 12;
/// <summary>
/// The number of partition contexts per block-size logarithm.
/// </summary>
public const int PartitionProbabilitySet = 4;
/// <summary>
/// The number of square transform-size contexts that can signal extended transforms.
/// </summary>
public const int ExtendedTransformCount = 4;
/// <summary>
/// The highest variable-transform depth index.
/// </summary>
public const int MaxVarTransform = 2;
/// <summary>
/// Number of items in the <see cref="Av1PlaneType"/> enumeration.
/// </summary>
public const int PlaneTypeCount = 2;
/// <summary>
/// Gets the number of payload bits used by each segmentation feature.
/// </summary>
public static ReadOnlySpan<int> SegmentationFeatureBits => [8, 6, 6, 6, 6, 3, 0, 0];
/// <summary>
/// Gets values indicating whether each segmentation feature is signed.
/// </summary>
public static ReadOnlySpan<int> SegmentationFeatureSigned => [1, 1, 1, 1, 1, 0, 0, 0];
/// <summary>
/// Gets the maximum magnitude or value permitted for each segmentation feature.
/// </summary>
public static ReadOnlySpan<int> SegmentationFeatureMax => [MaxQ, MaxLoopFilter, MaxLoopFilter, MaxLoopFilter, MaxLoopFilter, 7, 0, 0];
}

969
src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs

@ -0,0 +1,969 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Color;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.FilmGrain;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration;
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.PixelFormats.Utils;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Decodes bounded AV1 image payloads and image-sequence samples into ImageSharp frames.
/// </summary>
internal sealed class Av1Decoder : IAv1TileReader, IDisposable
{
/// <summary>
/// The open-bitstream-unit parser for the current image item.
/// </summary>
private readonly ObuReader obuReader;
/// <summary>
/// The configuration used for decoded image and scratch-memory allocation.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// The CDEF filtering stage and working storage shared by successive frames.
/// </summary>
private readonly Av1CdefDecoder cdefDecoder;
/// <summary>
/// The restoration boundary rows shared by successive frames.
/// </summary>
private readonly Av1LoopRestorationBoundary restorationBoundary;
/// <summary>
/// The restoration stage and working storage shared by successive frames.
/// </summary>
private readonly Av1LoopRestorationDecoder restorationDecoder;
/// <summary>
/// Reusable luma palette indices for the coding blocks in one superblock.
/// </summary>
private readonly Buffer2D<byte> lumaPaletteColorIndexMap;
/// <summary>
/// Reusable chroma palette indices for the coding blocks in one superblock.
/// </summary>
private readonly Buffer2D<byte> chromaPaletteColorIndexMap;
/// <summary>
/// The shared backing owner for both reusable palette maps.
/// </summary>
private readonly IMemoryOwner<byte> paletteColorIndexMapOwner;
/// <summary>
/// The reconstructed references and selected presentation output owned by the current bounded decode session.
/// </summary>
private readonly Av1ReferenceFrameStore referenceFrames = new();
/// <summary>
/// The frame-base, tile-working, and published entropy contexts created for the first coded frame and then reused
/// for this bounded decoder session.
/// </summary>
private Av1FrameEntropyContexts? entropyContexts;
/// <summary>
/// The coded sequence governing the active reference map and reusable entropy session.
/// </summary>
private ObuSequenceHeader? entropySequenceHeader;
/// <summary>
/// The item codec configuration validated before reconstructing a completed frame.
/// </summary>
private Av1CodecConfiguration? codecConfiguration;
/// <summary>
/// The container color description applied before reconstructing a completed frame.
/// </summary>
private CicpProfile? containerColorProfile;
/// <summary>
/// The sequence header already validated for the current bounded payload.
/// </summary>
private ObuSequenceHeader? validatedSequenceHeader;
/// <summary>
/// The complete parser, sample buffer, and reconstruction state for the frame currently being decoded.
/// </summary>
private FrameDecodeState? frameDecodeState;
/// <summary>
/// Retains reconstruction scratch across frames; the active frame borrows its memory until completion.
/// </summary>
private IMemoryOwner<short>? reconstructionWorkspace;
/// <summary>
/// Initializes a new instance of the <see cref="Av1Decoder"/> class.
/// </summary>
/// <param name="configuration">The configuration used for image and scratch-memory allocation.</param>
public Av1Decoder(Configuration configuration)
: this(configuration, 0)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Decoder"/> class for one selected AV1 operating point.
/// </summary>
/// <param name="configuration">The configuration used for image and scratch-memory allocation.</param>
/// <param name="operatingPointIndex">The zero-based sequence-header operating-point index to decode.</param>
public Av1Decoder(Configuration configuration, byte operatingPointIndex)
{
this.configuration = configuration;
this.obuReader = new(operatingPointIndex, this.referenceFrames);
this.cdefDecoder = new(configuration.MemoryAllocator);
this.restorationBoundary = new(configuration.MemoryAllocator);
this.restorationDecoder = new(configuration.MemoryAllocator);
// Sequential tile decoding needs only the palette indices belonging to the current superblock. One fixed
// owner keeps both maximum-superblock maps reusable across the bounded session without fragmented group rents.
int paletteMapLength = 1 << Av1Constants.MaxSuperBlockSizeLog2;
int paletteMapArea = paletteMapLength * paletteMapLength;
this.paletteColorIndexMapOwner = configuration.MemoryAllocator.Allocate<byte>(2 * paletteMapArea);
Memory<byte> paletteMaps = this.paletteColorIndexMapOwner.Memory;
this.lumaPaletteColorIndexMap = Buffer2D<byte>.WrapMemory(
paletteMaps[..paletteMapArea],
paletteMapLength,
paletteMapLength);
this.chromaPaletteColorIndexMap = Buffer2D<byte>.WrapMemory(
paletteMaps[paletteMapArea..],
paletteMapLength,
paletteMapLength);
}
/// <summary>
/// Gets the final retained shown-frame header, or <see langword="null"/> before a shown frame completes.
/// </summary>
public ObuFrameHeader? FrameHeader { get; private set; }
/// <summary>
/// Gets the sequence header governing the final retained shown frame, or <see langword="null"/> before one completes.
/// </summary>
public ObuSequenceHeader? SequenceHeader { get; private set; }
/// <summary>
/// Gets tile and superblock state for the most recently reconstructed frame, or <see langword="null"/> when no
/// frame was reconstructed or the output selected an existing reference without new tile syntax.
/// </summary>
public Av1FrameInfo? FrameInfo { get; private set; }
/// <summary>
/// Gets the inter-prediction features selected by every coded frame completed in the most recently decoded payload.
/// </summary>
public Av1InterPredictionFeatures DecodedInterPredictionFeatures { get; private set; }
/// <summary>
/// Gets or sets the chroma reconstruction mode for presented sequence frames.
/// </summary>
public HeifChromaUpsampling ChromaUpsampling { get; set; }
/// <summary>
/// Gets the native planes of the current retained shown frame, or <see langword="null"/> before one completes.
/// </summary>
public Av1FrameBuffer<byte>? FrameBuffer => this.referenceFrames.OutputFrame?.FrameBuffer;
/// <summary>
/// Decodes the next visible sample in a bounded AV1 image sequence directly into a caller-owned frame.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <param name="buffer">The complete AV1 sample payload.</param>
/// <param name="containerColorProfile">The container color description.</param>
/// <param name="codecConfiguration">The AV1 sample-entry configuration.</param>
/// <param name="expectedCodedSize">The coded dimensions declared by the visual sample entry.</param>
/// <param name="sourceRectangle">The clean-aperture region mapped to the complete destination frame.</param>
/// <param name="destination">The caller-owned packed-pixel frame receiving the presented sample.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
/// <param name="profile">The source profile selected for conversion, or null to preserve source colors.</param>
/// <param name="alphaFrame">The decoder-owned auxiliary frame, or null for opaque pixels.</param>
/// <param name="premultiplied">Whether source RGB is associated with alpha.</param>
public CicpProfile DecodeSequenceFrame<TPixel>(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration,
Size expectedCodedSize,
Rectangle sourceRectangle,
Buffer2DRegion<TPixel> destination,
HeifPixelTransform transform,
IccProfile? profile,
Av1FrameBuffer<byte>? alphaFrame,
bool premultiplied)
where TPixel : unmanaged, IPixel<TPixel>
{
CicpProfile effectiveColorProfile = this.DecodePayload(
buffer,
containerColorProfile,
codecConfiguration,
null,
requireShownFrame: true);
Av1ReferenceFrame outputFrame = this.referenceFrames.ResolveOutput();
Size codedSize = new(
outputFrame.FrameHeader.FrameSize.SuperResolutionUpscaledWidth,
outputFrame.FrameHeader.FrameSize.FrameHeight);
if (codedSize != expectedCodedSize)
{
throw new InvalidImageContentException(
"The decoded image-sequence sample dimensions do not match its visual sample entry.");
}
// Keep reconstructed reference state unchanged while honoring the container's presentation range.
Av1YuvConverter.ConvertToRgb(
this.configuration,
outputFrame.FrameBuffer,
sourceRectangle,
destination,
codedSize,
transform,
profile,
alphaFrame,
codedSize,
sourceRectangle,
premultiplied,
this.ChromaUpsampling,
containerColorProfile?.FullRange ?? outputFrame.FrameBuffer.ColorConfig.ColorRange);
return effectiveColorProfile;
}
/// <summary>
/// Decodes one non-presented AV1 image-sequence sample while retaining its reference state.
/// </summary>
/// <param name="buffer">The complete AV1 sample payload.</param>
/// <param name="containerColorProfile">The container color description.</param>
/// <param name="codecConfiguration">The AV1 sample-entry configuration.</param>
public void DecodeSequenceReference(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration)
=> _ = this.DecodePayload(
buffer,
containerColorProfile,
codecConfiguration,
null,
requireShownFrame: false);
/// <summary>
/// Decodes the next visible monochrome AV1 sequence sample into native samples.
/// </summary>
/// <param name="buffer">The complete AV1 sample payload.</param>
/// <param name="containerColorProfile">The container color description.</param>
/// <param name="codecConfiguration">The AV1 sample-entry configuration.</param>
/// <param name="expectedCodedSize">The required coded dimensions.</param>
/// <returns>The auxiliary frame retained by this decoder until the next sample is decoded or the decoder is disposed.</returns>
public Av1FrameBuffer<byte> DecodeSequenceAlpha(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration,
Size expectedCodedSize)
{
_ = this.DecodePayload(
buffer,
containerColorProfile,
codecConfiguration,
null,
requireShownFrame: true);
Av1ReferenceFrame outputFrame = this.referenceFrames.ResolveOutput();
Av1FrameBuffer<byte> frame = outputFrame.FrameBuffer;
if (frame.Width != expectedCodedSize.Width || frame.Height != expectedCodedSize.Height)
{
throw new InvalidImageContentException("The decoded alpha sample dimensions do not match its visual sample entry.");
}
if (frame.ColorFormat != Av1ColorFormat.Yuv400)
{
throw new InvalidImageContentException("An AV1 alpha sample must be monochrome.");
}
return frame;
}
/// <summary>
/// Decodes an AV1 elementary-stream payload and composes its luma plane directly into a packed color frame.
/// </summary>
/// <typeparam name="TPixel">The destination color pixel type.</typeparam>
/// <param name="buffer">The complete AV1 elementary-stream payload.</param>
/// <param name="containerColorProfile">
/// The container color description that supplies unspecified sequence-header color information.
/// </param>
/// <param name="codecConfiguration">The AV1 codec configuration validated against the coded sequence header.</param>
/// <param name="expectedCodedSize">The required coded dimensions, or an empty size when the item extent may differ.</param>
/// <param name="destination">The packed color frame receiving alpha values.</param>
/// <param name="outputSize">The complete presented size of the auxiliary image or grid tile.</param>
/// <param name="destinationRectangle">The destination region receiving the top-left portion of the presented alpha image.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
/// <param name="layeredImageIndex">The optional byte boundaries of a layered AV1 image item.</param>
public void DecodeAlpha<TPixel>(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration,
Size expectedCodedSize,
Buffer2DRegion<TPixel> destination,
Size outputSize,
Rectangle destinationRectangle,
bool premultiplied,
HeifPixelTransform transform,
Av1LayeredImageIndex? layeredImageIndex = null)
where TPixel : unmanaged, IPixel<TPixel>
{
using Av1FrameBuffer<byte> frameBuffer = this.DecodeFrameBuffer(
buffer,
containerColorProfile,
codecConfiguration,
out _,
layeredImageIndex);
this.ComposeAlpha(
frameBuffer,
expectedCodedSize,
destination,
outputSize,
destinationRectangle,
premultiplied,
transform);
}
/// <summary>
/// Composes one decoded monochrome plane into a packed color frame.
/// </summary>
/// <typeparam name="TPixel">The destination color pixel type.</typeparam>
/// <param name="frameBuffer">The decoded monochrome planes.</param>
/// <param name="expectedCodedSize">The required coded dimensions.</param>
/// <param name="destination">The packed color frame receiving alpha values.</param>
/// <param name="outputSize">The complete presented size of the auxiliary image.</param>
/// <param name="destinationRectangle">The destination region receiving the alpha image.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
private void ComposeAlpha<TPixel>(
Av1FrameBuffer<byte> frameBuffer,
Size expectedCodedSize,
Buffer2DRegion<TPixel> destination,
Size outputSize,
Rectangle destinationRectangle,
bool premultiplied,
HeifPixelTransform transform)
where TPixel : unmanaged, IPixel<TPixel>
=> this.ComposeAlpha(
frameBuffer,
expectedCodedSize,
new Rectangle(0, 0, frameBuffer.Width, frameBuffer.Height),
destination,
outputSize,
destinationRectangle,
premultiplied,
transform);
/// <summary>
/// Composes one decoded monochrome region into a packed color frame.
/// </summary>
/// <typeparam name="TPixel">The destination color pixel type.</typeparam>
/// <param name="frameBuffer">The decoded monochrome planes.</param>
/// <param name="expectedCodedSize">The required coded dimensions.</param>
/// <param name="sourceRectangle">The luma region mapped to the destination rectangle.</param>
/// <param name="destination">The packed color frame receiving alpha values.</param>
/// <param name="outputSize">The complete presented size of the auxiliary image.</param>
/// <param name="destinationRectangle">The destination region receiving the alpha image.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
private void ComposeAlpha<TPixel>(
Av1FrameBuffer<byte> frameBuffer,
Size expectedCodedSize,
Rectangle sourceRectangle,
Buffer2DRegion<TPixel> destination,
Size outputSize,
Rectangle destinationRectangle,
bool premultiplied,
HeifPixelTransform transform)
where TPixel : unmanaged, IPixel<TPixel>
{
if (expectedCodedSize != default && (frameBuffer.Width != expectedCodedSize.Width || frameBuffer.Height != expectedCodedSize.Height))
{
throw new InvalidImageContentException("The decoded alpha sample dimensions do not match its visual sample entry.");
}
if (frameBuffer.ColorFormat != Av1ColorFormat.Yuv400)
{
// AVIF auxiliary alpha is the luma plane of an AV1 monochrome image. Accepting chroma-bearing payloads
// would silently reinterpret a color image and contradict the Sequence Header mono_chrome requirement.
throw new InvalidImageContentException("An AV1 auxiliary alpha image must be encoded as monochrome.");
}
Av1YuvConverter.ComposeAlpha(
this.configuration,
frameBuffer,
sourceRectangle,
destination,
outputSize,
destinationRectangle,
premultiplied,
transform);
}
/// <summary>
/// Parses every coded frame in an AV1 payload and returns the final shown frame's native component planes.
/// </summary>
/// <param name="buffer">The complete AV1 elementary-stream payload.</param>
/// <param name="containerColorProfile">
/// The container color description that supplies unspecified sequence-header color information.
/// </param>
/// <param name="codecConfiguration">The AV1 codec configuration validated against the coded sequence header.</param>
/// <param name="effectiveColorProfile">Receives the effective CICP description associated with the native planes.</param>
/// <param name="layeredImageIndex">The optional byte boundaries of a layered AV1 image item.</param>
/// <returns>The reconstructed native frame buffer. Ownership transfers to the caller.</returns>
public Av1FrameBuffer<byte> DecodeFrameBuffer(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration,
out CicpProfile effectiveColorProfile,
Av1LayeredImageIndex? layeredImageIndex = null)
=> this.DecodeFrameBuffer(
buffer,
containerColorProfile,
codecConfiguration,
out effectiveColorProfile,
out _,
layeredImageIndex);
/// <summary>
/// Parses every coded frame in an AV1 payload and returns the final shown frame's native planes and header.
/// </summary>
private Av1FrameBuffer<byte> DecodeFrameBuffer(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration,
out CicpProfile effectiveColorProfile,
out ObuFrameHeader frameHeader,
Av1LayeredImageIndex? layeredImageIndex)
{
effectiveColorProfile = this.DecodePayload(
buffer,
containerColorProfile,
codecConfiguration,
layeredImageIndex,
requireShownFrame: true);
using Av1ReferenceFrame outputFrame = this.referenceFrames.TakeOutput();
frameHeader = outputFrame.FrameHeader;
return outputFrame.TakeFrameBuffer();
}
/// <summary>
/// Parses one bounded payload into the retained decoder session.
/// </summary>
/// <param name="buffer">The complete AV1 payload.</param>
/// <param name="containerColorProfile">The container color description.</param>
/// <param name="codecConfiguration">The AV1 codec configuration.</param>
/// <param name="layeredImageIndex">The optional layer byte boundaries.</param>
/// <param name="requireShownFrame">Whether the payload must produce a shown frame.</param>
/// <returns>The effective CICP description.</returns>
private CicpProfile DecodePayload(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration,
Av1LayeredImageIndex? layeredImageIndex,
bool requireShownFrame)
{
this.codecConfiguration = codecConfiguration;
this.containerColorProfile = containerColorProfile;
this.validatedSequenceHeader = null;
this.obuReader.ResetMetadata();
this.SequenceHeader = null;
this.FrameHeader = null;
this.DecodedInterPredictionFeatures = Av1InterPredictionFeatures.None;
// Full tile syntax describes only frames reconstructed by this payload. Reference slots already own the compact
// state needed by later frames, so release the previous payload's reconstruction graph before parsing the next.
this.FrameInfo?.ReleaseOwner();
this.FrameInfo = null;
try
{
if (layeredImageIndex is null)
{
Av1BitStreamReader reader = new(buffer);
this.obuReader.ReadAll(ref reader, buffer.Length, this, false);
}
else
{
int layerOffset = 0;
for (int layer = 0; layer < Av1Constants.MaxSpatialLayerCount - 1 && layerOffset < buffer.Length; layer++)
{
uint declaredLayerSize = layer switch
{
0 => layeredImageIndex.Value.FirstLayerSize,
1 => layeredImageIndex.Value.SecondLayerSize,
_ => layeredImageIndex.Value.ThirdLayerSize
};
if (declaredLayerSize == 0)
{
break;
}
int layerSize = (int)declaredLayerSize;
Av1BitStreamReader layerReader = new(buffer.Slice(layerOffset, layerSize));
this.obuReader.ReadAll(ref layerReader, layerSize, this, false);
layerOffset += layerSize;
}
if (layerOffset < buffer.Length)
{
Span<byte> finalLayer = buffer[layerOffset..];
Av1BitStreamReader finalLayerReader = new(finalLayer);
this.obuReader.ReadAll(ref finalLayerReader, finalLayer.Length, this, false);
}
}
ObuSequenceHeader sequenceHeader = this.obuReader.SequenceHeader
?? throw new InvalidImageContentException("The AV1 payload contains no sequence header.");
if (requireShownFrame)
{
_ = this.referenceFrames.ResolveOutput();
}
// Preserve the effective CICP description used for conversion, including container values that legally
// supplied unspecified bitstream fields. This also exposes bitstream-only color metadata to callers.
ObuColorConfig effectiveColorConfig = sequenceHeader.ColorConfig;
return new CicpProfile(
(byte)effectiveColorConfig.ColorPrimaries,
(byte)effectiveColorConfig.TransferCharacteristics,
(byte)effectiveColorConfig.MatrixCoefficients,
effectiveColorConfig.ColorRange);
}
catch
{
// A failed frame may own pooled neighbor contexts while earlier layers own reconstructed references and
// published CDF snapshots. None can be reused after a non-transactional frame transition has failed.
this.frameDecodeState?.Dispose();
this.frameDecodeState = null;
this.obuReader.Reset();
this.entropyContexts?.Reset();
this.entropySequenceHeader = null;
this.SequenceHeader = null;
this.FrameHeader = null;
this.DecodedInterPredictionFeatures = Av1InterPredictionFeatures.None;
this.FrameInfo?.ReleaseOwner();
this.FrameInfo = null;
throw;
}
finally
{
// Validation inputs belong to this bounded decode call. Completed native buffers retain no references to
// either description, so releasing them here prevents a reused decoder from observing stale item state.
this.codecConfiguration = null;
this.containerColorProfile = null;
this.validatedSequenceHeader = null;
}
}
/// <summary>
/// Validates the active sequence against its container declarations before reconstruction begins.
/// </summary>
/// <param name="sequenceHeader">The active sequence header.</param>
private void ValidateSequence(ObuSequenceHeader sequenceHeader)
{
if (ReferenceEquals(this.validatedSequenceHeader, sequenceHeader))
{
return;
}
this.codecConfiguration?.Validate(sequenceHeader);
CicpProfile? colorProfile = this.containerColorProfile;
if (colorProfile is not null)
{
ObuColorConfig colorConfig = sequenceHeader.ColorConfig;
ObuColorPrimaries containerColorPrimaries = (ObuColorPrimaries)colorProfile.ColorPrimaries;
ObuTransferCharacteristics containerTransferCharacteristics = (ObuTransferCharacteristics)colorProfile.TransferCharacteristics;
ObuMatrixCoefficients containerMatrixCoefficients = (ObuMatrixCoefficients)colorProfile.MatrixCoefficients;
// AV1-ISOBMFF permits nclx to supply only bitstream fields explicitly coded as unspecified. A different
// specified value is a conformance error rather than a container-level color override.
if (colorConfig.ColorPrimaries == ObuColorPrimaries.Unspecified)
{
colorConfig.ColorPrimaries = containerColorPrimaries;
}
else if (colorConfig.ColorPrimaries != containerColorPrimaries)
{
throw new InvalidImageContentException("The HEIF CICP color primaries do not match the AV1 sequence header.");
}
if (colorConfig.TransferCharacteristics == ObuTransferCharacteristics.Unspecified)
{
colorConfig.TransferCharacteristics = containerTransferCharacteristics;
}
else if (colorConfig.TransferCharacteristics != containerTransferCharacteristics)
{
throw new InvalidImageContentException("The HEIF CICP transfer characteristics do not match the AV1 sequence header.");
}
if (colorConfig.MatrixCoefficients == ObuMatrixCoefficients.Unspecified)
{
colorConfig.MatrixCoefficients = containerMatrixCoefficients;
}
else if (colorConfig.MatrixCoefficients != containerMatrixCoefficients)
{
throw new InvalidImageContentException("The HEIF CICP matrix coefficients do not match the AV1 sequence header.");
}
}
// The same sequence header governs subsequent layered frames until another header OBU replaces it.
this.validatedSequenceHeader = sequenceHeader;
}
/// <summary>
/// Parses one entropy-coded tile payload into the current frame state.
/// </summary>
/// <param name="tileData">The entropy-coded tile payload.</param>
/// <param name="tileNum">The raster-order tile index.</param>
public void ReadTile(Span<byte> tileData, int tileNum)
{
FrameDecodeState frameDecodeState;
if (this.frameDecodeState is null)
{
ObuSequenceHeader sequenceHeader = this.obuReader.CurrentSequenceHeader;
ObuFrameHeader frameHeader = this.obuReader.CurrentFrameHeader;
this.ValidateSequence(sequenceHeader);
// Sequence dimensions are an upper bound, not an allocation request. Check the active upscaled frame
// before renting its syntax state; a small frame can legally belong to a much larger sequence envelope.
Av1FrameBuffer<byte>.ValidateDimensions(
sequenceHeader,
sequenceHeader.ColorConfig.GetColorFormat(),
false,
frameHeader.FrameSize.SuperResolutionUpscaledWidth,
frameHeader.FrameSize.FrameHeight);
if (!ReferenceEquals(this.entropySequenceHeader, sequenceHeader))
{
if (this.entropySequenceHeader is not null)
{
// A coded-sequence boundary invalidates both sample references and their retained CDF snapshots.
// Returned snapshot graphs stay decoder-local and can be overwritten for the new sequence.
this.referenceFrames.Reset();
this.entropyContexts?.Reset();
}
this.entropySequenceHeader = sequenceHeader;
}
Av1FrameEntropyContext? primaryReferenceContext = null;
byte? primaryReferenceSlot = frameHeader.PrimaryReferenceSlot;
if (primaryReferenceSlot is not null)
{
// The uncompressed-header parser validates slot occupancy. Entropy ownership is checked here because
// only the reconstructed frame owner knows whether that slot retained a completed CDF snapshot.
Av1ReferenceFrame? primaryReference = this.referenceFrames.Resolve(primaryReferenceSlot.Value);
if (primaryReference is null || primaryReference.EntropyContext is null)
{
throw new InvalidImageContentException("The AV1 primary reference has no retained entropy context.");
}
primaryReferenceContext = primaryReference.EntropyContext;
}
// Every tile group in a frame contributes to the same mode-info and coefficient state.
Av1FrameEntropyContexts entropyContexts =
this.entropyContexts ??= new(frameHeader.QuantizationParameters.BaseQIndex);
Av1TileReader? tileReader = null;
Av1FrameBuffer<byte>? frameBuffer = null;
// Presentation-only samples contain no new tile syntax, so they keep the most recently reconstructed
// frame state. Release that state only when a new reconstruction begins to avoid overlapping two graphs.
this.FrameInfo?.ReleaseOwner();
this.FrameInfo = null;
try
{
tileReader = new Av1TileReader(
this.configuration,
sequenceHeader,
frameHeader,
entropyContexts,
primaryReferenceContext,
this.referenceFrames,
this.lumaPaletteColorIndexMap,
this.chromaPaletteColorIndexMap);
frameBuffer = new Av1FrameBuffer<byte>(
this.configuration,
sequenceHeader,
sequenceHeader.ColorConfig.GetColorFormat(),
false,
frameHeader.FrameSize.SuperResolutionUpscaledWidth,
frameHeader.FrameSize.FrameHeight)
{
Width = frameHeader.FrameSize.FrameWidth,
Height = frameHeader.FrameSize.FrameHeight
};
// No preceding frame is active here. Release an undersized owner before renting its replacement;
// a failed rent leaves the session empty and retryable, while completed frames reuse this storage.
int workspaceLength = Av1BlockDecoder.GetWorkspaceLength(sequenceHeader);
if (this.reconstructionWorkspace is null || this.reconstructionWorkspace.Memory.Length < workspaceLength)
{
this.reconstructionWorkspace?.Dispose();
this.reconstructionWorkspace = null;
this.reconstructionWorkspace = this.configuration.MemoryAllocator.Allocate<short>(workspaceLength);
}
Av1FrameDecoder frameDecoder = new(
sequenceHeader,
frameHeader,
tileReader.FrameInfo,
frameBuffer,
this.referenceFrames,
this.reconstructionWorkspace.Memory[..workspaceLength],
new Av1TileReader.PaletteColorIndexMaps(
this.lumaPaletteColorIndexMap,
this.chromaPaletteColorIndexMap));
tileReader.FrameDecoder = frameDecoder;
frameDecodeState = new(tileReader, frameBuffer, frameDecoder);
this.frameDecodeState = frameDecodeState;
}
catch
{
frameBuffer?.Dispose();
tileReader?.Dispose();
throw;
}
}
else
{
frameDecodeState = this.frameDecodeState.Value;
}
frameDecodeState.TileReader.ReadTile(tileData, tileNum);
}
/// <summary>
/// Reconstructs a frame after all of its tile payloads have been parsed.
/// </summary>
public void CompleteFrame()
{
ObuSequenceHeader sequenceHeader = this.obuReader.SequenceHeader
?? throw new InvalidImageContentException("An AV1 frame cannot complete before its sequence header.");
ObuFrameHeader frameHeader = this.obuReader.FrameHeader
?? throw new InvalidImageContentException("An AV1 frame cannot complete before its frame header.");
Av1FrameBuffer<byte>? frameBuffer = null;
Av1FrameDecoder? frameDecoder = null;
Av1TileReader? tileReader = null;
Av1FrameBuffer<byte>? presentationBuffer = null;
try
{
this.ValidateSequence(sequenceHeader);
if (frameHeader.ShowExistingFrame)
{
Av1ReferenceFrame existingFrame = this.referenceFrames.ShowExisting((int)frameHeader.FrameToShowMapIdx);
ObuFrameHeader existingFrameHeader = existingFrame.FrameHeader;
if (existingFrameHeader.FrameType == ObuFrameType.KeyFrame)
{
// Both the decoder working context and the context retained by the newly aliased key frame reset
// frame. Later primary-reference selection must therefore observe normative defaults.
existingFrame.ResetEntropyContext();
this.entropyContexts?.Reset();
}
if (existingFrameHeader.FilmGrainParameters.ApplyGrain)
{
presentationBuffer = Av1FrameBuffer<byte>.CreatePresentation(this.configuration, sequenceHeader, existingFrame.FrameBuffer);
// Retained reference samples remain ungrained. Existing-frame presentation receives its own
// allocator-owned copy only when the inherited film-grain parameters actually modify the output.
existingFrame.FrameBuffer.CopyVisibleTo(presentationBuffer);
Av1FilmGrainDecoder filmGrainDecoder = new(sequenceHeader, existingFrameHeader, presentationBuffer);
filmGrainDecoder.DecodeFrame();
Av1ReferenceFrame presentationFrame = new(presentationBuffer, existingFrameHeader);
presentationBuffer = null;
this.referenceFrames.CommitOutput(presentationFrame);
}
this.SequenceHeader = sequenceHeader;
this.FrameHeader = existingFrameHeader;
return;
}
FrameDecodeState? activeFrameDecodeState = this.frameDecodeState;
if (activeFrameDecodeState is null)
{
throw new InvalidImageContentException("The AV1 frame completed without tile syntax.");
}
this.frameDecodeState = null;
FrameDecodeState activeFrame = activeFrameDecodeState.Value;
frameBuffer = activeFrame.FrameBuffer;
frameDecoder = activeFrame.FrameDecoder;
tileReader = activeFrame.TileReader;
Av1FrameInfo frameInfo = tileReader.FrameInfo;
Av1FrameBuffer<byte> reconstructedFrameBuffer = frameBuffer;
frameDecoder.CompleteFrame(this.cdefDecoder, this.restorationBoundary, this.restorationDecoder);
bool retainsReference = (frameHeader.RefreshFrameFlags & byte.MaxValue) != 0;
if (retainsReference)
{
// Motion compensation may address any clamped position inside the decoder border. Extending once after
// all in-loop filters lets every later block use the full padded span without per-prediction edge copies.
Av1ReferenceFrameBorder.Extend(reconstructedFrameBuffer);
// Detach only the state libaom retains on RefCntBuffer before any later ownership transfer can fail.
// The full reconstruction graph remains local to the current result and expires independently.
frameInfo.PrepareReferenceState();
}
bool needsSeparatePresentation = frameHeader.ShowFrame && frameHeader.FilmGrainParameters.ApplyGrain && retainsReference;
if (needsSeparatePresentation)
{
presentationBuffer = Av1FrameBuffer<byte>.CreatePresentation(this.configuration, sequenceHeader, reconstructedFrameBuffer);
// Film grain must never contaminate a decoded reference. A shown frame that is also refreshed therefore
// receives one allocator-owned presentation copy; frames with no reference role are grained in place.
reconstructedFrameBuffer.CopyVisibleTo(presentationBuffer);
}
Av1FrameBuffer<byte> grainTarget = presentationBuffer ?? reconstructedFrameBuffer;
if (frameHeader.ShowFrame && frameHeader.FilmGrainParameters.ApplyGrain)
{
Av1FilmGrainDecoder filmGrainDecoder = new(sequenceHeader, frameHeader, grainTarget);
filmGrainDecoder.DecodeFrame();
}
Av1ReferenceFrame referenceFrame;
if (retainsReference)
{
Av1FrameEntropyContexts entropyContexts = tileReader.EntropyContexts;
Av1FrameEntropyContext entropySnapshot = entropyContexts.RentPublishedSnapshot();
try
{
referenceFrame = new(reconstructedFrameBuffer, frameHeader, frameInfo, entropySnapshot, entropyContexts);
}
catch
{
// The snapshot rent precedes the reference owner. Return it if object construction cannot accept it.
entropyContexts.ReturnSnapshot(entropySnapshot);
throw;
}
}
else
{
// Presentation-only frames can never become primary references, so they own no unused CDF graph.
referenceFrame = new(reconstructedFrameBuffer, frameHeader);
}
frameBuffer = null;
if (!this.referenceFrames.Commit(frameHeader.RefreshFrameFlags, referenceFrame, frameHeader.ShowFrame && !needsSeparatePresentation))
{
referenceFrame.Dispose();
}
if (presentationBuffer is not null)
{
Av1ReferenceFrame presentationFrame = new(presentationBuffer, frameHeader);
presentationBuffer = null;
this.referenceFrames.CommitOutput(presentationFrame);
}
this.SequenceHeader = sequenceHeader;
this.FrameHeader = frameHeader;
this.DecodedInterPredictionFeatures |= frameInfo.InterPredictionFeatures;
// Hidden frames can contain the inter syntax needed to validate a sequence. Retain only the latest full
// reconstruction state until the next bounded decode; reference-map entries keep their compact state.
frameInfo.AddOwner();
this.FrameInfo?.ReleaseOwner();
this.FrameInfo = frameInfo;
}
finally
{
// A non-shown frame or failed reconstruction never escapes this callback. The tile reader releases the
// reconstruction lease; a retained frame keeps only its compact reference state after neighbor contexts
// and the remaining frame-sized syntax are returned.
presentationBuffer?.Dispose();
frameBuffer?.Dispose();
tileReader?.Dispose();
this.frameDecodeState?.Dispose();
this.frameDecodeState = null;
}
}
/// <summary>
/// Releases the current tile parser, reference map, and retained presentation output.
/// </summary>
public void Dispose()
{
this.frameDecodeState?.Dispose();
this.frameDecodeState = null;
this.referenceFrames.Dispose();
this.reconstructionWorkspace?.Dispose();
this.reconstructionWorkspace = null;
this.cdefDecoder.Dispose();
this.restorationBoundary.Dispose();
this.restorationDecoder.Dispose();
this.FrameInfo?.ReleaseOwner();
this.FrameInfo = null;
this.lumaPaletteColorIndexMap.Dispose();
this.chromaPaletteColorIndexMap.Dispose();
this.paletteColorIndexMapOwner.Dispose();
}
/// <summary>
/// Carries the active frame resources as one valid state so no partially initialized combination can be observed.
/// </summary>
private readonly struct FrameDecodeState(
Av1TileReader tileReader,
Av1FrameBuffer<byte> frameBuffer,
Av1FrameDecoder frameDecoder) : IDisposable
{
/// <summary>
/// Gets the tile parser shared by all tile groups in the frame.
/// </summary>
public Av1TileReader TileReader { get; } = tileReader;
/// <summary>
/// Gets the destination sample buffer reconstructed by the frame pipeline.
/// </summary>
public Av1FrameBuffer<byte> FrameBuffer { get; } = frameBuffer;
/// <summary>
/// Gets the reconstruction pipeline for the frame.
/// </summary>
public Av1FrameDecoder FrameDecoder { get; } = frameDecoder;
/// <summary>
/// Releases every resource when ownership has not transferred to a completed frame.
/// </summary>
public void Dispose()
{
this.FrameBuffer.Dispose();
this.TileReader.Dispose();
}
}
}

817
src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs

@ -0,0 +1,817 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Owns the padded luma and chroma sample planes for one decoded AV1 frame.
/// </summary>
/// <typeparam name="T">The unmanaged storage-element type used by the plane allocations.</typeparam>
internal sealed class Av1FrameBuffer<T> : IDisposable
where T : unmanaged
{
/// <summary>
/// The number of luma border samples reserved for prediction and in-loop filtering.
/// </summary>
// Scaled prediction may start 284 luma samples outside a retained frame and then consume three preceding filter
// taps. The normative 288-sample border keeps that entire source window directly addressable without block copies.
public const int DecoderPaddingValue = 288;
/// <summary>
/// The number of <typeparamref name="T"/> elements occupied by one logical sample.
/// </summary>
private int storageElementsPerSample;
/// <summary>
/// The complete plane ownership state, or <see langword="null"/> after disposal.
/// </summary>
private FramePlanes? planes;
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameBuffer{T}"/> class at sequence-maximum dimensions.
/// </summary>
/// <param name="configuration">The configuration providing the plane allocator.</param>
/// <param name="sequenceHeader">The sequence header defining maximum dimensions, bit depth, and chroma layout.</param>
/// <param name="maxColorFormat">The maximum color format to allocate for a non-monochrome sequence.</param>
/// <param name="is16BitPipeline">Indicates whether reconstruction uses native 16-bit sample storage.</param>
/// <exception cref="InvalidImageContentException">The padded frame planes cannot be represented as contiguous allocations.</exception>
public Av1FrameBuffer(Configuration configuration, ObuSequenceHeader sequenceHeader, Av1ColorFormat maxColorFormat, bool is16BitPipeline)
: this(
configuration,
sequenceHeader,
maxColorFormat,
is16BitPipeline,
sequenceHeader.MaxFrameWidth,
sequenceHeader.MaxFrameHeight)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameBuffer{T}"/> class for one active frame allocation.
/// </summary>
/// <param name="configuration">The configuration providing the plane allocator.</param>
/// <param name="sequenceHeader">The sequence header defining bit depth and chroma layout.</param>
/// <param name="maxColorFormat">The color format to allocate for a non-monochrome sequence.</param>
/// <param name="is16BitPipeline">Indicates whether reconstruction uses native 16-bit sample storage.</param>
/// <param name="allocationWidth">The padded plane's active luma width before decoder borders.</param>
/// <param name="allocationHeight">The padded plane's active luma height before decoder borders.</param>
/// <exception cref="InvalidImageContentException">The padded frame planes cannot be represented as contiguous allocations.</exception>
public Av1FrameBuffer(
Configuration configuration,
ObuSequenceHeader sequenceHeader,
Av1ColorFormat maxColorFormat,
bool is16BitPipeline,
int allocationWidth,
int allocationHeight)
: this(
configuration.MemoryAllocator,
sequenceHeader,
maxColorFormat,
is16BitPipeline,
allocationWidth,
allocationHeight,
FrameBufferKind.Reconstruction)
{
}
private Av1FrameBuffer(
MemoryAllocator allocator,
ObuSequenceHeader sequenceHeader,
Av1ColorFormat maxColorFormat,
bool is16BitPipeline,
int allocationWidth,
int allocationHeight,
FrameBufferKind kind)
{
ValidateDimensions(sequenceHeader, maxColorFormat, is16BitPipeline, allocationWidth, allocationHeight);
this.MemoryAllocator = allocator;
Av1ColorFormat colorFormat = sequenceHeader.ColorConfig.IsMonochrome ? Av1ColorFormat.Yuv400 : maxColorFormat;
this.MaxWidth = allocationWidth;
this.MaxHeight = allocationHeight;
this.BitDepth = sequenceHeader.ColorConfig.BitDepth;
this.ColorConfig = sequenceHeader.ColorConfig;
this.BytesPerSample = this.BitDepth > Av1BitDepth.EightBit || is16BitPipeline ? 2 : 1;
this.storageElementsPerSample = Math.Max(
(this.BytesPerSample + Unsafe.SizeOf<T>() - 1) / Unsafe.SizeOf<T>(),
1);
this.ColorFormat = colorFormat;
this.Is16BitPipeline = is16BitPipeline;
int border = kind switch
{
FrameBufferKind.Presentation => 0,
FrameBufferKind.Restoration => 32,
_ => DecoderPaddingValue
};
this.StartPosition = new Point(border, border);
this.Width = this.MaxWidth;
this.Height = this.MaxHeight;
this.OriginX = border;
this.OriginY = border;
FrameBufferLayout layout = CreateFrameBufferLayout(
allocationWidth,
allocationHeight,
colorFormat,
this.storageElementsPerSample,
kind);
// One allocation owns every component plane. Restoration retains its capacity across frames;
// new storage starts cleared so unwritten alignment and border slots cannot expose pooled data.
AllocationOptions options = kind == FrameBufferKind.Restoration ? AllocationOptions.Clean : AllocationOptions.None;
IMemoryOwner<T> owner = allocator.Allocate<T>(layout.StorageLength, options);
this.planes = WrapPlanes(owner, layout, colorFormat);
}
/// <summary>
/// Selects the border and alignment required by the frame's use.
/// </summary>
private enum FrameBufferKind
{
Reconstruction,
Presentation,
Restoration
}
/// <summary>
/// Gets the padded luma-coordinate origin of the visible frame.
/// </summary>
public Point StartPosition { get; private set; }
/// <summary>
/// Gets the Y luma buffer.
/// </summary>
public Buffer2D<T>? BufferY => this.planes?.Luma;
/// <summary>
/// Gets the U chroma buffer.
/// </summary>
public Buffer2D<T>? BufferCb => this.planes?.Chroma?.Blue;
/// <summary>
/// Gets the V chroma buffer.
/// </summary>
public Buffer2D<T>? BufferCr => this.planes?.Chroma?.Red;
/// <summary>
/// Gets or sets the horizontal padding distance.
/// </summary>
public int OriginX { get; set; }
/// <summary>
/// Gets or sets the vertical padding distance.
/// </summary>
public int OriginY { get; set; }
/// <summary>
/// Gets or sets the luma picture width, excluding padding.
/// </summary>
public int Width { get; set; }
/// <summary>
/// Gets or sets the luma picture height, excluding padding.
/// </summary>
public int Height { get; set; }
/// <summary>
/// Gets or sets the maximum luma picture width.
/// </summary>
public int MaxWidth { get; set; }
/// <summary>
/// Gets or sets the pixel bit depth.
/// </summary>
public Av1BitDepth BitDepth { get; set; }
/// <summary>
/// Gets the number of bytes used to store each reconstructed sample.
/// </summary>
public int BytesPerSample { get; private set; }
/// <summary>
/// Gets the color configuration signaled by the AV1 sequence header.
/// </summary>
public ObuColorConfig ColorConfig { get; private set; }
/// <summary>
/// Gets or sets the luma and chroma plane sampling layout.
/// </summary>
public Av1ColorFormat ColorFormat { get; set; }
/// <summary>
/// Gets or sets the maximum luma picture height.
/// </summary>
public int MaxHeight { get; set; }
/// <summary>
/// Gets a value indicating whether reconstruction uses native 16-bit samples.
/// </summary>
public bool Is16BitPipeline { get; private set; }
/// <summary>
/// Gets the allocator used for frame-owned and frame-scoped working buffers.
/// </summary>
public MemoryAllocator MemoryAllocator { get; }
/// <summary>
/// Validates that the requested frame planes fit the decoder's contiguous ownership contract.
/// </summary>
/// <param name="sequenceHeader">The sequence header defining bit depth and chroma layout.</param>
/// <param name="maxColorFormat">The color format required by the frame.</param>
/// <param name="is16BitPipeline">Indicates whether reconstruction uses native 16-bit sample storage.</param>
/// <param name="allocationWidth">The active luma width required by the allocation.</param>
/// <param name="allocationHeight">The active luma height required by the allocation.</param>
public static void ValidateDimensions(
ObuSequenceHeader sequenceHeader,
Av1ColorFormat maxColorFormat,
bool is16BitPipeline,
int allocationWidth,
int allocationHeight)
{
int bytesPerSample = sequenceHeader.ColorConfig.BitDepth > Av1BitDepth.EightBit || is16BitPipeline ? 2 : 1;
int storageElementsPerSample = Math.Max(
(bytesPerSample + Unsafe.SizeOf<T>() - 1) / Unsafe.SizeOf<T>(),
1);
Av1ColorFormat colorFormat = sequenceHeader.ColorConfig.IsMonochrome ? Av1ColorFormat.Yuv400 : maxColorFormat;
_ = CreateFrameBufferLayout(
allocationWidth,
allocationHeight,
colorFormat,
storageElementsPerSample,
FrameBufferKind.Reconstruction);
}
/// <summary>
/// Creates an empty presentation frame for the source's visible picture.
/// </summary>
/// <param name="configuration">The configuration providing the plane allocator.</param>
/// <param name="sequenceHeader">The sequence describing the source samples.</param>
/// <param name="source">The frame whose visible dimensions and sample format are required.</param>
/// <returns>A frame ready to receive the source's visible samples.</returns>
public static Av1FrameBuffer<T> CreatePresentation(
Configuration configuration,
ObuSequenceHeader sequenceHeader,
Av1FrameBuffer<T> source)
=> new(
configuration.MemoryAllocator,
sequenceHeader,
source.ColorFormat,
source.Is16BitPipeline,
source.Width,
source.Height,
FrameBufferKind.Presentation);
/// <summary>
/// Creates a compact monochrome plane for a composed auxiliary image.
/// </summary>
/// <param name="configuration">The configuration providing sample storage.</param>
/// <param name="colorConfig">The sample precision and range of the auxiliary items.</param>
/// <param name="size">The complete auxiliary image extent.</param>
/// <returns>The compact writable auxiliary plane.</returns>
public static Av1FrameBuffer<T> CreateAuxiliary(
Configuration configuration,
ObuColorConfig colorConfig,
Size size)
=> new(
configuration.MemoryAllocator,
new ObuSequenceHeader { ColorConfig = colorConfig },
Av1ColorFormat.Yuv400,
colorConfig.BitDepth > Av1BitDepth.EightBit,
size.Width,
size.Height,
FrameBufferKind.Presentation);
/// <summary>
/// Creates an empty restoration frame with the source's visible dimensions and sample format.
/// </summary>
/// <param name="allocator">The allocator for the frame planes.</param>
/// <param name="sequenceHeader">The sequence describing the source samples.</param>
/// <param name="source">The reconstructed frame whose output geometry is required.</param>
/// <returns>A frame ready to receive restored samples.</returns>
public static Av1FrameBuffer<T> CreateRestoration(
MemoryAllocator allocator,
ObuSequenceHeader sequenceHeader,
Av1FrameBuffer<T> source)
=> new(allocator, sequenceHeader, source.ColorFormat, source.Is16BitPipeline, source.Width, source.Height, FrameBufferKind.Restoration);
/// <summary>
/// Prepares this restoration frame for the source's visible dimensions and sample format.
/// </summary>
/// <param name="sequenceHeader">The sequence describing the source samples.</param>
/// <param name="source">The reconstructed frame whose output geometry is required.</param>
public void ResizeRestoration(ObuSequenceHeader sequenceHeader, Av1FrameBuffer<T> source)
{
// The decoder owns this live restoration frame for its entire session. A different layout may
// need new views without needing a new allocation; capacity grows only when the new planes exceed it.
FramePlanes? retainedPlanes = this.planes;
if (retainedPlanes is null || this.Width != source.Width || this.Height != source.Height ||
this.ColorFormat != source.ColorFormat || this.BytesPerSample != source.BytesPerSample)
{
FrameBufferLayout layout = CreateFrameBufferLayout(
source.Width,
source.Height,
source.ColorFormat,
source.storageElementsPerSample,
FrameBufferKind.Restoration);
FramePlanes activePlanes = retainedPlanes.GetValueOrDefault();
if (retainedPlanes is null || activePlanes.Owner.Memory.Length < layout.StorageLength)
{
// Restoration needs none of the previous target's samples. Release its old allocation
// before growing so two complete output frames never overlap in memory. A failed rent
// leaves an empty target that session disposal or the next resize can handle.
this.Dispose();
IMemoryOwner<T> owner = this.MemoryAllocator.Allocate<T>(layout.StorageLength, AllocationOptions.Clean);
this.planes = WrapPlanes(owner, layout, source.ColorFormat);
}
else
{
this.planes = WrapPlanes(activePlanes.Owner, layout, source.ColorFormat);
activePlanes.DisposeViews();
}
}
this.Width = this.MaxWidth = source.Width;
this.Height = this.MaxHeight = source.Height;
this.BitDepth = source.BitDepth;
this.ColorConfig = sequenceHeader.ColorConfig;
this.ColorFormat = source.ColorFormat;
this.BytesPerSample = source.BytesPerSample;
this.storageElementsPerSample = source.storageElementsPerSample;
this.Is16BitPipeline = source.Is16BitPipeline;
}
/// <summary>
/// Gets the padded storage allocation for one component plane.
/// </summary>
/// <param name="plane">The requested component plane.</param>
/// <returns>The requested plane allocation.</returns>
public Buffer2D<T> GetPlaneBuffer(Av1Plane plane)
{
this.GetPlaneLayout(plane, 0, 0, out Buffer2D<T> buffer, out _, out _, out _, out _);
return buffer;
}
/// <summary>
/// Copies the visible sample planes and active picture geometry to another compatible frame buffer.
/// </summary>
/// <param name="destination">The frame buffer receiving the copied reconstruction.</param>
public void CopyVisibleTo(Av1FrameBuffer<T> destination)
{
// Origins, strides, and capacity belong to the destination allocation. Only the active picture extent
// transfers: a presentation copy may have no border and need much less storage than its source.
destination.Width = this.Width;
destination.Height = this.Height;
destination.BitDepth = this.BitDepth;
destination.ColorFormat = this.ColorFormat;
int planeCount = this.ColorFormat == Av1ColorFormat.Yuv400 ? 1 : 3;
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++)
{
Av1Plane plane = (Av1Plane)planeIndex;
int subX = plane != Av1Plane.Y && this.ColorFormat is Av1ColorFormat.Yuv420 or Av1ColorFormat.Yuv422 ? 1 : 0;
int subY = plane != Av1Plane.Y && this.ColorFormat == Av1ColorFormat.Yuv420 ? 1 : 0;
this.GetPlaneLayout(
plane,
subX,
subY,
out Buffer2D<T> sourceBuffer,
out int sourceOriginX,
out int sourceOriginY,
out int width,
out int height);
destination.GetPlaneLayout(
plane,
subX,
subY,
out Buffer2D<T> destinationBuffer,
out int destinationOriginX,
out int destinationOriginY,
out _,
out _);
int storageWidth = width * this.storageElementsPerSample;
int sourceStorageX = sourceOriginX * this.storageElementsPerSample;
int destinationStorageX = destinationOriginX * this.storageElementsPerSample;
// A film-grain presentation owns only the active picture. Grain synthesis creates its odd-edge
// extension before reading it, so copying reference borders or unused sequence-sized storage is waste.
for (int row = 0; row < height; row++)
{
sourceBuffer.DangerousGetRowSpan(sourceOriginY + row)
.Slice(sourceStorageX, storageWidth)
.CopyTo(destinationBuffer.DangerousGetRowSpan(destinationOriginY + row).Slice(destinationStorageX, storageWidth));
}
}
}
/// <summary>
/// Releases the owned luma and chroma plane allocations.
/// </summary>
public void Dispose()
{
FramePlanes? ownedPlanes = this.planes;
this.planes = null;
if (ownedPlanes is null)
{
return;
}
FramePlanes activePlanes = ownedPlanes.Value;
activePlanes.DisposeViews();
activePlanes.Owner.Dispose();
}
/// <summary>
/// Gets a storage-element span beginning one logical row before a block.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="locationInPixels">The block origin in plane samples.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <param name="stride">Receives the logical samples between adjacent rows.</param>
/// <returns>The span beginning one logical row before the block.</returns>
public Span<T> DeriveBlockPointer(Av1Plane plane, Point locationInPixels, int subX, int subY, out int stride)
{
this.GetPlaneLayout(
plane,
subX,
subY,
out Buffer2D<T> buffer,
out int originX,
out int originY,
out _,
out _);
int elementStride = buffer.Width;
stride = elementStride / this.storageElementsPerSample;
int blockOffset = (((originY + locationInPixels.Y) * stride) + originX + locationInPixels.X) *
this.storageElementsPerSample;
// Intra prediction addresses above neighbors relative to the destination span, so index zero is the previous row.
blockOffset -= elementStride;
Guard.MustBeGreaterThanOrEqualTo(blockOffset, 0, nameof(blockOffset));
return buffer.DangerousGetSingleSpan()[blockOffset..];
}
/// <summary>
/// Gets a native 16-bit sample span beginning one logical row before a block.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="locationInPixels">The block origin in plane samples.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <param name="stride">Receives the logical samples between adjacent rows.</param>
/// <returns>The 16-bit span beginning one logical row before the block.</returns>
public Span<short> DeriveBlockPointer16(Av1Plane plane, Point locationInPixels, int subX, int subY, out int stride)
{
this.GetPlaneLayout(
plane,
subX,
subY,
out Buffer2D<T> buffer,
out int originX,
out int originY,
out _,
out _);
stride = buffer.Width / this.storageElementsPerSample;
int blockOffset = ((originY + locationInPixels.Y - 1) * stride) + originX + locationInPixels.X;
Guard.MustBeGreaterThanOrEqualTo(blockOffset, 0, nameof(blockOffset));
// High-bit-depth reconstruction uses native 16-bit samples in the byte-backed frame planes.
return MemoryMarshal.Cast<T, short>(buffer.DangerousGetSingleSpan())[blockOffset..];
}
/// <summary>
/// Gets the visible sample region for one plane.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <returns>The plane region excluding decoder padding.</returns>
public Buffer2DRegion<T> DeriveBlockPointer(Av1Plane plane, int subX, int subY)
{
this.GetPlaneLayout(
plane,
subX,
subY,
out Buffer2D<T> buffer,
out int originX,
out int originY,
out int width,
out int height);
Rectangle region = new(
originX * this.storageElementsPerSample,
originY,
width * this.storageElementsPerSample,
height);
return new Buffer2DRegion<T>(buffer, region);
}
/// <summary>
/// Gets one visible row of native 16-bit samples from a plane.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="row">The zero-based visible row index.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <returns>The visible row without decoder padding.</returns>
public Span<ushort> GetHighBitDepthRowSpan(Av1Plane plane, int row, int subX, int subY)
{
this.GetPlaneLayout(
plane,
subX,
subY,
out Buffer2D<T> buffer,
out int originX,
out int originY,
out int width,
out _);
Span<ushort> samples = MemoryMarshal.Cast<T, ushort>(buffer.DangerousGetRowSpan(originY + row));
return samples.Slice(originX, width);
}
/// <summary>
/// Gets the complete padded storage allocation for one plane.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <param name="stride">Receives the number of logical samples between adjacent rows.</param>
/// <param name="origin">Receives the visible plane origin within the padded allocation.</param>
/// <returns>The complete plane allocation, including decoder padding.</returns>
public Span<T> GetPaddedPlaneSpan(Av1Plane plane, int subX, int subY, out int stride, out Point origin)
{
this.GetPlaneLayout(
plane,
subX,
subY,
out Buffer2D<T> buffer,
out int originX,
out int originY,
out _,
out _);
stride = buffer.Width / this.storageElementsPerSample;
origin = new(originX, originY);
return buffer.DangerousGetSingleSpan();
}
/// <summary>
/// Gets the complete padded storage allocation for one native 16-bit plane.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <param name="stride">Receives the number of logical samples between adjacent rows.</param>
/// <param name="origin">Receives the visible plane origin within the padded allocation.</param>
/// <returns>The complete plane allocation, including decoder padding.</returns>
public Span<ushort> GetPaddedPlaneSpan16(Av1Plane plane, int subX, int subY, out int stride, out Point origin)
=> MemoryMarshal.Cast<T, ushort>(this.GetPaddedPlaneSpan(plane, subX, subY, out stride, out origin));
/// <summary>
/// Resolves a plane allocation and its visible padded layout.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <param name="buffer">Receives the selected plane allocation.</param>
/// <param name="originX">Receives the horizontal visible origin in plane samples.</param>
/// <param name="originY">Receives the vertical visible origin in plane samples.</param>
/// <param name="width">Receives the visible plane width.</param>
/// <param name="height">Receives the visible plane height.</param>
private void GetPlaneLayout(
Av1Plane plane,
int subX,
int subY,
out Buffer2D<T> buffer,
out int originX,
out int originY,
out int width,
out int height)
{
FramePlanes? ownedPlanes = this.planes;
ObjectDisposedException.ThrowIf(ownedPlanes is null, this);
FramePlanes activePlanes = ownedPlanes.Value;
switch (plane)
{
case Av1Plane.Y:
buffer = activePlanes.Luma;
originX = this.OriginX;
originY = this.OriginY;
width = this.Width;
height = this.Height;
break;
case Av1Plane.U:
buffer = activePlanes.Chroma?.Blue
?? throw new InvalidOperationException("A monochrome AV1 frame has no blue-difference plane.");
originX = this.OriginX >> subX;
originY = this.OriginY >> subY;
width = Av1Math.DivideLog2Ceiling(this.Width, subX);
height = Av1Math.DivideLog2Ceiling(this.Height, subY);
break;
case Av1Plane.V:
default:
buffer = activePlanes.Chroma?.Red
?? throw new InvalidOperationException("A monochrome AV1 frame has no red-difference plane.");
originX = this.OriginX >> subX;
originY = this.OriginY >> subY;
width = Av1Math.DivideLog2Ceiling(this.Width, subX);
height = Av1Math.DivideLog2Ceiling(this.Height, subY);
break;
}
}
/// <summary>
/// Calculates the aligned physical plane layout retained by one frame owner.
/// </summary>
private static FrameBufferLayout CreateFrameBufferLayout(
int width,
int height,
Av1ColorFormat colorFormat,
int storageElementsPerSample,
FrameBufferKind kind)
{
// Reconstruction and restoration share eight-sample coded alignment but require different borders.
// Grain presentation extends only an odd final row/column and aligns rows in bytes at either bit depth.
bool isPresentation = kind == FrameBufferKind.Presentation;
long dimensionMask = isPresentation ? 1 : 7;
long border = kind switch
{
FrameBufferKind.Presentation => 0,
FrameBufferKind.Restoration => 32,
_ => DecoderPaddingValue
};
long rowAlignment = isPresentation ? Math.Max(16 / (storageElementsPerSample * Unsafe.SizeOf<T>()), 1) : 32;
long alignedWidth = (width + dimensionMask) & ~dimensionMask;
long alignedHeight = (height + dimensionMask) & ~dimensionMask;
long lumaStride = (alignedWidth + (2 * border) + rowAlignment - 1) & ~(rowAlignment - 1);
long lumaHeight = alignedHeight + (2 * border);
int subsamplingX = colorFormat is Av1ColorFormat.Yuv420 or Av1ColorFormat.Yuv422 ? 1 : 0;
int subsamplingY = colorFormat == Av1ColorFormat.Yuv420 ? 1 : 0;
long chromaStride = colorFormat == Av1ColorFormat.Yuv400 ? 0 : lumaStride >> subsamplingX;
long chromaHeight = colorFormat == Av1ColorFormat.Yuv400
? 0
: (alignedHeight >> subsamplingY) + (2 * (border >> subsamplingY));
long lumaStorageWidth = lumaStride * storageElementsPerSample;
long chromaStorageWidth = chromaStride * storageElementsPerSample;
long lumaElementCount = lumaStorageWidth * lumaHeight;
long chromaElementCount = chromaStorageWidth * chromaHeight;
long planeAlignment = isPresentation ? 1 : Math.Max(32 / Unsafe.SizeOf<T>(), 1);
long chromaBlueOffset = ((lumaElementCount + planeAlignment - 1) / planeAlignment) * planeAlignment;
long chromaRedOffset = ((chromaBlueOffset + chromaElementCount + planeAlignment - 1) / planeAlignment) * planeAlignment;
long storageLength = colorFormat == Av1ColorFormat.Yuv400
? lumaElementCount
: chromaRedOffset + chromaElementCount;
if (storageLength >= int.MaxValue)
{
// Reconstruction operators require one contiguous owner so every padded row remains directly addressable.
throw new InvalidImageContentException("The AV1 frame dimensions exceed the contiguous decoder frame limit.");
}
return new FrameBufferLayout(
(int)lumaStorageWidth,
(int)lumaHeight,
(int)lumaElementCount,
(int)chromaStorageWidth,
(int)chromaHeight,
(int)chromaElementCount,
(int)chromaBlueOffset,
(int)chromaRedOffset,
(int)storageLength);
}
/// <summary>
/// Builds non-owning row views over the complete frame allocation.
/// </summary>
private static FramePlanes WrapPlanes(IMemoryOwner<T> owner, FrameBufferLayout layout, Av1ColorFormat colorFormat)
{
Memory<T> storage = owner.Memory;
Buffer2D<T> luma = Buffer2D<T>.WrapMemory(
storage.Slice(0, layout.LumaElementCount),
layout.LumaStorageWidth,
layout.LumaHeight);
ChromaPlanes? chroma = null;
if (colorFormat != Av1ColorFormat.Yuv400)
{
Buffer2D<T> chromaBlue = Buffer2D<T>.WrapMemory(
storage.Slice(layout.ChromaBlueOffset, layout.ChromaElementCount),
layout.ChromaStorageWidth,
layout.ChromaHeight);
Buffer2D<T> chromaRed = Buffer2D<T>.WrapMemory(
storage.Slice(layout.ChromaRedOffset, layout.ChromaElementCount),
layout.ChromaStorageWidth,
layout.ChromaHeight);
chroma = new ChromaPlanes(chromaBlue, chromaRed);
}
return new(owner, luma, chroma);
}
/// <summary>
/// Carries the one frame owner, mandatory luma view, and optional complete chroma pair as one state.
/// </summary>
private readonly struct FramePlanes(IMemoryOwner<T> owner, Buffer2D<T> luma, ChromaPlanes? chroma)
{
/// <summary>
/// Gets the complete frame allocation.
/// </summary>
public IMemoryOwner<T> Owner { get; } = owner;
/// <summary>
/// Gets the padded luma plane.
/// </summary>
public Buffer2D<T> Luma { get; } = luma;
/// <summary>
/// Gets the padded chroma planes when the frame contains chroma.
/// </summary>
public ChromaPlanes? Chroma { get; } = chroma;
/// <summary>
/// Releases row views while leaving the complete allocation with its current owner.
/// </summary>
public void DisposeViews()
{
this.Luma.Dispose();
ChromaPlanes? chromaPlanes = this.Chroma;
if (chromaPlanes is not null)
{
chromaPlanes.Value.Blue.Dispose();
chromaPlanes.Value.Red.Dispose();
}
}
}
/// <summary>
/// Describes the physical storage slices used by the component-plane views.
/// </summary>
private readonly struct FrameBufferLayout(
int lumaStorageWidth,
int lumaHeight,
int lumaElementCount,
int chromaStorageWidth,
int chromaHeight,
int chromaElementCount,
int chromaBlueOffset,
int chromaRedOffset,
int storageLength)
{
public int LumaStorageWidth { get; } = lumaStorageWidth;
public int LumaHeight { get; } = lumaHeight;
public int LumaElementCount { get; } = lumaElementCount;
public int ChromaStorageWidth { get; } = chromaStorageWidth;
public int ChromaHeight { get; } = chromaHeight;
public int ChromaElementCount { get; } = chromaElementCount;
public int ChromaBlueOffset { get; } = chromaBlueOffset;
public int ChromaRedOffset { get; } = chromaRedOffset;
public int StorageLength { get; } = storageLength;
}
private readonly struct ChromaPlanes(Buffer2D<T> blue, Buffer2D<T> red)
{
/// <summary>
/// Gets the padded blue-difference plane.
/// </summary>
public Buffer2D<T> Blue { get; } = blue;
/// <summary>
/// Gets the padded red-difference plane.
/// </summary>
public Buffer2D<T> Red { get; } = red;
}
}

21
src/ImageSharp/Formats/Heif/Av1/Av1LayerSelector.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Identifies the AV1 spatial layer selected by an AVIF image item.
/// </summary>
/// <param name="layerId">The spatial-layer identifier, or <see cref="AllLayers"/> for progressive or final-layer decoding.</param>
internal readonly struct Av1LayerSelector(ushort layerId)
{
/// <summary>
/// The layer identifier that selects progressive exposure or the final layer rather than one specific spatial layer.
/// </summary>
public const ushort AllLayers = ushort.MaxValue;
/// <summary>
/// Gets the spatial-layer identifier, or <see cref="AllLayers"/> when no individual layer is selected.
/// </summary>
public ushort LayerId { get; } = layerId;
}

97
src/ImageSharp/Formats/Heif/Av1/Av1LayeredImageIndex.cs

@ -0,0 +1,97 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Describes the explicit payload sizes that delimit the first three layers of a layered AV1 image item.
/// </summary>
/// <param name="firstLayerSize">The first layer size in bytes.</param>
/// <param name="secondLayerSize">The second layer size in bytes.</param>
/// <param name="thirdLayerSize">The third layer size in bytes.</param>
internal readonly struct Av1LayeredImageIndex(uint firstLayerSize, uint secondLayerSize, uint thirdLayerSize)
{
/// <summary>
/// Gets the first layer size in bytes.
/// </summary>
public uint FirstLayerSize { get; } = firstLayerSize;
/// <summary>
/// Gets the second layer size in bytes.
/// </summary>
public uint SecondLayerSize { get; } = secondLayerSize;
/// <summary>
/// Gets the third layer size in bytes.
/// </summary>
public uint ThirdLayerSize { get; } = thirdLayerSize;
/// <summary>
/// Gets the number of item bytes needed to decode the selected spatial layer.
/// </summary>
/// <param name="itemSize">The complete logical image-item payload size.</param>
/// <param name="selector">The requested spatial layer, or <see langword="null"/> to decode the final layer.</param>
/// <returns>The cumulative payload size through the selected layer, or the complete item size for final-layer decoding.</returns>
public int GetPayloadLength(int itemSize, Av1LayerSelector? selector)
{
int selectedLayer = selector is null || selector.Value.LayerId == Av1LayerSelector.AllLayers
? -1
: selector.Value.LayerId;
uint remainingSize = (uint)itemSize;
uint selectedPayloadSize = 0;
int layerCount = 0;
for (int layer = 0; layer < Av1Constants.MaxSpatialLayerCount - 1; layer++)
{
uint layerSize = layer switch
{
0 => this.FirstLayerSize,
1 => this.SecondLayerSize,
_ => this.ThirdLayerSize
};
layerCount++;
if (layerSize == 0)
{
if (selectedLayer < 0 || selectedLayer == layer)
{
selectedPayloadSize += remainingSize;
}
remainingSize = 0;
break;
}
if (layerSize >= remainingSize)
{
// Every explicit layer must leave at least one byte for the final implicit layer. A zero entry instead
// identifies the current layer as final and consumes the complete remainder.
throw new InvalidImageContentException($"AV1 layered-image layer {layer} does not fit within the item payload.");
}
if (selectedLayer < 0 || layer <= selectedLayer)
{
selectedPayloadSize += layerSize;
}
remainingSize -= layerSize;
}
if (remainingSize != 0)
{
if (selectedLayer < 0 || selectedLayer == layerCount)
{
selectedPayloadSize += remainingSize;
}
layerCount++;
}
if (selectedLayer >= layerCount)
{
throw new InvalidImageContentException($"AV1 layer selector requests layer {selectedLayer}, but the item contains {layerCount} layers.");
}
return selectedLayer < 0 ? itemSize : (int)selectedPayloadSize;
}
}

309
src/ImageSharp/Formats/Heif/Av1/Av1Math.cs

@ -0,0 +1,309 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Provides the integer arithmetic primitives used by AV1 syntax and reconstruction.
/// </summary>
internal static class Av1Math
{
/// <summary>
/// Gets the zero-based position of the most significant set bit.
/// </summary>
/// <param name="value">A nonzero unsigned value.</param>
/// <returns>The most significant set-bit position.</returns>
public static int MostSignificantBit(uint value)
{
int log = 0;
int i;
Guard.IsTrue(value != 0, nameof(value), "Must have at least one bit set.");
for (i = 4; i >= 0; --i)
{
int shift = 1 << i;
uint x = value >> shift;
if (x != 0)
{
value = x;
log += shift;
}
}
return log;
}
/// <summary>
/// Gets the integer base-two logarithm of a positive value.
/// </summary>
/// <param name="n">The value.</param>
/// <returns>The zero-based position of the most significant set bit.</returns>
public static int Log2(int n)
{
int result = 0;
while ((n >>= 1) > 0)
{
result++;
}
return result;
}
/// <summary>
/// Gets the integer base-two logarithm of an unsigned 32-bit value.
/// </summary>
/// <param name="x">The value.</param>
/// <returns>The zero-based position of the most significant set bit.</returns>
public static uint Log2_32(uint x)
{
uint log = 0;
int i;
for (i = 4; i >= 0; --i)
{
uint shift = 1u << i;
uint n = x >> (int)shift;
if (n != 0)
{
x = n;
log += shift;
}
}
return log;
}
/// <summary>
/// Gets the greatest integer less than or equal to the base-two logarithm of a nonzero value.
/// </summary>
/// <param name="value">The nonzero value.</param>
/// <returns>The floor of the base-two logarithm.</returns>
public static uint FloorLog2(uint value)
{
uint s = 0;
while (value != 0U)
{
value >>= 1;
s++;
}
return s - 1;
}
/// <summary>
/// Gets the least integer greater than or equal to the base-two logarithm of a value.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>The ceiling of the base-two logarithm, or zero for values below two.</returns>
public static uint CeilLog2(uint value)
{
if (value < 2)
{
return 0;
}
uint i = 1;
uint p = 2;
while (p < value)
{
i++;
p <<= 1;
}
return i;
}
/// <summary>
/// Clips an unsigned sample to the range represented by a bit depth.
/// </summary>
/// <param name="value">The sample value.</param>
/// <param name="bitDepth">The number of sample bits.</param>
/// <returns>The clipped sample.</returns>
public static uint Clip1(uint value, int bitDepth) =>
Clip3(0, (1U << bitDepth) - 1, value);
/// <summary>
/// Clips an unsigned value to an inclusive range.
/// </summary>
/// <param name="min">The inclusive lower bound.</param>
/// <param name="max">The inclusive upper bound.</param>
/// <param name="value">The value to clip.</param>
/// <returns>The clipped value.</returns>
public static uint Clip3(uint min, uint max, uint value) => Math.Max(min, Math.Min(max, value));
/// <summary>
/// Clips a signed value to an inclusive range.
/// </summary>
/// <param name="min">The inclusive lower bound.</param>
/// <param name="max">The inclusive upper bound.</param>
/// <param name="value">The value to clip.</param>
/// <returns>The clipped value.</returns>
public static int Clip3(int min, int max, int value) => Math.Max(min, Math.Min(max, value));
/// <summary>
/// Divides an unsigned value by a power of two with nearest-integer rounding.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="n">The base-two divisor exponent.</param>
/// <returns>The rounded quotient.</returns>
public static uint Round2(uint value, int n)
{
if (n == 0)
{
return value;
}
return (uint)((value + (1 << (n - 1))) >> n);
}
/// <summary>
/// Divides the absolute magnitude of a signed value by a power of two with nearest-integer rounding.
/// </summary>
/// <param name="value">The signed value.</param>
/// <param name="n">The base-two divisor exponent.</param>
/// <returns>The rounded nonnegative magnitude.</returns>
public static int Round2(int value, int n)
{
if (value < 0)
{
value = -value;
}
return (int)Round2((uint)value, n);
}
/// <summary>
/// Aligns a value upward to a multiple of a power of two.
/// </summary>
/// <param name="value">The value to align.</param>
/// <param name="n">The base-two alignment exponent.</param>
/// <returns>The aligned value.</returns>
public static int AlignPowerOf2(int value, int n)
{
int mask = (1 << n) - 1;
return (value + mask) & ~mask;
}
/// <summary>
/// Divides a value by a power of two with nearest-integer rounding.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="n">The base-two divisor exponent.</param>
/// <returns>The rounded quotient.</returns>
public static int RoundPowerOf2(int value, int n) => (value + ((1 << n) >> 1)) >> n;
/// <summary>
/// Clamps a signed integer to an inclusive range.
/// </summary>
/// <param name="value">The value to clamp.</param>
/// <param name="low">The inclusive lower bound.</param>
/// <param name="high">The inclusive upper bound.</param>
/// <returns>The clamped value.</returns>
public static int Clamp(int value, int low, int high)
=> Math.Max(low, Math.Min(high, value));
/// <summary>
/// Clamps a signed long integer to an inclusive range.
/// </summary>
/// <param name="value">The value to clamp.</param>
/// <param name="low">The inclusive lower bound.</param>
/// <param name="high">The inclusive upper bound.</param>
/// <returns>The clamped value.</returns>
public static long Clamp(long value, long low, long high)
=> Math.Max(low, Math.Min(high, value));
/// <summary>
/// Divides a value by a power of two with floor rounding.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="n">The base-two divisor exponent.</param>
/// <returns>The floor-rounded quotient.</returns>
public static int DivideLog2Floor(int value, int n)
=> value >> n;
/// <summary>
/// Divides a nonnegative value by a power of two with ceiling rounding.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="n">The base-two divisor exponent.</param>
/// <returns>The ceiling-rounded quotient.</returns>
public static int DivideLog2Ceiling(int value, int n)
=> (value + (1 << n) - 1) >> n;
/// <summary>
/// Divides a value by a power of two with nearest-integer rounding.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="bitCount">The base-two divisor exponent.</param>
/// <returns>The rounded quotient.</returns>
public static int DivideRound(int value, int bitCount)
=> (value + (1 << (bitCount - 1))) >> bitCount;
/// <summary>
/// Gets the nonnegative remainder after division by eight.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>The low three bits of the value.</returns>
public static int Modulus8(int value) => value & 0x07;
/// <summary>
/// Divides a value by eight with floor rounding.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>The floor-rounded quotient.</returns>
public static int DivideBy8Floor(int value) => value >> 3;
/// <summary>
/// Divides a signed value by a power of two with symmetric nearest-integer rounding.
/// </summary>
/// <param name="value">The signed value.</param>
/// <param name="n">The base-two divisor exponent.</param>
/// <returns>The signed rounded quotient.</returns>
public static int RoundPowerOf2Signed(int value, int n)
=> (value < 0) ? -RoundPowerOf2(-value, n) : RoundPowerOf2(value, n);
/// <summary>
/// Right-shifts a long intermediate with nearest-integer rounding.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="bit">The positive shift count.</param>
/// <returns>The rounded signed result.</returns>
public static int RoundShift(long value, int bit)
{
DebugGuard.MustBeGreaterThanOrEqualTo(bit, 1, nameof(bit));
return (int)((value + (1L << (bit - 1))) >> bit);
}
/// <summary>
/// Evaluates logical implication from one Boolean condition to another.
/// </summary>
/// <param name="a">The antecedent.</param>
/// <param name="b">The consequent.</param>
/// <returns><see langword="false"/> only when <paramref name="a"/> is true and <paramref name="b"/> is false.</returns>
public static bool Implies(bool a, bool b) => !a || b;
/// <summary>
/// Gets one bit from an integer value.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="n">The zero-based bit position.</param>
/// <returns>Zero or one.</returns>
public static int GetBit(int value, int n)
=> (value & (1 << n)) >> n;
/// <summary>
/// Sets one bit in an integer value.
/// </summary>
/// <param name="endOfBlockExtra">The value to update.</param>
/// <param name="n">The zero-based bit position.</param>
public static void SetBit(ref int endOfBlockExtra, int n)
=> endOfBlockExtra |= 1 << n;
/// <summary>
/// Gets the absolute difference between two integers.
/// </summary>
/// <param name="a">The first value.</param>
/// <param name="b">The second value.</param>
/// <returns>The nonnegative absolute difference.</returns>
public static int AbsoluteDifference(int a, int b) => (a > b) ? a - b : b - a;
}

16
src/ImageSharp/Formats/Heif/Av1/Av1OperatingPointSelector.cs

@ -0,0 +1,16 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Identifies the AV1 sequence-header operating point selected by an AVIF image item.
/// </summary>
/// <param name="index">The zero-based operating-point index.</param>
internal readonly struct Av1OperatingPointSelector(byte index)
{
/// <summary>
/// Gets the zero-based operating-point index.
/// </summary>
public byte Index { get; } = index;
}

153
src/ImageSharp/Formats/Heif/Av1/Av1PartitionType.cs

@ -0,0 +1,153 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Identifies the subdivision pattern applied to an AV1 coding block.
/// </summary>
internal enum Av1PartitionType
{
/// <summary>
/// Not partitioned any further.
/// </summary>
/// <remarks>
/// <code>
/// ***
/// * *
/// ***
/// </code>
/// </remarks>
None = 0,
/// <summary>
/// Horizontally split in 2 partitions.
/// </summary>
/// <remarks>
/// <code>
/// ***
/// * *
/// ***
/// * *
/// ***
/// </code>
/// </remarks>
Horizontal = 1,
/// <summary>
/// Vertically split in 2 partitions.
/// </summary>
/// <remarks>
/// <code>
/// *****
/// * * *
/// *****
/// </code>
/// </remarks>
Vertical = 2,
/// <summary>
/// 4 equally sized partitions.
/// </summary>
/// <remarks>
/// <code>
/// *****
/// * * *
/// *****
/// * * *
/// *****
/// </code>
/// </remarks>
Split = 3,
/// <summary>
/// Horizontal split and the top partition is split again.
/// </summary>
/// <remarks>
/// <code>
/// *****
/// * * *
/// *****
/// * *
/// *****
/// </code>
/// </remarks>
HorizontalA = 4,
/// <summary>
/// Horizontal split and the bottom partition is split again.
/// </summary>
/// <remarks>
/// <code>
/// *****
/// * *
/// *****
/// * * *
/// *****
/// </code>
/// </remarks>
HorizontalB = 5,
/// <summary>
/// Vertical split and the left partition is split again.
/// </summary>
/// <remarks>
/// <code>
/// *****
/// * * *
/// *** *
/// * * *
/// *****
/// </code>
/// </remarks>
VerticalA = 6,
/// <summary>
/// Vertical split and the right partition is split again.
/// </summary>
/// <remarks>
/// <code>
/// *****
/// * * *
/// * ***
/// * * *
/// *****
/// </code>
/// </remarks>
VerticalB = 7,
/// <summary>
/// 4:1 horizontal partition.
/// </summary>
/// <remarks>
/// <code>
/// ***
/// * *
/// ***
/// * *
/// ***
/// * *
/// ***
/// * *
/// ***
/// </code>
/// </remarks>
Horizontal4 = 8,
/// <summary>
/// 4:1 vertical partition.
/// </summary>
/// <remarks>
/// <code>
/// *********
/// * * * * *
/// *********
/// </code>
/// </remarks>
Vertical4 = 9,
/// <summary>
/// Invalid value.
/// </summary>
Invalid = 255
}

116
src/ImageSharp/Formats/Heif/Av1/Av1PartitionTypeExtensions.cs

@ -0,0 +1,116 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Provides child-block geometry for AV1 partition types.
/// </summary>
internal static class Av1PartitionTypeExtensions
{
/// <summary>
/// Maps each partition type and parent block size to the size of its component blocks.
/// </summary>
private static readonly Av1BlockSize[][] PartitionSubSize = [
[
Av1BlockSize.Block4x4,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block128x128,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid
], [
Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x4,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block128x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid
], [
Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block4x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x128,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid
], [
Av1BlockSize.Block4x4,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block4x4,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid
], [
Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x4,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block128x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid
], [
Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x4,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block128x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid
], [
Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block4x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x128,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid
], [
Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block4x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x128,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid
], [
Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x4,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
], [
Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block4x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
]
];
/// <summary>
/// Gets the component block size produced by a partition operation.
/// </summary>
/// <param name="partition">The partition operation.</param>
/// <param name="blockSize">The parent block size.</param>
/// <returns>The component block size, or <see cref="Av1BlockSize.Invalid"/> when the partition is not permitted.</returns>
public static Av1BlockSize GetBlockSubSize(this Av1PartitionType partition, Av1BlockSize blockSize)
=> PartitionSubSize[(int)partition][(int)blockSize];
}

25
src/ImageSharp/Formats/Heif/Av1/Av1Plane.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Identifies an AV1 luma or chroma sample plane.
/// </summary>
internal enum Av1Plane : int
{
/// <summary>
/// The luma plane.
/// </summary>
Y = 0,
/// <summary>
/// The first chroma plane.
/// </summary>
U = 1,
/// <summary>
/// The second chroma plane.
/// </summary>
V = 2,
}

133
src/ImageSharp/Formats/Heif/Av1/Color/Av1PlanarSampleBuffer.cs

@ -0,0 +1,133 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Components;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color;
/// <summary>
/// Adapts reconstructed AV1 planes to the shared HEIF planar color pipeline.
/// </summary>
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
internal readonly struct Av1PlanarSampleBuffer<TSample> : IHeifPlanarSampleBuffer<TSample>
where TSample : unmanaged
{
/// <summary>
/// The reconstructed AV1 frame containing the component planes.
/// </summary>
private readonly Av1FrameBuffer<byte> frameBuffer;
/// <summary>
/// The visible luma plane in byte-backed storage.
/// </summary>
private readonly Buffer2DRegion<byte> luma;
/// <summary>
/// The visible blue-difference plane in byte-backed storage.
/// </summary>
private readonly Buffer2DRegion<byte> chromaBlue;
/// <summary>
/// The visible red-difference plane in byte-backed storage.
/// </summary>
private readonly Buffer2DRegion<byte> chromaRed;
/// <summary>
/// Initializes a new instance of the <see cref="Av1PlanarSampleBuffer{TSample}"/> struct.
/// </summary>
/// <param name="frameBuffer">The reconstructed AV1 frame.</param>
public Av1PlanarSampleBuffer(Av1FrameBuffer<byte> frameBuffer)
{
this.frameBuffer = frameBuffer;
this.luma = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0);
this.chromaBlue = this.IsMonochrome
? default
: frameBuffer.DeriveBlockPointer(Av1Plane.U, this.ChromaSubsamplingX, this.ChromaSubsamplingY);
this.chromaRed = this.IsMonochrome
? default
: frameBuffer.DeriveBlockPointer(Av1Plane.V, this.ChromaSubsamplingX, this.ChromaSubsamplingY);
}
/// <inheritdoc/>
public readonly int Width => this.frameBuffer.Width;
/// <inheritdoc/>
public readonly int Height => this.frameBuffer.Height;
/// <inheritdoc/>
public readonly int LumaBitDepth => this.frameBuffer.BitDepth.GetBitCount();
/// <inheritdoc/>
public readonly int ChromaBitDepth => this.frameBuffer.BitDepth.GetBitCount();
/// <inheritdoc/>
public readonly bool IsMonochrome => this.frameBuffer.ColorFormat == Av1ColorFormat.Yuv400;
/// <inheritdoc/>
public readonly int ChromaSubsamplingX => this.frameBuffer.ColorConfig.SubSamplingX ? 1 : 0;
/// <inheritdoc/>
public readonly int ChromaSubsamplingY => this.frameBuffer.ColorConfig.SubSamplingY ? 1 : 0;
/// <inheritdoc/>
public readonly int ChromaPositionX
{
get
{
if (this.ChromaSubsamplingX == 0)
{
return 0;
}
// AV1 4:2:2 chroma is centered horizontally. For 4:2:0, CSP_UNKNOWN is centered while the two
// explicitly positioned layouts are co-sited with the left luma sample.
bool isCentered = this.ChromaSubsamplingY == 0
|| this.frameBuffer.ColorConfig.ChromaSamplePosition == ObuChromoSamplePosition.Unknown;
return isCentered ? 1 : 0;
}
}
/// <inheritdoc/>
public readonly int ChromaPositionY
=> this.ChromaSubsamplingY != 0 && this.frameBuffer.ColorConfig.ChromaSamplePosition != ObuChromoSamplePosition.Colocated ? 1 : 0;
/// <inheritdoc/>
public Span<TSample> GetLumaRowSpan(int row)
{
if (typeof(TSample) == typeof(byte))
{
return MemoryMarshal.Cast<byte, TSample>(this.luma.DangerousGetRowSpan(row));
}
return MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0));
}
/// <inheritdoc/>
public Span<TSample> GetChromaBlueRowSpan(int row)
{
if (typeof(TSample) == typeof(byte))
{
return MemoryMarshal.Cast<byte, TSample>(this.chromaBlue.DangerousGetRowSpan(row));
}
return MemoryMarshal.Cast<ushort, TSample>(
this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, row, this.ChromaSubsamplingX, this.ChromaSubsamplingY));
}
/// <inheritdoc/>
public Span<TSample> GetChromaRedRowSpan(int row)
{
if (typeof(TSample) == typeof(byte))
{
return MemoryMarshal.Cast<byte, TSample>(this.chromaRed.DangerousGetRowSpan(row));
}
return MemoryMarshal.Cast<ushort, TSample>(
this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, row, this.ChromaSubsamplingX, this.ChromaSubsamplingY));
}
}

780
src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBuffer.cs

@ -0,0 +1,780 @@
// 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.Formats.Heif.Components;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color;
/// <summary>
/// Owns AV1 component planes scaled to an image item's presentation extent.
/// </summary>
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
/// <typeparam name="TBuffer">The reconstructed AV1 plane adapter.</typeparam>
internal sealed class Av1PresentationSampleBuffer<TSample, TBuffer> : IDisposable
where TSample : unmanaged
where TBuffer : struct, IHeifPlanarSampleBuffer<TSample>
{
/// <summary>
/// The allocator that owns the presentation planes and row workspace.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The complete set of owned presentation planes, or <see langword="null"/> after disposal.
/// </summary>
private PresentationPlanes? planes;
/// <summary>
/// Initializes a new instance of the <see cref="Av1PresentationSampleBuffer{TSample, TBuffer}"/> class.
/// </summary>
/// <param name="configuration">The configuration providing allocator-owned plane storage.</param>
/// <param name="source">The unscaled reconstructed AV1 planes.</param>
/// <param name="width">The presented luma width.</param>
/// <param name="height">The presented luma height.</param>
public Av1PresentationSampleBuffer(Configuration configuration, TBuffer source, int width, int height)
{
this.memoryAllocator = configuration.MemoryAllocator;
this.Width = width;
this.Height = height;
this.LumaBitDepth = source.LumaBitDepth;
this.ChromaBitDepth = source.ChromaBitDepth;
this.IsMonochrome = source.IsMonochrome;
this.ChromaSubsamplingX = source.ChromaSubsamplingX;
this.ChromaSubsamplingY = source.ChromaSubsamplingY;
this.ChromaPositionX = source.ChromaPositionX;
this.ChromaPositionY = source.ChromaPositionY;
int sourceChromaWidth = DivideCeiling(source.Width, 1 << source.ChromaSubsamplingX);
int sourceChromaHeight = DivideCeiling(source.Height, 1 << source.ChromaSubsamplingY);
int destinationChromaWidth = DivideCeiling(width, 1 << source.ChromaSubsamplingX);
int destinationChromaHeight = DivideCeiling(height, 1 << source.ChromaSubsamplingY);
Buffer2D<TSample>? luma = null;
Buffer2D<TSample>? chromaBlue = null;
Buffer2D<TSample>? chromaRed = null;
try
{
luma = this.memoryAllocator.Allocate2D<TSample>(width, height);
this.ScalePlane(source, Av1Plane.Y, source.Width, source.Height, luma);
ChromaPlanes? chroma = null;
if (!source.IsMonochrome)
{
chromaBlue = this.memoryAllocator.Allocate2D<TSample>(destinationChromaWidth, destinationChromaHeight);
this.ScalePlane(source, Av1Plane.U, sourceChromaWidth, sourceChromaHeight, chromaBlue);
chromaRed = this.memoryAllocator.Allocate2D<TSample>(destinationChromaWidth, destinationChromaHeight);
this.ScalePlane(source, Av1Plane.V, sourceChromaWidth, sourceChromaHeight, chromaRed);
chroma = new ChromaPlanes(chromaBlue, chromaRed);
}
// Publish ownership only after every required plane has been allocated and initialized.
this.planes = new PresentationPlanes(luma, chroma);
}
catch
{
luma?.Dispose();
chromaBlue?.Dispose();
chromaRed?.Dispose();
throw;
}
}
/// <summary>
/// Gets the presented luma width.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the presented luma height.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the luma sample bit depth.
/// </summary>
public int LumaBitDepth { get; }
/// <summary>
/// Gets the chroma sample bit depth.
/// </summary>
public int ChromaBitDepth { get; }
/// <summary>
/// Gets a value indicating whether only luma is present.
/// </summary>
public bool IsMonochrome { get; }
/// <summary>
/// Gets the horizontal chroma-subsampling shift.
/// </summary>
public int ChromaSubsamplingX { get; }
/// <summary>
/// Gets the vertical chroma-subsampling shift.
/// </summary>
public int ChromaSubsamplingY { get; }
/// <summary>
/// Gets the horizontal chroma position in half-luma-sample units.
/// </summary>
public int ChromaPositionX { get; }
/// <summary>
/// Gets the vertical chroma position in half-luma-sample units.
/// </summary>
public int ChromaPositionY { get; }
/// <summary>
/// Gets a borrowed adapter over the scaled planes.
/// </summary>
public Av1PresentationSampleBufferView<TSample, TBuffer> View => new(this);
/// <summary>
/// Releases the scaled planes.
/// </summary>
public void Dispose()
{
PresentationPlanes? planes = this.planes;
this.planes = null;
if (planes is null)
{
return;
}
planes.Value.Luma.Dispose();
ChromaPlanes? chroma = planes.Value.Chroma;
if (chroma is not null)
{
chroma.Value.Blue.Dispose();
chroma.Value.Red.Dispose();
}
}
/// <summary>
/// Gets one scaled component row.
/// </summary>
/// <param name="plane">The requested component plane.</param>
/// <param name="row">The zero-based plane row.</param>
/// <returns>The visible samples in the requested row.</returns>
public Span<TSample> GetRowSpan(Av1Plane plane, int row)
{
PresentationPlanes planes = this.planes
?? throw new ObjectDisposedException(nameof(Av1PresentationSampleBuffer<TSample, TBuffer>));
Buffer2D<TSample> buffer = plane switch
{
Av1Plane.Y => planes.Luma,
Av1Plane.U => planes.Chroma?.Blue
?? throw new InvalidOperationException("The AV1 presentation buffer has no blue-difference plane."),
_ => planes.Chroma?.Red
?? throw new InvalidOperationException("The AV1 presentation buffer has no red-difference plane.")
};
return buffer.DangerousGetRowSpan(row);
}
/// <summary>
/// Scales one component plane with the native integer filter used by pinned libavif's libyuv backend.
/// </summary>
/// <param name="source">The reconstructed component planes.</param>
/// <param name="plane">The component plane to scale.</param>
/// <param name="sourceWidth">The source plane width.</param>
/// <param name="sourceHeight">The source plane height.</param>
/// <param name="destination">The scaled destination plane.</param>
private void ScalePlane(
TBuffer source,
Av1Plane plane,
int sourceWidth,
int sourceHeight,
Buffer2D<TSample> destination)
{
int destinationWidth = destination.Width;
int destinationHeight = destination.Height;
if (sourceWidth == destinationWidth && sourceHeight == destinationHeight)
{
for (int y = 0; y < sourceHeight; y++)
{
GetSourceRow(source, plane, y)[..sourceWidth].CopyTo(destination.DangerousGetRowSpan(y));
}
return;
}
bool doublesWidth = (destinationWidth + 1) / 2 == sourceWidth;
bool doublesHeight = (destinationHeight + 1) / 2 == sourceHeight;
if (doublesWidth && doublesHeight)
{
ScaleUp2(source, plane, sourceWidth, sourceHeight, destination);
return;
}
if (doublesWidth && sourceHeight == destinationHeight)
{
for (int y = 0; y < sourceHeight; y++)
{
ScaleRowUp2Linear(
GetSourceRow(source, plane, y)[..sourceWidth],
destination.DangerousGetRowSpan(y));
}
return;
}
if (sourceHeight == destinationHeight)
{
int rowHorizontalStep = sourceWidth > 1 && destinationWidth > 1
? FixedDivideOne(sourceWidth, destinationWidth)
: 0;
for (int y = 0; y < sourceHeight; y++)
{
ScaleHorizontal(
GetSourceRow(source, plane, y)[..sourceWidth],
destination.DangerousGetRowSpan(y),
rowHorizontalStep);
}
return;
}
// Layer selection presents a lower spatial layer at the full item extent, so both dimensions are monotonic.
// The general libyuv path maps destination centers in 16.16 fixed point and retains only two horizontally
// filtered rows. This avoids a second full-plane intermediate and remains group-safe under small allocators.
using Buffer2D<TSample> horizontalRows = this.memoryAllocator.Allocate2D<TSample>(destinationWidth, 2);
int horizontalStep = sourceWidth > 1 && destinationWidth > 1
? FixedDivideOne(sourceWidth, destinationWidth)
: 0;
int verticalStep = sourceHeight > 1 && destinationHeight > 1
? FixedDivideOne(sourceHeight, destinationHeight)
: 0;
int sourcePositionY = 0;
int firstSourceRow = -1;
int secondSourceRow = -1;
int firstSlot = 0;
int secondSlot = 1;
for (int y = 0; y < destinationHeight; y++)
{
int sourceRow = sourcePositionY >> 16;
int nextSourceRow = Math.Min(sourceRow + 1, sourceHeight - 1);
if (sourceRow == secondSourceRow)
{
(firstSourceRow, secondSourceRow) = (secondSourceRow, firstSourceRow);
(firstSlot, secondSlot) = (secondSlot, firstSlot);
}
if (firstSourceRow != sourceRow)
{
ScaleHorizontal(
GetSourceRow(source, plane, sourceRow)[..sourceWidth],
horizontalRows.DangerousGetRowSpan(firstSlot),
horizontalStep);
firstSourceRow = sourceRow;
}
if (secondSourceRow != nextSourceRow)
{
ScaleHorizontal(
GetSourceRow(source, plane, nextSourceRow)[..sourceWidth],
horizontalRows.DangerousGetRowSpan(secondSlot),
horizontalStep);
secondSourceRow = nextSourceRow;
}
int verticalFraction = (sourcePositionY >> 8) & 255;
InterpolateRows(
horizontalRows.DangerousGetRowSpan(firstSlot),
horizontalRows.DangerousGetRowSpan(secondSlot),
destination.DangerousGetRowSpan(y),
verticalFraction);
sourcePositionY += verticalStep;
}
}
/// <summary>
/// Applies libyuv's edge-aware two-times bilinear kernel to one complete plane.
/// </summary>
/// <param name="source">The reconstructed component planes.</param>
/// <param name="plane">The component plane to scale.</param>
/// <param name="sourceWidth">The source plane width.</param>
/// <param name="sourceHeight">The source plane height.</param>
/// <param name="destination">The scaled destination plane.</param>
private static void ScaleUp2(
TBuffer source,
Av1Plane plane,
int sourceWidth,
int sourceHeight,
Buffer2D<TSample> destination)
{
Span<TSample> firstSource = GetSourceRow(source, plane, 0)[..sourceWidth];
Span<TSample> firstDestination = destination.DangerousGetRowSpan(0);
ScaleRowUp2Bilinear(firstSource, firstSource, firstDestination, firstDestination);
int destinationRow = 1;
for (int y = 0; y < sourceHeight - 1; y++)
{
ScaleRowUp2Bilinear(
GetSourceRow(source, plane, y)[..sourceWidth],
GetSourceRow(source, plane, y + 1)[..sourceWidth],
destination.DangerousGetRowSpan(destinationRow),
destination.DangerousGetRowSpan(destinationRow + 1));
destinationRow += 2;
}
if ((destination.Height & 1) == 0)
{
Span<TSample> lastSource = GetSourceRow(source, plane, sourceHeight - 1)[..sourceWidth];
Span<TSample> lastDestination = destination.DangerousGetRowSpan(destination.Height - 1);
ScaleRowUp2Bilinear(lastSource, lastSource, lastDestination, lastDestination);
}
}
/// <summary>
/// Gets one visible source row without boxing the codec adapter.
/// </summary>
/// <param name="source">The reconstructed component planes.</param>
/// <param name="plane">The requested component plane.</param>
/// <param name="row">The zero-based plane row.</param>
/// <returns>The source row.</returns>
private static Span<TSample> GetSourceRow(TBuffer source, Av1Plane plane, int row)
=> plane switch
{
Av1Plane.Y => source.GetLumaRowSpan(row),
Av1Plane.U => source.GetChromaBlueRowSpan(row),
_ => source.GetChromaRedRowSpan(row)
};
/// <summary>
/// Applies the edge-aware two-times bilinear row kernel.
/// </summary>
/// <param name="topSource">The upper source row.</param>
/// <param name="bottomSource">The lower source row.</param>
/// <param name="topDestination">The upper destination row.</param>
/// <param name="bottomDestination">The lower destination row.</param>
private static void ScaleRowUp2Bilinear(
ReadOnlySpan<TSample> topSource,
ReadOnlySpan<TSample> bottomSource,
Span<TSample> topDestination,
Span<TSample> bottomDestination)
{
if (typeof(TSample) == typeof(byte))
{
ScaleRowUp2BilinearByte(
MemoryMarshal.Cast<TSample, byte>(topSource),
MemoryMarshal.Cast<TSample, byte>(bottomSource),
MemoryMarshal.Cast<TSample, byte>(topDestination),
MemoryMarshal.Cast<TSample, byte>(bottomDestination));
return;
}
ScaleRowUp2BilinearUInt16(
MemoryMarshal.Cast<TSample, ushort>(topSource),
MemoryMarshal.Cast<TSample, ushort>(bottomSource),
MemoryMarshal.Cast<TSample, ushort>(topDestination),
MemoryMarshal.Cast<TSample, ushort>(bottomDestination));
}
/// <summary>
/// Applies the byte two-times bilinear row kernel through portable 128-bit lanes and a scalar tail.
/// </summary>
/// <param name="topSource">The upper source row.</param>
/// <param name="bottomSource">The lower source row.</param>
/// <param name="topDestination">The upper destination row.</param>
/// <param name="bottomDestination">The lower destination row.</param>
private static void ScaleRowUp2BilinearByte(
ReadOnlySpan<byte> topSource,
ReadOnlySpan<byte> bottomSource,
Span<byte> topDestination,
Span<byte> bottomDestination)
{
int lastSource = topSource.Length - 1;
topDestination[0] = (byte)(((3 * topSource[0]) + bottomSource[0] + 2) >> 2);
bottomDestination[0] = (byte)((topSource[0] + (3 * bottomSource[0]) + 2) >> 2);
int x = 0;
if (Vector128.IsHardwareAccelerated)
{
ref byte topSourceBase = ref MemoryMarshal.GetReference(topSource);
ref byte bottomSourceBase = ref MemoryMarshal.GetReference(bottomSource);
ref byte topDestinationBase = ref MemoryMarshal.GetReference(topDestination);
ref byte bottomDestinationBase = ref MemoryMarshal.GetReference(bottomDestination);
for (; x + 8 <= lastSource; x += 8)
{
Vector128<ushort> top0 = LoadEightBytes(ref topSourceBase, x);
Vector128<ushort> top1 = LoadEightBytes(ref topSourceBase, x + 1);
Vector128<ushort> bottom0 = LoadEightBytes(ref bottomSourceBase, x);
Vector128<ushort> bottom1 = LoadEightBytes(ref bottomSourceBase, x + 1);
CalculateBilinearPairs(
top0,
top1,
bottom0,
bottom1,
out Vector128<ushort> upperEven,
out Vector128<ushort> upperOdd,
out Vector128<ushort> lowerEven,
out Vector128<ushort> lowerOdd);
StoreInterleavedBytes(upperEven, upperOdd, ref topDestinationBase, 1 + (2 * x));
StoreInterleavedBytes(lowerEven, lowerOdd, ref bottomDestinationBase, 1 + (2 * x));
}
}
for (; x < lastSource; x++)
{
int top0 = topSource[x];
int top1 = topSource[x + 1];
int bottom0 = bottomSource[x];
int bottom1 = bottomSource[x + 1];
int destination = 1 + (2 * x);
topDestination[destination] = (byte)(((9 * top0) + (3 * top1) + (3 * bottom0) + bottom1 + 8) >> 4);
topDestination[destination + 1] = (byte)(((3 * top0) + (9 * top1) + bottom0 + (3 * bottom1) + 8) >> 4);
bottomDestination[destination] = (byte)(((3 * top0) + top1 + (9 * bottom0) + (3 * bottom1) + 8) >> 4);
bottomDestination[destination + 1] = (byte)((top0 + (3 * top1) + (3 * bottom0) + (9 * bottom1) + 8) >> 4);
}
int lastDestination = topDestination.Length - 1;
topDestination[lastDestination] = (byte)(((3 * topSource[lastSource]) + bottomSource[lastSource] + 2) >> 2);
bottomDestination[lastDestination] = (byte)((topSource[lastSource] + (3 * bottomSource[lastSource]) + 2) >> 2);
}
/// <summary>
/// Applies the unsigned 16-bit two-times bilinear row kernel through portable 128-bit lanes and a scalar tail.
/// </summary>
/// <param name="topSource">The upper source row.</param>
/// <param name="bottomSource">The lower source row.</param>
/// <param name="topDestination">The upper destination row.</param>
/// <param name="bottomDestination">The lower destination row.</param>
private static void ScaleRowUp2BilinearUInt16(
ReadOnlySpan<ushort> topSource,
ReadOnlySpan<ushort> bottomSource,
Span<ushort> topDestination,
Span<ushort> bottomDestination)
{
int lastSource = topSource.Length - 1;
topDestination[0] = (ushort)(((3 * topSource[0]) + bottomSource[0] + 2) >> 2);
bottomDestination[0] = (ushort)((topSource[0] + (3 * bottomSource[0]) + 2) >> 2);
int x = 0;
if (Vector128.IsHardwareAccelerated)
{
ref ushort topSourceBase = ref MemoryMarshal.GetReference(topSource);
ref ushort bottomSourceBase = ref MemoryMarshal.GetReference(bottomSource);
ref ushort topDestinationBase = ref MemoryMarshal.GetReference(topDestination);
ref ushort bottomDestinationBase = ref MemoryMarshal.GetReference(bottomDestination);
nuint vectorCount = topSource[..lastSource].Vector128Count<ushort>();
for (; vectorCount > 0; vectorCount--, x += Vector128<ushort>.Count)
{
Vector128<ushort> top0 = Vector128.LoadUnsafe(ref topSourceBase, (nuint)x);
Vector128<ushort> top1 = Vector128.LoadUnsafe(ref topSourceBase, (nuint)(x + 1));
Vector128<ushort> bottom0 = Vector128.LoadUnsafe(ref bottomSourceBase, (nuint)x);
Vector128<ushort> bottom1 = Vector128.LoadUnsafe(ref bottomSourceBase, (nuint)(x + 1));
CalculateBilinearPairs(
top0,
top1,
bottom0,
bottom1,
out Vector128<ushort> upperEven,
out Vector128<ushort> upperOdd,
out Vector128<ushort> lowerEven,
out Vector128<ushort> lowerOdd);
StoreInterleavedUInt16(upperEven, upperOdd, ref topDestinationBase, 1 + (2 * x));
StoreInterleavedUInt16(lowerEven, lowerOdd, ref bottomDestinationBase, 1 + (2 * x));
}
}
for (; x < lastSource; x++)
{
int top0 = topSource[x];
int top1 = topSource[x + 1];
int bottom0 = bottomSource[x];
int bottom1 = bottomSource[x + 1];
int destination = 1 + (2 * x);
topDestination[destination] = (ushort)(((9 * top0) + (3 * top1) + (3 * bottom0) + bottom1 + 8) >> 4);
topDestination[destination + 1] = (ushort)(((3 * top0) + (9 * top1) + bottom0 + (3 * bottom1) + 8) >> 4);
bottomDestination[destination] = (ushort)(((3 * top0) + top1 + (9 * bottom0) + (3 * bottom1) + 8) >> 4);
bottomDestination[destination + 1] = (ushort)((top0 + (3 * top1) + (3 * bottom0) + (9 * bottom1) + 8) >> 4);
}
int lastDestination = topDestination.Length - 1;
topDestination[lastDestination] = (ushort)(((3 * topSource[lastSource]) + bottomSource[lastSource] + 2) >> 2);
bottomDestination[lastDestination] = (ushort)((topSource[lastSource] + (3 * bottomSource[lastSource]) + 2) >> 2);
}
/// <summary>
/// Calculates the four interleaved bilinear products for eight source positions.
/// </summary>
/// <param name="top0">The upper-left samples.</param>
/// <param name="top1">The upper-right samples.</param>
/// <param name="bottom0">The lower-left samples.</param>
/// <param name="bottom1">The lower-right samples.</param>
/// <param name="upperEven">Receives the upper left-biased samples.</param>
/// <param name="upperOdd">Receives the upper right-biased samples.</param>
/// <param name="lowerEven">Receives the lower left-biased samples.</param>
/// <param name="lowerOdd">Receives the lower right-biased samples.</param>
private static void CalculateBilinearPairs(
Vector128<ushort> top0,
Vector128<ushort> top1,
Vector128<ushort> bottom0,
Vector128<ushort> bottom1,
out Vector128<ushort> upperEven,
out Vector128<ushort> upperOdd,
out Vector128<ushort> lowerEven,
out Vector128<ushort> lowerOdd)
{
Vector128<ushort> rounding = Vector128.Create((ushort)8);
// The largest twelve-bit weighted sum is 16 * 4095 + 8, which remains within unsigned 16-bit lanes.
// Keeping eight independent source positions per vector therefore avoids widening and preserves libyuv's
// exact add-before-shift rounding for both byte and high-bit-depth presentation planes.
upperEven = (((top0 << 3) + top0) + ((top1 << 1) + top1) + ((bottom0 << 1) + bottom0) + bottom1 + rounding) >> 4;
upperOdd = (((top0 << 1) + top0) + ((top1 << 3) + top1) + bottom0 + ((bottom1 << 1) + bottom1) + rounding) >> 4;
lowerEven = (((top0 << 1) + top0) + top1 + ((bottom0 << 3) + bottom0) + ((bottom1 << 1) + bottom1) + rounding) >> 4;
lowerOdd = (top0 + ((top1 << 1) + top1) + ((bottom0 << 1) + bottom0) + ((bottom1 << 3) + bottom1) + rounding) >> 4;
}
/// <summary>
/// Loads eight byte samples as unsigned 16-bit lanes.
/// </summary>
/// <param name="source">The first source byte.</param>
/// <param name="offset">The byte offset.</param>
/// <returns>The widened samples.</returns>
private static Vector128<ushort> LoadEightBytes(ref byte source, int offset)
{
ulong packed = Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref source, offset));
return Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte());
}
/// <summary>
/// Interleaves and stores eight pairs of byte results.
/// </summary>
/// <param name="even">The left-biased results.</param>
/// <param name="odd">The right-biased results.</param>
/// <param name="destination">The first destination byte.</param>
/// <param name="offset">The destination byte offset.</param>
private static void StoreInterleavedBytes(
Vector128<ushort> even,
Vector128<ushort> odd,
ref byte destination,
int offset)
{
Vector128<ushort> lower = Vector128_.UnpackLow(even.AsInt16(), odd.AsInt16()).AsUInt16();
Vector128<ushort> upper = Vector128_.UnpackHigh(even.AsInt16(), odd.AsInt16()).AsUInt16();
Vector128.Narrow(lower, upper).StoreUnsafe(ref destination, (nuint)offset);
}
/// <summary>
/// Interleaves and stores eight pairs of unsigned 16-bit results.
/// </summary>
/// <param name="even">The left-biased results.</param>
/// <param name="odd">The right-biased results.</param>
/// <param name="destination">The first destination sample.</param>
/// <param name="offset">The destination sample offset.</param>
private static void StoreInterleavedUInt16(
Vector128<ushort> even,
Vector128<ushort> odd,
ref ushort destination,
int offset)
{
Vector128_.UnpackLow(even.AsInt16(), odd.AsInt16()).AsUInt16().StoreUnsafe(ref destination, (nuint)offset);
Vector128_.UnpackHigh(even.AsInt16(), odd.AsInt16()).AsUInt16().StoreUnsafe(
ref destination,
(nuint)(offset + Vector128<ushort>.Count));
}
/// <summary>
/// Applies libyuv's edge-aware horizontal two-times linear kernel.
/// </summary>
/// <param name="source">The source row.</param>
/// <param name="destination">The destination row.</param>
private static void ScaleRowUp2Linear(ReadOnlySpan<TSample> source, Span<TSample> destination)
{
if (typeof(TSample) == typeof(byte))
{
ScaleRowUp2LinearByte(
MemoryMarshal.Cast<TSample, byte>(source),
MemoryMarshal.Cast<TSample, byte>(destination));
return;
}
ScaleRowUp2LinearUInt16(
MemoryMarshal.Cast<TSample, ushort>(source),
MemoryMarshal.Cast<TSample, ushort>(destination));
}
/// <summary>
/// Applies the byte horizontal two-times linear kernel.
/// </summary>
/// <param name="source">The source row.</param>
/// <param name="destination">The destination row.</param>
private static void ScaleRowUp2LinearByte(ReadOnlySpan<byte> source, Span<byte> destination)
{
destination[0] = source[0];
for (int x = 0; x < source.Length - 1; x++)
{
int destinationX = 1 + (2 * x);
destination[destinationX] = (byte)(((3 * source[x]) + source[x + 1] + 2) >> 2);
destination[destinationX + 1] = (byte)((source[x] + (3 * source[x + 1]) + 2) >> 2);
}
destination[^1] = source[^1];
}
/// <summary>
/// Applies the unsigned 16-bit horizontal two-times linear kernel.
/// </summary>
/// <param name="source">The source row.</param>
/// <param name="destination">The destination row.</param>
private static void ScaleRowUp2LinearUInt16(ReadOnlySpan<ushort> source, Span<ushort> destination)
{
destination[0] = source[0];
for (int x = 0; x < source.Length - 1; x++)
{
int destinationX = 1 + (2 * x);
destination[destinationX] = (ushort)(((3 * source[x]) + source[x + 1] + 2) >> 2);
destination[destinationX + 1] = (ushort)((source[x] + (3 * source[x + 1]) + 2) >> 2);
}
destination[^1] = source[^1];
}
/// <summary>
/// Horizontally maps one source row with libyuv's 16.16 fixed-point bilinear positions.
/// </summary>
/// <param name="source">The source row.</param>
/// <param name="destination">The destination row.</param>
/// <param name="step">The 16.16 source-position increment.</param>
private static void ScaleHorizontal(ReadOnlySpan<TSample> source, Span<TSample> destination, int step)
{
if (source.Length == destination.Length)
{
source.CopyTo(destination);
return;
}
if (source.Length == 1)
{
destination.Fill(source[0]);
return;
}
int sourcePosition = 0;
if (typeof(TSample) == typeof(byte))
{
ReadOnlySpan<byte> sourceBytes = MemoryMarshal.Cast<TSample, byte>(source);
Span<byte> destinationBytes = MemoryMarshal.Cast<TSample, byte>(destination);
for (int x = 0; x < destinationBytes.Length; x++)
{
int sourceX = sourcePosition >> 16;
int fraction = (sourcePosition & 0xFFFF) >> 9;
int left = sourceBytes[sourceX];
int right = sourceBytes[sourceX + 1];
destinationBytes[x] = (byte)(left + (((fraction * (right - left)) + 0x40) >> 7));
sourcePosition += step;
}
return;
}
ReadOnlySpan<ushort> sourceWords = MemoryMarshal.Cast<TSample, ushort>(source);
Span<ushort> destinationWords = MemoryMarshal.Cast<TSample, ushort>(destination);
for (int x = 0; x < destinationWords.Length; x++)
{
int sourceX = sourcePosition >> 16;
int fraction = sourcePosition & 0xFFFF;
int left = sourceWords[sourceX];
int right = sourceWords[sourceX + 1];
destinationWords[x] = (ushort)(left + ((((long)fraction * (right - left)) + 0x8000) >> 16));
sourcePosition += step;
}
}
/// <summary>
/// Vertically interpolates two horizontally scaled rows.
/// </summary>
/// <param name="top">The upper row.</param>
/// <param name="bottom">The lower row.</param>
/// <param name="destination">The destination row.</param>
/// <param name="bottomWeight">The lower-row weight with a denominator of 256.</param>
private static void InterpolateRows(
ReadOnlySpan<TSample> top,
ReadOnlySpan<TSample> bottom,
Span<TSample> destination,
int bottomWeight)
{
if (bottomWeight == 0)
{
top.CopyTo(destination);
return;
}
int topWeight = 256 - bottomWeight;
if (typeof(TSample) == typeof(byte))
{
ReadOnlySpan<byte> topBytes = MemoryMarshal.Cast<TSample, byte>(top);
ReadOnlySpan<byte> bottomBytes = MemoryMarshal.Cast<TSample, byte>(bottom);
Span<byte> destinationBytes = MemoryMarshal.Cast<TSample, byte>(destination);
for (int x = 0; x < destinationBytes.Length; x++)
{
destinationBytes[x] = (byte)(((topBytes[x] * topWeight) + (bottomBytes[x] * bottomWeight) + 128) >> 8);
}
return;
}
ReadOnlySpan<ushort> topWords = MemoryMarshal.Cast<TSample, ushort>(top);
ReadOnlySpan<ushort> bottomWords = MemoryMarshal.Cast<TSample, ushort>(bottom);
Span<ushort> destinationWords = MemoryMarshal.Cast<TSample, ushort>(destination);
for (int x = 0; x < destinationWords.Length; x++)
{
destinationWords[x] = (ushort)(((topWords[x] * topWeight) + (bottomWords[x] * bottomWeight) + 128) >> 8);
}
}
/// <summary>
/// Divides two decremented lengths into libyuv's 16.16 endpoint-preserving step.
/// </summary>
/// <param name="sourceLength">The source length.</param>
/// <param name="destinationLength">The destination length.</param>
/// <returns>The 16.16 source-position increment.</returns>
private static int FixedDivideOne(int sourceLength, int destinationLength)
=> (int)((((long)sourceLength << 16) - 0x00010001) / (destinationLength - 1));
/// <summary>
/// Divides a positive value by a positive divisor with ceiling rounding.
/// </summary>
/// <param name="value">The value to divide.</param>
/// <param name="divisor">The positive divisor.</param>
/// <returns>The ceiling-rounded quotient.</returns>
private static int DivideCeiling(int value, int divisor) => (value + divisor - 1) / divisor;
private readonly struct ChromaPlanes(Buffer2D<TSample> blue, Buffer2D<TSample> red)
{
public Buffer2D<TSample> Blue { get; } = blue;
public Buffer2D<TSample> Red { get; } = red;
}
private readonly struct PresentationPlanes(Buffer2D<TSample> luma, ChromaPlanes? chroma)
{
public Buffer2D<TSample> Luma { get; } = luma;
public ChromaPlanes? Chroma { get; } = chroma;
}
}

63
src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBufferView.cs

@ -0,0 +1,63 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Components;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color;
/// <summary>
/// Exposes borrowed rows from an owned AV1 presentation buffer to the shared HEIF color converter.
/// </summary>
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
/// <typeparam name="TBuffer">The reconstructed AV1 plane adapter owned by the presentation buffer.</typeparam>
internal readonly struct Av1PresentationSampleBufferView<TSample, TBuffer> : IHeifPlanarSampleBuffer<TSample>
where TSample : unmanaged
where TBuffer : struct, IHeifPlanarSampleBuffer<TSample>
{
/// <summary>
/// The owner that keeps all exposed rows alive.
/// </summary>
private readonly Av1PresentationSampleBuffer<TSample, TBuffer> owner;
/// <summary>
/// Initializes a new instance of the <see cref="Av1PresentationSampleBufferView{TSample, TBuffer}"/> struct.
/// </summary>
/// <param name="owner">The scaled plane owner.</param>
public Av1PresentationSampleBufferView(Av1PresentationSampleBuffer<TSample, TBuffer> owner) => this.owner = owner;
/// <inheritdoc/>
public int Width => this.owner.Width;
/// <inheritdoc/>
public int Height => this.owner.Height;
/// <inheritdoc/>
public int LumaBitDepth => this.owner.LumaBitDepth;
/// <inheritdoc/>
public int ChromaBitDepth => this.owner.ChromaBitDepth;
/// <inheritdoc/>
public bool IsMonochrome => this.owner.IsMonochrome;
/// <inheritdoc/>
public int ChromaSubsamplingX => this.owner.ChromaSubsamplingX;
/// <inheritdoc/>
public int ChromaSubsamplingY => this.owner.ChromaSubsamplingY;
/// <inheritdoc/>
public int ChromaPositionX => this.owner.ChromaPositionX;
/// <inheritdoc/>
public int ChromaPositionY => this.owner.ChromaPositionY;
/// <inheritdoc/>
public Span<TSample> GetLumaRowSpan(int row) => this.owner.GetRowSpan(Av1Plane.Y, row);
/// <inheritdoc/>
public Span<TSample> GetChromaBlueRowSpan(int row) => this.owner.GetRowSpan(Av1Plane.U, row);
/// <inheritdoc/>
public Span<TSample> GetChromaRedRowSpan(int row) => this.owner.GetRowSpan(Av1Plane.V, row);
}

299
src/ImageSharp/Formats/Heif/Av1/Color/Av1YuvConverter.cs

@ -0,0 +1,299 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Components;
using SixLabors.ImageSharp.Formats.Heif.Components.Alpha;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color;
/// <summary>
/// Adapts AV1 color signaling and reconstructed planes to the shared HEIF color pipeline.
/// </summary>
internal static class Av1YuvConverter
{
/// <summary>
/// Converts a rectangular region of reconstructed component planes directly to packed pixels.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
/// <param name="frameBuffer">The reconstructed AV1 frame.</param>
/// <param name="sourceRectangle">The luma-sample region mapped to the complete destination frame.</param>
/// <param name="destination">The destination pixel region.</param>
/// <param name="presentationSize">The spatial extent of the presented component planes.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
/// <param name="profile">The source profile selected for conversion, or null to preserve source colors.</param>
/// <param name="alphaFrame">The auxiliary plane, or null for opaque pixels.</param>
/// <param name="alphaOutputSize">The complete color extent covered by alpha.</param>
/// <param name="alphaRectangle">The exact matching auxiliary presentation region.</param>
/// <param name="premultiplied">Whether source RGB is associated with alpha.</param>
/// <param name="chromaUpsampling">The chroma reconstruction mode.</param>
/// <param name="isFullRange">Whether RGB conversion interprets the color planes as full-range samples.</param>
public static void ConvertToRgb<TPixel>(
Configuration configuration,
Av1FrameBuffer<byte> frameBuffer,
Rectangle sourceRectangle,
Buffer2DRegion<TPixel> destination,
Size presentationSize,
HeifPixelTransform transform,
IccProfile? profile,
Av1FrameBuffer<byte>? alphaFrame,
Size alphaOutputSize,
Rectangle alphaRectangle,
bool premultiplied,
HeifChromaUpsampling chromaUpsampling,
bool isFullRange)
where TPixel : unmanaged, IPixel<TPixel>
{
using HeifAlphaRowSource? alpha = alphaFrame is null
? null
: CreateAlphaRowSource(configuration, alphaFrame, alphaOutputSize, alphaRectangle);
HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer.ColorConfig, isFullRange, out HeifColorConversionMode mode);
if (frameBuffer.BitDepth == Av1BitDepth.EightBit)
{
Av1PlanarSampleBuffer<byte> buffer = new(frameBuffer);
if (presentationSize != new Size(buffer.Width, buffer.Height))
{
using Av1PresentationSampleBuffer<byte, Av1PlanarSampleBuffer<byte>> presented = new(
configuration, buffer, presentationSize.Width, presentationSize.Height);
HeifPlanarColorConverter.ConvertToRgb<
TPixel, Av1PresentationSampleBufferView<byte, Av1PlanarSampleBuffer<byte>>, byte, HeifByteSampleConverter>(
configuration, presented.View, destination, in parameters, mode, sourceRectangle.X, sourceRectangle.Y, sourceRectangle.Size, transform, profile, alpha, premultiplied, chromaUpsampling);
return;
}
HeifPlanarColorConverter.ConvertToRgb<TPixel, Av1PlanarSampleBuffer<byte>, byte, HeifByteSampleConverter>(
configuration,
buffer,
destination,
in parameters,
mode,
sourceRectangle.X,
sourceRectangle.Y,
sourceRectangle.Size,
transform,
profile,
alpha,
premultiplied,
chromaUpsampling);
return;
}
Av1PlanarSampleBuffer<ushort> highBitDepthBuffer = new(frameBuffer);
if (presentationSize != new Size(highBitDepthBuffer.Width, highBitDepthBuffer.Height))
{
using Av1PresentationSampleBuffer<ushort, Av1PlanarSampleBuffer<ushort>> presented = new(
configuration, highBitDepthBuffer, presentationSize.Width, presentationSize.Height);
HeifPlanarColorConverter.ConvertToRgb<TPixel, Av1PresentationSampleBufferView<ushort, Av1PlanarSampleBuffer<ushort>>>(
configuration, presented.View, destination, in parameters, mode, sourceRectangle.X, sourceRectangle.Y, sourceRectangle.Size, transform, profile, alpha, premultiplied, chromaUpsampling);
return;
}
HeifPlanarColorConverter.ConvertToRgb<TPixel, Av1PlanarSampleBuffer<ushort>>(
configuration,
highBitDepthBuffer,
destination,
in parameters,
mode,
sourceRectangle.X,
sourceRectangle.Y,
sourceRectangle.Size,
transform,
profile,
alpha,
premultiplied,
chromaUpsampling);
}
/// <summary>
/// Selects a native alpha row reader for the exact color region.
/// </summary>
/// <param name="configuration">The configuration providing scratch storage.</param>
/// <param name="frame">The native auxiliary samples retained by the caller.</param>
/// <param name="outputSize">The complete color extent before cropping and orientation.</param>
/// <param name="window">The color region within that extent.</param>
/// <returns>The row reader whose scratch storage must be disposed after conversion.</returns>
public static HeifAlphaRowSource CreateAlphaRowSource(
Configuration configuration,
Av1FrameBuffer<byte> frame,
Size outputSize,
Rectangle window)
{
HeifColorConversionParameters parameters = GetConversionParameters(frame.ColorConfig, frame.ColorConfig.ColorRange, out _);
Rectangle source = new(0, 0, frame.Width, frame.Height);
if (frame.BitDepth == Av1BitDepth.EightBit)
{
Av1PlanarSampleBuffer<byte> buffer = new(frame);
return source.Size == outputSize
? new HeifAlphaRowSource<Av1PlanarSampleBuffer<byte>, byte, HeifByteSampleConverter>(
configuration, buffer, in parameters, window)
: new HeifPlanarAlphaResizeWorker<Av1PlanarSampleBuffer<byte>, byte, HeifByteSampleConverter>(
configuration, buffer, in parameters, source, window, outputSize);
}
Av1PlanarSampleBuffer<ushort> highBitDepthBuffer = new(frame);
return source.Size == outputSize
? new HeifAlphaRowSource<Av1PlanarSampleBuffer<ushort>, ushort, HeifUShortSampleConverter>(
configuration, highBitDepthBuffer, in parameters, window)
: new HeifPlanarAlphaResizeWorker<Av1PlanarSampleBuffer<ushort>, ushort, HeifUShortSampleConverter>(
configuration, highBitDepthBuffer, in parameters, source, window, outputSize);
}
/// <summary>
/// Composes the reconstructed luma plane into a packed color frame as auxiliary alpha.
/// </summary>
/// <typeparam name="TPixel">The destination color pixel type.</typeparam>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
/// <param name="frameBuffer">The reconstructed AV1 frame containing the alpha luma plane.</param>
/// <param name="destination">The packed color frame receiving alpha values.</param>
/// <param name="outputSize">The complete presented size of the auxiliary image or grid tile.</param>
/// <param name="destinationRectangle">The destination region receiving the top-left portion of the presented alpha image.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
public static void ComposeAlpha<TPixel>(
Configuration configuration,
Av1FrameBuffer<byte> frameBuffer,
Buffer2DRegion<TPixel> destination,
Size outputSize,
Rectangle destinationRectangle,
bool premultiplied,
HeifPixelTransform transform)
where TPixel : unmanaged, IPixel<TPixel>
=> ComposeAlpha(
configuration,
frameBuffer,
new Rectangle(0, 0, frameBuffer.Width, frameBuffer.Height),
destination,
outputSize,
destinationRectangle,
premultiplied,
transform);
/// <summary>
/// Composes a rectangular reconstructed luma region into a packed color frame as auxiliary alpha.
/// </summary>
/// <typeparam name="TPixel">The destination color pixel type.</typeparam>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
/// <param name="frameBuffer">The reconstructed AV1 frame containing the alpha luma plane.</param>
/// <param name="sourceRectangle">The luma-sample region mapped to the destination rectangle.</param>
/// <param name="destination">The packed color frame receiving alpha values.</param>
/// <param name="outputSize">The complete presented size of the auxiliary image or grid tile.</param>
/// <param name="destinationRectangle">The destination region receiving the presented alpha image.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
public static void ComposeAlpha<TPixel>(
Configuration configuration,
Av1FrameBuffer<byte> frameBuffer,
Rectangle sourceRectangle,
Buffer2DRegion<TPixel> destination,
Size outputSize,
Rectangle destinationRectangle,
bool premultiplied,
HeifPixelTransform transform)
where TPixel : unmanaged, IPixel<TPixel>
{
HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer.ColorConfig, frameBuffer.ColorConfig.ColorRange, out _);
if (frameBuffer.BitDepth == Av1BitDepth.EightBit)
{
Av1PlanarSampleBuffer<byte> buffer = new(frameBuffer);
HeifPlanarAlphaCompositor.Compose<TPixel, Av1PlanarSampleBuffer<byte>, byte, HeifByteSampleConverter>(
configuration,
buffer,
destination,
in parameters,
sourceRectangle,
outputSize,
destinationRectangle,
premultiplied,
transform);
return;
}
Av1PlanarSampleBuffer<ushort> highBitDepthBuffer = new(frameBuffer);
HeifPlanarAlphaCompositor.Compose<TPixel, Av1PlanarSampleBuffer<ushort>, ushort, HeifUShortSampleConverter>(
configuration,
highBitDepthBuffer,
destination,
in parameters,
sourceRectangle,
outputSize,
destinationRectangle,
premultiplied,
transform);
}
/// <summary>
/// Converts packed pixels to the configured monochrome or component planes used by the AV1 encoder.
/// </summary>
/// <typeparam name="TPixel">The source pixel type.</typeparam>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
/// <param name="image">The source image frame.</param>
/// <param name="frameBuffer">The destination AV1 frame.</param>
public static void ConvertFromRgb<TPixel>(Configuration configuration, ImageFrame<TPixel> image, Av1FrameBuffer<byte> frameBuffer)
where TPixel : unmanaged, IPixel<TPixel>
{
HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer.ColorConfig, frameBuffer.ColorConfig.ColorRange, out HeifColorConversionMode mode);
if (frameBuffer.BitDepth == Av1BitDepth.EightBit)
{
Av1PlanarSampleBuffer<byte> buffer = new(frameBuffer);
HeifPlanarColorConverter.ConvertFromRgb<TPixel, Av1PlanarSampleBuffer<byte>, byte, HeifByteSampleConverter>(
configuration,
image,
buffer,
in parameters,
mode);
return;
}
Av1PlanarSampleBuffer<ushort> highBitDepthBuffer = new(frameBuffer);
HeifPlanarColorConverter.ConvertFromRgb<TPixel, Av1PlanarSampleBuffer<ushort>, ushort, HeifUShortSampleConverter>(
configuration,
image,
highBitDepthBuffer,
in parameters,
mode);
}
/// <summary>
/// Resolves the H.273 conversion mode, matrix coefficients, and sample range for a frame.
/// </summary>
/// <param name="colorConfig">The signaled AV1 color configuration.</param>
/// <param name="isFullRange">Whether conversion interprets the samples as full range.</param>
/// <param name="mode">The resolved conversion mode.</param>
/// <returns>The resolved conversion parameters.</returns>
public static HeifColorConversionParameters GetConversionParameters(
ObuColorConfig colorConfig,
bool isFullRange,
out HeifColorConversionMode mode)
{
if (colorConfig.ChromaSamplePosition == ObuChromoSamplePosition.Reserved)
{
throw new InvalidImageContentException("The reserved AV1 chroma sample position is invalid.");
}
bool isMonochrome = colorConfig.IsMonochrome;
return HeifColorConversionParameters.Create(
(CicpColorPrimaries)(byte)colorConfig.ColorPrimaries,
(CicpTransferCharacteristics)(byte)colorConfig.TransferCharacteristics,
(CicpMatrixCoefficients)(byte)colorConfig.MatrixCoefficients,
isFullRange,
colorConfig.BitDepth.GetBitCount(),
colorConfig.BitDepth.GetBitCount(),
isMonochrome,
colorConfig.GetColorFormat() == Av1ColorFormat.Yuv444,
out mode);
}
}

2800
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs

File diff suppressed because it is too large

424
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs

@ -0,0 +1,424 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Stores and adapts an AV1 inverse cumulative distribution used by the range coder.
/// </summary>
internal sealed class Av1Distribution
{
/// <summary>
/// The exclusive upper bound of the Q15 probability domain.
/// </summary>
public const int ProbabilityTop = 1 << ProbabilityBitCount;
/// <summary>
/// The minimum sub-range reserved for each symbol during range coding.
/// </summary>
public const int ProbabilityMinimum = 4;
/// <summary>
/// The shift that converts stored Q15 cumulative values to the range-coder precision.
/// </summary>
public const int CdfShift = 15 - ProbabilityBitCount;
/// <summary>
/// The precision reduction applied before multiplying a cumulative value by the coding range.
/// </summary>
public const int ProbabilityShift = 6;
/// <summary>
/// The number of fractional bits in a stored cumulative probability.
/// </summary>
private const int ProbabilityBitCount = 15;
/// <summary>
/// The Q15 inverse cumulative thresholds followed by the required zero sentinel.
/// </summary>
private InlineArray16<ushort> probabilities;
/// <summary>
/// The symbol-count contribution to the adaptive update rate.
/// </summary>
private readonly int speed;
/// <summary>
/// The capped number of observations already incorporated into this distribution.
/// </summary>
private int updateCount;
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a binary alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
public Av1Distribution(uint p0)
: this([p0, 0], 1)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a three-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
public Av1Distribution(uint p0, uint p1)
: this([p0, p1, 0], 1)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a four-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
public Av1Distribution(uint p0, uint p1, uint p2)
: this([p0, p1, p2, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a five-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3)
: this([p0, p1, p2, p3, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a six-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4)
: this([p0, p1, p2, p3, p4, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a seven-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5)
: this([p0, p1, p2, p3, p4, p5, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for an eight-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6)
: this([p0, p1, p2, p3, p4, p5, p6, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a nine-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
/// <param name="p7">The cumulative threshold following symbol seven.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7)
: this([p0, p1, p2, p3, p4, p5, p6, p7, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a ten-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
/// <param name="p7">The cumulative threshold following symbol seven.</param>
/// <param name="p8">The cumulative threshold following symbol eight.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8)
: this([p0, p1, p2, p3, p4, p5, p6, p7, p8, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for an eleven-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
/// <param name="p7">The cumulative threshold following symbol seven.</param>
/// <param name="p8">The cumulative threshold following symbol eight.</param>
/// <param name="p9">The cumulative threshold following symbol nine.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8, uint p9)
: this([p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a twelve-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
/// <param name="p7">The cumulative threshold following symbol seven.</param>
/// <param name="p8">The cumulative threshold following symbol eight.</param>
/// <param name="p9">The cumulative threshold following symbol nine.</param>
/// <param name="p10">The cumulative threshold following symbol ten.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8, uint p9, uint p10)
: this([p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a thirteen-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
/// <param name="p7">The cumulative threshold following symbol seven.</param>
/// <param name="p8">The cumulative threshold following symbol eight.</param>
/// <param name="p9">The cumulative threshold following symbol nine.</param>
/// <param name="p10">The cumulative threshold following symbol ten.</param>
/// <param name="p11">The cumulative threshold following symbol eleven.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8, uint p9, uint p10, uint p11)
: this([p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a fourteen-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
/// <param name="p7">The cumulative threshold following symbol seven.</param>
/// <param name="p8">The cumulative threshold following symbol eight.</param>
/// <param name="p9">The cumulative threshold following symbol nine.</param>
/// <param name="p10">The cumulative threshold following symbol ten.</param>
/// <param name="p11">The cumulative threshold following symbol eleven.</param>
/// <param name="p12">The cumulative threshold following symbol twelve.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8, uint p9, uint p10, uint p11, uint p12)
: this([p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a sixteen-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
/// <param name="p7">The cumulative threshold following symbol seven.</param>
/// <param name="p8">The cumulative threshold following symbol eight.</param>
/// <param name="p9">The cumulative threshold following symbol nine.</param>
/// <param name="p10">The cumulative threshold following symbol ten.</param>
/// <param name="p11">The cumulative threshold following symbol eleven.</param>
/// <param name="p12">The cumulative threshold following symbol twelve.</param>
/// <param name="p13">The cumulative threshold following symbol thirteen.</param>
/// <param name="p14">The cumulative threshold following symbol fourteen.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8, uint p9, uint p10, uint p11, uint p12, uint p13, uint p14)
: this([p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class from forward cumulative thresholds.
/// </summary>
/// <param name="props">The forward Q15 thresholds followed by a zero sentinel slot.</param>
/// <param name="speed">The symbol-count contribution to the update rate.</param>
private Av1Distribution(ReadOnlySpan<uint> props, int speed)
{
Span<ushort> probabilities = this.probabilities;
// Q15 thresholds range from zero through 32768, including zero-mass intervals, and fit in unsigned 16-bit storage.
// Convert the forward defaults before narrowing; the final zero sentinel is already initialized.
for (int i = 0; i < props.Length - 1; i++)
{
probabilities[i] = (ushort)(ProbabilityTop - props[i]);
}
this.NumberOfSymbols = props.Length;
this.speed = speed;
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class with the same probability and adaptation state as another distribution.
/// </summary>
/// <param name="source">The distribution state to copy.</param>
private Av1Distribution(Av1Distribution source)
{
ReadOnlySpan<ushort> sourceProbabilities = source.probabilities;
Span<ushort> probabilities = this.probabilities;
sourceProbabilities[..source.NumberOfSymbols].CopyTo(probabilities);
// The adaptation rate depends on both the alphabet size and prior update count, so copying only the
// thresholds would make the cloned frame context diverge after its next symbol.
this.speed = source.speed;
this.updateCount = source.updateCount;
this.NumberOfSymbols = source.NumberOfSymbols;
}
/// <summary>
/// Gets the number of symbols represented by the distribution.
/// </summary>
public int NumberOfSymbols { get; }
/// <summary>
/// Gets an inverse cumulative threshold by symbol index.
/// </summary>
/// <param name="index">The zero-based threshold index.</param>
/// <returns>The Q15 inverse cumulative threshold.</returns>
public uint this[int index] => this.probabilities[index];
/// <summary>
/// Creates an independently adaptable copy of a distribution.
/// </summary>
/// <returns>A distribution initialized with the same probabilities and update count.</returns>
public Av1Distribution CreateCopy() => new(this);
/// <summary>
/// Replaces the probability and adaptation state with the state of another distribution having the same alphabet.
/// </summary>
/// <param name="source">The distribution state to copy.</param>
public void CopyFrom(Av1Distribution source)
{
// Entropy contexts are created from the same fixed default table shape. Copy only mutable state so resetting a
// working tile never allocates or replaces the distribution objects referenced by the symbol decoder.
ReadOnlySpan<ushort> sourceProbabilities = source.probabilities;
Span<ushort> probabilities = this.probabilities;
sourceProbabilities[..source.NumberOfSymbols].CopyTo(probabilities);
this.updateCount = source.updateCount;
}
/// <summary>
/// Resets the observation count that controls the adaptive update rate without changing probability thresholds.
/// </summary>
public void ResetUpdateCount() => this.updateCount = 0;
/// <summary>
/// Creates independently adaptable copies of a distribution array.
/// </summary>
/// <param name="source">The distributions to copy.</param>
/// <returns>An array with the same shape and distribution state.</returns>
public static Av1Distribution[] CreateCopy(Av1Distribution[] source)
{
Av1Distribution[] result = new Av1Distribution[source.Length];
for (int i = 0; i < source.Length; i++)
{
result[i] = source[i].CreateCopy();
}
return result;
}
/// <summary>
/// Creates independently adaptable copies of a two-dimensional jagged distribution array.
/// </summary>
/// <param name="source">The distributions to copy.</param>
/// <returns>An array with the same shape and distribution state.</returns>
public static Av1Distribution[][] CreateCopy(Av1Distribution[][] source)
{
Av1Distribution[][] result = new Av1Distribution[source.Length][];
for (int i = 0; i < source.Length; i++)
{
result[i] = CreateCopy(source[i]);
}
return result;
}
/// <summary>
/// Creates independently adaptable copies of a three-dimensional jagged distribution array.
/// </summary>
/// <param name="source">The distributions to copy.</param>
/// <returns>An array with the same shape and distribution state.</returns>
public static Av1Distribution[][][] CreateCopy(Av1Distribution[][][] source)
{
Av1Distribution[][][] result = new Av1Distribution[source.Length][][];
for (int i = 0; i < source.Length; i++)
{
result[i] = CreateCopy(source[i]);
}
return result;
}
/// <summary>
/// Adapts the cumulative thresholds after coding one symbol.
/// </summary>
/// <param name="value">The zero-based symbol that was coded.</param>
public void Update(int value)
{
// AV1 slows adaptation after 16 and 32 observations. The symbol-count term is precomputed by each overload
// because every distribution has a fixed alphabet size.
int rate15 = this.updateCount > 15 ? 1 : 0;
int rate31 = this.updateCount > 31 ? 1 : 0;
int rate = 3 + rate15 + rate31 + this.speed;
int tmp = ProbabilityTop;
// Switching tmp to zero at the observed symbol moves the thresholds on either side toward the sample while
// preserving their inverse-cumulative ordering in one pass. Arithmetic stays wide until the stored update;
// each step moves toward zero or 32768, so narrowing cannot discard a significant probability bit.
for (int i = 0; i < this.NumberOfSymbols - 1; i++)
{
tmp = i == value ? 0 : tmp;
uint p = this.probabilities[i];
if (tmp < p)
{
this.probabilities[i] -= (ushort)((p - tmp) >> rate);
}
else
{
this.probabilities[i] += (ushort)((tmp - p) >> rate);
}
}
int rate32 = this.updateCount < 32 ? 1 : 0;
this.updateCount += rate32;
}
}

766
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContext.cs

@ -0,0 +1,766 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Owns the adaptive AV1 distributions currently implemented by the frame and tile syntax decoders.
/// </summary>
/// <remarks>
/// One frame context supplies the initial state copied into every tile context. Each tile adapts an independent working
/// copy, and only the tile selected by <c>context_update_tile_id</c> supplies the completed frame snapshot.
/// </remarks>
internal sealed class Av1FrameEntropyContext
{
/// <summary>
/// The inclusive upper bound of the first AV1 coefficient-probability quantizer band.
/// </summary>
private const int FirstQuantizerBandMaximum = 20;
/// <summary>
/// The inclusive upper bound of the second AV1 coefficient-probability quantizer band.
/// </summary>
private const int SecondQuantizerBandMaximum = 60;
/// <summary>
/// The inclusive upper bound of the third AV1 coefficient-probability quantizer band.
/// </summary>
private const int ThirdQuantizerBandMaximum = 120;
/// <summary>
/// The immutable normative contexts used to restore reusable frame state without rebuilding distribution graphs.
/// </summary>
private static readonly Av1FrameEntropyContext[] DefaultPrototypes =
[
new((byte)0),
new((byte)1),
new((byte)2),
new((byte)3)
];
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameEntropyContext"/> class from the normative default
/// distributions selected by a frame quantizer index.
/// </summary>
/// <param name="qIndex">The frame base quantizer index selecting coefficient distribution defaults.</param>
public Av1FrameEntropyContext(int qIndex)
: this(DefaultPrototypes[GetQContext(qIndex)])
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameEntropyContext"/> class as an immutable normative prototype.
/// </summary>
/// <param name="qContext">The zero-based coefficient-probability quantizer band.</param>
private Av1FrameEntropyContext(byte qContext)
{
int qIndex = qContext switch
{
0 => 0,
1 => FirstQuantizerBandMaximum + 1,
2 => SecondQuantizerBandMaximum + 1,
_ => ThirdQuantizerBandMaximum + 1
};
// Every default-distribution accessor constructs independently mutable state. Retaining those returned
// graphs directly confines generated-table construction to the four process-wide quantizer-band prototypes.
this.IntraBlockCopy = Av1DefaultDistributions.IntraBlockCopy;
// Normal motion vectors and intra-block-copy displacement vectors start from identical defaults, but AV1
// adapts NMVC and NDVC independently. Distinct object graphs preserve that separation for the prototype too.
this.MotionVector = new();
this.DisplacementVector = new();
this.SwitchableRestoration = Av1DefaultDistributions.SwitchableRestoration;
this.WienerRestoration = Av1DefaultDistributions.WienerRestoration;
this.SgrProjectionRestoration = Av1DefaultDistributions.SgrProjectionRestoration;
this.PaletteYMode = Av1DefaultDistributions.PaletteYMode;
this.PaletteUvMode = Av1DefaultDistributions.PaletteUvMode;
this.PaletteYSize = Av1DefaultDistributions.PaletteYSize;
this.PaletteUvSize = Av1DefaultDistributions.PaletteUvSize;
this.PaletteYColorIndex = Av1DefaultDistributions.PaletteYColorIndex;
this.PaletteUvColorIndex = Av1DefaultDistributions.PaletteUvColorIndex;
this.PartitionTypes = Av1DefaultDistributions.PartitionTypes;
this.FrameYMode = Av1DefaultDistributions.FrameYMode;
this.KeyFrameYMode = Av1DefaultDistributions.KeyFrameYMode;
this.IntraInter = Av1DefaultDistributions.IntraInter;
this.NewMv = Av1DefaultDistributions.NewMv;
this.ZeroMv = Av1DefaultDistributions.ZeroMv;
this.RefMv = Av1DefaultDistributions.RefMv;
this.Drl = Av1DefaultDistributions.Drl;
this.SingleReference = Av1DefaultDistributions.SingleReference;
this.CompInter = Av1DefaultDistributions.CompInter;
this.CompoundReferenceType = Av1DefaultDistributions.CompoundReferenceType;
this.UnidirectionalCompoundReference = Av1DefaultDistributions.UnidirectionalCompoundReference;
this.CompoundReference = Av1DefaultDistributions.CompoundReference;
this.CompoundBackwardReference = Av1DefaultDistributions.CompoundBackwardReference;
this.InterCompoundMode = Av1DefaultDistributions.InterCompoundMode;
this.InterIntra = Av1DefaultDistributions.InterIntra;
this.InterIntraMode = Av1DefaultDistributions.InterIntraMode;
this.WedgeInterIntra = Av1DefaultDistributions.WedgeInterIntra;
this.CompoundType = Av1DefaultDistributions.CompoundType;
this.WedgeIndex = Av1DefaultDistributions.WedgeIndex;
this.CompoundIndex = Av1DefaultDistributions.CompoundIndex;
this.CompoundGroupIndex = Av1DefaultDistributions.CompoundGroupIndex;
this.MotionMode = Av1DefaultDistributions.MotionMode;
this.Obmc = Av1DefaultDistributions.Obmc;
this.SwitchableInterpolation = Av1DefaultDistributions.SwitchableInterpolation;
this.UvMode = Av1DefaultDistributions.UvMode;
this.Skip = Av1DefaultDistributions.Skip;
this.SkipMode = Av1DefaultDistributions.SkipMode;
this.DeltaLoopFilterAbsolute = Av1DefaultDistributions.DeltaLoopFilterAbsolute;
this.DeltaLoopFilterMultiAbsolute = Av1DefaultDistributions.DeltaLoopFilterMultiAbsolute;
this.DeltaQuantizerAbsolute = Av1DefaultDistributions.DeltaQuantizerAbsolute;
this.SegmentId = Av1DefaultDistributions.SegmentId;
this.SegmentIdPredicted = Av1DefaultDistributions.SegmentIdPredicted;
this.AngleDelta = Av1DefaultDistributions.AngleDelta;
this.FilterIntraMode = Av1DefaultDistributions.FilterIntraMode;
this.FilterIntra = Av1DefaultDistributions.FilterIntra;
this.TransformSize = Av1DefaultDistributions.TransformSize;
this.TransformPartition = Av1DefaultDistributions.TransformPartition;
this.ChromaFromLumaSign = Av1DefaultDistributions.ChromaFromLumaSign;
this.ChromaFromLumaAlpha = Av1DefaultDistributions.ChromaFromLumaAlpha;
this.IntraExtendedTransform = Av1DefaultDistributions.IntraExtendedTransform;
this.InterExtendedTransform = Av1DefaultDistributions.InterExtendedTransform;
// Coefficient defaults use one of four quantizer bands. Their array shapes remain fixed, so later tile resets
// copy only thresholds and update counts into this context's already allocated distribution graph.
this.EndOfBlockFlag = Av1DefaultDistributions.GetEndOfBlockFlag(qIndex);
this.CoefficientsBase = Av1DefaultDistributions.GetCoefficientsBase(qIndex);
this.BaseEndOfBlock = Av1DefaultDistributions.GetBaseEndOfBlock(qIndex);
this.DcSign = Av1DefaultDistributions.GetDcSign(qIndex);
this.CoefficientsBaseRange = Av1DefaultDistributions.GetCoefficientsBaseRange(qIndex);
this.TransformBlockSkip = Av1DefaultDistributions.GetTransformBlockSkip(qIndex);
this.EndOfBlockExtra = Av1DefaultDistributions.GetEndOfBlockExtra(qIndex);
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameEntropyContext"/> class with an independently adaptable copy of a prototype.
/// </summary>
/// <param name="source">The prototype or retained context whose state is copied.</param>
private Av1FrameEntropyContext(Av1FrameEntropyContext source)
{
// Session and retained-frame contexts need one mutable graph, not four generated quantizer-band graphs whose
// unused bands are immediately discarded. Deep-copy the already selected prototype shape exactly once.
this.IntraBlockCopy = source.IntraBlockCopy.CreateCopy();
this.MotionVector = new();
this.MotionVector.CopyFrom(source.MotionVector);
this.DisplacementVector = new();
this.DisplacementVector.CopyFrom(source.DisplacementVector);
this.SwitchableRestoration = source.SwitchableRestoration.CreateCopy();
this.WienerRestoration = source.WienerRestoration.CreateCopy();
this.SgrProjectionRestoration = source.SgrProjectionRestoration.CreateCopy();
this.PaletteYMode = Av1Distribution.CreateCopy(source.PaletteYMode);
this.PaletteUvMode = Av1Distribution.CreateCopy(source.PaletteUvMode);
this.PaletteYSize = Av1Distribution.CreateCopy(source.PaletteYSize);
this.PaletteUvSize = Av1Distribution.CreateCopy(source.PaletteUvSize);
this.PaletteYColorIndex = Av1Distribution.CreateCopy(source.PaletteYColorIndex);
this.PaletteUvColorIndex = Av1Distribution.CreateCopy(source.PaletteUvColorIndex);
this.PartitionTypes = Av1Distribution.CreateCopy(source.PartitionTypes);
this.FrameYMode = Av1Distribution.CreateCopy(source.FrameYMode);
this.KeyFrameYMode = Av1Distribution.CreateCopy(source.KeyFrameYMode);
this.IntraInter = Av1Distribution.CreateCopy(source.IntraInter);
this.NewMv = Av1Distribution.CreateCopy(source.NewMv);
this.ZeroMv = Av1Distribution.CreateCopy(source.ZeroMv);
this.RefMv = Av1Distribution.CreateCopy(source.RefMv);
this.Drl = Av1Distribution.CreateCopy(source.Drl);
this.SingleReference = Av1Distribution.CreateCopy(source.SingleReference);
this.CompInter = Av1Distribution.CreateCopy(source.CompInter);
this.CompoundReferenceType = Av1Distribution.CreateCopy(source.CompoundReferenceType);
this.UnidirectionalCompoundReference = Av1Distribution.CreateCopy(source.UnidirectionalCompoundReference);
this.CompoundReference = Av1Distribution.CreateCopy(source.CompoundReference);
this.CompoundBackwardReference = Av1Distribution.CreateCopy(source.CompoundBackwardReference);
this.InterCompoundMode = Av1Distribution.CreateCopy(source.InterCompoundMode);
this.InterIntra = Av1Distribution.CreateCopy(source.InterIntra);
this.InterIntraMode = Av1Distribution.CreateCopy(source.InterIntraMode);
this.WedgeInterIntra = Av1Distribution.CreateCopy(source.WedgeInterIntra);
this.CompoundType = Av1Distribution.CreateCopy(source.CompoundType);
this.WedgeIndex = Av1Distribution.CreateCopy(source.WedgeIndex);
this.CompoundIndex = Av1Distribution.CreateCopy(source.CompoundIndex);
this.CompoundGroupIndex = Av1Distribution.CreateCopy(source.CompoundGroupIndex);
this.MotionMode = Av1Distribution.CreateCopy(source.MotionMode);
this.Obmc = Av1Distribution.CreateCopy(source.Obmc);
this.SwitchableInterpolation = Av1Distribution.CreateCopy(source.SwitchableInterpolation);
this.UvMode = Av1Distribution.CreateCopy(source.UvMode);
this.Skip = Av1Distribution.CreateCopy(source.Skip);
this.SkipMode = Av1Distribution.CreateCopy(source.SkipMode);
this.DeltaLoopFilterAbsolute = source.DeltaLoopFilterAbsolute.CreateCopy();
this.DeltaLoopFilterMultiAbsolute = Av1Distribution.CreateCopy(source.DeltaLoopFilterMultiAbsolute);
this.DeltaQuantizerAbsolute = source.DeltaQuantizerAbsolute.CreateCopy();
this.SegmentId = Av1Distribution.CreateCopy(source.SegmentId);
this.SegmentIdPredicted = Av1Distribution.CreateCopy(source.SegmentIdPredicted);
this.AngleDelta = Av1Distribution.CreateCopy(source.AngleDelta);
this.FilterIntraMode = source.FilterIntraMode.CreateCopy();
this.FilterIntra = Av1Distribution.CreateCopy(source.FilterIntra);
this.TransformSize = Av1Distribution.CreateCopy(source.TransformSize);
this.TransformPartition = Av1Distribution.CreateCopy(source.TransformPartition);
this.EndOfBlockFlag = Av1Distribution.CreateCopy(source.EndOfBlockFlag);
this.CoefficientsBase = Av1Distribution.CreateCopy(source.CoefficientsBase);
this.BaseEndOfBlock = Av1Distribution.CreateCopy(source.BaseEndOfBlock);
this.DcSign = Av1Distribution.CreateCopy(source.DcSign);
this.CoefficientsBaseRange = Av1Distribution.CreateCopy(source.CoefficientsBaseRange);
this.TransformBlockSkip = Av1Distribution.CreateCopy(source.TransformBlockSkip);
this.EndOfBlockExtra = Av1Distribution.CreateCopy(source.EndOfBlockExtra);
this.ChromaFromLumaSign = source.ChromaFromLumaSign.CreateCopy();
this.ChromaFromLumaAlpha = Av1Distribution.CreateCopy(source.ChromaFromLumaAlpha);
this.IntraExtendedTransform = Av1Distribution.CreateCopy(source.IntraExtendedTransform);
this.InterExtendedTransform = Av1Distribution.CreateCopy(source.InterExtendedTransform);
}
/// <summary>
/// Gets the intra-block-copy distribution.
/// </summary>
public Av1Distribution IntraBlockCopy { get; }
/// <summary>
/// Gets the entropy context used by normal inter-prediction motion vectors.
/// </summary>
public Av1MotionVectorContext MotionVector { get; }
/// <summary>
/// Gets the integer displacement-vector context used by intra-block copy.
/// </summary>
public Av1MotionVectorContext DisplacementVector { get; }
/// <summary>
/// Gets the switchable loop-restoration distribution.
/// </summary>
public Av1Distribution SwitchableRestoration { get; }
/// <summary>
/// Gets the Wiener loop-restoration distribution.
/// </summary>
public Av1Distribution WienerRestoration { get; }
/// <summary>
/// Gets the self-guided loop-restoration distribution.
/// </summary>
public Av1Distribution SgrProjectionRestoration { get; }
/// <summary>
/// Gets the luma palette-mode distributions.
/// </summary>
public Av1Distribution[][] PaletteYMode { get; }
/// <summary>
/// Gets the chroma palette-mode distributions.
/// </summary>
public Av1Distribution[] PaletteUvMode { get; }
/// <summary>
/// Gets the luma palette-size distributions.
/// </summary>
public Av1Distribution[] PaletteYSize { get; }
/// <summary>
/// Gets the chroma palette-size distributions.
/// </summary>
public Av1Distribution[] PaletteUvSize { get; }
/// <summary>
/// Gets the luma palette color-index distributions.
/// </summary>
public Av1Distribution[][] PaletteYColorIndex { get; }
/// <summary>
/// Gets the chroma palette color-index distributions.
/// </summary>
public Av1Distribution[][] PaletteUvColorIndex { get; }
/// <summary>
/// Gets the partition-type distributions.
/// </summary>
public Av1Distribution[] PartitionTypes { get; }
/// <summary>
/// Gets the inter-frame intra luma-mode distributions indexed by the normative block-size group.
/// </summary>
public Av1Distribution[] FrameYMode { get; }
/// <summary>
/// Gets the key-frame luma-mode distributions.
/// </summary>
public Av1Distribution[][] KeyFrameYMode { get; }
/// <summary>
/// Gets the distributions that select intra or inter prediction from the available spatial neighbors.
/// </summary>
public Av1Distribution[] IntraInter { get; }
/// <summary>
/// Gets the distributions that select a newly decoded motion vector before the remaining single-reference modes.
/// </summary>
public Av1Distribution[] NewMv { get; }
/// <summary>
/// Gets the distributions that select global motion before the spatial reference-motion-vector modes.
/// </summary>
public Av1Distribution[] ZeroMv { get; }
/// <summary>
/// Gets the distributions that select the nearest or near spatial reference motion vector.
/// </summary>
public Av1Distribution[] RefMv { get; }
/// <summary>
/// Gets the distributions that advance through the dynamic reference-motion-vector candidate list.
/// </summary>
public Av1Distribution[] Drl { get; }
/// <summary>
/// Gets the single-reference selection distributions indexed by spatial context and tree decision.
/// </summary>
public Av1Distribution[][] SingleReference { get; }
/// <summary>
/// Gets the distributions that select single-reference or compound-reference prediction for a block.
/// </summary>
public Av1Distribution[] CompInter { get; }
/// <summary>
/// Gets the distributions that select unidirectional or bidirectional compound reference pairs.
/// </summary>
public Av1Distribution[] CompoundReferenceType { get; }
/// <summary>
/// Gets the unidirectional compound-reference selection distributions.
/// </summary>
public Av1Distribution[][] UnidirectionalCompoundReference { get; }
/// <summary>
/// Gets the bidirectional compound forward-reference selection distributions.
/// </summary>
public Av1Distribution[][] CompoundReference { get; }
/// <summary>
/// Gets the bidirectional compound backward-reference selection distributions.
/// </summary>
public Av1Distribution[][] CompoundBackwardReference { get; }
/// <summary>
/// Gets the compound inter-mode distributions indexed by derived mode context.
/// </summary>
public Av1Distribution[] InterCompoundMode { get; }
/// <summary>
/// Gets the inter-intra prediction flag distributions indexed by block-size group.
/// </summary>
public Av1Distribution[] InterIntra { get; }
/// <summary>
/// Gets the inter-intra mode distributions indexed by block-size group.
/// </summary>
public Av1Distribution[] InterIntraMode { get; }
/// <summary>
/// Gets the inter-intra wedge-selection distributions indexed by block size.
/// </summary>
public Av1Distribution[] WedgeInterIntra { get; }
/// <summary>
/// Gets the masked-compound type distributions indexed by block size.
/// </summary>
public Av1Distribution[] CompoundType { get; }
/// <summary>
/// Gets the wedge-index distributions indexed by block size.
/// </summary>
public Av1Distribution[] WedgeIndex { get; }
/// <summary>
/// Gets the average-or-distance-weighted compound distributions indexed by derived context.
/// </summary>
public Av1Distribution[] CompoundIndex { get; }
/// <summary>
/// Gets the unmasked-or-masked compound-group distributions indexed by derived context.
/// </summary>
public Av1Distribution[] CompoundGroupIndex { get; }
/// <summary>
/// Gets the three-way motion-mode distributions indexed by block size.
/// </summary>
public Av1Distribution[] MotionMode { get; }
/// <summary>
/// Gets the binary Simple Translation or OBMC distributions indexed by block size.
/// </summary>
public Av1Distribution[] Obmc { get; }
/// <summary>
/// Gets the switchable interpolation-filter distributions.
/// </summary>
public Av1Distribution[] SwitchableInterpolation { get; }
/// <summary>
/// Gets the chroma intra-mode distributions.
/// </summary>
public Av1Distribution[][] UvMode { get; }
/// <summary>
/// Gets the transform-skip distributions.
/// </summary>
public Av1Distribution[] Skip { get; }
/// <summary>
/// Gets the skip-mode distributions.
/// </summary>
public Av1Distribution[] SkipMode { get; }
/// <summary>
/// Gets the absolute loop-filter delta distribution.
/// </summary>
public Av1Distribution DeltaLoopFilterAbsolute { get; }
/// <summary>
/// Gets independent delta distributions for vertical luma, horizontal luma, U, and V loop filters.
/// </summary>
public Av1Distribution[] DeltaLoopFilterMultiAbsolute { get; }
/// <summary>
/// Gets the absolute quantizer delta distribution.
/// </summary>
public Av1Distribution DeltaQuantizerAbsolute { get; }
/// <summary>
/// Gets the spatial segment-identifier distributions.
/// </summary>
public Av1Distribution[] SegmentId { get; }
/// <summary>
/// Gets the temporal segment-map prediction distributions.
/// </summary>
public Av1Distribution[] SegmentIdPredicted { get; }
/// <summary>
/// Gets the directional angle-delta distributions.
/// </summary>
public Av1Distribution[] AngleDelta { get; }
/// <summary>
/// Gets the filter-intra mode distribution.
/// </summary>
public Av1Distribution FilterIntraMode { get; }
/// <summary>
/// Gets the filter-intra enable distributions.
/// </summary>
public Av1Distribution[] FilterIntra { get; }
/// <summary>
/// Gets the transform-size distributions.
/// </summary>
public Av1Distribution[][] TransformSize { get; }
/// <summary>
/// Gets the variable-transform partition distributions.
/// </summary>
public Av1Distribution[] TransformPartition { get; }
/// <summary>
/// Gets the end-of-block token distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][][] EndOfBlockFlag { get; }
/// <summary>
/// Gets the coefficient base-level distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][][] CoefficientsBase { get; }
/// <summary>
/// Gets the final-nonzero coefficient distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][][] BaseEndOfBlock { get; }
/// <summary>
/// Gets the DC sign distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][] DcSign { get; }
/// <summary>
/// Gets the coefficient base-range distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][][] CoefficientsBaseRange { get; }
/// <summary>
/// Gets the transform-block skip distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][] TransformBlockSkip { get; }
/// <summary>
/// Gets the end-of-block extra-bit distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][][] EndOfBlockExtra { get; }
/// <summary>
/// Gets the joint chroma-from-luma sign distribution.
/// </summary>
public Av1Distribution ChromaFromLumaSign { get; }
/// <summary>
/// Gets the chroma-from-luma alpha-magnitude distributions.
/// </summary>
public Av1Distribution[] ChromaFromLumaAlpha { get; }
/// <summary>
/// Gets the intra transform-type distributions.
/// </summary>
public Av1Distribution[][][] IntraExtendedTransform { get; }
/// <summary>
/// Gets the inter transform-type distributions.
/// </summary>
public Av1Distribution[][] InterExtendedTransform { get; }
/// <summary>
/// Restores the normative frame defaults selected by a base quantizer index.
/// </summary>
/// <param name="qIndex">The frame base quantizer index selecting coefficient distribution defaults.</param>
public void ResetToDefaults(int qIndex)
{
int qContext = GetQContext(qIndex);
// The prototypes are never exposed to a range reader or writer. Copying their state lets each codec session
// reuse its mutable object graphs even when successive frames select different coefficient-model bands.
this.CopyFrom(DefaultPrototypes[qContext]);
}
/// <summary>
/// Maps a frame base quantizer to its normative coefficient-probability initialization band.
/// </summary>
/// <param name="qIndex">The frame base quantizer index.</param>
/// <returns>The zero-based quantizer-band index.</returns>
private static int GetQContext(int qIndex)
=> qIndex switch
{
<= FirstQuantizerBandMaximum => 0,
<= SecondQuantizerBandMaximum => 1,
<= ThirdQuantizerBandMaximum => 2,
_ => 3
};
/// <summary>
/// Replaces every probability threshold and adaptation count with state copied from another frame context.
/// </summary>
/// <param name="source">The frame context state to copy.</param>
public void CopyFrom(Av1FrameEntropyContext source)
{
this.IntraBlockCopy.CopyFrom(source.IntraBlockCopy);
this.MotionVector.CopyFrom(source.MotionVector);
this.DisplacementVector.CopyFrom(source.DisplacementVector);
this.SwitchableRestoration.CopyFrom(source.SwitchableRestoration);
this.WienerRestoration.CopyFrom(source.WienerRestoration);
this.SgrProjectionRestoration.CopyFrom(source.SgrProjectionRestoration);
CopyState(source.PaletteYMode, this.PaletteYMode);
CopyState(source.PaletteUvMode, this.PaletteUvMode);
CopyState(source.PaletteYSize, this.PaletteYSize);
CopyState(source.PaletteUvSize, this.PaletteUvSize);
CopyState(source.PaletteYColorIndex, this.PaletteYColorIndex);
CopyState(source.PaletteUvColorIndex, this.PaletteUvColorIndex);
CopyState(source.PartitionTypes, this.PartitionTypes);
CopyState(source.FrameYMode, this.FrameYMode);
CopyState(source.KeyFrameYMode, this.KeyFrameYMode);
CopyState(source.IntraInter, this.IntraInter);
CopyState(source.NewMv, this.NewMv);
CopyState(source.ZeroMv, this.ZeroMv);
CopyState(source.RefMv, this.RefMv);
CopyState(source.Drl, this.Drl);
CopyState(source.SingleReference, this.SingleReference);
CopyState(source.CompInter, this.CompInter);
CopyState(source.CompoundReferenceType, this.CompoundReferenceType);
CopyState(source.UnidirectionalCompoundReference, this.UnidirectionalCompoundReference);
CopyState(source.CompoundReference, this.CompoundReference);
CopyState(source.CompoundBackwardReference, this.CompoundBackwardReference);
CopyState(source.InterCompoundMode, this.InterCompoundMode);
CopyState(source.InterIntra, this.InterIntra);
CopyState(source.InterIntraMode, this.InterIntraMode);
CopyState(source.WedgeInterIntra, this.WedgeInterIntra);
CopyState(source.CompoundType, this.CompoundType);
CopyState(source.WedgeIndex, this.WedgeIndex);
CopyState(source.CompoundIndex, this.CompoundIndex);
CopyState(source.CompoundGroupIndex, this.CompoundGroupIndex);
CopyState(source.MotionMode, this.MotionMode);
CopyState(source.Obmc, this.Obmc);
CopyState(source.SwitchableInterpolation, this.SwitchableInterpolation);
CopyState(source.UvMode, this.UvMode);
CopyState(source.Skip, this.Skip);
CopyState(source.SkipMode, this.SkipMode);
this.DeltaLoopFilterAbsolute.CopyFrom(source.DeltaLoopFilterAbsolute);
CopyState(source.DeltaLoopFilterMultiAbsolute, this.DeltaLoopFilterMultiAbsolute);
this.DeltaQuantizerAbsolute.CopyFrom(source.DeltaQuantizerAbsolute);
CopyState(source.SegmentId, this.SegmentId);
CopyState(source.SegmentIdPredicted, this.SegmentIdPredicted);
CopyState(source.AngleDelta, this.AngleDelta);
this.FilterIntraMode.CopyFrom(source.FilterIntraMode);
CopyState(source.FilterIntra, this.FilterIntra);
CopyState(source.TransformSize, this.TransformSize);
CopyState(source.TransformPartition, this.TransformPartition);
CopyState(source.EndOfBlockFlag, this.EndOfBlockFlag);
CopyState(source.CoefficientsBase, this.CoefficientsBase);
CopyState(source.BaseEndOfBlock, this.BaseEndOfBlock);
CopyState(source.DcSign, this.DcSign);
CopyState(source.CoefficientsBaseRange, this.CoefficientsBaseRange);
CopyState(source.TransformBlockSkip, this.TransformBlockSkip);
CopyState(source.EndOfBlockExtra, this.EndOfBlockExtra);
this.ChromaFromLumaSign.CopyFrom(source.ChromaFromLumaSign);
CopyState(source.ChromaFromLumaAlpha, this.ChromaFromLumaAlpha);
CopyState(source.IntraExtendedTransform, this.IntraExtendedTransform);
CopyState(source.InterExtendedTransform, this.InterExtendedTransform);
}
/// <summary>
/// Copies this tile-adapted context into a destination used as completed frame state.
/// </summary>
/// <param name="destination">The independently owned frame context that receives the snapshot.</param>
/// <remarks>
/// AV1 resets CDF observation counters after publishing the context-update tile. The copied thresholds remain
/// adapted, while the next frame starts its update-rate history from zero.
/// </remarks>
public void SnapshotTo(Av1FrameEntropyContext destination)
{
destination.CopyFrom(this);
destination.ResetUpdateCounts();
}
/// <summary>
/// Resets the observation count of every distribution without changing its probability thresholds.
/// </summary>
private void ResetUpdateCounts()
{
this.IntraBlockCopy.ResetUpdateCount();
this.MotionVector.ResetUpdateCounts();
this.DisplacementVector.ResetUpdateCounts();
this.SwitchableRestoration.ResetUpdateCount();
this.WienerRestoration.ResetUpdateCount();
this.SgrProjectionRestoration.ResetUpdateCount();
ResetUpdateCounts(this.PaletteYMode);
ResetUpdateCounts(this.PaletteUvMode);
ResetUpdateCounts(this.PaletteYSize);
ResetUpdateCounts(this.PaletteUvSize);
ResetUpdateCounts(this.PaletteYColorIndex);
ResetUpdateCounts(this.PaletteUvColorIndex);
ResetUpdateCounts(this.PartitionTypes);
ResetUpdateCounts(this.FrameYMode);
ResetUpdateCounts(this.KeyFrameYMode);
ResetUpdateCounts(this.IntraInter);
ResetUpdateCounts(this.NewMv);
ResetUpdateCounts(this.ZeroMv);
ResetUpdateCounts(this.RefMv);
ResetUpdateCounts(this.Drl);
ResetUpdateCounts(this.SingleReference);
ResetUpdateCounts(this.CompInter);
ResetUpdateCounts(this.CompoundReferenceType);
ResetUpdateCounts(this.UnidirectionalCompoundReference);
ResetUpdateCounts(this.CompoundReference);
ResetUpdateCounts(this.CompoundBackwardReference);
ResetUpdateCounts(this.InterCompoundMode);
ResetUpdateCounts(this.InterIntra);
ResetUpdateCounts(this.InterIntraMode);
ResetUpdateCounts(this.WedgeInterIntra);
ResetUpdateCounts(this.CompoundType);
ResetUpdateCounts(this.WedgeIndex);
ResetUpdateCounts(this.CompoundIndex);
ResetUpdateCounts(this.CompoundGroupIndex);
ResetUpdateCounts(this.MotionMode);
ResetUpdateCounts(this.Obmc);
ResetUpdateCounts(this.SwitchableInterpolation);
ResetUpdateCounts(this.UvMode);
ResetUpdateCounts(this.Skip);
ResetUpdateCounts(this.SkipMode);
this.DeltaLoopFilterAbsolute.ResetUpdateCount();
ResetUpdateCounts(this.DeltaLoopFilterMultiAbsolute);
this.DeltaQuantizerAbsolute.ResetUpdateCount();
ResetUpdateCounts(this.SegmentId);
ResetUpdateCounts(this.SegmentIdPredicted);
ResetUpdateCounts(this.AngleDelta);
this.FilterIntraMode.ResetUpdateCount();
ResetUpdateCounts(this.FilterIntra);
ResetUpdateCounts(this.TransformSize);
ResetUpdateCounts(this.TransformPartition);
ResetUpdateCounts(this.EndOfBlockFlag);
ResetUpdateCounts(this.CoefficientsBase);
ResetUpdateCounts(this.BaseEndOfBlock);
ResetUpdateCounts(this.DcSign);
ResetUpdateCounts(this.CoefficientsBaseRange);
ResetUpdateCounts(this.TransformBlockSkip);
ResetUpdateCounts(this.EndOfBlockExtra);
this.ChromaFromLumaSign.ResetUpdateCount();
ResetUpdateCounts(this.ChromaFromLumaAlpha);
ResetUpdateCounts(this.IntraExtendedTransform);
ResetUpdateCounts(this.InterExtendedTransform);
}
/// <summary>
/// Copies one distribution row into an existing row with the same default-table shape.
/// </summary>
/// <param name="source">The source distribution row.</param>
/// <param name="destination">The destination distribution row.</param>
private static void CopyState(Av1Distribution[] source, Av1Distribution[] destination)
{
for (int index = 0; index < source.Length; index++)
{
destination[index].CopyFrom(source[index]);
}
}
/// <summary>
/// Copies a two-dimensional distribution table into an existing table with the same default-table shape.
/// </summary>
/// <param name="source">The source distribution table.</param>
/// <param name="destination">The destination distribution table.</param>
private static void CopyState(Av1Distribution[][] source, Av1Distribution[][] destination)
{
for (int index = 0; index < source.Length; index++)
{
CopyState(source[index], destination[index]);
}
}
/// <summary>
/// Copies a three-dimensional distribution table into an existing table with the same default-table shape.
/// </summary>
/// <param name="source">The source distribution table.</param>
/// <param name="destination">The destination distribution table.</param>
private static void CopyState(Av1Distribution[][][] source, Av1Distribution[][][] destination)
{
for (int index = 0; index < source.Length; index++)
{
CopyState(source[index], destination[index]);
}
}
/// <summary>
/// Resets observation counts in one distribution row.
/// </summary>
/// <param name="distributions">The distribution row to reset.</param>
private static void ResetUpdateCounts(Av1Distribution[] distributions)
{
for (int index = 0; index < distributions.Length; index++)
{
distributions[index].ResetUpdateCount();
}
}
/// <summary>
/// Resets observation counts in a two-dimensional distribution table.
/// </summary>
/// <param name="distributions">The distribution table to reset.</param>
private static void ResetUpdateCounts(Av1Distribution[][] distributions)
{
for (int index = 0; index < distributions.Length; index++)
{
ResetUpdateCounts(distributions[index]);
}
}
/// <summary>
/// Resets observation counts in a three-dimensional distribution table.
/// </summary>
/// <param name="distributions">The distribution table to reset.</param>
private static void ResetUpdateCounts(Av1Distribution[][][] distributions)
{
for (int index = 0; index < distributions.Length; index++)
{
ResetUpdateCounts(distributions[index]);
}
}
}

140
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs

@ -0,0 +1,140 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Owns the reusable frame-base, tile-working, and published AV1 entropy contexts for one decoder session.
/// </summary>
internal sealed class Av1FrameEntropyContexts
{
/// <summary>
/// The maximum number of live reference-map and presentation owners plus the newly reconstructed frame awaiting
/// commit.
/// </summary>
private const int MaximumSnapshotCount = Av1Constants.ReferenceFrameCount + 2;
/// <summary>
/// Session-local returned snapshot graphs available for later refreshed frames.
/// </summary>
private InlineArray10<Av1FrameEntropyContext?> returnedSnapshots;
/// <summary>
/// The base quantizer index used to initialize a newly required snapshot graph.
/// </summary>
private int currentQIndex;
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameEntropyContexts"/> class.
/// </summary>
/// <param name="qIndex">The initial frame base quantizer index.</param>
public Av1FrameEntropyContexts(int qIndex)
{
this.Base = new(qIndex);
this.Working = new(qIndex);
this.Published = new(qIndex);
this.currentQIndex = qIndex;
}
/// <summary>
/// Gets the unchanged frame context from which each independently decoded tile starts.
/// </summary>
public Av1FrameEntropyContext Base { get; }
/// <summary>
/// Gets the tile-local context reused sequentially for each tile in the current frame.
/// </summary>
public Av1FrameEntropyContext Working { get; }
/// <summary>
/// Gets the completed frame context selected by the signaled context-update tile, or the unchanged frame-base
/// context when frame-end updates are disabled.
/// </summary>
public Av1FrameEntropyContext Published { get; }
/// <summary>
/// Initializes frame entropy state from either a retained primary reference or normative quantizer-band defaults.
/// </summary>
/// <param name="qIndex">The frame base quantizer index selecting coefficient distribution defaults.</param>
/// <param name="primaryReferenceContext">
/// The retained primary-reference context, or <see langword="null"/> when the frame selects normative defaults.
/// </param>
public void BeginFrame(int qIndex, Av1FrameEntropyContext? primaryReferenceContext)
{
this.currentQIndex = qIndex;
if (primaryReferenceContext is null)
{
this.Base.ResetToDefaults(qIndex);
}
else
{
// A retained context is independent from the working and published graphs. Copying it here preserves the
// reference owner's snapshot while the current frame adapts its own tile-local state.
this.Base.CopyFrom(primaryReferenceContext);
}
// The context-update tile can precede later tiles. Published therefore cannot alias Working: a later tile
// must be free to overwrite Working while the selected completed-frame state remains available to the owner.
this.Base.SnapshotTo(this.Published);
}
/// <summary>
/// Clears active frame entropy state when a new coded sequence invalidates the complete reference map.
/// </summary>
public void Reset()
{
this.currentQIndex = 0;
this.Base.ResetToDefaults(this.currentQIndex);
this.Base.SnapshotTo(this.Working);
this.Base.SnapshotTo(this.Published);
// Returned graphs contain no live reference state and remain private to this decoder. Retaining them here
// allows the next sequence to reuse peak reference ownership without a static cross-decode pool.
}
/// <summary>
/// Rents an independently owned, reset-counter snapshot of the completed frame entropy context.
/// </summary>
/// <returns>The snapshot that must later be returned through <see cref="ReturnSnapshot"/>.</returns>
public Av1FrameEntropyContext RentPublishedSnapshot()
{
Av1FrameEntropyContext? snapshot = null;
for (int snapshotIndex = 0; snapshotIndex < MaximumSnapshotCount; snapshotIndex++)
{
Av1FrameEntropyContext? returnedSnapshot = this.returnedSnapshots[snapshotIndex];
if (returnedSnapshot is null)
{
continue;
}
this.returnedSnapshots[snapshotIndex] = null;
snapshot = returnedSnapshot;
break;
}
// Eight slots can own distinct frames while the selected output owns a ninth frame no longer present in
// the map. Rent one further graph before commit releases the owner displaced by the completed frame.
snapshot ??= new(this.currentQIndex);
this.Published.SnapshotTo(snapshot);
return snapshot;
}
/// <summary>
/// Returns a retained-frame entropy snapshot to this decoder session for later reuse.
/// </summary>
/// <param name="snapshot">The snapshot whose reference-frame ownership has ended.</param>
public void ReturnSnapshot(Av1FrameEntropyContext snapshot)
{
// The fixed capacity covers eight distinct slot owners, one detached presentation owner, and the replacement
// frame rented before commit. Av1ReferenceFrame returns each graph exactly once, so one slot is always free.
for (int snapshotIndex = 0; snapshotIndex < MaximumSnapshotCount; snapshotIndex++)
{
if (this.returnedSnapshots[snapshotIndex] is null)
{
this.returnedSnapshots[snapshotIndex] = snapshot;
return;
}
}
}
}

422
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorContext.cs

@ -0,0 +1,422 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Owns one independently adaptive AV1 motion-vector entropy context.
/// </summary>
/// <remarks>
/// Normal inter-prediction vectors and intra-block-copy displacement vectors use identical initial distributions, but
/// each syntax domain owns a separate instance so observations from one domain cannot adapt the other.
/// </remarks>
internal sealed class Av1MotionVectorContext
{
/// <summary>
/// The number of magnitude classes defined by AV1.
/// </summary>
private const int MagnitudeClassCount = 11;
/// <summary>
/// The number of integer magnitude bits coded directly for class zero.
/// </summary>
private const int ClassZeroBitCount = 1;
/// <summary>
/// The number of integer magnitude offsets represented by class zero.
/// </summary>
private const int ClassZeroSize = 1 << ClassZeroBitCount;
/// <summary>
/// Defines one non-mutating cost or mutating write operation over shared motion-vector syntax.
/// </summary>
public interface IMotionVectorSymbolOperation
{
/// <summary>
/// Processes one entropy-coded symbol.
/// </summary>
/// <param name="writer">The tile range encoder.</param>
/// <param name="symbol">The zero-based symbol.</param>
/// <param name="distribution">The live symbol distribution.</param>
/// <returns>The symbol cost in 1/512-bit units, or zero when writing.</returns>
public static abstract int ProcessSymbol(Av1SymbolWriter writer, int symbol, Av1Distribution distribution);
}
/// <summary>
/// Gets the distribution selecting which vector components are nonzero.
/// </summary>
public Av1Distribution Joint { get; } = new(4096, 11264, 19328);
/// <summary>
/// Gets the adaptive distributions for the vertical vector component.
/// </summary>
public Component Vertical { get; } = new();
/// <summary>
/// Gets the adaptive distributions for the horizontal vector component.
/// </summary>
public Component Horizontal { get; } = new();
/// <summary>
/// Replaces every motion-vector distribution with state copied from another context.
/// </summary>
/// <param name="source">The motion-vector context state to copy.</param>
public void CopyFrom(Av1MotionVectorContext source)
{
this.Joint.CopyFrom(source.Joint);
this.Vertical.CopyFrom(source.Vertical);
this.Horizontal.CopyFrom(source.Horizontal);
}
/// <summary>
/// Resets every observation count used to adapt the motion-vector distributions.
/// </summary>
public void ResetUpdateCounts()
{
this.Joint.ResetUpdateCount();
this.Vertical.ResetUpdateCounts();
this.Horizontal.ResetUpdateCounts();
}
/// <summary>
/// Reads a motion-vector delta relative to a spatially derived reference.
/// </summary>
/// <param name="reader">The tile range decoder.</param>
/// <param name="reference">The reference motion vector.</param>
/// <param name="precision">The fractional precision allowed by the current frame.</param>
/// <returns>The decoded motion vector in one-eighth-sample units.</returns>
public Av1MotionVector Read(ref Av1SymbolReader reader, Av1MotionVector reference, Av1MotionVectorPrecision precision)
{
int jointType = reader.ReadSymbol(this.Joint);
// Joint values 1 and 3 carry a horizontal delta; values 2 and 3 carry a vertical delta. Reading only the
// signaled components preserves the normative entropy-symbol order and leaves zero components unadapted.
int row = jointType >= 2 ? this.Vertical.Read(ref reader, precision) : 0;
int column = (jointType & 1) != 0 ? this.Horizontal.Read(ref reader, precision) : 0;
return reference + new Av1MotionVector(row, column);
}
/// <summary>
/// Writes a motion vector relative to a spatially derived reference.
/// </summary>
/// <param name="writer">The tile range encoder.</param>
/// <param name="value">The displacement vector to encode.</param>
/// <param name="reference">The spatially derived reference vector.</param>
/// <param name="precision">The fractional precision selected by the frame header.</param>
public void Write(Av1SymbolWriter writer, Av1MotionVector value, Av1MotionVector reference, Av1MotionVectorPrecision precision)
=> this.Write<Av1SymbolEncoder.SymbolWriteOperation>(writer, value, reference, precision);
/// <inheritdoc cref="Write(Av1SymbolWriter, Av1MotionVector, Av1MotionVector, Av1MotionVectorPrecision)"/>
/// <typeparam name="TOperation">The operation applied to each motion-vector symbol.</typeparam>
public void Write<TOperation>(
Av1SymbolWriter writer,
Av1MotionVector value,
Av1MotionVector reference,
Av1MotionVectorPrecision precision)
where TOperation : struct, Av1SymbolEncoder.ISymbolOperation
=> _ = this.Process<MotionVectorWriteOperation<TOperation>>(writer, value, reference, precision);
/// <summary>
/// Measures a motion-vector delta against the live distributions without changing them.
/// </summary>
/// <param name="writer">The tile range encoder associated with the live context.</param>
/// <param name="value">The motion vector to measure.</param>
/// <param name="reference">The spatially derived reference vector.</param>
/// <param name="precision">The fractional precision selected by the frame header.</param>
/// <returns>The syntax cost in 1/512-bit units.</returns>
public int GetCost(Av1SymbolWriter writer, Av1MotionVector value, Av1MotionVector reference, Av1MotionVectorPrecision precision)
=> this.Process<MotionVectorCostOperation>(writer, value, reference, precision);
/// <summary>
/// Processes one complete motion-vector delta through a closed symbol operation.
/// </summary>
private int Process<TOperation>(Av1SymbolWriter writer, Av1MotionVector value, Av1MotionVector reference, Av1MotionVectorPrecision precision)
where TOperation : struct, IMotionVectorSymbolOperation
{
int row = value.Row - reference.Row;
int column = value.Column - reference.Column;
// Bit zero signals a horizontal delta and bit one signals a vertical delta, producing the four normative
// zero/horizontal/vertical/both joint symbols without a lookup.
int jointType = (row != 0 ? 2 : 0) | (column != 0 ? 1 : 0);
int rate = TOperation.ProcessSymbol(writer, jointType, this.Joint);
if (row != 0)
{
rate += this.Vertical.Process<TOperation>(writer, row, precision);
}
if (column != 0)
{
rate += this.Horizontal.Process<TOperation>(writer, column, precision);
}
return rate;
}
/// <summary>
/// Emits motion-vector syntax and reports no estimated rate.
/// </summary>
private readonly struct MotionVectorWriteOperation<TOperation> : IMotionVectorSymbolOperation
where TOperation : struct, Av1SymbolEncoder.ISymbolOperation
{
/// <inheritdoc/>
public static int ProcessSymbol(Av1SymbolWriter writer, int symbol, Av1Distribution distribution)
=> TOperation.ProcessSymbol(ref writer, symbol, distribution);
}
/// <summary>
/// Measures motion-vector syntax against the live distributions without changing them.
/// </summary>
private readonly struct MotionVectorCostOperation : IMotionVectorSymbolOperation
{
/// <inheritdoc/>
public static int ProcessSymbol(Av1SymbolWriter writer, int symbol, Av1Distribution distribution)
=> Av1ProbabilityCost.GetSymbolCost(distribution, symbol);
}
/// <summary>
/// Owns the adaptive magnitude distributions for one motion-vector component.
/// </summary>
public sealed class Component
{
/// <summary>
/// Gets the distribution selecting the magnitude class of a nonzero component.
/// </summary>
public Av1Distribution MagnitudeClass { get; } = new(28672, 30976, 31858, 32320, 32551, 32656, 32740, 32757, 32762, 32767);
/// <summary>
/// Gets the fractional distributions selected by the two class-zero integer offsets.
/// </summary>
public Av1Distribution[] ClassZeroFractional { get; } =
[
new(16384, 24576, 26624),
new(12288, 21248, 24128)
];
/// <summary>
/// Gets the fractional distribution used by nonzero magnitude classes.
/// </summary>
public Av1Distribution Fractional { get; } = new(8192, 17408, 21248);
/// <summary>
/// Gets the distribution selecting the sign of a nonzero component.
/// </summary>
public Av1Distribution Sign { get; } = new(16384);
/// <summary>
/// Gets the eighth-sample distribution used by class-zero magnitudes.
/// </summary>
public Av1Distribution ClassZeroHighPrecision { get; } = new(20480);
/// <summary>
/// Gets the eighth-sample distribution used by nonzero magnitude classes.
/// </summary>
public Av1Distribution HighPrecision { get; } = new(16384);
/// <summary>
/// Gets the distribution selecting either of the two class-zero integer magnitude offsets.
/// </summary>
public Av1Distribution ClassZero { get; } = new(27648);
/// <summary>
/// Gets the binary distributions that reconstruct larger integer magnitude offsets from least to most significant bit.
/// </summary>
public Av1Distribution[] OffsetBits { get; } =
[
new(17408), new(17920), new(18944), new(20480), new(22528),
new(24576), new(28672), new(29952), new(29952), new(30720)
];
/// <summary>
/// Replaces every component distribution with state copied from another component.
/// </summary>
/// <param name="source">The component state to copy.</param>
public void CopyFrom(Component source)
{
this.MagnitudeClass.CopyFrom(source.MagnitudeClass);
for (int offset = 0; offset < this.ClassZeroFractional.Length; offset++)
{
this.ClassZeroFractional[offset].CopyFrom(source.ClassZeroFractional[offset]);
}
this.Fractional.CopyFrom(source.Fractional);
this.Sign.CopyFrom(source.Sign);
this.ClassZeroHighPrecision.CopyFrom(source.ClassZeroHighPrecision);
this.HighPrecision.CopyFrom(source.HighPrecision);
this.ClassZero.CopyFrom(source.ClassZero);
for (int bit = 0; bit < this.OffsetBits.Length; bit++)
{
this.OffsetBits[bit].CopyFrom(source.OffsetBits[bit]);
}
}
/// <summary>
/// Resets every observation count used to adapt one component's distributions.
/// </summary>
public void ResetUpdateCounts()
{
this.MagnitudeClass.ResetUpdateCount();
for (int offset = 0; offset < this.ClassZeroFractional.Length; offset++)
{
this.ClassZeroFractional[offset].ResetUpdateCount();
}
this.Fractional.ResetUpdateCount();
this.Sign.ResetUpdateCount();
this.ClassZeroHighPrecision.ResetUpdateCount();
this.HighPrecision.ResetUpdateCount();
this.ClassZero.ResetUpdateCount();
for (int bit = 0; bit < this.OffsetBits.Length; bit++)
{
this.OffsetBits[bit].ResetUpdateCount();
}
}
/// <summary>
/// Reads one signed motion-vector component at the requested precision.
/// </summary>
/// <param name="reader">The tile range decoder.</param>
/// <param name="precision">The fractional precision allowed by the current frame.</param>
/// <returns>The signed component in one-eighth-sample units.</returns>
public int Read(ref Av1SymbolReader reader, Av1MotionVectorPrecision precision)
{
bool isNegative = reader.ReadSymbol(this.Sign) != 0;
int magnitudeClass = reader.ReadSymbol(this.MagnitudeClass);
bool isClassZero = magnitudeClass == 0;
int integerOffset;
int magnitudeBase;
if (isClassZero)
{
integerOffset = reader.ReadSymbol(this.ClassZero);
magnitudeBase = 0;
}
else
{
int bitCount = magnitudeClass + ClassZeroBitCount - 1;
integerOffset = 0;
for (int bit = 0; bit < bitCount; bit++)
{
// AV1 transmits the integer offset least-significant bit first, with an independently adapting
// distribution for every bit position.
integerOffset |= reader.ReadSymbol(this.OffsetBits[bit]) << bit;
}
// Class one uses a base of two whole samples, or sixteen eighth-sample units, and every later class doubles
// that base. CLASS0_SIZE shifted by class + 2 expresses the same scale directly in eighth-sample units.
magnitudeBase = ClassZeroSize << (magnitudeClass + 2);
}
int fractional;
int highPrecision;
if (precision != Av1MotionVectorPrecision.Integer)
{
// Class-zero magnitudes select one of two fractional CDFs using the already decoded integer offset;
// larger classes share one fractional CDF because their expanded integer range supplies the context.
Av1Distribution fractionalDistribution = isClassZero ? this.ClassZeroFractional[integerOffset] : this.Fractional;
fractional = reader.ReadSymbol(fractionalDistribution);
// Quarter-sample motion omits the eighth-sample symbol. The normative implicit one, combined with the
// final increment below, constrains the result to even one-eighth-sample units.
highPrecision = precision == Av1MotionVectorPrecision.EighthSample
? reader.ReadSymbol(isClassZero ? this.ClassZeroHighPrecision : this.HighPrecision)
: 1;
}
else
{
// Integer motion omits both fractional symbols. The implicit maximum values make the low three bits
// all one before the final increment, constraining the result to whole-sample multiples of eight.
fractional = 3;
highPrecision = 1;
}
// The entropy syntax represents magnitude minus one. Integer offset occupies bits three and above,
// fractional occupies bits one and two, and high precision occupies bit zero, all in one-eighth-sample units.
int magnitude = magnitudeBase + ((integerOffset << 3) | (fractional << 1) | highPrecision) + 1;
return isNegative ? -magnitude : magnitude;
}
/// <summary>
/// Writes one signed motion-vector component.
/// </summary>
/// <param name="writer">The tile range encoder.</param>
/// <param name="value">The nonzero component in one-eighth-sample units.</param>
/// <param name="precision">The fractional precision selected by the frame header.</param>
public void Write(Av1SymbolWriter writer, int value, Av1MotionVectorPrecision precision)
=> _ = this.Process<MotionVectorWriteOperation<Av1SymbolEncoder.SymbolWriteOperation>>(writer, value, precision);
/// <summary>
/// Processes one nonzero signed component through the shared motion-vector symbol operation.
/// </summary>
public int Process<TOperation>(Av1SymbolWriter writer, int value, Av1MotionVectorPrecision precision)
where TOperation : struct, IMotionVectorSymbolOperation
{
int magnitude = Math.Abs(value);
int precisionMask = precision == Av1MotionVectorPrecision.Integer
? 7
: precision == Av1MotionVectorPrecision.QuarterSample ? 1 : 0;
DebugGuard.IsTrue(
magnitude > 0 && (magnitude & precisionMask) == 0,
"Motion-vector components must match the frame precision.");
// The coded value is magnitude minus one. Its whole-sample portion selects the doubling class,
// while the remainder carries integer offset, fractional phase, and the high-precision bit.
int codedMagnitude = magnitude - 1;
uint classValue = (uint)(codedMagnitude >> 3);
int magnitudeClass = classValue == 0 ? 0 : Av1Math.MostSignificantBit(classValue);
DebugGuard.MustBeLessThan(magnitudeClass, MagnitudeClassCount, nameof(magnitudeClass));
int magnitudeBase = magnitudeClass == 0 ? 0 : ClassZeroSize << (magnitudeClass + 2);
int offset = codedMagnitude - magnitudeBase;
int integerOffset = offset >> 3;
int fractional = (offset >> 1) & 3;
int highPrecision = offset & 1;
int rate = TOperation.ProcessSymbol(writer, value < 0 ? 1 : 0, this.Sign);
rate += TOperation.ProcessSymbol(writer, magnitudeClass, this.MagnitudeClass);
if (magnitudeClass == 0)
{
rate += TOperation.ProcessSymbol(writer, integerOffset, this.ClassZero);
}
else
{
for (int bit = 0; bit < magnitudeClass; bit++)
{
// Integer offsets are transmitted least-significant bit first through independent models.
rate += TOperation.ProcessSymbol(writer, (integerOffset >> bit) & 1, this.OffsetBits[bit]);
}
}
if (precision != Av1MotionVectorPrecision.Integer)
{
Av1Distribution fractionalDistribution = magnitudeClass == 0
? this.ClassZeroFractional[integerOffset]
: this.Fractional;
rate += TOperation.ProcessSymbol(writer, fractional, fractionalDistribution);
}
if (precision == Av1MotionVectorPrecision.EighthSample)
{
Av1Distribution highPrecisionDistribution = magnitudeClass == 0
? this.ClassZeroHighPrecision
: this.HighPrecision;
rate += TOperation.ProcessSymbol(writer, highPrecision, highPrecisionDistribution);
}
return rate;
}
}
}

205
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorCosts.cs

@ -0,0 +1,205 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Borrows the motion-vector rate tables retained by one encoder worker.
/// </summary>
internal readonly ref struct Av1MotionVectorCosts
{
/// <summary>
/// The largest representable signed component difference, in eighth-sample units.
/// </summary>
public const int MaximumComponent = (1 << 14) - 1;
private const int ComponentCount = (2 * MaximumComponent) + 1;
/// <summary>
/// Storage for the joint symbols and two component pairs, one for each fractional precision.
/// </summary>
public const int StorageLength = 4 + (4 * ComponentCount);
private readonly Span<int> joint;
private readonly Span<int> row;
private readonly Span<int> column;
private readonly Av1MotionVectorPrecision precision;
/// <summary>
/// Initializes a new instance of the <see cref="Av1MotionVectorCosts"/> struct.
/// </summary>
/// <param name="storage">Worker-lifetime storage containing both precision tables.</param>
/// <param name="precision">The precision used by the current frame.</param>
public Av1MotionVectorCosts(Span<int> storage, Av1MotionVectorPrecision precision)
{
// Integer and quarter-sample frames share one pair. The eighth-sample pair remains separate so changing
// frame precision does not change the worker's allocation or the layout of its other scratch regions.
int offset = 4 + (precision == Av1MotionVectorPrecision.EighthSample ? 2 * ComponentCount : 0);
this.joint = storage[..4];
this.row = storage.Slice(offset, ComponentCount);
this.column = storage.Slice(offset + ComponentCount, ComponentCount);
this.precision = precision;
}
/// <summary>
/// Captures component rates from the current tile distributions without adapting them.
/// </summary>
/// <param name="context">The tile's current motion-vector distributions.</param>
public void Fill(Av1MotionVectorContext context)
{
for (int i = 0; i < 4; i++)
{
this.joint[i] = Av1ProbabilityCost.GetSymbolCost(context.Joint, i);
}
FillComponent(this.row, context.Vertical, this.precision);
FillComponent(this.column, context.Horizontal, this.precision);
}
/// <summary>
/// Measures a candidate against its differential reference using the captured distributions.
/// </summary>
/// <param name="value">The candidate vector, in eighth-sample units.</param>
/// <param name="reference">The differential reference, in eighth-sample units.</param>
/// <returns>The rate in 1/512-bit units.</returns>
public int GetCost(Av1MotionVector value, Av1MotionVector reference)
{
int rowDifference = value.Row - reference.Row;
int columnDifference = value.Column - reference.Column;
int jointType = (rowDifference != 0 ? 2 : 0) | (columnDifference != 0 ? 1 : 0);
return this.joint[jointType] + this.row[MaximumComponent + rowDifference] + this.column[MaximumComponent + columnDifference];
}
/// <summary>
/// Builds a signed component table by reusing the costs of shorter binary magnitudes.
/// </summary>
/// <param name="destination">The complete signed component table, centered on zero.</param>
/// <param name="component">The distributions for this axis.</param>
/// <param name="precision">The fractional symbols present in the frame.</param>
private static void FillComponent(Span<int> destination, Av1MotionVectorContext.Component component, Av1MotionVectorPrecision precision)
{
Span<int> classCosts = stackalloc int[11];
Span<int> bitCosts = stackalloc int[20];
Span<int> fractionalCosts = stackalloc int[4];
Span<int> highPrecisionCosts = stackalloc int[2];
Span<int> costOffsets = stackalloc int[10];
int positiveSignCost = Av1ProbabilityCost.GetSymbolCost(component.Sign, 0);
int negativeSignCost = Av1ProbabilityCost.GetSymbolCost(component.Sign, 1);
int signDifference = negativeSignCost - positiveSignCost;
for (int i = 0; i < classCosts.Length; i++)
{
classCosts[i] = Av1ProbabilityCost.GetSymbolCost(component.MagnitudeClass, i);
}
for (int i = 0; i < costOffsets.Length; i++)
{
bitCosts[2 * i] = Av1ProbabilityCost.GetSymbolCost(component.OffsetBits[i], 0);
bitCosts[(2 * i) + 1] = Av1ProbabilityCost.GetSymbolCost(component.OffsetBits[i], 1);
}
// Omitting fractional symbols gives them zero rate. All entries are assigned explicitly because stack
// storage is uninitialized, including when integer motion disables both fractional syntax stages.
for (int i = 0; i < fractionalCosts.Length; i++)
{
fractionalCosts[i] = precision == Av1MotionVectorPrecision.Integer ? 0 : Av1ProbabilityCost.GetSymbolCost(component.Fractional, i);
}
for (int i = 0; i < highPrecisionCosts.Length; i++)
{
highPrecisionCosts[i] = precision == Av1MotionVectorPrecision.EighthSample
? Av1ProbabilityCost.GetSymbolCost(component.HighPrecision, i)
: 0;
}
costOffsets[0] = 0;
for (int i = 1; i < costOffsets.Length; i++)
{
// A shorter magnitude's leading one becomes an offset bit in a larger magnitude. Remove its
// former class rate and insert that bit's rate before adding the new magnitude class below.
costOffsets[i] = bitCosts[(2 * (i - 1)) + 1] - (i > 1 ? classCosts[i - 1] : 0);
}
destination[MaximumComponent] = 0;
for (int fractional = 0; fractional < 4; fractional++)
{
for (int highPrecision = 0; highPrecision < 2; highPrecision++)
{
int magnitude = (2 * fractional) + highPrecision + 1;
destination[MaximumComponent + magnitude] = fractionalCosts[fractional] + highPrecisionCosts[highPrecision] + positiveSignCost;
}
}
// Magnitudes encode value minus one. Each exponent doubles the integer offset range, reusing the
// previously completed lower half. The first eight entries temporarily carry fractional and sign rates
// alone; class-zero syntax is installed only after all larger magnitudes have consumed those seeds.
for (int exponentIndex = 0; exponentIndex < 10; exponentIndex++)
{
int exponent = 8 << exponentIndex;
int classCost = exponentIndex >= 1 ? classCosts[exponentIndex] : 0;
int mantissa = 0;
for (int bit = 0; bit <= exponentIndex; bit++)
{
for (; mantissa < (8 << bit); mantissa++)
{
int cost = destination[MaximumComponent + mantissa + 1] + classCost + costOffsets[bit];
int magnitude = exponent + mantissa + 1;
destination[MaximumComponent + magnitude] = cost;
destination[MaximumComponent - magnitude] = cost + signDifference;
}
// The next exponent introduces one more leading zero in this mantissa group.
costOffsets[bit] += bitCosts[2 * exponentIndex];
}
}
// The final exponent ends at 16383, one entry before the next power of two. Treat its upper mantissa
// separately to avoid producing the unrepresentable magnitude 16384 or reading an eleventh offset bit.
int finalMantissa = 0;
for (int bit = 0; bit < 10; bit++)
{
for (; finalMantissa < (8 << bit); finalMantissa++)
{
int cost = destination[MaximumComponent + finalMantissa + 1] + classCosts[10] + costOffsets[bit];
int magnitude = 8192 + finalMantissa + 1;
destination[MaximumComponent + magnitude] = cost;
destination[MaximumComponent - magnitude] = cost + signDifference;
}
}
int finalOffset = bitCosts[19] - classCosts[9];
for (; finalMantissa < 8191; finalMantissa++)
{
int cost = destination[MaximumComponent + finalMantissa + 1] + classCosts[10] + finalOffset;
int magnitude = 8192 + finalMantissa + 1;
destination[MaximumComponent + magnitude] = cost;
destination[MaximumComponent - magnitude] = cost + signDifference;
}
for (int integerOffset = 0; integerOffset < 2; integerOffset++)
{
int classZeroCost = classCosts[0] + Av1ProbabilityCost.GetSymbolCost(component.ClassZero, integerOffset);
for (int fractional = 0; fractional < 4; fractional++)
{
int cost = classZeroCost;
if (precision != Av1MotionVectorPrecision.Integer)
{
cost += Av1ProbabilityCost.GetSymbolCost(component.ClassZeroFractional[integerOffset], fractional);
}
for (int highPrecision = 0; highPrecision < 2; highPrecision++)
{
int magnitude = (8 * integerOffset) + (2 * fractional) + highPrecision + 1;
int fractionalCost = precision == Av1MotionVectorPrecision.EighthSample
? Av1ProbabilityCost.GetSymbolCost(component.ClassZeroHighPrecision, highPrecision)
: 0;
destination[MaximumComponent + magnitude] = cost + fractionalCost + positiveSignCost;
destination[MaximumComponent - magnitude] = cost + fractionalCost + negativeSignCost;
}
}
}
}
}

501
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1NzMap.cs

@ -0,0 +1,501 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Derives the AV1 nonzero-coefficient probability context from neighboring coefficient levels and transform geometry.
/// </summary>
internal static class Av1NzMap
{
/// <summary>
/// The first one-dimensional nonzero-map context, immediately after the 26 two-dimensional contexts.
/// </summary>
private const int NzMapContext0 = 26;
/// <summary>
/// The second one-dimensional position band.
/// </summary>
private const int NzMapContext5 = NzMapContext0 + 5;
/// <summary>
/// The final one-dimensional position band.
/// </summary>
private const int NzMapContext10 = NzMapContext0 + 10;
/// <summary>
/// Maps a horizontal or vertical coefficient coordinate to its one-dimensional context offset.
/// </summary>
private static readonly int[] NzMapContextOffset1d = [
NzMapContext0, NzMapContext5, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10,
NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10,
NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10,
NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10,
NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10,
];
/// <summary>
/// The row-major positional context offsets for a 4x4 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset4x4 = [
0, 1, 6, 6,
1, 6, 6, 21,
6, 6, 21, 21,
6, 21, 21, 21];
/// <summary>
/// The row-major positional context offsets for an 8x8 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset8x8 = [
0, 1, 6, 6, 21, 21, 21, 21,
1, 6, 6, 21, 21, 21, 21, 21,
6, 6, 21, 21, 21, 21, 21, 21,
6, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for a 16x16 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset16x16 = [
0, 1, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
1, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for a 32x32 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset32x32 = [
0, 1, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
1, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for an 8x4 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset8x4 = [
0, 16, 6, 6, 21, 21, 21, 21,
16, 16, 6, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for a 16x8 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset16x8 = [
0, 16, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for a 16x32 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset16x32 = [
0, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for a 32x16 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset32x16 = [
0, 16, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for the coded low-frequency region of a 32x64 transform.
/// </summary>
private static readonly int[] NzMapContextOffset32x64 = [
0, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for the coded low-frequency region of a 64x32 transform.
/// </summary>
private static readonly int[] NzMapContextOffset64x32 = [
0, 16, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for a 4x16 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset4x16 = [
0, 11, 11, 11,
11, 11, 11, 11,
6, 6, 21, 21,
6, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for a 16x4 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset16x4 = [
0, 16, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for an 8x32 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset8x32 = [
0, 11, 11, 11, 11, 11, 11, 11,
11, 11, 11, 11, 11, 11, 11, 11,
6, 6, 21, 21, 21, 21, 21, 21,
6, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for a 32x8 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset32x8 = [
0, 16, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// Maps each AV1 transform-size value to its row-major two-dimensional positional context table.
/// </summary>
private static readonly int[][] NzMapContextOffset = [
// Several rectangular transforms reuse a prefix of the next larger table. Every reused prefix has the same
// row-major stride as the adjusted coefficient plane, which avoids duplicating identical context offsets.
NzMapContextOffset4x4, // TX_4x4
NzMapContextOffset8x8, // TX_8x8
NzMapContextOffset16x16, // TX_16x16
NzMapContextOffset32x32, // TX_32x32
NzMapContextOffset32x32, // TX_64x64
NzMapContextOffset4x16, // TX_4x8
NzMapContextOffset8x4, // TX_8x4
NzMapContextOffset8x32, // TX_8x16
NzMapContextOffset16x8, // TX_16x8
NzMapContextOffset16x32, // TX_16x32
NzMapContextOffset32x16, // TX_32x16
NzMapContextOffset32x64, // TX_32x64
NzMapContextOffset64x32, // TX_64x32
NzMapContextOffset4x16, // TX_4x16
NzMapContextOffset16x4, // TX_16x4
NzMapContextOffset8x32, // TX_8x32
NzMapContextOffset32x8, // TX_32x8
NzMapContextOffset16x32, // TX_16x64
NzMapContextOffset64x32, // TX_64x16
];
/// <summary>
/// Sums the clipped magnitudes of the transform-class-specific forward coefficient neighbors.
/// </summary>
/// <param name="levels">The padded absolute-coefficient level plane.</param>
/// <param name="position">The coefficient position.</param>
/// <param name="transformClass">The transform direction class selecting the neighbor pattern.</param>
/// <returns>The summed neighbor magnitude used to select a nonzero-map context.</returns>
public static int GetNzMagnitude(Av1LevelBuffer levels, Point position, Av1TransformClass transformClass)
{
int mag;
Span<byte> row0 = levels.GetRow(position.Y)[position.X..];
Span<byte> row1 = levels.GetRow(position.Y + 1)[position.X..];
Span<byte> row2 = levels.GetRow(position.Y + 2)[position.X..];
// Large levels must not dominate probability selection; AV1 contributes at most three from each neighbor.
mag = ClipMax3(row0[1]); // { 0, 1 }
mag += ClipMax3(row1[0]); // { 1, 0 }
switch (transformClass)
{
case Av1TransformClass.Class2D:
mag += ClipMax3(row1[1]); // { 1, 1 }
mag += ClipMax3(row0[2]); // { 0, 2 }
mag += ClipMax3(row2[0]); // { 2, 0 }
break;
case Av1TransformClass.ClassVertical:
Span<byte> row3 = levels.GetRow(position.Y + 3)[position.X..];
Span<byte> row4 = levels.GetRow(position.Y + 4)[position.X..];
mag += ClipMax3(row2[0]); // { 2, 0 }
mag += ClipMax3(row3[0]); // { 3, 0 }
mag += ClipMax3(row4[0]); // { 4, 0 }
break;
case Av1TransformClass.ClassHorizontal:
mag += ClipMax3(row0[2]); // { 0, 2 }
mag += ClipMax3(row0[3]); // { 0, 3 }
mag += ClipMax3(row0[4]); // { 0, 4 }
break;
}
return mag;
}
/// <summary>
/// Combines a neighboring-level statistic with the coefficient's transform-class-specific position band.
/// </summary>
/// <param name="stats">The clipped sum of the applicable forward-neighbor magnitudes.</param>
/// <param name="position">The coefficient position.</param>
/// <param name="transformSize">The coded transform size selecting the positional table.</param>
/// <param name="transformClass">The transform direction class.</param>
/// <returns>The nonzero-map probability context.</returns>
public static int GetNzMapContextFromStats(int stats, Point position, Av1TransformSize transformSize, Av1TransformClass transformClass)
{
// The DC coefficient has a dedicated 2D context independent of neighboring levels.
if (transformClass == Av1TransformClass.Class2D && position.X == 0 && position.Y == 0)
{
return 0;
}
// Rounding the neighbor sum before clipping produces the five AV1 magnitude bands 0 through 4.
int ctx = (stats + 1) >> 1;
ctx = Math.Min(ctx, 4);
switch (transformClass)
{
case Av1TransformClass.Class2D:
// The tables preserve AV1's distinct early-row and early-column bands for rectangular transforms.
return ctx + GetNzMapContext(transformSize, position);
case Av1TransformClass.ClassHorizontal:
return ctx + NzMapContextOffset1d[position.X];
case Av1TransformClass.ClassVertical:
return ctx + NzMapContextOffset1d[position.Y];
default:
break;
}
return 0;
}
/// <summary>
/// Gets the two-dimensional positional context offset for a coefficient position.
/// </summary>
/// <param name="transformSize">The coded transform size.</param>
/// <param name="position">The coefficient position.</param>
/// <returns>The positional context offset.</returns>
public static int GetNzMapContext(Av1TransformSize transformSize, Point position)
{
// AV1 codes only the low-frequency 32-sample region of a 64-point transform dimension. The table still
// uses the signaled shape to select tall or wide bands, but its row-major stride follows the coded region.
int codedWidth = transformSize.GetAdjusted().GetWidth();
return GetNzMapContext(transformSize, position.X + (position.Y * codedWidth));
}
/// <summary>
/// Gets the two-dimensional positional context offset for a row-major coefficient index.
/// </summary>
/// <param name="transformSize">The coded transform size.</param>
/// <param name="position">The row-major coefficient index.</param>
/// <returns>The positional context offset.</returns>
public static int GetNzMapContext(Av1TransformSize transformSize, int position) => NzMapContextOffset[(int)transformSize][position];
/// <summary>
/// Clips a coefficient magnitude to the maximum contribution allowed per neighbor.
/// </summary>
/// <param name="value">The coefficient magnitude.</param>
/// <returns>The magnitude limited to three.</returns>
private static int ClipMax3(int value) => Math.Min(value, 3);
}

84
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1ProbabilityCost.cs

@ -0,0 +1,84 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Converts AV1 symbol probabilities into fixed-point encoder rate costs.
/// </summary>
internal static class Av1ProbabilityCost
{
/// <summary>
/// The number of fractional bits in an encoder rate cost.
/// </summary>
public const int CostShift = 9;
/// <summary>
/// Gets the probability costs for normalized eight-bit probabilities from 128 through 255.
/// </summary>
private static ReadOnlySpan<ushort> ProbabilityCosts =>
[
512, 506, 501, 495, 489, 484, 478, 473, 467, 462, 456, 451, 446, 441, 435,
430, 425, 420, 415, 410, 405, 400, 395, 390, 385, 380, 375, 371, 366, 361,
356, 352, 347, 343, 338, 333, 329, 324, 320, 316, 311, 307, 302, 298, 294,
289, 285, 281, 277, 273, 268, 264, 260, 256, 252, 248, 244, 240, 236, 232,
228, 224, 220, 216, 212, 209, 205, 201, 197, 194, 190, 186, 182, 179, 175,
171, 168, 164, 161, 157, 153, 150, 146, 143, 139, 136, 132, 129, 125, 122,
119, 115, 112, 109, 105, 102, 99, 95, 92, 89, 86, 82, 79, 76, 73, 70,
66, 63, 60, 57, 54, 51, 48, 45, 42, 38, 35, 32, 29, 26, 23, 20, 18, 15,
12, 9, 6, 3
];
/// <summary>
/// Gets the fixed-point cost of writing the requested number of equiprobable bits.
/// </summary>
/// <param name="bitCount">The number of bits.</param>
/// <returns>The rate cost in 1/512-bit units.</returns>
public static int GetLiteralCost(int bitCount) => bitCount << CostShift;
/// <summary>
/// Gets the fixed-point cost of coding one symbol from an inverse cumulative distribution.
/// </summary>
/// <param name="distribution">The distribution used by the entropy writer.</param>
/// <param name="symbol">The zero-based symbol.</param>
/// <returns>The rate cost in 1/512-bit units.</returns>
public static int GetSymbolCost(Av1Distribution distribution, int symbol)
{
int inverseLower = symbol == 0 ? Av1Distribution.ProbabilityTop : (int)distribution[symbol - 1];
int inverseUpper = (int)distribution[symbol];
return GetSymbolCost(inverseLower - inverseUpper);
}
/// <summary>
/// Gets the fixed-point cost of an entropy-coded symbol with a Q15 probability.
/// </summary>
/// <param name="probability">The Q15 probability numerator.</param>
/// <returns>The rate cost in 1/512-bit units.</returns>
public static int GetSymbolCost(int probability)
{
// The range coder reserves a minimum interval even when CDF adaptation collapses a symbol's mass.
// RD costs use that same floor; the raw probability conversion below retains its separate numerical domain.
return GetProbabilityCost(Math.Max(probability, Av1Distribution.ProbabilityMinimum));
}
/// <summary>
/// Gets the fixed-point cost of a Q15 probability.
/// </summary>
/// <param name="probability">The Q15 probability numerator.</param>
/// <returns>The rate cost in 1/512-bit units.</returns>
public static int GetProbabilityCost(int probability)
{
probability = Math.Clamp(probability, 1, Av1Distribution.ProbabilityTop - 1);
int shift = 14 - BitOperations.Log2((uint)probability);
// Normalization puts every probability in the upper half of an eight-bit range. The lookup therefore
// covers one binary order of magnitude, while the shift contributes the exact number of whole bits.
int normalizedProbability = (((probability << shift) * 256) + (Av1Distribution.ProbabilityTop >> 1))
/ Av1Distribution.ProbabilityTop;
normalizedProbability = Math.Min(normalizedProbability, 255);
return ProbabilityCosts[normalizedProbability - 128] + (shift << CostShift);
}
}

319
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1RateDistortion.cs

@ -0,0 +1,319 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Combines fixed-point AV1 rate and distortion values for encoder decisions.
/// </summary>
internal static class Av1RateDistortion
{
/// <summary>
/// Each fitted curve contains 65 equally spaced samples, including the cubic interpolation endpoints.
/// </summary>
private const int ModelCurveLength = 65;
/// <summary>
/// Gets the rate-curve category for each AV1 block geometry.
/// </summary>
private static ReadOnlySpan<byte> ModelRateCategories => [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 1, 1, 2, 2, 3, 3];
/// <summary>
/// Gets the four block-size rate curves in fixed-point bit-cost units per sample.
/// </summary>
private static ReadOnlySpan<double> ModelRateCurves =>
[
0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000, 118.257702, 120.210658, 121.434853, 122.100487,
122.377758, 122.436865, 72.290102, 96.974289, 101.652727,
126.830141, 140.417377, 157.644879, 184.315291, 215.823873,
262.300169, 335.919859, 420.624173, 519.185032, 619.854243,
726.053595, 827.663369, 933.127475, 1037.988755, 1138.839609,
1233.342933, 1333.508064, 1428.760126, 1533.396364, 1616.952052,
1744.539319, 1803.413586, 1951.466618, 1994.227838, 2086.031680,
2148.635443, 2239.068450, 2222.590637, 2338.859809, 2402.929011,
2418.727875, 2435.342670, 2471.159469, 2523.187446, 2591.183827,
2674.905840, 2774.110714, 2888.555675, 3017.997952, 3162.194773,
3320.903365, 3493.880956, 3680.884773, 3881.672045, 4096.000000,
0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000, 13.087244, 15.919735, 25.930313, 24.412411,
28.567417, 29.924194, 30.857010, 32.742979, 36.382570,
39.210386, 42.265690, 47.378572, 57.014850, 82.740067,
137.346562, 219.968084, 316.781856, 415.643773, 516.706538,
614.914364, 714.303763, 815.512135, 911.210485, 1008.501528,
1109.787854, 1213.772279, 1322.922561, 1414.752579, 1510.505641,
1615.741888, 1697.989032, 1780.123933, 1847.453790, 1913.742309,
1960.828122, 2047.500168, 2085.454095, 2129.230668, 2158.171824,
2182.231724, 2217.684864, 2269.589211, 2337.264824, 2420.618694,
2519.557814, 2633.989178, 2763.819779, 2908.956609, 3069.306660,
3244.776927, 3435.274401, 3640.706076, 3860.978945, 4096.000000,
0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000, 4.656893, 5.123633, 5.594132, 6.162376,
6.918433, 7.768444, 8.739415, 10.105862, 11.477328,
13.236604, 15.421030, 19.093623, 25.801871, 46.724612,
98.841054, 181.113466, 272.586364, 359.499769, 445.546343,
525.944439, 605.188743, 681.793483, 756.668359, 838.486885,
926.950356, 1015.482542, 1113.353926, 1204.897193, 1288.871992,
1373.464145, 1455.746628, 1527.796460, 1588.475066, 1658.144771,
1710.302500, 1807.563351, 1863.197608, 1927.281616, 1964.450872,
2022.719898, 2100.041145, 2185.205712, 2280.993936, 2387.616216,
2505.282950, 2634.204540, 2774.591385, 2926.653884, 3090.602436,
3266.647443, 3454.999303, 3655.868416, 3869.465182, 4096.000000,
0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000, 0.337370, 0.391916, 0.468839, 0.566334,
0.762564, 1.069225, 1.384361, 1.787581, 2.293948,
3.251909, 4.412991, 8.050068, 11.606073, 27.668092,
65.227758, 128.463938, 202.097653, 262.715851, 312.464873,
355.601398, 400.609054, 447.201352, 495.761568, 552.871938,
619.067625, 691.984883, 773.753288, 860.628503, 946.262808,
1019.805896, 1106.061360, 1178.422145, 1244.852258, 1302.173987,
1399.650266, 1548.092912, 1545.928652, 1670.817500, 1694.523823,
1779.195362, 1882.155494, 1990.662097, 2108.325181, 2235.456119,
2372.366287, 2519.367059, 2676.769812, 2844.885918, 3024.026754,
3214.503695, 3416.628115, 3630.711389, 3857.064892, 4096.000000,
];
/// <summary>
/// Gets the low- and high-error distortion curves in sixteenth-sample-error units.
/// </summary>
private static ReadOnlySpan<double> ModelDistortionCurves =>
[
16.000000, 15.962891, 15.925174, 15.886888, 15.848074,
15.808770, 15.769015, 15.728850, 15.688313, 15.647445,
15.606284, 15.564870, 15.525918, 15.483820, 15.373330,
15.126844, 14.637442, 14.184387, 13.560070, 12.880717,
12.165995, 11.378144, 10.438769, 9.130790, 7.487633,
5.688649, 4.267515, 3.196300, 2.434201, 1.834064,
1.369920, 1.035921, 0.775279, 0.574895, 0.427232,
0.314123, 0.233236, 0.171440, 0.128188, 0.092762,
0.067569, 0.049324, 0.036330, 0.027008, 0.019853,
0.015539, 0.011093, 0.008733, 0.007624, 0.008105,
0.005427, 0.004065, 0.003427, 0.002848, 0.002328,
0.001865, 0.001457, 0.001103, 0.000801, 0.000550,
0.000348, 0.000193, 0.000085, 0.000021, 0.000000,
16.000000, 15.996116, 15.984769, 15.966413, 15.941505,
15.910501, 15.873856, 15.832026, 15.785466, 15.734633,
15.679981, 15.621967, 15.560961, 15.460157, 15.288367,
15.052462, 14.466922, 13.921212, 13.073692, 12.222005,
11.237799, 9.985848, 8.898823, 7.423519, 5.995325,
4.773152, 3.744032, 2.938217, 2.294526, 1.762412,
1.327145, 1.020728, 0.765535, 0.570548, 0.425833,
0.313825, 0.232959, 0.171324, 0.128174, 0.092750,
0.067558, 0.049319, 0.036330, 0.027008, 0.019853,
0.015539, 0.011093, 0.008733, 0.007624, 0.008105,
0.005427, 0.004065, 0.003427, 0.002848, 0.002328,
0.001865, 0.001457, 0.001103, 0.000801, 0.000550,
0.000348, 0.000193, 0.000085, 0.000021, -0.000000,
];
/// <summary>
/// Gets the key-frame rate multiplier for an AV1 quantizer and sample precision.
/// </summary>
/// <param name="qIndex">The segment quantizer index.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
/// <returns>The rate multiplier.</returns>
public static int GetKeyFrameRateMultiplier(int qIndex, Av1BitDepth bitDepth)
{
int quantizer = Av1QuantizationLookup.GetDcQuant(qIndex, 0, bitDepth);
// Key frames use a quantizer-dependent weight over the squared DC step. High-bit-depth
// distortion is normalized back to the eight-bit domain, so its rate multiplier follows it.
long multiplier = (long)((quantizer * (long)quantizer) * (3.3 + (0.0015 * quantizer)));
int shift = (bitDepth.GetBitCount() - 8) * 2;
if (shift > 0)
{
multiplier = (multiplier + (1L << (shift - 1))) >> shift;
}
return (int)Math.Max(multiplier, 1);
}
/// <summary>
/// Gets the inter-frame rate multiplier for an AV1 quantizer and sample precision.
/// </summary>
/// <param name="qIndex">The segment quantizer index.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
/// <returns>The rate multiplier.</returns>
public static int GetInterFrameRateMultiplier(int qIndex, Av1BitDepth bitDepth)
{
int quantizer = Av1QuantizationLookup.GetDcQuant(qIndex, 0, bitDepth);
// Ordinary inter frames use a slightly lower rate weight than key frames, preserving more residual detail.
// Distortion remains normalized to the eight-bit domain before it is combined with this value.
long multiplier = (long)((quantizer * (long)quantizer) * (3.2 + (0.0015 * quantizer)));
int shift = (bitDepth.GetBitCount() - 8) * 2;
if (shift > 0)
{
multiplier = (multiplier + (1L << (shift - 1))) >> shift;
}
return (int)Math.Max(multiplier, 1);
}
/// <summary>
/// Gets a rate-distortion cost using the encoder probability-cost precision.
/// </summary>
/// <param name="rateMultiplier">The rate weight selected by the encoder quality model.</param>
/// <param name="rate">The syntax rate in 1/512-bit units.</param>
/// <param name="distortion">The sample-domain distortion.</param>
/// <returns>The rounded weighted rate plus distortion.</returns>
public static long GetCost(int rateMultiplier, int rate, long distortion)
{
long weightedRate = (long)rate * rateMultiplier;
long roundedRate = (weightedRate + (1 << (Av1ProbabilityCost.CostShift - 1))) >> Av1ProbabilityCost.CostShift;
return roundedRate + (distortion << 7);
}
/// <summary>
/// Gets the variance-domain cost of a full-pixel motion candidate.
/// </summary>
/// <param name="rateMultiplier">The rate weight selected by the encoder quality model.</param>
/// <param name="motionVectorRate">The motion-vector syntax rate in 1/512-bit units.</param>
/// <param name="variance">The normalized sample variance.</param>
/// <returns>The variance plus the motion-vector error cost.</returns>
public static int GetMotionSearchCost(int rateMultiplier, int motionVectorRate, int variance)
{
const int RateMultiplierShift = 6;
const int MotionErrorShift = 14;
int errorPerBit = Math.Max(rateMultiplier >> RateMultiplierShift, 1);
// Motion search compares pixel variance directly, so the syntax term is reduced to the same
// error domain instead of using the final mode-decision distortion scale.
long weightedRate = (long)motionVectorRate * errorPerBit;
int motionError = (int)((weightedRate + (1 << (MotionErrorShift - 1))) >> MotionErrorShift);
return variance + motionError;
}
/// <summary>
/// Gets the sum-of-absolute-differences rate scale for a frame quantizer.
/// </summary>
/// <param name="qIndex">The segment quantizer index.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
/// <returns>The multiplier that converts motion-vector rate into the absolute-difference domain.</returns>
public static int GetMotionSearchSadPerBit(int qIndex, Av1BitDepth bitDepth)
{
int quantizerDivisor = 1 << (bitDepth.GetBitCount() - 6);
double quantizer = Av1QuantizationLookup.GetAcQuant(qIndex, 0, bitDepth) / (double)quantizerDivisor;
return (int)((0.0418 * quantizer) + 2.4107);
}
/// <summary>
/// Gets the sum-of-absolute-differences cost of a full-pixel motion candidate.
/// </summary>
/// <param name="sadPerBit">The quantizer-derived motion-rate scale.</param>
/// <param name="motionVectorRate">The motion-vector syntax rate in 1/512-bit units.</param>
/// <param name="sumOfAbsoluteDifferences">The unnormalized sample-domain absolute difference.</param>
/// <returns>The absolute difference plus the motion-vector search cost.</returns>
public static int GetMotionSearchSadCost(int sadPerBit, int motionVectorRate, int sumOfAbsoluteDifferences)
{
const int MotionRateShift = 9;
// Full-pixel traversal uses absolute differences, so its quantizer-derived rate scale is deliberately
// distinct from the variance-domain error-per-bit scale used to compare the resulting search paths.
long weightedRate = (long)motionVectorRate * sadPerBit;
int motionError = (int)((weightedRate + (1 << (MotionRateShift - 1))) >> MotionRateShift);
return sumOfAbsoluteDifferences + motionError;
}
/// <summary>
/// Estimates residual rate and distortion from prediction error without running transforms or quantization.
/// </summary>
/// <param name="blockSize">The plane block geometry selecting the fitted rate curve.</param>
/// <param name="squaredError">The visible prediction error normalized to eight-bit precision.</param>
/// <param name="sampleCount">The number of visible samples contributing to the error.</param>
/// <param name="acQuantizer">The plane AC dequantization step at native sample precision.</param>
/// <param name="bitDepth">The native sample precision.</param>
/// <param name="rateMultiplier">The block's rate-distortion multiplier.</param>
/// <param name="rate">The estimated residual rate in 1/512-bit units.</param>
/// <param name="distortion">The estimated residual distortion in sixteenth-sample-error units.</param>
public static void ModelPredictionError(
Av1BlockSize blockSize,
long squaredError,
int sampleCount,
int acQuantizer,
Av1BitDepth bitDepth,
int rateMultiplier,
out int rate,
out long distortion)
{
if (squaredError == 0)
{
rate = 0;
distortion = 0;
return;
}
const double CurveStart = -15.5;
const double CurveStep = 0.5;
const double EndpointMargin = 1E-6;
const double HighErrorThreshold = 16;
const int DistortionScaleShift = 4;
// Transform dequantizers are scaled by eight. Normalize both their precision and the prediction error
// before taking the logarithmic feature, so the same fitted curves serve eight-, ten-, and twelve-bit input.
int quantizerStep = Math.Max(acQuantizer >> (bitDepth.GetBitCount() - 5), 1);
double normalizedError = (double)squaredError / sampleCount;
double feature = Math.Log2(normalizedError / ((double)quantizerStep * quantizerStep));
double lastCurvePosition = CurveStart + ((ModelCurveLength - 1) * CurveStep);
feature = Math.Clamp(feature, CurveStart + CurveStep + EndpointMargin, lastCurvePosition - CurveStep - EndpointMargin);
double position = (feature - CurveStart) / CurveStep;
int index = (int)position;
double fraction = position - index;
int rateCategory = ModelRateCategories[(int)blockSize];
int distortionCategory = normalizedError > HighErrorThreshold ? 1 : 0;
ReadOnlySpan<double> ratePoints = ModelRateCurves.Slice((rateCategory * ModelCurveLength) + index - 1, 4);
ReadOnlySpan<double> distortionPoints = ModelDistortionCurves.Slice((distortionCategory * ModelCurveLength) + index - 1, 4);
double rateEstimate;
double distortionEstimate;
if (Vector128.IsHardwareAccelerated)
{
// The two lanes evaluate rate and distortion together. Keep the cubic polynomial's operation order,
// including its separate multiplies and adds, so vector and scalar rounding agree at decision boundaries.
Vector128<double> p0 = Vector128.Create(ratePoints[0], distortionPoints[0]);
Vector128<double> p1 = Vector128.Create(ratePoints[1], distortionPoints[1]);
Vector128<double> p2 = Vector128.Create(ratePoints[2], distortionPoints[2]);
Vector128<double> p3 = Vector128.Create(ratePoints[3], distortionPoints[3]);
Vector128<double> x = Vector128.Create(fraction);
Vector128<double> cubic = (Vector128.Create(3.0) * (p1 - p2)) + p3 - p0;
Vector128<double> quadratic = (Vector128.Create(2.0) * p0) - (Vector128.Create(5.0) * p1) + (Vector128.Create(4.0) * p2) - p3;
Vector128<double> result = p1 + (Vector128.Create(0.5) * x * (p2 - p0 + (x * (quadratic + (x * cubic)))));
rateEstimate = result.GetElement(0);
distortionEstimate = result.GetElement(1);
}
else
{
rateEstimate = InterpolateModelCurve(ratePoints, fraction);
distortionEstimate = InterpolateModelCurve(distortionPoints, fraction);
}
rate = (int)(Math.Max(0, rateEstimate * sampleCount) + 0.5);
distortion = (long)(Math.Max(0, (distortionEstimate * normalizedError) * sampleCount) + 0.5);
long skipDistortion = squaredError << DistortionScaleShift;
// A modeled coded residual is useful only if it beats leaving the prediction unchanged. Preserve the
// reference model's zero-rate rule instead of returning an artificially low distortion for a skipped block.
if (rate == 0 || GetCost(rateMultiplier, rate, distortion) >= GetCost(rateMultiplier, 0, skipDistortion))
{
rate = 0;
distortion = skipDistortion;
}
}
/// <summary>
/// Evaluates one fitted curve's cubic segment without fusing arithmetic operations.
/// </summary>
private static double InterpolateModelCurve(ReadOnlySpan<double> points, double fraction)
{
double cubic = (3.0 * (points[1] - points[2])) + points[3] - points[0];
double quadratic = (2.0 * points[0]) - (5.0 * points[1]) + (4.0 * points[2]) - points[3];
return points[1] + (0.5 * fraction * (points[2] - points[0] + (fraction * (quadratic + (fraction * cubic)))));
}
}

62
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1RateDistortionStatistics.cs

@ -0,0 +1,62 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Holds the rate, distortion, and rounded cost of an encoder candidate.
/// </summary>
internal struct Av1RateDistortionStatistics
{
/// <summary>
/// Initializes a new instance of the <see cref="Av1RateDistortionStatistics"/> struct.
/// </summary>
/// <param name="rateMultiplier">The rate multiplier for the current block.</param>
/// <param name="rate">The estimated syntax rate in 1/512-bit units.</param>
/// <param name="distortion">The candidate distortion.</param>
public Av1RateDistortionStatistics(int rateMultiplier, int rate, long distortion)
{
this.Rate = rate;
this.Distortion = distortion;
this.Cost = Av1RateDistortion.GetCost(rateMultiplier, rate, distortion);
}
/// <summary>
/// Gets the sentinel for a candidate that cannot win a cost comparison.
/// </summary>
public static Av1RateDistortionStatistics Invalid => new()
{
Rate = int.MaxValue,
Distortion = long.MaxValue,
Cost = long.MaxValue
};
/// <summary>
/// Gets the estimated syntax rate in 1/512-bit units.
/// </summary>
public int Rate { get; private set; }
/// <summary>
/// Gets the candidate distortion.
/// </summary>
public long Distortion { get; private set; }
/// <summary>
/// Gets the rounded rate-distortion cost.
/// </summary>
public long Cost { get; private set; }
/// <summary>
/// Adds a valid candidate's rate and distortion and updates the combined cost.
/// </summary>
/// <param name="rateMultiplier">The rate multiplier for the combined candidate.</param>
/// <param name="other">The valid candidate to add.</param>
public void Add(int rateMultiplier, in Av1RateDistortionStatistics other)
{
// Round the combined rate only once. Adding the already rounded child costs can change
// partition and inter/intra decisions even when both children have the same reconstruction.
this.Rate += other.Rate;
this.Distortion += other.Distortion;
this.Cost = Av1RateDistortion.GetCost(rateMultiplier, this.Rate, this.Distortion);
}
}

1454
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolContextHelper.cs

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

@ -0,0 +1,290 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Reads AV1 literals and adaptively coded symbols from one bounded entropy-coded byte span.
/// </summary>
internal ref struct Av1SymbolReader
{
/// <summary>
/// The number of bits in the range-decoder code-value window.
/// </summary>
private const int DecoderWindowsSize = 32;
/// <summary>
/// The synthetic count used after the bounded input has been exhausted and zero padding begins.
/// </summary>
private const int LotsOfBits = 0x4000;
/// <summary>
/// The bounded entropy-coded bytes available to this reader.
/// </summary>
private readonly Span<byte> buffer;
/// <summary>
/// Indicates whether decoded symbols adapt their distributions.
/// </summary>
private readonly bool updateCdf;
/// <summary>
/// The next byte position to load into the code-value window.
/// </summary>
private int position;
/// <summary>
/// The difference between the upper end of the current range and the coded value, minus one.
/// </summary>
/// <remarks>
/// The decoder compares the upper 16 bits. Renormalization shifts consumed bits out and refills the lower portion
/// from <see cref="buffer"/> so the comparison remains aligned with <see cref="range"/>.
/// </remarks>
private uint difference;
/// <summary>
/// The number of code values in the current normalized interval.
/// </summary>
private uint range;
/// <summary>
/// The number of buffered bits below the 16-bit comparison window.
/// </summary>
private int count;
/// <summary>
/// The adjustment that preserves the logical consumed-bit count after the reader enters implicit zero padding.
/// </summary>
private int tellOffset;
/// <summary>
/// Initializes a new instance of the <see cref="Av1SymbolReader"/> struct over one entropy-coded span.
/// </summary>
/// <param name="span">The bounded entropy-coded bytes.</param>
/// <param name="updateCdf">A value indicating whether decoded symbols adapt their distributions.</param>
public Av1SymbolReader(Span<byte> span, bool updateCdf = true)
{
this.buffer = span;
this.updateCdf = updateCdf;
this.position = 0;
this.difference = (1U << (DecoderWindowsSize - 1)) - 1;
this.range = 0x8000;
this.count = -15;
this.tellOffset = 10 - (DecoderWindowsSize - 8);
this.Refill();
}
/// <summary>
/// Validates that range decoding remained within the bounded tile payload and ended at the required trailing-one bit.
/// </summary>
public void ValidateTrailingBits()
{
int consumedBitCount = this.GetConsumedBitCount();
int consumedByteCount = (consumedBitCount + 7) >> 3;
if (consumedByteCount > this.buffer.Length)
{
throw new InvalidImageContentException("The AV1 tile entropy stream is truncated.");
}
// The final consumed byte must contain one trailing-one bit at the range decoder's exact stopping position,
// followed only by zero bits. This is the same bounded-stream check performed after the reference decoder decodes a tile.
int trailingOneBit = 128 >> ((consumedBitCount - 1) & 7);
int trailingBitMask = (trailingOneBit << 1) - 1;
if ((this.buffer[consumedByteCount - 1] & trailingBitMask) != trailingOneBit)
{
throw new InvalidImageContentException("The AV1 tile entropy stream has invalid trailing bits.");
}
for (int i = consumedByteCount; i < this.buffer.Length; i++)
{
if (this.buffer[i] != 0)
{
throw new InvalidImageContentException("The AV1 tile entropy stream has nonzero padding bytes.");
}
}
}
/// <summary>
/// Reads one symbol and adapts its distribution when CDF updates are enabled.
/// </summary>
/// <param name="distribution">The inverse cumulative distribution for the symbol alphabet.</param>
/// <returns>The decoded zero-based symbol.</returns>
public int ReadSymbol(Av1Distribution distribution)
{
int value = this.DecodeIntegerQ15(distribution);
// disable_cdf_update freezes every tile distribution while leaving range decoding unchanged.
if (this.updateCdf)
{
distribution.Update(value);
}
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>
/// <param name="bitCount">The number of literal bits to read.</param>
/// <returns>The decoded literal.</returns>
public int ReadLiteral(int bitCount)
{
const uint prob = (0x7FFFFFU - (128 << 15) + 128) >> 8;
int literal = 0;
for (int bit = bitCount - 1; bit >= 0; bit--)
{
if (this.DecodeBoolQ15(prob))
{
literal |= 1 << bit;
}
}
return literal;
}
/// <summary>
/// Decode a single binary value.
/// </summary>
/// <param name="frequency">The probability that the bit is one, scaled by 32768.</param>
/// <returns>The decoded binary value.</returns>
private bool DecodeBoolQ15(uint frequency)
{
uint dif;
uint vw;
uint range;
uint newRange;
uint v;
bool ret;
dif = this.difference;
range = this.range;
// Reserve a minimum interval for both outcomes after reducing the Q15 frequency to the range-coder
// multiplication precision. This is the same rounding model used by Av1SymbolWriter.
v = ((range >> 8) * (frequency >> Av1Distribution.ProbabilityShift)) >> (7 - Av1Distribution.ProbabilityShift);
v += Av1Distribution.ProbabilityMinimum;
vw = v << (DecoderWindowsSize - 16);
ret = true;
newRange = v;
if (dif >= vw)
{
newRange = range - v;
dif -= vw;
ret = false;
}
this.Normalize(dif, newRange);
return ret;
}
/// <summary>
/// Decodes a symbol given an inverse cumulative distribution function(CDF) table in Q15.
/// </summary>
/// <param name="distribution">
/// CDF_PROB_TOP minus the CDF, such that symbol s falls in the range
/// [s > 0 ? (CDF_PROB_TOP - icdf[s - 1]) : 0, CDF_PROB_TOP - icdf[s]).
/// The values must be monotonically non - increasing, and icdf[nsyms - 1] must be 0.
/// </param>
/// <returns>The decoded symbol.</returns>
private int DecodeIntegerQ15(Av1Distribution distribution)
{
uint c;
uint u;
uint v;
int ret;
uint dif = this.difference;
uint r = this.range;
int n = distribution.NumberOfSymbols - 1;
DebugGuard.MustBeLessThan(dif >> (DecoderWindowsSize - 16), r, nameof(r));
DebugGuard.IsTrue(distribution[n] == 0, "Last value in probability array needs to be zero.");
DebugGuard.MustBeGreaterThanOrEqualTo(r, 32768U, nameof(r));
DebugGuard.MustBeGreaterThanOrEqualTo(7 - Av1Distribution.ProbabilityShift - Av1Distribution.CdfShift, 0, nameof(Av1Distribution.CdfShift));
c = dif >> (DecoderWindowsSize - 16);
v = r;
ret = -1;
do
{
u = v;
v = ((r >> 8) * (distribution[++ret] >> Av1Distribution.ProbabilityShift)) >> (7 - Av1Distribution.ProbabilityShift - Av1Distribution.CdfShift);
v += (uint)(Av1Distribution.ProbabilityMinimum * (n - ret));
}
while (c < v);
DebugGuard.MustBeLessThan(v, u, nameof(v));
DebugGuard.MustBeLessThanOrEqualTo(u, r, nameof(u));
r = u - v;
dif -= v << (DecoderWindowsSize - 16);
this.Normalize(dif, r);
return ret;
}
/// <summary>
/// Takes updated dif and range values, renormalizes them so that
/// <paramref name="rng"/> has value between 32768 and 65536 (reading more bytes from the stream into dif if
/// necessary), and stores them back in the decoder context.
/// </summary>
/// <param name="dif">The updated code-value difference.</param>
/// <param name="rng">The updated coding interval width.</param>
private void Normalize(uint dif, uint rng)
{
// Shifting by the leading-zero count restores the interval to [32768, 65536) and consumes the same number of
// code-value bits. Adding one before the shift preserves the decoder's difference-minus-one representation.
int d = 15 - Av1Math.MostSignificantBit(rng);
this.count -= d;
this.difference = ((dif + 1) << d) - 1;
this.range = rng << d;
if (this.count < 0)
{
this.Refill();
}
}
/// <summary>
/// Loads whole bytes into the lower portion of the code-value window after renormalization.
/// </summary>
private void Refill()
{
uint dif = this.difference;
int cnt = this.count;
int position = this.position;
int end = this.buffer.Length;
int s = DecoderWindowsSize - 9 - (cnt + 15);
for (; s >= 0 && position < end; s -= 8, position++)
{
// XOR inserts a source byte into the difference-minus-one representation. Advancing both the byte
// position and buffered-bit count leaves the logical number of consumed bits unchanged.
DebugGuard.MustBeLessThan(s, DecoderWindowsSize - 8, nameof(s));
dif ^= (uint)this.buffer[position] << s;
cnt += 8;
}
if (position >= end)
{
// AV1 range decoding permits the final interval to consume implicit zero padding. A large count models
// that padding without repeatedly attempting to refill it. Preserve the previous count in tellOffset so
// the logical position continues past the bounded source and truncated payloads remain detectable.
this.tellOffset += LotsOfBits - cnt;
cnt = LotsOfBits;
}
this.difference = dif;
this.count = cnt;
this.position = position;
}
/// <summary>
/// Gets the number of entropy bits consumed from the bounded tile payload, including the initial range-coder bit.
/// </summary>
/// <returns>The logical consumed-bit count.</returns>
private readonly int GetConsumedBitCount()
=> (this.position * 8) - this.count + this.tellOffset;
}

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

@ -0,0 +1,427 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Writes AV1 literals and adaptively coded symbols to a range-coded byte sequence.
/// </summary>
internal sealed class Av1SymbolWriter : IDisposable
{
/// <summary>
/// The normalized range before the first symbol narrows the coding interval.
/// </summary>
private const uint InitialRange = 0x8000U;
/// <summary>
/// The initial bit count that crosses the first byte-and-carry flush boundary after one output byte.
/// </summary>
private const int InitialCount = -9;
/// <summary>
/// The lower endpoint of the current coding interval.
/// </summary>
private ulong low;
/// <summary>
/// The width of the current normalized coding interval.
/// </summary>
private uint rng = InitialRange;
/// <summary>
/// The number of accumulated bits relative to the next byte-and-carry flush boundary.
/// </summary>
/// <remarks>
/// The initial value of -9 crosses zero after one output byte and its carry bit have accumulated.
/// </remarks>
private int cnt = InitialCount;
/// <summary>
/// The configuration that supplies output allocation.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// The owner of the output buffer shared by consecutively encoded tiles.
/// </summary>
private IMemoryOwner<byte> bufferOwner;
/// <summary>
/// The complete requested output allocation, including every consecutively encoded tile.
/// </summary>
private Memory<byte> outputBuffer;
/// <summary>
/// The requested output range, excluding any excess capacity returned by a pooling allocator.
/// </summary>
private Memory<byte> buffer;
/// <summary>
/// Indicates whether encoded symbols adapt their distributions.
/// </summary>
private readonly bool updateCdf;
/// <summary>
/// The next output byte position.
/// </summary>
private int position;
/// <summary>
/// Initializes a new instance of the <see cref="Av1SymbolWriter"/> class.
/// </summary>
/// <param name="configuration">The configuration that supplies output allocation.</param>
/// <param name="bufferLength">The initial output capacity in bytes.</param>
/// <param name="updateCdf">A value indicating whether encoded symbols adapt their distributions.</param>
public Av1SymbolWriter(Configuration configuration, int bufferLength, bool updateCdf)
{
this.configuration = configuration;
this.bufferOwner = configuration.MemoryAllocator.Allocate<byte>(bufferLength);
this.outputBuffer = this.bufferOwner.Memory[..bufferLength];
this.buffer = this.outputBuffer;
this.updateCdf = updateCdf;
}
/// <summary>
/// Restores the initial range-coder state and begins a new output sequence.
/// </summary>
public void Reset() => this.Reset(0);
/// <summary>
/// Restores the initial range-coder state and begins writing at an offset in the retained output allocation.
/// </summary>
/// <param name="outputOffset">The first byte available to the next range-coded tile.</param>
public void Reset(int outputOffset)
{
this.buffer = this.outputBuffer[outputOffset..];
this.low = 0;
this.rng = InitialRange;
this.cnt = InitialCount;
this.position = 0;
}
/// <summary>
/// Releases the tile output buffer.
/// </summary>
public void Dispose() => this.bufferOwner.Dispose();
/// <summary>
/// Writes one binary symbol and adapts its distribution when CDF updates are enabled.
/// </summary>
/// <param name="symbol">The binary symbol.</param>
/// <param name="distribution">The inverse cumulative distribution for the binary alphabet.</param>
public void WriteSymbol(bool symbol, Av1Distribution distribution)
=> this.WriteSymbol(symbol ? 1 : 0, distribution);
/// <summary>
/// Writes one symbol and adapts its distribution when CDF updates are enabled.
/// </summary>
/// <param name="symbol">The zero-based symbol.</param>
/// <param name="distribution">The inverse cumulative distribution for the symbol alphabet.</param>
public void WriteSymbol(int symbol, Av1Distribution distribution)
{
DebugGuard.MustBeGreaterThanOrEqualTo(symbol, 0, nameof(symbol));
DebugGuard.MustBeLessThan(symbol, distribution.NumberOfSymbols, nameof(symbol));
DebugGuard.IsTrue(distribution[distribution.NumberOfSymbols - 1] == 0, "Last entry in Probabilities table needs to be zero.");
this.EncodeIntegerQ15(symbol, distribution);
this.UpdateSymbol(symbol, distribution);
}
/// <summary>
/// Adapts a symbol distribution when probability updates are enabled, without emitting range-coded data.
/// </summary>
/// <param name="symbol">The zero-based symbol.</param>
/// <param name="distribution">The inverse cumulative distribution for the symbol alphabet.</param>
public void UpdateSymbol(int symbol, Av1Distribution distribution)
{
if (this.updateCdf)
{
distribution.Update(symbol);
}
}
/// <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>
/// <param name="value">The literal bit.</param>
public void WriteLiteral(bool value) => this.WriteLiteral(value ? 1u : 0u, 1);
/// <summary>
/// Writes the requested low-order bits in most-significant-bit-first order.
/// </summary>
/// <param name="value">The unsigned literal value.</param>
/// <param name="bitCount">The number of low-order bits to write.</param>
public void WriteLiteral(uint value, int bitCount)
{
const uint p = 0x4000U; // (0x7FFFFFU - (128 << 15) + 128) >> 8;
for (int bit = bitCount - 1; bit >= 0; bit--)
{
bool bitValue = ((value >> bit) & 0x1) > 0;
this.EncodeBoolQ15(bitValue, p);
}
}
/// <summary>
/// Terminates the range-coded sequence and propagates pending carries into an owned byte buffer.
/// </summary>
/// <returns>An owner containing the shortest byte sequence that preserves every encoded symbol.</returns>
public IMemoryOwner<byte> Exit()
{
int length = this.FinalizeRange();
IMemoryOwner<byte> output = this.configuration.MemoryAllocator.Allocate<byte>(length);
this.buffer.Span[..length].CopyTo(output.Memory.Span);
return output;
}
/// <summary>
/// Finalizes the range-coded sequence and exposes its encoded prefix without copying.
/// </summary>
/// <param name="length">The number of encoded bytes in the returned memory.</param>
/// <returns>The encoded prefix, valid until this writer is reset or disposed.</returns>
public ReadOnlyMemory<byte> Exit(out int length)
{
length = this.FinalizeRange();
return this.buffer[..length];
}
/// <summary>
/// Exposes a prefix containing consecutively encoded tiles without copying their bytes.
/// </summary>
/// <param name="length">The number of bytes in the prefix.</param>
/// <returns>The encoded prefix, valid until this writer is reset or disposed.</returns>
public ReadOnlyMemory<byte> GetOutput(int length) => this.outputBuffer[..length];
/// <summary>
/// Terminates the range-coded sequence in the current output allocation.
/// </summary>
/// <returns>The number of encoded bytes in the allocation.</returns>
private int FinalizeRange()
{
// Round the low endpoint into the current interval so the emitted prefix selects every symbol encoded so far,
// regardless of the bits that follow it.
ulong l = this.low;
int c = this.cnt;
int pos = this.position;
int s = 10;
ulong m = 0x3FFFU;
ulong e = ((l + m) & ~m) | (m + 1);
s += c;
int pendingByteCount = Math.Max((s + 7) >> 3, 0);
if (pos + pendingByteCount > this.buffer.Length)
{
// Finalization needs only the terminating bytes; ordinary word flushes reserve their own headroom.
this.ResizeBuffer(pos + pendingByteCount);
}
Span<byte> buffer = this.buffer.Span[..(pos + pendingByteCount)];
if (s > 0)
{
ulong n = (1UL << (c + 16)) - 1;
do
{
ushort value = (ushort)(e >> (c + 16));
buffer[pos] = (byte)value;
if ((value & 0x100) != 0)
{
PropagateCarryBackward(buffer, pos - 1);
}
pos++;
e &= n;
s -= 8;
c -= 8;
n >>= 8;
}
while (s > 0);
}
return pos;
}
/// <summary>
/// Encode a single binary value.
/// </summary>
/// <param name="val">The value to encode.</param>
/// <param name="frequency">The probability that the value is true, scaled by 32768.</param>
private void EncodeBoolQ15(bool val, uint frequency)
{
ulong l;
uint r;
uint v;
DebugGuard.MustBeGreaterThan(frequency, 0U, nameof(frequency));
DebugGuard.MustBeLessThanOrEqualTo(frequency, 32768U, nameof(frequency));
l = this.low;
r = this.rng;
DebugGuard.MustBeGreaterThanOrEqualTo(r, 32768U, nameof(r));
// Reduce the Q15 frequency to the range-coder multiplication precision and retain a nonzero interval for
// both outcomes. Av1SymbolReader applies the identical rounding model.
v = ((r >> 8) * (frequency >> Av1Distribution.ProbabilityShift)) >> (7 - Av1Distribution.ProbabilityShift);
v += Av1Distribution.ProbabilityMinimum;
if (val)
{
l += r - v;
r = v;
}
else
{
r -= v;
}
this.Normalize(l, r);
}
/// <summary>
/// Encodes a symbol given an inverse cumulative distribution function(CDF) table in Q15.
/// </summary>
/// <param name="symbol">The value to encode.</param>
/// <param name="distribution">
/// CDF_PROB_TOP minus the CDF, such that symbol s falls in the range
/// [s > 0 ? (CDF_PROB_TOP - icdf[s - 1]) : 0, CDF_PROB_TOP - icdf[s]).
/// The values must be monotonically non - increasing, and icdf[nsyms - 1] must be 0.
/// </param>
private void EncodeIntegerQ15(int symbol, Av1Distribution distribution)
=> this.EncodeIntegerQ15(symbol > 0 ? distribution[symbol - 1] : Av1Distribution.ProbabilityTop, distribution[symbol], symbol, distribution.NumberOfSymbols);
/// <summary>
/// Narrows the coding interval to one symbol's inverse-cumulative bounds.
/// </summary>
/// <param name="lowFrequency">The inverse cumulative threshold preceding the symbol.</param>
/// <param name="highFrequency">The inverse cumulative threshold following the symbol.</param>
/// <param name="symbol">The zero-based symbol.</param>
/// <param name="numberOfSymbols">The size of the symbol alphabet.</param>
private void EncodeIntegerQ15(uint lowFrequency, uint highFrequency, int symbol, int numberOfSymbols)
{
const int totalShift = 7 - Av1Distribution.ProbabilityShift - Av1Distribution.CdfShift;
ulong l = this.low;
uint r = this.rng;
DebugGuard.MustBeLessThanOrEqualTo(32768U, r, nameof(r));
DebugGuard.MustBeLessThanOrEqualTo(highFrequency, lowFrequency, nameof(highFrequency));
DebugGuard.MustBeLessThanOrEqualTo(lowFrequency, 32768U, nameof(lowFrequency));
DebugGuard.MustBeGreaterThanOrEqualTo(totalShift, 0, nameof(totalShift));
int n = numberOfSymbols - 1;
if (lowFrequency < Av1Distribution.ProbabilityTop)
{
uint u;
uint v;
u = (uint)((((r >> 8) * (lowFrequency >> Av1Distribution.ProbabilityShift)) >> totalShift) +
(Av1Distribution.ProbabilityMinimum * (n - (symbol - 1))));
v = (uint)((((r >> 8) * (highFrequency >> Av1Distribution.ProbabilityShift)) >> totalShift) +
(Av1Distribution.ProbabilityMinimum * (n - symbol)));
l += r - u;
r = u - v;
}
else
{
r -= (uint)((((r >> 8) * (highFrequency >> Av1Distribution.ProbabilityShift)) >> totalShift) +
(Av1Distribution.ProbabilityMinimum * (n - symbol)));
}
this.Normalize(l, r);
}
/// <summary>
/// Takes updated low and range values, renormalizes them so that <paramref name="rng"/>
/// lies between 32768 and 65536 (flushing bytes from low to the pre-carry buffer if necessary),
/// and stores them back in the encoder context.
/// </summary>
/// <param name="low">The new value of <see cref="low"/>.</param>
/// <param name="rng">The new value of <see cref="rng"/>.</param>
private void Normalize(ulong low, uint rng)
{
int c = this.cnt;
DebugGuard.MustBeLessThanOrEqualTo(rng, 65535U, nameof(rng));
int d = 15 - Av1Math.MostSignificantBit(rng);
int s = c + d;
// Keeping 16 bits free for the next symbol allows the 64-bit coding window to flush up to eight completed
// bytes together while preserving one carry bit.
if (s >= 40)
{
if (this.position + sizeof(ulong) > this.buffer.Length)
{
// A word store touches eight bytes even when fewer become logical output. Double the current
// tile capacity and add one word, matching the range coder's amortized growth from an empty buffer.
this.ResizeBuffer(checked((2 * this.buffer.Length) + sizeof(ulong)));
}
Span<byte> buffer = this.buffer.Span[..(this.position + sizeof(ulong))];
int readyByteCount = (s >> 3) + 1;
c += 24 - (readyByteCount << 3);
ulong output = low >> c;
low &= (1UL << c) - 1;
ulong carryMask = 1UL << (readyByteCount << 3);
bool hasCarry = (output & carryMask) != 0;
output &= carryMask - 1;
// Writing one big-endian word avoids a byte-at-a-time hot loop. Only readyByteCount bytes become part
// of the logical output; the following bytes are overwritten by the next flush.
BinaryPrimitives.WriteUInt64BigEndian(
buffer.Slice(this.position, sizeof(ulong)),
output << ((sizeof(ulong) - readyByteCount) << 3));
if (hasCarry)
{
PropagateCarryBackward(buffer, this.position - 1);
}
this.position += readyByteCount;
s = c + d - 24;
}
this.low = low << d;
this.rng = rng << d;
this.cnt = s;
}
/// <summary>
/// Replaces the output owner while retaining finalized tiles and the current tile's completed bytes.
/// </summary>
/// <param name="tileCapacity">The required capacity starting at the current tile's output offset.</param>
private void ResizeBuffer(int tileCapacity)
{
int outputOffset = this.outputBuffer.Length - this.buffer.Length;
int capacity = checked(outputOffset + tileCapacity);
IMemoryOwner<byte> replacement = this.configuration.MemoryAllocator.Allocate<byte>(capacity);
Memory<byte> replacementBuffer = replacement.Memory[..capacity];
// Previous tile bytes remain part of the frame payload. The current tile's completed prefix also carries
// backward into earlier bytes, so preserve that prefix before returning the old owner. Pending bits stay
// in low/cnt and need no copy. If allocation fails, the original owner remains available for disposal.
this.outputBuffer.Span[..(outputOffset + this.position)].CopyTo(replacementBuffer.Span);
this.bufferOwner.Dispose();
this.bufferOwner = replacement;
this.outputBuffer = replacementBuffer;
this.buffer = replacementBuffer[outputOffset..];
}
/// <summary>
/// Adds a carry to the completed output prefix.
/// </summary>
/// <param name="buffer">The accumulated output bytes.</param>
/// <param name="offset">The final completed byte.</param>
private static void PropagateCarryBackward(Span<byte> buffer, int offset)
{
int carry;
do
{
int sum = buffer[offset] + 1;
buffer[offset] = (byte)sum;
carry = sum >> 8;
offset--;
}
while (carry != 0);
}
}

22
src/ImageSharp/Formats/Heif/Av1/IAv1TileReader.cs

@ -0,0 +1,22 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Defines tile-payload consumption and completion for one coded AV1 frame.
/// </summary>
internal interface IAv1TileReader
{
/// <summary>
/// Reads one entropy-coded tile payload into the current frame state.
/// </summary>
/// <param name="tileData">The bounded bitstream bytes belonging to the tile.</param>
/// <param name="tileNum">The zero-based tile index in raster order.</param>
void ReadTile(Span<byte> tileData, int tileNum);
/// <summary>
/// Completes the current coded frame after all tile payloads have been read and releases frame-scoped resources.
/// </summary>
void CompleteFrame();
}

19
src/ImageSharp/Formats/Heif/Av1/IAv1TileWriter.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Interface for writing of image tiles.
/// </summary>
internal interface IAv1TileWriter
{
/// <summary>
/// Gets the encoded bytes for a single tile.
/// </summary>
/// <param name="tileNum">The index of the encoded tile.</param>
/// <returns>
/// The bytes of encoded data in the bitstream dedicated to this tile.
/// </returns>
ReadOnlySpan<byte> GetTileData(int tileNum);
}

530
src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionParameters.cs

@ -0,0 +1,530 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
/// <summary>
/// Stores one AV1 global-motion model in the codec's fixed-point affine matrix domain.
/// </summary>
internal struct Av1GlobalMotionParameters
{
/// <summary>
/// The number of fractional bits carried by every stored matrix parameter.
/// </summary>
public const int ModelPrecisionBits = 16;
/// <summary>
/// The fixed-point representation of one in the global-motion matrix domain.
/// </summary>
public const int ModelScale = 1 << ModelPrecisionBits;
/// <summary>
/// The initial finite-subexponential group width used by every global-motion parameter.
/// </summary>
public const int SubexponentialGroupBitCount = 3;
/// <summary>
/// The finite signed-domain size parameter for coded affine coefficients.
/// </summary>
public const int AlphaValueMagnitude = (1 << 12) + 1;
/// <summary>
/// The number of fractional bits carried by coded affine coefficients.
/// </summary>
public const int AlphaPrecisionBits = 15;
/// <summary>
/// The precision increase from a coded affine coefficient to the stored matrix.
/// </summary>
public const int AlphaPrecisionDifference = ModelPrecisionBits - AlphaPrecisionBits;
/// <summary>
/// The scale factor that restores a coded affine coefficient to the stored matrix precision.
/// </summary>
public const int AlphaDecodeFactor = 1 << AlphaPrecisionDifference;
/// <summary>
/// The signed magnitude bit count of a general affine model's translation components.
/// </summary>
public const int AbsoluteTranslationBits = 12;
/// <summary>
/// The signed magnitude bit count of a translation-only model before precision adjustment.
/// </summary>
public const int AbsoluteTranslationOnlyBits = 9;
/// <summary>
/// The number of fractional bits carried by general affine translation components.
/// </summary>
public const int TranslationPrecisionBits = 6;
/// <summary>
/// The number of fractional bits carried by translation-only components.
/// </summary>
public const int TranslationOnlyPrecisionBits = 3;
/// <summary>
/// The number of low-order bits removed from the derived shear parameters.
/// </summary>
private const int ShearParameterReductionBits = 6;
/// <summary>
/// The number of fractional bits carried by entries in <see cref="ReciprocalTable"/>.
/// </summary>
private const int ReciprocalPrecisionBits = 14;
/// <summary>
/// The number of divisor-fraction bits used to index <see cref="ReciprocalTable"/>.
/// </summary>
private const int ReciprocalIndexBits = 8;
/// <summary>
/// The largest difference between a retained neighbor motion vector and the current block motion vector.
/// </summary>
private const int LocalProjectionMotionVectorLimit = 256;
/// <summary>
/// The maximum magnitude of a non-diagonal affine coefficient relative to the identity matrix.
/// </summary>
private const int NonDiagonalAffineClamp = 1 << (ModelPrecisionBits - 3);
/// <summary>
/// The exclusive upper magnitude of either translation coefficient.
/// </summary>
private const int TranslationClamp = 128 << ModelPrecisionBits;
/// <summary>
/// The six parameters ordered as horizontal translation, vertical translation, and the four affine coefficients.
/// </summary>
private InlineArray6<int> matrix;
/// <summary>
/// Gets an identity global-motion model.
/// </summary>
public static Av1GlobalMotionParameters Identity
{
get
{
Av1GlobalMotionParameters result = default;
result.matrix[2] = ModelScale;
result.matrix[5] = ModelScale;
return result;
}
}
/// <summary>
/// Gets or sets the geometric model represented by the matrix parameters.
/// </summary>
public Av1GlobalMotionType Type { get; set; }
/// <summary>
/// Gets the reduced horizontal scale delta used by warped prediction.
/// </summary>
public short Alpha { get; private set; }
/// <summary>
/// Gets the reduced horizontal shear used by warped prediction.
/// </summary>
public short Beta { get; private set; }
/// <summary>
/// Gets the reduced vertical shear used by warped prediction.
/// </summary>
public short Gamma { get; private set; }
/// <summary>
/// Gets the reduced vertical scale delta used by warped prediction.
/// </summary>
public short Delta { get; private set; }
/// <summary>
/// Gets a value indicating whether the affine model violates AV1's permitted shear bounds.
/// </summary>
public bool IsInvalid { get; private set; }
/// <summary>
/// Gets the fixed-point reciprocal lookup used by AV1's affine shear derivation.
/// </summary>
private static ReadOnlySpan<ushort> ReciprocalTable =>
[
16384, 16320, 16257, 16194, 16132, 16070, 16009, 15948, 15888, 15828, 15768,
15709, 15650, 15592, 15534, 15477, 15420, 15364, 15308, 15252, 15197, 15142,
15087, 15033, 14980, 14926, 14873, 14821, 14769, 14717, 14665, 14614, 14564,
14513, 14463, 14413, 14364, 14315, 14266, 14218, 14170, 14122, 14075, 14028,
13981, 13935, 13888, 13843, 13797, 13752, 13707, 13662, 13618, 13574, 13530,
13487, 13443, 13400, 13358, 13315, 13273, 13231, 13190, 13148, 13107, 13066,
13026, 12985, 12945, 12906, 12866, 12827, 12788, 12749, 12710, 12672, 12633,
12596, 12558, 12520, 12483, 12446, 12409, 12373, 12336, 12300, 12264, 12228,
12193, 12157, 12122, 12087, 12053, 12018, 11984, 11950, 11916, 11882, 11848,
11815, 11782, 11749, 11716, 11683, 11651, 11619, 11586, 11555, 11523, 11491,
11460, 11429, 11398, 11367, 11336, 11305, 11275, 11245, 11215, 11185, 11155,
11125, 11096, 11067, 11038, 11009, 10980, 10951, 10923, 10894, 10866, 10838,
10810, 10782, 10755, 10727, 10700, 10673, 10645, 10618, 10592, 10565, 10538,
10512, 10486, 10460, 10434, 10408, 10382, 10356, 10331, 10305, 10280, 10255,
10230, 10205, 10180, 10156, 10131, 10107, 10082, 10058, 10034, 10010, 9986,
9963, 9939, 9916, 9892, 9869, 9846, 9823, 9800, 9777, 9754, 9732, 9709, 9687,
9664, 9642, 9620, 9598, 9576, 9554, 9533, 9511, 9489, 9468, 9447, 9425, 9404,
9383, 9362, 9341, 9321, 9300, 9279, 9259, 9239, 9218, 9198, 9178, 9158, 9138,
9118, 9098, 9079, 9059, 9039, 9020, 9001, 8981, 8962, 8943, 8924, 8905, 8886,
8867, 8849, 8830, 8812, 8793, 8775, 8756, 8738, 8720, 8702, 8684, 8666, 8648,
8630, 8613, 8595, 8577, 8560, 8542, 8525, 8508, 8490, 8473, 8456, 8439, 8422,
8405, 8389, 8372, 8355, 8339, 8322, 8306, 8289, 8273, 8257, 8240, 8224, 8208,
8192,
];
/// <summary>
/// Gets or sets a matrix parameter in AV1 affine-transform order.
/// </summary>
/// <param name="index">The zero-based matrix parameter index.</param>
/// <returns>The fixed-point matrix parameter.</returns>
public int this[int index]
{
get => this.matrix[index];
set => this.matrix[index] = value;
}
/// <summary>
/// Gets the translational motion vector represented by this model at the center of a coding block.
/// </summary>
/// <param name="allowHighPrecisionMotionVector">
/// A value indicating whether motion vectors may retain one-eighth-sample precision.
/// </param>
/// <param name="blockSize">The coding block size.</param>
/// <param name="modeInfoPosition">The block origin in 4x4 mode-information units.</param>
/// <param name="forceIntegerMotionVector">
/// A value indicating whether the result is rounded to an integer-sample displacement.
/// </param>
/// <returns>The global motion vector in one-eighth-sample units.</returns>
public readonly Av1MotionVector GetMotionVector(
bool allowHighPrecisionMotionVector,
Av1BlockSize blockSize,
Point modeInfoPosition,
bool forceIntegerMotionVector)
{
if (this.Type == Av1GlobalMotionType.Identity)
{
return default;
}
int row;
int column;
if (this.Type == Av1GlobalMotionType.Translation)
{
// AV1 accidentally assigns the horizontal translation parameter to the row component and the vertical
// parameter to the column component. Decoders preserve that published bitstream behavior for conformance.
row = this.matrix[0] >> (ModelPrecisionBits - 3);
column = this.matrix[1] >> (ModelPrecisionBits - 3);
}
else
{
int blockCenterX = (modeInfoPosition.X << Av1Constants.ModeInfoSizeLog2) + (blockSize.GetWidth() >> 1) - 1;
int blockCenterY = (modeInfoPosition.Y << Av1Constants.ModeInfoSizeLog2) + (blockSize.GetHeight() >> 1) - 1;
int horizontal = ((this.matrix[2] - ModelScale) * blockCenterX) +
(this.matrix[3] * blockCenterY) +
this.matrix[0];
int vertical = (this.matrix[4] * blockCenterX) +
((this.matrix[5] - ModelScale) * blockCenterY) +
this.matrix[1];
int precisionBits = allowHighPrecisionMotionVector ? ModelPrecisionBits - 3 : ModelPrecisionBits - 2;
column = Av1Math.RoundPowerOf2Signed(horizontal, precisionBits);
row = Av1Math.RoundPowerOf2Signed(vertical, precisionBits);
if (!allowHighPrecisionMotionVector)
{
column *= 2;
row *= 2;
}
}
return new Av1MotionVector(row, column).LowerPrecision(
allowHighPrecision: allowHighPrecisionMotionVector,
forceInteger: forceIntegerMotionVector);
}
/// <summary>
/// Derives the local affine model for a warped inter block from its spatial neighbor samples.
/// </summary>
/// <param name="sourcePoints">The neighbor-center positions relative to the current block in one-eighth-sample units.</param>
/// <param name="referencePoints">The corresponding positions in the selected reference frame.</param>
/// <param name="blockSize">The current coding block size.</param>
/// <param name="motionVector">The current block motion vector in one-eighth-sample units.</param>
/// <param name="modeInfoPosition">The current block origin in 4x4 mode-information units.</param>
/// <returns>The derived affine model, marked invalid when AV1's projection or shear constraints cannot be satisfied.</returns>
public static Av1GlobalMotionParameters DeriveLocalProjection(
ReadOnlySpan<Point> sourcePoints,
ReadOnlySpan<Point> referencePoints,
Av1BlockSize blockSize,
Av1MotionVector motionVector,
Point modeInfoPosition)
{
Av1GlobalMotionParameters result = Identity;
result.Type = Av1GlobalMotionType.Affine;
int blockWidth = blockSize.GetWidth();
int blockHeight = blockSize.GetHeight();
int sampleThreshold = Math.Clamp(Math.Max(blockWidth, blockHeight), 16, 112);
bool hasSelectedSample = sourcePoints.Length == 1;
if (sourcePoints.Length > 1)
{
for (int index = 0; index < sourcePoints.Length; index++)
{
int difference = Math.Abs(referencePoints[index].X - sourcePoints[index].X - motionVector.Column) +
Math.Abs(referencePoints[index].Y - sourcePoints[index].Y - motionVector.Row);
hasSelectedSample |= difference <= sampleThreshold;
}
}
int sourceCenterX = ((blockWidth >> 1) - 1) << 3;
int sourceCenterY = ((blockHeight >> 1) - 1) << 3;
int referenceCenterX = sourceCenterX + motionVector.Column;
int referenceCenterY = sourceCenterY + motionVector.Row;
int a00 = 0;
int a01 = 0;
int a11 = 0;
int bx0 = 0;
int bx1 = 0;
int by0 = 0;
int by1 = 0;
for (int index = 0; index < sourcePoints.Length; index++)
{
int motionVectorDifference = Math.Abs(referencePoints[index].X - sourcePoints[index].X - motionVector.Column) +
Math.Abs(referencePoints[index].Y - sourcePoints[index].Y - motionVector.Row);
// av1_selectSamples retains the original first sample when every candidate exceeds the threshold. Keeping
// that rule here is important because the selected Warped syntax still requires a deterministic model.
if (sourcePoints.Length > 1 && motionVectorDifference > sampleThreshold && (hasSelectedSample || index != 0))
{
continue;
}
int sourceX = sourcePoints[index].X - sourceCenterX;
int sourceY = sourcePoints[index].Y - sourceCenterY;
int referenceX = referencePoints[index].X - referenceCenterX;
int referenceY = referencePoints[index].Y - referenceCenterY;
if (Math.Abs(sourceX - referenceX) >= LocalProjectionMotionVectorLimit ||
Math.Abs(sourceY - referenceY) >= LocalProjectionMotionVectorLimit)
{
continue;
}
// These biased products are the normative reduced-precision P'P, P'q, and P'r matrices. Computing them
// directly preserves the reference decoder's integer least-squares rounding instead of introducing floating-point drift.
a00 += LeastSquaresSquare(sourceX);
a01 += LeastSquaresProduct1(sourceX, sourceY);
a11 += LeastSquaresSquare(sourceY);
bx0 += LeastSquaresProduct2(sourceX, referenceX);
bx1 += LeastSquaresProduct1(sourceY, referenceX);
by0 += LeastSquaresProduct1(sourceX, referenceY);
by1 += LeastSquaresProduct2(sourceY, referenceY);
}
long determinant = ((long)a00 * a11) - ((long)a01 * a01);
if (determinant == 0)
{
result.IsInvalid = true;
return result;
}
int inverseDeterminant = ResolveDivisor((ulong)Math.Abs(determinant), out int determinantShift) *
(determinant < 0 ? -1 : 1);
determinantShift -= ModelPrecisionBits;
if (determinantShift < 0)
{
inverseDeterminant <<= -determinantShift;
determinantShift = 0;
}
long projectionX0 = ((long)a11 * bx0) - ((long)a01 * bx1);
long projectionX1 = -((long)a01 * bx0) + ((long)a00 * bx1);
long projectionY0 = ((long)a11 * by0) - ((long)a01 * by1);
long projectionY1 = -((long)a01 * by0) + ((long)a00 * by1);
result.matrix[2] = ResolveProjectionCoefficient(
projectionX0,
inverseDeterminant,
determinantShift,
ModelScale - NonDiagonalAffineClamp + 1,
ModelScale + NonDiagonalAffineClamp - 1);
result.matrix[3] = ResolveProjectionCoefficient(
projectionX1,
inverseDeterminant,
determinantShift,
-NonDiagonalAffineClamp + 1,
NonDiagonalAffineClamp - 1);
result.matrix[4] = ResolveProjectionCoefficient(
projectionY0,
inverseDeterminant,
determinantShift,
-NonDiagonalAffineClamp + 1,
NonDiagonalAffineClamp - 1);
result.matrix[5] = ResolveProjectionCoefficient(
projectionY1,
inverseDeterminant,
determinantShift,
ModelScale - NonDiagonalAffineClamp + 1,
ModelScale + NonDiagonalAffineClamp - 1);
int absoluteCenterX = (modeInfoPosition.X << Av1Constants.ModeInfoSizeLog2) + (blockWidth >> 1) - 1;
int absoluteCenterY = (modeInfoPosition.Y << Av1Constants.ModeInfoSizeLog2) + (blockHeight >> 1) - 1;
int horizontalTranslation = (motionVector.Column << (ModelPrecisionBits - 3)) -
(absoluteCenterX * (result.matrix[2] - ModelScale)) -
(absoluteCenterY * result.matrix[3]);
int verticalTranslation = (motionVector.Row << (ModelPrecisionBits - 3)) -
(absoluteCenterX * result.matrix[4]) -
(absoluteCenterY * (result.matrix[5] - ModelScale));
result.matrix[0] = Math.Clamp(horizontalTranslation, -TranslationClamp, TranslationClamp - 1);
result.matrix[1] = Math.Clamp(verticalTranslation, -TranslationClamp, TranslationClamp - 1);
result.UpdateShearParameters();
return result;
}
/// <summary>
/// Derives the reduced shear parameters and records whether the complete affine model is valid.
/// </summary>
public void UpdateShearParameters()
{
Span<int> values = this.matrix;
this.Alpha = 0;
this.Beta = 0;
this.Gamma = 0;
this.Delta = 0;
if (values[2] <= 0)
{
this.IsInvalid = true;
return;
}
this.Alpha = (short)Math.Clamp(values[2] - ModelScale, short.MinValue, short.MaxValue);
this.Beta = (short)Math.Clamp(values[3], short.MinValue, short.MaxValue);
// AV1 derives gamma and delta by multiplying with a fixed-precision reciprocal of the horizontal scale.
// The reciprocal lookup is normative; integer division would produce different warped sample positions.
int reciprocal = ResolveDivisor((uint)values[2], out int reciprocalShift);
long scaledVerticalCoefficient = (long)values[4] * ModelScale * reciprocal;
this.Gamma = (short)Math.Clamp(RoundPowerOf2Signed(scaledVerticalCoefficient, reciprocalShift), short.MinValue, short.MaxValue);
long scaledCrossCoefficient = (long)values[3] * values[4] * reciprocal;
long verticalScaleDelta = values[5] - RoundPowerOf2Signed(scaledCrossCoefficient, reciprocalShift) - ModelScale;
this.Delta = (short)Math.Clamp(verticalScaleDelta, short.MinValue, short.MaxValue);
// Warped filtering addresses a coarser parameter grid than the stored affine matrix. Symmetric rounding is
// required here so negative shear values are quantized identically to their positive counterparts.
this.Alpha = ReduceShearParameter(this.Alpha);
this.Beta = ReduceShearParameter(this.Beta);
this.Gamma = ReduceShearParameter(this.Gamma);
this.Delta = ReduceShearParameter(this.Delta);
// These weighted L1 bounds are the AV1 validity test for the two shear axes. Equality is invalid because the
// warped-filter footprint would no longer remain inside the permitted affine sampling envelope.
this.IsInvalid =
((4 * Math.Abs((int)this.Alpha)) + (7 * Math.Abs((int)this.Beta)) >= ModelScale) ||
((4 * Math.Abs((int)this.Gamma)) + (4 * Math.Abs((int)this.Delta)) >= ModelScale);
}
/// <summary>
/// Quantizes one signed shear parameter to AV1's warped-filter precision.
/// </summary>
/// <param name="value">The full-precision shear parameter.</param>
/// <returns>The reduced shear parameter.</returns>
private static short ReduceShearParameter(short value)
=> (short)(RoundPowerOf2Signed(value, ShearParameterReductionBits) * (1 << ShearParameterReductionBits));
/// <summary>
/// Resolves a positive divisor into AV1's fixed-point reciprocal representation.
/// </summary>
/// <param name="divisor">The positive divisor.</param>
/// <param name="shift">Receives the reciprocal's binary scale.</param>
/// <returns>The fixed-point reciprocal multiplier.</returns>
private static int ResolveDivisor(uint divisor, out int shift)
{
// Normalize the divisor around its highest set bit, then quantize the remaining fraction to the normative
// eight-bit table index. Adding the table's fourteen fractional bits yields the scale used by the caller's
// rounded multiply instead of a platform-dependent integer division.
shift = BitOperations.Log2(divisor);
int remainder = (int)(divisor - (1U << shift));
int reciprocalIndex = shift > ReciprocalIndexBits
? RoundPowerOf2(remainder, shift - ReciprocalIndexBits)
: remainder << (ReciprocalIndexBits - shift);
shift += ReciprocalPrecisionBits;
return ReciprocalTable[reciprocalIndex];
}
/// <summary>
/// Resolves a positive 64-bit divisor into AV1's fixed-point reciprocal representation.
/// </summary>
/// <param name="divisor">The positive divisor.</param>
/// <param name="shift">Receives the reciprocal's binary scale.</param>
/// <returns>The fixed-point reciprocal multiplier.</returns>
private static int ResolveDivisor(ulong divisor, out int shift)
{
shift = BitOperations.Log2(divisor);
ulong remainder = divisor - (1UL << shift);
int reciprocalIndex = shift > ReciprocalIndexBits
? (int)((remainder + (1UL << (shift - ReciprocalIndexBits - 1))) >> (shift - ReciprocalIndexBits))
: (int)(remainder << (ReciprocalIndexBits - shift));
shift += ReciprocalPrecisionBits;
return ReciprocalTable[reciprocalIndex];
}
/// <summary>
/// Resolves one adjugate numerator into a clamped affine matrix coefficient.
/// </summary>
private static int ResolveProjectionCoefficient(long numerator, int inverseDeterminant, int shift, int minimum, int maximum)
{
long product = numerator * inverseDeterminant;
long value = shift > 0 ? RoundPowerOf2Signed(product, shift) : product << -shift;
return (int)Math.Clamp(value, minimum, maximum);
}
/// <summary>
/// Computes one reduced-precision diagonal element of the local projection matrix.
/// </summary>
private static int LeastSquaresSquare(int value)
=> ((value * value * 4) + (value * 32) + 128) >> 4;
/// <summary>
/// Computes one reduced-precision off-diagonal product of the local projection matrix.
/// </summary>
private static int LeastSquaresProduct1(int first, int second)
=> ((first * second * 4) + ((first + second) * 16) + 64) >> 4;
/// <summary>
/// Computes one reduced-precision source-to-reference product of the local projection matrix.
/// </summary>
private static int LeastSquaresProduct2(int first, int second)
=> ((first * second * 4) + ((first + second) * 16) + 128) >> 4;
/// <summary>
/// Divides a nonnegative integer by a power of two with nearest-integer rounding.
/// </summary>
/// <param name="value">The nonnegative value.</param>
/// <param name="bitCount">The base-two divisor exponent.</param>
/// <returns>The rounded quotient.</returns>
private static int RoundPowerOf2(int value, int bitCount)
=> (value + ((1 << bitCount) >> 1)) >> bitCount;
/// <summary>
/// Divides a signed integer by a power of two with symmetric nearest-integer rounding.
/// </summary>
/// <param name="value">The signed value.</param>
/// <param name="bitCount">The base-two divisor exponent.</param>
/// <returns>The rounded quotient.</returns>
private static long RoundPowerOf2Signed(long value, int bitCount)
=> value < 0
? -(((-value) + ((1L << bitCount) >> 1)) >> bitCount)
: (value + ((1L << bitCount) >> 1)) >> bitCount;
}

30
src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionType.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
/// <summary>
/// Identifies the geometric model carried by AV1 global-motion parameters.
/// </summary>
internal enum Av1GlobalMotionType : byte
{
/// <summary>
/// No geometric displacement is applied.
/// </summary>
Identity = 0,
/// <summary>
/// Horizontal and vertical translation are applied.
/// </summary>
Translation = 1,
/// <summary>
/// Translation, rotation, and uniform zoom are applied.
/// </summary>
RotationZoom = 2,
/// <summary>
/// A general six-parameter affine transformation is applied.
/// </summary>
Affine = 3
}

626
src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopy.cs

@ -0,0 +1,626 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
/// <summary>
/// Derives and validates AV1 intra-block-copy displacement vectors.
/// </summary>
internal static class Av1IntraBlockCopy
{
/// <summary>
/// The number of surrounding mode-information rows and columns searched for reference vectors.
/// </summary>
private const int ReferenceSearchDistance = 3;
/// <summary>
/// The weight separating immediately adjacent candidates from the outer search area.
/// </summary>
private const int NearestCandidateWeight = 640;
/// <summary>
/// The number of 64-sample blocks that an intra-block-copy source must precede the active block.
/// </summary>
private const int Delay64 = 4;
/// <summary>
/// Finds the spatial reference used to differentially decode an intra-block-copy displacement vector.
/// </summary>
/// <param name="partitionInfo">The current block geometry and decoded neighbors.</param>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <param name="superblockModeInfoSize">The superblock width in 4x4 mode-information units.</param>
/// <param name="candidates">Reusable storage for up to eight unique reference vectors.</param>
/// <param name="weights">Reusable storage for the corresponding spatial weights.</param>
/// <returns>The nearest nonzero spatial candidate, or the normative tile-relative fallback.</returns>
public static Av1MotionVector FindReference(
ref Av1PartitionInfo partitionInfo,
Av1TileInfo tileInfo,
int superblockModeInfoSize,
Span<Av1MotionVector> candidates,
Span<int> weights)
{
ReferenceContext context = new(ref partitionInfo, superblockModeInfoSize);
return FindReference(ref context, tileInfo, superblockModeInfoSize, candidates, weights);
}
/// <summary>
/// Finds the spatial reference used to differentially encode an intra-block-copy displacement vector.
/// </summary>
/// <param name="picture">The encoded frame's mapped mode and displacement state.</param>
/// <param name="macroBlock">The current block's frame edges and tile availability.</param>
/// <param name="modeInfoPosition">The current block origin in 4x4 mode-information units.</param>
/// <param name="blockSize">The current block size.</param>
/// <param name="partitionType">The partition type that produced the block.</param>
/// <param name="candidates">Reusable storage for up to eight unique reference vectors.</param>
/// <param name="weights">Reusable storage for the corresponding spatial weights.</param>
/// <returns>The nearest nonzero spatial candidate, or the normative tile-relative fallback.</returns>
public static Av1MotionVector FindReference(
Av1PictureControlSet picture,
Av1MacroBlockD macroBlock,
Point modeInfoPosition,
Av1BlockSize blockSize,
Av1PartitionType partitionType,
Span<Av1MotionVector> candidates,
Span<int> weights)
{
int superblockModeInfoSize = picture.Sequence.SequenceHeader.SuperblockModeInfoSize;
ReferenceContext context = new(
picture,
macroBlock,
modeInfoPosition,
blockSize,
partitionType,
superblockModeInfoSize);
return FindReference(
ref context,
macroBlock.Tile,
superblockModeInfoSize,
candidates,
weights);
}
/// <summary>
/// Ranks the shared decoder or encoder reference context without allocating candidate state.
/// </summary>
private static Av1MotionVector FindReference(
ref ReferenceContext context,
Av1TileInfo tileInfo,
int superblockModeInfoSize,
Span<Av1MotionVector> candidates,
Span<int> weights)
{
Av1BlockSize blockSize = context.BlockSize;
int width = blockSize.Get4x4WideCount();
int height = blockSize.Get4x4HighCount();
int row = context.RowIndex;
int column = context.ColumnIndex;
int rowAdjustment = height < 2 && (row & 1) != 0 ? 1 : 0;
int columnAdjustment = width < 2 && (column & 1) != 0 ? 1 : 0;
int maximumRowOffset = 0;
int maximumColumnOffset = 0;
if (context.AvailableAbove)
{
maximumRowOffset = height < 2 ? -4 + rowAdjustment : -(ReferenceSearchDistance << 1) + rowAdjustment;
maximumRowOffset = Math.Clamp(maximumRowOffset, tileInfo.ModeInfoRowStart - row, tileInfo.ModeInfoRowEnd - row - 1);
}
if (context.AvailableLeft)
{
maximumColumnOffset = width < 2 ? -4 + columnAdjustment : -(ReferenceSearchDistance << 1) + columnAdjustment;
maximumColumnOffset = Math.Clamp(maximumColumnOffset, tileInfo.ModeInfoColumnStart - column, tileInfo.ModeInfoColumnEnd - column - 1);
}
int candidateCount = 0;
int processedRows = 0;
int processedColumns = 0;
if (Math.Abs(maximumRowOffset) >= 1)
{
ScanRow(ref context, -1, maximumRowOffset, candidates, weights, ref candidateCount, ref processedRows);
}
if (Math.Abs(maximumColumnOffset) >= 1)
{
ScanColumn(ref context, -1, maximumColumnOffset, candidates, weights, ref candidateCount, ref processedColumns);
}
if (context.HasTopRight)
{
AddBlock(ref context, -1, width, tileInfo, candidates, weights, ref candidateCount);
}
int nearestCandidateCount = candidateCount;
for (int index = 0; index < nearestCandidateCount; index++)
{
weights[index] += NearestCandidateWeight;
}
// The top-left sample begins the outer search region. Sorting the adjacent and outer regions independently
// preserves the reference decoder's nearest/near ordering while still accumulating repeated vectors across both regions.
AddBlock(ref context, -1, -1, tileInfo, candidates, weights, ref candidateCount);
for (int index = 2; index <= ReferenceSearchDistance; index++)
{
int rowOffset = -(index << 1) + 1 + rowAdjustment;
int columnOffset = -(index << 1) + 1 + columnAdjustment;
if (Math.Abs(rowOffset) <= Math.Abs(maximumRowOffset) && Math.Abs(rowOffset) > processedRows)
{
ScanRow(ref context, rowOffset, maximumRowOffset, candidates, weights, ref candidateCount, ref processedRows);
}
if (Math.Abs(columnOffset) <= Math.Abs(maximumColumnOffset) && Math.Abs(columnOffset) > processedColumns)
{
ScanColumn(ref context, columnOffset, maximumColumnOffset, candidates, weights, ref candidateCount, ref processedColumns);
}
}
SortByWeight(candidates, weights, 0, nearestCandidateCount);
SortByWeight(candidates, weights, nearestCandidateCount, candidateCount);
// The reference decoder clamps the ranked stack before selecting nearest and near. The displacement entropy syntax is
// differential, so using an unclamped spatial candidate changes every following component even though the
// final decoded displacement is validated separately against the stricter intra-block-copy source limits.
for (int index = 0; index < candidateCount; index++)
{
candidates[index] = candidates[index].ClampReference(
blockSize.GetWidth(),
blockSize.GetHeight(),
context.ModeBlockToLeftEdge,
context.ModeBlockToRightEdge,
context.ModeBlockToTopEdge,
context.ModeBlockToBottomEdge);
}
Av1MotionVector reference = candidateCount > 0 ? candidates[0] : default;
if (reference.IsZero && candidateCount > 1)
{
reference = candidates[1];
}
if (!reference.IsZero)
{
return reference;
}
const int modeInfoSampleSize = 1 << Av1Constants.ModeInfoSizeLog2;
const int eighthSampleScale = 8;
int fallbackRow = -modeInfoSampleSize * superblockModeInfoSize * eighthSampleScale;
int fallbackColumn = fallbackRow - (Delay64 * 64 * eighthSampleScale);
return (row - superblockModeInfoSize) < tileInfo.ModeInfoRowStart
? new Av1MotionVector(0, fallbackColumn)
: new Av1MotionVector(fallbackRow, 0);
}
/// <summary>
/// Determines whether a decoded displacement vector references an earlier reconstructable block inside the tile.
/// </summary>
/// <param name="vector">The decoded displacement vector in one-eighth-sample units.</param>
/// <param name="partitionInfo">The current block geometry.</param>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <param name="sequenceHeader">The sequence-level superblock and chroma configuration.</param>
/// <returns><see langword="true"/> when the complete source block is a permitted reference; otherwise, <see langword="false"/>.</returns>
public static bool IsValid(Av1MotionVector vector, ref Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo, ObuSequenceHeader sequenceHeader)
=> IsValid(
vector,
new Point(partitionInfo.ColumnIndex, partitionInfo.RowIndex),
partitionInfo.ModeInfo.BlockSize,
partitionInfo.IsChroma,
tileInfo,
sequenceHeader);
/// <summary>
/// Determines whether an encoder displacement vector references an earlier reconstructable block inside the tile.
/// </summary>
/// <param name="vector">The displacement vector in one-eighth-sample units.</param>
/// <param name="modeInfoPosition">The current block origin in 4x4 mode-information units.</param>
/// <param name="blockSize">The current block size.</param>
/// <param name="isChroma">Indicates whether chroma subsampling constraints apply.</param>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <param name="sequenceHeader">The sequence-level superblock and chroma configuration.</param>
/// <returns><see langword="true"/> when the complete source block is a permitted reference; otherwise, <see langword="false"/>.</returns>
public static bool IsValid(
Av1MotionVector vector,
Point modeInfoPosition,
Av1BlockSize blockSize,
bool isChroma,
Av1TileInfo tileInfo,
ObuSequenceHeader sequenceHeader)
{
const int eighthSampleScale = 8;
const int modeInfoSampleSize = 1 << Av1Constants.ModeInfoSizeLog2;
if ((vector.Row & (eighthSampleScale - 1)) != 0 || (vector.Column & (eighthSampleScale - 1)) != 0 ||
vector.Row <= -(1 << 14) || vector.Row >= (1 << 14) || vector.Column <= -(1 << 14) || vector.Column >= (1 << 14))
{
return false;
}
int row = modeInfoPosition.Y;
int column = modeInfoPosition.X;
int blockWidth = blockSize.GetWidth();
int blockHeight = blockSize.GetHeight();
int sourceTop = (row * modeInfoSampleSize * eighthSampleScale) + vector.Row;
int sourceLeft = (column * modeInfoSampleSize * eighthSampleScale) + vector.Column;
int sourceBottom = (((row * modeInfoSampleSize) + blockHeight) * eighthSampleScale) + vector.Row;
int sourceRight = (((column * modeInfoSampleSize) + blockWidth) * eighthSampleScale) + vector.Column;
int tileTop = tileInfo.ModeInfoRowStart * modeInfoSampleSize * eighthSampleScale;
int tileLeft = tileInfo.ModeInfoColumnStart * modeInfoSampleSize * eighthSampleScale;
int tileBottom = tileInfo.ModeInfoRowEnd * modeInfoSampleSize * eighthSampleScale;
int tileRight = tileInfo.ModeInfoColumnEnd * modeInfoSampleSize * eighthSampleScale;
if (sourceTop < tileTop || sourceLeft < tileLeft || sourceBottom > tileBottom || sourceRight > tileRight)
{
return false;
}
ObuColorConfig colorConfig = sequenceHeader.ColorConfig;
if (isChroma && colorConfig.PlaneCount > 1)
{
// A sub-8x8 luma block can map to a chroma block whose rounded origin lies one additional luma unit
// inside the tile. These checks prevent that chroma reference from crossing the tile boundary.
if (blockWidth < 8 && colorConfig.SubSamplingX && sourceLeft < tileLeft + (modeInfoSampleSize * eighthSampleScale))
{
return false;
}
if (blockHeight < 8 && colorConfig.SubSamplingY && sourceTop < tileTop + (modeInfoSampleSize * eighthSampleScale))
{
return false;
}
}
int superblockModeInfoSize = sequenceHeader.SuperblockModeInfoSize;
int superblockSize = superblockModeInfoSize * modeInfoSampleSize;
int superblockModeInfoSizeLog2 = sequenceHeader.SuperblockSizeLog2 - Av1Constants.ModeInfoSizeLog2;
int activeSuperblockRow = row >> superblockModeInfoSizeLog2;
int active64Column = (column * modeInfoSampleSize) >> 6;
int sourceSuperblockRow = ((sourceBottom >> 3) - 1) / superblockSize;
int source64Column = ((sourceRight >> 3) - 1) >> 6;
int tile64ColumnCount = ((tileInfo.ModeInfoColumnEnd - tileInfo.ModeInfoColumnStart - 1) >> 4) + 1;
int active64 = (activeSuperblockRow * tile64ColumnCount) + active64Column;
int source64 = (sourceSuperblockRow * tile64ColumnCount) + source64Column;
if (source64 >= active64 - Delay64)
{
return false;
}
// The wavefront boundary reserves four completed 64-sample columns and advances farther right for every
// completed source row. A 128x128 superblock adds one column to account for its two 64-sample halves.
int gradient = 1 + Delay64 + (superblockSize > 64 ? 1 : 0);
int wavefrontOffset = gradient * (activeSuperblockRow - sourceSuperblockRow);
return sourceSuperblockRow <= activeSuperblockRow && source64Column < active64Column - Delay64 + wavefrontOffset;
}
/// <summary>
/// Scans a mode-information row using AV1's block-size-dependent steps and weights.
/// </summary>
private static void ScanRow(
ref ReferenceContext context,
int rowOffset,
int maximumRowOffset,
Span<Av1MotionVector> candidates,
Span<int> weights,
ref int candidateCount,
ref int processedRows)
{
int width = context.BlockSize.Get4x4WideCount();
int end = Math.Min(context.GetMaxBlockWide(), 16);
int columnOffset = 0;
if (Math.Abs(rowOffset) > 1)
{
columnOffset = 1;
if ((context.ColumnIndex & 1) != 0 && width < 2)
{
columnOffset--;
}
}
// Blocks below 64 samples use the finer two-mode-info-unit scan step.
bool useFourUnitStep = width >= 16;
for (int index = 0; index < end;)
{
ReferenceBlock candidate = context.GetModeInfoAt(
new Point(context.ColumnIndex + columnOffset + index, context.RowIndex + rowOffset));
int candidateWidth = candidate.BlockSize.Get4x4WideCount();
int length = Math.Min(width, candidateWidth);
if (useFourUnitStep)
{
length = Math.Max(4, length);
}
else if (Math.Abs(rowOffset) > 1)
{
length = Math.Max(2, length);
}
int weight = 2;
if (width >= 2 && width <= candidateWidth)
{
int increment = Math.Min(-maximumRowOffset + rowOffset + 1, candidate.BlockSize.Get4x4HighCount());
weight = Math.Max(weight, increment);
processedRows = increment - rowOffset - 1;
}
AddCandidate(candidate, length * weight, candidates, weights, ref candidateCount);
index += length;
}
}
/// <summary>
/// Scans a mode-information column using AV1's block-size-dependent steps and weights.
/// </summary>
private static void ScanColumn(
ref ReferenceContext context,
int columnOffset,
int maximumColumnOffset,
Span<Av1MotionVector> candidates,
Span<int> weights,
ref int candidateCount,
ref int processedColumns)
{
int height = context.BlockSize.Get4x4HighCount();
int end = Math.Min(context.GetMaxBlockHigh(), 16);
int rowOffset = 0;
if (Math.Abs(columnOffset) > 1)
{
rowOffset = 1;
if ((context.RowIndex & 1) != 0 && height < 2)
{
rowOffset--;
}
}
// Blocks below 64 samples use the finer two-mode-info-unit scan step.
bool useFourUnitStep = height >= 16;
for (int index = 0; index < end;)
{
ReferenceBlock candidate = context.GetModeInfoAt(
new Point(context.ColumnIndex + columnOffset, context.RowIndex + rowOffset + index));
int candidateHeight = candidate.BlockSize.Get4x4HighCount();
int length = Math.Min(height, candidateHeight);
if (useFourUnitStep)
{
length = Math.Max(4, length);
}
else if (Math.Abs(columnOffset) > 1)
{
length = Math.Max(2, length);
}
int weight = 2;
if (height >= 2 && height <= candidateHeight)
{
int increment = Math.Min(-maximumColumnOffset + columnOffset + 1, candidate.BlockSize.Get4x4WideCount());
weight = Math.Max(weight, increment);
processedColumns = increment - columnOffset - 1;
}
AddCandidate(candidate, length * weight, candidates, weights, ref candidateCount);
index += length;
}
}
/// <summary>
/// Adds the intra-block-copy vector at one tile-relative search position.
/// </summary>
private static void AddBlock(
ref ReferenceContext context,
int rowOffset,
int columnOffset,
Av1TileInfo tileInfo,
Span<Av1MotionVector> candidates,
Span<int> weights,
ref int candidateCount)
{
int row = context.RowIndex + rowOffset;
int column = context.ColumnIndex + columnOffset;
if (row < tileInfo.ModeInfoRowStart || row >= tileInfo.ModeInfoRowEnd ||
column < tileInfo.ModeInfoColumnStart || column >= tileInfo.ModeInfoColumnEnd)
{
return;
}
ReferenceBlock candidate = context.GetModeInfoAt(new Point(column, row));
AddCandidate(candidate, 4, candidates, weights, ref candidateCount);
}
/// <summary>
/// Accumulates one unique intra-block-copy candidate and its spatial weight.
/// </summary>
private static void AddCandidate(
ReferenceBlock candidate,
int weight,
Span<Av1MotionVector> candidates,
Span<int> weights,
ref int candidateCount)
{
if (!candidate.UseIntraBlockCopy)
{
return;
}
Av1MotionVector vector = candidate.DisplacementVector;
int index = 0;
for (; index < candidateCount; index++)
{
if (candidates[index] == vector)
{
weights[index] += weight;
return;
}
}
if (candidateCount < candidates.Length)
{
candidates[candidateCount] = vector;
weights[candidateCount] = weight;
candidateCount++;
}
}
/// <summary>
/// Sorts one candidate region by descending accumulated weight.
/// </summary>
private static void SortByWeight(Span<Av1MotionVector> candidates, Span<int> weights, int start, int end)
{
int length = end;
while (length > start)
{
int lastSwap = start;
for (int index = start + 1; index < length; index++)
{
if (weights[index - 1] < weights[index])
{
Av1MotionVector candidate = candidates[index - 1];
candidates[index - 1] = candidates[index];
candidates[index] = candidate;
int weight = weights[index - 1];
weights[index - 1] = weights[index];
weights[index] = weight;
lastSwap = index;
}
}
length = lastSwap;
}
}
/// <summary>
/// Provides one allocation-free view over decoder or encoder mode-information storage.
/// </summary>
private readonly struct ReferenceContext
{
private readonly Av1SuperblockInfo decodedSuperblock;
private readonly Av1PictureControlSet? encodedPicture;
public ReferenceContext(ref Av1PartitionInfo partitionInfo, int superblockModeInfoSize)
{
this.decodedSuperblock = partitionInfo.SuperblockInfo;
this.encodedPicture = null;
this.BlockSize = partitionInfo.ModeInfo.BlockSize;
this.RowIndex = partitionInfo.RowIndex;
this.ColumnIndex = partitionInfo.ColumnIndex;
this.AvailableAbove = partitionInfo.AvailableAbove;
this.AvailableLeft = partitionInfo.AvailableLeft;
this.ModeBlockToLeftEdge = partitionInfo.ModeBlockToLeftEdge;
this.ModeBlockToRightEdge = partitionInfo.ModeBlockToRightEdge;
this.ModeBlockToTopEdge = partitionInfo.ModeBlockToTopEdge;
this.ModeBlockToBottomEdge = partitionInfo.ModeBlockToBottomEdge;
this.HasTopRight = partitionInfo.HasTopRight(superblockModeInfoSize);
}
public ReferenceContext(
Av1PictureControlSet picture,
Av1MacroBlockD macroBlock,
Point modeInfoPosition,
Av1BlockSize blockSize,
Av1PartitionType partitionType,
int superblockModeInfoSize)
{
this.decodedSuperblock = default;
this.encodedPicture = picture;
this.BlockSize = blockSize;
this.RowIndex = modeInfoPosition.Y;
this.ColumnIndex = modeInfoPosition.X;
this.AvailableAbove = macroBlock.IsUpAvailable;
this.AvailableLeft = macroBlock.IsLeftAvailable;
this.ModeBlockToLeftEdge = macroBlock.ToLeftEdge;
this.ModeBlockToRightEdge = macroBlock.ToRightEdge;
this.ModeBlockToTopEdge = macroBlock.ToTopEdge;
this.ModeBlockToBottomEdge = macroBlock.ToBottomEdge;
this.HasTopRight = Av1PartitionInfo.HasTopRight(
blockSize,
partitionType,
modeInfoPosition.Y,
modeInfoPosition.X,
superblockModeInfoSize);
}
public Av1BlockSize BlockSize { get; }
public int RowIndex { get; }
public int ColumnIndex { get; }
public bool AvailableAbove { get; }
public bool AvailableLeft { get; }
public int ModeBlockToLeftEdge { get; }
public int ModeBlockToRightEdge { get; }
public int ModeBlockToTopEdge { get; }
public int ModeBlockToBottomEdge { get; }
public bool HasTopRight { get; }
public int GetMaxBlockWide()
{
int width = this.BlockSize.GetWidth();
if (this.ModeBlockToRightEdge < 0)
{
width += this.ModeBlockToRightEdge >> 3;
}
return width >> Av1Constants.ModeInfoSizeLog2;
}
public int GetMaxBlockHigh()
{
int height = this.BlockSize.GetHeight();
if (this.ModeBlockToBottomEdge < 0)
{
height += this.ModeBlockToBottomEdge >> 3;
}
return height >> Av1Constants.ModeInfoSizeLog2;
}
public ReferenceBlock GetModeInfoAt(Point position)
{
Av1PictureControlSet? picture = this.encodedPicture;
if (picture is not null)
{
Av1MacroBlockModeInfo encodedModeInfo = picture.GetFromModeInfoGrid(position);
return new ReferenceBlock(
encodedModeInfo.Block.BlockSize,
encodedModeInfo.Block.UseIntraBlockCopy,
picture.GetDisplacementVector(position));
}
Av1BlockModeInfo decodedModeInfo = this.decodedSuperblock.GetModeInfoAt(position);
return new ReferenceBlock(
decodedModeInfo.BlockSize,
decodedModeInfo.UseIntraBlockCopy,
decodedModeInfo.DisplacementVector);
}
}
/// <summary>
/// Carries the three neighboring mode fields consumed by displacement-reference ranking.
/// </summary>
private readonly struct ReferenceBlock
{
public ReferenceBlock(
Av1BlockSize blockSize,
bool useIntraBlockCopy,
Av1MotionVector displacementVector)
{
this.BlockSize = blockSize;
this.UseIntraBlockCopy = useIntraBlockCopy;
this.DisplacementVector = displacementVector;
}
public Av1BlockSize BlockSize { get; }
public bool UseIntraBlockCopy { get; }
public Av1MotionVector DisplacementVector { get; }
}
}

1094
src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopySearchIndex.cs

File diff suppressed because it is too large

89
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.ByteOperator.cs

@ -0,0 +1,89 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
internal static partial class Av1MotionSearchBase
{
/// <summary>
/// Measures eight-bit sample errors with the shared vector-width residual traversal.
/// </summary>
public readonly struct ByteOperator : IMotionSearchOperator<byte>
{
/// <inheritdoc/>
public static void PreparePrediction(
ReadOnlySpan<byte> source,
int sourceStride,
ReadOnlySpan<byte> reference,
int referenceStride,
int referenceOrigin,
Span<byte> prediction,
Span<short> residual,
Span<short> scratch,
int width,
int height,
Av1InterpolationFilter horizontalFilter,
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int verticalPhase,
int bitDepth)
{
Av1TranslationalInterPredictor.Predict(
reference,
referenceStride,
referenceOrigin,
prediction,
width,
width,
height,
horizontalFilter,
verticalFilter,
horizontalPhase,
verticalPhase,
scratch);
Av1ResidualBuilder.Subtract(source, sourceStride, prediction, width, residual, width, width, height);
}
/// <inheritdoc/>
public static void Predict(
ReadOnlySpan<byte> reference,
int referenceStride,
int referenceOrigin,
Span<byte> buffer,
int width,
int height,
int horizontalPhase,
int verticalPhase,
int taps,
int bitDepth)
=> Av1TranslationalInterPredictor.PredictForSearch(
reference, referenceStride, referenceOrigin, buffer, width, height, horizontalPhase, verticalPhase, taps);
/// <inheritdoc/>
public static int SumAbsoluteDifferences(
ReadOnlySpan<byte> source,
int sourceStride,
ReadOnlySpan<byte> prediction,
int predictionStride,
int width,
int height,
int rowStep)
=> Av1ResidualBuilder.SumAbsoluteDifferences(source, sourceStride, prediction, predictionStride, width, height, rowStep);
/// <inheritdoc/>
public static void GetMoments(
ReadOnlySpan<byte> source,
int sourceStride,
ReadOnlySpan<byte> prediction,
int predictionStride,
int width,
int height,
out int sum,
out long squares)
=> Av1ResidualBuilder.GetMoments(source, sourceStride, prediction, predictionStride, width, height, out sum, out squares);
}
}

340
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.Fractional.cs

@ -0,0 +1,340 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Motion.Av1MotionSearchSettings;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
internal static partial class Av1MotionSearchBase
{
/// <summary>
/// Retains the fractional winner and the error statistics used to select it.
/// </summary>
public readonly struct FractionalResult
{
/// <summary>
/// Initializes a new instance of the <see cref="FractionalResult"/> struct.
/// </summary>
/// <param name="vector">The displacement in eighth-sample units.</param>
/// <param name="variance">The normalized residual variance.</param>
/// <param name="squaredError">The normalized squared residual sum.</param>
/// <param name="motionCost">The variance-domain rate cost.</param>
public FractionalResult(Av1MotionVector vector, int variance, int squaredError, int motionCost)
{
this.Vector = vector;
this.Variance = variance;
this.SquaredError = squaredError;
this.MotionCost = motionCost;
}
/// <summary>
/// Gets the displacement in eighth-sample units.
/// </summary>
public Av1MotionVector Vector { get; }
/// <summary>
/// Gets the normalized residual variance.
/// </summary>
public int Variance { get; }
/// <summary>
/// Gets the normalized squared residual sum.
/// </summary>
public int SquaredError { get; }
/// <summary>
/// Gets the variance-domain rate cost.
/// </summary>
public int MotionCost { get; }
/// <summary>
/// Gets the combined selection cost.
/// </summary>
public int Cost => this.Variance + this.MotionCost;
}
/// <summary>
/// Refines an unscaled reference prediction while borrowing the worker's sample and entropy storage.
/// </summary>
/// <typeparam name="TSample">The unsigned sample storage type.</typeparam>
/// <typeparam name="TOperator">The closed prediction and error operator.</typeparam>
public readonly ref struct FractionalSearch<TSample, TOperator>
where TSample : unmanaged
where TOperator : struct, IMotionSearchOperator<TSample>
{
private readonly ReadOnlySpan<TSample> source;
private readonly ReadOnlySpan<TSample> reference;
private readonly Span<TSample> prediction;
private readonly int sourceStride;
private readonly int referenceStride;
private readonly int referenceOrigin;
private readonly Size blockSize;
private readonly Rectangle bounds;
private readonly Av1MotionVector referenceVector;
private readonly Av1MotionVectorCosts costs;
private readonly int bitDepth;
private readonly int rateMultiplier;
/// <summary>
/// Initializes a new instance of the <see cref="FractionalSearch{TSample, TOperator}"/> struct.
/// </summary>
/// <param name="source">Source samples beginning at the block origin.</param>
/// <param name="sourceStride">The source row stride in samples.</param>
/// <param name="reference">The complete bordered reference plane.</param>
/// <param name="referenceStride">The reference row stride in samples.</param>
/// <param name="referenceOrigin">The reference index corresponding to the current block origin.</param>
/// <param name="prediction">The worker's reusable fractional prediction buffer.</param>
/// <param name="blockSize">The prediction dimensions.</param>
/// <param name="bounds">The permitted eighth-sample displacements, with exclusive upper edges.</param>
/// <param name="referenceVector">The spatial entropy reference in eighth-sample units.</param>
/// <param name="costs">The retained motion-rate tables.</param>
/// <param name="bitDepth">The coded component precision.</param>
/// <param name="rateMultiplier">The block rate multiplier.</param>
public FractionalSearch(
ReadOnlySpan<TSample> source,
int sourceStride,
ReadOnlySpan<TSample> reference,
int referenceStride,
int referenceOrigin,
Span<TSample> prediction,
Size blockSize,
Rectangle bounds,
Av1MotionVector referenceVector,
Av1MotionVectorCosts costs,
Av1BitDepth bitDepth,
int rateMultiplier)
{
this.source = source;
this.sourceStride = sourceStride;
this.reference = reference;
this.referenceStride = referenceStride;
this.referenceOrigin = referenceOrigin;
this.prediction = prediction;
this.blockSize = blockSize;
this.bounds = bounds;
this.referenceVector = referenceVector;
this.costs = costs;
this.bitDepth = bitDepth.GetBitCount();
this.rateMultiplier = rateMultiplier;
}
/// <summary>
/// Runs the selected fractional tree and publishes its retained winner.
/// </summary>
/// <param name="start">The initial eighth-sample displacement.</param>
/// <param name="startStatistics">Integer-search statistics when that starting prediction was already measured.</param>
/// <param name="method">The fractional decision policy.</param>
/// <param name="precision">The finest permitted search step.</param>
/// <param name="allowHighPrecision">Whether eighth-sample candidates are enabled by the frame.</param>
/// <param name="iterationsPerStep">The number of refinement levels at each precision.</param>
/// <param name="taps">The full tree's interpolation tap count.</param>
/// <param name="costList">The integer center, left, down, right, and up costs, or an empty span when unavailable.</param>
/// <param name="previousCenters">Three retained precision centers, or an empty span when duplicate pruning is disabled.</param>
/// <param name="result">The selected vector and its error statistics.</param>
/// <returns>The selected cost, or <see cref="int.MaxValue"/> when a previously searched center terminates the path.</returns>
public int Search(
Av1MotionVector start,
FullPixelResult? startStatistics,
FractionalSearchMethod method,
SearchPrecision precision,
bool allowHighPrecision,
int iterationsPerStep,
int taps,
ReadOnlySpan<int> costList,
Span<Av1MotionVector> previousCenters,
out FractionalResult result)
{
// Integer search has already paid for these moments. Retain that exact error domain, including
// its signed high-depth rounding, until a fractional candidate strictly improves the total cost.
if (startStatistics.HasValue)
{
FullPixelResult statistics = startStatistics.Value;
result = new FractionalResult(start, statistics.Variance, statistics.SquaredError, statistics.MotionCost);
}
else
{
result = this.Measure(start, method == FractionalSearchMethod.TwoLevelTree ? taps : 2);
}
int rounds = Math.Min(3 - (int)precision, allowHighPrecision ? 3 : 2);
for (int iteration = 0, step = 4; iteration < rounds; iteration++, step >>= 1)
{
Av1MotionVector center = result.Vector;
if (!previousCenters.IsEmpty)
{
// Each slot belongs to one precision. Another starting candidate reaching the same
// center has the same remaining tree, so the caller can discard this duplicate path.
if (previousCenters[iteration] == center)
{
return int.MaxValue;
}
previousCenters[iteration] = center;
}
bool finiteNeighborhood = costList.Length == 5
&& costList[0] != int.MaxValue
&& costList[1] != int.MaxValue
&& costList[2] != int.MaxValue
&& costList[3] != int.MaxValue
&& costList[4] != int.MaxValue;
if (iteration == 0 && method == FractionalSearchMethod.PrunedTree && finiteNeighborhood)
{
// Half-sample pruning chooses one quadrant from the integer cost surface. Ties select
// right and up here; the measured-cardinal tree below instead breaks ties left and up.
int column = costList[1] < costList[3] ? -step : step;
int row = costList[2] < costList[4] ? step : -step;
this.Check(new Av1MotionVector(center.Row, center.Column + column), 2, ref result);
this.Check(new Av1MotionVector(center.Row + row, center.Column), 2, ref result);
this.Check(new Av1MotionVector(center.Row + row, center.Column + column), 2, ref result);
continue;
}
if (iteration == 0 && method == FractionalSearchMethod.MorePrunedTree && finiteNeighborhood
&& costList[0] < costList[1] && costList[0] < costList[2]
&& costList[0] < costList[3] && costList[0] < costList[4])
{
// A strictly lower center gives positive curvature on both axes. The minimum of each
// fitted parabola is (negative-side cost - positive-side cost) / (2 * curvature).
// Multiplying that location by two gives half-sample units; signed division rounds
// the displacement to the nearest such unit before converting it to eighth samples.
int columnNumerator = costList[1] - costList[3];
int columnDenominator = costList[1] - (2 * costList[0]) + costList[3];
int rowNumerator = costList[4] - costList[2];
int rowDenominator = costList[4] - (2 * costList[0]) + costList[2];
int column = (columnNumerator + (columnNumerator < 0 ? -columnDenominator / 2 : columnDenominator / 2))
/ columnDenominator;
int row = (rowNumerator + (rowNumerator < 0 ? -rowDenominator / 2 : rowDenominator / 2)) / rowDenominator;
if ((row | column) != 0)
{
this.Check(new Av1MotionVector(center.Row + (row * step), center.Column + (column * step)), 2, ref result);
}
continue;
}
int selectedTaps = method == FractionalSearchMethod.TwoLevelTree ? taps : 2;
int left = this.Check(new Av1MotionVector(center.Row, center.Column - step), selectedTaps, ref result);
int right = this.Check(new Av1MotionVector(center.Row, center.Column + step), selectedTaps, ref result);
int up = this.Check(new Av1MotionVector(center.Row - step, center.Column), selectedTaps, ref result);
int down = this.Check(new Av1MotionVector(center.Row + step, center.Column), selectedTaps, ref result);
int diagonalRow = up <= down ? -step : step;
int diagonalColumn = left <= right ? -step : step;
this.Check(new Av1MotionVector(center.Row + diagonalRow, center.Column + diagonalColumn), selectedTaps, ref result);
if (iterationsPerStep > 1 && result.Vector != center)
{
// All second-level sites are anchored to the first-level winner. Updating that winner
// while measuring these sites must not move the remaining sites of the same level.
Av1MotionVector winner = result.Vector;
if (method == FractionalSearchMethod.TwoLevelTree)
{
if (winner.Row == center.Row)
{
diagonalRow = -diagonalRow;
}
else if (winner.Column == center.Column)
{
diagonalColumn = -diagonalColumn;
}
int previousCost = result.Cost;
this.Check(new Av1MotionVector(winner.Row + diagonalRow, winner.Column), selectedTaps, ref result);
this.Check(new Av1MotionVector(winner.Row, winner.Column + diagonalColumn), selectedTaps, ref result);
// Extend to the outward diagonal only when an outward cardinal site improved.
if (result.Cost < previousCost)
{
this.Check(new Av1MotionVector(winner.Row + diagonalRow, winner.Column + diagonalColumn), selectedTaps, ref result);
}
}
else if (winner.Row != center.Row && winner.Column != center.Column)
{
this.Check(new Av1MotionVector(winner.Row, winner.Column + diagonalColumn), 2, ref result);
this.Check(new Av1MotionVector(winner.Row + diagonalRow, winner.Column), 2, ref result);
}
else if (winner.Row == center.Row)
{
this.Check(new Av1MotionVector(winner.Row + step, winner.Column + diagonalColumn), 2, ref result);
this.Check(new Av1MotionVector(winner.Row - step, winner.Column + diagonalColumn), 2, ref result);
this.Check(new Av1MotionVector(winner.Row - diagonalRow, winner.Column), 2, ref result);
}
else
{
this.Check(new Av1MotionVector(winner.Row + diagonalRow, winner.Column + step), 2, ref result);
this.Check(new Av1MotionVector(winner.Row + diagonalRow, winner.Column - step), 2, ref result);
this.Check(new Av1MotionVector(winner.Row, winner.Column - diagonalColumn), 2, ref result);
}
}
}
return result.Cost;
}
/// <summary>
/// Measures an in-range candidate and replaces the retained winner only for a strictly smaller cost.
/// </summary>
private int Check(Av1MotionVector vector, int taps, ref FractionalResult best)
{
if (!this.bounds.Contains(vector.Column, vector.Row))
{
return int.MaxValue;
}
FractionalResult candidate = this.Measure(vector, taps);
if (candidate.Cost < best.Cost)
{
best = candidate;
}
return candidate.Cost;
}
/// <summary>
/// Filters the borrowed reference and measures prediction-minus-source moments in the search error domain.
/// </summary>
private FractionalResult Measure(Av1MotionVector vector, int taps)
{
int referenceIndex = this.referenceOrigin + ((vector.Row >> 3) * this.referenceStride) + (vector.Column >> 3);
TOperator.Predict(
this.reference,
this.referenceStride,
referenceIndex,
this.prediction,
this.blockSize.Width,
this.blockSize.Height,
vector.Column & 7,
vector.Row & 7,
taps,
this.bitDepth);
TOperator.GetMoments(
this.prediction,
this.blockSize.Width,
this.source,
this.sourceStride,
this.blockSize.Width,
this.blockSize.Height,
out int sum,
out long squares);
int precisionShift = this.bitDepth - 8;
if (precisionShift != 0)
{
// Prediction is the first operand: signed rounding is asymmetric for negative residual
// sums. Normalize that sum and its squares independently before subtracting the mean.
sum = (sum + (1 << (precisionShift - 1))) >> precisionShift;
int squaredShift = precisionShift * 2;
squares = (squares + (1L << (squaredShift - 1))) >> squaredShift;
}
int variance = (int)Math.Max(squares - (((long)sum * sum) / (this.blockSize.Width * this.blockSize.Height)), 0);
int rate = this.costs.GetCost(vector, this.referenceVector);
int motionCost = Av1RateDistortion.GetMotionSearchCost(this.rateMultiplier, rate, 0);
return new FractionalResult(vector, variance, (int)squares, motionCost);
}
}
}

118
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.Operator.cs

@ -0,0 +1,118 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
internal static partial class Av1MotionSearchBase
{
/// <summary>
/// Measures unsigned sample planes without changing the motion controller's error domains.
/// </summary>
/// <typeparam name="TSample">The unsigned component storage type.</typeparam>
public interface IMotionSearchOperator<TSample>
where TSample : unmanaged
{
/// <summary>
/// Builds the final inter predictor and its residual for transform-based winner selection.
/// </summary>
/// <param name="source">The source samples at the block origin.</param>
/// <param name="sourceStride">The source row stride.</param>
/// <param name="reference">The complete bordered reference plane.</param>
/// <param name="referenceStride">The reference row stride.</param>
/// <param name="referenceOrigin">The displaced integer reference origin.</param>
/// <param name="prediction">The packed prediction destination.</param>
/// <param name="residual">The packed residual destination.</param>
/// <param name="scratch">The signed intermediate convolution storage.</param>
/// <param name="width">The prediction width.</param>
/// <param name="height">The prediction height.</param>
/// <param name="horizontalFilter">The final horizontal interpolation family.</param>
/// <param name="verticalFilter">The final vertical interpolation family.</param>
/// <param name="horizontalPhase">The horizontal phase in one-sixteenth-sample units.</param>
/// <param name="verticalPhase">The vertical phase in one-sixteenth-sample units.</param>
/// <param name="bitDepth">The coded sample precision.</param>
public static abstract void PreparePrediction(
ReadOnlySpan<TSample> source,
int sourceStride,
ReadOnlySpan<TSample> reference,
int referenceStride,
int referenceOrigin,
Span<TSample> prediction,
Span<short> residual,
Span<short> scratch,
int width,
int height,
Av1InterpolationFilter horizontalFilter,
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int verticalPhase,
int bitDepth);
/// <summary>
/// Produces a packed fractional prediction with each filter pass rounded to sample precision.
/// </summary>
/// <param name="reference">The bordered reference plane.</param>
/// <param name="referenceStride">The reference row stride.</param>
/// <param name="referenceOrigin">The integer prediction origin.</param>
/// <param name="buffer">The borrowed prediction and intermediate buffer.</param>
/// <param name="width">The block width.</param>
/// <param name="height">The block height.</param>
/// <param name="horizontalPhase">The horizontal eighth-sample phase.</param>
/// <param name="verticalPhase">The vertical eighth-sample phase.</param>
/// <param name="taps">The search filter's tap count.</param>
/// <param name="bitDepth">The coded precision.</param>
static abstract void Predict(
ReadOnlySpan<TSample> reference,
int referenceStride,
int referenceOrigin,
Span<TSample> buffer,
int width,
int height,
int horizontalPhase,
int verticalPhase,
int taps,
int bitDepth);
/// <summary>
/// Measures raw absolute differences, doubling alternate-row results before precision normalization.
/// </summary>
/// <param name="source">The source block samples.</param>
/// <param name="sourceStride">The source row stride.</param>
/// <param name="prediction">The prediction block samples.</param>
/// <param name="predictionStride">The prediction row stride.</param>
/// <param name="width">The block width.</param>
/// <param name="height">The block height.</param>
/// <param name="rowStep">One for all rows or two for alternate rows.</param>
/// <returns>The raw absolute-difference sum.</returns>
static abstract int SumAbsoluteDifferences(
ReadOnlySpan<TSample> source,
int sourceStride,
ReadOnlySpan<TSample> prediction,
int predictionStride,
int width,
int height,
int rowStep);
/// <summary>
/// Measures raw signed and squared residual sums without materializing a residual plane.
/// </summary>
/// <param name="source">The source block samples.</param>
/// <param name="sourceStride">The source row stride.</param>
/// <param name="prediction">The prediction block samples.</param>
/// <param name="predictionStride">The prediction row stride.</param>
/// <param name="width">The block width.</param>
/// <param name="height">The block height.</param>
/// <param name="sum">The raw signed residual sum.</param>
/// <param name="squares">The raw squared residual sum.</param>
static abstract void GetMoments(
ReadOnlySpan<TSample> source,
int sourceStride,
ReadOnlySpan<TSample> prediction,
int predictionStride,
int width,
int height,
out int sum,
out long squares);
}
}

651
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.SingleReference.cs

@ -0,0 +1,651 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Motion.Av1MotionSearchSettings;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
internal static partial class Av1MotionSearchBase
{
/// <summary>
/// Converts a full-sample motion extent to the initial number of excluded search stages.
/// </summary>
/// <param name="size">The frame dimension or retained spatial motion magnitude.</param>
/// <returns>The initial search-step parameter.</returns>
public static int GetInitialStepParameter(int size)
{
size = Math.Max(size, 16);
int step = 0;
while ((size << step) < 1023)
{
step++;
}
return Math.Min(step, 9);
}
/// <summary>
/// Collects weighted temporal starting vectors for one prediction block.
/// </summary>
/// <param name="spatialStart">The rounded spatial reference displacement in full samples.</param>
/// <param name="temporalVectors">The temporal analysis vectors at the block's analysis-grid origin.</param>
/// <param name="temporalStride">The analysis row stride in vectors.</param>
/// <param name="analysisSize">The number of analysis columns and rows covered by the block.</param>
/// <param name="candidates">Storage for the spatial start and every covered analysis block.</param>
/// <param name="totalWeight">The represented weight, or zero when analysis is incomplete.</param>
/// <returns>The number of collected starting candidates.</returns>
public static int CollectStartingCandidates(
Point spatialStart,
ReadOnlySpan<Av1MotionVector> temporalVectors,
int temporalStride,
Size analysisSize,
Span<StartingCandidate> candidates,
out int totalWeight)
{
candidates[0] = new StartingCandidate(spatialStart, 0);
totalWeight = 0;
int count = 1;
int analysisCount = analysisSize.Width * analysisSize.Height;
if (analysisCount != 0)
{
// The spatial start receives one vote per analysis block before temporal votes are added.
// It therefore remains among the first starts even when the temporal field is fragmented.
candidates[0] = new StartingCandidate(spatialStart, analysisCount);
for (int y = 0; y < analysisSize.Height; y++)
{
for (int x = 0; x < analysisSize.Width; x++)
{
Av1MotionVector vector = temporalVectors[(y * temporalStride) + x];
if (vector.Row == short.MinValue && vector.Column == short.MinValue)
{
// Analysis may end partway through a block. Retain the collected prefix, but do not
// apply completed-field weighting or reorder it as if all temporal votes were available.
return count;
}
Point position = new(vector.Column >> 3, vector.Row >> 3);
int rowGroup = (position.Y + 3 + (position.Y >= 0 ? 1 : 0)) >> 3;
int columnGroup = (position.X + 3 + (position.X >= 0 ? 1 : 0)) >> 3;
int index = 0;
for (; index < count; index++)
{
Point existing = candidates[index].Vector;
// Temporal starts are grouped into rounded eight-sample cells after conversion to
// full samples. Keep the first representative position while accumulating its votes.
if (((existing.Y + 3 + (existing.Y >= 0 ? 1 : 0)) >> 3) == rowGroup
&& ((existing.X + 3 + (existing.X >= 0 ? 1 : 0)) >> 3) == columnGroup)
{
candidates[index] = new StartingCandidate(existing, candidates[index].Weight + 1);
break;
}
}
if (index == count)
{
candidates[count++] = new StartingCandidate(position, 1);
}
}
}
totalWeight = 2 * analysisCount;
if (count > 2)
{
candidates[..count].Sort(default(StartingCandidateWeightComparer));
}
}
return count;
}
/// <summary>
/// Holds one weighted full-sample starting position from spatial or temporal analysis.
/// </summary>
public readonly struct StartingCandidate
{
/// <summary>
/// Initializes a new instance of the <see cref="StartingCandidate"/> struct.
/// </summary>
/// <param name="vector">The starting displacement in full samples.</param>
/// <param name="weight">The number of represented analysis blocks.</param>
public StartingCandidate(Point vector, int weight)
{
this.Vector = vector;
this.Weight = weight;
}
/// <summary>
/// Gets the full-sample displacement.
/// </summary>
public Point Vector { get; }
/// <summary>
/// Gets the number of represented analysis blocks.
/// </summary>
public int Weight { get; }
}
/// <summary>
/// Retains motion-search results and mode decisions for one differential-reference choice.
/// </summary>
public struct ReferenceSearchResult
{
public Av1MotionVector ReferenceVector;
public Av1MotionVector FullVector;
public Av1MotionVector Vector;
public int FullRate;
public int FullCost;
public int Rate;
public int DrlRate;
public bool HasFullResult;
public bool IsValid;
public bool Skip;
}
/// <summary>
/// Retains the six possible starts and three reference results across one block's new-motion modes.
/// </summary>
public struct SingleReferenceState
{
public InlineArray6<Point> Starts;
public InlineArray6<byte> StartReferenceIndices;
public InlineArray3<ReferenceSearchResult> References;
public int StartCount;
}
/// <summary>
/// Coordinates single-reference starting candidates, full-pixel search, fractional refinement, and winner estimation.
/// </summary>
/// <typeparam name="TSample">The unsigned sample storage type.</typeparam>
/// <typeparam name="TOperator">The sample-specific prediction and error operations.</typeparam>
public readonly ref struct SingleReferenceSearch<TSample, TOperator>
where TSample : unmanaged
where TOperator : struct, IMotionSearchOperator<TSample>
{
private readonly ReadOnlySpan<TSample> source;
private readonly int sourceStride;
private readonly ReadOnlySpan<TSample> reference;
private readonly int referenceStride;
private readonly int referenceOrigin;
private readonly Av1BlockSize blockSize;
private readonly Rectangle frameBounds;
private readonly Av1EncoderBlockWorkspace workspace;
private readonly Span<TSample> prediction;
private readonly Span<short> residual;
private readonly Span<short> convolutionScratch;
private readonly Span<int> quantized;
private readonly Av1SymbolEncoder writer;
private readonly ReadOnlySpan<byte> aboveContexts;
private readonly ReadOnlySpan<byte> leftContexts;
private readonly Av1BitDepth bitDepth;
private readonly int qIndex;
private readonly int dcDeltaQ;
private readonly int sharpness;
private readonly bool lossless;
private readonly int rateMultiplier;
private readonly int transformSizeRate;
private readonly int noSkipRate;
private readonly int skipRate;
private readonly Av1InterpolationFilter horizontalFilter;
private readonly Av1InterpolationFilter verticalFilter;
private readonly Av1MotionVectorCosts motionCosts;
/// <summary>
/// Initializes a new instance of the <see cref="SingleReferenceSearch{TSample, TOperator}"/> struct.
/// </summary>
/// <param name="source">The source samples at the prediction-block origin.</param>
/// <param name="sourceStride">The source row stride.</param>
/// <param name="reference">The complete bordered reference plane.</param>
/// <param name="referenceStride">The reference row stride.</param>
/// <param name="referenceOrigin">The reference origin corresponding to zero displacement.</param>
/// <param name="blockSize">The containing prediction block size.</param>
/// <param name="frameBounds">The full-sample frame search limits before differential-vector limits.</param>
/// <param name="workspace">The worker transform and search-site storage.</param>
/// <param name="prediction">The worker search prediction buffer, also reused for final predictions.</param>
/// <param name="residual">The packed block residual destination.</param>
/// <param name="convolutionScratch">The signed intermediate storage for final prediction.</param>
/// <param name="quantized">The scratch quantized coefficients for one transform.</param>
/// <param name="writer">The current tile probability state.</param>
/// <param name="aboveContexts">The incoming top coefficient contexts.</param>
/// <param name="leftContexts">The incoming left coefficient contexts.</param>
/// <param name="bitDepth">The coded sample precision.</param>
/// <param name="qIndex">The effective segment quantizer index.</param>
/// <param name="dcDeltaQ">The luma DC quantizer adjustment.</param>
/// <param name="sharpness">The quantization sharpness setting.</param>
/// <param name="lossless">Whether the segment is coded losslessly.</param>
/// <param name="rateMultiplier">The block rate-distortion multiplier.</param>
/// <param name="transformSizeRate">The transform partition rate used by winner estimation.</param>
/// <param name="noSkipRate">The rate of a non-skipped prediction block.</param>
/// <param name="skipRate">The rate of a skipped prediction block.</param>
/// <param name="horizontalFilter">The final horizontal interpolation family.</param>
/// <param name="verticalFilter">The final vertical interpolation family.</param>
/// <param name="motionCosts">The retained differential motion-rate table.</param>
public SingleReferenceSearch(
ReadOnlySpan<TSample> source,
int sourceStride,
ReadOnlySpan<TSample> reference,
int referenceStride,
int referenceOrigin,
Av1BlockSize blockSize,
Rectangle frameBounds,
Av1EncoderBlockWorkspace workspace,
Span<TSample> prediction,
Span<short> residual,
Span<short> convolutionScratch,
Span<int> quantized,
Av1SymbolEncoder writer,
ReadOnlySpan<byte> aboveContexts,
ReadOnlySpan<byte> leftContexts,
Av1BitDepth bitDepth,
int qIndex,
int dcDeltaQ,
int sharpness,
bool lossless,
int rateMultiplier,
int transformSizeRate,
int noSkipRate,
int skipRate,
Av1InterpolationFilter horizontalFilter,
Av1InterpolationFilter verticalFilter,
Av1MotionVectorCosts motionCosts)
{
this.source = source;
this.sourceStride = sourceStride;
this.reference = reference;
this.referenceStride = referenceStride;
this.referenceOrigin = referenceOrigin;
this.blockSize = blockSize;
this.frameBounds = frameBounds;
this.workspace = workspace;
this.prediction = prediction;
this.residual = residual;
this.convolutionScratch = convolutionScratch;
this.quantized = quantized;
this.writer = writer;
this.aboveContexts = aboveContexts;
this.leftContexts = leftContexts;
this.bitDepth = bitDepth;
this.qIndex = qIndex;
this.dcDeltaQ = dcDeltaQ;
this.sharpness = sharpness;
this.lossless = lossless;
this.rateMultiplier = rateMultiplier;
this.transformSizeRate = transformSizeRate;
this.noSkipRate = noSkipRate;
this.skipRate = skipRate;
this.horizontalFilter = horizontalFilter;
this.verticalFilter = verticalFilter;
this.motionCosts = motionCosts;
}
/// <summary>
/// Searches one differential-reference choice while retaining state for subsequent choices.
/// </summary>
/// <param name="settings">The resolved frame search policy.</param>
/// <param name="frameStepParameter">The frame's initial number of excluded outer search stages.</param>
/// <param name="spatialMagnitude">The largest full-sample magnitude in this reference's spatial context.</param>
/// <param name="showFrame">Whether the current frame is presented.</param>
/// <param name="searchRange">The optional range reduction, or the maximum integer for no reduction.</param>
/// <param name="forceInteger">Whether the frame prohibits fractional motion vectors.</param>
/// <param name="allowHighPrecision">Whether eighth-sample vectors are permitted.</param>
/// <param name="fineMeshInterval">Whether content classification caps the first mesh interval.</param>
/// <param name="referenceIndex">The current dynamic-reference index.</param>
/// <param name="referenceVector">The differential coding reference in eighth-sample units.</param>
/// <param name="drlRate">The syntax rate selecting this differential reference.</param>
/// <param name="starts">Weighted starting positions in decreasing weight order.</param>
/// <param name="totalWeight">The total represented weight before selecting the first two starts.</param>
/// <param name="state">The block's retained results; initialize once before its first new-motion mode.</param>
/// <param name="result">The selected displacement and prediction-error statistics.</param>
/// <returns>Whether the search produced a valid candidate.</returns>
public bool Search(
Av1MotionSearchSettings settings,
int frameStepParameter,
int spatialMagnitude,
bool showFrame,
int searchRange,
bool forceInteger,
bool allowHighPrecision,
bool fineMeshInterval,
int referenceIndex,
Av1MotionVector referenceVector,
int drlRate,
ReadOnlySpan<StartingCandidate> starts,
int totalWeight,
ref SingleReferenceState state,
out FractionalResult result)
{
ref ReferenceSearchResult current = ref state.References[referenceIndex];
current.ReferenceVector = referenceVector;
current.DrlRate = drlRate;
int stepParameter = frameStepParameter;
if (settings.AutomaticStepSizeLevel != 0 && showFrame)
{
stepParameter = (GetInitialStepParameter(spatialMagnitude) + frameStepParameter) / 2;
}
// The frame may supply many temporal starts, but only its first two ranked candidates enter
// this search. Record both before searching: the weight cutoff does not undo start history.
int candidateCount = Math.Min(2, starts.Length);
Span<bool> rejected = stackalloc bool[2];
rejected.Clear();
Point fullReference = new(
(referenceVector.Column + 3 + (referenceVector.Column >= 0 ? 1 : 0)) >> 3,
(referenceVector.Row + 3 + (referenceVector.Row >= 0 ? 1 : 0)) >> 3);
if (settings.StartCandidatePruningLevel != 0)
{
for (int candidateIndex = 0; candidateIndex < candidateCount; candidateIndex++)
{
Point start = starts[candidateIndex].Vector;
for (int historyIndex = 0; historyIndex < state.StartCount; historyIndex++)
{
int previousIndex = state.StartReferenceIndices[historyIndex];
ref ReferenceSearchResult previous = ref state.References[previousIndex];
if (!previous.IsValid && previousIndex != referenceIndex)
{
continue;
}
Point previousStart = state.Starts[historyIndex];
Av1MotionVector previousReference = previous.ReferenceVector;
int previousColumn = (previousReference.Column + 3 + (previousReference.Column >= 0 ? 1 : 0)) >> 3;
int previousRow = (previousReference.Row + 3 + (previousReference.Row >= 0 ? 1 : 0)) >> 3;
int startX = Math.Abs(start.X - previousStart.X);
int startY = Math.Abs(start.Y - previousStart.Y);
int referenceX = Math.Abs(fullReference.X - previousColumn);
int referenceY = Math.Abs(fullReference.Y - previousRow);
bool duplicates = settings.StartCandidatePruningLevel >= 2
? startX <= 1 && startY <= 1 && referenceX <= 1 && referenceY <= 1
: startX + startY <= 1 && referenceX + referenceY <= 1;
if (duplicates)
{
rejected[candidateIndex] = true;
break;
}
}
if (!rejected[candidateIndex])
{
state.Starts[state.StartCount] = start;
state.StartReferenceIndices[state.StartCount++] = (byte)referenceIndex;
}
}
}
FullPixelSearchMethod method = settings.GetFullPixelMethod(this.blockSize);
Av1MotionSearchSites sites = this.workspace.GetMotionSearchSites(method, this.referenceStride);
if (searchRange < int.MaxValue)
{
if (searchRange < 1)
{
stepParameter = sites.StageCount;
}
else
{
while (sites.StageCount - stepParameter - 1 > 0
&& sites.GetRadius(sites.StageCount - stepParameter - 1) > (searchRange << 1))
{
stepParameter++;
}
}
}
Size size = new(this.blockSize.GetWidth(), this.blockSize.GetHeight());
FullPixelSearch<TSample, TOperator> fullSearch = new(
this.source,
this.sourceStride,
this.reference,
this.referenceStride,
this.referenceOrigin,
size,
referenceVector.GetFullPixelSearchBounds(this.frameBounds),
referenceVector,
this.motionCosts,
this.bitDepth,
Av1RateDistortion.GetMotionSearchSadPerBit(this.qIndex, this.bitDepth),
this.rateMultiplier);
FullPixelResult best = default;
Point? second = null;
bool hasBest = false;
int sumWeight = 0;
for (int candidateIndex = 0; candidateIndex < candidateCount; candidateIndex++)
{
if (rejected[candidateIndex])
{
continue;
}
// Non-realtime motion policy disables neighborhood publication. Fractional pruning therefore
// measures its own candidates instead of fitting the optional five-cost integer surface.
FullPixelResult candidate = fullSearch.Search(
starts[candidateIndex].Vector,
stepParameter,
method,
sites,
settings,
keyFrame: false,
fineMeshInterval,
Span<int>.Empty,
out Point? candidateSecond);
if (candidate.Cost < (hasBest ? best.Cost : int.MaxValue))
{
best = candidate;
second = candidateSecond;
hasBest = true;
}
sumWeight += starts[candidateIndex].Weight;
if (4 * sumWeight > 3 * totalWeight)
{
break;
}
}
result = default;
if (!hasBest)
{
return false;
}
Av1MotionVector integerVector = new(best.Vector.Y * 8, best.Vector.X * 8);
int integerRate = ((this.motionCosts.GetCost(integerVector, referenceVector) * 108) + 64) >> 7;
current.FullVector = integerVector;
current.FullCost = best.Cost;
current.FullRate = integerRate;
current.HasFullResult = true;
int pruningLevel = settings.ReferenceCandidatePruningLevel;
if (pruningLevel >= 2)
{
for (int previousIndex = 0; previousIndex < referenceIndex; previousIndex++)
{
ref ReferenceSearchResult previous = ref state.References[previousIndex];
if (!previous.HasFullResult)
{
continue;
}
if (previous.FullVector == integerVector && previous.FullRate + previous.DrlRate <= integerRate + drlRate)
{
return false;
}
// Level three permits a quarter more search error; level four compares the original
// error directly. This only prunes when the earlier reference also has cheaper selection syntax.
int threshold = pruningLevel == 3 ? previous.FullCost + (previous.FullCost >> 2) : previous.FullCost;
if (pruningLevel >= 3 && best.Cost > threshold && previous.DrlRate < drlRate)
{
return false;
}
}
}
result = new FractionalResult(integerVector, best.Variance, best.SquaredError, best.MotionCost);
if (!forceInteger && best.Cost < int.MaxValue)
{
Rectangle fractionalBounds = referenceVector.GetSubpixelSearchBounds(this.frameBounds);
FractionalSearch<TSample, TOperator> fractionalSearch = new(
this.source,
this.sourceStride,
this.reference,
this.referenceStride,
this.referenceOrigin,
this.prediction,
size,
fractionalBounds,
referenceVector,
this.motionCosts,
this.bitDepth,
this.rateMultiplier);
Span<Av1MotionVector> centers = stackalloc Av1MotionVector[3];
centers.Fill(new Av1MotionVector(short.MinValue, short.MinValue));
int firstCost = fractionalSearch.Search(
integerVector,
best,
settings.FractionalMethod,
SearchPrecision.EighthSample,
allowHighPrecision,
settings.FractionalIterationsPerStep,
settings.FractionalInterpolationTaps,
ReadOnlySpan<int>.Empty,
centers,
out result);
if (second.HasValue && second.Value != best.Vector && settings.SecondCandidateSelection <= CandidateSelection.Variance)
{
Point secondPoint = second.Value;
Av1MotionVector secondStart = new(secondPoint.Y * 8, secondPoint.X * 8);
if (fractionalBounds.Contains(secondStart.Column, secondStart.Row))
{
int secondCost = fractionalSearch.Search(
secondStart,
null,
settings.FractionalMethod,
SearchPrecision.EighthSample,
allowHighPrecision,
settings.FractionalIterationsPerStep,
settings.FractionalInterpolationTaps,
ReadOnlySpan<int>.Empty,
centers,
out FractionalResult secondResult);
if (settings.SecondCandidateSelection == CandidateSelection.RateDistortion && secondCost != int.MaxValue)
{
long firstRateDistortion = this.EstimateCandidate(result.Vector, referenceVector);
long secondRateDistortion = this.EstimateCandidate(secondResult.Vector, referenceVector);
if (secondRateDistortion < firstRateDistortion)
{
result = secondResult;
}
}
else if (secondCost < firstCost)
{
result = secondResult;
}
}
}
if (pruningLevel >= 1)
{
int fractionalRate = ((this.motionCosts.GetCost(result.Vector, referenceVector) * 108) + 64) >> 7;
for (int previousIndex = 0; previousIndex < referenceIndex; previousIndex++)
{
ref ReferenceSearchResult previous = ref state.References[previousIndex];
if (!previous.IsValid || previous.Vector != result.Vector)
{
continue;
}
// A previously skipped matching mode remains skipped regardless of rate. Otherwise,
// preserve the earlier mode whenever its motion-plus-reference syntax is no more expensive.
if (previous.Skip || previous.Rate + previous.DrlRate <= fractionalRate + drlRate)
{
current.Skip = true;
break;
}
}
}
}
// Weight only the motion-vector syntax. The transform and differential-reference rates retain
// their own 1/512-bit units; applying this factor to their sum would change the mode decision.
current.Rate = ((this.motionCosts.GetCost(result.Vector, referenceVector) * 108) + 64) >> 7;
current.Vector = result.Vector;
current.IsValid = true;
return true;
}
/// <summary>
/// Compares a refined vector using final prediction, transform rate, and differential motion rate.
/// </summary>
/// <param name="vector">The refined candidate vector.</param>
/// <param name="referenceVector">The differential coding reference.</param>
/// <returns>The rate-distortion estimate excluding the block skip-header cost.</returns>
private long EstimateCandidate(Av1MotionVector vector, Av1MotionVector referenceVector)
{
int width = this.blockSize.GetWidth();
int height = this.blockSize.GetHeight();
int origin = this.referenceOrigin + ((vector.Row >> 3) * this.referenceStride) + (vector.Column >> 3);
TOperator.PreparePrediction(
this.source,
this.sourceStride,
this.reference,
this.referenceStride,
origin,
this.prediction,
this.residual,
this.convolutionScratch,
width,
height,
this.horizontalFilter,
this.verticalFilter,
(vector.Column & 7) << 1,
(vector.Row & 7) << 1,
this.bitDepth.GetBitCount());
Av1TransformBlockEncoder.EstimateInterTransform(
this.workspace,
this.residual,
width,
this.quantized,
this.writer,
this.aboveContexts,
this.leftContexts,
this.blockSize,
new Size(width, height),
this.blockSize.GetMaximumTransformSize(),
this.qIndex,
this.dcDeltaQ,
this.bitDepth,
this.sharpness,
this.lossless,
this.rateMultiplier,
this.transformSizeRate,
this.noSkipRate,
this.skipRate,
long.MaxValue,
out Av1RateDistortionStatistics statistics,
out _,
out _);
int motionRate = ((this.motionCosts.GetCost(vector, referenceVector) * 108) + 64) >> 7;
return Av1RateDistortion.GetCost(this.rateMultiplier, statistics.Rate + motionRate, statistics.Distortion);
}
}
/// <summary>
/// Orders temporal starts by descending represented analysis weight.
/// </summary>
private readonly struct StartingCandidateWeightComparer : IComparer<StartingCandidate>
{
/// <inheritdoc/>
public int Compare(StartingCandidate x, StartingCandidate y) => y.Weight.CompareTo(x.Weight);
}
}

90
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.UInt16Operator.cs

@ -0,0 +1,90 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
internal static partial class Av1MotionSearchBase
{
/// <summary>
/// Measures high-bit-depth sample errors with the shared vector-width residual traversal.
/// </summary>
public readonly struct UInt16Operator : IMotionSearchOperator<ushort>
{
/// <inheritdoc/>
public static void PreparePrediction(
ReadOnlySpan<ushort> source,
int sourceStride,
ReadOnlySpan<ushort> reference,
int referenceStride,
int referenceOrigin,
Span<ushort> prediction,
Span<short> residual,
Span<short> scratch,
int width,
int height,
Av1InterpolationFilter horizontalFilter,
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int verticalPhase,
int bitDepth)
{
Av1TranslationalInterPredictor.Predict(
reference,
referenceStride,
referenceOrigin,
prediction,
width,
width,
height,
horizontalFilter,
verticalFilter,
horizontalPhase,
verticalPhase,
bitDepth,
scratch);
Av1ResidualBuilder.Subtract(source, sourceStride, prediction, width, residual, width, width, height);
}
/// <inheritdoc/>
public static void Predict(
ReadOnlySpan<ushort> reference,
int referenceStride,
int referenceOrigin,
Span<ushort> buffer,
int width,
int height,
int horizontalPhase,
int verticalPhase,
int taps,
int bitDepth)
=> Av1TranslationalInterPredictor.PredictForSearch(
reference, referenceStride, referenceOrigin, buffer, width, height, horizontalPhase, verticalPhase, taps, bitDepth);
/// <inheritdoc/>
public static int SumAbsoluteDifferences(
ReadOnlySpan<ushort> source,
int sourceStride,
ReadOnlySpan<ushort> prediction,
int predictionStride,
int width,
int height,
int rowStep)
=> Av1ResidualBuilder.SumAbsoluteDifferences(source, sourceStride, prediction, predictionStride, width, height, rowStep);
/// <inheritdoc/>
public static void GetMoments(
ReadOnlySpan<ushort> source,
int sourceStride,
ReadOnlySpan<ushort> prediction,
int predictionStride,
int width,
int height,
out int sum,
out long squares)
=> Av1ResidualBuilder.GetMoments(source, sourceStride, prediction, predictionStride, width, height, out sum, out squares);
}
}

674
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.cs

@ -0,0 +1,674 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Motion.Av1MotionSearchSettings;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
/// <summary>
/// Owns motion-search traversal while closed sample operators measure prediction errors.
/// </summary>
internal static partial class Av1MotionSearchBase
{
/// <summary>
/// Retains the integer winner's distortion and rate separately for fractional refinement.
/// </summary>
public readonly struct FullPixelResult
{
/// <summary>
/// Initializes a new instance of the <see cref="FullPixelResult"/> struct.
/// </summary>
/// <param name="vector">The displacement in full samples.</param>
/// <param name="variance">The variance in the eight-bit error domain.</param>
/// <param name="squaredError">The squared residual sum in the eight-bit error domain.</param>
/// <param name="motionCost">The variance-domain motion-rate cost.</param>
public FullPixelResult(Point vector, int variance, int squaredError, int motionCost)
{
this.Vector = vector;
this.Variance = variance;
this.SquaredError = squaredError;
this.MotionCost = motionCost;
}
/// <summary>
/// Gets the displacement in full samples.
/// </summary>
public Point Vector { get; }
/// <summary>
/// Gets the normalized residual variance.
/// </summary>
public int Variance { get; }
/// <summary>
/// Gets the normalized squared residual sum.
/// </summary>
public int SquaredError { get; }
/// <summary>
/// Gets the variance-domain motion-rate cost.
/// </summary>
public int MotionCost { get; }
/// <summary>
/// Gets the total cost used to compare completed search paths.
/// </summary>
public int Cost => this.Variance + this.MotionCost;
}
/// <summary>
/// Borrows source, reference, and rate state for all integer candidates of a prediction block.
/// </summary>
/// <typeparam name="TSample">The unsigned component storage type.</typeparam>
/// <typeparam name="TOperator">The closed sample-error operator.</typeparam>
public readonly ref struct FullPixelSearch<TSample, TOperator>
where TSample : unmanaged
where TOperator : struct, IMotionSearchOperator<TSample>
{
private readonly ReadOnlySpan<TSample> source;
private readonly ReadOnlySpan<TSample> reference;
private readonly int sourceStride;
private readonly int referenceStride;
private readonly int referenceOrigin;
private readonly Size blockSize;
private readonly Rectangle bounds;
private readonly Av1MotionVector referenceVector;
private readonly Av1MotionVector integerReferenceVector;
private readonly Av1MotionVectorCosts costs;
private readonly int precisionShift;
private readonly int sadPerBit;
private readonly int rateMultiplier;
/// <summary>
/// Initializes a new instance of the <see cref="FullPixelSearch{TSample, TOperator}"/> struct.
/// </summary>
/// <param name="source">Source samples beginning at the block origin.</param>
/// <param name="sourceStride">The source row stride in samples.</param>
/// <param name="reference">The complete retained reference storage including its border.</param>
/// <param name="referenceStride">The reference row stride in samples.</param>
/// <param name="referenceOrigin">The reference index corresponding to the current block origin.</param>
/// <param name="blockSize">The prediction dimensions.</param>
/// <param name="bounds">The permitted displacement rectangle, with exclusive upper edges.</param>
/// <param name="referenceVector">The spatial reference in eighth-sample units.</param>
/// <param name="costs">The retained motion-rate tables.</param>
/// <param name="bitDepth">The coded component precision.</param>
/// <param name="sadPerBit">The quantizer-derived rate scale for absolute differences.</param>
/// <param name="rateMultiplier">The block rate multiplier for variance costs.</param>
public FullPixelSearch(
ReadOnlySpan<TSample> source,
int sourceStride,
ReadOnlySpan<TSample> reference,
int referenceStride,
int referenceOrigin,
Size blockSize,
Rectangle bounds,
Av1MotionVector referenceVector,
Av1MotionVectorCosts costs,
Av1BitDepth bitDepth,
int sadPerBit,
int rateMultiplier)
{
this.source = source;
this.sourceStride = sourceStride;
this.reference = reference;
this.referenceStride = referenceStride;
this.referenceOrigin = referenceOrigin;
this.blockSize = blockSize;
this.bounds = bounds;
this.referenceVector = referenceVector;
this.costs = costs;
this.precisionShift = bitDepth.GetBitCount() - 8;
this.sadPerBit = sadPerBit;
this.rateMultiplier = rateMultiplier;
// Nearest full-sample rounding breaks half-sample ties away from zero. SAD compares integer
// differences from that rounded reference; variance retains the original subpixel difference.
int row = (referenceVector.Row + 3 + (referenceVector.Row >= 0 ? 1 : 0)) >> 3;
int column = (referenceVector.Column + 3 + (referenceVector.Column >= 0 ? 1 : 0)) >> 3;
this.integerReferenceVector = new Av1MotionVector(row * 8, column * 8);
}
/// <summary>
/// Runs the selected full-pixel search, restarts, mesh decision, and neighboring cost publication.
/// </summary>
/// <param name="start">The initial displacement in full samples.</param>
/// <param name="stepParameter">The number of outer search stages already excluded by frame and block policy.</param>
/// <param name="method">The block-selected search method.</param>
/// <param name="sites">The retained geometry configured for this method and reference stride.</param>
/// <param name="settings">The resolved frame motion policy.</param>
/// <param name="keyFrame">Whether key-frame policy prevents adaptive alternate-row SAD.</param>
/// <param name="fineMeshInterval">Whether content classification caps the initial mesh interval at four.</param>
/// <param name="costList">Five costs: center, left, down, right, and up; empty when neighborhood publication is disabled.</param>
/// <param name="secondBest">The preceding integer winner, when the selected traversal supplies one.</param>
/// <returns>The integer winner with its retained variance, squared error, and motion cost.</returns>
public FullPixelResult Search(
Point start,
int stepParameter,
FullPixelSearchMethod method,
Av1MotionSearchSites sites,
Av1MotionSearchSettings settings,
bool keyFrame,
bool fineMeshInterval,
Span<int> costList,
out Point? secondBest)
{
Point clampedStart = this.Clamp(start);
int rowStep = 1;
if (this.blockSize.Height >= 16)
{
if (settings.DownsampledSadLevel == 2)
{
rowStep = 2;
}
else if (settings.DownsampledSadLevel == 1 && !keyFrame)
{
int evenSad = this.GetSad(clampedStart, 2, 0);
int oddSad = this.GetSad(clampedStart, 2, 1);
if ((Math.Abs(evenSad - oddSad) * 4) < evenSad)
{
rowStep = 2;
}
}
}
// An alternate-row search may alias vertical texture. If its final candidate exposes that aliasing,
// repeat the same complete search with full SAD; candidate state and cost-list state both restart.
while (true)
{
secondBest = null;
FullPixelResult best;
bool centerCostOnly = false;
if (method <= FullPixelSearchMethod.ClampedDiamond)
{
best = this.SearchDiamond(clampedStart, stepParameter, sites, rowStep, ref secondBest);
}
else
{
best = this.SearchPattern(clampedStart, stepParameter, method, sites, rowStep, out centerCostOnly);
}
if (centerCostOnly && !costList.IsEmpty)
{
// An initial finest-scale winner skips the four-point refinement stage. Its neighbors
// have not been published, so fractional pruning must see them as unavailable.
costList.Fill(int.MaxValue);
costList[0] = this.GetSadCost(best.Vector, rowStep);
}
else if (!costList.IsEmpty)
{
this.FillCostList(best.Vector, rowStep, costList);
}
int areaLog2 = BitOperations.Log2((uint)(this.blockSize.Width * this.blockSize.Height));
bool runMesh = method is FullPixelSearchMethod.NStep or FullPixelSearchMethod.EightPointNStep
&& best.Cost > (settings.MeshErrorThreshold >> (14 - areaLog2));
// Distance is measured from the caller's original start, before range clamping.
if (settings.MeshPruningLevel == 2 &&
Math.Max(Math.Abs(start.X - best.Vector.X), Math.Abs(start.Y - best.Vector.Y)) <= 4)
{
runMesh = false;
}
if (rowStep == 2)
{
int fullSad = this.GetSad(best.Vector, 1, 0);
int skippedSad = this.GetSad(best.Vector, 2, 0);
int threshold = (this.blockSize.Width * this.blockSize.Height) >> 4;
if (fullSad > threshold && Math.Abs(skippedSad - fullSad) * 10 >= Math.Max(fullSad, 1) * 9)
{
rowStep = 1;
continue;
}
}
if (runMesh)
{
FullPixelResult mesh = this.SearchMesh(
best.Vector, settings.GetMeshPattern(intraBlockCopy: false), fineMeshInterval, rowStep, ref secondBest);
// The mesh publishes its neighborhood and preceding winner before its final variance comparison.
// Keep that publication order so later fractional selection sees the same retained search state.
if (!costList.IsEmpty)
{
this.FillCostList(mesh.Vector, rowStep, costList);
}
if (mesh.Cost < best.Cost)
{
best = mesh;
}
}
return best;
}
}
/// <summary>
/// Runs decreasing-radius searches from the same start and compares their winners using variance.
/// </summary>
private FullPixelResult SearchDiamond(
Point start,
int stepParameter,
Av1MotionSearchSites sites,
int rowStep,
ref Point? secondBest)
{
int startCost = this.GetSadCost(start, rowStep);
Point winner = this.SearchDiamondSteps(start, startCost, stepParameter, sites, rowStep, ref secondBest, out int centeredSteps);
FullPixelResult best = this.GetVarianceResult(winner);
int furtherSteps = sites.StageCount - 1 - stepParameter;
while (centeredSteps < furtherSteps)
{
centeredSteps++;
winner = this.SearchDiamondSteps(
start, startCost, stepParameter + centeredSteps, sites, rowStep, ref secondBest, out int skippedSteps);
FullPixelResult candidate = this.GetVarianceResult(winner);
if (candidate.Cost < best.Cost)
{
best = candidate;
}
centeredSteps += skippedSteps;
}
return best;
}
/// <summary>
/// Visits ordered sites once per radius, retaining initial center stays for later restart pruning.
/// </summary>
private Point SearchDiamondSteps(
Point start,
int startCost,
int stepParameter,
Av1MotionSearchSites sites,
int rowStep,
ref Point? secondBest,
out int centeredSteps)
{
Point best = start;
int bestCost = startCost;
bool movedFromStart = false;
centeredSteps = 0;
for (int stage = sites.StageCount - stepParameter - 1; stage >= 0; stage--)
{
ReadOnlySpan<Av1MotionSearchSites.Site> stageSites = sites.GetSites(stage);
int centerIndex = this.referenceOrigin + (best.Y * this.referenceStride) + best.X;
int bestSite = 0;
for (int index = 1; index <= sites.GetCandidateCount(stage); index++)
{
Av1MotionSearchSites.Site site = stageSites[index];
Point candidate = new(best.X + site.Column, best.Y + site.Row);
if (this.bounds.Contains(candidate) && this.TryImproveSad(candidate, centerIndex + site.Offset, rowStep, ref bestCost))
{
bestSite = index;
}
}
if (bestSite != 0)
{
secondBest = best;
Av1MotionSearchSites.Site site = stageSites[bestSite];
best = new Point(best.X + site.Column, best.Y + site.Row);
movedFromStart = true;
}
if (!movedFromStart)
{
centeredSteps++;
}
// Repeated outer radii can be skipped after a center stay; after a move they must remain eligible.
if (bestSite == 0 && stage > 2)
{
while (stage > 2 && sites.GetRadius(stage - 1) == sites.GetRadius(stage))
{
centeredSteps++;
stage--;
}
}
}
return best;
}
/// <summary>
/// Selects an initial scale, then walks adjacent sites around each winning direction before reducing scale.
/// </summary>
private FullPixelResult SearchPattern(
Point start,
int stepParameter,
FullPixelSearchMethod method,
Av1MotionSearchSites sites,
int rowStep,
out bool centerCostOnly)
{
bool initialSearch = method is FullPixelSearchMethod.Hexagon or FullPixelSearchMethod.BigDiamond;
centerCostOnly = false;
int minimumStep = method switch
{
FullPixelSearchMethod.FastBigDiamond => 8,
FullPixelSearchMethod.FastDiamond => 9,
FullPixelSearchMethod.VeryFastDiamond => 10,
_ => 0
};
int initialScale = 10 - Math.Min(Math.Max(stepParameter, minimumStep), 10);
int bestCost = this.GetSadCost(start, rowStep);
Point best = start;
int direction = -1;
if (initialSearch)
{
int maximumScale = initialScale;
initialScale = -1;
for (int scale = 0; scale <= maximumScale; scale++)
{
int candidateIndex = this.FindBestSite(start, scale, sites, rowStep, ref bestCost);
if (candidateIndex >= 0)
{
initialScale = scale;
direction = candidateIndex;
}
}
if (initialScale >= 0)
{
Av1MotionSearchSites.Site site = sites.GetSites(initialScale)[direction];
best = new Point(start.X + site.Column, start.Y + site.Row);
}
}
if (initialScale >= 0)
{
bool fourPointFinalStage = sites.GetCandidateCount(0) == 4;
centerCostOnly = fourPointFinalStage && initialSearch && initialScale == 0;
int lastScale = fourPointFinalStage ? 1 : 0;
for (int scale = initialScale; scale >= lastScale; scale--)
{
ReadOnlySpan<Av1MotionSearchSites.Site> stageSites = sites.GetSites(scale);
if (!initialSearch || scale != initialScale)
{
int candidateIndex = this.FindBestSite(best, scale, sites, rowStep, ref bestCost);
if (candidateIndex < 0)
{
continue;
}
direction = candidateIndex;
Av1MotionSearchSites.Site site = stageSites[direction];
best = new Point(best.X + site.Column, best.Y + site.Row);
}
best = this.FollowPatternDirection(best, scale, direction, sites, rowStep, ref bestCost);
}
// Four-point patterns retain a separate final-stage entry decision. When the initial scale
// is already zero, its initial winner is published without another directional walk.
if (fourPointFinalStage && (!initialSearch || initialScale != 0))
{
int candidateIndex = this.FindBestSite(best, 0, sites, rowStep, ref bestCost);
if (candidateIndex >= 0)
{
Av1MotionSearchSites.Site site = sites.GetSites(0)[candidateIndex];
best = new Point(best.X + site.Column, best.Y + site.Row);
best = this.FollowPatternDirection(best, 0, candidateIndex, sites, rowStep, ref bestCost);
}
}
}
return this.GetVarianceResult(best);
}
/// <summary>
/// Tests the complete stage around a fixed center, keeping the first candidate on equal cost.
/// </summary>
private int FindBestSite(Point center, int stage, Av1MotionSearchSites sites, int rowStep, ref int bestCost)
{
ReadOnlySpan<Av1MotionSearchSites.Site> stageSites = sites.GetSites(stage);
int centerIndex = this.referenceOrigin + (center.Y * this.referenceStride) + center.X;
int count = sites.GetCandidateCount(stage);
int radius = sites.GetRadius(stage);
if (center.X - radius >= this.bounds.Left && center.X + radius < this.bounds.Right &&
center.Y - radius >= this.bounds.Top && center.Y + radius < this.bounds.Bottom)
{
// Interior pattern stages visit complete four-site groups. For a six-site hexagon the final
// two sites are visited only by the boundary path, so range classification affects selection.
count &= ~3;
}
int bestIndex = -1;
for (int index = 0; index < count; index++)
{
Av1MotionSearchSites.Site site = stageSites[index];
Point candidate = new(center.X + site.Column, center.Y + site.Row);
if (this.bounds.Contains(candidate) && this.TryImproveSad(candidate, centerIndex + site.Offset, rowStep, ref bestCost))
{
bestIndex = index;
}
}
return bestIndex;
}
/// <summary>
/// Walks the previous, same, and next directions around the ring until none improves the current center.
/// </summary>
private Point FollowPatternDirection(
Point center,
int stage,
int direction,
Av1MotionSearchSites sites,
int rowStep,
ref int bestCost)
{
int count = sites.GetCandidateCount(stage);
ReadOnlySpan<Av1MotionSearchSites.Site> stageSites = sites.GetSites(stage);
while (true)
{
int centerIndex = this.referenceOrigin + (center.Y * this.referenceStride) + center.X;
int bestIndex = -1;
for (int relative = -1; relative <= 1; relative++)
{
int index = (direction + relative + count) % count;
Av1MotionSearchSites.Site site = stageSites[index];
Point candidate = new(center.X + site.Column, center.Y + site.Row);
if (this.bounds.Contains(candidate) && this.TryImproveSad(candidate, centerIndex + site.Offset, rowStep, ref bestCost))
{
bestIndex = index;
}
}
if (bestIndex < 0)
{
return center;
}
direction = bestIndex;
Av1MotionSearchSites.Site winningSite = stageSites[direction];
center = new Point(center.X + winningSite.Column, center.Y + winningSite.Row);
}
}
/// <summary>
/// Runs content-selected mesh passes, adjusting the initial range to the current displacement magnitude.
/// </summary>
private FullPixelResult SearchMesh(Point start, ReadOnlySpan<int> pattern, bool fineInterval, int rowStep, ref Point? secondBest)
{
int originalRange = pattern[0];
int interval = pattern[1];
int range = Math.Min(Math.Max(originalRange, (5 * Math.Max(Math.Abs(start.X), Math.Abs(start.Y))) / 4), 256);
interval = Math.Max(interval, range / (originalRange / interval));
if (fineInterval)
{
interval = Math.Min(interval, 4);
}
Point best = this.SearchMeshPass(start, range, interval, rowStep, ref secondBest);
if (interval > 1 && range > 7)
{
for (int pass = 1; pass < 4; pass++)
{
best = this.SearchMeshPass(best, pattern[pass * 2], pattern[(pass * 2) + 1], rowStep, ref secondBest);
if (pattern[(pass * 2) + 1] == 1)
{
break;
}
}
}
return this.GetVarianceResult(best);
}
/// <summary>
/// Scans mesh rows from a fixed center; each strict replacement retains the previous winner.
/// </summary>
private Point SearchMeshPass(Point start, int range, int interval, int rowStep, ref Point? secondBest)
{
start = this.Clamp(start);
Point best = start;
int bestCost = this.GetSadCost(start, rowStep);
int minimumRow = Math.Max(-range, this.bounds.Top - start.Y);
int maximumRow = Math.Min(range, this.bounds.Bottom - 1 - start.Y);
int minimumColumn = Math.Max(-range, this.bounds.Left - start.X);
int maximumColumn = Math.Min(range, this.bounds.Right - 1 - start.X);
int columnStep = interval > 1 ? interval : 4;
for (int row = minimumRow; row <= maximumRow; row += interval)
{
for (int column = minimumColumn; column <= maximumColumn; column += columnStep)
{
// A complete unit-step group visits four adjacent columns in order. The partial terminal
// group has an exclusive end; preserve that edge rule rather than widening the searched set.
int count = interval > 1 ? 1 : column + 3 <= maximumColumn ? 4 : maximumColumn - column;
for (int index = 0; index < count; index++)
{
Point candidate = new(start.X + column + index, start.Y + row);
if (this.TryImproveSad(candidate, rowStep, ref bestCost))
{
secondBest = best;
best = candidate;
}
}
}
}
return best;
}
/// <summary>
/// Publishes SAD-plus-rate values at the center and its four axial neighbors for fractional pruning.
/// </summary>
private void FillCostList(Point best, int rowStep, Span<int> costList)
{
costList[0] = this.GetSadCost(best, rowStep);
ReadOnlySpan<sbyte> offsets = [0, -1, 1, 0, 0, 1, -1, 0];
for (int index = 0; index < 4; index++)
{
Point candidate = new(best.X + offsets[(index * 2) + 1], best.Y + offsets[index * 2]);
costList[index + 1] = this.bounds.Contains(candidate) ? this.GetSadCost(candidate, rowStep) : int.MaxValue;
}
}
/// <summary>
/// Clamps a starting displacement to the prediction-distinct full-pixel range.
/// </summary>
private Point Clamp(Point vector)
=> new(Math.Clamp(vector.X, this.bounds.Left, this.bounds.Right - 1), Math.Clamp(vector.Y, this.bounds.Top, this.bounds.Bottom - 1));
/// <summary>
/// Rejects candidates whose prediction error alone already reaches the best combined cost.
/// </summary>
private bool TryImproveSad(Point vector, int rowStep, ref int bestCost)
=> this.TryImproveSad(vector, this.referenceOrigin + (vector.Y * this.referenceStride) + vector.X, rowStep, ref bestCost);
/// <summary>
/// Measures a candidate using the retained site's offset, avoiding repeated stride multiplication.
/// </summary>
private bool TryImproveSad(Point vector, int referenceIndex, int rowStep, ref int bestCost)
{
int sad = this.GetSad(referenceIndex, rowStep, 0);
if (sad >= bestCost)
{
return false;
}
int rate = this.costs.GetCost(new Av1MotionVector(vector.Y * 8, vector.X * 8), this.integerReferenceVector);
int cost = Av1RateDistortion.GetMotionSearchSadCost(this.sadPerBit, rate, sad);
if (cost >= bestCost)
{
return false;
}
bestCost = cost;
return true;
}
/// <summary>
/// Measures the complete absolute-difference cost in the eight-bit error domain.
/// </summary>
private int GetSadCost(Point vector, int rowStep)
{
int sad = this.GetSad(vector, rowStep, 0);
int rate = this.costs.GetCost(new Av1MotionVector(vector.Y * 8, vector.X * 8), this.integerReferenceVector);
return Av1RateDistortion.GetMotionSearchSadCost(this.sadPerBit, rate, sad);
}
/// <summary>
/// Measures raw sample differences and truncates only after alternate-row scaling.
/// </summary>
private int GetSad(Point vector, int rowStep, int firstRow)
=> this.GetSad(this.referenceOrigin + (vector.Y * this.referenceStride) + vector.X, rowStep, firstRow);
/// <summary>
/// Measures the requested row parity at a retained reference offset.
/// </summary>
private int GetSad(int referenceIndex, int rowStep, int firstRow)
{
referenceIndex += firstRow * this.referenceStride;
int sad = TOperator.SumAbsoluteDifferences(
this.source[(firstRow * this.sourceStride)..],
this.sourceStride,
this.reference[referenceIndex..],
this.referenceStride,
this.blockSize.Width,
this.blockSize.Height - firstRow,
rowStep);
return sad >> this.precisionShift;
}
/// <summary>
/// Retains normalized moments and subpixel-reference motion rate for a completed integer winner.
/// </summary>
private FullPixelResult GetVarianceResult(Point vector)
{
int referenceIndex = this.referenceOrigin + (vector.Y * this.referenceStride) + vector.X;
TOperator.GetMoments(
this.source,
this.sourceStride,
this.reference[referenceIndex..],
this.referenceStride,
this.blockSize.Width,
this.blockSize.Height,
out int sum,
out long squares);
if (this.precisionShift != 0)
{
// Signed sums and squared sums have different scales. Round each before removing the mean;
// cancellation may make the rounded variance negative, so clamp the final variance to zero.
sum = (sum + (1 << (this.precisionShift - 1))) >> this.precisionShift;
int squaredShift = this.precisionShift * 2;
squares = (squares + (1L << (squaredShift - 1))) >> squaredShift;
}
int variance = (int)Math.Max(squares - (((long)sum * sum) / (this.blockSize.Width * this.blockSize.Height)), 0);
int rate = this.costs.GetCost(new Av1MotionVector(vector.Y * 8, vector.X * 8), this.referenceVector);
int motionCost = Av1RateDistortion.GetMotionSearchCost(this.rateMultiplier, rate, 0);
return new FullPixelResult(vector, variance, (int)squares, motionCost);
}
}
}

485
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchSettings.cs

@ -0,0 +1,485 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
/// <summary>
/// Resolves frame-dependent motion-search policies before block traversal.
/// </summary>
internal readonly struct Av1MotionSearchSettings
{
private readonly HeifEncodingSpeed speed;
private readonly FullPixelSearchMethod fullPixelMethod;
private readonly int fasterSearchMinimumDimension;
/// <summary>
/// Initializes a new instance of the <see cref="Av1MotionSearchSettings"/> struct.
/// </summary>
/// <param name="speed">The encoding speed.</param>
/// <param name="intraOnly">Whether every frame is coded independently.</param>
/// <param name="frameSize">The visible frame dimensions.</param>
/// <param name="qIndex">The base quantizer index.</param>
/// <param name="boostedFrame">Whether this is a key, golden, or alternate-reference frame with boosted quality.</param>
/// <param name="screenContent">Whether the content classification identifies graphics or screen content.</param>
public Av1MotionSearchSettings(
HeifEncodingSpeed speed,
bool intraOnly,
Size frameSize,
int qIndex,
bool boostedFrame,
bool screenContent)
{
this.speed = speed;
this.fullPixelMethod = FullPixelSearchMethod.NStep;
this.FractionalMethod = FractionalSearchMethod.TwoLevelTree;
this.FractionalIterationsPerStep = 2;
this.FractionalInterpolationTaps = 8;
this.SimpleMotionPrecision = SearchPrecision.EighthSample;
this.SecondCandidateSelection = CandidateSelection.RateDistortion;
this.AllowIntraBlockCopy = true;
this.MeshErrorThreshold = 1 << (screenContent ? 20 : 25);
// Apply coding-mode choices before resolution and quantizer overrides. Reversing that order can
// incorrectly suppress a second motion candidate or replace a quantizer-selected search pattern.
if (speed >= HeifEncodingSpeed.Level1)
{
this.MeshErrorThreshold <<= 1;
}
if (intraOnly)
{
this.PruneIntraBlockCopyHashCandidates = speed >= HeifEncodingSpeed.Level1;
this.AutomaticStepSizeLevel = speed >= HeifEncodingSpeed.Level2 ? 1 : 0;
this.LimitFullPixelStartingCandidates = speed >= HeifEncodingSpeed.Level3;
if (speed >= HeifEncodingSpeed.Level3)
{
this.fullPixelMethod = FullPixelSearchMethod.Diamond;
}
if (speed >= HeifEncodingSpeed.Level4)
{
this.FractionalMethod = FractionalSearchMethod.MorePrunedTree;
this.SimpleMotionPrecision = SearchPrecision.HalfSample;
this.ReduceSearchRange = true;
this.LimitIntraBlockCopyHashBlockSize = true;
}
this.MeshPruningLevel = speed >= HeifEncodingSpeed.Level5 ? 2 : 0;
if (speed >= HeifEncodingSpeed.Level6)
{
this.fasterSearchMinimumDimension = 32;
this.UseFastIntraBlockCopySearch = true;
}
}
else
{
this.DisableExtensiveJointSearch = true;
if (speed >= HeifEncodingSpeed.Level1)
{
this.UseRefiningObmcSearch = true;
this.FractionalInterpolationTaps = 4;
}
if (speed >= HeifEncodingSpeed.Level2)
{
this.SimpleMotionPrecision = SearchPrecision.QuarterSample;
this.FractionalIterationsPerStep = 1;
this.ReduceSearchRange = true;
}
if (speed >= HeifEncodingSpeed.Level3)
{
this.FractionalMethod = FractionalSearchMethod.PrunedTree;
this.fullPixelMethod = FullPixelSearchMethod.Diamond;
this.SecondCandidateSelection = CandidateSelection.FirstOnly;
this.MeshPruningLevel = 1;
this.AllowIntraBlockCopy = false;
this.MotionCostUpdate = CostUpdateFrequency.SuperblockRow;
}
if (speed >= HeifEncodingSpeed.Level4)
{
this.FractionalMethod = FractionalSearchMethod.MorePrunedTree;
this.SimpleMotionPrecision = SearchPrecision.HalfSample;
this.MeshPruningLevel = 2;
}
this.UseDiamondWarpSearch = speed >= HeifEncodingSpeed.Level5;
if (speed >= HeifEncodingSpeed.Level6)
{
this.SimpleMotionPrecision = SearchPrecision.Integer;
}
}
// Resolution classes use the shorter dimension, so rotating a frame does not change its class.
int minimumDimension = Math.Min(frameSize.Width, frameSize.Height);
bool is720pOrLarger = minimumDimension >= 720;
this.DownsampledSadLevel = is720pOrLarger ? 2 : 0;
if (!intraOnly)
{
this.ReferenceCandidatePruningLevel = speed >= HeifEncodingSpeed.Level5 ? 4
: speed >= HeifEncodingSpeed.Level4 && minimumDimension <= 480 ? 3
: speed >= HeifEncodingSpeed.Level3 ? 2
: speed >= HeifEncodingSpeed.Level1 ? 1 : 0;
if (speed >= HeifEncodingSpeed.Level2)
{
this.AutomaticStepSizeLevel = is720pOrLarger ? 1 : 2;
this.SecondCandidateSelection = !is720pOrLarger
? CandidateSelection.Variance
: boostedFrame ? CandidateSelection.RateDistortion : CandidateSelection.FirstOnly;
}
if (speed >= HeifEncodingSpeed.Level4 && minimumDimension < 480)
{
this.StartCandidatePruningLevel = boostedFrame ? 0 : 1;
}
if (speed >= HeifEncodingSpeed.Level5)
{
this.StartCandidatePruningLevel = boostedFrame ? 0 : 1;
if (!is720pOrLarger)
{
this.DownsampledSadLevel = 1;
this.MotionCostUpdate = CostUpdateFrequency.SuperblockRowSet;
}
}
if (speed >= HeifEncodingSpeed.Level6)
{
this.StartCandidatePruningLevel = boostedFrame ? 0 : 2;
this.fasterSearchMinimumDimension = is720pOrLarger ? 128 : 64;
}
}
// Coarse quantization selects a less expensive full-pixel pattern even at the slower speed levels.
// These thresholds apply to the coding pass; first-pass statistics use a separate configuration.
if (speed <= HeifEncodingSpeed.Level2)
{
int coarseThreshold;
int intermediateThreshold;
if (is720pOrLarger)
{
coarseThreshold = speed == HeifEncodingSpeed.Level2 ? 200 : 255;
intermediateThreshold = speed == HeifEncodingSpeed.Level0 ? 200 : -1;
}
else
{
coarseThreshold = speed == HeifEncodingSpeed.Level0 ? 200 : 170;
intermediateThreshold = speed switch
{
HeifEncodingSpeed.Level0 => 70,
HeifEncodingSpeed.Level1 => 50,
_ => 40
};
}
if (qIndex > coarseThreshold)
{
this.fullPixelMethod = is720pOrLarger
? FullPixelSearchMethod.Diamond
: FullPixelSearchMethod.ClampedDiamond;
}
else if (qIndex > intermediateThreshold)
{
this.fullPixelMethod = FullPixelSearchMethod.EightPointNStep;
}
}
}
/// <summary>
/// The serial tile traversal boundaries at which motion costs are refreshed.
/// </summary>
public enum CostUpdateFrequency
{
/// <summary>
/// Refresh before each superblock.
/// </summary>
Superblock,
/// <summary>
/// Refresh at the first superblock of each tile row.
/// </summary>
SuperblockRow,
/// <summary>
/// Refresh at evenly spaced sets of superblock rows within a tile.
/// </summary>
SuperblockRowSet
}
/// <summary>
/// The full-pixel search pattern.
/// </summary>
public enum FullPixelSearchMethod
{
/// <summary>
/// Repeated shrinking diamond searches.
/// </summary>
Diamond,
/// <summary>
/// Searches with eight or twelve sites at progressively smaller radii.
/// </summary>
NStep,
/// <summary>
/// Searches with eight sites at every radius.
/// </summary>
EightPointNStep,
/// <summary>
/// Diamond search with repeated, bounded initial radii.
/// </summary>
ClampedDiamond,
/// <summary>
/// Hexagonal search followed by local refinement.
/// </summary>
Hexagon,
/// <summary>
/// Large diamond search followed by local refinement.
/// </summary>
BigDiamond,
/// <summary>
/// Diamond search beginning at a reduced scale.
/// </summary>
FastDiamond,
/// <summary>
/// Large diamond search beginning at a reduced scale.
/// </summary>
FastBigDiamond,
/// <summary>
/// Diamond search with only the smallest scales.
/// </summary>
VeryFastDiamond
}
/// <summary>
/// The fractional-pixel search traversal.
/// </summary>
public enum FractionalSearchMethod
{
/// <summary>
/// Cardinal and selected diagonal searches with a second refinement level.
/// </summary>
TwoLevelTree,
/// <summary>
/// Pruned tree search using the integer cost neighborhood when available.
/// </summary>
PrunedTree,
/// <summary>
/// Pruned tree search with additional quadratic cost-surface prediction.
/// </summary>
MorePrunedTree
}
/// <summary>
/// The finest displacement examined by a fractional search.
/// </summary>
public enum SearchPrecision
{
/// <summary>
/// One eighth of a luma sample.
/// </summary>
EighthSample,
/// <summary>
/// One quarter of a luma sample.
/// </summary>
QuarterSample,
/// <summary>
/// One half of a luma sample.
/// </summary>
HalfSample,
/// <summary>
/// Whole luma samples.
/// </summary>
Integer
}
/// <summary>
/// The comparison used after refining a second motion candidate.
/// </summary>
public enum CandidateSelection
{
/// <summary>
/// Compare estimated transform rate and distortion.
/// </summary>
RateDistortion,
/// <summary>
/// Compare prediction variance and motion-vector rate.
/// </summary>
Variance,
/// <summary>
/// Refine only the first full-pixel winner.
/// </summary>
FirstOnly
}
/// <summary>
/// Gets the frequency at which selected motion symbols refresh the search cost tables.
/// </summary>
public CostUpdateFrequency MotionCostUpdate { get; }
/// <summary>
/// Gets the adaptation level for the initial full-pixel step.
/// </summary>
public int AutomaticStepSizeLevel { get; }
/// <summary>
/// Gets the fractional search traversal.
/// </summary>
public FractionalSearchMethod FractionalMethod { get; }
/// <summary>
/// Gets the refinement iterations at each fractional precision.
/// </summary>
public int FractionalIterationsPerStep { get; }
/// <summary>
/// Gets the interpolation tap count used during fractional search.
/// </summary>
public int FractionalInterpolationTaps { get; }
/// <summary>
/// Gets the finest precision used by preliminary simple-motion analysis.
/// </summary>
public SearchPrecision SimpleMotionPrecision { get; }
/// <summary>
/// Gets the variance threshold for following a stepped search with a mesh search.
/// </summary>
public int MeshErrorThreshold { get; }
/// <summary>
/// Gets a value indicating whether earlier reference-index results restrict subsequent search ranges.
/// </summary>
public bool ReduceSearchRange { get; }
/// <summary>
/// Gets the level used to prune mesh search based on motion displacement.
/// </summary>
public int MeshPruningLevel { get; }
/// <summary>
/// Gets a value indicating whether overlapped prediction uses local full-pixel refinement.
/// </summary>
public bool UseRefiningObmcSearch { get; }
/// <summary>
/// Gets a value indicating whether full-pixel search omits additional temporal-analysis starting candidates.
/// </summary>
public bool LimitFullPixelStartingCandidates { get; }
/// <summary>
/// Gets a value indicating whether intra-block-copy motion search is enabled.
/// </summary>
public bool AllowIntraBlockCopy { get; }
/// <summary>
/// Gets a value indicating whether block-copy hash search stops after the first 64 candidates.
/// </summary>
public bool PruneIntraBlockCopyHashCandidates { get; }
/// <summary>
/// Gets a value indicating whether block copy restricts geometry and uses pixel search only after hash search fails.
/// </summary>
public bool UseFastIntraBlockCopySearch { get; }
/// <summary>
/// Gets a value indicating whether block-copy hashing is restricted to 4x4 and 8x8 blocks.
/// </summary>
public bool LimitIntraBlockCopyHashBlockSize { get; }
/// <summary>
/// Gets the row-subsampling policy: zero disables it, one checks the starting SAD, and two checks only the final SAD.
/// </summary>
public int DownsampledSadLevel { get; }
/// <summary>
/// Gets a value indicating whether compound motion omits the extensive joint refinement search.
/// </summary>
public bool DisableExtensiveJointSearch { get; }
/// <summary>
/// Gets how two fractional motion candidates are compared.
/// </summary>
public CandidateSelection SecondCandidateSelection { get; }
/// <summary>
/// Gets a value indicating whether zero, four, or eight neighboring start/reference positions can reuse an earlier search.
/// </summary>
public int StartCandidatePruningLevel { get; }
/// <summary>
/// Gets the pruning level applied across dynamic reference-vector choices after motion search.
/// </summary>
public int ReferenceCandidatePruningLevel { get; }
/// <summary>
/// Gets a value indicating whether warped-motion refinement uses a diamond instead of a square.
/// </summary>
public bool UseDiamondWarpSearch { get; }
/// <summary>
/// Gets the full-pixel method for the current block geometry.
/// </summary>
/// <param name="blockSize">The prediction block size.</param>
/// <returns>The frame-selected method after its block-size override.</returns>
public FullPixelSearchMethod GetFullPixelMethod(Av1BlockSize blockSize)
{
int minimumDimension = Math.Min(blockSize.GetWidth(), blockSize.GetHeight());
if (this.fasterSearchMinimumDimension == 0 || minimumDimension < this.fasterSearchMinimumDimension)
{
return this.fullPixelMethod;
}
return this.fullPixelMethod switch
{
FullPixelSearchMethod.NStep or FullPixelSearchMethod.EightPointNStep => FullPixelSearchMethod.Diamond,
FullPixelSearchMethod.Diamond or FullPixelSearchMethod.ClampedDiamond => FullPixelSearchMethod.BigDiamond,
FullPixelSearchMethod.BigDiamond => FullPixelSearchMethod.Hexagon,
FullPixelSearchMethod.Hexagon => FullPixelSearchMethod.FastDiamond,
FullPixelSearchMethod.FastDiamond => FullPixelSearchMethod.VeryFastDiamond,
_ => this.fullPixelMethod
};
}
/// <summary>
/// Gets the successive mesh ranges and sampling intervals in full luma samples.
/// </summary>
/// <param name="intraBlockCopy">Whether the search references the current reconstruction.</param>
/// <returns>Four range/interval pairs. Traversal ends after the first interval of one.</returns>
public ReadOnlySpan<int> GetMeshPattern(bool intraBlockCopy)
{
// The alternating range/interval layout is immutable static storage. A frame or candidate does not
// allocate a pattern, and an interval of one terminates refinement before unused trailing entries.
if (intraBlockCopy)
{
return this.speed switch
{
<= HeifEncodingSpeed.Level1 => [256, 1, 256, 1, 0, 0, 0, 0],
<= HeifEncodingSpeed.Level3 => [64, 1, 64, 1, 0, 0, 0, 0],
_ => [64, 4, 16, 1, 0, 0, 0, 0]
};
}
return this.speed switch
{
<= HeifEncodingSpeed.Level1 => [64, 8, 28, 4, 15, 1, 7, 1],
HeifEncodingSpeed.Level2 => [64, 8, 14, 2, 7, 1, 7, 1],
_ => [64, 16, 24, 8, 12, 4, 7, 1]
};
}
}

189
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchSites.cs

@ -0,0 +1,189 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Motion.Av1MotionSearchSettings;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
/// <summary>
/// Borrows one retained search-site configuration, including stride-relative sample offsets.
/// </summary>
internal readonly ref struct Av1MotionSearchSites
{
/// <summary>
/// The number of integer storage elements for sites, stage metadata, and the configured stride.
/// </summary>
public const int StorageLength = (22 * 17 * 2) + (22 * 2) + 2;
private const int StageCapacity = 22;
private const int SitesPerStage = 17;
private const int SiteStorageLength = StageCapacity * SitesPerStage * 2;
private const int StageCountOffset = SiteStorageLength + (StageCapacity * 2);
private readonly Span<int> storage;
/// <summary>
/// Initializes a new instance of the <see cref="Av1MotionSearchSites"/> struct.
/// </summary>
/// <param name="storage">The retained configuration storage, whose stride slot is initialized by its owner.</param>
public Av1MotionSearchSites(Span<int> storage) => this.storage = storage;
/// <summary>
/// Gets the number of populated search stages.
/// </summary>
public int StageCount => this.storage[StageCountOffset];
/// <summary>
/// Gets the number of non-center candidates at the given stage.
/// </summary>
/// <param name="stage">The stage, ordered from the smallest search radius.</param>
/// <returns>The candidate count.</returns>
public int GetCandidateCount(int stage) => this.storage[SiteStorageLength + stage];
/// <summary>
/// Gets the radius of the given stage in full samples.
/// </summary>
/// <param name="stage">The stage, ordered from the smallest search radius.</param>
/// <returns>The search radius.</returns>
public int GetRadius(int stage) => this.storage[SiteStorageLength + StageCapacity + stage];
/// <summary>
/// Gets the ordered candidate sites for a stage.
/// </summary>
/// <param name="stage">The stage, ordered from the smallest search radius.</param>
/// <returns>The fixed stage slot; only the configured candidate entries are populated.</returns>
public ReadOnlySpan<Site> GetSites(int stage)
=> MemoryMarshal.Cast<int, Site>(this.storage[..SiteStorageLength]).Slice(stage * SitesPerStage, SitesPerStage);
/// <summary>
/// Initializes or refreshes offsets when the retained reference plane's stride changes.
/// </summary>
/// <param name="method">The distinct search shape owned by this configuration.</param>
/// <param name="stride">The reference plane stride in samples.</param>
public void Configure(FullPixelSearchMethod method, int stride)
{
if (this.storage[StageCountOffset + 1] == stride)
{
return;
}
Span<Site> sites = MemoryMarshal.Cast<int, Site>(this.storage[..SiteStorageLength]);
Span<int> counts = this.storage.Slice(SiteStorageLength, StageCapacity);
Span<int> radii = this.storage.Slice(SiteStorageLength + StageCapacity, StageCapacity);
bool nStep = method is FullPixelSearchMethod.NStep or FullPixelSearchMethod.EightPointNStep;
bool diamond = method is FullPixelSearchMethod.Diamond or FullPixelSearchMethod.ClampedDiamond;
int stageCount = nStep ? (method == FullPixelSearchMethod.NStep ? 15 : 16) : 11;
int radius = 1;
for (int stage = 0; stage < stageCount; stage++)
{
Span<Site> stageSites = sites.Slice(stage * SitesPerStage, SitesPerStage);
if (diamond)
{
// The clamped shape repeats its three outer stages at radius 256. Retaining those stages
// matters because a move at one stage permits another move at the same radius.
radius = 1 << Math.Min(stage, method == FullPixelSearchMethod.ClampedDiamond ? 8 : 10);
}
else if (!nStep)
{
radius = 1 << stage;
}
radii[stage] = radius;
if (nStep || diamond)
{
bool twelveSites = nStep && radius > 5 && method != FullPixelSearchMethod.EightPointNStep;
int tangent = twelveSites ? Math.Max((int)(0.41 * radius), 1) : radius;
counts[stage] = twelveSites ? 12 : 8;
stageSites[0] = new Site(0, 0, stride);
stageSites[1] = new Site(-radius, 0, stride);
stageSites[2] = new Site(radius, 0, stride);
stageSites[3] = new Site(0, -radius, stride);
stageSites[4] = new Site(0, radius, stride);
stageSites[5] = new Site(-radius, -tangent, stride);
stageSites[6] = new Site(radius, tangent, stride);
stageSites[7] = new Site(-tangent, radius, stride);
stageSites[8] = new Site(tangent, -radius, stride);
if (twelveSites)
{
stageSites[9] = new Site(-radius, tangent, stride);
stageSites[10] = new Site(radius, -tangent, stride);
stageSites[11] = new Site(tangent, radius, stride);
stageSites[12] = new Site(-tangent, -radius, stride);
}
// N-step radii grow by rounded halves through stage twelve, then retain the outer radius.
if (nStep && stage < 12)
{
radius = Math.Max(((3 * radius) + 1) / 2, radius + 1);
}
}
else
{
// Pattern sites omit the center. Pairs are row then column, in traversal order.
// Beyond scale zero, multiply the half-radius by these integer coordinates.
ReadOnlySpan<sbyte> coordinates;
int scale;
if (method == FullPixelSearchMethod.Hexagon)
{
coordinates = stage == 0
? [-1, -1, 0, -1, 1, -1, 1, 0, 1, 1, 0, 1, -1, 1, -1, 0]
: [-1, -2, 1, -2, 2, 0, 1, 2, -1, 2, -2, 0];
scale = stage == 0 ? 1 : radius / 2;
}
else
{
coordinates = stage == 0
? [0, -1, 1, 0, 0, 1, -1, 0]
: [-1, -1, 0, -2, 1, -1, 2, 0, 1, 1, 0, 2, -1, 1, -2, 0];
scale = stage == 0 ? 1 : radius / 2;
}
counts[stage] = coordinates.Length / 2;
for (int index = 0; index < counts[stage]; index++)
{
stageSites[index] = new Site(coordinates[index * 2] * scale, coordinates[(index * 2) + 1] * scale, stride);
}
}
}
this.storage[StageCountOffset] = stageCount;
this.storage[StageCountOffset + 1] = stride;
}
/// <summary>
/// Stores one full-sample displacement and its reference-plane offset in eight bytes.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly struct Site
{
/// <summary>
/// Initializes a new instance of the <see cref="Site"/> struct.
/// </summary>
/// <param name="row">The vertical full-sample displacement.</param>
/// <param name="column">The horizontal full-sample displacement.</param>
/// <param name="stride">The reference row stride in samples.</param>
public Site(int row, int column, int stride)
{
this.Row = (short)row;
this.Column = (short)column;
this.Offset = (row * stride) + column;
}
/// <summary>
/// Gets the vertical full-sample displacement.
/// </summary>
public short Row { get; }
/// <summary>
/// Gets the horizontal full-sample displacement.
/// </summary>
public short Column { get; }
/// <summary>
/// Gets the signed displacement in reference-plane samples.
/// </summary>
public int Offset { get; }
}
}

283
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVariationCandidates.cs

@ -0,0 +1,283 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
/// <summary>
/// Derives the neighboring-block state and fixed-capacity projection samples used to select an AV1 motion mode.
/// </summary>
internal sealed class Av1MotionVariationCandidates
{
/// <summary>
/// The maximum number of neighboring motion samples retained for a local warped-motion projection.
/// </summary>
private const int ProjectionSampleCapacity = 8;
/// <summary>
/// The largest neighbor step used by overlapping motion compensation, measured in 4x4 mode-information units.
/// </summary>
private const int MaximumNeighborStep = 16;
/// <summary>
/// The number of fractional bits in an AV1 motion vector and warped-motion sample position.
/// </summary>
private const int MotionVectorSubpixelBits = 3;
/// <summary>
/// Stores sample positions relative to the current block origin in one-eighth-sample units.
/// </summary>
private InlineArray8<Point> sourcePoints;
/// <summary>
/// Stores the corresponding reference-frame positions in one-eighth-sample units.
/// </summary>
private InlineArray8<Point> referencePoints;
/// <summary>
/// Gets the number of valid entries in <see cref="SourcePoints"/> and <see cref="ReferencePoints"/>.
/// </summary>
public int Count { get; private set; }
/// <summary>
/// Gets a value indicating whether an inter-coded block overlaps the current block's above or left edge.
/// </summary>
public bool HasOverlappableNeighbor { get; private set; }
/// <summary>
/// Gets the retained current-frame sample positions in one-eighth-sample units relative to the current block.
/// </summary>
public ReadOnlySpan<Point> SourcePoints => this.sourcePoints[..this.Count];
/// <summary>
/// Gets the retained reference-frame sample positions in one-eighth-sample units relative to the current block.
/// </summary>
public ReadOnlySpan<Point> ReferencePoints => this.referencePoints[..this.Count];
/// <summary>
/// Derives the spatial state used to select Simple Translation, OBMC, or Warped motion for one inter block.
/// </summary>
/// <param name="partitionInfo">The current block geometry and frame-wide decoded mode map.</param>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <param name="sequenceHeader">The sequence-level superblock geometry.</param>
/// <param name="frameHeader">The current frame dimensions.</param>
/// <param name="referenceFrame">The current block's primary canonical reference.</param>
public void Build(
ref Av1PartitionInfo partitionInfo,
Av1TileInfo tileInfo,
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Av1ReferenceFrameType referenceFrame)
{
this.Count = 0;
this.CollectProjectionSamples(ref partitionInfo, tileInfo, sequenceHeader, frameHeader, referenceFrame);
this.HasOverlappableNeighbor = FindOverlappableNeighbor(ref partitionInfo, frameHeader);
}
/// <summary>
/// Collects the at most eight spatial samples permitted by AV1's local warped-motion model.
/// </summary>
/// <param name="partitionInfo">The current block geometry and frame-wide decoded mode map.</param>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <param name="sequenceHeader">The sequence-level superblock geometry.</param>
/// <param name="frameHeader">The current frame dimensions.</param>
/// <param name="referenceFrame">The current block's primary canonical reference.</param>
private void CollectProjectionSamples(
ref Av1PartitionInfo partitionInfo,
Av1TileInfo tileInfo,
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Av1ReferenceFrameType referenceFrame)
{
Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize;
int width = blockSize.Get4x4WideCount();
int height = blockSize.Get4x4HighCount();
int row = partitionInfo.RowIndex;
int column = partitionInfo.ColumnIndex;
bool includeTopLeft = true;
bool includeTopRight = true;
if (partitionInfo.AvailableAbove)
{
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column, row - 1));
int candidateWidth = candidate.BlockSize.Get4x4WideCount();
if (width <= candidateWidth)
{
// A wider above block can also cover the diagonal search positions. The signed alignment offset
// prevents those positions from contributing the same block a second time.
int columnOffset = -column % candidateWidth;
includeTopLeft = columnOffset >= 0;
includeTopRight = columnOffset + candidateWidth <= width;
this.AddProjectionSample(candidate, referenceFrame, 0, -1, columnOffset, 1);
}
else
{
int end = Math.Min(width, frameHeader.ModeInfoColumnCount - column);
for (int index = 0; index < end && this.Count < ProjectionSampleCapacity; index += candidateWidth)
{
candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column + index, row - 1));
candidateWidth = candidate.BlockSize.Get4x4WideCount();
this.AddProjectionSample(candidate, referenceFrame, 0, -1, index, 1);
}
}
}
if (partitionInfo.AvailableLeft && this.Count < ProjectionSampleCapacity)
{
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, row));
int candidateHeight = candidate.BlockSize.Get4x4HighCount();
if (height <= candidateHeight)
{
// The same alignment rule suppresses a duplicate top-left sample when one tall left block covers it.
int rowOffset = -row % candidateHeight;
includeTopLeft &= rowOffset >= 0;
this.AddProjectionSample(candidate, referenceFrame, rowOffset, 1, 0, -1);
}
else
{
int end = Math.Min(height, frameHeader.ModeInfoRowCount - row);
for (int index = 0; index < end && this.Count < ProjectionSampleCapacity; index += candidateHeight)
{
candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, row + index));
candidateHeight = candidate.BlockSize.Get4x4HighCount();
this.AddProjectionSample(candidate, referenceFrame, index, 1, 0, -1);
}
}
}
if (includeTopLeft && partitionInfo.AvailableAbove && partitionInfo.AvailableLeft && this.Count < ProjectionSampleCapacity)
{
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, row - 1));
this.AddProjectionSample(candidate, referenceFrame, 0, -1, 0, -1);
}
int topRightRow = row - 1;
int topRightColumn = column + width;
bool topRightInsideTile =
topRightRow >= tileInfo.ModeInfoRowStart &&
topRightRow < tileInfo.ModeInfoRowEnd &&
topRightColumn >= tileInfo.ModeInfoColumnStart &&
topRightColumn < tileInfo.ModeInfoColumnEnd;
if (includeTopRight &&
this.Count < ProjectionSampleCapacity &&
partitionInfo.HasTopRight(sequenceHeader.SuperblockModeInfoSize) &&
topRightInsideTile)
{
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(topRightColumn, topRightRow));
this.AddProjectionSample(candidate, referenceFrame, 0, -1, width, 1);
}
}
/// <summary>
/// Determines whether an inter-coded neighbor covers either complete prediction edge of the current block.
/// </summary>
/// <param name="partitionInfo">The current block geometry and frame-wide decoded mode map.</param>
/// <param name="frameHeader">The current frame dimensions.</param>
/// <returns><see langword="true"/> when an above or left inter block can contribute overlapping prediction.</returns>
private static bool FindOverlappableNeighbor(ref Av1PartitionInfo partitionInfo, ObuFrameHeader frameHeader)
{
Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize;
int width = blockSize.Get4x4WideCount();
int height = blockSize.Get4x4HighCount();
int row = partitionInfo.RowIndex;
int column = partitionInfo.ColumnIndex;
if (partitionInfo.AvailableAbove)
{
int endColumn = Math.Min(column + width, frameHeader.ModeInfoColumnCount);
for (int aboveColumn = column; aboveColumn < endColumn;)
{
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(aboveColumn, row - 1));
int step = Math.Min(candidate.BlockSize.Get4x4WideCount(), MaximumNeighborStep);
if (step == 1)
{
// AV1 treats a 4-sample-wide neighbor as one half of an 8-sample pair and reads the mode record
// attached to the pair's second cell before advancing across both cells.
aboveColumn &= ~1;
candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(aboveColumn + 1, row - 1));
step = 2;
}
if (IsOverlappable(candidate))
{
return true;
}
aboveColumn += step;
}
}
if (partitionInfo.AvailableLeft)
{
int endRow = Math.Min(row + height, frameHeader.ModeInfoRowCount);
for (int leftRow = row; leftRow < endRow;)
{
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, leftRow));
int step = Math.Min(candidate.BlockSize.Get4x4HighCount(), MaximumNeighborStep);
if (step == 1)
{
// The vertical scan applies the corresponding 4-sample-high pairing rule.
leftRow &= ~1;
candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, leftRow + 1));
step = 2;
}
if (IsOverlappable(candidate))
{
return true;
}
leftRow += step;
}
}
return false;
}
/// <summary>
/// Appends one neighboring single-reference sample when it uses the current block's primary reference.
/// </summary>
/// <param name="candidate">The neighboring decoded block.</param>
/// <param name="referenceFrame">The current block's primary canonical reference.</param>
/// <param name="rowOffset">The neighbor center row offset in 4x4 mode-information units.</param>
/// <param name="rowSign">The direction from the current block toward the neighbor on the vertical axis.</param>
/// <param name="columnOffset">The neighbor center column offset in 4x4 mode-information units.</param>
/// <param name="columnSign">The direction from the current block toward the neighbor on the horizontal axis.</param>
private void AddProjectionSample(
Av1BlockModeInfo candidate,
Av1ReferenceFrameType referenceFrame,
int rowOffset,
int rowSign,
int columnOffset,
int columnSign)
{
Span<Av1ReferenceFrameType> candidateReferences = candidate.ReferenceFrames;
if (candidateReferences[0] != referenceFrame || candidateReferences[1] != Av1ReferenceFrameType.None)
{
return;
}
const int modeInfoSampleSize = 1 << Av1Constants.ModeInfoSizeLog2;
int sourceX = (columnOffset * modeInfoSampleSize) + (columnSign * (candidate.BlockSize.GetWidth() >> 1)) - 1;
int sourceY = (rowOffset * modeInfoSampleSize) + (rowSign * (candidate.BlockSize.GetHeight() >> 1)) - 1;
Point sourcePoint = new(sourceX << MotionVectorSubpixelBits, sourceY << MotionVectorSubpixelBits);
Av1MotionVector motionVector = candidate.MotionVectors[0];
// Neighbor centers and motion vectors share Q3 precision. Adding them directly produces the corresponding
// reference position without rounding away the fractional displacement needed by the projection solver.
this.sourcePoints[this.Count] = sourcePoint;
this.referencePoints[this.Count] = new Point(sourcePoint.X + motionVector.Column, sourcePoint.Y + motionVector.Row);
this.Count++;
}
/// <summary>
/// Determines whether a decoded neighbor can participate in overlapping motion compensation.
/// </summary>
/// <param name="candidate">The neighboring decoded block.</param>
/// <returns><see langword="true"/> for inter prediction or intra-block copy; otherwise, <see langword="false"/>.</returns>
private static bool IsOverlappable(Av1BlockModeInfo candidate)
=> candidate.UseIntraBlockCopy || candidate.ReferenceFrames[0] > Av1ReferenceFrameType.Intra;
}

284
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs

@ -0,0 +1,284 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
/// <summary>
/// Represents an AV1 motion or displacement vector in one-eighth-sample units.
/// </summary>
internal readonly struct Av1MotionVector : IEquatable<Av1MotionVector>
{
/// <summary>
/// The greatest absolute temporal distance used by AV1 motion-vector projection.
/// </summary>
public const int MaximumTemporalDistance = 31;
/// <summary>
/// The number of fractional bits used by AV1 motion-vector components.
/// </summary>
public const int SubpixelBits = 3;
/// <summary>
/// The number of motion-vector units in one full pixel.
/// </summary>
public const int SubpixelScale = 1 << SubpixelBits;
/// <summary>
/// The reserved lower endpoint of the signed AV1 motion-vector domain.
/// </summary>
private const int LowerBound = -16384;
/// <summary>
/// The exclusive upper endpoint of the signed AV1 motion-vector domain.
/// </summary>
private const int UpperBound = 16384;
/// <summary>
/// The additional sixteen-sample border admitted while deriving spatial reference candidates, in one-eighth-sample units.
/// </summary>
private const int ReferenceBorder = 16 << 3;
/// <summary>
/// Initializes a new instance of the <see cref="Av1MotionVector"/> struct.
/// </summary>
/// <param name="row">The signed vertical displacement in one-eighth-sample units.</param>
/// <param name="column">The signed horizontal displacement in one-eighth-sample units.</param>
public Av1MotionVector(int row, int column)
{
this.Row = row;
this.Column = column;
}
/// <summary>
/// Gets the signed vertical displacement in one-eighth-sample units.
/// </summary>
public int Row { get; }
/// <summary>
/// Gets the signed horizontal displacement in one-eighth-sample units.
/// </summary>
public int Column { get; }
/// <summary>
/// Gets a value indicating whether both displacement components are zero.
/// </summary>
public bool IsZero => this.Row == 0 && this.Column == 0;
/// <summary>
/// Gets a value indicating whether both components lie strictly between the two reserved AV1 endpoints.
/// </summary>
public bool IsValid =>
this.Row > LowerBound &&
this.Row < UpperBound &&
this.Column > LowerBound &&
this.Column < UpperBound;
/// <summary>
/// Gets the reciprocal table used by AV1 temporal projection in fourteen-bit fixed-point precision.
/// </summary>
private static ReadOnlySpan<int> ProjectionDivisors =>
[0, 16384, 8192, 5461, 4096, 3276, 2730, 2340, 2048, 1820, 1638, 1489, 1365, 1260, 1170, 1092,
1024, 963, 910, 862, 819, 780, 744, 712, 682, 655, 630, 606, 585, 564, 546, 528];
/// <summary>
/// Adds a component delta to this vector.
/// </summary>
/// <param name="value">The reference vector.</param>
/// <param name="delta">The decoded component delta.</param>
/// <returns>The component-wise sum.</returns>
public static Av1MotionVector operator +(Av1MotionVector value, Av1MotionVector delta)
=> new(value.Row + delta.Row, value.Column + delta.Column);
/// <summary>
/// Determines whether two vectors have equal components.
/// </summary>
/// <param name="left">The first vector.</param>
/// <param name="right">The second vector.</param>
/// <returns><see langword="true"/> when both components are equal; otherwise, <see langword="false"/>.</returns>
public static bool operator ==(Av1MotionVector left, Av1MotionVector right) => left.Equals(right);
/// <summary>
/// Determines whether two vectors have different components.
/// </summary>
/// <param name="left">The first vector.</param>
/// <param name="right">The second vector.</param>
/// <returns><see langword="true"/> when either component differs; otherwise, <see langword="false"/>.</returns>
public static bool operator !=(Av1MotionVector left, Av1MotionVector right) => !left.Equals(right);
/// <summary>
/// Computes the full-pixel displacement region that can produce distinct, padded block predictions.
/// </summary>
/// <param name="block">The block's luma rectangle in the coded frame.</param>
/// <param name="frameSize">The frame dimensions rounded to mode-information units.</param>
/// <param name="border">The allocated luma border on every side.</param>
/// <returns>The displacement region with exclusive right and bottom edges.</returns>
public static Rectangle GetFrameSearchBounds(Rectangle block, Size frameSize, int border)
{
// Reserve eight samples for interpolation support and restrict candidates beyond a replicated edge
// once moving farther cannot change the prediction. Bounds describe displacement from this block,
// so interior blocks can move across the frame rather than being restricted to the border width.
int minimumColumn = Math.Max(-(block.X + border - 8), -(block.Right + 8));
int minimumRow = Math.Max(-(block.Y + border - 8), -(block.Bottom + 8));
int maximumColumn = Math.Min(frameSize.Width - block.Right + border - 8, frameSize.Width - block.X + 8);
int maximumRow = Math.Min(frameSize.Height - block.Bottom + border - 8, frameSize.Height - block.Y + 8);
return Rectangle.FromLTRB(minimumColumn, minimumRow, maximumColumn + 1, maximumRow + 1);
}
/// <summary>
/// Restricts a frame displacement region to representable full-pixel candidates around this reference.
/// </summary>
/// <param name="frameBounds">The full-pixel region from <see cref="GetFrameSearchBounds"/>.</param>
/// <returns>The full-pixel search region with exclusive right and bottom edges.</returns>
public Rectangle GetFullPixelSearchBounds(Rectangle frameBounds)
{
const int MaximumDisplacement = 1023;
// Both endpoints must fit inside a 1023-pixel displacement from the fractional reference. Round the
// lower endpoint toward positive infinity and the upper toward negative infinity, including for
// negative references. Keep the reserved vector-domain endpoints out of the search as well.
int minimumColumn = Math.Max(frameBounds.Left, Math.Max(((this.Column + 7) >> 3) - MaximumDisplacement, (LowerBound >> 3) + 1));
int minimumRow = Math.Max(frameBounds.Top, Math.Max(((this.Row + 7) >> 3) - MaximumDisplacement, (LowerBound >> 3) + 1));
int maximumColumn = Math.Min(frameBounds.Right - 1, Math.Min((this.Column >> 3) + MaximumDisplacement, (UpperBound >> 3) - 1));
int maximumRow = Math.Min(frameBounds.Bottom - 1, Math.Min((this.Row >> 3) + MaximumDisplacement, (UpperBound >> 3) - 1));
maximumColumn = Math.Max(minimumColumn, maximumColumn);
maximumRow = Math.Max(minimumRow, maximumRow);
return Rectangle.FromLTRB(minimumColumn, minimumRow, maximumColumn + 1, maximumRow + 1);
}
/// <summary>
/// Restricts a frame displacement region to representable fractional candidates around this reference.
/// </summary>
/// <param name="frameBounds">The full-pixel region from <see cref="GetFrameSearchBounds"/>.</param>
/// <returns>The eighth-sample search region with exclusive right and bottom edges.</returns>
public Rectangle GetSubpixelSearchBounds(Rectangle frameBounds)
{
const int MaximumDisplacement = 1023 * SubpixelScale;
// Refine against the original frame region, not the rounded full-pixel intersection. Otherwise the
// fractional portion between an integer endpoint and the reference-centered limit would be lost.
int minimumColumn = Math.Max(frameBounds.Left * SubpixelScale, this.Column - MaximumDisplacement);
int minimumRow = Math.Max(frameBounds.Top * SubpixelScale, this.Row - MaximumDisplacement);
int maximumColumn = Math.Min((frameBounds.Right - 1) * SubpixelScale, this.Column + MaximumDisplacement);
int maximumRow = Math.Min((frameBounds.Bottom - 1) * SubpixelScale, this.Row + MaximumDisplacement);
maximumColumn = Math.Min(UpperBound - 1, Math.Max(minimumColumn, maximumColumn));
maximumRow = Math.Min(UpperBound - 1, Math.Max(minimumRow, maximumRow));
minimumColumn = Math.Max(LowerBound + 1, minimumColumn);
minimumRow = Math.Max(LowerBound + 1, minimumRow);
return Rectangle.FromLTRB(minimumColumn, minimumRow, maximumColumn + 1, maximumRow + 1);
}
/// <summary>
/// Reduces this vector to the motion-vector precision selected by the current frame.
/// </summary>
/// <param name="allowHighPrecision">
/// A value indicating whether one-eighth-sample precision may be retained.
/// </param>
/// <param name="forceInteger">
/// A value indicating whether both components must be rounded to integer-sample precision.
/// </param>
/// <returns>The precision-reduced vector.</returns>
public Av1MotionVector LowerPrecision(bool allowHighPrecision, bool forceInteger)
{
if (forceInteger)
{
return new(RoundToIntegerPrecision(this.Row), RoundToIntegerPrecision(this.Column));
}
if (allowHighPrecision)
{
return this;
}
// Low precision removes the one-eighth-sample bit. Odd components move toward zero rather than rounding to
// the nearest even value, which is the normative lower_mv_precision behavior used by spatial and temporal MVs.
int row = (this.Row & 1) != 0 ? this.Row + (this.Row > 0 ? -1 : 1) : this.Row;
int column = (this.Column & 1) != 0 ? this.Column + (this.Column > 0 ? -1 : 1) : this.Column;
return new(row, column);
}
/// <summary>
/// Clamps this vector to the spatial reference-candidate limits for a coding block.
/// </summary>
/// <param name="blockWidth">The coding-block width in luma samples.</param>
/// <param name="blockHeight">The coding-block height in luma samples.</param>
/// <param name="blockToLeftEdge">The signed distance to the left frame edge in one-eighth-sample units.</param>
/// <param name="blockToRightEdge">The signed distance to the right frame edge in one-eighth-sample units.</param>
/// <param name="blockToTopEdge">The signed distance to the top frame edge in one-eighth-sample units.</param>
/// <param name="blockToBottomEdge">The signed distance to the bottom frame edge in one-eighth-sample units.</param>
/// <returns>The vector clamped to the permitted spatial reference-candidate range.</returns>
public Av1MotionVector ClampReference(
int blockWidth,
int blockHeight,
int blockToLeftEdge,
int blockToRightEdge,
int blockToTopEdge,
int blockToBottomEdge)
{
int blockWidthSubpixel = blockWidth << 3;
int blockHeightSubpixel = blockHeight << 3;
// Candidate derivation permits the complete block extent plus sixteen further luma samples beyond each
// visible frame edge. These are stack limits, not the tighter UMV limits applied later while sampling pixels.
int minimumColumn = blockToLeftEdge - blockWidthSubpixel - ReferenceBorder;
int maximumColumn = blockToRightEdge + blockWidthSubpixel + ReferenceBorder;
int minimumRow = blockToTopEdge - blockHeightSubpixel - ReferenceBorder;
int maximumRow = blockToBottomEdge + blockHeightSubpixel + ReferenceBorder;
return new(
Av1Math.Clip3(minimumRow, maximumRow, this.Row),
Av1Math.Clip3(minimumColumn, maximumColumn, this.Column));
}
/// <summary>
/// Projects this vector across a ratio of temporal frame distances.
/// </summary>
/// <param name="numerator">The signed source-to-target frame distance.</param>
/// <param name="denominator">The positive source-to-reference frame distance.</param>
/// <returns>The projected vector clamped inside the AV1 motion-vector domain.</returns>
public Av1MotionVector ProjectTemporal(int numerator, int denominator)
{
denominator = Math.Min(denominator, MaximumTemporalDistance);
numerator = Av1Math.Clip3(-MaximumTemporalDistance, MaximumTemporalDistance, numerator);
// The reciprocal table represents 1 / denominator in Q14. Signed power-of-two rounding preserves symmetry
// for negative components, and AV1 excludes the two reserved endpoints from projected motion vectors.
// Motion-field retention limits each source component to 4095, keeping the complete Q14 product inside Int32.
int row = Av1Math.RoundPowerOf2Signed(this.Row * numerator * ProjectionDivisors[denominator], 14);
int column = Av1Math.RoundPowerOf2Signed(this.Column * numerator * ProjectionDivisors[denominator], 14);
row = Av1Math.Clip3(LowerBound + 1, UpperBound - 1, row);
column = Av1Math.Clip3(LowerBound + 1, UpperBound - 1, column);
return new(row, column);
}
/// <summary>
/// Determines whether this vector has the same components as another vector.
/// </summary>
/// <param name="other">The vector to compare.</param>
/// <returns><see langword="true"/> when both components are equal; otherwise, <see langword="false"/>.</returns>
public bool Equals(Av1MotionVector other) => this.Row == other.Row && this.Column == other.Column;
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is Av1MotionVector other && this.Equals(other);
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.Row, this.Column);
/// <summary>
/// Rounds one component to the nearest integer-sample displacement.
/// </summary>
/// <param name="value">The component in one-eighth-sample units.</param>
/// <returns>The integer-precision component in one-eighth-sample units.</returns>
private static int RoundToIntegerPrecision(int value)
{
int remainder = value % 8;
value -= remainder;
// Exactly half an integer sample has magnitude four. AV1 leaves that truncated base unchanged, so both
// positive and negative half ties move toward zero; only larger remainders advance to the adjacent sample.
if (Math.Abs(remainder) > 4)
{
value += remainder > 0 ? 8 : -8;
}
return value;
}
}

25
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVectorPrecision.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
/// <summary>
/// Identifies the fractional precision used to decode an AV1 motion-vector delta.
/// </summary>
internal enum Av1MotionVectorPrecision : sbyte
{
/// <summary>
/// Restricts components to whole-sample increments.
/// </summary>
Integer = -1,
/// <summary>
/// Allows components in quarter-sample increments.
/// </summary>
QuarterSample,
/// <summary>
/// Allows components in eighth-sample increments.
/// </summary>
EighthSample
}

1598
src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs

File diff suppressed because it is too large

30
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuChromoSamplePosition.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Identifies the position of chroma samples relative to luma samples.
/// </summary>
internal enum ObuChromoSamplePosition : byte
{
/// <summary>
/// Unknown.
/// </summary>
Unknown = 0,
/// <summary>
/// The chroma sample is horizontally co-located with the top-left luma sample and lies between two luma rows.
/// </summary>
Vertical = 1,
/// <summary>
/// The chroma sample is co-located with the top-left luma sample.
/// </summary>
Colocated = 2,
/// <summary>
/// Reserved and invalid for AV1 content.
/// </summary>
Reserved = 3,
}

101
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorConfig.cs

@ -0,0 +1,101 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the color configuration signaled by an AV1 sequence header.
/// </summary>
internal sealed class ObuColorConfig
{
/// <summary>
/// Gets or sets a value indicating whether color-description syntax is present.
/// </summary>
public bool IsColorDescriptionPresent { get; set; }
/// <summary>
/// Gets the number of color channels in this image. Can have the value 1 or 3.
/// </summary>
public int PlaneCount => this.IsMonochrome ? 1 : Av1Constants.MaxPlanes;
/// <summary>
/// Gets or sets a value indicating whether the image has a single greyscale plane, will have
/// <see cref="Av1Constants.MaxPlanes"/> color planes otherwise.
/// </summary>
public bool IsMonochrome { get; set; }
/// <summary>
/// Gets or sets the color-primary chromaticities.
/// </summary>
public ObuColorPrimaries ColorPrimaries { get; set; }
/// <summary>
/// Gets or sets the transfer characteristics.
/// </summary>
public ObuTransferCharacteristics TransferCharacteristics { get; set; }
/// <summary>
/// Gets or sets the matrix coefficients used to derive luma and chroma components.
/// </summary>
public ObuMatrixCoefficients MatrixCoefficients { get; set; }
/// <summary>
/// Gets or sets a value indicating whether samples use the full numeric range.
/// </summary>
public bool ColorRange { get; set; }
/// <summary>
/// Gets or sets a value indicating whether chroma is subsampled horizontally.
/// </summary>
public bool SubSamplingX { get; set; }
/// <summary>
/// Gets or sets a value indicating whether chroma is subsampled vertically.
/// </summary>
public bool SubSamplingY { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the U and V planes use separate quantizer deltas.
/// </summary>
public bool HasSeparateUvDelta { get; set; }
/// <summary>
/// Gets or sets the chroma sample position for vertically subsampled images.
/// </summary>
public ObuChromoSamplePosition ChromaSamplePosition { get; set; }
/// <summary>
/// Gets or sets the encoded sample bit depth.
/// </summary>
public Av1BitDepth BitDepth { get; set; }
/// <summary>
/// Gets the color format represented by the monochrome and chroma-subsampling flags.
/// </summary>
/// <returns>The corresponding AV1 color format.</returns>
public Av1ColorFormat GetColorFormat()
{
if (this.IsMonochrome)
{
// AV1 sets both subsampling flags for monochrome sequences even though no chroma planes exist. The
// mono_chrome syntax therefore owns the plane layout and must take precedence over those derived flags.
return Av1ColorFormat.Yuv400;
}
Av1ColorFormat format = Av1ColorFormat.Yuv400;
if (this.SubSamplingX && this.SubSamplingY)
{
format = Av1ColorFormat.Yuv420;
}
else if (this.SubSamplingX & !this.SubSamplingY)
{
format = Av1ColorFormat.Yuv422;
}
else if (!this.SubSamplingX && !this.SubSamplingY)
{
format = Av1ColorFormat.Yuv444;
}
return format;
}
}

75
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorPrimaries.cs

@ -0,0 +1,75 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Identifies the CICP color-primary chromaticities signaled by an AV1 sequence.
/// </summary>
internal enum ObuColorPrimaries
{
/// <summary>
/// The reserved zero value.
/// </summary>
Reserved = 0,
/// <summary>
/// ITU-R BT.709 primaries.
/// </summary>
Bt709 = 1,
/// <summary>
/// Unspecified primaries.
/// </summary>
Unspecified = 2,
/// <summary>
/// ITU-R BT.470 System M primaries.
/// </summary>
Bt470M = 4,
/// <summary>
/// ITU-R BT.470 System B and G primaries.
/// </summary>
Bt470BG = 5,
/// <summary>
/// ITU-R BT.601 primaries.
/// </summary>
Bt601 = 6,
/// <summary>
/// SMPTE 240M primaries.
/// </summary>
Smpte240 = 7,
/// <summary>
/// Generic film primaries.
/// </summary>
GenericFilm = 8,
/// <summary>
/// ITU-R BT.2020 and BT.2100 primaries.
/// </summary>
Bt2020 = 9,
/// <summary>
/// SMPTE ST 428 CIE XYZ primaries.
/// </summary>
Xyz = 10,
/// <summary>
/// SMPTE RP 431-2 primaries.
/// </summary>
Smpte431 = 11,
/// <summary>
/// SMPTE EG 432-1 primaries.
/// </summary>
Smpte432 = 12,
/// <summary>
/// EBU Tech. 3213-E primaries.
/// </summary>
Ebu3213 = 22,
}

40
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuConstraintDirectionalEnhancementFilterParameters.cs

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the constrained directional enhancement filter parameters for an AV1 frame.
/// </summary>
internal sealed class ObuConstraintDirectionalEnhancementFilterParameters
{
/// <summary>
/// Stores the fixed sixteen luma filter strengths without a per-frame array allocation.
/// </summary>
private InlineArray16<int> yStrength;
/// <summary>
/// Stores the fixed sixteen chroma filter strengths without a per-frame array allocation.
/// </summary>
private InlineArray16<int> uvStrength;
/// <summary>
/// Gets or sets the number of bits used to select a filter-strength entry.
/// </summary>
public int BitCount { get; set; }
/// <summary>
/// Gets or sets the filter damping value.
/// </summary>
public int Damping { get; set; } = 3;
/// <summary>
/// Gets the primary and secondary luma strengths for each filter entry.
/// </summary>
public Span<int> YStrength => this.yStrength;
/// <summary>
/// Gets the primary and secondary chroma strengths for each filter entry.
/// </summary>
public Span<int> UvStrength => this.uvStrength;
}

32
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDecoderModelInfo.cs

@ -0,0 +1,32 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Contains the field widths and decoding-clock units used by the AV1 decoder model.
/// </summary>
internal sealed class ObuDecoderModelInfo
{
/// <summary>
/// Gets or sets BufferDelayLength. Specifies the length of the decoder_buffer_delay and the encoder_buffer_delay
/// syntax elements, in bits.
/// </summary>
public uint BufferDelayLength { get; set; }
/// <summary>
/// Gets or sets NumUnitsInDecodingTick. This is the number of time units of a decoding clock operating at the frequency time_scale Hz
/// that corresponds to one increment of a clock tick counter.
/// </summary>
public uint NumUnitsInDecodingTick { get; set; }
/// <summary>
/// Gets or sets BufferRemovalTimeLength. Specifies the length of the buffer_removal_time syntax element, in bits.
/// </summary>
public uint BufferRemovalTimeLength { get; set; }
/// <summary>
/// Gets or sets the FramePresentationTimeLength. Specifies the length of the frame_presentation_time syntax element, in bits.
/// </summary>
public uint FramePresentationTimeLength { get; set; }
}

25
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDeltaParameters.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the delta-quantizer or delta-loop-filter signaling parameters for an AV1 frame.
/// </summary>
internal sealed class ObuDeltaParameters
{
/// <summary>
/// Gets or sets a value indicating whether per-block delta values are present.
/// </summary>
public bool IsPresent { get; set; }
/// <summary>
/// Gets or sets the delta-value multiplier, which is one, two, four, or eight.
/// </summary>
public int Resolution { get; set; }
/// <summary>
/// Gets or sets a value indicating whether separate loop-filter deltas are signaled for multiple filter targets.
/// </summary>
public bool IsMulti { get; set; }
}

251
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs

@ -0,0 +1,251 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the parameters used to synthesize AV1 film grain for a decoded frame.
/// </summary>
internal sealed class ObuFilmGrainParameters
{
/// <summary>
/// Stores the luma scaling-point coordinates without a per-frame array allocation.
/// </summary>
private InlineArray14<byte> pointYValue;
/// <summary>
/// Stores the luma scaling-point values without a per-frame array allocation.
/// </summary>
private InlineArray14<byte> pointYScaling;
/// <summary>
/// Stores the blue-difference scaling-point coordinates without a per-frame array allocation.
/// </summary>
private InlineArray10<byte> pointCbValue;
/// <summary>
/// Stores the blue-difference scaling-point values without a per-frame array allocation.
/// </summary>
private InlineArray10<byte> pointCbScaling;
/// <summary>
/// Stores the red-difference scaling-point coordinates without a per-frame array allocation.
/// </summary>
private InlineArray10<byte> pointCrValue;
/// <summary>
/// Stores the red-difference scaling-point values without a per-frame array allocation.
/// </summary>
private InlineArray10<byte> pointCrScaling;
/// <summary>
/// Stores the luma autoregressive coefficients without a per-frame array allocation.
/// </summary>
private InlineArray24<byte> arCoeffsYPlus128;
/// <summary>
/// Stores the blue-difference autoregressive coefficients without a per-frame array allocation.
/// </summary>
private InlineArray25<byte> arCoeffsCbPlus128;
/// <summary>
/// Stores the red-difference autoregressive coefficients without a per-frame array allocation.
/// </summary>
private InlineArray25<byte> arCoeffsCrPlus128;
/// <summary>
/// Gets or sets a value indicating whether film grain is applied to the displayed frame.
/// </summary>
public bool ApplyGrain { get; set; }
/// <summary>
/// Gets or sets the 16-bit seed that initializes pseudo-random film-grain synthesis for this frame.
/// </summary>
public uint GrainSeed { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this frame signals a complete parameter set instead of inheriting one.
/// </summary>
public bool UpdateGrain { get; set; }
/// <summary>
/// Gets or sets the physical reference-map index from which this frame inherited its film-grain parameters.
/// The index must match one of the frame's seven selected inter-reference slots.
/// </summary>
public uint FilmGrainParamsRefIdx { get; set; }
/// <summary>
/// Gets or sets the number of active luma scaling points in the inclusive range zero through fourteen.
/// </summary>
public uint NumYPoints { get; set; }
/// <summary>
/// Gets the fourteen-entry storage for the luma scaling-point coordinates.
/// </summary>
/// <remarks>
/// Only the first <see cref="NumYPoints"/> entries are active. Coordinates use the eight-bit scale and must be
/// strictly increasing; 10-bit and 12-bit sample values are divided by four and sixteen respectively.
/// </remarks>
public Span<byte> PointYValue => this.pointYValue;
/// <summary>
/// Gets the fourteen-entry storage for the luma scaling-point output values.
/// </summary>
/// <remarks>Only the first <see cref="NumYPoints"/> entries are active.</remarks>
public Span<byte> PointYScaling => this.pointYScaling;
/// <summary>
/// Gets or sets a value indicating whether both chroma scaling functions are derived from luma samples.
/// </summary>
public bool ChromaScalingFromLuma { get; set; }
/// <summary>
/// Gets or sets the number of active blue-difference scaling points in the inclusive range zero through ten.
/// </summary>
public uint NumCbPoints { get; set; }
/// <summary>
/// Gets or sets the number of active red-difference scaling points in the inclusive range zero through ten.
/// </summary>
public uint NumCrPoints { get; set; }
/// <summary>
/// Gets the ten-entry storage for the blue-difference scaling-point coordinates.
/// </summary>
/// <remarks>Only the first <see cref="NumCbPoints"/> entries are active, and active coordinates must be strictly increasing.</remarks>
public Span<byte> PointCbValue => this.pointCbValue;
/// <summary>
/// Gets the ten-entry storage for the blue-difference scaling-point output values.
/// </summary>
/// <remarks>Only the first <see cref="NumCbPoints"/> entries are active.</remarks>
public Span<byte> PointCbScaling => this.pointCbScaling;
/// <summary>
/// Gets the ten-entry storage for the red-difference scaling-point coordinates.
/// </summary>
/// <remarks>Only the first <see cref="NumCrPoints"/> entries are active, and active coordinates must be strictly increasing.</remarks>
public Span<byte> PointCrValue => this.pointCrValue;
/// <summary>
/// Gets the ten-entry storage for the red-difference scaling-point output values.
/// </summary>
/// <remarks>Only the first <see cref="NumCrPoints"/> entries are active.</remarks>
public Span<byte> PointCrScaling => this.pointCrScaling;
/// <summary>
/// Gets or sets the scaling-function shift minus eight. Values from zero through three select an effective shift
/// from eight through eleven for every luma and chroma scaling value.
/// </summary>
public uint GrainScalingMinus8 { get; set; }
/// <summary>
/// Gets or sets the autoregressive neighborhood lag in the inclusive range zero through three.
/// </summary>
public uint ArCoeffLag { get; set; }
/// <summary>
/// Gets the twenty-four-entry storage for biased luma autoregressive coefficients.
/// </summary>
/// <remarks>The active entry count is <c>2 * ArCoeffLag * (ArCoeffLag + 1)</c>.</remarks>
public Span<byte> ArCoeffsYPlus128 => this.arCoeffsYPlus128;
/// <summary>
/// Gets the twenty-five-entry storage for biased blue-difference autoregressive coefficients.
/// </summary>
/// <remarks>The active entry count includes one additional luma coefficient when luma scaling points are present.</remarks>
public Span<byte> ArCoeffsCbPlus128 => this.arCoeffsCbPlus128;
/// <summary>
/// Gets the twenty-five-entry storage for biased red-difference autoregressive coefficients.
/// </summary>
/// <remarks>The active entry count includes one additional luma coefficient when luma scaling points are present.</remarks>
public Span<byte> ArCoeffsCrPlus128 => this.arCoeffsCrPlus128;
/// <summary>
/// Gets or sets the autoregressive coefficient shift minus six in the inclusive range zero through three.
/// </summary>
public uint ArCoeffShiftMinus6 { get; set; }
/// <summary>
/// Gets or sets the right shift applied to generated Gaussian grain samples in the inclusive range zero through three.
/// </summary>
public uint GrainScaleShift { get; set; }
/// <summary>
/// Gets or sets the 8-bit blue-difference sample multiplier used to derive the chroma scaling index.
/// </summary>
public uint CbMult { get; set; }
/// <summary>
/// Gets or sets the 8-bit average-luma multiplier used to derive the blue-difference scaling index.
/// </summary>
public uint CbLumaMult { get; set; }
/// <summary>
/// Gets or sets the 9-bit offset used to derive the blue-difference scaling index.
/// </summary>
public uint CbOffset { get; set; }
/// <summary>
/// Gets or sets the 8-bit red-difference sample multiplier used to derive the chroma scaling index.
/// </summary>
public uint CrMult { get; set; }
/// <summary>
/// Gets or sets the 8-bit average-luma multiplier used to derive the red-difference scaling index.
/// </summary>
public uint CrLumaMult { get; set; }
/// <summary>
/// Gets or sets the 9-bit offset used to derive the red-difference scaling index.
/// </summary>
public uint CrOffset { get; set; }
/// <summary>
/// Gets or sets a value indicating whether neighboring film-grain blocks are blended across their boundaries.
/// </summary>
public bool OverlapFlag { get; set; }
/// <summary>
/// Gets or sets a value indicating whether grained samples are clipped to the restricted range instead of the full range.
/// </summary>
public bool ClipToRestrictedRange { get; set; }
/// <summary>
/// Replaces the complete film-grain parameter set with values retained by a reference frame.
/// </summary>
/// <param name="source">The retained reference-frame parameters.</param>
public void CopyFrom(ObuFilmGrainParameters source)
{
this.ApplyGrain = source.ApplyGrain;
this.GrainSeed = source.GrainSeed;
this.UpdateGrain = source.UpdateGrain;
this.FilmGrainParamsRefIdx = source.FilmGrainParamsRefIdx;
this.NumYPoints = source.NumYPoints;
this.pointYValue = source.pointYValue;
this.pointYScaling = source.pointYScaling;
this.ChromaScalingFromLuma = source.ChromaScalingFromLuma;
this.NumCbPoints = source.NumCbPoints;
this.NumCrPoints = source.NumCrPoints;
this.pointCbValue = source.pointCbValue;
this.pointCbScaling = source.pointCbScaling;
this.pointCrValue = source.pointCrValue;
this.pointCrScaling = source.pointCrScaling;
this.GrainScalingMinus8 = source.GrainScalingMinus8;
this.ArCoeffLag = source.ArCoeffLag;
this.arCoeffsYPlus128 = source.arCoeffsYPlus128;
this.arCoeffsCbPlus128 = source.arCoeffsCbPlus128;
this.arCoeffsCrPlus128 = source.arCoeffsCrPlus128;
this.ArCoeffShiftMinus6 = source.ArCoeffShiftMinus6;
this.GrainScaleShift = source.GrainScaleShift;
this.CbMult = source.CbMult;
this.CbLumaMult = source.CbLumaMult;
this.CbOffset = source.CbOffset;
this.CrMult = source.CrMult;
this.CrLumaMult = source.CrLumaMult;
this.CrOffset = source.CrOffset;
this.OverlapFlag = source.OverlapFlag;
this.ClipToRestrictedRange = source.ClipToRestrictedRange;
}
}

352
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs

@ -0,0 +1,352 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Stores the decoded syntax and derived state for one AV1 frame header.
/// </summary>
internal sealed class ObuFrameHeader
{
/// <summary>
/// Stores the validity state of the eight reference-frame slots without a per-header array allocation.
/// </summary>
private InlineArray8<bool> referenceValid;
/// <summary>
/// Stores the multi-bit order hint associated with each of the eight reference-frame slots.
/// </summary>
private InlineArray8<uint> referenceOrderHint;
/// <summary>
/// Stores the frame identifier associated with each of the eight reference-map slots.
/// </summary>
private InlineArray8<uint> referenceFrameId;
/// <summary>
/// Stores the reference-map slot selected for each of the seven inter reference types.
/// </summary>
private InlineArray8<uint> referenceFrameIndex;
/// <summary>
/// Stores the global-motion model associated with each of the seven inter reference types.
/// </summary>
private InlineArray7<Av1GlobalMotionParameters> globalMotionParameters;
/// <summary>
/// Stores the lossless-coding flag for each of the eight segments without a per-header array allocation.
/// </summary>
private InlineArray8<bool> losslessArray;
/// <summary>
/// Gets or sets the temporal-layer identifier carried by the primary frame-header OBU.
/// </summary>
public int TemporalId { get; set; }
/// <summary>
/// Gets or sets the spatial-layer identifier carried by the primary frame-header OBU.
/// </summary>
public int SpatialId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether motion vectors use integer-sample precision.
/// </summary>
public bool ForceIntegerMotionVector { get; set; }
/// <summary>
/// Gets or sets a value indicating whether intra-block copy is permitted.
/// </summary>
public bool AllowIntraBlockCopy { get; set; }
/// <summary>
/// Gets or sets a value indicating whether reference-frame motion vectors may be used.
/// </summary>
public bool UseReferenceFrameMotionVectors { get; set; }
/// <summary>
/// Gets or sets a value indicating whether motion vectors may use high precision.
/// </summary>
public bool AllowHighPrecisionMotionVector { get; set; }
/// <summary>
/// Gets the component precision selected by the integer and high-precision frame flags.
/// </summary>
public Av1MotionVectorPrecision MotionVectorPrecision => this.ForceIntegerMotionVector
? Av1MotionVectorPrecision.Integer
: this.AllowHighPrecisionMotionVector
? Av1MotionVectorPrecision.EighthSample
: Av1MotionVectorPrecision.QuarterSample;
/// <summary>
/// Gets or sets the frame-level interpolation filter used for inter prediction.
/// </summary>
public Av1InterpolationFilter InterpolationFilter { get; set; }
/// <summary>
/// Gets or sets a value indicating whether inter blocks may select a non-translational motion mode.
/// </summary>
public bool IsMotionModeSwitchable { get; set; }
/// <summary>
/// Gets or sets the decoded tile layout.
/// </summary>
public ObuTileGroupHeader TilesInfo { get; set; } = new ObuTileGroupHeader();
/// <summary>
/// Gets or sets a value indicating whether every segment uses lossless coding.
/// </summary>
public bool CodedLossless { get; set; }
/// <summary>
/// Gets the mutable lossless-coding flags for each segment.
/// </summary>
public Span<bool> LosslessArray => this.losslessArray;
/// <summary>
/// Gets or sets the frame quantization parameters.
/// </summary>
public ObuQuantizationParameters QuantizationParameters { get; set; } = new ObuQuantizationParameters();
/// <summary>
/// Gets or sets the frame segmentation parameters.
/// </summary>
public ObuSegmentationParameters SegmentationParameters { get; set; } = new ObuSegmentationParameters();
/// <summary>
/// Gets or sets a value indicating whether coding is lossless and no super-resolution scaling is applied.
/// </summary>
public bool AllLossless { get; set; }
/// <summary>
/// Gets or sets a value indicating whether warped motion is permitted.
/// </summary>
public bool AllowWarpedMotion { get; set; }
/// <summary>
/// Gets or sets the permitted reference prediction mode.
/// </summary>
public ObuReferenceMode ReferenceMode { get; set; }
/// <summary>
/// Gets or sets the film-grain synthesis parameters.
/// </summary>
public ObuFilmGrainParameters FilmGrainParameters { get; set; } = new ObuFilmGrainParameters();
/// <summary>
/// Gets or sets a value indicating whether the reduced transform set is used.
/// </summary>
public bool UseReducedTransformSet { get; set; }
/// <summary>
/// Gets or sets the loop-filter parameters.
/// </summary>
public ObuLoopFilterParameters LoopFilterParameters { get; set; } = new ObuLoopFilterParameters();
/// <summary>
/// Gets or sets the loop-restoration parameters.
/// </summary>
public ObuLoopRestorationParameters LoopRestorationParameters { get; set; } = new ObuLoopRestorationParameters();
/// <summary>
/// Gets or sets the constrained directional enhancement filter parameters.
/// </summary>
public ObuConstraintDirectionalEnhancementFilterParameters CdefParameters { get; set; } = new ObuConstraintDirectionalEnhancementFilterParameters();
/// <summary>
/// Gets or sets the number of mode-information columns in one stored row.
/// </summary>
public int ModeInfoStride { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the frame-end probability update is disabled.
/// </summary>
public bool DisableFrameEndUpdateCdf { get; set; }
/// <summary>
/// Gets or sets the skip-mode parameters.
/// </summary>
public ObuSkipModeParameters SkipModeParameters { get; set; } = new ObuSkipModeParameters();
/// <summary>
/// Gets or sets the transform-size selection mode.
/// </summary>
public Av1TransformMode TransformMode { get; set; }
/// <summary>
/// Gets or sets the loop-filter delta parameters.
/// </summary>
public ObuDeltaParameters DeltaLoopFilterParameters { get; set; } = new ObuDeltaParameters();
/// <summary>
/// Gets or sets the quantizer delta parameters.
/// </summary>
public ObuDeltaParameters DeltaQParameters { get; set; } = new ObuDeltaParameters();
/// <summary>
/// Gets a value indicating whether the frame uses intra prediction only.
/// </summary>
public bool IsIntra => this.FrameType is ObuFrameType.IntraOnlyFrame or ObuFrameType.KeyFrame;
/// <summary>
/// Gets or sets the decoded and rendered frame dimensions.
/// </summary>
public ObuFrameSize FrameSize { get; set; } = new ObuFrameSize();
/// <summary>
/// Gets or sets the frame width in mode-information units.
/// </summary>
public int ModeInfoColumnCount { get; set; }
/// <summary>
/// Gets or sets the frame height in mode-information units.
/// </summary>
public int ModeInfoRowCount { get; set; }
/// <summary>
/// Gets or sets a value indicating whether an existing reference frame is displayed without decoding a new frame.
/// </summary>
public bool ShowExistingFrame { get; set; }
/// <summary>
/// Gets or sets the coded frame type.
/// </summary>
public ObuFrameType FrameType { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the decoded frame is immediately displayed.
/// </summary>
public bool ShowFrame { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the frame may be displayed by a later header.
/// </summary>
public bool ShowableFrame { get; set; }
/// <summary>
/// Gets or sets the reference map index selected when showing an existing frame.
/// </summary>
public uint FrameToShowMapIdx { get; set; }
/// <summary>
/// Gets or sets the display frame identifier.
/// </summary>
public uint DisplayFrameId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the frame can be decoded without state from earlier frames.
/// </summary>
public bool ErrorResilientMode { get; set; }
/// <summary>
/// Gets or sets a value indicating whether screen-content coding tools are permitted.
/// </summary>
public bool AllowScreenContentTools { get; set; }
/// <summary>
/// Gets or sets a value indicating whether probability adaptation is disabled for this frame.
/// </summary>
public bool DisableCdfUpdate { get; set; }
/// <summary>
/// Gets or sets the identifier of the current frame.
/// </summary>
public uint CurrentFrameId { get; set; }
/// <summary>
/// Gets or sets the frame order hint.
/// </summary>
public uint OrderHint { get; set; }
/// <summary>
/// Gets or sets the zero-based inter-reference type that supplies the initial frame context, or the no-reference
/// sentinel.
/// </summary>
public uint PrimaryReferenceFrame { get; set; } = Av1Constants.PrimaryReferenceFrameNone;
/// <summary>
/// Gets or sets the resolved reference-map slot supplying the initial frame context, or <see langword="null"/> when
/// the frame uses the default context.
/// </summary>
public byte? PrimaryReferenceSlot { get; set; }
/// <summary>
/// Gets or sets the bit mask of reference slots refreshed by this frame.
/// </summary>
public uint RefreshFrameFlags { get; set; }
/// <summary>
/// Gets or sets the presentation time signaled by temporal point information.
/// </summary>
public uint FramePresentationTime { get; set; }
/// <summary>
/// Gets the validity state of each reference-frame slot.
/// </summary>
/// <returns>The mutable eight-entry reference-validity table.</returns>
public Span<bool> GetReferenceValidity() => this.referenceValid;
/// <summary>
/// Gets the multi-bit order hint associated with each reference-frame slot.
/// </summary>
/// <returns>The mutable eight-entry reference-order-hint table.</returns>
public Span<uint> GetReferenceOrderHints() => this.referenceOrderHint;
/// <summary>
/// Gets the frame identifier associated with each reference-map slot.
/// </summary>
/// <returns>The mutable eight-entry reference-frame-identifier table.</returns>
public Span<uint> GetReferenceFrameIds() => this.referenceFrameId;
/// <summary>
/// Gets the reference-map slot selected for each inter reference type.
/// </summary>
/// <returns>The mutable seven-entry inter-reference-map table.</returns>
public Span<uint> GetReferenceFrameIndices() => this.referenceFrameIndex[..Av1Constants.ReferencesPerFrame];
/// <summary>
/// Gets the global-motion model associated with each canonical inter reference type from LAST through ALTREF.
/// </summary>
/// <returns>The mutable seven-entry global-motion parameter table.</returns>
public Span<Av1GlobalMotionParameters> GetGlobalMotionParameters() => this.globalMotionParameters;
/// <summary>
/// Invalidates retained reference slots whose frame identifiers fall outside the permitted backwards window.
/// </summary>
/// <param name="frameIdLength">The number of bits in the modulo frame-identifier domain.</param>
/// <param name="deltaFrameIdLength">The number of bits used to signal reference-frame identifier deltas.</param>
public void MarkReferenceFrames(int frameIdLength, int deltaFrameIdLength)
{
uint referenceWindow = 1U << deltaFrameIdLength;
uint frameIdModulus = 1U << frameIdLength;
Span<uint> referenceFrameIds = this.GetReferenceFrameIds();
Span<bool> referenceValidity = this.GetReferenceValidity();
for (int slot = 0; slot < Av1Constants.ReferenceFrameCount; slot++)
{
uint referenceFrameId = referenceFrameIds[slot];
if (this.CurrentFrameId > referenceWindow)
{
// Without wraparound, a retained identifier is valid only in the closed interval ending at the
// current identifier and extending referenceWindow values backwards.
if (referenceFrameId > this.CurrentFrameId || referenceFrameId < this.CurrentFrameId - referenceWindow)
{
referenceValidity[slot] = false;
}
}
else
{
// When the backwards window crosses zero, valid identifiers occupy both ends of the modulo domain.
// Only the open interval between the current identifier and the wrapped lower bound is invalid.
uint wrappedLowerBound = frameIdModulus + this.CurrentFrameId - referenceWindow;
if (referenceFrameId > this.CurrentFrameId && referenceFrameId < wrappedLowerBound)
{
referenceValidity[slot] = false;
}
}
}
}
}

101
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameReferenceState.cs

@ -0,0 +1,101 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Stores the uncompressed-header reference state retained by one AV1 OBU reader session.
/// </summary>
/// <remarks>
/// This state describes the eight reference-map slots but does not own reconstructed sample buffers. Pixel ownership
/// remains with the decoder's reference-frame store and is committed before this syntax state is completed. CDF,
/// segmentation, loop-filter, motion, and layer metadata remain on that retained frame owner; the current header's
/// resolved primary-reference slot selects the shared owner instead of duplicating those values here.
/// </remarks>
internal struct ObuFrameReferenceState
{
/// <summary>
/// Stores whether each of the eight reference-map slots can be selected by a later frame.
/// </summary>
private InlineArray8<bool> referenceValidity;
/// <summary>
/// Stores the frame identifier associated with each of the eight reference-map slots.
/// </summary>
private InlineArray8<uint> referenceFrameIds;
/// <summary>
/// Stores the order hint associated with each of the eight reference-map slots.
/// </summary>
private InlineArray8<uint> referenceOrderHints;
/// <summary>
/// Gets a value indicating whether a completed frame identifier is available for the next header.
/// </summary>
public bool HasCurrentFrameId { get; private set; }
/// <summary>
/// Gets the frame identifier of the most recently completed frame.
/// </summary>
public uint CurrentFrameId { get; private set; }
/// <summary>
/// Copies the completed reference-map state into a newly created frame header.
/// </summary>
/// <param name="frameHeader">The frame header that will parse and derive state from the retained map.</param>
public void InitializeFrameHeader(ObuFrameHeader frameHeader)
{
ReadOnlySpan<bool> referenceValidity = this.referenceValidity;
ReadOnlySpan<uint> referenceFrameIds = this.referenceFrameIds;
ReadOnlySpan<uint> referenceOrderHints = this.referenceOrderHints;
// Only the eight retained-slot tables cross a frame boundary. The seven inter-reference roles are signaled or
// derived afresh for each frame, and the primary context source is resolved from that per-frame mapping.
referenceValidity.CopyTo(frameHeader.GetReferenceValidity());
referenceFrameIds.CopyTo(frameHeader.GetReferenceFrameIds());
referenceOrderHints.CopyTo(frameHeader.GetReferenceOrderHints());
}
/// <summary>
/// Publishes the reference-map transition produced by a successfully completed frame.
/// </summary>
/// <param name="frameHeader">The completed frame header whose refresh mask selects the replaced slots.</param>
/// <param name="frameIdNumbersPresent">
/// A value indicating whether the sequence carries modulo frame identifiers.
/// </param>
public void CompleteFrame(ObuFrameHeader frameHeader, bool frameIdNumbersPresent)
{
Span<bool> referenceValidity = frameHeader.GetReferenceValidity();
Span<uint> referenceFrameIds = frameHeader.GetReferenceFrameIds();
Span<uint> referenceOrderHints = frameHeader.GetReferenceOrderHints();
// Refresh is published only at this successful completion boundary. Updating the completed header first keeps
// the same object retained by the reconstructed frame owner synchronized with the next parser-session snapshot.
for (int slot = 0; slot < Av1Constants.ReferenceFrameCount; slot++)
{
if ((frameHeader.RefreshFrameFlags & (1U << slot)) != 0)
{
referenceValidity[slot] = true;
referenceFrameIds[slot] = frameHeader.CurrentFrameId;
referenceOrderHints[slot] = frameHeader.OrderHint;
}
}
referenceValidity.CopyTo(this.referenceValidity);
referenceFrameIds.CopyTo(this.referenceFrameIds);
referenceOrderHints.CopyTo(this.referenceOrderHints);
if (frameIdNumbersPresent)
{
// the reference decoder keeps one current_frame_id in decoder-session state. The following header snapshots this value as
// its previous identifier before consuming its own current_frame_id syntax.
this.CurrentFrameId = frameHeader.CurrentFrameId;
this.HasCurrentFrameId = true;
}
}
/// <summary>
/// Clears the completed frame identifier and every retained reference-map slot.
/// </summary>
public void Reset() => this = default;
}

40
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameSize.cs

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the coded, upscaled, and rendered dimensions of an AV1 frame.
/// </summary>
internal sealed class ObuFrameSize
{
/// <summary>
/// Gets or sets the coded frame width.
/// </summary>
public int FrameWidth { get; set; }
/// <summary>
/// Gets or sets the coded frame height.
/// </summary>
public int FrameHeight { get; set; }
/// <summary>
/// Gets or sets the denominator used by AV1 super-resolution scaling.
/// </summary>
public int SuperResolutionDenominator { get; set; }
/// <summary>
/// Gets or sets the frame width after super-resolution upscaling.
/// </summary>
public int SuperResolutionUpscaledWidth { get; set; }
/// <summary>
/// Gets or sets the intended display width.
/// </summary>
public int RenderWidth { get; set; }
/// <summary>
/// Gets or sets the intended display height.
/// </summary>
public int RenderHeight { get; set; }
}

30
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameType.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Identifies the AV1 frame type signaled by a frame header.
/// </summary>
internal enum ObuFrameType
{
/// <summary>
/// A key frame that is decoded without reference to another frame.
/// </summary>
KeyFrame = 0,
/// <summary>
/// An inter frame that can refer to previously decoded frames.
/// </summary>
InterFrame = 1,
/// <summary>
/// An intra-only frame that does not refresh all reference slots.
/// </summary>
IntraOnlyFrame = 2,
/// <summary>
/// A switch frame that permits switching between coded sequences.
/// </summary>
SwitchFrame = 3,
}

45
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuHeader.cs

@ -0,0 +1,45 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the parsed header and payload size of an AV1 open bitstream unit.
/// </summary>
internal sealed class ObuHeader
{
/// <summary>
/// Gets or sets the number of bytes occupied by the fixed OBU header and optional extension.
/// </summary>
public int Size { get; set; }
/// <summary>
/// Gets or sets the OBU payload type.
/// </summary>
public ObuType Type { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the OBU carries an explicit payload-size field.
/// </summary>
public bool HasSize { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the OBU carries temporal and spatial identifiers.
/// </summary>
public bool HasExtension { get; set; }
/// <summary>
/// Gets or sets the temporal-layer identifier.
/// </summary>
public int TemporalId { get; set; }
/// <summary>
/// Gets or sets the spatial-layer identifier.
/// </summary>
public int SpatialId { get; set; }
/// <summary>
/// Gets or sets the OBU payload size, in bytes.
/// </summary>
public int PayloadSize { get; set; }
}

77
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopFilterParameters.cs

@ -0,0 +1,77 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the AV1 deblocking-loop-filter parameters for a frame.
/// </summary>
internal sealed class ObuLoopFilterParameters
{
/// <summary>
/// Stores the horizontal and vertical luma filter levels.
/// </summary>
private InlineArray4<int> filterLevel;
/// <summary>
/// Stores the fixed reference-frame delta table.
/// </summary>
private InlineArray8<int> referenceDeltas;
/// <summary>
/// Stores the fixed prediction-mode delta table.
/// </summary>
private InlineArray4<int> modeDeltas;
/// <summary>
/// Initializes a new instance of the <see cref="ObuLoopFilterParameters"/> class with the AV1 default reference and mode deltas.
/// </summary>
public ObuLoopFilterParameters()
{
// AV1 indexes this table from Intra through Alternate. Golden is -1; Backward remains 0.
this.referenceDeltas[0] = 1;
this.referenceDeltas[4] = -1;
this.referenceDeltas[6] = -1;
this.referenceDeltas[7] = -1;
}
/// <summary>
/// Gets the horizontal and vertical luma filter levels.
/// </summary>
public Span<int> FilterLevel => this.filterLevel[..2];
/// <summary>
/// Gets or sets the U-plane filter level.
/// </summary>
public int FilterLevelU { get; set; }
/// <summary>
/// Gets or sets the V-plane filter level.
/// </summary>
public int FilterLevelV { get; set; }
/// <summary>
/// Gets or sets the filter sharpness level.
/// </summary>
public int SharpnessLevel { get; set; }
/// <summary>
/// Gets or sets a value indicating whether reference-frame and mode deltas are enabled.
/// </summary>
public bool ReferenceDeltaModeEnabled { get; set; }
/// <summary>
/// Gets or sets a value indicating whether reference-frame and mode deltas are updated by this frame.
/// </summary>
public bool ReferenceDeltaModeUpdate { get; set; }
/// <summary>
/// Gets the filter-level deltas for the AV1 reference-frame categories.
/// </summary>
public Span<int> ReferenceDeltas => this.referenceDeltas;
/// <summary>
/// Gets the filter-level deltas for the AV1 prediction modes.
/// </summary>
public Span<int> ModeDeltas => this.modeDeltas[..2];
}

20
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationItem.cs

@ -0,0 +1,20 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the loop-restoration type and unit size for one color plane.
/// </summary>
internal struct ObuLoopRestorationItem
{
/// <summary>
/// Gets or sets the restoration-unit size, in samples.
/// </summary>
public int Size { get; set; }
/// <summary>
/// Gets or sets the restoration filter type.
/// </summary>
public ObuRestorationType Type { get; set; }
}

40
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationParameters.cs

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the AV1 loop-restoration configuration for a frame.
/// </summary>
internal sealed class ObuLoopRestorationParameters
{
/// <summary>
/// Stores the fixed three plane configurations without an outer array or per-plane object allocation.
/// </summary>
private InlineArray4<ObuLoopRestorationItem> items;
/// <summary>
/// Gets or sets a value indicating whether any plane uses loop restoration.
/// </summary>
public bool UsesLoopRestoration { get; set; }
/// <summary>
/// Gets or sets a value indicating whether either chroma plane uses loop restoration.
/// </summary>
public bool UsesChromaLoopRestoration { get; set; }
/// <summary>
/// Gets the loop-restoration configuration for each plane.
/// </summary>
public Span<ObuLoopRestorationItem> Items => this.items[..3];
/// <summary>
/// Gets or sets the luma restoration-unit size shift.
/// </summary>
public int UnitShift { get; set; }
/// <summary>
/// Gets or sets the chroma restoration-unit size shift relative to luma.
/// </summary>
public int UVShift { get; set; }
}

95
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs

@ -0,0 +1,95 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Identifies the CICP matrix coefficients used to derive luma and chroma components.
/// </summary>
internal enum ObuMatrixCoefficients
{
/// <summary>
/// The identity matrix used for GBR component ordering.
/// </summary>
Identity = 0,
/// <summary>
/// ITU-R BT.709 coefficients.
/// </summary>
Bt709 = 1,
/// <summary>
/// Unspecified coefficients.
/// </summary>
Unspecified = 2,
/// <summary>
/// United States FCC 73.628 coefficients.
/// </summary>
Fcc = 4,
/// <summary>
/// ITU-R BT.470 System B and G coefficients.
/// </summary>
Bt470BG = 5,
/// <summary>
/// ITU-R BT.601 coefficients.
/// </summary>
Bt601 = 6,
/// <summary>
/// SMPTE 240M coefficients.
/// </summary>
Smpte240 = 7,
/// <summary>
/// SMPTE YCgCo coefficients.
/// </summary>
SmpteYCgCo = 8,
/// <summary>
/// ITU-R BT.2020 non-constant-luminance coefficients.
/// </summary>
Bt2020NonConstantLuminance = 9,
/// <summary>
/// ITU-R BT.2020 constant-luminance coefficients.
/// </summary>
Bt2020ConstantLuminance = 10,
/// <summary>
/// SMPTE ST 2085 YDzDx coefficients.
/// </summary>
Smpte2085 = 11,
/// <summary>
/// Chromaticity-derived non-constant-luminance coefficients.
/// </summary>
ChromaticityDerivedNonConstantLuminance = 12,
/// <summary>
/// Chromaticity-derived constant-luminance coefficients.
/// </summary>
ChromaticityDerivedConstantLuminance = 13,
/// <summary>
/// ITU-R BT.2100 ICtCp coefficients.
/// </summary>
Bt2100ICtCp = 14,
/// <summary>
/// IPT-C2 coefficients.
/// </summary>
IptC2 = 15,
/// <summary>
/// Reversible YCgCo coefficients with two additional encoded bits.
/// </summary>
YCgCoRe = 16,
/// <summary>
/// Reversible YCgCo coefficients with one additional encoded bit.
/// </summary>
YCgCoRo = 17,
}

40
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMetadataType.cs

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Identifies the payload carried by an AV1 metadata OBU.
/// </summary>
internal enum ObuMetadataType
{
/// <summary>
/// The reserved zero value.
/// </summary>
Reserved = 0,
/// <summary>
/// Content light-level metadata.
/// </summary>
HdrCll = 1,
/// <summary>
/// Mastering-display color-volume metadata.
/// </summary>
HdrMdcv = 2,
/// <summary>
/// Scalability-structure metadata.
/// </summary>
Scalability = 3,
/// <summary>
/// ITU-T T.35 terminal-provider metadata.
/// </summary>
ItutT35 = 4,
/// <summary>
/// Timecode metadata.
/// </summary>
Timecode = 5,
}

61
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOperatingPoint.cs

@ -0,0 +1,61 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the sequence-level constraints for an AV1 operating point.
/// </summary>
internal sealed class ObuOperatingPoint
{
/// <summary>
/// Gets or sets the operating-point index.
/// </summary>
public int OperatorIndex { get; set; }
/// <summary>
/// Gets or sets the AV1 sequence-level index.
/// </summary>
public int SequenceLevelIndex { get; set; }
/// <summary>
/// Gets or sets the sequence tier.
/// </summary>
public int SequenceTier { get; set; }
/// <summary>
/// Gets or sets a value indicating whether decoder-model timing is present for this operating point.
/// </summary>
public bool IsDecoderModelInfoPresent { get; set; }
/// <summary>
/// Gets or sets the decoder-buffer delay measured in decoding ticks.
/// </summary>
public uint DecoderBufferDelay { get; set; }
/// <summary>
/// Gets or sets the encoder-buffer delay measured in decoding ticks.
/// </summary>
public uint EncoderBufferDelay { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the operating point uses the low-delay decoding model.
/// </summary>
public bool LowDelayMode { get; set; }
/// <summary>
/// Gets or sets a value indicating whether an initial display delay is present for this operating point.
/// </summary>
public bool IsInitialDisplayDelayPresent { get; set; }
/// <summary>
/// Gets or sets the initial display delay, in decoded frames.
/// </summary>
public uint InitialDisplayDelay { get; set; }
/// <summary>
/// Gets or sets the bitmask selecting temporal and spatial layers for the operating point.
/// A value of zero selects the complete coded sequence.
/// </summary>
public uint Idc { get; set; }
}

53
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOrderHintInfo.cs

@ -0,0 +1,53 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the sequence-level order-hint and dependent prediction-tool settings.
/// </summary>
internal sealed class ObuOrderHintInfo
{
/// <summary>
/// Gets or sets a value indicating whether order hints are enabled.
/// </summary>
public bool EnableOrderHint { get; set; }
/// <summary>
/// Gets or sets a value indicating whether joint compound prediction is enabled.
/// </summary>
public bool EnableJointCompound { get; set; }
/// <summary>
/// Gets or sets a value indicating whether reference-frame motion vectors are enabled.
/// </summary>
public bool EnableReferenceFrameMotionVectors { get; set; }
/// <summary>
/// Gets or sets the number of bits used to encode order hints.
/// </summary>
public int OrderHintBits { get; set; }
/// <summary>
/// Computes the signed distance between two order hints in the sequence's modulo order-hint domain.
/// </summary>
/// <param name="first">The first order hint.</param>
/// <param name="second">The order hint subtracted from <paramref name="first"/>.</param>
/// <returns>
/// The shortest signed modulo distance, or zero when order hints are disabled for the sequence.
/// </returns>
public int GetRelativeDistance(uint first, uint second)
{
if (!this.EnableOrderHint)
{
return 0;
}
int difference = (int)first - (int)second;
int signBit = 1 << (this.OrderHintBits - 1);
// Folding around the sign bit maps the unsigned difference to [-2^(bits - 1), 2^(bits - 1)), including
// the wraparound between the highest encoded order hint and zero.
return (difference & (signBit - 1)) - (difference & signBit);
}
}

65
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuQuantizationParameters.cs

@ -0,0 +1,65 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the AV1 quantization parameters for a frame.
/// </summary>
internal sealed class ObuQuantizationParameters
{
/// <summary>
/// Stores the effective quantizer index for each of the eight segments without a per-header array allocation.
/// </summary>
private InlineArray8<int> qIndex;
/// <summary>
/// Stores the three plane DC quantizer-index deltas without a per-frame array allocation.
/// </summary>
private InlineArray4<int> deltaQDc;
/// <summary>
/// Stores the three plane AC quantizer-index deltas without a per-frame array allocation.
/// </summary>
private InlineArray4<int> deltaQAc;
/// <summary>
/// Stores the three plane quantization-matrix levels without a per-frame array allocation.
/// </summary>
private InlineArray4<int> qMatrix;
/// <summary>
/// Gets or sets the base quantizer index.
/// </summary>
public int BaseQIndex { get; set; }
/// <summary>
/// Gets the mutable effective quantizer indices for each segment.
/// </summary>
public Span<int> QIndex => this.qIndex;
/// <summary>
/// Gets or sets a value indicating whether quantization matrices are enabled.
/// </summary>
public bool IsUsingQMatrix { get; set; }
/// <summary>
/// Gets the DC quantizer-index deltas for the Y, U, and V planes.
/// </summary>
public Span<int> DeltaQDc => this.deltaQDc[..3];
/// <summary>
/// Gets the AC quantizer-index deltas for the Y, U, and V planes.
/// </summary>
public Span<int> DeltaQAc => this.deltaQAc[..3];
/// <summary>
/// Gets the quantization-matrix level for the Y, U, and V planes.
/// </summary>
public Span<int> QMatrix => this.qMatrix[..3];
/// <summary>
/// Gets or sets a value indicating whether the U and V planes use separate quantizer deltas.
/// </summary>
public bool HasSeparateUvDelta { get; set; }
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save