diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 3084fdbf43..894951df93 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -370,6 +370,35 @@ Film-grain decoder source comparison after `ef8b1a823`: passing managed tests. This is bounded same-bitstream decoder evidence, not complete conformance or encoder parity. No benchmark ran, and no fixture, native integration, or generated comparison output was added to the repository. +RD accumulation correction after `4c7880d`: + +- Numerical defect: before this correction, `Av1IntraSuperblockEncoder.ModeDecision.cs:349-381` added + rounded child costs, `:727-731` added rounded luma/chroma costs, and `:1193-1212` added separately rounded + block syntax. Reference `av1/encoder/rd.h:32-34,208-233` keeps raw rate and distortion; rectangle and split + accumulation in `partition_search.c:3487-3504,4605-4607` rounds the combined rate. For multiplier 128, + two rates of 2 cost 1 jointly but cost 2 when rounded separately; two rates of 1 show the opposite error. +- `Av1RateDistortionStatistics.cs:9-61` now carries raw rate, distortion, and the comparison cost through + spatial, tiled, filter-intra, palette, CfL, IBC, and inter winners. Mode/transform comparisons retain their + existing strict tie rules and scalar bounds. Bounded-out split candidates retain an invalid sentinel; + only valid selected statistics are accumulated. Existing per-transform raw accumulation is preserved. +- Partition evaluation now combines those statistics in `Av1IntraSuperblockEncoder.ModeDecision.cs:336-390`; + ordinary block syntax and luma/chroma aggregation use the same retained raw inputs. This adds value state, + not a buffer or allocation. It does not implement native partition pruning, mode order, cost refresh, + quantization stages, reference control, or adaptive rate multipliers. No performance improvement is claimed. +- Seven focused regressions pass in Release .NET 11 through serialized Visual Studio VSTest + (`rd-statistics-focused.trx`, 2.6557 seconds). Four production block cases independently recount selected + syntax and pixel-domain SSE for monochrome/color and zero/nonzero residuals. Three fixed arithmetic cases + cover both rounding directions and 64-bit distortion. +- After the final C# edit, the Release .NET 11 build completed with zero errors and 1,009 existing warnings; + Roslynk reported zero compiler errors. Serialized Visual Studio VSTest passed 2,259 entropy, + intra-superblock, HEIF encoder, and reconstruction-conformance cases in 2.1328 minutes + (`rd-statistics-final.trx`), then 132 encoder-frame cases in 18.7250 seconds (`rd-statistics-frames.trx`). + The seven new cases are included in those totals. Tests encoding the existing effort policy only verify + current behavior; their success does not validate that policy against libaom. +- Optimized libaom decoding of the regenerated eight partition, 23 palette, and twelve color-sequence + streams matches all 38,973 samples exactly: maximum error 0, zero samples exceeding one. This is + same-bitstream reconstruction evidence. Separate-encoder acceptance remains unmet; no benchmark ran. + Range-writer output-capacity correction, verified after `93aba785f`: - `Av1SymbolWriter.cs:213-214,339` before correction sliced a fixed initial allocation for finalization diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1RateDistortionStatistics.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1RateDistortionStatistics.cs new file mode 100644 index 0000000000..0ec646f05d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1RateDistortionStatistics.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; + +/// +/// Holds the rate, distortion, and rounded cost of an encoder candidate. +/// +internal struct Av1RateDistortionStatistics +{ + /// + /// Initializes a new instance of the struct. + /// + /// The rate multiplier for the current block. + /// The estimated syntax rate in 1/512-bit units. + /// The candidate distortion. + public Av1RateDistortionStatistics(int rateMultiplier, int rate, long distortion) + { + this.Rate = rate; + this.Distortion = distortion; + this.Cost = Av1RateDistortion.GetCost(rateMultiplier, rate, distortion); + } + + /// + /// Gets the sentinel for a candidate that cannot win a cost comparison. + /// + public static Av1RateDistortionStatistics Invalid => new() + { + Rate = int.MaxValue, + Distortion = long.MaxValue, + Cost = long.MaxValue + }; + + /// + /// Gets the estimated syntax rate in 1/512-bit units. + /// + public int Rate { get; private set; } + + /// + /// Gets the candidate distortion. + /// + public long Distortion { get; private set; } + + /// + /// Gets the rounded rate-distortion cost. + /// + public long Cost { get; private set; } + + /// + /// Adds a valid candidate's rate and distortion and updates the combined cost. + /// + /// The rate multiplier for the combined candidate. + /// The valid candidate to add. + public void Add(int rateMultiplier, in Av1RateDistortionStatistics other) + { + // Round the combined rate only once. Adding the already rounded child costs can change + // partition and inter/intra decisions even when both children have the same reconstruction. + this.Rate += other.Rate; + this.Distortion += other.Distortion; + this.Cost = Av1RateDistortion.GetCost(rateMultiplier, this.Rate, this.Distortion); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs index f60f263d8a..ae7283eced 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs @@ -58,7 +58,7 @@ internal static partial class Av1IntraSuperblockEncoder out int selectedAngleDelta, out byte selectedChromaFromLumaIndex, out sbyte selectedChromaFromLumaSigns, - out long selectedCost) + out Av1RateDistortionStatistics selectedStatistics) { Av1EncoderModeDecisionWorkspace workspace = this.blockWorkspace.GetModeDecisionWorkspace(); @@ -94,7 +94,7 @@ internal static partial class Av1IntraSuperblockEncoder out selectedAngleDelta, out selectedChromaFromLumaIndex, out selectedChromaFromLumaSigns, - out selectedCost); + out selectedStatistics); } int modeInfoRow = lumaOrigin.Y >> Av1Constants.ModeInfoSizeLog2; @@ -201,7 +201,7 @@ internal static partial class Av1IntraSuperblockEncoder Span candidateRedReconstruction = workspace.GetCandidateReconstruction(1); Span candidateBlueCoefficients = workspace.GetCandidateCoefficients(0); Span candidateRedCoefficients = workspace.GetCandidateCoefficients(1); - long bestCost = long.MaxValue; + Av1RateDistortionStatistics bestStatistics = Av1RateDistortionStatistics.Invalid; Av1ChromaPredictionMode bestMode = Av1ChromaPredictionMode.DC; selectedAngleDelta = 0; selectedChromaFromLumaIndex = 0; @@ -249,7 +249,7 @@ internal static partial class Av1IntraSuperblockEncoder // A chroma mode and angle are shared by U and V, so neither plane can replace the // retained result independently. Their complete rate and distortion compete jointly. - long candidateCost = this.GetChromaCandidateCost( + Av1RateDistortionStatistics candidateStatistics = this.GetChromaCandidateCost( writer, modeInfo, lumaMode, @@ -277,7 +277,7 @@ internal static partial class Av1IntraSuperblockEncoder ref candidateBlueState, ref candidateRedState); - if (candidateCost < bestCost) + if (candidateStatistics.Cost < bestStatistics.Cost) { CopyCandidate( candidateBlueReconstruction, @@ -299,7 +299,7 @@ internal static partial class Av1IntraSuperblockEncoder candidateRedState, ref retainedRedStates[0]); - bestCost = candidateCost; + bestStatistics = candidateStatistics; bestMode = chromaMode; selectedAngleDelta = angleDelta; } @@ -430,15 +430,15 @@ internal static partial class Av1IntraSuperblockEncoder + writer.GetChromaFromLumaCost(packedIndex, jointSign); long distortion = blueDistortions[blueCandidateIndex] + redDistortions[redCandidateIndex]; - long candidateCost = Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion); - bool winsSearchOrderTie = candidateCost == bestCost + Av1RateDistortionStatistics candidateStatistics = new(this.rateMultiplier, rate, distortion); + bool winsSearchOrderTie = candidateStatistics.Cost == bestStatistics.Cost && !chromaFromLumaSelected && bestMode != Av1ChromaPredictionMode.DC; // CfL follows DC and precedes every other chroma mode in the reference search order. - if (candidateCost < bestCost || winsSearchOrderTie) + if (candidateStatistics.Cost < bestStatistics.Cost || winsSearchOrderTie) { - bestCost = candidateCost; + bestStatistics = candidateStatistics; bestMode = Av1ChromaPredictionMode.ChromaFromLuma; selectedAngleDelta = 0; selectedBlueCandidateIndex = blueCandidateIndex; @@ -532,7 +532,7 @@ internal static partial class Av1IntraSuperblockEncoder retainedRedCoefficients, ref retainedBlueStates[0], ref retainedRedStates[0], - ref bestCost, + ref bestStatistics, ref paletteInfo)) { bestMode = Av1ChromaPredictionMode.DC; @@ -541,7 +541,7 @@ internal static partial class Av1IntraSuperblockEncoder selectedChromaFromLumaSigns = 0; } - selectedCost = bestCost; + selectedStatistics = bestStatistics; return bestMode; } @@ -564,7 +564,7 @@ internal static partial class Av1IntraSuperblockEncoder out int selectedAngleDelta, out byte selectedChromaFromLumaIndex, out sbyte selectedChromaFromLumaSigns, - out long selectedCost) + out Av1RateDistortionStatistics selectedStatistics) { Av1EncoderModeDecisionWorkspace workspace = this.blockWorkspace.GetModeDecisionWorkspace(); @@ -650,7 +650,7 @@ internal static partial class Av1IntraSuperblockEncoder _ => baseModeCount }; - long bestCost = long.MaxValue; + Av1RateDistortionStatistics bestStatistics = Av1RateDistortionStatistics.Invalid; Av1ChromaPredictionMode bestMode = Av1ChromaPredictionMode.DC; selectedAngleDelta = 0; selectedChromaFromLumaIndex = 0; @@ -745,8 +745,8 @@ internal static partial class Av1IntraSuperblockEncoder rate += paletteDisabledCost; } - long candidateCost = Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion); - if (candidateCost < bestCost) + Av1RateDistortionStatistics candidateStatistics = new(this.rateMultiplier, rate, distortion); + if (candidateStatistics.Cost < bestStatistics.Cost) { CopyTiledCandidate( candidateBlueReconstruction, @@ -772,13 +772,13 @@ internal static partial class Av1IntraSuperblockEncoder retainedRedCoefficients, retainedRedStates); - bestCost = candidateCost; + bestStatistics = candidateStatistics; bestMode = chromaMode; selectedAngleDelta = angleDelta; } } - selectedCost = bestCost; + selectedStatistics = bestStatistics; return bestMode; } @@ -1083,7 +1083,7 @@ internal static partial class Av1IntraSuperblockEncoder && modeInfo.UvMode is Av1ChromaPredictionMode.Smooth or Av1ChromaPredictionMode.SmoothVertical or Av1ChromaPredictionMode.SmoothHorizontal; } - private long GetChromaCandidateCost( + private Av1RateDistortionStatistics GetChromaCandidateCost( Av1SymbolEncoder writer, Av1MacroBlockModeInfo modeInfo, Av1PredictionMode lumaMode, @@ -1209,7 +1209,7 @@ internal static partial class Av1IntraSuperblockEncoder Av1FilterIntraMode.AllFilterIntraModes, usesInterTransformSet: false); - return Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion); + return new(this.rateMultiplier, rate, distortion); } /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaPaletteModeDecision.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaPaletteModeDecision.cs index e50dcef246..8ffdbe2cf2 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaPaletteModeDecision.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaPaletteModeDecision.cs @@ -38,7 +38,7 @@ internal static partial class Av1IntraSuperblockEncoder Span retainedRedCoefficients, ref Av1EncoderTransformBlockState retainedBlueState, ref Av1EncoderTransformBlockState retainedRedState, - ref long bestCost, + ref Av1RateDistortionStatistics bestStatistics, ref Av1EncoderPaletteInfo paletteInfo) { const Av1BlockSize BlockSize = Av1BlockSize.Block8x8; @@ -336,8 +336,8 @@ internal static partial class Av1IntraSuperblockEncoder Av1FilterIntraMode.AllFilterIntraModes, usesInterTransformSet: false); - long candidateCost = Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion); - if (candidateCost < bestCost) + Av1RateDistortionStatistics candidateStatistics = new(this.rateMultiplier, rate, distortion); + if (candidateStatistics.Cost < bestStatistics.Cost) { // Every following palette size overwrites the shared maps and candidate spans, so a // global improvement must retain reconstruction, coefficients, colors, and indices together. @@ -372,7 +372,7 @@ internal static partial class Av1IntraSuperblockEncoder paletteInfo.PaletteSizes[1] = (byte)paletteSize; paletteInfo.SetColors(Av1Plane.U, bluePaletteColors); paletteInfo.SetColors(Av1Plane.V, redPaletteColors); - bestCost = candidateCost; + bestStatistics = candidateStatistics; paletteSelected = true; } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs index 8f76f64e25..05e148eb17 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs @@ -139,7 +139,6 @@ internal static partial class Av1IntraSuperblockEncoder private readonly int effort; private int codedAreaLuma; private int codedAreaChroma; - private long selectedBlockCost; /// /// Initializes a new instance of the struct. @@ -178,9 +177,14 @@ internal static partial class Av1IntraSuperblockEncoder this.effort = effort; this.codedAreaLuma = 0; this.codedAreaChroma = 0; - this.selectedBlockCost = 0; + this.SelectedBlockStatistics = default; } + /// + /// Gets the statistics of the most recently encoded block. + /// + public Av1RateDistortionStatistics SelectedBlockStatistics { get; private set; } + /// public Av1PartitionType SelectPartition( Av1SymbolEncoder writer, @@ -254,7 +258,7 @@ internal static partial class Av1IntraSuperblockEncoder int savedLumaArea = this.codedAreaLuma; int savedChromaArea = this.codedAreaChroma; this.SavePartitionTrialContexts(blockOrigin, tileIndex, blockSize); - long bestCost = long.MaxValue; + Av1RateDistortionStatistics bestStatistics = Av1RateDistortionStatistics.Invalid; Av1PartitionType selectedPartition = Av1PartitionType.None; ReadOnlySpan searchOrder = PartitionSearchOrder; int candidateCount = blockSize == Av1BlockSize.Block8x8 ? 4 : searchOrder.Length; @@ -266,7 +270,7 @@ internal static partial class Av1IntraSuperblockEncoder continue; } - long candidateCost = this.EvaluatePartitionCandidate( + Av1RateDistortionStatistics candidateStatistics = this.EvaluatePartitionCandidate( writer, macroBlock, blockOrigin, @@ -275,9 +279,9 @@ internal static partial class Av1IntraSuperblockEncoder partitionType, publishFinalContexts: false); - if (candidateCost < bestCost) + if (candidateStatistics.Cost < bestStatistics.Cost) { - bestCost = candidateCost; + bestStatistics = candidateStatistics; selectedPartition = partitionType; } @@ -292,7 +296,7 @@ internal static partial class Av1IntraSuperblockEncoder return selectedPartition; } - private long EvaluateSelectedPartitionTree( + private Av1RateDistortionStatistics EvaluateSelectedPartitionTree( Av1SymbolEncoder writer, Av1MacroBlockD macroBlock, Point blockOrigin, @@ -307,7 +311,7 @@ internal static partial class Av1IntraSuperblockEncoder tileIndex, blockSize); - long cost = this.EvaluatePartitionCandidate( + Av1RateDistortionStatistics statistics = this.EvaluatePartitionCandidate( writer, macroBlock, blockOrigin, @@ -326,10 +330,10 @@ internal static partial class Av1IntraSuperblockEncoder selectedPartition); } - return cost; + return statistics; } - private long EvaluatePartitionCandidate( + private Av1RateDistortionStatistics EvaluatePartitionCandidate( Av1SymbolEncoder writer, Av1MacroBlockD macroBlock, Point blockOrigin, @@ -346,7 +350,7 @@ internal static partial class Av1IntraSuperblockEncoder blockOrigin, this.picture.PartitionContexts[tileIndex]); - long cost = Av1RateDistortion.GetCost(this.rateMultiplier, rate, 0); + Av1RateDistortionStatistics statistics = new(this.rateMultiplier, rate, 0); int leafCount = GetPartitionLeafCount(partitionType); // Child reconstruction and syntax contexts become input to the next child. Publishing only @@ -362,7 +366,7 @@ internal static partial class Av1IntraSuperblockEncoder out Av1BlockSize leafSize); bool publishContexts = leafIndex < leafCount - 1 || publishFinalContexts; - cost += partitionType == Av1PartitionType.Split && blockSize > Av1BlockSize.Block8x8 + Av1RateDistortionStatistics childStatistics = partitionType == Av1PartitionType.Split && blockSize > Av1BlockSize.Block8x8 ? this.EvaluateSelectedPartitionTree( writer, macroBlock, @@ -378,9 +382,11 @@ internal static partial class Av1IntraSuperblockEncoder leafSize, partitionType == Av1PartitionType.Split ? Av1PartitionType.None : partitionType, publishContexts); + + statistics.Add(this.rateMultiplier, in childStatistics); } - return cost; + return statistics; } private void ResetPartitionTrial( @@ -613,7 +619,7 @@ internal static partial class Av1IntraSuperblockEncoder out int lumaAngleDelta, out Av1FilterIntraMode filterIntraMode, out Av1TransformSize lumaTransformSize, - out long lumaCost); + out Av1RateDistortionStatistics lumaStatistics); block.PredictionUnit.AngleDelta[(int)Av1PlaneType.Y] = (sbyte)lumaAngleDelta; block.FilterIntraMode = filterIntraMode; @@ -626,37 +632,37 @@ internal static partial class Av1IntraSuperblockEncoder bool allowIntraBlockCopy = blockSize == Av1BlockSize.Block8x8 && this.picture.Parent.FrameHeader.AllowIntraBlockCopy; - long regularCost = this.GetRegularBlockCost( + Av1RateDistortionStatistics regularStatistics = this.GetRegularBlockCost( writer, macroBlock, - lumaCost, + lumaStatistics, allowIntraBlockCopy); if (!this.picture.Parent.FrameHeader.IsIntra) { - this.selectedBlockCost = this.SelectInterPrediction( + this.SelectedBlockStatistics = this.SelectInterPrediction( writer, macroBlock, blockOrigin, tileIndex, - regularCost, + regularStatistics, ref modeInfo, ref block, ref paletteInfo); } else { - this.selectedBlockCost = allowIntraBlockCopy + this.SelectedBlockStatistics = allowIntraBlockCopy ? this.SelectIntraBlockCopy( writer, macroBlock, blockOrigin, tileIndex, - regularCost, + regularStatistics, ref modeInfo, ref block, ref paletteInfo) - : regularCost; + : regularStatistics; } this.codedAreaLuma += blockSize.GetWidth() * blockSize.GetHeight(); @@ -693,7 +699,7 @@ internal static partial class Av1IntraSuperblockEncoder Span retainedBlueStates = blueTransformBlocks[chromaTransformIndex..]; Span retainedRedStates = redTransformBlocks[chromaTransformIndex..]; - long chromaCost = 0; + Av1RateDistortionStatistics chromaStatistics = default; if (block.HasChroma) { modeInfo.Block.UvMode = this.SelectChromaMode( @@ -714,7 +720,7 @@ internal static partial class Av1IntraSuperblockEncoder out int chromaAngleDelta, out byte chromaFromLumaIndex, out sbyte chromaFromLumaSigns, - out chromaCost); + out chromaStatistics); block.PredictionUnit.AngleDelta[(int)Av1PlaneType.Uv] = (sbyte)chromaAngleDelta; block.PredictionUnit.ChromaFromLumaIndex = chromaFromLumaIndex; @@ -724,37 +730,38 @@ internal static partial class Av1IntraSuperblockEncoder bool allowColorIntraBlockCopy = blockSize == Av1BlockSize.Block8x8 && this.picture.Parent.FrameHeader.AllowIntraBlockCopy; - long regularColorCost = this.GetRegularBlockCost( + lumaStatistics.Add(this.rateMultiplier, in chromaStatistics); + Av1RateDistortionStatistics regularColorStatistics = this.GetRegularBlockCost( writer, macroBlock, - lumaCost + chromaCost, + lumaStatistics, allowColorIntraBlockCopy); if (!this.picture.Parent.FrameHeader.IsIntra) { - this.selectedBlockCost = this.SelectInterPrediction( + this.SelectedBlockStatistics = this.SelectInterPrediction( writer, macroBlock, blockOrigin, tileIndex, - regularColorCost, + regularColorStatistics, ref modeInfo, ref block, ref paletteInfo); } else { - this.selectedBlockCost = allowColorIntraBlockCopy + this.SelectedBlockStatistics = allowColorIntraBlockCopy ? this.SelectIntraBlockCopy( writer, macroBlock, blockOrigin, tileIndex, - regularColorCost, + regularColorStatistics, ref modeInfo, ref block, ref paletteInfo) - : regularColorCost; + : regularColorStatistics; } this.codedAreaLuma += blockSize.GetWidth() * blockSize.GetHeight(); @@ -764,7 +771,7 @@ internal static partial class Av1IntraSuperblockEncoder } } - private long EvaluatePartitionLeaf( + private Av1RateDistortionStatistics EvaluatePartitionLeaf( Av1SymbolEncoder writer, Av1MacroBlockD macroBlock, Point blockOrigin, @@ -812,7 +819,7 @@ internal static partial class Av1IntraSuperblockEncoder paletteInfo); } - return this.selectedBlockCost; + return this.SelectedBlockStatistics; } private void SetBlockGeometry( @@ -1190,10 +1197,10 @@ internal static partial class Av1IntraSuperblockEncoder offset += left.Length; } - private long GetRegularBlockCost( + private Av1RateDistortionStatistics GetRegularBlockCost( Av1SymbolEncoder writer, Av1MacroBlockD macroBlock, - long modeCost, + Av1RateDistortionStatistics modeStatistics, bool allowIntraBlockCopy) { int rateAdjustment = writer.GetSkipCost(false, Av1TileWriter.GetSkipContext(macroBlock)); @@ -1208,7 +1215,7 @@ internal static partial class Av1IntraSuperblockEncoder rateAdjustment += writer.GetUseIntraBlockCopyCost(false); } - return modeCost + Av1RateDistortion.GetCost(this.rateMultiplier, rateAdjustment, 0); + return new(this.rateMultiplier, modeStatistics.Rate + rateAdjustment, modeStatistics.Distortion); } private Av1PredictionMode SelectLumaMode( @@ -1223,7 +1230,7 @@ internal static partial class Av1IntraSuperblockEncoder out int selectedAngleDelta, out Av1FilterIntraMode selectedFilterIntraMode, out Av1TransformSize selectedTransformSize, - out long selectedCost) + out Av1RateDistortionStatistics selectedStatistics) { bool codedLossless = this.picture.Parent.FrameHeader.CodedLossless; Av1TransformSize transformSize = codedLossless @@ -1251,7 +1258,7 @@ internal static partial class Av1IntraSuperblockEncoder out selectedAngleDelta, out selectedFilterIntraMode, out selectedTransformSize, - out selectedCost); + out selectedStatistics); } bool hasLeft = macroBlock.IsLeftAvailable; @@ -1347,7 +1354,7 @@ internal static partial class Av1IntraSuperblockEncoder Span candidateCoefficients = workspace.GetCandidateCoefficients(0); Span prediction = workspace.Prediction; Span residual = workspace.Residual; - long bestCost = long.MaxValue; + Av1RateDistortionStatistics bestStatistics = Av1RateDistortionStatistics.Invalid; Av1PredictionMode bestMode = Av1PredictionMode.DC; selectedAngleDelta = 0; selectedFilterIntraMode = Av1FilterIntraMode.AllFilterIntraModes; @@ -1439,7 +1446,7 @@ internal static partial class Av1IntraSuperblockEncoder } Av1EncoderTransformBlockState candidateState = default; - long candidateCost = this.GetLumaCandidateCost( + Av1RateDistortionStatistics candidateStatistics = this.GetLumaCandidateCost( writer, macroBlock, sourcePlane, @@ -1458,7 +1465,7 @@ internal static partial class Av1IntraSuperblockEncoder candidateCoefficients, ref candidateState); - if (candidateCost < bestCost) + if (candidateStatistics.Cost < bestStatistics.Cost) { // The shared candidate spans are overwritten by the next transform. Copy only a // global improvement into final block storage so no per-mode retained buffer is needed. @@ -1472,7 +1479,7 @@ internal static partial class Av1IntraSuperblockEncoder candidateState, ref retainedStates[0]); - bestCost = candidateCost; + bestStatistics = candidateStatistics; bestMode = mode; selectedAngleDelta = angleDelta; selectedTransformSize = transformSize; @@ -1484,7 +1491,7 @@ internal static partial class Av1IntraSuperblockEncoder if (searchEveryTransformSize && this.picture.Parent.FrameHeader.TransformMode == Av1TransformMode.Select) { - long splitCost = this.GetSplitLumaCandidateCost( + Av1RateDistortionStatistics splitStatistics = this.GetSplitLumaCandidateCost( writer, macroBlock, sourcePlane, @@ -1499,12 +1506,12 @@ internal static partial class Av1IntraSuperblockEncoder 0, paletteDisabledCost, transformSizeContext, - bestCost, + bestStatistics.Cost, candidateReconstruction, candidateCoefficients, workspace.CandidateTransformBlocks); - if (splitCost < bestCost) + if (splitStatistics.Cost < bestStatistics.Cost) { CopySplitCandidate( candidateReconstruction, @@ -1515,7 +1522,7 @@ internal static partial class Av1IntraSuperblockEncoder retainedCoefficients, retainedStates); - bestCost = splitCost; + bestStatistics = splitStatistics; bestMode = mode; selectedAngleDelta = angleDelta; selectedTransformSize = Av1TransformSize.Size4x4; @@ -1525,7 +1532,7 @@ internal static partial class Av1IntraSuperblockEncoder // Midrange effort refines the preliminary mode only. Higher effort already searched every // mode-transform pair above, so repeating the winning mode would add no candidates. - long bestTransformCost = bestCost; + Av1RateDistortionStatistics bestTransformStatistics = bestStatistics; if (!codedLossless && this.effort >= 3 && !searchEveryTransformType) { // The shared spans now contain the last mode visited above, so rebuild the preliminary @@ -1557,7 +1564,7 @@ internal static partial class Av1IntraSuperblockEncoder } Av1EncoderTransformBlockState candidateState = default; - long candidateCost = this.GetLumaCandidateCost( + Av1RateDistortionStatistics candidateStatistics = this.GetLumaCandidateCost( writer, macroBlock, sourcePlane, @@ -1576,7 +1583,7 @@ internal static partial class Av1IntraSuperblockEncoder candidateCoefficients, ref candidateState); - if (candidateCost < bestTransformCost) + if (candidateStatistics.Cost < bestTransformStatistics.Cost) { CopyCandidate( candidateReconstruction, @@ -1588,7 +1595,7 @@ internal static partial class Av1IntraSuperblockEncoder candidateState, ref retainedStates[0]); - bestTransformCost = candidateCost; + bestTransformStatistics = candidateStatistics; } } } @@ -1630,7 +1637,7 @@ internal static partial class Av1IntraSuperblockEncoder } Av1EncoderTransformBlockState candidateState = default; - long candidateCost = this.GetFilterIntraCandidateCost( + Av1RateDistortionStatistics candidateStatistics = this.GetFilterIntraCandidateCost( writer, macroBlock, sourcePlane, @@ -1648,7 +1655,7 @@ internal static partial class Av1IntraSuperblockEncoder candidateCoefficients, ref candidateState); - if (candidateCost < bestTransformCost) + if (candidateStatistics.Cost < bestTransformStatistics.Cost) { CopyCandidate( candidateReconstruction, @@ -1660,7 +1667,7 @@ internal static partial class Av1IntraSuperblockEncoder candidateState, ref retainedStates[0]); - bestTransformCost = candidateCost; + bestTransformStatistics = candidateStatistics; bestMode = Av1PredictionMode.DC; selectedAngleDelta = 0; selectedFilterIntraMode = filterIntraMode; @@ -1673,7 +1680,7 @@ internal static partial class Av1IntraSuperblockEncoder if (searchEveryTransformSize && this.picture.Parent.FrameHeader.TransformMode == Av1TransformMode.Select) { - long splitCost = this.GetSplitLumaCandidateCost( + Av1RateDistortionStatistics splitStatistics = this.GetSplitLumaCandidateCost( writer, macroBlock, sourcePlane, @@ -1688,12 +1695,12 @@ internal static partial class Av1IntraSuperblockEncoder 0, paletteDisabledCost, transformSizeContext, - bestTransformCost, + bestTransformStatistics.Cost, candidateReconstruction, candidateCoefficients, workspace.CandidateTransformBlocks); - if (splitCost < bestTransformCost) + if (splitStatistics.Cost < bestTransformStatistics.Cost) { CopySplitCandidate( candidateReconstruction, @@ -1704,7 +1711,7 @@ internal static partial class Av1IntraSuperblockEncoder retainedCoefficients, retainedStates); - bestTransformCost = splitCost; + bestTransformStatistics = splitStatistics; bestMode = Av1PredictionMode.DC; selectedAngleDelta = 0; selectedFilterIntraMode = filterIntraMode; @@ -1732,7 +1739,7 @@ internal static partial class Av1IntraSuperblockEncoder candidateCoefficients, retainedCoefficients, retainedStates, - ref bestTransformCost, + ref bestTransformStatistics, ref paletteInfo, ref selectedTransformSize)) { @@ -1749,7 +1756,7 @@ internal static partial class Av1IntraSuperblockEncoder this.picture.Parent.FrameHeader.TransformMode == Av1TransformMode.Select && paletteInfo.PaletteSizes[0] == 0) { - long splitCost = this.GetSplitLumaCandidateCost( + Av1RateDistortionStatistics splitStatistics = this.GetSplitLumaCandidateCost( writer, macroBlock, sourcePlane, @@ -1764,12 +1771,12 @@ internal static partial class Av1IntraSuperblockEncoder 0, paletteDisabledCost, transformSizeContext, - bestTransformCost, + bestTransformStatistics.Cost, candidateReconstruction, candidateCoefficients, workspace.CandidateTransformBlocks); - if (splitCost < bestTransformCost) + if (splitStatistics.Cost < bestTransformStatistics.Cost) { CopySplitCandidate( candidateReconstruction, @@ -1780,12 +1787,12 @@ internal static partial class Av1IntraSuperblockEncoder retainedCoefficients, retainedStates); - bestTransformCost = splitCost; + bestTransformStatistics = splitStatistics; selectedTransformSize = Av1TransformSize.Size4x4; } } - selectedCost = bestTransformCost; + selectedStatistics = bestTransformStatistics; return bestMode; } @@ -1801,7 +1808,7 @@ internal static partial class Av1IntraSuperblockEncoder out int selectedAngleDelta, out Av1FilterIntraMode selectedFilterIntraMode, out Av1TransformSize selectedTransformSize, - out long selectedCost) + out Av1RateDistortionStatistics selectedStatistics) { Av1EncoderModeDecisionWorkspace workspace = this.blockWorkspace.GetModeDecisionWorkspace(); @@ -1868,7 +1875,7 @@ internal static partial class Av1IntraSuperblockEncoder Buffer2DRegion sourcePlane = this.source.GetPlane(Av1Plane.Y); Buffer2DRegion reconstructionPlane = this.reconstruction.GetPlane(Av1Plane.Y); - long bestCost = long.MaxValue; + Av1RateDistortionStatistics bestStatistics = Av1RateDistortionStatistics.Invalid; Av1PredictionMode bestMode = Av1PredictionMode.DC; selectedAngleDelta = 0; selectedFilterIntraMode = Av1FilterIntraMode.AllFilterIntraModes; @@ -1942,8 +1949,8 @@ internal static partial class Av1IntraSuperblockEncoder } } - long candidateCost = Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion); - if (candidateCost < bestCost) + Av1RateDistortionStatistics candidateStatistics = new(this.rateMultiplier, rate, distortion); + if (candidateStatistics.Cost < bestStatistics.Cost) { CopyTiledCandidate( candidateReconstruction, @@ -1957,17 +1964,17 @@ internal static partial class Av1IntraSuperblockEncoder retainedCoefficients, retainedStates); - bestCost = candidateCost; + bestStatistics = candidateStatistics; bestMode = mode; selectedAngleDelta = angleDelta; } } - selectedCost = bestCost; + selectedStatistics = bestStatistics; return bestMode; } - private long GetSplitLumaCandidateCost( + private Av1RateDistortionStatistics GetSplitLumaCandidateCost( Av1SymbolEncoder writer, Av1MacroBlockD macroBlock, Buffer2DRegion sourcePlane, @@ -2267,12 +2274,12 @@ internal static partial class Av1IntraSuperblockEncoder // Every remaining transform can only add nonnegative rate and distortion. if (Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion) >= costLimit) { - return long.MaxValue; + return Av1RateDistortionStatistics.Invalid; } } } - return Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion); + return new(this.rateMultiplier, rate, distortion); } private void PrepareTransformReferenceSamples( @@ -2471,7 +2478,7 @@ internal static partial class Av1IntraSuperblockEncoder leftStorage[0] = corner; } - private long GetLumaCandidateCost( + private Av1RateDistortionStatistics GetLumaCandidateCost( Av1SymbolEncoder writer, Av1MacroBlockD macroBlock, Buffer2DRegion sourcePlane, @@ -2546,10 +2553,10 @@ internal static partial class Av1IntraSuperblockEncoder Av1FilterIntraMode.AllFilterIntraModes, usesInterTransformSet: false); - return Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion); + return new(this.rateMultiplier, rate, distortion); } - private long GetFilterIntraCandidateCost( + private Av1RateDistortionStatistics GetFilterIntraCandidateCost( Av1SymbolEncoder writer, Av1MacroBlockD macroBlock, Buffer2DRegion sourcePlane, @@ -2608,7 +2615,7 @@ internal static partial class Av1IntraSuperblockEncoder filterIntraMode, usesInterTransformSet: false); - return Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion); + return new(this.rateMultiplier, rate, distortion); } private static void CopyCandidate( diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.PaletteModeDecision.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.PaletteModeDecision.cs index b0ccb8ee9c..756042f0b3 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.PaletteModeDecision.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.PaletteModeDecision.cs @@ -34,7 +34,7 @@ internal static partial class Av1IntraSuperblockEncoder Span candidateCoefficients, Span retainedCoefficients, Span retainedStates, - ref long bestCost, + ref Av1RateDistortionStatistics bestStatistics, ref Av1EncoderPaletteInfo paletteInfo, ref Av1TransformSize selectedTransformSize) { @@ -164,7 +164,7 @@ internal static partial class Av1IntraSuperblockEncoder retainedStates, retainedColorIndexMap, reconstructionPlane, - ref bestCost, + ref bestStatistics, ref paletteInfo, ref selectedTransformSize, ref paletteSelected); @@ -197,7 +197,7 @@ internal static partial class Av1IntraSuperblockEncoder retainedStates, retainedColorIndexMap, reconstructionPlane, - ref bestCost, + ref bestStatistics, ref paletteInfo, ref selectedTransformSize, ref paletteSelected); @@ -239,7 +239,7 @@ internal static partial class Av1IntraSuperblockEncoder retainedStates, retainedColorIndexMap, reconstructionPlane, - ref bestCost, + ref bestStatistics, ref paletteInfo, ref selectedTransformSize, ref paletteSelected); @@ -281,7 +281,7 @@ internal static partial class Av1IntraSuperblockEncoder Span retainedStates, Span retainedColorIndexMap, Buffer2DRegion reconstructionPlane, - ref long bestCost, + ref Av1RateDistortionStatistics bestStatistics, ref Av1EncoderPaletteInfo paletteInfo, ref Av1TransformSize selectedTransformSize, ref bool paletteSelected) @@ -429,8 +429,8 @@ internal static partial class Av1IntraSuperblockEncoder Av1FilterIntraMode.AllFilterIntraModes, usesInterTransformSet: false); - long candidateCost = Av1RateDistortion.GetCost(this.rateMultiplier, candidateRate, distortion); - if (candidateCost < bestCost) + Av1RateDistortionStatistics candidateStatistics = new(this.rateMultiplier, candidateRate, distortion); + if (candidateStatistics.Cost < bestStatistics.Cost) { // Later palette sizes reuse every candidate span and the shared color map. Publish the // complete palette state only when this candidate improves the global luma decision. @@ -453,7 +453,7 @@ internal static partial class Av1IntraSuperblockEncoder paletteInfo.PaletteSizes[0] = (byte)paletteSize; paletteInfo.SetColors(Av1Plane.Y, paletteColors); selectedTransformSize = TransformSize; - bestCost = candidateCost; + bestStatistics = candidateStatistics; paletteSelected = true; } } @@ -463,7 +463,7 @@ internal static partial class Av1IntraSuperblockEncoder { // Transform size is part of each palette candidate's RD result. Searching it here preserves // candidates whose 4x4 residual partition wins even when their 8x8 result does not. - long splitCost = this.GetSplitLumaCandidateCost( + Av1RateDistortionStatistics splitStatistics = this.GetSplitLumaCandidateCost( writer, macroBlock, this.source.GetPlane(Av1Plane.Y), @@ -478,12 +478,12 @@ internal static partial class Av1IntraSuperblockEncoder rate, 0, transformSizeContext, - bestCost, + bestStatistics.Cost, candidateReconstruction, candidateCoefficients, modeDecisionWorkspace.CandidateTransformBlocks); - if (splitCost < bestCost) + if (splitStatistics.Cost < bestStatistics.Cost) { CopySplitCandidate( candidateReconstruction, @@ -503,7 +503,7 @@ internal static partial class Av1IntraSuperblockEncoder paletteInfo.PaletteSizes[0] = (byte)paletteSize; paletteInfo.SetColors(Av1Plane.Y, paletteColors); selectedTransformSize = Av1TransformSize.Size4x4; - bestCost = splitCost; + bestStatistics = splitStatistics; paletteSelected = true; } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ReferenceModeDecision.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ReferenceModeDecision.cs index 45824e474d..229b809730 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ReferenceModeDecision.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ReferenceModeDecision.cs @@ -57,12 +57,12 @@ internal static partial class Av1IntraSuperblockEncoder where TSample : unmanaged where TOperator : struct, IBlockEncodingOperator { - private long SelectIntraBlockCopy( + private Av1RateDistortionStatistics SelectIntraBlockCopy( Av1SymbolEncoder writer, Av1MacroBlockD macroBlock, Point blockOrigin, ushort tileIndex, - long regularCost, + Av1RateDistortionStatistics regularStatistics, ref Av1MacroBlockModeInfo modeInfo, ref Av1EncoderBlockStruct block, ref Av1EncoderPaletteInfo paletteInfo) @@ -135,11 +135,11 @@ internal static partial class Av1IntraSuperblockEncoder if (uniqueCandidateCount == 0) { - return regularCost; + return regularStatistics; } int skipContext = Av1TileWriter.GetSkipContext(macroBlock); - long bestCost = regularCost; + Av1RateDistortionStatistics bestStatistics = regularStatistics; bool hasSelectedCandidate = false; bool selectedSkip = false; Av1MotionVector selectedVector = default; @@ -336,7 +336,7 @@ internal static partial class Av1IntraSuperblockEncoder redRate; long candidateDistortion = lumaDistortion + blueDistortion + redDistortion; - long candidateCost = Av1RateDistortion.GetCost(this.rateMultiplier, candidateRate, candidateDistortion); + Av1RateDistortionStatistics candidateStatistics = new(this.rateMultiplier, candidateRate, candidateDistortion); bool candidateSkip = false; // The skip alternative is available only when every coded plane has an empty transform. Its @@ -348,21 +348,21 @@ internal static partial class Av1IntraSuperblockEncoder writer.GetSkipCost(true, skipContext); long skipDistortion = emptyLumaDistortion + emptyBlueDistortion + emptyRedDistortion; - long skipCost = Av1RateDistortion.GetCost(this.rateMultiplier, skipRate, skipDistortion); - if (skipCost < candidateCost) + Av1RateDistortionStatistics skipStatistics = new(this.rateMultiplier, skipRate, skipDistortion); + if (skipStatistics.Cost < candidateStatistics.Cost) { - candidateCost = skipCost; + candidateStatistics = skipStatistics; candidateSkip = true; } } // Conventional intra and earlier IBC vectors retain strict search-order precedence on equal RD. - if (candidateCost >= bestCost) + if (candidateStatistics.Cost >= bestStatistics.Cost) { continue; } - bestCost = candidateCost; + bestStatistics = candidateStatistics; hasSelectedCandidate = true; selectedSkip = candidateSkip; selectedVector = candidate; @@ -410,7 +410,7 @@ internal static partial class Av1IntraSuperblockEncoder if (!hasSelectedCandidate) { - return bestCost; + return bestStatistics; } // Only the winning vector is now visible to later coding blocks. This single publication keeps @@ -481,18 +481,18 @@ internal static partial class Av1IntraSuperblockEncoder block.PredictionUnit.ChromaFromLumaSigns = 0; paletteInfo = default; this.picture.SetDisplacementVector(modeInfoPosition, selectedVector); - return bestCost; + return bestStatistics; } /// /// Compares the retained intra result with an inter candidate without disturbing the intra result on loss. /// - private long SelectInterPrediction( + private Av1RateDistortionStatistics SelectInterPrediction( Av1SymbolEncoder writer, Av1MacroBlockD macroBlock, Point blockOrigin, ushort tileIndex, - long regularCost, + Av1RateDistortionStatistics regularStatistics, ref Av1MacroBlockModeInfo modeInfo, ref Av1EncoderBlockStruct block, ref Av1EncoderPaletteInfo paletteInfo) @@ -500,35 +500,35 @@ internal static partial class Av1IntraSuperblockEncoder Av1MacroBlockModeInfo interModeInfo = modeInfo; Av1EncoderBlockStruct interBlock = block; Av1EncoderPaletteInfo interPaletteInfo = default; - long selectedCost = this.SelectInterBlock( + Av1RateDistortionStatistics selectedStatistics = this.SelectInterBlock( writer, macroBlock, blockOrigin, tileIndex, - regularCost, + regularStatistics, ref interModeInfo, ref interBlock, ref interPaletteInfo); - if (selectedCost < regularCost) + if (selectedStatistics.Cost < regularStatistics.Cost) { modeInfo = interModeInfo; block = interBlock; paletteInfo = interPaletteInfo; } - return selectedCost; + return selectedStatistics; } /// /// Evaluates the supported LAST_FRAME modes and publishes only a strict improvement over the intra result. /// - private long SelectInterBlock( + private Av1RateDistortionStatistics SelectInterBlock( Av1SymbolEncoder writer, Av1MacroBlockD macroBlock, Point blockOrigin, ushort tileIndex, - long regularCost, + Av1RateDistortionStatistics regularStatistics, ref Av1MacroBlockModeInfo modeInfo, ref Av1EncoderBlockStruct block, ref Av1EncoderPaletteInfo paletteInfo) @@ -651,7 +651,7 @@ internal static partial class Av1IntraSuperblockEncoder transformPartitionRate = writer.GetTransformPartitionCost(false, transformPartitionContext); } - long selectedCost = regularCost; + Av1RateDistortionStatistics selectedStatistics = regularStatistics; Av1MotionVector selectedVector = default; Av1PredictionMode selectedMode = default; int selectedReferenceIndex = 0; @@ -785,7 +785,7 @@ internal static partial class Av1IntraSuperblockEncoder } } - long candidateCost = this.EvaluateInterCandidate( + Av1RateDistortionStatistics candidateStatistics = this.EvaluateInterCandidate( writer, blockOrigin, tileIndex, @@ -812,7 +812,7 @@ internal static partial class Av1IntraSuperblockEncoder out Av1EncoderTransformBlockState candidateRedState); // Strict replacement preserves predictor-stack, global, then new-motion order on equal RD cost. - if (candidateCost >= selectedCost) + if (candidateStatistics.Cost >= selectedStatistics.Cost) { continue; } @@ -841,7 +841,7 @@ internal static partial class Av1IntraSuperblockEncoder selectedRedCoefficients = candidateRedCoefficients; candidateRedCoefficients = previousRedCoefficients; - selectedCost = candidateCost; + selectedStatistics = candidateStatistics; selectedVector = candidateVectors[candidateIndex]; selectedMode = candidateModes[candidateIndex]; selectedHorizontalFilter = horizontalFilter; @@ -858,7 +858,7 @@ internal static partial class Av1IntraSuperblockEncoder // when no inter candidate strictly improves its rate-distortion cost. if (!hasInterWinner) { - return regularCost; + return regularStatistics; } Span retainedLumaCoefficients = this.coefficientBuffer.GetPlaneSpan(this.superblock.Index, Av1Plane.Y); @@ -930,7 +930,7 @@ internal static partial class Av1IntraSuperblockEncoder modeInfo.Block.HorizontalInterpolationFilter = selectedHorizontalFilter; block.ReferenceMotionVectorIndex = selectedReferenceIndex; this.picture.SetDisplacementVector(modeInfoPosition, selectedVector); - return selectedCost; + return selectedStatistics; } /// @@ -1034,7 +1034,7 @@ internal static partial class Av1IntraSuperblockEncoder /// /// Evaluates one inter mode through prediction, transform, coefficient, skip, and distortion selection. /// - private long EvaluateInterCandidate( + private Av1RateDistortionStatistics EvaluateInterCandidate( Av1SymbolEncoder writer, Point blockOrigin, ushort tileIndex, @@ -1238,16 +1238,16 @@ internal static partial class Av1IntraSuperblockEncoder redRate; long codedDistortion = lumaDistortion + blueDistortion + redDistortion; - long selectedCost = Av1RateDistortion.GetCost(this.rateMultiplier, codedRate, codedDistortion); + Av1RateDistortionStatistics selectedStatistics = new(this.rateMultiplier, codedRate, codedDistortion); skip = false; if (hasEmptyLuma && hasEmptyBlue && hasEmptyRed) { int skipRate = predictionRate + writer.GetSkipCost(true, skipContext); long skipDistortion = emptyLumaDistortion + emptyBlueDistortion + emptyRedDistortion; - long skipCost = Av1RateDistortion.GetCost(this.rateMultiplier, skipRate, skipDistortion); - if (skipCost < selectedCost) + Av1RateDistortionStatistics skipStatistics = new(this.rateMultiplier, skipRate, skipDistortion); + if (skipStatistics.Cost < selectedStatistics.Cost) { - selectedCost = skipCost; + selectedStatistics = skipStatistics; skip = true; workspace.LumaPrediction[..LumaTransformSize.GetSize2d()].CopyTo(lumaReconstruction); lumaCoefficients[..LumaTransformSize.GetSize2d()].Clear(); @@ -1265,7 +1265,7 @@ internal static partial class Av1IntraSuperblockEncoder } } - return selectedCost; + return selectedStatistics; } /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs index bbaddfb107..b9187859a9 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs @@ -817,6 +817,29 @@ public class Av1EntropyTests Assert.Equal(before, after); } + [Theory] + [InlineData(128, 2, 0L, 2, 0L, 1L)] + [InlineData(128, 1, 0L, 1, 0L, 1L)] + [InlineData(64, 512, 2_000_000_000L, 512, 2_000_000_000L, 512_000_000_128L)] + public void RateDistortionStatisticsRoundCombinedRate( + int multiplier, + int firstRate, + long firstDistortion, + int secondRate, + long secondDistortion, + long expectedCost) + { + // The first two cases cross opposite sides of the half-unit boundary: independently rounded + // child costs would be two and zero, while the combined reference cost is one in both cases. + Av1RateDistortionStatistics combined = new(multiplier, firstRate, firstDistortion); + Av1RateDistortionStatistics second = new(multiplier, secondRate, secondDistortion); + combined.Add(multiplier, in second); + + Assert.Equal(firstRate + secondRate, combined.Rate); + Assert.Equal(firstDistortion + secondDistortion, combined.Distortion); + Assert.Equal(expectedCost, combined.Cost); + } + [Theory] [InlineData(1, 255, 0L, 0L)] [InlineData(1, 256, 0L, 1L)] diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs index 7d5c558aea..36f25efbb1 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs @@ -888,6 +888,171 @@ public class Av1IntraSuperblockEncoderTests Assert.InRange(reconstructedSample, (ushort)(byte.MaxValue + 1), (ushort)4095); } + [Theory] + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + public void BlockDecisionRetainsUnroundedRateAndDistortion(bool isMonochrome, bool textured) + { + const int Width = 8; + const int Height = 8; + const int QIndex = 37; + Av1ColorFormat colorFormat = isMonochrome ? Av1ColorFormat.Yuv400 : Av1ColorFormat.Yuv420; + ObuColorConfig colorConfig = new() + { + IsMonochrome = isMonochrome, + SubSamplingX = true, + SubSamplingY = true, + BitDepth = Av1BitDepth.EightBit + }; + + using Av1EncoderFrameBuffer source = new(Configuration.Default, Width, Height, 8, colorFormat, 1, 1); + using Av1EncoderFrameBuffer reconstruction = new(Configuration.Default, Width, Height, 8, colorFormat, 1, 1); + int planeCount = isMonochrome ? 1 : 3; + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) + { + Buffer2DRegion plane = source.Frame.CodedView.GetPlane((Av1Plane)planeIndex); + int length = planeIndex == 0 ? 8 : 4; + for (int y = 0; y < length; y++) + { + Span row = plane.DangerousGetRowSpan(y); + for (int x = 0; x < length; x++) + { + row[x] = textured ? (byte)(114 + (((x * 13) + (y * 7) + (planeIndex * 5)) % 29)) : (byte)128; + } + } + } + + using Av1EncoderModeInfoBuffer modeInfoBuffer = new(Configuration.Default, Width, Height, disallow4x4AllFrames: true); + Av1PictureControlSet template = CreatePicture(modeInfoBuffer, colorConfig, use128x128Superblock: false, QIndex); + template.Parent.FrameHeader.TransformMode = Av1TransformMode.Largest; + using Av1EncoderPictureBuffer pictureBuffer = new( + Configuration.Default, + template.Sequence.SequenceHeader, + template.Parent.FrameHeader, + Width, + Height, + disallow4x4AllFrames: true); + + Av1PictureControlSet picture = pictureBuffer.Picture; + using Av1EncoderCoefficientBuffer coefficients = new(Configuration.Default, template.Sequence.SequenceHeader, Width, Height); + using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); + using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); + Av1Superblock superblock = new() + { + Workspace = superblockWorkspace, + TileInfo = new Av1TileInfo(0, 0, picture.Parent.FrameHeader), + Index = 0 + }; + + Av1IntraSuperblockEncoder.Prepare(picture, superblock, Point.Empty); + Av1MacroBlockD macroBlock = new() { Tile = superblock.TileInfo }; + Av1TileWriter.SetModeInfoRowAndColumn( + picture, + macroBlock, + superblock.TileInfo, + Point.Empty, + Av1BlockSize.Block8x8, + picture.Parent.Common.ModeInfoStride, + picture.Parent.Common.ModeInfoRowCount, + picture.Parent.Common.ModeInfoColumnCount); + + Av1IntraSuperblockEncoder.ModeDecision decision = new( + source.Frame, + reconstruction.Frame, + reconstruction.Frame, + picture, + superblock, + coefficients, + blockWorkspace, + effort: 0); + + ref Av1MacroBlockModeInfo modeInfo = ref picture.GetMacroBlockModeInfo(Point.Empty); + Av1EncoderBlockStruct block = default; + Av1EncoderPaletteInfo palette = default; + using Av1SymbolEncoder writer = new(Configuration.Default, 256, QIndex, updateCdf: true); + decision.EncodeBlock(writer, macroBlock, Point.Empty, 0, ref modeInfo, ref block, ref palette); + + Assert.Equal(Av1PredictionMode.DC, modeInfo.Block.Mode); + Assert.Equal(Av1TransformSize.Size8x8, modeInfo.Block.TransformSize); + Assert.False(modeInfo.Block.Skip); + int expectedRate = writer.GetSkipCost(false, Av1TileWriter.GetSkipContext(macroBlock)); + expectedRate += Av1TileWriter.GetLumaModeCost( + writer, + macroBlock, + Av1BlockSize.Block8x8, + Av1PredictionMode.DC, + 0, + isIntraFrame: true); + + if (!isMonochrome) + { + Assert.Equal(Av1ChromaPredictionMode.DC, modeInfo.Block.UvMode); + expectedRate += Av1TileWriter.GetChromaModeCost( + writer, + picture.Parent.FrameHeader, + colorConfig, + modeInfo, + Av1BlockSize.Block8x8, + Av1PredictionMode.DC, + Av1ChromaPredictionMode.DC, + 0); + } + + long squaredError = 0; + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) + { + Av1Plane plane = (Av1Plane)planeIndex; + Av1TransformSize transformSize = planeIndex == 0 ? Av1TransformSize.Size8x8 : Av1TransformSize.Size4x4; + Av1BlockSize blockSize = planeIndex == 0 ? Av1BlockSize.Block8x8 : Av1BlockSize.Block4x4; + Av1ComponentType component = planeIndex == 0 ? Av1ComponentType.Luminance : Av1ComponentType.Chroma; + Av1NeighborArrayUnit neighbors = planeIndex switch + { + 0 => picture.LuminanceDcSignLevelCoefficientNeighbors[0], + 1 => picture.CbDcSignLevelCoefficientNeighbors[0], + _ => picture.CrDcSignLevelCoefficientNeighbors[0] + }; + + Av1EncoderTransformBlockState state = coefficients.GetTransformBlockSpan(0, plane)[0]; + expectedRate += writer.GetCoefficientCost( + transformSize, + state.TransformType, + Av1PredictionMode.DC, + coefficients.GetPlaneSpan(0, plane)[..transformSize.GetSize2d()], + component, + Av1TileWriter.GetTransformBlockContexts(component, neighbors, Point.Empty, blockSize, transformSize), + state.EndOfBlock, + picture.Parent.FrameHeader.UseReducedTransformSet, + Av1FilterIntraMode.AllFilterIntraModes, + usesInterTransformSet: false); + + Buffer2DRegion sourcePlane = source.Frame.CodedView.GetPlane(plane); + Buffer2DRegion reconstructedPlane = reconstruction.Frame.CodedView.GetPlane(plane); + int length = planeIndex == 0 ? 8 : 4; + for (int y = 0; y < length; y++) + { + ReadOnlySpan sourceRow = sourcePlane.DangerousGetRowSpan(y); + ReadOnlySpan reconstructedRow = reconstructedPlane.DangerousGetRowSpan(y); + for (int x = 0; x < length; x++) + { + int difference = sourceRow[x] - reconstructedRow[x]; + squaredError += difference * difference; + } + } + } + + // Pixel-domain SSE uses the reference's four fractional distortion bits. The probability rate + // is rounded after all planes and block syntax have been counted, before adding scaled distortion. + long expectedDistortion = squaredError * 16; + int multiplier = Av1RateDistortion.GetKeyFrameRateMultiplier(QIndex, Av1BitDepth.EightBit); + long expectedCost = ((((long)expectedRate * multiplier) + 256) / 512) + (expectedDistortion * 128); + Assert.Equal(textured, squaredError > 0); + Assert.Equal(expectedRate, decision.SelectedBlockStatistics.Rate); + Assert.Equal(expectedDistortion, decision.SelectedBlockStatistics.Distortion); + Assert.Equal(expectedCost, decision.SelectedBlockStatistics.Cost); + } + [Fact] public void BlockDecisionObservesLiveCdfInWriterOrder() {