mirror of https://github.com/SixLabors/ImageSharp
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
728 lines
37 KiB
728 lines
37 KiB
// Copyright (c) Six Labors.
|
|
// Licensed under the Six Labors Split License.
|
|
|
|
using System.Buffers;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
|
|
|
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
|
|
|
|
/// <summary>
|
|
/// Reconstructs AV1 transform blocks by combining prediction, inverse quantization, and inverse transforms.
|
|
/// </summary>
|
|
internal sealed class Av1BlockDecoder : IDisposable
|
|
{
|
|
/// <summary>
|
|
/// The sequence-level syntax that determines superblock size, plane layout, and sample depth.
|
|
/// </summary>
|
|
private readonly ObuSequenceHeader sequenceHeader;
|
|
|
|
/// <summary>
|
|
/// The current frame syntax that determines quantization, lossless segments, and reconstruction geometry.
|
|
/// </summary>
|
|
private readonly ObuFrameHeader frameHeader;
|
|
|
|
/// <summary>
|
|
/// The reconstructed Y, U, and V sample planes receiving prediction and residual output.
|
|
/// </summary>
|
|
private readonly Av1FrameBuffer<byte> frameBuffer;
|
|
|
|
/// <summary>
|
|
/// The per-plane transform-size map consumed after reconstruction by the deblocking stage.
|
|
/// </summary>
|
|
private readonly Av1LoopFilterContext loopFilterContext;
|
|
|
|
/// <summary>
|
|
/// The frame-owned inverse quantizer carrying the active superblock delta-Q state.
|
|
/// </summary>
|
|
private readonly Av1InverseQuantizer inverseQuantizer;
|
|
|
|
/// <summary>
|
|
/// The retained reconstructed frames addressable by inter prediction, when reconstruction belongs to a decoder session.
|
|
/// </summary>
|
|
private readonly Av1ReferenceFrameStore? referenceFrames;
|
|
|
|
/// <summary>
|
|
/// Owns the reusable raster-order inverse-quantization buffer.
|
|
/// </summary>
|
|
private readonly IMemoryOwner<int> inverseQuantizationOwner;
|
|
|
|
/// <summary>
|
|
/// Owns the reusable two-dimensional inverse-transform workspace.
|
|
/// </summary>
|
|
private readonly IMemoryOwner<int> transformWorkspaceOwner;
|
|
|
|
/// <summary>
|
|
/// Owns the reusable directional and filter-intra prediction workspace.
|
|
/// </summary>
|
|
private readonly IMemoryOwner<short> predictionScratchOwner;
|
|
|
|
/// <summary>
|
|
/// Reconstructs intra-predicted blocks using the frame-owned prediction workspace.
|
|
/// </summary>
|
|
private readonly Av1PredictionDecoder predictionDecoder;
|
|
|
|
/// <summary>
|
|
/// Indicates whether transform traversal must also populate loop-filter parameters.
|
|
/// </summary>
|
|
private readonly bool isLoopFilterEnabled;
|
|
|
|
/// <summary>
|
|
/// The next packed coefficient position for each plane in the current superblock.
|
|
/// </summary>
|
|
private readonly int[] currentCoefficientIndex;
|
|
|
|
/// <summary>
|
|
/// Accumulates reconstructed luma samples until a chroma-from-luma prediction block can consume them.
|
|
/// </summary>
|
|
private readonly Av1ChromaFromLumaContext chromaFromLumaContext;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Av1BlockDecoder"/> class.
|
|
/// </summary>
|
|
/// <param name="sequenceHeader">The decoded sequence header.</param>
|
|
/// <param name="frameHeader">The decoded frame header.</param>
|
|
/// <param name="frameBuffer">The frame buffer receiving reconstructed samples.</param>
|
|
/// <param name="loopFilterContext">The transform-size map populated while reconstructing blocks.</param>
|
|
/// <param name="inverseQuantizer">The inverse quantizer carrying the active superblock delta-Q state.</param>
|
|
/// <param name="referenceFrames">The retained reconstructed frames selected by inter blocks, or <see langword="null"/> for intra-only use.</param>
|
|
public Av1BlockDecoder(
|
|
ObuSequenceHeader sequenceHeader,
|
|
ObuFrameHeader frameHeader,
|
|
Av1FrameBuffer<byte> frameBuffer,
|
|
Av1LoopFilterContext loopFilterContext,
|
|
Av1InverseQuantizer inverseQuantizer,
|
|
Av1ReferenceFrameStore? referenceFrames = null)
|
|
{
|
|
this.sequenceHeader = sequenceHeader;
|
|
this.frameHeader = frameHeader;
|
|
this.frameBuffer = frameBuffer;
|
|
this.loopFilterContext = loopFilterContext;
|
|
this.inverseQuantizer = inverseQuantizer;
|
|
this.referenceFrames = referenceFrames;
|
|
int ySize = (1 << this.sequenceHeader.SuperblockSizeLog2) * (1 << this.sequenceHeader.SuperblockSizeLog2);
|
|
|
|
// One scratch plane is reused for every transform unit. Its maximum size must cover a complete superblock
|
|
// across all coded planes, with chroma dimensions reduced independently by their subsampling axes.
|
|
int inverseQuantizationSize = ySize +
|
|
(this.sequenceHeader.ColorConfig.SubSamplingX ? ySize >> 2 : ySize) +
|
|
(this.sequenceHeader.ColorConfig.SubSamplingY ? ySize >> 2 : ySize);
|
|
|
|
IMemoryOwner<int>? inverseQuantizationOwner = null;
|
|
IMemoryOwner<int>? transformWorkspaceOwner = null;
|
|
IMemoryOwner<short>? predictionScratchOwner = null;
|
|
try
|
|
{
|
|
inverseQuantizationOwner = this.frameBuffer.MemoryAllocator.Allocate<int>(inverseQuantizationSize);
|
|
transformWorkspaceOwner = this.frameBuffer.MemoryAllocator.Allocate<int>(Av1TransformWorkspace.MaximumLength);
|
|
int maximumBlockLength = 1 << sequenceHeader.SuperblockSizeLog2;
|
|
int predictionScratchLength = Math.Max(
|
|
Av1PredictionDecoder.ScratchLength,
|
|
Av1InterPredictor.GetScratchLength(maximumBlockLength, maximumBlockLength));
|
|
|
|
predictionScratchOwner = this.frameBuffer.MemoryAllocator.Allocate<short>(predictionScratchLength);
|
|
|
|
this.inverseQuantizationOwner = inverseQuantizationOwner;
|
|
this.transformWorkspaceOwner = transformWorkspaceOwner;
|
|
this.predictionScratchOwner = predictionScratchOwner;
|
|
this.predictionDecoder = new(sequenceHeader, frameHeader, predictionScratchOwner.Memory);
|
|
this.isLoopFilterEnabled = frameHeader.LoopFilterParameters.FilterLevel[0] != 0 ||
|
|
frameHeader.LoopFilterParameters.FilterLevel[1] != 0;
|
|
|
|
this.currentCoefficientIndex = new int[3];
|
|
this.chromaFromLumaContext = new(sequenceHeader.ColorConfig);
|
|
}
|
|
catch
|
|
{
|
|
// A constructor that does not return transfers no ownership to its caller. Unwind successful rents in
|
|
// reverse order so allocator diagnostics and pooled buffers remain balanced after any later allocation.
|
|
predictionScratchOwner?.Dispose();
|
|
transformWorkspaceOwner?.Dispose();
|
|
inverseQuantizationOwner?.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the reusable raster-order coefficient buffer populated by inverse quantization.
|
|
/// </summary>
|
|
public Span<int> CurrentInverseQuantizationCoefficients => this.inverseQuantizationOwner.Memory.Span;
|
|
|
|
/// <summary>
|
|
/// Releases the pooled reconstruction workspaces owned by this decoder.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
this.predictionScratchOwner.Dispose();
|
|
this.transformWorkspaceOwner.Dispose();
|
|
this.inverseQuantizationOwner.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the per-plane packed coefficient cursors before reconstructing a superblock.
|
|
/// </summary>
|
|
/// <param name="superblockInfo">The superblock whose coefficient streams will be consumed.</param>
|
|
public void UpdateSuperblock(Av1SuperblockInfo superblockInfo)
|
|
{
|
|
// Each superblock owns independent packed coefficient streams for Y, U, and V. The first value for each
|
|
// transform unit stores its coefficient count, so DecodeBlock advances a plane cursor as units are consumed.
|
|
this.currentCoefficientIndex[0] = 0;
|
|
this.currentCoefficientIndex[1] = 0;
|
|
this.currentCoefficientIndex[2] = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reconstructs every luma and chroma transform unit belonging to one decoded AV1 block.
|
|
/// </summary>
|
|
/// <param name="modeInfo">The decoded prediction, segmentation, skip, and transform state.</param>
|
|
/// <param name="modeInfoPosition">The block origin in units of four luma samples.</param>
|
|
/// <param name="blockSize">The decoded block size.</param>
|
|
/// <param name="superblockInfo">The owning superblock's transform and coefficient storage.</param>
|
|
/// <param name="tileInfo">The tile boundaries used to determine neighbor availability.</param>
|
|
public void DecodeBlock(Av1BlockModeInfo modeInfo, Point modeInfoPosition, Av1BlockSize blockSize, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo)
|
|
{
|
|
Span<int> transformWorkspace = this.transformWorkspaceOwner.Memory.Span;
|
|
|
|
ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig;
|
|
Av1TransformType transformType;
|
|
Av1TransformSize transformSize;
|
|
int transformUnitCount;
|
|
bool hasChroma = Av1TileReader.HasChroma(this.sequenceHeader, modeInfoPosition, blockSize);
|
|
Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, hasChroma, modeInfo.PartitionType)
|
|
{
|
|
ColumnIndex = modeInfoPosition.X,
|
|
RowIndex = modeInfoPosition.Y,
|
|
ChromaFromLumaContext = this.chromaFromLumaContext
|
|
};
|
|
|
|
partitionInfo.ComputeBoundaryOffsets(this.sequenceHeader, this.frameHeader, tileInfo);
|
|
|
|
if (hasChroma)
|
|
{
|
|
// A one-unit luma edge maps to the same chroma sample as the adjacent unit on a subsampled axis. In that
|
|
// case the usable chroma neighbor is two mode-info units away rather than immediately above or left.
|
|
if (colorConfig.SubSamplingY && blockSize.Get4x4HighCount() == 1)
|
|
{
|
|
partitionInfo.AvailableAboveForChroma = modeInfoPosition.Y - 2 >= tileInfo.ModeInfoRowStart;
|
|
}
|
|
|
|
if (colorConfig.SubSamplingX && blockSize.Get4x4WideCount() == 1)
|
|
{
|
|
partitionInfo.AvailableLeftForChroma = modeInfoPosition.X - 2 >= tileInfo.ModeInfoColumnStart;
|
|
}
|
|
}
|
|
|
|
partitionInfo.PopulateModeInfoNeighbors(colorConfig);
|
|
|
|
int maxBlocksWide = partitionInfo.GetMaxBlockWide(blockSize, false);
|
|
int maxBlocksHigh = partitionInfo.GetMaxBlockHigh(blockSize, false);
|
|
|
|
bool isLossless = this.frameHeader.LosslessArray[modeInfo.SegmentId];
|
|
bool isLosslessBlock = isLossless && ((blockSize >= Av1BlockSize.Block64x64) && (blockSize <= Av1BlockSize.Block128x128));
|
|
int chromaTransformUnitCount = isLosslessBlock
|
|
? (maxBlocksWide * maxBlocksHigh) >> ((colorConfig.SubSamplingX ? 1 : 0) + (colorConfig.SubSamplingY ? 1 : 0))
|
|
: modeInfo.GetTransformUnitCount(Av1Plane.U);
|
|
|
|
bool isInterBlock = modeInfo.ReferenceFrames[0] >= Av1ReferenceFrameType.Last;
|
|
Av1FrameBuffer<byte>? referenceFrameBuffer = null;
|
|
if (isInterBlock)
|
|
{
|
|
int canonicalReferenceIndex = (int)modeInfo.ReferenceFrames[0] - (int)Av1ReferenceFrameType.Last;
|
|
Av1GlobalMotionParameters globalMotion = this.frameHeader.GetGlobalMotionParameters()[canonicalReferenceIndex];
|
|
if (modeInfo.YMode == Av1PredictionMode.GlobalMotionVector &&
|
|
Math.Min(modeInfo.BlockSize.GetWidth(), modeInfo.BlockSize.GetHeight()) >= 8 &&
|
|
globalMotion.Type > Av1GlobalMotionType.Translation)
|
|
{
|
|
// A qualifying rotation/zoom or affine GLOBALMV block samples the complete warped model. Its center
|
|
// vector is a stack fallback only and cannot be substituted into the translational predictor.
|
|
throw new NotSupportedException("AV1 non-translational global prediction is not implemented.");
|
|
}
|
|
|
|
uint referenceSlot = this.frameHeader.GetReferenceFrameIndices()[canonicalReferenceIndex];
|
|
|
|
// The uncompressed-header parser validates each selected slot and the reference store remains unchanged
|
|
// until frame reconstruction completes, so every parsed inter block resolves the same retained owner.
|
|
referenceFrameBuffer = this.referenceFrames!.Resolve((int)referenceSlot)!.FrameBuffer;
|
|
if (referenceFrameBuffer.Width != this.frameHeader.FrameSize.FrameWidth || referenceFrameBuffer.Height != this.frameHeader.FrameSize.FrameHeight)
|
|
{
|
|
// Scaled prediction changes both the source coordinate and the per-output-sample step. Running the
|
|
// unit-step predictor here would silently reconstruct valid scaled-reference streams incorrectly.
|
|
throw new NotSupportedException("AV1 scaled-reference inter prediction is not implemented.");
|
|
}
|
|
}
|
|
|
|
bool highBitDepth = this.frameBuffer.BytesPerSample == 2;
|
|
for (int plane = 0; plane < colorConfig.PlaneCount; plane++)
|
|
{
|
|
int subX = (plane > 0) && colorConfig.SubSamplingX ? 1 : 0;
|
|
int subY = (plane > 0) && colorConfig.SubSamplingY ? 1 : 0;
|
|
|
|
if (plane != 0 && !partitionInfo.IsChroma)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Luma transform descriptors occupy their own stream. U and V share one stream, with the V descriptors
|
|
// following the U descriptors for this block, so the V base includes the complete U transform-unit count.
|
|
int transformInfoIndex = plane switch
|
|
{
|
|
2 => superblockInfo.TransformInfoIndexUv + modeInfo.GetFirstTransformLocation(Av1Plane.V) + chromaTransformUnitCount,
|
|
1 => superblockInfo.TransformInfoIndexUv + modeInfo.GetFirstTransformLocation(Av1Plane.U),
|
|
0 => superblockInfo.TransformInfoIndexY + modeInfo.GetFirstTransformLocation(Av1Plane.Y),
|
|
_ => throw new InvalidImageContentException("Maximum of 3 color planes")
|
|
};
|
|
Span<Av1TransformInfo> transformInfo = superblockInfo.GetTransformInfo(plane)[transformInfoIndex..];
|
|
Guard.NotNull(transformInfo[0]);
|
|
|
|
if (isLosslessBlock)
|
|
{
|
|
Guard.IsTrue(transformInfo[0].Size == Av1TransformSize.Size4x4, nameof(transformInfo), "Lossless may only have 4x4 blocks.");
|
|
transformUnitCount = (maxBlocksWide * maxBlocksHigh) >> (subX + subY);
|
|
}
|
|
else
|
|
{
|
|
transformUnitCount = modeInfo.GetTransformUnitCount((Av1Plane)plane);
|
|
}
|
|
|
|
Guard.IsFalse(transformUnitCount == 0, nameof(transformUnitCount), "Must have at least a single transform unit to decode.");
|
|
|
|
Point pixelPosition = new(
|
|
(modeInfoPosition.X >> subX) << Av1Constants.ModeInfoSizeLog2,
|
|
(modeInfoPosition.Y >> subY) << Av1Constants.ModeInfoSizeLog2);
|
|
|
|
Span<byte> blockReconstructionBuffer = default;
|
|
Span<short> highBitDepthBlockReconstructionBuffer = default;
|
|
int reconstructionStride;
|
|
|
|
// Prediction reads the row immediately above the destination through negative-relative neighbor offsets.
|
|
// The frame-buffer helpers therefore return a span beginning one logical sample row before the block.
|
|
if (highBitDepth)
|
|
{
|
|
highBitDepthBlockReconstructionBuffer = this.frameBuffer.DeriveBlockPointer16((Av1Plane)plane, pixelPosition, subX, subY, out reconstructionStride);
|
|
}
|
|
else
|
|
{
|
|
blockReconstructionBuffer = this.frameBuffer.DeriveBlockPointer((Av1Plane)plane, pixelPosition, subX, subY, out reconstructionStride);
|
|
}
|
|
|
|
if (isInterBlock)
|
|
{
|
|
Av1MotionVector motionVector = modeInfo.MotionVectors[0];
|
|
int predictionWidth = Math.Max(4, blockSize.GetWidth() >> subX);
|
|
int predictionHeight = Math.Max(4, blockSize.GetHeight() >> subY);
|
|
|
|
// AV1 predicts the complete declared plane block even when its luma extent crosses the frame boundary.
|
|
// Subsampled dimensions retain the mandatory four-sample minimum used by set_plane_n4 in libaom.
|
|
int horizontalMotionQ4 = motionVector.Column << (1 - subX);
|
|
int verticalMotionQ4 = motionVector.Row << (1 - subY);
|
|
int horizontalExtensionQ4 = (4 + predictionWidth) << 4;
|
|
int verticalExtensionQ4 = (4 + predictionHeight) << 4;
|
|
int horizontalEdgeScale = 1 << (1 - subX);
|
|
int verticalEdgeScale = 1 << (1 - subY);
|
|
|
|
// The UMV clamp is expressed in one-sixteenth plane-sample units. A 128-sample block can legally
|
|
// address 135 samples beyond an edge once its prediction extent and eight-tap filter support are
|
|
// included; the frame-owned 144-sample luma border keeps that source directly addressable.
|
|
horizontalMotionQ4 = Av1Math.Clip3(
|
|
(partitionInfo.ModeBlockToLeftEdge * horizontalEdgeScale) - horizontalExtensionQ4,
|
|
(partitionInfo.ModeBlockToRightEdge * horizontalEdgeScale) + horizontalExtensionQ4 - 16,
|
|
horizontalMotionQ4);
|
|
|
|
verticalMotionQ4 = Av1Math.Clip3(
|
|
(partitionInfo.ModeBlockToTopEdge * verticalEdgeScale) - verticalExtensionQ4,
|
|
(partitionInfo.ModeBlockToBottomEdge * verticalEdgeScale) + verticalExtensionQ4 - 16,
|
|
verticalMotionQ4);
|
|
|
|
int sourceColumnQ4 = (pixelPosition.X << 4) + horizontalMotionQ4;
|
|
int sourceRowQ4 = (pixelPosition.Y << 4) + verticalMotionQ4;
|
|
|
|
// Motion vectors use one-eighth luma-sample units. Shifting by one minus the plane subsampling converts
|
|
// them directly to the predictor's one-sixteenth-plane-sample phase; masking then preserves the signed
|
|
// floor used to select the integer source sample.
|
|
int horizontalPhase = sourceColumnQ4 & 15;
|
|
int verticalPhase = sourceRowQ4 & 15;
|
|
Span<short> predictionScratch = this.predictionScratchOwner.Memory.Span;
|
|
|
|
if (highBitDepth)
|
|
{
|
|
Span<ushort> source = referenceFrameBuffer!.GetPaddedPlaneSpan16(
|
|
(Av1Plane)plane,
|
|
subX,
|
|
subY,
|
|
out int sourceStride,
|
|
out Point sourceOrigin);
|
|
|
|
int sourceIndex =
|
|
((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + sourceOrigin.X + (sourceColumnQ4 >> 4);
|
|
|
|
Span<ushort> destination =
|
|
MemoryMarshal.Cast<short, ushort>(highBitDepthBlockReconstructionBuffer[reconstructionStride..]);
|
|
|
|
Av1InterPredictor.Predict(
|
|
source,
|
|
sourceStride,
|
|
sourceIndex,
|
|
destination,
|
|
reconstructionStride,
|
|
predictionWidth,
|
|
predictionHeight,
|
|
modeInfo.InterpolationFilters[1],
|
|
modeInfo.InterpolationFilters[0],
|
|
horizontalPhase,
|
|
verticalPhase,
|
|
this.frameBuffer.BitDepth.GetBitCount(),
|
|
predictionScratch);
|
|
}
|
|
else
|
|
{
|
|
Span<byte> source = referenceFrameBuffer!.GetPaddedPlaneSpan(
|
|
(Av1Plane)plane,
|
|
subX,
|
|
subY,
|
|
out int sourceStride,
|
|
out Point sourceOrigin);
|
|
|
|
int sourceIndex =
|
|
((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + sourceOrigin.X + (sourceColumnQ4 >> 4);
|
|
|
|
Av1InterPredictor.Predict(
|
|
source,
|
|
sourceStride,
|
|
sourceIndex,
|
|
blockReconstructionBuffer[reconstructionStride..],
|
|
reconstructionStride,
|
|
predictionWidth,
|
|
predictionHeight,
|
|
modeInfo.InterpolationFilters[1],
|
|
modeInfo.InterpolationFilters[0],
|
|
horizontalPhase,
|
|
verticalPhase,
|
|
predictionScratch);
|
|
}
|
|
}
|
|
|
|
for (int tu = 0; tu < transformUnitCount; tu++)
|
|
{
|
|
Span<byte> transformBlockReconstructionBuffer = default;
|
|
Span<short> highBitDepthTransformBlockReconstructionBuffer = default;
|
|
int transformBlockOffset;
|
|
|
|
transformSize = transformInfo[0].Size;
|
|
Span<int> coefficients = superblockInfo.GetCoefficients((Av1Plane)plane)[this.currentCoefficientIndex[plane]..];
|
|
|
|
// Transform offsets are stored in mode-info units. Reconstruction strides are expressed in logical
|
|
// samples for both storage pipelines, so no byte scaling is applied to the high-bit-depth offset.
|
|
transformBlockOffset = ((transformInfo[0].OffsetY * reconstructionStride) + transformInfo[0].OffsetX) << Av1Constants.ModeInfoSizeLog2;
|
|
if (highBitDepth)
|
|
{
|
|
highBitDepthTransformBlockReconstructionBuffer = highBitDepthBlockReconstructionBuffer[transformBlockOffset..];
|
|
}
|
|
else
|
|
{
|
|
transformBlockReconstructionBuffer = blockReconstructionBuffer[transformBlockOffset..];
|
|
}
|
|
|
|
if (this.isLoopFilterEnabled)
|
|
{
|
|
// U and V share transform geometry. Store the chroma map once so the later plane passes consume
|
|
// identical sizes without retaining duplicate state.
|
|
if (plane != 2)
|
|
{
|
|
Point transformPosition = new(
|
|
(modeInfoPosition.X >> subX) + transformInfo[0].OffsetX,
|
|
(modeInfoPosition.Y >> subY) + transformInfo[0].OffsetY);
|
|
|
|
this.loopFilterContext.SetTransformSize((Av1Plane)plane, transformPosition, transformSize);
|
|
}
|
|
}
|
|
|
|
// Intra-block copy is signaled on an intra-only frame but follows AV1's inter prediction and transform
|
|
// rules. Its validated displacement always references an earlier reconstructed region of this frame.
|
|
if (modeInfo.UseIntraBlockCopy)
|
|
{
|
|
// libaom predicts the complete coding block before traversing its residual transforms. The mandatory
|
|
// 256-pixel source delay prevents overlap, and the two-tap interpolation is translation-invariant,
|
|
// so predicting the matching source rectangle for each transform unit produces the same samples.
|
|
Point transformPixelPosition = new(
|
|
pixelPosition.X + (transformInfo[0].OffsetX << Av1Constants.ModeInfoSizeLog2),
|
|
pixelPosition.Y + (transformInfo[0].OffsetY << Av1Constants.ModeInfoSizeLog2));
|
|
|
|
// Displacement vectors use one-eighth luma-sample units. Converting them to the plane's q4 grid
|
|
// leaves luma on an integer sample and can leave subsampled chroma exactly at phase eight.
|
|
int sourceColumnQ4 = (transformPixelPosition.X << 4) +
|
|
(modeInfo.DisplacementVector.Column << (1 - subX));
|
|
|
|
int sourceRowQ4 = (transformPixelPosition.Y << 4) +
|
|
(modeInfo.DisplacementVector.Row << (1 - subY));
|
|
|
|
int sourcePhaseX = sourceColumnQ4 & 15;
|
|
int sourcePhaseY = sourceRowQ4 & 15;
|
|
DebugGuard.IsTrue(sourcePhaseX is 0 or 8, "Intra-block-copy horizontal phase must be an integer or half sample.");
|
|
DebugGuard.IsTrue(sourcePhaseY is 0 or 8, "Intra-block-copy vertical phase must be an integer or half sample.");
|
|
|
|
Point sourcePixelPosition = new(sourceColumnQ4 >> 4, sourceRowQ4 >> 4);
|
|
int transformWidth = transformSize.GetWidth();
|
|
int transformHeight = transformSize.GetHeight();
|
|
|
|
if (highBitDepth)
|
|
{
|
|
Span<short> source = this.frameBuffer.DeriveBlockPointer16(
|
|
(Av1Plane)plane,
|
|
sourcePixelPosition,
|
|
subX,
|
|
subY,
|
|
out int sourceStride);
|
|
|
|
Av1IntraBlockCopyPredictor.Predict(
|
|
source[sourceStride..],
|
|
sourceStride,
|
|
highBitDepthTransformBlockReconstructionBuffer[reconstructionStride..],
|
|
reconstructionStride,
|
|
transformWidth,
|
|
transformHeight,
|
|
sourcePhaseX != 0,
|
|
sourcePhaseY != 0);
|
|
}
|
|
else
|
|
{
|
|
Span<byte> source = this.frameBuffer.DeriveBlockPointer(
|
|
(Av1Plane)plane,
|
|
sourcePixelPosition,
|
|
subX,
|
|
subY,
|
|
out int sourceStride);
|
|
|
|
Av1IntraBlockCopyPredictor.Predict(
|
|
source[sourceStride..],
|
|
sourceStride,
|
|
transformBlockReconstructionBuffer[reconstructionStride..],
|
|
reconstructionStride,
|
|
transformWidth,
|
|
transformHeight,
|
|
sourcePhaseX != 0,
|
|
sourcePhaseY != 0);
|
|
}
|
|
}
|
|
else if (!isInterBlock)
|
|
{
|
|
// Conventional intra prediction consumes the reference-prefixed destination span before the
|
|
// transform residual is reconstructed over its first output row.
|
|
if (highBitDepth)
|
|
{
|
|
this.predictionDecoder.Decode(
|
|
ref partitionInfo,
|
|
(Av1Plane)plane,
|
|
transformSize,
|
|
tileInfo,
|
|
highBitDepthTransformBlockReconstructionBuffer,
|
|
reconstructionStride,
|
|
this.frameBuffer.BitDepth,
|
|
transformInfo[0].OffsetX,
|
|
transformInfo[0].OffsetY);
|
|
}
|
|
else
|
|
{
|
|
this.predictionDecoder.Decode(
|
|
ref partitionInfo,
|
|
(Av1Plane)plane,
|
|
transformSize,
|
|
tileInfo,
|
|
transformBlockReconstructionBuffer,
|
|
reconstructionStride,
|
|
this.frameBuffer.BitDepth,
|
|
transformInfo[0].OffsetX,
|
|
transformInfo[0].OffsetY);
|
|
}
|
|
}
|
|
|
|
int numberOfCoefficients = 0;
|
|
|
|
if (!modeInfo.Skip && transformInfo[0].CodeBlockFlag)
|
|
{
|
|
Span<int> quantizationCoefficients = this.CurrentInverseQuantizationCoefficients;
|
|
int inverseQuantizationSize = transformSize.GetWidth() * transformSize.GetHeight();
|
|
quantizationCoefficients[..inverseQuantizationSize].Clear();
|
|
transformType = transformInfo[0].Type;
|
|
|
|
// Inverse quantization writes raster coefficients into the reusable superblock scratch plane.
|
|
numberOfCoefficients = this.inverseQuantizer.InverseQuantize(
|
|
modeInfo, coefficients, quantizationCoefficients, transformType, transformSize, (Av1Plane)plane);
|
|
if (numberOfCoefficients != 0)
|
|
{
|
|
// The packed coefficient stream prefixes every transform unit with its decoded coefficient
|
|
// count. Advance past that prefix as well as the coefficient values before the next unit.
|
|
this.currentCoefficientIndex[plane] += numberOfCoefficients + 1;
|
|
|
|
if (highBitDepth)
|
|
{
|
|
// Prediction receives a reference-prefixed span beginning on the previous row. Inverse
|
|
// reconstruction operates on the transform itself, so advance to the first destination row.
|
|
Av1InverseTransformer.ReconstructHighBitDepth(
|
|
quantizationCoefficients,
|
|
highBitDepthTransformBlockReconstructionBuffer[reconstructionStride..],
|
|
reconstructionStride,
|
|
transformSize,
|
|
transformType,
|
|
plane,
|
|
numberOfCoefficients,
|
|
isLossless,
|
|
this.frameBuffer.BitDepth,
|
|
transformWorkspace);
|
|
}
|
|
else
|
|
{
|
|
// Keep the reference-prefix convention local to prediction; residuals are added at the
|
|
// first reconstructed row rather than the top-neighbor row.
|
|
Av1InverseTransformer.Reconstruct8Bit(
|
|
quantizationCoefficients,
|
|
transformBlockReconstructionBuffer[reconstructionStride..],
|
|
reconstructionStride,
|
|
transformSize,
|
|
transformType,
|
|
plane,
|
|
numberOfCoefficients,
|
|
isLossless,
|
|
transformWorkspace);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Store Luma for CFL if required!
|
|
if (plane == (int)Av1Plane.Y && StoreChromaFromLumaRequired(colorConfig, ref partitionInfo))
|
|
{
|
|
// The predictor span begins on the previous row; CFL storage consumes reconstructed samples from
|
|
// the transform block itself, hence the explicit one-stride advance for both sample pipelines.
|
|
if (highBitDepth)
|
|
{
|
|
this.chromaFromLumaContext.Store(
|
|
highBitDepthTransformBlockReconstructionBuffer[reconstructionStride..],
|
|
reconstructionStride,
|
|
transformInfo[0].OffsetY,
|
|
transformInfo[0].OffsetX,
|
|
transformSize,
|
|
blockSize,
|
|
modeInfoPosition.Y,
|
|
modeInfoPosition.X);
|
|
}
|
|
else
|
|
{
|
|
this.chromaFromLumaContext.Store(
|
|
transformBlockReconstructionBuffer[reconstructionStride..],
|
|
reconstructionStride,
|
|
transformInfo[0].OffsetY,
|
|
transformInfo[0].OffsetX,
|
|
transformSize,
|
|
blockSize,
|
|
modeInfoPosition.Y,
|
|
modeInfoPosition.X);
|
|
}
|
|
}
|
|
|
|
// Transform descriptors are stored in the same traversal order as their packed coefficient groups.
|
|
transformInfo = transformInfo[1..];
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Derives a byte-addressed reconstruction span beginning one row before a block.
|
|
/// </summary>
|
|
/// <param name="frameBuffer">The frame buffer containing the destination planes.</param>
|
|
/// <param name="plane">The zero-based Y, U, or V plane index.</param>
|
|
/// <param name="blockColumnInPixels">The horizontal block origin in plane samples.</param>
|
|
/// <param name="blockRowInPixels">The vertical block origin in plane samples.</param>
|
|
/// <param name="blockReconstructionBuffer">The resulting span beginning one row before the block.</param>
|
|
/// <param name="reconstructionStride">The number of logical samples between rows.</param>
|
|
/// <param name="subX">The chroma horizontal subsampling shift.</param>
|
|
/// <param name="subY">The chroma vertical subsampling shift.</param>
|
|
private static void DeriveBlockPointers(
|
|
Av1FrameBuffer<byte> frameBuffer,
|
|
int plane,
|
|
int blockColumnInPixels,
|
|
int blockRowInPixels,
|
|
out Span<byte> blockReconstructionBuffer,
|
|
out int reconstructionStride,
|
|
int subX,
|
|
int subY)
|
|
{
|
|
int blockOffset;
|
|
|
|
switch (plane)
|
|
{
|
|
case 0:
|
|
reconstructionStride = frameBuffer.BufferY!.Width;
|
|
blockOffset = ((frameBuffer.OriginY + blockRowInPixels) * reconstructionStride) +
|
|
(frameBuffer.OriginX + blockColumnInPixels);
|
|
break;
|
|
case 1:
|
|
reconstructionStride = frameBuffer.BufferCb!.Width;
|
|
blockOffset = (((frameBuffer.OriginY >> subY) + blockRowInPixels) * reconstructionStride) +
|
|
((frameBuffer.OriginX >> subX) + blockColumnInPixels);
|
|
break;
|
|
default:
|
|
reconstructionStride = frameBuffer.BufferCr!.Width;
|
|
blockOffset = (((frameBuffer.OriginY >> subY) + blockRowInPixels) * reconstructionStride) +
|
|
((frameBuffer.OriginX >> subX) + blockColumnInPixels);
|
|
break;
|
|
}
|
|
|
|
// Prediction addresses above samples relative to the returned span, so expose the previous row as index zero.
|
|
blockOffset -= reconstructionStride;
|
|
Guard.MustBeGreaterThanOrEqualTo(blockOffset, 0, nameof(blockOffset));
|
|
|
|
if (frameBuffer.BitDepth != Av1BitDepth.EightBit || frameBuffer.Is16BitPipeline)
|
|
{
|
|
// The legacy byte view represents each high-bit-depth sample with two adjacent storage elements.
|
|
blockOffset *= 2;
|
|
if (plane == 0)
|
|
{
|
|
blockReconstructionBuffer = frameBuffer.BufferY!.DangerousGetSingleSpan()[blockOffset..];
|
|
}
|
|
else if (plane == 1)
|
|
{
|
|
blockReconstructionBuffer = frameBuffer.BufferCb!.DangerousGetSingleSpan()[blockOffset..];
|
|
}
|
|
else
|
|
{
|
|
blockReconstructionBuffer = frameBuffer.BufferCr!.DangerousGetSingleSpan()[blockOffset..];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (plane == 0)
|
|
{
|
|
blockReconstructionBuffer = frameBuffer.BufferY!.DangerousGetSingleSpan()[blockOffset..];
|
|
}
|
|
else if (plane == 1)
|
|
{
|
|
blockReconstructionBuffer = frameBuffer.BufferCb!.DangerousGetSingleSpan()[blockOffset..];
|
|
}
|
|
else
|
|
{
|
|
blockReconstructionBuffer = frameBuffer.BufferCr!.DangerousGetSingleSpan()[blockOffset..];
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether reconstructed luma samples must be retained for a later chroma-from-luma prediction.
|
|
/// </summary>
|
|
/// <param name="colorConfig">The sequence color-plane configuration.</param>
|
|
/// <param name="partitionInfo">The current block and its prediction modes.</param>
|
|
/// <returns>
|
|
/// <see langword="true"/> when chroma is present and the current luma block can contribute to a chroma-from-luma block.
|
|
/// </returns>
|
|
private static bool StoreChromaFromLumaRequired(ObuColorConfig colorConfig, ref Av1PartitionInfo partitionInfo)
|
|
=> !colorConfig.IsMonochrome &&
|
|
(!partitionInfo.IsChroma || partitionInfo.ModeInfo.UvMode == Av1ChromaPredictionMode.ChromaFromLuma);
|
|
}
|
|
|