// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Buffers; using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; /// /// Verifies the entropy state and block-size groups used by the AV1 inter-intra prediction flag. /// [Trait("Format", "Avif")] public class Av1InterIntraEntropyTests { /// /// Verifies the four block-size-group distributions against the reference decoder's forward Q15 defaults. /// [Fact] public void DefaultsMatchReference() { ReadOnlySpan forwardThresholds = [16384, 26887, 27597, 30237]; Av1Distribution[] distributions = Av1DefaultDistributions.InterIntra; Assert.Equal(forwardThresholds.Length, distributions.Length); for (int group = 0; group < distributions.Length; group++) { // Av1Distribution stores inverse cumulative thresholds, so convert the reference decoder's forward threshold before // comparing the exact Q15 state consumed by the range decoder. uint expected = (uint)Av1Distribution.ProbabilityTop - forwardThresholds[group]; Assert.Equal(expected, distributions[group][0]); Assert.Equal(2, distributions[group].NumberOfSymbols); } } /// /// Verifies every AV1 block size against the normative size-group conversion table. /// /// The AV1 block-size enumeration value. /// The normative zero-based size group. [Theory] [MemberData(nameof(GetBlockSizeGroups))] public void GetSizeGroupMatchesNormativeTable(int blockSizeValue, int expectedGroup) { Av1BlockSize blockSize = (Av1BlockSize)blockSizeValue; Assert.Equal(expectedGroup, blockSize.GetSizeGroup()); } /// /// Verifies that the inter-intra flag reader selects and adapts the distribution for each size group. /// /// A block-size enumeration value representing one size group. /// The expected zero-based size group. [Theory] [InlineData((int)Av1BlockSize.Block4x4, 0)] [InlineData((int)Av1BlockSize.Block8x8, 1)] [InlineData((int)Av1BlockSize.Block16x16, 2)] [InlineData((int)Av1BlockSize.Block32x32, 3)] public void ReaderUsesBlockSizeGroup(int blockSizeValue, int sizeGroup) { bool[] expected = [false, true, true, false, true, false, false, true]; Av1Distribution writerDistribution = Av1DefaultDistributions.InterIntra[sizeGroup]; using Av1SymbolWriter writer = new(Configuration.Default, expected.Length, updateCdf: true); foreach (bool value in expected) { writer.WriteSymbol(value, writerDistribution); } using IMemoryOwner encoded = writer.Exit(); Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); Av1BlockSize blockSize = (Av1BlockSize)blockSizeValue; foreach (bool value in expected) { Assert.Equal(value, decoder.ReadIsInterIntra(blockSize)); } } /// /// Verifies that frame-context copies retain adapted inter-intra state without sharing mutable distributions. /// [Fact] public void FrameEntropyCopyRetainsIndependentState() { Av1FrameEntropyContext source = new(0); Av1FrameEntropyContext destination = new(0); source.InterIntra[2].Update(1); destination.CopyFrom(source); Assert.NotSame(source.InterIntra[2], destination.InterIntra[2]); Assert.Equal(source.InterIntra[2][0], destination.InterIntra[2][0]); source.InterIntra[2].Update(0); Assert.NotEqual(source.InterIntra[2][0], destination.InterIntra[2][0]); } /// /// Verifies that resetting a frame context restores the default threshold and adaptation state. /// [Fact] public void FrameEntropyResetRestoresDefaultState() { Av1FrameEntropyContext context = new(0); Av1FrameEntropyContext expected = new(0); context.InterIntra[3].Update(1); context.ResetToDefaults(0); Assert.Equal(expected.InterIntra[3][0], context.InterIntra[3][0]); // Applying the same next observation proves that reset restored the update-rate history as well as the visible // threshold; otherwise two equal thresholds would diverge because their adaptation rates differ. context.InterIntra[3].Update(0); expected.InterIntra[3].Update(0); Assert.Equal(expected.InterIntra[3][0], context.InterIntra[3][0]); } /// /// Verifies that a published frame snapshot preserves adapted thresholds and resets their update-rate history. /// [Fact] public void FrameEntropySnapshotResetsUpdateCount() { const int updateCount = 20; Av1FrameEntropyContext source = new(0); Av1FrameEntropyContext snapshot = new(0); for (int i = 0; i < updateCount; i++) { source.InterIntra[1].Update(1); } source.SnapshotTo(snapshot); Assert.Equal(source.InterIntra[1][0], snapshot.InterIntra[1][0]); // The source retains twenty observations while the published snapshot restarts at zero. Their next identical // observation must therefore move the shared starting threshold by different update rates. source.InterIntra[1].Update(0); snapshot.InterIntra[1].Update(0); Assert.NotEqual(source.InterIntra[1][0], snapshot.InterIntra[1][0]); } /// /// Provides the normative AV1 size-group table in block-size enumeration order. /// /// Every decoded block size paired with its size group. public static TheoryData GetBlockSizeGroups() { // These are the explicit Size_Group values from AV1 section 9.3 and the normative lookup table. The test keeps the // expected table independent from the production geometry formula so a shared calculation cannot mask errors. int[] sizeGroups = [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 0, 0, 1, 1, 2, 2]; TheoryData result = []; for (int blockSize = 0; blockSize < sizeGroups.Length; blockSize++) { result.Add(blockSize, sizeGroups[blockSize]); } return result; } }