// 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; } } }