From 115d7765ca5bf3d140ef3cfc28dcd4619e3195ab Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 26 Aug 2026 03:56:24 +1000 Subject: [PATCH] Implement HEVC intra picture reconstruction --- .../Formats/Heif/Hevc/HevcCabacContexts.cs | 14 +- .../Formats/Heif/Hevc/HevcCabacDecoder.cs | 67 ++ .../Heif/Hevc/HevcCabacSyntaxReader.cs | 30 + .../Formats/Heif/Hevc/HevcCodedBlockFlags.cs | 53 ++ .../Formats/Heif/Hevc/HevcCodingTreeState.cs | 38 + .../Hevc/HevcCoefficientCodingParameters.cs | 9 +- .../Heif/Hevc/HevcCoefficientDecoder.cs | 17 + .../Formats/Heif/Hevc/HevcDeblockingFilter.cs | 614 +++++++++++++++ .../Formats/Heif/Hevc/HevcDeblockingState.cs | 118 +++ .../Hevc/HevcInverseTransformer.Operations.cs | 2 +- .../Formats/Heif/Hevc/HevcPictureBuffer.cs | 35 +- .../Hevc/HevcPictureDecoder.Deblocking.cs | 390 ++++++++++ .../Hevc/HevcPictureDecoder.Prediction.cs | 216 ++++++ ...HevcPictureDecoder.SampleAdaptiveOffset.cs | 250 ++++++ .../Hevc/HevcPictureDecoder.TransformTree.cs | 562 ++++++++++++++ .../Heif/Hevc/HevcPictureDecoder.Traversal.cs | 364 +++++++++ .../Formats/Heif/Hevc/HevcPictureDecoder.cs | 299 +++++++ .../Heif/Hevc/HevcQuantizationParameters.cs | 14 +- .../Heif/Hevc/HevcReconstructionState.cs | 221 ++++++ .../Heif/Hevc/HevcResidualReconstructor.cs | 107 +++ .../Hevc/HevcSampleAdaptiveOffsetFilter.cs | 733 ++++++++++++++++++ .../HevcSampleAdaptiveOffsetParameters.cs | 443 +++++++++++ .../Heif/Hevc/HevcSliceSegmentHeader.cs | 75 +- .../Formats/Heif/Hevc/HevcTileLayout.cs | 196 +++++ .../Hevc/HevcTransformComponentGeometry.cs | 59 ++ .../Heif/Hevc/HevcTransformUnitGeometry.cs | 158 ++++ .../Heif/Hevc/HevcCabacDecoderTests.cs | 52 ++ .../Hevc/HevcResidualReconstructorTests.cs | 33 + .../Formats/Heif/Hevc/HevcTileLayoutTests.cs | 65 ++ .../Hevc/HevcTransformUnitGeometryTests.cs | 74 ++ 30 files changed, 5293 insertions(+), 15 deletions(-) create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcCodedBlockFlags.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingFilter.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingState.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Deblocking.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Prediction.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.SampleAdaptiveOffset.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.TransformTree.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Traversal.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcReconstructionState.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetFilter.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetParameters.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcTileLayout.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcTransformComponentGeometry.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/HevcTransformUnitGeometry.cs create mode 100644 tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcCabacDecoderTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcTileLayoutTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcTransformUnitGeometryTests.cs 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 current, + Vector512 neighbor0, + Vector512 neighbor1, + in KernelParameters kernel) + { + Vector512 one = Vector512.Create((short)1); + Vector512 sign0 = (Vector512.GreaterThan(current, neighbor0) & one) - (Vector512.LessThan(current, neighbor0) & one); + Vector512 sign1 = (Vector512.GreaterThan(current, neighbor1) & one) - (Vector512.LessThan(current, neighbor1) & one); + return sign0 + sign1 + Vector512.Create((short)2); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Classify( + Vector256 current, + Vector256 neighbor0, + Vector256 neighbor1, + in KernelParameters kernel) + { + Vector256 one = Vector256.Create((short)1); + Vector256 sign0 = (Vector256.GreaterThan(current, neighbor0) & one) - (Vector256.LessThan(current, neighbor0) & one); + Vector256 sign1 = (Vector256.GreaterThan(current, neighbor1) & one) - (Vector256.LessThan(current, neighbor1) & one); + return sign0 + sign1 + Vector256.Create((short)2); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Classify( + Vector128 current, + Vector128 neighbor0, + Vector128 neighbor1, + in KernelParameters kernel) + { + Vector128 one = Vector128.Create((short)1); + Vector128 sign0 = (Vector128.GreaterThan(current, neighbor0) & one) - (Vector128.LessThan(current, neighbor0) & one); + Vector128 sign1 = (Vector128.GreaterThan(current, neighbor1) & one) - (Vector128.LessThan(current, neighbor1) & one); + return sign0 + sign1 + Vector128.Create((short)2); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Classify(short current, short neighbor0, short neighbor1, in KernelParameters kernel) + => Math.Sign(current - neighbor0) + Math.Sign(current - neighbor1) + 2; + } + + /// + /// Contains one block's scaled offsets and invariant classification values. + /// + private readonly struct KernelParameters + { + /// + /// Initializes a new instance of the struct. + /// + /// The decoded signed offsets. + /// The component sample precision. + /// The component offset scale. + public KernelParameters(in HevcSampleAdaptiveOffsetParameters parameters, int bitDepth, int offsetScaleLog2) + { + // Range Extensions scales each coded offset once before filtering. Hoisting the shifts here keeps the + // classification loops to comparisons, table selection, one addition, and saturation. + this.Offset0 = (short)(parameters.Offset0 << offsetScaleLog2); + this.Offset1 = (short)(parameters.Offset1 << offsetScaleLog2); + this.Offset2 = (short)(parameters.Offset2 << offsetScaleLog2); + this.Offset3 = (short)(parameters.Offset3 << offsetScaleLog2); + this.Offset4 = (short)(parameters.Offset4 << offsetScaleLog2); + this.BandPosition = (short)parameters.BandPosition; + this.BandShift = bitDepth - 5; + this.Maximum = (short)((1 << bitDepth) - 1); + } + + /// + /// Gets the first scaled class offset. + /// + public short Offset0 { get; } + + /// + /// Gets the second scaled class offset. + /// + public short Offset1 { get; } + + /// + /// Gets the third scaled class offset. + /// + public short Offset2 { get; } + + /// + /// Gets the fourth scaled class offset. + /// + public short Offset3 { get; } + + /// + /// Gets the fifth scaled class offset. + /// + public short Offset4 { get; } + + /// + /// Gets the first active band class. + /// + public short BandPosition { get; } + + /// + /// Gets the number of low sample bits discarded to form one of thirty-two band classes. + /// + public int BandShift { get; } + + /// + /// Gets the largest component sample value. + /// + public short Maximum { get; } + } +} diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetParameters.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetParameters.cs new file mode 100644 index 000000000..bd7e6a675 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetParameters.cs @@ -0,0 +1,443 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc; + +/// +/// Identifies the HEVC sample-adaptive-offset classifier selected for one component coding-tree block. +/// +internal enum HevcSampleAdaptiveOffsetType : byte +{ + /// + /// No sample-adaptive offset is applied. + /// + Off, + + /// + /// Samples are classified by their most-significant sample-value band. + /// + Band, + + /// + /// Samples are classified by horizontal neighboring samples. + /// + EdgeHorizontal, + + /// + /// Samples are classified by vertical neighboring samples. + /// + EdgeVertical, + + /// + /// Samples are classified by neighbors on the descending diagonal. + /// + EdgeDescending, + + /// + /// Samples are classified by neighbors on the ascending diagonal. + /// + EdgeAscending, +} + +/// +/// Contains the resolved HEVC sample-adaptive offsets for one component coding-tree block. +/// +internal readonly struct HevcSampleAdaptiveOffsetParameters +{ + /// + /// Initializes a new instance of the struct. + /// + /// The sample classifier. + /// The first of four consecutive band classes. + /// The first band or full-valley offset. + /// The second band or half-valley offset. + /// The third band or plain-edge offset. + /// The fourth band or half-peak offset. + /// The full-peak offset. + public HevcSampleAdaptiveOffsetParameters( + HevcSampleAdaptiveOffsetType type, + int bandPosition, + int offset0, + int offset1, + int offset2, + int offset3, + int offset4) + { + this.Type = type; + this.BandPosition = bandPosition; + this.Offset0 = offset0; + this.Offset1 = offset1; + this.Offset2 = offset2; + this.Offset3 = offset3; + this.Offset4 = offset4; + } + + /// + /// Gets the sample classifier. + /// + public HevcSampleAdaptiveOffsetType Type { get; } + + /// + /// Gets the first of four consecutive band classes. + /// + public int BandPosition { get; } + + /// + /// Gets the first band or full-valley offset. + /// + public int Offset0 { get; } + + /// + /// Gets the second band or half-valley offset. + /// + public int Offset1 { get; } + + /// + /// Gets the third band or plain-edge offset. + /// + public int Offset2 { get; } + + /// + /// Gets the fourth band or half-peak offset. + /// + public int Offset3 { get; } + + /// + /// Gets the full-peak offset. + /// + public int Offset4 { get; } +} + +/// +/// Identifies the slice and tile governing in-loop filtering for one coding-tree block. +/// +internal readonly struct HevcLoopFilterRegion +{ + /// + /// Initializes a new instance of the struct. + /// + /// The first coding-tree block of the independent slice in tile-scan order. + /// The zero-based tile index. + /// Whether the governing slice permits filtering across its slice boundary. + /// Whether the governing slice disables deblocking. + /// Half the slice beta-threshold offset. + /// Half the slice clipping-threshold offset. + public HevcLoopFilterRegion( + int sliceStartAddressInTileScan, + int tileIndex, + bool loopFilterAcrossSlicesEnabled, + bool deblockingFilterDisabled, + int deblockingFilterBetaOffsetDiv2, + int deblockingFilterTcOffsetDiv2) + { + this.SliceStartAddressInTileScan = sliceStartAddressInTileScan; + this.TileIndex = tileIndex; + this.LoopFilterAcrossSlicesEnabled = loopFilterAcrossSlicesEnabled; + this.DeblockingFilterDisabled = deblockingFilterDisabled; + this.DeblockingFilterBetaOffsetDiv2 = deblockingFilterBetaOffsetDiv2; + this.DeblockingFilterTcOffsetDiv2 = deblockingFilterTcOffsetDiv2; + } + + /// + /// Gets the first coding-tree block of the independent slice in tile-scan order. + /// + public int SliceStartAddressInTileScan { get; } + + /// + /// Gets the zero-based tile index. + /// + public int TileIndex { get; } + + /// + /// Gets a value indicating whether the governing slice permits filtering across its slice boundary. + /// + public bool LoopFilterAcrossSlicesEnabled { get; } + + /// + /// Gets a value indicating whether the governing slice disables deblocking. + /// + public bool DeblockingFilterDisabled { get; } + + /// + /// Gets half the governing slice's beta-threshold offset. + /// + public int DeblockingFilterBetaOffsetDiv2 { get; } + + /// + /// Gets half the governing slice's clipping-threshold offset. + /// + public int DeblockingFilterTcOffsetDiv2 { get; } +} + +/// +/// Contains the eight coding-tree-block neighbor availability values used by HEVC in-loop filters. +/// +internal readonly struct HevcLoopFilterBoundaryAvailability +{ + /// + /// Initializes a new instance of the struct. + /// + /// Whether the left block is available. + /// Whether the right block is available. + /// Whether the block above is available. + /// Whether the block below is available. + /// Whether the upper-left block is available. + /// Whether the upper-right block is available. + /// Whether the lower-left block is available. + /// Whether the lower-right block is available. + public HevcLoopFilterBoundaryAvailability( + bool left, + bool right, + bool above, + bool below, + bool aboveLeft, + bool aboveRight, + bool belowLeft, + bool belowRight) + { + this.Left = left; + this.Right = right; + this.Above = above; + this.Below = below; + this.AboveLeft = aboveLeft; + this.AboveRight = aboveRight; + this.BelowLeft = belowLeft; + this.BelowRight = belowRight; + } + + /// + /// Gets a value indicating whether the left block is available. + /// + public bool Left { get; } + + /// + /// Gets a value indicating whether the right block is available. + /// + public bool Right { get; } + + /// + /// Gets a value indicating whether the block above is available. + /// + public bool Above { get; } + + /// + /// Gets a value indicating whether the block below is available. + /// + public bool Below { get; } + + /// + /// Gets a value indicating whether the upper-left block is available. + /// + public bool AboveLeft { get; } + + /// + /// Gets a value indicating whether the upper-right block is available. + /// + public bool AboveRight { get; } + + /// + /// Gets a value indicating whether the lower-left block is available. + /// + public bool BelowLeft { get; } + + /// + /// Gets a value indicating whether the lower-right block is available. + /// + public bool BelowRight { get; } +} + +/// +/// Owns resolved sample-adaptive-offset parameters and prediction and filter region identifiers for one picture. +/// +internal sealed class HevcSampleAdaptiveOffsetState : IDisposable +{ + /// + /// Whether any decoded component block enables sample-adaptive offset. + /// + private bool hasEnabledParameters; + + /// + /// The three component records for every raster-ordered coding-tree block. + /// + private readonly IMemoryOwner parameters; + + /// + /// The independent-slice and tile prediction region of every coding-tree block. + /// + private readonly IMemoryOwner regions; + + /// + /// The independent-slice and tile filter region of every coding-tree block and color plane. + /// + private readonly IMemoryOwner loopFilterRegions; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration providing pooled picture state. + /// The raster-ordered coding-tree-block count. + public HevcSampleAdaptiveOffsetState(Configuration configuration, int codingTreeBlockCount) + { + this.parameters = configuration.MemoryAllocator.Allocate(codingTreeBlockCount * 3); + this.regions = configuration.MemoryAllocator.Allocate(codingTreeBlockCount * 3); + this.loopFilterRegions = configuration.MemoryAllocator.Allocate(codingTreeBlockCount * 3); + } + + /// + /// Gets a value indicating whether any component block enables sample-adaptive offset. + /// + public bool HasEnabledParameters => this.hasEnabledParameters; + + /// + /// Gets the resolved component parameters for one coding-tree block. + /// + /// The raster-scan coding-tree-block address. + /// The component plane. + /// The resolved sample-adaptive-offset parameters. + public HevcSampleAdaptiveOffsetParameters Get(int rasterAddress, HevcPlane plane) + => this.parameters.Memory.Span[(rasterAddress * 3) + (int)plane]; + + /// + /// Stores resolved component parameters for one coding-tree block. + /// + /// The raster-scan coding-tree-block address. + /// The component plane. + /// The resolved sample-adaptive-offset parameters. + public void Set(int rasterAddress, HevcPlane plane, HevcSampleAdaptiveOffsetParameters value) + { + this.parameters.Memory.Span[(rasterAddress * 3) + (int)plane] = value; + this.hasEnabledParameters |= value.Type != HevcSampleAdaptiveOffsetType.Off; + } + + /// + /// Gets whether one coding-tree block belongs to the selected prediction region. + /// + /// The raster-scan coding-tree-block address. + /// The independently coded color plane, or luma for combined-plane coding. + /// The current independent-slice and tile prediction-region identifier. + /// when the block belongs to the region; otherwise, . + public bool IsInRegion(int rasterAddress, HevcPlane plane, int regionId) + => this.regions.Memory.Span[(rasterAddress * 3) + (int)plane] == regionId; + + /// + /// Records the prediction region after one coding-tree block's parameters are decoded. + /// + /// The raster-scan coding-tree-block address. + /// The independently coded color plane, or luma for combined-plane coding. + /// The positive prediction-region identifier. + public void SetRegion(int rasterAddress, HevcPlane plane, int regionId) + => this.regions.Memory.Span[(rasterAddress * 3) + (int)plane] = regionId; + + /// + /// Records the in-loop filter region for one coding-tree block. + /// + /// The raster-scan coding-tree-block address. + /// The independently coded color plane, or luma for combined-plane coding. + /// The governing independent-slice and tile state. + public void SetLoopFilterRegion(int rasterAddress, HevcPlane plane, HevcLoopFilterRegion value) + => this.loopFilterRegions.Memory.Span[(rasterAddress * 3) + (int)plane] = value; + + /// + /// Derives the picture, slice, and tile boundary availability used by the in-loop filters for one coding-tree block. + /// + /// The raster-scan coding-tree-block address. + /// The independently coded color plane, or luma for combined-plane coding. + /// The picture width in coding-tree blocks. + /// The picture height in coding-tree blocks. + /// Whether the picture permits filtering across tile boundaries. + /// The availability of all eight neighboring coding-tree blocks. + public HevcLoopFilterBoundaryAvailability GetLoopFilterBoundaryAvailability( + int rasterAddress, + HevcPlane plane, + int pictureWidth, + int pictureHeight, + bool loopFilterAcrossTilesEnabled) + { + int x = rasterAddress % pictureWidth; + int y = rasterAddress / pictureWidth; + HevcLoopFilterRegion current = this.GetLoopFilterRegion(rasterAddress, plane); + + // H.265 assigns left, above, and upper-left boundaries to the current slice, while right, below, and + // lower-right boundaries belong to the neighboring slice. This asymmetry makes filtering independent of CTB order. + bool left = x > 0 + && IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress - 1, plane), true, loopFilterAcrossTilesEnabled); + bool right = x + 1 < pictureWidth + && IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress + 1, plane), false, loopFilterAcrossTilesEnabled); + bool above = y > 0 + && IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress - pictureWidth, plane), true, loopFilterAcrossTilesEnabled); + bool below = y + 1 < pictureHeight + && IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress + pictureWidth, plane), false, loopFilterAcrossTilesEnabled); + bool aboveLeft = x > 0 && y > 0 + && IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress - pictureWidth - 1, plane), true, loopFilterAcrossTilesEnabled); + bool belowRight = x + 1 < pictureWidth && y + 1 < pictureHeight + && IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress + pictureWidth + 1, plane), false, loopFilterAcrossTilesEnabled); + + // The crossed diagonals do not have a fixed owner in raster order. The later independent slice owns the + // boundary flag, which is identified by its greater tile-scan start address. + bool aboveRight = x + 1 < pictureWidth && y > 0 + && IsLoopFilterDiagonalAvailable(current, this.GetLoopFilterRegion(rasterAddress - pictureWidth + 1, plane), loopFilterAcrossTilesEnabled); + bool belowLeft = x > 0 && y + 1 < pictureHeight + && IsLoopFilterDiagonalAvailable(current, this.GetLoopFilterRegion(rasterAddress + pictureWidth - 1, plane), loopFilterAcrossTilesEnabled); + + return new HevcLoopFilterBoundaryAvailability(left, right, above, below, aboveLeft, aboveRight, belowLeft, belowRight); + } + + /// + /// Gets the retained in-loop filter region for one coding-tree block. + /// + /// The raster-scan coding-tree-block address. + /// The independently coded color plane, or luma for combined-plane coding. + /// The retained slice and tile state. + public HevcLoopFilterRegion GetLoopFilterRegion(int rasterAddress, HevcPlane plane) + => this.loopFilterRegions.Memory.Span[(rasterAddress * 3) + (int)plane]; + + /// + /// Determines availability across a boundary with a direction-selected slice owner. + /// + /// The current block's region. + /// The neighboring block's region. + /// Whether the current slice controls a boundary between different slices. + /// Whether tile boundaries permit filtering. + /// when both slice and tile rules permit filtering. + private static bool IsLoopFilterNeighborAvailable( + HevcLoopFilterRegion current, + HevcLoopFilterRegion neighbor, + bool currentOwnsSliceBoundary, + bool loopFilterAcrossTilesEnabled) + { + bool sameSlice = current.SliceStartAddressInTileScan == neighbor.SliceStartAddressInTileScan; + bool sliceAvailable = sameSlice + || (currentOwnsSliceBoundary ? current.LoopFilterAcrossSlicesEnabled : neighbor.LoopFilterAcrossSlicesEnabled); + + return sliceAvailable && (loopFilterAcrossTilesEnabled || current.TileIndex == neighbor.TileIndex); + } + + /// + /// Determines availability across a crossed-diagonal boundary using the later slice as its owner. + /// + /// The current block's region. + /// The diagonally neighboring block's region. + /// Whether tile boundaries permit filtering. + /// when both slice and tile rules permit filtering. + private static bool IsLoopFilterDiagonalAvailable( + HevcLoopFilterRegion current, + HevcLoopFilterRegion neighbor, + bool loopFilterAcrossTilesEnabled) + { + bool currentOwnsSliceBoundary = current.SliceStartAddressInTileScan > neighbor.SliceStartAddressInTileScan; + return IsLoopFilterNeighborAvailable(current, neighbor, currentOwnsSliceBoundary, loopFilterAcrossTilesEnabled); + } + + /// + /// Releases the pooled sample-adaptive-offset picture state. + /// + public void Dispose() + { + this.loopFilterRegions.Dispose(); + this.regions.Dispose(); + this.parameters.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs index 085e900ae..013c64be3 100644 --- a/src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs @@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Formats.Heif.Hevc; /// internal sealed class HevcSliceSegmentHeader { + /// + /// The decoded-byte lengths preceding each tile or wavefront entropy entry point. + /// + private int[] entryPointOffsets = []; + /// /// Initializes a new instance of the class. /// @@ -119,14 +124,26 @@ internal sealed class HevcSliceSegmentHeader int availableEncodedData = nalUnit.EncodedPayloadLength - encodedHeaderLength; int cumulativeEntryPointOffset = 0; - foreach (int entryPointOffset in this.EntryPointOffsets) + int previousDecodedBoundary = this.HeaderLength; + int[] entryPointOffsets = this.entryPointOffsets; + for (int index = 0; index < entryPointOffsets.Length; index++) { + int entryPointOffset = entryPointOffsets[index]; if (cumulativeEntryPointOffset > availableEncodedData - entryPointOffset) { throw new InvalidImageContentException("The HEVC slice entry point extends beyond its NAL unit."); } cumulativeEntryPointOffset += entryPointOffset; + int encodedBoundary = encodedHeaderLength + cumulativeEntryPointOffset; + int decodedBoundary = GetDecodedPayloadOffset( + encodedBoundary, + nalUnit.EmulationPreventionBytePositions.Span); + + // entry_point_offset_minus1 counts encoded NAL bytes. The entropy decoder consumes the de-escaped RBSP, + // so each retained substream length must exclude prevention bytes from its own encoded interval. + entryPointOffsets[index] = decodedBoundary - previousDecodedBoundary; + previousDecodedBoundary = decodedBoundary; } } @@ -216,9 +233,14 @@ internal sealed class HevcSliceSegmentHeader public bool? LoopFilterAcrossSlicesEnabled { get; private set; } /// - /// Gets the encoded-byte lengths that separate tile or wavefront entropy substreams after the first substream. + /// Gets the decoded-byte lengths that separate tile or wavefront entropy substreams after the first substream. + /// + public IReadOnlyList EntryPointOffsets => this.entryPointOffsets; + + /// + /// Gets the number of independently initialized tile or wavefront entropy substreams in this slice segment. /// - public IReadOnlyList EntryPointOffsets { get; private set; } = Array.Empty(); + public int EntropySubstreamCount => this.entryPointOffsets.Length + 1; /// /// Gets the slice-header length in decoded raw-byte-sequence payload bytes. @@ -230,6 +252,27 @@ internal sealed class HevcSliceSegmentHeader /// public ReadOnlyMemory SliceData { get; } + /// + /// Gets one bounded entropy substream in slice coding order. + /// + /// The zero-based entropy-substream index. + /// The decoded raw-byte-sequence payload bytes belonging to the selected substream. + public ReadOnlyMemory GetEntropySubstream(int index) + { + DebugGuard.MustBeBetweenOrEqualTo(index, 0, this.entryPointOffsets.Length, nameof(index)); + int offset = 0; + for (int precedingIndex = 0; precedingIndex < index; precedingIndex++) + { + offset += this.entryPointOffsets[precedingIndex]; + } + + int length = index < this.entryPointOffsets.Length + ? this.entryPointOffsets[index] + : this.SliceData.Length - offset; + + return this.SliceData.Slice(offset, length); + } + /// /// Reads fields carried only by an independent slice-segment header. /// @@ -398,7 +441,7 @@ internal sealed class HevcSliceSegmentHeader entryPointOffsets[entryPoint] = (int)entryPointOffsetMinusOne + 1; } - this.EntryPointOffsets = entryPointOffsets; + this.entryPointOffsets = entryPointOffsets; } /// @@ -424,4 +467,28 @@ internal sealed class HevcSliceSegmentHeader return encodedOffset; } + + /// + /// Converts an encoded-payload byte boundary to its corresponding RBSP boundary. + /// + /// The encoded byte-sequence payload offset. + /// The removed encoded-payload byte positions. + /// The decoded raw-byte-sequence payload offset at the same syntax boundary. + private static int GetDecodedPayloadOffset( + int encodedOffset, + ReadOnlySpan emulationPreventionBytePositions) + { + int decodedOffset = encodedOffset; + foreach (int preventionBytePosition in emulationPreventionBytePositions) + { + if (preventionBytePosition >= encodedOffset) + { + break; + } + + decodedOffset--; + } + + return decodedOffset; + } } diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcTileLayout.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcTileLayout.cs new file mode 100644 index 000000000..83bd366c7 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcTileLayout.cs @@ -0,0 +1,196 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc; + +/// +/// Maps HEVC coding-tree blocks between picture raster order and tile-scan order. +/// +internal readonly struct HevcTileLayout +{ + /// + /// The tile widths in coding-tree blocks. + /// + private readonly IReadOnlyList columnWidths; + + /// + /// The tile heights in coding-tree blocks. + /// + private readonly IReadOnlyList rowHeights; + + /// + /// Initializes a new instance of the struct. + /// + /// The picture tile geometry. + public HevcTileLayout(HevcPictureParameterSet pictureParameterSet) + : this(pictureParameterSet.TileColumnWidths, pictureParameterSet.TileRowHeights) + { + } + + /// + /// Initializes a new instance of the struct from validated tile dimensions. + /// + /// The tile-column widths in coding-tree blocks. + /// The tile-row heights in coding-tree blocks. + public HevcTileLayout(IReadOnlyList columnWidths, IReadOnlyList rowHeights) + { + this.columnWidths = columnWidths; + this.rowHeights = rowHeights; + this.ColumnCount = this.columnWidths.Count; + this.RowCount = this.rowHeights.Count; + this.Width = Sum(this.columnWidths); + this.Height = Sum(this.rowHeights); + } + + /// + /// Gets the number of tile columns. + /// + public int ColumnCount { get; } + + /// + /// Gets the number of tile rows. + /// + public int RowCount { get; } + + /// + /// Gets the picture width in coding-tree blocks. + /// + public int Width { get; } + + /// + /// Gets the picture height in coding-tree blocks. + /// + public int Height { get; } + + /// + /// Gets the number of tiles in the picture. + /// + public int TileCount => this.ColumnCount * this.RowCount; + + /// + /// Converts a picture raster-scan address to tile-scan order. + /// + /// The raster-scan coding-tree-block address. + /// The corresponding tile-scan address. + public int GetTileScanAddress(int rasterAddress) + { + int x = rasterAddress % this.Width; + int y = rasterAddress / this.Width; + this.FindTile(x, y, out int tileColumn, out int tileRow, out int tileStartX, out int tileStartY); + int address = 0; + for (int row = 0; row < tileRow; row++) + { + address += this.rowHeights[row] * this.Width; + } + + for (int column = 0; column < tileColumn; column++) + { + address += this.columnWidths[column] * this.rowHeights[tileRow]; + } + + return address + ((y - tileStartY) * this.columnWidths[tileColumn]) + x - tileStartX; + } + + /// + /// Converts a tile-scan coding-tree-block address to picture raster order. + /// + /// The tile-scan address. + /// The corresponding raster-scan address. + public int GetRasterAddress(int tileScanAddress) + { + int remaining = tileScanAddress; + int tileStartY = 0; + for (int tileRow = 0; tileRow < this.RowCount; tileRow++) + { + int tileStartX = 0; + for (int tileColumn = 0; tileColumn < this.ColumnCount; tileColumn++) + { + int tileWidth = this.columnWidths[tileColumn]; + int tileHeight = this.rowHeights[tileRow]; + int tileArea = tileWidth * tileHeight; + if (remaining < tileArea) + { + int x = tileStartX + (remaining % tileWidth); + int y = tileStartY + (remaining / tileWidth); + return (y * this.Width) + x; + } + + remaining -= tileArea; + tileStartX += tileWidth; + } + + tileStartY += this.rowHeights[tileRow]; + } + + return this.Width * this.Height; + } + + /// + /// Gets the tile and tile-local position of one raster-scan coding-tree block. + /// + /// The raster-scan address. + /// The zero-based tile index. + /// The horizontal coding-tree-block offset within the tile. + /// The vertical coding-tree-block offset within the tile. + /// The tile width in coding-tree blocks. + /// The tile height in coding-tree blocks. + public void GetTilePosition( + int rasterAddress, + out int tileIndex, + out int columnInTile, + out int rowInTile, + out int tileWidth, + out int tileHeight) + { + int x = rasterAddress % this.Width; + int y = rasterAddress / this.Width; + this.FindTile(x, y, out int tileColumn, out int tileRow, out int tileStartX, out int tileStartY); + tileIndex = (tileRow * this.ColumnCount) + tileColumn; + columnInTile = x - tileStartX; + rowInTile = y - tileStartY; + tileWidth = this.columnWidths[tileColumn]; + tileHeight = this.rowHeights[tileRow]; + } + + /// + /// Locates the tile containing one coding-tree-block coordinate. + /// + /// The raster coding-tree-block X coordinate. + /// The raster coding-tree-block Y coordinate. + /// The containing tile column. + /// The containing tile row. + /// The containing tile's left coding-tree-block coordinate. + /// The containing tile's top coding-tree-block coordinate. + private void FindTile(int x, int y, out int tileColumn, out int tileRow, out int tileStartX, out int tileStartY) + { + tileStartX = 0; + tileColumn = 0; + while (x >= tileStartX + this.columnWidths[tileColumn]) + { + tileStartX += this.columnWidths[tileColumn++]; + } + + tileStartY = 0; + tileRow = 0; + while (y >= tileStartY + this.rowHeights[tileRow]) + { + tileStartY += this.rowHeights[tileRow++]; + } + } + + /// + /// Sums one complete tile dimension. + /// + /// The tile widths or heights. + /// The complete picture dimension in coding-tree blocks. + private static int Sum(IReadOnlyList values) + { + int sum = 0; + foreach (int value in values) + { + sum += value; + } + + return sum; + } +} diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcTransformComponentGeometry.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcTransformComponentGeometry.cs new file mode 100644 index 000000000..1725946c1 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcTransformComponentGeometry.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc; + +/// +/// Describes one component rectangle within an HEVC transform-tree node. +/// +internal readonly struct HevcTransformComponentGeometry +{ + /// + /// Initializes a new instance of the struct. + /// + /// The component rectangle left coordinate. + /// The component rectangle top coordinate. + /// The component rectangle width. + /// The component rectangle height. + /// Whether this transform-tree section owns the component rectangle. + /// Whether every child section owns a distinct component rectangle. + public HevcTransformComponentGeometry(int x, int y, int width, int height, bool process, bool processesAllQuadrants) + { + this.X = x; + this.Y = y; + this.Width = width; + this.Height = height; + this.Process = process; + this.ProcessesAllQuadrants = processesAllQuadrants; + } + + /// + /// Gets the component rectangle left coordinate. + /// + public int X { get; } + + /// + /// Gets the component rectangle top coordinate. + /// + public int Y { get; } + + /// + /// Gets the component rectangle width. + /// + public int Width { get; } + + /// + /// Gets the component rectangle height. + /// + public int Height { get; } + + /// + /// Gets a value indicating whether this transform-tree section owns the component rectangle. + /// + public bool Process { get; } + + /// + /// Gets a value indicating whether each child section owns a distinct component rectangle. + /// + public bool ProcessesAllQuadrants { get; } +} diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcTransformUnitGeometry.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcTransformUnitGeometry.cs new file mode 100644 index 000000000..82b990304 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcTransformUnitGeometry.cs @@ -0,0 +1,158 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc; + +/// +/// Maps one luma transform-tree node to its primary and subsampled component rectangles. +/// +internal readonly struct HevcTransformUnitGeometry +{ + /// + /// Initializes a new instance of the struct. + /// + /// The base-two logarithm of the luma transform-node side. + /// The primary plane coded with luma syntax. + /// The primary component rectangle. + /// The blue-difference chroma rectangle. + /// The red-difference chroma rectangle. + /// Whether chroma syntax accompanies the primary luma syntax. + private HevcTransformUnitGeometry( + int log2LumaSize, + HevcPlane primaryPlane, + HevcTransformComponentGeometry primary, + HevcTransformComponentGeometry chromaBlue, + HevcTransformComponentGeometry chromaRed, + bool hasCombinedChroma) + { + this.Log2LumaSize = log2LumaSize; + this.PrimaryPlane = primaryPlane; + this.Primary = primary; + this.ChromaBlue = chromaBlue; + this.ChromaRed = chromaRed; + this.HasCombinedChroma = hasCombinedChroma; + } + + /// + /// Gets the base-two logarithm of the luma transform-node side. + /// + public int Log2LumaSize { get; } + + /// + /// Gets the plane coded with luma transform syntax. + /// + public HevcPlane PrimaryPlane { get; } + + /// + /// Gets the primary component rectangle. + /// + public HevcTransformComponentGeometry Primary { get; } + + /// + /// Gets the blue-difference chroma rectangle. + /// + public HevcTransformComponentGeometry ChromaBlue { get; } + + /// + /// Gets the red-difference chroma rectangle. + /// + public HevcTransformComponentGeometry ChromaRed { get; } + + /// + /// Gets a value indicating whether combined chroma syntax accompanies the primary luma syntax. + /// + public bool HasCombinedChroma { get; } + + /// + /// Creates the root component geometry for one coding unit. + /// + /// The coding-unit left luma coordinate. + /// The coding-unit top luma coordinate. + /// The base-two logarithm of the coding-unit side. + /// The sequence chroma-format identifier. + /// Whether each 4:4:4 component is coded as an independent color plane. + /// The selected separate-color plane, or zero for combined coding. + /// The root transform-unit geometry. + public static HevcTransformUnitGeometry CreateRoot( + int x, + int y, + int log2Size, + byte chromaFormat, + bool separateColorPlane, + int colorPlaneIndex) + { + int size = 1 << log2Size; + HevcPlane primaryPlane = separateColorPlane ? (HevcPlane)colorPlaneIndex : HevcPlane.Y; + HevcTransformComponentGeometry primary = new(x, y, size, size, true, true); + if (chromaFormat == 0 || separateColorPlane) + { + return new HevcTransformUnitGeometry(log2Size, primaryPlane, primary, default, default, false); + } + + int subsamplingX = chromaFormat is 1 or 2 ? 1 : 0; + int subsamplingY = chromaFormat == 1 ? 1 : 0; + HevcTransformComponentGeometry chroma = new( + x >> subsamplingX, + y >> subsamplingY, + size >> subsamplingX, + size >> subsamplingY, + true, + true); + + return new HevcTransformUnitGeometry(log2Size, primaryPlane, primary, chroma, chroma, true); + } + + /// + /// Creates one of the four Z-ordered child transform nodes. + /// + /// The child section from zero through three. + /// The selected child geometry. + public HevcTransformUnitGeometry CreateChild(int section) + => new( + this.Log2LumaSize - 1, + this.PrimaryPlane, + SplitComponent(this.Primary, section), + SplitComponent(this.ChromaBlue, section), + SplitComponent(this.ChromaRed, section), + this.HasCombinedChroma); + + /// + /// Splits one component rectangle while retaining sub-minimum chroma at the owning parent level. + /// + /// The parent component rectangle. + /// The luma child section from zero through three. + /// The component rectangle visible from the selected child. + private static HevcTransformComponentGeometry SplitComponent(HevcTransformComponentGeometry parent, int section) + { + if (!parent.Process || parent.Width == 0) + { + return default; + } + + int width = parent.Width >> 1; + int height = parent.Height >> 1; + int sampleCount = width * height; + if ((width < 4 || height < 4) && sampleCount < 16) + { + // A component transform cannot be smaller than four by four. Its parent rectangle is associated with + // the final luma quadrant so CBF and coefficient syntax are consumed exactly once. + return new HevcTransformComponentGeometry(parent.X, parent.Y, parent.Width, parent.Height, section == 3, false); + } + + if (width < 4) + { + width = 4; + height = sampleCount / width; + } + else if (height < 4) + { + height = 4; + width = sampleCount / height; + } + + int columns = parent.Width / width; + int x = parent.X + ((section % columns) * width); + int y = parent.Y + ((section / columns) * height); + return new HevcTransformComponentGeometry(x, y, width, height, true, true); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcCabacDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcCabacDecoderTests.cs new file mode 100644 index 000000000..775c0faa9 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcCabacDecoderTests.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Hevc; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Hevc; + +/// +/// Verifies HEVC arithmetic-decoder suspension and restart around pulse-code-modulated coding units. +/// +[Trait("Format", "Heic")] +public class HevcCabacDecoderTests +{ + /// + /// Verifies that PCM samples begin after the terminating arithmetic bytes and that arithmetic decoding resumes after the raw payload. + /// + [Fact] + public void PcmPayloadSuspendsAndRestartsArithmeticDecoding() + { + ReadOnlySpan data = [0xFF, 0xFF, 0xAB, 0xFF, 0xFF]; + HevcCabacDecoder decoder = new(data); + + Assert.True(decoder.ReadPcmFlag()); + Assert.Equal((ushort)0xA, decoder.ReadPcmSample(4)); + Assert.Equal((ushort)0xB, decoder.ReadPcmSample(4)); + + decoder.RestartAfterPcm(); + + Assert.True(decoder.ReadTerminate()); + } + + /// + /// Verifies that a PCM sample cannot read beyond its bounded entropy substream. + /// + [Fact] + public void PcmPayloadRejectsTruncatedSample() + { + Assert.Throws(ReadTruncatedPcmSample); + } + + /// + /// Attempts to read a sample wider than the remaining raw PCM payload. + /// + private static void ReadTruncatedPcmSample() + { + ReadOnlySpan data = [0xFF, 0xFF, 0x80]; + HevcCabacDecoder decoder = new(data); + + Assert.True(decoder.ReadPcmFlag()); + decoder.ReadPcmSample(16); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcResidualReconstructorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcResidualReconstructorTests.cs index 2d1f91a53..713b77888 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcResidualReconstructorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcResidualReconstructorTests.cs @@ -175,6 +175,39 @@ public class HevcResidualReconstructorTests Assert.Equal(HevcResidualDpcmMode.Vertical, HevcResidualReconstructor.GetImplicitResidualDpcmMode(26, true)); } + /// + /// Compares cross-component residual prediction with the scalar signed-precision oracle across SIMD widths and a tail. + /// + /// The luma precision minus the chroma precision. + [Theory] + [InlineData(-2)] + [InlineData(0)] + [InlineData(2)] + public void CrossComponentPredictionMatchesScalarOracle(int bitDepthDifference) + { + const int sampleCount = 257; + const int alpha = -8; + int[] luma = new int[sampleCount]; + int[] actual = new int[sampleCount]; + int[] expected = new int[sampleCount]; + for (int index = 0; index < sampleCount; index++) + { + luma[index] = (((index * 7919) + 1229) & 65535) - 32768; + actual[index] = (((index * 4099) + 811) & 65535) - 32768; + expected[index] = actual[index]; + } + + for (int index = 0; index < sampleCount; index++) + { + int adjustedLuma = bitDepthDifference >= 0 ? luma[index] >> bitDepthDifference : luma[index] << -bitDepthDifference; + expected[index] = Math.Clamp(expected[index] + ((alpha * adjustedLuma) >> 3), short.MinValue, short.MaxValue); + } + + HevcResidualReconstructor.ApplyCrossComponentPrediction(luma, actual, sampleCount, alpha, bitDepthDifference); + + Assert.True(expected.AsSpan().SequenceEqual(actual)); + } + /// /// Applies the normative transform-skip normalization as a scalar test oracle. /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcTileLayoutTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcTileLayoutTests.cs new file mode 100644 index 000000000..4fbcb9ce0 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcTileLayoutTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Hevc; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Hevc; + +/// +/// Verifies HEVC coding-tree-block mappings for unequal tile dimensions. +/// +[Trait("Format", "Heic")] +public class HevcTileLayoutTests +{ + /// + /// Verifies the normative tile-row, tile-column, and in-tile raster traversal order. + /// + [Fact] + public void MapsEveryAddressBetweenRasterAndTileScanOrder() + { + HevcTileLayout layout = new(new[] { 2, 1 }, new[] { 1, 2 }); + + ReadOnlySpan expectedRasterAddresses = [0, 1, 2, 3, 4, 6, 7, 5, 8]; + for (int tileScanAddress = 0; tileScanAddress < expectedRasterAddresses.Length; tileScanAddress++) + { + int rasterAddress = expectedRasterAddresses[tileScanAddress]; + Assert.Equal(rasterAddress, layout.GetRasterAddress(tileScanAddress)); + Assert.Equal(tileScanAddress, layout.GetTileScanAddress(rasterAddress)); + } + } + + /// + /// Verifies tile identity, local coordinates, and dimensions on both sides of each tile boundary. + /// + [Theory] + [InlineData(0, 0, 0, 0, 2, 1)] + [InlineData(2, 1, 0, 0, 1, 1)] + [InlineData(3, 2, 0, 0, 2, 2)] + [InlineData(7, 2, 1, 1, 2, 2)] + [InlineData(5, 3, 0, 0, 1, 2)] + [InlineData(8, 3, 0, 1, 1, 2)] + public void ResolvesTileLocalPosition( + int rasterAddress, + int expectedTileIndex, + int expectedColumn, + int expectedRow, + int expectedWidth, + int expectedHeight) + { + HevcTileLayout layout = new(new[] { 2, 1 }, new[] { 1, 2 }); + + layout.GetTilePosition( + rasterAddress, + out int tileIndex, + out int column, + out int row, + out int width, + out int height); + + Assert.Equal(expectedTileIndex, tileIndex); + Assert.Equal(expectedColumn, column); + Assert.Equal(expectedRow, row); + Assert.Equal(expectedWidth, width); + Assert.Equal(expectedHeight, height); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcTransformUnitGeometryTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcTransformUnitGeometryTests.cs new file mode 100644 index 000000000..4722be9ca --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcTransformUnitGeometryTests.cs @@ -0,0 +1,74 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Hevc; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Hevc; + +/// +/// Verifies HEVC transform-tree component geometry across chroma sampling layouts. +/// +[Trait("Format", "Heic")] +public class HevcTransformUnitGeometryTests +{ + /// + /// Verifies that a sub-minimum 4:2:0 chroma block is retained and processed with the final luma quadrant. + /// + [Fact] + public void Chroma420RetainsMinimumBlockAtFinalLumaQuadrant() + { + HevcTransformUnitGeometry root = HevcTransformUnitGeometry.CreateRoot(16, 24, 3, 1, false, 0); + + for (int childIndex = 0; childIndex < 3; childIndex++) + { + HevcTransformUnitGeometry child = root.CreateChild(childIndex); + Assert.False(child.ChromaBlue.Process); + } + + HevcTransformComponentGeometry chroma = root.CreateChild(3).ChromaBlue; + Assert.True(chroma.Process); + Assert.False(chroma.ProcessesAllQuadrants); + Assert.Equal(8, chroma.X); + Assert.Equal(12, chroma.Y); + Assert.Equal(4, chroma.Width); + Assert.Equal(4, chroma.Height); + } + + /// + /// Verifies that a 4:2:2 rectangular chroma transform is retained for two vertical four-by-four coefficient blocks. + /// + [Fact] + public void Chroma422RetainsVerticalSubTransformsAtFinalLumaQuadrant() + { + HevcTransformUnitGeometry root = HevcTransformUnitGeometry.CreateRoot(16, 24, 3, 2, false, 0); + + for (int childIndex = 0; childIndex < 3; childIndex++) + { + Assert.False(root.CreateChild(childIndex).ChromaBlue.Process); + } + + HevcTransformComponentGeometry chroma = root.CreateChild(3).ChromaBlue; + Assert.True(chroma.Process); + Assert.False(chroma.ProcessesAllQuadrants); + Assert.Equal(8, chroma.X); + Assert.Equal(24, chroma.Y); + Assert.Equal(4, chroma.Width); + Assert.Equal(8, chroma.Height); + } + + /// + /// Verifies that separate 4:4:4 planes retain full-resolution primary geometry and omit combined chroma syntax. + /// + [Fact] + public void SeparateColorPlaneUsesFullResolutionPrimaryGeometry() + { + HevcTransformUnitGeometry root = HevcTransformUnitGeometry.CreateRoot(16, 24, 5, 3, true, 2); + + Assert.Equal(HevcPlane.Cr, root.PrimaryPlane); + Assert.Equal(16, root.Primary.X); + Assert.Equal(24, root.Primary.Y); + Assert.Equal(32, root.Primary.Width); + Assert.Equal(32, root.Primary.Height); + Assert.False(root.HasCombinedChroma); + } +}