mirror of https://github.com/SixLabors/ImageSharp
23 changed files with 2717 additions and 101 deletions
@ -0,0 +1,176 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Decodes integer intra-block-copy displacement vectors with tile-adaptive AV1 distributions.
|
||||
|
/// </summary>
|
||||
|
internal sealed class Av1DisplacementVectorContext |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The number of magnitude classes defined by AV1.
|
||||
|
/// </summary>
|
||||
|
private const int MagnitudeClassCount = 11; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The number of class-zero integer magnitude bits.
|
||||
|
/// </summary>
|
||||
|
private const int ClassZeroBitCount = 1; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The tile-adaptive distribution selecting which vector components are nonzero.
|
||||
|
/// </summary>
|
||||
|
private readonly Av1Distribution joint = new(4096, 11264, 19328); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The tile-adaptive vertical component distributions.
|
||||
|
/// </summary>
|
||||
|
private readonly Component vertical = new(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The tile-adaptive horizontal component distributions.
|
||||
|
/// </summary>
|
||||
|
private readonly Component horizontal = new(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads an integer displacement vector relative to a spatially derived reference.
|
||||
|
/// </summary>
|
||||
|
/// <param name="reader">The tile range decoder.</param>
|
||||
|
/// <param name="reference">The reference displacement vector.</param>
|
||||
|
/// <returns>The decoded displacement vector in one-eighth-sample units.</returns>
|
||||
|
public Av1MotionVector Read(ref Av1SymbolReader reader, Av1MotionVector reference) |
||||
|
{ |
||||
|
int jointType = reader.ReadSymbol(this.joint); |
||||
|
|
||||
|
// Joint values 1 and 3 carry a horizontal delta; values 2 and 3 carry a vertical delta. Intra-block copy
|
||||
|
// fixes precision to whole luma samples, so the component reader consumes no fractional or high-precision CDFs.
|
||||
|
int row = jointType >= 2 ? this.vertical.Read(ref reader) : 0; |
||||
|
int column = (jointType & 1) != 0 ? this.horizontal.Read(ref reader) : 0; |
||||
|
return reference + new Av1MotionVector(row, column); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Writes an integer displacement vector relative to a spatially derived reference.
|
||||
|
/// </summary>
|
||||
|
/// <param name="writer">The tile range encoder.</param>
|
||||
|
/// <param name="value">The displacement vector to encode.</param>
|
||||
|
/// <param name="reference">The spatially derived reference vector.</param>
|
||||
|
public void Write(Av1SymbolWriter writer, Av1MotionVector value, Av1MotionVector reference) |
||||
|
{ |
||||
|
int row = value.Row - reference.Row; |
||||
|
int column = value.Column - reference.Column; |
||||
|
int jointType = (row != 0 ? 2 : 0) | (column != 0 ? 1 : 0); |
||||
|
writer.WriteSymbol(jointType, this.joint); |
||||
|
|
||||
|
if (row != 0) |
||||
|
{ |
||||
|
this.vertical.Write(writer, row); |
||||
|
} |
||||
|
|
||||
|
if (column != 0) |
||||
|
{ |
||||
|
this.horizontal.Write(writer, column); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Stores the adaptive magnitude distributions for one displacement-vector component.
|
||||
|
/// </summary>
|
||||
|
private sealed class Component |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The distribution selecting the signed magnitude class.
|
||||
|
/// </summary>
|
||||
|
private readonly Av1Distribution magnitudeClass = new(28672, 30976, 31858, 32320, 32551, 32656, 32740, 32757, 32762, 32767); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The distribution selecting the sign of a nonzero component.
|
||||
|
/// </summary>
|
||||
|
private readonly Av1Distribution sign = new(16384); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The distribution selecting either of the two class-zero integer magnitudes.
|
||||
|
/// </summary>
|
||||
|
private readonly Av1Distribution classZero = new(27648); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The binary distributions that reconstruct larger magnitude offsets from least to most significant bit.
|
||||
|
/// </summary>
|
||||
|
private readonly Av1Distribution[] offsetBits = |
||||
|
[ |
||||
|
new(17408), new(17920), new(18944), new(20480), new(22528), |
||||
|
new(24576), new(28672), new(29952), new(29952), new(30720) |
||||
|
]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads one signed integer-precision component.
|
||||
|
/// </summary>
|
||||
|
/// <param name="reader">The tile range decoder.</param>
|
||||
|
/// <returns>The signed component in one-eighth-sample units.</returns>
|
||||
|
public int Read(ref Av1SymbolReader reader) |
||||
|
{ |
||||
|
bool isNegative = reader.ReadSymbol(this.sign) != 0; |
||||
|
int magnitudeClass = reader.ReadSymbol(this.magnitudeClass); |
||||
|
int offset; |
||||
|
int magnitudeBase; |
||||
|
|
||||
|
if (magnitudeClass == 0) |
||||
|
{ |
||||
|
offset = reader.ReadSymbol(this.classZero); |
||||
|
magnitudeBase = 0; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
int bitCount = magnitudeClass + ClassZeroBitCount - 1; |
||||
|
offset = 0; |
||||
|
for (int bit = 0; bit < bitCount; bit++) |
||||
|
{ |
||||
|
// AV1 transmits the integer offset least-significant bit first, with an independently adapting
|
||||
|
// distribution for every bit position.
|
||||
|
offset |= reader.ReadSymbol(this.offsetBits[bit]) << bit; |
||||
|
} |
||||
|
|
||||
|
magnitudeBase = (1 << ClassZeroBitCount) << (magnitudeClass + 2); |
||||
|
} |
||||
|
|
||||
|
// Integer precision substitutes the normative fractional values fr=3 and hp=1. The low three bits are
|
||||
|
// consequently all one, and the final increment converts the zero-based magnitude representation.
|
||||
|
int magnitude = magnitudeBase + (offset << 3) + 8; |
||||
|
return isNegative ? -magnitude : magnitude; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Writes one signed integer-precision component.
|
||||
|
/// </summary>
|
||||
|
/// <param name="writer">The tile range encoder.</param>
|
||||
|
/// <param name="value">The nonzero component in one-eighth-sample units.</param>
|
||||
|
public void Write(Av1SymbolWriter writer, int value) |
||||
|
{ |
||||
|
int magnitude = Math.Abs(value); |
||||
|
DebugGuard.IsTrue(magnitude > 0 && (magnitude & 7) == 0, "Displacement-vector components must use whole-sample precision."); |
||||
|
|
||||
|
int magnitudeClass = magnitude <= 16 ? 0 : Av1Math.MostSignificantBit((uint)(magnitude - 1)) - 3; |
||||
|
DebugGuard.MustBeLessThan(magnitudeClass, MagnitudeClassCount, nameof(magnitudeClass)); |
||||
|
writer.WriteSymbol(value < 0, this.sign); |
||||
|
writer.WriteSymbol(magnitudeClass, this.magnitudeClass); |
||||
|
|
||||
|
if (magnitudeClass == 0) |
||||
|
{ |
||||
|
writer.WriteSymbol((magnitude >> 3) - 1, this.classZero); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
int magnitudeBase = 8 << magnitudeClass; |
||||
|
int offset = (magnitude - magnitudeBase - 8) >> 3; |
||||
|
for (int bit = 0; bit < magnitudeClass; bit++) |
||||
|
{ |
||||
|
// The decoder reconstructs offsets least-significant bit first, so each adaptive bit model must be
|
||||
|
// updated in the same order during encoding.
|
||||
|
writer.WriteSymbol(((offset >> bit) & 1) != 0, this.offsetBits[bit]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,451 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Derives and validates AV1 intra-block-copy displacement vectors.
|
||||
|
/// </summary>
|
||||
|
internal static class Av1IntraBlockCopy |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The number of surrounding mode-information rows and columns searched for reference vectors.
|
||||
|
/// </summary>
|
||||
|
private const int ReferenceSearchDistance = 3; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The weight separating immediately adjacent candidates from the outer search area.
|
||||
|
/// </summary>
|
||||
|
private const int NearestCandidateWeight = 640; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The number of 64-sample blocks that an intra-block-copy source must precede the active block.
|
||||
|
/// </summary>
|
||||
|
private const int Delay64 = 4; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Finds the spatial reference used to differentially decode an intra-block-copy displacement vector.
|
||||
|
/// </summary>
|
||||
|
/// <param name="partitionInfo">The current block geometry and decoded neighbors.</param>
|
||||
|
/// <param name="tileInfo">The active tile boundaries.</param>
|
||||
|
/// <param name="superblockModeInfoSize">The superblock width in 4x4 mode-information units.</param>
|
||||
|
/// <param name="candidates">Reusable storage for up to eight unique reference vectors.</param>
|
||||
|
/// <param name="weights">Reusable storage for the corresponding spatial weights.</param>
|
||||
|
/// <returns>The nearest nonzero spatial candidate, or the normative tile-relative fallback.</returns>
|
||||
|
public static Av1MotionVector FindReference( |
||||
|
Av1PartitionInfo partitionInfo, |
||||
|
Av1TileInfo tileInfo, |
||||
|
int superblockModeInfoSize, |
||||
|
Span<Av1MotionVector> candidates, |
||||
|
Span<int> weights) |
||||
|
{ |
||||
|
Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize; |
||||
|
int width = blockSize.Get4x4WideCount(); |
||||
|
int height = blockSize.Get4x4HighCount(); |
||||
|
int row = partitionInfo.RowIndex; |
||||
|
int column = partitionInfo.ColumnIndex; |
||||
|
int rowAdjustment = height < 2 && (row & 1) != 0 ? 1 : 0; |
||||
|
int columnAdjustment = width < 2 && (column & 1) != 0 ? 1 : 0; |
||||
|
int maximumRowOffset = 0; |
||||
|
int maximumColumnOffset = 0; |
||||
|
|
||||
|
if (partitionInfo.AvailableAbove) |
||||
|
{ |
||||
|
maximumRowOffset = height < 2 ? -4 + rowAdjustment : -(ReferenceSearchDistance << 1) + rowAdjustment; |
||||
|
maximumRowOffset = Math.Clamp(maximumRowOffset, tileInfo.ModeInfoRowStart - row, tileInfo.ModeInfoRowEnd - row - 1); |
||||
|
} |
||||
|
|
||||
|
if (partitionInfo.AvailableLeft) |
||||
|
{ |
||||
|
maximumColumnOffset = width < 2 ? -4 + columnAdjustment : -(ReferenceSearchDistance << 1) + columnAdjustment; |
||||
|
maximumColumnOffset = Math.Clamp(maximumColumnOffset, tileInfo.ModeInfoColumnStart - column, tileInfo.ModeInfoColumnEnd - column - 1); |
||||
|
} |
||||
|
|
||||
|
int candidateCount = 0; |
||||
|
int processedRows = 0; |
||||
|
int processedColumns = 0; |
||||
|
if (Math.Abs(maximumRowOffset) >= 1) |
||||
|
{ |
||||
|
ScanRow(partitionInfo, -1, maximumRowOffset, candidates, weights, ref candidateCount, ref processedRows); |
||||
|
} |
||||
|
|
||||
|
if (Math.Abs(maximumColumnOffset) >= 1) |
||||
|
{ |
||||
|
ScanColumn(partitionInfo, -1, maximumColumnOffset, candidates, weights, ref candidateCount, ref processedColumns); |
||||
|
} |
||||
|
|
||||
|
if (HasTopRight(partitionInfo, superblockModeInfoSize)) |
||||
|
{ |
||||
|
AddBlock(partitionInfo, -1, width, tileInfo, candidates, weights, ref candidateCount); |
||||
|
} |
||||
|
|
||||
|
int nearestCandidateCount = candidateCount; |
||||
|
for (int index = 0; index < nearestCandidateCount; index++) |
||||
|
{ |
||||
|
weights[index] += NearestCandidateWeight; |
||||
|
} |
||||
|
|
||||
|
// The top-left sample begins the outer search region. Sorting the adjacent and outer regions independently
|
||||
|
// preserves libaom's nearest/near ordering while still accumulating repeated vectors across both regions.
|
||||
|
AddBlock(partitionInfo, -1, -1, tileInfo, candidates, weights, ref candidateCount); |
||||
|
for (int index = 2; index <= ReferenceSearchDistance; index++) |
||||
|
{ |
||||
|
int rowOffset = -(index << 1) + 1 + rowAdjustment; |
||||
|
int columnOffset = -(index << 1) + 1 + columnAdjustment; |
||||
|
if (Math.Abs(rowOffset) <= Math.Abs(maximumRowOffset) && Math.Abs(rowOffset) > processedRows) |
||||
|
{ |
||||
|
ScanRow(partitionInfo, rowOffset, maximumRowOffset, candidates, weights, ref candidateCount, ref processedRows); |
||||
|
} |
||||
|
|
||||
|
if (Math.Abs(columnOffset) <= Math.Abs(maximumColumnOffset) && Math.Abs(columnOffset) > processedColumns) |
||||
|
{ |
||||
|
ScanColumn(partitionInfo, columnOffset, maximumColumnOffset, candidates, weights, ref candidateCount, ref processedColumns); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
SortByWeight(candidates, weights, 0, nearestCandidateCount); |
||||
|
SortByWeight(candidates, weights, nearestCandidateCount, candidateCount); |
||||
|
|
||||
|
Av1MotionVector reference = candidateCount > 0 ? candidates[0] : default; |
||||
|
if (reference.IsZero && candidateCount > 1) |
||||
|
{ |
||||
|
reference = candidates[1]; |
||||
|
} |
||||
|
|
||||
|
if (!reference.IsZero) |
||||
|
{ |
||||
|
return reference; |
||||
|
} |
||||
|
|
||||
|
const int modeInfoSampleSize = 1 << Av1Constants.ModeInfoSizeLog2; |
||||
|
const int eighthSampleScale = 8; |
||||
|
int fallbackRow = -modeInfoSampleSize * superblockModeInfoSize * eighthSampleScale; |
||||
|
int fallbackColumn = fallbackRow - (Delay64 * 64 * eighthSampleScale); |
||||
|
|
||||
|
return (row - superblockModeInfoSize) < tileInfo.ModeInfoRowStart |
||||
|
? new Av1MotionVector(0, fallbackColumn) |
||||
|
: new Av1MotionVector(fallbackRow, 0); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines whether a decoded displacement vector references an earlier reconstructable block inside the tile.
|
||||
|
/// </summary>
|
||||
|
/// <param name="vector">The decoded displacement vector in one-eighth-sample units.</param>
|
||||
|
/// <param name="partitionInfo">The current block geometry.</param>
|
||||
|
/// <param name="tileInfo">The active tile boundaries.</param>
|
||||
|
/// <param name="sequenceHeader">The sequence-level superblock and chroma configuration.</param>
|
||||
|
/// <returns><see langword="true"/> when the complete source block is a permitted reference; otherwise, <see langword="false"/>.</returns>
|
||||
|
public static bool IsValid(Av1MotionVector vector, Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo, ObuSequenceHeader sequenceHeader) |
||||
|
{ |
||||
|
const int eighthSampleScale = 8; |
||||
|
const int modeInfoSampleSize = 1 << Av1Constants.ModeInfoSizeLog2; |
||||
|
if ((vector.Row & (eighthSampleScale - 1)) != 0 || (vector.Column & (eighthSampleScale - 1)) != 0 || |
||||
|
vector.Row <= -(1 << 14) || vector.Row >= (1 << 14) || vector.Column <= -(1 << 14) || vector.Column >= (1 << 14)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
int row = partitionInfo.RowIndex; |
||||
|
int column = partitionInfo.ColumnIndex; |
||||
|
int blockWidth = partitionInfo.ModeInfo.BlockSize.GetWidth(); |
||||
|
int blockHeight = partitionInfo.ModeInfo.BlockSize.GetHeight(); |
||||
|
int sourceTop = (row * modeInfoSampleSize * eighthSampleScale) + vector.Row; |
||||
|
int sourceLeft = (column * modeInfoSampleSize * eighthSampleScale) + vector.Column; |
||||
|
int sourceBottom = (((row * modeInfoSampleSize) + blockHeight) * eighthSampleScale) + vector.Row; |
||||
|
int sourceRight = (((column * modeInfoSampleSize) + blockWidth) * eighthSampleScale) + vector.Column; |
||||
|
int tileTop = tileInfo.ModeInfoRowStart * modeInfoSampleSize * eighthSampleScale; |
||||
|
int tileLeft = tileInfo.ModeInfoColumnStart * modeInfoSampleSize * eighthSampleScale; |
||||
|
int tileBottom = tileInfo.ModeInfoRowEnd * modeInfoSampleSize * eighthSampleScale; |
||||
|
int tileRight = tileInfo.ModeInfoColumnEnd * modeInfoSampleSize * eighthSampleScale; |
||||
|
if (sourceTop < tileTop || sourceLeft < tileLeft || sourceBottom > tileBottom || sourceRight > tileRight) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
ObuColorConfig colorConfig = sequenceHeader.ColorConfig; |
||||
|
if (partitionInfo.IsChroma && colorConfig.PlaneCount > 1) |
||||
|
{ |
||||
|
// A sub-8x8 luma block can map to a chroma block whose rounded origin lies one additional luma unit
|
||||
|
// inside the tile. These checks prevent that chroma reference from crossing the tile boundary.
|
||||
|
if (blockWidth < 8 && colorConfig.SubSamplingX && sourceLeft < tileLeft + (modeInfoSampleSize * eighthSampleScale)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (blockHeight < 8 && colorConfig.SubSamplingY && sourceTop < tileTop + (modeInfoSampleSize * eighthSampleScale)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
int superblockModeInfoSize = sequenceHeader.SuperblockModeInfoSize; |
||||
|
int superblockSize = superblockModeInfoSize * modeInfoSampleSize; |
||||
|
int superblockModeInfoSizeLog2 = sequenceHeader.SuperblockSizeLog2 - Av1Constants.ModeInfoSizeLog2; |
||||
|
int activeSuperblockRow = row >> superblockModeInfoSizeLog2; |
||||
|
int active64Column = (column * modeInfoSampleSize) >> 6; |
||||
|
int sourceSuperblockRow = ((sourceBottom >> 3) - 1) / superblockSize; |
||||
|
int source64Column = ((sourceRight >> 3) - 1) >> 6; |
||||
|
int tile64ColumnCount = ((tileInfo.ModeInfoColumnEnd - tileInfo.ModeInfoColumnStart - 1) >> 4) + 1; |
||||
|
int active64 = (activeSuperblockRow * tile64ColumnCount) + active64Column; |
||||
|
int source64 = (sourceSuperblockRow * tile64ColumnCount) + source64Column; |
||||
|
if (source64 >= active64 - Delay64) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// The wavefront boundary reserves four completed 64-sample columns and advances farther right for every
|
||||
|
// completed source row. A 128x128 superblock adds one column to account for its two 64-sample halves.
|
||||
|
int gradient = 1 + Delay64 + (superblockSize > 64 ? 1 : 0); |
||||
|
int wavefrontOffset = gradient * (activeSuperblockRow - sourceSuperblockRow); |
||||
|
return sourceSuperblockRow <= activeSuperblockRow && source64Column < active64Column - Delay64 + wavefrontOffset; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Scans a mode-information row using AV1's block-size-dependent steps and weights.
|
||||
|
/// </summary>
|
||||
|
private static void ScanRow( |
||||
|
Av1PartitionInfo partitionInfo, |
||||
|
int rowOffset, |
||||
|
int maximumRowOffset, |
||||
|
Span<Av1MotionVector> candidates, |
||||
|
Span<int> weights, |
||||
|
ref int candidateCount, |
||||
|
ref int processedRows) |
||||
|
{ |
||||
|
int width = partitionInfo.ModeInfo.BlockSize.Get4x4WideCount(); |
||||
|
int end = Math.Min(partitionInfo.GetMaxBlockWide(partitionInfo.ModeInfo.BlockSize, false), 16); |
||||
|
int columnOffset = 0; |
||||
|
if (Math.Abs(rowOffset) > 1) |
||||
|
{ |
||||
|
columnOffset = 1; |
||||
|
if ((partitionInfo.ColumnIndex & 1) != 0 && width < 2) |
||||
|
{ |
||||
|
columnOffset--; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
bool useFourUnitStep = width >= 4; |
||||
|
for (int index = 0; index < end;) |
||||
|
{ |
||||
|
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt( |
||||
|
new Point(partitionInfo.ColumnIndex + columnOffset + index, partitionInfo.RowIndex + rowOffset)); |
||||
|
|
||||
|
int candidateWidth = candidate.BlockSize.Get4x4WideCount(); |
||||
|
int length = Math.Min(width, candidateWidth); |
||||
|
if (useFourUnitStep) |
||||
|
{ |
||||
|
length = Math.Max(4, length); |
||||
|
} |
||||
|
else if (Math.Abs(rowOffset) > 1) |
||||
|
{ |
||||
|
length = Math.Max(2, length); |
||||
|
} |
||||
|
|
||||
|
int weight = 2; |
||||
|
if (width >= 2 && width <= candidateWidth) |
||||
|
{ |
||||
|
int increment = Math.Min(-maximumRowOffset + rowOffset + 1, candidate.BlockSize.Get4x4HighCount()); |
||||
|
weight = Math.Max(weight, increment); |
||||
|
processedRows = increment - rowOffset - 1; |
||||
|
} |
||||
|
|
||||
|
AddCandidate(candidate, length * weight, candidates, weights, ref candidateCount); |
||||
|
index += length; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Scans a mode-information column using AV1's block-size-dependent steps and weights.
|
||||
|
/// </summary>
|
||||
|
private static void ScanColumn( |
||||
|
Av1PartitionInfo partitionInfo, |
||||
|
int columnOffset, |
||||
|
int maximumColumnOffset, |
||||
|
Span<Av1MotionVector> candidates, |
||||
|
Span<int> weights, |
||||
|
ref int candidateCount, |
||||
|
ref int processedColumns) |
||||
|
{ |
||||
|
int height = partitionInfo.ModeInfo.BlockSize.Get4x4HighCount(); |
||||
|
int end = Math.Min(partitionInfo.GetMaxBlockHigh(partitionInfo.ModeInfo.BlockSize, false), 16); |
||||
|
int rowOffset = 0; |
||||
|
if (Math.Abs(columnOffset) > 1) |
||||
|
{ |
||||
|
rowOffset = 1; |
||||
|
if ((partitionInfo.RowIndex & 1) != 0 && height < 2) |
||||
|
{ |
||||
|
rowOffset--; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
bool useFourUnitStep = height >= 4; |
||||
|
for (int index = 0; index < end;) |
||||
|
{ |
||||
|
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt( |
||||
|
new Point(partitionInfo.ColumnIndex + columnOffset, partitionInfo.RowIndex + rowOffset + index)); |
||||
|
|
||||
|
int candidateHeight = candidate.BlockSize.Get4x4HighCount(); |
||||
|
int length = Math.Min(height, candidateHeight); |
||||
|
if (useFourUnitStep) |
||||
|
{ |
||||
|
length = Math.Max(4, length); |
||||
|
} |
||||
|
else if (Math.Abs(columnOffset) > 1) |
||||
|
{ |
||||
|
length = Math.Max(2, length); |
||||
|
} |
||||
|
|
||||
|
int weight = 2; |
||||
|
if (height >= 2 && height <= candidateHeight) |
||||
|
{ |
||||
|
int increment = Math.Min(-maximumColumnOffset + columnOffset + 1, candidate.BlockSize.Get4x4WideCount()); |
||||
|
weight = Math.Max(weight, increment); |
||||
|
processedColumns = increment - columnOffset - 1; |
||||
|
} |
||||
|
|
||||
|
AddCandidate(candidate, length * weight, candidates, weights, ref candidateCount); |
||||
|
index += length; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Adds the intra-block-copy vector at one tile-relative search position.
|
||||
|
/// </summary>
|
||||
|
private static void AddBlock( |
||||
|
Av1PartitionInfo partitionInfo, |
||||
|
int rowOffset, |
||||
|
int columnOffset, |
||||
|
Av1TileInfo tileInfo, |
||||
|
Span<Av1MotionVector> candidates, |
||||
|
Span<int> weights, |
||||
|
ref int candidateCount) |
||||
|
{ |
||||
|
int row = partitionInfo.RowIndex + rowOffset; |
||||
|
int column = partitionInfo.ColumnIndex + columnOffset; |
||||
|
if (row < tileInfo.ModeInfoRowStart || row >= tileInfo.ModeInfoRowEnd || |
||||
|
column < tileInfo.ModeInfoColumnStart || column >= tileInfo.ModeInfoColumnEnd) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column, row)); |
||||
|
AddCandidate(candidate, 4, candidates, weights, ref candidateCount); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Accumulates one unique intra-block-copy candidate and its spatial weight.
|
||||
|
/// </summary>
|
||||
|
private static void AddCandidate( |
||||
|
Av1BlockModeInfo candidate, |
||||
|
int weight, |
||||
|
Span<Av1MotionVector> candidates, |
||||
|
Span<int> weights, |
||||
|
ref int candidateCount) |
||||
|
{ |
||||
|
if (!candidate.UseIntraBlockCopy) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
Av1MotionVector vector = candidate.DisplacementVector; |
||||
|
int index = 0; |
||||
|
for (; index < candidateCount; index++) |
||||
|
{ |
||||
|
if (candidates[index] == vector) |
||||
|
{ |
||||
|
weights[index] += weight; |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (candidateCount < candidates.Length) |
||||
|
{ |
||||
|
candidates[candidateCount] = vector; |
||||
|
weights[candidateCount] = weight; |
||||
|
candidateCount++; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sorts one candidate region by descending accumulated weight.
|
||||
|
/// </summary>
|
||||
|
private static void SortByWeight(Span<Av1MotionVector> candidates, Span<int> weights, int start, int end) |
||||
|
{ |
||||
|
int length = end; |
||||
|
while (length > start) |
||||
|
{ |
||||
|
int lastSwap = start; |
||||
|
for (int index = start + 1; index < length; index++) |
||||
|
{ |
||||
|
if (weights[index - 1] < weights[index]) |
||||
|
{ |
||||
|
Av1MotionVector candidate = candidates[index - 1]; |
||||
|
candidates[index - 1] = candidates[index]; |
||||
|
candidates[index] = candidate; |
||||
|
|
||||
|
int weight = weights[index - 1]; |
||||
|
weights[index - 1] = weights[index]; |
||||
|
weights[index] = weight; |
||||
|
lastSwap = index; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
length = lastSwap; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines whether the current partition is parsed after the block at its top-right search position.
|
||||
|
/// </summary>
|
||||
|
private static bool HasTopRight(Av1PartitionInfo partitionInfo, int superblockModeInfoSize) |
||||
|
{ |
||||
|
int width = partitionInfo.ModeInfo.BlockSize.Get4x4WideCount(); |
||||
|
int height = partitionInfo.ModeInfo.BlockSize.Get4x4HighCount(); |
||||
|
int blockSize = Math.Max(width, height); |
||||
|
if (blockSize > 16) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
int row = partitionInfo.RowIndex & (superblockModeInfoSize - 1); |
||||
|
int column = partitionInfo.ColumnIndex & (superblockModeInfoSize - 1); |
||||
|
bool hasTopRight = !((row & blockSize) != 0 && (column & blockSize) != 0); |
||||
|
int traversalSize = blockSize; |
||||
|
while (traversalSize < superblockModeInfoSize) |
||||
|
{ |
||||
|
if ((column & traversalSize) == 0) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if ((column & (traversalSize << 1)) != 0 && (row & (traversalSize << 1)) != 0) |
||||
|
{ |
||||
|
hasTopRight = false; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
traversalSize <<= 1; |
||||
|
} |
||||
|
|
||||
|
if (width < height && ((partitionInfo.ColumnIndex + width) & (height - 1)) != 0) |
||||
|
{ |
||||
|
hasTopRight = true; |
||||
|
} |
||||
|
|
||||
|
if (width > height && (partitionInfo.RowIndex & (width - 1)) != 0) |
||||
|
{ |
||||
|
hasTopRight = false; |
||||
|
} |
||||
|
|
||||
|
// The lower-left square of a vertical-A partition is decoded before its right-hand rectangle.
|
||||
|
if (partitionInfo.Type == Av1PartitionType.VerticalA && width == height && (row & traversalSize) != 0) |
||||
|
{ |
||||
|
hasTopRight = false; |
||||
|
} |
||||
|
|
||||
|
return hasTopRight; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,74 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents an AV1 motion or displacement vector in one-eighth-sample units.
|
||||
|
/// </summary>
|
||||
|
internal readonly struct Av1MotionVector : IEquatable<Av1MotionVector> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="Av1MotionVector"/> struct.
|
||||
|
/// </summary>
|
||||
|
/// <param name="row">The signed vertical displacement in one-eighth-sample units.</param>
|
||||
|
/// <param name="column">The signed horizontal displacement in one-eighth-sample units.</param>
|
||||
|
public Av1MotionVector(int row, int column) |
||||
|
{ |
||||
|
this.Row = row; |
||||
|
this.Column = column; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the signed vertical displacement in one-eighth-sample units.
|
||||
|
/// </summary>
|
||||
|
public int Row { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the signed horizontal displacement in one-eighth-sample units.
|
||||
|
/// </summary>
|
||||
|
public int Column { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether both displacement components are zero.
|
||||
|
/// </summary>
|
||||
|
public bool IsZero => this.Row == 0 && this.Column == 0; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Adds a component delta to this vector.
|
||||
|
/// </summary>
|
||||
|
/// <param name="value">The reference vector.</param>
|
||||
|
/// <param name="delta">The decoded component delta.</param>
|
||||
|
/// <returns>The component-wise sum.</returns>
|
||||
|
public static Av1MotionVector operator +(Av1MotionVector value, Av1MotionVector delta) |
||||
|
=> new(value.Row + delta.Row, value.Column + delta.Column); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines whether two vectors have equal components.
|
||||
|
/// </summary>
|
||||
|
/// <param name="left">The first vector.</param>
|
||||
|
/// <param name="right">The second vector.</param>
|
||||
|
/// <returns><see langword="true"/> when both components are equal; otherwise, <see langword="false"/>.</returns>
|
||||
|
public static bool operator ==(Av1MotionVector left, Av1MotionVector right) => left.Equals(right); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines whether two vectors have different components.
|
||||
|
/// </summary>
|
||||
|
/// <param name="left">The first vector.</param>
|
||||
|
/// <param name="right">The second vector.</param>
|
||||
|
/// <returns><see langword="true"/> when either component differs; otherwise, <see langword="false"/>.</returns>
|
||||
|
public static bool operator !=(Av1MotionVector left, Av1MotionVector right) => !left.Equals(right); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines whether this vector has the same components as another vector.
|
||||
|
/// </summary>
|
||||
|
/// <param name="other">The vector to compare.</param>
|
||||
|
/// <returns><see langword="true"/> when both components are equal; otherwise, <see langword="false"/>.</returns>
|
||||
|
public bool Equals(Av1MotionVector other) => this.Row == other.Row && this.Column == other.Column; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override bool Equals(object? obj) => obj is Av1MotionVector other && this.Equals(other); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override int GetHashCode() => HashCode.Combine(this.Row, this.Column); |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Runtime.Intrinsics; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; |
||||
|
|
||||
|
/// <content>
|
||||
|
/// Provides the overflow-free rounded-average arithmetic shared by the interpolation operators.
|
||||
|
/// </content>
|
||||
|
internal static partial class Av1IntraBlockCopyPredictor |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Computes the AV1 rounded average of two unsigned 8-bit vectors without widening their lanes.
|
||||
|
/// </summary>
|
||||
|
/// <param name="left">The first source vector.</param>
|
||||
|
/// <param name="right">The second source vector.</param>
|
||||
|
/// <returns>The lane-wise rounded averages.</returns>
|
||||
|
private static Vector128<byte> AverageRounded(Vector128<byte> left, Vector128<byte> right) |
||||
|
=> (left | right) - ((left ^ right) >> 1); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Computes the AV1 rounded average of two unsigned 8-bit vectors without widening their lanes.
|
||||
|
/// </summary>
|
||||
|
/// <param name="left">The first source vector.</param>
|
||||
|
/// <param name="right">The second source vector.</param>
|
||||
|
/// <returns>The lane-wise rounded averages.</returns>
|
||||
|
private static Vector256<byte> AverageRounded(Vector256<byte> left, Vector256<byte> right) |
||||
|
=> (left | right) - ((left ^ right) >> 1); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Computes the AV1 rounded average of two unsigned 8-bit vectors without widening their lanes.
|
||||
|
/// </summary>
|
||||
|
/// <param name="left">The first source vector.</param>
|
||||
|
/// <param name="right">The second source vector.</param>
|
||||
|
/// <returns>The lane-wise rounded averages.</returns>
|
||||
|
private static Vector512<byte> AverageRounded(Vector512<byte> left, Vector512<byte> right) |
||||
|
=> (left | right) - ((left ^ right) >> 1); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Computes the AV1 rounded average of two nonnegative high-bit-depth vectors without widening their lanes.
|
||||
|
/// </summary>
|
||||
|
/// <param name="left">The first source vector.</param>
|
||||
|
/// <param name="right">The second source vector.</param>
|
||||
|
/// <returns>The lane-wise rounded averages.</returns>
|
||||
|
private static Vector128<short> AverageRounded(Vector128<short> left, Vector128<short> right) |
||||
|
{ |
||||
|
Vector128<ushort> leftUnsigned = left.AsUInt16(); |
||||
|
Vector128<ushort> rightUnsigned = right.AsUInt16(); |
||||
|
|
||||
|
// (a | b) - ((a ^ b) >> 1) is ceil((a + b) / 2) without an overflowing lane-wise addition.
|
||||
|
return ((leftUnsigned | rightUnsigned) - ((leftUnsigned ^ rightUnsigned) >> 1)).AsInt16(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Computes the AV1 rounded average of two nonnegative high-bit-depth vectors without widening their lanes.
|
||||
|
/// </summary>
|
||||
|
/// <param name="left">The first source vector.</param>
|
||||
|
/// <param name="right">The second source vector.</param>
|
||||
|
/// <returns>The lane-wise rounded averages.</returns>
|
||||
|
private static Vector256<short> AverageRounded(Vector256<short> left, Vector256<short> right) |
||||
|
{ |
||||
|
Vector256<ushort> leftUnsigned = left.AsUInt16(); |
||||
|
Vector256<ushort> rightUnsigned = right.AsUInt16(); |
||||
|
return ((leftUnsigned | rightUnsigned) - ((leftUnsigned ^ rightUnsigned) >> 1)).AsInt16(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Computes the AV1 rounded average of two nonnegative high-bit-depth vectors without widening their lanes.
|
||||
|
/// </summary>
|
||||
|
/// <param name="left">The first source vector.</param>
|
||||
|
/// <param name="right">The second source vector.</param>
|
||||
|
/// <returns>The lane-wise rounded averages.</returns>
|
||||
|
private static Vector512<short> AverageRounded(Vector512<short> left, Vector512<short> right) |
||||
|
{ |
||||
|
Vector512<ushort> leftUnsigned = left.AsUInt16(); |
||||
|
Vector512<ushort> rightUnsigned = right.AsUInt16(); |
||||
|
return ((leftUnsigned | rightUnsigned) - ((leftUnsigned ^ rightUnsigned) >> 1)).AsInt16(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,378 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Runtime.Intrinsics; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; |
||||
|
|
||||
|
/// <content>
|
||||
|
/// Provides the width-progressive SIMD traversal shared by the intra-block-copy interpolation operators.
|
||||
|
/// </content>
|
||||
|
internal static partial class Av1IntraBlockCopyPredictor |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Applies one closed interpolation operator to an 8-bit source block.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TOperator">The source-phase-specific interpolation arithmetic.</typeparam>
|
||||
|
private static void Predict<TOperator>( |
||||
|
ReadOnlySpan<byte> source, |
||||
|
int sourceStride, |
||||
|
Span<byte> destination, |
||||
|
int destinationStride, |
||||
|
int width, |
||||
|
int height) |
||||
|
where TOperator : struct, IOperator |
||||
|
{ |
||||
|
ref byte sourceBase = ref MemoryMarshal.GetReference(source); |
||||
|
ref byte destinationBase = ref MemoryMarshal.GetReference(destination); |
||||
|
|
||||
|
if (Vector128.IsHardwareAccelerated && width is 4 or 8) |
||||
|
{ |
||||
|
// AV1 permits 4- and 8-sample transform widths, both smaller than a byte Vector128. The frame allocation's
|
||||
|
// 72-sample prediction border makes each full source load readable; exact-width stores avoid touching
|
||||
|
// destination padding.
|
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); |
||||
|
ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); |
||||
|
Vector128<byte> topLeft = Vector128.LoadUnsafe(ref sourceRow); |
||||
|
Vector128<byte> topRight = TOperator.UsesRight ? Vector128.LoadUnsafe(ref sourceRow, 1) : default; |
||||
|
Vector128<byte> bottomLeft = TOperator.UsesBottom ? Vector128.LoadUnsafe(ref sourceRow, (nuint)sourceStride) : default; |
||||
|
Vector128<byte> bottomRight = TOperator.UsesRight && TOperator.UsesBottom |
||||
|
? Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + 1)) |
||||
|
: default; |
||||
|
|
||||
|
Vector128<byte> prediction = TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight); |
||||
|
if (width == 8) |
||||
|
{ |
||||
|
prediction.GetLower().StoreUnsafe(ref destinationRow); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Unsafe.As<byte, uint>(ref destinationRow) = prediction.AsUInt32().GetElement(0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
int processedColumns = 0; |
||||
|
|
||||
|
// AV1 transform widths are powers of two. The widest supported tier normally consumes the complete row; the
|
||||
|
// cumulative narrower tiers preserve the same contract for future legal widths without over-reading a tail.
|
||||
|
if (Vector512.IsHardwareAccelerated) |
||||
|
{ |
||||
|
int vectorizedColumns = width - (width % Vector512<byte>.Count); |
||||
|
if (vectorizedColumns > 0) |
||||
|
{ |
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); |
||||
|
ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); |
||||
|
|
||||
|
for (int column = 0; column < vectorizedColumns; column += Vector512<byte>.Count) |
||||
|
{ |
||||
|
Vector512<byte> topLeft = Vector512.LoadUnsafe(ref sourceRow, (nuint)column); |
||||
|
Vector512<byte> topRight = TOperator.UsesRight ? Vector512.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default; |
||||
|
Vector512<byte> bottomLeft = TOperator.UsesBottom |
||||
|
? Vector512.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)) |
||||
|
: default; |
||||
|
Vector512<byte> bottomRight = TOperator.UsesRight && TOperator.UsesBottom |
||||
|
? Vector512.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)) |
||||
|
: default; |
||||
|
|
||||
|
TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
processedColumns = vectorizedColumns; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (Vector256.IsHardwareAccelerated) |
||||
|
{ |
||||
|
int remainingColumns = width - processedColumns; |
||||
|
int vectorizedColumns = remainingColumns - (remainingColumns % Vector256<byte>.Count); |
||||
|
int endColumn = processedColumns + vectorizedColumns; |
||||
|
|
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); |
||||
|
ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); |
||||
|
|
||||
|
for (int column = processedColumns; column < endColumn; column += Vector256<byte>.Count) |
||||
|
{ |
||||
|
Vector256<byte> topLeft = Vector256.LoadUnsafe(ref sourceRow, (nuint)column); |
||||
|
Vector256<byte> topRight = TOperator.UsesRight ? Vector256.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default; |
||||
|
Vector256<byte> bottomLeft = TOperator.UsesBottom |
||||
|
? Vector256.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)) |
||||
|
: default; |
||||
|
Vector256<byte> bottomRight = TOperator.UsesRight && TOperator.UsesBottom |
||||
|
? Vector256.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)) |
||||
|
: default; |
||||
|
|
||||
|
TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
processedColumns = endColumn; |
||||
|
} |
||||
|
|
||||
|
if (Vector128.IsHardwareAccelerated) |
||||
|
{ |
||||
|
int remainingColumns = width - processedColumns; |
||||
|
int vectorizedColumns = remainingColumns - (remainingColumns % Vector128<byte>.Count); |
||||
|
int endColumn = processedColumns + vectorizedColumns; |
||||
|
|
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); |
||||
|
ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); |
||||
|
|
||||
|
for (int column = processedColumns; column < endColumn; column += Vector128<byte>.Count) |
||||
|
{ |
||||
|
Vector128<byte> topLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)column); |
||||
|
Vector128<byte> topRight = TOperator.UsesRight ? Vector128.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default; |
||||
|
Vector128<byte> bottomLeft = TOperator.UsesBottom |
||||
|
? Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)) |
||||
|
: default; |
||||
|
Vector128<byte> bottomRight = TOperator.UsesRight && TOperator.UsesBottom |
||||
|
? Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)) |
||||
|
: default; |
||||
|
|
||||
|
TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
processedColumns = endColumn; |
||||
|
} |
||||
|
|
||||
|
// FeatureTestRunner can disable every intrinsic tier. Keeping the scalar continuation in the same traversal
|
||||
|
// proves the fallback without changing source addressing or the normative rounding performed by the operator.
|
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); |
||||
|
ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); |
||||
|
|
||||
|
for (int column = processedColumns; column < width; column++) |
||||
|
{ |
||||
|
byte topLeft = Unsafe.Add(ref sourceRow, column); |
||||
|
byte topRight = TOperator.UsesRight ? Unsafe.Add(ref sourceRow, column + 1) : default; |
||||
|
byte bottomLeft = TOperator.UsesBottom ? Unsafe.Add(ref sourceRow, sourceStride + column) : default; |
||||
|
byte bottomRight = TOperator.UsesRight && TOperator.UsesBottom |
||||
|
? Unsafe.Add(ref sourceRow, sourceStride + column + 1) |
||||
|
: default; |
||||
|
|
||||
|
Unsafe.Add(ref destinationRow, column) = TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Applies one closed interpolation operator to a high-bit-depth source block.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TOperator">The source-phase-specific interpolation arithmetic.</typeparam>
|
||||
|
private static void Predict<TOperator>( |
||||
|
ReadOnlySpan<short> source, |
||||
|
int sourceStride, |
||||
|
Span<short> destination, |
||||
|
int destinationStride, |
||||
|
int width, |
||||
|
int height) |
||||
|
where TOperator : struct, IOperator |
||||
|
{ |
||||
|
ref short sourceBase = ref MemoryMarshal.GetReference(source); |
||||
|
ref short destinationBase = ref MemoryMarshal.GetReference(destination); |
||||
|
|
||||
|
if (Vector128.IsHardwareAccelerated && width == 4) |
||||
|
{ |
||||
|
// Four high-bit-depth samples occupy the lower half of a Vector128. The frame allocation's prediction
|
||||
|
// border makes the full source load readable; storing only the lower four lanes avoids relying on writable
|
||||
|
// samples beyond the transform boundary.
|
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); |
||||
|
ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); |
||||
|
Vector128<short> topLeft = Vector128.LoadUnsafe(ref sourceRow); |
||||
|
Vector128<short> topRight = TOperator.UsesRight ? Vector128.LoadUnsafe(ref sourceRow, 1) : default; |
||||
|
Vector128<short> bottomLeft = TOperator.UsesBottom ? Vector128.LoadUnsafe(ref sourceRow, (nuint)sourceStride) : default; |
||||
|
Vector128<short> bottomRight = TOperator.UsesRight && TOperator.UsesBottom |
||||
|
? Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + 1)) |
||||
|
: default; |
||||
|
|
||||
|
TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).GetLower().StoreUnsafe(ref destinationRow); |
||||
|
} |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
int processedColumns = 0; |
||||
|
|
||||
|
// High-bit-depth lanes hold half as many samples, but retain the same descending-width traversal and one scalar
|
||||
|
// continuation as the byte path.
|
||||
|
if (Vector512.IsHardwareAccelerated) |
||||
|
{ |
||||
|
int vectorizedColumns = width - (width % Vector512<short>.Count); |
||||
|
if (vectorizedColumns > 0) |
||||
|
{ |
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); |
||||
|
ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); |
||||
|
|
||||
|
for (int column = 0; column < vectorizedColumns; column += Vector512<short>.Count) |
||||
|
{ |
||||
|
Vector512<short> topLeft = Vector512.LoadUnsafe(ref sourceRow, (nuint)column); |
||||
|
Vector512<short> topRight = TOperator.UsesRight ? Vector512.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default; |
||||
|
Vector512<short> bottomLeft = TOperator.UsesBottom |
||||
|
? Vector512.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)) |
||||
|
: default; |
||||
|
Vector512<short> bottomRight = TOperator.UsesRight && TOperator.UsesBottom |
||||
|
? Vector512.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)) |
||||
|
: default; |
||||
|
|
||||
|
TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
processedColumns = vectorizedColumns; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (Vector256.IsHardwareAccelerated) |
||||
|
{ |
||||
|
int remainingColumns = width - processedColumns; |
||||
|
int vectorizedColumns = remainingColumns - (remainingColumns % Vector256<short>.Count); |
||||
|
int endColumn = processedColumns + vectorizedColumns; |
||||
|
|
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); |
||||
|
ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); |
||||
|
|
||||
|
for (int column = processedColumns; column < endColumn; column += Vector256<short>.Count) |
||||
|
{ |
||||
|
Vector256<short> topLeft = Vector256.LoadUnsafe(ref sourceRow, (nuint)column); |
||||
|
Vector256<short> topRight = TOperator.UsesRight ? Vector256.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default; |
||||
|
Vector256<short> bottomLeft = TOperator.UsesBottom |
||||
|
? Vector256.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)) |
||||
|
: default; |
||||
|
Vector256<short> bottomRight = TOperator.UsesRight && TOperator.UsesBottom |
||||
|
? Vector256.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)) |
||||
|
: default; |
||||
|
|
||||
|
TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
processedColumns = endColumn; |
||||
|
} |
||||
|
|
||||
|
if (Vector128.IsHardwareAccelerated) |
||||
|
{ |
||||
|
int remainingColumns = width - processedColumns; |
||||
|
int vectorizedColumns = remainingColumns - (remainingColumns % Vector128<short>.Count); |
||||
|
int endColumn = processedColumns + vectorizedColumns; |
||||
|
|
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); |
||||
|
ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); |
||||
|
|
||||
|
for (int column = processedColumns; column < endColumn; column += Vector128<short>.Count) |
||||
|
{ |
||||
|
Vector128<short> topLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)column); |
||||
|
Vector128<short> topRight = TOperator.UsesRight ? Vector128.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default; |
||||
|
Vector128<short> bottomLeft = TOperator.UsesBottom |
||||
|
? Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)) |
||||
|
: default; |
||||
|
Vector128<short> bottomRight = TOperator.UsesRight && TOperator.UsesBottom |
||||
|
? Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)) |
||||
|
: default; |
||||
|
|
||||
|
TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
processedColumns = endColumn; |
||||
|
} |
||||
|
|
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); |
||||
|
ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); |
||||
|
|
||||
|
for (int column = processedColumns; column < width; column++) |
||||
|
{ |
||||
|
short topLeft = Unsafe.Add(ref sourceRow, column); |
||||
|
short topRight = TOperator.UsesRight ? Unsafe.Add(ref sourceRow, column + 1) : default; |
||||
|
short bottomLeft = TOperator.UsesBottom ? Unsafe.Add(ref sourceRow, sourceStride + column) : default; |
||||
|
short bottomRight = TOperator.UsesRight && TOperator.UsesBottom |
||||
|
? Unsafe.Add(ref sourceRow, sourceStride + column + 1) |
||||
|
: default; |
||||
|
|
||||
|
Unsafe.Add(ref destinationRow, column) = TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Applies one closed interpolation operator to an 8-bit source block without explicit hardware intrinsics.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TOperator">The source-phase-specific interpolation arithmetic.</typeparam>
|
||||
|
private static void PredictScalar<TOperator>( |
||||
|
ReadOnlySpan<byte> source, |
||||
|
int sourceStride, |
||||
|
Span<byte> destination, |
||||
|
int destinationStride, |
||||
|
int width, |
||||
|
int height) |
||||
|
where TOperator : struct, IOperator |
||||
|
{ |
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
int sourceRow = row * sourceStride; |
||||
|
int destinationRow = row * destinationStride; |
||||
|
|
||||
|
for (int column = 0; column < width; column++) |
||||
|
{ |
||||
|
byte topLeft = source[sourceRow + column]; |
||||
|
byte topRight = TOperator.UsesRight ? source[sourceRow + column + 1] : default; |
||||
|
byte bottomLeft = TOperator.UsesBottom ? source[sourceRow + sourceStride + column] : default; |
||||
|
byte bottomRight = TOperator.UsesRight && TOperator.UsesBottom ? source[sourceRow + sourceStride + column + 1] : default; |
||||
|
destination[destinationRow + column] = TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Applies one closed interpolation operator to a high-bit-depth source block without explicit hardware intrinsics.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TOperator">The source-phase-specific interpolation arithmetic.</typeparam>
|
||||
|
private static void PredictScalar<TOperator>( |
||||
|
ReadOnlySpan<short> source, |
||||
|
int sourceStride, |
||||
|
Span<short> destination, |
||||
|
int destinationStride, |
||||
|
int width, |
||||
|
int height) |
||||
|
where TOperator : struct, IOperator |
||||
|
{ |
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
int sourceRow = row * sourceStride; |
||||
|
int destinationRow = row * destinationStride; |
||||
|
|
||||
|
for (int column = 0; column < width; column++) |
||||
|
{ |
||||
|
short topLeft = source[sourceRow + column]; |
||||
|
short topRight = TOperator.UsesRight ? source[sourceRow + column + 1] : default; |
||||
|
short bottomLeft = TOperator.UsesBottom ? source[sourceRow + sourceStride + column] : default; |
||||
|
short bottomRight = TOperator.UsesRight && TOperator.UsesBottom ? source[sourceRow + sourceStride + column + 1] : default; |
||||
|
destination[destinationRow + column] = TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,136 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Runtime.Intrinsics; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; |
||||
|
|
||||
|
/// <content>
|
||||
|
/// Defines the scalar and SIMD contract for closed intra-block-copy filter operators.
|
||||
|
/// </content>
|
||||
|
internal static partial class Av1IntraBlockCopyPredictor |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines lane-wise arithmetic for one intra-block-copy filter phase.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Every SIMD lane corresponds to one output column. The generic traversal supplies the integer source sample and
|
||||
|
/// its right, lower, and lower-right neighbors; closed operator types allow the JIT to remove unused source loads.
|
||||
|
/// </remarks>
|
||||
|
private interface IOperator |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether the operator consumes the source sample to the right.
|
||||
|
/// </summary>
|
||||
|
public static abstract bool UsesRight { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating whether the operator consumes the source sample on the following row.
|
||||
|
/// </summary>
|
||||
|
public static abstract bool UsesBottom { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Filters one 8-bit sample.
|
||||
|
/// </summary>
|
||||
|
/// <param name="topLeft">The integer-position source sample.</param>
|
||||
|
/// <param name="topRight">The source sample one column to the right.</param>
|
||||
|
/// <param name="bottomLeft">The source sample one row below.</param>
|
||||
|
/// <param name="bottomRight">The source sample one row below and one column to the right.</param>
|
||||
|
/// <returns>The filtered 8-bit sample.</returns>
|
||||
|
public static abstract byte Filter(byte topLeft, byte topRight, byte bottomLeft, byte bottomRight); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Filters sixteen 8-bit samples in parallel.
|
||||
|
/// </summary>
|
||||
|
/// <param name="topLeft">The integer-position source samples.</param>
|
||||
|
/// <param name="topRight">The source samples one column to the right.</param>
|
||||
|
/// <param name="bottomLeft">The source samples one row below.</param>
|
||||
|
/// <param name="bottomRight">The source samples one row below and one column to the right.</param>
|
||||
|
/// <returns>The filtered 8-bit samples.</returns>
|
||||
|
public static abstract Vector128<byte> Filter( |
||||
|
Vector128<byte> topLeft, |
||||
|
Vector128<byte> topRight, |
||||
|
Vector128<byte> bottomLeft, |
||||
|
Vector128<byte> bottomRight); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Filters thirty-two 8-bit samples in parallel.
|
||||
|
/// </summary>
|
||||
|
/// <param name="topLeft">The integer-position source samples.</param>
|
||||
|
/// <param name="topRight">The source samples one column to the right.</param>
|
||||
|
/// <param name="bottomLeft">The source samples one row below.</param>
|
||||
|
/// <param name="bottomRight">The source samples one row below and one column to the right.</param>
|
||||
|
/// <returns>The filtered 8-bit samples.</returns>
|
||||
|
public static abstract Vector256<byte> Filter( |
||||
|
Vector256<byte> topLeft, |
||||
|
Vector256<byte> topRight, |
||||
|
Vector256<byte> bottomLeft, |
||||
|
Vector256<byte> bottomRight); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Filters sixty-four 8-bit samples in parallel.
|
||||
|
/// </summary>
|
||||
|
/// <param name="topLeft">The integer-position source samples.</param>
|
||||
|
/// <param name="topRight">The source samples one column to the right.</param>
|
||||
|
/// <param name="bottomLeft">The source samples one row below.</param>
|
||||
|
/// <param name="bottomRight">The source samples one row below and one column to the right.</param>
|
||||
|
/// <returns>The filtered 8-bit samples.</returns>
|
||||
|
public static abstract Vector512<byte> Filter( |
||||
|
Vector512<byte> topLeft, |
||||
|
Vector512<byte> topRight, |
||||
|
Vector512<byte> bottomLeft, |
||||
|
Vector512<byte> bottomRight); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Filters one high-bit-depth sample.
|
||||
|
/// </summary>
|
||||
|
/// <param name="topLeft">The integer-position source sample.</param>
|
||||
|
/// <param name="topRight">The source sample one column to the right.</param>
|
||||
|
/// <param name="bottomLeft">The source sample one row below.</param>
|
||||
|
/// <param name="bottomRight">The source sample one row below and one column to the right.</param>
|
||||
|
/// <returns>The filtered high-bit-depth sample.</returns>
|
||||
|
public static abstract short Filter(short topLeft, short topRight, short bottomLeft, short bottomRight); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Filters eight high-bit-depth samples in parallel.
|
||||
|
/// </summary>
|
||||
|
/// <param name="topLeft">The integer-position source samples.</param>
|
||||
|
/// <param name="topRight">The source samples one column to the right.</param>
|
||||
|
/// <param name="bottomLeft">The source samples one row below.</param>
|
||||
|
/// <param name="bottomRight">The source samples one row below and one column to the right.</param>
|
||||
|
/// <returns>The filtered high-bit-depth samples.</returns>
|
||||
|
public static abstract Vector128<short> Filter( |
||||
|
Vector128<short> topLeft, |
||||
|
Vector128<short> topRight, |
||||
|
Vector128<short> bottomLeft, |
||||
|
Vector128<short> bottomRight); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Filters sixteen high-bit-depth samples in parallel.
|
||||
|
/// </summary>
|
||||
|
/// <param name="topLeft">The integer-position source samples.</param>
|
||||
|
/// <param name="topRight">The source samples one column to the right.</param>
|
||||
|
/// <param name="bottomLeft">The source samples one row below.</param>
|
||||
|
/// <param name="bottomRight">The source samples one row below and one column to the right.</param>
|
||||
|
/// <returns>The filtered high-bit-depth samples.</returns>
|
||||
|
public static abstract Vector256<short> Filter( |
||||
|
Vector256<short> topLeft, |
||||
|
Vector256<short> topRight, |
||||
|
Vector256<short> bottomLeft, |
||||
|
Vector256<short> bottomRight); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Filters thirty-two high-bit-depth samples in parallel.
|
||||
|
/// </summary>
|
||||
|
/// <param name="topLeft">The integer-position source samples.</param>
|
||||
|
/// <param name="topRight">The source samples one column to the right.</param>
|
||||
|
/// <param name="bottomLeft">The source samples one row below.</param>
|
||||
|
/// <param name="bottomRight">The source samples one row below and one column to the right.</param>
|
||||
|
/// <returns>The filtered high-bit-depth samples.</returns>
|
||||
|
public static abstract Vector512<short> Filter( |
||||
|
Vector512<short> topLeft, |
||||
|
Vector512<short> topRight, |
||||
|
Vector512<short> bottomLeft, |
||||
|
Vector512<short> bottomRight); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,279 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Runtime.Intrinsics; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; |
||||
|
|
||||
|
/// <content>
|
||||
|
/// Defines the closed interpolation operators used by intra-block-copy prediction.
|
||||
|
/// </content>
|
||||
|
internal static partial class Av1IntraBlockCopyPredictor |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Averages horizontally adjacent source samples for a half-sample horizontal phase.
|
||||
|
/// </summary>
|
||||
|
private readonly struct HorizontalOperator : IOperator |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public static bool UsesRight => true; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static bool UsesBottom => false; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static byte Filter(byte topLeft, byte topRight, byte bottomLeft, byte bottomRight) => (byte)((topLeft + topRight + 1) >> 1); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector128<byte> Filter( |
||||
|
Vector128<byte> topLeft, |
||||
|
Vector128<byte> topRight, |
||||
|
Vector128<byte> bottomLeft, |
||||
|
Vector128<byte> bottomRight) |
||||
|
=> AverageRounded(topLeft, topRight); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector256<byte> Filter( |
||||
|
Vector256<byte> topLeft, |
||||
|
Vector256<byte> topRight, |
||||
|
Vector256<byte> bottomLeft, |
||||
|
Vector256<byte> bottomRight) |
||||
|
=> AverageRounded(topLeft, topRight); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector512<byte> Filter( |
||||
|
Vector512<byte> topLeft, |
||||
|
Vector512<byte> topRight, |
||||
|
Vector512<byte> bottomLeft, |
||||
|
Vector512<byte> bottomRight) |
||||
|
=> AverageRounded(topLeft, topRight); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static short Filter(short topLeft, short topRight, short bottomLeft, short bottomRight) => (short)((topLeft + topRight + 1) >> 1); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector128<short> Filter( |
||||
|
Vector128<short> topLeft, |
||||
|
Vector128<short> topRight, |
||||
|
Vector128<short> bottomLeft, |
||||
|
Vector128<short> bottomRight) |
||||
|
=> AverageRounded(topLeft, topRight); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector256<short> Filter( |
||||
|
Vector256<short> topLeft, |
||||
|
Vector256<short> topRight, |
||||
|
Vector256<short> bottomLeft, |
||||
|
Vector256<short> bottomRight) |
||||
|
=> AverageRounded(topLeft, topRight); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector512<short> Filter( |
||||
|
Vector512<short> topLeft, |
||||
|
Vector512<short> topRight, |
||||
|
Vector512<short> bottomLeft, |
||||
|
Vector512<short> bottomRight) |
||||
|
=> AverageRounded(topLeft, topRight); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Averages vertically adjacent source samples for a half-sample vertical phase.
|
||||
|
/// </summary>
|
||||
|
private readonly struct VerticalOperator : IOperator |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public static bool UsesRight => false; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static bool UsesBottom => true; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static byte Filter(byte topLeft, byte topRight, byte bottomLeft, byte bottomRight) => (byte)((topLeft + bottomLeft + 1) >> 1); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector128<byte> Filter( |
||||
|
Vector128<byte> topLeft, |
||||
|
Vector128<byte> topRight, |
||||
|
Vector128<byte> bottomLeft, |
||||
|
Vector128<byte> bottomRight) |
||||
|
=> AverageRounded(topLeft, bottomLeft); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector256<byte> Filter( |
||||
|
Vector256<byte> topLeft, |
||||
|
Vector256<byte> topRight, |
||||
|
Vector256<byte> bottomLeft, |
||||
|
Vector256<byte> bottomRight) |
||||
|
=> AverageRounded(topLeft, bottomLeft); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector512<byte> Filter( |
||||
|
Vector512<byte> topLeft, |
||||
|
Vector512<byte> topRight, |
||||
|
Vector512<byte> bottomLeft, |
||||
|
Vector512<byte> bottomRight) |
||||
|
=> AverageRounded(topLeft, bottomLeft); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static short Filter(short topLeft, short topRight, short bottomLeft, short bottomRight) => (short)((topLeft + bottomLeft + 1) >> 1); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector128<short> Filter( |
||||
|
Vector128<short> topLeft, |
||||
|
Vector128<short> topRight, |
||||
|
Vector128<short> bottomLeft, |
||||
|
Vector128<short> bottomRight) |
||||
|
=> AverageRounded(topLeft, bottomLeft); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector256<short> Filter( |
||||
|
Vector256<short> topLeft, |
||||
|
Vector256<short> topRight, |
||||
|
Vector256<short> bottomLeft, |
||||
|
Vector256<short> bottomRight) |
||||
|
=> AverageRounded(topLeft, bottomLeft); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector512<short> Filter( |
||||
|
Vector512<short> topLeft, |
||||
|
Vector512<short> topRight, |
||||
|
Vector512<short> bottomLeft, |
||||
|
Vector512<short> bottomRight) |
||||
|
=> AverageRounded(topLeft, bottomLeft); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Applies the separable two-dimensional interpolation required when both source axes have a half-sample phase.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// The offsets in libaom's separable two-pass implementation cancel algebraically to
|
||||
|
/// <c>(topLeft + topRight + bottomLeft + bottomRight + 2) >> 2</c>, so the closed operator produces the exact
|
||||
|
/// result directly without an intermediate image buffer.
|
||||
|
///
|
||||
|
/// Byte lanes widen to unsigned 16-bit halves before the four-source sum, while high-bit-depth lanes widen to
|
||||
|
/// unsigned 32-bit halves. Narrowing recombines those halves in source-column order after the rounded result has
|
||||
|
/// returned to the original sample range.
|
||||
|
/// </remarks>
|
||||
|
private readonly struct BilinearOperator : IOperator |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public static bool UsesRight => true; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static bool UsesBottom => true; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static byte Filter(byte topLeft, byte topRight, byte bottomLeft, byte bottomRight) |
||||
|
=> (byte)((topLeft + topRight + bottomLeft + bottomRight + 2) >> 2); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector128<byte> Filter( |
||||
|
Vector128<byte> topLeft, |
||||
|
Vector128<byte> topRight, |
||||
|
Vector128<byte> bottomLeft, |
||||
|
Vector128<byte> bottomRight) |
||||
|
{ |
||||
|
(Vector128<ushort> topLeftLow, Vector128<ushort> topLeftHigh) = Vector128.Widen(topLeft); |
||||
|
(Vector128<ushort> topRightLow, Vector128<ushort> topRightHigh) = Vector128.Widen(topRight); |
||||
|
(Vector128<ushort> bottomLeftLow, Vector128<ushort> bottomLeftHigh) = Vector128.Widen(bottomLeft); |
||||
|
(Vector128<ushort> bottomRightLow, Vector128<ushort> bottomRightHigh) = Vector128.Widen(bottomRight); |
||||
|
|
||||
|
// Four byte samples can sum to 1020, so ushort lanes preserve the complete value before AV1's +2
|
||||
|
// rounding term and divide-by-four shift. Narrowing is exact because the result remains in byte range.
|
||||
|
Vector128<ushort> low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + Vector128.Create((ushort)2)) >> 2; |
||||
|
Vector128<ushort> high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + Vector128.Create((ushort)2)) >> 2; |
||||
|
return Vector128.Narrow(low, high); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector256<byte> Filter( |
||||
|
Vector256<byte> topLeft, |
||||
|
Vector256<byte> topRight, |
||||
|
Vector256<byte> bottomLeft, |
||||
|
Vector256<byte> bottomRight) |
||||
|
{ |
||||
|
(Vector256<ushort> topLeftLow, Vector256<ushort> topLeftHigh) = Vector256.Widen(topLeft); |
||||
|
(Vector256<ushort> topRightLow, Vector256<ushort> topRightHigh) = Vector256.Widen(topRight); |
||||
|
(Vector256<ushort> bottomLeftLow, Vector256<ushort> bottomLeftHigh) = Vector256.Widen(bottomLeft); |
||||
|
(Vector256<ushort> bottomRightLow, Vector256<ushort> bottomRightHigh) = Vector256.Widen(bottomRight); |
||||
|
Vector256<ushort> rounding = Vector256.Create((ushort)2); |
||||
|
Vector256<ushort> low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + rounding) >> 2; |
||||
|
Vector256<ushort> high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + rounding) >> 2; |
||||
|
return Vector256.Narrow(low, high); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector512<byte> Filter( |
||||
|
Vector512<byte> topLeft, |
||||
|
Vector512<byte> topRight, |
||||
|
Vector512<byte> bottomLeft, |
||||
|
Vector512<byte> bottomRight) |
||||
|
{ |
||||
|
(Vector512<ushort> topLeftLow, Vector512<ushort> topLeftHigh) = Vector512.Widen(topLeft); |
||||
|
(Vector512<ushort> topRightLow, Vector512<ushort> topRightHigh) = Vector512.Widen(topRight); |
||||
|
(Vector512<ushort> bottomLeftLow, Vector512<ushort> bottomLeftHigh) = Vector512.Widen(bottomLeft); |
||||
|
(Vector512<ushort> bottomRightLow, Vector512<ushort> bottomRightHigh) = Vector512.Widen(bottomRight); |
||||
|
Vector512<ushort> rounding = Vector512.Create((ushort)2); |
||||
|
Vector512<ushort> low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + rounding) >> 2; |
||||
|
Vector512<ushort> high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + rounding) >> 2; |
||||
|
return Vector512.Narrow(low, high); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static short Filter(short topLeft, short topRight, short bottomLeft, short bottomRight) |
||||
|
=> (short)((topLeft + topRight + bottomLeft + bottomRight + 2) >> 2); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector128<short> Filter( |
||||
|
Vector128<short> topLeft, |
||||
|
Vector128<short> topRight, |
||||
|
Vector128<short> bottomLeft, |
||||
|
Vector128<short> bottomRight) |
||||
|
{ |
||||
|
(Vector128<uint> topLeftLow, Vector128<uint> topLeftHigh) = Vector128.Widen(topLeft.AsUInt16()); |
||||
|
(Vector128<uint> topRightLow, Vector128<uint> topRightHigh) = Vector128.Widen(topRight.AsUInt16()); |
||||
|
(Vector128<uint> bottomLeftLow, Vector128<uint> bottomLeftHigh) = Vector128.Widen(bottomLeft.AsUInt16()); |
||||
|
(Vector128<uint> bottomRightLow, Vector128<uint> bottomRightHigh) = Vector128.Widen(bottomRight.AsUInt16()); |
||||
|
|
||||
|
// High-bit-depth storage is signed for integration with transform code, but reconstructed samples are
|
||||
|
// nonnegative. Unsigned widening therefore preserves 10- and 12-bit values through the four-input sum.
|
||||
|
Vector128<uint> low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + Vector128.Create(2U)) >> 2; |
||||
|
Vector128<uint> high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + Vector128.Create(2U)) >> 2; |
||||
|
return Vector128.Narrow(low, high).AsInt16(); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector256<short> Filter( |
||||
|
Vector256<short> topLeft, |
||||
|
Vector256<short> topRight, |
||||
|
Vector256<short> bottomLeft, |
||||
|
Vector256<short> bottomRight) |
||||
|
{ |
||||
|
(Vector256<uint> topLeftLow, Vector256<uint> topLeftHigh) = Vector256.Widen(topLeft.AsUInt16()); |
||||
|
(Vector256<uint> topRightLow, Vector256<uint> topRightHigh) = Vector256.Widen(topRight.AsUInt16()); |
||||
|
(Vector256<uint> bottomLeftLow, Vector256<uint> bottomLeftHigh) = Vector256.Widen(bottomLeft.AsUInt16()); |
||||
|
(Vector256<uint> bottomRightLow, Vector256<uint> bottomRightHigh) = Vector256.Widen(bottomRight.AsUInt16()); |
||||
|
Vector256<uint> rounding = Vector256.Create(2U); |
||||
|
Vector256<uint> low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + rounding) >> 2; |
||||
|
Vector256<uint> high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + rounding) >> 2; |
||||
|
return Vector256.Narrow(low, high).AsInt16(); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static Vector512<short> Filter( |
||||
|
Vector512<short> topLeft, |
||||
|
Vector512<short> topRight, |
||||
|
Vector512<short> bottomLeft, |
||||
|
Vector512<short> bottomRight) |
||||
|
{ |
||||
|
(Vector512<uint> topLeftLow, Vector512<uint> topLeftHigh) = Vector512.Widen(topLeft.AsUInt16()); |
||||
|
(Vector512<uint> topRightLow, Vector512<uint> topRightHigh) = Vector512.Widen(topRight.AsUInt16()); |
||||
|
(Vector512<uint> bottomLeftLow, Vector512<uint> bottomLeftHigh) = Vector512.Widen(bottomLeft.AsUInt16()); |
||||
|
(Vector512<uint> bottomRightLow, Vector512<uint> bottomRightHigh) = Vector512.Widen(bottomRight.AsUInt16()); |
||||
|
Vector512<uint> rounding = Vector512.Create(2U); |
||||
|
Vector512<uint> low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + rounding) >> 2; |
||||
|
Vector512<uint> high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + rounding) >> 2; |
||||
|
return Vector512.Narrow(low, high).AsInt16(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,231 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reconstructs AV1 intra-block-copy predictions from an earlier region of the current frame.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Whole-sample luma displacements can map to half-sample chroma positions. The predictor therefore selects direct
|
||||
|
/// copy, horizontal two-tap, vertical two-tap, or separable two-dimensional bilinear reconstruction per plane.
|
||||
|
/// Filtered paths use the widest preferred SIMD width and retain an explicit scalar fallback for feature-disabled
|
||||
|
/// execution. Narrow rows read from the frame buffer's prediction padding but use exact-width stores, so vectorization
|
||||
|
/// never depends on writable destination padding.
|
||||
|
/// </remarks>
|
||||
|
internal static partial class Av1IntraBlockCopyPredictor |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Reconstructs an 8-bit intra-block-copy prediction.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The source region beginning at the integer sample preceding any half-sample phase.</param>
|
||||
|
/// <param name="sourceStride">The distance, in samples, between source rows.</param>
|
||||
|
/// <param name="destination">The destination block origin.</param>
|
||||
|
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
|
||||
|
/// <param name="width">The prediction width in samples.</param>
|
||||
|
/// <param name="height">The prediction height in samples.</param>
|
||||
|
/// <param name="halfX">Indicates whether the horizontal source phase is one half-sample.</param>
|
||||
|
/// <param name="halfY">Indicates whether the vertical source phase is one half-sample.</param>
|
||||
|
public static void Predict( |
||||
|
ReadOnlySpan<byte> source, |
||||
|
int sourceStride, |
||||
|
Span<byte> destination, |
||||
|
int destinationStride, |
||||
|
int width, |
||||
|
int height, |
||||
|
bool halfX, |
||||
|
bool halfY) |
||||
|
{ |
||||
|
if (!halfX && !halfY) |
||||
|
{ |
||||
|
Copy(source, sourceStride, destination, destinationStride, width, height); |
||||
|
} |
||||
|
else if (halfX && halfY) |
||||
|
{ |
||||
|
Predict<BilinearOperator>(source, sourceStride, destination, destinationStride, width, height); |
||||
|
} |
||||
|
else if (halfX) |
||||
|
{ |
||||
|
Predict<HorizontalOperator>(source, sourceStride, destination, destinationStride, width, height); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Predict<VerticalOperator>(source, sourceStride, destination, destinationStride, width, height); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reconstructs a high-bit-depth intra-block-copy prediction.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The source region beginning at the integer sample preceding any half-sample phase.</param>
|
||||
|
/// <param name="sourceStride">The distance, in samples, between source rows.</param>
|
||||
|
/// <param name="destination">The destination block origin.</param>
|
||||
|
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
|
||||
|
/// <param name="width">The prediction width in samples.</param>
|
||||
|
/// <param name="height">The prediction height in samples.</param>
|
||||
|
/// <param name="halfX">Indicates whether the horizontal source phase is one half-sample.</param>
|
||||
|
/// <param name="halfY">Indicates whether the vertical source phase is one half-sample.</param>
|
||||
|
public static void Predict( |
||||
|
ReadOnlySpan<short> source, |
||||
|
int sourceStride, |
||||
|
Span<short> destination, |
||||
|
int destinationStride, |
||||
|
int width, |
||||
|
int height, |
||||
|
bool halfX, |
||||
|
bool halfY) |
||||
|
{ |
||||
|
if (!halfX && !halfY) |
||||
|
{ |
||||
|
Copy(source, sourceStride, destination, destinationStride, width, height); |
||||
|
} |
||||
|
else if (halfX && halfY) |
||||
|
{ |
||||
|
Predict<BilinearOperator>(source, sourceStride, destination, destinationStride, width, height); |
||||
|
} |
||||
|
else if (halfX) |
||||
|
{ |
||||
|
Predict<HorizontalOperator>(source, sourceStride, destination, destinationStride, width, height); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Predict<VerticalOperator>(source, sourceStride, destination, destinationStride, width, height); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reconstructs an 8-bit intra-block-copy prediction without explicit hardware intrinsics.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The source region beginning at the integer sample preceding any half-sample phase.</param>
|
||||
|
/// <param name="sourceStride">The distance, in samples, between source rows.</param>
|
||||
|
/// <param name="destination">The destination block origin.</param>
|
||||
|
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
|
||||
|
/// <param name="width">The prediction width in samples.</param>
|
||||
|
/// <param name="height">The prediction height in samples.</param>
|
||||
|
/// <param name="halfX">Indicates whether the horizontal source phase is one half-sample.</param>
|
||||
|
/// <param name="halfY">Indicates whether the vertical source phase is one half-sample.</param>
|
||||
|
public static void PredictScalar( |
||||
|
ReadOnlySpan<byte> source, |
||||
|
int sourceStride, |
||||
|
Span<byte> destination, |
||||
|
int destinationStride, |
||||
|
int width, |
||||
|
int height, |
||||
|
bool halfX, |
||||
|
bool halfY) |
||||
|
{ |
||||
|
if (!halfX && !halfY) |
||||
|
{ |
||||
|
CopyScalar(source, sourceStride, destination, destinationStride, width, height); |
||||
|
} |
||||
|
else if (halfX && halfY) |
||||
|
{ |
||||
|
PredictScalar<BilinearOperator>(source, sourceStride, destination, destinationStride, width, height); |
||||
|
} |
||||
|
else if (halfX) |
||||
|
{ |
||||
|
PredictScalar<HorizontalOperator>(source, sourceStride, destination, destinationStride, width, height); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
PredictScalar<VerticalOperator>(source, sourceStride, destination, destinationStride, width, height); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reconstructs a high-bit-depth intra-block-copy prediction without explicit hardware intrinsics.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The source region beginning at the integer sample preceding any half-sample phase.</param>
|
||||
|
/// <param name="sourceStride">The distance, in samples, between source rows.</param>
|
||||
|
/// <param name="destination">The destination block origin.</param>
|
||||
|
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
|
||||
|
/// <param name="width">The prediction width in samples.</param>
|
||||
|
/// <param name="height">The prediction height in samples.</param>
|
||||
|
/// <param name="halfX">Indicates whether the horizontal source phase is one half-sample.</param>
|
||||
|
/// <param name="halfY">Indicates whether the vertical source phase is one half-sample.</param>
|
||||
|
public static void PredictScalar( |
||||
|
ReadOnlySpan<short> source, |
||||
|
int sourceStride, |
||||
|
Span<short> destination, |
||||
|
int destinationStride, |
||||
|
int width, |
||||
|
int height, |
||||
|
bool halfX, |
||||
|
bool halfY) |
||||
|
{ |
||||
|
if (!halfX && !halfY) |
||||
|
{ |
||||
|
CopyScalar(source, sourceStride, destination, destinationStride, width, height); |
||||
|
} |
||||
|
else if (halfX && halfY) |
||||
|
{ |
||||
|
PredictScalar<BilinearOperator>(source, sourceStride, destination, destinationStride, width, height); |
||||
|
} |
||||
|
else if (halfX) |
||||
|
{ |
||||
|
PredictScalar<HorizontalOperator>(source, sourceStride, destination, destinationStride, width, height); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
PredictScalar<VerticalOperator>(source, sourceStride, destination, destinationStride, width, height); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Copies an 8-bit whole-sample source block to its destination.
|
||||
|
/// </summary>
|
||||
|
private static void Copy(ReadOnlySpan<byte> source, int sourceStride, Span<byte> destination, int destinationStride, int width, int height) |
||||
|
{ |
||||
|
// Span copying delegates each complete row to the runtime's overlap-safe native-width implementation. The
|
||||
|
// displacement validity rules keep source and destination blocks separate, so no intermediate buffer is needed.
|
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
source.Slice(row * sourceStride, width).CopyTo(destination.Slice(row * destinationStride, width)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Copies a high-bit-depth whole-sample source block to its destination.
|
||||
|
/// </summary>
|
||||
|
private static void Copy(ReadOnlySpan<short> source, int sourceStride, Span<short> destination, int destinationStride, int width, int height) |
||||
|
{ |
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
source.Slice(row * sourceStride, width).CopyTo(destination.Slice(row * destinationStride, width)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Copies an 8-bit whole-sample source block with scalar sample assignments.
|
||||
|
/// </summary>
|
||||
|
private static void CopyScalar(ReadOnlySpan<byte> source, int sourceStride, Span<byte> destination, int destinationStride, int width, int height) |
||||
|
{ |
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
for (int column = 0; column < width; column++) |
||||
|
{ |
||||
|
destination[(row * destinationStride) + column] = source[(row * sourceStride) + column]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Copies a high-bit-depth whole-sample source block with scalar sample assignments.
|
||||
|
/// </summary>
|
||||
|
private static void CopyScalar( |
||||
|
ReadOnlySpan<short> source, |
||||
|
int sourceStride, |
||||
|
Span<short> destination, |
||||
|
int destinationStride, |
||||
|
int width, |
||||
|
int height) |
||||
|
{ |
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
for (int column = 0; column < width; column++) |
||||
|
{ |
||||
|
destination[(row * destinationStride) + column] = source[(row * sourceStride) + column]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,235 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; |
||||
|
using SixLabors.ImageSharp.Tests.TestUtilities; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies AV1 intra-block-copy interpolation across the supported hardware-intrinsic configurations.
|
||||
|
/// </summary>
|
||||
|
[Trait("Format", "Heif")] |
||||
|
public class Av1IntraBlockCopyPredictorTests |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Exercises each SIMD register-width tier and the complete scalar fallback.
|
||||
|
/// </summary>
|
||||
|
private const HwIntrinsics PredictorConfigurations = |
||||
|
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies all four source phases for 8-bit samples at every AV1 transform size.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void EightBitPredictionMatchesScalarAcrossIntrinsicWidths() |
||||
|
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateEightBitPrediction, PredictorConfigurations); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies all four source phases for high-bit-depth samples at every AV1 transform size.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void HighBitDepthPredictionMatchesScalarAcrossIntrinsicWidths() |
||||
|
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthPrediction, PredictorConfigurations); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies the four normative interpolation equations against independently calculated sample blocks.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void PredictionMatchesKnownInterpolationValues() |
||||
|
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateKnownInterpolationValues, PredictorConfigurations); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Compares the SIMD-first 8-bit implementation with its scalar definition and verifies that row padding is unchanged.
|
||||
|
/// </summary>
|
||||
|
private static void ValidateEightBitPrediction() |
||||
|
{ |
||||
|
for (int sizeIndex = 0; sizeIndex < (int)Av1TransformSize.AllSizes; sizeIndex++) |
||||
|
{ |
||||
|
Av1TransformSize transformSize = (Av1TransformSize)sizeIndex; |
||||
|
int width = transformSize.GetWidth(); |
||||
|
int height = transformSize.GetHeight(); |
||||
|
int sourceStride = width + 17; |
||||
|
int destinationStride = width + 7; |
||||
|
byte[] source = new byte[sourceStride * (height + 1)]; |
||||
|
|
||||
|
for (int i = 0; i < source.Length; i++) |
||||
|
{ |
||||
|
source[i] = (byte)((i * 29) + 17); |
||||
|
} |
||||
|
|
||||
|
for (int phase = 0; phase < 4; phase++) |
||||
|
{ |
||||
|
byte[] expected = Enumerable.Repeat((byte)0xA5, destinationStride * height).ToArray(); |
||||
|
byte[] actual = Enumerable.Repeat((byte)0xA5, destinationStride * height).ToArray(); |
||||
|
bool halfX = (phase & 1) != 0; |
||||
|
bool halfY = (phase & 2) != 0; |
||||
|
|
||||
|
Av1IntraBlockCopyPredictor.PredictScalar( |
||||
|
source, |
||||
|
sourceStride, |
||||
|
expected, |
||||
|
destinationStride, |
||||
|
width, |
||||
|
height, |
||||
|
halfX, |
||||
|
halfY); |
||||
|
|
||||
|
Av1IntraBlockCopyPredictor.Predict( |
||||
|
source, |
||||
|
sourceStride, |
||||
|
actual, |
||||
|
destinationStride, |
||||
|
width, |
||||
|
height, |
||||
|
halfX, |
||||
|
halfY); |
||||
|
|
||||
|
Assert.Equal(expected, actual); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Compares the SIMD-first high-bit-depth implementation with its scalar definition and verifies exact-width stores.
|
||||
|
/// </summary>
|
||||
|
private static void ValidateHighBitDepthPrediction() |
||||
|
{ |
||||
|
for (int sizeIndex = 0; sizeIndex < (int)Av1TransformSize.AllSizes; sizeIndex++) |
||||
|
{ |
||||
|
Av1TransformSize transformSize = (Av1TransformSize)sizeIndex; |
||||
|
int width = transformSize.GetWidth(); |
||||
|
int height = transformSize.GetHeight(); |
||||
|
int sourceStride = width + 9; |
||||
|
int destinationStride = width + 5; |
||||
|
short[] source = new short[sourceStride * (height + 1)]; |
||||
|
|
||||
|
for (int i = 0; i < source.Length; i++) |
||||
|
{ |
||||
|
source[i] = (short)(((i * 53) + 31) & 0xFFF); |
||||
|
} |
||||
|
|
||||
|
for (int phase = 0; phase < 4; phase++) |
||||
|
{ |
||||
|
short[] expected = Enumerable.Repeat((short)0x5A5A, destinationStride * height).ToArray(); |
||||
|
short[] actual = Enumerable.Repeat((short)0x5A5A, destinationStride * height).ToArray(); |
||||
|
bool halfX = (phase & 1) != 0; |
||||
|
bool halfY = (phase & 2) != 0; |
||||
|
|
||||
|
Av1IntraBlockCopyPredictor.PredictScalar( |
||||
|
source, |
||||
|
sourceStride, |
||||
|
expected, |
||||
|
destinationStride, |
||||
|
width, |
||||
|
height, |
||||
|
halfX, |
||||
|
halfY); |
||||
|
|
||||
|
Av1IntraBlockCopyPredictor.Predict( |
||||
|
source, |
||||
|
sourceStride, |
||||
|
actual, |
||||
|
destinationStride, |
||||
|
width, |
||||
|
height, |
||||
|
halfX, |
||||
|
halfY); |
||||
|
|
||||
|
Assert.Equal(expected, actual); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Applies each source phase to a four-by-four block whose expected results are simple arithmetic progressions.
|
||||
|
/// </summary>
|
||||
|
private static void ValidateKnownInterpolationValues() |
||||
|
{ |
||||
|
const int sourceStride = 21; |
||||
|
byte[] source = new byte[sourceStride * 5]; |
||||
|
for (int row = 0; row < 5; row++) |
||||
|
{ |
||||
|
for (int column = 0; column < 5; column++) |
||||
|
{ |
||||
|
source[(row * sourceStride) + column] = (byte)((row * 20) + (column * 4)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ReadOnlySpan<byte> copied = |
||||
|
[ |
||||
|
0, 4, 8, 12, |
||||
|
20, 24, 28, 32, |
||||
|
40, 44, 48, 52, |
||||
|
60, 64, 68, 72, |
||||
|
]; |
||||
|
|
||||
|
ReadOnlySpan<byte> horizontal = |
||||
|
[ |
||||
|
2, 6, 10, 14, |
||||
|
22, 26, 30, 34, |
||||
|
42, 46, 50, 54, |
||||
|
62, 66, 70, 74, |
||||
|
]; |
||||
|
|
||||
|
ReadOnlySpan<byte> vertical = |
||||
|
[ |
||||
|
10, 14, 18, 22, |
||||
|
30, 34, 38, 42, |
||||
|
50, 54, 58, 62, |
||||
|
70, 74, 78, 82, |
||||
|
]; |
||||
|
|
||||
|
ReadOnlySpan<byte> bilinear = |
||||
|
[ |
||||
|
12, 16, 20, 24, |
||||
|
32, 36, 40, 44, |
||||
|
52, 56, 60, 64, |
||||
|
72, 76, 80, 84, |
||||
|
]; |
||||
|
|
||||
|
ValidateKnownPhase(source, sourceStride, copied, false, false); |
||||
|
ValidateKnownPhase(source, sourceStride, horizontal, true, false); |
||||
|
ValidateKnownPhase(source, sourceStride, vertical, false, true); |
||||
|
ValidateKnownPhase(source, sourceStride, bilinear, true, true); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies one four-by-four source phase for both 8-bit and translated high-bit-depth samples.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The five-by-five 8-bit source region.</param>
|
||||
|
/// <param name="sourceStride">The number of source samples per row.</param>
|
||||
|
/// <param name="expected">The independently calculated four-by-four prediction.</param>
|
||||
|
/// <param name="halfX">Indicates whether the horizontal phase is one half-sample.</param>
|
||||
|
/// <param name="halfY">Indicates whether the vertical phase is one half-sample.</param>
|
||||
|
private static void ValidateKnownPhase( |
||||
|
ReadOnlySpan<byte> source, |
||||
|
int sourceStride, |
||||
|
ReadOnlySpan<byte> expected, |
||||
|
bool halfX, |
||||
|
bool halfY) |
||||
|
{ |
||||
|
byte[] actual = new byte[16]; |
||||
|
Av1IntraBlockCopyPredictor.Predict(source, sourceStride, actual, 4, 4, 4, halfX, halfY); |
||||
|
Assert.Equal(expected, actual); |
||||
|
|
||||
|
const int highBitDepthOffset = 1024; |
||||
|
short[] highBitDepthSource = new short[source.Length]; |
||||
|
short[] highBitDepthExpected = new short[expected.Length]; |
||||
|
short[] highBitDepthActual = new short[16]; |
||||
|
|
||||
|
for (int i = 0; i < source.Length; i++) |
||||
|
{ |
||||
|
highBitDepthSource[i] = (short)(source[i] + highBitDepthOffset); |
||||
|
} |
||||
|
|
||||
|
for (int i = 0; i < expected.Length; i++) |
||||
|
{ |
||||
|
highBitDepthExpected[i] = (short)(expected[i] + highBitDepthOffset); |
||||
|
} |
||||
|
|
||||
|
Av1IntraBlockCopyPredictor.Predict(highBitDepthSource, sourceStride, highBitDepthActual, 4, 4, 4, halfX, halfY); |
||||
|
Assert.Equal(highBitDepthExpected, highBitDepthActual); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,149 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies AV1 intra-block-copy reference derivation and displacement-vector legality rules.
|
||||
|
/// </summary>
|
||||
|
[Trait("Format", "Heif")] |
||||
|
public class Av1IntraBlockCopyTests |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Verifies the horizontal fallback used in the tile's first superblock row.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void FindReferenceUsesDelayedHorizontalFallbackInFirstSuperblockRow() |
||||
|
{ |
||||
|
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); |
||||
|
Av1FrameInfo frameInfo = new(sequenceHeader); |
||||
|
Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(5, 0)); |
||||
|
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, Point.Empty); |
||||
|
Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) |
||||
|
{ |
||||
|
ColumnIndex = 80, |
||||
|
RowIndex = 0, |
||||
|
}; |
||||
|
|
||||
|
Av1TileInfo tileInfo = CreateTileInfo(); |
||||
|
Av1MotionVector[] candidates = new Av1MotionVector[8]; |
||||
|
int[] weights = new int[8]; |
||||
|
|
||||
|
Av1MotionVector actual = Av1IntraBlockCopy.FindReference( |
||||
|
partitionInfo, |
||||
|
tileInfo, |
||||
|
sequenceHeader.SuperblockModeInfoSize, |
||||
|
candidates, |
||||
|
weights); |
||||
|
|
||||
|
Assert.Equal(new Av1MotionVector(0, -2560), actual); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies the vertical fallback used after the tile's first superblock row when spatial candidates are absent.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void FindReferenceUsesPreviousSuperblockRowFallback() |
||||
|
{ |
||||
|
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); |
||||
|
Av1FrameInfo frameInfo = new(sequenceHeader); |
||||
|
Av1SuperblockInfo aboveSuperblock = frameInfo.GetSuperblock(new Point(5, 0)); |
||||
|
Av1BlockModeInfo aboveModeInfo = new(Av1BlockSize.Block64x64, Point.Empty); |
||||
|
frameInfo.UpdateModeInfo(aboveModeInfo, aboveSuperblock); |
||||
|
aboveSuperblock.BlockCount++; |
||||
|
|
||||
|
Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(5, 1)); |
||||
|
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, Point.Empty); |
||||
|
Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) |
||||
|
{ |
||||
|
AvailableAbove = true, |
||||
|
ColumnIndex = 80, |
||||
|
RowIndex = 16, |
||||
|
}; |
||||
|
|
||||
|
Av1TileInfo tileInfo = CreateTileInfo(); |
||||
|
Av1MotionVector[] candidates = new Av1MotionVector[8]; |
||||
|
int[] weights = new int[8]; |
||||
|
|
||||
|
Av1MotionVector actual = Av1IntraBlockCopy.FindReference( |
||||
|
partitionInfo, |
||||
|
tileInfo, |
||||
|
sequenceHeader.SuperblockModeInfoSize, |
||||
|
candidates, |
||||
|
weights); |
||||
|
|
||||
|
Assert.Equal(new Av1MotionVector(-512, 0), actual); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies tile bounds, whole-sample precision, the four-block delay, and wavefront ordering.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void IsValidEnforcesIntraBlockCopySourceRestrictions() |
||||
|
{ |
||||
|
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); |
||||
|
Av1FrameInfo frameInfo = new(sequenceHeader); |
||||
|
Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(8, 2)); |
||||
|
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, Point.Empty); |
||||
|
Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) |
||||
|
{ |
||||
|
ColumnIndex = 128, |
||||
|
RowIndex = 32, |
||||
|
}; |
||||
|
|
||||
|
Av1TileInfo tileInfo = CreateTileInfo(); |
||||
|
|
||||
|
// A source five 64-sample columns earlier satisfies both the four-column delay and same-row wavefront limit.
|
||||
|
Assert.True(Av1IntraBlockCopy.IsValid(new Av1MotionVector(0, -2560), partitionInfo, tileInfo, sequenceHeader)); |
||||
|
|
||||
|
// Moving the source one 64-sample column to the right reaches the forbidden delay boundary exactly.
|
||||
|
Assert.False(Av1IntraBlockCopy.IsValid(new Av1MotionVector(0, -2048), partitionInfo, tileInfo, sequenceHeader)); |
||||
|
Assert.False(Av1IntraBlockCopy.IsValid(new Av1MotionVector(0, -2559), partitionInfo, tileInfo, sequenceHeader)); |
||||
|
Assert.False(Av1IntraBlockCopy.IsValid(new Av1MotionVector(0, -4608), partitionInfo, tileInfo, sequenceHeader)); |
||||
|
Assert.False(Av1IntraBlockCopy.IsValid(new Av1MotionVector(512, -2560), partitionInfo, tileInfo, sequenceHeader)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates the 640-by-256, 4:2:0 sequence geometry shared by the displacement tests.
|
||||
|
/// </summary>
|
||||
|
private static ObuSequenceHeader CreateSequenceHeader() |
||||
|
=> new() |
||||
|
{ |
||||
|
MaxFrameWidth = 640, |
||||
|
MaxFrameHeight = 256, |
||||
|
Use128x128Superblock = false, |
||||
|
ColorConfig = new ObuColorConfig |
||||
|
{ |
||||
|
IsMonochrome = false, |
||||
|
SubSamplingX = true, |
||||
|
SubSamplingY = true, |
||||
|
BitDepth = Av1BitDepth.EightBit, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates one tile covering the complete test frame.
|
||||
|
/// </summary>
|
||||
|
private static Av1TileInfo CreateTileInfo() |
||||
|
{ |
||||
|
ObuFrameHeader frameHeader = new() |
||||
|
{ |
||||
|
ModeInfoColumnCount = 160, |
||||
|
ModeInfoRowCount = 64, |
||||
|
TilesInfo = new ObuTileGroupHeader |
||||
|
{ |
||||
|
TileColumnCount = 1, |
||||
|
TileRowCount = 1, |
||||
|
TileColumnStartModeInfo = [0, 160], |
||||
|
TileRowStartModeInfo = [0, 64], |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
return new Av1TileInfo(0, 0, frameHeader); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue