// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
///
/// Verifies AV1 translational inter prediction against an independent implementation of the normative fixed-point convolution rules.
///
[Trait("Format", "Avif")]
public class Av1InterPredictorTests
{
///
/// The number of fractional coefficient bits in AV1 interpolation kernels.
///
private const int FilterBits = 7;
///
/// The default first convolution shift used for 8- and 10-bit predictions.
///
private const int Round0Bits = 3;
///
/// The number of stored coefficient positions in every tested interpolation kernel.
///
private const int FilterTapCount = 8;
///
/// The number of coefficient positions preceding the integer-position sample.
///
private const int FilterCenterOffset = 3;
///
/// The source samples retained before the integer-position column.
///
private const int SourceLeftPadding = 3;
///
/// The source samples retained after each active row for a complete 512-bit byte load.
///
private const int SourceRightPadding = 64;
///
/// The source rows retained before the integer-position row.
///
private const int SourceTopPadding = 3;
///
/// The source rows retained after the prediction block.
///
private const int SourceBottomPadding = 4;
///
/// The guarded destination elements preceding the first active row.
///
private const int DestinationPrefix = 11;
///
/// The guarded destination elements following the final padded row.
///
private const int DestinationSuffix = 17;
///
/// The guarded destination elements following each active row.
///
private const int DestinationRowPadding = 13;
///
/// The non-image value stored in every guarded 8-bit destination element.
///
private const byte ByteDestinationSentinel = 0xD3;
///
/// The non-image value stored in every guarded ushort destination element.
///
private const ushort HighBitDepthDestinationSentinel = 0xDEAD;
///
/// Exercises the native vector width, the 256-bit path, the 128-bit path, and the complete scalar fallback.
///
///
/// Disabling AVX also disables AVX2 and leaves the x86 128-bit vector tier enabled, which is the established
/// configuration used by the other AV1 SIMD tests.
///
private const HwIntrinsics PredictorConfigurations =
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic;
///
/// Verifies exact 8-bit copy and convolution output, scalar tails, and untouched destination padding under every SIMD configuration.
///
[Fact]
public void BytePredictionMatchesLibaomOracleAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateBytePredictions, PredictorConfigurations);
///
/// Verifies exact 8-, 10-, and 12-bit ushort output, scalar tails, and untouched destination padding under every SIMD configuration.
///
[Fact]
public void HighBitDepthPredictionMatchesLibaomOracleAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthPredictions, PredictorConfigurations);
///
/// Verifies that SIMD compound intermediates retain scalar-equivalent values and untouched destination padding.
///
[Fact]
public void CompoundPredictionMatchesScalarAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateCompoundPredictions, PredictorConfigurations);
///
/// Applies every byte prediction scenario to the SIMD-first and explicitly scalar entry points.
///
private static void ValidateBytePredictions()
{
foreach (PredictionCase testCase in CreatePredictionCases())
{
byte[] source = CreateByteSource(testCase, out int sourceStride, out int sourceOrigin);
int destinationStride = testCase.Width + DestinationRowPadding;
byte[] expected = CreateByteDestination(testCase, destinationStride);
byte[] actual = (byte[])expected.Clone();
byte[] scalar = (byte[])expected.Clone();
short[] simdScratch = CreateScratch(testCase);
short[] scalarScratch = CreateScratch(testCase);
ApplyReference(source, sourceStride, sourceOrigin, expected, DestinationPrefix, destinationStride, testCase, 8);
Av1InterPredictor.Predict(
source, sourceStride, sourceOrigin, actual.AsSpan(DestinationPrefix), destinationStride, testCase.Width, testCase.Height,
testCase.HorizontalFilter, testCase.VerticalFilter, testCase.HorizontalPhase, testCase.VerticalPhase, simdScratch);
Av1InterPredictor.PredictScalar(
source, sourceStride, sourceOrigin, scalar.AsSpan(DestinationPrefix), destinationStride, testCase.Width, testCase.Height,
testCase.HorizontalFilter, testCase.VerticalFilter, testCase.HorizontalPhase, testCase.VerticalPhase, scalarScratch);
AssertEqual(expected, actual, testCase, "SIMD-first byte");
AssertEqual(expected, scalar, testCase, "scalar byte");
}
}
///
/// Applies every ushort prediction scenario at each supported sample precision to the SIMD-first and scalar entry points.
///
private static void ValidateHighBitDepthPredictions()
{
int[] bitDepths = [8, 10, 12];
foreach (int bitDepth in bitDepths)
{
foreach (PredictionCase testCase in CreatePredictionCases())
{
ushort[] source = CreateHighBitDepthSource(testCase, bitDepth, out int sourceStride, out int sourceOrigin);
int destinationStride = testCase.Width + DestinationRowPadding;
ushort[] expected = CreateHighBitDepthDestination(testCase, destinationStride);
ushort[] actual = (ushort[])expected.Clone();
ushort[] scalar = (ushort[])expected.Clone();
short[] simdScratch = CreateScratch(testCase);
short[] scalarScratch = CreateScratch(testCase);
ApplyReference(source, sourceStride, sourceOrigin, expected, DestinationPrefix, destinationStride, testCase, bitDepth);
Av1InterPredictor.Predict(
source, sourceStride, sourceOrigin, actual.AsSpan(DestinationPrefix), destinationStride, testCase.Width, testCase.Height,
testCase.HorizontalFilter, testCase.VerticalFilter, testCase.HorizontalPhase, testCase.VerticalPhase, bitDepth, simdScratch);
Av1InterPredictor.PredictScalar(
source, sourceStride, sourceOrigin, scalar.AsSpan(DestinationPrefix), destinationStride, testCase.Width, testCase.Height,
testCase.HorizontalFilter, testCase.VerticalFilter, testCase.HorizontalPhase, testCase.VerticalPhase, bitDepth, scalarScratch);
AssertEqual(expected, actual, testCase, $"SIMD-first {bitDepth}-bit ushort");
AssertEqual(expected, scalar, testCase, $"scalar {bitDepth}-bit ushort");
}
}
}
///
/// Applies every byte prediction scenario to the SIMD-first and scalar compound-intermediate entry points.
///
private static void ValidateCompoundPredictions()
{
foreach (PredictionCase testCase in CreatePredictionCases())
{
byte[] source = CreateByteSource(testCase, out int sourceStride, out int sourceOrigin);
int destinationStride = testCase.Width + DestinationRowPadding;
ushort[] expected = CreateHighBitDepthDestination(testCase, destinationStride);
ushort[] actual = (ushort[])expected.Clone();
short[] simdScratch = CreateScratch(testCase);
short[] scalarScratch = CreateScratch(testCase);
Av1CompoundInterPredictor.PredictCompoundScalar(
source,
sourceStride,
sourceOrigin,
expected.AsSpan(DestinationPrefix),
destinationStride,
testCase.Width,
testCase.Height,
testCase.HorizontalFilter,
testCase.VerticalFilter,
testCase.HorizontalPhase,
testCase.VerticalPhase,
scalarScratch);
Av1CompoundInterPredictor.PredictCompound(
source,
sourceStride,
sourceOrigin,
actual.AsSpan(DestinationPrefix),
destinationStride,
testCase.Width,
testCase.Height,
testCase.HorizontalFilter,
testCase.VerticalFilter,
testCase.HorizontalPhase,
testCase.VerticalPhase,
simdScratch);
AssertEqual(expected, actual, testCase, "SIMD-first compound intermediate");
}
}
///
/// Creates the named operation matrix covering copy, each one-dimensional direction, separable filtering, reduced kernels, and vector tails.
///
/// The prediction scenarios.
private static PredictionCase[] CreatePredictionCases() =>
[
new("copy-sub8x8-chroma", 2, 4, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Sharp, 0, 0),
new("regular-horizontal-sub8x8-chroma", 2, 4, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 3, 0),
new("regular-vertical-sub8x8-chroma", 4, 2, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 0, 3),
new("smooth-sharp-sub8x8-chroma", 2, 2, Av1InterpolationFilter.Smooth, Av1InterpolationFilter.Sharp, 7, 13),
new("copy-wide-tail", 68, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Sharp, 0, 0),
new("regular-horizontal-wide-tail", 68, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 1, 0),
new("smooth-horizontal-256-tail", 36, 8, Av1InterpolationFilter.Smooth, Av1InterpolationFilter.Regular, 7, 0),
new("sharp-horizontal-128-tail", 20, 8, Av1InterpolationFilter.Sharp, Av1InterpolationFilter.Regular, 8, 0),
new("bilinear-horizontal", 8, 8, Av1InterpolationFilter.Bilinear, Av1InterpolationFilter.Regular, 15, 0),
new("regular-horizontal-reduced", 4, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 3, 0),
new("smooth-horizontal-reduced", 4, 8, Av1InterpolationFilter.Smooth, Av1InterpolationFilter.Regular, 13, 0),
new("sharp-horizontal-reduced", 4, 8, Av1InterpolationFilter.Sharp, Av1InterpolationFilter.Regular, 5, 0),
new("regular-vertical-reduced", 68, 4, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 0, 3),
new("smooth-vertical-reduced", 36, 4, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Smooth, 0, 13),
new("sharp-vertical-reduced", 20, 4, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Sharp, 0, 5),
new("bilinear-vertical", 8, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Bilinear, 0, 8),
new("regular-smooth-two-dimensional", 68, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Smooth, 1, 15),
new("sharp-bilinear-two-dimensional", 36, 8, Av1InterpolationFilter.Sharp, Av1InterpolationFilter.Bilinear, 8, 3),
new("smooth-sharp-reduced-two-dimensional", 4, 4, Av1InterpolationFilter.Smooth, Av1InterpolationFilter.Sharp, 7, 13),
new("bilinear-regular-small-height", 20, 4, Av1InterpolationFilter.Bilinear, Av1InterpolationFilter.Regular, 11, 5),
new("sharp-smooth-small-width", 4, 8, Av1InterpolationFilter.Sharp, Av1InterpolationFilter.Smooth, 5, 7)
];
///
/// Creates an 8-bit padded reference plane and returns the integer-position source origin within that plane.
///
/// The prediction geometry used to size the plane.
/// Receives the padded source-row stride.
/// Receives the integer-position sample index.
/// The complete padded source plane.
private static byte[] CreateByteSource(PredictionCase testCase, out int sourceStride, out int sourceOrigin)
{
sourceStride = SourceLeftPadding + testCase.Width + SourceRightPadding;
int sourceHeight = SourceTopPadding + testCase.Height + SourceBottomPadding;
byte[] source = new byte[sourceStride * sourceHeight];
for (int row = 0; row < sourceHeight; row++)
{
for (int column = 0; column < sourceStride; column++)
{
// Distinct row, column, and cross-term multipliers prevent a wrong stride or tap direction from
// producing the same arithmetic progression as the correctly addressed source window.
source[(row * sourceStride) + column] = (byte)(((row * 59) + (column * 37) + (row * column * 11) + 17) & byte.MaxValue);
}
}
sourceOrigin = (SourceTopPadding * sourceStride) + SourceLeftPadding;
return source;
}
///
/// Creates a padded high-bit-depth reference plane spanning the legal range for the requested precision.
///
/// The prediction geometry used to size the plane.
/// The decoded sample precision.
/// Receives the padded source-row stride.
/// Receives the integer-position sample index.
/// The complete padded source plane.
private static ushort[] CreateHighBitDepthSource(PredictionCase testCase, int bitDepth, out int sourceStride, out int sourceOrigin)
{
sourceStride = SourceLeftPadding + testCase.Width + SourceRightPadding;
int sourceHeight = SourceTopPadding + testCase.Height + SourceBottomPadding;
int maximum = (1 << bitDepth) - 1;
ushort[] source = new ushort[sourceStride * sourceHeight];
for (int row = 0; row < sourceHeight; row++)
{
for (int column = 0; column < sourceStride; column++)
{
// The high-bit-depth pattern uses different coprime multipliers and spans the complete requested
// range, exercising negative-lobe clipping as well as low and high sample values.
source[(row * sourceStride) + column] = (ushort)(((row * 977) + (column * 353) + (row * column * 29) + 101) & maximum);
}
}
sourceOrigin = (SourceTopPadding * sourceStride) + SourceLeftPadding;
return source;
}
///
/// Creates an 8-bit destination whose prefix, row padding, and suffix expose stores outside the prediction block.
///
/// The prediction geometry used to size the destination.
/// The padded destination-row stride.
/// The guarded destination storage.
private static byte[] CreateByteDestination(PredictionCase testCase, int destinationStride)
=> Enumerable.Repeat(ByteDestinationSentinel, DestinationPrefix + (destinationStride * testCase.Height) + DestinationSuffix).ToArray();
///
/// Creates a ushort destination whose prefix, row padding, and suffix expose stores outside the prediction block.
///
/// The prediction geometry used to size the destination.
/// The padded destination-row stride.
/// The guarded destination storage.
private static ushort[] CreateHighBitDepthDestination(PredictionCase testCase, int destinationStride)
=> Enumerable
.Repeat(HighBitDepthDestinationSentinel, DestinationPrefix + (destinationStride * testCase.Height) + DestinationSuffix)
.ToArray();
///
/// Creates caller-owned two-dimensional intermediate storage using AV1's eight-tap vertical extent.
///
/// The prediction geometry and fractional phases.
/// The required scratch storage, or an empty array for copy and one-dimensional predictions.
private static short[] CreateScratch(PredictionCase testCase)
=> testCase.HorizontalPhase == 0 || testCase.VerticalPhase == 0
? []
: new short[Math.Max(testCase.Width, 16) * (testCase.Height + FilterTapCount - 1)];
///
/// Applies libaom's single-reference copy or convolution equations to an 8-bit prediction block.
///
/// The complete padded reference plane.
/// The source-row stride.
/// The integer-position sample index.
/// The guarded destination storage.
/// The first active destination index.
/// The destination-row stride.
/// The prediction filters, phases, and geometry.
/// The decoded sample precision.
private static void ApplyReference(
byte[] source,
int sourceStride,
int sourceOrigin,
byte[] destination,
int destinationOrigin,
int destinationStride,
PredictionCase testCase,
int bitDepth)
{
if (testCase.HorizontalPhase == 0 && testCase.VerticalPhase == 0)
{
for (int row = 0; row < testCase.Height; row++)
{
source.AsSpan(sourceOrigin + (row * sourceStride), testCase.Width)
.CopyTo(destination.AsSpan(destinationOrigin + (row * destinationStride), testCase.Width));
}
return;
}
if (testCase.VerticalPhase == 0)
{
ReadOnlySpan horizontal = GetCoefficients(testCase.HorizontalFilter, testCase.HorizontalPhase, testCase.Width <= 4);
int round0 = GetRound0Bits(bitDepth);
for (int row = 0; row < testCase.Height; row++)
{
for (int column = 0; column < testCase.Width; column++)
{
int sourceIndex = sourceOrigin + (row * sourceStride) + column - FilterCenterOffset;
int sum = Convolve(source, sourceIndex, 1, horizontal);
int value = RoundPowerOfTwo(RoundPowerOfTwo(sum, round0), FilterBits - round0);
destination[destinationOrigin + (row * destinationStride) + column] = (byte)Math.Clamp(value, 0, byte.MaxValue);
}
}
return;
}
if (testCase.HorizontalPhase == 0)
{
ReadOnlySpan vertical = GetCoefficients(testCase.VerticalFilter, testCase.VerticalPhase, testCase.Height <= 4);
for (int row = 0; row < testCase.Height; row++)
{
for (int column = 0; column < testCase.Width; column++)
{
int sourceIndex = sourceOrigin + ((row - FilterCenterOffset) * sourceStride) + column;
int sum = Convolve(source, sourceIndex, sourceStride, vertical);
int value = RoundPowerOfTwo(sum, FilterBits);
destination[destinationOrigin + (row * destinationStride) + column] = (byte)Math.Clamp(value, 0, byte.MaxValue);
}
}
return;
}
ReadOnlySpan horizontalCoefficients = GetCoefficients(testCase.HorizontalFilter, testCase.HorizontalPhase, testCase.Width <= 4);
ReadOnlySpan verticalCoefficients = GetCoefficients(testCase.VerticalFilter, testCase.VerticalPhase, testCase.Height <= 4);
ApplyTwoDimensionalReference(
source, sourceStride, sourceOrigin, destination, destinationOrigin, destinationStride, testCase,
horizontalCoefficients, verticalCoefficients, bitDepth);
}
///
/// Applies libaom's single-reference copy or convolution equations to a high-bit-depth prediction block.
///
/// The complete padded reference plane.
/// The source-row stride.
/// The integer-position sample index.
/// The guarded destination storage.
/// The first active destination index.
/// The destination-row stride.
/// The prediction filters, phases, and geometry.
/// The decoded sample precision.
private static void ApplyReference(
ushort[] source,
int sourceStride,
int sourceOrigin,
ushort[] destination,
int destinationOrigin,
int destinationStride,
PredictionCase testCase,
int bitDepth)
{
if (testCase.HorizontalPhase == 0 && testCase.VerticalPhase == 0)
{
for (int row = 0; row < testCase.Height; row++)
{
source.AsSpan(sourceOrigin + (row * sourceStride), testCase.Width)
.CopyTo(destination.AsSpan(destinationOrigin + (row * destinationStride), testCase.Width));
}
return;
}
int maximum = (1 << bitDepth) - 1;
if (testCase.VerticalPhase == 0)
{
ReadOnlySpan horizontal = GetCoefficients(testCase.HorizontalFilter, testCase.HorizontalPhase, testCase.Width <= 4);
int round0 = GetRound0Bits(bitDepth);
for (int row = 0; row < testCase.Height; row++)
{
for (int column = 0; column < testCase.Width; column++)
{
int sourceIndex = sourceOrigin + (row * sourceStride) + column - FilterCenterOffset;
int sum = Convolve(source, sourceIndex, 1, horizontal);
int value = RoundPowerOfTwo(RoundPowerOfTwo(sum, round0), FilterBits - round0);
destination[destinationOrigin + (row * destinationStride) + column] = (ushort)Math.Clamp(value, 0, maximum);
}
}
return;
}
if (testCase.HorizontalPhase == 0)
{
ReadOnlySpan vertical = GetCoefficients(testCase.VerticalFilter, testCase.VerticalPhase, testCase.Height <= 4);
for (int row = 0; row < testCase.Height; row++)
{
for (int column = 0; column < testCase.Width; column++)
{
int sourceIndex = sourceOrigin + ((row - FilterCenterOffset) * sourceStride) + column;
int sum = Convolve(source, sourceIndex, sourceStride, vertical);
int value = RoundPowerOfTwo(sum, FilterBits);
destination[destinationOrigin + (row * destinationStride) + column] = (ushort)Math.Clamp(value, 0, maximum);
}
}
return;
}
ReadOnlySpan horizontalCoefficients = GetCoefficients(testCase.HorizontalFilter, testCase.HorizontalPhase, testCase.Width <= 4);
ReadOnlySpan verticalCoefficients = GetCoefficients(testCase.VerticalFilter, testCase.VerticalPhase, testCase.Height <= 4);
ApplyTwoDimensionalReference(
source, sourceStride, sourceOrigin, destination, destinationOrigin, destinationStride, testCase,
horizontalCoefficients, verticalCoefficients, bitDepth);
}
///
/// Applies libaom's biased two-pass 8-bit convolution and removes both intermediate bias terms after vertical filtering.
///
/// The complete padded reference plane.
/// The source-row stride.
/// The integer-position sample index.
/// The guarded destination storage.
/// The first active destination index.
/// The destination-row stride.
/// The prediction geometry and phases.
/// The horizontal Q7 coefficient row.
/// The vertical Q7 coefficient row.
/// The decoded sample precision.
private static void ApplyTwoDimensionalReference(
byte[] source,
int sourceStride,
int sourceOrigin,
byte[] destination,
int destinationOrigin,
int destinationStride,
PredictionCase testCase,
ReadOnlySpan horizontalCoefficients,
ReadOnlySpan verticalCoefficients,
int bitDepth)
{
short[] intermediate = new short[(testCase.Height + FilterTapCount - 1) * testCase.Width];
int horizontalBias = 1 << (bitDepth + FilterBits - 1);
int round0 = GetRound0Bits(bitDepth);
for (int row = 0; row < testCase.Height + FilterTapCount - 1; row++)
{
for (int column = 0; column < testCase.Width; column++)
{
int sourceIndex = sourceOrigin + ((row - FilterCenterOffset) * sourceStride) + column - FilterCenterOffset;
int sum = horizontalBias + Convolve(source, sourceIndex, 1, horizontalCoefficients);
intermediate[(row * testCase.Width) + column] = (short)RoundPowerOfTwo(sum, round0);
}
}
WriteTwoDimensionalReference(intermediate, destination, destinationOrigin, destinationStride, testCase, verticalCoefficients, bitDepth);
}
///
/// Applies libaom's biased two-pass high-bit-depth convolution and removes both intermediate bias terms after vertical filtering.
///
/// The complete padded reference plane.
/// The source-row stride.
/// The integer-position sample index.
/// The guarded destination storage.
/// The first active destination index.
/// The destination-row stride.
/// The prediction geometry and phases.
/// The horizontal Q7 coefficient row.
/// The vertical Q7 coefficient row.
/// The decoded sample precision.
private static void ApplyTwoDimensionalReference(
ushort[] source,
int sourceStride,
int sourceOrigin,
ushort[] destination,
int destinationOrigin,
int destinationStride,
PredictionCase testCase,
ReadOnlySpan horizontalCoefficients,
ReadOnlySpan verticalCoefficients,
int bitDepth)
{
short[] intermediate = new short[(testCase.Height + FilterTapCount - 1) * testCase.Width];
int horizontalBias = 1 << (bitDepth + FilterBits - 1);
int round0 = GetRound0Bits(bitDepth);
for (int row = 0; row < testCase.Height + FilterTapCount - 1; row++)
{
for (int column = 0; column < testCase.Width; column++)
{
int sourceIndex = sourceOrigin + ((row - FilterCenterOffset) * sourceStride) + column - FilterCenterOffset;
int sum = horizontalBias + Convolve(source, sourceIndex, 1, horizontalCoefficients);
intermediate[(row * testCase.Width) + column] = (short)RoundPowerOfTwo(sum, round0);
}
}
WriteTwoDimensionalReference(intermediate, destination, destinationOrigin, destinationStride, testCase, verticalCoefficients, bitDepth);
}
///
/// Completes an 8-bit two-dimensional prediction from the independently generated biased intermediate block.
///
/// The horizontally filtered signed intermediate block.
/// The guarded destination storage.
/// The first active destination index.
/// The destination-row stride.
/// The prediction geometry.
/// The vertical Q7 coefficient row.
/// The decoded sample precision.
private static void WriteTwoDimensionalReference(
short[] intermediate,
byte[] destination,
int destinationOrigin,
int destinationStride,
PredictionCase testCase,
ReadOnlySpan verticalCoefficients,
int bitDepth)
{
int round0 = GetRound0Bits(bitDepth);
int round1 = (2 * FilterBits) - round0;
int offsetBits = bitDepth + (2 * FilterBits) - round0;
int verticalBias = 1 << offsetBits;
int roundOffset = (1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1));
for (int row = 0; row < testCase.Height; row++)
{
for (int column = 0; column < testCase.Width; column++)
{
int sum = verticalBias + Convolve(intermediate, (row * testCase.Width) + column, testCase.Width, verticalCoefficients);
int value = RoundPowerOfTwo(sum, round1) - roundOffset;
destination[destinationOrigin + (row * destinationStride) + column] = (byte)Math.Clamp(value, 0, byte.MaxValue);
}
}
}
///
/// Completes a high-bit-depth two-dimensional prediction from the independently generated biased intermediate block.
///
/// The horizontally filtered signed intermediate block.
/// The guarded destination storage.
/// The first active destination index.
/// The destination-row stride.
/// The prediction geometry.
/// The vertical Q7 coefficient row.
/// The decoded sample precision.
private static void WriteTwoDimensionalReference(
short[] intermediate,
ushort[] destination,
int destinationOrigin,
int destinationStride,
PredictionCase testCase,
ReadOnlySpan verticalCoefficients,
int bitDepth)
{
int round0 = GetRound0Bits(bitDepth);
int round1 = (2 * FilterBits) - round0;
int offsetBits = bitDepth + (2 * FilterBits) - round0;
int verticalBias = 1 << offsetBits;
int roundOffset = (1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1));
int maximum = (1 << bitDepth) - 1;
for (int row = 0; row < testCase.Height; row++)
{
for (int column = 0; column < testCase.Width; column++)
{
int sum = verticalBias + Convolve(intermediate, (row * testCase.Width) + column, testCase.Width, verticalCoefficients);
int value = RoundPowerOfTwo(sum, round1) - roundOffset;
destination[destinationOrigin + (row * destinationStride) + column] = (ushort)Math.Clamp(value, 0, maximum);
}
}
}
///
/// Computes one eight-tap Q7 convolution from 8-bit samples.
///
/// The complete source storage.
/// The first coefficient's source index.
/// The source-element distance between taps.
/// The eight Q7 coefficients.
/// The unrounded convolution sum.
private static int Convolve(byte[] source, int sourceIndex, int sourceStep, ReadOnlySpan coefficients)
{
int sum = 0;
for (int tap = 0; tap < FilterTapCount; tap++)
{
sum += source[sourceIndex + (tap * sourceStep)] * coefficients[tap];
}
return sum;
}
///
/// Computes one eight-tap Q7 convolution from high-bit-depth samples.
///
/// The complete source storage.
/// The first coefficient's source index.
/// The source-element distance between taps.
/// The eight Q7 coefficients.
/// The unrounded convolution sum.
private static int Convolve(ushort[] source, int sourceIndex, int sourceStep, ReadOnlySpan coefficients)
{
int sum = 0;
for (int tap = 0; tap < FilterTapCount; tap++)
{
sum += source[sourceIndex + (tap * sourceStep)] * coefficients[tap];
}
return sum;
}
///
/// Computes one eight-tap Q7 convolution from signed biased intermediate samples.
///
/// The complete intermediate storage.
/// The first coefficient's source index.
/// The source-element distance between taps.
/// The eight Q7 coefficients.
/// The unrounded convolution sum.
private static int Convolve(short[] source, int sourceIndex, int sourceStep, ReadOnlySpan coefficients)
{
int sum = 0;
for (int tap = 0; tap < FilterTapCount; tap++)
{
sum += source[sourceIndex + (tap * sourceStep)] * coefficients[tap];
}
return sum;
}
///
/// Selects one normative Q7 coefficient row independently of the production filter storage.
///
/// The interpolation-filter family.
/// The one-sixteenth-sample phase.
/// A value indicating whether the dimension is four samples.
/// The eight-position Q7 coefficient row.
private static ReadOnlySpan GetCoefficients(Av1InterpolationFilter filter, int phase, bool useReducedFilter)
{
if (filter == Av1InterpolationFilter.Bilinear)
{
return phase switch
{
3 => BilinearPhase3,
8 => BilinearPhase8,
11 => BilinearPhase11,
15 => BilinearPhase15,
_ => throw new InvalidOperationException($"The test oracle has no bilinear coefficient row for phase {phase}.")
};
}
if (useReducedFilter && filter == Av1InterpolationFilter.Sharp)
{
// AV1 maps sharp filtering on a four-sample dimension to the reduced regular table before convolution.
filter = Av1InterpolationFilter.Regular;
}
return (filter, useReducedFilter, phase) switch
{
(Av1InterpolationFilter.Regular, false, 1) => RegularEightTapPhase1,
(Av1InterpolationFilter.Smooth, false, 7) => SmoothEightTapPhase7,
(Av1InterpolationFilter.Smooth, false, 15) => SmoothEightTapPhase15,
(Av1InterpolationFilter.Sharp, false, 8) => SharpEightTapPhase8,
(Av1InterpolationFilter.Regular, true, 3) => RegularFourTapPhase3,
(Av1InterpolationFilter.Regular, true, 5) => RegularFourTapPhase5,
(Av1InterpolationFilter.Regular, true, 13) => RegularFourTapPhase13,
(Av1InterpolationFilter.Smooth, true, 7) => SmoothFourTapPhase7,
(Av1InterpolationFilter.Smooth, true, 13) => SmoothFourTapPhase13,
_ => throw new InvalidOperationException(
$"The test oracle has no coefficient row for {filter}, phase {phase}, reduced {useReducedFilter}.")
};
}
///
/// Gets the regular eight-tap Q7 kernel for phase 1 from AOM's normative decoder table.
///
private static ReadOnlySpan RegularEightTapPhase1 => [0, 2, -6, 126, 8, -2, 0, 0];
///
/// Gets the smooth eight-tap Q7 kernel for phase 7 from AOM's normative decoder table.
///
private static ReadOnlySpan SmoothEightTapPhase7 => [0, -2, 16, 54, 48, 12, 0, 0];
///
/// Gets the smooth eight-tap Q7 kernel for phase 15 from AOM's normative decoder table.
///
private static ReadOnlySpan SmoothEightTapPhase15 => [0, 0, 2, 34, 62, 28, 2, 0];
///
/// Gets the sharp eight-tap Q7 kernel for phase 8 from AOM's normative decoder table.
///
private static ReadOnlySpan SharpEightTapPhase8 => [-4, 12, -24, 80, 80, -24, 12, -4];
///
/// Gets the bilinear Q7 kernel for phase 3 from AOM's normative decoder table.
///
private static ReadOnlySpan BilinearPhase3 => [0, 0, 0, 104, 24, 0, 0, 0];
///
/// Gets the bilinear Q7 kernel for phase 8 from AOM's normative decoder table.
///
private static ReadOnlySpan BilinearPhase8 => [0, 0, 0, 64, 64, 0, 0, 0];
///
/// Gets the bilinear Q7 kernel for phase 11 from AOM's normative decoder table.
///
private static ReadOnlySpan BilinearPhase11 => [0, 0, 0, 40, 88, 0, 0, 0];
///
/// Gets the bilinear Q7 kernel for phase 15 from AOM's normative decoder table.
///
private static ReadOnlySpan BilinearPhase15 => [0, 0, 0, 8, 120, 0, 0, 0];
///
/// Gets the reduced regular Q7 kernel for phase 3 from AOM's normative decoder table.
///
private static ReadOnlySpan RegularFourTapPhase3 => [0, 0, -10, 116, 28, -6, 0, 0];
///
/// Gets the reduced regular Q7 kernel for phase 5 from AOM's normative decoder table.
///
private static ReadOnlySpan RegularFourTapPhase5 => [0, 0, -12, 102, 48, -10, 0, 0];
///
/// Gets the reduced regular Q7 kernel for phase 13 from AOM's normative decoder table.
///
private static ReadOnlySpan RegularFourTapPhase13 => [0, 0, -6, 28, 116, -10, 0, 0];
///
/// Gets the reduced smooth Q7 kernel for phase 7 from AOM's normative decoder table.
///
private static ReadOnlySpan SmoothFourTapPhase7 => [0, 0, 14, 54, 48, 12, 0, 0];
///
/// Gets the reduced smooth Q7 kernel for phase 13 from AOM's normative decoder table.
///
private static ReadOnlySpan SmoothFourTapPhase13 => [0, 0, 4, 40, 62, 22, 0, 0];
///
/// Gets AOM's first convolution shift while keeping the biased intermediate within sixteen signed bits.
///
/// The decoded sample precision.
/// The first convolution shift.
private static int GetRound0Bits(int bitDepth)
{
int round0 = Round0Bits;
int intermediateBitCount = bitDepth + FilterBits - round0 + 2;
if (intermediateBitCount > 16)
{
round0 += intermediateBitCount - 16;
}
return round0;
}
///
/// Applies AOM's integer power-of-two rounding rule.
///
/// The signed integer to divide.
/// The base-2 divisor exponent.
/// The rounded quotient.
private static int RoundPowerOfTwo(int value, int bits) => (value + (1 << (bits - 1))) >> bits;
///
/// Reports the first differing byte, including guarded padding, for one named prediction path.
///
/// The independently generated destination storage.
/// The production destination storage.
/// The prediction scenario.
/// The production execution path.
private static void AssertEqual(byte[] expected, byte[] actual, PredictionCase testCase, string path)
{
for (int i = 0; i < expected.Length; i++)
{
if (expected[i] != actual[i])
{
Assert.Fail($"{path} prediction '{testCase.Name}' differs at storage index {i}: expected {expected[i]}, actual {actual[i]}.");
}
}
}
///
/// Reports the first differing ushort, including guarded padding, for one named prediction path.
///
/// The independently generated destination storage.
/// The production destination storage.
/// The prediction scenario.
/// The production execution path.
private static void AssertEqual(ushort[] expected, ushort[] actual, PredictionCase testCase, string path)
{
for (int i = 0; i < expected.Length; i++)
{
if (expected[i] != actual[i])
{
Assert.Fail($"{path} prediction '{testCase.Name}' differs at storage index {i}: expected {expected[i]}, actual {actual[i]}.");
}
}
}
///
/// Describes one prediction path, filter pair, phase pair, and block geometry.
///
private readonly struct PredictionCase
{
///
/// Initializes a new instance of the struct.
///
/// The diagnostic scenario name.
/// The active prediction width.
/// The active prediction height.
/// The horizontal interpolation-filter family.
/// The vertical interpolation-filter family.
/// The horizontal one-sixteenth-sample phase.
/// The vertical one-sixteenth-sample phase.
public PredictionCase(
string name,
int width,
int height,
Av1InterpolationFilter horizontalFilter,
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int verticalPhase)
{
this.Name = name;
this.Width = width;
this.Height = height;
this.HorizontalFilter = horizontalFilter;
this.VerticalFilter = verticalFilter;
this.HorizontalPhase = horizontalPhase;
this.VerticalPhase = verticalPhase;
}
///
/// Gets the diagnostic scenario name.
///
public string Name { get; }
///
/// Gets the active prediction width.
///
public int Width { get; }
///
/// Gets the active prediction height.
///
public int Height { get; }
///
/// Gets the horizontal interpolation-filter family.
///
public Av1InterpolationFilter HorizontalFilter { get; }
///
/// Gets the vertical interpolation-filter family.
///
public Av1InterpolationFilter VerticalFilter { get; }
///
/// Gets the horizontal one-sixteenth-sample phase.
///
public int HorizontalPhase { get; }
///
/// Gets the vertical one-sixteenth-sample phase.
///
public int VerticalPhase { get; }
}
}