From 762dd6a50dd6c9352e77046f6ab2c2b1799f96b6 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 25 Aug 2026 06:54:22 +1000 Subject: [PATCH] Pool AV1 parse neighbor contexts --- src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs | 11 ++- .../Heif/Av1/Entropy/Av1SymbolDecoder.cs | 20 ++--- .../Tiling/Av1ParseAboveNeighbor4x4Context.cs | 86 +++++++++++------- .../Tiling/Av1ParseLeftNeighbor4x4Context.cs | 87 ++++++++++++------- .../Formats/Heif/Av1/Tiling/Av1TileReader.cs | 30 +++++-- .../Formats/Heif/Av1HeifItemDecoder.cs | 2 +- 6 files changed, 160 insertions(+), 76 deletions(-) diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs index bc7e6ceb0..b808095e5 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1; /// /// Decodes one AV1 still-image elementary stream into an ImageSharp image. /// -internal class Av1Decoder : IAv1TileReader +internal sealed class Av1Decoder : IAv1TileReader, IDisposable { /// /// The open-bitstream-unit parser for the current image item. @@ -185,4 +185,13 @@ internal class Av1Decoder : IAv1TileReader this.tileReader.ReadTile(tileData, tileNum); } + + /// + /// Releases the tile reader and its frame-scoped parsing storage. + /// + public void Dispose() + { + this.tileReader?.Dispose(); + this.tileReader = null; + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs index 05867e9be..a401f45f3 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs @@ -777,8 +777,8 @@ internal ref struct Av1SymbolDecoder public int ReadCoefficients( Av1BlockModeInfo modeInfo, Point blockPosition, - int[] aboveContexts, - int[] leftContexts, + Span aboveContexts, + Span leftContexts, int aboveOffset, int leftOffset, int plane, @@ -1176,8 +1176,8 @@ internal ref struct Av1SymbolDecoder /// The signed distance from the mode block to the right frame edge. /// The signed distance from the mode block to the bottom frame edge. private static void UpdateCoefficientContext( - int[] aboveContexts, - int[] leftContexts, + Span aboveContexts, + Span leftContexts, int blocksWide, int blocksHigh, Av1TransformSize transformSize, @@ -1194,23 +1194,23 @@ internal ref struct Av1SymbolDecoder if (modeBlockToRightEdge < 0) { int aboveContextCount = Math.Min(transformSizeWide, blocksWide - blockPosition.X); - Array.Fill(aboveContexts, culLevel, aboveOffset, aboveContextCount); - Array.Fill(aboveContexts, 0, aboveOffset + aboveContextCount, transformSizeWide - aboveContextCount); + aboveContexts.Slice(aboveOffset, aboveContextCount).Fill(culLevel); + aboveContexts.Slice(aboveOffset + aboveContextCount, transformSizeWide - aboveContextCount).Clear(); } else { - Array.Fill(aboveContexts, culLevel, aboveOffset, transformSizeWide); + aboveContexts.Slice(aboveOffset, transformSizeWide).Fill(culLevel); } if (modeBlockToBottomEdge < 0) { int leftContextCount = Math.Min(transformSizeHigh, blocksHigh - blockPosition.Y); - Array.Fill(leftContexts, culLevel, leftOffset, leftContextCount); - Array.Fill(leftContexts, 0, leftOffset + leftContextCount, transformSizeHigh - leftContextCount); + leftContexts.Slice(leftOffset, leftContextCount).Fill(culLevel); + leftContexts.Slice(leftOffset + leftContextCount, transformSizeHigh - leftContextCount).Clear(); } else { - Array.Fill(leftContexts, culLevel, leftOffset, transformSizeHigh); + leftContexts.Slice(leftOffset, transformSizeHigh).Fill(culLevel); } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ParseAboveNeighbor4x4Context.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ParseAboveNeighbor4x4Context.cs index 829d0e425..1737c29fa 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ParseAboveNeighbor4x4Context.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ParseAboveNeighbor4x4Context.cs @@ -1,65 +1,85 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; /// /// Stores entropy, partition, and transform contexts for 4-by-4 blocks above the current block. /// -internal class Av1ParseAboveNeighbor4x4Context +internal sealed class Av1ParseAboveNeighbor4x4Context : IDisposable { /// - /// Stores DC-sign and cumulative coefficient-level contexts for each plane above the current block. + /// The region containing transform-width contexts. /// - private readonly int[][] aboveContext = new int[Av1Constants.MaxPlanes][]; + private const int TransformWidthRegionIndex = 0; /// - /// Stores segmentation-prediction contexts from the preceding 4x4 row. + /// The region containing partition-width contexts. /// - private readonly int[] aboveSegmentIdPredictionContext; + private const int PartitionWidthRegionIndex = 1; /// - /// Stores compound-reference group contexts from the preceding 4x4 row. + /// The first region containing a color plane's coefficient contexts. /// - private readonly int[] aboveCompGroupIndex; + private const int PlaneContextRegionStart = 2; + + /// + /// Owns the contiguous above-neighbor storage until this instance is disposed. + /// + private IMemoryOwner? memory; + + /// + /// The number of mode-information columns stored in each logical region. + /// + private readonly int contextLength; /// /// Initializes a new instance of the class. /// + /// The configuration providing the memory allocator. /// The number of color planes. /// The frame width in 4x4 mode-information columns. - public Av1ParseAboveNeighbor4x4Context(int planesCount, int modeInfoColumnCount) + public Av1ParseAboveNeighbor4x4Context(Configuration configuration, int planesCount, int modeInfoColumnCount) { - this.AboveTransformWidth = new int[modeInfoColumnCount]; - this.AbovePartitionWidth = new int[modeInfoColumnCount]; - for (int i = 0; i < planesCount; i++) - { - this.aboveContext[i] = new int[modeInfoColumnCount]; - } + this.contextLength = modeInfoColumnCount; + int regionCount = PlaneContextRegionStart + planesCount; + int totalLength = checked(regionCount * modeInfoColumnCount); - this.aboveSegmentIdPredictionContext = new int[modeInfoColumnCount]; - this.aboveCompGroupIndex = new int[modeInfoColumnCount]; + // Every region spans the same aligned frame width and shares the tile-reader lifetime. + // One clean rent replaces the jagged array and its per-region arrays while preserving zero initialization. + this.memory = configuration.MemoryAllocator.Allocate(totalLength, AllocationOptions.Clean); } /// /// Gets a buffer holding the partition context of the previous 4x4 block row. /// - public int[] AbovePartitionWidth { get; } + public Span AbovePartitionWidth => this.GetRegion(PartitionWidthRegionIndex); /// /// Gets a buffer holding the transform sizes of the previous 4x4 block row. /// - public int[] AboveTransformWidth { get; } + public Span AboveTransformWidth => this.GetRegion(TransformWidthRegionIndex); /// /// Gets the coefficient context row for the specified plane. /// /// The zero-based plane index. /// The coefficient contexts for the plane. - public int[] GetContext(int plane) => this.aboveContext[plane]; + public Span GetContext(int plane) => this.GetRegion(PlaneContextRegionStart + plane); + + /// + /// Returns the above-neighbor storage to the configured memory allocator. + /// + public void Dispose() + { + this.memory?.Dispose(); + this.memory = null; + } /// /// Resets above-neighbor state for the active tile-column range. @@ -71,15 +91,12 @@ internal class Av1ParseAboveNeighbor4x4Context { int planeCount = sequenceHeader.ColorConfig.PlaneCount; int width = modeInfoColumnEnd - modeInfoColumnStart; - Array.Fill(this.AboveTransformWidth, Av1TransformSize.Size64x64.GetWidth(), 0, width); - Array.Fill(this.AbovePartitionWidth, 0, 0, width); + this.AboveTransformWidth[..width].Fill(Av1TransformSize.Size64x64.GetWidth()); + this.AbovePartitionWidth[..width].Clear(); for (int i = 0; i < planeCount; i++) { - Array.Fill(this.aboveContext[i], 0, 0, width); + this.GetContext(i)[..width].Clear(); } - - Array.Fill(this.aboveSegmentIdPredictionContext, 0, 0, width); - Array.Fill(this.aboveCompGroupIndex, 0, 0, width); } /// @@ -97,7 +114,7 @@ internal class Av1ParseAboveNeighbor4x4Context int value = Av1PartitionContext.GetAboveContext(subSize); DebugGuard.MustBeLessThanOrEqualTo(startIndex, this.AboveTransformWidth.Length - bw, nameof(startIndex)); - Array.Fill(this.AbovePartitionWidth, value, startIndex, bw); + this.AbovePartitionWidth.Slice(startIndex, bw).Fill(value); } /// @@ -120,7 +137,7 @@ internal class Av1ParseAboveNeighbor4x4Context } DebugGuard.MustBeLessThanOrEqualTo(startIndex, this.AboveTransformWidth.Length - n4w, nameof(startIndex)); - Array.Fill(this.AboveTransformWidth, transformWidth, startIndex, n4w); + this.AboveTransformWidth.Slice(startIndex, n4w).Fill(transformWidth); } /// @@ -129,6 +146,17 @@ internal class Av1ParseAboveNeighbor4x4Context /// The zero-based plane index. /// The first context index to clear. /// The number of context entries to clear. - internal void ClearContext(int plane, int offset, int length) - => Array.Fill(this.aboveContext[plane], 0, offset, length); + public void ClearContext(int plane, int offset, int length) + => this.GetContext(plane).Slice(offset, length).Clear(); + + /// + /// Gets one logical row from the contiguous above-neighbor allocation. + /// + /// The zero-based logical region index. + /// The requested context row. + private Span GetRegion(int regionIndex) + { + ObjectDisposedException.ThrowIf(this.memory is null, this); + return this.memory.Memory.Span.Slice(regionIndex * this.contextLength, this.contextLength); + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ParseLeftNeighbor4x4Context.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ParseLeftNeighbor4x4Context.cs index 576e483d8..e433c2b11 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ParseLeftNeighbor4x4Context.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ParseLeftNeighbor4x4Context.cs @@ -1,60 +1,80 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; /// /// Stores entropy, partition, and transform contexts for 4-by-4 blocks left of the current block. /// -internal class Av1ParseLeftNeighbor4x4Context +internal sealed class Av1ParseLeftNeighbor4x4Context : IDisposable { /// - /// Stores DC-sign and cumulative coefficient-level contexts for each plane left of the current block. + /// The region containing transform-height contexts. /// - private readonly int[][] leftContext = new int[Av1Constants.MaxPlanes][]; + private const int TransformHeightRegionIndex = 0; /// - /// Stores segmentation-prediction contexts for the current superblock row. + /// The region containing partition-height contexts. /// - private readonly int[] leftSegmentIdPredictionContext; + private const int PartitionHeightRegionIndex = 1; /// - /// Stores compound-reference group contexts for the current superblock row. + /// The first region containing a color plane's coefficient contexts. /// - private readonly int[] leftCompGroupIndex; + private const int PlaneContextRegionStart = 2; + + /// + /// Owns the contiguous left-neighbor storage until this instance is disposed. + /// + private IMemoryOwner? memory; + + /// + /// The number of superblock-row entries stored in each logical region. + /// + private readonly int contextLength; /// /// Initializes a new instance of the class. /// + /// The configuration providing the memory allocator. /// The number of color planes. /// The superblock height in 4x4 mode-information rows. - public Av1ParseLeftNeighbor4x4Context(int planesCount, int superblockModeInfoSize) + public Av1ParseLeftNeighbor4x4Context(Configuration configuration, int planesCount, int superblockModeInfoSize) { - this.LeftTransformHeight = new int[superblockModeInfoSize]; - this.LeftPartitionHeight = new int[superblockModeInfoSize]; - for (int i = 0; i < planesCount; i++) - { - this.leftContext[i] = new int[superblockModeInfoSize]; - } + this.contextLength = superblockModeInfoSize; + int regionCount = PlaneContextRegionStart + planesCount; + int totalLength = checked(regionCount * superblockModeInfoSize); - this.leftSegmentIdPredictionContext = new int[superblockModeInfoSize]; - this.leftCompGroupIndex = new int[superblockModeInfoSize]; + // Every region spans the same superblock height and shares the tile-reader lifetime. + // One clean rent replaces the jagged array and its per-region arrays while preserving zero initialization. + this.memory = configuration.MemoryAllocator.Allocate(totalLength, AllocationOptions.Clean); } /// /// Gets a buffer holding the partition context of the left 4x4 blocks corresponding /// to the current super block row. /// - public int[] LeftPartitionHeight { get; } + public Span LeftPartitionHeight => this.GetRegion(PartitionHeightRegionIndex); /// /// Gets a buffer holding the transform sizes of the left 4x4 blocks corresponding /// to the current super block row. /// - public int[] LeftTransformHeight { get; } + public Span LeftTransformHeight => this.GetRegion(TransformHeightRegionIndex); + + /// + /// Returns the left-neighbor storage to the configured memory allocator. + /// + public void Dispose() + { + this.memory?.Dispose(); + this.memory = null; + } /// /// Resets all left-neighbor state for a new superblock row. @@ -64,16 +84,12 @@ internal class Av1ParseLeftNeighbor4x4Context { int blockCount = sequenceHeader.SuperblockModeInfoSize; int planeCount = sequenceHeader.ColorConfig.PlaneCount; - int neighbor4x4Count = sequenceHeader.SuperblockModeInfoSize; - Array.Fill(this.LeftTransformHeight, Av1TransformSize.Size64x64.GetHeight(), 0, blockCount); - Array.Fill(this.LeftPartitionHeight, 0, 0, blockCount); + this.LeftTransformHeight[..blockCount].Fill(Av1TransformSize.Size64x64.GetHeight()); + this.LeftPartitionHeight[..blockCount].Clear(); for (int i = 0; i < planeCount; i++) { - Array.Fill(this.leftContext[i], 0, 0, blockCount); + this.GetContext(i)[..blockCount].Clear(); } - - Array.Fill(this.leftSegmentIdPredictionContext, 0, 0, blockCount); - Array.Fill(this.leftCompGroupIndex, 0, 0, blockCount); } /// @@ -90,7 +106,7 @@ internal class Av1ParseLeftNeighbor4x4Context int bh = blockSize.Get4x4HighCount(); int value = Av1PartitionContext.GetLeftContext(subSize); DebugGuard.MustBeLessThanOrEqualTo(startIndex, this.LeftPartitionHeight.Length - bh, nameof(startIndex)); - Array.Fill(this.LeftPartitionHeight, value, startIndex, bh); + this.LeftPartitionHeight.Slice(startIndex, bh).Fill(value); } /// @@ -113,7 +129,7 @@ internal class Av1ParseLeftNeighbor4x4Context } DebugGuard.MustBeLessThanOrEqualTo(startIndex, this.LeftTransformHeight.Length - n4h, nameof(startIndex)); - Array.Fill(this.LeftTransformHeight, transformHeight, startIndex, n4h); + this.LeftTransformHeight.Slice(startIndex, n4h).Fill(transformHeight); } /// @@ -122,13 +138,24 @@ internal class Av1ParseLeftNeighbor4x4Context /// The zero-based plane index. /// The first context index to clear. /// The number of context entries to clear. - internal void ClearContext(int plane, int offset, int length) - => Array.Fill(this.leftContext[plane], 0, offset, length); + public void ClearContext(int plane, int offset, int length) + => this.GetContext(plane).Slice(offset, length).Clear(); /// /// Gets the coefficient context column for the specified plane. /// /// The zero-based plane index. /// The coefficient contexts for the plane. - internal int[] GetContext(int plane) => this.leftContext[plane]; + public Span GetContext(int plane) => this.GetRegion(PlaneContextRegionStart + plane); + + /// + /// Gets one logical column from the contiguous left-neighbor allocation. + /// + /// The zero-based logical region index. + /// The requested context column. + private Span GetRegion(int regionIndex) + { + ObjectDisposedException.ThrowIf(this.memory is null, this); + return this.memory.Memory.Span.Slice(regionIndex * this.contextLength, this.contextLength); + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs index bd7d7c2a1..338a987b5 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; /// /// Parses partition, mode, transform, and coefficient syntax for one AV1 tile. /// -internal class Av1TileReader : IAv1TileReader +internal sealed class Av1TileReader : IAv1TileReader, IDisposable { /// /// The default self-guided restoration projection coefficients for each color plane. @@ -180,13 +180,24 @@ internal class Av1TileReader : IAv1TileReader Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, sequenceHeader.SuperblockSizeLog2) >> sequenceHeader.SuperblockSizeLog2; int modeInfoWideColumnCount = superblockColumnCount * sequenceHeader.SuperblockModeInfoSize; modeInfoWideColumnCount = Av1Math.AlignPowerOf2(modeInfoWideColumnCount, sequenceHeader.SuperblockSizeLog2 - Av1Constants.ModeInfoSizeLog2); - this.aboveNeighborContext = new Av1ParseAboveNeighbor4x4Context(planesCount, modeInfoWideColumnCount); - this.leftNeighborContext = new Av1ParseLeftNeighbor4x4Context(planesCount, sequenceHeader.SuperblockModeInfoSize); this.transformUnitCount = new int[Av1Constants.MaxPlanes][]; this.transformUnitCount[0] = new int[this.FrameInfo.ModeInfoCount]; this.transformUnitCount[1] = new int[this.FrameInfo.ModeInfoCount]; this.transformUnitCount[2] = new int[this.FrameInfo.ModeInfoCount]; this.coefficientIndex = new int[Av1Constants.MaxPlanes]; + + this.aboveNeighborContext = new Av1ParseAboveNeighbor4x4Context(configuration, planesCount, modeInfoWideColumnCount); + try + { + this.leftNeighborContext = new Av1ParseLeftNeighbor4x4Context(configuration, planesCount, sequenceHeader.SuperblockModeInfoSize); + } + catch + { + // The reader is not returned when its second context allocation fails, so release + // the first rent here rather than relying on an owner that the caller cannot reach. + this.aboveNeighborContext.Dispose(); + throw; + } } /// @@ -215,6 +226,15 @@ internal class Av1TileReader : IAv1TileReader /// public Av1FrameInfo FrameInfo { get; } + /// + /// Returns the tile-neighbor context storage to the configured memory allocator. + /// + public void Dispose() + { + this.aboveNeighborContext.Dispose(); + this.leftNeighborContext.Dispose(); + } + /// /// Parses one tile's partition, mode, transform, coefficient, and filter syntax in superblock order. /// @@ -984,8 +1004,8 @@ internal class Av1TileReader : IAv1TileReader int leftOffset) { Av1TransformBlockContext transformBlockContext = new(); - ReadOnlySpan aboveContext = this.aboveNeighborContext.GetContext(plane).AsSpan(aboveOffset); - ReadOnlySpan leftContext = this.leftNeighborContext.GetContext(plane).AsSpan(leftOffset); + ReadOnlySpan aboveContext = this.aboveNeighborContext.GetContext(plane)[aboveOffset..]; + ReadOnlySpan leftContext = this.leftNeighborContext.GetContext(plane)[leftOffset..]; int dcSign = 0; int k = 0; int mask = (1 << Av1Constants.CoefficientContextBitCount) - 1; diff --git a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs index c8ba7e052..a6fe3a737 100644 --- a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs @@ -61,7 +61,7 @@ internal class Av1HeifItemDecoder : IHeifItemDecoder out HeifContentLightLevel? obuContentLightLevel, out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume); - Av1Decoder decoder = new(configuration); + using Av1Decoder decoder = new(configuration); Image image = decoder.Decode(data, colorProfile, codecConfiguration); HeifMetadata metadata = image.Metadata.GetHeifMetadata(); metadata.CompressionMethod = this.CompressionMethod;