📷 A modern, cross-platform, 2D Graphics library for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

770 lines
31 KiB

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
/// <summary>
/// Verifies AV1 intra prediction against scalar definitions across the supported hardware-intrinsic configurations.
/// </summary>
[Trait("Format", "Heif")]
public class Av1PredictorTests
{
/// <summary>
/// The offset within directional reference storage that leaves readable samples before both edge origins.
/// </summary>
private const int ReferenceOrigin = 128;
/// <summary>
/// The hardware configurations required to exercise each SIMD tier and the complete scalar fallback.
/// </summary>
private const HwIntrinsics PredictorConfigurations =
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic;
/// <summary>
/// Cardinal, base, and adjusted angles covering every directional projection zone.
/// </summary>
private static ReadOnlySpan<int> DirectionalAngles => [36, 45, 54, 67, 90, 104, 113, 126, 135, 148, 157, 166, 180, 194, 203, 212];
/// <summary>
/// The complete set of AV1 filter-intra coefficient modes.
/// </summary>
private static ReadOnlySpan<Av1FilterIntraMode> FilterIntraModes =>
[
Av1FilterIntraMode.DC,
Av1FilterIntraMode.Vertical,
Av1FilterIntraMode.Horizontal,
Av1FilterIntraMode.Directional157,
Av1FilterIntraMode.Paeth,
];
/// <summary>
/// Verifies DC prediction with each register-width tier and the scalar fallback.
/// </summary>
[Fact]
public void DcPredictorsMatchScalarDefinitionsAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateDcPredictors, PredictorConfigurations);
/// <summary>
/// Verifies horizontal prediction with each register-width tier and the scalar fallback.
/// </summary>
[Fact]
public void HorizontalPredictorMatchesScalarDefinitionAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHorizontalPredictor, PredictorConfigurations);
/// <summary>
/// Verifies vertical prediction with each register-width tier and the scalar fallback.
/// </summary>
[Fact]
public void VerticalPredictorMatchesScalarDefinitionAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateVerticalPredictor, PredictorConfigurations);
/// <summary>
/// Verifies Paeth prediction with each register-width tier and the scalar fallback.
/// </summary>
[Fact]
public void PaethPredictorMatchesScalarDefinitionAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidatePaethPredictor, PredictorConfigurations);
/// <summary>
/// Verifies smooth prediction with each register-width tier and the scalar fallback.
/// </summary>
[Fact]
public void SmoothPredictorMatchesScalarDefinitionAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateSmoothPredictor, PredictorConfigurations);
/// <summary>
/// Verifies horizontal smooth prediction with each register-width tier and the scalar fallback.
/// </summary>
[Fact]
public void SmoothHorizontalPredictorMatchesScalarDefinitionAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateSmoothHorizontalPredictor, PredictorConfigurations);
/// <summary>
/// Verifies vertical smooth prediction with each register-width tier and the scalar fallback.
/// </summary>
[Fact]
public void SmoothVerticalPredictorMatchesScalarDefinitionAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateSmoothVerticalPredictor, PredictorConfigurations);
/// <summary>
/// Verifies directional prediction with each register-width tier and the scalar fallback.
/// </summary>
[Fact]
public void DirectionalPredictorsMatchScalarDefinitionsAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateDirectionalPredictors, PredictorConfigurations);
/// <summary>
/// Verifies filter-intra prediction with each register-width tier and the scalar fallback.
/// </summary>
[Fact]
public void FilterIntraPredictorsMatchScalarDefinitionsAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateFilterIntraPredictors, PredictorConfigurations);
/// <summary>
/// Verifies intra-edge upsampling with Vector128 and the scalar fallback.
/// </summary>
[Fact]
public void EdgeUpsamplingMatchesScalarDefinitionsAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateEdgeUpsampling, HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic);
/// <summary>
/// Verifies intra-edge filtering with Vector128 and the scalar fallback.
/// </summary>
[Fact]
public void EdgeFilteringMatchesScalarDefinitionsAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateEdgeFiltering, HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic);
/// <summary>
/// Verifies all four DC neighbor-availability combinations at every AV1 transform size.
/// </summary>
private static void ValidateDcPredictors()
{
for (int sizeIndex = 0; sizeIndex < (int)Av1TransformSize.AllSizes; sizeIndex++)
{
Av1TransformSize transformSize = (Av1TransformSize)sizeIndex;
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
int stride = width + 5;
byte[] above = CreateByteSamples(width, 17);
byte[] left = CreateByteSamples(height, 43);
short[] aboveHigh = CreateHighBitDepthSamples(width, 17);
short[] leftHigh = CreateHighBitDepthSamples(height, 43);
for (int availability = 0; availability < 4; availability++)
{
bool hasLeft = (availability & 1) != 0;
bool hasAbove = (availability & 2) != 0;
byte[] expected = CreateByteDestination(stride, height);
byte[] actual = CreateByteDestination(stride, height);
short[] expectedHigh = CreateHighBitDepthDestination(stride, height);
short[] actualHigh = CreateHighBitDepthDestination(stride, height);
Av1DcIntraPredictor.PredictScalar(hasLeft, hasAbove, expected, stride, above, left, width, height);
Av1DcIntraPredictor.Predict(hasLeft, hasAbove, actual, stride, above, left, width, height);
Av1DcIntraPredictor.PredictScalar(hasLeft, hasAbove, expectedHigh, stride, aboveHigh, leftHigh, width, height, 12);
Av1DcIntraPredictor.Predict(hasLeft, hasAbove, actualHigh, stride, aboveHigh, leftHigh, width, height, 12);
Assert.Equal(expected, actual);
Assert.Equal(expectedHigh, actualHigh);
}
}
}
/// <summary>
/// Verifies horizontal prediction at every AV1 transform size and sample precision.
/// </summary>
private static void ValidateHorizontalPredictor() => ValidateNonDirectionalPredictor(Av1PredictionMode.Horizontal);
/// <summary>
/// Verifies vertical prediction at every AV1 transform size and sample precision.
/// </summary>
private static void ValidateVerticalPredictor() => ValidateNonDirectionalPredictor(Av1PredictionMode.Vertical);
/// <summary>
/// Verifies Paeth prediction at every AV1 transform size and sample precision.
/// </summary>
private static void ValidatePaethPredictor() => ValidateNonDirectionalPredictor(Av1PredictionMode.Paeth);
/// <summary>
/// Verifies smooth prediction at every AV1 transform size and sample precision.
/// </summary>
private static void ValidateSmoothPredictor() => ValidateNonDirectionalPredictor(Av1PredictionMode.Smooth);
/// <summary>
/// Verifies horizontal smooth prediction at every AV1 transform size and sample precision.
/// </summary>
private static void ValidateSmoothHorizontalPredictor() => ValidateNonDirectionalPredictor(Av1PredictionMode.SmoothHorizontal);
/// <summary>
/// Verifies vertical smooth prediction at every AV1 transform size and sample precision.
/// </summary>
private static void ValidateSmoothVerticalPredictor() => ValidateNonDirectionalPredictor(Av1PredictionMode.SmoothVertical);
/// <summary>
/// Verifies one closed non-directional operator at every AV1 transform size and sample precision.
/// </summary>
/// <param name="mode">The prediction mode to verify.</param>
private static void ValidateNonDirectionalPredictor(Av1PredictionMode mode)
{
Av1IntraPredictorBase predictor = Av1IntraPredictorBase.GetPredictor(mode);
for (int sizeIndex = 0; sizeIndex < (int)Av1TransformSize.AllSizes; sizeIndex++)
{
Av1TransformSize transformSize = (Av1TransformSize)sizeIndex;
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
int stride = width + 5;
byte[] aboveStorage = CreateByteSamples(width + 1, 19);
byte[] left = CreateByteSamples(height, 71);
short[] aboveHighStorage = CreateHighBitDepthSamples(width + 1, 19);
short[] leftHigh = CreateHighBitDepthSamples(height, 71);
byte[] expected = CreateByteDestination(stride, height);
byte[] actual = CreateByteDestination(stride, height);
short[] expectedHigh = CreateHighBitDepthDestination(stride, height);
short[] actualHigh = CreateHighBitDepthDestination(stride, height);
predictor.PredictScalar(expected, stride, aboveStorage.AsSpan(1), left, width, height);
predictor.Predict(actual, stride, aboveStorage.AsSpan(1), left, width, height);
predictor.PredictScalar(expectedHigh, stride, aboveHighStorage.AsSpan(1), leftHigh, width, height);
predictor.Predict(actualHigh, stride, aboveHighStorage.AsSpan(1), leftHigh, width, height);
Assert.Equal(expected, actual);
Assert.Equal(expectedHigh, actualHigh);
}
ValidateKnownNonDirectionalVector(mode, predictor);
}
/// <summary>
/// Verifies one non-directional operator against a byte-exact reference block and its translated high-bit-depth equivalent.
/// </summary>
/// <param name="mode">The prediction mode being verified.</param>
/// <param name="predictor">The closed operator-driven predictor.</param>
private static void ValidateKnownNonDirectionalVector(Av1PredictionMode mode, Av1IntraPredictorBase predictor)
{
byte[] aboveStorage;
byte[] left;
byte[] expected;
if (mode == Av1PredictionMode.Paeth)
{
aboveStorage = [50, 60, 10, 90, 40];
left = [20, 80, 30, 100];
expected =
[
20, 10, 50, 20,
80, 50, 90, 80,
30, 10, 90, 30,
100, 50, 100, 100,
];
}
else
{
aboveStorage = [0, 20, 40, 60, 80];
left = [20, 40, 60, 80];
expected = mode switch
{
Av1PredictionMode.Horizontal =>
[
20, 20, 20, 20,
40, 40, 40, 40,
60, 60, 60, 60,
80, 80, 80, 80,
],
Av1PredictionMode.Vertical =>
[
20, 40, 60, 80,
20, 40, 60, 80,
20, 40, 60, 80,
20, 40, 60, 80,
],
Av1PredictionMode.Smooth =>
[
20, 43, 60, 73,
43, 57, 68, 75,
60, 68, 73, 78,
73, 75, 78, 80,
],
Av1PredictionMode.SmoothHorizontal =>
[
20, 45, 60, 65,
40, 57, 67, 70,
60, 68, 73, 75,
80, 80, 80, 80,
],
_ =>
[
20, 40, 60, 80,
45, 57, 68, 80,
60, 67, 73, 80,
65, 70, 75, 80,
],
};
}
byte[] actual = new byte[16];
predictor.Predict(actual, 4, aboveStorage.AsSpan(1), left, 4, 4);
Assert.Equal(expected, actual);
const int offset = 512;
short[] aboveHighStorage = new short[aboveStorage.Length];
short[] leftHigh = new short[left.Length];
short[] expectedHigh = new short[expected.Length];
short[] actualHigh = new short[16];
for (int i = 0; i < aboveStorage.Length; i++)
{
aboveHighStorage[i] = (short)(aboveStorage[i] + offset);
}
for (int i = 0; i < left.Length; i++)
{
leftHigh[i] = (short)(left[i] + offset);
}
for (int i = 0; i < expected.Length; i++)
{
expectedHigh[i] = (short)(expected[i] + offset);
}
predictor.Predict(actualHigh, 4, aboveHighStorage.AsSpan(1), leftHigh, 4, 4);
Assert.Equal(expectedHigh, actualHigh);
}
/// <summary>
/// Verifies every directional zone, rectangular transpose, and edge-upsampling index rule.
/// </summary>
private static void ValidateDirectionalPredictors()
{
byte[] aboveStorage = CreateByteSamples(512, 23);
byte[] leftStorage = CreateByteSamples(512, 89);
short[] aboveHighStorage = CreateHighBitDepthSamples(512, 23);
short[] leftHighStorage = CreateHighBitDepthSamples(512, 89);
ReadOnlySpan<byte> above = aboveStorage.AsSpan(ReferenceOrigin);
ReadOnlySpan<byte> left = leftStorage.AsSpan(ReferenceOrigin);
ReadOnlySpan<short> aboveHigh = aboveHighStorage.AsSpan(ReferenceOrigin);
ReadOnlySpan<short> leftHigh = leftHighStorage.AsSpan(ReferenceOrigin);
foreach (int angle in DirectionalAngles)
{
for (int sizeIndex = 0; sizeIndex < (int)Av1TransformSize.AllSizes; sizeIndex++)
{
ValidateDirectionalCase((Av1TransformSize)sizeIndex, angle, false, false, above, left, aboveHigh, leftHigh);
}
}
// Edge upsampling is permitted only for small blocks. These cases exercise top-only, both-edge,
// and left-only indexing without asking an invalid large transform to consume an upsampled edge.
ValidateDirectionalCase(Av1TransformSize.Size4x4, 45, true, false, above, left, aboveHigh, leftHigh);
ValidateDirectionalCase(Av1TransformSize.Size4x4, 135, true, true, above, left, aboveHigh, leftHigh);
ValidateDirectionalCase(Av1TransformSize.Size4x4, 203, false, true, above, left, aboveHigh, leftHigh);
ValidateKnownDirectionalVectors();
}
/// <summary>
/// Verifies all three projection zones against byte-exact reference blocks.
/// </summary>
private static void ValidateKnownDirectionalVectors()
{
ValidateKnownDirectionalVector(
45,
[0, 10, 20, 30, 40, 50, 60, 70, 80, 90],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[
20, 30, 40, 50,
30, 40, 50, 60,
40, 50, 60, 70,
50, 60, 70, 80,
]);
ValidateKnownDirectionalVector(
135,
[5, 10, 20, 30, 40, 50, 60, 70, 80, 90],
[5, 50, 60, 70, 80, 90, 100, 110, 120, 130],
[
5, 10, 20, 30,
50, 5, 10, 20,
60, 50, 5, 10,
70, 60, 50, 5,
]);
ValidateKnownDirectionalVector(
203,
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 10, 20, 30, 40, 50, 60, 70, 80, 90],
[
14, 18, 23, 27,
24, 28, 33, 37,
34, 38, 43, 47,
44, 48, 53, 57,
]);
}
/// <summary>
/// Verifies one directional projection and its translated high-bit-depth equivalent.
/// </summary>
/// <param name="angle">The adjusted directional angle.</param>
/// <param name="aboveStorage">The top-left prefix followed by the top reference.</param>
/// <param name="leftStorage">The top-left prefix followed by the left reference.</param>
/// <param name="expected">The byte-exact predicted block.</param>
private static void ValidateKnownDirectionalVector(
int angle,
ReadOnlySpan<byte> aboveStorage,
ReadOnlySpan<byte> leftStorage,
ReadOnlySpan<byte> expected)
{
ReadOnlySpan<byte> above = aboveStorage[1..];
ReadOnlySpan<byte> left = leftStorage[1..];
byte[] actual = new byte[16];
byte[] scratch = new byte[Av1DirectionalIntraPredictor.ScratchLength];
Av1DirectionalIntraPredictor.Predict(actual, 4, Av1TransformSize.Size4x4, above, left, false, false, angle, scratch);
Assert.Equal(expected, actual);
const int offset = 512;
short[] aboveHighStorage = new short[aboveStorage.Length];
short[] leftHighStorage = new short[leftStorage.Length];
short[] expectedHigh = new short[expected.Length];
short[] actualHigh = new short[16];
short[] scratchHigh = new short[Av1DirectionalIntraPredictor.ScratchLength];
for (int i = 0; i < aboveStorage.Length; i++)
{
aboveHighStorage[i] = (short)(aboveStorage[i] + offset);
}
for (int i = 0; i < leftStorage.Length; i++)
{
leftHighStorage[i] = (short)(leftStorage[i] + offset);
}
for (int i = 0; i < expected.Length; i++)
{
expectedHigh[i] = (short)(expected[i] + offset);
}
Av1DirectionalIntraPredictor.Predict(
actualHigh,
4,
Av1TransformSize.Size4x4,
aboveHighStorage.AsSpan(1),
leftHighStorage.AsSpan(1),
false,
false,
angle,
scratchHigh);
Assert.Equal(expectedHigh, actualHigh);
}
/// <summary>
/// Verifies one directional prediction configuration for both native sample representations.
/// </summary>
private static void ValidateDirectionalCase(
Av1TransformSize transformSize,
int angle,
bool upsampleAbove,
bool upsampleLeft,
ReadOnlySpan<byte> above,
ReadOnlySpan<byte> left,
ReadOnlySpan<short> aboveHigh,
ReadOnlySpan<short> leftHigh)
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
int stride = width + 5;
byte[] expected = CreateByteDestination(stride, height);
byte[] actual = CreateByteDestination(stride, height);
short[] expectedHigh = CreateHighBitDepthDestination(stride, height);
short[] actualHigh = CreateHighBitDepthDestination(stride, height);
byte[] scratch = new byte[Av1DirectionalIntraPredictor.ScratchLength];
short[] scratchHigh = new short[Av1DirectionalIntraPredictor.ScratchLength];
Av1DirectionalIntraPredictor.PredictScalar(expected, stride, transformSize, above, left, upsampleAbove, upsampleLeft, angle);
Av1DirectionalIntraPredictor.Predict(actual, stride, transformSize, above, left, upsampleAbove, upsampleLeft, angle, scratch);
Av1DirectionalIntraPredictor.PredictScalar(expectedHigh, stride, transformSize, aboveHigh, leftHigh, upsampleAbove, upsampleLeft, angle);
Av1DirectionalIntraPredictor.Predict(actualHigh, stride, transformSize, aboveHigh, leftHigh, upsampleAbove, upsampleLeft, angle, scratchHigh);
Assert.Equal(expected, actual);
Assert.Equal(expectedHigh, actualHigh);
}
/// <summary>
/// Verifies every filter-intra operator at each transform size permitted by the AV1 syntax.
/// </summary>
private static void ValidateFilterIntraPredictors()
{
foreach (Av1FilterIntraMode mode in FilterIntraModes)
{
Av1FilterIntraPredictorBase predictor = Av1FilterIntraPredictorBase.GetPredictor(mode);
for (int sizeIndex = 0; sizeIndex < (int)Av1TransformSize.AllSizes; sizeIndex++)
{
Av1TransformSize transformSize = (Av1TransformSize)sizeIndex;
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
if (width > 32 || height > 32)
{
continue;
}
int stride = width + 5;
byte[] aboveStorage = CreateByteSamples(width + 1, 29);
byte[] left = CreateByteSamples(height, 97);
short[] aboveHighStorage = CreateHighBitDepthSamples(width + 1, 29);
short[] leftHigh = CreateHighBitDepthSamples(height, 97);
byte[] expected = CreateByteDestination(stride, height);
byte[] actual = CreateByteDestination(stride, height);
short[] expectedHigh = CreateHighBitDepthDestination(stride, height);
short[] actualHigh = CreateHighBitDepthDestination(stride, height);
byte[] expectedScratch = new byte[Av1FilterIntraPredictorBase.ScratchLength];
byte[] actualScratch = new byte[Av1FilterIntraPredictorBase.ScratchLength];
short[] expectedHighScratch = new short[Av1FilterIntraPredictorBase.ScratchLength];
short[] actualHighScratch = new short[Av1FilterIntraPredictorBase.ScratchLength];
predictor.PredictScalar(expected, stride, aboveStorage.AsSpan(1), left, width, height, expectedScratch);
predictor.Predict(actual, stride, aboveStorage.AsSpan(1), left, width, height, actualScratch);
predictor.PredictScalar(expectedHigh, stride, aboveHighStorage.AsSpan(1), leftHigh, width, height, 12, expectedHighScratch);
predictor.Predict(actualHigh, stride, aboveHighStorage.AsSpan(1), leftHigh, width, height, 12, actualHighScratch);
Assert.Equal(expected, actual);
Assert.Equal(expectedHigh, actualHigh);
}
}
ValidateKnownFilterIntraVectors();
}
/// <summary>
/// Retains byte-exact libaom vectors so scalar and SIMD code cannot share the same mistranslation unnoticed.
/// </summary>
private static void ValidateKnownFilterIntraVectors()
{
byte[][] expectedByMode =
[
[42, 65, 89, 123, 72, 77, 91, 110, 105, 100, 104, 112, 142, 128, 123, 124],
[44, 79, 116, 153, 69, 94, 126, 158, 94, 109, 136, 163, 119, 124, 146, 168],
[47, 67, 87, 107, 83, 93, 103, 113, 122, 127, 132, 137, 161, 163, 166, 168],
[38, 55, 81, 111, 64, 62, 73, 92, 97, 83, 81, 86, 134, 113, 103, 100],
[49, 81, 114, 148, 82, 105, 132, 159, 117, 132, 153, 174, 152, 159, 177, 190],
];
// These edge values are the input to the five reference vectors above. The leading top value is the
// shared top-left sample addressed through above[-1] by the normative recursive filter process.
byte[] aboveStorage = [17, 30, 70, 110, 150];
byte[] left = [40, 80, 120, 160];
short[] aboveHighStorage = [529, 542, 582, 622, 662];
short[] leftHigh = [552, 592, 632, 672];
for (int modeIndex = 0; modeIndex < FilterIntraModes.Length; modeIndex++)
{
byte[] actual = new byte[16];
byte[] scratch = new byte[Av1FilterIntraPredictorBase.ScratchLength];
short[] actualHigh = new short[16];
short[] expectedHigh = new short[16];
short[] scratchHigh = new short[Av1FilterIntraPredictorBase.ScratchLength];
Av1FilterIntraPredictorBase predictor = Av1FilterIntraPredictorBase.GetPredictor(FilterIntraModes[modeIndex]);
predictor.Predict(actual, 4, aboveStorage.AsSpan(1), left, 4, 4, scratch);
for (int i = 0; i < expectedHigh.Length; i++)
{
expectedHigh[i] = (short)(expectedByMode[modeIndex][i] + 512);
}
predictor.Predict(actualHigh, 4, aboveHighStorage.AsSpan(1), leftHigh, 4, 4, 10, scratchHigh);
Assert.Equal(expectedByMode[modeIndex], actual);
Assert.Equal(expectedHigh, actualHigh);
}
}
/// <summary>
/// Verifies vector interleaving, endpoint extension, clamping, and scalar tails in edge upsampling.
/// </summary>
private static void ValidateEdgeUpsampling()
{
ReadOnlySpan<int> counts = [4, 8, 12, 16];
foreach (int count in counts)
{
byte[] actual = CreateUpsampleByteEdge(count);
byte[] expected = (byte[])actual.Clone();
byte[] scratch = new byte[160];
UpsampleEdgeScalar(expected, count, 8);
Av1PredictionDecoder.UpsampleIntraEdge(actual.AsSpan(2), count, scratch);
Assert.Equal(expected, actual);
ReadOnlySpan<int> bitDepths = [10, 12];
foreach (int bitDepth in bitDepths)
{
short[] actualHigh = CreateUpsampleHighBitDepthEdge(count, bitDepth);
short[] expectedHigh = (short[])actualHigh.Clone();
short[] scratchHigh = new short[160];
UpsampleEdgeScalar(expectedHigh, count, bitDepth);
Av1PredictionDecoder.UpsampleIntraEdge(actualHigh.AsSpan(2), count, bitDepth, scratchHigh);
Assert.Equal(expectedHigh, actualHigh);
}
}
}
/// <summary>
/// Verifies all three edge-filter kernels across vector boundaries and the maximum normative edge length.
/// </summary>
private static void ValidateEdgeFiltering()
{
ReadOnlySpan<int> counts = [4, 8, 9, 16, 31, 64, 129];
foreach (int count in counts)
{
for (int strength = 1; strength <= 3; strength++)
{
byte[] actual = CreateByteSamples(count, 31);
byte[] expected = (byte[])actual.Clone();
byte[] source = (byte[])actual.Clone();
byte[] scratch = new byte[160];
FilterEdgeScalar(source, expected, strength);
Av1PredictionDecoder.FilterIntraEdge(ref actual[0], count, strength, scratch);
Assert.Equal(expected, actual);
short[] actualHigh = CreateHighBitDepthSamples(count, 31);
short[] expectedHigh = (short[])actualHigh.Clone();
short[] sourceHigh = (short[])actualHigh.Clone();
short[] scratchHigh = new short[160];
FilterEdgeScalar(sourceHigh, expectedHigh, strength);
Av1PredictionDecoder.FilterIntraEdge(ref actualHigh[0], count, strength, scratchHigh);
Assert.Equal(expectedHigh, actualHigh);
}
}
}
/// <summary>
/// Creates deterministic 8-bit samples with enough variation to expose lane-order mistakes.
/// </summary>
private static byte[] CreateByteSamples(int length, int seed)
{
byte[] samples = new byte[length];
for (int i = 0; i < samples.Length; i++)
{
samples[i] = (byte)(((i * 73) + (seed * 29) + ((i * i) * 7)) & 255);
}
return samples;
}
/// <summary>
/// Creates deterministic 12-bit samples with values spanning the full reconstructed range.
/// </summary>
private static short[] CreateHighBitDepthSamples(int length, int seed)
{
short[] samples = new short[length];
for (int i = 0; i < samples.Length; i++)
{
samples[i] = (short)(((i * 977) + (seed * 131) + ((i * i) * 37)) & 4095);
}
return samples;
}
/// <summary>
/// Creates a strided byte destination initialized with a padding sentinel.
/// </summary>
private static byte[] CreateByteDestination(int stride, int height)
{
byte[] destination = new byte[stride * height];
Array.Fill(destination, (byte)0xCD);
return destination;
}
/// <summary>
/// Creates a strided high-bit-depth destination initialized with a padding sentinel.
/// </summary>
private static short[] CreateHighBitDepthDestination(int stride, int height)
{
short[] destination = new short[stride * height];
Array.Fill(destination, (short)-1234);
return destination;
}
/// <summary>
/// Creates an 8-bit edge with two prefix samples and room for all interleaved outputs.
/// </summary>
private static byte[] CreateUpsampleByteEdge(int count)
{
byte[] edge = new byte[(2 * count) + 4];
Array.Fill(edge, (byte)0xA5);
edge[1] = 231;
for (int i = 0; i < count; i++)
{
edge[i + 2] = (byte)(((i * 97) + 41) & 255);
}
return edge;
}
/// <summary>
/// Creates a high-bit-depth edge containing extrema that exercise interpolation clamping.
/// </summary>
private static short[] CreateUpsampleHighBitDepthEdge(int count, int bitDepth)
{
int maximum = (1 << bitDepth) - 1;
short[] edge = new short[(2 * count) + 4];
Array.Fill(edge, (short)-1);
edge[1] = (short)maximum;
for (int i = 0; i < count; i++)
{
edge[i + 2] = (short)((i & 1) == 0 ? 0 : maximum);
}
return edge;
}
/// <summary>
/// Applies the normative four-tap upsampling formula to an edge stored at index two.
/// </summary>
private static void UpsampleEdgeScalar<T>(T[] edge, int count, int bitDepth)
where T : unmanaged, IBinaryInteger<T>
{
T[] input = new T[count + 3];
input[0] = edge[1];
input[1] = edge[1];
for (int i = 0; i < count; i++)
{
input[i + 2] = edge[i + 2];
}
input[count + 2] = input[count + 1];
edge[0] = input[0];
int maximum = (1 << bitDepth) - 1;
for (int i = 0; i < count; i++)
{
int value = -int.CreateChecked(input[i])
+ (9 * int.CreateChecked(input[i + 1]))
+ (9 * int.CreateChecked(input[i + 2]))
- int.CreateChecked(input[i + 3]);
edge[(2 * i) + 1] = T.CreateChecked(Math.Clamp((value + 8) >> 4, 0, maximum));
edge[(2 * i) + 2] = input[i + 2];
}
}
/// <summary>
/// Applies the normative AV1 edge-filter definition to an independent source copy.
/// </summary>
private static void FilterEdgeScalar<T>(T[] source, T[] destination, int strength)
where T : unmanaged, IBinaryInteger<T>
{
ReadOnlySpan<int> kernel = strength switch
{
1 => [0, 4, 8, 4, 0],
2 => [0, 5, 6, 5, 0],
_ => [2, 4, 4, 4, 2],
};
for (int i = 1; i < source.Length; i++)
{
int sum = 0;
for (int tap = 0; tap < kernel.Length; tap++)
{
int sourceIndex = Math.Clamp(i - 2 + tap, 0, source.Length - 1);
sum += int.CreateChecked(source[sourceIndex]) * kernel[tap];
}
destination[i] = T.CreateChecked((sum + 8) >> 4);
}
}
}