From d78734dc763da46257deeee8b7969f289dac0fe3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 7 Sep 2026 13:36:42 +1000 Subject: [PATCH] Normalize high-bit-depth AV1 intra-copy search SAD Scale each completed absolute-difference sum into the eight-bit motion-rate domain before comparing candidates, including batched mesh candidates. Verified in the current worktree with Release .NET 11 and serialized Visual Studio VSTest: 138 focused cases passed, including 10-bit and 12-bit candidate-ranking regressions. Decoder comparison of 12 freshly encoded streams was sample exact across 21,348 samples. The regression test additions remain with the pending explicit-border constructor changes. This checkpoint does not establish encoder parity or complete motion search. --- .../Motion/Av1IntraBlockCopySearchIndex.cs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopySearchIndex.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopySearchIndex.cs index f92b4f92fb..ffcc73d5c1 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopySearchIndex.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopySearchIndex.cs @@ -686,6 +686,9 @@ internal readonly struct Av1IntraBlockCopySearchIndex where TSample : unmanaged where TOperation : struct, ISearchOperation { + // Sample operators retain native precision. Truncate the complete SAD into the eight-bit domain before + // adding motion rate; scaling only the rate would change integer rounding and candidate ties. + int sadShift = bitDepth.GetBitCount() - 8; SearchDiamond( source, reconstruction, @@ -693,6 +696,7 @@ internal readonly struct Av1IntraBlockCopySearchIndex writer, reference, sadPerBit, + sadShift, searchStepParameter, minimumColumnOffset, minimumRowOffset, @@ -724,6 +728,7 @@ internal readonly struct Av1IntraBlockCopySearchIndex writer, reference, sadPerBit, + sadShift, searchStepParameter + shortenedBy, minimumColumnOffset, minimumRowOffset, @@ -763,6 +768,7 @@ internal readonly struct Av1IntraBlockCopySearchIndex writer, reference, sadPerBit, + sadShift, minimumColumnOffset, minimumRowOffset, maximumColumnOffset, @@ -795,6 +801,7 @@ internal readonly struct Av1IntraBlockCopySearchIndex Av1SymbolEncoder writer, Av1MotionVector reference, int sadPerBit, + int sadShift, int searchStepParameter, int minimumColumnOffset, int minimumRowOffset, @@ -816,6 +823,7 @@ internal readonly struct Av1IntraBlockCopySearchIndex writer, reference, sadPerBit, + sadShift, best); for (int stage = SearchRadii.Length - 1 - searchStepParameter; stage >= 0; stage--) @@ -839,7 +847,7 @@ internal readonly struct Av1IntraBlockCopySearchIndex source, blockOrigin, reconstruction, - predictionOrigin); + predictionOrigin) >> sadShift; // Motion-vector cost is nonnegative, so a raw absolute difference that already reaches the // best combined cost cannot win and does not need an entropy-rate lookup. @@ -891,6 +899,7 @@ internal readonly struct Av1IntraBlockCopySearchIndex Av1SymbolEncoder writer, Av1MotionVector reference, int sadPerBit, + int sadShift, int minimumColumnOffset, int minimumRowOffset, int maximumColumnOffset, @@ -907,6 +916,7 @@ internal readonly struct Av1IntraBlockCopySearchIndex writer, reference, sadPerBit, + sadShift, start); int startColumn = Math.Max(-ExhaustiveSearchRange, minimumColumnOffset - start.X); @@ -924,8 +934,8 @@ internal readonly struct Av1IntraBlockCopySearchIndex blockOrigin.X + firstCandidate.X, blockOrigin.Y + firstCandidate.Y); - // Four adjacent candidates share the source load and row traversal, matching the batch width - // used by the native full-resolution pass without allocating temporary candidate buffers. + // Four adjacent candidates share the source load and row traversal. Normalize each complete sum + // independently so batching preserves individual candidate costs and their tie order. TOperation.GetFourSumsOfAbsoluteDifferences( source, blockOrigin, @@ -935,7 +945,7 @@ internal readonly struct Av1IntraBlockCopySearchIndex for (int i = 0; i < ExhaustiveSearchBatchSize; i++) { - int sumOfAbsoluteDifferences = sumsOfAbsoluteDifferences[i]; + int sumOfAbsoluteDifferences = sumsOfAbsoluteDifferences[i] >> sadShift; if (sumOfAbsoluteDifferences >= bestCost) { continue; @@ -967,7 +977,7 @@ internal readonly struct Av1IntraBlockCopySearchIndex source, blockOrigin, reconstruction, - predictionOrigin); + predictionOrigin) >> sadShift; if (sumOfAbsoluteDifferences >= bestCost) { @@ -999,6 +1009,7 @@ internal readonly struct Av1IntraBlockCopySearchIndex Av1SymbolEncoder writer, Av1MotionVector reference, int sadPerBit, + int sadShift, Point candidate) where TSample : unmanaged where TOperation : struct, ISearchOperation @@ -1008,7 +1019,7 @@ internal readonly struct Av1IntraBlockCopySearchIndex source, blockOrigin, reconstruction, - predictionOrigin); + predictionOrigin) >> sadShift; Av1MotionVector vector = new(candidate.Y * 8, candidate.X * 8); int rate = writer.GetDisplacementVectorSearchCost(vector, reference);