diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md
index 358e0c187c..dc91af89f3 100644
--- a/HEIF_IMPLEMENTATION_PLAN.md
+++ b/HEIF_IMPLEMENTATION_PLAN.md
@@ -828,7 +828,7 @@ Encoder verification contract:
- [ ] Implement inter mode search for bounded sequences, including reference selection and the decoder-supported inter tools.
- [~] Current-libaom `av1_quantize_fp_no_qmatrix` arithmetic is implemented as a closed generic forward-quantizer family with Vector512, Vector256, Vector128, and scalar paths, raster-order output, coded 64-point coefficient limits, and scan-order EOB selection. Transform search, coefficient optimization, and lossless behavior remain.
- [ ] 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.
+- [~] 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. Partition, transform, and coefficient neighbor state now retains only the above and left context regions used by current libaom; the unused third top-left region, its granularity state, and its unused sentinel are removed. One clean allocation contains the two active edges, and the exact requested length plus exactly-once return pass with the complete 77-case coefficient and entropy class in direct net11 VSTest Release. 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. The visible dimensions are aligned to eight luma samples, the grid stride and allocated row count are aligned to 32 mode-information units, and optional 8x8 allocation granularity reduces the value store in both dimensions exactly as current libaom does. One clean ImageSharp byte owner contains both independently typed regions, reducing libaom's two allocation lifetimes to one without a copy. At 4K, the 4x4 layout occupies about 6.0 MiB in total; the 8x8 layout occupies about 3.0 MiB. Exact geometry, clean allocation, typed lengths, aligned mapping, untouched row padding, and exactly-once return pass 4 of 4 direct net11 VSTest cases in Release. Every coded 4x4 cell covered by square, rectangular, or clipped edge blocks maps to its owning allocation entry before context-dependent symbols are written. 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.
- [~] 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. Construction and the explicit per-superblock reset initialize every syntax field, including the nonzero sentinel that disables filter-intra prediction; pooled palette, quantizer, prediction, and partition bytes cannot leak into the next decision pass. Exact allocation, size, initialization, reset, return, repeated-run, writer, entropy, and OBU coverage pass 1,957 of 1,957 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.
diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs
index 6af171473a..892993aabd 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs
@@ -2,23 +2,17 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
-using System.Numerics;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
///
-/// Stores left, top, and top-left neighbor values at the granularity required by AV1 encoder contexts.
+/// Stores left and top neighbor values at the granularity required by AV1 encoder contexts.
///
-/// The context value type, including its invalid sentinel value.
+/// The context value type.
internal sealed class Av1NeighborArrayUnit : IDisposable
- where T : struct, IMinMaxValue
+ where T : struct
{
- ///
- /// The sentinel used for neighbor positions that have not been populated.
- ///
- public static readonly T InvalidNeighborData = T.MaxValue;
-
///
/// Owns the contiguous neighbor storage until this instance is disposed.
///
@@ -34,27 +28,20 @@ internal sealed class Av1NeighborArrayUnit : IDisposable
///
private readonly int topLength;
- ///
- /// The number of context values indexed by the diagonal difference between horizontal and vertical positions.
- ///
- private readonly int topLeftLength;
-
///
/// Initializes a new instance of the class.
///
/// The configuration providing the memory allocator.
/// The number of values in the left-neighbor storage.
/// The number of values in the top-neighbor storage.
- /// The number of values in the diagonal-neighbor storage.
- public Av1NeighborArrayUnit(Configuration configuration, int leftSize, int topSize, int topLeftSize)
+ public Av1NeighborArrayUnit(Configuration configuration, int leftSize, int topSize)
{
this.leftLength = leftSize;
this.topLength = topSize;
- this.topLeftLength = topLeftSize;
- int totalLength = checked(leftSize + topSize + topLeftSize);
+ int totalLength = checked(leftSize + topSize);
- // All three neighbor regions share the picture lifetime, so one clean allocator-backed
- // buffer avoids three managed arrays and preserves their zero-initialized starting state.
+ // Both context edges share the picture lifetime, so one clean allocator-backed buffer
+ // preserves their zero-initialized starting state without separate owner lifetimes.
this.memory = configuration.MemoryAllocator.Allocate(totalLength, AllocationOptions.Clean);
}
@@ -73,11 +60,6 @@ internal sealed class Av1NeighborArrayUnit : IDisposable
/// Update the top-neighbor storage.
///
Top = 2,
-
- ///
- /// Update the top-left diagonal storage.
- ///
- TopLeft = 4,
}
///
@@ -104,28 +86,11 @@ internal sealed class Av1NeighborArrayUnit : IDisposable
}
}
- ///
- /// Gets the top-left diagonal storage.
- ///
- public Span TopLeft
- {
- get
- {
- ObjectDisposedException.ThrowIf(this.memory is null, this);
- return this.memory.Memory.Span.Slice(this.leftLength + this.topLength, this.topLeftLength);
- }
- }
-
///
/// Gets or sets the base-2 logarithm of the top and left context granularity in samples.
///
public required int GranularityNormalLog2 { get; set; }
- ///
- /// Gets or sets the base-2 logarithm of the diagonal context granularity in samples.
- ///
- public required int GranularityTopLeftLog2 { get; set; }
-
///
/// Gets the left-neighbor unit index for a sample position.
///
@@ -140,14 +105,6 @@ internal sealed class Av1NeighborArrayUnit : IDisposable
/// The top-neighbor unit index.
public int GetTopIndex(Point loc) => loc.X >> this.GranularityNormalLog2;
- ///
- /// Gets the diagonal-neighbor unit index for a sample position.
- ///
- /// The sample position.
- /// The top-left neighbor index derived from the position's diagonal.
- public int GetTopLeftIndex(Point loc)
- => this.leftLength + (loc.X >> this.GranularityTopLeftLog2) - (loc.Y >> this.GranularityTopLeftLog2);
-
///
/// Returns the neighbor storage to the configured memory allocator.
///
@@ -211,30 +168,5 @@ internal sealed class Av1NeighborArrayUnit : IDisposable
int count = blockSize.Height >> this.GranularityNormalLog2;
this.Left.Slice(offset, count).Fill(value);
}
-
- if ((mask & UnitMask.TopLeft) == UnitMask.TopLeft)
- {
- // Top-left Neighbor Array
- //
- // 4-5--6--7------------
- // 3 \ \
- // 2 \ \
- // 1 \ \
- // |\ xxxxxx7
- // | \ x 6
- // | \ x 5
- // | \1x2x3x4
- // |
- //
- // The top-left neighbor array is updated with the reversed samples
- // from the right column and bottom row of the source block
- //
- // Index = org_x - org_y
- Point topLeft = origin;
- topLeft.Offset(0, blockSize.Height - 1);
- int offset = this.GetTopLeftIndex(topLeft);
- int count = ((blockSize.Width + blockSize.Height) >> this.GranularityTopLeftLog2) - 1;
- this.TopLeft.Slice(offset, count).Fill(value);
- }
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs
index 54c0113783..5b2c0b489e 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs
@@ -25,11 +25,9 @@ public class Av1CoefficientsEntropyTests
using Av1NeighborArrayUnit neighbors = new(
Configuration.Default,
leftSize: 8,
- topSize: 8,
- topLeftSize: 16)
+ topSize: 8)
{
- GranularityNormalLog2 = 2,
- GranularityTopLeftLog2 = 2
+ GranularityNormalLog2 = 2
};
neighbors.UnitModeWrite(
@@ -42,6 +40,32 @@ public class Av1CoefficientsEntropyTests
Assert.Equal(new byte[] { 0, 37, 37, 0, 0, 0, 0, 0 }, neighbors.Left.ToArray());
}
+ [Fact]
+ public void NeighborArrayOwnsOnlyLeftAndTopContexts()
+ {
+ TestMemoryAllocator allocator = new();
+ allocator.EnableNonThreadSafeLogging();
+ Configuration configuration = Configuration.Default.Clone();
+ configuration.MemoryAllocator = allocator;
+
+ TestMemoryAllocator.AllocationRequest allocation;
+ using (Av1NeighborArrayUnit neighbors = new(configuration, leftSize: 8, topSize: 12)
+ {
+ GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
+ })
+ {
+ allocation = Assert.Single(allocator.AllocationLog);
+ Assert.Empty(allocator.ReturnLog);
+ Assert.Equal(20, allocation.Length);
+ Assert.Equal(AllocationOptions.Clean, allocation.AllocationOptions);
+ Assert.Equal(8, neighbors.Left.Length);
+ Assert.Equal(12, neighbors.Top.Length);
+ }
+
+ TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
+ Assert.Equal(allocation.AllocationId, returned.AllocationId);
+ }
+
[Theory]
[InlineData((int)Av1ComponentType.Luminance, 5)]
[InlineData((int)Av1ComponentType.Chroma, 12)]
@@ -52,11 +76,9 @@ public class Av1CoefficientsEntropyTests
using Av1NeighborArrayUnit neighbors = new(
Configuration.Default,
leftSize: 8,
- topSize: 8,
- topLeftSize: 16)
+ topSize: 8)
{
- GranularityNormalLog2 = 2,
- GranularityTopLeftLog2 = 2
+ GranularityNormalLog2 = 2
};
// The high bits carry positive, positive, and negative DC signs. The low bits select
@@ -358,11 +380,9 @@ public class Av1CoefficientsEntropyTests
using Av1NeighborArrayUnit luma = new(
Configuration.Default,
leftSize: 128,
- topSize: 128,
- topLeftSize: 256)
+ topSize: 128)
{
- GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2,
- GranularityTopLeftLog2 = Av1Constants.ModeInfoSizeLog2
+ GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
using Av1EncoderCoefficientBuffer coefficients = new(
@@ -447,11 +467,9 @@ public class Av1CoefficientsEntropyTests
using Av1NeighborArrayUnit transforms = new(
Configuration.Default,
leftSize: 64,
- topSize: 64,
- topLeftSize: 128)
+ topSize: 64)
{
- GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2,
- GranularityTopLeftLog2 = Av1Constants.ModeInfoSizeLog2
+ GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
int topIndex = transforms.GetTopIndex(blockOrigin);
@@ -510,11 +528,9 @@ public class Av1CoefficientsEntropyTests
using Av1NeighborArrayUnit neighbors = new(
Configuration.Default,
leftSize: 16,
- topSize: 16,
- topLeftSize: 32)
+ topSize: 16)
{
- GranularityNormalLog2 = 2,
- GranularityTopLeftLog2 = 2
+ GranularityNormalLog2 = 2
};
Av1PartitionType partition = (Av1PartitionType)partitionValue;
@@ -543,11 +559,9 @@ public class Av1CoefficientsEntropyTests
using Av1NeighborArrayUnit neighbors = new(
Configuration.Default,
leftSize: 4,
- topSize: 4,
- topLeftSize: 8)
+ topSize: 4)
{
- GranularityNormalLog2 = 2,
- GranularityTopLeftLog2 = 2
+ GranularityNormalLog2 = 2
};
Av1TileWriter.UpdatePartitionContexts(
@@ -633,51 +647,41 @@ public class Av1CoefficientsEntropyTests
using Av1NeighborArrayUnit partitions = new(
Configuration.Default,
leftSize: 16,
- topSize: 32,
- topLeftSize: 48)
+ topSize: 32)
{
- GranularityNormalLog2 = 2,
- GranularityTopLeftLog2 = 2
+ GranularityNormalLog2 = 2
};
using Av1NeighborArrayUnit luma = new(
Configuration.Default,
leftSize: 16,
- topSize: 32,
- topLeftSize: 48)
+ topSize: 32)
{
- GranularityNormalLog2 = 2,
- GranularityTopLeftLog2 = 2
+ GranularityNormalLog2 = 2
};
using Av1NeighborArrayUnit red = new(
Configuration.Default,
leftSize: 16,
- topSize: 32,
- topLeftSize: 48)
+ topSize: 32)
{
- GranularityNormalLog2 = 2,
- GranularityTopLeftLog2 = 2
+ GranularityNormalLog2 = 2
};
using Av1NeighborArrayUnit blue = new(
Configuration.Default,
leftSize: 16,
- topSize: 32,
- topLeftSize: 48)
+ topSize: 32)
{
- GranularityNormalLog2 = 2,
- GranularityTopLeftLog2 = 2
+ GranularityNormalLog2 = 2
};
using Av1NeighborArrayUnit transforms = new(
Configuration.Default,
leftSize: 16,
- topSize: 32,
- topLeftSize: 48)
+ topSize: 32)
{
- GranularityNormalLog2 = 2,
- GranularityTopLeftLog2 = 2
+ GranularityNormalLog2 = 2
};
picture.PartitionContexts = [partitions];
@@ -785,11 +789,9 @@ public class Av1CoefficientsEntropyTests
using Av1NeighborArrayUnit neighbors = new(
Configuration.Default,
leftSize: 1,
- topSize: 1,
- topLeftSize: 2)
+ topSize: 1)
{
- GranularityNormalLog2 = 2,
- GranularityTopLeftLog2 = 2
+ GranularityNormalLog2 = 2
};
Av1PartitionType nonSplitPartition = bottomEdge ? Av1PartitionType.Horizontal : Av1PartitionType.Vertical;