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)]