// 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.Pipeline.LoopFilter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Tests.Memory;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
///
/// Verifies AV1 frame-plane allocation contracts and constructor rollback ownership.
///
[Trait("Format", "Avif")]
public class Av1FrameBufferTests
{
///
/// Verifies that padded frame planes remain contiguous when the allocator would otherwise split the buffer.
///
[Fact]
public void ConstructorRequestsContiguousPaddedPlanes()
{
TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 10_000 };
Configuration configuration = Configuration.Default.Clone();
configuration.MemoryAllocator = allocator;
ObuSequenceHeader sequenceHeader = new()
{
MaxFrameWidth = 64,
MaxFrameHeight = 64,
ColorConfig = new ObuColorConfig
{
IsMonochrome = true,
BitDepth = Av1BitDepth.EightBit
}
};
using Av1FrameBuffer frameBuffer = new(
configuration,
sequenceHeader,
Av1ColorFormat.Yuv400,
false);
MemoryGroup memoryGroup = frameBuffer.BufferY!.FastMemoryGroup;
Assert.Equal(1, memoryGroup.Count);
Assert.True(memoryGroup.TotalLength > allocator.BufferCapacityInBytes);
}
///
/// Verifies that a failure while renting the final chroma plane releases every previously rented plane.
///
[Fact]
public void ConstructorFailureOnThirdPlaneReleasesEarlierPlanes()
{
FailingTestMemoryAllocator allocator = new(failureAllocationNumber: 3);
Configuration configuration = Configuration.Default.Clone();
configuration.MemoryAllocator = allocator;
ObuSequenceHeader sequenceHeader = new()
{
MaxFrameWidth = 1,
MaxFrameHeight = 1,
ColorConfig = new ObuColorConfig
{
IsMonochrome = false,
SubSamplingX = true,
SubSamplingY = true,
BitDepth = Av1BitDepth.EightBit
}
};
// All three padded planes fit in one backing owner each, making attempt three the Cr plane rent after Y and
// Cb have succeeded. The allocator log therefore contains exactly the two owners requiring rollback.
Assert.Throws(
() => new Av1FrameBuffer(configuration, sequenceHeader, Av1ColorFormat.Yuv420, false));
Assert.Equal(3, allocator.AllocationAttemptCount);
Assert.Equal(2, allocator.AllocationLog.Count);
Assert.Equal(2, allocator.ReturnLog.Count);
Assert.Equal(allocator.AllocationLog[0].HashCodeOfBuffer, allocator.ReturnLog[0].HashCodeOfBuffer);
Assert.Equal(allocator.AllocationLog[1].HashCodeOfBuffer, allocator.ReturnLog[1].HashCodeOfBuffer);
}
///
/// Verifies that a block-decoder workspace failure releases every workspace rented earlier in construction.
///
[Fact]
public void BlockDecoderConstructorFailureReleasesEarlierWorkspaces()
{
FailingTestMemoryAllocator allocator = new(failureAllocationNumber: 3);
Configuration configuration = Configuration.Default.Clone();
configuration.MemoryAllocator = allocator;
ObuSequenceHeader sequenceHeader = new()
{
MaxFrameWidth = 64,
MaxFrameHeight = 64,
Use128x128Superblock = false,
ColorConfig = new ObuColorConfig
{
IsMonochrome = true,
BitDepth = Av1BitDepth.EightBit
}
};
using Av1FrameBuffer frameBuffer = new(configuration, sequenceHeader, Av1ColorFormat.Yuv400, false);
ObuFrameHeader frameHeader = new();
Av1LoopFilterContext loopFilterContext = new(sequenceHeader);
Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader);
// The frame's luma plane is allocation attempt one. Resetting only the logs preserves that counter, so the
// inverse-quantization workspace succeeds on attempt two and the transform workspace fails on attempt three.
allocator.EnableNonThreadSafeLogging();
Assert.Throws(
() => new Av1BlockDecoder(sequenceHeader, frameHeader, frameBuffer, loopFilterContext, inverseQuantizer));
Assert.Equal(3, allocator.AllocationAttemptCount);
Assert.Single(allocator.AllocationLog);
Assert.Single(allocator.ReturnLog);
Assert.Equal(allocator.AllocationLog[0].HashCodeOfBuffer, allocator.ReturnLog[0].HashCodeOfBuffer);
}
///
/// Provides tracked plane owners until the configured allocation attempt fails.
///
private sealed class FailingTestMemoryAllocator : TestMemoryAllocator
{
private readonly int failureAllocationNumber;
private int allocationAttemptCount;
///
/// Initializes a new instance of the class.
///
/// The one-based allocation attempt that must fail.
public FailingTestMemoryAllocator(int failureAllocationNumber)
{
this.failureAllocationNumber = failureAllocationNumber;
this.EnableNonThreadSafeLogging();
}
///
/// Gets the number of backing-owner allocation attempts made through this allocator.
///
public int AllocationAttemptCount => this.allocationAttemptCount;
///
protected override AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None)
{
this.allocationAttemptCount++;
if (this.allocationAttemptCount == this.failureAllocationNumber)
{
// Fail before delegating so this attempt never creates or tracks an owner. Diagnostic counts then
// describe only the successfully published owners that the constructor is responsible for releasing.
throw new InvalidMemoryOperationException("The configured AV1 allocation failed.");
}
return base.AllocateCore(length, options);
}
}
}