Browse Source

Unwind failed AV1 encoder construction

pull/2633/head
James Jackson-South 4 weeks ago
parent
commit
c778217a9b
  1. 98
      HEIF_IMPLEMENTATION_PLAN.md
  2. 18
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
  3. 231
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs
  4. 427
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPictureBuffer.cs
  5. 75
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs

98
HEIF_IMPLEMENTATION_PLAN.md

File diff suppressed because one or more lines are too long

18
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs

@ -268,11 +268,21 @@ internal sealed class Av1SymbolEncoder : IDisposable
// Transform dimensions are bounded by the AV1 coefficient-coding rules, so the complete entropy scratch // Transform dimensions are bounded by the AV1 coefficient-coding rules, so the complete entropy scratch
// is known with the tile output capacity and remains valid for every transform in every sequence sample. // is known with the tile output capacity and remains valid for every transform in every sequence sample.
this.levels = new Av1LevelBuffer(configuration); this.levels = new Av1LevelBuffer(configuration);
this.coefficientContexts = try
configuration.MemoryAllocator.Allocate<sbyte>(MaximumCoefficientContextCount); {
this.coefficientContexts =
configuration.MemoryAllocator.Allocate<sbyte>(MaximumCoefficientContextCount);
this.writer = new(configuration, bufferLength, updateCdf); this.writer = new(configuration, bufferLength, updateCdf);
this.baseQIndex = qIndex; this.baseQIndex = qIndex;
}
catch
{
// The level buffer is already owned here; a later allocation failure cannot be unwound by the caller.
this.coefficientContexts?.Dispose();
this.levels.Dispose();
throw;
}
} }
/// <summary> /// <summary>

231
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs

@ -1519,44 +1519,54 @@ internal static class Av1FrameEncoder
encodeAlpha, encodeAlpha,
usesHighBitDepth); usesHighBitDepth);
bool allocateScreenContentState = effort >= 5; try
bool allocateIntraBlockCopySearch = {
allocateScreenContentState && bool allocateScreenContentState = effort >= 5;
!this.FrameHeader.CodedLossless; bool allocateIntraBlockCopySearch =
allocateScreenContentState &&
// Sequence geometry and maximum tool capacity are fixed before the first sample. Reusing this owner !this.FrameHeader.CodedLossless;
// avoids renting the complete mode grid and optional screen-content index for every frame.
this.PictureBuffer = new Av1EncoderPictureBuffer( // Sequence geometry and maximum tool capacity are fixed before the first sample. Reusing this owner
configuration, // avoids renting the complete mode grid and optional screen-content index for every frame.
this.SequenceHeader, this.PictureBuffer = new Av1EncoderPictureBuffer(
this.FrameHeader, configuration,
width, this.SequenceHeader,
height, this.FrameHeader,
disallow4x4AllFrames: !this.FrameHeader.CodedLossless && effort < 9, width,
allocateScreenContentState: allocateScreenContentState, height,
allocateMotionVectorState: true, disallow4x4AllFrames: !this.FrameHeader.CodedLossless && effort < 9,
allocateIntraBlockCopySearch: allocateIntraBlockCopySearch); allocateScreenContentState: allocateScreenContentState,
allocateMotionVectorState: true,
allocateIntraBlockCopySearch: allocateIntraBlockCopySearch);
this.Coefficients = new Av1EncoderCoefficientBuffer( this.Coefficients = new Av1EncoderCoefficientBuffer(
configuration, configuration,
this.SequenceHeader, this.SequenceHeader,
width, width,
height); height);
this.SuperblockWorkspace = new Av1EncoderSuperblockWorkspace(configuration); this.SuperblockWorkspace = new Av1EncoderSuperblockWorkspace(configuration);
this.TileWorkspace = new Av1EncoderTileWorkspace(this.FrameHeader, this.SuperblockWorkspace); this.TileWorkspace = new Av1EncoderTileWorkspace(this.FrameHeader, this.SuperblockWorkspace);
this.BlockWorkspace = new Av1EncoderBlockWorkspace(configuration); this.BlockWorkspace = new Av1EncoderBlockWorkspace(configuration);
// Tile probabilities adapt within a sample, while error-resilient frame headers prohibit carrying // Tile probabilities adapt within a sample, while error-resilient frame headers prohibit carrying
// those updates into the next sample. The retained encoder is therefore reset before each frame. // those updates into the next sample. The retained encoder is therefore reset before each frame.
this.SymbolEncoder = new Av1SymbolEncoder( this.SymbolEncoder = new Av1SymbolEncoder(
configuration, configuration,
this.TileBufferLength, this.TileBufferLength,
qIndex, qIndex,
updateCdf: true); updateCdf: true);
this.ObuWriter = new ObuWriter(configuration); this.ObuWriter = new ObuWriter(configuration);
}
catch
{
// The caller receives no encoder when construction fails. Release only completed common owners;
// derived frame construction has not started and must not be reached through virtual disposal.
this.DisposeResources();
throw;
}
} }
/// <summary> /// <summary>
@ -1637,13 +1647,7 @@ internal static class Av1FrameEncoder
public void Dispose() public void Dispose()
{ {
this.DisposeFrames(); this.DisposeFrames();
this.ConversionWorkspace.Dispose(); this.DisposeResources();
this.PictureBuffer.Dispose();
this.Coefficients.Dispose();
this.SuperblockWorkspace.Dispose();
this.BlockWorkspace.Dispose();
this.ObuWriter.Dispose();
this.SymbolEncoder.Dispose();
} }
/// <summary> /// <summary>
@ -1657,6 +1661,19 @@ internal static class Av1FrameEncoder
ObuFrameType frameType, ObuFrameType frameType,
bool writeSequenceHeader) bool writeSequenceHeader)
where TPixel : unmanaged, IPixel<TPixel>; where TPixel : unmanaged, IPixel<TPixel>;
private void DisposeResources()
{
// Construction can stop between any two allocations. Successful instances have every owner;
// failed constructors retain only the prefix completed before the allocator rejected a request.
this.ObuWriter?.Dispose();
this.SymbolEncoder?.Dispose();
this.BlockWorkspace?.Dispose();
this.SuperblockWorkspace?.Dispose();
this.Coefficients?.Dispose();
this.PictureBuffer?.Dispose();
this.ConversionWorkspace.Dispose();
}
} }
private sealed class ByteSequenceEncoder : SequenceEncoder private sealed class ByteSequenceEncoder : SequenceEncoder
@ -1683,40 +1700,50 @@ internal static class Av1FrameEncoder
encodeAlpha, encodeAlpha,
usesHighBitDepth: false) usesHighBitDepth: false)
{ {
Av1ColorFormat colorFormat = colorConfig.GetColorFormat(); try
this.source = new( {
configuration, Av1ColorFormat colorFormat = colorConfig.GetColorFormat();
width, this.source = new(
height, configuration,
ByteSampleBitDepth, width,
colorFormat, height,
CenteredChromaSamplePosition, ByteSampleBitDepth,
CenteredChromaSamplePosition); colorFormat,
CenteredChromaSamplePosition,
CenteredChromaSamplePosition);
this.reference = new( this.reference = new(
configuration, configuration,
width, width,
height, height,
ByteSampleBitDepth, ByteSampleBitDepth,
colorFormat, colorFormat,
CenteredChromaSamplePosition, CenteredChromaSamplePosition,
CenteredChromaSamplePosition); CenteredChromaSamplePosition);
this.reconstruction = new( this.reconstruction = new(
configuration, configuration,
width, width,
height, height,
ByteSampleBitDepth, ByteSampleBitDepth,
colorFormat, colorFormat,
CenteredChromaSamplePosition, CenteredChromaSamplePosition,
CenteredChromaSamplePosition); CenteredChromaSamplePosition);
}
catch
{
// The common state already exists, and any preceding frame allocations also need returning.
this.Dispose();
throw;
}
} }
protected override void DisposeFrames() protected override void DisposeFrames()
{ {
this.source.Dispose(); // A derived constructor can fail before all three frame owners exist.
this.reference.Dispose(); this.reconstruction?.Dispose();
this.reconstruction.Dispose(); this.reference?.Dispose();
this.source?.Dispose();
} }
protected override void EncodeFrame<TPixel>( protected override void EncodeFrame<TPixel>(
@ -1793,41 +1820,51 @@ internal static class Av1FrameEncoder
encodeAlpha, encodeAlpha,
usesHighBitDepth: true) usesHighBitDepth: true)
{ {
int bitDepth = colorConfig.BitDepth.GetBitCount(); try
Av1ColorFormat colorFormat = colorConfig.GetColorFormat(); {
this.source = new( int bitDepth = colorConfig.BitDepth.GetBitCount();
configuration, Av1ColorFormat colorFormat = colorConfig.GetColorFormat();
width, this.source = new(
height, configuration,
bitDepth, width,
colorFormat, height,
CenteredChromaSamplePosition, bitDepth,
CenteredChromaSamplePosition); colorFormat,
CenteredChromaSamplePosition,
CenteredChromaSamplePosition);
this.reference = new( this.reference = new(
configuration, configuration,
width, width,
height, height,
bitDepth, bitDepth,
colorFormat, colorFormat,
CenteredChromaSamplePosition, CenteredChromaSamplePosition,
CenteredChromaSamplePosition); CenteredChromaSamplePosition);
this.reconstruction = new( this.reconstruction = new(
configuration, configuration,
width, width,
height, height,
bitDepth, bitDepth,
colorFormat, colorFormat,
CenteredChromaSamplePosition, CenteredChromaSamplePosition,
CenteredChromaSamplePosition); CenteredChromaSamplePosition);
}
catch
{
// The common state already exists, and any preceding frame allocations also need returning.
this.Dispose();
throw;
}
} }
protected override void DisposeFrames() protected override void DisposeFrames()
{ {
this.source.Dispose(); // A derived constructor can fail before all three frame owners exist.
this.reference.Dispose(); this.reconstruction?.Dispose();
this.reconstruction.Dispose(); this.reference?.Dispose();
this.source?.Dispose();
} }
protected override void EncodeFrame<TPixel>( protected override void EncodeFrame<TPixel>(

427
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPictureBuffer.cs

@ -90,235 +90,246 @@ internal sealed class Av1EncoderPictureBuffer : IDisposable
height, height,
disallow4x4AllFrames); disallow4x4AllFrames);
int alignedModeInfoRowCount = Av1Math.AlignPowerOf2(this.modeInfo.ModeInfoRowCount, ContextAlignmentLog2); try
int lumaLeftLength = alignedModeInfoRowCount;
int lumaTopLength = this.modeInfo.ModeInfoStride;
ObuColorConfig colorConfig = sequenceHeader.ColorConfig;
int chromaLeftLength = colorConfig.IsMonochrome
? 0
: lumaLeftLength >> (colorConfig.SubSamplingY ? 1 : 0);
int chromaTopLength = colorConfig.IsMonochrome
? 0
: lumaTopLength >> (colorConfig.SubSamplingX ? 1 : 0);
int tileCount = frameHeader.TilesInfo.TileColumnCount * frameHeader.TilesInfo.TileRowCount;
int lumaContextLength = checked(lumaLeftLength + lumaTopLength);
int chromaContextLength = checked(chromaLeftLength + chromaTopLength);
int byteContextLengthPerTile = checked((2 * lumaContextLength) + (2 * chromaContextLength));
int partitionContextLength = checked(tileCount * lumaContextLength);
int segmentationLength = checked(this.modeInfo.ModeInfoColumnCount * this.modeInfo.ModeInfoRowCount);
int partitionContextSize = Unsafe.SizeOf<Av1PartitionContext>();
int partitionStorageOffset = checked(
((segmentationLength + partitionContextSize - 1) / partitionContextSize) * partitionContextSize);
int partitionStorageLength = checked(
partitionContextLength * Unsafe.SizeOf<Av1PartitionContext>());
int byteContextStorageOffset = checked(partitionStorageOffset + partitionStorageLength);
int byteContextStorageLength = checked(tileCount * byteContextLengthPerTile);
int byteContextStorageEnd = checked(byteContextStorageOffset + byteContextStorageLength);
int paletteLeftLength = alignedModeInfoRowCount;
int paletteTopLength = this.modeInfo.ModeInfoStride;
int paletteContextLength = checked(paletteLeftLength + paletteTopLength);
int paletteStorageOffset = allocateScreenContentState
? Av1Math.AlignPowerOf2(byteContextStorageEnd, 1)
: byteContextStorageEnd;
int paletteStorageLength = allocateScreenContentState
? checked(tileCount * paletteContextLength * Unsafe.SizeOf<Av1EncoderPaletteInfo>())
: 0;
int paletteStorageEnd = checked(paletteStorageOffset + paletteStorageLength);
int displacementVectorLength = allocateMotionVectorState ? this.modeInfo.Allocation.Length : 0;
int displacementVectorStorageOffset = allocateMotionVectorState
? Av1Math.AlignPowerOf2(paletteStorageEnd, 1)
: paletteStorageEnd;
int displacementVectorStorageLength = checked(
displacementVectorLength * Unsafe.SizeOf<Av1EncoderDisplacementVector>());
int displacementVectorStorageEnd = checked(displacementVectorStorageOffset + displacementVectorStorageLength);
int intraBlockCopySearchStorageOffset = allocateIntraBlockCopySearch
? Av1Math.AlignPowerOf2(displacementVectorStorageEnd, 2)
: displacementVectorStorageEnd;
int intraBlockCopySearchStorageLength = allocateIntraBlockCopySearch
? Av1IntraBlockCopySearchIndex.GetStorageLength(width, height)
: 0;
int intraBlockCopySearchStorageEnd = checked(
intraBlockCopySearchStorageOffset + intraBlockCopySearchStorageLength);
int tileStateStorageOffset = Av1Math.AlignPowerOf2(intraBlockCopySearchStorageEnd, 2);
int cdefPresetLength = tileCount * Av1Constants.CdefUnitsPerSuperblock;
int tileStateLength = cdefPresetLength + (3 * tileCount);
int tileStateStorageLength = tileStateLength * sizeof(int);
int stateStorageLength = checked(tileStateStorageOffset + tileStateStorageLength);
// Segmentation and every tile edge share one clean picture lifetime. The partition region begins at its
// native alignment. CDEF, quantizer, and encoded-tile bounds occupy one aligned trailing integer region
// instead of allocating separate managed arrays for every picture.
this.stateStorage = configuration.MemoryAllocator.Allocate<byte>(
stateStorageLength,
AllocationOptions.Clean);
this.stateMemory = this.stateStorage.Memory[..stateStorageLength];
Memory<byte> stateStorage = this.stateMemory;
this.partitionContextMemory = new ByteMemoryManager<Av1PartitionContext>(
stateStorage.Slice(partitionStorageOffset, partitionStorageLength));
Memory<Av1PartitionContext> partitionStorage = this.partitionContextMemory.Memory;
Memory<byte> byteContextStorage = stateStorage.Slice(byteContextStorageOffset, byteContextStorageLength);
this.partitionContexts = new Av1NeighborArrayUnit<Av1PartitionContext>[tileCount];
this.lumaCoefficientContexts = new Av1NeighborArrayUnit<byte>[tileCount];
this.blueCoefficientContexts = new Av1NeighborArrayUnit<byte>[tileCount];
this.redCoefficientContexts = new Av1NeighborArrayUnit<byte>[tileCount];
this.transformContexts = new Av1NeighborArrayUnit<byte>[tileCount];
Memory<Av1EncoderPaletteInfo> paletteStorage = Memory<Av1EncoderPaletteInfo>.Empty;
if (allocateScreenContentState)
{ {
// Palette entries contain 16-bit colors, so their packed typed region begins at an even byte offset. int alignedModeInfoRowCount = Av1Math.AlignPowerOf2(this.modeInfo.ModeInfoRowCount, ContextAlignmentLog2);
ByteMemoryManager<Av1EncoderPaletteInfo> paletteMemory = new( int lumaLeftLength = alignedModeInfoRowCount;
stateStorage.Slice(paletteStorageOffset, paletteStorageLength)); int lumaTopLength = this.modeInfo.ModeInfoStride;
ObuColorConfig colorConfig = sequenceHeader.ColorConfig;
paletteStorage = paletteMemory.Memory; int chromaLeftLength = colorConfig.IsMonochrome
this.paletteContexts = new Av1NeighborArrayUnit<Av1EncoderPaletteInfo>[tileCount]; ? 0
} : lumaLeftLength >> (colorConfig.SubSamplingY ? 1 : 0);
else
{ int chromaTopLength = colorConfig.IsMonochrome
this.paletteContexts = []; ? 0
} : lumaTopLength >> (colorConfig.SubSamplingX ? 1 : 0);
Memory<Av1EncoderDisplacementVector> displacementVectors = Memory<Av1EncoderDisplacementVector>.Empty; int tileCount = frameHeader.TilesInfo.TileColumnCount * frameHeader.TilesInfo.TileRowCount;
if (allocateMotionVectorState) int lumaContextLength = checked(lumaLeftLength + lumaTopLength);
{ int chromaContextLength = checked(chromaLeftLength + chromaTopLength);
// Each component lies strictly inside plus or minus 16384. Two signed 16-bit fields preserve both int byteContextLengthPerTile = checked((2 * lumaContextLength) + (2 * chromaContextLength));
// inter and intra-block-copy vectors without expanding every compact mode-information entry. int partitionContextLength = checked(tileCount * lumaContextLength);
ByteMemoryManager<Av1EncoderDisplacementVector> displacementVectorMemory = new( int segmentationLength = checked(this.modeInfo.ModeInfoColumnCount * this.modeInfo.ModeInfoRowCount);
stateStorage.Slice(displacementVectorStorageOffset, displacementVectorStorageLength)); int partitionContextSize = Unsafe.SizeOf<Av1PartitionContext>();
int partitionStorageOffset = checked(
((segmentationLength + partitionContextSize - 1) / partitionContextSize) * partitionContextSize);
int partitionStorageLength = checked(
partitionContextLength * Unsafe.SizeOf<Av1PartitionContext>());
int byteContextStorageOffset = checked(partitionStorageOffset + partitionStorageLength);
int byteContextStorageLength = checked(tileCount * byteContextLengthPerTile);
int byteContextStorageEnd = checked(byteContextStorageOffset + byteContextStorageLength);
int paletteLeftLength = alignedModeInfoRowCount;
int paletteTopLength = this.modeInfo.ModeInfoStride;
int paletteContextLength = checked(paletteLeftLength + paletteTopLength);
int paletteStorageOffset = allocateScreenContentState
? Av1Math.AlignPowerOf2(byteContextStorageEnd, 1)
: byteContextStorageEnd;
int paletteStorageLength = allocateScreenContentState
? checked(tileCount * paletteContextLength * Unsafe.SizeOf<Av1EncoderPaletteInfo>())
: 0;
int paletteStorageEnd = checked(paletteStorageOffset + paletteStorageLength);
int displacementVectorLength = allocateMotionVectorState ? this.modeInfo.Allocation.Length : 0;
int displacementVectorStorageOffset = allocateMotionVectorState
? Av1Math.AlignPowerOf2(paletteStorageEnd, 1)
: paletteStorageEnd;
int displacementVectorStorageLength = checked(
displacementVectorLength * Unsafe.SizeOf<Av1EncoderDisplacementVector>());
int displacementVectorStorageEnd = checked(displacementVectorStorageOffset + displacementVectorStorageLength);
int intraBlockCopySearchStorageOffset = allocateIntraBlockCopySearch
? Av1Math.AlignPowerOf2(displacementVectorStorageEnd, 2)
: displacementVectorStorageEnd;
int intraBlockCopySearchStorageLength = allocateIntraBlockCopySearch
? Av1IntraBlockCopySearchIndex.GetStorageLength(width, height)
: 0;
int intraBlockCopySearchStorageEnd = checked(
intraBlockCopySearchStorageOffset + intraBlockCopySearchStorageLength);
int tileStateStorageOffset = Av1Math.AlignPowerOf2(intraBlockCopySearchStorageEnd, 2);
int cdefPresetLength = tileCount * Av1Constants.CdefUnitsPerSuperblock;
int tileStateLength = cdefPresetLength + (3 * tileCount);
int tileStateStorageLength = tileStateLength * sizeof(int);
int stateStorageLength = checked(tileStateStorageOffset + tileStateStorageLength);
// Segmentation and every tile edge share one clean picture lifetime. The partition region begins at its
// native alignment. CDEF, quantizer, and encoded-tile bounds occupy one aligned trailing integer region
// instead of allocating separate managed arrays for every picture.
this.stateStorage = configuration.MemoryAllocator.Allocate<byte>(
stateStorageLength,
AllocationOptions.Clean);
this.stateMemory = this.stateStorage.Memory[..stateStorageLength];
Memory<byte> stateStorage = this.stateMemory;
this.partitionContextMemory = new ByteMemoryManager<Av1PartitionContext>(
stateStorage.Slice(partitionStorageOffset, partitionStorageLength));
Memory<Av1PartitionContext> partitionStorage = this.partitionContextMemory.Memory;
Memory<byte> byteContextStorage = stateStorage.Slice(byteContextStorageOffset, byteContextStorageLength);
this.partitionContexts = new Av1NeighborArrayUnit<Av1PartitionContext>[tileCount];
this.lumaCoefficientContexts = new Av1NeighborArrayUnit<byte>[tileCount];
this.blueCoefficientContexts = new Av1NeighborArrayUnit<byte>[tileCount];
this.redCoefficientContexts = new Av1NeighborArrayUnit<byte>[tileCount];
this.transformContexts = new Av1NeighborArrayUnit<byte>[tileCount];
Memory<Av1EncoderPaletteInfo> paletteStorage = Memory<Av1EncoderPaletteInfo>.Empty;
if (allocateScreenContentState)
{
// Palette entries contain 16-bit colors, so their packed typed region begins at an even byte offset.
ByteMemoryManager<Av1EncoderPaletteInfo> paletteMemory = new(
stateStorage.Slice(paletteStorageOffset, paletteStorageLength));
displacementVectors = displacementVectorMemory.Memory; paletteStorage = paletteMemory.Memory;
} this.paletteContexts = new Av1NeighborArrayUnit<Av1EncoderPaletteInfo>[tileCount];
}
else
{
this.paletteContexts = [];
}
Av1IntraBlockCopySearchIndex intraBlockCopySearch = default; Memory<Av1EncoderDisplacementVector> displacementVectors = Memory<Av1EncoderDisplacementVector>.Empty;
if (allocateIntraBlockCopySearch) if (allocateMotionVectorState)
{ {
// The search index casts its packed workspace to 32-bit links, so its non-owning region begins at // Each component lies strictly inside plus or minus 16384. Two signed 16-bit fields preserve both
// a four-byte boundary inside the existing picture-state rent. // inter and intra-block-copy vectors without expanding every compact mode-information entry.
intraBlockCopySearch = new Av1IntraBlockCopySearchIndex( ByteMemoryManager<Av1EncoderDisplacementVector> displacementVectorMemory = new(
stateStorage.Slice(intraBlockCopySearchStorageOffset, intraBlockCopySearchStorageLength), stateStorage.Slice(displacementVectorStorageOffset, displacementVectorStorageLength));
width,
height);
}
ByteMemoryManager<int> tileStateMemory = new( displacementVectors = displacementVectorMemory.Memory;
stateStorage.Slice(tileStateStorageOffset, tileStateStorageLength)); }
Memory<int> tileState = tileStateMemory.Memory; Av1IntraBlockCopySearchIndex intraBlockCopySearch = default;
Memory<int> cdefPreset = tileState[..cdefPresetLength]; if (allocateIntraBlockCopySearch)
Memory<int> previousQIndex = tileState.Slice(cdefPresetLength, tileCount);
Memory<int> tileDataOffsets = tileState.Slice(cdefPresetLength + tileCount, tileCount);
Memory<int> tileDataLengths = tileState.Slice(cdefPresetLength + (2 * tileCount), tileCount);
cdefPreset.Span.Fill(-1);
for (int tileIndex = 0; tileIndex < tileCount; tileIndex++)
{
this.partitionContexts[tileIndex] = new Av1NeighborArrayUnit<Av1PartitionContext>(
partitionStorage.Slice(tileIndex * lumaContextLength, lumaContextLength),
lumaLeftLength,
lumaTopLength)
{ {
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 // The search index casts its packed workspace to 32-bit links, so its non-owning region begins at
}; // a four-byte boundary inside the existing picture-state rent.
intraBlockCopySearch = new Av1IntraBlockCopySearchIndex(
stateStorage.Slice(intraBlockCopySearchStorageOffset, intraBlockCopySearchStorageLength),
width,
height);
}
int byteContextOffset = tileIndex * byteContextLengthPerTile; ByteMemoryManager<int> tileStateMemory = new(
this.lumaCoefficientContexts[tileIndex] = new Av1NeighborArrayUnit<byte>( stateStorage.Slice(tileStateStorageOffset, tileStateStorageLength));
byteContextStorage.Slice(byteContextOffset, lumaContextLength),
lumaLeftLength,
lumaTopLength)
{
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
byteContextOffset += lumaContextLength; Memory<int> tileState = tileStateMemory.Memory;
this.blueCoefficientContexts[tileIndex] = new Av1NeighborArrayUnit<byte>( Memory<int> cdefPreset = tileState[..cdefPresetLength];
byteContextStorage.Slice(byteContextOffset, chromaContextLength), Memory<int> previousQIndex = tileState.Slice(cdefPresetLength, tileCount);
chromaLeftLength, Memory<int> tileDataOffsets = tileState.Slice(cdefPresetLength + tileCount, tileCount);
chromaTopLength) Memory<int> tileDataLengths = tileState.Slice(cdefPresetLength + (2 * tileCount), tileCount);
cdefPreset.Span.Fill(-1);
for (int tileIndex = 0; tileIndex < tileCount; tileIndex++)
{ {
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 this.partitionContexts[tileIndex] = new Av1NeighborArrayUnit<Av1PartitionContext>(
}; partitionStorage.Slice(tileIndex * lumaContextLength, lumaContextLength),
lumaLeftLength,
lumaTopLength)
{
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
byteContextOffset += chromaContextLength; int byteContextOffset = tileIndex * byteContextLengthPerTile;
this.redCoefficientContexts[tileIndex] = new Av1NeighborArrayUnit<byte>( this.lumaCoefficientContexts[tileIndex] = new Av1NeighborArrayUnit<byte>(
byteContextStorage.Slice(byteContextOffset, chromaContextLength), byteContextStorage.Slice(byteContextOffset, lumaContextLength),
chromaLeftLength, lumaLeftLength,
chromaTopLength) lumaTopLength)
{ {
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
}; };
byteContextOffset += chromaContextLength; byteContextOffset += lumaContextLength;
this.transformContexts[tileIndex] = new Av1NeighborArrayUnit<byte>( this.blueCoefficientContexts[tileIndex] = new Av1NeighborArrayUnit<byte>(
byteContextStorage.Slice(byteContextOffset, lumaContextLength), byteContextStorage.Slice(byteContextOffset, chromaContextLength),
lumaLeftLength, chromaLeftLength,
lumaTopLength) chromaTopLength)
{ {
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
}; };
// Variable-transform contexts consult both edges without separate availability flags. The largest byteContextOffset += chromaContextLength;
// transform makes an unavailable edge compare as unsplit until a coded neighbor publishes its size. this.redCoefficientContexts[tileIndex] = new Av1NeighborArrayUnit<byte>(
this.transformContexts[tileIndex].Left.Fill((byte)Av1Constants.MaxTransformSize); byteContextStorage.Slice(byteContextOffset, chromaContextLength),
this.transformContexts[tileIndex].Top.Fill((byte)Av1Constants.MaxTransformSize); chromaLeftLength,
chromaTopLength)
{
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
if (allocateScreenContentState) byteContextOffset += chromaContextLength;
{ this.transformContexts[tileIndex] = new Av1NeighborArrayUnit<byte>(
this.paletteContexts[tileIndex] = new Av1NeighborArrayUnit<Av1EncoderPaletteInfo>( byteContextStorage.Slice(byteContextOffset, lumaContextLength),
paletteStorage.Slice(tileIndex * paletteContextLength, paletteContextLength), lumaLeftLength,
paletteLeftLength, lumaTopLength)
paletteTopLength)
{ {
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
}; };
}
previousQIndex.Span[tileIndex] = frameHeader.QuantizationParameters.BaseQIndex; // Variable-transform contexts consult both edges without separate availability flags. The largest
} // transform makes an unavailable edge compare as unsplit until a coded neighbor publishes its size.
this.transformContexts[tileIndex].Left.Fill((byte)Av1Constants.MaxTransformSize);
this.transformContexts[tileIndex].Top.Fill((byte)Av1Constants.MaxTransformSize);
this.Picture = new Av1PictureControlSet if (allocateScreenContentState)
{ {
PartitionContexts = this.partitionContexts, this.paletteContexts[tileIndex] = new Av1NeighborArrayUnit<Av1EncoderPaletteInfo>(
LuminanceDcSignLevelCoefficientNeighbors = this.lumaCoefficientContexts, paletteStorage.Slice(tileIndex * paletteContextLength, paletteContextLength),
CbDcSignLevelCoefficientNeighbors = this.blueCoefficientContexts, paletteLeftLength,
CrDcSignLevelCoefficientNeighbors = this.redCoefficientContexts, paletteTopLength)
TransformFunctionContexts = this.transformContexts, {
PaletteContexts = this.paletteContexts, GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
Sequence = new Av1SequenceControlSet { SequenceHeader = sequenceHeader }, };
Parent = new Av1PictureParentControlSet }
previousQIndex.Span[tileIndex] = frameHeader.QuantizationParameters.BaseQIndex;
}
this.Picture = new Av1PictureControlSet
{ {
Common = new Av1EncoderCommon PartitionContexts = this.partitionContexts,
LuminanceDcSignLevelCoefficientNeighbors = this.lumaCoefficientContexts,
CbDcSignLevelCoefficientNeighbors = this.blueCoefficientContexts,
CrDcSignLevelCoefficientNeighbors = this.redCoefficientContexts,
TransformFunctionContexts = this.transformContexts,
PaletteContexts = this.paletteContexts,
Sequence = new Av1SequenceControlSet { SequenceHeader = sequenceHeader },
Parent = new Av1PictureParentControlSet
{ {
ModeInfoColumnCount = this.modeInfo.ModeInfoColumnCount, Common = new Av1EncoderCommon
ModeInfoRowCount = this.modeInfo.ModeInfoRowCount, {
ModeInfoStride = this.modeInfo.ModeInfoStride, ModeInfoColumnCount = this.modeInfo.ModeInfoColumnCount,
FrameSize = frameHeader.FrameSize, ModeInfoRowCount = this.modeInfo.ModeInfoRowCount,
TilesInfo = frameHeader.TilesInfo ModeInfoStride = this.modeInfo.ModeInfoStride,
FrameSize = frameHeader.FrameSize,
TilesInfo = frameHeader.TilesInfo
},
FrameHeader = frameHeader,
PreviousQIndex = previousQIndex
}, },
FrameHeader = frameHeader, SegmentationNeighborMap = stateStorage[..segmentationLength],
PreviousQIndex = previousQIndex ModeInfoGrid = this.modeInfo.Grid,
}, ModeInfoAllocation = this.modeInfo.Allocation,
SegmentationNeighborMap = stateStorage[..segmentationLength], DisplacementVectors = displacementVectors,
ModeInfoGrid = this.modeInfo.Grid, IntraBlockCopySearch = intraBlockCopySearch,
ModeInfoAllocation = this.modeInfo.Allocation, ModeInfoStride = this.modeInfo.ModeInfoStride,
DisplacementVectors = displacementVectors, Disallow4x4AllFrames = this.modeInfo.Disallow4x4AllFrames,
IntraBlockCopySearch = intraBlockCopySearch, CdefPreset = cdefPreset,
ModeInfoStride = this.modeInfo.ModeInfoStride, TileDataOffsets = tileDataOffsets,
Disallow4x4AllFrames = this.modeInfo.Disallow4x4AllFrames, TileDataLengths = tileDataLengths
CdefPreset = cdefPreset, };
TileDataOffsets = tileDataOffsets, }
TileDataLengths = tileDataLengths catch
}; {
// The context objects only borrow these two owners. A failed constructor must release the
// completed allocations itself because the enclosing sequence never receives this picture.
this.stateStorage?.Dispose();
this.modeInfo.Dispose();
throw;
}
} }
/// <summary> /// <summary>

75
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs

@ -593,6 +593,55 @@ public class Av1EncoderFrameTests
Assert.Equal(second.Size, decodedSecond.Size); Assert.Equal(second.Size, decodedSecond.Size);
} }
[Theory]
[InlineData(false, EightBit)]
[InlineData(false, TenBit)]
[InlineData(false, TwelveBit)]
[InlineData(true, EightBit)]
[InlineData(true, TenBit)]
[InlineData(true, TwelveBit)]
public void SequenceEncoderConstructionFailureReturnsEveryAllocation(bool encodeAlpha, int bitDepthValue)
{
ObuColorConfig colorConfig = CreateColorConfig(
(Av1BitDepth)bitDepthValue,
encodeAlpha ? Av1ColorFormat.Yuv400 : Av1ColorFormat.Yuv420);
Configuration configuration = Configuration.Default.Clone();
TestMemoryAllocator successfulAllocator = new();
successfulAllocator.EnableNonThreadSafeLogging();
configuration.MemoryAllocator = successfulAllocator;
using (Av1FrameEncoder.SequenceEncoder encoder = encodeAlpha
? Av1FrameEncoder.CreateAlphaSequenceEncoder(configuration, 32, 32, colorConfig, 17, 9)
: Av1FrameEncoder.CreateColorSequenceEncoder(configuration, 32, 32, colorConfig, 17, 9))
{
Assert.NotEmpty(successfulAllocator.AllocationLog);
}
Assert.Equal(successfulAllocator.AllocationLog.Count, successfulAllocator.ReturnLog.Count);
for (int failureIndex = 0; failureIndex < successfulAllocator.AllocationLog.Count; failureIndex++)
{
FailingSequenceAllocator allocator = new(failureIndex);
configuration.MemoryAllocator = allocator;
// Fail each real allocator request, including those made inside nested constructors. A constructor
// that throws never reaches the caller's using statement, so its completed owners must unwind there.
InvalidMemoryOperationException exception = Assert.Throws<InvalidMemoryOperationException>(() =>
{
using Av1FrameEncoder.SequenceEncoder encoder = encodeAlpha
? Av1FrameEncoder.CreateAlphaSequenceEncoder(configuration, 32, 32, colorConfig, 17, 9)
: Av1FrameEncoder.CreateColorSequenceEncoder(configuration, 32, 32, colorConfig, 17, 9);
});
Assert.Equal("Sequence allocation failure.", exception.Message);
Assert.Equal(failureIndex, allocator.AllocationLog.Count);
Assert.All(
allocator.AllocationLog,
allocation => Assert.Single(allocator.ReturnLog, returned => returned.AllocationId == allocation.AllocationId));
Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count);
}
}
[Theory] [Theory]
[InlineData(false, EightBit, Yuv420, 384)] [InlineData(false, EightBit, Yuv420, 384)]
[InlineData(false, TwelveBit, Yuv444, 288)] [InlineData(false, TwelveBit, Yuv444, 288)]
@ -1879,4 +1928,30 @@ public class Av1EncoderFrameTests
} }
} }
} }
private sealed class FailingSequenceAllocator : TestMemoryAllocator
{
private readonly int failureIndex;
/// <summary>
/// Initializes a new instance of the <see cref="FailingSequenceAllocator"/> class.
/// </summary>
/// <param name="failureIndex">The zero-based allocation request that fails.</param>
public FailingSequenceAllocator(int failureIndex)
{
this.failureIndex = failureIndex;
this.EnableNonThreadSafeLogging();
}
/// <inheritdoc/>
protected override AllocationTrackedMemoryManager<T> AllocateCore<T>(int length, AllocationOptions options)
{
if (this.AllocationLog.Count == this.failureIndex)
{
throw new InvalidMemoryOperationException("Sequence allocation failure.");
}
return base.AllocateCore<T>(length, options);
}
}
} }

Loading…
Cancel
Save