diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 1a3beb5872..17011a480a 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, 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. The complete normalized probability table, Q15 boundaries, multi-symbol interval arithmetic, literal-bit precision, zero-allocation hot path, syntax-query parity, and live-update behavior pass 17 of 17 focused direct net11 Release VSTest cases; all 1,899 `Av1EntropyTests` cases pass. Transform-size, 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, 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. - [~] 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/Av1RateDistortion.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1RateDistortion.cs new file mode 100644 index 0000000000..c0edfb1ccd --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1RateDistortion.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; + +/// +/// Combines fixed-point AV1 rate and distortion values for encoder decisions. +/// +internal static class Av1RateDistortion +{ + /// + /// Gets a rate-distortion cost using the encoder probability-cost precision. + /// + /// The rate weight selected by the encoder quality model. + /// The syntax rate in 1/512-bit units. + /// The sample-domain distortion. + /// The rounded weighted rate plus distortion. + public static long GetCost(int rateMultiplier, int rate, long distortion) + { + long weightedRate = (long)rate * rateMultiplier; + long roundedRate = (weightedRate + (1 << (Av1ProbabilityCost.CostShift - 1))) >> Av1ProbabilityCost.CostShift; + return roundedRate + distortion; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs index ede81daba3..9fa2584b93 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs @@ -450,6 +450,19 @@ internal class Av1SymbolEncoder : IDisposable w.WriteSymbol(skip, this.transformBlockSkip[(int)transformSizeContext][skipContext]); } + /// + /// Gets the current fixed-point cost of a transform-size subdivision depth. + /// + /// The block size defining the maximum transform. + /// The selected transform size. + /// The neighboring transform-size context. + /// The rate cost in 1/512-bit units. + public int GetTransformSizeCost(Av1BlockSize blockSize, Av1TransformSize transformSize, int context) + { + int selectedDepth = GetTransformSizeDepth(blockSize, transformSize, out int categoryDepth); + return Av1ProbabilityCost.GetSymbolCost(this.transformSize[categoryDepth - 1][context], selectedDepth); + } + /// /// Writes the selected transform size as its subdivision depth from the block maximum. /// @@ -457,10 +470,20 @@ internal class Av1SymbolEncoder : IDisposable /// The selected transform size. /// The neighboring transform-size context. public void WriteTransformSize(Av1BlockSize blockSize, Av1TransformSize transformSize, int context) + { + int selectedDepth = GetTransformSizeDepth(blockSize, transformSize, out int categoryDepth); + ref Av1SymbolWriter w = ref this.writer; + w.WriteSymbol(selectedDepth, this.transformSize[categoryDepth - 1][context]); + } + + private static int GetTransformSizeDepth( + Av1BlockSize blockSize, + Av1TransformSize transformSize, + out int categoryDepth) { Av1TransformSize maximumTransformSize = blockSize.GetMaximumTransformSize(); Av1TransformSize currentTransformSize = maximumTransformSize; - int categoryDepth = 0; + categoryDepth = 0; while (currentTransformSize != Av1TransformSize.Size4x4) { categoryDepth++; @@ -476,8 +499,7 @@ internal class Av1SymbolEncoder : IDisposable } DebugGuard.IsTrue(currentTransformSize == transformSize, nameof(transformSize)); - ref Av1SymbolWriter w = ref this.writer; - w.WriteSymbol(selectedDepth, this.transformSize[categoryDepth - 1][context]); + return selectedDepth; } /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs index e02905849e..8549b7015f 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs @@ -123,6 +123,15 @@ public class Av1EntropyTests Av1ProbabilityCost.GetSymbolCost(skip, 1), encoder.GetSkipCost(true, SkipContext)); + Av1Distribution transformSize = Av1DefaultDistributions.TransformSize[0][SkipContext]; + Assert.Equal( + Av1ProbabilityCost.GetSymbolCost(transformSize, 0), + encoder.GetTransformSizeCost(BlockSize, TransformSize, SkipContext)); + + Assert.Equal( + Av1ProbabilityCost.GetSymbolCost(transformSize, 1), + encoder.GetTransformSizeCost(BlockSize, Av1TransformSize.Size4x4, SkipContext)); + Av1Distribution transformSkip = Av1DefaultDistributions .GetTransformBlockSkip(BaseQIndex)[(int)TransformSize][SkipContext]; @@ -149,6 +158,19 @@ public class Av1EntropyTests encoder.GetLumaModeCost(Av1PredictionMode.DC, 0, 0)); } + [Theory] + [InlineData(1, 255, 0L, 0L)] + [InlineData(1, 256, 0L, 1L)] + [InlineData(128, 512, 1000L, 1128L)] + [InlineData(512, 512, 1000L, 1512L)] + [InlineData(64, 1024, 4_000_000_000L, 4_000_000_128L)] + public void RateDistortionCostMatchesCurrentLibaom( + int rateMultiplier, + int rate, + long distortion, + long expected) + => Assert.Equal(expected, Av1RateDistortion.GetCost(rateMultiplier, rate, distortion)); + [Fact] public void SymbolWriterMatchesCurrentLibaomCarryRegression() {