Browse Source

Add exact AV1 probability costs

pull/2633/head
James Jackson-South 1 month ago
parent
commit
1be256faf3
  1. 1
      HEIF_IMPLEMENTATION_PLAN.md
  2. 72
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1ProbabilityCost.cs
  3. 61
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs

1
HEIF_IMPLEMENTATION_PLAN.md

@ -828,6 +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.
- [~] Encoder rate accounting now converts the entropy writer's live inverse cumulative distributions into current-libaom fixed-point symbol costs without allocating or duplicating probability state. The complete normalized probability table, Q15 boundaries, multi-symbol interval arithmetic, literal-bit precision, and zero-allocation hot path pass 15 of 15 focused direct net11 Release VSTest cases; all 1,897 `Av1EntropyTests` cases pass. Mode, transform, skip, and coefficient candidate accounting still need to compose these costs into real rate-distortion selection.
- [~] 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 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 picture owner now packs segmentation plus every tile's partition, luma, chroma, and transform edges into one clean byte allocation with typed non-owning views; together with the separately typed packed mode-information owner, the complete picture state uses two allocator rents rather than seven. Exact aligned lengths, clean initialization, and balanced exactly-once returns are covered in Release. Multi-tile payload ownership 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 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 picture owner now packs segmentation plus every tile's partition, luma, chroma, and transform edges into one clean byte allocation with typed non-owning views; together with the separately typed packed mode-information owner, the complete picture state uses two allocator rents rather than seven. Exact aligned lengths, clean initialization, and balanced exactly-once returns are covered in Release. Multi-tile payload ownership 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.

72
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1ProbabilityCost.cs

@ -0,0 +1,72 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Converts AV1 symbol probabilities into fixed-point encoder rate costs.
/// </summary>
internal static class Av1ProbabilityCost
{
/// <summary>
/// The number of fractional bits in an encoder rate cost.
/// </summary>
public const int CostShift = 9;
/// <summary>
/// Gets the probability costs for normalized eight-bit probabilities from 128 through 255.
/// </summary>
private static ReadOnlySpan<ushort> ProbabilityCosts =>
[
512, 506, 501, 495, 489, 484, 478, 473, 467, 462, 456, 451, 446, 441, 435,
430, 425, 420, 415, 410, 405, 400, 395, 390, 385, 380, 375, 371, 366, 361,
356, 352, 347, 343, 338, 333, 329, 324, 320, 316, 311, 307, 302, 298, 294,
289, 285, 281, 277, 273, 268, 264, 260, 256, 252, 248, 244, 240, 236, 232,
228, 224, 220, 216, 212, 209, 205, 201, 197, 194, 190, 186, 182, 179, 175,
171, 168, 164, 161, 157, 153, 150, 146, 143, 139, 136, 132, 129, 125, 122,
119, 115, 112, 109, 105, 102, 99, 95, 92, 89, 86, 82, 79, 76, 73, 70,
66, 63, 60, 57, 54, 51, 48, 45, 42, 38, 35, 32, 29, 26, 23, 20, 18, 15,
12, 9, 6, 3
];
/// <summary>
/// Gets the fixed-point cost of writing the requested number of equiprobable bits.
/// </summary>
/// <param name="bitCount">The number of bits.</param>
/// <returns>The rate cost in 1/512-bit units.</returns>
public static int GetLiteralCost(int bitCount) => bitCount << CostShift;
/// <summary>
/// Gets the fixed-point cost of coding one symbol from an inverse cumulative distribution.
/// </summary>
/// <param name="distribution">The distribution used by the entropy writer.</param>
/// <param name="symbol">The zero-based symbol.</param>
/// <returns>The rate cost in 1/512-bit units.</returns>
public static int GetSymbolCost(Av1Distribution distribution, int symbol)
{
int inverseLower = symbol == 0 ? Av1Distribution.ProbabilityTop : (int)distribution[symbol - 1];
int inverseUpper = (int)distribution[symbol];
return GetProbabilityCost(inverseLower - inverseUpper);
}
/// <summary>
/// Gets the fixed-point cost of a Q15 probability.
/// </summary>
/// <param name="probability">The Q15 probability numerator.</param>
/// <returns>The rate cost in 1/512-bit units.</returns>
public static int GetProbabilityCost(int probability)
{
probability = Math.Clamp(probability, 1, Av1Distribution.ProbabilityTop - 1);
int shift = 14 - BitOperations.Log2((uint)probability);
// Normalization puts every probability in the upper half of an eight-bit range. The lookup therefore
// covers one binary order of magnitude, while the shift contributes the exact number of whole bits.
int normalizedProbability = (((probability << shift) * 256) + (Av1Distribution.ProbabilityTop >> 1))
/ Av1Distribution.ProbabilityTop;
normalizedProbability = Math.Min(normalizedProbability, 255);
return ProbabilityCosts[normalizedProbability - 128] + (shift << CostShift);
}
}

61
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs

@ -18,6 +18,67 @@ public class Av1EntropyTests
{ {
private const int BaseQIndex = 23; private const int BaseQIndex = 23;
[Fact]
public void ProbabilityCostTableMatchesDefinition()
{
for (int normalizedProbability = 128; normalizedProbability < 256; normalizedProbability++)
{
int expected = (int)Math.Round(
-Math.Log2(normalizedProbability / 256D) * (1 << Av1ProbabilityCost.CostShift),
MidpointRounding.AwayFromZero);
int actual = Av1ProbabilityCost.GetProbabilityCost(normalizedProbability << 7);
Assert.Equal(expected, actual);
}
}
[Theory]
[InlineData(0, 7680)]
[InlineData(1, 7680)]
[InlineData(4, 6656)]
[InlineData(8192, 1024)]
[InlineData(10000, 878)]
[InlineData(16384, 512)]
[InlineData(24576, 212)]
[InlineData(32767, 3)]
[InlineData(32768, 3)]
public void ProbabilityCostMatchesCurrentLibaom(int probability, int expected)
=> Assert.Equal(expected, Av1ProbabilityCost.GetProbabilityCost(probability));
[Fact]
public void SymbolCostUsesDistributionIntervals()
{
Av1Distribution distribution = new(8192, 24576);
Assert.Equal(1024, Av1ProbabilityCost.GetSymbolCost(distribution, 0));
Assert.Equal(512, Av1ProbabilityCost.GetSymbolCost(distribution, 1));
Assert.Equal(1024, Av1ProbabilityCost.GetSymbolCost(distribution, 2));
}
[Theory]
[InlineData(0, 0)]
[InlineData(1, 512)]
[InlineData(7, 3584)]
public void LiteralCostUsesProbabilityCostPrecision(int bitCount, int expected)
=> Assert.Equal(expected, Av1ProbabilityCost.GetLiteralCost(bitCount));
[Fact]
public void ProbabilityCostDoesNotAllocate()
{
Av1Distribution distribution = new(8192, 24576);
_ = Av1ProbabilityCost.GetSymbolCost(distribution, 0);
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 1000; i++)
{
_ = Av1ProbabilityCost.GetSymbolCost(distribution, i % distribution.NumberOfSymbols);
}
long after = GC.GetAllocatedBytesForCurrentThread();
Assert.Equal(before, after);
}
[Fact] [Fact]
public void SymbolWriterMatchesCurrentLibaomCarryRegression() public void SymbolWriterMatchesCurrentLibaomCarryRegression()
{ {

Loading…
Cancel
Save