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
// is known with the tile output capacity and remains valid for every transform in every sequence sample.
this.levels = new Av1LevelBuffer(configuration);
this.coefficientContexts =
configuration.MemoryAllocator.Allocate<sbyte>(MaximumCoefficientContextCount);
try
{
this.coefficientContexts =
configuration.MemoryAllocator.Allocate<sbyte>(MaximumCoefficientContextCount);
this.writer = new(configuration, bufferLength, updateCdf);
this.baseQIndex = qIndex;
this.writer = new(configuration, bufferLength, updateCdf);
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>

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

@ -1519,44 +1519,54 @@ internal static class Av1FrameEncoder
encodeAlpha,
usesHighBitDepth);
bool allocateScreenContentState = effort >= 5;
bool allocateIntraBlockCopySearch =
allocateScreenContentState &&
!this.FrameHeader.CodedLossless;
// Sequence geometry and maximum tool capacity are fixed before the first sample. Reusing this owner
// avoids renting the complete mode grid and optional screen-content index for every frame.
this.PictureBuffer = new Av1EncoderPictureBuffer(
configuration,
this.SequenceHeader,
this.FrameHeader,
width,
height,
disallow4x4AllFrames: !this.FrameHeader.CodedLossless && effort < 9,
allocateScreenContentState: allocateScreenContentState,
allocateMotionVectorState: true,
allocateIntraBlockCopySearch: allocateIntraBlockCopySearch);
try
{
bool allocateScreenContentState = effort >= 5;
bool allocateIntraBlockCopySearch =
allocateScreenContentState &&
!this.FrameHeader.CodedLossless;
// Sequence geometry and maximum tool capacity are fixed before the first sample. Reusing this owner
// avoids renting the complete mode grid and optional screen-content index for every frame.
this.PictureBuffer = new Av1EncoderPictureBuffer(
configuration,
this.SequenceHeader,
this.FrameHeader,
width,
height,
disallow4x4AllFrames: !this.FrameHeader.CodedLossless && effort < 9,
allocateScreenContentState: allocateScreenContentState,
allocateMotionVectorState: true,
allocateIntraBlockCopySearch: allocateIntraBlockCopySearch);
this.Coefficients = new Av1EncoderCoefficientBuffer(
configuration,
this.SequenceHeader,
width,
height);
this.Coefficients = new Av1EncoderCoefficientBuffer(
configuration,
this.SequenceHeader,
width,
height);
this.SuperblockWorkspace = new Av1EncoderSuperblockWorkspace(configuration);
this.SuperblockWorkspace = new Av1EncoderSuperblockWorkspace(configuration);
this.TileWorkspace = new Av1EncoderTileWorkspace(this.FrameHeader, this.SuperblockWorkspace);
this.BlockWorkspace = new Av1EncoderBlockWorkspace(configuration);
this.TileWorkspace = new Av1EncoderTileWorkspace(this.FrameHeader, this.SuperblockWorkspace);
this.BlockWorkspace = new Av1EncoderBlockWorkspace(configuration);
// 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.
this.SymbolEncoder = new Av1SymbolEncoder(
configuration,
this.TileBufferLength,
qIndex,
updateCdf: true);
// 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.
this.SymbolEncoder = new Av1SymbolEncoder(
configuration,
this.TileBufferLength,
qIndex,
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>
@ -1637,13 +1647,7 @@ internal static class Av1FrameEncoder
public void Dispose()
{
this.DisposeFrames();
this.ConversionWorkspace.Dispose();
this.PictureBuffer.Dispose();
this.Coefficients.Dispose();
this.SuperblockWorkspace.Dispose();
this.BlockWorkspace.Dispose();
this.ObuWriter.Dispose();
this.SymbolEncoder.Dispose();
this.DisposeResources();
}
/// <summary>
@ -1657,6 +1661,19 @@ internal static class Av1FrameEncoder
ObuFrameType frameType,
bool writeSequenceHeader)
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
@ -1683,40 +1700,50 @@ internal static class Av1FrameEncoder
encodeAlpha,
usesHighBitDepth: false)
{
Av1ColorFormat colorFormat = colorConfig.GetColorFormat();
this.source = new(
configuration,
width,
height,
ByteSampleBitDepth,
colorFormat,
CenteredChromaSamplePosition,
CenteredChromaSamplePosition);
try
{
Av1ColorFormat colorFormat = colorConfig.GetColorFormat();
this.source = new(
configuration,
width,
height,
ByteSampleBitDepth,
colorFormat,
CenteredChromaSamplePosition,
CenteredChromaSamplePosition);
this.reference = new(
configuration,
width,
height,
ByteSampleBitDepth,
colorFormat,
CenteredChromaSamplePosition,
CenteredChromaSamplePosition);
this.reference = new(
configuration,
width,
height,
ByteSampleBitDepth,
colorFormat,
CenteredChromaSamplePosition,
CenteredChromaSamplePosition);
this.reconstruction = new(
configuration,
width,
height,
ByteSampleBitDepth,
colorFormat,
CenteredChromaSamplePosition,
CenteredChromaSamplePosition);
this.reconstruction = new(
configuration,
width,
height,
ByteSampleBitDepth,
colorFormat,
CenteredChromaSamplePosition,
CenteredChromaSamplePosition);
}
catch
{
// The common state already exists, and any preceding frame allocations also need returning.
this.Dispose();
throw;
}
}
protected override void DisposeFrames()
{
this.source.Dispose();
this.reference.Dispose();
this.reconstruction.Dispose();
// A derived constructor can fail before all three frame owners exist.
this.reconstruction?.Dispose();
this.reference?.Dispose();
this.source?.Dispose();
}
protected override void EncodeFrame<TPixel>(
@ -1793,41 +1820,51 @@ internal static class Av1FrameEncoder
encodeAlpha,
usesHighBitDepth: true)
{
int bitDepth = colorConfig.BitDepth.GetBitCount();
Av1ColorFormat colorFormat = colorConfig.GetColorFormat();
this.source = new(
configuration,
width,
height,
bitDepth,
colorFormat,
CenteredChromaSamplePosition,
CenteredChromaSamplePosition);
try
{
int bitDepth = colorConfig.BitDepth.GetBitCount();
Av1ColorFormat colorFormat = colorConfig.GetColorFormat();
this.source = new(
configuration,
width,
height,
bitDepth,
colorFormat,
CenteredChromaSamplePosition,
CenteredChromaSamplePosition);
this.reference = new(
configuration,
width,
height,
bitDepth,
colorFormat,
CenteredChromaSamplePosition,
CenteredChromaSamplePosition);
this.reference = new(
configuration,
width,
height,
bitDepth,
colorFormat,
CenteredChromaSamplePosition,
CenteredChromaSamplePosition);
this.reconstruction = new(
configuration,
width,
height,
bitDepth,
colorFormat,
CenteredChromaSamplePosition,
CenteredChromaSamplePosition);
this.reconstruction = new(
configuration,
width,
height,
bitDepth,
colorFormat,
CenteredChromaSamplePosition,
CenteredChromaSamplePosition);
}
catch
{
// The common state already exists, and any preceding frame allocations also need returning.
this.Dispose();
throw;
}
}
protected override void DisposeFrames()
{
this.source.Dispose();
this.reference.Dispose();
this.reconstruction.Dispose();
// A derived constructor can fail before all three frame owners exist.
this.reconstruction?.Dispose();
this.reference?.Dispose();
this.source?.Dispose();
}
protected override void EncodeFrame<TPixel>(

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

@ -90,235 +90,246 @@ internal sealed class Av1EncoderPictureBuffer : IDisposable
height,
disallow4x4AllFrames);
int alignedModeInfoRowCount = Av1Math.AlignPowerOf2(this.modeInfo.ModeInfoRowCount, ContextAlignmentLog2);
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)
try
{
// 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));
paletteStorage = paletteMemory.Memory;
this.paletteContexts = new Av1NeighborArrayUnit<Av1EncoderPaletteInfo>[tileCount];
}
else
{
this.paletteContexts = [];
}
Memory<Av1EncoderDisplacementVector> displacementVectors = Memory<Av1EncoderDisplacementVector>.Empty;
if (allocateMotionVectorState)
{
// Each component lies strictly inside plus or minus 16384. Two signed 16-bit fields preserve both
// inter and intra-block-copy vectors without expanding every compact mode-information entry.
ByteMemoryManager<Av1EncoderDisplacementVector> displacementVectorMemory = new(
stateStorage.Slice(displacementVectorStorageOffset, displacementVectorStorageLength));
int alignedModeInfoRowCount = Av1Math.AlignPowerOf2(this.modeInfo.ModeInfoRowCount, ContextAlignmentLog2);
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.
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;
if (allocateIntraBlockCopySearch)
{
// 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);
}
Memory<Av1EncoderDisplacementVector> displacementVectors = Memory<Av1EncoderDisplacementVector>.Empty;
if (allocateMotionVectorState)
{
// Each component lies strictly inside plus or minus 16384. Two signed 16-bit fields preserve both
// inter and intra-block-copy vectors without expanding every compact mode-information entry.
ByteMemoryManager<Av1EncoderDisplacementVector> displacementVectorMemory = new(
stateStorage.Slice(displacementVectorStorageOffset, displacementVectorStorageLength));
ByteMemoryManager<int> tileStateMemory = new(
stateStorage.Slice(tileStateStorageOffset, tileStateStorageLength));
displacementVectors = displacementVectorMemory.Memory;
}
Memory<int> tileState = tileStateMemory.Memory;
Memory<int> cdefPreset = tileState[..cdefPresetLength];
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)
Av1IntraBlockCopySearchIndex intraBlockCopySearch = default;
if (allocateIntraBlockCopySearch)
{
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;
this.lumaCoefficientContexts[tileIndex] = new Av1NeighborArrayUnit<byte>(
byteContextStorage.Slice(byteContextOffset, lumaContextLength),
lumaLeftLength,
lumaTopLength)
{
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
ByteMemoryManager<int> tileStateMemory = new(
stateStorage.Slice(tileStateStorageOffset, tileStateStorageLength));
byteContextOffset += lumaContextLength;
this.blueCoefficientContexts[tileIndex] = new Av1NeighborArrayUnit<byte>(
byteContextStorage.Slice(byteContextOffset, chromaContextLength),
chromaLeftLength,
chromaTopLength)
Memory<int> tileState = tileStateMemory.Memory;
Memory<int> cdefPreset = tileState[..cdefPresetLength];
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++)
{
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
this.partitionContexts[tileIndex] = new Av1NeighborArrayUnit<Av1PartitionContext>(
partitionStorage.Slice(tileIndex * lumaContextLength, lumaContextLength),
lumaLeftLength,
lumaTopLength)
{
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
byteContextOffset += chromaContextLength;
this.redCoefficientContexts[tileIndex] = new Av1NeighborArrayUnit<byte>(
byteContextStorage.Slice(byteContextOffset, chromaContextLength),
chromaLeftLength,
chromaTopLength)
{
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
int byteContextOffset = tileIndex * byteContextLengthPerTile;
this.lumaCoefficientContexts[tileIndex] = new Av1NeighborArrayUnit<byte>(
byteContextStorage.Slice(byteContextOffset, lumaContextLength),
lumaLeftLength,
lumaTopLength)
{
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
byteContextOffset += chromaContextLength;
this.transformContexts[tileIndex] = new Av1NeighborArrayUnit<byte>(
byteContextStorage.Slice(byteContextOffset, lumaContextLength),
lumaLeftLength,
lumaTopLength)
{
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
byteContextOffset += lumaContextLength;
this.blueCoefficientContexts[tileIndex] = new Av1NeighborArrayUnit<byte>(
byteContextStorage.Slice(byteContextOffset, chromaContextLength),
chromaLeftLength,
chromaTopLength)
{
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
// 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);
byteContextOffset += chromaContextLength;
this.redCoefficientContexts[tileIndex] = new Av1NeighborArrayUnit<byte>(
byteContextStorage.Slice(byteContextOffset, chromaContextLength),
chromaLeftLength,
chromaTopLength)
{
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
if (allocateScreenContentState)
{
this.paletteContexts[tileIndex] = new Av1NeighborArrayUnit<Av1EncoderPaletteInfo>(
paletteStorage.Slice(tileIndex * paletteContextLength, paletteContextLength),
paletteLeftLength,
paletteTopLength)
byteContextOffset += chromaContextLength;
this.transformContexts[tileIndex] = new Av1NeighborArrayUnit<byte>(
byteContextStorage.Slice(byteContextOffset, lumaContextLength),
lumaLeftLength,
lumaTopLength)
{
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
{
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
if (allocateScreenContentState)
{
this.paletteContexts[tileIndex] = new Av1NeighborArrayUnit<Av1EncoderPaletteInfo>(
paletteStorage.Slice(tileIndex * paletteContextLength, paletteContextLength),
paletteLeftLength,
paletteTopLength)
{
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
}
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,
ModeInfoRowCount = this.modeInfo.ModeInfoRowCount,
ModeInfoStride = this.modeInfo.ModeInfoStride,
FrameSize = frameHeader.FrameSize,
TilesInfo = frameHeader.TilesInfo
Common = new Av1EncoderCommon
{
ModeInfoColumnCount = this.modeInfo.ModeInfoColumnCount,
ModeInfoRowCount = this.modeInfo.ModeInfoRowCount,
ModeInfoStride = this.modeInfo.ModeInfoStride,
FrameSize = frameHeader.FrameSize,
TilesInfo = frameHeader.TilesInfo
},
FrameHeader = frameHeader,
PreviousQIndex = previousQIndex
},
FrameHeader = frameHeader,
PreviousQIndex = previousQIndex
},
SegmentationNeighborMap = stateStorage[..segmentationLength],
ModeInfoGrid = this.modeInfo.Grid,
ModeInfoAllocation = this.modeInfo.Allocation,
DisplacementVectors = displacementVectors,
IntraBlockCopySearch = intraBlockCopySearch,
ModeInfoStride = this.modeInfo.ModeInfoStride,
Disallow4x4AllFrames = this.modeInfo.Disallow4x4AllFrames,
CdefPreset = cdefPreset,
TileDataOffsets = tileDataOffsets,
TileDataLengths = tileDataLengths
};
SegmentationNeighborMap = stateStorage[..segmentationLength],
ModeInfoGrid = this.modeInfo.Grid,
ModeInfoAllocation = this.modeInfo.Allocation,
DisplacementVectors = displacementVectors,
IntraBlockCopySearch = intraBlockCopySearch,
ModeInfoStride = this.modeInfo.ModeInfoStride,
Disallow4x4AllFrames = this.modeInfo.Disallow4x4AllFrames,
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>

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

@ -593,6 +593,55 @@ public class Av1EncoderFrameTests
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]
[InlineData(false, EightBit, Yuv420, 384)]
[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