Browse Source

Keep AV1 loop-filter delta entropy independent per channel

pull/2633/head
James Jackson-South 4 weeks ago
parent
commit
77f5358286
  1. 45
      HEIF_IMPLEMENTATION_PLAN.md
  2. 11
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs
  3. 9
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContext.cs
  4. 10
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs
  5. 2
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs
  6. 94
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs

45
HEIF_IMPLEMENTATION_PLAN.md

@ -298,6 +298,51 @@ Intra-edge integration after checkpoint `2424ff9f9`, verified on 2026-09-05:
### Required completion gates
Frame-context investigation after checkpoint `1639550a0`:
- `Av1SymbolEncoder.cs:355-369` resets tile CDFs to quantizer-band defaults. `Av1TileEncoder.cs:281-337`
does that between tiles and retains only their output offsets and lengths. Reference
`av1/encoder/encodeframe.c:1456` initializes each tile from the unchanged frame context, then
`av1/encoder/bitstream.c:4074-4079` signals the largest encoded tile as the update source.
`av1/encoder/encoder.c:4489-4495` copies that tile's CDFs, resets observation counters, and stores them
with the reconstructed reference frame. Encoder publication is missing in the managed path.
- Primary-reference selection is coupled to the reference-role and frame-layer controller
(`av1/encoder/encode_strategy.c:168-230`), not simply the last encoded frame. The managed encoder retains
only the preceding reconstruction and always writes global-motion models relative to identity
(`ObuWriter.cs:1206-1226`). Enabling primary-reference reuse requires reconciling those inherited models,
loop-filter deltas, segmentation, refresh slots, and entropy state together. No encoder frame-context flag
or reference policy was changed during this investigation.
- Existing `Av1FrameEntropyContexts.BeginFrame`, `Av1FrameEntropyContext.CopyFrom`, and `SnapshotTo`
already implement decoder base/working/published state and counter reset. They are existing reusable
contracts to consider when implementing encoder publication; a new ownership framework is not justified.
Independent loop-filter delta entropy correction, verified on 2026-09-05:
- The context-family comparison found a numerical decoder defect. Before correction,
`Av1SymbolDecoder.cs:921-924` always read `DeltaLoopFilterAbsolute`, including the per-channel loop at
`Av1TileReader.cs:3065-3067`. Native `av1/decoder/decodemv.c:749-765` instead selects independent
`delta_lf_multi_cdf[lf_id]` distributions for multi-delta syntax and the shared CDF otherwise.
Defaults are in `av1/common/entropymode.c:844-851`; counter reset is in `av1/common/entropy.c:166-169`.
- A new regression encodes independent channel histories using explicit reference defaults and signed magnitude
syntax from `av1/encoder/bitstream.c:323-353`. The original decoder failed on the third symbol, returning
**-1 instead of -2** (`delta-lf-before.trx`, stopped on the first failure).
- Four independent distributions now participate in prototype construction, deep copying, default restoration,
and frame snapshot counter reset. The existing tile loop passes its parsed multi-delta flag and channel index
to the symbol reader. No per-symbol owner, allocation, new guard, or rejection policy was introduced.
- Tests cover four color channels, two monochrome channels, shared-delta syntax, disabled CDF adaptation,
signed escape magnitudes, independent copies, counter reset, and unchanged shared-delta defaults.
Final Release net11.0 build: zero errors and 1,009 existing warnings; Roslynk: zero compiler errors.
Serialized Visual Studio VSTest passed **2,095/2,095** in `delta-lf-final.trx` (1.8771 minutes), covering
entropy tests, frame-context lifecycle tests, and the AV1 reconstruction conformance suite.
- Current libaom normal encoding sets `DEFAULT_DELTA_LF_MULTI` to zero (`av1/common/enums.h:73`,
`av1/encoder/encodeframe.c:2357`). Existing native output must not be assumed to exercise multi-delta syntax.
A complete independently authored multi-delta bitstream remains a verification gap. This correction does not
establish encoder parity or complete decoder correctness. No benchmark was run.
- Following the decoded values into deblocking found a further clipping-order discrepancy:
`Av1LoopFilterDecoder.cs:373-390` clips the reference adjustment before adding the mode adjustment;
native `av1/common/av1_loopfilter.c:95-101,195-201` clips their combined result once. A production-frame
regression and correction are still pending for that separately identified numerical defect.
Motion-controller investigation continued after correction checkpoint `578ec34d9`:
- Managed `Av1IntraSuperblockEncoder.ReferenceModeDecision.cs:1278-1493` uses the same normalized squared-error

11
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs

@ -495,6 +495,17 @@ internal static class Av1DefaultDistributions
/// </summary>
public static Av1Distribution DeltaLoopFilterAbsolute => new(28160, 32120, 32677);
/// <summary>
/// Gets independent loop-filter delta distributions for vertical luma, horizontal luma, U, and V.
/// </summary>
public static Av1Distribution[] DeltaLoopFilterMultiAbsolute =>
[
new(28160, 32120, 32677),
new(28160, 32120, 32677),
new(28160, 32120, 32677),
new(28160, 32120, 32677)
];
/// <summary>
/// Gets the distribution for an absolute quantizer delta magnitude.
/// </summary>

9
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContext.cs

@ -108,6 +108,7 @@ internal sealed class Av1FrameEntropyContext
this.Skip = Av1DefaultDistributions.Skip;
this.SkipMode = Av1DefaultDistributions.SkipMode;
this.DeltaLoopFilterAbsolute = Av1DefaultDistributions.DeltaLoopFilterAbsolute;
this.DeltaLoopFilterMultiAbsolute = Av1DefaultDistributions.DeltaLoopFilterMultiAbsolute;
this.DeltaQuantizerAbsolute = Av1DefaultDistributions.DeltaQuantizerAbsolute;
this.SegmentId = Av1DefaultDistributions.SegmentId;
this.SegmentIdPredicted = Av1DefaultDistributions.SegmentIdPredicted;
@ -183,6 +184,7 @@ internal sealed class Av1FrameEntropyContext
this.Skip = Av1Distribution.CreateCopy(source.Skip);
this.SkipMode = Av1Distribution.CreateCopy(source.SkipMode);
this.DeltaLoopFilterAbsolute = source.DeltaLoopFilterAbsolute.CreateCopy();
this.DeltaLoopFilterMultiAbsolute = Av1Distribution.CreateCopy(source.DeltaLoopFilterMultiAbsolute);
this.DeltaQuantizerAbsolute = source.DeltaQuantizerAbsolute.CreateCopy();
this.SegmentId = Av1Distribution.CreateCopy(source.SegmentId);
this.SegmentIdPredicted = Av1Distribution.CreateCopy(source.SegmentIdPredicted);
@ -409,6 +411,11 @@ internal sealed class Av1FrameEntropyContext
/// </summary>
public Av1Distribution DeltaLoopFilterAbsolute { get; }
/// <summary>
/// Gets independent delta distributions for vertical luma, horizontal luma, U, and V loop filters.
/// </summary>
public Av1Distribution[] DeltaLoopFilterMultiAbsolute { get; }
/// <summary>
/// Gets the absolute quantizer delta distribution.
/// </summary>
@ -578,6 +585,7 @@ internal sealed class Av1FrameEntropyContext
CopyState(source.Skip, this.Skip);
CopyState(source.SkipMode, this.SkipMode);
this.DeltaLoopFilterAbsolute.CopyFrom(source.DeltaLoopFilterAbsolute);
CopyState(source.DeltaLoopFilterMultiAbsolute, this.DeltaLoopFilterMultiAbsolute);
this.DeltaQuantizerAbsolute.CopyFrom(source.DeltaQuantizerAbsolute);
CopyState(source.SegmentId, this.SegmentId);
CopyState(source.SegmentIdPredicted, this.SegmentIdPredicted);
@ -659,6 +667,7 @@ internal sealed class Av1FrameEntropyContext
ResetUpdateCounts(this.Skip);
ResetUpdateCounts(this.SkipMode);
this.DeltaLoopFilterAbsolute.ResetUpdateCount();
ResetUpdateCounts(this.DeltaLoopFilterMultiAbsolute);
this.DeltaQuantizerAbsolute.ResetUpdateCount();
ResetUpdateCounts(this.SegmentId);
ResetUpdateCounts(this.SegmentIdPredicted);

10
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs

@ -917,11 +917,17 @@ internal ref struct Av1SymbolDecoder
/// <summary>
/// Reads a signed loop-filter delta value.
/// </summary>
/// <param name="isMulti">Whether the frame signals independent deltas for each loop-filter channel.</param>
/// <param name="channel">The loop-filter channel, used only when <paramref name="isMulti"/> is true.</param>
/// <returns>The decoded loop-filter delta.</returns>
public int ReadDeltaLoopFilter()
public int ReadDeltaLoopFilter(bool isMulti, int channel)
{
ref Av1SymbolReader r = ref this.reader;
int deltaLoopFilterAbsolute = r.ReadSymbol(this.context.DeltaLoopFilterAbsolute);
// Multi-delta syntax adapts one CDF per filter channel. Sharing the scalar-delta CDF would let
// an earlier channel change the range intervals used to decode the next channel in the same block.
Av1Distribution distribution = isMulti ? this.context.DeltaLoopFilterMultiAbsolute[channel] : this.context.DeltaLoopFilterAbsolute;
int deltaLoopFilterAbsolute = r.ReadSymbol(distribution);
if (deltaLoopFilterAbsolute == Av1Constants.DeltaLoopFilterSmall)
{
int deltaLoopFilterRemainingBits = r.ReadLiteral(3) + 1;

2
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs

@ -3064,7 +3064,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
for (int i = 0; i < frameLoopFilterCount; i++)
{
int reducedDeltaLoopFilterLevel = reader.ReadDeltaLoopFilter();
int reducedDeltaLoopFilterLevel = reader.ReadDeltaLoopFilter(this.FrameHeader.DeltaLoopFilterParameters.IsMulti, i);
int deltaLoopFilterResolution = this.FrameHeader.DeltaLoopFilterParameters.Resolution;
this.currentDeltaLoopFilter[i] = Av1Math.Clip3(
-Av1Constants.MaxLoopFilter,

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

@ -24,6 +24,100 @@ public class Av1EntropyTests
// Short syntax round trips encode only their small in-method symbol vectors.
private const int ShortSyntaxBufferLength = 64;
[Theory]
[InlineData(4, true)]
[InlineData(2, true)]
[InlineData(1, true)]
[InlineData(4, false)]
public void DeltaLoopFilterChannelsAdaptIndependently(int channelCount, bool updateCdf)
{
ReadOnlySpan<int> deltas = [0, 1, -2, 3, -1, 0, 17, -3, 2, -9, 0, 1];
Av1Distribution[] channels = new Av1Distribution[channelCount];
for (int channel = 0; channel < channelCount; channel++)
{
// libaom entropymode.c gives each multi-delta channel this independent initial CDF.
channels[channel] = new(28160, 32120, 32677);
}
using Av1SymbolWriter writer = new(Configuration.Default, 512, updateCdf);
for (int index = 0; index < 96; index++)
{
int delta = deltas[index % deltas.Length];
int magnitude = Math.Abs(delta);
writer.WriteSymbol(Math.Min(magnitude, 3), channels[index % channelCount]);
if (magnitude >= 3)
{
// The escape magnitude is 2^bits + 1 plus the transmitted remainder, followed by its sign.
int bits = BitOperations.Log2((uint)(magnitude - 1));
writer.WriteLiteral((uint)(bits - 1), 3);
writer.WriteLiteral((uint)(magnitude - (1 << bits) - 1), bits);
}
if (magnitude != 0)
{
writer.WriteLiteral(delta < 0 ? 1U : 0U, 1);
}
}
using IMemoryOwner<byte> payload = writer.Exit();
Av1SymbolDecoder reader = new(Configuration.Default, payload.Memory.Span, BaseQIndex, updateCdf);
for (int index = 0; index < 96; index++)
{
Assert.Equal(deltas[index % deltas.Length], reader.ReadDeltaLoopFilter(channelCount > 1, index % channelCount));
}
}
[Fact]
public void FrameEntropyLifecyclePreservesIndependentDeltaLoopFilterChannels()
{
Av1FrameEntropyContext source = new(BaseQIndex);
Av1FrameEntropyContext copy = new(BaseQIndex);
Av1FrameEntropyContext snapshot = new(BaseQIndex);
Av1Distribution defaults = new(28160, 32120, 32677);
for (int channel = 0; channel < 4; channel++)
{
for (int observation = 0; observation < 20; observation++)
{
source.DeltaLoopFilterMultiAbsolute[channel].Update((channel % 3) + 1);
}
}
copy.CopyFrom(source);
source.SnapshotTo(snapshot);
for (int channel = 0; channel < 4; channel++)
{
Av1Distribution original = source.DeltaLoopFilterMultiAbsolute[channel];
Av1Distribution copied = copy.DeltaLoopFilterMultiAbsolute[channel];
Av1Distribution published = snapshot.DeltaLoopFilterMultiAbsolute[channel];
Assert.NotSame(original, copied);
Assert.NotSame(original, published);
for (int symbol = 0; symbol < 4; symbol++)
{
Assert.Equal(original[symbol], copied[symbol]);
Assert.Equal(original[symbol], published[symbol]);
Assert.Equal(defaults[symbol], source.DeltaLoopFilterAbsolute[symbol]);
}
// A published CDF preserves probabilities but restarts its observation history. The next
// identical symbol must therefore move its threshold further than in the twenty-count source.
original.Update(0);
published.Update(0);
Assert.NotEqual(original[0], published[0]);
Assert.NotEqual(original[0], copied[0]);
}
copy.ResetToDefaults(255);
Av1FrameEntropyContext fresh = new(BaseQIndex);
for (int channel = 0; channel < 4; channel++)
{
for (int symbol = 0; symbol < 4; symbol++)
{
Assert.Equal(defaults[symbol], copy.DeltaLoopFilterMultiAbsolute[channel][symbol]);
Assert.Equal(defaults[symbol], fresh.DeltaLoopFilterMultiAbsolute[channel][symbol]);
}
}
}
[Fact]
public void ProbabilityCostTableMatchesDefinition()
{

Loading…
Cancel
Save