📷 A modern, cross-platform, 2D Graphics library for .NET
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.
 
 

1923 lines
90 KiB

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// <summary>
/// Reconstructs AV1 intra-predicted transform blocks from neighboring samples and decoded mode information.
/// </summary>
/// <remarks>
/// This type implements the intra prediction portion of the AV1 reconstruction process for 8-, 10-, and 12-bit
/// samples. Intra-edge filtering and upsampling operate on caller-owned padded scratch: adjacent reference samples map
/// to adjacent SIMD lanes, exact-width stores interleave filtered half samples with the original edge, and scalar
/// continuations handle only incomplete vectors. The completed edges then feed the closed prediction operators.
/// </remarks>
internal class Av1PredictionDecoder
{
/// <summary>
/// The largest edge length for which AV1 permits intra-edge upsampling.
/// </summary>
private const int MaxUpsampleSize = 16;
/// <summary>
/// The number of samples reserved for one prepared AV1 intra-prediction edge.
/// </summary>
private const int ReferenceBufferLength = (Av1Constants.MaxTransformSize * 2) + 32;
/// <summary>
/// The padded sample count required by the widest intra-edge SIMD loads.
/// </summary>
private const int EdgeScratchLength = 160;
/// <summary>
/// The number of high-bit-depth samples required by the reusable prediction workspace.
/// </summary>
public const int ScratchLength = Av1DirectionalIntraPredictor.ScratchLength + (2 * ReferenceBufferLength) + EdgeScratchLength;
/// <summary>
/// The sequence-level syntax that controls chroma sampling, bit depth, superblock size, and intra-edge filtering.
/// </summary>
private readonly ObuSequenceHeader sequenceHeader;
/// <summary>
/// The frame-level syntax that controls segment lossless state and prediction behavior.
/// </summary>
private readonly ObuFrameHeader frameHeader;
/// <summary>
/// The frame-owned workspace shared by directional and filter-intra predictors.
/// </summary>
private readonly Memory<short> predictorScratch;
/// <summary>
/// Initializes a new instance of the <see cref="Av1PredictionDecoder"/> class.
/// </summary>
/// <param name="sequenceHeader">The decoded sequence header for the current image.</param>
/// <param name="frameHeader">The decoded frame header for the current image.</param>
/// <param name="predictorScratch">The reusable predictor workspace owned by the containing block decoder.</param>
public Av1PredictionDecoder(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, Memory<short> predictorScratch)
{
this.sequenceHeader = sequenceHeader;
this.frameHeader = frameHeader;
this.predictorScratch = predictorScratch;
}
/// <summary>
/// Reconstructs an 8-bit intra-predicted transform block.
/// </summary>
/// <param name="partitionInfo">The decoded partition and mode state for the containing block.</param>
/// <param name="plane">The color plane being reconstructed.</param>
/// <param name="transformSize">The dimensions of the transform block.</param>
/// <param name="tileInfo">The tile boundaries used to determine neighboring-sample availability.</param>
/// <param name="pixelBuffer">The sample buffer beginning at the row above the destination block.</param>
/// <param name="pixelStride">The distance, in samples, between pixel rows.</param>
/// <param name="bitDepth">The bit depth of the reconstructed samples.</param>
/// <param name="blockModeInfoColumnOffset">The transform block's horizontal offset within the mode-information block.</param>
/// <param name="blockModeInfoRowOffset">The transform block's vertical offset within the mode-information block.</param>
/// <remarks>Corresponds to <c>svt_av1_predict_intra</c> in SVT-AV1.</remarks>
public void Decode(
ref Av1PartitionInfo partitionInfo,
Av1Plane plane,
Av1TransformSize transformSize,
Av1TileInfo tileInfo,
Span<byte> pixelBuffer,
int pixelStride,
Av1BitDepth bitDepth,
int blockModeInfoColumnOffset,
int blockModeInfoRowOffset)
=> this.DecodeCore(
ref partitionInfo,
plane,
transformSize,
tileInfo,
pixelBuffer,
pixelStride,
bitDepth,
blockModeInfoColumnOffset,
blockModeInfoRowOffset);
/// <summary>
/// Reconstructs a 10-bit or 12-bit intra-predicted transform block.
/// </summary>
/// <param name="partitionInfo">The decoded partition and mode state for the containing block.</param>
/// <param name="plane">The color plane being reconstructed.</param>
/// <param name="transformSize">The dimensions of the transform block.</param>
/// <param name="tileInfo">The tile boundaries used to determine neighboring-sample availability.</param>
/// <param name="pixelBuffer">The sample buffer beginning at the row above the destination block.</param>
/// <param name="pixelStride">The distance, in samples, between pixel rows.</param>
/// <param name="bitDepth">The bit depth of the reconstructed samples.</param>
/// <param name="blockModeInfoColumnOffset">The transform block's horizontal offset within the mode-information block.</param>
/// <param name="blockModeInfoRowOffset">The transform block's vertical offset within the mode-information block.</param>
/// <remarks>Implements the intra prediction portion of section 7.11.2 of the AV1 specification.</remarks>
public void Decode(
ref Av1PartitionInfo partitionInfo,
Av1Plane plane,
Av1TransformSize transformSize,
Av1TileInfo tileInfo,
Span<short> pixelBuffer,
int pixelStride,
Av1BitDepth bitDepth,
int blockModeInfoColumnOffset,
int blockModeInfoRowOffset)
=> this.DecodeCore(
ref partitionInfo,
plane,
transformSize,
tileInfo,
pixelBuffer,
pixelStride,
bitDepth,
blockModeInfoColumnOffset,
blockModeInfoRowOffset);
/// <summary>
/// Reconstructs an intra-predicted transform block in its native sample representation.
/// </summary>
/// <typeparam name="T">The 8-bit or high-bit-depth sample type.</typeparam>
/// <param name="partitionInfo">The decoded partition and mode state for the containing block.</param>
/// <param name="plane">The color plane being reconstructed.</param>
/// <param name="transformSize">The dimensions of the transform block.</param>
/// <param name="tileInfo">The tile boundaries used to determine neighboring-sample availability.</param>
/// <param name="pixelBuffer">The sample buffer beginning at the row above the destination block.</param>
/// <param name="pixelStride">The distance, in samples, between pixel rows.</param>
/// <param name="bitDepth">The bit depth of the reconstructed samples.</param>
/// <param name="blockModeInfoColumnOffset">The transform block's horizontal offset within the mode-information block.</param>
/// <param name="blockModeInfoRowOffset">The transform block's vertical offset within the mode-information block.</param>
private void DecodeCore<T>(
ref Av1PartitionInfo partitionInfo,
Av1Plane plane,
Av1TransformSize transformSize,
Av1TileInfo tileInfo,
Span<T> pixelBuffer,
int pixelStride,
Av1BitDepth bitDepth,
int blockModeInfoColumnOffset,
int blockModeInfoRowOffset)
where T : unmanaged, IBinaryInteger<T>
{
int stride = pixelStride;
// Unlike SVT's separate destination and reference pointers, this span begins at the
// previous row. That layout exposes the top, top-left, and strided left samples without copying.
Span<T> topNeighbor = pixelBuffer;
Span<T> leftNeighbor = pixelBuffer[(stride - 1)..];
Span<T> startOfPixels = pixelBuffer[stride..];
Av1PredictionMode mode = partitionInfo.ModeInfo.YMode;
if (plane != Av1Plane.Y && partitionInfo.ModeInfo.UvMode == Av1ChromaPredictionMode.ChromaFromLuma)
{
this.PredictIntraBlock(
ref partitionInfo,
plane,
transformSize,
tileInfo,
startOfPixels,
stride,
topNeighbor,
leftNeighbor,
stride,
Av1PredictionMode.DC,
blockModeInfoColumnOffset,
blockModeInfoRowOffset,
bitDepth);
this.PredictChromaFromLumaBlock(
ref partitionInfo,
partitionInfo.ChromaFromLumaContext,
startOfPixels,
stride,
transformSize,
plane);
return;
}
if (plane != Av1Plane.Y)
{
// Chroma and luma modes are separate bitstream domains. Shared spatial predictors consume the explicit
// libaom get_uv_mode() equivalent rather than relying on their matching ordinal values.
mode = partitionInfo.ModeInfo.UvMode.ToLumaMode();
}
this.PredictIntraBlock(
ref partitionInfo,
plane,
transformSize,
tileInfo,
startOfPixels,
stride,
topNeighbor,
leftNeighbor,
stride,
mode,
blockModeInfoColumnOffset,
blockModeInfoRowOffset,
bitDepth);
}
/// <summary>
/// Applies chroma-from-luma scaling to the DC prediction for one chroma transform block.
/// </summary>
/// <typeparam name="T">The 8-bit or high-bit-depth sample type.</typeparam>
/// <param name="partitionInfo">The decoded partition and mode state for the containing block.</param>
/// <param name="chromaFromLumaContext">The block-level luma prediction context shared by the chroma planes.</param>
/// <param name="pixelBuffer">The DC-predicted chroma samples that receive the luma-derived adjustment.</param>
/// <param name="stride">The distance, in samples, between pixel rows.</param>
/// <param name="transformSize">The dimensions of the chroma transform block.</param>
/// <param name="plane">The U or V plane being reconstructed.</param>
private void PredictChromaFromLumaBlock<T>(
ref Av1PartitionInfo partitionInfo,
Av1ChromaFromLumaContext? chromaFromLumaContext,
Span<T> pixelBuffer,
int stride,
Av1TransformSize transformSize,
Av1Plane plane)
where T : unmanaged, IBinaryInteger<T>
{
Av1BlockModeInfo modeInfo = partitionInfo.ModeInfo;
bool isChromaFromLumaAllowedFlag = IsChromaFromLumaAllowedWithFrameHeader(ref partitionInfo, this.sequenceHeader.ColorConfig, this.frameHeader);
DebugGuard.IsTrue(isChromaFromLumaAllowedFlag, "Chroma from Luma should be allowed then computing it.");
if (chromaFromLumaContext == null)
{
throw new InvalidOperationException("CFL context should have been defined already.");
}
// U computes the shared subsampled-luma parameters first; V reuses them for the
// same block because both chroma planes have identical sampling geometry.
if (!chromaFromLumaContext.AreParametersComputed)
{
chromaFromLumaContext.ComputeParameters(transformSize);
}
int alphaQ3 = ChromaFromLumaIndexToAlpha(modeInfo.ChromaFromLumaAlphaIndex, modeInfo.ChromaFromLumaAlphaSign, plane);
Av1BitDepth bitDepth = this.sequenceHeader.ColorConfig.BitDepth;
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
if (typeof(T) == typeof(byte))
{
Av1ChromaFromLumaPredictor.Predict(chromaFromLumaContext.Q3Buffer, MemoryMarshal.Cast<T, byte>(pixelBuffer), stride, alphaQ3, width, height);
}
else
{
Av1ChromaFromLumaPredictor.Predict(chromaFromLumaContext.Q3Buffer, MemoryMarshal.Cast<T, short>(pixelBuffer), stride, alphaQ3, bitDepth.GetBitCount(), width, height);
}
}
/// <summary>
/// Determines whether chroma-from-luma prediction is permitted for the current block and frame state.
/// </summary>
/// <param name="partitionInfo">The decoded partition and mode state for the containing block.</param>
/// <param name="colorConfig">The sequence color configuration.</param>
/// <param name="frameHeader">The decoded frame header.</param>
/// <returns><see langword="true"/> when the block may use chroma-from-luma prediction; otherwise, <see langword="false"/>.</returns>
private static bool IsChromaFromLumaAllowedWithFrameHeader(ref Av1PartitionInfo partitionInfo, ObuColorConfig colorConfig, ObuFrameHeader frameHeader)
{
Av1BlockModeInfo modeInfo = partitionInfo.ModeInfo;
Av1BlockSize blockSize = modeInfo.BlockSize;
DebugGuard.MustBeLessThan((int)blockSize, (int)Av1BlockSize.AllSizes, nameof(blockSize));
if (frameHeader.LosslessArray[modeInfo.SegmentId])
{
// In lossless, CfL is available when the partition size is equal to the
// transform size.
bool subX = colorConfig.SubSamplingX;
bool subY = colorConfig.SubSamplingY;
Av1BlockSize planeBlockSize = blockSize.GetSubsampled(subX, subY);
return planeBlockSize == Av1BlockSize.Block4x4;
}
// Outside lossless mode, AV1 limits CfL to luma partitions no larger than 32 by 32.
return blockSize.GetWidth() <= 32 && blockSize.GetHeight() <= 32;
}
/// <summary>
/// Converts the packed chroma-from-luma magnitude and joint sign into a signed Q3 scaling factor.
/// </summary>
/// <param name="alphaIndex">The packed U and V alpha magnitudes.</param>
/// <param name="jointSign">The joint U and V alpha-sign symbol.</param>
/// <param name="plane">The U or V plane whose alpha value is selected.</param>
/// <returns>The signed Q3 alpha value for the selected chroma plane.</returns>
public static int ChromaFromLumaIndexToAlpha(int alphaIndex, int jointSign, Av1Plane plane)
{
int alphaSign = (plane == Av1Plane.U) ? Av1ChromaFromLumaMath.SignU(jointSign) : Av1ChromaFromLumaMath.SignV(jointSign);
if (alphaSign == Av1ChromaFromLumaMath.SignZero)
{
return 0;
}
int absAlphaQ3 = (plane == Av1Plane.U) ? Av1ChromaFromLumaMath.IndexU(alphaIndex) : Av1ChromaFromLumaMath.IndexV(alphaIndex);
return (alphaSign == Av1ChromaFromLumaMath.SignPositive) ? absAlphaQ3 + 1 : -absAlphaQ3 - 1;
}
/// <summary>
/// Determines available reference samples and dispatches prediction for one transform block.
/// </summary>
/// <typeparam name="T">The 8-bit or high-bit-depth sample type.</typeparam>
/// <param name="partitionInfo">The decoded partition and mode state for the containing block.</param>
/// <param name="plane">The color plane being reconstructed.</param>
/// <param name="transformSize">The dimensions of the transform block.</param>
/// <param name="tileInfo">The tile boundaries used to determine neighboring-sample availability.</param>
/// <param name="pixelBuffer">The destination samples for the transform block.</param>
/// <param name="pixelBufferStride">The distance, in samples, between destination rows.</param>
/// <param name="topNeighbor">The reconstructed samples along the top edge.</param>
/// <param name="leftNeighbor">The reconstructed samples along the left edge.</param>
/// <param name="referenceStride">The distance, in samples, between consecutive left-edge references.</param>
/// <param name="mode">The intra prediction mode to apply.</param>
/// <param name="blockModeInfoColumnOffset">The transform block's horizontal offset within the mode-information block.</param>
/// <param name="blockModeInfoRowOffset">The transform block's vertical offset within the mode-information block.</param>
/// <param name="bitDepth">The bit depth of the reconstructed samples.</param>
private void PredictIntraBlock<T>(
ref Av1PartitionInfo partitionInfo,
Av1Plane plane,
Av1TransformSize transformSize,
Av1TileInfo tileInfo,
Span<T> pixelBuffer,
int pixelBufferStride,
Span<T> topNeighbor,
ReadOnlySpan<T> leftNeighbor,
int referenceStride,
Av1PredictionMode mode,
int blockModeInfoColumnOffset,
int blockModeInfoRowOffset,
Av1BitDepth bitDepth)
where T : unmanaged, IBinaryInteger<T>
{
ObuColorConfig cc = this.sequenceHeader.ColorConfig;
int subX = plane != Av1Plane.Y ? cc.SubSamplingX ? 1 : 0 : 0;
int subY = plane != Av1Plane.Y ? cc.SubSamplingY ? 1 : 0 : 0;
Av1BlockModeInfo modeInfo = partitionInfo.ModeInfo;
int transformWidth = transformSize.GetWidth();
int transformHeight = transformSize.GetHeight();
int transformWidthInModeInfoUnits = transformSize.Get4x4WideCount();
int transformHeightInModeInfoUnits = transformSize.Get4x4HighCount();
bool usePalette = modeInfo.GetPaletteSize(plane) > 0;
if (usePalette)
{
ReadOnlySpan<ushort> paletteColors = modeInfo.GetPaletteColors(plane);
ReadOnlySpan<byte> colorIndexMap = modeInfo.GetPaletteColorIndexMap(plane);
int paletteStride = partitionInfo.GetWidthInPixels(plane);
int mapOffset = ((blockModeInfoRowOffset << Av1Constants.ModeInfoSizeLog2) * paletteStride) +
(blockModeInfoColumnOffset << Av1Constants.ModeInfoSizeLog2);
// Every transform reconstructs its own window of the block-level palette map. Keeping the map padded to
// the coded block dimensions lets edge transforms use the same addressing rule as interior transforms.
if (typeof(T) == typeof(byte))
{
Span<byte> byteDestination = MemoryMarshal.Cast<T, byte>(pixelBuffer);
Av1PalettePredictor.Predict(paletteColors, colorIndexMap[mapOffset..], paletteStride, byteDestination, pixelBufferStride, transformWidth, transformHeight);
}
else
{
Span<short> highBitDepthDestination = MemoryMarshal.Cast<T, short>(pixelBuffer);
Av1PalettePredictor.Predict(paletteColors, colorIndexMap[mapOffset..], paletteStride, highBitDepthDestination, pixelBufferStride, transformWidth, transformHeight);
}
return;
}
Av1FilterIntraMode filterIntraMode = (plane == Av1Plane.Y && modeInfo.UseFilterIntra)
? modeInfo.FilterIntraMode : Av1FilterIntraMode.AllFilterIntraModes;
int angleDelta = modeInfo.GetAngleDelta(plane);
Av1BlockSize blockSize = modeInfo.BlockSize;
bool haveTop = blockModeInfoRowOffset > 0 || (subY > 0 ? partitionInfo.AvailableAboveForChroma : partitionInfo.AvailableAbove);
bool haveLeft = blockModeInfoColumnOffset > 0 || (subX > 0 ? partitionInfo.AvailableLeftForChroma : partitionInfo.AvailableLeft);
int modeInfoRow = -partitionInfo.ModeBlockToTopEdge >> (3 + Av1Constants.ModeInfoSizeLog2);
int modeInfoColumn = -partitionInfo.ModeBlockToLeftEdge >> (3 + Av1Constants.ModeInfoSizeLog2);
int xrOffset = 0;
int ydOffset = 0;
// These distances bound edge extension at the coded frame rather than allowing
// a transform to read padding that happens to exist beyond the visible image.
int xr = (partitionInfo.ModeBlockToRightEdge >> (3 + subX)) +
(partitionInfo.GetWidthInPixels(plane) - (blockModeInfoColumnOffset << Av1Constants.ModeInfoSizeLog2) - transformWidth) -
xrOffset;
int yd = (partitionInfo.ModeBlockToBottomEdge >> (3 + subY)) +
(partitionInfo.GetHeightInPixels(plane) - (blockModeInfoRowOffset << Av1Constants.ModeInfoSizeLog2) - transformHeight) - ydOffset;
bool rightAvailable = modeInfoColumn + ((blockModeInfoColumnOffset + transformWidthInModeInfoUnits) << subX) < tileInfo.ModeInfoColumnEnd;
bool bottomAvailable = (yd > 0) && (modeInfoRow + ((blockModeInfoRowOffset + transformHeightInModeInfoUnits) << subY) < tileInfo.ModeInfoRowEnd);
Av1PartitionType partition = modeInfo.PartitionType;
// Chroma prediction geometry cannot be smaller than 4 by 4 after subsampling.
blockSize = ScaleChromaBlockSize(blockSize, subX == 1, subY == 1);
bool haveTopRight = IntraHasTopRight(
this.sequenceHeader.SuperblockSize,
blockSize,
modeInfoRow,
modeInfoColumn,
haveTop,
rightAvailable,
partition,
transformSize,
blockModeInfoRowOffset,
blockModeInfoColumnOffset,
subX,
subY);
bool haveBottomLeft = IntraHasBottomLeft(
this.sequenceHeader.SuperblockSize,
blockSize,
modeInfoRow,
modeInfoColumn,
bottomAvailable,
haveLeft,
partition,
transformSize,
blockModeInfoRowOffset,
blockModeInfoColumnOffset,
subX,
subY);
bool disableEdgeFilter = !this.sequenceHeader.EnableIntraEdgeFilter;
// Calling all other intra predictors except CFL and palette.
this.DecodeBuildIntraPredictors(
ref partitionInfo,
topNeighbor,
leftNeighbor,
(nuint)referenceStride,
pixelBuffer,
(nuint)pixelBufferStride,
mode,
angleDelta,
filterIntraMode,
transformSize,
disableEdgeFilter,
haveTop ? Math.Min(transformWidth, xr + transformWidth) : 0,
haveTopRight ? Math.Min(transformWidth, xr) : 0,
haveLeft ? Math.Min(transformHeight, yd + transformHeight) : 0,
haveBottomLeft ? Math.Min(transformHeight, yd) : 0,
plane,
bitDepth.GetBitCount());
}
/// <summary>
/// Adjusts sub-8-by-8 luma block geometry to the minimum chroma prediction block size.
/// </summary>
/// <param name="blockSize">The luma block size.</param>
/// <param name="subX">A value indicating whether chroma is horizontally subsampled.</param>
/// <param name="subY">A value indicating whether chroma is vertically subsampled.</param>
/// <returns>The block size used to evaluate chroma reference availability.</returns>
private static Av1BlockSize ScaleChromaBlockSize(Av1BlockSize blockSize, bool subX, bool subY)
{
Av1BlockSize bs = blockSize;
switch (blockSize)
{
case Av1BlockSize.Block4x4:
if (subX && subY)
{
bs = Av1BlockSize.Block8x8;
}
else if (subX)
{
bs = Av1BlockSize.Block8x4;
}
else if (subY)
{
bs = Av1BlockSize.Block4x8;
}
break;
case Av1BlockSize.Block4x8:
if (subX && subY)
{
bs = Av1BlockSize.Block8x8;
}
else if (subX)
{
bs = Av1BlockSize.Block8x8;
}
else if (subY)
{
bs = Av1BlockSize.Block4x8;
}
break;
case Av1BlockSize.Block8x4:
if (subX && subY)
{
bs = Av1BlockSize.Block8x8;
}
else if (subX)
{
bs = Av1BlockSize.Block8x4;
}
else if (subY)
{
bs = Av1BlockSize.Block8x8;
}
break;
case Av1BlockSize.Block4x16:
if (subX && subY)
{
bs = Av1BlockSize.Block8x16;
}
else if (subX)
{
bs = Av1BlockSize.Block8x16;
}
else if (subY)
{
bs = Av1BlockSize.Block4x16;
}
break;
case Av1BlockSize.Block16x4:
if (subX && subY)
{
bs = Av1BlockSize.Block16x8;
}
else if (subX)
{
bs = Av1BlockSize.Block16x4;
}
else if (subY)
{
bs = Av1BlockSize.Block16x8;
}
break;
default:
break;
}
return bs;
}
/// <summary>
/// Determines whether every bottom-left reference sample required by a transform is already reconstructed.
/// </summary>
/// <param name="superblockSize">The sequence superblock size.</param>
/// <param name="blockSize">The containing block size in the current plane's geometry.</param>
/// <param name="modeInfoRow">The containing block row in 4-by-4 mode-information units.</param>
/// <param name="modeInfoColumn">The containing block column in 4-by-4 mode-information units.</param>
/// <param name="bottomAvailable">A value indicating whether the required rows remain inside the frame and tile.</param>
/// <param name="haveLeft">A value indicating whether reconstructed samples exist immediately to the left.</param>
/// <param name="partition">The partition type that determines reconstruction order.</param>
/// <param name="transformSize">The transform size whose extended edge is required.</param>
/// <param name="blockModeInfoRowOffset">The transform row offset within the containing block.</param>
/// <param name="blockModeInfoColumnOffset">The transform column offset within the containing block.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <returns><see langword="true"/> when the bottom-left reference extension is available; otherwise, <see langword="false"/>.</returns>
private static bool IntraHasBottomLeft(Av1BlockSize superblockSize, Av1BlockSize blockSize, int modeInfoRow, int modeInfoColumn, bool bottomAvailable, bool haveLeft, Av1PartitionType partition, Av1TransformSize transformSize, int blockModeInfoRowOffset, int blockModeInfoColumnOffset, int subX, int subY)
{
if (!bottomAvailable || !haveLeft)
{
return false;
}
// A 128-wide block is reconstructed as two 64-wide regions in raster order,
// so the right half can consume references that already belong to the left half.
if (blockSize.GetWidth() > 64 && blockModeInfoColumnOffset > 0)
{
int block64WidthInUnits = Av1BlockSize.Block64x64.Get4x4WideCount();
int planeBlockWidthInUnits64 = block64WidthInUnits >> subX;
int columnOffset64 = blockModeInfoColumnOffset % planeBlockWidthInUnits64;
if (columnOffset64 == 0)
{
// We are at the left edge of top-right or bottom-right 64x* block.
int block64HeightInUnits = Av1BlockSize.Block64x64.Get4x4HighCount();
int planeBlockHeightInUnits64 = block64HeightInUnits >> subY;
int rowOffset64 = blockModeInfoRowOffset % planeBlockHeightInUnits64;
int planeBlockHeightInUnits = Math.Min(blockSize.Get4x4HighCount() >> subY, planeBlockHeightInUnits64);
// Check if all bottom-left pixels are in the left 64x* block (which is
// already coded).
return rowOffset64 + transformSize.Get4x4HighCount() < planeBlockHeightInUnits;
}
}
if (blockModeInfoColumnOffset > 0)
{
// Bottom-left pixels are in the bottom-left block, which is not available.
return false;
}
else
{
int blockHeightInUnits = blockSize.GetHeight() >> Av1TransformSize.Size4x4.GetBlockHeightLog2();
int planeBlockHeightInUnits = Math.Max(blockHeightInUnits >> subY, 1);
int bottomLeftUnitCount = transformSize.Get4x4HighCount();
// All bottom-left pixels are in the left block, which is already available.
if (blockModeInfoRowOffset + bottomLeftUnitCount < planeBlockHeightInUnits)
{
return true;
}
int blockWidthInModeInfoLog2 = blockSize.Get4x4WidthLog2();
int blockHeightInModeInfoLog2 = blockSize.Get4x4HeightLog2();
int superblockModeInfoSize = superblockSize.Get4x4HighCount();
int blockRowInSuperblock = (modeInfoRow & (superblockModeInfoSize - 1)) >> blockHeightInModeInfoLog2;
int blockColumnInSuperblock = (modeInfoColumn & (superblockModeInfoSize - 1)) >> blockWidthInModeInfoLog2;
// Leftmost column of superblock: so bottom-left pixels maybe in the left
// and/or bottom-left superblocks. But only the left superblock is
// available, so check if all required pixels fall in that superblock.
if (blockColumnInSuperblock == 0)
{
int blockStartRowOffset = blockRowInSuperblock << (blockHeightInModeInfoLog2 + Av1Constants.ModeInfoSizeLog2 - Av1TransformSize.Size4x4.GetBlockWidthLog2()) >> subY;
int rowOffsetInSuperblock = blockStartRowOffset + blockModeInfoRowOffset;
int superblockHeightInUnits = superblockModeInfoSize >> subY;
return rowOffsetInSuperblock + bottomLeftUnitCount < superblockHeightInUnits;
}
// Bottom row of superblock (and not the leftmost column): so bottom-left
// pixels fall in the bottom superblock, which is not available yet.
if (((blockRowInSuperblock + 1) << blockHeightInModeInfoLog2) >= superblockModeInfoSize)
{
return false;
}
// General case (neither leftmost column nor bottom row): check if the
// bottom-left block is coded before the current block.
int thisBlockIndex = ((blockRowInSuperblock + 0) << (Av1Constants.MaxSuperBlockSizeLog2 - Av1Constants.ModeInfoSizeLog2 - blockWidthInModeInfoLog2)) + blockColumnInSuperblock + 0;
return Av1BottomRightTopLeftConstants.HasBottomLeft(partition, blockSize, thisBlockIndex);
}
}
/// <summary>
/// Determines whether every top-right reference sample required by a transform is already reconstructed.
/// </summary>
/// <param name="superblockSize">The sequence superblock size.</param>
/// <param name="blockSize">The containing block size in the current plane's geometry.</param>
/// <param name="modeInfoRow">The containing block row in 4-by-4 mode-information units.</param>
/// <param name="modeInfoColumn">The containing block column in 4-by-4 mode-information units.</param>
/// <param name="haveTop">A value indicating whether reconstructed samples exist immediately above.</param>
/// <param name="rightAvailable">A value indicating whether the required columns remain inside the frame and tile.</param>
/// <param name="partition">The partition type that determines reconstruction order.</param>
/// <param name="transformSize">The transform size whose extended edge is required.</param>
/// <param name="blockModeInfoRowOffset">The transform row offset within the containing block.</param>
/// <param name="blockModeInfoColumnOffset">The transform column offset within the containing block.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <returns><see langword="true"/> when the top-right reference extension is available; otherwise, <see langword="false"/>.</returns>
private static bool IntraHasTopRight(Av1BlockSize superblockSize, Av1BlockSize blockSize, int modeInfoRow, int modeInfoColumn, bool haveTop, bool rightAvailable, Av1PartitionType partition, Av1TransformSize transformSize, int blockModeInfoRowOffset, int blockModeInfoColumnOffset, int subX, int subY)
{
if (!haveTop || !rightAvailable)
{
return false;
}
int blockWideInUnits = blockSize.GetWidth() >> 2;
int planeBlockWidthInUnits = Math.Max(blockWideInUnits >> subX, 1);
int topRightUnitCount = transformSize.Get4x4WideCount();
if (blockModeInfoRowOffset > 0)
{
// Transforms below the first row obtain their top edge from the containing block,
// so only the reconstructed width to their right constrains availability.
if (blockSize.GetWidth() > 64)
{
// Special case: For 128x128 blocks, the transform unit whose
// top-right corner is at the center of the block does in fact have
// pixels available at its top-right corner.
int block64WidthInUnits = Av1BlockSize.Block64x64.Get4x4WideCount();
int block64HeightInUnits = Av1BlockSize.Block64x64.Get4x4HighCount();
if (blockModeInfoRowOffset == block64HeightInUnits >> subY &&
blockModeInfoColumnOffset + topRightUnitCount == block64WidthInUnits >> subX)
{
return true;
}
int planeBlockWidthInUnits64 = block64WidthInUnits >> subX;
int blockModeInfoColumnOffset64 = blockModeInfoColumnOffset % planeBlockWidthInUnits64;
return blockModeInfoColumnOffset64 + topRightUnitCount < planeBlockWidthInUnits64;
}
return blockModeInfoColumnOffset + topRightUnitCount < planeBlockWidthInUnits;
}
else
{
// All top-right pixels are in the block above, which is already available.
if (blockModeInfoColumnOffset + topRightUnitCount < planeBlockWidthInUnits)
{
return true;
}
int blockWidthInModeInfoLog2 = blockSize.Get4x4WidthLog2();
int blockHeightInModeInfeLog2 = blockSize.Get4x4HeightLog2();
int superBlockModeInfoSize = superblockSize.Get4x4HighCount();
int blockRowInSuperblock = (modeInfoRow & (superBlockModeInfoSize - 1)) >> blockHeightInModeInfeLog2;
int blockColumnInSuperBlock = (modeInfoColumn & (superBlockModeInfoSize - 1)) >> blockWidthInModeInfoLog2;
// Top row of superblock: so top-right pixels are in the top and/or
// top-right superblocks, both of which are already available.
if (blockRowInSuperblock == 0)
{
return true;
}
// Rightmost column of superblock (and not the top row): so top-right pixels
// fall in the right superblock, which is not available yet.
if (((blockColumnInSuperBlock + 1) << blockWidthInModeInfoLog2) >= superBlockModeInfoSize)
{
return false;
}
// General case (neither top row nor rightmost column): check if the
// top-right block is coded before the current block.
int thisBlockIndex = ((blockRowInSuperblock + 0) << (Av1Constants.MaxSuperBlockSizeLog2 - Av1Constants.ModeInfoSizeLog2 - blockWidthInModeInfoLog2)) + blockColumnInSuperBlock + 0;
return Av1BottomRightTopLeftConstants.HasTopRight(partition, blockSize, thisBlockIndex);
}
}
/// <summary>
/// Prepares normative reference-edge samples and runs the selected intra predictor.
/// </summary>
/// <typeparam name="T">The 8-bit or high-bit-depth sample type.</typeparam>
/// <param name="partitionInfo">The decoded partition and neighboring mode state.</param>
/// <param name="aboveNeighbor">The reconstructed top and top-right reference samples.</param>
/// <param name="leftNeighbor">The reconstructed left and bottom-left reference samples.</param>
/// <param name="referenceStride">The distance, in samples, between consecutive left-edge references.</param>
/// <param name="destination">The buffer that receives the prediction block.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="mode">The intra prediction mode to apply.</param>
/// <param name="angleDelta">The coded directional angle adjustment.</param>
/// <param name="filterIntraMode">The selected filter intra mode, or the sentinel indicating that filter intra is disabled.</param>
/// <param name="transformSize">The dimensions of the prediction block.</param>
/// <param name="disableEdgeFilter">A value indicating whether intra-edge filtering and upsampling are disabled.</param>
/// <param name="topPixelCount">The number of available top samples.</param>
/// <param name="topRightPixelCount">The number of available top-right extension samples.</param>
/// <param name="leftPixelCount">The number of available left samples.</param>
/// <param name="bottomLeftPixelCount">The number of available bottom-left extension samples.</param>
/// <param name="plane">The color plane being reconstructed.</param>
/// <param name="bitDepth">The number of bits used to represent each sample.</param>
private void DecodeBuildIntraPredictors<T>(
ref Av1PartitionInfo partitionInfo,
Span<T> aboveNeighbor,
ReadOnlySpan<T> leftNeighbor,
nuint referenceStride,
Span<T> destination,
nuint destinationStride,
Av1PredictionMode mode,
int angleDelta,
Av1FilterIntraMode filterIntraMode,
Av1TransformSize transformSize,
bool disableEdgeFilter,
int topPixelCount,
int topRightPixelCount,
int leftPixelCount,
int bottomLeftPixelCount,
Av1Plane plane,
int bitDepth)
where T : unmanaged, IBinaryInteger<T>
{
int baseValue = 128 << (bitDepth - 8);
// The frame-owned allocation is sized in high-bit-depth samples. Reinterpreting it as T gives the byte
// path additional capacity while preserving the same sample offsets for the larger short representation.
Span<T> scratch = MemoryMarshal.Cast<short, T>(this.predictorScratch.Span);
Span<T> aboveData = scratch.Slice(Av1DirectionalIntraPredictor.ScratchLength, ReferenceBufferLength);
Span<T> leftData = scratch.Slice(Av1DirectionalIntraPredictor.ScratchLength + ReferenceBufferLength, ReferenceBufferLength);
Span<T> edgeScratch = scratch.Slice(Av1DirectionalIntraPredictor.ScratchLength + (2 * ReferenceBufferLength), EdgeScratchLength);
// Prefix storage is required because AV1 addresses the shared top-left sample at -1
// and writes upsampled edge samples as far back as -2.
aboveData.Fill(T.CreateChecked(baseValue - 1));
leftData.Fill(T.CreateChecked(baseValue + 1));
Span<T> aboveRow = aboveData[16..];
Span<T> leftColumn = leftData[16..];
int transformWidth = transformSize.GetWidth();
int transformHeight = transformSize.GetHeight();
bool isDirectionalMode = mode.IsDirectional();
Av1NeighborNeed need = mode.GetNeighborNeed();
bool needLeft = (need & Av1NeighborNeed.Left) == Av1NeighborNeed.Left;
bool needAbove = (need & Av1NeighborNeed.Above) == Av1NeighborNeed.Above;
bool needAboveLeft = (need & Av1NeighborNeed.AboveLeft) == Av1NeighborNeed.AboveLeft;
int angle = 0;
bool useFilterIntra = filterIntraMode != Av1FilterIntraMode.AllFilterIntraModes;
if (isDirectionalMode)
{
angle = mode.ToAngle() + (angleDelta * Av1Constants.AngleStep);
if (angle <= 90)
{
needAbove = true;
needLeft = false;
needAboveLeft = true;
}
else if (angle < 180)
{
needAbove = true;
needLeft = true;
needAboveLeft = true;
}
else
{
needAbove = false;
needLeft = true;
needAboveLeft = true;
}
}
if (useFilterIntra)
{
needAbove = true;
needLeft = true;
needAboveLeft = true;
}
DebugGuard.MustBeGreaterThanOrEqualTo(topPixelCount, 0, nameof(topPixelCount));
DebugGuard.MustBeGreaterThanOrEqualTo(topRightPixelCount, 0, nameof(topRightPixelCount));
DebugGuard.MustBeGreaterThanOrEqualTo(leftPixelCount, 0, nameof(leftPixelCount));
DebugGuard.MustBeGreaterThanOrEqualTo(bottomLeftPixelCount, 0, nameof(bottomLeftPixelCount));
if ((!needAbove && leftPixelCount == 0) || (!needLeft && topPixelCount == 0))
{
// Pure horizontal or vertical prediction with its sole required edge missing
// degenerates to the first perpendicular sample or the normative midpoint offset.
T value;
if (needLeft)
{
value = topPixelCount > 0 ? aboveNeighbor[0] : T.CreateChecked(baseValue + 1);
}
else
{
value = leftPixelCount > 0 ? leftNeighbor[0] : T.CreateChecked(baseValue - 1);
}
for (int i = 0; i < transformHeight; ++i)
{
destination.Slice(i * (int)destinationStride, transformWidth).Fill(value);
}
return;
}
// Copy the available left and bottom-left samples, then extend the final sample
// through any unavailable portion required by the selected predictor.
if (needLeft)
{
bool needBottom = (need & Av1NeighborNeed.BottomLeft) == Av1NeighborNeed.BottomLeft;
if (useFilterIntra)
{
needBottom = false;
}
if (isDirectionalMode)
{
needBottom = angle > 180;
}
int numLeftPixelsNeeded = transformHeight + (needBottom ? transformWidth : 0);
int i = 0;
if (leftPixelCount > 0)
{
for (; i < leftPixelCount; i++)
{
leftColumn[i] = leftNeighbor[i * (int)referenceStride];
}
if (needBottom && bottomLeftPixelCount > 0)
{
Guard.IsTrue(i == transformHeight, nameof(i), string.Empty);
for (; i < transformHeight + bottomLeftPixelCount; i++)
{
leftColumn[i] = leftNeighbor[i * (int)referenceStride];
}
}
if (i < numLeftPixelsNeeded)
{
leftColumn.Slice(i, numLeftPixelsNeeded - i).Fill(leftColumn[i - 1]);
}
}
else
{
if (topPixelCount > 0)
{
leftColumn[..numLeftPixelsNeeded].Fill(aboveNeighbor[0]);
}
else
{
leftColumn[..numLeftPixelsNeeded].Fill(T.CreateChecked(baseValue + 1));
}
}
}
// Prepare the top edge by the same copy-and-extend rule. Unlike the left edge,
// these samples are contiguous in the reconstructed pixel buffer.
if (needAbove)
{
bool needRight = (need & Av1NeighborNeed.AboveRight) == Av1NeighborNeed.AboveRight;
if (useFilterIntra)
{
needRight = false;
}
if (isDirectionalMode)
{
needRight = angle < 90;
}
int numTopPixelsNeeded = transformWidth + (needRight ? transformHeight : 0);
if (topPixelCount > 0)
{
aboveNeighbor[..topPixelCount].CopyTo(aboveRow);
int i = topPixelCount;
if (topRightPixelCount > 0)
{
Guard.IsTrue(topPixelCount == transformWidth, nameof(topPixelCount), string.Empty);
aboveNeighbor.Slice(transformWidth, topRightPixelCount).CopyTo(aboveRow[transformWidth..]);
i += topRightPixelCount;
}
if (i < numTopPixelsNeeded)
{
aboveRow.Slice(i, numTopPixelsNeeded - i).Fill(aboveRow[i - 1]);
}
}
else
{
if (leftPixelCount > 0)
{
aboveRow[..numTopPixelsNeeded].Fill(leftNeighbor[0]);
}
else
{
aboveRow[..numTopPixelsNeeded].Fill(T.CreateChecked(baseValue - 1));
}
}
}
if (needAboveLeft)
{
// AV1 synthesizes the shared corner from the closest available edge when only
// one edge exists, and uses the bit-depth midpoint when neither edge exists.
ref T aboveLeft = ref Unsafe.Subtract(ref aboveRow[0], 1);
if (topPixelCount > 0 && leftPixelCount > 0)
{
aboveLeft = Unsafe.Subtract(ref aboveNeighbor[0], 1);
}
else if (topPixelCount > 0)
{
aboveLeft = aboveNeighbor[0];
}
else if (leftPixelCount > 0)
{
aboveLeft = leftNeighbor[0];
}
else
{
aboveLeft = T.CreateChecked(baseValue);
}
Unsafe.Subtract(ref leftColumn[0], 1) = aboveLeft;
}
if (useFilterIntra)
{
this.FilterIntraPredictor(destination, destinationStride, transformSize, aboveRow, leftColumn, filterIntraMode, bitDepth);
return;
}
if (isDirectionalMode)
{
bool upsampleAbove = false;
bool upsampleLeft = false;
if (!disableEdgeFilter)
{
bool needRight = angle < 90;
bool needBottom = angle > 180;
bool filterType = GetFilterType(ref partitionInfo, plane);
if (angle is not 90 and not 180)
{
int ab_le = needAboveLeft ? 1 : 0;
if (needAbove && needLeft && (transformWidth + transformHeight >= 24))
{
FilterIntraEdgeCorner(aboveRow, leftColumn);
}
if (needAbove && topPixelCount > 0)
{
int strength = IntraEdgeFilterStrength(transformWidth, transformHeight, angle - 90, filterType);
int pixelCount = topPixelCount + ab_le + (needRight ? transformHeight : 0);
FilterIntraEdge(ref Unsafe.Subtract(ref aboveRow[0], ab_le), pixelCount, strength, edgeScratch);
}
if (needLeft && leftPixelCount > 0)
{
int strength = IntraEdgeFilterStrength(transformHeight, transformWidth, angle - 180, filterType);
int pixelCount = leftPixelCount + ab_le + (needBottom ? transformWidth : 0);
FilterIntraEdge(ref Unsafe.Subtract(ref leftColumn[0], ab_le), pixelCount, strength, edgeScratch);
}
}
upsampleAbove = UseIntraEdgeUpsample(transformWidth, transformHeight, angle - 90, filterType);
if (needAbove && upsampleAbove)
{
int pixelCount = transformWidth + (needRight ? transformHeight : 0);
UpsampleIntraEdge(aboveRow, pixelCount, bitDepth, edgeScratch);
}
upsampleLeft = UseIntraEdgeUpsample(transformHeight, transformWidth, angle - 180, filterType);
if (needLeft && upsampleLeft)
{
int pixelCount = transformHeight + (needBottom ? transformWidth : 0);
UpsampleIntraEdge(leftColumn, pixelCount, bitDepth, edgeScratch);
}
}
this.DirectionalPredictor(destination, destinationStride, transformSize, aboveRow, leftColumn, upsampleAbove, upsampleLeft, angle);
return;
}
if (mode == Av1PredictionMode.DC)
{
DcPredictor(leftPixelCount > 0, topPixelCount > 0, transformSize, destination, destinationStride, aboveRow, leftColumn, bitDepth);
}
else
{
GeneralPredictor(mode, transformSize, destination, destinationStride, aboveRow, leftColumn);
}
}
/// <summary>
/// Dispatches DC prediction to the 8-bit or high-bit-depth implementation.
/// </summary>
/// <typeparam name="T">The byte or 16-bit sample type.</typeparam>
/// <param name="hasLeft">A value indicating whether reconstructed left samples are available.</param>
/// <param name="hasAbove">A value indicating whether reconstructed top samples are available.</param>
/// <param name="transformSize">The dimensions of the prediction block.</param>
/// <param name="destination">The buffer that receives the predicted samples.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="above">The prepared top reference samples.</param>
/// <param name="left">The prepared left reference samples.</param>
/// <param name="bitDepth">The number of bits used to represent each sample.</param>
private static void DcPredictor<T>(bool hasLeft, bool hasAbove, Av1TransformSize transformSize, Span<T> destination, nuint destinationStride, Span<T> above, Span<T> left, int bitDepth)
where T : unmanaged
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
// DecodeCore is reachable only through byte and short overloads, so this type
// dispatch permits shared reference preparation without boxing or allocating.
if (typeof(T) == typeof(byte))
{
Av1DcIntraPredictor.Predict(
hasLeft,
hasAbove,
MemoryMarshal.Cast<T, byte>(destination),
(int)destinationStride,
MemoryMarshal.Cast<T, byte>(above),
MemoryMarshal.Cast<T, byte>(left),
width,
height);
}
else
{
Av1DcIntraPredictor.Predict(
hasLeft,
hasAbove,
MemoryMarshal.Cast<T, short>(destination),
(int)destinationStride,
MemoryMarshal.Cast<T, short>(above),
MemoryMarshal.Cast<T, short>(left),
width,
height,
bitDepth);
}
}
/// <summary>
/// Dispatches nondirectional prediction to the 8-bit or high-bit-depth implementation.
/// </summary>
/// <typeparam name="T">The byte or 16-bit sample type.</typeparam>
/// <param name="mode">The nondirectional prediction mode to apply.</param>
/// <param name="transformSize">The dimensions of the prediction block.</param>
/// <param name="destination">The buffer that receives the predicted samples.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="above">The prepared top reference samples.</param>
/// <param name="left">The prepared left reference samples.</param>
private static void GeneralPredictor<T>(Av1PredictionMode mode, Av1TransformSize transformSize, Span<T> destination, nuint destinationStride, Span<T> above, Span<T> left)
where T : unmanaged
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
Av1IntraPredictorBase predictor = Av1IntraPredictorBase.GetPredictor(mode);
if (typeof(T) == typeof(byte))
{
predictor.Predict(
MemoryMarshal.Cast<T, byte>(destination),
(int)destinationStride,
MemoryMarshal.Cast<T, byte>(above),
MemoryMarshal.Cast<T, byte>(left),
width,
height);
}
else
{
predictor.Predict(
MemoryMarshal.Cast<T, short>(destination),
(int)destinationStride,
MemoryMarshal.Cast<T, short>(above),
MemoryMarshal.Cast<T, short>(left),
width,
height);
}
}
/// <summary>
/// Dispatches directional prediction to the 8-bit or high-bit-depth implementation.
/// </summary>
/// <typeparam name="T">The byte or 16-bit sample type.</typeparam>
/// <param name="destination">The buffer that receives the predicted samples.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="transformSize">The dimensions of the prediction block.</param>
/// <param name="above">The prepared top reference samples.</param>
/// <param name="left">The prepared left reference samples.</param>
/// <param name="upsampleAbove">A value indicating whether the top edge was upsampled.</param>
/// <param name="upsampleLeft">A value indicating whether the left edge was upsampled.</param>
/// <param name="angle">The adjusted prediction angle in degrees.</param>
private void DirectionalPredictor<T>(Span<T> destination, nuint destinationStride, Av1TransformSize transformSize, Span<T> above, Span<T> left, bool upsampleAbove, bool upsampleLeft, int angle)
where T : unmanaged
{
if (typeof(T) == typeof(byte))
{
Span<byte> scratch = MemoryMarshal.AsBytes(this.predictorScratch.Span)[..Av1DirectionalIntraPredictor.ScratchLength];
Av1DirectionalIntraPredictor.Predict(
MemoryMarshal.Cast<T, byte>(destination),
(int)destinationStride,
transformSize,
MemoryMarshal.Cast<T, byte>(above),
MemoryMarshal.Cast<T, byte>(left),
upsampleAbove,
upsampleLeft,
angle,
scratch);
}
else
{
Av1DirectionalIntraPredictor.Predict(
MemoryMarshal.Cast<T, short>(destination),
(int)destinationStride,
transformSize,
MemoryMarshal.Cast<T, short>(above),
MemoryMarshal.Cast<T, short>(left),
upsampleAbove,
upsampleLeft,
angle,
this.predictorScratch.Span);
}
}
/// <summary>
/// Dispatches filter intra prediction to the 8-bit or high-bit-depth implementation.
/// </summary>
/// <typeparam name="T">The byte or 16-bit sample type.</typeparam>
/// <param name="destination">The buffer that receives the predicted samples.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="transformSize">The dimensions of the prediction block.</param>
/// <param name="above">The prepared top reference samples.</param>
/// <param name="left">The prepared left reference samples.</param>
/// <param name="mode">The filter intra mode whose coefficient set is applied.</param>
/// <param name="bitDepth">The number of bits used to represent each sample.</param>
private void FilterIntraPredictor<T>(Span<T> destination, nuint destinationStride, Av1TransformSize transformSize, Span<T> above, Span<T> left, Av1FilterIntraMode mode, int bitDepth)
where T : unmanaged
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
Av1FilterIntraPredictorBase predictor = Av1FilterIntraPredictorBase.GetPredictor(mode);
if (typeof(T) == typeof(byte))
{
Span<byte> scratch = MemoryMarshal.AsBytes(this.predictorScratch.Span)[..Av1FilterIntraPredictorBase.ScratchLength];
predictor.Predict(
MemoryMarshal.Cast<T, byte>(destination),
(int)destinationStride,
MemoryMarshal.Cast<T, byte>(above),
MemoryMarshal.Cast<T, byte>(left),
width,
height,
scratch);
}
else
{
predictor.Predict(
MemoryMarshal.Cast<T, short>(destination),
(int)destinationStride,
MemoryMarshal.Cast<T, short>(above),
MemoryMarshal.Cast<T, short>(left),
width,
height,
bitDepth,
this.predictorScratch.Span[..Av1FilterIntraPredictorBase.ScratchLength]);
}
}
/// <summary>
/// Inserts half-sample positions into a prepared intra-prediction edge.
/// </summary>
/// <typeparam name="T">The 8-bit or high-bit-depth sample type.</typeparam>
/// <param name="buffer">The edge buffer, including writable prefix storage at indices -2 and -1.</param>
/// <param name="count">The number of original edge samples to upsample.</param>
/// <param name="bitDepth">The number of bits used to clamp interpolated samples.</param>
/// <param name="scratch">The reusable padded source workspace.</param>
private static void UpsampleIntraEdge<T>(Span<T> buffer, int count, int bitDepth, Span<T> scratch)
where T : unmanaged, IBinaryInteger<T>
{
DebugGuard.MustBeLessThanOrEqualTo(count, MaxUpsampleSize, nameof(count));
// DecodeBuildIntraPredictors is closed only over byte and short. Keeping that dispatch outside the
// kernels gives the JIT concrete vector element types and removes generic arithmetic from their loops.
if (typeof(T) == typeof(byte))
{
UpsampleIntraEdge(MemoryMarshal.Cast<T, byte>(buffer), count, MemoryMarshal.Cast<T, byte>(scratch));
}
else
{
UpsampleIntraEdge(MemoryMarshal.Cast<T, short>(buffer), count, bitDepth, MemoryMarshal.Cast<T, short>(scratch));
}
}
/// <summary>
/// Inserts half-sample positions into an 8-bit intra-prediction edge.
/// </summary>
/// <param name="buffer">The edge buffer, including writable prefix storage at indices -2 and -1.</param>
/// <param name="count">The number of original edge samples to upsample.</param>
/// <param name="scratch">The reusable padded source workspace.</param>
public static void UpsampleIntraEdge(Span<byte> buffer, int count, Span<byte> scratch)
{
ref byte bufferBase = ref MemoryMarshal.GetReference(buffer);
ref byte inputBase = ref MemoryMarshal.GetReference(scratch);
byte beforeBuffer = Unsafe.Subtract(ref bufferBase, 1);
byte finalSample = Unsafe.Add(ref bufferBase, count - 1);
// Vector loads intentionally extend past the logical edge. Initializing the complete load window with
// the final sample provides the AV1 endpoint extension and keeps every unaligned read inside scratch.
scratch[..32].Fill(finalSample);
inputBase = beforeBuffer;
Unsafe.Add(ref inputBase, 1) = beforeBuffer;
buffer[..count].CopyTo(scratch[2..]);
Unsafe.Subtract(ref bufferBase, 2) = beforeBuffer;
int i = 0;
if (Vector128.IsHardwareAccelerated)
{
int eightSamplesFromEnd = count - 8;
for (; i <= eightSamplesFromEnd; i += 8)
{
Vector128<byte> interpolated = InterpolateEightBytes(ref inputBase, i);
Vector128<byte> originals = Vector128.LoadUnsafe(ref inputBase, (nuint)(i + 2));
Vector128<byte> interleaved = Vector128_.UnpackLow(interpolated, originals);
interleaved.StoreUnsafe(ref Unsafe.Add(ref bufferBase, (2 * i) - 1));
}
// AV1 upsampled edges are normally multiples of four. A half-vector store handles that common
// remainder without overwriting the prepared extension beyond the logical output edge.
if (i <= count - 4)
{
Vector128<byte> interpolated = InterpolateEightBytes(ref inputBase, i);
Vector128<byte> originals = Vector128.LoadUnsafe(ref inputBase, (nuint)(i + 2));
Vector128<byte> interleaved = Vector128_.UnpackLow(interpolated, originals);
Unsafe.As<byte, ulong>(ref Unsafe.Add(ref bufferBase, (2 * i) - 1)) = interleaved.AsUInt64().GetElement(0);
i += 4;
}
}
UpsampleIntraEdgeScalar(ref bufferBase, ref inputBase, i, count, 255);
}
/// <summary>
/// Inserts half-sample positions into a high-bit-depth intra-prediction edge.
/// </summary>
/// <param name="buffer">The edge buffer, including writable prefix storage at indices -2 and -1.</param>
/// <param name="count">The number of original edge samples to upsample.</param>
/// <param name="bitDepth">The number of bits used to clamp interpolated samples.</param>
/// <param name="scratch">The reusable padded source workspace.</param>
public static void UpsampleIntraEdge(Span<short> buffer, int count, int bitDepth, Span<short> scratch)
{
ref short bufferBase = ref MemoryMarshal.GetReference(buffer);
ref short inputBase = ref MemoryMarshal.GetReference(scratch);
short beforeBuffer = Unsafe.Subtract(ref bufferBase, 1);
short finalSample = Unsafe.Add(ref bufferBase, count - 1);
// The same padded layout is used for 10- and 12-bit edges. Arithmetic widens to Int32 before applying
// the four-tap kernel because the 12-bit intermediate exceeds the unsigned 16-bit range.
scratch[..32].Fill(finalSample);
inputBase = beforeBuffer;
Unsafe.Add(ref inputBase, 1) = beforeBuffer;
buffer[..count].CopyTo(scratch[2..]);
Unsafe.Subtract(ref bufferBase, 2) = beforeBuffer;
int maximum = (1 << bitDepth) - 1;
int i = 0;
if (Vector128.IsHardwareAccelerated)
{
int eightSamplesFromEnd = count - 8;
for (; i <= eightSamplesFromEnd; i += 8)
{
Vector128<short> interpolated = InterpolateEightHighBitDepthSamples(ref inputBase, i, maximum);
Vector128<short> originals = Vector128.LoadUnsafe(ref inputBase, (nuint)(i + 2));
Vector128<short> interleavedLow = Vector128_.UnpackLow(interpolated, originals);
Vector128<short> interleavedHigh = Vector128_.UnpackHigh(interpolated, originals);
ref short destination = ref Unsafe.Add(ref bufferBase, (2 * i) - 1);
interleavedLow.StoreUnsafe(ref destination);
interleavedHigh.StoreUnsafe(ref destination, (nuint)Vector128<short>.Count);
}
if (i <= count - 4)
{
Vector128<short> interpolated = InterpolateEightHighBitDepthSamples(ref inputBase, i, maximum);
Vector128<short> originals = Vector128.LoadUnsafe(ref inputBase, (nuint)(i + 2));
Vector128<short> interleaved = Vector128_.UnpackLow(interpolated, originals);
interleaved.StoreUnsafe(ref Unsafe.Add(ref bufferBase, (2 * i) - 1));
i += 4;
}
}
UpsampleIntraEdgeScalar(ref bufferBase, ref inputBase, i, count, maximum);
}
/// <summary>
/// Calculates eight 8-bit half-sample values in parallel.
/// </summary>
/// <param name="input">The first padded input sample.</param>
/// <param name="offset">The first output sample index.</param>
/// <returns>The interpolated samples in the lower eight lanes.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> InterpolateEightBytes(ref byte input, int offset)
{
Vector128<byte> source0 = Vector128.LoadUnsafe(ref input, (nuint)offset);
Vector128<byte> source1 = Vector128.LoadUnsafe(ref input, (nuint)(offset + 1));
Vector128<byte> source2 = Vector128.LoadUnsafe(ref input, (nuint)(offset + 2));
Vector128<byte> source3 = Vector128.LoadUnsafe(ref input, (nuint)(offset + 3));
(Vector128<ushort> source0Low, _) = Vector128.Widen(source0);
(Vector128<ushort> source1Low, _) = Vector128.Widen(source1);
(Vector128<ushort> source2Low, _) = Vector128.Widen(source2);
(Vector128<ushort> source3Low, _) = Vector128.Widen(source3);
Vector128<short> interpolation = (((source1Low + source2Low) * Vector128.Create((ushort)9)) - (source0Low + source3Low)).AsInt16();
interpolation = Vector128.Clamp((interpolation + Vector128.Create((short)8)) >> 4, Vector128<short>.Zero, Vector128.Create((short)255));
return Vector128.Narrow(interpolation.AsUInt16(), Vector128<ushort>.Zero);
}
/// <summary>
/// Calculates eight high-bit-depth half-sample values in parallel.
/// </summary>
/// <param name="input">The first padded input sample.</param>
/// <param name="offset">The first output sample index.</param>
/// <param name="maximum">The maximum reconstructed sample value.</param>
/// <returns>The interpolated samples.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<short> InterpolateEightHighBitDepthSamples(ref short input, int offset, int maximum)
{
Vector128<short> source0 = Vector128.LoadUnsafe(ref input, (nuint)offset);
Vector128<short> source1 = Vector128.LoadUnsafe(ref input, (nuint)(offset + 1));
Vector128<short> source2 = Vector128.LoadUnsafe(ref input, (nuint)(offset + 2));
Vector128<short> source3 = Vector128.LoadUnsafe(ref input, (nuint)(offset + 3));
(Vector128<int> source0Low, Vector128<int> source0High) = Vector128.Widen(source0);
(Vector128<int> source1Low, Vector128<int> source1High) = Vector128.Widen(source1);
(Vector128<int> source2Low, Vector128<int> source2High) = Vector128.Widen(source2);
(Vector128<int> source3Low, Vector128<int> source3High) = Vector128.Widen(source3);
Vector128<int> coefficient = Vector128.Create(9);
Vector128<int> rounding = Vector128.Create(8);
Vector128<int> maximumVector = Vector128.Create(maximum);
Vector128<int> low = ((((source1Low + source2Low) * coefficient) - (source0Low + source3Low)) + rounding) >> 4;
Vector128<int> high = ((((source1High + source2High) * coefficient) - (source0High + source3High)) + rounding) >> 4;
low = Vector128.Clamp(low, Vector128<int>.Zero, maximumVector);
high = Vector128.Clamp(high, Vector128<int>.Zero, maximumVector);
return Vector128.Narrow(low, high);
}
/// <summary>
/// Inserts the scalar remainder of an intra-edge upsample operation.
/// </summary>
/// <typeparam name="T">The byte or 16-bit sample type.</typeparam>
/// <param name="buffer">The first original edge sample.</param>
/// <param name="input">The first padded input sample.</param>
/// <param name="start">The first sample not processed by SIMD.</param>
/// <param name="count">The number of original edge samples.</param>
/// <param name="maximum">The maximum reconstructed sample value.</param>
private static void UpsampleIntraEdgeScalar<T>(ref T buffer, ref T input, int start, int count, int maximum)
where T : unmanaged, IBinaryInteger<T>
{
for (int i = start; i < count; i++)
{
int interpolation = -int.CreateChecked(Unsafe.Add(ref input, i))
+ (9 * int.CreateChecked(Unsafe.Add(ref input, i + 1)))
+ (9 * int.CreateChecked(Unsafe.Add(ref input, i + 2)))
- int.CreateChecked(Unsafe.Add(ref input, i + 3));
interpolation = Av1Math.Clamp((interpolation + 8) >> 4, 0, maximum);
Unsafe.Add(ref buffer, (2 * i) - 1) = T.CreateChecked(interpolation);
Unsafe.Add(ref buffer, 2 * i) = Unsafe.Add(ref input, i + 2);
}
}
/// <summary>
/// Determines whether AV1 intra-edge upsampling applies to a directional prediction edge.
/// </summary>
/// <param name="width">The edge's primary block dimension.</param>
/// <param name="height">The edge's secondary block dimension.</param>
/// <param name="delta">The prediction angle relative to the edge's cardinal direction.</param>
/// <param name="filterType">A value indicating whether a neighboring smooth mode selects the alternate thresholds.</param>
/// <returns><see langword="true"/> when the edge must be upsampled; otherwise, <see langword="false"/>.</returns>
/// <remarks>Corresponds to <c>svt_aom_use_intra_edge_upsample</c> in SVT-AV1.</remarks>
private static bool UseIntraEdgeUpsample(int width, int height, int delta, bool filterType)
{
int d = Math.Abs(delta);
if (d is <= 0 or >= 40)
{
return false;
}
int widthHeight = width + height;
return filterType ? (widthHeight <= 8) : (widthHeight <= 16);
}
/// <summary>
/// Applies the AV1 intra-edge smoothing kernel at the requested strength.
/// </summary>
/// <typeparam name="T">The 8-bit or high-bit-depth sample type.</typeparam>
/// <param name="buffer">A reference to the first edge sample to filter.</param>
/// <param name="count">The number of edge samples.</param>
/// <param name="strength">The AV1 filter-strength index from zero through three.</param>
/// <param name="scratch">The reusable padded source workspace.</param>
/// <remarks>Corresponds to <c>svt_av1_filter_intra_edge_c</c> in SVT-AV1.</remarks>
private static void FilterIntraEdge<T>(ref T buffer, int count, int strength, Span<T> scratch)
where T : unmanaged, IBinaryInteger<T>
{
if (strength == 0)
{
return;
}
// As with edge upsampling, closing the kernel over a concrete sample type keeps vector arithmetic
// outside the generic decoder while the valid strength-zero no-op remains at the owning boundary.
if (typeof(T) == typeof(byte))
{
FilterIntraEdge(ref Unsafe.As<T, byte>(ref buffer), count, strength, MemoryMarshal.Cast<T, byte>(scratch));
}
else
{
FilterIntraEdge(ref Unsafe.As<T, short>(ref buffer), count, strength, MemoryMarshal.Cast<T, short>(scratch));
}
}
/// <summary>
/// Applies an AV1 intra-edge smoothing kernel to 8-bit samples.
/// </summary>
/// <param name="buffer">A reference to the first edge sample to filter.</param>
/// <param name="count">The number of edge samples.</param>
/// <param name="strength">The AV1 filter-strength index from one through three.</param>
/// <param name="scratch">The reusable padded source workspace.</param>
public static void FilterIntraEdge(ref byte buffer, int count, int strength, Span<byte> scratch)
{
byte finalSample = Unsafe.Add(ref buffer, count - 1);
// The original edge is retained because each convolution window must observe unfiltered neighbors.
// Padding both endpoints also makes every complete vector use the same contiguous load pattern.
scratch[..EdgeScratchLength].Fill(finalSample);
scratch[0] = buffer;
MemoryMarshal.CreateReadOnlySpan(ref buffer, count).CopyTo(scratch[1..]);
ref byte edge = ref MemoryMarshal.GetReference(scratch);
int outputCount = count - 1;
int processed = 0;
if (Vector128.IsHardwareAccelerated)
{
int eightSamplesFromEnd = outputCount - 8;
switch (strength)
{
case 1:
for (; processed <= eightSamplesFromEnd; processed += 8)
{
Vector128<ushort> source0 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 1)));
Vector128<ushort> source1 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 2)));
Vector128<ushort> source2 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 3)));
Vector128<byte> result = Vector128.Narrow(FilterEdgeStrength1(source0, source1, source2), Vector128<ushort>.Zero);
Unsafe.As<byte, ulong>(ref Unsafe.Add(ref buffer, processed + 1)) = result.AsUInt64().GetElement(0);
}
break;
case 2:
for (; processed <= eightSamplesFromEnd; processed += 8)
{
Vector128<ushort> source0 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 1)));
Vector128<ushort> source1 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 2)));
Vector128<ushort> source2 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 3)));
Vector128<byte> result = Vector128.Narrow(FilterEdgeStrength2(source0, source1, source2), Vector128<ushort>.Zero);
Unsafe.As<byte, ulong>(ref Unsafe.Add(ref buffer, processed + 1)) = result.AsUInt64().GetElement(0);
}
break;
default:
for (; processed <= eightSamplesFromEnd; processed += 8)
{
Vector128<ushort> source0 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)processed));
Vector128<ushort> source1 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 1)));
Vector128<ushort> source2 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 2)));
Vector128<ushort> source3 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 3)));
Vector128<ushort> source4 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 4)));
Vector128<byte> result = Vector128.Narrow(FilterEdgeStrength3(source0, source1, source2, source3, source4), Vector128<ushort>.Zero);
Unsafe.As<byte, ulong>(ref Unsafe.Add(ref buffer, processed + 1)) = result.AsUInt64().GetElement(0);
}
break;
}
}
FilterIntraEdgeScalar(ref buffer, ref edge, processed, outputCount, strength);
}
/// <summary>
/// Applies an AV1 intra-edge smoothing kernel to high-bit-depth samples.
/// </summary>
/// <param name="buffer">A reference to the first edge sample to filter.</param>
/// <param name="count">The number of edge samples.</param>
/// <param name="strength">The AV1 filter-strength index from one through three.</param>
/// <param name="scratch">The reusable padded source workspace.</param>
public static void FilterIntraEdge(ref short buffer, int count, int strength, Span<short> scratch)
{
short finalSample = Unsafe.Add(ref buffer, count - 1);
scratch[..EdgeScratchLength].Fill(finalSample);
scratch[0] = buffer;
MemoryMarshal.CreateReadOnlySpan(ref buffer, count).CopyTo(scratch[1..]);
ref short edge = ref MemoryMarshal.GetReference(scratch);
int outputCount = count - 1;
int processed = 0;
// The largest 12-bit weighted sum is 65520; the greatest rounding bias raises that only to 65528.
// Unsigned 16-bit lanes therefore preserve every normative strength without widening to 32-bit vectors.
if (Vector128.IsHardwareAccelerated)
{
int eightSamplesFromEnd = outputCount - Vector128<short>.Count;
switch (strength)
{
case 1:
for (; processed <= eightSamplesFromEnd; processed += Vector128<short>.Count)
{
Vector128<ushort> source0 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 1)).AsUInt16();
Vector128<ushort> source1 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 2)).AsUInt16();
Vector128<ushort> source2 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 3)).AsUInt16();
FilterEdgeStrength1(source0, source1, source2).AsInt16().StoreUnsafe(ref buffer, (nuint)(processed + 1));
}
break;
case 2:
for (; processed <= eightSamplesFromEnd; processed += Vector128<short>.Count)
{
Vector128<ushort> source0 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 1)).AsUInt16();
Vector128<ushort> source1 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 2)).AsUInt16();
Vector128<ushort> source2 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 3)).AsUInt16();
FilterEdgeStrength2(source0, source1, source2).AsInt16().StoreUnsafe(ref buffer, (nuint)(processed + 1));
}
break;
default:
for (; processed <= eightSamplesFromEnd; processed += Vector128<short>.Count)
{
Vector128<ushort> source0 = Vector128.LoadUnsafe(ref edge, (nuint)processed).AsUInt16();
Vector128<ushort> source1 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 1)).AsUInt16();
Vector128<ushort> source2 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 2)).AsUInt16();
Vector128<ushort> source3 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 3)).AsUInt16();
Vector128<ushort> source4 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 4)).AsUInt16();
FilterEdgeStrength3(source0, source1, source2, source3, source4).AsInt16().StoreUnsafe(ref buffer, (nuint)(processed + 1));
}
break;
}
}
FilterIntraEdgeScalar(ref buffer, ref edge, processed, outputCount, strength);
}
/// <summary>
/// Widens the lower eight lanes of a byte vector for edge-filter arithmetic.
/// </summary>
/// <param name="source">The packed source samples.</param>
/// <returns>The widened samples.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<ushort> WidenLower(Vector128<byte> source)
{
(Vector128<ushort> lower, _) = Vector128.Widen(source);
return lower;
}
/// <summary>
/// Applies the strength-one three-tap edge filter to eight samples.
/// </summary>
/// <param name="source0">The preceding samples.</param>
/// <param name="source1">The centered samples.</param>
/// <param name="source2">The following samples.</param>
/// <returns>The filtered samples.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<ushort> FilterEdgeStrength1(Vector128<ushort> source0, Vector128<ushort> source1, Vector128<ushort> source2)
=> (source0 + (source1 << 1) + source2 + Vector128.Create((ushort)2)) >> 2;
/// <summary>
/// Applies the strength-two three-tap edge filter to eight samples.
/// </summary>
/// <param name="source0">The preceding samples.</param>
/// <param name="source1">The centered samples.</param>
/// <param name="source2">The following samples.</param>
/// <returns>The filtered samples.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<ushort> FilterEdgeStrength2(Vector128<ushort> source0, Vector128<ushort> source1, Vector128<ushort> source2)
=> (((source0 + source2) * Vector128.Create((ushort)5)) + (source1 * Vector128.Create((ushort)6)) + Vector128.Create((ushort)8)) >> 4;
/// <summary>
/// Applies the strength-three five-tap edge filter to eight samples.
/// </summary>
/// <param name="source0">The samples two positions before each output.</param>
/// <param name="source1">The preceding samples.</param>
/// <param name="source2">The centered samples.</param>
/// <param name="source3">The following samples.</param>
/// <param name="source4">The samples two positions after each output.</param>
/// <returns>The filtered samples.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<ushort> FilterEdgeStrength3(
Vector128<ushort> source0,
Vector128<ushort> source1,
Vector128<ushort> source2,
Vector128<ushort> source3,
Vector128<ushort> source4)
=> (source0 + ((source1 + source2 + source3) << 1) + source4 + Vector128.Create((ushort)4)) >> 3;
/// <summary>
/// Applies an AV1 edge filter to samples not consumed by the vector loop.
/// </summary>
/// <typeparam name="T">The byte or 16-bit sample type.</typeparam>
/// <param name="buffer">The first destination sample.</param>
/// <param name="edge">The first padded source sample.</param>
/// <param name="start">The first output index not processed by SIMD.</param>
/// <param name="count">The number of filtered outputs following the preserved first sample.</param>
/// <param name="strength">The AV1 filter-strength index.</param>
private static void FilterIntraEdgeScalar<T>(ref T buffer, ref T edge, int start, int count, int strength)
where T : unmanaged, IBinaryInteger<T>
{
switch (strength)
{
case 1:
for (int i = start; i < count; i++)
{
int sourceOffset = i + 1;
int value = int.CreateChecked(Unsafe.Add(ref edge, sourceOffset))
+ (2 * int.CreateChecked(Unsafe.Add(ref edge, sourceOffset + 1)))
+ int.CreateChecked(Unsafe.Add(ref edge, sourceOffset + 2));
Unsafe.Add(ref buffer, i + 1) = T.CreateChecked((value + 2) >> 2);
}
break;
case 2:
for (int i = start; i < count; i++)
{
int sourceOffset = i + 1;
int value = (5 * int.CreateChecked(Unsafe.Add(ref edge, sourceOffset)))
+ (6 * int.CreateChecked(Unsafe.Add(ref edge, sourceOffset + 1)))
+ (5 * int.CreateChecked(Unsafe.Add(ref edge, sourceOffset + 2)));
Unsafe.Add(ref buffer, i + 1) = T.CreateChecked((value + 8) >> 4);
}
break;
default:
for (int i = start; i < count; i++)
{
int value = int.CreateChecked(Unsafe.Add(ref edge, i))
+ (2 * (int.CreateChecked(Unsafe.Add(ref edge, i + 1))
+ int.CreateChecked(Unsafe.Add(ref edge, i + 2))
+ int.CreateChecked(Unsafe.Add(ref edge, i + 3))))
+ int.CreateChecked(Unsafe.Add(ref edge, i + 4));
Unsafe.Add(ref buffer, i + 1) = T.CreateChecked((value + 4) >> 3);
}
break;
}
}
/// <summary>
/// Selects the AV1 intra-edge filter strength for the block dimensions and prediction angle.
/// </summary>
/// <param name="width">The edge's primary block dimension.</param>
/// <param name="height">The edge's secondary block dimension.</param>
/// <param name="delta">The prediction angle relative to the edge's cardinal direction.</param>
/// <param name="filterType">A value indicating whether a neighboring smooth mode selects the alternate thresholds.</param>
/// <returns>The filter strength from zero for no filtering through three for the strongest kernel.</returns>
/// <remarks>Corresponds to <c>svt_aom_intra_edge_filter_strength</c> in SVT-AV1.</remarks>
private static int IntraEdgeFilterStrength(int width, int height, int delta, bool filterType)
{
int d = Math.Abs(delta);
int strength = 0;
int widthHeight = width + height;
if (!filterType)
{
if (widthHeight <= 8)
{
if (d >= 56)
{
strength = 1;
}
}
else if (widthHeight <= 12)
{
if (d >= 40)
{
strength = 1;
}
}
else if (widthHeight <= 16)
{
if (d >= 40)
{
strength = 1;
}
}
else if (widthHeight <= 24)
{
if (d >= 8)
{
strength = 1;
}
if (d >= 16)
{
strength = 2;
}
if (d >= 32)
{
strength = 3;
}
}
else if (widthHeight <= 32)
{
if (d >= 1)
{
strength = 1;
}
if (d >= 4)
{
strength = 2;
}
if (d >= 32)
{
strength = 3;
}
}
else
{
if (d >= 1)
{
strength = 3;
}
}
}
else
{
if (widthHeight <= 8)
{
if (d >= 40)
{
strength = 1;
}
if (d >= 64)
{
strength = 2;
}
}
else if (widthHeight <= 16)
{
if (d >= 20)
{
strength = 1;
}
if (d >= 48)
{
strength = 2;
}
}
else if (widthHeight <= 24)
{
if (d >= 4)
{
strength = 3;
}
}
else
{
if (d >= 1)
{
strength = 3;
}
}
}
return strength;
}
/// <summary>
/// Smooths the shared top-left reference sample where the prepared top and left edges meet.
/// </summary>
/// <typeparam name="T">The 8-bit or high-bit-depth sample type.</typeparam>
/// <param name="above">The prepared top edge with writable top-left prefix storage.</param>
/// <param name="left">The prepared left edge with writable top-left prefix storage.</param>
private static void FilterIntraEdgeCorner<T>(Span<T> above, Span<T> left)
where T : unmanaged, IBinaryInteger<T>
{
int[] kernel = [5, 6, 5];
ref T aboveRef = ref above[0];
ref T leftRef = ref left[0];
ref T abovePreviousRef = ref Unsafe.Subtract(ref aboveRef, 1);
ref T leftPreviousRef = ref Unsafe.Subtract(ref leftRef, 1);
int s = (int.CreateChecked(leftRef) * kernel[0]) + (int.CreateChecked(abovePreviousRef) * kernel[1]) + (int.CreateChecked(aboveRef) * kernel[2]);
s = (s + 8) >> 4;
// Both edge spans reserve their own prefix location for the same logical corner,
// so keep the two scratch representations synchronized after filtering.
abovePreviousRef = T.CreateChecked(s);
leftPreviousRef = T.CreateChecked(s);
}
/// <summary>
/// Determines the directional edge-filter threshold class from neighboring prediction modes.
/// </summary>
/// <param name="partitionInfo">The decoded partition and neighboring mode state.</param>
/// <param name="plane">The color plane whose neighbors are inspected.</param>
/// <returns><see langword="true"/> when either relevant neighbor uses a smooth mode; otherwise, <see langword="false"/>.</returns>
private static bool GetFilterType(ref Av1PartitionInfo partitionInfo, Av1Plane plane)
{
Av1BlockModeInfo? above;
Av1BlockModeInfo? left;
if (plane == Av1Plane.Y)
{
above = partitionInfo.AboveModeInfo;
left = partitionInfo.LeftModeInfo;
}
else
{
above = partitionInfo.AboveModeInfoForChroma;
left = partitionInfo.LeftModeInfoForChroma;
}
bool aboveIsSmooth = (above != null) && IsSmooth(above, plane);
bool leftIsSmooth = (left != null) && IsSmooth(left, plane);
return aboveIsSmooth || leftIsSmooth;
}
/// <summary>
/// Determines whether a block uses any AV1 smooth intra prediction mode on the requested plane.
/// </summary>
/// <param name="modeInfo">The neighboring block's decoded mode state.</param>
/// <param name="plane">The luma or chroma plane class whose mode is inspected.</param>
/// <returns><see langword="true"/> for smooth, smooth-horizontal, or smooth-vertical prediction; otherwise, <see langword="false"/>.</returns>
private static bool IsSmooth(Av1BlockModeInfo modeInfo, Av1Plane plane)
{
if (plane == Av1Plane.Y)
{
Av1PredictionMode mode = modeInfo.YMode;
return mode is Av1PredictionMode.Smooth or
Av1PredictionMode.SmoothVertical or
Av1PredictionMode.SmoothHorizontal;
}
else
{
// Inter mode not supported here.
Av1ChromaPredictionMode uvMode = modeInfo.UvMode;
return uvMode is Av1ChromaPredictionMode.Smooth or
Av1ChromaPredictionMode.SmoothVertical or
Av1ChromaPredictionMode.SmoothHorizontal;
}
}
}