diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md
index 553c96f52e..6aee22365f 100644
--- a/HEIF_IMPLEMENTATION_PLAN.md
+++ b/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
diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs
index 277d77449d..f70c722fcd 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs
@@ -495,6 +495,17 @@ internal static class Av1DefaultDistributions
///
public static Av1Distribution DeltaLoopFilterAbsolute => new(28160, 32120, 32677);
+ ///
+ /// Gets independent loop-filter delta distributions for vertical luma, horizontal luma, U, and V.
+ ///
+ public static Av1Distribution[] DeltaLoopFilterMultiAbsolute =>
+ [
+ new(28160, 32120, 32677),
+ new(28160, 32120, 32677),
+ new(28160, 32120, 32677),
+ new(28160, 32120, 32677)
+ ];
+
///
/// Gets the distribution for an absolute quantizer delta magnitude.
///
diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContext.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContext.cs
index b7e5cb17d8..c1b0d82e6c 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContext.cs
+++ b/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
///
public Av1Distribution DeltaLoopFilterAbsolute { get; }
+ ///
+ /// Gets independent delta distributions for vertical luma, horizontal luma, U, and V loop filters.
+ ///
+ public Av1Distribution[] DeltaLoopFilterMultiAbsolute { get; }
+
///
/// Gets the absolute quantizer delta distribution.
///
@@ -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);
diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs
index 5134a1467e..ef5423167d 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs
@@ -917,11 +917,17 @@ internal ref struct Av1SymbolDecoder
///
/// Reads a signed loop-filter delta value.
///
+ /// Whether the frame signals independent deltas for each loop-filter channel.
+ /// The loop-filter channel, used only when is true.
/// The decoded loop-filter delta.
- 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;
diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs
index 124c66c527..8fdb67afa5 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs
+++ b/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,
diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs
index c27fa959c3..9aa31ce5cb 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs
+++ b/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 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 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()
{