// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; 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; /// /// Verifies compound prediction through the production block-reconstruction branch. /// [Trait("Format", "Avif")] public class Av1CompoundBlockDecoderTests { /// /// The hardware configurations covering the warped predictor's vector and scalar paths. /// private const HwIntrinsics GlobalWarpConfigurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic; /// /// Verifies that two retained reference planes are predicted and averaged before residual reconstruction. /// /// The native sample depth. [Theory] [InlineData((int)Av1BitDepth.EightBit)] [InlineData((int)Av1BitDepth.TenBit)] [InlineData((int)Av1BitDepth.TwelveBit)] public void DecodeBlockReconstructsEqualAverageCompoundPrediction(int bitDepthValue) { Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue; ushort firstValue = bitDepth == Av1BitDepth.EightBit ? (ushort)20 : (ushort)100; ushort secondValue = bitDepth switch { Av1BitDepth.EightBit => 41, Av1BitDepth.TenBit => 701, _ => 3001, }; ushort expected = (ushort)((firstValue + secondValue + 1) >> 1); ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth); ObuFrameHeader frameHeader = CreateFrameHeader(); frameHeader.GetReferenceFrameIndices()[0] = 0; frameHeader.GetReferenceFrameIndices()[1] = 1; using Av1ReferenceFrameStore referenceFrames = new(); Assert.True(referenceFrames.Commit(1, CreateReferenceFrame(sequenceHeader, firstValue), showFrame: false)); Assert.True(referenceFrames.Commit(2, CreateReferenceFrame(sequenceHeader, secondValue), showFrame: false)); using Av1FrameBuffer frameBuffer = new( Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); using Av1FrameInfo frameInfo = new(sequenceHeader); Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty) { Skip = true, YMode = Av1PredictionMode.NearestNearestMotionVector, CompoundIndex = true, CompoundType = Av1CompoundType.Average, }; modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Last2; modeInfo.InterpolationFilters.Fill(Av1InterpolationFilter.Regular); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); Av1LoopFilterContext loopFilterContext = new(sequenceHeader); Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); using Av1BlockDecoder decoder = new( sequenceHeader, frameHeader, frameBuffer, loopFilterContext, inverseQuantizer, referenceFrames); decoder.UpdateSuperblock(superblockInfo); decoder.DecodeBlock( modeInfo, Point.Empty, Av1BlockSize.Block8x8, superblockInfo, new Av1TileInfo(0, 0, frameHeader)); for (int row = 0; row < 8; row++) { if (bitDepth == Av1BitDepth.EightBit) { Span samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row); for (int column = 0; column < 8; column++) { Assert.Equal((byte)expected, samples[column]); } } else { Span samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0); for (int column = 0; column < 8; column++) { Assert.Equal(expected, samples[column]); } } } } /// /// Verifies that both references of a GLOBAL_GLOBALMV block use their complete matrix before compound averaging. /// [Fact] public void DecodeBlockReconstructsCompoundGlobalWarpPrediction() => FeatureTestRunner.RunWithHwIntrinsicsFeature( ValidateCompoundGlobalWarpPrediction, GlobalWarpConfigurations); /// /// Verifies that the production block branch maps a smaller current frame into a larger retained reference. /// /// The native sample depth. [Theory] [InlineData((int)Av1BitDepth.EightBit)] [InlineData((int)Av1BitDepth.TenBit)] [InlineData((int)Av1BitDepth.TwelveBit)] public void DecodeBlockReconstructsScaledSingleReferencePrediction(int bitDepthValue) { Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue; ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth, 16); ObuFrameHeader frameHeader = CreateFrameHeader(); frameHeader.GetReferenceFrameIndices()[0] = 0; using Av1ReferenceFrameStore referenceFrames = new(); Assert.True(referenceFrames.Commit(1, CreateScaledPatternReferenceFrame(sequenceHeader), showFrame: false)); using Av1FrameBuffer frameBuffer = new( Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); frameBuffer.Width = 8; frameBuffer.Height = 8; using Av1FrameInfo frameInfo = new(sequenceHeader); Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); Av1BlockModeInfo modeInfo = CreateSingleReferenceModeInfo(Av1BlockSize.Block8x8, Point.Empty); modeInfo.InterpolationFilters.Fill(Av1InterpolationFilter.Bilinear); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); Av1LoopFilterContext loopFilterContext = new(sequenceHeader); Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); using Av1BlockDecoder decoder = new( sequenceHeader, frameHeader, frameBuffer, loopFilterContext, inverseQuantizer, referenceFrames); decoder.UpdateSuperblock(superblockInfo); decoder.DecodeBlock( modeInfo, Point.Empty, Av1BlockSize.Block8x8, superblockInfo, new Av1TileInfo(0, 0, frameHeader)); for (int row = 0; row < 8; row++) { for (int column = 0; column < 8; column++) { ushort expected = (ushort)(5 + (column * 4) + (row * 16)); if (bitDepth == Av1BitDepth.EightBit) { Span samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row); Assert.Equal((byte)expected, samples[column]); } else { Span samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0); Assert.Equal(expected, samples[column]); } } } } /// /// Verifies selectable compound reconstruction through the production block branch at every supported bit depth. /// /// The native sample depth. /// The selected compound operation. [Theory] [InlineData((int)Av1BitDepth.EightBit, (int)Av1CompoundType.DistanceWeighted)] [InlineData((int)Av1BitDepth.TenBit, (int)Av1CompoundType.DistanceWeighted)] [InlineData((int)Av1BitDepth.TwelveBit, (int)Av1CompoundType.DistanceWeighted)] [InlineData((int)Av1BitDepth.EightBit, (int)Av1CompoundType.Wedge)] [InlineData((int)Av1BitDepth.TenBit, (int)Av1CompoundType.Wedge)] [InlineData((int)Av1BitDepth.TwelveBit, (int)Av1CompoundType.Wedge)] [InlineData((int)Av1BitDepth.EightBit, (int)Av1CompoundType.DifferenceWeighted)] [InlineData((int)Av1BitDepth.TenBit, (int)Av1CompoundType.DifferenceWeighted)] [InlineData((int)Av1BitDepth.TwelveBit, (int)Av1CompoundType.DifferenceWeighted)] public void DecodeBlockReconstructsSelectableCompoundPrediction(int bitDepthValue, int compoundTypeValue) { Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue; Av1CompoundType compoundType = (Av1CompoundType)compoundTypeValue; ushort firstValue = bitDepth == Av1BitDepth.EightBit ? (ushort)20 : (ushort)100; ushort secondValue = bitDepth switch { Av1BitDepth.EightBit => 41, Av1BitDepth.TenBit => 701, _ => 3001, }; ReadOnlySpan wedgeMask = [ 0, 0, 0, 1, 1, 2, 4, 6, 0, 1, 1, 2, 4, 6, 11, 18, 1, 2, 4, 6, 11, 18, 27, 37, 4, 6, 11, 18, 27, 37, 46, 53, 11, 18, 27, 37, 46, 53, 58, 60, 27, 37, 46, 53, 58, 60, 62, 63, 46, 53, 58, 60, 62, 63, 63, 64, 58, 60, 62, 63, 63, 64, 64, 64, ]; ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth); sequenceHeader.OrderHintInfo.EnableOrderHint = true; sequenceHeader.OrderHintInfo.OrderHintBits = 5; ObuFrameHeader frameHeader = CreateFrameHeader(); frameHeader.OrderHint = 10; frameHeader.GetReferenceFrameIndices()[0] = 0; frameHeader.GetReferenceFrameIndices()[1] = 1; frameHeader.GetReferenceOrderHints()[0] = 9; frameHeader.GetReferenceOrderHints()[1] = 5; using Av1ReferenceFrameStore referenceFrames = new(); Assert.True(referenceFrames.Commit(1, CreateReferenceFrame(sequenceHeader, firstValue), showFrame: false)); Assert.True(referenceFrames.Commit(2, CreateReferenceFrame(sequenceHeader, secondValue), showFrame: false)); using Av1FrameBuffer frameBuffer = new( Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); using Av1FrameInfo frameInfo = new(sequenceHeader); Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty) { Skip = true, YMode = Av1PredictionMode.NearestNearestMotionVector, CompoundIndex = compoundType != Av1CompoundType.DistanceWeighted, CompoundType = compoundType, CompoundWedgeIndex = 0, CompoundWedgeSign = true, DifferenceWeightedMaskType = Av1DifferenceWeightedMaskType.Type38, }; modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Last2; modeInfo.InterpolationFilters.Clear(); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); Av1LoopFilterContext loopFilterContext = new(sequenceHeader); Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); using Av1BlockDecoder decoder = new( sequenceHeader, frameHeader, frameBuffer, loopFilterContext, inverseQuantizer, referenceFrames); decoder.UpdateSuperblock(superblockInfo); decoder.DecodeBlock( modeInfo, Point.Empty, Av1BlockSize.Block8x8, superblockInfo, new Av1TileInfo(0, 0, frameHeader)); int differenceShift = bitDepth.GetBitCount() - 8 + 4; int differenceAlpha = Math.Min(64, 38 + (Math.Abs(firstValue - secondValue) >> differenceShift)); for (int row = 0; row < 8; row++) { for (int column = 0; column < 8; column++) { int alpha = compoundType switch { Av1CompoundType.Wedge => wedgeMask[(row * 8) + column], Av1CompoundType.DifferenceWeighted => differenceAlpha, _ => 52, }; ushort expected = (ushort)(((alpha * firstValue) + ((64 - alpha) * secondValue) + 32) >> 6); if (bitDepth == Av1BitDepth.EightBit) { Span samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row); Assert.Equal((byte)expected, samples[column]); } else { Span samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0); Assert.Equal(expected, samples[column]); } } } } /// /// Verifies smooth inter-intra reconstruction through the production block branch at every supported bit depth. /// /// The native sample depth. [Theory] [InlineData((int)Av1BitDepth.EightBit)] [InlineData((int)Av1BitDepth.TenBit)] [InlineData((int)Av1BitDepth.TwelveBit)] public void DecodeBlockReconstructsSmoothInterIntraPrediction(int bitDepthValue) { Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue; ushort interValue = bitDepth == Av1BitDepth.EightBit ? (ushort)20 : (ushort)100; ushort intraValue = (ushort)(1 << (bitDepth.GetBitCount() - 1)); ushort expected = (ushort)((interValue + intraValue + 1) >> 1); ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth); ObuFrameHeader frameHeader = CreateFrameHeader(); frameHeader.GetReferenceFrameIndices()[0] = 0; using Av1ReferenceFrameStore referenceFrames = new(); Assert.True(referenceFrames.Commit(1, CreateReferenceFrame(sequenceHeader, interValue), showFrame: false)); using Av1FrameBuffer frameBuffer = new( Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); using Av1FrameInfo frameInfo = new(sequenceHeader); Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty) { Skip = true, YMode = Av1PredictionMode.NearestMotionVector, InterIntraMode = Av1InterIntraMode.DC, UseInterIntraWedge = false, }; modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Intra; modeInfo.InterpolationFilters.Clear(); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); Av1LoopFilterContext loopFilterContext = new(sequenceHeader); Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); using Av1BlockDecoder decoder = new( sequenceHeader, frameHeader, frameBuffer, loopFilterContext, inverseQuantizer, referenceFrames); decoder.UpdateSuperblock(superblockInfo); decoder.DecodeBlock( modeInfo, Point.Empty, Av1BlockSize.Block8x8, superblockInfo, new Av1TileInfo(0, 0, frameHeader)); for (int row = 0; row < 8; row++) { if (bitDepth == Av1BitDepth.EightBit) { Span samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row); for (int column = 0; column < 8; column++) { Assert.Equal((byte)expected, samples[column]); } } else { Span samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0); for (int column = 0; column < 8; column++) { Assert.Equal(expected, samples[column]); } } } } /// /// Verifies above-then-left OBMC reconstruction through the production block branch at every supported bit depth. /// /// The native sample depth. [Theory] [InlineData((int)Av1BitDepth.EightBit)] [InlineData((int)Av1BitDepth.TenBit)] [InlineData((int)Av1BitDepth.TwelveBit)] public void DecodeBlockReconstructsOverlappedMotionCompensation(int bitDepthValue) { const int frameSize = 24; const int blockOrigin = 8; Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue; ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth, frameSize); ObuFrameHeader frameHeader = CreateFrameHeader(frameSize); frameHeader.GetReferenceFrameIndices()[0] = 0; using Av1ReferenceFrameStore referenceFrames = new(); Assert.True(referenceFrames.Commit(1, CreatePatternReferenceFrame(sequenceHeader, frameHeader), showFrame: false)); using Av1FrameBuffer frameBuffer = new( Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); using Av1FrameInfo frameInfo = new(sequenceHeader); Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); Av1BlockModeInfo above = CreateSingleReferenceModeInfo(Av1BlockSize.Block8x8, new Point(2, 0)); above.MotionVectors[0] = new Av1MotionVector(0, 8); frameInfo.UpdateModeInfo(above, superblockInfo); Av1BlockModeInfo left = CreateSingleReferenceModeInfo(Av1BlockSize.Block8x8, new Point(0, 2)); left.MotionVectors[0] = new Av1MotionVector(8, 0); frameInfo.UpdateModeInfo(left, superblockInfo); Av1BlockModeInfo current = CreateSingleReferenceModeInfo(Av1BlockSize.Block8x8, new Point(2, 2)); current.MotionMode = Av1MotionMode.Obmc; current.SetTransformUnitCount(Av1PlaneType.Y, 1); frameInfo.UpdateModeInfo(current, superblockInfo); Av1LoopFilterContext loopFilterContext = new(sequenceHeader); Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); using Av1BlockDecoder decoder = new( sequenceHeader, frameHeader, frameBuffer, loopFilterContext, inverseQuantizer, referenceFrames); decoder.UpdateSuperblock(superblockInfo); decoder.DecodeBlock( current, new Point(2, 2), Av1BlockSize.Block8x8, superblockInfo, new Av1TileInfo(0, 0, frameHeader)); ReadOnlySpan mask = [39, 50, 59, 64]; for (int row = 0; row < 8; row++) { for (int column = 0; column < 8; column++) { int first = GetPatternValue(blockOrigin + column, blockOrigin + row); if (row < mask.Length) { int aboveValue = GetPatternValue(blockOrigin + column + 1, blockOrigin + row); first = ((mask[row] * first) + ((64 - mask[row]) * aboveValue) + 32) >> 6; } if (column < mask.Length) { int leftValue = GetPatternValue(blockOrigin + column, blockOrigin + row + 1); first = ((mask[column] * first) + ((64 - mask[column]) * leftValue) + 32) >> 6; } if (bitDepth == Av1BitDepth.EightBit) { Span samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(blockOrigin + row); Assert.Equal((byte)first, samples[blockOrigin + column]); } else { Span samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, blockOrigin + row, 0, 0); Assert.Equal((ushort)first, samples[blockOrigin + column]); } } } } /// /// Verifies the above and left OBMC rectangles on horizontally and vertically subsampled chroma planes. /// /// The chroma-subsampling layout to reconstruct. [Theory] [InlineData((int)Av1ColorFormat.Yuv420)] [InlineData((int)Av1ColorFormat.Yuv422)] public void DecodeBlockReconstructsSubsampledOverlappedMotionCompensation(int colorFormatValue) { const int frameSize = 48; const int blockOrigin = 16; Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue; ObuSequenceHeader sequenceHeader = CreateSequenceHeader(Av1BitDepth.EightBit, frameSize, colorFormat); ObuFrameHeader frameHeader = CreateFrameHeader(frameSize); frameHeader.GetReferenceFrameIndices()[0] = 0; using Av1ReferenceFrameStore referenceFrames = new(); Assert.True(referenceFrames.Commit(1, CreatePatternReferenceFrame(sequenceHeader, frameHeader, colorFormat), showFrame: false)); using Av1FrameBuffer frameBuffer = new( Configuration.Default, sequenceHeader, colorFormat, false); using Av1FrameInfo frameInfo = new(sequenceHeader); Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size16x16, 0, 0); superblockInfo.GetTransformInfoUv()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); superblockInfo.GetTransformInfoUv()[1] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); Av1BlockModeInfo above = CreateSingleReferenceModeInfo(Av1BlockSize.Block16x16, new Point(4, 0)); above.MotionVectors[0] = new Av1MotionVector(0, 16); frameInfo.UpdateModeInfo(above, superblockInfo); Av1BlockModeInfo left = CreateSingleReferenceModeInfo(Av1BlockSize.Block16x16, new Point(0, 4)); left.MotionVectors[0] = new Av1MotionVector(16, 0); frameInfo.UpdateModeInfo(left, superblockInfo); Av1BlockModeInfo current = CreateSingleReferenceModeInfo(Av1BlockSize.Block16x16, new Point(4, 4)); current.MotionMode = Av1MotionMode.Obmc; current.SetTransformUnitCount(Av1PlaneType.Y, 1); current.SetTransformUnitCount(Av1PlaneType.Uv, 1); frameInfo.UpdateModeInfo(current, superblockInfo); Av1LoopFilterContext loopFilterContext = new(sequenceHeader); Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); using Av1BlockDecoder decoder = new( sequenceHeader, frameHeader, frameBuffer, loopFilterContext, inverseQuantizer, referenceFrames); decoder.UpdateSuperblock(superblockInfo); decoder.DecodeBlock( current, new Point(4, 4), Av1BlockSize.Block16x16, superblockInfo, new Av1TileInfo(0, 0, frameHeader)); ReadOnlySpan mask4 = [39, 50, 59, 64]; ReadOnlySpan mask8 = [36, 42, 48, 53, 57, 61, 64, 64]; for (int plane = (int)Av1Plane.U; plane <= (int)Av1Plane.V; plane++) { int subY = colorFormat == Av1ColorFormat.Yuv420 ? 1 : 0; int planeOriginX = blockOrigin >> 1; int planeOriginY = blockOrigin >> subY; int predictionWidth = Av1BlockSize.Block16x16.GetWidth() >> 1; int predictionHeight = Av1BlockSize.Block16x16.GetHeight() >> subY; int leftSourceRowOffset = 2 >> subY; ReadOnlySpan verticalMask = subY == 0 ? mask8 : mask4; for (int row = 0; row < predictionHeight; row++) { Span samples = frameBuffer.DeriveBlockPointer((Av1Plane)plane, 1, subY).DangerousGetRowSpan(planeOriginY + row); for (int column = 0; column < predictionWidth; column++) { int expected = GetPlanePatternValue(plane, planeOriginX + column, planeOriginY + row); if (row < verticalMask.Length) { int aboveValue = GetPlanePatternValue(plane, planeOriginX + column + 1, planeOriginY + row); expected = ((verticalMask[row] * expected) + ((64 - verticalMask[row]) * aboveValue) + 32) >> 6; } if (column < mask4.Length) { int leftValue = GetPlanePatternValue(plane, planeOriginX + column, planeOriginY + row + leftSourceRowOffset); expected = ((mask4[column] * expected) + ((64 - mask4[column]) * leftValue) + 32) >> 6; } Assert.Equal((byte)expected, samples[planeOriginX + column]); } } } } /// /// Creates one skipped single-reference mode record for direct block-reconstruction tests. /// private static Av1BlockModeInfo CreateSingleReferenceModeInfo(Av1BlockSize blockSize, Point position) { Av1BlockModeInfo modeInfo = new(blockSize, position) { Skip = true, YMode = Av1PredictionMode.NearestMotionVector, }; modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.None; modeInfo.InterpolationFilters.Clear(); return modeInfo; } /// /// Reconstructs a compound global-warp block at every supported native sample depth. /// private static void ValidateCompoundGlobalWarpPrediction() { foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.EightBit, Av1BitDepth.TenBit, Av1BitDepth.TwelveBit }) { ValidateCompoundGlobalWarpPredictionAtBitDepth(bitDepth); } } /// /// Reconstructs one compound global-warp block and compares it with independently invoked scalar predictors. /// /// The native sample depth. private static void ValidateCompoundGlobalWarpPredictionAtBitDepth(Av1BitDepth bitDepth) { const int frameSize = 32; const int blockOrigin = 8; const int blockSize = 8; const int compoundRoundBits = 4; const int compoundRoundOffset = (1 << 12) + (1 << 11); ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth, frameSize); ObuFrameHeader frameHeader = CreateFrameHeader(frameSize); frameHeader.GetReferenceFrameIndices()[0] = 0; frameHeader.GetReferenceFrameIndices()[1] = 1; Av1GlobalMotionParameters globalMotionParameters = Av1GlobalMotionParameters.Identity; globalMotionParameters.Type = Av1GlobalMotionType.RotationZoom; globalMotionParameters[0] = -357376; globalMotionParameters[1] = 372736; globalMotionParameters[2] = 65468; globalMotionParameters[3] = 2856; globalMotionParameters[4] = -2856; globalMotionParameters[5] = 65468; globalMotionParameters.UpdateShearParameters(); Assert.False(globalMotionParameters.IsInvalid); frameHeader.GetGlobalMotionParameters()[0] = globalMotionParameters; frameHeader.GetGlobalMotionParameters()[1] = globalMotionParameters; using Av1ReferenceFrameStore referenceFrames = new(); Assert.True(referenceFrames.Commit( 1, CreatePatternReferenceFrame(sequenceHeader, CreateFrameHeader(frameSize)), showFrame: false)); Assert.True(referenceFrames.Commit( 2, CreatePatternReferenceFrame(sequenceHeader, CreateFrameHeader(frameSize), sampleOffset: 40), showFrame: false)); Av1FrameBuffer firstReference = referenceFrames.Resolve(0)!.FrameBuffer; Av1FrameBuffer secondReference = referenceFrames.Resolve(1)!.FrameBuffer; ushort[] firstHighBitDepthPrediction = new ushort[blockSize * blockSize]; ushort[] secondHighBitDepthPrediction = new ushort[blockSize * blockSize]; short[] firstScratch = new short[Av1WarpedInterPredictor.WarpedScratchLength]; short[] secondScratch = new short[Av1WarpedInterPredictor.WarpedScratchLength]; Point blockPosition = new(blockOrigin, blockOrigin); if (bitDepth == Av1BitDepth.EightBit) { Span firstSource = firstReference.GetPaddedPlaneSpan( Av1Plane.Y, 0, 0, out int firstStride, out Point firstOrigin); Span secondSource = secondReference.GetPaddedPlaneSpan( Av1Plane.Y, 0, 0, out int secondStride, out Point secondOrigin); Av1WarpedInterPredictor.PredictWarpedCompoundScalar( firstSource, firstStride, firstOrigin, frameSize, frameSize, firstHighBitDepthPrediction, blockSize, blockPosition, blockSize, blockSize, 0, 0, globalMotionParameters, firstScratch); Av1WarpedInterPredictor.PredictWarpedCompoundScalar( secondSource, secondStride, secondOrigin, frameSize, frameSize, secondHighBitDepthPrediction, blockSize, blockPosition, blockSize, blockSize, 0, 0, globalMotionParameters, secondScratch); } else { Span firstSource = firstReference.GetPaddedPlaneSpan16( Av1Plane.Y, 0, 0, out int firstStride, out Point firstOrigin); Span secondSource = secondReference.GetPaddedPlaneSpan16( Av1Plane.Y, 0, 0, out int secondStride, out Point secondOrigin); int bitDepthValue = bitDepth.GetBitCount(); Av1WarpedInterPredictor.PredictWarpedScalar( firstSource, firstStride, firstOrigin, frameSize, frameSize, firstHighBitDepthPrediction, blockSize, blockPosition, blockSize, blockSize, 0, 0, bitDepthValue, globalMotionParameters, firstScratch); Av1WarpedInterPredictor.PredictWarpedScalar( secondSource, secondStride, secondOrigin, frameSize, frameSize, secondHighBitDepthPrediction, blockSize, blockPosition, blockSize, blockSize, 0, 0, bitDepthValue, globalMotionParameters, secondScratch); } using Av1FrameBuffer frameBuffer = new( Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); using Av1FrameInfo frameInfo = new(sequenceHeader); Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, new Point(2, 2)) { Skip = true, YMode = Av1PredictionMode.GlobalGlobalMotionVector, CompoundIndex = true, CompoundType = Av1CompoundType.Average, }; modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Last2; modeInfo.InterpolationFilters.Clear(); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); Av1LoopFilterContext loopFilterContext = new(sequenceHeader); Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); using Av1BlockDecoder decoder = new( sequenceHeader, frameHeader, frameBuffer, loopFilterContext, inverseQuantizer, referenceFrames); decoder.UpdateSuperblock(superblockInfo); decoder.DecodeBlock( modeInfo, new Point(2, 2), Av1BlockSize.Block8x8, superblockInfo, new Av1TileInfo(0, 0, frameHeader)); for (int row = 0; row < blockSize; row++) { for (int column = 0; column < blockSize; column++) { int predictionIndex = (row * blockSize) + column; if (bitDepth == Av1BitDepth.EightBit) { // Libaom truncates the equal average before removing the Q4 compound bias, then performs the // sole final rounding step. Averaging two already reconstructed pixels can differ by one. int intermediate = ((firstHighBitDepthPrediction[predictionIndex] + secondHighBitDepthPrediction[predictionIndex]) >> 1) - compoundRoundOffset; byte expected = (byte)Math.Clamp( (intermediate + (1 << (compoundRoundBits - 1))) >> compoundRoundBits, 0, byte.MaxValue); Span samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(blockOrigin + row); Assert.Equal(expected, samples[blockOrigin + column]); } else { ushort expected = (ushort)((firstHighBitDepthPrediction[predictionIndex] + secondHighBitDepthPrediction[predictionIndex] + 1) >> 1); Span samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, blockOrigin + row, 0, 0); Assert.Equal(expected, samples[blockOrigin + column]); } } } } /// /// Creates one retained frame whose integer-coordinate luma samples make both OBMC axes observable. /// private static Av1ReferenceFrame CreatePatternReferenceFrame( ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, Av1ColorFormat colorFormat = Av1ColorFormat.Yuv400, int sampleOffset = 0) { Av1FrameBuffer frameBuffer = new( Configuration.Default, sequenceHeader, colorFormat, false); for (int plane = 0; plane < sequenceHeader.ColorConfig.PlaneCount; plane++) { int subX = plane > 0 && sequenceHeader.ColorConfig.SubSamplingX ? 1 : 0; int subY = plane > 0 && sequenceHeader.ColorConfig.SubSamplingY ? 1 : 0; int planeWidth = sequenceHeader.MaxFrameWidth >> subX; int planeHeight = sequenceHeader.MaxFrameHeight >> subY; for (int row = 0; row < planeHeight; row++) { if (sequenceHeader.ColorConfig.BitDepth == Av1BitDepth.EightBit) { Span samples = frameBuffer.DeriveBlockPointer((Av1Plane)plane, subX, subY).DangerousGetRowSpan(row); for (int column = 0; column < planeWidth; column++) { samples[column] = (byte)(GetPlanePatternValue(plane, column, row) + sampleOffset); } } else { Span samples = frameBuffer.GetHighBitDepthRowSpan((Av1Plane)plane, row, subX, subY); for (int column = 0; column < planeWidth; column++) { samples[column] = (ushort)(GetPlanePatternValue(plane, column, row) + sampleOffset); } } } } Av1ReferenceFrameBorder.Extend(frameBuffer); using Av1FrameInfo frameInfo = new(sequenceHeader); return new Av1ReferenceFrame(frameBuffer, frameHeader, frameInfo); } /// /// Creates a 16x16 retained frame whose linear pattern has an exact half-sample bilinear result. /// private static Av1ReferenceFrame CreateScaledPatternReferenceFrame(ObuSequenceHeader sequenceHeader) { Av1FrameBuffer frameBuffer = new( Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); frameBuffer.Width = 16; frameBuffer.Height = 16; for (int row = 0; row < 16; row++) { if (sequenceHeader.ColorConfig.BitDepth == Av1BitDepth.EightBit) { Span samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row); for (int column = 0; column < 16; column++) { samples[column] = (byte)((column * 2) + (row * 8)); } } else { Span samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0); for (int column = 0; column < 16; column++) { samples[column] = (ushort)((column * 2) + (row * 8)); } } } Av1ReferenceFrameBorder.Extend(frameBuffer); using Av1FrameInfo frameInfo = new(sequenceHeader); return new Av1ReferenceFrame(frameBuffer, CreateFrameHeader(16), frameInfo); } /// /// Gets the deterministic luma value stored at one reference-frame coordinate. /// private static int GetPatternValue(int column, int row) => column + (row * 4); /// /// Gets the deterministic plane value stored at one reference-frame coordinate. /// private static int GetPlanePatternValue(int plane, int column, int row) => GetPatternValue(column, row) + (plane * 20); /// /// Creates one independently owned retained frame filled with a constant visible luma value. /// private static Av1ReferenceFrame CreateReferenceFrame(ObuSequenceHeader sequenceHeader, ushort value) { Av1FrameBuffer frameBuffer = new( Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); if (sequenceHeader.ColorConfig.BitDepth == Av1BitDepth.EightBit) { for (int row = 0; row < 8; row++) { frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row).Fill((byte)value); } } else { for (int row = 0; row < 8; row++) { frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0).Fill(value); } } using Av1FrameInfo frameInfo = new(sequenceHeader); return new Av1ReferenceFrame(frameBuffer, CreateFrameHeader(), frameInfo); } /// /// Creates the monochrome 8x8 sequence used by direct reconstruction tests. /// private static ObuSequenceHeader CreateSequenceHeader( Av1BitDepth bitDepth, int frameSize = 8, Av1ColorFormat colorFormat = Av1ColorFormat.Yuv400) => new() { MaxFrameWidth = frameSize, MaxFrameHeight = frameSize, Use128x128Superblock = false, ColorConfig = new ObuColorConfig { IsMonochrome = colorFormat == Av1ColorFormat.Yuv400, BitDepth = bitDepth, SubSamplingX = colorFormat is Av1ColorFormat.Yuv420 or Av1ColorFormat.Yuv422, SubSamplingY = colorFormat == Av1ColorFormat.Yuv420, }, }; /// /// Creates an unscaled 8x8 inter-frame header with one complete tile. /// private static ObuFrameHeader CreateFrameHeader(int frameSize = 8) { ObuFrameHeader frameHeader = new() { FrameType = ObuFrameType.InterFrame, ModeInfoColumnCount = frameSize >> Av1Constants.ModeInfoSizeLog2, ModeInfoRowCount = frameSize >> Av1Constants.ModeInfoSizeLog2, FrameSize = new ObuFrameSize { FrameWidth = frameSize, FrameHeight = frameSize, }, }; frameHeader.TilesInfo.TileColumnStartModeInfo[1] = frameHeader.ModeInfoColumnCount; frameHeader.TilesInfo.TileRowStartModeInfo[1] = frameHeader.ModeInfoRowCount; return frameHeader; } }