From a658a2cb7cb1971fdad47b1c9a89344d6f93dfca Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 7 Sep 2026 16:50:06 +1000 Subject: [PATCH] Store AV1 cumulative probabilities in 16 bits --- .../Heif/Av1/Entropy/Av1Distribution.cs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs index d962e5a605..0d618019bd 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs @@ -34,9 +34,9 @@ internal sealed class Av1Distribution private const int ProbabilityBitCount = 15; /// - /// The inverse cumulative thresholds followed by the required zero sentinel. + /// The Q15 inverse cumulative thresholds followed by the required zero sentinel. /// - private InlineArray16 probabilities; + private InlineArray16 probabilities; /// /// The symbol-count contribution to the adaptive update rate. @@ -273,13 +273,13 @@ internal sealed class Av1Distribution /// The symbol-count contribution to the update rate. private Av1Distribution(ReadOnlySpan props, int speed) { - Span probabilities = this.probabilities; + Span probabilities = this.probabilities; - // AV1 range coding consumes inverse cumulative thresholds. The defaults are written in the more readable - // forward form, so convert every real threshold while leaving the final zero sentinel untouched. + // Q15 thresholds range from zero through 32768, including zero-mass intervals, and fit in unsigned 16-bit storage. + // Convert the forward defaults before narrowing; the final zero sentinel is already initialized. for (int i = 0; i < props.Length - 1; i++) { - probabilities[i] = ProbabilityTop - props[i]; + probabilities[i] = (ushort)(ProbabilityTop - props[i]); } this.NumberOfSymbols = props.Length; @@ -292,8 +292,8 @@ internal sealed class Av1Distribution /// The distribution state to copy. private Av1Distribution(Av1Distribution source) { - ReadOnlySpan sourceProbabilities = source.probabilities; - Span probabilities = this.probabilities; + ReadOnlySpan sourceProbabilities = source.probabilities; + Span probabilities = this.probabilities; sourceProbabilities[..source.NumberOfSymbols].CopyTo(probabilities); // The adaptation rate depends on both the alphabet size and prior update count, so copying only the @@ -329,8 +329,8 @@ internal sealed class Av1Distribution { // Entropy contexts are created from the same fixed default table shape. Copy only mutable state so resetting a // working tile never allocates or replaces the distribution objects referenced by the symbol decoder. - ReadOnlySpan sourceProbabilities = source.probabilities; - Span probabilities = this.probabilities; + ReadOnlySpan sourceProbabilities = source.probabilities; + Span probabilities = this.probabilities; sourceProbabilities[..source.NumberOfSymbols].CopyTo(probabilities); this.updateCount = source.updateCount; } @@ -402,7 +402,8 @@ internal sealed class Av1Distribution int tmp = ProbabilityTop; // Switching tmp to zero at the observed symbol moves the thresholds on either side toward the sample while - // preserving their inverse-cumulative ordering in one pass. + // preserving their inverse-cumulative ordering in one pass. Arithmetic stays wide until the stored update; + // each step moves toward zero or 32768, so narrowing cannot discard a significant probability bit. for (int i = 0; i < this.NumberOfSymbols - 1; i++) { tmp = i == value ? 0 : tmp;