From ca8898ef5617220d01bebb0f75ed9a698efbd13a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 2 Sep 2026 22:11:24 +1000 Subject: [PATCH] Add optimized AV1 coefficient rate costs --- HEIF_IMPLEMENTATION_PLAN.md | 2 +- .../Heif/Av1/Entropy/Av1SymbolEncoder.cs | 483 +++++++++++++++--- .../Formats/Heif/Av1/Tiling/Av1LevelBuffer.cs | 29 +- .../Formats/Heif/Av1/Av1EntropyTests.cs | 374 ++++++++++++++ 4 files changed, 817 insertions(+), 71 deletions(-) diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 17011a480a..6a48feeabe 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -828,7 +828,7 @@ Encoder verification contract: - [ ] Implement inter mode search for bounded sequences, including reference selection and the decoder-supported inter tools. - [~] Current-libaom `av1_quantize_fp_no_qmatrix` arithmetic is implemented as a closed generic forward-quantizer family with Vector512, Vector256, Vector128, and scalar paths, raster-order output, coded 64-point coefficient limits, and scan-order EOB selection. Transform search, coefficient optimization, and lossless behavior remain. - [ ] Implement real rate-distortion selection and make quality and effort change work, size, and output quality. -- [~] Encoder rate accounting now converts the entropy writer's live inverse cumulative distributions into current-libaom fixed-point symbol costs without allocating or duplicating probability state. Read-only luma-mode, directional-delta, filter-intra, chroma-mode, block-skip, transform-size, and transform-block-skip queries share the exact distributions mutated by the subsequent entropy write, and a regression proves the reported luma cost follows that adaptation. Transform-size writing and costing share one subdivision-depth calculation. The current-libaom fixed-point RD combiner preserves 64-bit distortion and rounds the weighted 1/512-bit rate at the required boundary; the quality model still needs to select its multiplier. The complete normalized probability table, Q15 boundaries, multi-symbol interval arithmetic, literal-bit precision, zero-allocation hot path, syntax-query parity, live-update behavior, transform-depth parity, and RD arithmetic pass 22 of 22 focused direct net11 Release VSTest cases; all 1,904 `Av1EntropyTests` cases pass. Coefficient and complete candidate accounting still need to compose these costs into real rate-distortion selection. +- [~] Encoder rate accounting now converts the entropy writer's live inverse cumulative distributions into current-libaom fixed-point symbol costs without allocating or duplicating probability state. Read-only luma-mode, directional-delta, filter-intra, chroma-mode, block-skip, transform-size, transform-block-skip, and complete transform-coefficient queries share the exact distributions mutated by the subsequent entropy write. Complete coefficient costing follows current libaom's optimized shape: it returns immediately for an empty transform, uses the EOB-specific base-range context, fuses magnitude, sign, base-range, and Golomb accounting into one reverse traversal, and combines repeated full base-range chunks instead of replaying each emitted symbol. Tile-lifetime level and context scratch is reused, the one-coefficient path neither clears nor initializes the forward-neighbor level map, and steady-state queries allocate nothing. Transform-size writing and costing share one subdivision-depth calculation, while shared closed symbol operations keep the writer and cost mappings for transform skip, transform type, and EOB syntax identical without forcing the estimator through the writer's slower two-pass coefficient traversal. The current-libaom fixed-point RD combiner preserves 64-bit distortion and rounds the weighted 1/512-bit rate at the required boundary; the quality model still needs to select its multiplier. Exact empty, EOB-only, adaptive-state, complete nonzero-map, base-range, sign, Golomb, allocation, probability, syntax-query, transform-depth, and RD cases pass 27 of 27 focused direct net11 Release VSTest cases; all 1,909 `Av1EntropyTests`, all 84 `Av1CoefficientsEntropyTests`, and all 8,890 complete HEIF/AV1 namespace cases pass. Complete candidate accounting still needs to combine prediction mode rate, coefficient rate, distortion, and the quality-derived multiplier into real selection. - [~] The tile writer now publishes one packed coefficient context per covered 4x4 edge unit and derives luma/chroma skip plus DC-sign contexts from the complete transform edges using current-libaom units. Partition, transform, and coefficient neighbor state retains only the above and left context regions used by current libaom; the unused third top-left region, its granularity state, and its unused sentinel are removed. One picture owner now packs segmentation plus every tile's partition, luma, chroma, and transform edges into one clean byte allocation with typed non-owning views; together with the separately typed packed mode-information owner, the complete picture state uses two allocator rents rather than seven. Exact aligned lengths, clean initialization, and balanced exactly-once returns are covered in Release. Multi-tile payload ownership and verified CDF update behavior remain. - [~] Encoder mode information now uses a frame-owned integer alias grid over a packed 8-byte value allocation, matching current libaom's `mi_grid_base` and `mi_alloc` relationship without a managed object or reference per 4x4 entry. The visible dimensions are aligned to eight luma samples, the grid stride and allocated row count are aligned to 32 mode-information units, and optional 8x8 allocation granularity reduces the value store in both dimensions exactly as current libaom does. One clean ImageSharp byte owner contains both independently typed regions, reducing libaom's two allocation lifetimes to one without a copy. At 4K, the 4x4 layout occupies about 6.0 MiB in total; the 8x8 layout occupies about 3.0 MiB. Exact geometry, clean allocation, typed lengths, aligned mapping, untouched row padding, and exactly-once return pass 4 of 4 direct net11 VSTest cases in Release. Every coded 4x4 cell covered by square, rectangular, or clipped edge blocks maps to its owning allocation entry before context-dependent symbols are written. Packed syntax, relative neighbor lookup, full block mapping, writer traversal, entropy, and OBU coverage pass 1,947 of 1,947 direct net11 VSTest cases in Release; complete mode decision still remains. - [~] The final-block decision workspace uses one reusable 10.3 KiB ImageSharp allocator owner. It contains 1,024 explicitly packed 10-byte final-block entries and the 341 preorder partition bytes required by a complete 128x128-through-8x8 quadtree, replacing separate managed arrays. Construction and the explicit per-superblock reset initialize every syntax field, including the nonzero sentinel that disables filter-intra prediction; pooled palette, quantizer, prediction, and partition bytes cannot leak into the next decision pass. Exact allocation, size, initialization, reset, return, repeated-run, writer, entropy, and OBU coverage pass 1,957 of 1,957 direct net11 VSTest cases in Release; complete mode decision still remains. diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs index 9fa2584b93..8dcce60c21 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs @@ -205,6 +205,36 @@ internal class Av1SymbolEncoder : IDisposable this.baseQIndex = qIndex; } + /// + /// Defines how shared coefficient-syntax helpers handle one adaptive symbol or literal bit field. + /// + private interface ICoefficientSymbolOperation + { + /// + /// Handles one symbol from an adaptive distribution. + /// + /// The tile range writer. + /// The zero-based symbol. + /// The symbol distribution. + /// The symbol's rate contribution. + public static abstract int ProcessSymbol( + ref Av1SymbolWriter writer, + int symbol, + Av1Distribution distribution); + + /// + /// Handles one most-significant-bit-first literal field. + /// + /// The tile range writer. + /// The low-order literal bits. + /// The number of bits. + /// The literal's rate contribution. + public static abstract int ProcessLiteral( + ref Av1SymbolWriter writer, + uint value, + int bitCount); + } + /// /// Writes the frame-local intra-block-copy flag. /// @@ -279,66 +309,79 @@ internal class Av1SymbolEncoder : IDisposable Av1TransformSize transformSize, Av1TransformType transformType, Av1PredictionMode intraDirection, - Span coefficientBuffer, + ReadOnlySpan coefficientBuffer, Av1ComponentType componentType, Av1TransformBlockContext transformBlockContext, ushort endOfBlock, bool useReducedTransformSet, Av1FilterIntraMode filterIntraMode) { - int c; - Av1TransformSize adjustedTransformSize = transformSize.GetAdjusted(); - int width = adjustedTransformSize.GetWidth(); - int height = adjustedTransformSize.GetHeight(); - Av1TransformClass transformClass = transformType.ToClass(); - Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType); - ReadOnlySpan scan = scanOrder.Scan; Av1TransformSize transformSizeContext = Av1SymbolContextHelper.GetTransformSizeContext(transformSize); - Guard.MustBeLessThan((int)transformSizeContext, (int)Av1TransformSize.AllSizes, nameof(transformSizeContext)); + DebugGuard.MustBeLessThan((int)transformSizeContext, (int)Av1TransformSize.AllSizes, nameof(transformSizeContext)); - this.WriteTransformBlockSkip(endOfBlock == 0, transformSizeContext, transformBlockContext.SkipContext); + _ = this.ProcessTransformBlockSkip( + endOfBlock == 0, + transformSizeContext, + transformBlockContext.SkipContext); if (endOfBlock == 0) { return 0; } - ref Av1SymbolWriter w = ref this.writer; - Av1LevelBuffer levels = this.levels ??= new(this.configuration); - IMemoryOwner coefficientContextOwner = this.coefficientContexts ??= - this.configuration.MemoryAllocator.Allocate(MaximumCoefficientContextCount); + Av1TransformSize adjustedTransformSize = transformSize.GetAdjusted(); + int width = adjustedTransformSize.GetWidth(); + int height = adjustedTransformSize.GetHeight(); + Av1TransformClass transformClass = transformType.ToClass(); + ReadOnlySpan scan = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType).Scan; + Av1LevelBuffer levels = this.PrepareCoefficientScratch( + width, + height, + clearLevels: true, + out Span coefficientContexts); - // AV1 omits high-frequency coefficients beyond 32 samples on every 64-point transform dimension. The tile - // creates maximum-sized workspaces only when nonzero coefficient syntax needs them, then changes only their active views. - levels.Reset(new Size(width, height)); - Span coefficientContexts = coefficientContextOwner.Memory.Span[..(width * height)]; - coefficientContexts.Clear(); levels.Initialize(coefficientBuffer); if (componentType == Av1ComponentType.Luminance) { - this.WriteTransformType(transformType, transformSize, useReducedTransformSet, this.baseQIndex, filterIntraMode, intraDirection); + _ = this.ProcessTransformType( + transformType, + transformSize, + useReducedTransformSet, + this.baseQIndex, + filterIntraMode, + intraDirection); } - this.WriteEndOfBlockPosition(endOfBlock, componentType, transformClass, transformSize, transformSizeContext); + _ = this.ProcessEndOfBlockPosition( + endOfBlock, + componentType, + transformClass, + transformSize, + transformSizeContext); Av1SymbolContextHelper.GetNzMapContexts(levels, scan, endOfBlock, transformSize, transformClass, coefficientContexts); int limitedTransformSizeContext = Math.Min((int)transformSizeContext, (int)Av1TransformSize.Size32x32); - for (c = endOfBlock - 1; c >= 0; --c) + ref Av1SymbolWriter w = ref this.writer; + for (int c = endOfBlock - 1; c >= 0; --c) { short pos = scan[c]; - int v = coefficientBuffer[pos]; - short coeffContext = coefficientContexts[pos]; + int value = coefficientBuffer[pos]; + short coefficientContext = coefficientContexts[pos]; Point position = levels.GetPosition(pos); - int level = Math.Abs(v); + int level = Math.Abs(value); if (c == endOfBlock - 1) { - w.WriteSymbol(Math.Min(level, 3) - 1, this.coefficientsBaseEndOfBlock[(int)transformSizeContext][(int)componentType][coeffContext]); + w.WriteSymbol( + Math.Min(level, 3) - 1, + this.coefficientsBaseEndOfBlock[(int)transformSizeContext][(int)componentType][coefficientContext]); } else { - w.WriteSymbol(Math.Min(level, 3), this.coefficientsBase[(int)transformSizeContext][(int)componentType][coeffContext]); + w.WriteSymbol( + Math.Min(level, 3), + this.coefficientsBase[(int)transformSizeContext][(int)componentType][coefficientContext]); } if (level > Av1Constants.BaseLevelsCount) @@ -348,9 +391,12 @@ internal class Av1SymbolEncoder : IDisposable int baseRangeContext = Av1SymbolContextHelper.GetBaseRangeContext(levels, position, transformClass); for (int idx = 0; idx < Av1Constants.CoefficientBaseRange; idx += Av1Constants.BaseRangeSizeMinus1) { - int k = Math.Min(baseRange - idx, Av1Constants.BaseRangeSizeMinus1); - w.WriteSymbol(k, this.coefficientsBaseRange[limitedTransformSizeContext][(int)componentType][baseRangeContext]); - if (k < Av1Constants.BaseRangeSizeMinus1) + int symbol = Math.Min(baseRange - idx, Av1Constants.BaseRangeSizeMinus1); + w.WriteSymbol( + symbol, + this.coefficientsBaseRange[limitedTransformSizeContext][(int)componentType][baseRangeContext]); + + if (symbol < Av1Constants.BaseRangeSizeMinus1) { break; } @@ -360,19 +406,21 @@ internal class Av1SymbolEncoder : IDisposable // Signs follow every magnitude so the DC sign can use its neighboring context and AC signs remain literals. int culLevel = 0; - for (c = 0; c < endOfBlock; ++c) + for (int c = 0; c < endOfBlock; ++c) { short pos = scan[c]; - int v = coefficientBuffer[pos]; - int level = Math.Abs(v); + int value = coefficientBuffer[pos]; + int level = Math.Abs(value); culLevel += level; - uint sign = v < 0 ? 1u : 0u; + uint sign = value < 0 ? 1u : 0u; if (level > 0) { if (c == 0) { - w.WriteSymbol((int)sign, this.dcSign[(int)componentType][transformBlockContext.DcSignContext]); + w.WriteSymbol( + (int)sign, + this.dcSign[(int)componentType][transformBlockContext.DcSignContext]); } else { @@ -381,7 +429,8 @@ internal class Av1SymbolEncoder : IDisposable if (level > (Av1Constants.CoefficientBaseRange + Av1Constants.BaseLevelsCount)) { - this.WriteGolomb(level - Av1Constants.CoefficientBaseRange - 1 - Av1Constants.BaseLevelsCount); + this.WriteGolomb( + level - Av1Constants.CoefficientBaseRange - 1 - Av1Constants.BaseLevelsCount); } } } @@ -393,6 +442,187 @@ internal class Av1SymbolEncoder : IDisposable return culLevel; } + /// + /// Gets the current fixed-point rate cost of one transform block's complete coefficient syntax. + /// + /// The signaled transform size. + /// The transform type selecting the scan and context class. + /// The block's intra prediction mode. + /// The raster-ordered signed coefficient levels. + /// The luma or chroma component category. + /// The neighboring skip and DC sign contexts. + /// The one-based final nonzero scan position, or zero for an empty block. + /// Indicates whether the frame restricts transform choices. + /// The selected filter-intra mode, or the disabled sentinel. + /// The rate cost in 1/512-bit units. + public int GetCoefficientCost( + Av1TransformSize transformSize, + Av1TransformType transformType, + Av1PredictionMode intraDirection, + ReadOnlySpan coefficientBuffer, + Av1ComponentType componentType, + Av1TransformBlockContext transformBlockContext, + ushort endOfBlock, + bool useReducedTransformSet, + Av1FilterIntraMode filterIntraMode) + { + Av1TransformSize transformSizeContext = Av1SymbolContextHelper.GetTransformSizeContext(transformSize); + + DebugGuard.MustBeLessThan((int)transformSizeContext, (int)Av1TransformSize.AllSizes, nameof(transformSizeContext)); + + int rate = this.ProcessTransformBlockSkip( + endOfBlock == 0, + transformSizeContext, + transformBlockContext.SkipContext); + + if (endOfBlock == 0) + { + return rate; + } + + Av1TransformSize adjustedTransformSize = transformSize.GetAdjusted(); + int width = adjustedTransformSize.GetWidth(); + int height = adjustedTransformSize.GetHeight(); + Av1TransformClass transformClass = transformType.ToClass(); + ReadOnlySpan scan = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType).Scan; + bool needsLevelMap = endOfBlock > 1; + Av1LevelBuffer levels = this.PrepareCoefficientScratch( + width, + height, + needsLevelMap, + out Span coefficientContexts); + + // The final coefficient uses scan-position contexts only. Earlier coefficients need the complete + // forward-neighbor level map, so a one-coefficient candidate avoids initializing that plane. + if (needsLevelMap) + { + levels.Initialize(coefficientBuffer); + } + + if (componentType == Av1ComponentType.Luminance) + { + rate += this.ProcessTransformType( + transformType, + transformSize, + useReducedTransformSet, + this.baseQIndex, + filterIntraMode, + intraDirection); + } + + rate += this.ProcessEndOfBlockPosition( + endOfBlock, + componentType, + transformClass, + transformSize, + transformSizeContext); + + Av1SymbolContextHelper.GetNzMapContexts(levels, scan, endOfBlock, transformSize, transformClass, coefficientContexts); + int limitedTransformSizeContext = Math.Min((int)transformSizeContext, (int)Av1TransformSize.Size32x32); + int c = endOfBlock - 1; + int pos = scan[c]; + int value = coefficientBuffer[pos]; + int level = Math.Abs(value); + int coefficientContext = coefficientContexts[pos]; + rate += Av1ProbabilityCost.GetSymbolCost( + this.coefficientsBaseEndOfBlock[(int)transformSizeContext][(int)componentType][coefficientContext], + Math.Min(level, 3) - 1); + + if (level > Av1Constants.BaseLevelsCount) + { + int baseRangeContext = Av1SymbolContextHelper.GetBaseRangeContextEndOfBlock( + levels.GetPosition(pos), + transformClass); + + rate += GetBaseRangeCost( + level, + this.coefficientsBaseRange[limitedTransformSizeContext][(int)componentType][baseRangeContext]); + } + + if (c == 0) + { + return rate + Av1ProbabilityCost.GetSymbolCost( + this.dcSign[(int)componentType][transformBlockContext.DcSignContext], + value < 0 ? 1 : 0); + } + + rate += Av1ProbabilityCost.GetLiteralCost(1); + for (c = endOfBlock - 2; c >= 1; --c) + { + pos = scan[c]; + value = coefficientBuffer[pos]; + level = Math.Abs(value); + coefficientContext = coefficientContexts[pos]; + rate += Av1ProbabilityCost.GetSymbolCost( + this.coefficientsBase[(int)transformSizeContext][(int)componentType][coefficientContext], + Math.Min(level, 3)); + + if (level == 0) + { + continue; + } + + rate += Av1ProbabilityCost.GetLiteralCost(1); + if (level > Av1Constants.BaseLevelsCount) + { + int baseRangeContext = Av1SymbolContextHelper.GetBaseRangeContext( + levels, + levels.GetPosition(pos), + transformClass); + + rate += GetBaseRangeCost( + level, + this.coefficientsBaseRange[limitedTransformSizeContext][(int)componentType][baseRangeContext]); + } + } + + pos = scan[0]; + value = coefficientBuffer[pos]; + level = Math.Abs(value); + coefficientContext = coefficientContexts[pos]; + rate += Av1ProbabilityCost.GetSymbolCost( + this.coefficientsBase[(int)transformSizeContext][(int)componentType][coefficientContext], + Math.Min(level, 3)); + + if (level > 0) + { + rate += Av1ProbabilityCost.GetSymbolCost( + this.dcSign[(int)componentType][transformBlockContext.DcSignContext], + value < 0 ? 1 : 0); + + if (level > Av1Constants.BaseLevelsCount) + { + int baseRangeContext = Av1SymbolContextHelper.GetBaseRangeContext( + levels, + levels.GetPosition(pos), + transformClass); + + rate += GetBaseRangeCost( + level, + this.coefficientsBaseRange[limitedTransformSizeContext][(int)componentType][baseRangeContext]); + } + } + + return rate; + } + + private Av1LevelBuffer PrepareCoefficientScratch( + int width, + int height, + bool clearLevels, + out Span coefficientContexts) + { + Av1LevelBuffer levels = this.levels ??= new(this.configuration); + IMemoryOwner coefficientContextOwner = this.coefficientContexts ??= + this.configuration.MemoryAllocator.Allocate(MaximumCoefficientContextCount); + + // AV1 omits high-frequency coefficients beyond 32 samples on every 64-point transform dimension. The tile + // creates maximum-sized workspaces once, then changes only the active views for subsequent transform blocks. + levels.Reset(new Size(width, height), clearLevels); + coefficientContexts = coefficientContextOwner.Memory.Span[..(width * height)]; + return levels; + } + /// /// Writes an end-of-block token and its context-coded and literal suffix bits. /// @@ -402,9 +632,29 @@ internal 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.ProcessEndOfBlockPosition( + endOfBlock, + componentType, + transformClass, + transformSize, + transformSizeContext); + } + + private int ProcessEndOfBlockPosition( + ushort endOfBlock, + Av1ComponentType componentType, + Av1TransformClass transformClass, + Av1TransformSize transformSize, + Av1TransformSize transformSizeContext) + where TOperation : struct, ICoefficientSymbolOperation { short endOfBlockPosition = Av1SymbolContextHelper.GetEndOfBlockPosition(endOfBlock, out int eobExtra); - this.WriteEndOfBlockFlag(componentType, transformClass, transformSize, endOfBlockPosition); + int rate = this.ProcessEndOfBlockFlag( + componentType, + transformClass, + transformSize, + endOfBlockPosition); int eobOffsetBitCount = Av1SymbolContextHelper.EndOfBlockOffsetBits[endOfBlockPosition]; if (eobOffsetBitCount > 0) @@ -416,14 +666,17 @@ internal class Av1SymbolEncoder : IDisposable // The local table retains placeholders for the first three tokens, unlike the reference decoder's compact table, // so the encoded token is also the distribution index. int endOfBlockContext = endOfBlockPosition; - w.WriteSymbol(bit, this.endOfBlockExtra[(int)transformSizeContext][(int)componentType][endOfBlockContext]); - for (int i = 1; i < eobOffsetBitCount; i++) - { - eobShift = eobOffsetBitCount - 1 - i; - bit = Av1Math.GetBit(eobExtra, eobShift); - w.WriteLiteral((uint)bit, 1); - } + rate += TOperation.ProcessSymbol( + ref w, + bit, + this.endOfBlockExtra[(int)transformSizeContext][(int)componentType][endOfBlockContext]); + + // The context-coded high bit has already been consumed. The literal writer emits the remaining + // low-order suffix most-significant-bit first, preserving the AV1 syntax with one traversal call. + rate += TOperation.ProcessLiteral(ref w, (uint)eobExtra, eobOffsetBitCount - 1); } + + return rate; } /// @@ -434,9 +687,7 @@ internal class Av1SymbolEncoder : IDisposable /// The context derived from neighboring coefficient blocks. /// The rate cost in 1/512-bit units. public int GetTransformBlockSkipCost(bool skip, Av1TransformSize transformSizeContext, int skipContext) - => Av1ProbabilityCost.GetSymbolCost( - this.transformBlockSkip[(int)transformSizeContext][skipContext], - skip ? 1 : 0); + => this.ProcessTransformBlockSkip(skip, transformSizeContext, skipContext); /// /// Writes whether a transform block has no coded coefficients. @@ -445,9 +696,21 @@ internal 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.ProcessTransformBlockSkip(skip, transformSizeContext, skipContext); + } + + private int ProcessTransformBlockSkip( + bool skip, + Av1TransformSize transformSizeContext, + int skipContext) + where TOperation : struct, ICoefficientSymbolOperation { ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(skip, this.transformBlockSkip[(int)transformSizeContext][skipContext]); + return TOperation.ProcessSymbol( + ref w, + skip ? 1 : 0, + this.transformBlockSkip[(int)transformSizeContext][skipContext]); } /// @@ -544,22 +807,47 @@ internal class Av1SymbolEncoder : IDisposable public void WriteGolomb(int level) { uint x = (uint)level + 1u; - int length = (int)Av1Math.Log2_32(x) + 1; + int length = GetGolombBitLength(level); + ref Av1SymbolWriter w = ref this.writer; + w.WriteLiteral(0u, length - 1); + w.WriteLiteral(x, length); + } - Guard.MustBeGreaterThan(length, 0, nameof(length)); + private static int GetBaseRangeCost(int level, Av1Distribution distribution) + { + int baseRange = Math.Min( + level - 1 - Av1Constants.BaseLevelsCount, + Av1Constants.CoefficientBaseRange); - ref Av1SymbolWriter w = ref this.writer; - for (int i = 0; i < length - 1; ++i) + int fullChunkCount = baseRange / Av1Constants.BaseRangeSizeMinus1; + int rate = 0; + if (fullChunkCount > 0) + { + rate = fullChunkCount * Av1ProbabilityCost.GetSymbolCost( + distribution, + Av1Constants.BaseRangeSizeMinus1); + } + + // A partial range ends with its remainder symbol. Reaching the complete base range consumes four + // maximum symbols and has no terminating remainder before the Golomb escape. + if (baseRange < Av1Constants.CoefficientBaseRange) { - w.WriteLiteral(0u, 1); + int remainder = baseRange - (fullChunkCount * Av1Constants.BaseRangeSizeMinus1); + rate += Av1ProbabilityCost.GetSymbolCost(distribution, remainder); } - for (int j = length - 1; j >= 0; --j) + if (level > (Av1Constants.CoefficientBaseRange + Av1Constants.BaseLevelsCount)) { - w.WriteLiteral((x >> j) & 0x01, 1); + int golombValue = level - Av1Constants.CoefficientBaseRange - 1 - Av1Constants.BaseLevelsCount; + int length = GetGolombBitLength(golombValue); + rate += Av1ProbabilityCost.GetLiteralCost((2 * length) - 1); } + + return rate; } + private static int GetGolombBitLength(int level) => (int)Av1Math.Log2_32((uint)level + 1u) + 1; + /// /// Writes the end-of-block token for a transform coefficient-count category. /// @@ -567,12 +855,20 @@ internal class Av1SymbolEncoder : IDisposable /// The transform direction class. /// The signaled transform size. /// The one-based end-of-block token. - private void WriteEndOfBlockFlag(Av1ComponentType componentType, Av1TransformClass transformClass, Av1TransformSize transformSize, int endOfBlockPosition) + private int ProcessEndOfBlockFlag( + Av1ComponentType componentType, + Av1TransformClass transformClass, + Av1TransformSize transformSize, + int endOfBlockPosition) + where TOperation : struct, ICoefficientSymbolOperation { int endOfBlockMultiSize = transformSize.GetLog2Minus4(); int endOfBlockContext = transformClass == Av1TransformClass.Class2D ? 0 : 1; ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(endOfBlockPosition - 1, this.endOfBlockFlag[endOfBlockMultiSize][(int)componentType][endOfBlockContext]); + return TOperation.ProcessSymbol( + ref w, + endOfBlockPosition - 1, + this.endOfBlockFlag[endOfBlockMultiSize][(int)componentType][endOfBlockContext]); } /// @@ -591,18 +887,36 @@ internal class Av1SymbolEncoder : IDisposable int baseQIndex, Av1FilterIntraMode filterIntraMode, Av1PredictionMode intraDirection) + { + _ = this.ProcessTransformType( + transformType, + transformSize, + useReducedTransformSet, + baseQIndex, + filterIntraMode, + intraDirection); + } + + private int ProcessTransformType( + Av1TransformType transformType, + Av1TransformSize transformSize, + bool useReducedTransformSet, + int baseQIndex, + Av1FilterIntraMode filterIntraMode, + Av1PredictionMode intraDirection) + where TOperation : struct, ICoefficientSymbolOperation { // Still-image encoding reaches this path only for intra blocks, so the intra transform set is authoritative. Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType(transformSize, useReducedTransformSet); if (Av1SymbolContextHelper.GetExtendedTransformTypeCount(transformSetType) > 1 && baseQIndex > 0) { Av1TransformSize squareTransformSize = transformSize.GetSquareSize(); - Guard.MustBeLessThanOrEqualTo((int)squareTransformSize, Av1Constants.ExtendedTransformCount, nameof(squareTransformSize)); + DebugGuard.MustBeLessThanOrEqualTo((int)squareTransformSize, Av1Constants.ExtendedTransformCount, nameof(squareTransformSize)); int extendedSet = Av1SymbolContextHelper.GetExtendedTransformSet(transformSetType); // Set zero contains only DCT-DCT, which was excluded by the multiple-choice condition above. - Guard.MustBeGreaterThan(extendedSet, 0, nameof(extendedSet)); + DebugGuard.MustBeGreaterThan(extendedSet, 0, nameof(extendedSet)); Av1PredictionMode intraDirectionContext; if (filterIntraMode != Av1FilterIntraMode.AllFilterIntraModes) @@ -614,13 +928,16 @@ internal class Av1SymbolEncoder : IDisposable intraDirectionContext = intraDirection; } - Guard.MustBeLessThan((int)intraDirectionContext, 13, nameof(intraDirectionContext)); - Guard.MustBeLessThan((int)squareTransformSize, 4, nameof(squareTransformSize)); + DebugGuard.MustBeLessThan((int)intraDirectionContext, 13, nameof(intraDirectionContext)); + DebugGuard.MustBeLessThan((int)squareTransformSize, 4, nameof(squareTransformSize)); ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol( + return TOperation.ProcessSymbol( + ref w, Av1SymbolContextHelper.GetExtendedTransformIndex(transformSetType, transformType), this.intraExtendedTransform[extendedSet][(int)squareTransformSize][(int)intraDirectionContext]); } + + return 0; } /// @@ -837,4 +1154,46 @@ internal class Av1SymbolEncoder : IDisposable w.WriteSymbol(indexV, this.chromaFromLumaAlpha[contextV]); } } + + /// + /// Emits coefficient syntax and reports no estimated rate. + /// + private readonly struct CoefficientWriteOperation : ICoefficientSymbolOperation + { + public static int ProcessSymbol( + ref Av1SymbolWriter writer, + int symbol, + Av1Distribution distribution) + { + writer.WriteSymbol(symbol, distribution); + return 0; + } + + public static int ProcessLiteral( + ref Av1SymbolWriter writer, + uint value, + int bitCount) + { + writer.WriteLiteral(value, bitCount); + return 0; + } + } + + /// + /// Measures coefficient syntax against the live tile distributions without changing them. + /// + private readonly struct CoefficientCostOperation : ICoefficientSymbolOperation + { + public static int ProcessSymbol( + ref Av1SymbolWriter writer, + int symbol, + Av1Distribution distribution) + => Av1ProbabilityCost.GetSymbolCost(distribution, symbol); + + public static int ProcessLiteral( + ref Av1SymbolWriter writer, + uint value, + int bitCount) + => Av1ProbabilityCost.GetLiteralCost(bitCount); + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LevelBuffer.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LevelBuffer.cs index f9abba8727..63ddd315db 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LevelBuffer.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LevelBuffer.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System; using System.Buffers; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; @@ -66,14 +66,17 @@ internal sealed class Av1LevelBuffer : IDisposable /// Initializes the unpadded level plane from raster-ordered coefficient magnitudes. /// /// The coefficient levels to copy. - public void Initialize(Span coefficientBuffer) + public void Initialize(ReadOnlySpan coefficientBuffer) { ObjectDisposedException.ThrowIf(this.memory == null, this); ArgumentOutOfRangeException.ThrowIfLessThan(coefficientBuffer.Length, this.Size.Width * this.Size.Height, nameof(coefficientBuffer)); for (int y = 0; y < this.Size.Height; y++) { ref byte destRef = ref this.GetRow(y)[0]; - ref int sourceRef = ref coefficientBuffer[y * this.Size.Width]; + ref int sourceRef = ref Unsafe.Add( + ref MemoryMarshal.GetReference(coefficientBuffer), + y * this.Size.Width); + for (int x = 0; x < this.Size.Width; x++) { // Entropy contexts use the absolute level, saturated to the signed-byte range used by the @@ -129,16 +132,26 @@ internal sealed class Av1LevelBuffer : IDisposable /// Selects new active coefficient dimensions and clears their padded context storage. /// /// The unpadded coefficient dimensions. - public void Reset(Size size) + public void Reset(Size size) => this.Reset(size, clear: true); + + /// + /// Selects new active coefficient dimensions and optionally clears their padded context storage. + /// + /// The unpadded coefficient dimensions. + /// Indicates whether to clear the active level plane and its context padding. + public void Reset(Size size, bool clear) { ObjectDisposedException.ThrowIf(this.memory == null, this); this.Size = size; this.Stride = Av1Constants.TransformPadHorizontal + size.Width; - // Tile parsing is sequential, so one maximum-sized rent can serve every transform. Clear only the active - // layout because stale neighboring levels would otherwise select the wrong coefficient distributions. - int totalHeight = Av1Constants.TransformPadTop + size.Height + Av1Constants.TransformPadBottom; - this.memory.Memory.Span[..(this.Stride * totalHeight)].Clear(); + if (clear) + { + // Tile parsing is sequential, so one maximum-sized rent can serve every transform. Clear only the active + // layout because stale neighboring levels would otherwise select the wrong coefficient distributions. + int totalHeight = Av1Constants.TransformPadTop + size.Height + Av1Constants.TransformPadBottom; + this.memory.Memory.Span[..(this.Stride * totalHeight)].Clear(); + } } /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs index 8549b7015f..966d78d8df 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs @@ -158,6 +158,367 @@ public class Av1EntropyTests encoder.GetLumaModeCost(Av1PredictionMode.DC, 0, 0)); } + [Fact] + public void CoefficientCostMatchesCurrentLibaomForEmptyAndDcBlocks() + { + const int qIndex = 0; + const Av1TransformSize transformSize = Av1TransformSize.Size4x4; + const Av1ComponentType componentType = Av1ComponentType.Luminance; + Av1TransformBlockContext transformBlockContext = default; + Span coefficients = stackalloc int[16]; + using Av1SymbolEncoder encoder = new(Configuration.Default, 64, qIndex, updateCdf: false); + Av1TransformSize transformSizeContext = Av1SymbolContextHelper.GetTransformSizeContext(transformSize); + Av1Distribution transformSkip = Av1DefaultDistributions + .GetTransformBlockSkip(qIndex)[(int)transformSizeContext][transformBlockContext.SkipContext]; + + int emptyCost = encoder.GetCoefficientCost( + transformSize, + Av1TransformType.DctDct, + Av1PredictionMode.DC, + coefficients, + componentType, + transformBlockContext, + 0, + false, + Av1FilterIntraMode.AllFilterIntraModes); + + Assert.Equal(Av1ProbabilityCost.GetSymbolCost(transformSkip, 1), emptyCost); + + // Prime every active level with nonzero data so the one-coefficient path proves its EOB-only + // context derivation does not depend on clearing or rebuilding the forward-neighbor map. + coefficients.Fill(7); + _ = encoder.GetCoefficientCost( + transformSize, + Av1TransformType.DctDct, + Av1PredictionMode.DC, + coefficients, + componentType, + transformBlockContext, + 16, + false, + Av1FilterIntraMode.AllFilterIntraModes); + + coefficients.Clear(); + coefficients[0] = 1; + Av1Distribution endOfBlock = Av1DefaultDistributions + .GetEndOfBlockFlag(qIndex)[transformSize.GetLog2Minus4()][(int)componentType][0]; + Av1Distribution coefficientBaseEnd = Av1DefaultDistributions + .GetBaseEndOfBlock(qIndex)[(int)transformSizeContext][(int)componentType][0]; + Av1Distribution dcSign = Av1DefaultDistributions + .GetDcSign(qIndex)[(int)componentType][transformBlockContext.DcSignContext]; + int expectedDcCost = + Av1ProbabilityCost.GetSymbolCost(transformSkip, 0) + + Av1ProbabilityCost.GetSymbolCost(endOfBlock, 0) + + Av1ProbabilityCost.GetSymbolCost(coefficientBaseEnd, 0) + + Av1ProbabilityCost.GetSymbolCost(dcSign, 0); + + int dcCost = encoder.GetCoefficientCost( + transformSize, + Av1TransformType.DctDct, + Av1PredictionMode.DC, + coefficients, + componentType, + transformBlockContext, + 1, + false, + Av1FilterIntraMode.AllFilterIntraModes); + + Assert.Equal(expectedDcCost, dcCost); + } + + [Fact] + public void CoefficientCostMatchesCurrentLibaomBaseRangeAndGolomb() + { + const int qIndex = 0; + const int level = 25; + const Av1TransformSize transformSize = Av1TransformSize.Size4x4; + const Av1ComponentType componentType = Av1ComponentType.Luminance; + Av1TransformBlockContext transformBlockContext = default; + Span coefficients = stackalloc int[16]; + coefficients[0] = -level; + using Av1SymbolEncoder encoder = new(Configuration.Default, 64, qIndex, updateCdf: false); + Av1TransformSize transformSizeContext = Av1SymbolContextHelper.GetTransformSizeContext(transformSize); + Av1Distribution transformSkip = Av1DefaultDistributions + .GetTransformBlockSkip(qIndex)[(int)transformSizeContext][transformBlockContext.SkipContext]; + Av1Distribution endOfBlock = Av1DefaultDistributions + .GetEndOfBlockFlag(qIndex)[transformSize.GetLog2Minus4()][(int)componentType][0]; + Av1Distribution coefficientBaseEnd = Av1DefaultDistributions + .GetBaseEndOfBlock(qIndex)[(int)transformSizeContext][(int)componentType][0]; + Av1Distribution coefficientBaseRange = Av1DefaultDistributions + .GetCoefficientsBaseRange(qIndex)[(int)transformSizeContext][(int)componentType][0]; + Av1Distribution dcSign = Av1DefaultDistributions + .GetDcSign(qIndex)[(int)componentType][transformBlockContext.DcSignContext]; + + // Level 25 consumes all four three-symbol base-range chunks, followed by the seven-bit code for Golomb value 10. + int expected = + Av1ProbabilityCost.GetSymbolCost(transformSkip, 0) + + Av1ProbabilityCost.GetSymbolCost(endOfBlock, 0) + + Av1ProbabilityCost.GetSymbolCost(coefficientBaseEnd, 2) + + (4 * Av1ProbabilityCost.GetSymbolCost(coefficientBaseRange, 3)) + + Av1ProbabilityCost.GetSymbolCost(dcSign, 1) + + Av1ProbabilityCost.GetLiteralCost(7); + + int actual = encoder.GetCoefficientCost( + transformSize, + Av1TransformType.DctDct, + Av1PredictionMode.DC, + coefficients, + componentType, + transformBlockContext, + 1, + false, + Av1FilterIntraMode.AllFilterIntraModes); + + Assert.Equal(expected, actual); + } + + [Fact] + public void CoefficientCostMatchesCurrentLibaomCoefficientTraversal() + { + const ushort endOfBlock = 4; + const Av1TransformSize transformSize = Av1TransformSize.Size4x4; + const Av1TransformType transformType = Av1TransformType.DctDct; + const Av1ComponentType componentType = Av1ComponentType.Luminance; + const Av1PredictionMode intraDirection = Av1PredictionMode.DC; + const Av1FilterIntraMode filterIntraMode = Av1FilterIntraMode.AllFilterIntraModes; + Av1TransformBlockContext transformBlockContext = default; + ReadOnlySpan scan = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType).Scan; + Span coefficients = stackalloc int[16]; + coefficients[scan[0]] = -25; + coefficients[scan[2]] = 3; + coefficients[scan[3]] = -4; + using Av1LevelBuffer levels = new(Configuration.Default, new Size(4, 4)); + levels.Initialize(coefficients); + Span coefficientContexts = stackalloc sbyte[16]; + Av1TransformClass transformClass = transformType.ToClass(); + Av1SymbolContextHelper.GetNzMapContexts( + levels, + scan, + endOfBlock, + transformSize, + transformClass, + coefficientContexts); + + Av1TransformSize transformSizeContext = Av1SymbolContextHelper.GetTransformSizeContext(transformSize); + Av1Distribution transformSkip = Av1DefaultDistributions + .GetTransformBlockSkip(BaseQIndex)[(int)transformSizeContext][transformBlockContext.SkipContext]; + Av1Distribution endOfBlockFlag = Av1DefaultDistributions + .GetEndOfBlockFlag(BaseQIndex)[transformSize.GetLog2Minus4()][(int)componentType][0]; + Av1Distribution[][][] coefficientBase = Av1DefaultDistributions.GetCoefficientsBase(BaseQIndex); + Av1Distribution[][][] coefficientBaseEnd = Av1DefaultDistributions.GetBaseEndOfBlock(BaseQIndex); + Av1Distribution[][][] coefficientBaseRange = Av1DefaultDistributions.GetCoefficientsBaseRange(BaseQIndex); + Av1Distribution dcSign = Av1DefaultDistributions + .GetDcSign(BaseQIndex)[(int)componentType][transformBlockContext.DcSignContext]; + int expected = Av1ProbabilityCost.GetSymbolCost(transformSkip, 0); + + Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType( + transformSize, + false); + + int extendedSet = Av1SymbolContextHelper.GetExtendedTransformSet(transformSetType); + int transformTypeIndex = Av1SymbolContextHelper.GetExtendedTransformIndex(transformSetType, transformType); + expected += Av1ProbabilityCost.GetSymbolCost( + Av1DefaultDistributions.IntraExtendedTransform[extendedSet][(int)transformSize.GetSquareSize()][(int)intraDirection], + transformTypeIndex); + + short endOfBlockPosition = Av1SymbolContextHelper.GetEndOfBlockPosition(endOfBlock, out int endOfBlockExtra); + expected += Av1ProbabilityCost.GetSymbolCost(endOfBlockFlag, endOfBlockPosition - 1); + int endOfBlockOffsetBitCount = Av1SymbolContextHelper.EndOfBlockOffsetBits[endOfBlockPosition]; + int endOfBlockBit = Av1Math.GetBit(endOfBlockExtra, endOfBlockOffsetBitCount - 1); + expected += Av1ProbabilityCost.GetSymbolCost( + Av1DefaultDistributions.GetEndOfBlockExtra(BaseQIndex)[(int)transformSizeContext][(int)componentType][endOfBlockPosition], + endOfBlockBit); + expected += Av1ProbabilityCost.GetLiteralCost(endOfBlockOffsetBitCount - 1); + + int eobPosition = scan[3]; + int eobContext = coefficientContexts[eobPosition]; + int eobBaseRangeContext = Av1SymbolContextHelper.GetBaseRangeContextEndOfBlock( + levels.GetPosition(eobPosition), + transformClass); + + expected += Av1ProbabilityCost.GetSymbolCost( + coefficientBaseEnd[(int)transformSizeContext][(int)componentType][eobContext], + 2); + expected += Av1ProbabilityCost.GetSymbolCost( + coefficientBaseRange[(int)transformSizeContext][(int)componentType][eobBaseRangeContext], + 1); + expected += Av1ProbabilityCost.GetLiteralCost(1); + + int acPosition = scan[2]; + int acContext = coefficientContexts[acPosition]; + int acBaseRangeContext = Av1SymbolContextHelper.GetBaseRangeContext( + levels, + levels.GetPosition(acPosition), + transformClass); + + expected += Av1ProbabilityCost.GetSymbolCost( + coefficientBase[(int)transformSizeContext][(int)componentType][acContext], + 3); + expected += Av1ProbabilityCost.GetSymbolCost( + coefficientBaseRange[(int)transformSizeContext][(int)componentType][acBaseRangeContext], + 0); + expected += Av1ProbabilityCost.GetLiteralCost(1); + + int zeroPosition = scan[1]; + expected += Av1ProbabilityCost.GetSymbolCost( + coefficientBase[(int)transformSizeContext][(int)componentType][coefficientContexts[zeroPosition]], + 0); + + int dcPosition = scan[0]; + int dcContext = coefficientContexts[dcPosition]; + int dcBaseRangeContext = Av1SymbolContextHelper.GetBaseRangeContext( + levels, + levels.GetPosition(dcPosition), + transformClass); + + expected += Av1ProbabilityCost.GetSymbolCost( + coefficientBase[(int)transformSizeContext][(int)componentType][dcContext], + 3); + expected += 4 * Av1ProbabilityCost.GetSymbolCost( + coefficientBaseRange[(int)transformSizeContext][(int)componentType][dcBaseRangeContext], + 3); + expected += Av1ProbabilityCost.GetLiteralCost(7); + expected += Av1ProbabilityCost.GetSymbolCost(dcSign, 1); + + using Av1SymbolEncoder encoder = new(Configuration.Default, 64, BaseQIndex, updateCdf: false); + int actual = encoder.GetCoefficientCost( + transformSize, + transformType, + intraDirection, + coefficients, + componentType, + transformBlockContext, + endOfBlock, + false, + filterIntraMode); + + Assert.Equal(expected, actual); + } + + [Fact] + public void CoefficientCostDoesNotChangeWriterOrLiveDistributions() + { + const Av1TransformSize transformSize = Av1TransformSize.Size4x4; + const Av1TransformType transformType = Av1TransformType.DctDct; + const Av1ComponentType componentType = Av1ComponentType.Luminance; + const ushort endOfBlock = 4; + Av1TransformBlockContext transformBlockContext = default; + Span coefficients = stackalloc int[16]; + ReadOnlySpan scan = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType).Scan; + coefficients[scan[0]] = -25; + coefficients[scan[2]] = 3; + coefficients[scan[3]] = 1; + using Av1SymbolEncoder actualEncoder = new(Configuration.Default, 64, BaseQIndex); + using Av1SymbolEncoder expectedEncoder = new(Configuration.Default, 64, BaseQIndex); + + int initialCost = actualEncoder.GetCoefficientCost( + transformSize, + transformType, + Av1PredictionMode.DC, + coefficients, + componentType, + transformBlockContext, + endOfBlock, + false, + Av1FilterIntraMode.AllFilterIntraModes); + + Assert.Equal( + initialCost, + actualEncoder.GetCoefficientCost( + transformSize, + transformType, + Av1PredictionMode.DC, + coefficients, + componentType, + transformBlockContext, + endOfBlock, + false, + Av1FilterIntraMode.AllFilterIntraModes)); + + int actualContext = actualEncoder.WriteCoefficients( + transformSize, + transformType, + Av1PredictionMode.DC, + coefficients, + componentType, + transformBlockContext, + endOfBlock, + false, + Av1FilterIntraMode.AllFilterIntraModes); + + int adaptedCost = actualEncoder.GetCoefficientCost( + transformSize, + transformType, + Av1PredictionMode.DC, + coefficients, + componentType, + transformBlockContext, + endOfBlock, + false, + Av1FilterIntraMode.AllFilterIntraModes); + + int expectedContext = expectedEncoder.WriteCoefficients( + transformSize, + transformType, + Av1PredictionMode.DC, + coefficients, + componentType, + transformBlockContext, + endOfBlock, + false, + Av1FilterIntraMode.AllFilterIntraModes); + + using IMemoryOwner actual = actualEncoder.Exit(); + using IMemoryOwner expected = expectedEncoder.Exit(); + + Assert.NotEqual(initialCost, adaptedCost); + Assert.Equal(expectedContext, actualContext); + Assert.True(expected.GetSpan().SequenceEqual(actual.GetSpan())); + } + + [Fact] + public void CoefficientCostDoesNotAllocateAfterScratchInitialization() + { + const Av1TransformSize transformSize = Av1TransformSize.Size4x4; + const Av1TransformType transformType = Av1TransformType.DctDct; + const ushort endOfBlock = 4; + ReadOnlySpan scan = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType).Scan; + Span coefficients = stackalloc int[16]; + coefficients[scan[0]] = -25; + coefficients[scan[2]] = 3; + coefficients[scan[3]] = 1; + using Av1SymbolEncoder encoder = new(Configuration.Default, 64, BaseQIndex, updateCdf: false); + + _ = encoder.GetCoefficientCost( + transformSize, + transformType, + Av1PredictionMode.DC, + coefficients, + Av1ComponentType.Luminance, + default, + endOfBlock, + false, + Av1FilterIntraMode.AllFilterIntraModes); + + long before = GC.GetAllocatedBytesForCurrentThread(); + for (int i = 0; i < 1000; i++) + { + _ = encoder.GetCoefficientCost( + transformSize, + transformType, + Av1PredictionMode.DC, + coefficients, + Av1ComponentType.Luminance, + default, + endOfBlock, + false, + Av1FilterIntraMode.AllFilterIntraModes); + } + + long after = GC.GetAllocatedBytesForCurrentThread(); + Assert.Equal(before, after); + } + [Theory] [InlineData(1, 255, 0L, 0L)] [InlineData(1, 256, 0L, 1L)] @@ -274,6 +635,19 @@ public class Av1EntropyTests Assert.Single(allocator.AllocationLog); coefficients[0] = 1; + _ = encoder.GetCoefficientCost( + Av1TransformSize.Size4x4, + Av1TransformType.DctDct, + Av1PredictionMode.DC, + coefficients, + Av1ComponentType.Luminance, + default, + 1, + false, + Av1FilterIntraMode.DC); + + Assert.Equal(3, allocator.AllocationLog.Count); + encoder.WriteCoefficients( Av1TransformSize.Size4x4, Av1TransformType.DctDct,