diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcCabacContexts.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcCabacContexts.cs
index 096ba6756..c4b747de7 100644
--- a/src/ImageSharp/Formats/Heif/Hevc/HevcCabacContexts.cs
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcCabacContexts.cs
@@ -111,7 +111,7 @@ internal sealed class HevcCabacContexts
///
/// The number of contexts used by the independently coded intra-picture syntax.
///
- private const int ContextCount = 178;
+ public const int ContextCount = 178;
///
/// The contiguous adaptive context storage owned by the entropy substream.
@@ -304,4 +304,16 @@ internal sealed class HevcCabacContexts
///
public Span CrossComponentPrediction =>
this.contexts.AsSpan(CrossComponentPredictionOffset, 10);
+
+ ///
+ /// Copies every adaptive probability context to caller-owned wavefront state.
+ ///
+ /// The destination containing at least elements.
+ public void CopyTo(Span destination) => this.contexts.CopyTo(destination);
+
+ ///
+ /// Restores every adaptive probability context from caller-owned wavefront state.
+ ///
+ /// The source containing at least elements.
+ public void CopyFrom(ReadOnlySpan source) => source[..ContextCount].CopyTo(this.contexts);
}
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcCabacDecoder.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcCabacDecoder.cs
index 5b21e0a6f..f59244f15 100644
--- a/src/ImageSharp/Formats/Heif/Hevc/HevcCabacDecoder.cs
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcCabacDecoder.cs
@@ -33,6 +33,11 @@ internal ref struct HevcCabacDecoder
///
private int bitsNeeded;
+ ///
+ /// The raw-bit position used while a pulse-code-modulated coding unit suspends arithmetic decoding.
+ ///
+ private int pcmBitOffset;
+
///
/// Initializes a new instance of the struct.
///
@@ -50,6 +55,7 @@ internal ref struct HevcCabacDecoder
this.range = 510;
this.value = ((uint)data[0] << 8) | data[1];
this.bitsNeeded = -8;
+ this.pcmBitOffset = 0;
}
///
@@ -263,6 +269,67 @@ internal ref struct HevcCabacDecoder
return false;
}
+ ///
+ /// Decodes the terminating-bin flag that enters pulse-code-modulated sample syntax.
+ ///
+ /// when raw PCM samples follow; otherwise, .
+ public bool ReadPcmFlag()
+ {
+ bool pcm = this.ReadTerminate();
+ if (pcm)
+ {
+ // A successful terminating bin leaves the underlying byte reader at the first byte after the CABAC
+ // alignment pattern. PCM sample bits start there and temporarily bypass the arithmetic registers.
+ this.pcmBitOffset = this.byteOffset * 8;
+ }
+
+ return pcm;
+ }
+
+ ///
+ /// Reads one unsigned pulse-code-modulated sample while arithmetic decoding is suspended.
+ ///
+ /// The number of most-significant-bit-first sample bits.
+ /// The decoded sample value.
+ /// The entropy substream ends within the PCM sample.
+ public ushort ReadPcmSample(int bitDepth)
+ {
+ DebugGuard.MustBeBetweenOrEqualTo(bitDepth, 1, 16, nameof(bitDepth));
+ if (this.pcmBitOffset > (this.data.Length * 8) - bitDepth)
+ {
+ throw new InvalidImageContentException("The HEVC pulse-code-modulated sample data is truncated.");
+ }
+
+ uint sample = 0;
+ int bitsRemaining = bitDepth;
+ while (bitsRemaining > 0)
+ {
+ int byteIndex = this.pcmBitOffset >> 3;
+ int bitIndex = this.pcmBitOffset & 7;
+ int bitsFromByte = Math.Min(8 - bitIndex, bitsRemaining);
+ int shift = 8 - bitIndex - bitsFromByte;
+ uint mask = (uint)((1 << bitsFromByte) - 1);
+ sample = (sample << bitsFromByte) | ((uint)(this.data[byteIndex] >> shift) & mask);
+ this.pcmBitOffset += bitsFromByte;
+ bitsRemaining -= bitsFromByte;
+ }
+
+ return (ushort)sample;
+ }
+
+ ///
+ /// Restarts arithmetic decoding after a complete byte-aligned PCM coding unit.
+ ///
+ /// The following arithmetic substream is truncated.
+ public void RestartAfterPcm()
+ {
+ DebugGuard.IsTrue((this.pcmBitOffset & 7) == 0, "The complete HEVC PCM payload must end on a byte boundary.");
+ this.byteOffset = this.pcmBitOffset >> 3;
+ this.range = 510;
+ this.bitsNeeded = -8;
+ this.value = ((uint)this.ReadByte() << 8) | this.ReadByte();
+ }
+
///
/// Validates the stop bit and zero padding following a terminating entropy-coded value.
///
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcCabacSyntaxReader.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcCabacSyntaxReader.cs
index 0ef0fd6d2..bf908c4ed 100644
--- a/src/ImageSharp/Formats/Heif/Hevc/HevcCabacSyntaxReader.cs
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcCabacSyntaxReader.cs
@@ -45,6 +45,18 @@ internal ref struct HevcCabacSyntaxReader
///
public readonly int BytesConsumed => this.decoder.BytesConsumed;
+ ///
+ /// Copies the adaptive contexts required to initialize a later wavefront row.
+ ///
+ /// The caller-owned context destination.
+ public readonly void CopyContextsTo(Span destination) => this.contexts.CopyTo(destination);
+
+ ///
+ /// Restores adaptive contexts captured after the second coding-tree block of the preceding wavefront row.
+ ///
+ /// The saved wavefront contexts.
+ public readonly void CopyContextsFrom(ReadOnlySpan source) => this.contexts.CopyFrom(source);
+
///
/// Decodes the coding-unit transquant-bypass flag.
///
@@ -87,6 +99,24 @@ internal ref struct HevcCabacSyntaxReader
return !this.decoder.ReadDecision(ref selectedContexts[0]);
}
+ ///
+ /// Decodes whether a square intra coding unit carries raw pulse-code-modulated samples.
+ ///
+ /// when PCM sample syntax follows; otherwise, .
+ public bool ReadPcmFlag() => this.decoder.ReadPcmFlag();
+
+ ///
+ /// Reads one pulse-code-modulated component sample.
+ ///
+ /// The PCM sample precision.
+ /// The decoded unsigned sample.
+ public ushort ReadPcmSample(int bitDepth) => this.decoder.ReadPcmSample(bitDepth);
+
+ ///
+ /// Restarts arithmetic decoding after the complete PCM coding-unit payload.
+ ///
+ public void RestartAfterPcm() => this.decoder.RestartAfterPcm();
+
///
/// Decodes whether a luma intra mode is selected from the three most-probable modes.
///
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcCodedBlockFlags.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcCodedBlockFlags.cs
new file mode 100644
index 000000000..e2929df81
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcCodedBlockFlags.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
+
+///
+/// Contains one or two coded-block flags for a square or vertically split HEVC component transform section.
+///
+internal readonly struct HevcCodedBlockFlags
+{
+ ///
+ /// Initializes a new instance of the struct for one square block.
+ ///
+ /// The square block's coded-block flag.
+ public HevcCodedBlockFlags(bool first)
+ {
+ this.First = first;
+ this.Second = false;
+ this.IsSplit = false;
+ }
+
+ ///
+ /// Initializes a new instance of the struct for two rectangular sub-blocks.
+ ///
+ /// The first square sub-block's coded-block flag.
+ /// The second square sub-block's coded-block flag.
+ public HevcCodedBlockFlags(bool first, bool second)
+ {
+ this.First = first;
+ this.Second = second;
+ this.IsSplit = true;
+ }
+
+ ///
+ /// Gets a value indicating whether the first or only coefficient block contains coded residual data.
+ ///
+ public bool First { get; }
+
+ ///
+ /// Gets a value indicating whether the second rectangular sub-block contains coded residual data.
+ ///
+ public bool Second { get; }
+
+ ///
+ /// Gets a value indicating whether two square sub-block flags are present.
+ ///
+ public bool IsSplit { get; }
+
+ ///
+ /// Gets a value indicating whether either governed coefficient block contains coded residual data.
+ ///
+ public bool Any => this.First || this.Second;
+}
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcCodingTreeState.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcCodingTreeState.cs
index 5935ffb7b..b07cd6f37 100644
--- a/src/ImageSharp/Formats/Heif/Hevc/HevcCodingTreeState.cs
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcCodingTreeState.cs
@@ -30,6 +30,16 @@ internal sealed class HevcCodingTreeState : IDisposable
///
private readonly Buffer2D quantizationParameters;
+ ///
+ /// The combined picture, slice, and coding-unit Cb quantization offsets at minimum-coding-block resolution.
+ ///
+ private readonly Buffer2D chromaBlueQuantizationOffsets;
+
+ ///
+ /// The combined picture, slice, and coding-unit Cr quantization offsets at minimum-coding-block resolution.
+ ///
+ private readonly Buffer2D chromaRedQuantizationOffsets;
+
///
/// The packed bypass and PCM flags at minimum-coding-block resolution.
///
@@ -59,6 +69,14 @@ internal sealed class HevcCodingTreeState : IDisposable
this.WidthInMinCodingBlocks,
this.HeightInMinCodingBlocks);
+ this.chromaBlueQuantizationOffsets = configuration.MemoryAllocator.Allocate2D(
+ this.WidthInMinCodingBlocks,
+ this.HeightInMinCodingBlocks);
+
+ this.chromaRedQuantizationOffsets = configuration.MemoryAllocator.Allocate2D(
+ this.WidthInMinCodingBlocks,
+ this.HeightInMinCodingBlocks);
+
this.flags = configuration.MemoryAllocator.Allocate2D(
this.WidthInMinCodingBlocks,
this.HeightInMinCodingBlocks);
@@ -114,6 +132,8 @@ internal sealed class HevcCodingTreeState : IDisposable
/// The base-two logarithm of the square coding-unit size.
/// The coding-tree depth.
/// The effective luma quantization parameter.
+ /// The combined Cb quantization-parameter offset.
+ /// The combined Cr quantization-parameter offset.
/// A value indicating whether transform and quantization are bypassed.
/// A value indicating whether the coding unit contains pulse-code-modulated samples.
public void SetCodingUnit(
@@ -122,6 +142,8 @@ internal sealed class HevcCodingTreeState : IDisposable
int log2Size,
int depth,
int quantizationParameter,
+ int chromaBlueQuantizationOffset,
+ int chromaRedQuantizationOffset,
bool transquantBypass,
bool pcm)
{
@@ -138,6 +160,8 @@ internal sealed class HevcCodingTreeState : IDisposable
{
this.depths.DangerousGetRowSpan(row)[unitX..endX].Fill((byte)depth);
this.quantizationParameters.DangerousGetRowSpan(row)[unitX..endX].Fill((sbyte)quantizationParameter);
+ this.chromaBlueQuantizationOffsets.DangerousGetRowSpan(row)[unitX..endX].Fill((sbyte)chromaBlueQuantizationOffset);
+ this.chromaRedQuantizationOffsets.DangerousGetRowSpan(row)[unitX..endX].Fill((sbyte)chromaRedQuantizationOffset);
this.flags.DangerousGetRowSpan(row)[unitX..endX].Fill(packedFlags);
}
}
@@ -160,6 +184,18 @@ internal sealed class HevcCodingTreeState : IDisposable
public int GetQuantizationParameter(int x, int y)
=> this.quantizationParameters.DangerousGetRowSpan(y >> this.MinCodingBlockLog2)[x >> this.MinCodingBlockLog2];
+ ///
+ /// Gets the combined chroma quantization-parameter offset at a luma sample coordinate.
+ ///
+ /// The Cb or Cr reconstruction plane.
+ /// The luma sample X coordinate.
+ /// The luma sample Y coordinate.
+ /// The selected picture, slice, and coding-unit offset.
+ public int GetChromaQuantizationOffset(HevcPlane plane, int x, int y)
+ => plane == HevcPlane.Cb
+ ? this.chromaBlueQuantizationOffsets.DangerousGetRowSpan(y >> this.MinCodingBlockLog2)[x >> this.MinCodingBlockLog2]
+ : this.chromaRedQuantizationOffsets.DangerousGetRowSpan(y >> this.MinCodingBlockLog2)[x >> this.MinCodingBlockLog2];
+
///
/// Gets a value indicating whether the coding unit at a luma sample coordinate bypasses transform and quantization.
///
@@ -187,6 +223,8 @@ internal sealed class HevcCodingTreeState : IDisposable
{
this.depths.Dispose();
this.quantizationParameters.Dispose();
+ this.chromaBlueQuantizationOffsets.Dispose();
+ this.chromaRedQuantizationOffsets.Dispose();
this.flags.Dispose();
}
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientCodingParameters.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientCodingParameters.cs
index a92f60135..78d4d9027 100644
--- a/src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientCodingParameters.cs
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientCodingParameters.cs
@@ -126,6 +126,7 @@ internal readonly struct HevcCoefficientCodingParameters
/// Whether the transform block bypasses the inverse transform.
/// Whether the coding unit bypasses inverse quantization and inverse transform.
/// The residual differential-pulse-code-modulation mode selected for the block.
+ /// Whether a separately coded color plane uses the luma coefficient context set.
/// The coefficient entropy-coding parameters for the transform block.
public static HevcCoefficientCodingParameters Create(
HevcPictureParameterSet pictureParameterSet,
@@ -136,15 +137,17 @@ internal readonly struct HevcCoefficientCodingParameters
int intraPredictionMode,
bool transformSkip,
bool transquantBypass,
- HevcResidualDpcmMode residualDpcmMode)
+ HevcResidualDpcmMode residualDpcmMode,
+ bool useLumaSyntax = false)
{
HevcSequenceParameterSet sequenceParameterSet = pictureParameterSet.SequenceParameterSet;
- bool isChroma = plane != HevcPlane.Y;
+ HevcPlane codingPlane = useLumaSyntax ? HevcPlane.Y : plane;
+ bool isChroma = codingPlane != HevcPlane.Y;
bool nonTransformed = transformSkip || transquantBypass;
HevcCoefficientScanType scanType = SelectScanType(
width,
height,
- plane,
+ codingPlane,
isIntra,
intraPredictionMode,
sequenceParameterSet.ChromaFormat,
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientDecoder.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientDecoder.cs
index 264f1c602..657f1ed48 100644
--- a/src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientDecoder.cs
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientDecoder.cs
@@ -96,6 +96,23 @@ internal sealed class HevcCoefficientDecoder : IDisposable
8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9,
];
+ ///
+ /// Clears all persistent Rice adaptation statistics for a newly initialized entropy substream.
+ ///
+ public void ResetRiceAdaptation() => this.riceAdaptationStatistics = default;
+
+ ///
+ /// Copies the four persistent Rice adaptation statistics to caller-owned wavefront state.
+ ///
+ /// The four-element destination.
+ public void CopyRiceAdaptationTo(Span destination) => this.riceAdaptationStatistics[..4].CopyTo(destination);
+
+ ///
+ /// Restores the four persistent Rice adaptation statistics captured for a later wavefront row.
+ ///
+ /// The four saved statistics.
+ public void CopyRiceAdaptationFrom(ReadOnlySpan source) => source[..4].CopyTo(this.riceAdaptationStatistics[..4]);
+
///
/// Decodes one transform block into raster-ordered signed coefficient levels.
///
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingFilter.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingFilter.cs
new file mode 100644
index 000000000..da2178259
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingFilter.cs
@@ -0,0 +1,614 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.Intrinsics;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
+
+///
+/// Applies the HEVC luma and chroma deblocking kernels to four-sample edge segments.
+///
+internal static class HevcDeblockingFilter
+{
+ ///
+ /// Defines orientation-specific access to the four samples running along one deblocking edge segment.
+ ///
+ private interface IEdgeOperator
+ {
+ ///
+ /// Loads four samples at one signed distance across the edge.
+ ///
+ /// The reconstructed picture.
+ /// The component plane.
+ /// The first Q-side sample X coordinate.
+ /// The first Q-side sample Y coordinate.
+ /// The signed sample distance across the edge.
+ /// Four widened samples ordered along the edge.
+ public static abstract Vector128 LoadVector(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance);
+
+ ///
+ /// Stores four samples at one signed distance across the edge.
+ ///
+ /// The reconstructed picture.
+ /// The component plane.
+ /// The first Q-side sample X coordinate.
+ /// The first Q-side sample Y coordinate.
+ /// The signed sample distance across the edge.
+ /// The four widened samples ordered along the edge.
+ /// The number of low lanes to store.
+ public static abstract void StoreVector(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int distance,
+ Vector128 value,
+ int count);
+
+ ///
+ /// Loads one scalar sample at a signed distance across and an offset along the edge.
+ ///
+ /// The reconstructed picture.
+ /// The component plane.
+ /// The first Q-side sample X coordinate.
+ /// The first Q-side sample Y coordinate.
+ /// The signed sample distance across the edge.
+ /// The sample offset along the edge.
+ /// The selected sample.
+ public static abstract int LoadScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index);
+
+ ///
+ /// Stores one scalar sample at a signed distance across and an offset along the edge.
+ ///
+ /// The reconstructed picture.
+ /// The component plane.
+ /// The first Q-side sample X coordinate.
+ /// The first Q-side sample Y coordinate.
+ /// The signed sample distance across the edge.
+ /// The sample offset along the edge.
+ /// The filtered sample.
+ public static abstract void StoreScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index, int value);
+ }
+
+ ///
+ /// Filters four rows crossing one vertical luma boundary.
+ ///
+ /// The reconstructed picture.
+ /// The component plane.
+ /// The first Q-side sample X coordinate.
+ /// The top sample Y coordinate.
+ /// The scaled discontinuity threshold.
+ /// The scaled clipping threshold.
+ /// Whether the P-side block retains its original samples.
+ /// Whether the Q-side block retains its original samples.
+ /// The component sample precision.
+ public static void FilterVerticalLuma(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int beta,
+ int tc,
+ bool partPNoFilter,
+ bool partQNoFilter,
+ int bitDepth)
+ => FilterLuma(picture, plane, x, y, beta, tc, partPNoFilter, partQNoFilter, bitDepth);
+
+ ///
+ /// Filters four columns crossing one horizontal luma boundary.
+ ///
+ /// The reconstructed picture.
+ /// The component plane.
+ /// The left sample X coordinate.
+ /// The first Q-side sample Y coordinate.
+ /// The scaled discontinuity threshold.
+ /// The scaled clipping threshold.
+ /// Whether the P-side block retains its original samples.
+ /// Whether the Q-side block retains its original samples.
+ /// The component sample precision.
+ public static void FilterHorizontalLuma(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int beta,
+ int tc,
+ bool partPNoFilter,
+ bool partQNoFilter,
+ int bitDepth)
+ => FilterLuma(picture, plane, x, y, beta, tc, partPNoFilter, partQNoFilter, bitDepth);
+
+ ///
+ /// Filters four rows crossing one vertical chroma boundary.
+ ///
+ /// The reconstructed picture.
+ /// The Cb or Cr component plane.
+ /// The first Q-side sample X coordinate.
+ /// The top sample Y coordinate.
+ /// The scaled clipping threshold.
+ /// Whether the P-side block retains its original samples.
+ /// Whether the Q-side block retains its original samples.
+ /// The component sample precision.
+ /// The number of samples in the edge segment.
+ public static void FilterVerticalChroma(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int tc,
+ bool partPNoFilter,
+ bool partQNoFilter,
+ int bitDepth,
+ int count)
+ => FilterChroma(picture, plane, x, y, tc, partPNoFilter, partQNoFilter, bitDepth, count);
+
+ ///
+ /// Filters four columns crossing one horizontal chroma boundary.
+ ///
+ /// The reconstructed picture.
+ /// The Cb or Cr component plane.
+ /// The left sample X coordinate.
+ /// The first Q-side sample Y coordinate.
+ /// The scaled clipping threshold.
+ /// Whether the P-side block retains its original samples.
+ /// Whether the Q-side block retains its original samples.
+ /// The component sample precision.
+ /// The number of samples in the edge segment.
+ public static void FilterHorizontalChroma(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int tc,
+ bool partPNoFilter,
+ bool partQNoFilter,
+ int bitDepth,
+ int count)
+ => FilterChroma(picture, plane, x, y, tc, partPNoFilter, partQNoFilter, bitDepth, count);
+
+ ///
+ /// Applies the strong or weak luma kernel through one closed edge-orientation operator.
+ ///
+ /// The vertical or horizontal sample-access operator.
+ /// The reconstructed picture.
+ /// The component plane.
+ /// The first Q-side sample X coordinate.
+ /// The first Q-side sample Y coordinate.
+ /// The scaled discontinuity threshold.
+ /// The scaled clipping threshold.
+ /// Whether the P-side block retains its original samples.
+ /// Whether the Q-side block retains its original samples.
+ /// The component sample precision.
+ private static void FilterLuma(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int beta,
+ int tc,
+ bool partPNoFilter,
+ bool partQNoFilter,
+ int bitDepth)
+ where TOperator : struct, IEdgeOperator
+ {
+ if (beta == 0)
+ {
+ return;
+ }
+
+ int p2Start = TOperator.LoadScalar(picture, plane, x, y, -3, 0);
+ int p1Start = TOperator.LoadScalar(picture, plane, x, y, -2, 0);
+ int p0Start = TOperator.LoadScalar(picture, plane, x, y, -1, 0);
+ int q0Start = TOperator.LoadScalar(picture, plane, x, y, 0, 0);
+ int q1Start = TOperator.LoadScalar(picture, plane, x, y, 1, 0);
+ int q2Start = TOperator.LoadScalar(picture, plane, x, y, 2, 0);
+ int p2End = TOperator.LoadScalar(picture, plane, x, y, -3, 3);
+ int p1End = TOperator.LoadScalar(picture, plane, x, y, -2, 3);
+ int p0End = TOperator.LoadScalar(picture, plane, x, y, -1, 3);
+ int q0End = TOperator.LoadScalar(picture, plane, x, y, 0, 3);
+ int q1End = TOperator.LoadScalar(picture, plane, x, y, 1, 3);
+ int q2End = TOperator.LoadScalar(picture, plane, x, y, 2, 3);
+ int dpStart = Math.Abs(p2Start - (2 * p1Start) + p0Start);
+ int dqStart = Math.Abs(q0Start - (2 * q1Start) + q2Start);
+ int dpEnd = Math.Abs(p2End - (2 * p1End) + p0End);
+ int dqEnd = Math.Abs(q0End - (2 * q1End) + q2End);
+ int dp = dpStart + dpEnd;
+ int dq = dqStart + dqEnd;
+ int discontinuity = dp + dq;
+ if (discontinuity >= beta)
+ {
+ return;
+ }
+
+ int sideThreshold = (beta + (beta >> 1)) >> 3;
+ bool filterSecondP = dp < sideThreshold;
+ bool filterSecondQ = dq < sideThreshold;
+ bool strong = UsesStrongFiltering(picture, plane, x, y, 0, 2 * (dpStart + dqStart), beta, tc)
+ && UsesStrongFiltering(picture, plane, x, y, 3, 2 * (dpEnd + dqEnd), beta, tc);
+
+ if (!Vector128.IsHardwareAccelerated)
+ {
+ for (int index = 0; index < 4; index++)
+ {
+ FilterLumaScalar(
+ picture,
+ plane,
+ x,
+ y,
+ index,
+ tc,
+ strong,
+ partPNoFilter,
+ partQNoFilter,
+ tc * 10,
+ filterSecondP,
+ filterSecondQ,
+ bitDepth);
+ }
+
+ return;
+ }
+
+ Vector128 p3 = TOperator.LoadVector(picture, plane, x, y, -4);
+ Vector128 p2 = TOperator.LoadVector(picture, plane, x, y, -3);
+ Vector128 p1 = TOperator.LoadVector(picture, plane, x, y, -2);
+ Vector128 p0 = TOperator.LoadVector(picture, plane, x, y, -1);
+ Vector128 q0 = TOperator.LoadVector(picture, plane, x, y, 0);
+ Vector128 q1 = TOperator.LoadVector(picture, plane, x, y, 1);
+ Vector128 q2 = TOperator.LoadVector(picture, plane, x, y, 2);
+ Vector128 q3 = TOperator.LoadVector(picture, plane, x, y, 3);
+
+ // Each Int32 lane is one row or column along the edge. The threshold decision is shared by all four lanes,
+ // while the filter arithmetic stays lane-local and exactly matches the scalar equations below.
+ if (strong)
+ {
+ Vector128 twiceTc = Vector128.Create(2 * tc);
+ Vector128 four = Vector128.Create(4);
+ Vector128 two = Vector128.Create(2);
+ Vector128 filteredP0 = Vector128.Clamp((p2 + (p1 * 2) + (p0 * 2) + (q0 * 2) + q1 + four) >> 3, p0 - twiceTc, p0 + twiceTc);
+ Vector128 filteredQ0 = Vector128.Clamp((p1 + (p0 * 2) + (q0 * 2) + (q1 * 2) + q2 + four) >> 3, q0 - twiceTc, q0 + twiceTc);
+ Vector128 filteredP1 = Vector128.Clamp((p2 + p1 + p0 + q0 + two) >> 2, p1 - twiceTc, p1 + twiceTc);
+ Vector128 filteredQ1 = Vector128.Clamp((p0 + q0 + q1 + q2 + two) >> 2, q1 - twiceTc, q1 + twiceTc);
+ Vector128 filteredP2 = Vector128.Clamp(((p3 * 2) + (p2 * 3) + p1 + p0 + q0 + four) >> 3, p2 - twiceTc, p2 + twiceTc);
+ Vector128 filteredQ2 = Vector128.Clamp((p0 + q0 + q1 + (q2 * 3) + (q3 * 2) + four) >> 3, q2 - twiceTc, q2 + twiceTc);
+
+ TOperator.StoreVector(picture, plane, x, y, -3, partPNoFilter ? p2 : filteredP2, 4);
+ TOperator.StoreVector(picture, plane, x, y, -2, partPNoFilter ? p1 : filteredP1, 4);
+ TOperator.StoreVector(picture, plane, x, y, -1, partPNoFilter ? p0 : filteredP0, 4);
+ TOperator.StoreVector(picture, plane, x, y, 0, partQNoFilter ? q0 : filteredQ0, 4);
+ TOperator.StoreVector(picture, plane, x, y, 1, partQNoFilter ? q1 : filteredQ1, 4);
+ TOperator.StoreVector(picture, plane, x, y, 2, partQNoFilter ? q2 : filteredQ2, 4);
+ return;
+ }
+
+ Vector128 primaryDifference = (q0 - p0) * 9;
+ Vector128 secondaryDifference = (q1 - p1) * 3;
+ Vector128 delta = (primaryDifference - secondaryDifference + Vector128.Create(8)) >> 4;
+ Vector128 filterMask = Vector128.LessThan(Vector128.Abs(delta), Vector128.Create(tc * 10));
+ delta = Vector128.Clamp(delta, Vector128.Create(-tc), Vector128.Create(tc));
+ Vector128 minimum = Vector128.Zero;
+ Vector128 maximum = Vector128.Create((1 << bitDepth) - 1);
+ Vector128 filteredP0Weak = Vector128.ConditionalSelect(filterMask, Vector128.Clamp(p0 + delta, minimum, maximum), p0);
+ Vector128 filteredQ0Weak = Vector128.ConditionalSelect(filterMask, Vector128.Clamp(q0 - delta, minimum, maximum), q0);
+ TOperator.StoreVector(picture, plane, x, y, -1, partPNoFilter ? p0 : filteredP0Weak, 4);
+ TOperator.StoreVector(picture, plane, x, y, 0, partQNoFilter ? q0 : filteredQ0Weak, 4);
+
+ int halfTc = tc >> 1;
+ if (filterSecondP && !partPNoFilter)
+ {
+ Vector128 secondary = (((p2 + p0 + Vector128.One) >> 1) - p1 + delta) >> 1;
+ secondary = Vector128.Clamp(secondary, Vector128.Create(-halfTc), Vector128.Create(halfTc));
+ Vector128 filtered = Vector128.ConditionalSelect(filterMask, Vector128.Clamp(p1 + secondary, minimum, maximum), p1);
+ TOperator.StoreVector(picture, plane, x, y, -2, filtered, 4);
+ }
+
+ if (filterSecondQ && !partQNoFilter)
+ {
+ Vector128 secondary = (((q2 + q0 + Vector128.One) >> 1) - q1 - delta) >> 1;
+ secondary = Vector128.Clamp(secondary, Vector128.Create(-halfTc), Vector128.Create(halfTc));
+ Vector128 filtered = Vector128.ConditionalSelect(filterMask, Vector128.Clamp(q1 + secondary, minimum, maximum), q1);
+ TOperator.StoreVector(picture, plane, x, y, 1, filtered, 4);
+ }
+ }
+
+ ///
+ /// Applies the chroma kernel through one closed edge-orientation operator.
+ ///
+ /// The vertical or horizontal sample-access operator.
+ /// The reconstructed picture.
+ /// The Cb or Cr component plane.
+ /// The first Q-side sample X coordinate.
+ /// The first Q-side sample Y coordinate.
+ /// The scaled clipping threshold.
+ /// Whether the P-side block retains its original samples.
+ /// Whether the Q-side block retains its original samples.
+ /// The component sample precision.
+ /// The number of samples in the edge segment.
+ private static void FilterChroma(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int tc,
+ bool partPNoFilter,
+ bool partQNoFilter,
+ int bitDepth,
+ int count)
+ where TOperator : struct, IEdgeOperator
+ {
+ if (tc == 0)
+ {
+ return;
+ }
+
+ if (!Vector128.IsHardwareAccelerated)
+ {
+ int maximum = (1 << bitDepth) - 1;
+ for (int index = 0; index < count; index++)
+ {
+ int p1 = TOperator.LoadScalar(picture, plane, x, y, -2, index);
+ int p0 = TOperator.LoadScalar(picture, plane, x, y, -1, index);
+ int q0 = TOperator.LoadScalar(picture, plane, x, y, 0, index);
+ int q1 = TOperator.LoadScalar(picture, plane, x, y, 1, index);
+ int delta = Math.Clamp((((q0 - p0) << 2) + p1 - q1 + 4) >> 3, -tc, tc);
+ if (!partPNoFilter)
+ {
+ TOperator.StoreScalar(picture, plane, x, y, -1, index, Math.Clamp(p0 + delta, 0, maximum));
+ }
+
+ if (!partQNoFilter)
+ {
+ TOperator.StoreScalar(picture, plane, x, y, 0, index, Math.Clamp(q0 - delta, 0, maximum));
+ }
+ }
+
+ return;
+ }
+
+ Vector128 p1Vector = TOperator.LoadVector(picture, plane, x, y, -2);
+ Vector128 p0Vector = TOperator.LoadVector(picture, plane, x, y, -1);
+ Vector128 q0Vector = TOperator.LoadVector(picture, plane, x, y, 0);
+ Vector128 q1Vector = TOperator.LoadVector(picture, plane, x, y, 1);
+ Vector128 deltaVector = (((q0Vector - p0Vector) * 4) + p1Vector - q1Vector + Vector128.Create(4)) >> 3;
+ deltaVector = Vector128.Clamp(deltaVector, Vector128.Create(-tc), Vector128.Create(tc));
+ Vector128 minimum = Vector128.Zero;
+ Vector128 maximumVector = Vector128.Create((1 << bitDepth) - 1);
+
+ if (!partPNoFilter)
+ {
+ TOperator.StoreVector(picture, plane, x, y, -1, Vector128.Clamp(p0Vector + deltaVector, minimum, maximumVector), count);
+ }
+
+ if (!partQNoFilter)
+ {
+ TOperator.StoreVector(picture, plane, x, y, 0, Vector128.Clamp(q0Vector - deltaVector, minimum, maximumVector), count);
+ }
+ }
+
+ ///
+ /// Applies the scalar luma equations to one sample along an edge.
+ ///
+ /// The vertical or horizontal sample-access operator.
+ /// The reconstructed picture.
+ /// The component plane.
+ /// The first Q-side sample X coordinate.
+ /// The first Q-side sample Y coordinate.
+ /// The sample offset along the edge.
+ /// The scaled clipping threshold.
+ /// Whether the strong six-sample filter is selected.
+ /// Whether the P-side block retains its original samples.
+ /// Whether the Q-side block retains its original samples.
+ /// The weak-filter delta threshold.
+ /// Whether the second P-side sample is filtered.
+ /// Whether the second Q-side sample is filtered.
+ /// The component sample precision.
+ private static void FilterLumaScalar(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int index,
+ int tc,
+ bool strong,
+ bool partPNoFilter,
+ bool partQNoFilter,
+ int thresholdCut,
+ bool filterSecondP,
+ bool filterSecondQ,
+ int bitDepth)
+ where TOperator : struct, IEdgeOperator
+ {
+ int p3 = TOperator.LoadScalar(picture, plane, x, y, -4, index);
+ int p2 = TOperator.LoadScalar(picture, plane, x, y, -3, index);
+ int p1 = TOperator.LoadScalar(picture, plane, x, y, -2, index);
+ int p0 = TOperator.LoadScalar(picture, plane, x, y, -1, index);
+ int q0 = TOperator.LoadScalar(picture, plane, x, y, 0, index);
+ int q1 = TOperator.LoadScalar(picture, plane, x, y, 1, index);
+ int q2 = TOperator.LoadScalar(picture, plane, x, y, 2, index);
+ int q3 = TOperator.LoadScalar(picture, plane, x, y, 3, index);
+ if (strong)
+ {
+ if (!partPNoFilter)
+ {
+ int filteredP0 = Math.Clamp(
+ (p2 + (2 * p1) + (2 * p0) + (2 * q0) + q1 + 4) >> 3,
+ p0 - (2 * tc),
+ p0 + (2 * tc));
+
+ TOperator.StoreScalar(picture, plane, x, y, -1, index, filteredP0);
+ TOperator.StoreScalar(picture, plane, x, y, -2, index, Math.Clamp((p2 + p1 + p0 + q0 + 2) >> 2, p1 - (2 * tc), p1 + (2 * tc)));
+ TOperator.StoreScalar(picture, plane, x, y, -3, index, Math.Clamp(((2 * p3) + (3 * p2) + p1 + p0 + q0 + 4) >> 3, p2 - (2 * tc), p2 + (2 * tc)));
+ }
+
+ if (!partQNoFilter)
+ {
+ int filteredQ0 = Math.Clamp(
+ (p1 + (2 * p0) + (2 * q0) + (2 * q1) + q2 + 4) >> 3,
+ q0 - (2 * tc),
+ q0 + (2 * tc));
+
+ TOperator.StoreScalar(picture, plane, x, y, 0, index, filteredQ0);
+ TOperator.StoreScalar(picture, plane, x, y, 1, index, Math.Clamp((p0 + q0 + q1 + q2 + 2) >> 2, q1 - (2 * tc), q1 + (2 * tc)));
+ TOperator.StoreScalar(picture, plane, x, y, 2, index, Math.Clamp((p0 + q0 + q1 + (3 * q2) + (2 * q3) + 4) >> 3, q2 - (2 * tc), q2 + (2 * tc)));
+ }
+
+ return;
+ }
+
+ int delta = ((9 * (q0 - p0)) - (3 * (q1 - p1)) + 8) >> 4;
+ if (Math.Abs(delta) >= thresholdCut)
+ {
+ return;
+ }
+
+ delta = Math.Clamp(delta, -tc, tc);
+ int maximum = (1 << bitDepth) - 1;
+ if (!partPNoFilter)
+ {
+ TOperator.StoreScalar(picture, plane, x, y, -1, index, Math.Clamp(p0 + delta, 0, maximum));
+ if (filterSecondP)
+ {
+ int secondary = (((p2 + p0 + 1) >> 1) - p1 + delta) >> 1;
+ secondary = Math.Clamp(secondary, -(tc >> 1), tc >> 1);
+ TOperator.StoreScalar(picture, plane, x, y, -2, index, Math.Clamp(p1 + secondary, 0, maximum));
+ }
+ }
+
+ if (!partQNoFilter)
+ {
+ TOperator.StoreScalar(picture, plane, x, y, 0, index, Math.Clamp(q0 - delta, 0, maximum));
+ if (filterSecondQ)
+ {
+ int secondary = (((q2 + q0 + 1) >> 1) - q1 - delta) >> 1;
+ secondary = Math.Clamp(secondary, -(tc >> 1), tc >> 1);
+ TOperator.StoreScalar(picture, plane, x, y, 1, index, Math.Clamp(q1 + secondary, 0, maximum));
+ }
+ }
+ }
+
+ ///
+ /// Determines whether one endpoint satisfies the strong-filter conditions.
+ ///
+ /// The vertical or horizontal sample-access operator.
+ /// The reconstructed picture.
+ /// The component plane.
+ /// The first Q-side sample X coordinate.
+ /// The first Q-side sample Y coordinate.
+ /// The endpoint offset along the edge.
+ /// Twice the endpoint's second-derivative sum.
+ /// The scaled discontinuity threshold.
+ /// The scaled clipping threshold.
+ /// when strong filtering is permitted; otherwise, .
+ private static bool UsesStrongFiltering(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int index,
+ int discontinuity,
+ int beta,
+ int tc)
+ where TOperator : struct, IEdgeOperator
+ {
+ int p3 = TOperator.LoadScalar(picture, plane, x, y, -4, index);
+ int p0 = TOperator.LoadScalar(picture, plane, x, y, -1, index);
+ int q0 = TOperator.LoadScalar(picture, plane, x, y, 0, index);
+ int q3 = TOperator.LoadScalar(picture, plane, x, y, 3, index);
+ int strongDiscontinuity = Math.Abs(p3 - p0) + Math.Abs(q3 - q0);
+ int strongThreshold = ((5 * tc) + 1) >> 1;
+ return strongDiscontinuity < (beta >> 3)
+ && discontinuity < (beta >> 2)
+ && Math.Abs(p0 - q0) < strongThreshold;
+ }
+
+ ///
+ /// Accesses four rows across a vertical edge.
+ ///
+ private readonly struct VerticalEdgeOperator : IEdgeOperator
+ {
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 LoadVector(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance)
+ => Vector128.Create(
+ (int)picture.GetRowSpan(plane, y)[x + distance],
+ picture.GetRowSpan(plane, y + 1)[x + distance],
+ picture.GetRowSpan(plane, y + 2)[x + distance],
+ picture.GetRowSpan(plane, y + 3)[x + distance]);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void StoreVector(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int distance,
+ Vector128 value,
+ int count)
+ {
+ for (int index = 0; index < count; index++)
+ {
+ picture.GetRowSpan(plane, y + index)[x + distance] = (ushort)value.GetElement(index);
+ }
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int LoadScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index)
+ => picture.GetRowSpan(plane, y + index)[x + distance];
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void StoreScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index, int value)
+ => picture.GetRowSpan(plane, y + index)[x + distance] = (ushort)value;
+ }
+
+ ///
+ /// Accesses four columns across a horizontal edge.
+ ///
+ private readonly struct HorizontalEdgeOperator : IEdgeOperator
+ {
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 LoadVector(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance)
+ {
+ ref ushort source = ref picture.GetRowSpan(plane, y + distance)[x];
+ Vector64 packed = Unsafe.As>(ref source);
+ return Vector128.WidenLower(Vector128.Create(packed, Vector64.Zero)).AsInt32();
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void StoreVector(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int distance,
+ Vector128 value,
+ int count)
+ {
+ ref ushort destination = ref picture.GetRowSpan(plane, y + distance)[x];
+ if (count == 4)
+ {
+ Vector64 packed = Vector128.Narrow(value, Vector128.Zero).AsUInt16().GetLower();
+ Unsafe.As>(ref destination) = packed;
+ return;
+ }
+
+ destination = (ushort)value.GetElement(0);
+ Unsafe.Add(ref destination, 1) = (ushort)value.GetElement(1);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int LoadScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index)
+ => picture.GetRowSpan(plane, y + distance)[x + index];
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void StoreScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index, int value)
+ => picture.GetRowSpan(plane, y + distance)[x + index] = (ushort)value;
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingState.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingState.cs
new file mode 100644
index 000000000..681b72307
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingState.cs
@@ -0,0 +1,118 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
+
+///
+/// Tracks luma transform and prediction boundaries at the four-sample resolution used to derive HEVC deblocking edges.
+///
+internal sealed class HevcDeblockingState : IDisposable
+{
+ ///
+ /// The base-two logarithm of the boundary-map unit side.
+ ///
+ private const int UnitLog2 = 2;
+
+ ///
+ /// The packed flag identifying a vertical boundary at a unit's left edge.
+ ///
+ private const byte VerticalBoundary = 1 << 0;
+
+ ///
+ /// The packed flag identifying a horizontal boundary at a unit's top edge.
+ ///
+ private const byte HorizontalBoundary = 1 << 1;
+
+ ///
+ /// The boundary maps for the primary plane of combined coding or each independently coded color plane.
+ ///
+ private readonly Buffer2D[] boundaries;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration providing the image memory allocator.
+ /// The coded picture dimensions.
+ public HevcDeblockingState(Configuration configuration, HevcSequenceParameterSet sequenceParameterSet)
+ {
+ int width = DivideCeilingByPowerOfTwo(sequenceParameterSet.Width, UnitLog2);
+ int height = DivideCeilingByPowerOfTwo(sequenceParameterSet.Height, UnitLog2);
+ this.boundaries =
+ [
+ configuration.MemoryAllocator.Allocate2D(width, height),
+ configuration.MemoryAllocator.Allocate2D(width, height),
+ configuration.MemoryAllocator.Allocate2D(width, height),
+ ];
+ }
+
+ ///
+ /// Records the left and top edges of one leaf transform or pulse-code-modulated coding block.
+ ///
+ /// The primary coding plane.
+ /// The block left coordinate in full-resolution primary-plane samples.
+ /// The block top coordinate in full-resolution primary-plane samples.
+ /// The block width in samples.
+ /// The block height in samples.
+ public void MarkBlock(HevcPlane plane, int x, int y, int width, int height)
+ {
+ Buffer2D map = this.boundaries[(int)plane];
+ int unitX = x >> UnitLog2;
+ int unitY = y >> UnitLog2;
+ int endX = Math.Min(DivideCeilingByPowerOfTwo(x + width, UnitLog2), map.Width);
+ int endY = Math.Min(DivideCeilingByPowerOfTwo(y + height, UnitLog2), map.Height);
+
+ // A transform boundary covers every four-sample segment along its edge. Packing both orientations into one
+ // byte keeps the decoder state contiguous and lets the later eight-sample deblocking traversal reject edges cheaply.
+ for (int row = unitY; row < endY; row++)
+ {
+ map.DangerousGetRowSpan(row)[unitX] |= VerticalBoundary;
+ }
+
+ Span top = map.DangerousGetRowSpan(unitY);
+ for (int column = unitX; column < endX; column++)
+ {
+ top[column] |= HorizontalBoundary;
+ }
+ }
+
+ ///
+ /// Gets whether a four-sample segment begins at a vertical transform or prediction boundary.
+ ///
+ /// The primary coding plane.
+ /// The segment left coordinate in full-resolution primary-plane samples.
+ /// The segment top coordinate in full-resolution primary-plane samples.
+ /// when the segment is a vertical boundary; otherwise, .
+ public bool IsVerticalBoundary(HevcPlane plane, int x, int y)
+ => (this.boundaries[(int)plane].DangerousGetRowSpan(y >> UnitLog2)[x >> UnitLog2] & VerticalBoundary) != 0;
+
+ ///
+ /// Gets whether a four-sample segment begins at a horizontal transform or prediction boundary.
+ ///
+ /// The primary coding plane.
+ /// The segment left coordinate in full-resolution primary-plane samples.
+ /// The segment top coordinate in full-resolution primary-plane samples.
+ /// when the segment is a horizontal boundary; otherwise, .
+ public bool IsHorizontalBoundary(HevcPlane plane, int x, int y)
+ => (this.boundaries[(int)plane].DangerousGetRowSpan(y >> UnitLog2)[x >> UnitLog2] & HorizontalBoundary) != 0;
+
+ ///
+ /// Releases the allocator-owned boundary maps.
+ ///
+ public void Dispose()
+ {
+ foreach (Buffer2D map in this.boundaries)
+ {
+ map.Dispose();
+ }
+ }
+
+ ///
+ /// Divides a nonnegative sample count by a power of two with upward rounding.
+ ///
+ /// The sample count.
+ /// The base-two divisor logarithm.
+ /// The upward-rounded quotient.
+ private static int DivideCeilingByPowerOfTwo(int value, int shift) => (value + (1 << shift) - 1) >> shift;
+}
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.Operations.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.Operations.cs
index 573008bc5..ef10e5509 100644
--- a/src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.Operations.cs
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.Operations.cs
@@ -422,7 +422,7 @@ internal static partial class HevcInverseTransformer
/// The transform-block width.
/// The transform-block height.
/// The reconstructed component precision.
- private static void AddResidual(ReadOnlySpan residual, Span destination, int destinationStride, int width, int height, int bitDepth)
+ public static void AddResidual(ReadOnlySpan residual, Span destination, int destinationStride, int width, int height, int bitDepth)
{
int maximum = (1 << bitDepth) - 1;
for (int y = 0; y < height; y++)
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcPictureBuffer.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureBuffer.cs
index 152757fb8..dbfb9ba0c 100644
--- a/src/ImageSharp/Formats/Heif/Hevc/HevcPictureBuffer.cs
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureBuffer.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Numerics;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
@@ -33,7 +34,8 @@ internal sealed class HevcPictureBuffer : IDisposable
sequenceParameterSet.BitDepthLuma,
sequenceParameterSet.BitDepthChroma,
sequenceParameterSet.ChromaFormat,
- sequenceParameterSet.SeparateColorPlaneFlag)
+ sequenceParameterSet.SeparateColorPlaneFlag,
+ 1 << sequenceParameterSet.MinCodingBlockLog2)
{
}
@@ -47,6 +49,7 @@ internal sealed class HevcPictureBuffer : IDisposable
/// The chroma sample precision.
/// The HEVC chroma-format identifier.
/// Whether 4:4:4 components are coded as separate color planes.
+ /// The luma sample alignment applied to the owned reconstruction planes.
public HevcPictureBuffer(
Configuration configuration,
int width,
@@ -54,7 +57,8 @@ internal sealed class HevcPictureBuffer : IDisposable
int bitDepthLuma,
int bitDepthChroma,
byte chromaFormat,
- bool separateColorPlane)
+ bool separateColorPlane,
+ int storageAlignment = 1)
{
this.Width = width;
this.Height = height;
@@ -66,11 +70,13 @@ internal sealed class HevcPictureBuffer : IDisposable
// Separate color planes are independently coded at full resolution even though chroma_format_idc is 4:4:4.
this.chromaSubsamplingX = !this.SeparateColorPlane && this.ChromaFormat is 1 or 2 ? 1 : 0;
this.chromaSubsamplingY = !this.SeparateColorPlane && this.ChromaFormat == 1 ? 1 : 0;
- this.Luma = configuration.MemoryAllocator.Allocate2D(this.Width, this.Height);
+ int storageWidth = DivideCeilingByPowerOfTwo(this.Width, BitOperations.Log2((uint)storageAlignment)) * storageAlignment;
+ int storageHeight = DivideCeilingByPowerOfTwo(this.Height, BitOperations.Log2((uint)storageAlignment)) * storageAlignment;
+ this.Luma = configuration.MemoryAllocator.Allocate2D(storageWidth, storageHeight);
if (this.ChromaFormat != 0)
{
- int chromaWidth = DivideCeilingByPowerOfTwo(this.Width, this.chromaSubsamplingX);
- int chromaHeight = DivideCeilingByPowerOfTwo(this.Height, this.chromaSubsamplingY);
+ int chromaWidth = DivideCeilingByPowerOfTwo(storageWidth, this.chromaSubsamplingX);
+ int chromaHeight = DivideCeilingByPowerOfTwo(storageHeight, this.chromaSubsamplingY);
this.ChromaBlue = configuration.MemoryAllocator.Allocate2D(chromaWidth, chromaHeight);
this.ChromaRed = configuration.MemoryAllocator.Allocate2D(chromaWidth, chromaHeight);
@@ -171,6 +177,25 @@ internal sealed class HevcPictureBuffer : IDisposable
_ => this.ChromaRed!.DangerousGetRowSpan(row),
};
+ ///
+ /// Copies the complete coded component planes to another picture buffer with the same dimensions and chroma layout.
+ ///
+ /// The destination picture buffer.
+ public void CopyTo(HevcPictureBuffer destination)
+ {
+ int planeCount = this.ChromaFormat == 0 ? 1 : 3;
+ for (int planeIndex = 0; planeIndex < planeCount; planeIndex++)
+ {
+ HevcPlane plane = (HevcPlane)planeIndex;
+ int width = this.GetWidth(plane);
+ int height = this.GetHeight(plane);
+ for (int row = 0; row < height; row++)
+ {
+ this.GetRowSpan(plane, row)[..width].CopyTo(destination.GetRowSpan(plane, row));
+ }
+ }
+ }
+
///
/// Releases the owned luma and chroma plane allocations.
///
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Deblocking.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Deblocking.cs
new file mode 100644
index 000000000..be2d11f66
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Deblocking.cs
@@ -0,0 +1,390 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
+
+///
+/// Implements picture-level HEVC deblocking traversal and threshold derivation.
+///
+internal sealed partial class HevcPictureDecoder
+{
+ ///
+ /// Defines the orientation-dependent boundary lookup and four-sample filter dispatch.
+ ///
+ private interface IDeblockingDirection
+ {
+ ///
+ /// Gets a value indicating whether the boundary is vertical.
+ ///
+ public static abstract bool IsVertical { get; }
+
+ ///
+ /// Gets whether the selected four-sample segment is a transform or prediction boundary.
+ ///
+ /// The decoded deblocking boundary state.
+ /// The primary coding plane.
+ /// The segment left luma coordinate.
+ /// The segment top luma coordinate.
+ /// when the segment is a filter candidate; otherwise, .
+ public static abstract bool IsBoundary(HevcDeblockingState state, HevcPlane plane, int x, int y);
+
+ ///
+ /// Applies the orientation-specific luma kernel.
+ ///
+ /// The reconstructed picture.
+ /// The component plane.
+ /// The first Q-side sample X coordinate.
+ /// The first Q-side sample Y coordinate.
+ /// The scaled discontinuity threshold.
+ /// The scaled clipping threshold.
+ /// Whether the P-side block retains its original samples.
+ /// Whether the Q-side block retains its original samples.
+ /// The component sample precision.
+ public static abstract void FilterLuma(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int beta,
+ int tc,
+ bool partPNoFilter,
+ bool partQNoFilter,
+ int bitDepth);
+
+ ///
+ /// Applies the orientation-specific chroma kernel.
+ ///
+ /// The reconstructed picture.
+ /// The Cb or Cr component plane.
+ /// The first Q-side sample X coordinate.
+ /// The first Q-side sample Y coordinate.
+ /// The scaled clipping threshold.
+ /// Whether the P-side block retains its original samples.
+ /// Whether the Q-side block retains its original samples.
+ /// The component sample precision.
+ /// The number of samples in the edge segment.
+ public static abstract void FilterChroma(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int tc,
+ bool partPNoFilter,
+ bool partQNoFilter,
+ int bitDepth,
+ int count);
+ }
+
+ ///
+ /// Gets the H.265 Table 8-20 clipping thresholds indexed by the effective boundary quantization parameter.
+ ///
+ private static ReadOnlySpan DeblockingTcTable =>
+ [
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4,
+ 4, 4, 5, 5, 6, 6, 7, 8, 9, 10, 11, 13, 14, 16, 18, 20, 22, 24,
+ ];
+
+ ///
+ /// Gets the H.265 Table 8-20 discontinuity thresholds indexed by the effective boundary quantization parameter.
+ ///
+ private static ReadOnlySpan DeblockingBetaTable =>
+ [
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 22, 24, 26,
+ 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64,
+ ];
+
+ ///
+ /// Applies vertical edges across the complete picture before applying any horizontal edge.
+ ///
+ /// The picture tile mapping.
+ private void ApplyDeblockingFilter(in HevcTileLayout tileLayout)
+ {
+ this.ApplyDeblockingDirection(in tileLayout);
+ this.ApplyDeblockingDirection(in tileLayout);
+ }
+
+ ///
+ /// Applies one closed deblocking direction to every coded component plane.
+ ///
+ /// The vertical or horizontal boundary operator.
+ /// The picture tile mapping.
+ private void ApplyDeblockingDirection(in HevcTileLayout tileLayout)
+ where TDirection : struct, IDeblockingDirection
+ {
+ if (this.sequenceParameterSet.SeparateColorPlaneFlag)
+ {
+ for (int planeIndex = 0; planeIndex < 3; planeIndex++)
+ {
+ this.ApplyLumaDeblocking((HevcPlane)planeIndex, planeIndex, in tileLayout);
+ }
+
+ return;
+ }
+
+ this.ApplyLumaDeblocking(HevcPlane.Y, 0, in tileLayout);
+ if (this.sequenceParameterSet.ChromaFormat != 0)
+ {
+ this.ApplyChromaDeblocking(HevcPlane.Cb, in tileLayout);
+ this.ApplyChromaDeblocking(HevcPlane.Cr, in tileLayout);
+ }
+ }
+
+ ///
+ /// Applies one deblocking direction with the luma kernel to a primary coded plane.
+ ///
+ /// The vertical or horizontal boundary operator.
+ /// The primary coded plane.
+ /// The coding-tree state selected for the plane.
+ /// The picture tile mapping.
+ private void ApplyLumaDeblocking(HevcPlane plane, int codingTreeStateIndex, in HevcTileLayout tileLayout)
+ where TDirection : struct, IDeblockingDirection
+ {
+ int width = this.Picture.GetWidth(plane);
+ int height = this.Picture.GetHeight(plane);
+ int acrossLimit = TDirection.IsVertical ? width : height;
+ int alongLimit = TDirection.IsVertical ? height : width;
+ int bitDepth = this.Picture.GetBitDepth(plane);
+ int bitDepthScale = 1 << (bitDepth - 8);
+ int codingTreeBlockSize = 1 << this.sequenceParameterSet.CodingTreeBlockLog2;
+ HevcCodingTreeState codingTreeState = this.codingTreeStates[codingTreeStateIndex];
+
+ // Deblocking visits only eight-sample grid lines, but each candidate is retained at four-sample resolution
+ // because transform and prediction boundaries can differ between the two halves of that grid interval.
+ for (int edge = 8; edge < acrossLimit; edge += 8)
+ {
+ for (int along = 0; along < alongLimit; along += 4)
+ {
+ int x = TDirection.IsVertical ? edge : along;
+ int y = TDirection.IsVertical ? along : edge;
+ if (!TDirection.IsBoundary(this.deblockingState, plane, x, y))
+ {
+ continue;
+ }
+
+ int rasterAddress = ((y / codingTreeBlockSize) * tileLayout.Width) + (x / codingTreeBlockSize);
+ HevcLoopFilterRegion region = this.sampleAdaptiveOffsetState.GetLoopFilterRegion(rasterAddress, plane);
+ if (region.DeblockingFilterDisabled
+ || !this.IsDeblockingCtbBoundaryAvailable(rasterAddress, plane, x, y, codingTreeBlockSize, in tileLayout))
+ {
+ continue;
+ }
+
+ int pX = x - (TDirection.IsVertical ? 1 : 0);
+ int pY = y - (TDirection.IsVertical ? 0 : 1);
+ int qX = x;
+ int qY = y;
+ int quantizationParameterP = codingTreeState.GetQuantizationParameter(pX, pY);
+ int quantizationParameterQ = codingTreeState.GetQuantizationParameter(qX, qY);
+ int averageQuantizationParameter = (quantizationParameterP + quantizationParameterQ + 1) >> 1;
+ int tcIndex = Math.Clamp(averageQuantizationParameter + 2 + (region.DeblockingFilterTcOffsetDiv2 << 1), 0, 53);
+ int betaIndex = Math.Clamp(averageQuantizationParameter + (region.DeblockingFilterBetaOffsetDiv2 << 1), 0, 51);
+ int tc = DeblockingTcTable[tcIndex] * bitDepthScale;
+ int beta = DeblockingBetaTable[betaIndex] * bitDepthScale;
+ bool partPNoFilter = this.IsDeblockingSuppressed(codingTreeState, pX, pY);
+ bool partQNoFilter = this.IsDeblockingSuppressed(codingTreeState, qX, qY);
+
+ TDirection.FilterLuma(this.Picture, plane, x, y, beta, tc, partPNoFilter, partQNoFilter, bitDepth);
+ }
+ }
+ }
+
+ ///
+ /// Applies one deblocking direction with the chroma kernel to a combined Cb or Cr plane.
+ ///
+ /// The vertical or horizontal boundary operator.
+ /// The Cb or Cr component plane.
+ /// The picture tile mapping.
+ private void ApplyChromaDeblocking(HevcPlane plane, in HevcTileLayout tileLayout)
+ where TDirection : struct, IDeblockingDirection
+ {
+ int subsamplingX = this.Picture.GetSubsamplingX(plane);
+ int subsamplingY = this.Picture.GetSubsamplingY(plane);
+ int width = this.Picture.GetWidth(plane);
+ int height = this.Picture.GetHeight(plane);
+ int acrossLimit = TDirection.IsVertical ? width : height;
+ int alongLimit = TDirection.IsVertical ? height : width;
+ int alongSubsampling = TDirection.IsVertical ? subsamplingY : subsamplingX;
+ int segmentLength = 4 >> alongSubsampling;
+ int bitDepth = this.Picture.GetBitDepth(plane);
+ int bitDepthScale = 1 << (bitDepth - 8);
+ int codingTreeBlockSize = 1 << this.sequenceParameterSet.CodingTreeBlockLog2;
+ HevcCodingTreeState codingTreeState = this.codingTreeStates[0];
+
+ // Chroma deblocking uses eight-sample component-grid edges. A two-lane segment in subsampled directions still
+ // enters the SIMD kernel, but only its valid low lanes are committed because QP and suppression state can change next.
+ for (int edge = 8; edge < acrossLimit; edge += 8)
+ {
+ for (int along = 0; along < alongLimit; along += segmentLength)
+ {
+ int x = TDirection.IsVertical ? edge : along;
+ int y = TDirection.IsVertical ? along : edge;
+ int lumaX = x << subsamplingX;
+ int lumaY = y << subsamplingY;
+ if (!TDirection.IsBoundary(this.deblockingState, HevcPlane.Y, lumaX, lumaY))
+ {
+ continue;
+ }
+
+ int rasterAddress = ((lumaY / codingTreeBlockSize) * tileLayout.Width) + (lumaX / codingTreeBlockSize);
+ HevcLoopFilterRegion region = this.sampleAdaptiveOffsetState.GetLoopFilterRegion(rasterAddress, HevcPlane.Y);
+ if (region.DeblockingFilterDisabled
+ || !this.IsDeblockingCtbBoundaryAvailable(
+ rasterAddress,
+ HevcPlane.Y,
+ lumaX,
+ lumaY,
+ codingTreeBlockSize,
+ in tileLayout))
+ {
+ continue;
+ }
+
+ int pX = lumaX - (TDirection.IsVertical ? 1 : 0);
+ int pY = lumaY - (TDirection.IsVertical ? 0 : 1);
+ int qX = lumaX;
+ int qY = lumaY;
+ int quantizationParameterP = codingTreeState.GetQuantizationParameter(pX, pY);
+ int quantizationParameterQ = codingTreeState.GetQuantizationParameter(qX, qY);
+ int averageQuantizationParameter = (quantizationParameterP + quantizationParameterQ + 1) >> 1;
+ int componentOffset = codingTreeState.GetChromaQuantizationOffset(plane, qX, qY);
+ int chromaQuantizationParameter = HevcQuantizationParameters.GetChromaQuantizationParameter(
+ averageQuantizationParameter,
+ componentOffset,
+ 0,
+ this.sequenceParameterSet.ChromaFormat);
+
+ int tcIndex = Math.Clamp(chromaQuantizationParameter + 2 + (region.DeblockingFilterTcOffsetDiv2 << 1), 0, 53);
+ int tc = DeblockingTcTable[tcIndex] * bitDepthScale;
+ bool partPNoFilter = this.IsDeblockingSuppressed(codingTreeState, pX, pY);
+ bool partQNoFilter = this.IsDeblockingSuppressed(codingTreeState, qX, qY);
+
+ TDirection.FilterChroma(this.Picture, plane, x, y, tc, partPNoFilter, partQNoFilter, bitDepth, segmentLength);
+ }
+ }
+ }
+
+ ///
+ /// Gets whether an edge crossing a coding-tree-block boundary is permitted by slice and tile rules.
+ ///
+ /// The vertical or horizontal boundary operator.
+ /// The Q-side coding-tree-block raster address.
+ /// The primary coding plane.
+ /// The edge luma X coordinate.
+ /// The edge luma Y coordinate.
+ /// The coding-tree-block side in luma samples.
+ /// The picture tile mapping.
+ /// for an internal or permitted external boundary; otherwise, .
+ private bool IsDeblockingCtbBoundaryAvailable(
+ int rasterAddress,
+ HevcPlane plane,
+ int x,
+ int y,
+ int codingTreeBlockSize,
+ in HevcTileLayout tileLayout)
+ where TDirection : struct, IDeblockingDirection
+ {
+ int acrossCoordinate = TDirection.IsVertical ? x : y;
+ if (acrossCoordinate % codingTreeBlockSize != 0)
+ {
+ return true;
+ }
+
+ HevcLoopFilterBoundaryAvailability availability = this.sampleAdaptiveOffsetState.GetLoopFilterBoundaryAvailability(
+ rasterAddress,
+ plane,
+ tileLayout.Width,
+ tileLayout.Height,
+ this.pictureParameterSet.LoopFilterAcrossTilesEnabled);
+
+ return TDirection.IsVertical ? availability.Left : availability.Above;
+ }
+
+ ///
+ /// Gets whether PCM or transform-bypass syntax preserves one side of a filtered boundary.
+ ///
+ /// The coding-tree state for the selected primary plane.
+ /// The luma sample X coordinate.
+ /// The luma sample Y coordinate.
+ /// when the reconstructed side must not be modified; otherwise, .
+ private bool IsDeblockingSuppressed(HevcCodingTreeState state, int x, int y)
+ => (this.sequenceParameterSet.PcmLoopFilterDisabled && state.IsPcm(x, y))
+ || (this.pictureParameterSet.TransquantizationBypassEnabled && state.IsTransquantBypass(x, y));
+
+ ///
+ /// Selects vertical boundary lookup and filtering.
+ ///
+ private readonly struct VerticalDeblockingDirection : IDeblockingDirection
+ {
+ ///
+ public static bool IsVertical => true;
+
+ ///
+ public static bool IsBoundary(HevcDeblockingState state, HevcPlane plane, int x, int y)
+ => state.IsVerticalBoundary(plane, x, y);
+
+ ///
+ public static void FilterLuma(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int beta,
+ int tc,
+ bool partPNoFilter,
+ bool partQNoFilter,
+ int bitDepth)
+ => HevcDeblockingFilter.FilterVerticalLuma(picture, plane, x, y, beta, tc, partPNoFilter, partQNoFilter, bitDepth);
+
+ ///
+ public static void FilterChroma(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int tc,
+ bool partPNoFilter,
+ bool partQNoFilter,
+ int bitDepth,
+ int count)
+ => HevcDeblockingFilter.FilterVerticalChroma(picture, plane, x, y, tc, partPNoFilter, partQNoFilter, bitDepth, count);
+ }
+
+ ///
+ /// Selects horizontal boundary lookup and filtering.
+ ///
+ private readonly struct HorizontalDeblockingDirection : IDeblockingDirection
+ {
+ ///
+ public static bool IsVertical => false;
+
+ ///
+ public static bool IsBoundary(HevcDeblockingState state, HevcPlane plane, int x, int y)
+ => state.IsHorizontalBoundary(plane, x, y);
+
+ ///
+ public static void FilterLuma(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int beta,
+ int tc,
+ bool partPNoFilter,
+ bool partQNoFilter,
+ int bitDepth)
+ => HevcDeblockingFilter.FilterHorizontalLuma(picture, plane, x, y, beta, tc, partPNoFilter, partQNoFilter, bitDepth);
+
+ ///
+ public static void FilterChroma(
+ HevcPictureBuffer picture,
+ HevcPlane plane,
+ int x,
+ int y,
+ int tc,
+ bool partPNoFilter,
+ bool partQNoFilter,
+ int bitDepth,
+ int count)
+ => HevcDeblockingFilter.FilterHorizontalChroma(picture, plane, x, y, tc, partPNoFilter, partQNoFilter, bitDepth, count);
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Prediction.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Prediction.cs
new file mode 100644
index 000000000..d8bc059df
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Prediction.cs
@@ -0,0 +1,216 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
+
+///
+/// Implements intra prediction, reconstructed-plane writes, and PCM sample reconstruction.
+///
+internal sealed partial class HevcPictureDecoder
+{
+ ///
+ /// Reconstructs one packed intra-prediction block in caller-owned scratch.
+ ///
+ /// The reconstructed component plane.
+ /// The prediction-block left coordinate in component samples.
+ /// The prediction-block top coordinate in component samples.
+ /// The base-two logarithm of the square prediction-block side.
+ /// The current independent-slice and tile prediction region.
+ /// The selected separate-color plane, or zero for combined coding.
+ /// The packed predicted samples.
+ private Span PredictComponentBlock(HevcPlane plane, int x, int y, int log2Size, int regionId, int colorPlaneIndex)
+ {
+ int size = 1 << log2Size;
+ int sampleCount = size * size;
+ int referenceLength = (size * 2) + 1;
+ Span scratch = this.predictionScratch.Memory.Span;
+ Span prediction = scratch[..sampleCount];
+ Span top = scratch.Slice(MaximumTransformSampleCount, MaximumReferenceLength);
+ Span left = scratch.Slice(MaximumTransformSampleCount + MaximumReferenceLength, MaximumReferenceLength);
+ Span filteredTop = scratch.Slice(MaximumTransformSampleCount + (MaximumReferenceLength * 2), MaximumReferenceLength);
+ Span filteredLeft = scratch.Slice(MaximumTransformSampleCount + (MaximumReferenceLength * 3), MaximumReferenceLength);
+ int referenceScratchOffset = MaximumTransformSampleCount + (MaximumReferenceLength * 4);
+ int unitWidth = this.reconstructionState.GetUnitWidth(plane);
+ int unitHeight = this.reconstructionState.GetUnitHeight(plane);
+ int referenceScratchLength = HevcIntraPredictor.GetReferenceScratchLength(log2Size, unitWidth);
+ Span referenceScratch = scratch.Slice(referenceScratchOffset, referenceScratchLength);
+ Span operationScratch = scratch[(referenceScratchOffset + referenceScratchLength)..];
+ Span availability = this.availabilityScratch.Memory.Span;
+ int availabilityCount = this.reconstructionState.BuildReferenceAvailability(
+ plane,
+ x,
+ y,
+ log2Size,
+ regionId,
+ availability);
+
+ HevcIntraPredictor.PrepareReferenceSamples(
+ this.Picture,
+ plane,
+ x,
+ y,
+ log2Size,
+ unitWidth,
+ unitHeight,
+ availability[..availabilityCount],
+ top,
+ left,
+ referenceScratch);
+
+ int lumaX = x << this.Picture.GetSubsamplingX(plane);
+ int lumaY = y << this.Picture.GetSubsamplingY(plane);
+ bool useLumaSyntax = plane == HevcPlane.Y || this.sequenceParameterSet.SeparateColorPlaneFlag;
+ int mode = useLumaSyntax
+ ? this.intraPredictionStates[colorPlaneIndex].GetLumaMode(lumaX, lumaY)
+ : this.intraPredictionStates[colorPlaneIndex].GetEffectiveChromaMode(lumaX, lumaY);
+
+ if (!useLumaSyntax && this.sequenceParameterSet.ChromaFormat == 2)
+ {
+ mode = HevcIntraPredictionMode.RemapChroma422(mode);
+ }
+
+ bool filterReferences = HevcIntraPredictor.ShouldFilterReferenceSamples(
+ useLumaSyntax ? HevcPlane.Y : plane,
+ mode,
+ log2Size,
+ this.sequenceParameterSet.ChromaFormat,
+ this.sequenceParameterSet.IntraSmoothingDisabled);
+
+ ReadOnlySpan selectedTop = top[..referenceLength];
+ ReadOnlySpan selectedLeft = left[..referenceLength];
+ if (filterReferences)
+ {
+ HevcIntraPredictor.FilterReferenceSamples(
+ selectedTop,
+ selectedLeft,
+ filteredTop,
+ filteredLeft,
+ log2Size,
+ this.Picture.GetBitDepth(plane),
+ this.sequenceParameterSet.StrongIntraSmoothingEnabled);
+
+ selectedTop = filteredTop[..referenceLength];
+ selectedLeft = filteredLeft[..referenceLength];
+ }
+
+ HevcIntraPredictor.Predict(
+ selectedTop,
+ selectedLeft,
+ prediction,
+ size,
+ log2Size,
+ mode,
+ this.Picture.GetBitDepth(plane),
+ useLumaSyntax,
+ operationScratch);
+
+ return prediction;
+ }
+
+ ///
+ /// Copies one packed reconstructed block into the allocator-owned picture plane.
+ ///
+ /// The packed reconstructed samples.
+ /// The destination component plane.
+ /// The destination left coordinate.
+ /// The destination top coordinate.
+ /// The square block side.
+ private void CopyPredictionToPicture(ReadOnlySpan source, HevcPlane plane, int x, int y, int size)
+ {
+ for (int row = 0; row < size; row++)
+ {
+ source.Slice(row * size, size).CopyTo(this.Picture.GetRowSpan(plane, y + row)[x..]);
+ }
+ }
+
+ ///
+ /// Reads and writes every raw sample in one PCM coding unit before arithmetic decoding restarts.
+ ///
+ /// The suspended entropy-substream reader.
+ /// The coding-unit left luma coordinate.
+ /// The coding-unit top luma coordinate.
+ /// The base-two logarithm of the coding-unit side.
+ /// The current independent-slice and tile prediction region.
+ /// The selected separate-color plane, or zero for combined coding.
+ private void DecodePcmCodingUnit(
+ ref HevcCabacSyntaxReader reader,
+ int x,
+ int y,
+ int log2Size,
+ int regionId,
+ int colorPlaneIndex)
+ {
+ int size = 1 << log2Size;
+ if (this.sequenceParameterSet.SeparateColorPlaneFlag)
+ {
+ HevcPlane plane = (HevcPlane)colorPlaneIndex;
+ this.DecodePcmPlane(ref reader, plane, x, y, size, size, this.sequenceParameterSet.PcmBitDepthLuma, regionId);
+ return;
+ }
+
+ this.DecodePcmPlane(ref reader, HevcPlane.Y, x, y, size, size, this.sequenceParameterSet.PcmBitDepthLuma, regionId);
+ if (this.sequenceParameterSet.ChromaFormat == 0)
+ {
+ return;
+ }
+
+ int subsamplingX = this.Picture.GetSubsamplingX(HevcPlane.Cb);
+ int subsamplingY = this.Picture.GetSubsamplingY(HevcPlane.Cb);
+ int chromaWidth = size >> subsamplingX;
+ int chromaHeight = size >> subsamplingY;
+ int chromaX = x >> subsamplingX;
+ int chromaY = y >> subsamplingY;
+ this.DecodePcmPlane(
+ ref reader,
+ HevcPlane.Cb,
+ chromaX,
+ chromaY,
+ chromaWidth,
+ chromaHeight,
+ this.sequenceParameterSet.PcmBitDepthChroma,
+ regionId);
+
+ this.DecodePcmPlane(
+ ref reader,
+ HevcPlane.Cr,
+ chromaX,
+ chromaY,
+ chromaWidth,
+ chromaHeight,
+ this.sequenceParameterSet.PcmBitDepthChroma,
+ regionId);
+ }
+
+ ///
+ /// Reads one rectangular PCM component plane directly into the reconstructed picture.
+ ///
+ /// The suspended entropy-substream reader.
+ /// The destination component plane.
+ /// The destination left coordinate.
+ /// The destination top coordinate.
+ /// The component rectangle width.
+ /// The component rectangle height.
+ /// The PCM sample precision.
+ /// The current independent-slice and tile prediction region.
+ private void DecodePcmPlane(
+ ref HevcCabacSyntaxReader reader,
+ HevcPlane plane,
+ int x,
+ int y,
+ int width,
+ int height,
+ int bitDepth,
+ int regionId)
+ {
+ for (int row = 0; row < height; row++)
+ {
+ Span destination = this.Picture.GetRowSpan(plane, y + row).Slice(x, width);
+ for (int column = 0; column < width; column++)
+ {
+ destination[column] = reader.ReadPcmSample(bitDepth);
+ }
+ }
+
+ this.reconstructionState.MarkReconstructed(plane, x, y, width, height, regionId);
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.SampleAdaptiveOffset.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.SampleAdaptiveOffset.cs
new file mode 100644
index 000000000..358614382
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.SampleAdaptiveOffset.cs
@@ -0,0 +1,250 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
+
+///
+/// Implements sample-adaptive-offset syntax decoding and merge resolution.
+///
+internal sealed partial class HevcPictureDecoder
+{
+ ///
+ /// Applies the resolved sample-adaptive offsets to every component after deblocking has completed.
+ ///
+ /// The immutable deblocked picture used to classify every sample.
+ /// The picture tile mapping used to derive coding-tree-block boundaries.
+ private void ApplySampleAdaptiveOffset(HevcPictureBuffer source, in HevcTileLayout tileLayout)
+ {
+ int codingTreeBlockSize = 1 << this.sequenceParameterSet.CodingTreeBlockLog2;
+ int planeCount = this.sequenceParameterSet.ChromaFormat == 0 ? 1 : 3;
+ for (int planeIndex = 0; planeIndex < planeCount; planeIndex++)
+ {
+ HevcPlane plane = (HevcPlane)planeIndex;
+ HevcPlane regionPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? plane : HevcPlane.Y;
+ int subsamplingX = this.Picture.GetSubsamplingX(plane);
+ int subsamplingY = this.Picture.GetSubsamplingY(plane);
+ int blockWidth = codingTreeBlockSize >> subsamplingX;
+ int blockHeight = codingTreeBlockSize >> subsamplingY;
+ int planeWidth = this.Picture.GetWidth(plane);
+ int planeHeight = this.Picture.GetHeight(plane);
+ int offsetScaleLog2 = plane == HevcPlane.Y
+ ? this.pictureParameterSet.SampleAdaptiveOffsetScaleLumaLog2
+ : this.pictureParameterSet.SampleAdaptiveOffsetScaleChromaLog2;
+
+ for (int codingTreeBlockY = 0; codingTreeBlockY < tileLayout.Height; codingTreeBlockY++)
+ {
+ for (int codingTreeBlockX = 0; codingTreeBlockX < tileLayout.Width; codingTreeBlockX++)
+ {
+ int rasterAddress = (codingTreeBlockY * tileLayout.Width) + codingTreeBlockX;
+ HevcSampleAdaptiveOffsetParameters parameters = this.sampleAdaptiveOffsetState.Get(rasterAddress, plane);
+ if (parameters.Type == HevcSampleAdaptiveOffsetType.Off)
+ {
+ continue;
+ }
+
+ HevcLoopFilterBoundaryAvailability availability = this.sampleAdaptiveOffsetState.GetLoopFilterBoundaryAvailability(
+ rasterAddress,
+ regionPlane,
+ tileLayout.Width,
+ tileLayout.Height,
+ this.pictureParameterSet.LoopFilterAcrossTilesEnabled);
+
+ int x = codingTreeBlockX * blockWidth;
+ int y = codingTreeBlockY * blockHeight;
+ int width = Math.Min(blockWidth, planeWidth - x);
+ int height = Math.Min(blockHeight, planeHeight - y);
+
+ // Every classification reads the immutable post-deblocking picture. Later CTBs can therefore never
+ // observe offsets already written by an earlier CTB, including across permitted slice and tile boundaries.
+ HevcSampleAdaptiveOffsetFilter.ApplyBlock(
+ source,
+ this.Picture,
+ plane,
+ x,
+ y,
+ width,
+ height,
+ in parameters,
+ offsetScaleLog2,
+ availability.Left,
+ availability.Right,
+ availability.Above,
+ availability.Below,
+ availability.AboveLeft,
+ availability.AboveRight,
+ availability.BelowLeft,
+ availability.BelowRight);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Decodes and resolves the sample-adaptive-offset parameters for one coding-tree block.
+ ///
+ /// The active entropy-substream reader.
+ /// The independent slice governing component enable flags.
+ /// The coding-tree block's raster-scan address.
+ /// The horizontal coding-tree-block coordinate.
+ /// The vertical coding-tree-block coordinate.
+ /// The current independent-slice and tile prediction region.
+ private void DecodeSampleAdaptiveOffset(
+ ref HevcCabacSyntaxReader reader,
+ HevcSliceSegmentHeader independentSlice,
+ int rasterAddress,
+ int codingTreeBlockX,
+ int codingTreeBlockY,
+ int regionId)
+ {
+ HevcPlane regionPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? (HevcPlane)independentSlice.ColorPlaneId : HevcPlane.Y;
+ bool lumaEnabled = independentSlice.SampleAdaptiveOffsetLumaEnabled == true;
+ bool chromaEnabled = independentSlice.SampleAdaptiveOffsetChromaEnabled == true;
+ if (!lumaEnabled && !chromaEnabled)
+ {
+ this.sampleAdaptiveOffsetState.SetRegion(rasterAddress, regionPlane, regionId);
+ return;
+ }
+
+ int codingTreeBlockWidth = HevcParameterSetSyntax.GetCodingTreeBlockCount(
+ this.sequenceParameterSet.Width,
+ this.sequenceParameterSet.CodingTreeBlockLog2);
+
+ int leftAddress = rasterAddress - 1;
+ bool leftAvailable = codingTreeBlockX > 0 && this.sampleAdaptiveOffsetState.IsInRegion(leftAddress, regionPlane, regionId);
+ bool mergeLeft = leftAvailable && reader.ReadSampleAdaptiveOffsetMerge();
+ int aboveAddress = rasterAddress - codingTreeBlockWidth;
+ bool aboveAvailable = codingTreeBlockY > 0 && this.sampleAdaptiveOffsetState.IsInRegion(aboveAddress, regionPlane, regionId);
+ bool mergeAbove = !mergeLeft && aboveAvailable && reader.ReadSampleAdaptiveOffsetMerge();
+ if (mergeLeft || mergeAbove)
+ {
+ int sourceAddress = mergeLeft ? leftAddress : aboveAddress;
+ this.CopySampleAdaptiveOffsetParameters(sourceAddress, rasterAddress, lumaEnabled, chromaEnabled, independentSlice.ColorPlaneId);
+ this.sampleAdaptiveOffsetState.SetRegion(rasterAddress, regionPlane, regionId);
+ return;
+ }
+
+ if (this.sequenceParameterSet.SeparateColorPlaneFlag)
+ {
+ HevcPlane plane = (HevcPlane)independentSlice.ColorPlaneId;
+ this.sampleAdaptiveOffsetState.Set(rasterAddress, plane, ReadSampleAdaptiveOffsetParameters(ref reader, this.Picture.GetBitDepth(plane), -1));
+ }
+ else
+ {
+ if (lumaEnabled)
+ {
+ this.sampleAdaptiveOffsetState.Set(
+ rasterAddress,
+ HevcPlane.Y,
+ ReadSampleAdaptiveOffsetParameters(ref reader, this.sequenceParameterSet.BitDepthLuma, -1));
+ }
+
+ if (chromaEnabled)
+ {
+ HevcSampleAdaptiveOffsetParameters chromaBlue = ReadSampleAdaptiveOffsetParameters(
+ ref reader,
+ this.sequenceParameterSet.BitDepthChroma,
+ -1);
+
+ this.sampleAdaptiveOffsetState.Set(rasterAddress, HevcPlane.Cb, chromaBlue);
+ this.sampleAdaptiveOffsetState.Set(
+ rasterAddress,
+ HevcPlane.Cr,
+ ReadSampleAdaptiveOffsetParameters(ref reader, this.sequenceParameterSet.BitDepthChroma, (int)chromaBlue.Type));
+ }
+ }
+
+ this.sampleAdaptiveOffsetState.SetRegion(rasterAddress, regionPlane, regionId);
+ }
+
+ ///
+ /// Copies resolved merge-source parameters for the components enabled by the current slice.
+ ///
+ /// The merge-source coding-tree-block address.
+ /// The current coding-tree-block address.
+ /// Whether the current slice enables luma sample-adaptive offset.
+ /// Whether the current slice enables chroma sample-adaptive offset.
+ /// The selected separate-color-plane identifier.
+ private void CopySampleAdaptiveOffsetParameters(
+ int sourceAddress,
+ int destinationAddress,
+ bool lumaEnabled,
+ bool chromaEnabled,
+ byte colorPlaneId)
+ {
+ if (this.sequenceParameterSet.SeparateColorPlaneFlag)
+ {
+ HevcPlane plane = (HevcPlane)colorPlaneId;
+ this.sampleAdaptiveOffsetState.Set(destinationAddress, plane, this.sampleAdaptiveOffsetState.Get(sourceAddress, plane));
+ return;
+ }
+
+ if (lumaEnabled)
+ {
+ this.sampleAdaptiveOffsetState.Set(destinationAddress, HevcPlane.Y, this.sampleAdaptiveOffsetState.Get(sourceAddress, HevcPlane.Y));
+ }
+
+ if (chromaEnabled)
+ {
+ this.sampleAdaptiveOffsetState.Set(destinationAddress, HevcPlane.Cb, this.sampleAdaptiveOffsetState.Get(sourceAddress, HevcPlane.Cb));
+ this.sampleAdaptiveOffsetState.Set(destinationAddress, HevcPlane.Cr, this.sampleAdaptiveOffsetState.Get(sourceAddress, HevcPlane.Cr));
+ }
+ }
+
+ ///
+ /// Decodes one component's new or disabled sample-adaptive-offset mode.
+ ///
+ /// The active entropy-substream reader.
+ /// The component sample precision.
+ /// The Cb type inherited by Cr, or negative one when the type is signaled.
+ /// The resolved component parameters.
+ private static HevcSampleAdaptiveOffsetParameters ReadSampleAdaptiveOffsetParameters(
+ ref HevcCabacSyntaxReader reader,
+ int bitDepth,
+ int inheritedType)
+ {
+ int type = inheritedType >= 0
+ ? inheritedType == (int)HevcSampleAdaptiveOffsetType.Off ? 0 : inheritedType == (int)HevcSampleAdaptiveOffsetType.Band ? 1 : 2
+ : reader.ReadSampleAdaptiveOffsetType();
+
+ if (type == 0)
+ {
+ return default;
+ }
+
+ int maximumOffset = (1 << (Math.Min(bitDepth, 10) - 5)) - 1;
+ int offset0 = reader.ReadSampleAdaptiveOffsetAbsolute(maximumOffset);
+ int offset1 = reader.ReadSampleAdaptiveOffsetAbsolute(maximumOffset);
+ int offset2 = reader.ReadSampleAdaptiveOffsetAbsolute(maximumOffset);
+ int offset3 = reader.ReadSampleAdaptiveOffsetAbsolute(maximumOffset);
+ if (type == 1)
+ {
+ offset0 = ApplySampleAdaptiveOffsetSign(ref reader, offset0);
+ offset1 = ApplySampleAdaptiveOffsetSign(ref reader, offset1);
+ offset2 = ApplySampleAdaptiveOffsetSign(ref reader, offset2);
+ offset3 = ApplySampleAdaptiveOffsetSign(ref reader, offset3);
+ return new HevcSampleAdaptiveOffsetParameters(
+ HevcSampleAdaptiveOffsetType.Band,
+ reader.ReadSampleAdaptiveOffsetBandPosition(),
+ offset0,
+ offset1,
+ offset2,
+ offset3,
+ 0);
+ }
+
+ HevcSampleAdaptiveOffsetType edgeType = inheritedType >= 0
+ ? (HevcSampleAdaptiveOffsetType)inheritedType
+ : (HevcSampleAdaptiveOffsetType)((int)HevcSampleAdaptiveOffsetType.EdgeHorizontal + reader.ReadSampleAdaptiveOffsetEdgeClass());
+
+ return new HevcSampleAdaptiveOffsetParameters(edgeType, 0, offset0, offset1, 0, -offset2, -offset3);
+ }
+
+ ///
+ /// Applies an explicitly coded sign to a nonzero band-offset magnitude.
+ ///
+ /// The active entropy-substream reader.
+ /// The decoded unsigned magnitude.
+ /// The signed magnitude.
+ private static int ApplySampleAdaptiveOffsetSign(ref HevcCabacSyntaxReader reader, int magnitude)
+ => magnitude != 0 && reader.ReadSampleAdaptiveOffsetSign() ? -magnitude : magnitude;
+}
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.TransformTree.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.TransformTree.cs
new file mode 100644
index 000000000..88a081168
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.TransformTree.cs
@@ -0,0 +1,562 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
+
+///
+/// Implements transform-tree syntax, coefficient reconstruction, and intra sample reconstruction.
+///
+internal sealed partial class HevcPictureDecoder
+{
+ ///
+ /// Decodes and reconstructs one transform-tree node.
+ ///
+ /// The active entropy-substream reader.
+ /// The luma and component rectangles at this transform depth.
+ /// The transform depth relative to the coding-unit root.
+ /// The smallest luma transform permitted in the coding unit.
+ /// Whether the coding unit has four luma prediction partitions.
+ /// Whether the coding unit bypasses inverse quantization and transform.
+ /// The current independent-slice and tile prediction region.
+ /// The selected separate-color plane, or zero for combined coding.
+ /// The blue-difference coded-block flags inherited from the parent.
+ /// The red-difference coded-block flags inherited from the parent.
+ private void DecodeTransformTree(
+ ref HevcCabacSyntaxReader reader,
+ in HevcTransformUnitGeometry geometry,
+ int transformDepth,
+ int minimumTransformLog2,
+ bool usesNxNPartitions,
+ bool transquantBypass,
+ int regionId,
+ int colorPlaneIndex,
+ HevcCodedBlockFlags parentChromaBlueFlags,
+ HevcCodedBlockFlags parentChromaRedFlags)
+ {
+ int log2Size = geometry.Log2LumaSize;
+ HevcTransformComponentGeometry primaryGeometry = geometry.Primary;
+ HevcTransformComponentGeometry chromaBlueGeometry = geometry.ChromaBlue;
+ HevcTransformComponentGeometry chromaRedGeometry = geometry.ChromaRed;
+ bool split;
+ if (usesNxNPartitions && transformDepth == 0)
+ {
+ split = true;
+ }
+ else if (log2Size > this.sequenceParameterSet.MaxTransformBlockLog2)
+ {
+ split = true;
+ }
+ else if (log2Size == this.sequenceParameterSet.MinTransformBlockLog2 || log2Size == minimumTransformLog2)
+ {
+ split = false;
+ }
+ else
+ {
+ split = reader.ReadTransformSubdivision(log2Size);
+ }
+
+ HevcCodedBlockFlags chromaBlueFlags = parentChromaBlueFlags;
+ HevcCodedBlockFlags chromaRedFlags = parentChromaRedFlags;
+ if (geometry.HasCombinedChroma)
+ {
+ chromaBlueFlags = DecodeChromaCodedBlockFlags(
+ ref reader,
+ in chromaBlueGeometry,
+ transformDepth,
+ split,
+ parentChromaBlueFlags);
+
+ chromaRedFlags = DecodeChromaCodedBlockFlags(
+ ref reader,
+ in chromaRedGeometry,
+ transformDepth,
+ split,
+ parentChromaRedFlags);
+ }
+
+ if (split)
+ {
+ for (int child = 0; child < 4; child++)
+ {
+ HevcTransformUnitGeometry childGeometry = geometry.CreateChild(child);
+ this.DecodeTransformTree(
+ ref reader,
+ in childGeometry,
+ transformDepth + 1,
+ minimumTransformLog2,
+ usesNxNPartitions,
+ transquantBypass,
+ regionId,
+ colorPlaneIndex,
+ chromaBlueFlags,
+ chromaRedFlags);
+ }
+
+ return;
+ }
+
+ this.deblockingState.MarkBlock(
+ geometry.PrimaryPlane,
+ primaryGeometry.X,
+ primaryGeometry.Y,
+ primaryGeometry.Width,
+ primaryGeometry.Height);
+
+ HevcCodedBlockFlags primaryFlags = new(reader.ReadTransformCodedBlockFlag(false, transformDepth == 0 ? 1 : 0));
+ bool hasCodedResidual = primaryFlags.Any || chromaBlueFlags.Any || chromaRedFlags.Any;
+ if (hasCodedResidual && this.quantizationParameterDeltaPending)
+ {
+ this.ApplyQuantizationParameterDelta(reader.ReadDeltaQuantizationParameter());
+ this.quantizationParameterDeltaPending = false;
+ }
+
+ if ((chromaBlueFlags.Any || chromaRedFlags.Any)
+ && this.chromaQuantizationAdjustmentPending
+ && !transquantBypass)
+ {
+ this.currentChromaQuantizationAdjustment = reader.ReadChromaQuantizationAdjustment(
+ this.pictureParameterSet.ChromaQuantizationParameterOffsetsCb.Count);
+
+ this.chromaQuantizationAdjustmentPending = false;
+ }
+
+ HevcQuantizationParameters quantizationParameters = this.CreateQuantizationParameters();
+ Span lumaResidual = this.integerScratch.Memory.Span.Slice(MaximumTransformSampleCount * 3, MaximumTransformSampleCount);
+ lumaResidual.Clear();
+ this.DecodeComponentSections(
+ ref reader,
+ geometry.PrimaryPlane,
+ in primaryGeometry,
+ primaryFlags,
+ transquantBypass,
+ regionId,
+ colorPlaneIndex,
+ in quantizationParameters,
+ lumaResidual,
+ true,
+ 0,
+ in primaryGeometry);
+
+ if (!geometry.HasCombinedChroma)
+ {
+ return;
+ }
+
+ int chromaMode = this.intraPredictionStates[colorPlaneIndex].GetChromaMode(geometry.Primary.X, geometry.Primary.Y);
+ int chromaBlueAlpha = 0;
+ bool canPredictAcrossComponents = this.pictureParameterSet.CrossComponentPredictionEnabled
+ && primaryFlags.Any
+ && chromaMode == 36
+ && chromaBlueGeometry.Width == chromaBlueGeometry.Height;
+
+ if (canPredictAcrossComponents)
+ {
+ chromaBlueAlpha = reader.ReadCrossComponentPredictionScale(0);
+ }
+
+ this.DecodeComponentSections(
+ ref reader,
+ HevcPlane.Cb,
+ in chromaBlueGeometry,
+ chromaBlueFlags,
+ transquantBypass,
+ regionId,
+ colorPlaneIndex,
+ in quantizationParameters,
+ lumaResidual,
+ false,
+ chromaBlueAlpha,
+ in primaryGeometry);
+
+ int chromaRedAlpha = 0;
+ if (canPredictAcrossComponents)
+ {
+ // The Cr scale follows the complete Cb residual syntax. Reading both scales together changes every
+ // subsequent CABAC decision whenever Cb carries coefficients.
+ chromaRedAlpha = reader.ReadCrossComponentPredictionScale(1);
+ }
+
+ this.DecodeComponentSections(
+ ref reader,
+ HevcPlane.Cr,
+ in chromaRedGeometry,
+ chromaRedFlags,
+ transquantBypass,
+ regionId,
+ colorPlaneIndex,
+ in quantizationParameters,
+ lumaResidual,
+ false,
+ chromaRedAlpha,
+ in primaryGeometry);
+ }
+
+ ///
+ /// Decodes chroma coded-block flags at the highest transform level that owns the component rectangle.
+ ///
+ /// The active entropy-substream reader.
+ /// The current chroma component rectangle.
+ /// The luma transform depth.
+ /// Whether the current luma transform node subdivides.
+ /// The coded-block flags inherited from the parent transform node.
+ /// The flags governing the current component rectangle.
+ private static HevcCodedBlockFlags DecodeChromaCodedBlockFlags(
+ ref HevcCabacSyntaxReader reader,
+ in HevcTransformComponentGeometry geometry,
+ int transformDepth,
+ bool lumaSplit,
+ HevcCodedBlockFlags parentFlags)
+ {
+ if (!geometry.Process)
+ {
+ return parentFlags;
+ }
+
+ bool shouldDecode = transformDepth == 0 || (geometry.ProcessesAllQuadrants && parentFlags.Any);
+ if (!shouldDecode)
+ {
+ return parentFlags;
+ }
+
+ int context = transformDepth;
+ bool canQuadSplit = geometry.Width >= 8 && geometry.Height >= 8;
+ if (geometry.Width != geometry.Height && (!lumaSplit || !canQuadSplit))
+ {
+ bool first = reader.ReadTransformCodedBlockFlag(true, context);
+ bool second = reader.ReadTransformCodedBlockFlag(true, context);
+ return new HevcCodedBlockFlags(first, second);
+ }
+
+ return new HevcCodedBlockFlags(reader.ReadTransformCodedBlockFlag(true, context));
+ }
+
+ ///
+ /// Decodes one square component block or the two square sub-blocks of a rectangular 4:2:2 transform section.
+ ///
+ /// The active entropy-substream reader.
+ /// The reconstructed component plane.
+ /// The component rectangle.
+ /// The component coded-block flags.
+ /// Whether the coding unit bypasses inverse quantization and transform.
+ /// The current independent-slice and tile prediction region.
+ /// The selected separate-color plane, or zero for combined coding.
+ /// The effective component quantization parameters.
+ /// The current luma residual retained for cross-component prediction.
+ /// Whether reconstructed residuals are copied to .
+ /// The signed inverse cross-component prediction scale.
+ /// The luma transform rectangle governing cross-component residual addressing.
+ private void DecodeComponentSections(
+ ref HevcCabacSyntaxReader reader,
+ HevcPlane plane,
+ in HevcTransformComponentGeometry geometry,
+ HevcCodedBlockFlags codedBlockFlags,
+ bool transquantBypass,
+ int regionId,
+ int colorPlaneIndex,
+ in HevcQuantizationParameters quantizationParameters,
+ Span lumaResidual,
+ bool retainResidual,
+ int crossComponentAlpha,
+ in HevcTransformComponentGeometry lumaGeometry)
+ {
+ if (!geometry.Process)
+ {
+ return;
+ }
+
+ if (geometry.Width == geometry.Height)
+ {
+ this.DecodeComponentBlock(
+ ref reader,
+ plane,
+ geometry.X,
+ geometry.Y,
+ geometry.Width,
+ codedBlockFlags.First,
+ transquantBypass,
+ regionId,
+ colorPlaneIndex,
+ in quantizationParameters,
+ lumaResidual,
+ retainResidual,
+ crossComponentAlpha,
+ this.GetLumaResidualOffset(plane, geometry.X, geometry.Y, in lumaGeometry),
+ lumaGeometry.Width);
+
+ return;
+ }
+
+ int size = Math.Min(geometry.Width, geometry.Height);
+ int secondX = geometry.Width > geometry.Height ? geometry.X + size : geometry.X;
+ int secondY = geometry.Height > geometry.Width ? geometry.Y + size : geometry.Y;
+ this.DecodeComponentBlock(
+ ref reader,
+ plane,
+ geometry.X,
+ geometry.Y,
+ size,
+ codedBlockFlags.First,
+ transquantBypass,
+ regionId,
+ colorPlaneIndex,
+ in quantizationParameters,
+ lumaResidual,
+ retainResidual,
+ crossComponentAlpha,
+ this.GetLumaResidualOffset(plane, geometry.X, geometry.Y, in lumaGeometry),
+ lumaGeometry.Width);
+
+ this.DecodeComponentBlock(
+ ref reader,
+ plane,
+ secondX,
+ secondY,
+ size,
+ codedBlockFlags.Second,
+ transquantBypass,
+ regionId,
+ colorPlaneIndex,
+ in quantizationParameters,
+ lumaResidual,
+ retainResidual,
+ crossComponentAlpha,
+ this.GetLumaResidualOffset(plane, secondX, secondY, in lumaGeometry),
+ lumaGeometry.Width);
+ }
+
+ ///
+ /// Decodes, predicts, and reconstructs one square transform block.
+ ///
+ /// The active entropy-substream reader.
+ /// The reconstructed component plane.
+ /// The block left coordinate in component samples.
+ /// The block top coordinate in component samples.
+ /// The square transform-block side.
+ /// Whether coefficient syntax is present.
+ /// Whether the coding unit bypasses inverse quantization and transform.
+ /// The current independent-slice and tile prediction region.
+ /// The selected separate-color plane, or zero for combined coding.
+ /// The effective component quantization parameters.
+ /// The current luma residual retained for cross-component prediction.
+ /// Whether reconstructed residuals are copied to .
+ /// The signed inverse cross-component prediction scale.
+ /// The first colocated sample in the retained luma residual.
+ /// The retained luma residual row stride.
+ private void DecodeComponentBlock(
+ ref HevcCabacSyntaxReader reader,
+ HevcPlane plane,
+ int x,
+ int y,
+ int size,
+ bool codedBlockFlag,
+ bool transquantBypass,
+ int regionId,
+ int colorPlaneIndex,
+ in HevcQuantizationParameters quantizationParameters,
+ Span lumaResidual,
+ bool retainResidual,
+ int crossComponentAlpha,
+ int lumaResidualOffset,
+ int lumaResidualStride)
+ {
+ int log2Size = BitOperations.Log2((uint)size);
+ int sampleCount = size * size;
+ Span integerScratch = this.integerScratch.Memory.Span;
+ Span quantized = integerScratch[..MaximumTransformSampleCount];
+ Span dequantized = integerScratch.Slice(MaximumTransformSampleCount, MaximumTransformSampleCount);
+ Span residual = integerScratch.Slice(MaximumTransformSampleCount * 2, MaximumTransformSampleCount);
+ Span transformScratch = integerScratch.Slice(MaximumTransformSampleCount * 4, MaximumTransformSampleCount * 2);
+ Span prediction = this.PredictComponentBlock(plane, x, y, log2Size, regionId, colorPlaneIndex);
+ residual[..sampleCount].Clear();
+ bool useLumaSyntax = this.sequenceParameterSet.SeparateColorPlaneFlag;
+ HevcPlane codingPlane = useLumaSyntax ? HevcPlane.Y : plane;
+ int lumaX = x << this.Picture.GetSubsamplingX(plane);
+ int lumaY = y << this.Picture.GetSubsamplingY(plane);
+ int codingPredictionMode = plane == HevcPlane.Y || useLumaSyntax
+ ? this.intraPredictionStates[colorPlaneIndex].GetLumaMode(lumaX, lumaY)
+ : this.intraPredictionStates[colorPlaneIndex].GetEffectiveChromaMode(lumaX, lumaY);
+
+ int predictionMode = codingPredictionMode;
+ if (plane != HevcPlane.Y && !useLumaSyntax && this.sequenceParameterSet.ChromaFormat == 2)
+ {
+ predictionMode = HevcIntraPredictionMode.RemapChroma422(predictionMode);
+ }
+
+ bool transformSkip = codedBlockFlag
+ && !transquantBypass
+ && this.pictureParameterSet.TransformSkipEnabled
+ && log2Size <= this.pictureParameterSet.MaxTransformSkipBlockLog2
+ && reader.ReadTransformSkip(codingPlane != HevcPlane.Y);
+
+ HevcResidualDpcmMode residualDpcmMode = this.sequenceParameterSet.ImplicitResidualDpcmEnabled && (transformSkip || transquantBypass)
+ ? HevcResidualReconstructor.GetImplicitResidualDpcmMode(predictionMode, false)
+ : HevcResidualDpcmMode.None;
+
+ if (codedBlockFlag)
+ {
+ HevcCoefficientCodingParameters codingParameters = HevcCoefficientCodingParameters.Create(
+ this.pictureParameterSet,
+ size,
+ size,
+ plane,
+ true,
+ codingPredictionMode,
+ transformSkip,
+ transquantBypass,
+ residualDpcmMode,
+ useLumaSyntax);
+
+ this.coefficientDecoder.Decode(ref reader, quantized, in codingParameters);
+ bool rotate = HevcResidualReconstructor.IsNonTransformedResidualRotated(
+ this.sequenceParameterSet.TransformSkipRotationEnabled,
+ true,
+ size);
+
+ if (transquantBypass)
+ {
+ HevcResidualReconstructor.CopyBypassed(quantized[..sampleCount], residual, rotate);
+ }
+ else
+ {
+ int bitDepth = this.Picture.GetBitDepth(plane);
+ int maxTransformDynamicRange = this.sequenceParameterSet.GetMaxTransformDynamicRange(codingPlane);
+ int quantizationParameter = useLumaSyntax
+ ? quantizationParameters.Luma
+ : quantizationParameters.Get(plane);
+
+ HevcInverseQuantizer.Dequantize(
+ quantized,
+ dequantized,
+ log2Size,
+ bitDepth,
+ maxTransformDynamicRange,
+ quantizationParameter,
+ this.sequenceParameterSet.ScalingListEnabled,
+ this.pictureParameterSet.ScalingList,
+ codingPlane,
+ true,
+ transformSkip,
+ this.sequenceParameterSet.ExtendedPrecisionProcessingEnabled);
+
+ if (transformSkip)
+ {
+ HevcResidualReconstructor.ApplyTransformSkip(
+ dequantized,
+ residual,
+ size,
+ size,
+ bitDepth,
+ maxTransformDynamicRange,
+ log2Size,
+ this.sequenceParameterSet.ExtendedPrecisionProcessingEnabled,
+ rotate);
+ }
+ else
+ {
+ HevcInverseTransformer.Transform(
+ dequantized,
+ residual,
+ log2Size,
+ log2Size,
+ bitDepth,
+ maxTransformDynamicRange,
+ codingPlane == HevcPlane.Y && log2Size == 2,
+ transformScratch);
+ }
+ }
+
+ HevcResidualReconstructor.ApplyResidualDpcm(residual, size, size, residualDpcmMode);
+ }
+
+ if (crossComponentAlpha != 0)
+ {
+ for (int row = 0; row < size; row++)
+ {
+ HevcResidualReconstructor.ApplyCrossComponentPrediction(
+ lumaResidual.Slice(lumaResidualOffset + (row * lumaResidualStride), size),
+ residual.Slice(row * size, size),
+ size,
+ crossComponentAlpha,
+ this.sequenceParameterSet.BitDepthLuma - this.sequenceParameterSet.BitDepthChroma);
+ }
+ }
+
+ if (retainResidual)
+ {
+ for (int row = 0; row < size; row++)
+ {
+ residual.Slice(row * size, size).CopyTo(lumaResidual.Slice(lumaResidualOffset + (row * lumaResidualStride), size));
+ }
+ }
+
+ HevcInverseTransformer.AddResidual(
+ residual,
+ prediction,
+ size,
+ size,
+ size,
+ this.Picture.GetBitDepth(plane));
+
+ this.CopyPredictionToPicture(prediction, plane, x, y, size);
+ this.reconstructionState.MarkReconstructed(plane, x, y, size, size, regionId);
+ }
+
+ ///
+ /// Gets the packed luma-residual offset colocated with one component block.
+ ///
+ /// The component plane.
+ /// The component block left coordinate.
+ /// The component block top coordinate.
+ /// The governing luma transform rectangle.
+ /// The zero-based packed luma-residual offset.
+ private int GetLumaResidualOffset(HevcPlane plane, int x, int y, in HevcTransformComponentGeometry lumaGeometry)
+ {
+ int lumaX = x << this.Picture.GetSubsamplingX(plane);
+ int lumaY = y << this.Picture.GetSubsamplingY(plane);
+ return ((lumaY - lumaGeometry.Y) * lumaGeometry.Width) + lumaX - lumaGeometry.X;
+ }
+
+ ///
+ /// Applies the signed coding-unit luma quantization delta with bit-depth-dependent modular wrapping.
+ ///
+ /// The decoded signed delta.
+ private void ApplyQuantizationParameterDelta(int delta)
+ {
+ int bitDepthOffset = 6 * (this.sequenceParameterSet.BitDepthLuma - 8);
+ int modulus = 52 + bitDepthOffset;
+ int value = this.currentQuantizationParameter + delta + bitDepthOffset;
+ value %= modulus;
+ if (value < 0)
+ {
+ value += modulus;
+ }
+
+ this.currentQuantizationParameter = value - bitDepthOffset;
+ }
+
+ ///
+ /// Creates the component quantization parameters selected by picture, slice, and coding-unit offsets.
+ ///
+ /// The effective luma, Cb, and Cr quantization parameters.
+ private HevcQuantizationParameters CreateQuantizationParameters()
+ {
+ int cbOffset = this.pictureParameterSet.ChromaCbQuantizationParameterOffset + this.currentSliceChromaBlueQuantizationOffset;
+ int crOffset = this.pictureParameterSet.ChromaCrQuantizationParameterOffset + this.currentSliceChromaRedQuantizationOffset;
+ if (this.currentChromaQuantizationAdjustment > 0)
+ {
+ int adjustmentIndex = this.currentChromaQuantizationAdjustment - 1;
+ cbOffset += this.pictureParameterSet.ChromaQuantizationParameterOffsetsCb[adjustmentIndex];
+ crOffset += this.pictureParameterSet.ChromaQuantizationParameterOffsetsCr[adjustmentIndex];
+ }
+
+ return new HevcQuantizationParameters(
+ this.currentQuantizationParameter,
+ this.sequenceParameterSet.BitDepthLuma,
+ this.sequenceParameterSet.BitDepthChroma,
+ this.sequenceParameterSet.ChromaFormat,
+ cbOffset,
+ crOffset);
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Traversal.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Traversal.cs
new file mode 100644
index 000000000..05828d1fb
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Traversal.cs
@@ -0,0 +1,364 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
+
+///
+/// Implements slice, coding-tree, and coding-unit traversal.
+///
+internal sealed partial class HevcPictureDecoder
+{
+ ///
+ /// Decodes one ordered slice segment and returns the next tile-scan coding-tree-block address.
+ ///
+ /// The current independent or dependent slice segment.
+ /// The independent header governing inherited slice fields.
+ /// The one-based independent-slice index within the selected color plane.
+ /// The picture tile mapping.
+ /// The first coding-tree block in tile-scan order.
+ /// The governing independent slice's first coding-tree block in tile-scan order.
+ /// The tile-scan address immediately following the decoded segment.
+ private int DecodeSliceSegment(
+ HevcSliceSegmentHeader slice,
+ HevcSliceSegmentHeader independentSlice,
+ int independentSliceIndex,
+ in HevcTileLayout tileLayout,
+ int startAddressInTileScan,
+ int independentSliceStartAddressInTileScan)
+ {
+ int sliceQuantizationParameter = independentSlice.QuantizationParameter!.Value;
+ int colorPlaneIndex = this.sequenceParameterSet.SeparateColorPlaneFlag ? independentSlice.ColorPlaneId : 0;
+ this.lastCodedQuantizationParameter = sliceQuantizationParameter;
+ this.currentQuantizationParameter = sliceQuantizationParameter;
+ this.currentChromaQuantizationAdjustment = 0;
+ this.currentSliceChromaBlueQuantizationOffset = independentSlice.ChromaCbQuantizationParameterOffset;
+ this.currentSliceChromaRedQuantizationOffset = independentSlice.ChromaCrQuantizationParameterOffset;
+ this.quantizationParameterDeltaPending = this.pictureParameterSet.CodingUnitQuantizationParameterDeltaEnabled;
+ this.chromaQuantizationAdjustmentPending = independentSlice.ChromaQuantizationParameterOffsetListEnabled == true;
+ int substreamIndex = 0;
+ HevcCabacSyntaxReader reader = new(slice.GetEntropySubstream(substreamIndex).Span, sliceQuantizationParameter);
+ this.coefficientDecoder.ResetRiceAdaptation();
+
+ int contextOffset = colorPlaneIndex * HevcCabacContexts.ContextCount;
+ int riceOffset = colorPlaneIndex * 4;
+ if (slice.DependentSliceSegment && this.hasSliceSegmentContexts[colorPlaneIndex])
+ {
+ reader.CopyContextsFrom(this.sliceSegmentContexts.AsSpan(contextOffset, HevcCabacContexts.ContextCount));
+ this.coefficientDecoder.CopyRiceAdaptationFrom(this.sliceSegmentRiceAdaptation.AsSpan(riceOffset, 4));
+ }
+
+ int codingTreeBlockSize = 1 << this.sequenceParameterSet.CodingTreeBlockLog2;
+ int tileScanAddress = startAddressInTileScan;
+ bool firstCodingTreeBlock = true;
+ bool wavefrontStateAvailable = false;
+ while (tileScanAddress < tileLayout.Width * tileLayout.Height)
+ {
+ int rasterAddress = tileLayout.GetRasterAddress(tileScanAddress);
+ tileLayout.GetTilePosition(
+ rasterAddress,
+ out int tileIndex,
+ out int columnInTile,
+ out int rowInTile,
+ out int tileWidth,
+ out int tileHeight);
+
+ bool startsTile = columnInTile == 0 && rowInTile == 0;
+ bool startsWavefrontRow = this.pictureParameterSet.EntropyCodingSynchronizationEnabled && columnInTile == 0 && rowInTile > 0;
+ if (!firstCodingTreeBlock && (startsTile || startsWavefrontRow))
+ {
+ if (!reader.ReadTerminate())
+ {
+ throw new InvalidImageContentException("The HEVC entropy substream does not terminate at its tile or wavefront boundary.");
+ }
+
+ reader.ValidateTerminationAlignment();
+ substreamIndex++;
+ if (substreamIndex >= slice.EntropySubstreamCount)
+ {
+ throw new InvalidImageContentException("The HEVC slice segment has too few entropy entry points.");
+ }
+
+ reader = new HevcCabacSyntaxReader(slice.GetEntropySubstream(substreamIndex).Span, sliceQuantizationParameter);
+ this.coefficientDecoder.ResetRiceAdaptation();
+ this.lastCodedQuantizationParameter = sliceQuantizationParameter;
+ if (startsWavefrontRow && tileWidth > 1 && wavefrontStateAvailable)
+ {
+ reader.CopyContextsFrom(this.wavefrontContexts);
+ this.coefficientDecoder.CopyRiceAdaptationFrom(this.wavefrontRiceAdaptation[..4]);
+ }
+ }
+
+ int ctbX = rasterAddress % tileLayout.Width;
+ int ctbY = rasterAddress / tileLayout.Width;
+ int x = ctbX * codingTreeBlockSize;
+ int y = ctbY * codingTreeBlockSize;
+ int regionId = ((independentSliceIndex - 1) * tileLayout.TileCount) + tileIndex + 1;
+ HevcPlane regionPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? (HevcPlane)colorPlaneIndex : HevcPlane.Y;
+ HevcLoopFilterRegion loopFilterRegion = new(
+ independentSliceStartAddressInTileScan,
+ tileIndex,
+ independentSlice.LoopFilterAcrossSlicesEnabled == true,
+ independentSlice.DeblockingFilterDisabled == true,
+ independentSlice.DeblockingFilterBetaOffsetDiv2,
+ independentSlice.DeblockingFilterTcOffsetDiv2);
+
+ this.sampleAdaptiveOffsetState.SetLoopFilterRegion(rasterAddress, regionPlane, loopFilterRegion);
+ this.DecodeSampleAdaptiveOffset(ref reader, independentSlice, rasterAddress, ctbX, ctbY, regionId);
+ bool endOfSliceSegment = this.DecodeCodingTree(
+ ref reader,
+ x,
+ y,
+ this.sequenceParameterSet.CodingTreeBlockLog2,
+ 0,
+ regionId,
+ colorPlaneIndex);
+
+ // Wavefront synchronization copies probability and persistent Rice state after the second CTB of each
+ // row. The next row starts with those contexts but a newly initialized arithmetic register.
+ if (this.pictureParameterSet.EntropyCodingSynchronizationEnabled && columnInTile == 1)
+ {
+ reader.CopyContextsTo(this.wavefrontContexts);
+ this.coefficientDecoder.CopyRiceAdaptationTo(this.wavefrontRiceAdaptation[..4]);
+ wavefrontStateAvailable = true;
+ }
+
+ tileScanAddress++;
+ firstCodingTreeBlock = false;
+ if (endOfSliceSegment)
+ {
+ reader.ValidateTerminationAlignment();
+ if (substreamIndex + 1 != slice.EntropySubstreamCount)
+ {
+ throw new InvalidImageContentException("The HEVC slice segment has unused entropy entry points.");
+ }
+
+ reader.CopyContextsTo(this.sliceSegmentContexts.AsSpan(contextOffset, HevcCabacContexts.ContextCount));
+ this.coefficientDecoder.CopyRiceAdaptationTo(this.sliceSegmentRiceAdaptation.AsSpan(riceOffset, 4));
+ this.hasSliceSegmentContexts[colorPlaneIndex] = true;
+ return tileScanAddress;
+ }
+
+ bool atTileEnd = columnInTile == tileWidth - 1 && rowInTile == tileHeight - 1;
+ bool atWavefrontRowEnd = this.pictureParameterSet.EntropyCodingSynchronizationEnabled && columnInTile == tileWidth - 1;
+ if (atTileEnd || atWavefrontRowEnd)
+ {
+ // A non-final tile or wavefront row has a second terminating bin after the coding-unit end flag.
+ // It is consumed when the following loop iteration opens the next bounded entropy substream.
+ continue;
+ }
+ }
+
+ throw new InvalidImageContentException("The HEVC slice segment reaches the picture boundary without termination.");
+ }
+
+ ///
+ /// Decodes one coding-tree node in depth-first Z order.
+ ///
+ /// The active entropy-substream reader.
+ /// The coding-node left luma coordinate.
+ /// The coding-node top luma coordinate.
+ /// The base-two logarithm of the coding-node side.
+ /// The coding-tree depth below the coding-tree-block root.
+ /// The current independent-slice and tile prediction region.
+ /// The selected separate-color plane, or zero for combined coding.
+ /// when the current leaf terminates the slice segment.
+ private bool DecodeCodingTree(
+ ref HevcCabacSyntaxReader reader,
+ int x,
+ int y,
+ int log2Size,
+ int depth,
+ int regionId,
+ int colorPlaneIndex)
+ {
+ int size = 1 << log2Size;
+ bool crossesPictureBoundary = x + size > this.sequenceParameterSet.Width || y + size > this.sequenceParameterSet.Height;
+ bool canSplit = log2Size > this.sequenceParameterSet.MinCodingBlockLog2;
+ HevcCodingTreeState codingTreeState = this.codingTreeStates[colorPlaneIndex];
+ bool split = false;
+ if (canSplit)
+ {
+ if (crossesPictureBoundary)
+ {
+ split = true;
+ }
+ else
+ {
+ bool leftAvailable = this.reconstructionState.IsReconstructed((HevcPlane)colorPlaneIndex, x - 1, y, regionId);
+ bool aboveAvailable = this.reconstructionState.IsReconstructed((HevcPlane)colorPlaneIndex, x, y - 1, regionId);
+ int context = codingTreeState.GetSplitContext(x, y, depth, leftAvailable, aboveAvailable);
+ split = reader.ReadSplit(context);
+ }
+ }
+
+ if (depth == this.pictureParameterSet.QuantizationParameterDeltaDepth
+ && this.pictureParameterSet.CodingUnitQuantizationParameterDeltaEnabled)
+ {
+ this.BeginQuantizationGroup(x, y, regionId, colorPlaneIndex);
+ }
+
+ if (depth == this.pictureParameterSet.ChromaQuantizationParameterOffsetDepth
+ && this.pictureParameterSet.ChromaQuantizationParameterOffsetsCb.Count != 0)
+ {
+ this.currentChromaQuantizationAdjustment = 0;
+ this.chromaQuantizationAdjustmentPending = true;
+ }
+
+ if (split)
+ {
+ int childLog2Size = log2Size - 1;
+ int childSize = 1 << childLog2Size;
+ for (int child = 0; child < 4; child++)
+ {
+ int childX = x + ((child & 1) * childSize);
+ int childY = y + ((child >> 1) * childSize);
+ if (childX >= this.sequenceParameterSet.Width || childY >= this.sequenceParameterSet.Height)
+ {
+ continue;
+ }
+
+ if (this.DecodeCodingTree(
+ ref reader,
+ childX,
+ childY,
+ childLog2Size,
+ depth + 1,
+ regionId,
+ colorPlaneIndex))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ return this.DecodeCodingUnit(ref reader, x, y, log2Size, depth, regionId, colorPlaneIndex);
+ }
+
+ ///
+ /// Decodes and reconstructs one intra-coded leaf coding unit.
+ ///
+ /// The active entropy-substream reader.
+ /// The coding-unit left luma coordinate.
+ /// The coding-unit top luma coordinate.
+ /// The base-two logarithm of the coding-unit side.
+ /// The coding-tree depth.
+ /// The current independent-slice and tile prediction region.
+ /// The selected separate-color plane, or zero for combined coding.
+ /// when this coding unit terminates the slice segment.
+ private bool DecodeCodingUnit(
+ ref HevcCabacSyntaxReader reader,
+ int x,
+ int y,
+ int log2Size,
+ int depth,
+ int regionId,
+ int colorPlaneIndex)
+ {
+ bool transquantBypass = this.pictureParameterSet.TransquantizationBypassEnabled && reader.ReadTransquantBypass();
+ bool usesNxNPartitions = reader.ReadIntraNxNPartition(log2Size == this.sequenceParameterSet.MinCodingBlockLog2);
+ HevcPlane primaryPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? (HevcPlane)colorPlaneIndex : HevcPlane.Y;
+ bool pcm = this.sequenceParameterSet.PcmEnabled
+ && !usesNxNPartitions
+ && log2Size >= this.sequenceParameterSet.MinPcmCodingBlockLog2
+ && log2Size <= this.sequenceParameterSet.MaxPcmCodingBlockLog2
+ && reader.ReadPcmFlag();
+
+ if (pcm)
+ {
+ int size = 1 << log2Size;
+ this.deblockingState.MarkBlock(primaryPlane, x, y, size, size);
+ this.DecodePcmCodingUnit(ref reader, x, y, log2Size, regionId, colorPlaneIndex);
+ reader.RestartAfterPcm();
+ }
+ else
+ {
+ HevcIntraPredictionState predictionState = this.intraPredictionStates[colorPlaneIndex];
+ HevcPlane boundaryPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? (HevcPlane)colorPlaneIndex : HevcPlane.Y;
+ bool leftAvailable = this.reconstructionState.IsReconstructed(boundaryPlane, x - 1, y, regionId);
+ bool aboveAvailable = this.reconstructionState.IsReconstructed(boundaryPlane, x, y - 1, regionId);
+ predictionState.DecodeLumaModes(ref reader, x, y, log2Size, usesNxNPartitions, leftAvailable, aboveAvailable);
+ if (this.sequenceParameterSet.ChromaFormat != 0 && !this.sequenceParameterSet.SeparateColorPlaneFlag)
+ {
+ predictionState.DecodeChromaMode(ref reader, x, y, log2Size);
+ }
+
+ int minimumTransformLog2 = GetMinimumTransformLog2Size(this.sequenceParameterSet, log2Size, usesNxNPartitions);
+ HevcTransformUnitGeometry geometry = HevcTransformUnitGeometry.CreateRoot(
+ x,
+ y,
+ log2Size,
+ this.sequenceParameterSet.ChromaFormat,
+ this.sequenceParameterSet.SeparateColorPlaneFlag,
+ colorPlaneIndex);
+
+ this.DecodeTransformTree(
+ ref reader,
+ in geometry,
+ 0,
+ minimumTransformLog2,
+ usesNxNPartitions,
+ transquantBypass,
+ regionId,
+ colorPlaneIndex,
+ default,
+ default);
+ }
+
+ HevcQuantizationParameters quantizationParameters = this.CreateQuantizationParameters();
+ this.codingTreeStates[colorPlaneIndex].SetCodingUnit(
+ x,
+ y,
+ log2Size,
+ depth,
+ this.currentQuantizationParameter,
+ quantizationParameters.CbOffset,
+ quantizationParameters.CrOffset,
+ transquantBypass,
+ pcm);
+
+ this.lastCodedQuantizationParameter = this.currentQuantizationParameter;
+ return reader.ReadTerminate();
+ }
+
+ ///
+ /// Begins one luma quantization group using available spatial predictors.
+ ///
+ /// The quantization-group left luma coordinate.
+ /// The quantization-group top luma coordinate.
+ /// The current independent-slice and tile prediction region.
+ /// The selected separate-color plane, or zero for combined coding.
+ private void BeginQuantizationGroup(int x, int y, int regionId, int colorPlaneIndex)
+ {
+ HevcPlane plane = this.sequenceParameterSet.SeparateColorPlaneFlag ? (HevcPlane)colorPlaneIndex : HevcPlane.Y;
+ bool leftAvailable = this.reconstructionState.IsReconstructed(plane, x - 1, y, regionId);
+ bool aboveAvailable = this.reconstructionState.IsReconstructed(plane, x, y - 1, regionId);
+ int fallback = this.lastCodedQuantizationParameter;
+ HevcCodingTreeState codingTreeState = this.codingTreeStates[colorPlaneIndex];
+ int left = leftAvailable ? codingTreeState.GetQuantizationParameter(x - 1, y) : fallback;
+ int above = aboveAvailable ? codingTreeState.GetQuantizationParameter(x, y - 1) : fallback;
+ this.currentQuantizationParameter = (left + above + 1) >> 1;
+ this.quantizationParameterDeltaPending = true;
+ }
+
+ ///
+ /// Derives the smallest luma transform permitted within one intra coding unit.
+ ///
+ /// The transform hierarchy limits.
+ /// The base-two logarithm of the coding-unit side.
+ /// Whether the coding unit has four luma prediction partitions.
+ /// The minimum luma transform side as a base-two logarithm.
+ private static int GetMinimumTransformLog2Size(
+ HevcSequenceParameterSet sequenceParameterSet,
+ int codingUnitLog2Size,
+ bool usesNxNPartitions)
+ {
+ int hierarchyReduction = sequenceParameterSet.MaxTransformHierarchyDepthIntra - 1 + (usesNxNPartitions ? 1 : 0);
+ int minimum = codingUnitLog2Size < sequenceParameterSet.MinTransformBlockLog2 + hierarchyReduction
+ ? sequenceParameterSet.MinTransformBlockLog2
+ : codingUnitLog2Size - hierarchyReduction;
+
+ return Math.Min(minimum, sequenceParameterSet.MaxTransformBlockLog2);
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.cs
new file mode 100644
index 000000000..0c5e1e1cf
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.cs
@@ -0,0 +1,299 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
+
+///
+/// Owns the bounded state used to reconstruct one independently decodable HEVC still picture.
+///
+internal sealed partial class HevcPictureDecoder : IDisposable
+{
+ ///
+ /// The maximum square transform-block sample count.
+ ///
+ private const int MaximumTransformSampleCount = 32 * 32;
+
+ ///
+ /// The largest reference array used by a thirty-two-sample prediction block.
+ ///
+ private const int MaximumReferenceLength = (2 * 32) + 1;
+
+ ///
+ /// The configuration providing picture-lifetime allocations.
+ ///
+ private readonly Configuration configuration;
+
+ ///
+ /// The active picture parameters.
+ ///
+ private readonly HevcPictureParameterSet pictureParameterSet;
+
+ ///
+ /// The active sequence parameters.
+ ///
+ private readonly HevcSequenceParameterSet sequenceParameterSet;
+
+ ///
+ /// The decoded coding-unit state.
+ ///
+ private readonly HevcCodingTreeState[] codingTreeStates;
+
+ ///
+ /// The decoded intra-prediction modes.
+ ///
+ private readonly HevcIntraPredictionState[] intraPredictionStates;
+
+ ///
+ /// The completed prediction-block state used for reference availability.
+ ///
+ private readonly HevcReconstructionState reconstructionState;
+
+ ///
+ /// The reusable coefficient entropy decoder.
+ ///
+ private readonly HevcCoefficientDecoder coefficientDecoder;
+
+ ///
+ /// The resolved sample-adaptive-offset parameters for every coding-tree block.
+ ///
+ private readonly HevcSampleAdaptiveOffsetState sampleAdaptiveOffsetState;
+
+ ///
+ /// The transform and prediction boundaries required by the deblocking stage.
+ ///
+ private readonly HevcDeblockingState deblockingState;
+
+ ///
+ /// The integer coefficient, residual, and transform workspace.
+ ///
+ private readonly IMemoryOwner integerScratch;
+
+ ///
+ /// The prediction, reference, and reference-substitution workspace.
+ ///
+ private readonly IMemoryOwner predictionScratch;
+
+ ///
+ /// The ordered intra-reference availability workspace.
+ ///
+ private readonly IMemoryOwner availabilityScratch;
+
+ ///
+ /// The adaptive contexts captured after the second coding-tree block of a wavefront row.
+ ///
+ private readonly HevcCabacContext[] wavefrontContexts = new HevcCabacContext[HevcCabacContexts.ContextCount];
+
+ ///
+ /// The persistent Rice statistics captured with the wavefront probability contexts.
+ ///
+ private InlineArray4 wavefrontRiceAdaptation;
+
+ ///
+ /// The adaptive contexts retained at the end of a dependent-slice prediction region.
+ ///
+ private readonly HevcCabacContext[] sliceSegmentContexts = new HevcCabacContext[HevcCabacContexts.ContextCount * 3];
+
+ ///
+ /// The persistent Rice statistics retained with dependent-slice probability contexts.
+ ///
+ private readonly int[] sliceSegmentRiceAdaptation = new int[12];
+
+ ///
+ /// Whether retained dependent-slice contexts are available.
+ ///
+ private InlineArray4 hasSliceSegmentContexts;
+
+ ///
+ /// The luma quantization parameter most recently coded in the current prediction region.
+ ///
+ private int lastCodedQuantizationParameter;
+
+ ///
+ /// The effective luma quantization parameter of the current quantization group.
+ ///
+ private int currentQuantizationParameter;
+
+ ///
+ /// The one-based chroma quantization-offset-list selector of the current quantization group.
+ ///
+ private int currentChromaQuantizationAdjustment;
+
+ ///
+ /// The Cb quantization-parameter offset signaled by the governing independent slice.
+ ///
+ private int currentSliceChromaBlueQuantizationOffset;
+
+ ///
+ /// The Cr quantization-parameter offset signaled by the governing independent slice.
+ ///
+ private int currentSliceChromaRedQuantizationOffset;
+
+ ///
+ /// Whether the current quantization group can still signal its luma delta.
+ ///
+ private bool quantizationParameterDeltaPending;
+
+ ///
+ /// Whether the current quantization group can still signal its chroma adjustment.
+ ///
+ private bool chromaQuantizationAdjustmentPending;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration providing all decoder-owned memory.
+ /// The picture parameters governing the coded still image.
+ public HevcPictureDecoder(Configuration configuration, HevcPictureParameterSet pictureParameterSet)
+ {
+ this.configuration = configuration;
+ this.pictureParameterSet = pictureParameterSet;
+ this.sequenceParameterSet = pictureParameterSet.SequenceParameterSet;
+ this.Picture = new HevcPictureBuffer(configuration, this.sequenceParameterSet);
+ int codingTreeStateCount = this.sequenceParameterSet.SeparateColorPlaneFlag ? 3 : 1;
+ this.codingTreeStates = new HevcCodingTreeState[codingTreeStateCount];
+ for (int index = 0; index < this.codingTreeStates.Length; index++)
+ {
+ this.codingTreeStates[index] = new HevcCodingTreeState(configuration, this.sequenceParameterSet);
+ }
+
+ int intraPredictionStateCount = this.sequenceParameterSet.SeparateColorPlaneFlag ? 3 : 1;
+ this.intraPredictionStates = new HevcIntraPredictionState[intraPredictionStateCount];
+ for (int index = 0; index < this.intraPredictionStates.Length; index++)
+ {
+ this.intraPredictionStates[index] = new HevcIntraPredictionState(configuration, this.sequenceParameterSet);
+ }
+
+ this.reconstructionState = new HevcReconstructionState(configuration, this.sequenceParameterSet);
+ this.coefficientDecoder = new HevcCoefficientDecoder(configuration);
+ int codingTreeBlockCount = HevcParameterSetSyntax.GetCodingTreeBlockCount(
+ this.sequenceParameterSet.Width,
+ this.sequenceParameterSet.CodingTreeBlockLog2)
+ * HevcParameterSetSyntax.GetCodingTreeBlockCount(
+ this.sequenceParameterSet.Height,
+ this.sequenceParameterSet.CodingTreeBlockLog2);
+
+ this.sampleAdaptiveOffsetState = new HevcSampleAdaptiveOffsetState(configuration, codingTreeBlockCount);
+ this.deblockingState = new HevcDeblockingState(configuration, this.sequenceParameterSet);
+
+ // Six transform-sized integer regions retain quantized, dequantized, reconstructed, cross-component, and
+ // two-pass inverse-transform data without allocating in coding-unit or transform-unit loops.
+ this.integerScratch = configuration.MemoryAllocator.Allocate(MaximumTransformSampleCount * 6);
+ int maximumPredictionScratch = HevcIntraPredictor.GetScratchLength(5);
+ int maximumReferenceScratch = HevcIntraPredictor.GetReferenceScratchLength(5, 4);
+ this.predictionScratch = configuration.MemoryAllocator.Allocate(
+ MaximumTransformSampleCount + maximumPredictionScratch + maximumReferenceScratch + (MaximumReferenceLength * 4));
+
+ this.availabilityScratch = configuration.MemoryAllocator.Allocate((4 * 32 / 2) + 1);
+ }
+
+ ///
+ /// Gets the native-precision reconstructed component planes.
+ ///
+ public HevcPictureBuffer Picture { get; }
+
+ ///
+ /// Reconstructs every ordered slice segment in one independently decodable image item.
+ ///
+ /// The validated image-item NAL units and slice segments.
+ ///
+ /// A slice changes the coded picture parameters, overlaps an earlier segment, or does not terminate at a valid
+ /// coding-tree boundary.
+ ///
+ public void Decode(HevcImageItemBitstream bitstream)
+ {
+ HevcTileLayout tileLayout = new(this.pictureParameterSet);
+ int planeCount = this.sequenceParameterSet.SeparateColorPlaneFlag ? 3 : 1;
+ int[] nextCodingTreeBlockAddressesInTileScan = new int[planeCount];
+ int[] independentSliceIndices = new int[planeCount];
+ HevcSliceSegmentHeader?[] independentSlices = new HevcSliceSegmentHeader?[planeCount];
+ for (int sliceIndex = 0; sliceIndex < bitstream.SliceSegments.Count; sliceIndex++)
+ {
+ HevcSliceSegmentHeader slice = bitstream.SliceSegments[sliceIndex];
+ int colorPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? slice.ColorPlaneId : 0;
+ if (slice.PictureParameterSet.Id != this.pictureParameterSet.Id
+ || slice.PictureParameterSet.SequenceParameterSetId != this.pictureParameterSet.SequenceParameterSetId)
+ {
+ throw new InvalidImageContentException("The HEVC still picture changes parameter sets between slice segments.");
+ }
+
+ if (!slice.DependentSliceSegment)
+ {
+ independentSlices[colorPlane] = slice;
+ independentSliceIndices[colorPlane]++;
+ }
+
+ HevcSliceSegmentHeader? independentSlice = independentSlices[colorPlane];
+ if (independentSlice is null)
+ {
+ throw new InvalidImageContentException("The HEVC still picture begins with a dependent slice segment.");
+ }
+
+ int sliceStartAddressInTileScan = tileLayout.GetTileScanAddress(slice.SliceSegmentAddress);
+ if (sliceStartAddressInTileScan != nextCodingTreeBlockAddressesInTileScan[colorPlane])
+ {
+ throw new InvalidImageContentException("The HEVC slice segments do not cover the coded picture in order.");
+ }
+
+ nextCodingTreeBlockAddressesInTileScan[colorPlane] = this.DecodeSliceSegment(
+ slice,
+ independentSlice,
+ independentSliceIndices[colorPlane],
+ in tileLayout,
+ sliceStartAddressInTileScan,
+ tileLayout.GetTileScanAddress(independentSlice.SliceSegmentAddress));
+ }
+
+ int codingTreeBlockCount = HevcParameterSetSyntax.GetCodingTreeBlockCount(
+ this.sequenceParameterSet.Width,
+ this.sequenceParameterSet.CodingTreeBlockLog2)
+ * HevcParameterSetSyntax.GetCodingTreeBlockCount(
+ this.sequenceParameterSet.Height,
+ this.sequenceParameterSet.CodingTreeBlockLog2);
+
+ foreach (int nextAddress in nextCodingTreeBlockAddressesInTileScan)
+ {
+ if (nextAddress != codingTreeBlockCount)
+ {
+ throw new InvalidImageContentException("The HEVC slice segments do not reconstruct the complete coded picture.");
+ }
+ }
+
+ this.ApplyDeblockingFilter(in tileLayout);
+ if (this.sampleAdaptiveOffsetState.HasEnabledParameters)
+ {
+ // SAO classification always observes the complete post-deblocking picture, never samples already offset by an
+ // earlier CTB. One picture-lifetime snapshot provides that invariant without row allocations or filter-order coupling.
+ using HevcPictureBuffer sampleAdaptiveOffsetSource = new(this.configuration, this.sequenceParameterSet);
+ this.Picture.CopyTo(sampleAdaptiveOffsetSource);
+ this.ApplySampleAdaptiveOffset(sampleAdaptiveOffsetSource, in tileLayout);
+ }
+ }
+
+ ///
+ /// Releases all current-picture state and reconstructed planes.
+ ///
+ public void Dispose()
+ {
+ this.availabilityScratch.Dispose();
+ this.predictionScratch.Dispose();
+ this.integerScratch.Dispose();
+ this.deblockingState.Dispose();
+ this.sampleAdaptiveOffsetState.Dispose();
+ this.coefficientDecoder.Dispose();
+ this.reconstructionState.Dispose();
+ foreach (HevcIntraPredictionState state in this.intraPredictionStates)
+ {
+ state.Dispose();
+ }
+
+ foreach (HevcCodingTreeState state in this.codingTreeStates)
+ {
+ state.Dispose();
+ }
+
+ this.Picture.Dispose();
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcQuantizationParameters.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcQuantizationParameters.cs
index 4647b0867..3f7a5e288 100644
--- a/src/ImageSharp/Formats/Heif/Hevc/HevcQuantizationParameters.cs
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcQuantizationParameters.cs
@@ -27,6 +27,8 @@ internal readonly struct HevcQuantizationParameters
{
int lumaBitDepthOffset = 6 * (lumaBitDepth - 8);
int chromaBitDepthOffset = 6 * (chromaBitDepth - 8);
+ this.CbOffset = cbQuantizationParameterOffset;
+ this.CrOffset = crQuantizationParameterOffset;
this.Luma = lumaQuantizationParameter + lumaBitDepthOffset;
this.Cb = GetChromaQuantizationParameter(lumaQuantizationParameter, cbQuantizationParameterOffset, chromaBitDepthOffset, chromaFormat);
this.Cr = GetChromaQuantizationParameter(lumaQuantizationParameter, crQuantizationParameterOffset, chromaBitDepthOffset, chromaFormat);
@@ -47,6 +49,16 @@ internal readonly struct HevcQuantizationParameters
///
public int Cr { get; }
+ ///
+ /// Gets the combined picture, slice, and coding-unit Cb quantization-parameter offset.
+ ///
+ public int CbOffset { get; }
+
+ ///
+ /// Gets the combined picture, slice, and coding-unit Cr quantization-parameter offset.
+ ///
+ public int CrOffset { get; }
+
///
/// Gets the H.265 Table 8-10 chroma quantization-parameter mapping for 4:2:0 pictures.
///
@@ -76,7 +88,7 @@ internal readonly struct HevcQuantizationParameters
/// Six times the number of chroma bits above eight.
/// The sequence chroma-format identifier.
/// The effective nonnegative chroma quantization parameter including its bit-depth offset.
- private static int GetChromaQuantizationParameter(
+ public static int GetChromaQuantizationParameter(
int lumaQuantizationParameter,
int componentOffset,
int chromaBitDepthOffset,
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcReconstructionState.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcReconstructionState.cs
new file mode 100644
index 000000000..fcba031c0
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcReconstructionState.cs
@@ -0,0 +1,221 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
+
+///
+/// Tracks reconstructed minimum prediction blocks for HEVC intra-reference availability.
+///
+internal sealed class HevcReconstructionState : IDisposable
+{
+ ///
+ /// The base-two logarithm of the minimum luma prediction-block side.
+ ///
+ private const int MinPredictionBlockLog2 = 2;
+
+ ///
+ /// The reconstruction-region identifiers for the three component planes.
+ ///
+ private readonly Buffer2D[] regions;
+
+ ///
+ /// The horizontal chroma subsampling shift.
+ ///
+ private readonly int chromaSubsamplingX;
+
+ ///
+ /// The vertical chroma subsampling shift.
+ ///
+ private readonly int chromaSubsamplingY;
+
+ ///
+ /// The coded luma width used to reject padded right-edge units.
+ ///
+ private readonly int width;
+
+ ///
+ /// The coded luma height used to reject padded bottom-edge units.
+ ///
+ private readonly int height;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration providing the image memory allocator.
+ /// The coded picture and chroma geometry.
+ public HevcReconstructionState(Configuration configuration, HevcSequenceParameterSet sequenceParameterSet)
+ {
+ this.width = sequenceParameterSet.Width;
+ this.height = sequenceParameterSet.Height;
+ int widthInUnits = DivideCeilingByPowerOfTwo(this.width, MinPredictionBlockLog2);
+ int heightInUnits = DivideCeilingByPowerOfTwo(this.height, MinPredictionBlockLog2);
+ this.chromaSubsamplingX = !sequenceParameterSet.SeparateColorPlaneFlag && sequenceParameterSet.ChromaFormat is 1 or 2 ? 1 : 0;
+ this.chromaSubsamplingY = !sequenceParameterSet.SeparateColorPlaneFlag && sequenceParameterSet.ChromaFormat == 1 ? 1 : 0;
+ this.regions =
+ [
+ configuration.MemoryAllocator.Allocate2D(widthInUnits, heightInUnits),
+ configuration.MemoryAllocator.Allocate2D(widthInUnits, heightInUnits),
+ configuration.MemoryAllocator.Allocate2D(widthInUnits, heightInUnits),
+ ];
+ }
+
+ ///
+ /// Gets the horizontal availability-unit width for a component plane.
+ ///
+ /// The component plane.
+ /// The availability-unit width in component samples.
+ public int GetUnitWidth(HevcPlane plane) => 1 << (MinPredictionBlockLog2 - this.GetSubsamplingX(plane));
+
+ ///
+ /// Gets the vertical availability-unit height for a component plane.
+ ///
+ /// The component plane.
+ /// The availability-unit height in component samples.
+ public int GetUnitHeight(HevcPlane plane) => 1 << (MinPredictionBlockLog2 - this.GetSubsamplingY(plane));
+
+ ///
+ /// Marks a reconstructed component rectangle as available within one slice-and-tile prediction region.
+ ///
+ /// The reconstructed component plane.
+ /// The rectangle left coordinate in component samples.
+ /// The rectangle top coordinate in component samples.
+ /// The rectangle width in component samples.
+ /// The rectangle height in component samples.
+ /// The positive identifier shared by prediction blocks in the same slice segment and tile.
+ public void MarkReconstructed(HevcPlane plane, int x, int y, int width, int height, int regionId)
+ {
+ DebugGuard.MustBeGreaterThan(regionId, 0, nameof(regionId));
+ int subsamplingX = this.GetSubsamplingX(plane);
+ int subsamplingY = this.GetSubsamplingY(plane);
+ int unitX = (x << subsamplingX) >> MinPredictionBlockLog2;
+ int unitY = (y << subsamplingY) >> MinPredictionBlockLog2;
+ int endX = DivideCeilingByPowerOfTwo((x + width) << subsamplingX, MinPredictionBlockLog2);
+ int endY = DivideCeilingByPowerOfTwo((y + height) << subsamplingY, MinPredictionBlockLog2);
+ Buffer2D map = this.regions[(int)plane];
+ endX = Math.Min(endX, map.Width);
+ endY = Math.Min(endY, map.Height);
+
+ // Chroma availability units map back to the same four-by-four luma grid used by HEVC neighbor derivation.
+ // Filling the complete rectangle makes later sub-TUs observe only samples whose reconstruction has finished.
+ for (int row = unitY; row < endY; row++)
+ {
+ map.DangerousGetRowSpan(row)[unitX..endX].Fill(regionId);
+ }
+ }
+
+ ///
+ /// Builds the ordered availability flags consumed by HEVC reference-sample substitution.
+ ///
+ /// The component plane containing the prediction block.
+ /// The prediction-block left coordinate in component samples.
+ /// The prediction-block top coordinate in component samples.
+ /// The base-two logarithm of the square prediction-block side.
+ /// The current slice-and-tile prediction-region identifier.
+ ///
+ /// The destination ordered from the bottom-most below-left unit through top-left and then the above-right units.
+ ///
+ /// The number of flags written.
+ public int BuildReferenceAvailability(HevcPlane plane, int x, int y, int log2Size, int regionId, Span destination)
+ {
+ DebugGuard.MustBeBetweenOrEqualTo(log2Size, 2, 5, nameof(log2Size));
+ DebugGuard.MustBeGreaterThan(regionId, 0, nameof(regionId));
+ int size = 1 << log2Size;
+ int unitWidth = this.GetUnitWidth(plane);
+ int unitHeight = this.GetUnitHeight(plane);
+ int leftUnitCount = (size * 2) / unitHeight;
+ int aboveUnitCount = (size * 2) / unitWidth;
+ int flagCount = leftUnitCount + aboveUnitCount + 1;
+ Span availability = destination[..flagCount];
+
+ for (int unit = 0; unit < leftUnitCount; unit++)
+ {
+ int unitY = y + ((leftUnitCount - unit - 1) * unitHeight);
+ availability[unit] = this.IsAvailable(plane, x - 1, unitY, regionId);
+ }
+
+ availability[leftUnitCount] = this.IsAvailable(plane, x - 1, y - 1, regionId);
+ for (int unit = 0; unit < aboveUnitCount; unit++)
+ {
+ availability[leftUnitCount + unit + 1] = this.IsAvailable(plane, x + (unit * unitWidth), y - 1, regionId);
+ }
+
+ return flagCount;
+ }
+
+ ///
+ /// Gets whether one component sample has already been reconstructed in the selected prediction region.
+ ///
+ /// The component plane.
+ /// The component sample X coordinate.
+ /// The component sample Y coordinate.
+ /// The current slice-and-tile prediction-region identifier.
+ /// when the sample is available; otherwise, .
+ public bool IsReconstructed(HevcPlane plane, int x, int y, int regionId) => this.IsAvailable(plane, x, y, regionId);
+
+ ///
+ /// Releases the owned reconstruction-region maps.
+ ///
+ public void Dispose()
+ {
+ foreach (Buffer2D map in this.regions)
+ {
+ map.Dispose();
+ }
+ }
+
+ ///
+ /// Gets whether a component sample belongs to an already reconstructed block in the selected prediction region.
+ ///
+ /// The component plane.
+ /// The component sample X coordinate.
+ /// The component sample Y coordinate.
+ /// The current slice-and-tile prediction-region identifier.
+ /// when the sample is available; otherwise, .
+ private bool IsAvailable(HevcPlane plane, int x, int y, int regionId)
+ {
+ if (x < 0 || y < 0)
+ {
+ return false;
+ }
+
+ int subsamplingX = this.GetSubsamplingX(plane);
+ int subsamplingY = this.GetSubsamplingY(plane);
+ int planeWidth = DivideCeilingByPowerOfTwo(this.width, subsamplingX);
+ int planeHeight = DivideCeilingByPowerOfTwo(this.height, subsamplingY);
+ if (x >= planeWidth || y >= planeHeight)
+ {
+ return false;
+ }
+
+ int unitX = (x << subsamplingX) >> MinPredictionBlockLog2;
+ int unitY = (y << subsamplingY) >> MinPredictionBlockLog2;
+ Buffer2D map = this.regions[(int)plane];
+ return (uint)unitX < (uint)map.Width
+ && (uint)unitY < (uint)map.Height
+ && map.DangerousGetRowSpan(unitY)[unitX] == regionId;
+ }
+
+ ///
+ /// Gets the horizontal chroma shift selected by a component plane.
+ ///
+ /// The component plane.
+ /// Zero for luma and full-resolution planes; otherwise, the chroma shift.
+ private int GetSubsamplingX(HevcPlane plane) => plane == HevcPlane.Y ? 0 : this.chromaSubsamplingX;
+
+ ///
+ /// Gets the vertical chroma shift selected by a component plane.
+ ///
+ /// The component plane.
+ /// Zero for luma and full-resolution planes; otherwise, the chroma shift.
+ private int GetSubsamplingY(HevcPlane plane) => plane == HevcPlane.Y ? 0 : this.chromaSubsamplingY;
+
+ ///
+ /// Divides a nonnegative sample count by a power of two with upward rounding.
+ ///
+ /// The sample count.
+ /// The base-two divisor logarithm.
+ /// The upward-rounded quotient.
+ private static int DivideCeilingByPowerOfTwo(int value, int shift) => (value + (1 << shift) - 1) >> shift;
+}
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcResidualReconstructor.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcResidualReconstructor.cs
index b06696440..13827bd41 100644
--- a/src/ImageSharp/Formats/Heif/Hevc/HevcResidualReconstructor.cs
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcResidualReconstructor.cs
@@ -164,6 +164,113 @@ internal static class HevcResidualReconstructor
}
}
+ ///
+ /// Adds the scaled luma residual to one chroma residual block for inverse cross-component prediction.
+ ///
+ /// The packed luma residual samples colocated with the chroma block.
+ /// The packed chroma residual block updated in place.
+ /// The number of residual samples in each block.
+ /// The signed cross-component scale from minus eight through eight.
+ /// The luma bit depth minus the chroma bit depth.
+ public static void ApplyCrossComponentPrediction(
+ ReadOnlySpan lumaResidual,
+ Span chromaResidual,
+ int sampleCount,
+ int alpha,
+ int bitDepthDifference)
+ {
+ ref int lumaBase = ref MemoryMarshal.GetReference(lumaResidual);
+ ref int chromaBase = ref MemoryMarshal.GetReference(chromaResidual);
+ int index = 0;
+
+ // The scale denominator is eight. Adjusting luma precision first preserves the normative arithmetic shift
+ // for negative residuals before the signed alpha multiplication is applied independently to every lane.
+ if (Vector512.IsHardwareAccelerated)
+ {
+ Vector512 alphaVector = Vector512.Create(alpha);
+ Vector512 minimum = Vector512.Create(ResidualMinimum);
+ Vector512 maximum = Vector512.Create(ResidualMaximum);
+ for (; index <= sampleCount - Vector512.Count; index += Vector512.Count)
+ {
+ Vector512 luma = AdjustBitDepth(Vector512.LoadUnsafe(ref lumaBase, (nuint)index), bitDepthDifference);
+ Vector512 chroma = Vector512.LoadUnsafe(ref chromaBase, (nuint)index);
+ Vector512.Clamp(chroma + ((luma * alphaVector) >> 3), minimum, maximum).StoreUnsafe(ref chromaBase, (nuint)index);
+ }
+ }
+
+ if (Vector256.IsHardwareAccelerated)
+ {
+ Vector256 alphaVector = Vector256.Create(alpha);
+ Vector256 minimum = Vector256.Create(ResidualMinimum);
+ Vector256 maximum = Vector256.Create(ResidualMaximum);
+ for (; index <= sampleCount - Vector256.Count; index += Vector256.Count)
+ {
+ Vector256 luma = AdjustBitDepth(Vector256.LoadUnsafe(ref lumaBase, (nuint)index), bitDepthDifference);
+ Vector256 chroma = Vector256.LoadUnsafe(ref chromaBase, (nuint)index);
+ Vector256.Clamp(chroma + ((luma * alphaVector) >> 3), minimum, maximum).StoreUnsafe(ref chromaBase, (nuint)index);
+ }
+ }
+
+ if (Vector128.IsHardwareAccelerated)
+ {
+ Vector128 alphaVector = Vector128.Create(alpha);
+ Vector128 minimum = Vector128.Create(ResidualMinimum);
+ Vector128 maximum = Vector128.Create(ResidualMaximum);
+ for (; index <= sampleCount - Vector128.Count; index += Vector128.Count)
+ {
+ Vector128 luma = AdjustBitDepth(Vector128.LoadUnsafe(ref lumaBase, (nuint)index), bitDepthDifference);
+ Vector128 chroma = Vector128.LoadUnsafe(ref chromaBase, (nuint)index);
+ Vector128.Clamp(chroma + ((luma * alphaVector) >> 3), minimum, maximum).StoreUnsafe(ref chromaBase, (nuint)index);
+ }
+ }
+
+ for (; index < sampleCount; index++)
+ {
+ int luma = AdjustBitDepth(Unsafe.Add(ref lumaBase, index), bitDepthDifference);
+ int chroma = Unsafe.Add(ref chromaBase, index) + ((alpha * luma) >> 3);
+ Unsafe.Add(ref chromaBase, index) = Math.Clamp(chroma, ResidualMinimum, ResidualMaximum);
+ }
+ }
+
+ ///
+ /// Adjusts sixteen luma residuals to chroma precision.
+ ///
+ /// The luma residuals.
+ /// The luma bit depth minus the chroma bit depth.
+ /// The precision-adjusted residuals.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector512 AdjustBitDepth(Vector512 values, int difference)
+ => difference >= 0 ? values >> difference : values << -difference;
+
+ ///
+ /// Adjusts eight luma residuals to chroma precision.
+ ///
+ /// The luma residuals.
+ /// The luma bit depth minus the chroma bit depth.
+ /// The precision-adjusted residuals.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector256 AdjustBitDepth(Vector256 values, int difference)
+ => difference >= 0 ? values >> difference : values << -difference;
+
+ ///
+ /// Adjusts four luma residuals to chroma precision.
+ ///
+ /// The luma residuals.
+ /// The luma bit depth minus the chroma bit depth.
+ /// The precision-adjusted residuals.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector128 AdjustBitDepth(Vector128 values, int difference)
+ => difference >= 0 ? values >> difference : values << -difference;
+
+ ///
+ /// Adjusts one luma residual to chroma precision.
+ ///
+ /// The luma residual.
+ /// The luma bit depth minus the chroma bit depth.
+ /// The precision-adjusted residual.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int AdjustBitDepth(int value, int difference) => difference >= 0 ? value >> difference : value << -difference;
+
///
/// Applies one transform-skip normalization operator to a complete coefficient block.
///
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetFilter.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetFilter.cs
new file mode 100644
index 000000000..0fee497a6
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetFilter.cs
@@ -0,0 +1,733 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
+
+///
+/// Applies HEVC sample-adaptive offsets to reconstructed component blocks.
+///
+internal static class HevcSampleAdaptiveOffsetFilter
+{
+ ///
+ /// Defines the sample classifier shared by the SIMD row traversal and scalar tail.
+ ///
+ private interface ISampleClassifier
+ {
+ ///
+ /// Gets a value indicating whether classification reads the two neighboring sample rows.
+ ///
+ public static abstract bool UsesNeighbors { get; }
+
+ ///
+ /// Classifies thirty-two current samples against their two classifier inputs.
+ ///
+ /// The current sample lanes.
+ /// The first neighboring sample lanes.
+ /// The second neighboring sample lanes.
+ /// The scaled offset and band-class state.
+ /// The zero-based offset-table indices.
+ public static abstract Vector512 Classify(
+ Vector512 current,
+ Vector512 neighbor0,
+ Vector512 neighbor1,
+ in KernelParameters kernel);
+
+ ///
+ /// Classifies sixteen current samples against their two classifier inputs.
+ ///
+ /// The current sample lanes.
+ /// The first neighboring sample lanes.
+ /// The second neighboring sample lanes.
+ /// The scaled offset and band-class state.
+ /// The zero-based offset-table indices.
+ public static abstract Vector256 Classify(
+ Vector256 current,
+ Vector256 neighbor0,
+ Vector256 neighbor1,
+ in KernelParameters kernel);
+
+ ///
+ /// Classifies eight current samples against their two classifier inputs.
+ ///
+ /// The current sample lanes.
+ /// The first neighboring sample lanes.
+ /// The second neighboring sample lanes.
+ /// The scaled offset and band-class state.
+ /// The zero-based offset-table indices.
+ public static abstract Vector128 Classify(
+ Vector128 current,
+ Vector128 neighbor0,
+ Vector128 neighbor1,
+ in KernelParameters kernel);
+
+ ///
+ /// Classifies one current sample against its two classifier inputs.
+ ///
+ /// The current sample.
+ /// The first neighboring sample.
+ /// The second neighboring sample.
+ /// The scaled offset and band-class state.
+ /// The zero-based offset-table index.
+ public static abstract int Classify(short current, short neighbor0, short neighbor1, in KernelParameters kernel);
+ }
+
+ ///
+ /// Applies one resolved sample-adaptive-offset mode to a component coding-tree block.
+ ///
+ /// The immutable pre-SAO picture used for every classification.
+ /// The picture receiving filtered samples.
+ /// The component plane.
+ /// The block's left coordinate in component samples.
+ /// The block's top coordinate in component samples.
+ /// The block width in component samples.
+ /// The block height in component samples.
+ /// The resolved coded offsets and classifier.
+ /// The component offset scale from the picture range-extension parameters.
+ /// Whether classification may read the block immediately to the left.
+ /// Whether classification may read the block immediately to the right.
+ /// Whether classification may read the block immediately above.
+ /// Whether classification may read the block immediately below.
+ /// Whether classification may read the upper-left diagonal block.
+ /// Whether classification may read the upper-right diagonal block.
+ /// Whether classification may read the lower-left diagonal block.
+ /// Whether classification may read the lower-right diagonal block.
+ public static void ApplyBlock(
+ HevcPictureBuffer source,
+ HevcPictureBuffer destination,
+ HevcPlane plane,
+ int x,
+ int y,
+ int width,
+ int height,
+ in HevcSampleAdaptiveOffsetParameters parameters,
+ int offsetScaleLog2,
+ bool leftAvailable,
+ bool rightAvailable,
+ bool aboveAvailable,
+ bool belowAvailable,
+ bool aboveLeftAvailable,
+ bool aboveRightAvailable,
+ bool belowLeftAvailable,
+ bool belowRightAvailable)
+ {
+ if (parameters.Type == HevcSampleAdaptiveOffsetType.Off)
+ {
+ return;
+ }
+
+ KernelParameters kernel = new(parameters, source.GetBitDepth(plane), offsetScaleLog2);
+ switch (parameters.Type)
+ {
+ case HevcSampleAdaptiveOffsetType.Band:
+ ApplyBand(source, destination, plane, x, y, width, height, in kernel);
+ break;
+ case HevcSampleAdaptiveOffsetType.EdgeHorizontal:
+ ApplyHorizontalEdges(source, destination, plane, x, y, width, height, leftAvailable, rightAvailable, in kernel);
+ break;
+ case HevcSampleAdaptiveOffsetType.EdgeVertical:
+ ApplyVerticalEdges(source, destination, plane, x, y, width, height, aboveAvailable, belowAvailable, in kernel);
+ break;
+ case HevcSampleAdaptiveOffsetType.EdgeDescending:
+ ApplyDescendingEdges(
+ source,
+ destination,
+ plane,
+ x,
+ y,
+ width,
+ height,
+ leftAvailable,
+ rightAvailable,
+ aboveAvailable,
+ belowAvailable,
+ aboveLeftAvailable,
+ belowRightAvailable,
+ in kernel);
+ break;
+ case HevcSampleAdaptiveOffsetType.EdgeAscending:
+ ApplyAscendingEdges(
+ source,
+ destination,
+ plane,
+ x,
+ y,
+ width,
+ height,
+ leftAvailable,
+ rightAvailable,
+ aboveAvailable,
+ belowAvailable,
+ aboveRightAvailable,
+ belowLeftAvailable,
+ in kernel);
+ break;
+ }
+ }
+
+ ///
+ /// Applies band offsets to every sample in a component block.
+ ///
+ /// The immutable pre-SAO picture.
+ /// The destination picture.
+ /// The component plane.
+ /// The block's left coordinate.
+ /// The block's top coordinate.
+ /// The block width.
+ /// The block height.
+ /// The scaled offset and band-class state.
+ private static void ApplyBand(
+ HevcPictureBuffer source,
+ HevcPictureBuffer destination,
+ HevcPlane plane,
+ int x,
+ int y,
+ int width,
+ int height,
+ in KernelParameters kernel)
+ {
+ for (int row = y; row < y + height; row++)
+ {
+ ReadOnlySpan sourceRow = source.GetRowSpan(plane, row).Slice(x, width);
+ Span destinationRow = destination.GetRowSpan(plane, row).Slice(x, width);
+
+ // Band classification depends only on the current sample. The closed classifier's UsesNeighbors value removes
+ // the two neighbor loads when this generic traversal is specialized for BandClassifier.
+ ApplyRow(sourceRow, sourceRow, sourceRow, destinationRow, in kernel);
+ }
+ }
+
+ ///
+ /// Applies horizontal edge offsets within the available left and right boundaries.
+ ///
+ /// The immutable pre-SAO picture.
+ /// The destination picture.
+ /// The component plane.
+ /// The block's left coordinate.
+ /// The block's top coordinate.
+ /// The block width.
+ /// The block height.
+ /// Whether the left neighboring block is available.
+ /// Whether the right neighboring block is available.
+ /// The scaled offset state.
+ private static void ApplyHorizontalEdges(
+ HevcPictureBuffer source,
+ HevcPictureBuffer destination,
+ HevcPlane plane,
+ int x,
+ int y,
+ int width,
+ int height,
+ bool leftAvailable,
+ bool rightAvailable,
+ in KernelParameters kernel)
+ {
+ int start = x + (leftAvailable ? 0 : 1);
+ int end = x + width - (rightAvailable ? 0 : 1);
+ int count = end - start;
+ if (count <= 0)
+ {
+ return;
+ }
+
+ for (int row = y; row < y + height; row++)
+ {
+ ReadOnlySpan sourceRow = source.GetRowSpan(plane, row);
+ ApplyRow(
+ sourceRow.Slice(start, count),
+ sourceRow.Slice(start - 1, count),
+ sourceRow.Slice(start + 1, count),
+ destination.GetRowSpan(plane, row).Slice(start, count),
+ in kernel);
+ }
+ }
+
+ ///
+ /// Applies vertical edge offsets within the available upper and lower boundaries.
+ ///
+ /// The immutable pre-SAO picture.
+ /// The destination picture.
+ /// The component plane.
+ /// The block's left coordinate.
+ /// The block's top coordinate.
+ /// The block width.
+ /// The block height.
+ /// Whether the upper neighboring block is available.
+ /// Whether the lower neighboring block is available.
+ /// The scaled offset state.
+ private static void ApplyVerticalEdges(
+ HevcPictureBuffer source,
+ HevcPictureBuffer destination,
+ HevcPlane plane,
+ int x,
+ int y,
+ int width,
+ int height,
+ bool aboveAvailable,
+ bool belowAvailable,
+ in KernelParameters kernel)
+ {
+ int start = y + (aboveAvailable ? 0 : 1);
+ int end = y + height - (belowAvailable ? 0 : 1);
+ for (int row = start; row < end; row++)
+ {
+ ApplyRow(
+ source.GetRowSpan(plane, row).Slice(x, width),
+ source.GetRowSpan(plane, row - 1).Slice(x, width),
+ source.GetRowSpan(plane, row + 1).Slice(x, width),
+ destination.GetRowSpan(plane, row).Slice(x, width),
+ in kernel);
+ }
+ }
+
+ ///
+ /// Applies descending-diagonal edge offsets within the eight resolved block boundaries.
+ ///
+ /// The immutable pre-SAO picture.
+ /// The destination picture.
+ /// The component plane.
+ /// The block's left coordinate.
+ /// The block's top coordinate.
+ /// The block width.
+ /// The block height.
+ /// Whether the left neighboring block is available.
+ /// Whether the right neighboring block is available.
+ /// Whether the upper neighboring block is available.
+ /// Whether the lower neighboring block is available.
+ /// Whether the upper-left neighboring block is available.
+ /// Whether the lower-right neighboring block is available.
+ /// The scaled offset state.
+ private static void ApplyDescendingEdges(
+ HevcPictureBuffer source,
+ HevcPictureBuffer destination,
+ HevcPlane plane,
+ int x,
+ int y,
+ int width,
+ int height,
+ bool leftAvailable,
+ bool rightAvailable,
+ bool aboveAvailable,
+ bool belowAvailable,
+ bool aboveLeftAvailable,
+ bool belowRightAvailable,
+ in KernelParameters kernel)
+ {
+ int commonStart = x + (leftAvailable ? 0 : 1);
+ int commonEnd = x + width - (rightAvailable ? 0 : 1);
+ int lastRow = y + height - 1;
+ for (int row = y; row <= lastRow; row++)
+ {
+ int start = commonStart;
+ int end = commonEnd;
+ if (row == y)
+ {
+ start = aboveLeftAvailable ? x : x + 1;
+ end = aboveAvailable ? commonEnd : x + 1;
+ }
+
+ if (row == lastRow)
+ {
+ start = Math.Max(start, belowAvailable ? commonStart : x + width - 1);
+ end = Math.Min(end, belowRightAvailable ? x + width : x + width - 1);
+ }
+
+ int count = end - start;
+ if (count <= 0)
+ {
+ continue;
+ }
+
+ ApplyRow(
+ source.GetRowSpan(plane, row).Slice(start, count),
+ source.GetRowSpan(plane, row - 1).Slice(start - 1, count),
+ source.GetRowSpan(plane, row + 1).Slice(start + 1, count),
+ destination.GetRowSpan(plane, row).Slice(start, count),
+ in kernel);
+ }
+ }
+
+ ///
+ /// Applies ascending-diagonal edge offsets within the eight resolved block boundaries.
+ ///
+ /// The immutable pre-SAO picture.
+ /// The destination picture.
+ /// The component plane.
+ /// The block's left coordinate.
+ /// The block's top coordinate.
+ /// The block width.
+ /// The block height.
+ /// Whether the left neighboring block is available.
+ /// Whether the right neighboring block is available.
+ /// Whether the upper neighboring block is available.
+ /// Whether the lower neighboring block is available.
+ /// Whether the upper-right neighboring block is available.
+ /// Whether the lower-left neighboring block is available.
+ /// The scaled offset state.
+ private static void ApplyAscendingEdges(
+ HevcPictureBuffer source,
+ HevcPictureBuffer destination,
+ HevcPlane plane,
+ int x,
+ int y,
+ int width,
+ int height,
+ bool leftAvailable,
+ bool rightAvailable,
+ bool aboveAvailable,
+ bool belowAvailable,
+ bool aboveRightAvailable,
+ bool belowLeftAvailable,
+ in KernelParameters kernel)
+ {
+ int commonStart = x + (leftAvailable ? 0 : 1);
+ int commonEnd = x + width - (rightAvailable ? 0 : 1);
+ int lastRow = y + height - 1;
+ for (int row = y; row <= lastRow; row++)
+ {
+ int start = commonStart;
+ int end = commonEnd;
+ if (row == y)
+ {
+ start = aboveAvailable ? commonStart : x + width - 1;
+ end = aboveRightAvailable ? x + width : x + width - 1;
+ }
+
+ if (row == lastRow)
+ {
+ start = Math.Max(start, belowLeftAvailable ? x : x + 1);
+ end = Math.Min(end, belowAvailable ? commonEnd : x + 1);
+ }
+
+ int count = end - start;
+ if (count <= 0)
+ {
+ continue;
+ }
+
+ ApplyRow(
+ source.GetRowSpan(plane, row).Slice(start, count),
+ source.GetRowSpan(plane, row - 1).Slice(start + 1, count),
+ source.GetRowSpan(plane, row + 1).Slice(start - 1, count),
+ destination.GetRowSpan(plane, row).Slice(start, count),
+ in kernel);
+ }
+ }
+
+ ///
+ /// Applies one closed classifier to a contiguous row range using every accelerated SIMD width before the scalar tail.
+ ///
+ /// The band or edge classifier selected before entering the row.
+ /// The current source samples.
+ /// The first classifier input samples.
+ /// The second classifier input samples.
+ /// The destination samples.
+ /// The scaled offset and clamp state.
+ private static void ApplyRow(
+ ReadOnlySpan current,
+ ReadOnlySpan neighbor0,
+ ReadOnlySpan neighbor1,
+ Span destination,
+ in KernelParameters kernel)
+ where TClassifier : struct, ISampleClassifier
+ {
+ ref ushort currentBase = ref MemoryMarshal.GetReference(current);
+ ref ushort neighbor0Base = ref MemoryMarshal.GetReference(neighbor0);
+ ref ushort neighbor1Base = ref MemoryMarshal.GetReference(neighbor1);
+ ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
+ int index = 0;
+
+ // HEVC's exposed 8/10/12-bit profiles keep every sample and scaled offset inside Int16. Signed lanes therefore
+ // provide comparisons, addition, and saturation without the two widening stages an Int32 implementation needs.
+ if (Vector512.IsHardwareAccelerated)
+ {
+ Vector512 minimum = Vector512.Zero;
+ Vector512 maximum = Vector512.Create(kernel.Maximum);
+ for (; index <= current.Length - Vector512.Count; index += Vector512.Count)
+ {
+ Vector512 value = Vector512.LoadUnsafe(ref currentBase, (nuint)index).AsInt16();
+ Vector512 first = TClassifier.UsesNeighbors ? Vector512.LoadUnsafe(ref neighbor0Base, (nuint)index).AsInt16() : default;
+ Vector512 second = TClassifier.UsesNeighbors ? Vector512.LoadUnsafe(ref neighbor1Base, (nuint)index).AsInt16() : default;
+ Vector512 classes = TClassifier.Classify(value, first, second, in kernel);
+ Vector512 filtered = Vector512.Clamp(value + SelectOffset(classes, in kernel), minimum, maximum);
+ filtered.AsUInt16().StoreUnsafe(ref destinationBase, (nuint)index);
+ }
+ }
+
+ if (Vector256.IsHardwareAccelerated)
+ {
+ Vector256 minimum = Vector256.Zero;
+ Vector256 maximum = Vector256.Create(kernel.Maximum);
+ for (; index <= current.Length - Vector256.Count; index += Vector256.Count)
+ {
+ Vector256 value = Vector256.LoadUnsafe(ref currentBase, (nuint)index).AsInt16();
+ Vector256 first = TClassifier.UsesNeighbors ? Vector256.LoadUnsafe(ref neighbor0Base, (nuint)index).AsInt16() : default;
+ Vector256 second = TClassifier.UsesNeighbors ? Vector256.LoadUnsafe(ref neighbor1Base, (nuint)index).AsInt16() : default;
+ Vector256 classes = TClassifier.Classify(value, first, second, in kernel);
+ Vector256 filtered = Vector256.Clamp(value + SelectOffset(classes, in kernel), minimum, maximum);
+ filtered.AsUInt16().StoreUnsafe(ref destinationBase, (nuint)index);
+ }
+ }
+
+ if (Vector128.IsHardwareAccelerated)
+ {
+ Vector128 minimum = Vector128.Zero;
+ Vector128 maximum = Vector128.Create(kernel.Maximum);
+ for (; index <= current.Length - Vector128.Count; index += Vector128.Count)
+ {
+ Vector128 value = Vector128.LoadUnsafe(ref currentBase, (nuint)index).AsInt16();
+ Vector128 first = TClassifier.UsesNeighbors ? Vector128.LoadUnsafe(ref neighbor0Base, (nuint)index).AsInt16() : default;
+ Vector128 second = TClassifier.UsesNeighbors ? Vector128.LoadUnsafe(ref neighbor1Base, (nuint)index).AsInt16() : default;
+ Vector128 classes = TClassifier.Classify(value, first, second, in kernel);
+ Vector128 filtered = Vector128.Clamp(value + SelectOffset(classes, in kernel), minimum, maximum);
+ filtered.AsUInt16().StoreUnsafe(ref destinationBase, (nuint)index);
+ }
+ }
+
+ for (; index < current.Length; index++)
+ {
+ short currentValue = (short)Unsafe.Add(ref currentBase, index);
+ short first = TClassifier.UsesNeighbors ? (short)Unsafe.Add(ref neighbor0Base, index) : default;
+ short second = TClassifier.UsesNeighbors ? (short)Unsafe.Add(ref neighbor1Base, index) : default;
+ int offsetIndex = TClassifier.Classify(currentValue, first, second, in kernel);
+
+ int filtered = Unsafe.Add(ref currentBase, index) + SelectOffset(offsetIndex, in kernel);
+ Unsafe.Add(ref destinationBase, index) = (ushort)Math.Clamp(filtered, 0, kernel.Maximum);
+ }
+ }
+
+ ///
+ /// Selects one of five signed offsets for thirty-two classifier indices.
+ ///
+ /// The zero-based classifier indices.
+ /// The five scaled offsets.
+ /// The selected signed offset in every lane.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector512 SelectOffset(Vector512 classes, in KernelParameters kernel)
+ {
+ Vector512 selected = Vector512.Zero;
+ selected = Vector512.ConditionalSelect(Vector512.Equals(classes, Vector512.Create((short)0)), Vector512.Create(kernel.Offset0), selected);
+ selected = Vector512.ConditionalSelect(Vector512.Equals(classes, Vector512.Create((short)1)), Vector512.Create(kernel.Offset1), selected);
+ selected = Vector512.ConditionalSelect(Vector512.Equals(classes, Vector512.Create((short)2)), Vector512.Create(kernel.Offset2), selected);
+ selected = Vector512.ConditionalSelect(Vector512.Equals(classes, Vector512.Create((short)3)), Vector512.Create(kernel.Offset3), selected);
+ return Vector512.ConditionalSelect(Vector512.Equals(classes, Vector512.Create((short)4)), Vector512.Create(kernel.Offset4), selected);
+ }
+
+ ///
+ /// Selects one of five signed offsets for sixteen classifier indices.
+ ///
+ /// The zero-based classifier indices.
+ /// The five scaled offsets.
+ /// The selected signed offset in every lane.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector256 SelectOffset(Vector256 classes, in KernelParameters kernel)
+ {
+ Vector256 selected = Vector256.Zero;
+ selected = Vector256.ConditionalSelect(Vector256.Equals(classes, Vector256.Create((short)0)), Vector256.Create(kernel.Offset0), selected);
+ selected = Vector256.ConditionalSelect(Vector256.Equals(classes, Vector256.Create((short)1)), Vector256.Create(kernel.Offset1), selected);
+ selected = Vector256.ConditionalSelect(Vector256.Equals(classes, Vector256.Create((short)2)), Vector256.Create(kernel.Offset2), selected);
+ selected = Vector256.ConditionalSelect(Vector256.Equals(classes, Vector256.Create((short)3)), Vector256.Create(kernel.Offset3), selected);
+ return Vector256.ConditionalSelect(Vector256.Equals(classes, Vector256.Create((short)4)), Vector256.Create(kernel.Offset4), selected);
+ }
+
+ ///
+ /// Selects one of five signed offsets for eight classifier indices.
+ ///
+ /// The zero-based classifier indices.
+ /// The five scaled offsets.
+ /// The selected signed offset in every lane.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector128 SelectOffset(Vector128 classes, in KernelParameters kernel)
+ {
+ Vector128 selected = Vector128.Zero;
+ selected = Vector128.ConditionalSelect(Vector128.Equals(classes, Vector128.Create((short)0)), Vector128.Create(kernel.Offset0), selected);
+ selected = Vector128.ConditionalSelect(Vector128.Equals(classes, Vector128.Create((short)1)), Vector128.Create(kernel.Offset1), selected);
+ selected = Vector128.ConditionalSelect(Vector128.Equals(classes, Vector128.Create((short)2)), Vector128.Create(kernel.Offset2), selected);
+ selected = Vector128.ConditionalSelect(Vector128.Equals(classes, Vector128.Create((short)3)), Vector128.Create(kernel.Offset3), selected);
+ return Vector128.ConditionalSelect(Vector128.Equals(classes, Vector128.Create((short)4)), Vector128.Create(kernel.Offset4), selected);
+ }
+
+ ///
+ /// Selects one of five signed offsets for one classifier index.
+ ///
+ /// The zero-based classifier index.
+ /// The five scaled offsets.
+ /// The selected signed offset, or zero for an unmodified class.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int SelectOffset(int classification, in KernelParameters kernel)
+ => classification switch
+ {
+ 0 => kernel.Offset0,
+ 1 => kernel.Offset1,
+ 2 => kernel.Offset2,
+ 3 => kernel.Offset3,
+ 4 => kernel.Offset4,
+ _ => 0,
+ };
+
+ ///
+ /// Classifies samples by one of thirty-two most-significant-value bands.
+ ///
+ private readonly struct BandClassifier : ISampleClassifier
+ {
+ ///
+ public static bool UsesNeighbors => false;
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector512 Classify(
+ Vector512 current,
+ Vector512 neighbor0,
+ Vector512 neighbor1,
+ in KernelParameters kernel)
+ => (Vector512.ShiftRightArithmetic(current, kernel.BandShift) - Vector512.Create(kernel.BandPosition)) & Vector512.Create((short)31);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 Classify(
+ Vector256 current,
+ Vector256 neighbor0,
+ Vector256 neighbor1,
+ in KernelParameters kernel)
+ => (Vector256.ShiftRightArithmetic(current, kernel.BandShift) - Vector256.Create(kernel.BandPosition)) & Vector256.Create((short)31);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Classify(
+ Vector128 current,
+ Vector128 neighbor0,
+ Vector128 neighbor1,
+ in KernelParameters kernel)
+ => (Vector128.ShiftRightArithmetic(current, kernel.BandShift) - Vector128.Create(kernel.BandPosition)) & Vector128.Create((short)31);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int Classify(short current, short neighbor0, short neighbor1, in KernelParameters kernel)
+ => ((current >> kernel.BandShift) - kernel.BandPosition) & 31;
+ }
+
+ ///
+ /// Classifies samples by the sum of their signs relative to two directional neighbors.
+ ///
+ private readonly struct EdgeClassifier : ISampleClassifier
+ {
+ ///
+ public static bool UsesNeighbors => true;
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector512 Classify(
+ Vector512