// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Numerics; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; /// /// Verifies affine warped-motion prediction through native SIMD and scalar execution. /// [Trait("Format", "Avif")] public class Av1WarpedInterPredictorTests { /// /// The hardware configurations covering every descending SIMD width and the scalar fallback. /// private const HwIntrinsics PredictorConfigurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; /// /// Verifies exact 8-bit native and compound prediction against independent scalar equations. /// [Fact] public void BytePredictionMatchesReference() => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateBytePrediction, PredictorConfigurations); /// /// Verifies exact 8-, 10-, and 12-bit native and compound prediction against independent scalar equations. /// [Fact] public void HighBitDepthPredictionMatchesReference() => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthPrediction, PredictorConfigurations); /// /// Applies the reference multi-sample affine model to deterministic byte storage. /// private static void ValidateBytePrediction() { const int activeSize = 96; const int padding = 24; const int sourceStride = activeSize + (2 * padding); const int width = 13; const int height = 11; const int destinationStride = width + 7; byte[] source = new byte[sourceStride * sourceStride]; for (int row = 0; row < sourceStride; row++) { for (int column = 0; column < sourceStride; column++) { source[(row * sourceStride) + column] = (byte)(((row * 29) + (column * 47) + (row * column * 3)) & byte.MaxValue); } } Av1GlobalMotionParameters parameters = CreateReferenceParameters(); for (int subsampling = 0; subsampling <= 1; subsampling++) { byte[] expected = new byte[destinationStride * height]; byte[] actual = new byte[destinationStride * height]; Array.Fill(expected, (byte)0xD3); Array.Fill(actual, (byte)0xD3); short[] actualScratch = new short[Av1WarpedInterPredictor.WarpedScratchLength]; Point destinationPosition = subsampling == 0 ? new Point(32, 24) : new Point(16, 12); PredictReference( source, sourceStride, new Point(padding, padding), activeSize, activeSize, expected, destinationStride, destinationPosition, width, height, subsampling, subsampling, 8, parameters, compound: false); Av1WarpedInterPredictor.PredictWarped( source, sourceStride, new Point(padding, padding), activeSize, activeSize, actual, destinationStride, destinationPosition, width, height, subsampling, subsampling, parameters, actualScratch); Assert.Equal(expected, actual); ushort[] expectedCompound = new ushort[destinationStride * height]; ushort[] actualCompound = new ushort[destinationStride * height]; Array.Fill(expectedCompound, (ushort)0xDEAD); Array.Fill(actualCompound, (ushort)0xDEAD); PredictReference( source, sourceStride, new Point(padding, padding), activeSize, activeSize, expectedCompound, destinationStride, destinationPosition, width, height, subsampling, subsampling, 8, parameters, compound: true); Av1WarpedInterPredictor.PredictWarpedCompound( source, sourceStride, new Point(padding, padding), activeSize, activeSize, actualCompound, destinationStride, destinationPosition, width, height, subsampling, subsampling, parameters, actualScratch); Assert.Equal(expectedCompound, actualCompound); } } /// /// Applies the multi-sample affine model to every supported high-bit-depth precision. /// private static void ValidateHighBitDepthPrediction() { const int activeSize = 96; const int padding = 24; const int sourceStride = activeSize + (2 * padding); const int width = 13; const int height = 11; const int destinationStride = width + 7; Av1GlobalMotionParameters parameters = CreateReferenceParameters(); foreach (int bitDepth in new[] { 8, 10, 12 }) { int maximum = (1 << bitDepth) - 1; ushort[] source = new ushort[sourceStride * sourceStride]; for (int row = 0; row < sourceStride; row++) { for (int column = 0; column < sourceStride; column++) { source[(row * sourceStride) + column] = (ushort)(((row * 269) + (column * 443) + (row * column * 31)) & maximum); } } for (int subsampling = 0; subsampling <= 1; subsampling++) { ushort[] expected = new ushort[destinationStride * height]; ushort[] actual = new ushort[destinationStride * height]; Array.Fill(expected, (ushort)0xDEAD); Array.Fill(actual, (ushort)0xDEAD); short[] actualScratch = new short[Av1WarpedInterPredictor.WarpedScratchLength]; Point destinationPosition = subsampling == 0 ? new Point(32, 24) : new Point(16, 12); PredictReference( source, sourceStride, new Point(padding, padding), activeSize, activeSize, expected, destinationStride, destinationPosition, width, height, subsampling, subsampling, bitDepth, parameters, compound: false); Av1WarpedInterPredictor.PredictWarped( source, sourceStride, new Point(padding, padding), activeSize, activeSize, actual, destinationStride, destinationPosition, width, height, subsampling, subsampling, bitDepth, parameters, actualScratch); Assert.Equal(expected, actual); ushort[] expectedCompound = new ushort[destinationStride * height]; ushort[] actualCompound = new ushort[destinationStride * height]; Array.Fill(expectedCompound, (ushort)0xDEAD); Array.Fill(actualCompound, (ushort)0xDEAD); PredictReference( source, sourceStride, new Point(padding, padding), activeSize, activeSize, expectedCompound, destinationStride, destinationPosition, width, height, subsampling, subsampling, bitDepth, parameters, compound: true); Av1WarpedInterPredictor.PredictWarpedCompound( source, sourceStride, new Point(padding, padding), activeSize, activeSize, actualCompound, destinationStride, destinationPosition, width, height, subsampling, subsampling, bitDepth, parameters, actualScratch); Assert.Equal(expectedCompound, actualCompound); } } } /// /// Reconstructs one warped block with scalar affine loops. /// private static void PredictReference( ReadOnlySpan source, int sourceStride, Point sourceOrigin, int sourceWidth, int sourceHeight, Span destination, int destinationStride, Point destinationPosition, int width, int height, int subsamplingX, int subsamplingY, int bitDepth, Av1GlobalMotionParameters parameters, bool compound) where TSource : unmanaged, IBinaryInteger where TDestination : unmanaged, IBinaryInteger { const int filterBits = 7; const int filterTaps = 8; const int tileSize = 8; const int modelPrecisionBits = 16; const int phasePrecisionBits = 10; const int pixelPrecisionShifts = 64; Span intermediate = stackalloc int[15 * tileSize]; // The reference decoder raises round0 by two for 12-bit sources so the biased horizontal intermediate // remains representable in 16 bits, then removes those two bits from the vertical rounding. int intermediateRange = bitDepth + filterBits - 3 + 2; int round0 = 3 + Math.Max(intermediateRange - 16, 0); int verticalRound = compound ? 7 : (2 * filterBits) - round0; int horizontalBias = 1 << (bitDepth + filterBits - 1); int verticalBias = 1 << (bitDepth + (2 * filterBits) - round0); int maximum = (1 << bitDepth) - 1; for (int tileRow = destinationPosition.Y; tileRow < destinationPosition.Y + height; tileRow += tileSize) { for (int tileColumn = destinationPosition.X; tileColumn < destinationPosition.X + width; tileColumn += tileSize) { int centerX = (tileColumn + 4) << subsamplingX; int centerY = (tileRow + 4) << subsamplingY; long projectedX = ((long)parameters[2] * centerX) + ((long)parameters[3] * centerY) + parameters[0]; long projectedY = ((long)parameters[4] * centerX) + ((long)parameters[5] * centerY) + parameters[1]; long planeX = projectedX >> subsamplingX; long planeY = projectedY >> subsamplingY; int integerX = (int)(planeX >> modelPrecisionBits); int integerY = (int)(planeY >> modelPrecisionBits); int phaseX = (int)planeX & ((1 << modelPrecisionBits) - 1); int phaseY = (int)planeY & ((1 << modelPrecisionBits) - 1); phaseX += (-4 * parameters.Alpha) + (-4 * parameters.Beta); phaseY += (-4 * parameters.Gamma) + (-4 * parameters.Delta); phaseX &= -1 << 6; phaseY &= -1 << 6; for (int row = -7; row < 8; row++) { int sourceY = Math.Clamp(integerY + row, 0, sourceHeight - 1); int phase = phaseX + (parameters.Beta * (row + 4)); for (int column = -4; column < 4; column++) { int sourceX = integerX + column - 3; int filterIndex = RoundPowerOfTwo(phase, phasePrecisionBits) + pixelPrecisionShifts; int coefficientOffset = filterIndex * filterTaps; int sum = horizontalBias; for (int tap = 0; tap < filterTaps; tap++) { int sampleX = Math.Clamp(sourceX + tap, 0, sourceWidth - 1); int sourceIndex = ((sourceOrigin.Y + sourceY) * sourceStride) + sourceOrigin.X + sampleX; int sample = int.CreateChecked(source[sourceIndex]); sum += sample * ReferenceWarpedFilter[coefficientOffset + tap]; } intermediate[((row + 7) * tileSize) + column + 4] = RoundPowerOfTwo(sum, round0); phase += parameters.Alpha; } } int lastRow = Math.Min(4, destinationPosition.Y + height - tileRow - 4); for (int row = -4; row < lastRow; row++) { int phase = phaseY + (parameters.Delta * (row + 4)); int lastColumn = Math.Min(4, destinationPosition.X + width - tileColumn - 4); for (int column = -4; column < lastColumn; column++) { int filterIndex = RoundPowerOfTwo(phase, phasePrecisionBits) + pixelPrecisionShifts; int coefficientOffset = filterIndex * filterTaps; int sum = verticalBias; for (int tap = 0; tap < filterTaps; tap++) { int intermediateIndex = ((row + tap + 4) * tileSize) + column + 4; sum += intermediate[intermediateIndex] * ReferenceWarpedFilter[coefficientOffset + tap]; } int value = RoundPowerOfTwo(sum, verticalRound); if (!compound) { value -= (1 << (bitDepth - 1)) + (1 << bitDepth); value = Math.Clamp(value, 0, maximum); } int destinationIndex = ((tileRow - destinationPosition.Y + row + 4) * destinationStride) + tileColumn - destinationPosition.X + column + 4; destination[destinationIndex] = TDestination.CreateChecked(value); phase += parameters.Gamma; } } } } } /// /// Gets the 193 reference eight-tap warped-filter phases used by the independent scalar oracle. /// private static ReadOnlySpan ReferenceWarpedFilter => [ 0, 0, 127, 1, 0, 0, 0, 0, 0, -1, 127, 2, 0, 0, 0, 0, 1, -3, 127, 4, -1, 0, 0, 0, 1, -4, 126, 6, -2, 1, 0, 0, 1, -5, 126, 8, -3, 1, 0, 0, 1, -6, 125, 11, -4, 1, 0, 0, 1, -7, 124, 13, -4, 1, 0, 0, 2, -8, 123, 15, -5, 1, 0, 0, 2, -9, 122, 18, -6, 1, 0, 0, 2, -10, 121, 20, -6, 1, 0, 0, 2, -11, 120, 22, -7, 2, 0, 0, 2, -12, 119, 25, -8, 2, 0, 0, 3, -13, 117, 27, -8, 2, 0, 0, 3, -13, 116, 29, -9, 2, 0, 0, 3, -14, 114, 32, -10, 3, 0, 0, 3, -15, 113, 35, -10, 2, 0, 0, 3, -15, 111, 37, -11, 3, 0, 0, 3, -16, 109, 40, -11, 3, 0, 0, 3, -16, 108, 42, -12, 3, 0, 0, 4, -17, 106, 45, -13, 3, 0, 0, 4, -17, 104, 47, -13, 3, 0, 0, 4, -17, 102, 50, -14, 3, 0, 0, 4, -17, 100, 52, -14, 3, 0, 0, 4, -18, 98, 55, -15, 4, 0, 0, 4, -18, 96, 58, -15, 3, 0, 0, 4, -18, 94, 60, -16, 4, 0, 0, 4, -18, 91, 63, -16, 4, 0, 0, 4, -18, 89, 65, -16, 4, 0, 0, 4, -18, 87, 68, -17, 4, 0, 0, 4, -18, 85, 70, -17, 4, 0, 0, 4, -18, 82, 73, -17, 4, 0, 0, 4, -18, 80, 75, -17, 4, 0, 0, 4, -18, 78, 78, -18, 4, 0, 0, 4, -17, 75, 80, -18, 4, 0, 0, 4, -17, 73, 82, -18, 4, 0, 0, 4, -17, 70, 85, -18, 4, 0, 0, 4, -17, 68, 87, -18, 4, 0, 0, 4, -16, 65, 89, -18, 4, 0, 0, 4, -16, 63, 91, -18, 4, 0, 0, 4, -16, 60, 94, -18, 4, 0, 0, 3, -15, 58, 96, -18, 4, 0, 0, 4, -15, 55, 98, -18, 4, 0, 0, 3, -14, 52, 100, -17, 4, 0, 0, 3, -14, 50, 102, -17, 4, 0, 0, 3, -13, 47, 104, -17, 4, 0, 0, 3, -13, 45, 106, -17, 4, 0, 0, 3, -12, 42, 108, -16, 3, 0, 0, 3, -11, 40, 109, -16, 3, 0, 0, 3, -11, 37, 111, -15, 3, 0, 0, 2, -10, 35, 113, -15, 3, 0, 0, 3, -10, 32, 114, -14, 3, 0, 0, 2, -9, 29, 116, -13, 3, 0, 0, 2, -8, 27, 117, -13, 3, 0, 0, 2, -8, 25, 119, -12, 2, 0, 0, 2, -7, 22, 120, -11, 2, 0, 0, 1, -6, 20, 121, -10, 2, 0, 0, 1, -6, 18, 122, -9, 2, 0, 0, 1, -5, 15, 123, -8, 2, 0, 0, 1, -4, 13, 124, -7, 1, 0, 0, 1, -4, 11, 125, -6, 1, 0, 0, 1, -3, 8, 126, -5, 1, 0, 0, 1, -2, 6, 126, -4, 1, 0, 0, 0, -1, 4, 127, -3, 1, 0, 0, 0, 0, 2, 127, -1, 0, 0, 0, 0, 0, 0, 127, 1, 0, 0, 0, 0, 0, -1, 127, 2, 0, 0, 0, 0, 1, -3, 127, 4, -2, 1, 0, 0, 1, -5, 127, 6, -2, 1, 0, 0, 2, -6, 126, 8, -3, 1, 0, -1, 2, -7, 126, 11, -4, 2, -1, -1, 3, -8, 125, 13, -5, 2, -1, -1, 3, -10, 124, 16, -6, 3, -1, -1, 4, -11, 123, 18, -7, 3, -1, -1, 4, -12, 122, 20, -7, 3, -1, -1, 4, -13, 121, 23, -8, 3, -1, -2, 5, -14, 120, 25, -9, 4, -1, -1, 5, -15, 119, 27, -10, 4, -1, -1, 5, -16, 118, 30, -11, 4, -1, -2, 6, -17, 116, 33, -12, 5, -1, -2, 6, -17, 114, 35, -12, 5, -1, -2, 6, -18, 113, 38, -13, 5, -1, -2, 7, -19, 111, 41, -14, 6, -2, -2, 7, -19, 110, 43, -15, 6, -2, -2, 7, -20, 108, 46, -15, 6, -2, -2, 7, -20, 106, 49, -16, 6, -2, -2, 7, -21, 104, 51, -16, 7, -2, -2, 7, -21, 102, 54, -17, 7, -2, -2, 8, -21, 100, 56, -18, 7, -2, -2, 8, -22, 98, 59, -18, 7, -2, -2, 8, -22, 96, 62, -19, 7, -2, -2, 8, -22, 94, 64, -19, 7, -2, -2, 8, -22, 91, 67, -20, 8, -2, -2, 8, -22, 89, 69, -20, 8, -2, -2, 8, -22, 87, 72, -21, 8, -2, -2, 8, -21, 84, 74, -21, 8, -2, -2, 8, -22, 82, 77, -21, 8, -2, -2, 8, -21, 79, 79, -21, 8, -2, -2, 8, -21, 77, 82, -22, 8, -2, -2, 8, -21, 74, 84, -21, 8, -2, -2, 8, -21, 72, 87, -22, 8, -2, -2, 8, -20, 69, 89, -22, 8, -2, -2, 8, -20, 67, 91, -22, 8, -2, -2, 7, -19, 64, 94, -22, 8, -2, -2, 7, -19, 62, 96, -22, 8, -2, -2, 7, -18, 59, 98, -22, 8, -2, -2, 7, -18, 56, 100, -21, 8, -2, -2, 7, -17, 54, 102, -21, 7, -2, -2, 7, -16, 51, 104, -21, 7, -2, -2, 6, -16, 49, 106, -20, 7, -2, -2, 6, -15, 46, 108, -20, 7, -2, -2, 6, -15, 43, 110, -19, 7, -2, -2, 6, -14, 41, 111, -19, 7, -2, -1, 5, -13, 38, 113, -18, 6, -2, -1, 5, -12, 35, 114, -17, 6, -2, -1, 5, -12, 33, 116, -17, 6, -2, -1, 4, -11, 30, 118, -16, 5, -1, -1, 4, -10, 27, 119, -15, 5, -1, -1, 4, -9, 25, 120, -14, 5, -2, -1, 3, -8, 23, 121, -13, 4, -1, -1, 3, -7, 20, 122, -12, 4, -1, -1, 3, -7, 18, 123, -11, 4, -1, -1, 3, -6, 16, 124, -10, 3, -1, -1, 2, -5, 13, 125, -8, 3, -1, -1, 2, -4, 11, 126, -7, 2, -1, 0, 1, -3, 8, 126, -6, 2, 0, 0, 1, -2, 6, 127, -5, 1, 0, 0, 1, -2, 4, 127, -3, 1, 0, 0, 0, 0, 2, 127, -1, 0, 0, 0, 0, 0, 1, 127, 0, 0, 0, 0, 0, 0, -1, 127, 2, 0, 0, 0, 0, 1, -3, 127, 4, -1, 0, 0, 0, 1, -4, 126, 6, -2, 1, 0, 0, 1, -5, 126, 8, -3, 1, 0, 0, 1, -6, 125, 11, -4, 1, 0, 0, 1, -7, 124, 13, -4, 1, 0, 0, 2, -8, 123, 15, -5, 1, 0, 0, 2, -9, 122, 18, -6, 1, 0, 0, 2, -10, 121, 20, -6, 1, 0, 0, 2, -11, 120, 22, -7, 2, 0, 0, 2, -12, 119, 25, -8, 2, 0, 0, 3, -13, 117, 27, -8, 2, 0, 0, 3, -13, 116, 29, -9, 2, 0, 0, 3, -14, 114, 32, -10, 3, 0, 0, 3, -15, 113, 35, -10, 2, 0, 0, 3, -15, 111, 37, -11, 3, 0, 0, 3, -16, 109, 40, -11, 3, 0, 0, 3, -16, 108, 42, -12, 3, 0, 0, 4, -17, 106, 45, -13, 3, 0, 0, 4, -17, 104, 47, -13, 3, 0, 0, 4, -17, 102, 50, -14, 3, 0, 0, 4, -17, 100, 52, -14, 3, 0, 0, 4, -18, 98, 55, -15, 4, 0, 0, 4, -18, 96, 58, -15, 3, 0, 0, 4, -18, 94, 60, -16, 4, 0, 0, 4, -18, 91, 63, -16, 4, 0, 0, 4, -18, 89, 65, -16, 4, 0, 0, 4, -18, 87, 68, -17, 4, 0, 0, 4, -18, 85, 70, -17, 4, 0, 0, 4, -18, 82, 73, -17, 4, 0, 0, 4, -18, 80, 75, -17, 4, 0, 0, 4, -18, 78, 78, -18, 4, 0, 0, 4, -17, 75, 80, -18, 4, 0, 0, 4, -17, 73, 82, -18, 4, 0, 0, 4, -17, 70, 85, -18, 4, 0, 0, 4, -17, 68, 87, -18, 4, 0, 0, 4, -16, 65, 89, -18, 4, 0, 0, 4, -16, 63, 91, -18, 4, 0, 0, 4, -16, 60, 94, -18, 4, 0, 0, 3, -15, 58, 96, -18, 4, 0, 0, 4, -15, 55, 98, -18, 4, 0, 0, 3, -14, 52, 100, -17, 4, 0, 0, 3, -14, 50, 102, -17, 4, 0, 0, 3, -13, 47, 104, -17, 4, 0, 0, 3, -13, 45, 106, -17, 4, 0, 0, 3, -12, 42, 108, -16, 3, 0, 0, 3, -11, 40, 109, -16, 3, 0, 0, 3, -11, 37, 111, -15, 3, 0, 0, 2, -10, 35, 113, -15, 3, 0, 0, 3, -10, 32, 114, -14, 3, 0, 0, 2, -9, 29, 116, -13, 3, 0, 0, 2, -8, 27, 117, -13, 3, 0, 0, 2, -8, 25, 119, -12, 2, 0, 0, 2, -7, 22, 120, -11, 2, 0, 0, 1, -6, 20, 121, -10, 2, 0, 0, 1, -6, 18, 122, -9, 2, 0, 0, 1, -5, 15, 123, -8, 2, 0, 0, 1, -4, 13, 124, -7, 1, 0, 0, 1, -4, 11, 125, -6, 1, 0, 0, 1, -3, 8, 126, -5, 1, 0, 0, 1, -2, 6, 126, -4, 1, 0, 0, 0, -1, 4, 127, -3, 1, 0, 0, 0, 0, 2, 127, -1, 0, 0, 0, 0, 0, 2, 127, -1, 0, ]; /// /// Divides a signed fixed-point value by a power of two with the reference decoder's rounding. /// private static int RoundPowerOfTwo(int value, int bitCount) => (value + (1 << (bitCount - 1))) >> bitCount; /// /// Creates one nontrivial affine model traced from the independently verified two-frame local-warp fixture. /// private static Av1GlobalMotionParameters CreateReferenceParameters() { Av1GlobalMotionParameters parameters = Av1GlobalMotionParameters.Identity; parameters.Type = Av1GlobalMotionType.Affine; parameters[0] = -191565; parameters[1] = 599107; parameters[2] = 61755; parameters[3] = -140; parameters[4] = -6909; parameters[5] = 62012; parameters.UpdateShearParameters(); Assert.False(parameters.IsInvalid); Assert.Equal(-3776, parameters.Alpha); Assert.Equal(-128, parameters.Beta); Assert.Equal(-7360, parameters.Gamma); Assert.Equal(-3520, parameters.Delta); return parameters; } }