diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md
index dea9a97980..86f3d01365 100644
--- a/HEIF_IMPLEMENTATION_PLAN.md
+++ b/HEIF_IMPLEMENTATION_PLAN.md
@@ -246,6 +246,33 @@ Further controller-state findings after `b2aee3036` on 2026-09-05:
Updating CDFs after each selected block is not the same as refreshing every RD cost after that block.
This missing cost-state lifecycle is an architectural deviation. Its contribution to time or quality has
not been measured; no isolated cache or invented effort-dependent refresh policy has been introduced.
+- Reference `av1/encoder/rd.c:687-853` initializes the complete cost state and combines control and speed-feature
+ update frequencies. `av1/av1_cx_iface.c:391-394,1342-1348` defaults controls to superblock updates and disables
+ MV-cost updates for all-intra configuration. These are configured policies, not a public-effort mapping.
+ `av1/encoder/encoder_alloc.h:58-88` omits the main MV-cost allocation for all-intra; displacement costs are
+ initialized only when needed (`rd.c:843-851`). Worker costs are separately owned when their update policy
+ requires independent state (`av1/encoder/ethread.c:1610-1641`). Managed sizing and lifetime must follow those
+ usage boundaries rather than adding every table to every frame or candidate.
+
+CDF symbol-cost floor correction, verified after `37541f7f3` on 2026-09-05:
+
+- `Av1ProbabilityCost.cs:47-52` previously passed each CDF interval directly to raw probability conversion.
+ Native `av1/encoder/cost.c:30-48` first floors symbol mass at `EC_MIN_PROB` (4, `aom_dsp/entcode.h:21`).
+ Raw conversion has a different domain and still permits 1 (`av1/encoder/cost.h:32-42`). Conflating the two
+ overcharged sufficiently rare symbols by up to 1,024 rate units, or two bits.
+- CDF symbol conversion now applies the existing range-coder minimum before raw conversion. Gathered edge
+ partition costs (`Av1SymbolEncoder.cs:969-980,1003-1014`) use that same symbol boundary, matching native
+ `av1/encoder/partition_search.c:3419-3449`. Existing raw-probability behavior and tests remain unchanged.
+- The new zero-mass middle-interval regression failed before correction: expected 6,656, actual 7,680
+ (`symbol-floor-red.trx`). Cases at masses 0 through 4 and 8 exercise both ordinary CDF and gathered-symbol
+ conversion. They do not replace the missing RD-cost refresh lifecycle or coefficient optimizer.
+- Final Release .NET 11 build: zero errors and 1,009 existing warnings; Roslynk: zero compiler errors.
+ Serialized Visual Studio VSTest passes 2,283/2,283 in 27.0448 seconds (`symbol-floor-final.trx`), covering
+ entropy, intra-superblock, encoder-frame, and public HEIF encoder tests.
+- Optimized libaom decoding was repeated after the final edit for 23 palette, eight partition, and twelve color
+ sequence streams: 38,973 samples, maximum error 0, and zero samples exceeding one. Per-plane reports remain
+ in the temporary takeover directory. These are bounded same-bitstream checks, not separate-encoder parity,
+ a quality improvement claim, or performance acceptance. No benchmark was run.
Palette coded-boundary correction, verified after `b2aee3036` on 2026-09-05:
diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1ProbabilityCost.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1ProbabilityCost.cs
index 6e26eda8de..d9ff3958b1 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1ProbabilityCost.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1ProbabilityCost.cs
@@ -48,7 +48,19 @@ internal static class Av1ProbabilityCost
{
int inverseLower = symbol == 0 ? Av1Distribution.ProbabilityTop : (int)distribution[symbol - 1];
int inverseUpper = (int)distribution[symbol];
- return GetProbabilityCost(inverseLower - inverseUpper);
+ return GetSymbolCost(inverseLower - inverseUpper);
+ }
+
+ ///
+ /// Gets the fixed-point cost of an entropy-coded symbol with a Q15 probability.
+ ///
+ /// The Q15 probability numerator.
+ /// The rate cost in 1/512-bit units.
+ public static int GetSymbolCost(int probability)
+ {
+ // The range coder reserves a minimum interval even when CDF adaptation collapses a symbol's mass.
+ // RD costs use that same floor; the raw probability conversion below retains its separate numerical domain.
+ return GetProbabilityCost(Math.Max(probability, Av1Distribution.ProbabilityMinimum));
}
///
diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
index feeb409917..f706004430 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
@@ -973,7 +973,7 @@ internal sealed class Av1SymbolEncoder : IDisposable
blockSize,
context);
- return Av1ProbabilityCost.GetProbabilityCost(
+ return Av1ProbabilityCost.GetSymbolCost(
partitionType == Av1PartitionType.Split
? frequency
: Av1Distribution.ProbabilityTop - frequency);
@@ -1007,7 +1007,7 @@ internal sealed class Av1SymbolEncoder : IDisposable
blockSize,
context);
- return Av1ProbabilityCost.GetProbabilityCost(
+ return Av1ProbabilityCost.GetSymbolCost(
partitionType == Av1PartitionType.Split
? frequency
: Av1Distribution.ProbabilityTop - frequency);
diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs
index 9aa31ce5cb..ce9a817545 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs
@@ -156,6 +156,22 @@ public class Av1EntropyTests
Assert.Equal(1024, Av1ProbabilityCost.GetSymbolCost(distribution, 2));
}
+ [Theory]
+ [InlineData(0, 6656)]
+ [InlineData(1, 6656)]
+ [InlineData(2, 6656)]
+ [InlineData(3, 6656)]
+ [InlineData(4, 6656)]
+ [InlineData(8, 6144)]
+ public void SymbolCostUsesRangeCoderMinimumProbability(uint probability, int expected)
+ {
+ // A middle interval can collapse during adaptation. Libaom cost.c floors its mass at EC_MIN_PROB=4,
+ // giving 13 * 512 rate units at and below that floor, while mass 8 costs 12 * 512 units.
+ Av1Distribution distribution = new(16384, 16384 + probability);
+ Assert.Equal(expected, Av1ProbabilityCost.GetSymbolCost(distribution, 1));
+ Assert.Equal(expected, Av1ProbabilityCost.GetSymbolCost((int)probability));
+ }
+
[Theory]
[InlineData(0, 0)]
[InlineData(1, 512)]