diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md
index 931ff88356..988185931d 100644
--- a/HEIF_IMPLEMENTATION_PLAN.md
+++ b/HEIF_IMPLEMENTATION_PLAN.md
@@ -829,7 +829,7 @@ Encoder verification contract:
- [ ] Implement real rate-distortion selection and make quality and effort change work, size, and output quality.
- [~] The tile writer now publishes one packed coefficient context per covered 4x4 edge unit and derives luma/chroma skip plus DC-sign contexts from the complete transform edges using current-libaom units. Complete tile traversal, initialized picture state, and verified CDF update behavior remain.
- [~] Encoder mode information now uses a frame-owned integer alias grid over a packed 8-byte value allocation, matching current libaom's `mi_grid_base` and `mi_alloc` relationship without a managed object or reference per 4x4 entry. Every coded 4x4 cell covered by square, rectangular, or clipped edge blocks maps to its owning allocation entry before context-dependent symbols are written. At 4K, mode values occupy about 4.0 MiB and the alias grid about 2.0 MiB. Packed syntax, relative neighbor lookup, full block mapping, writer traversal, entropy, and OBU coverage pass 1,947 of 1,947 direct net11 VSTest cases in Release; complete mode decision still remains.
-- [~] Final block decisions now use contiguous value storage with palette and prediction syntax inline. Macroblock edge and neighbor state is reused by the entropy-coding operation instead of allocating one managed object per final block; directional deltas retain their signed range, while complete mode decision still remains.
+- [~] The final-block decision workspace uses one reusable 10.3 KiB ImageSharp allocator owner. It contains 1,024 explicitly packed 10-byte final-block entries and the 341 preorder partition bytes required by a complete 128x128-through-8x8 quadtree, replacing separate managed arrays. Initial rental is clean, and the explicit per-superblock reset clears the complete owner so pooled palette, quantizer, prediction, and partition bytes cannot leak into the next decision pass. Exact allocation, size, reset, return, repeated-run, writer, entropy, and OBU coverage pass 1,948 of 1,948 direct net11 VSTest cases in Release; complete mode decision still remains.
- [~] Finalized transform coefficients and packed EOB/type state now use raster-ordered, per-superblock plane segments matching current libaom's coefficient-pool geometry. One ImageSharp allocator owner replaces libaom's separate coefficient, EOB, and entropy-context allocations while preserving the full 1024 luma and 256-per-chroma 4x4 state capacity of a 128x128 4:2:0 superblock. The transform-block boundary can populate the owner's quantized coefficient and state slices directly; mode-decision traversal still needs to select and invoke it.
- [~] Tile partition writing now follows current libaom's recursive `write_modes_sb` preorder traversal and `update_ext_partition_context` edge updates directly. The obsolete SVT-derived global geometry catalog and its unimplemented lookup are removed; transform geometry is derived in libaom's bounded 64x64 residual order, fixed intra transform-size symbols use the reference depth and neighbor contexts, frame-edge and segmentation syntax use mode-information units, and 128x128 CDEF units use libaom's 0-to-3 indexing and first-block strength ownership. Partition and mode analysis still need to populate these retained decisions; variable inter-transform syntax remains part of later inter-frame support.
- [ ] Implement legal deblocking, CDEF, restoration, super-resolution, and film-grain signaling decisions.
diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockStruct.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockStruct.cs
index d43486f145..7277077937 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockStruct.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockStruct.cs
@@ -3,14 +3,21 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
///
/// Stores encoder block geometry and its selected coding-mode information.
///
+[StructLayout(LayoutKind.Sequential, Pack = 1, Size = StorageSize)]
internal struct Av1EncoderBlockStruct
{
+ ///
+ /// The fixed byte width of one packed final-block decision.
+ ///
+ public const int StorageSize = 10;
+
///
/// Stores the luma and shared chroma palette sizes inline with the block.
///
@@ -21,25 +28,48 @@ internal struct Av1EncoderBlockStruct
///
private Av1EncoderPredictionUnit predictionUnit;
+ // These syntax values have AV1-defined byte-sized ranges. Storing their encoded widths explicitly
+ // prevents CLR field alignment from inflating every entry in the 1,024-element decision workspace.
+ private byte hasChroma;
+ private byte quantizationIndex;
+ private byte segmentId;
+ private byte filterIntraMode;
+
///
/// Gets or sets a value indicating whether this luma block owns the corresponding chroma syntax.
///
- public bool HasChroma { get; set; }
+ public bool HasChroma
+ {
+ readonly get => this.hasChroma != 0;
+ set => this.hasChroma = value ? (byte)1 : (byte)0;
+ }
///
/// Gets or sets the quantizer index used for the block.
///
- public int QuantizationIndex { get; set; }
+ public int QuantizationIndex
+ {
+ readonly get => this.quantizationIndex;
+ set => this.quantizationIndex = (byte)value;
+ }
///
/// Gets or sets the segmentation identifier assigned to the block.
///
- public int SegmentId { get; set; }
+ public int SegmentId
+ {
+ readonly get => this.segmentId;
+ set => this.segmentId = (byte)value;
+ }
///
/// Gets or sets the filter-intra mode selected for the block.
///
- public Av1FilterIntraMode FilterIntraMode { get; set; }
+ public Av1FilterIntraMode FilterIntraMode
+ {
+ readonly get => (Av1FilterIntraMode)this.filterIntraMode;
+ set => this.filterIntraMode = (byte)value;
+ }
///
/// Gets the writable palette sizes for luma and for the shared chroma mode.
diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPredictionUnit.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPredictionUnit.cs
index 39c17cb5c1..47865e43f1 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPredictionUnit.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPredictionUnit.cs
@@ -3,12 +3,14 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
///
/// Stores encoder-selected intra prediction modes and directional-angle adjustments for one block.
///
+[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 4)]
internal struct Av1EncoderPredictionUnit
{
///
diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderSuperblockWorkspace.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderSuperblockWorkspace.cs
new file mode 100644
index 0000000000..5b63694936
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderSuperblockWorkspace.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
+
+///
+/// Owns the reusable final-block and partition decisions for one AV1 superblock.
+///
+internal sealed class Av1EncoderSuperblockWorkspace : IDisposable
+{
+ ///
+ /// The maximum number of 4x4 final blocks in a 128x128 superblock.
+ ///
+ public const int MaximumFinalBlockCount = 1 << (2 * (Av1Constants.MaxSuperBlockSizeLog2 - Av1Constants.ModeInfoSizeLog2));
+
+ ///
+ /// The maximum number of partition nodes in the complete 128x128 through 8x8 quadtree.
+ ///
+ public const int MaximumPartitionCount = 1 + 4 + 16 + 64 + 256;
+
+ ///
+ /// The complete workspace length in packed final-block storage elements.
+ ///
+ public const int StorageLength = MaximumFinalBlockCount + ((MaximumPartitionCount + Av1EncoderBlockStruct.StorageSize - 1) / Av1EncoderBlockStruct.StorageSize);
+
+ private readonly IMemoryOwner owner;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration providing the encoder allocator.
+ public Av1EncoderSuperblockWorkspace(Configuration configuration)
+ => this.owner = configuration.MemoryAllocator.Allocate(StorageLength, AllocationOptions.Clean);
+
+ ///
+ /// Gets the maximum-size final-block decision span in partition traversal order.
+ ///
+ public Span FinalBlocks => this.owner.Memory.Span[..MaximumFinalBlockCount];
+
+ ///
+ /// Gets the maximum-size partition-type span in partition-tree preorder.
+ ///
+ public Span PartitionTypes
+ => MemoryMarshal.AsBytes(this.owner.Memory.Span[MaximumFinalBlockCount..])[..MaximumPartitionCount];
+
+ ///
+ /// Clears all decisions before the workspace is reused for another superblock.
+ ///
+ public void Reset() => this.owner.Memory.Span.Clear();
+
+ ///
+ /// Releases the reusable superblock workspace.
+ ///
+ public void Dispose() => this.owner.Dispose();
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1Superblock.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1Superblock.cs
index 59dd140d48..73d38af89c 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1Superblock.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1Superblock.cs
@@ -9,9 +9,14 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
internal class Av1Superblock
{
///
- /// Gets or sets the final encoder decisions in partition traversal order.
+ /// Gets or sets the reusable final-block and partition-decision workspace.
///
- public required Av1EncoderBlockStruct[] FinalBlocks { get; set; }
+ public required Av1EncoderSuperblockWorkspace Workspace { get; set; }
+
+ ///
+ /// Gets the final encoder decisions in partition traversal order.
+ ///
+ public Span FinalBlocks => this.Workspace.FinalBlocks;
///
/// Gets or sets the tile containing the superblock.
@@ -19,9 +24,9 @@ internal class Av1Superblock
public required Av1TileInfo TileInfo { get; set; }
///
- /// Gets or sets the selected partition type for each partition-tree node.
+ /// Gets the selected partition type for each partition-tree node.
///
- public required Av1PartitionType[] CodingUnitPartitionTypes { get; set; }
+ public Span CodingUnitPartitionTypes => this.Workspace.PartitionTypes;
///
/// Gets or sets the superblock index within the picture.
diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs
index b015fd7f2f..e14950226c 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs
@@ -112,7 +112,7 @@ internal partial class Av1TileWriter
return;
}
- Av1PartitionType partition = superblock.CodingUnitPartitionTypes[partitionIndex++];
+ Av1PartitionType partition = (Av1PartitionType)superblock.CodingUnitPartitionTypes[partitionIndex++];
Av1BlockSize subSize = partition.GetBlockSubSize(blockSize);
int halfBlockSize = blockSize.GetWidth() >> 1;
int quarterBlockSize = blockSize.GetWidth() >> 2;
diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs
index 15b966495d..8b0967b7e0 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs
@@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Tests.Memory;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
@@ -192,6 +193,46 @@ public class Av1CoefficientsEntropyTests
Assert.Equal(8, Unsafe.SizeOf());
}
+ [Fact]
+ public void EncoderSuperblockWorkspaceUsesOneExactSizeOwner()
+ {
+ TestMemoryAllocator allocator = new();
+ allocator.EnableNonThreadSafeLogging();
+ Configuration configuration = Configuration.Default.Clone();
+ configuration.MemoryAllocator = allocator;
+
+ TestMemoryAllocator.AllocationRequest allocation;
+ using (Av1EncoderSuperblockWorkspace workspace = new(configuration))
+ {
+ allocation = Assert.Single(allocator.AllocationLog);
+ Assert.Empty(allocator.ReturnLog);
+ Assert.Equal(typeof(Av1EncoderBlockStruct), allocation.ElementType);
+ Assert.Equal(AllocationOptions.Clean, allocation.AllocationOptions);
+ Assert.Equal(Av1EncoderSuperblockWorkspace.StorageLength, allocation.Length);
+ Assert.Equal(Av1EncoderSuperblockWorkspace.MaximumFinalBlockCount, workspace.FinalBlocks.Length);
+ Assert.Equal(Av1EncoderSuperblockWorkspace.MaximumPartitionCount, workspace.PartitionTypes.Length);
+ Assert.Equal(Av1EncoderBlockStruct.StorageSize, Unsafe.SizeOf());
+ Assert.Equal(0, workspace.FinalBlocks[0].PaletteSize[0]);
+ Assert.Equal(0, workspace.FinalBlocks[^1].QuantizationIndex);
+ Assert.Equal(0, workspace.PartitionTypes[^1]);
+
+ workspace.FinalBlocks[0].PaletteSize[0] = 7;
+ workspace.FinalBlocks[^1].QuantizationIndex = 255;
+ workspace.PartitionTypes.Fill(byte.MaxValue);
+ workspace.Reset();
+
+ Assert.Equal(0, workspace.FinalBlocks[0].PaletteSize[0]);
+ Assert.Equal(0, workspace.FinalBlocks[^1].QuantizationIndex);
+ for (int index = 0; index < workspace.PartitionTypes.Length; index++)
+ {
+ Assert.Equal(0, workspace.PartitionTypes[index]);
+ }
+ }
+
+ TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
+ Assert.Equal(allocation.AllocationId, returned.AllocationId);
+ }
+
[Fact]
public void EncoderBlocksKeepInlineModeStateWithoutPerBlockAllocations()
{
@@ -634,7 +675,7 @@ public class Av1CoefficientsEntropyTests
picture.TransformFunctionContexts = [transforms];
Av1TileInfo tile = new(0, 0, picture.Parent.FrameHeader);
Point[] blockPositions = [new(16, 0), new(24, 0), new(16, 8), new(24, 8)];
- Av1EncoderBlockStruct[] blocks = new Av1EncoderBlockStruct[blockPositions.Length];
+ using Av1EncoderSuperblockWorkspace workspace = new(Configuration.Default);
for (int index = 0; index < blockPositions.Length; index++)
{
Point position = blockPositions[index];
@@ -645,21 +686,23 @@ public class Av1CoefficientsEntropyTests
blockMode.Skip = true;
blockMode.Mode = Av1PredictionMode.DC;
blockMode.UvMode = Av1ChromaPredictionMode.DC;
- blocks[index] = new Av1EncoderBlockStruct { HasChroma = false };
+ workspace.FinalBlocks[index].HasChroma = false;
}
+ ReadOnlySpan partitionTypes =
+ [
+ (byte)Av1PartitionType.Split,
+ (byte)Av1PartitionType.None,
+ (byte)Av1PartitionType.None,
+ (byte)Av1PartitionType.None,
+ (byte)Av1PartitionType.None
+ ];
+
+ partitionTypes.CopyTo(workspace.PartitionTypes);
Av1Superblock superblock = new()
{
- FinalBlocks = blocks,
+ Workspace = workspace,
TileInfo = tile,
- CodingUnitPartitionTypes =
- [
- Av1PartitionType.Split,
- Av1PartitionType.None,
- Av1PartitionType.None,
- Av1PartitionType.None,
- Av1PartitionType.None
- ],
Index = 1
};
Av1TileWriter.Av1EntropyCodingContext context = new()