mirror of https://github.com/SixLabors/ImageSharp
17 changed files with 1424 additions and 95 deletions
@ -0,0 +1,257 @@ |
|||||
|
// 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.Formats.Heif.Av1.Prediction; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies the entropy state and spatial contexts used by intra-coded blocks inside AV1 inter frames.
|
||||
|
/// </summary>
|
||||
|
[Trait("Format", "Avif")] |
||||
|
public class Av1InterFrameIntraEntropyTests |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets libaom's four forward Q15 luma-mode CDF rows in block-size-group order.
|
||||
|
/// </summary>
|
||||
|
private static ReadOnlySpan<ushort> FrameYModeForwardThresholds => |
||||
|
[ |
||||
|
22801, 23489, 24293, 24756, 25601, 26123, 26606, 27418, 27945, 29228, 29685, 30349, |
||||
|
18673, 19845, 22631, 23318, 23950, 24649, 25527, 27364, 28152, 29701, 29984, 30852, |
||||
|
19770, 20979, 23396, 23939, 24241, 24654, 25136, 27073, 27830, 29360, 29730, 30659, |
||||
|
20155, 21301, 22838, 23178, 23261, 23533, 23703, 24804, 25352, 26575, 27016, 28049, |
||||
|
]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies the four normative intra/inter distributions against libaom's forward Q15 defaults.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void IntraInterDefaultsMatchLibaom() |
||||
|
{ |
||||
|
uint[] forwardThresholds = [806, 16662, 20186, 26538]; |
||||
|
Av1Distribution[] distributions = Av1DefaultDistributions.IntraInter; |
||||
|
|
||||
|
Assert.Equal(forwardThresholds.Length, distributions.Length); |
||||
|
for (int context = 0; context < distributions.Length; context++) |
||||
|
{ |
||||
|
// Av1Distribution stores inverse cumulative thresholds, so compare each libaom default after the same
|
||||
|
// forward-to-inverse conversion performed by its constructor.
|
||||
|
Assert.Equal((uint)Av1Distribution.ProbabilityTop - forwardThresholds[context], distributions[context][0]); |
||||
|
Assert.Equal(2, distributions[context].NumberOfSymbols); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies every inter-frame intra luma-mode threshold against libaom's forward Q15 defaults.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void FrameYModeDefaultsMatchLibaom() |
||||
|
{ |
||||
|
const int thresholdsPerGroup = 12; |
||||
|
ReadOnlySpan<ushort> forwardThresholds = FrameYModeForwardThresholds; |
||||
|
Av1Distribution[] distributions = Av1DefaultDistributions.FrameYMode; |
||||
|
|
||||
|
Assert.Equal(4, distributions.Length); |
||||
|
for (int group = 0; group < distributions.Length; group++) |
||||
|
{ |
||||
|
Assert.Equal(thresholdsPerGroup + 1, distributions[group].NumberOfSymbols); |
||||
|
|
||||
|
for (int threshold = 0; threshold < thresholdsPerGroup; threshold++) |
||||
|
{ |
||||
|
uint expected = (uint)Av1Distribution.ProbabilityTop - forwardThresholds[(group * thresholdsPerGroup) + threshold]; |
||||
|
Assert.Equal(expected, distributions[group][threshold]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that the intra/inter reader selects and adapts each of the four spatial-context distributions.
|
||||
|
/// </summary>
|
||||
|
/// <param name="context">The intra/inter spatial context.</param>
|
||||
|
[Theory] |
||||
|
[InlineData(0)] |
||||
|
[InlineData(1)] |
||||
|
[InlineData(2)] |
||||
|
[InlineData(3)] |
||||
|
public void ReadIsInterUsesRequestedContext(int context) |
||||
|
{ |
||||
|
bool[] expected = [false, true, true, false, true, false, false, true]; |
||||
|
Av1Distribution writerDistribution = Av1DefaultDistributions.IntraInter[context]; |
||||
|
using Av1SymbolWriter writer = new(Configuration.Default, 8, updateCdf: true); |
||||
|
|
||||
|
foreach (bool value in expected) |
||||
|
{ |
||||
|
writer.WriteSymbol(value, writerDistribution); |
||||
|
} |
||||
|
|
||||
|
using IMemoryOwner<byte> encoded = writer.Exit(); |
||||
|
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); |
||||
|
|
||||
|
foreach (bool value in expected) |
||||
|
{ |
||||
|
Assert.Equal(value, decoder.ReadIsInter(context)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that inter-frame intra luma modes use the normative size group for every AV1 block size.
|
||||
|
/// </summary>
|
||||
|
/// <param name="blockSizeValue">The AV1 block-size enumeration value.</param>
|
||||
|
/// <param name="sizeGroup">The normative size group from AV1 section 9.3.</param>
|
||||
|
[Theory] |
||||
|
[MemberData(nameof(GetBlockSizeGroups))] |
||||
|
public void ReadInterFrameYModeUsesNormativeSizeGroup(int blockSizeValue, int sizeGroup) |
||||
|
{ |
||||
|
Av1BlockSize blockSize = (Av1BlockSize)blockSizeValue; |
||||
|
Av1PredictionMode[] expected = |
||||
|
[ |
||||
|
Av1PredictionMode.DC, |
||||
|
Av1PredictionMode.Directional45Degrees, |
||||
|
Av1PredictionMode.Smooth, |
||||
|
Av1PredictionMode.Paeth, |
||||
|
Av1PredictionMode.Horizontal, |
||||
|
Av1PredictionMode.Directional157Degrees, |
||||
|
]; |
||||
|
|
||||
|
Av1Distribution writerDistribution = Av1DefaultDistributions.FrameYMode[sizeGroup]; |
||||
|
using Av1SymbolWriter writer = new(Configuration.Default, 8, updateCdf: true); |
||||
|
|
||||
|
foreach (Av1PredictionMode mode in expected) |
||||
|
{ |
||||
|
writer.WriteSymbol((int)mode, writerDistribution); |
||||
|
} |
||||
|
|
||||
|
using IMemoryOwner<byte> encoded = writer.Exit(); |
||||
|
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); |
||||
|
|
||||
|
foreach (Av1PredictionMode mode in expected) |
||||
|
{ |
||||
|
Assert.Equal(mode, decoder.ReadInterFrameYMode(blockSize)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that all four intra/inter contexts follow the normative above-and-left neighbor classification.
|
||||
|
/// </summary>
|
||||
|
/// <param name="hasAbove">Whether the above block is available.</param>
|
||||
|
/// <param name="aboveIsInter">Whether the available above block uses inter prediction.</param>
|
||||
|
/// <param name="hasLeft">Whether the left block is available.</param>
|
||||
|
/// <param name="leftIsInter">Whether the available left block uses inter prediction.</param>
|
||||
|
/// <param name="expected">The expected intra/inter context.</param>
|
||||
|
[Theory] |
||||
|
[InlineData(false, false, false, false, 0)] |
||||
|
[InlineData(true, true, false, false, 0)] |
||||
|
[InlineData(true, false, false, false, 2)] |
||||
|
[InlineData(false, false, true, true, 0)] |
||||
|
[InlineData(false, false, true, false, 2)] |
||||
|
[InlineData(true, true, true, true, 0)] |
||||
|
[InlineData(true, false, true, true, 1)] |
||||
|
[InlineData(true, true, true, false, 1)] |
||||
|
[InlineData(true, false, true, false, 3)] |
||||
|
public void IntraInterContextMatchesNeighborPredictionTypes( |
||||
|
bool hasAbove, |
||||
|
bool aboveIsInter, |
||||
|
bool hasLeft, |
||||
|
bool leftIsInter, |
||||
|
int expected) |
||||
|
{ |
||||
|
Av1BlockModeInfo above = hasAbove ? CreateModeInfo(aboveIsInter) : null; |
||||
|
Av1BlockModeInfo left = hasLeft ? CreateModeInfo(leftIsInter) : null; |
||||
|
|
||||
|
int actual = Av1SymbolContextHelper.GetIntraInterContext(above, left); |
||||
|
|
||||
|
Assert.Equal(expected, actual); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that frame-context copies retain adapted inter-frame intra state without sharing mutable distributions.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void FrameEntropyCopyRetainsIndependentInterFrameIntraState() |
||||
|
{ |
||||
|
Av1FrameEntropyContext source = new(0); |
||||
|
Av1FrameEntropyContext destination = new(0); |
||||
|
source.FrameYMode[2].Update((int)Av1PredictionMode.Smooth); |
||||
|
source.IntraInter[3].Update(1); |
||||
|
|
||||
|
destination.CopyFrom(source); |
||||
|
|
||||
|
Assert.Equal(source.FrameYMode[2][0], destination.FrameYMode[2][0]); |
||||
|
Assert.Equal(source.IntraInter[3][0], destination.IntraInter[3][0]); |
||||
|
|
||||
|
source.FrameYMode[2].Update((int)Av1PredictionMode.Paeth); |
||||
|
source.IntraInter[3].Update(0); |
||||
|
|
||||
|
Assert.NotEqual(source.FrameYMode[2][0], destination.FrameYMode[2][0]); |
||||
|
Assert.NotEqual(source.IntraInter[3][0], destination.IntraInter[3][0]); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that a published frame snapshot preserves adapted thresholds but resets their update-rate history.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void FrameEntropySnapshotResetsInterFrameIntraUpdateCounts() |
||||
|
{ |
||||
|
const int updateCount = 20; |
||||
|
Av1FrameEntropyContext source = new(0); |
||||
|
Av1FrameEntropyContext snapshot = new(0); |
||||
|
|
||||
|
for (int i = 0; i < updateCount; i++) |
||||
|
{ |
||||
|
source.FrameYMode[1].Update((int)Av1PredictionMode.Vertical); |
||||
|
source.IntraInter[1].Update(1); |
||||
|
} |
||||
|
|
||||
|
source.SnapshotTo(snapshot); |
||||
|
|
||||
|
Assert.Equal(source.FrameYMode[1][0], snapshot.FrameYMode[1][0]); |
||||
|
Assert.Equal(source.IntraInter[1][0], snapshot.IntraInter[1][0]); |
||||
|
|
||||
|
// The source retains twenty observations while the published snapshot restarts at zero. Applying the same
|
||||
|
// symbol therefore moves identical thresholds by different update rates only when reset wiring is complete.
|
||||
|
source.FrameYMode[1].Update((int)Av1PredictionMode.DC); |
||||
|
snapshot.FrameYMode[1].Update((int)Av1PredictionMode.DC); |
||||
|
source.IntraInter[1].Update(0); |
||||
|
snapshot.IntraInter[1].Update(0); |
||||
|
|
||||
|
Assert.NotEqual(source.FrameYMode[1][0], snapshot.FrameYMode[1][0]); |
||||
|
Assert.NotEqual(source.IntraInter[1][0], snapshot.IntraInter[1][0]); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides the normative AV1 size-group table in block-size enumeration order.
|
||||
|
/// </summary>
|
||||
|
/// <returns>Every decoded block size paired with its luma-mode size group.</returns>
|
||||
|
public static TheoryData<int, int> GetBlockSizeGroups() |
||||
|
{ |
||||
|
// This is size_group_lookup from AV1 section 9.3 and libaom common_data.h. Keeping expected values explicit
|
||||
|
// ensures that the test does not reproduce the production formula it is intended to verify.
|
||||
|
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<int, int> result = []; |
||||
|
|
||||
|
for (int blockSize = 0; blockSize < sizeGroups.Length; blockSize++) |
||||
|
{ |
||||
|
result.Add(blockSize, sizeGroups[blockSize]); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates decoded neighbor state with either an intra or inter primary reference.
|
||||
|
/// </summary>
|
||||
|
/// <param name="isInter">Whether the neighbor uses inter prediction.</param>
|
||||
|
/// <returns>The initialized block mode state.</returns>
|
||||
|
private static Av1BlockModeInfo CreateModeInfo(bool isInter) |
||||
|
{ |
||||
|
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block4x4, Point.Empty); |
||||
|
modeInfo.ReferenceFrames[0] = isInter ? Av1ReferenceFrameType.Last : Av1ReferenceFrameType.Intra; |
||||
|
modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.None; |
||||
|
return modeInfo; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,133 @@ |
|||||
|
// 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.Formats.Heif.Av1.OpenBitstreamUnit; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies the common inter-frame mode prefix and its intra-coded-block branch.
|
||||
|
/// </summary>
|
||||
|
[Trait("Format", "Avif")] |
||||
|
public class Av1InterFrameModeInfoTests |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Verifies that an inter frame can select an intra-coded block using the block-size luma distribution.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void ReadInterFrameModeInfoReadsIntraCodedBlock() |
||||
|
{ |
||||
|
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); |
||||
|
ObuFrameHeader frameHeader = CreateFrameHeader(); |
||||
|
using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); |
||||
|
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty); |
||||
|
Av1SuperblockInfo superblockInfo = new(tileReader.FrameInfo, Point.Empty); |
||||
|
Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None); |
||||
|
|
||||
|
Av1Distribution skip = Av1DefaultDistributions.Skip[0]; |
||||
|
Av1Distribution intraInter = Av1DefaultDistributions.IntraInter[0]; |
||||
|
Av1Distribution yMode = Av1DefaultDistributions.FrameYMode[1]; |
||||
|
using Av1SymbolWriter writer = new(Configuration.Default, 1, updateCdf: true); |
||||
|
writer.WriteSymbol(false, skip); |
||||
|
writer.WriteSymbol(false, intraInter); |
||||
|
writer.WriteSymbol((int)Av1PredictionMode.DC, yMode); |
||||
|
using IMemoryOwner<byte> encoded = writer.Exit(); |
||||
|
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); |
||||
|
|
||||
|
tileReader.ReadInterFrameModeInfo(ref decoder, ref partitionInfo); |
||||
|
|
||||
|
Assert.False(modeInfo.SkipMode); |
||||
|
Assert.False(modeInfo.Skip); |
||||
|
Assert.Equal(Av1ReferenceFrameType.Intra, modeInfo.ReferenceFrames[0]); |
||||
|
Assert.Equal(Av1ReferenceFrameType.None, modeInfo.ReferenceFrames[1]); |
||||
|
Assert.Equal(Av1PredictionMode.DC, modeInfo.YMode); |
||||
|
Assert.Equal(Av1ChromaPredictionMode.DC, modeInfo.UvMode); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that skip mode omits the residual-skip and intra-inter symbols and marks the block as inter coded.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void ReadInterFrameModeInfoSkipModeForcesInterBlockAndResidualSkip() |
||||
|
{ |
||||
|
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); |
||||
|
ObuFrameHeader frameHeader = CreateFrameHeader(); |
||||
|
frameHeader.SkipModeParameters.SkipModeFlag = true; |
||||
|
using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); |
||||
|
Av1BlockModeInfo aboveModeInfo = new(Av1BlockSize.Block8x8, Point.Empty) { SkipMode = true }; |
||||
|
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty); |
||||
|
|
||||
|
Av1Distribution skipMode = Av1DefaultDistributions.SkipMode[1]; |
||||
|
using Av1SymbolWriter writer = new(Configuration.Default, 1, updateCdf: true); |
||||
|
writer.WriteSymbol(true, skipMode); |
||||
|
using IMemoryOwner<byte> encoded = writer.Exit(); |
||||
|
Memory<byte> encodedMemory = encoded.Memory; |
||||
|
|
||||
|
Assert.Throws<NotSupportedException>(() => ReadInterFrameModeInfo(tileReader, encodedMemory, modeInfo, aboveModeInfo)); |
||||
|
Assert.True(modeInfo.SkipMode); |
||||
|
Assert.True(modeInfo.Skip); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Invokes the ref-struct mode parser for exception assertions that cannot capture its parameters directly.
|
||||
|
/// </summary>
|
||||
|
/// <param name="tileReader">The tile reader.</param>
|
||||
|
/// <param name="encoded">The range-coded block-prefix symbols.</param>
|
||||
|
/// <param name="modeInfo">The current coding block.</param>
|
||||
|
/// <param name="aboveModeInfo">The available above block supplying skip-mode context.</param>
|
||||
|
private static void ReadInterFrameModeInfo( |
||||
|
Av1TileReader tileReader, |
||||
|
Memory<byte> encoded, |
||||
|
Av1BlockModeInfo modeInfo, |
||||
|
Av1BlockModeInfo aboveModeInfo) |
||||
|
{ |
||||
|
Av1SuperblockInfo superblockInfo = new(tileReader.FrameInfo, Point.Empty); |
||||
|
Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None) |
||||
|
{ |
||||
|
AvailableAbove = true, |
||||
|
AboveModeInfo = aboveModeInfo, |
||||
|
}; |
||||
|
|
||||
|
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Span, 0, updateCdf: true); |
||||
|
tileReader.ReadInterFrameModeInfo(ref decoder, ref partitionInfo); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates the monochrome 64x64 sequence geometry used by direct mode-prefix tests.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The initialized sequence header.</returns>
|
||||
|
private static ObuSequenceHeader CreateSequenceHeader() |
||||
|
=> new() |
||||
|
{ |
||||
|
MaxFrameWidth = 64, |
||||
|
MaxFrameHeight = 64, |
||||
|
Use128x128Superblock = false, |
||||
|
EnableCdef = false, |
||||
|
EnableFilterIntra = false, |
||||
|
ColorConfig = new ObuColorConfig |
||||
|
{ |
||||
|
IsMonochrome = true, |
||||
|
BitDepth = Av1BitDepth.EightBit, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates an inter-frame header whose optional block-prefix tools are disabled.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The initialized frame header.</returns>
|
||||
|
private static ObuFrameHeader CreateFrameHeader() |
||||
|
=> new() |
||||
|
{ |
||||
|
FrameType = ObuFrameType.InterFrame, |
||||
|
ModeInfoColumnCount = 16, |
||||
|
ModeInfoRowCount = 16, |
||||
|
CodedLossless = true, |
||||
|
AllowScreenContentTools = false, |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,275 @@ |
|||||
|
// 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.Formats.Heif.Av1.OpenBitstreamUnit; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies AV1 temporal segment-map prediction against libaom's decoder rules.
|
||||
|
/// </summary>
|
||||
|
[Trait("Format", "Avif")] |
||||
|
public class Av1TemporalSegmentationTests |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Verifies temporal segment-map prediction symbols through each of AV1's three neighbor contexts.
|
||||
|
/// </summary>
|
||||
|
/// <param name="context">The sum of predicted above and left neighbors.</param>
|
||||
|
[Theory] |
||||
|
[InlineData(0)] |
||||
|
[InlineData(1)] |
||||
|
[InlineData(2)] |
||||
|
public void SegmentIdPredictedRoundTrips(int context) |
||||
|
{ |
||||
|
bool[] expected = [false, true, true, false, true, false]; |
||||
|
using Av1SymbolWriter writer = new(Configuration.Default, 1, updateCdf: true); |
||||
|
Av1Distribution writerDistribution = Av1DefaultDistributions.SegmentIdPredicted[context]; |
||||
|
|
||||
|
foreach (bool value in expected) |
||||
|
{ |
||||
|
writer.WriteSymbol(value, writerDistribution); |
||||
|
} |
||||
|
|
||||
|
using IMemoryOwner<byte> encoded = writer.Exit(); |
||||
|
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); |
||||
|
|
||||
|
foreach (bool value in expected) |
||||
|
{ |
||||
|
Assert.Equal(value, decoder.ReadSegmentIdPredicted(context)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that the frame entropy graph copies adapted temporal-prediction state instead of restoring defaults.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void FrameEntropyCopyRetainsAdaptedSegmentPrediction() |
||||
|
{ |
||||
|
Av1FrameEntropyContext source = new(0); |
||||
|
Av1FrameEntropyContext destination = new(0); |
||||
|
source.SegmentIdPredicted[2].Update(1); |
||||
|
|
||||
|
destination.CopyFrom(source); |
||||
|
|
||||
|
Assert.Equal(source.SegmentIdPredicted[2][0], destination.SegmentIdPredicted[2][0]); |
||||
|
Assert.NotEqual(16384U, destination.SegmentIdPredicted[2][0]); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that only neighboring blocks which selected temporal prediction contribute to the binary CDF context.
|
||||
|
/// </summary>
|
||||
|
/// <param name="hasAbove">Whether an above block is available.</param>
|
||||
|
/// <param name="abovePredicted">Whether the available above block selected temporal prediction.</param>
|
||||
|
/// <param name="hasLeft">Whether a left block is available.</param>
|
||||
|
/// <param name="leftPredicted">Whether the available left block selected temporal prediction.</param>
|
||||
|
/// <param name="expected">The expected context in the inclusive range zero through two.</param>
|
||||
|
[Theory] |
||||
|
[InlineData(false, false, false, false, 0)] |
||||
|
[InlineData(true, false, true, false, 0)] |
||||
|
[InlineData(true, true, false, false, 1)] |
||||
|
[InlineData(false, false, true, true, 1)] |
||||
|
[InlineData(true, true, true, true, 2)] |
||||
|
public void SegmentPredictionContextCountsPredictedNeighbors( |
||||
|
bool hasAbove, |
||||
|
bool abovePredicted, |
||||
|
bool hasLeft, |
||||
|
bool leftPredicted, |
||||
|
int expected) |
||||
|
{ |
||||
|
Av1BlockModeInfo aboveModeInfo = hasAbove ? CreateModeInfo(abovePredicted) : null; |
||||
|
Av1BlockModeInfo leftModeInfo = hasLeft ? CreateModeInfo(leftPredicted) : null; |
||||
|
|
||||
|
int actual = Av1SymbolContextHelper.GetSegmentIdPredictedContext(aboveModeInfo, leftModeInfo); |
||||
|
|
||||
|
Assert.Equal(expected, actual); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that a temporal-prediction symbol selects the minimum retained segment across the complete block and writes it to the current map.
|
||||
|
/// </summary>
|
||||
|
/// <param name="segmentIdPrecedesSkip">Whether segment syntax precedes the residual-skip flag.</param>
|
||||
|
[Theory] |
||||
|
[InlineData(false)] |
||||
|
[InlineData(true)] |
||||
|
public void ReadInterSegmentIdUsesRetainedPrimaryMap(bool segmentIdPrecedesSkip) |
||||
|
{ |
||||
|
const int modeInfoSize = 16; |
||||
|
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(64, 64); |
||||
|
ObuFrameHeader primaryHeader = CreateFrameHeader(modeInfoSize, modeInfoSize, segmentationUpdateMap: 1, segmentationTemporalUpdate: 0); |
||||
|
Av1FrameInfo primaryFrameInfo = new(sequenceHeader); |
||||
|
primaryFrameInfo.InitializeSegmentIds(primaryHeader, null); |
||||
|
primaryFrameInfo.SetSegmentId(Av1BlockSize.Block64x64, Point.Empty, 6); |
||||
|
|
||||
|
// The target 16x16 block covers sixteen 4x4 cells. One lower retained value proves that prediction scans the
|
||||
|
// complete clipped coverage rather than reading only the block origin.
|
||||
|
Point lowSegmentPosition = new(4, 4); |
||||
|
primaryFrameInfo.SetSegmentId(Av1BlockSize.Block4x4, lowSegmentPosition, 2); |
||||
|
|
||||
|
// The production reference store owns complete reconstructed frames. A minimal monochrome frame buffer keeps
|
||||
|
// this test on the real ownership path while the assertions remain confined to retained segmentation state.
|
||||
|
Av1FrameBuffer<byte> primaryBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); |
||||
|
Av1ReferenceFrame primaryFrame = new(primaryBuffer, primaryHeader, primaryFrameInfo); |
||||
|
using Av1ReferenceFrameStore referenceFrames = new(); |
||||
|
referenceFrames.Commit(1, primaryFrame, showFrame: false); |
||||
|
|
||||
|
ObuFrameHeader currentHeader = CreateFrameHeader(modeInfoSize, modeInfoSize, segmentationUpdateMap: 1, segmentationTemporalUpdate: 1); |
||||
|
currentHeader.FrameType = ObuFrameType.InterFrame; |
||||
|
currentHeader.PrimaryReferenceFrame = 0; |
||||
|
currentHeader.PrimaryReferenceSlot = 0; |
||||
|
currentHeader.SegmentationParameters.SegmentIdPrecedesSkip = segmentIdPrecedesSkip; |
||||
|
Av1FrameEntropyContexts entropyContexts = new(0); |
||||
|
using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, currentHeader, entropyContexts, null, referenceFrames); |
||||
|
|
||||
|
Point blockPosition = new(2, 2); |
||||
|
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, blockPosition); |
||||
|
Av1SuperblockInfo superblockInfo = new(tileReader.FrameInfo, Point.Empty); |
||||
|
Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None) |
||||
|
{ |
||||
|
ColumnIndex = blockPosition.X, |
||||
|
RowIndex = blockPosition.Y, |
||||
|
AvailableAbove = true, |
||||
|
AvailableLeft = true, |
||||
|
AboveModeInfo = CreateModeInfo(predicted: true), |
||||
|
LeftModeInfo = CreateModeInfo(predicted: false) |
||||
|
}; |
||||
|
|
||||
|
const int predictionContext = 1; |
||||
|
using Av1SymbolWriter writer = new(Configuration.Default, 1, updateCdf: true); |
||||
|
writer.WriteSymbol(true, Av1DefaultDistributions.SegmentIdPredicted[predictionContext]); |
||||
|
using IMemoryOwner<byte> encoded = writer.Exit(); |
||||
|
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); |
||||
|
|
||||
|
tileReader.ReadInterSegmentId(ref decoder, ref partitionInfo, beforeSkip: segmentIdPrecedesSkip); |
||||
|
|
||||
|
Assert.True(modeInfo.SegmentIdPredicted); |
||||
|
Assert.Equal(2, modeInfo.SegmentId); |
||||
|
for (int row = blockPosition.Y; row < blockPosition.Y + modeInfo.BlockSize.Get4x4HighCount(); row++) |
||||
|
{ |
||||
|
for (int column = blockPosition.X; column < blockPosition.X + modeInfo.BlockSize.Get4x4WideCount(); column++) |
||||
|
{ |
||||
|
Assert.Equal(2, tileReader.FrameInfo.GetSegmentId(row, column)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that a skipped inter block uses the spatial predictor without reading a temporal-prediction symbol.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void SkippedInterBlockClearsTemporalPredictionAndUsesSpatialSegment() |
||||
|
{ |
||||
|
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(64, 64); |
||||
|
ObuFrameHeader frameHeader = CreateFrameHeader(16, 16, segmentationUpdateMap: 1, segmentationTemporalUpdate: 1); |
||||
|
using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); |
||||
|
Point blockPosition = new(2, 2); |
||||
|
|
||||
|
// Three equal spatial neighbors select segment three without consuming a spatial segment symbol. The block is
|
||||
|
// initialized as predicted to prove that the normative skipped-block branch explicitly clears the stale flag.
|
||||
|
tileReader.FrameInfo.SetSegmentId(Av1BlockSize.Block4x4, new Point(1, 1), 3); |
||||
|
tileReader.FrameInfo.SetSegmentId(Av1BlockSize.Block4x4, new Point(2, 1), 3); |
||||
|
tileReader.FrameInfo.SetSegmentId(Av1BlockSize.Block4x4, new Point(1, 2), 3); |
||||
|
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, blockPosition) |
||||
|
{ |
||||
|
Skip = true, |
||||
|
SegmentIdPredicted = true |
||||
|
}; |
||||
|
|
||||
|
Av1SuperblockInfo superblockInfo = new(tileReader.FrameInfo, Point.Empty); |
||||
|
Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None) |
||||
|
{ |
||||
|
ColumnIndex = blockPosition.X, |
||||
|
RowIndex = blockPosition.Y, |
||||
|
AvailableAbove = true, |
||||
|
AvailableLeft = true |
||||
|
}; |
||||
|
|
||||
|
using Av1SymbolWriter writer = new(Configuration.Default, 1, updateCdf: true); |
||||
|
using IMemoryOwner<byte> encoded = writer.Exit(); |
||||
|
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); |
||||
|
|
||||
|
tileReader.ReadInterSegmentId(ref decoder, ref partitionInfo, beforeSkip: false); |
||||
|
|
||||
|
Assert.False(modeInfo.SegmentIdPredicted); |
||||
|
Assert.Equal(3, modeInfo.SegmentId); |
||||
|
Assert.Equal(3, tileReader.FrameInfo.GetSegmentId(blockPosition.Y, blockPosition.X)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that retained segmentation maps with different mode-info geometry are unavailable for temporal prediction.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void PredictedSegmentIdIsZeroForMismatchedPrimaryGeometry() |
||||
|
{ |
||||
|
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(64, 64); |
||||
|
ObuFrameHeader currentHeader = CreateFrameHeader(16, 16, segmentationUpdateMap: 1, segmentationTemporalUpdate: 1); |
||||
|
ObuFrameHeader primaryHeader = CreateFrameHeader(8, 16, segmentationUpdateMap: 1, segmentationTemporalUpdate: 0); |
||||
|
Av1FrameInfo currentFrameInfo = new(sequenceHeader); |
||||
|
Av1FrameInfo primaryFrameInfo = new(sequenceHeader); |
||||
|
currentFrameInfo.InitializeSegmentIds(currentHeader, null); |
||||
|
primaryFrameInfo.InitializeSegmentIds(primaryHeader, null); |
||||
|
primaryFrameInfo.SetSegmentId(Av1BlockSize.Block32x64, Point.Empty, 5); |
||||
|
|
||||
|
int actual = currentFrameInfo.GetPredictedSegmentId(primaryFrameInfo, Av1BlockSize.Block16x16, Point.Empty); |
||||
|
|
||||
|
Assert.Equal(0, actual); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates block mode state with the requested temporal segment-prediction flag.
|
||||
|
/// </summary>
|
||||
|
/// <param name="predicted">Whether the block selected its segment identifier from the retained map.</param>
|
||||
|
/// <returns>The initialized block mode state.</returns>
|
||||
|
private static Av1BlockModeInfo CreateModeInfo(bool predicted) |
||||
|
=> new(Av1BlockSize.Block4x4, Point.Empty) { SegmentIdPredicted = predicted }; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates the fixed 64x64-superblock sequence geometry used by segmentation-map tests.
|
||||
|
/// </summary>
|
||||
|
/// <param name="width">The maximum coded width in pixels.</param>
|
||||
|
/// <param name="height">The maximum coded height in pixels.</param>
|
||||
|
/// <returns>The initialized monochrome sequence header.</returns>
|
||||
|
private static ObuSequenceHeader CreateSequenceHeader(int width, int height) |
||||
|
=> new() |
||||
|
{ |
||||
|
MaxFrameWidth = width, |
||||
|
MaxFrameHeight = height, |
||||
|
Use128x128Superblock = false, |
||||
|
ColorConfig = new ObuColorConfig |
||||
|
{ |
||||
|
IsMonochrome = true, |
||||
|
BitDepth = Av1BitDepth.EightBit |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates the frame geometry and segmentation controls used by direct map tests.
|
||||
|
/// </summary>
|
||||
|
/// <param name="modeInfoColumnCount">The active width in 4x4 mode-info units.</param>
|
||||
|
/// <param name="modeInfoRowCount">The active height in 4x4 mode-info units.</param>
|
||||
|
/// <param name="segmentationUpdateMap">Whether the frame updates its segment map.</param>
|
||||
|
/// <param name="segmentationTemporalUpdate">Whether map updates may select the retained primary map.</param>
|
||||
|
/// <returns>The initialized frame header.</returns>
|
||||
|
private static ObuFrameHeader CreateFrameHeader( |
||||
|
int modeInfoColumnCount, |
||||
|
int modeInfoRowCount, |
||||
|
int segmentationUpdateMap, |
||||
|
int segmentationTemporalUpdate) |
||||
|
=> new() |
||||
|
{ |
||||
|
ModeInfoColumnCount = modeInfoColumnCount, |
||||
|
ModeInfoRowCount = modeInfoRowCount, |
||||
|
SegmentationParameters = new ObuSegmentationParameters |
||||
|
{ |
||||
|
Enabled = true, |
||||
|
LastActiveSegmentId = Av1Constants.MaxSegmentCount - 1, |
||||
|
SegmentationUpdateMap = segmentationUpdateMap, |
||||
|
SegmentationTemporalUpdate = segmentationTemporalUpdate |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,246 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies the frame-level derivation of AV1 skip-mode reference pairs.
|
||||
|
/// </summary>
|
||||
|
[Trait("Format", "Avif")] |
||||
|
public class ObuSkipModeParametersTests |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The public theory-data representation of <see cref="ObuFrameType.KeyFrame"/>.
|
||||
|
/// </summary>
|
||||
|
private const int KeyFrameValue = (int)ObuFrameType.KeyFrame; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The public theory-data representation of <see cref="ObuFrameType.InterFrame"/>.
|
||||
|
/// </summary>
|
||||
|
private const int InterFrameValue = (int)ObuFrameType.InterFrame; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The public theory-data representation of <see cref="ObuReferenceMode.SingleReference"/>.
|
||||
|
/// </summary>
|
||||
|
private const int SingleReferenceValue = (int)ObuReferenceMode.SingleReference; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The public theory-data representation of <see cref="ObuReferenceMode.ReferenceModeSelect"/>.
|
||||
|
/// </summary>
|
||||
|
private const int ReferenceModeSelectValue = (int)ObuReferenceMode.ReferenceModeSelect; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies signed order-hint distances across the modulo-domain boundary.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void GetRelativeDistanceWrapsWithinConfiguredDomain() |
||||
|
{ |
||||
|
ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); |
||||
|
|
||||
|
Assert.Equal(-2, orderHintInfo.GetRelativeDistance(15, 1)); |
||||
|
Assert.Equal(2, orderHintInfo.GetRelativeDistance(1, 15)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that disabled order hints have no temporal ordering.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void GetRelativeDistanceReturnsZeroWhenOrderHintsAreDisabled() |
||||
|
{ |
||||
|
ObuOrderHintInfo orderHintInfo = new(); |
||||
|
|
||||
|
Assert.Equal(0, orderHintInfo.GetRelativeDistance(15, 1)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that skip mode selects the nearest past and future canonical reference roles.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void DeriveSelectsNearestForwardAndBackwardReferences() |
||||
|
{ |
||||
|
ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); |
||||
|
ObuFrameHeader frameHeader = CreateInterFrame(8, [7, 3, 6, 2, 10, 12, 15]); |
||||
|
|
||||
|
frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); |
||||
|
|
||||
|
Assert.True(frameHeader.SkipModeParameters.SkipModeAllowed); |
||||
|
Assert.Equal(Av1ReferenceFrameType.Last, frameHeader.SkipModeParameters.FirstReferenceFrame); |
||||
|
Assert.Equal(Av1ReferenceFrameType.Backward, frameHeader.SkipModeParameters.SecondReferenceFrame); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that the derived pair identifies canonical roles rather than their physical reference-map slots.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void DeriveOrdersCanonicalRolesIndependentlyOfMappedSlots() |
||||
|
{ |
||||
|
ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); |
||||
|
ObuFrameHeader frameHeader = CreateInterFrame(8, [7, 3, 6, 2, 10, 12, 15]); |
||||
|
Span<uint> referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); |
||||
|
Span<uint> referenceOrderHints = frameHeader.GetReferenceOrderHints(); |
||||
|
|
||||
|
// Several canonical roles deliberately share physical slot seven. The first matching role remains LAST, while
|
||||
|
// the future BWDREF role maps to slot four; neither physical slot number becomes part of the derived pair.
|
||||
|
referenceFrameIndices.Fill(7); |
||||
|
referenceFrameIndices[(int)Av1ReferenceFrameType.Backward - 1] = 4; |
||||
|
referenceOrderHints[7] = 7; |
||||
|
|
||||
|
frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); |
||||
|
|
||||
|
Assert.True(frameHeader.SkipModeParameters.SkipModeAllowed); |
||||
|
Assert.Equal(Av1ReferenceFrameType.Last, frameHeader.SkipModeParameters.FirstReferenceFrame); |
||||
|
Assert.Equal(Av1ReferenceFrameType.Backward, frameHeader.SkipModeParameters.SecondReferenceFrame); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that a frame with only future references cannot use skip mode.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void DeriveDisallowsSkipModeWithoutForwardReference() |
||||
|
{ |
||||
|
ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); |
||||
|
ObuFrameHeader frameHeader = CreateInterFrame(8, [9, 10, 11, 12, 13, 14, 15]); |
||||
|
|
||||
|
frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); |
||||
|
|
||||
|
Assert.False(frameHeader.SkipModeParameters.SkipModeAllowed); |
||||
|
Assert.Equal(Av1ReferenceFrameType.None, frameHeader.SkipModeParameters.FirstReferenceFrame); |
||||
|
Assert.Equal(Av1ReferenceFrameType.None, frameHeader.SkipModeParameters.SecondReferenceFrame); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that a forward-only frame selects the two closest distinct past reference orders.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void DeriveSelectsTwoNearestForwardReferencesWhenNoBackwardReferenceExists() |
||||
|
{ |
||||
|
ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); |
||||
|
ObuFrameHeader frameHeader = CreateInterFrame(8, [7, 3, 6, 2, 1, 5, 4]); |
||||
|
|
||||
|
frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); |
||||
|
|
||||
|
Assert.True(frameHeader.SkipModeParameters.SkipModeAllowed); |
||||
|
Assert.Equal(Av1ReferenceFrameType.Last, frameHeader.SkipModeParameters.FirstReferenceFrame); |
||||
|
Assert.Equal(Av1ReferenceFrameType.Last3, frameHeader.SkipModeParameters.SecondReferenceFrame); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that modulo wraparound participates in nearest-reference selection.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void DeriveSelectsReferencesAcrossOrderHintWraparound() |
||||
|
{ |
||||
|
ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); |
||||
|
ObuFrameHeader frameHeader = CreateInterFrame(1, [12, 15, 11, 10, 2, 5, 7]); |
||||
|
|
||||
|
frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); |
||||
|
|
||||
|
Assert.True(frameHeader.SkipModeParameters.SkipModeAllowed); |
||||
|
Assert.Equal(Av1ReferenceFrameType.Last2, frameHeader.SkipModeParameters.FirstReferenceFrame); |
||||
|
Assert.Equal(Av1ReferenceFrameType.Backward, frameHeader.SkipModeParameters.SecondReferenceFrame); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that skip mode remains unavailable without two temporally distinct usable reference orders.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void DeriveDisallowsSkipModeWithoutReferencePair() |
||||
|
{ |
||||
|
ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); |
||||
|
ObuFrameHeader frameHeader = CreateInterFrame(8, [7, 8, 8, 8, 8, 8, 8]); |
||||
|
|
||||
|
frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); |
||||
|
|
||||
|
Assert.False(frameHeader.SkipModeParameters.SkipModeAllowed); |
||||
|
Assert.Equal(Av1ReferenceFrameType.None, frameHeader.SkipModeParameters.FirstReferenceFrame); |
||||
|
Assert.Equal(Av1ReferenceFrameType.None, frameHeader.SkipModeParameters.SecondReferenceFrame); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that deriving an ineligible frame clears a reference pair retained by an earlier derivation.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void DeriveClearsPreviousReferencePair() |
||||
|
{ |
||||
|
ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); |
||||
|
ObuFrameHeader frameHeader = CreateInterFrame(8, [7, 3, 6, 2, 10, 12, 15]); |
||||
|
frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); |
||||
|
|
||||
|
frameHeader.ReferenceMode = ObuReferenceMode.SingleReference; |
||||
|
frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); |
||||
|
|
||||
|
Assert.False(frameHeader.SkipModeParameters.SkipModeAllowed); |
||||
|
Assert.Equal(Av1ReferenceFrameType.None, frameHeader.SkipModeParameters.FirstReferenceFrame); |
||||
|
Assert.Equal(Av1ReferenceFrameType.None, frameHeader.SkipModeParameters.SecondReferenceFrame); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies the frame modes for which the AV1 syntax forbids skip-mode signaling.
|
||||
|
/// </summary>
|
||||
|
/// <param name="enableOrderHint">Whether the sequence enables order hints.</param>
|
||||
|
/// <param name="frameTypeValue">The numeric coded-frame-type value.</param>
|
||||
|
/// <param name="referenceModeValue">The numeric frame-level reference-mode value.</param>
|
||||
|
[Theory] |
||||
|
[InlineData(false, InterFrameValue, ReferenceModeSelectValue)] |
||||
|
[InlineData(true, KeyFrameValue, ReferenceModeSelectValue)] |
||||
|
[InlineData(true, InterFrameValue, SingleReferenceValue)] |
||||
|
public void DeriveDisallowsSkipModeForIneligibleFrameSyntax( |
||||
|
bool enableOrderHint, |
||||
|
int frameTypeValue, |
||||
|
int referenceModeValue) |
||||
|
{ |
||||
|
ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); |
||||
|
orderHintInfo.EnableOrderHint = enableOrderHint; |
||||
|
ObuFrameHeader frameHeader = CreateInterFrame(8, [7, 3, 6, 2, 10, 12, 15]); |
||||
|
frameHeader.FrameType = (ObuFrameType)frameTypeValue; |
||||
|
frameHeader.ReferenceMode = (ObuReferenceMode)referenceModeValue; |
||||
|
|
||||
|
frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); |
||||
|
|
||||
|
Assert.False(frameHeader.SkipModeParameters.SkipModeAllowed); |
||||
|
Assert.Equal(Av1ReferenceFrameType.None, frameHeader.SkipModeParameters.FirstReferenceFrame); |
||||
|
Assert.Equal(Av1ReferenceFrameType.None, frameHeader.SkipModeParameters.SecondReferenceFrame); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates the four-bit modulo order-hint configuration used by the derivation scenarios.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The enabled order-hint configuration.</returns>
|
||||
|
private static ObuOrderHintInfo CreateOrderHintInfo() |
||||
|
=> new() |
||||
|
{ |
||||
|
EnableOrderHint = true, |
||||
|
OrderHintBits = 4, |
||||
|
}; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates an inter frame whose seven canonical roles map directly to slots zero through six.
|
||||
|
/// </summary>
|
||||
|
/// <param name="currentOrderHint">The current frame order hint.</param>
|
||||
|
/// <param name="referenceOrderHints">The order hint selected by each canonical role.</param>
|
||||
|
/// <returns>The initialized inter-frame header.</returns>
|
||||
|
private static ObuFrameHeader CreateInterFrame(uint currentOrderHint, ReadOnlySpan<uint> referenceOrderHints) |
||||
|
{ |
||||
|
ObuFrameHeader frameHeader = new() |
||||
|
{ |
||||
|
FrameType = ObuFrameType.InterFrame, |
||||
|
OrderHint = currentOrderHint, |
||||
|
ReferenceMode = ObuReferenceMode.ReferenceModeSelect, |
||||
|
}; |
||||
|
|
||||
|
Span<uint> referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); |
||||
|
Span<uint> referenceMapOrderHints = frameHeader.GetReferenceOrderHints(); |
||||
|
|
||||
|
for (int referenceIndex = 0; referenceIndex < Av1Constants.ReferencesPerFrame; referenceIndex++) |
||||
|
{ |
||||
|
referenceFrameIndices[referenceIndex] = (uint)referenceIndex; |
||||
|
referenceMapOrderHints[referenceIndex] = referenceOrderHints[referenceIndex]; |
||||
|
} |
||||
|
|
||||
|
return frameHeader; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue