Browse Source

Remove unused AV1 neighbor storage

pull/2633/head
James Jackson-South 1 month ago
parent
commit
316e2cf204
  1. 2
      HEIF_IMPLEMENTATION_PLAN.md
  2. 82
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs
  3. 98
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs

2
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. - [ ] 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. - [~] 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. - [ ] 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. - [~] 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. - [~] 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. - [~] 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.

82
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs

@ -2,23 +2,17 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers; using System.Buffers;
using System.Numerics;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <typeparam name="T">The context value type, including its invalid sentinel value.</typeparam> /// <typeparam name="T">The context value type.</typeparam>
internal sealed class Av1NeighborArrayUnit<T> : IDisposable internal sealed class Av1NeighborArrayUnit<T> : IDisposable
where T : struct, IMinMaxValue<T> where T : struct
{ {
/// <summary>
/// The sentinel used for neighbor positions that have not been populated.
/// </summary>
public static readonly T InvalidNeighborData = T.MaxValue;
/// <summary> /// <summary>
/// Owns the contiguous neighbor storage until this instance is disposed. /// Owns the contiguous neighbor storage until this instance is disposed.
/// </summary> /// </summary>
@ -34,27 +28,20 @@ internal sealed class Av1NeighborArrayUnit<T> : IDisposable
/// </summary> /// </summary>
private readonly int topLength; private readonly int topLength;
/// <summary>
/// The number of context values indexed by the diagonal difference between horizontal and vertical positions.
/// </summary>
private readonly int topLeftLength;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1NeighborArrayUnit{T}"/> class. /// Initializes a new instance of the <see cref="Av1NeighborArrayUnit{T}"/> class.
/// </summary> /// </summary>
/// <param name="configuration">The configuration providing the memory allocator.</param> /// <param name="configuration">The configuration providing the memory allocator.</param>
/// <param name="leftSize">The number of values in the left-neighbor storage.</param> /// <param name="leftSize">The number of values in the left-neighbor storage.</param>
/// <param name="topSize">The number of values in the top-neighbor storage.</param> /// <param name="topSize">The number of values in the top-neighbor storage.</param>
/// <param name="topLeftSize">The number of values in the diagonal-neighbor storage.</param> public Av1NeighborArrayUnit(Configuration configuration, int leftSize, int topSize)
public Av1NeighborArrayUnit(Configuration configuration, int leftSize, int topSize, int topLeftSize)
{ {
this.leftLength = leftSize; this.leftLength = leftSize;
this.topLength = topSize; this.topLength = topSize;
this.topLeftLength = topLeftSize; int totalLength = checked(leftSize + topSize);
int totalLength = checked(leftSize + topSize + topLeftSize);
// All three neighbor regions share the picture lifetime, so one clean allocator-backed // Both context edges share the picture lifetime, so one clean allocator-backed buffer
// buffer avoids three managed arrays and preserves their zero-initialized starting state. // preserves their zero-initialized starting state without separate owner lifetimes.
this.memory = configuration.MemoryAllocator.Allocate<T>(totalLength, AllocationOptions.Clean); this.memory = configuration.MemoryAllocator.Allocate<T>(totalLength, AllocationOptions.Clean);
} }
@ -73,11 +60,6 @@ internal sealed class Av1NeighborArrayUnit<T> : IDisposable
/// Update the top-neighbor storage. /// Update the top-neighbor storage.
/// </summary> /// </summary>
Top = 2, Top = 2,
/// <summary>
/// Update the top-left diagonal storage.
/// </summary>
TopLeft = 4,
} }
/// <summary> /// <summary>
@ -104,28 +86,11 @@ internal sealed class Av1NeighborArrayUnit<T> : IDisposable
} }
} }
/// <summary>
/// Gets the top-left diagonal storage.
/// </summary>
public Span<T> TopLeft
{
get
{
ObjectDisposedException.ThrowIf(this.memory is null, this);
return this.memory.Memory.Span.Slice(this.leftLength + this.topLength, this.topLeftLength);
}
}
/// <summary> /// <summary>
/// Gets or sets the base-2 logarithm of the top and left context granularity in samples. /// Gets or sets the base-2 logarithm of the top and left context granularity in samples.
/// </summary> /// </summary>
public required int GranularityNormalLog2 { get; set; } public required int GranularityNormalLog2 { get; set; }
/// <summary>
/// Gets or sets the base-2 logarithm of the diagonal context granularity in samples.
/// </summary>
public required int GranularityTopLeftLog2 { get; set; }
/// <summary> /// <summary>
/// Gets the left-neighbor unit index for a sample position. /// Gets the left-neighbor unit index for a sample position.
/// </summary> /// </summary>
@ -140,14 +105,6 @@ internal sealed class Av1NeighborArrayUnit<T> : IDisposable
/// <returns>The top-neighbor unit index.</returns> /// <returns>The top-neighbor unit index.</returns>
public int GetTopIndex(Point loc) => loc.X >> this.GranularityNormalLog2; public int GetTopIndex(Point loc) => loc.X >> this.GranularityNormalLog2;
/// <summary>
/// Gets the diagonal-neighbor unit index for a sample position.
/// </summary>
/// <param name="loc">The sample position.</param>
/// <returns>The top-left neighbor index derived from the position's diagonal.</returns>
public int GetTopLeftIndex(Point loc)
=> this.leftLength + (loc.X >> this.GranularityTopLeftLog2) - (loc.Y >> this.GranularityTopLeftLog2);
/// <summary> /// <summary>
/// Returns the neighbor storage to the configured memory allocator. /// Returns the neighbor storage to the configured memory allocator.
/// </summary> /// </summary>
@ -211,30 +168,5 @@ internal sealed class Av1NeighborArrayUnit<T> : IDisposable
int count = blockSize.Height >> this.GranularityNormalLog2; int count = blockSize.Height >> this.GranularityNormalLog2;
this.Left.Slice(offset, count).Fill(value); 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);
}
} }
} }

98
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs

@ -25,11 +25,9 @@ public class Av1CoefficientsEntropyTests
using Av1NeighborArrayUnit<byte> neighbors = new( using Av1NeighborArrayUnit<byte> neighbors = new(
Configuration.Default, Configuration.Default,
leftSize: 8, leftSize: 8,
topSize: 8, topSize: 8)
topLeftSize: 16)
{ {
GranularityNormalLog2 = 2, GranularityNormalLog2 = 2
GranularityTopLeftLog2 = 2
}; };
neighbors.UnitModeWrite( neighbors.UnitModeWrite(
@ -42,6 +40,32 @@ public class Av1CoefficientsEntropyTests
Assert.Equal(new byte[] { 0, 37, 37, 0, 0, 0, 0, 0 }, neighbors.Left.ToArray()); 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<byte> 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] [Theory]
[InlineData((int)Av1ComponentType.Luminance, 5)] [InlineData((int)Av1ComponentType.Luminance, 5)]
[InlineData((int)Av1ComponentType.Chroma, 12)] [InlineData((int)Av1ComponentType.Chroma, 12)]
@ -52,11 +76,9 @@ public class Av1CoefficientsEntropyTests
using Av1NeighborArrayUnit<byte> neighbors = new( using Av1NeighborArrayUnit<byte> neighbors = new(
Configuration.Default, Configuration.Default,
leftSize: 8, leftSize: 8,
topSize: 8, topSize: 8)
topLeftSize: 16)
{ {
GranularityNormalLog2 = 2, GranularityNormalLog2 = 2
GranularityTopLeftLog2 = 2
}; };
// The high bits carry positive, positive, and negative DC signs. The low bits select // The high bits carry positive, positive, and negative DC signs. The low bits select
@ -358,11 +380,9 @@ public class Av1CoefficientsEntropyTests
using Av1NeighborArrayUnit<byte> luma = new( using Av1NeighborArrayUnit<byte> luma = new(
Configuration.Default, Configuration.Default,
leftSize: 128, leftSize: 128,
topSize: 128, topSize: 128)
topLeftSize: 256)
{ {
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2, GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
GranularityTopLeftLog2 = Av1Constants.ModeInfoSizeLog2
}; };
using Av1EncoderCoefficientBuffer coefficients = new( using Av1EncoderCoefficientBuffer coefficients = new(
@ -447,11 +467,9 @@ public class Av1CoefficientsEntropyTests
using Av1NeighborArrayUnit<byte> transforms = new( using Av1NeighborArrayUnit<byte> transforms = new(
Configuration.Default, Configuration.Default,
leftSize: 64, leftSize: 64,
topSize: 64, topSize: 64)
topLeftSize: 128)
{ {
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2, GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
GranularityTopLeftLog2 = Av1Constants.ModeInfoSizeLog2
}; };
int topIndex = transforms.GetTopIndex(blockOrigin); int topIndex = transforms.GetTopIndex(blockOrigin);
@ -510,11 +528,9 @@ public class Av1CoefficientsEntropyTests
using Av1NeighborArrayUnit<Av1PartitionContext> neighbors = new( using Av1NeighborArrayUnit<Av1PartitionContext> neighbors = new(
Configuration.Default, Configuration.Default,
leftSize: 16, leftSize: 16,
topSize: 16, topSize: 16)
topLeftSize: 32)
{ {
GranularityNormalLog2 = 2, GranularityNormalLog2 = 2
GranularityTopLeftLog2 = 2
}; };
Av1PartitionType partition = (Av1PartitionType)partitionValue; Av1PartitionType partition = (Av1PartitionType)partitionValue;
@ -543,11 +559,9 @@ public class Av1CoefficientsEntropyTests
using Av1NeighborArrayUnit<Av1PartitionContext> neighbors = new( using Av1NeighborArrayUnit<Av1PartitionContext> neighbors = new(
Configuration.Default, Configuration.Default,
leftSize: 4, leftSize: 4,
topSize: 4, topSize: 4)
topLeftSize: 8)
{ {
GranularityNormalLog2 = 2, GranularityNormalLog2 = 2
GranularityTopLeftLog2 = 2
}; };
Av1TileWriter.UpdatePartitionContexts( Av1TileWriter.UpdatePartitionContexts(
@ -633,51 +647,41 @@ public class Av1CoefficientsEntropyTests
using Av1NeighborArrayUnit<Av1PartitionContext> partitions = new( using Av1NeighborArrayUnit<Av1PartitionContext> partitions = new(
Configuration.Default, Configuration.Default,
leftSize: 16, leftSize: 16,
topSize: 32, topSize: 32)
topLeftSize: 48)
{ {
GranularityNormalLog2 = 2, GranularityNormalLog2 = 2
GranularityTopLeftLog2 = 2
}; };
using Av1NeighborArrayUnit<byte> luma = new( using Av1NeighborArrayUnit<byte> luma = new(
Configuration.Default, Configuration.Default,
leftSize: 16, leftSize: 16,
topSize: 32, topSize: 32)
topLeftSize: 48)
{ {
GranularityNormalLog2 = 2, GranularityNormalLog2 = 2
GranularityTopLeftLog2 = 2
}; };
using Av1NeighborArrayUnit<byte> red = new( using Av1NeighborArrayUnit<byte> red = new(
Configuration.Default, Configuration.Default,
leftSize: 16, leftSize: 16,
topSize: 32, topSize: 32)
topLeftSize: 48)
{ {
GranularityNormalLog2 = 2, GranularityNormalLog2 = 2
GranularityTopLeftLog2 = 2
}; };
using Av1NeighborArrayUnit<byte> blue = new( using Av1NeighborArrayUnit<byte> blue = new(
Configuration.Default, Configuration.Default,
leftSize: 16, leftSize: 16,
topSize: 32, topSize: 32)
topLeftSize: 48)
{ {
GranularityNormalLog2 = 2, GranularityNormalLog2 = 2
GranularityTopLeftLog2 = 2
}; };
using Av1NeighborArrayUnit<byte> transforms = new( using Av1NeighborArrayUnit<byte> transforms = new(
Configuration.Default, Configuration.Default,
leftSize: 16, leftSize: 16,
topSize: 32, topSize: 32)
topLeftSize: 48)
{ {
GranularityNormalLog2 = 2, GranularityNormalLog2 = 2
GranularityTopLeftLog2 = 2
}; };
picture.PartitionContexts = [partitions]; picture.PartitionContexts = [partitions];
@ -785,11 +789,9 @@ public class Av1CoefficientsEntropyTests
using Av1NeighborArrayUnit<Av1PartitionContext> neighbors = new( using Av1NeighborArrayUnit<Av1PartitionContext> neighbors = new(
Configuration.Default, Configuration.Default,
leftSize: 1, leftSize: 1,
topSize: 1, topSize: 1)
topLeftSize: 2)
{ {
GranularityNormalLog2 = 2, GranularityNormalLog2 = 2
GranularityTopLeftLog2 = 2
}; };
Av1PartitionType nonSplitPartition = bottomEdge ? Av1PartitionType.Horizontal : Av1PartitionType.Vertical; Av1PartitionType nonSplitPartition = bottomEdge ? Av1PartitionType.Horizontal : Av1PartitionType.Vertical;

Loading…
Cancel
Save