diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorContext.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorContext.cs index 5a28d06735..278da523c3 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorContext.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorContext.cs @@ -107,7 +107,17 @@ internal sealed class Av1MotionVectorContext /// The spatially derived reference vector. /// The fractional precision selected by the frame header. public void Write(Av1SymbolWriter writer, Av1MotionVector value, Av1MotionVector reference, Av1MotionVectorPrecision precision) - => _ = this.Process(writer, value, reference, precision); + => this.Write(writer, value, reference, precision); + + /// + /// The operation applied to each motion-vector symbol. + public void Write( + Av1SymbolWriter writer, + Av1MotionVector value, + Av1MotionVector reference, + Av1MotionVectorPrecision precision) + where TOperation : struct, Av1SymbolEncoder.ISymbolOperation + => _ = this.Process>(writer, value, reference, precision); /// /// Measures a motion-vector delta against the live distributions without changing them. @@ -150,14 +160,12 @@ internal sealed class Av1MotionVectorContext /// /// Emits motion-vector syntax and reports no estimated rate. /// - private readonly struct MotionVectorWriteOperation : IMotionVectorSymbolOperation + private readonly struct MotionVectorWriteOperation : IMotionVectorSymbolOperation + where TOperation : struct, Av1SymbolEncoder.ISymbolOperation { /// public static int ProcessSymbol(Av1SymbolWriter writer, int symbol, Av1Distribution distribution) - { - writer.WriteSymbol(symbol, distribution); - return 0; - } + => TOperation.ProcessSymbol(ref writer, symbol, distribution); } /// @@ -346,7 +354,7 @@ internal sealed class Av1MotionVectorContext /// The nonzero component in one-eighth-sample units. /// The fractional precision selected by the frame header. public void Write(Av1SymbolWriter writer, int value, Av1MotionVectorPrecision precision) - => _ = this.Process(writer, value, precision); + => _ = this.Process>(writer, value, precision); /// /// Processes one nonzero signed component through the shared motion-vector symbol operation. diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs index 1d6fb6b805..2c99974983 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs @@ -286,10 +286,15 @@ internal sealed class Av1SymbolEncoder : IDisposable } /// - /// Defines how shared coefficient-syntax helpers handle one adaptive symbol or literal bit field. + /// Defines how a syntax traversal handles one adaptive symbol or literal bit field. /// - private interface ICoefficientSymbolOperation + public interface ISymbolOperation { + /// + /// Gets a value indicating whether this operation emits a bitstream. + /// + public static abstract bool WritesOutput { get; } + /// /// Handles one symbol from an adaptive distribution. /// @@ -302,6 +307,24 @@ internal sealed class Av1SymbolEncoder : IDisposable int symbol, Av1Distribution distribution); + /// + /// Handles one binary symbol from an adaptive distribution. + /// + /// The tile range writer. + /// The binary symbol. + /// The symbol distribution. + /// The symbol's rate contribution. + public static abstract int ProcessSymbol(ref Av1SymbolWriter writer, bool symbol, Av1Distribution distribution); + + /// + /// Handles one binary symbol with a fixed probability. + /// + /// The tile range writer. + /// The binary value. + /// The probability of true, scaled by 32768. + /// The symbol's rate contribution. + public static abstract int ProcessBoolean(ref Av1SymbolWriter writer, bool value, uint frequency); + /// /// Handles one most-significant-bit-first literal field. /// @@ -320,6 +343,11 @@ internal sealed class Av1SymbolEncoder : IDisposable /// private interface IPaletteColorMapOperation { + /// + /// Gets a value indicating whether the traversal retains color tokens for later packing. + /// + static abstract bool RetainsTokens { get; } + /// /// Handles the first uniformly coded palette index. /// @@ -374,9 +402,18 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The low-order literal bits. /// The number of bits to write. public void WriteLiteral(uint value, int bitCount) + => this.WriteLiteral(value, bitCount); + + /// + /// The operation applied to each symbol and literal. + public void WriteLiteral(uint value, int bitCount) + where TOperation : struct, ISymbolOperation { - ref Av1SymbolWriter w = ref this.writer; - w.WriteLiteral(value, bitCount); + if (TOperation.WritesOutput) + { + ref Av1SymbolWriter w = ref this.writer; + _ = TOperation.ProcessLiteral(ref w, value, bitCount); + } } /// @@ -385,20 +422,29 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The number of possible values. /// The value in the range from zero through minus one. public void WriteUniform(int valueCount, int value) + => this.WriteUniform(valueCount, value); + + /// + /// The operation applied to each symbol and literal. + public void WriteUniform(int valueCount, int value) + where TOperation : struct, ISymbolOperation { - ref Av1SymbolWriter w = ref this.writer; - int bitCount = Av1Math.Log2(valueCount) + 1; - int threshold = (1 << bitCount) - valueCount; - if (value < threshold) + if (TOperation.WritesOutput) { - // The lower values use the short prefix; every remaining value carries one final disambiguating bit. - w.WriteLiteral((uint)value, bitCount - 1); - return; - } + ref Av1SymbolWriter w = ref this.writer; + int bitCount = Av1Math.Log2(valueCount) + 1; + int threshold = (1 << bitCount) - valueCount; + if (value < threshold) + { + // The lower values use the short prefix; every remaining value carries one final disambiguating bit. + _ = TOperation.ProcessLiteral(ref w, (uint)value, bitCount - 1); + return; + } - int offset = value - threshold; - w.WriteLiteral((uint)(threshold + (offset >> 1)), bitCount - 1); - w.WriteLiteral((uint)(offset & 1), 1); + int offset = value - threshold; + _ = TOperation.ProcessLiteral(ref w, (uint)(threshold + (offset >> 1)), bitCount - 1); + _ = TOperation.ProcessLiteral(ref w, (uint)(offset & 1), 1); + } } /// @@ -435,9 +481,15 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The block-area context in the range from zero through six. /// The number of available above and left luma neighbors that use palettes. public void WritePaletteYMode(bool usePalette, int blockSizeContext, int neighborContext) + => this.WritePaletteYMode(usePalette, blockSizeContext, neighborContext); + + /// + /// The operation applied to each symbol and literal. + public void WritePaletteYMode(bool usePalette, int blockSizeContext, int neighborContext) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(usePalette, this.entropyContext.PaletteYMode[blockSizeContext][neighborContext]); + _ = TOperation.ProcessSymbol(ref w, usePalette, this.entropyContext.PaletteYMode[blockSizeContext][neighborContext]); } /// @@ -459,9 +511,15 @@ internal sealed class Av1SymbolEncoder : IDisposable /// Indicates whether the block uses chroma palette prediction. /// Indicates whether the current block uses a luma palette. public void WritePaletteUvMode(bool usePalette, bool hasLumaPalette) + => this.WritePaletteUvMode(usePalette, hasLumaPalette); + + /// + /// The operation applied to each symbol and literal. + public void WritePaletteUvMode(bool usePalette, bool hasLumaPalette) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(usePalette, this.entropyContext.PaletteUvMode[hasLumaPalette ? 1 : 0]); + _ = TOperation.ProcessSymbol(ref w, usePalette, this.entropyContext.PaletteUvMode[hasLumaPalette ? 1 : 0]); } /// @@ -487,13 +545,19 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The block-area context in the range from zero through six. /// The luma or chroma plane class. public void WritePaletteSize(int paletteSize, int blockSizeContext, Av1PlaneType planeType) + => this.WritePaletteSize(paletteSize, blockSizeContext, planeType); + + /// + /// The operation applied to each symbol and literal. + public void WritePaletteSize(int paletteSize, int blockSizeContext, Av1PlaneType planeType) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; Av1Distribution distribution = planeType == Av1PlaneType.Y ? this.entropyContext.PaletteYSize[blockSizeContext] : this.entropyContext.PaletteUvSize[blockSizeContext]; - w.WriteSymbol(paletteSize - 2, distribution); + _ = TOperation.ProcessSymbol(ref w, paletteSize - 2, distribution); } /// @@ -529,13 +593,23 @@ internal sealed class Av1SymbolEncoder : IDisposable int paletteSize, int colorContext, Av1PlaneType planeType) + => this.WritePaletteColorIndex(colorOrderIndex, paletteSize, colorContext, planeType); + + /// + /// The operation applied to each symbol and literal. + public void WritePaletteColorIndex( + int colorOrderIndex, + int paletteSize, + int colorContext, + Av1PlaneType planeType) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; Av1Distribution distribution = planeType == Av1PlaneType.Y ? this.entropyContext.PaletteYColorIndex[paletteSize - 2][colorContext] : this.entropyContext.PaletteUvColorIndex[paletteSize - 2][colorContext]; - w.WriteSymbol(colorOrderIndex, distribution); + _ = TOperation.ProcessSymbol(ref w, colorOrderIndex, distribution); } /// @@ -575,24 +649,36 @@ internal sealed class Av1SymbolEncoder : IDisposable ReadOnlySpan colorCache, ReadOnlySpan colors, int bitDepth) - { - Span cacheColorFound = stackalloc byte[Av1Constants.PaletteMaxSize * 2]; - Span uncachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; - int uncachedColorCount = IndexColorCache( - colorCache, - colors, - cacheColorFound, - uncachedColors); + => this.WritePaletteYColors(colorCache, colors, bitDepth); - int cachedColorCount = 0; - for (int i = 0; i < colorCache.Length && cachedColorCount < colors.Length; i++) + /// + /// The operation applied to each symbol and literal. + public void WritePaletteYColors( + ReadOnlySpan colorCache, + ReadOnlySpan colors, + int bitDepth) + where TOperation : struct, ISymbolOperation + { + if (TOperation.WritesOutput) { - byte found = cacheColorFound[i]; - this.WriteLiteral(found, 1); - cachedColorCount += found; - } + Span cacheColorFound = stackalloc byte[Av1Constants.PaletteMaxSize * 2]; + Span uncachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; + int uncachedColorCount = IndexColorCache( + colorCache, + colors, + cacheColorFound, + uncachedColors); + + int cachedColorCount = 0; + for (int i = 0; i < colorCache.Length && cachedColorCount < colors.Length; i++) + { + byte found = cacheColorFound[i]; + this.WriteLiteral(found, 1); + cachedColorCount += found; + } - this.WriteDeltaEncodedColors(uncachedColors[..uncachedColorCount], bitDepth, minimumDelta: 1); + this.WriteDeltaEncodedColors(uncachedColors[..uncachedColorCount], bitDepth, minimumDelta: 1); + } } /// @@ -640,61 +726,74 @@ internal sealed class Av1SymbolEncoder : IDisposable ReadOnlySpan uColors, ReadOnlySpan vColors, int bitDepth) - { - Span cacheColorFound = stackalloc byte[Av1Constants.PaletteMaxSize * 2]; - Span uncachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; - int uncachedColorCount = IndexColorCache( - colorCache, - uColors, - cacheColorFound, - uncachedColors); - - int cachedColorCount = 0; - for (int i = 0; i < colorCache.Length && cachedColorCount < uColors.Length; i++) - { - byte found = cacheColorFound[i]; - this.WriteLiteral(found, 1); - cachedColorCount += found; - } + => this.WritePaletteUvColors(colorCache, uColors, vColors, bitDepth); - this.WriteDeltaEncodedColors(uncachedColors[..uncachedColorCount], bitDepth, minimumDelta: 0); - - int deltaBits = GetPaletteVDeltaBitCount(vColors, bitDepth, out int zeroCount, out int minimumBits); - int deltaBitCount = 2 + bitDepth + ((deltaBits + 1) * (vColors.Length - 1)) - zeroCount; - int rawBitCount = bitDepth * vColors.Length; - bool useDelta = deltaBitCount < rawBitCount; - this.WriteLiteral(useDelta ? 1u : 0u, 1); - if (!useDelta) + /// + /// The operation applied to each symbol and literal. + public void WritePaletteUvColors( + ReadOnlySpan colorCache, + ReadOnlySpan uColors, + ReadOnlySpan vColors, + int bitDepth) + where TOperation : struct, ISymbolOperation + { + if (TOperation.WritesOutput) { - for (int i = 0; i < vColors.Length; i++) + Span cacheColorFound = stackalloc byte[Av1Constants.PaletteMaxSize * 2]; + Span uncachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; + int uncachedColorCount = IndexColorCache( + colorCache, + uColors, + cacheColorFound, + uncachedColors); + + int cachedColorCount = 0; + for (int i = 0; i < colorCache.Length && cachedColorCount < uColors.Length; i++) { - this.WriteLiteral(vColors[i], bitDepth); + byte found = cacheColorFound[i]; + this.WriteLiteral(found, 1); + cachedColorCount += found; } - return; - } - - this.WriteLiteral((uint)(deltaBits - minimumBits), 2); - this.WriteLiteral(vColors[0], bitDepth); - int sampleRange = 1 << bitDepth; - for (int i = 1; i < vColors.Length; i++) - { - int signedDelta = vColors[i] - vColors[i - 1]; - int delta = Math.Abs(signedDelta); + this.WriteDeltaEncodedColors(uncachedColors[..uncachedColorCount], bitDepth, minimumDelta: 0); - // Chroma wraps in its unsigned sample domain, so signal whichever circular direction has less magnitude. - if (delta <= sampleRange - delta) + int deltaBits = GetPaletteVDeltaBitCount(vColors, bitDepth, out int zeroCount, out int minimumBits); + int deltaBitCount = 2 + bitDepth + ((deltaBits + 1) * (vColors.Length - 1)) - zeroCount; + int rawBitCount = bitDepth * vColors.Length; + bool useDelta = deltaBitCount < rawBitCount; + this.WriteLiteral(useDelta ? 1u : 0u, 1); + if (!useDelta) { - this.WriteLiteral((uint)delta, deltaBits); - if (delta != 0) + for (int i = 0; i < vColors.Length; i++) { - this.WriteLiteral(signedDelta < 0 ? 1u : 0u, 1); + this.WriteLiteral(vColors[i], bitDepth); } + + return; } - else + + this.WriteLiteral((uint)(deltaBits - minimumBits), 2); + this.WriteLiteral(vColors[0], bitDepth); + int sampleRange = 1 << bitDepth; + for (int i = 1; i < vColors.Length; i++) { - this.WriteLiteral((uint)(sampleRange - delta), deltaBits); - this.WriteLiteral(signedDelta < 0 ? 0u : 1u, 1); + int signedDelta = vColors[i] - vColors[i - 1]; + int delta = Math.Abs(signedDelta); + + // Chroma wraps in its unsigned sample domain, so signal whichever circular direction has less magnitude. + if (delta <= sampleRange - delta) + { + this.WriteLiteral((uint)delta, deltaBits); + if (delta != 0) + { + this.WriteLiteral(signedDelta < 0 ? 1u : 0u, 1); + } + } + else + { + this.WriteLiteral((uint)(sampleRange - delta), deltaBits); + this.WriteLiteral(signedDelta < 0 ? 0u : 1u, 1); + } } } } @@ -719,7 +818,8 @@ internal sealed class Av1SymbolEncoder : IDisposable planeType, rows, columns, - colorIndexMap); + colorIndexMap, + Span.Empty); /// /// Writes a complete palette color-index map in AV1 diagonal wavefront order. @@ -735,13 +835,67 @@ internal sealed class Av1SymbolEncoder : IDisposable int rows, int columns, Buffer2DRegion colorIndexMap) + => this.WritePaletteColorMap(paletteSize, planeType, rows, columns, colorIndexMap); + + /// + /// The operation applied to each symbol and literal. + public void WritePaletteColorMap( + int paletteSize, + Av1PlaneType planeType, + int rows, + int columns, + Buffer2DRegion colorIndexMap) + where TOperation : struct, ISymbolOperation { - _ = this.ProcessPaletteColorMap( + _ = this.ProcessPaletteColorMap>( paletteSize, planeType, rows, columns, - colorIndexMap); + colorIndexMap, + Span.Empty); + } + + /// + /// Retains palette color tokens and updates their adaptive probabilities without writing output bytes. + /// + /// The number of colors in the palette. + /// The luma or chroma plane class. + /// The number of coded map rows. + /// The number of coded map columns. + /// The selected color-index map. + /// The destination with one byte per coded sample. + public void TokenizePaletteColorMap( + int paletteSize, + Av1PlaneType planeType, + int rows, + int columns, + Buffer2DRegion colorIndexMap, + Span tokens) + { + _ = this.ProcessPaletteColorMap( + paletteSize, + planeType, + rows, + columns, + colorIndexMap, + tokens); + } + + /// + /// Writes retained palette tokens in their previously selected order. + /// + /// The number of colors in the palette. + /// The luma or chroma plane class. + /// The raw first index followed by packed context and color-rank tokens. + public void WritePaletteTokens(int paletteSize, Av1PlaneType planeType, ReadOnlySpan tokens) + { + this.WriteUniform(paletteSize, tokens[0]); + for (int i = 1; i < tokens.Length; i++) + { + byte token = tokens[i]; + this.WritePaletteColorIndex(token & 7, paletteSize, token >> 4, planeType); + } } /// @@ -749,9 +903,15 @@ internal sealed class Av1SymbolEncoder : IDisposable /// /// Indicates whether intra-block copy is selected. public void WriteUseIntraBlockCopy(bool value) + => this.WriteUseIntraBlockCopy(value); + + /// + /// The operation applied to each symbol and literal. + public void WriteUseIntraBlockCopy(bool value) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(value, this.tileIntraBlockCopy); + _ = TOperation.ProcessSymbol(ref w, value, this.tileIntraBlockCopy); } /// @@ -768,7 +928,13 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The displacement vector to encode. /// The spatially derived reference vector. public void WriteDisplacementVector(Av1MotionVector value, Av1MotionVector reference) - => this.displacementVector.Write(this.writer, value, reference, Av1MotionVectorPrecision.Integer); + => this.WriteDisplacementVector(value, reference); + + /// + /// The operation applied to each symbol and literal. + public void WriteDisplacementVector(Av1MotionVector value, Av1MotionVector reference) + where TOperation : struct, ISymbolOperation + => this.displacementVector.Write(this.writer, value, reference, Av1MotionVectorPrecision.Integer); /// /// Measures an integer intra-block-copy displacement vector against the live distributions. @@ -818,7 +984,13 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The regular, smooth, or sharp filter. /// The spatial filter context for the selected direction. public void WriteSwitchableInterpolationFilter(Av1InterpolationFilter filter, int context) - => this.writer.WriteSymbol((int)filter, this.entropyContext.SwitchableInterpolation[context]); + => this.WriteSwitchableInterpolationFilter(filter, context); + + /// + /// The operation applied to each symbol and literal. + public void WriteSwitchableInterpolationFilter(Av1InterpolationFilter filter, int context) + where TOperation : struct, ISymbolOperation + => TOperation.ProcessSymbol(ref this.writer, (int)filter, this.entropyContext.SwitchableInterpolation[context]); /// /// Measures a single-reference inter mode against the live branch distributions. @@ -859,23 +1031,30 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The new, global, nearest, or near motion-vector mode. /// The packed context derived from the reference-vector stack. public void WriteInterMode(Av1PredictionMode mode, int modeContext) + => this.WriteInterMode(mode, modeContext); + + /// + /// The operation applied to each symbol and literal. + public void WriteInterMode(Av1PredictionMode mode, int modeContext) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; bool isNotNew = mode != Av1PredictionMode.NewMotionVector; - w.WriteSymbol(isNotNew, this.newMotionVector[Av1SymbolContextHelper.GetNewMvContext(modeContext)]); + _ = TOperation.ProcessSymbol(ref w, isNotNew, this.newMotionVector[Av1SymbolContextHelper.GetNewMvContext(modeContext)]); if (!isNotNew) { return; } bool isNotGlobal = mode != Av1PredictionMode.GlobalMotionVector; - w.WriteSymbol(isNotGlobal, this.zeroMotionVector[Av1SymbolContextHelper.GetZeroMvContext(modeContext)]); + _ = TOperation.ProcessSymbol(ref w, isNotGlobal, this.zeroMotionVector[Av1SymbolContextHelper.GetZeroMvContext(modeContext)]); if (!isNotGlobal) { return; } - w.WriteSymbol( + _ = TOperation.ProcessSymbol( + ref w, mode == Av1PredictionMode.NearMotionVector, this.referenceMotionVector[Av1SymbolContextHelper.GetRefMvContext(modeContext)]); } @@ -895,9 +1074,15 @@ internal sealed class Av1SymbolEncoder : IDisposable /// Whether selection advances to the next candidate. /// The candidate-weight context. public void WriteDynamicReferenceList(bool advance, int context) + => this.WriteDynamicReferenceList(advance, context); + + /// + /// The operation applied to each symbol and literal. + public void WriteDynamicReferenceList(bool advance, int context) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(advance, this.dynamicReferenceList[context]); + _ = TOperation.ProcessSymbol(ref w, advance, this.dynamicReferenceList[context]); } /// @@ -929,7 +1114,16 @@ internal sealed class Av1SymbolEncoder : IDisposable Av1MotionVector value, Av1MotionVector reference, Av1MotionVectorPrecision precision) - => this.motionVector.Write(this.writer, value, reference, precision); + => this.WriteMotionVector(value, reference, precision); + + /// + /// The operation applied to each symbol and literal. + public void WriteMotionVector( + Av1MotionVector value, + Av1MotionVector reference, + Av1MotionVectorPrecision precision) + where TOperation : struct, ISymbolOperation + => this.motionVector.Write(this.writer, value, reference, precision); /// /// Gets the current fixed-point cost of a complete block partition symbol. @@ -946,9 +1140,15 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The partition type to encode. /// The partition probability context. public void WritePartitionType(Av1PartitionType partitionType, int context) + => this.WritePartitionType(partitionType, context); + + /// + /// The operation applied to each symbol and literal. + public void WritePartitionType(Av1PartitionType partitionType, int context) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol((int)partitionType, this.tilePartitionTypes[context]); + _ = TOperation.ProcessSymbol(ref w, (int)partitionType, this.tilePartitionTypes[context]); } /// @@ -958,11 +1158,20 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The current block size. /// The partition probability context. public void WriteSplitOrHorizontal(Av1PartitionType partitionType, Av1BlockSize blockSize, int context) + => this.WriteSplitOrHorizontal(partitionType, blockSize, context); + + /// + /// The operation applied to each symbol and literal. + public void WriteSplitOrHorizontal(Av1PartitionType partitionType, Av1BlockSize blockSize, int context) + where TOperation : struct, ISymbolOperation { - uint frequency = Av1SymbolDecoder.GetSplitOrHorizontalFrequency(this.tilePartitionTypes, blockSize, context); - bool value = partitionType == Av1PartitionType.Split; - ref Av1SymbolWriter w = ref this.writer; - w.WriteBoolean(value, frequency); + if (TOperation.WritesOutput) + { + uint frequency = Av1SymbolDecoder.GetSplitOrHorizontalFrequency(this.tilePartitionTypes, blockSize, context); + bool value = partitionType == Av1PartitionType.Split; + ref Av1SymbolWriter w = ref this.writer; + _ = TOperation.ProcessBoolean(ref w, value, frequency); + } } /// @@ -992,11 +1201,20 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The current block size. /// The partition probability context. public void WriteSplitOrVertical(Av1PartitionType partitionType, Av1BlockSize blockSize, int context) + => this.WriteSplitOrVertical(partitionType, blockSize, context); + + /// + /// The operation applied to each symbol and literal. + public void WriteSplitOrVertical(Av1PartitionType partitionType, Av1BlockSize blockSize, int context) + where TOperation : struct, ISymbolOperation { - uint frequency = Av1SymbolDecoder.GetSplitOrVerticalFrequency(this.tilePartitionTypes, blockSize, context); - bool value = partitionType == Av1PartitionType.Split; - ref Av1SymbolWriter w = ref this.writer; - w.WriteBoolean(value, frequency); + if (TOperation.WritesOutput) + { + uint frequency = Av1SymbolDecoder.GetSplitOrVerticalFrequency(this.tilePartitionTypes, blockSize, context); + bool value = partitionType == Av1PartitionType.Split; + ref Av1SymbolWriter w = ref this.writer; + _ = TOperation.ProcessBoolean(ref w, value, frequency); + } } /// @@ -1044,12 +1262,51 @@ internal sealed class Av1SymbolEncoder : IDisposable bool useReducedTransformSet, Av1FilterIntraMode filterIntraMode, bool usesInterTransformSet) + => this.WriteCoefficients( + transformSize, + transformType, + intraDirection, + coefficientBuffer, + componentType, + transformBlockContext, + endOfBlock, + useReducedTransformSet, + filterIntraMode, + usesInterTransformSet); + + /// + /// Processes finalized coefficient symbols and returns the neighboring coefficient context. + /// + /// The operation applied to each symbol and literal. + /// The transform dimensions. + /// The selected transform type. + /// The luma prediction mode. + /// The quantized raster coefficients. + /// The luma or chroma component. + /// The neighboring skip and DC sign contexts. + /// The one-based final nonzero scan position, or zero for an empty transform. + /// Whether the reduced transform set applies. + /// The filter-intra prediction mode. + /// Whether inter transform syntax applies. + /// The coefficient context consumed by adjacent transforms. + public int WriteCoefficients( + Av1TransformSize transformSize, + Av1TransformType transformType, + Av1PredictionMode intraDirection, + ReadOnlySpan coefficientBuffer, + Av1ComponentType componentType, + Av1TransformBlockContext transformBlockContext, + ushort endOfBlock, + bool useReducedTransformSet, + Av1FilterIntraMode filterIntraMode, + bool usesInterTransformSet) + where TOperation : struct, ISymbolOperation { Av1TransformSize transformSizeContext = Av1SymbolContextHelper.GetTransformSizeContext(transformSize); DebugGuard.MustBeLessThan((int)transformSizeContext, (int)Av1TransformSize.AllSizes, nameof(transformSizeContext)); - _ = this.ProcessTransformBlockSkip( + _ = this.ProcessTransformBlockSkip( endOfBlock == 0, transformSizeContext, transformBlockContext.SkipContext); @@ -1073,7 +1330,7 @@ internal sealed class Av1SymbolEncoder : IDisposable levels.Initialize(coefficientBuffer); if (componentType == Av1ComponentType.Luminance) { - _ = this.ProcessTransformType( + _ = this.ProcessTransformType( transformType, transformSize, usesInterTransformSet, @@ -1083,7 +1340,7 @@ internal sealed class Av1SymbolEncoder : IDisposable intraDirection); } - _ = this.ProcessEndOfBlockPosition( + _ = this.ProcessEndOfBlockPosition( endOfBlock, componentType, transformClass, @@ -1103,13 +1360,15 @@ internal sealed class Av1SymbolEncoder : IDisposable if (c == endOfBlock - 1) { - w.WriteSymbol( + _ = TOperation.ProcessSymbol( + ref w, Math.Min(level, 3) - 1, this.coefficientsBaseEndOfBlock[(int)transformSizeContext][(int)componentType][coefficientContext]); } else { - w.WriteSymbol( + _ = TOperation.ProcessSymbol( + ref w, Math.Min(level, 3), this.coefficientsBase[(int)transformSizeContext][(int)componentType][coefficientContext]); } @@ -1122,7 +1381,8 @@ internal sealed class Av1SymbolEncoder : IDisposable for (int idx = 0; idx < Av1Constants.CoefficientBaseRange; idx += Av1Constants.BaseRangeSizeMinus1) { int symbol = Math.Min(baseRange - idx, Av1Constants.BaseRangeSizeMinus1); - w.WriteSymbol( + _ = TOperation.ProcessSymbol( + ref w, symbol, this.coefficientsBaseRange[limitedTransformSizeContext][(int)componentType][baseRangeContext]); @@ -1148,18 +1408,19 @@ internal sealed class Av1SymbolEncoder : IDisposable { if (c == 0) { - w.WriteSymbol( + _ = TOperation.ProcessSymbol( + ref w, (int)sign, this.dcSign[(int)componentType][transformBlockContext.DcSignContext]); } else { - w.WriteLiteral(sign, 1); + _ = TOperation.ProcessLiteral(ref w, sign, 1); } if (level > (Av1Constants.CoefficientBaseRange + Av1Constants.BaseLevelsCount)) { - this.WriteGolomb( + this.WriteGolomb( level - Av1Constants.CoefficientBaseRange - 1 - Av1Constants.BaseLevelsCount); } } @@ -1361,8 +1622,19 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The signaled transform size selecting the token alphabet. /// The square transform-size probability context. public void WriteEndOfBlockPosition(ushort endOfBlock, Av1ComponentType componentType, Av1TransformClass transformClass, Av1TransformSize transformSize, Av1TransformSize transformSizeContext) + => this.WriteEndOfBlockPosition(endOfBlock, componentType, transformClass, transformSize, transformSizeContext); + + /// + /// The operation applied to each symbol and literal. + public void WriteEndOfBlockPosition( + ushort endOfBlock, + Av1ComponentType componentType, + Av1TransformClass transformClass, + Av1TransformSize transformSize, + Av1TransformSize transformSizeContext) + where TOperation : struct, ISymbolOperation { - _ = this.ProcessEndOfBlockPosition( + _ = this.ProcessEndOfBlockPosition( endOfBlock, componentType, transformClass, @@ -1376,7 +1648,7 @@ internal sealed class Av1SymbolEncoder : IDisposable Av1TransformClass transformClass, Av1TransformSize transformSize, Av1TransformSize transformSizeContext) - where TOperation : struct, ICoefficientSymbolOperation + where TOperation : struct, ISymbolOperation { short endOfBlockPosition = Av1SymbolContextHelper.GetEndOfBlockPosition(endOfBlock, out int eobExtra); int rate = this.ProcessEndOfBlockFlag( @@ -1425,15 +1697,21 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The square transform-size probability context. /// The context derived from neighboring coefficient blocks. public void WriteTransformBlockSkip(bool skip, Av1TransformSize transformSizeContext, int skipContext) + => this.WriteTransformBlockSkip(skip, transformSizeContext, skipContext); + + /// + /// The operation applied to each symbol and literal. + public void WriteTransformBlockSkip(bool skip, Av1TransformSize transformSizeContext, int skipContext) + where TOperation : struct, ISymbolOperation { - _ = this.ProcessTransformBlockSkip(skip, transformSizeContext, skipContext); + _ = this.ProcessTransformBlockSkip(skip, transformSizeContext, skipContext); } private int ProcessTransformBlockSkip( bool skip, Av1TransformSize transformSizeContext, int skipContext) - where TOperation : struct, ICoefficientSymbolOperation + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; return TOperation.ProcessSymbol( @@ -1462,10 +1740,16 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The selected transform size. /// The neighboring transform-size context. public void WriteTransformSize(Av1BlockSize blockSize, Av1TransformSize transformSize, int context) + => this.WriteTransformSize(blockSize, transformSize, context); + + /// + /// The operation applied to each symbol and literal. + public void WriteTransformSize(Av1BlockSize blockSize, Av1TransformSize transformSize, int context) + where TOperation : struct, ISymbolOperation { int selectedDepth = GetTransformSizeDepth(blockSize, transformSize, out int categoryDepth); ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(selectedDepth, this.transformSize[categoryDepth - 1][context]); + _ = TOperation.ProcessSymbol(ref w, selectedDepth, this.transformSize[categoryDepth - 1][context]); } /// @@ -1483,9 +1767,15 @@ internal sealed class Av1SymbolEncoder : IDisposable /// Indicates whether the current transform node is split. /// The neighboring variable-transform context. public void WriteTransformPartition(bool split, int context) + => this.WriteTransformPartition(split, context); + + /// + /// The operation applied to each symbol and literal. + public void WriteTransformPartition(bool split, int context) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(split ? 1 : 0, this.transformPartition[context]); + _ = TOperation.ProcessSymbol(ref w, split ? 1 : 0, this.transformPartition[context]); } private static int GetTransformSizeDepth( @@ -1556,12 +1846,15 @@ internal sealed class Av1SymbolEncoder : IDisposable /// /// The nonnegative suffix value. public void WriteGolomb(int level) + => this.WriteGolomb(level); + + private void WriteGolomb(int level) + where TOperation : struct, ISymbolOperation { uint x = (uint)level + 1u; int length = GetGolombBitLength(level); - ref Av1SymbolWriter w = ref this.writer; - w.WriteLiteral(0u, length - 1); - w.WriteLiteral(x, length); + _ = TOperation.ProcessLiteral(ref this.writer, 0u, length - 1); + _ = TOperation.ProcessLiteral(ref this.writer, x, length); } private static int GetBaseRangeCost(int level, Av1Distribution distribution) @@ -1611,7 +1904,7 @@ internal sealed class Av1SymbolEncoder : IDisposable Av1TransformClass transformClass, Av1TransformSize transformSize, int endOfBlockPosition) - where TOperation : struct, ICoefficientSymbolOperation + where TOperation : struct, ISymbolOperation { int endOfBlockMultiSize = transformSize.GetLog2Minus4(); int endOfBlockContext = transformClass == Av1TransformClass.Class2D ? 0 : 1; @@ -1668,8 +1961,28 @@ internal sealed class Av1SymbolEncoder : IDisposable Av1FilterIntraMode filterIntraMode, Av1PredictionMode intraDirection, bool usesInterTransformSet) + => this.WriteTransformType( + transformType, + transformSize, + useReducedTransformSet, + baseQIndex, + filterIntraMode, + intraDirection, + usesInterTransformSet); + + /// + /// The operation applied to each symbol and literal. + public void WriteTransformType( + Av1TransformType transformType, + Av1TransformSize transformSize, + bool useReducedTransformSet, + int baseQIndex, + Av1FilterIntraMode filterIntraMode, + Av1PredictionMode intraDirection, + bool usesInterTransformSet) + where TOperation : struct, ISymbolOperation { - _ = this.ProcessTransformType( + _ = this.ProcessTransformType( transformType, transformSize, usesInterTransformSet, @@ -1687,7 +2000,7 @@ internal sealed class Av1SymbolEncoder : IDisposable int baseQIndex, Av1FilterIntraMode filterIntraMode, Av1PredictionMode intraDirection) - where TOperation : struct, ICoefficientSymbolOperation + where TOperation : struct, ISymbolOperation { Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType( transformSize, @@ -1742,9 +2055,15 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The segment identifier. /// The context derived from neighboring segment identifiers. public void WriteSegmentId(int segmentId, int context) + => this.WriteSegmentId(segmentId, context); + + /// + /// The operation applied to each symbol and literal. + public void WriteSegmentId(int segmentId, int context) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(segmentId, this.segmentId[context]); + _ = TOperation.ProcessSymbol(ref w, segmentId, this.segmentId[context]); } /// @@ -1762,9 +2081,15 @@ internal sealed class Av1SymbolEncoder : IDisposable /// Indicates whether the block contains no coded transform coefficients. /// The neighboring skip context. public void WriteSkip(bool skip, int context) + => this.WriteSkip(skip, context); + + /// + /// The operation applied to each symbol and literal. + public void WriteSkip(bool skip, int context) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(skip, this.skip[context]); + _ = TOperation.ProcessSymbol(ref w, skip, this.skip[context]); } /// @@ -1773,9 +2098,15 @@ internal sealed class Av1SymbolEncoder : IDisposable /// Indicates whether skip mode is selected. /// The neighboring skip-mode context. public void WriteSkipMode(bool skip, int context) + => this.WriteSkipMode(skip, context); + + /// + /// The operation applied to each symbol and literal. + public void WriteSkipMode(bool skip, int context) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(skip, this.skipMode[context]); + _ = TOperation.ProcessSymbol(ref w, skip, this.skipMode[context]); } /// @@ -1802,13 +2133,19 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The selected filter-intra mode, or the disabled sentinel. /// The block size selecting the enable distribution. public void WriteFilterIntraMode(Av1FilterIntraMode filterIntraMode, Av1BlockSize blockSize) + => this.WriteFilterIntraMode(filterIntraMode, blockSize); + + /// + /// The operation applied to each symbol and literal. + public void WriteFilterIntraMode(Av1FilterIntraMode filterIntraMode, Av1BlockSize blockSize) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; bool useFilter = filterIntraMode != Av1FilterIntraMode.AllFilterIntraModes; - w.WriteSymbol(useFilter, this.filterIntra[(int)blockSize]); + _ = TOperation.ProcessSymbol(ref w, useFilter, this.filterIntra[(int)blockSize]); if (useFilter) { - w.WriteSymbol((int)filterIntraMode, this.filterIntraMode); + _ = TOperation.ProcessSymbol(ref w, (int)filterIntraMode, this.filterIntraMode); } } @@ -1817,26 +2154,32 @@ internal sealed class Av1SymbolEncoder : IDisposable /// /// The signed quantizer-index delta. public void WriteDeltaQuantizerIndex(int deltaQindex) + => this.WriteDeltaQuantizerIndex(deltaQindex); + + /// + /// The operation applied to each symbol and literal. + public void WriteDeltaQuantizerIndex(int deltaQindex) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; bool sign = deltaQindex < 0; int abs = Math.Abs(deltaQindex); bool isSmallValue = abs < Av1Constants.DeltaQuantizerSmall; - w.WriteSymbol(Math.Min(abs, Av1Constants.DeltaQuantizerSmall), this.deltaQuantizerAbsolute); + _ = TOperation.ProcessSymbol(ref w, Math.Min(abs, Av1Constants.DeltaQuantizerSmall), this.deltaQuantizerAbsolute); if (!isSmallValue) { // Escape magnitudes encode their bit width first, followed by the offset within that width's range. int remainingBitCount = Av1Math.MostSignificantBit((uint)(abs - 1)); int threshold = (1 << remainingBitCount) + 1; - w.WriteLiteral((uint)(remainingBitCount - 1), 3); - w.WriteLiteral((uint)(abs - threshold), remainingBitCount); + _ = TOperation.ProcessLiteral(ref w, (uint)(remainingBitCount - 1), 3); + _ = TOperation.ProcessLiteral(ref w, (uint)(abs - threshold), remainingBitCount); } if (abs > 0) { - w.WriteLiteral(sign); + _ = TOperation.ProcessLiteral(ref w, sign ? 1u : 0u, 1); } } @@ -1859,9 +2202,15 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The reduced above-mode context. /// The reduced left-mode context. public void WriteLumaMode(Av1PredictionMode lumaMode, byte topContext, byte leftContext) + => this.WriteLumaMode(lumaMode, topContext, leftContext); + + /// + /// The operation applied to each symbol and literal. + public void WriteLumaMode(Av1PredictionMode lumaMode, byte topContext, byte leftContext) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol((int)lumaMode, this.keyFrameYMode[topContext][leftContext]); + _ = TOperation.ProcessSymbol(ref w, (int)lumaMode, this.keyFrameYMode[topContext][leftContext]); } /// @@ -1879,9 +2228,15 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The intra luma mode. /// The coding block size selecting the size group. public void WriteInterFrameLumaMode(Av1PredictionMode lumaMode, Av1BlockSize blockSize) + => this.WriteInterFrameLumaMode(lumaMode, blockSize); + + /// + /// The operation applied to each symbol and literal. + public void WriteInterFrameLumaMode(Av1PredictionMode lumaMode, Av1BlockSize blockSize) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol((int)lumaMode, this.frameYMode[blockSize.GetSizeGroup()]); + _ = TOperation.ProcessSymbol(ref w, (int)lumaMode, this.frameYMode[blockSize.GetSizeGroup()]); } /// @@ -1899,9 +2254,15 @@ internal sealed class Av1SymbolEncoder : IDisposable /// Whether the block uses a retained reference frame. /// The neighboring prediction-domain context. public void WriteIsInter(bool isInter, int context) + => this.WriteIsInter(isInter, context); + + /// + /// The operation applied to each symbol and literal. + public void WriteIsInter(bool isInter, int context) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(isInter, this.intraInter[context]); + _ = TOperation.ProcessSymbol(ref w, isInter, this.intraInter[context]); } /// @@ -1958,23 +2319,32 @@ internal sealed class Av1SymbolEncoder : IDisposable public void WriteSingleReference( Av1ReferenceFrameType referenceFrame, ReadOnlySpan referenceCounts) + => this.WriteSingleReference(referenceFrame, referenceCounts); + + /// + /// The operation applied to each symbol and literal. + public void WriteSingleReference( + Av1ReferenceFrameType referenceFrame, + ReadOnlySpan referenceCounts) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; bool isBackward = referenceFrame >= Av1ReferenceFrameType.Backward; int context = Av1SymbolContextHelper.GetSingleReferenceBackwardContext(referenceCounts); - w.WriteSymbol(isBackward, this.singleReference[context][0]); + _ = TOperation.ProcessSymbol(ref w, isBackward, this.singleReference[context][0]); if (isBackward) { bool isAlternate = referenceFrame == Av1ReferenceFrameType.Alternate; context = Av1SymbolContextHelper.GetSingleReferenceAlternateContext(referenceCounts); - w.WriteSymbol(isAlternate, this.singleReference[context][1]); + _ = TOperation.ProcessSymbol(ref w, isAlternate, this.singleReference[context][1]); if (isAlternate) { return; } context = Av1SymbolContextHelper.GetSingleReferenceAlternate2Context(referenceCounts); - w.WriteSymbol( + _ = TOperation.ProcessSymbol( + ref w, referenceFrame == Av1ReferenceFrameType.Alternate2, this.singleReference[context][5]); @@ -1983,11 +2353,12 @@ internal sealed class Av1SymbolEncoder : IDisposable bool isLast3OrGolden = referenceFrame is Av1ReferenceFrameType.Last3 or Av1ReferenceFrameType.Golden; context = Av1SymbolContextHelper.GetSingleReferenceLast3OrGoldenContext(referenceCounts); - w.WriteSymbol(isLast3OrGolden, this.singleReference[context][2]); + _ = TOperation.ProcessSymbol(ref w, isLast3OrGolden, this.singleReference[context][2]); if (isLast3OrGolden) { context = Av1SymbolContextHelper.GetSingleReferenceGoldenContext(referenceCounts); - w.WriteSymbol( + _ = TOperation.ProcessSymbol( + ref w, referenceFrame == Av1ReferenceFrameType.Golden, this.singleReference[context][4]); @@ -1995,7 +2366,8 @@ internal sealed class Av1SymbolEncoder : IDisposable } context = Av1SymbolContextHelper.GetSingleReferenceLast2Context(referenceCounts); - w.WriteSymbol( + _ = TOperation.ProcessSymbol( + ref w, referenceFrame == Av1ReferenceFrameType.Last2, this.singleReference[context][3]); } @@ -2017,9 +2389,15 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The signed angle delta offset by . /// The directional prediction mode selecting the distribution. public void WriteAngleDelta(int angleDelta, Av1PredictionMode context) + => this.WriteAngleDelta(angleDelta, context); + + /// + /// The operation applied to each symbol and literal. + public void WriteAngleDelta(int angleDelta, Av1PredictionMode context) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(angleDelta, this.angleDelta[context - Av1PredictionMode.Vertical]); + _ = TOperation.ProcessSymbol(ref w, angleDelta, this.angleDelta[context - Av1PredictionMode.Vertical]); } /// @@ -2028,9 +2406,18 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The CDEF strength index. /// The number of signaled bits. public void WriteCdefStrength(int cdefStrength, int bitCount) + => this.WriteCdefStrength(cdefStrength, bitCount); + + /// + /// The operation applied to each symbol and literal. + public void WriteCdefStrength(int cdefStrength, int bitCount) + where TOperation : struct, ISymbolOperation { - ref Av1SymbolWriter w = ref this.writer; - w.WriteLiteral((uint)cdefStrength, bitCount); + if (TOperation.WritesOutput) + { + ref Av1SymbolWriter w = ref this.writer; + _ = TOperation.ProcessLiteral(ref w, (uint)cdefStrength, bitCount); + } } /// @@ -2081,10 +2468,16 @@ internal sealed class Av1SymbolEncoder : IDisposable /// Indicates whether chroma-from-luma is valid for the block. /// The block's luma prediction mode. public void WriteChromaMode(Av1ChromaPredictionMode chromaMode, bool isChromaFromLumaAllowed, Av1PredictionMode lumaMode) + => this.WriteChromaMode(chromaMode, isChromaFromLumaAllowed, lumaMode); + + /// + /// The operation applied to each symbol and literal. + public void WriteChromaMode(Av1ChromaPredictionMode chromaMode, bool isChromaFromLumaAllowed, Av1PredictionMode lumaMode) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; int cflAllowed = isChromaFromLumaAllowed ? 1 : 0; - w.WriteSymbol((int)chromaMode, this.uvMode[cflAllowed][(int)lumaMode]); + _ = TOperation.ProcessSymbol(ref w, (int)chromaMode, this.uvMode[cflAllowed][(int)lumaMode]); } /// @@ -2093,9 +2486,15 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The packed U/V alpha-magnitude indices. /// The joint U/V sign symbol. public void WriteChromaFromLumaAlphas(int chromaFromLumaIndex, int joinedSign) + => this.WriteChromaFromLumaAlphas(chromaFromLumaIndex, joinedSign); + + /// + /// The operation applied to each symbol and literal. + public void WriteChromaFromLumaAlphas(int chromaFromLumaIndex, int joinedSign) + where TOperation : struct, ISymbolOperation { ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(joinedSign, this.chromaFromLumaSign); + _ = TOperation.ProcessSymbol(ref w, joinedSign, this.chromaFromLumaSign); // Magnitudes are only signaled for nonzero signs; the shared helper keeps encoder and decoder mappings exact. int signU = Av1ChromaFromLumaMath.SignU(joinedSign); @@ -2103,7 +2502,7 @@ internal sealed class Av1SymbolEncoder : IDisposable { int contextU = Av1ChromaFromLumaMath.ContextU(joinedSign); int indexU = Av1ChromaFromLumaMath.IndexU(chromaFromLumaIndex); - w.WriteSymbol(indexU, this.chromaFromLumaAlpha[contextU]); + _ = TOperation.ProcessSymbol(ref w, indexU, this.chromaFromLumaAlpha[contextU]); } int signV = Av1ChromaFromLumaMath.SignV(joinedSign); @@ -2111,7 +2510,7 @@ internal sealed class Av1SymbolEncoder : IDisposable { int contextV = Av1ChromaFromLumaMath.ContextV(joinedSign); int indexV = Av1ChromaFromLumaMath.IndexV(chromaFromLumaIndex); - w.WriteSymbol(indexV, this.chromaFromLumaAlpha[contextV]); + _ = TOperation.ProcessSymbol(ref w, indexV, this.chromaFromLumaAlpha[contextV]); } } @@ -2125,16 +2524,24 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The number of coded map columns. /// The complete row-addressable color-index map. /// The rate cost in 1/512-bit units, or zero while writing. + /// The token destination for retaining operations; otherwise an empty span. private int ProcessPaletteColorMap( int paletteSize, Av1PlaneType planeType, int rows, int columns, - Buffer2DRegion colorIndexMap) + Buffer2DRegion colorIndexMap, + Span tokens) where TOperation : struct, IPaletteColorMapOperation { int colorIndex = colorIndexMap.DangerousGetRowSpan(0)[0]; int cost = TOperation.ProcessFirstIndex(this, paletteSize, colorIndex); + if (TOperation.RetainsTokens) + { + tokens[0] = (byte)colorIndex; + } + + int tokenIndex = 1; Span colorOrder = stackalloc byte[Av1Constants.PaletteMaxSize]; for (int diagonal = 1; diagonal < rows + columns - 1; diagonal++) { @@ -2153,6 +2560,13 @@ internal sealed class Av1SymbolEncoder : IDisposable colorOrder, out int colorOrderIndex); + if (TOperation.RetainsTokens) + { + // Three low bits retain the color rank; the upper nibble retains its spatial context. + // Packing later reads this byte without consulting a reused prediction map. + tokens[tokenIndex++] = (byte)((colorContext << 4) | colorOrderIndex); + } + cost += TOperation.ProcessColorIndex( this, paletteSize, @@ -2264,17 +2678,18 @@ internal sealed class Av1SymbolEncoder : IDisposable /// The sorted colors. /// The number of bits in each color sample. /// The minimum representable difference between adjacent colors. - private void WriteDeltaEncodedColors( + private void WriteDeltaEncodedColors( ReadOnlySpan colors, int bitDepth, int minimumDelta) + where TOperation : struct, ISymbolOperation { if (colors.IsEmpty) { return; } - this.WriteLiteral(colors[0], bitDepth); + this.WriteLiteral(colors[0], bitDepth); if (colors.Length == 1) { return; @@ -2291,12 +2706,12 @@ internal sealed class Av1SymbolEncoder : IDisposable (int)Av1Math.CeilLog2((uint)(maximumDelta + 1 - minimumDelta)), minimumBits); - this.WriteLiteral((uint)(bits - minimumBits), 2); + this.WriteLiteral((uint)(bits - minimumBits), 2); int range = (1 << bitDepth) - colors[0] - minimumDelta; for (int i = 1; i < colors.Length; i++) { int delta = colors[i] - colors[i - 1]; - this.WriteLiteral((uint)(delta - minimumDelta), bits); + this.WriteLiteral((uint)(delta - minimumDelta), bits); range -= delta; bits = Math.Min(bits, (int)Av1Math.CeilLog2((uint)range)); } @@ -2335,10 +2750,14 @@ internal sealed class Av1SymbolEncoder : IDisposable } /// - /// Emits coefficient syntax and reports no estimated rate. + /// Emits symbols and literals and reports no estimated rate. /// - private readonly struct CoefficientWriteOperation : ICoefficientSymbolOperation + public readonly struct SymbolWriteOperation : ISymbolOperation { + /// + public static bool WritesOutput => true; + + /// public static int ProcessSymbol( ref Av1SymbolWriter writer, int symbol, @@ -2348,6 +2767,18 @@ internal sealed class Av1SymbolEncoder : IDisposable return 0; } + /// + public static int ProcessSymbol(ref Av1SymbolWriter writer, bool symbol, Av1Distribution distribution) + => ProcessSymbol(ref writer, symbol ? 1 : 0, distribution); + + /// + public static int ProcessBoolean(ref Av1SymbolWriter writer, bool value, uint frequency) + { + writer.WriteBoolean(value, frequency); + return 0; + } + + /// public static int ProcessLiteral( ref Av1SymbolWriter writer, uint value, @@ -2358,17 +2789,56 @@ internal sealed class Av1SymbolEncoder : IDisposable } } + /// + /// Updates adaptive probabilities without emitting symbols or literals. + /// + public readonly struct SymbolUpdateOperation : ISymbolOperation + { + /// + public static bool WritesOutput => false; + + /// + public static int ProcessSymbol( + ref Av1SymbolWriter writer, + int symbol, + Av1Distribution distribution) + { + writer.UpdateSymbol(symbol, distribution); + return 0; + } + + /// + public static int ProcessSymbol(ref Av1SymbolWriter writer, bool symbol, Av1Distribution distribution) + => ProcessSymbol(ref writer, symbol ? 1 : 0, distribution); + + /// + public static int ProcessBoolean(ref Av1SymbolWriter writer, bool value, uint frequency) + => 0; + + /// + public static int ProcessLiteral(ref Av1SymbolWriter writer, uint value, int bitCount) + => 0; + } + /// /// Measures coefficient syntax against the live tile distributions without changing them. /// - private readonly struct CoefficientCostOperation : ICoefficientSymbolOperation + private readonly struct CoefficientCostOperation : ISymbolOperation { + public static bool WritesOutput => false; + public static int ProcessSymbol( ref Av1SymbolWriter writer, int symbol, Av1Distribution distribution) => Av1ProbabilityCost.GetSymbolCost(distribution, symbol); + public static int ProcessSymbol(ref Av1SymbolWriter writer, bool symbol, Av1Distribution distribution) + => ProcessSymbol(ref writer, symbol ? 1 : 0, distribution); + + public static int ProcessBoolean(ref Av1SymbolWriter writer, bool value, uint frequency) + => Av1ProbabilityCost.GetSymbolCost((int)(value ? frequency : Av1Distribution.ProbabilityTop - frequency)); + public static int ProcessLiteral( ref Av1SymbolWriter writer, uint value, @@ -2379,14 +2849,17 @@ internal sealed class Av1SymbolEncoder : IDisposable /// /// Emits palette-map syntax and reports no estimated rate. /// - private readonly struct PaletteColorMapWriteOperation : IPaletteColorMapOperation + private readonly struct PaletteColorMapWriteOperation : IPaletteColorMapOperation + where TOperation : struct, ISymbolOperation { + public static bool RetainsTokens => false; + public static int ProcessFirstIndex( Av1SymbolEncoder encoder, int paletteSize, int colorIndex) { - encoder.WriteUniform(paletteSize, colorIndex); + encoder.WriteUniform(paletteSize, colorIndex); return 0; } @@ -2397,7 +2870,7 @@ internal sealed class Av1SymbolEncoder : IDisposable int colorContext, int colorOrderIndex) { - encoder.WritePaletteColorIndex( + encoder.WritePaletteColorIndex( colorOrderIndex, paletteSize, colorContext, @@ -2407,11 +2880,35 @@ internal sealed class Av1SymbolEncoder : IDisposable } } + /// + /// Retains color tokens while adapting the selected palette distributions. + /// + private readonly struct PaletteColorMapTokenOperation : IPaletteColorMapOperation + { + public static bool RetainsTokens => true; + + public static int ProcessFirstIndex(Av1SymbolEncoder encoder, int paletteSize, int colorIndex) + => 0; + + public static int ProcessColorIndex( + Av1SymbolEncoder encoder, + int paletteSize, + Av1PlaneType planeType, + int colorContext, + int colorOrderIndex) + { + encoder.WritePaletteColorIndex(colorOrderIndex, paletteSize, colorContext, planeType); + return 0; + } + } + /// /// Measures palette-map syntax against the live tile distributions without changing them. /// private readonly struct PaletteColorMapCostOperation : IPaletteColorMapOperation { + public static bool RetainsTokens => false; + public static int ProcessFirstIndex( Av1SymbolEncoder encoder, int paletteSize, diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs index 6b26d84d2e..bb35f564d1 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs @@ -128,8 +128,16 @@ internal sealed class Av1SymbolWriter : IDisposable DebugGuard.IsTrue(distribution[distribution.NumberOfSymbols - 1] == 0, "Last entry in Probabilities table needs to be zero."); this.EncodeIntegerQ15(symbol, distribution); + this.UpdateSymbol(symbol, distribution); + } - // disable_cdf_update freezes every tile distribution while leaving range encoding unchanged. + /// + /// Adapts a symbol distribution when probability updates are enabled, without emitting range-coded data. + /// + /// The zero-based symbol. + /// The inverse cumulative distribution for the symbol alphabet. + public void UpdateSymbol(int symbol, Av1Distribution distribution) + { if (this.updateCdf) { distribution.Update(symbol); diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs index b9187859a9..fe7710b630 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs @@ -165,8 +165,8 @@ public class Av1EntropyTests [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. + // Adaptation can collapse a symbol's probability interval. A minimum mass of 4 keeps its estimated + // cost finite: 13 bits at 512 rate units per bit, compared with 12 bits for a mass of 8. Av1Distribution distribution = new(16384, 16384 + probability); Assert.Equal(expected, Av1ProbabilityCost.GetSymbolCost(distribution, 1)); Assert.Equal(expected, Av1ProbabilityCost.GetSymbolCost((int)probability)); @@ -1792,6 +1792,63 @@ public class Av1EntropyTests decoder.ValidateTrailingBits(); } + [Theory] + [InlineData(true, (int)Av1PlaneType.Y)] + [InlineData(false, (int)Av1PlaneType.Y)] + [InlineData(true, (int)Av1PlaneType.Uv)] + [InlineData(false, (int)Av1PlaneType.Uv)] + public void PaletteTokensSurviveMapReuseWithoutWritingAnalysisBytes(bool updateCdf, int planeTypeValue) + { + const int Rows = 5; + const int Columns = 7; + const int PaletteSize = 7; + Av1PlaneType planeType = (Av1PlaneType)planeTypeValue; + Configuration configuration = Configuration.Default; + using Buffer2D source = configuration.MemoryAllocator.Allocate2D(9, 6); + Buffer2DRegion region = new(source); + Span tokens = stackalloc byte[Rows * Columns]; + for (int row = 0; row < Rows; row++) + { + Span samples = source.DangerousGetRowSpan(row); + for (int column = 0; column < Columns; column++) + { + samples[column] = (byte)(((row * 3) + (column * 5)) % PaletteSize); + } + } + + using Av1SymbolEncoder analysis = new(configuration, 128, BaseQIndex, updateCdf); + using Av1SymbolEncoder immediate = new(configuration, 128, BaseQIndex, updateCdf); + using Av1SymbolEncoder packing = new(configuration, 128, BaseQIndex, updateCdf); + using Av1SymbolEncoder empty = new(configuration, 128, BaseQIndex, updateCdf); + analysis.TokenizePaletteColorMap(PaletteSize, planeType, Rows, Columns, region, tokens); + immediate.WritePaletteColorMap(PaletteSize, planeType, Rows, Columns, region); + + for (int context = 0; context < 5; context++) + { + for (int color = 0; color < PaletteSize; color++) + { + Assert.Equal( + immediate.GetPaletteColorIndexCost(color, PaletteSize, context, planeType), + analysis.GetPaletteColorIndexCost(color, PaletteSize, context, planeType)); + } + } + + // The prediction workspace is reusable immediately after analysis. Poison every row, including its + // padding, so final packing must consume retained tokens rather than consult the old color map. + for (int row = 0; row < source.Height; row++) + { + source.DangerousGetRowSpan(row).Fill(byte.MaxValue); + } + + packing.WritePaletteTokens(PaletteSize, planeType, tokens); + using IMemoryOwner analyzedBytes = analysis.Exit(); + using IMemoryOwner emptyBytes = empty.Exit(); + using IMemoryOwner immediateBytes = immediate.Exit(); + using IMemoryOwner packedBytes = packing.Exit(); + Assert.True(emptyBytes.GetSpan().SequenceEqual(analyzedBytes.GetSpan())); + Assert.True(immediateBytes.GetSpan().SequenceEqual(packedBytes.GetSpan())); + } + [Fact] public void PaletteColorMapCostDoesNotAllocateAfterEntropyInitialization() {