diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorCosts.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorCosts.cs
new file mode 100644
index 0000000000..e89e0a39b2
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorCosts.cs
@@ -0,0 +1,205 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
+
+///
+/// Borrows the motion-vector rate tables retained by one encoder worker.
+///
+internal readonly ref struct Av1MotionVectorCosts
+{
+ ///
+ /// The largest representable signed component difference, in eighth-sample units.
+ ///
+ public const int MaximumComponent = (1 << 14) - 1;
+
+ private const int ComponentCount = (2 * MaximumComponent) + 1;
+
+ ///
+ /// Storage for the joint symbols and two component pairs, one for each fractional precision.
+ ///
+ public const int StorageLength = 4 + (4 * ComponentCount);
+
+ private readonly Span joint;
+ private readonly Span row;
+ private readonly Span column;
+ private readonly Av1MotionVectorPrecision precision;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// Worker-lifetime storage containing both precision tables.
+ /// The precision used by the current frame.
+ public Av1MotionVectorCosts(Span storage, Av1MotionVectorPrecision precision)
+ {
+ // Integer and quarter-sample frames share one pair. The eighth-sample pair remains separate so changing
+ // frame precision does not change the worker's allocation or the layout of its other scratch regions.
+ int offset = 4 + (precision == Av1MotionVectorPrecision.EighthSample ? 2 * ComponentCount : 0);
+ this.joint = storage[..4];
+ this.row = storage.Slice(offset, ComponentCount);
+ this.column = storage.Slice(offset + ComponentCount, ComponentCount);
+ this.precision = precision;
+ }
+
+ ///
+ /// Captures component rates from the current tile distributions without adapting them.
+ ///
+ /// The tile's current motion-vector distributions.
+ public void Fill(Av1MotionVectorContext context)
+ {
+ for (int i = 0; i < 4; i++)
+ {
+ this.joint[i] = Av1ProbabilityCost.GetSymbolCost(context.Joint, i);
+ }
+
+ FillComponent(this.row, context.Vertical, this.precision);
+ FillComponent(this.column, context.Horizontal, this.precision);
+ }
+
+ ///
+ /// Measures a candidate against its differential reference using the captured distributions.
+ ///
+ /// The candidate vector, in eighth-sample units.
+ /// The differential reference, in eighth-sample units.
+ /// The rate in 1/512-bit units.
+ public int GetCost(Av1MotionVector value, Av1MotionVector reference)
+ {
+ int rowDifference = value.Row - reference.Row;
+ int columnDifference = value.Column - reference.Column;
+ int jointType = (rowDifference != 0 ? 2 : 0) | (columnDifference != 0 ? 1 : 0);
+ return this.joint[jointType] + this.row[MaximumComponent + rowDifference] + this.column[MaximumComponent + columnDifference];
+ }
+
+ ///
+ /// Builds a signed component table by reusing the costs of shorter binary magnitudes.
+ ///
+ /// The complete signed component table, centered on zero.
+ /// The distributions for this axis.
+ /// The fractional symbols present in the frame.
+ private static void FillComponent(Span destination, Av1MotionVectorContext.Component component, Av1MotionVectorPrecision precision)
+ {
+ Span classCosts = stackalloc int[11];
+ Span bitCosts = stackalloc int[20];
+ Span fractionalCosts = stackalloc int[4];
+ Span highPrecisionCosts = stackalloc int[2];
+ Span costOffsets = stackalloc int[10];
+ int positiveSignCost = Av1ProbabilityCost.GetSymbolCost(component.Sign, 0);
+ int negativeSignCost = Av1ProbabilityCost.GetSymbolCost(component.Sign, 1);
+ int signDifference = negativeSignCost - positiveSignCost;
+
+ for (int i = 0; i < classCosts.Length; i++)
+ {
+ classCosts[i] = Av1ProbabilityCost.GetSymbolCost(component.MagnitudeClass, i);
+ }
+
+ for (int i = 0; i < costOffsets.Length; i++)
+ {
+ bitCosts[2 * i] = Av1ProbabilityCost.GetSymbolCost(component.OffsetBits[i], 0);
+ bitCosts[(2 * i) + 1] = Av1ProbabilityCost.GetSymbolCost(component.OffsetBits[i], 1);
+ }
+
+ // Omitting fractional symbols gives them zero rate. All entries are assigned explicitly because stack
+ // storage is uninitialized, including when integer motion disables both fractional syntax stages.
+ for (int i = 0; i < fractionalCosts.Length; i++)
+ {
+ fractionalCosts[i] = precision == Av1MotionVectorPrecision.Integer ? 0 : Av1ProbabilityCost.GetSymbolCost(component.Fractional, i);
+ }
+
+ for (int i = 0; i < highPrecisionCosts.Length; i++)
+ {
+ highPrecisionCosts[i] = precision == Av1MotionVectorPrecision.EighthSample
+ ? Av1ProbabilityCost.GetSymbolCost(component.HighPrecision, i)
+ : 0;
+ }
+
+ costOffsets[0] = 0;
+ for (int i = 1; i < costOffsets.Length; i++)
+ {
+ // A shorter magnitude's leading one becomes an offset bit in a larger magnitude. Remove its
+ // former class rate and insert that bit's rate before adding the new magnitude class below.
+ costOffsets[i] = bitCosts[(2 * (i - 1)) + 1] - (i > 1 ? classCosts[i - 1] : 0);
+ }
+
+ destination[MaximumComponent] = 0;
+ for (int fractional = 0; fractional < 4; fractional++)
+ {
+ for (int highPrecision = 0; highPrecision < 2; highPrecision++)
+ {
+ int magnitude = (2 * fractional) + highPrecision + 1;
+ destination[MaximumComponent + magnitude] = fractionalCosts[fractional] + highPrecisionCosts[highPrecision] + positiveSignCost;
+ }
+ }
+
+ // Magnitudes encode value minus one. Each exponent doubles the integer offset range, reusing the
+ // previously completed lower half. The first eight entries temporarily carry fractional and sign rates
+ // alone; class-zero syntax is installed only after all larger magnitudes have consumed those seeds.
+ for (int exponentIndex = 0; exponentIndex < 10; exponentIndex++)
+ {
+ int exponent = 8 << exponentIndex;
+ int classCost = exponentIndex >= 1 ? classCosts[exponentIndex] : 0;
+ int mantissa = 0;
+ for (int bit = 0; bit <= exponentIndex; bit++)
+ {
+ for (; mantissa < (8 << bit); mantissa++)
+ {
+ int cost = destination[MaximumComponent + mantissa + 1] + classCost + costOffsets[bit];
+ int magnitude = exponent + mantissa + 1;
+ destination[MaximumComponent + magnitude] = cost;
+ destination[MaximumComponent - magnitude] = cost + signDifference;
+ }
+
+ // The next exponent introduces one more leading zero in this mantissa group.
+ costOffsets[bit] += bitCosts[2 * exponentIndex];
+ }
+ }
+
+ // The final exponent ends at 16383, one entry before the next power of two. Treat its upper mantissa
+ // separately to avoid producing the unrepresentable magnitude 16384 or reading an eleventh offset bit.
+ int finalMantissa = 0;
+ for (int bit = 0; bit < 10; bit++)
+ {
+ for (; finalMantissa < (8 << bit); finalMantissa++)
+ {
+ int cost = destination[MaximumComponent + finalMantissa + 1] + classCosts[10] + costOffsets[bit];
+ int magnitude = 8192 + finalMantissa + 1;
+ destination[MaximumComponent + magnitude] = cost;
+ destination[MaximumComponent - magnitude] = cost + signDifference;
+ }
+ }
+
+ int finalOffset = bitCosts[19] - classCosts[9];
+ for (; finalMantissa < 8191; finalMantissa++)
+ {
+ int cost = destination[MaximumComponent + finalMantissa + 1] + classCosts[10] + finalOffset;
+ int magnitude = 8192 + finalMantissa + 1;
+ destination[MaximumComponent + magnitude] = cost;
+ destination[MaximumComponent - magnitude] = cost + signDifference;
+ }
+
+ for (int integerOffset = 0; integerOffset < 2; integerOffset++)
+ {
+ int classZeroCost = classCosts[0] + Av1ProbabilityCost.GetSymbolCost(component.ClassZero, integerOffset);
+ for (int fractional = 0; fractional < 4; fractional++)
+ {
+ int cost = classZeroCost;
+ if (precision != Av1MotionVectorPrecision.Integer)
+ {
+ cost += Av1ProbabilityCost.GetSymbolCost(component.ClassZeroFractional[integerOffset], fractional);
+ }
+
+ for (int highPrecision = 0; highPrecision < 2; highPrecision++)
+ {
+ int magnitude = (8 * integerOffset) + (2 * fractional) + highPrecision + 1;
+ int fractionalCost = precision == Av1MotionVectorPrecision.EighthSample
+ ? Av1ProbabilityCost.GetSymbolCost(component.ClassZeroHighPrecision, highPrecision)
+ : 0;
+
+ destination[MaximumComponent + magnitude] = cost + fractionalCost + positiveSignCost;
+ destination[MaximumComponent - magnitude] = cost + fractionalCost + negativeSignCost;
+ }
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
index 1720c5898e..1d6fb6b805 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
@@ -900,6 +900,12 @@ internal sealed class Av1SymbolEncoder : IDisposable
w.WriteSymbol(advance, this.dynamicReferenceList[context]);
}
+ ///
+ /// Captures the current motion-vector distributions for a subsequent motion-search interval.
+ ///
+ /// The worker-owned rate tables to refresh.
+ public void FillMotionVectorCosts(Av1MotionVectorCosts costs) => costs.Fill(this.motionVector);
+
///
/// Measures an inter motion vector relative to its selected stack reference.
///
diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.ByteOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.ByteOperator.cs
new file mode 100644
index 0000000000..0bbcf15271
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.ByteOperator.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
+
+internal static partial class Av1MotionSearchBase
+{
+ ///
+ /// Measures eight-bit sample errors with the shared vector-width residual traversal.
+ ///
+ public readonly struct ByteOperator : IMotionSearchOperator
+ {
+ ///
+ public static void PreparePrediction(
+ ReadOnlySpan source,
+ int sourceStride,
+ ReadOnlySpan reference,
+ int referenceStride,
+ int referenceOrigin,
+ Span prediction,
+ Span residual,
+ Span scratch,
+ int width,
+ int height,
+ Av1InterpolationFilter horizontalFilter,
+ Av1InterpolationFilter verticalFilter,
+ int horizontalPhase,
+ int verticalPhase,
+ int bitDepth)
+ {
+ Av1TranslationalInterPredictor.Predict(
+ reference,
+ referenceStride,
+ referenceOrigin,
+ prediction,
+ width,
+ width,
+ height,
+ horizontalFilter,
+ verticalFilter,
+ horizontalPhase,
+ verticalPhase,
+ scratch);
+
+ Av1ResidualBuilder.Subtract(source, sourceStride, prediction, width, residual, width, width, height);
+ }
+
+ ///
+ public static void Predict(
+ ReadOnlySpan reference,
+ int referenceStride,
+ int referenceOrigin,
+ Span buffer,
+ int width,
+ int height,
+ int horizontalPhase,
+ int verticalPhase,
+ int taps,
+ int bitDepth)
+ => Av1TranslationalInterPredictor.PredictForSearch(
+ reference, referenceStride, referenceOrigin, buffer, width, height, horizontalPhase, verticalPhase, taps);
+
+ ///
+ public static int SumAbsoluteDifferences(
+ ReadOnlySpan source,
+ int sourceStride,
+ ReadOnlySpan prediction,
+ int predictionStride,
+ int width,
+ int height,
+ int rowStep)
+ => Av1ResidualBuilder.SumAbsoluteDifferences(source, sourceStride, prediction, predictionStride, width, height, rowStep);
+
+ ///
+ public static void GetMoments(
+ ReadOnlySpan source,
+ int sourceStride,
+ ReadOnlySpan prediction,
+ int predictionStride,
+ int width,
+ int height,
+ out int sum,
+ out long squares)
+ => Av1ResidualBuilder.GetMoments(source, sourceStride, prediction, predictionStride, width, height, out sum, out squares);
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.Fractional.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.Fractional.cs
new file mode 100644
index 0000000000..c2cf4e2e01
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.Fractional.cs
@@ -0,0 +1,340 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
+using static SixLabors.ImageSharp.Formats.Heif.Av1.Motion.Av1MotionSearchSettings;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
+
+internal static partial class Av1MotionSearchBase
+{
+ ///
+ /// Retains the fractional winner and the error statistics used to select it.
+ ///
+ public readonly struct FractionalResult
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The displacement in eighth-sample units.
+ /// The normalized residual variance.
+ /// The normalized squared residual sum.
+ /// The variance-domain rate cost.
+ public FractionalResult(Av1MotionVector vector, int variance, int squaredError, int motionCost)
+ {
+ this.Vector = vector;
+ this.Variance = variance;
+ this.SquaredError = squaredError;
+ this.MotionCost = motionCost;
+ }
+
+ ///
+ /// Gets the displacement in eighth-sample units.
+ ///
+ public Av1MotionVector Vector { get; }
+
+ ///
+ /// Gets the normalized residual variance.
+ ///
+ public int Variance { get; }
+
+ ///
+ /// Gets the normalized squared residual sum.
+ ///
+ public int SquaredError { get; }
+
+ ///
+ /// Gets the variance-domain rate cost.
+ ///
+ public int MotionCost { get; }
+
+ ///
+ /// Gets the combined selection cost.
+ ///
+ public int Cost => this.Variance + this.MotionCost;
+ }
+
+ ///
+ /// Refines an unscaled reference prediction while borrowing the worker's sample and entropy storage.
+ ///
+ /// The unsigned sample storage type.
+ /// The closed prediction and error operator.
+ public readonly ref struct FractionalSearch
+ where TSample : unmanaged
+ where TOperator : struct, IMotionSearchOperator
+ {
+ private readonly ReadOnlySpan source;
+ private readonly ReadOnlySpan reference;
+ private readonly Span prediction;
+ private readonly int sourceStride;
+ private readonly int referenceStride;
+ private readonly int referenceOrigin;
+ private readonly Size blockSize;
+ private readonly Rectangle bounds;
+ private readonly Av1MotionVector referenceVector;
+ private readonly Av1MotionVectorCosts costs;
+ private readonly int bitDepth;
+ private readonly int rateMultiplier;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// Source samples beginning at the block origin.
+ /// The source row stride in samples.
+ /// The complete bordered reference plane.
+ /// The reference row stride in samples.
+ /// The reference index corresponding to the current block origin.
+ /// The worker's reusable fractional prediction buffer.
+ /// The prediction dimensions.
+ /// The permitted eighth-sample displacements, with exclusive upper edges.
+ /// The spatial entropy reference in eighth-sample units.
+ /// The retained motion-rate tables.
+ /// The coded component precision.
+ /// The block rate multiplier.
+ public FractionalSearch(
+ ReadOnlySpan source,
+ int sourceStride,
+ ReadOnlySpan reference,
+ int referenceStride,
+ int referenceOrigin,
+ Span prediction,
+ Size blockSize,
+ Rectangle bounds,
+ Av1MotionVector referenceVector,
+ Av1MotionVectorCosts costs,
+ Av1BitDepth bitDepth,
+ int rateMultiplier)
+ {
+ this.source = source;
+ this.sourceStride = sourceStride;
+ this.reference = reference;
+ this.referenceStride = referenceStride;
+ this.referenceOrigin = referenceOrigin;
+ this.prediction = prediction;
+ this.blockSize = blockSize;
+ this.bounds = bounds;
+ this.referenceVector = referenceVector;
+ this.costs = costs;
+ this.bitDepth = bitDepth.GetBitCount();
+ this.rateMultiplier = rateMultiplier;
+ }
+
+ ///
+ /// Runs the selected fractional tree and publishes its retained winner.
+ ///
+ /// The initial eighth-sample displacement.
+ /// Integer-search statistics when that starting prediction was already measured.
+ /// The fractional decision policy.
+ /// The finest permitted search step.
+ /// Whether eighth-sample candidates are enabled by the frame.
+ /// The number of refinement levels at each precision.
+ /// The full tree's interpolation tap count.
+ /// The integer center, left, down, right, and up costs, or an empty span when unavailable.
+ /// Three retained precision centers, or an empty span when duplicate pruning is disabled.
+ /// The selected vector and its error statistics.
+ /// The selected cost, or when a previously searched center terminates the path.
+ public int Search(
+ Av1MotionVector start,
+ FullPixelResult? startStatistics,
+ FractionalSearchMethod method,
+ SearchPrecision precision,
+ bool allowHighPrecision,
+ int iterationsPerStep,
+ int taps,
+ ReadOnlySpan costList,
+ Span previousCenters,
+ out FractionalResult result)
+ {
+ // Integer search has already paid for these moments. Retain that exact error domain, including
+ // its signed high-depth rounding, until a fractional candidate strictly improves the total cost.
+ if (startStatistics.HasValue)
+ {
+ FullPixelResult statistics = startStatistics.Value;
+ result = new FractionalResult(start, statistics.Variance, statistics.SquaredError, statistics.MotionCost);
+ }
+ else
+ {
+ result = this.Measure(start, method == FractionalSearchMethod.TwoLevelTree ? taps : 2);
+ }
+
+ int rounds = Math.Min(3 - (int)precision, allowHighPrecision ? 3 : 2);
+ for (int iteration = 0, step = 4; iteration < rounds; iteration++, step >>= 1)
+ {
+ Av1MotionVector center = result.Vector;
+ if (!previousCenters.IsEmpty)
+ {
+ // Each slot belongs to one precision. Another starting candidate reaching the same
+ // center has the same remaining tree, so the caller can discard this duplicate path.
+ if (previousCenters[iteration] == center)
+ {
+ return int.MaxValue;
+ }
+
+ previousCenters[iteration] = center;
+ }
+
+ bool finiteNeighborhood = costList.Length == 5
+ && costList[0] != int.MaxValue
+ && costList[1] != int.MaxValue
+ && costList[2] != int.MaxValue
+ && costList[3] != int.MaxValue
+ && costList[4] != int.MaxValue;
+
+ if (iteration == 0 && method == FractionalSearchMethod.PrunedTree && finiteNeighborhood)
+ {
+ // Half-sample pruning chooses one quadrant from the integer cost surface. Ties select
+ // right and up here; the measured-cardinal tree below instead breaks ties left and up.
+ int column = costList[1] < costList[3] ? -step : step;
+ int row = costList[2] < costList[4] ? step : -step;
+ this.Check(new Av1MotionVector(center.Row, center.Column + column), 2, ref result);
+ this.Check(new Av1MotionVector(center.Row + row, center.Column), 2, ref result);
+ this.Check(new Av1MotionVector(center.Row + row, center.Column + column), 2, ref result);
+ continue;
+ }
+
+ if (iteration == 0 && method == FractionalSearchMethod.MorePrunedTree && finiteNeighborhood
+ && costList[0] < costList[1] && costList[0] < costList[2]
+ && costList[0] < costList[3] && costList[0] < costList[4])
+ {
+ // A strictly lower center gives positive curvature on both axes. The minimum of each
+ // fitted parabola is (negative-side cost - positive-side cost) / (2 * curvature).
+ // Multiplying that location by two gives half-sample units; signed division rounds
+ // the displacement to the nearest such unit before converting it to eighth samples.
+ int columnNumerator = costList[1] - costList[3];
+ int columnDenominator = costList[1] - (2 * costList[0]) + costList[3];
+ int rowNumerator = costList[4] - costList[2];
+ int rowDenominator = costList[4] - (2 * costList[0]) + costList[2];
+ int column = (columnNumerator + (columnNumerator < 0 ? -columnDenominator / 2 : columnDenominator / 2))
+ / columnDenominator;
+ int row = (rowNumerator + (rowNumerator < 0 ? -rowDenominator / 2 : rowDenominator / 2)) / rowDenominator;
+
+ if ((row | column) != 0)
+ {
+ this.Check(new Av1MotionVector(center.Row + (row * step), center.Column + (column * step)), 2, ref result);
+ }
+
+ continue;
+ }
+
+ int selectedTaps = method == FractionalSearchMethod.TwoLevelTree ? taps : 2;
+ int left = this.Check(new Av1MotionVector(center.Row, center.Column - step), selectedTaps, ref result);
+ int right = this.Check(new Av1MotionVector(center.Row, center.Column + step), selectedTaps, ref result);
+ int up = this.Check(new Av1MotionVector(center.Row - step, center.Column), selectedTaps, ref result);
+ int down = this.Check(new Av1MotionVector(center.Row + step, center.Column), selectedTaps, ref result);
+ int diagonalRow = up <= down ? -step : step;
+ int diagonalColumn = left <= right ? -step : step;
+ this.Check(new Av1MotionVector(center.Row + diagonalRow, center.Column + diagonalColumn), selectedTaps, ref result);
+
+ if (iterationsPerStep > 1 && result.Vector != center)
+ {
+ // All second-level sites are anchored to the first-level winner. Updating that winner
+ // while measuring these sites must not move the remaining sites of the same level.
+ Av1MotionVector winner = result.Vector;
+ if (method == FractionalSearchMethod.TwoLevelTree)
+ {
+ if (winner.Row == center.Row)
+ {
+ diagonalRow = -diagonalRow;
+ }
+ else if (winner.Column == center.Column)
+ {
+ diagonalColumn = -diagonalColumn;
+ }
+
+ int previousCost = result.Cost;
+ this.Check(new Av1MotionVector(winner.Row + diagonalRow, winner.Column), selectedTaps, ref result);
+ this.Check(new Av1MotionVector(winner.Row, winner.Column + diagonalColumn), selectedTaps, ref result);
+
+ // Extend to the outward diagonal only when an outward cardinal site improved.
+ if (result.Cost < previousCost)
+ {
+ this.Check(new Av1MotionVector(winner.Row + diagonalRow, winner.Column + diagonalColumn), selectedTaps, ref result);
+ }
+ }
+ else if (winner.Row != center.Row && winner.Column != center.Column)
+ {
+ this.Check(new Av1MotionVector(winner.Row, winner.Column + diagonalColumn), 2, ref result);
+ this.Check(new Av1MotionVector(winner.Row + diagonalRow, winner.Column), 2, ref result);
+ }
+ else if (winner.Row == center.Row)
+ {
+ this.Check(new Av1MotionVector(winner.Row + step, winner.Column + diagonalColumn), 2, ref result);
+ this.Check(new Av1MotionVector(winner.Row - step, winner.Column + diagonalColumn), 2, ref result);
+ this.Check(new Av1MotionVector(winner.Row - diagonalRow, winner.Column), 2, ref result);
+ }
+ else
+ {
+ this.Check(new Av1MotionVector(winner.Row + diagonalRow, winner.Column + step), 2, ref result);
+ this.Check(new Av1MotionVector(winner.Row + diagonalRow, winner.Column - step), 2, ref result);
+ this.Check(new Av1MotionVector(winner.Row, winner.Column - diagonalColumn), 2, ref result);
+ }
+ }
+ }
+
+ return result.Cost;
+ }
+
+ ///
+ /// Measures an in-range candidate and replaces the retained winner only for a strictly smaller cost.
+ ///
+ private int Check(Av1MotionVector vector, int taps, ref FractionalResult best)
+ {
+ if (!this.bounds.Contains(vector.Column, vector.Row))
+ {
+ return int.MaxValue;
+ }
+
+ FractionalResult candidate = this.Measure(vector, taps);
+ if (candidate.Cost < best.Cost)
+ {
+ best = candidate;
+ }
+
+ return candidate.Cost;
+ }
+
+ ///
+ /// Filters the borrowed reference and measures prediction-minus-source moments in the search error domain.
+ ///
+ private FractionalResult Measure(Av1MotionVector vector, int taps)
+ {
+ int referenceIndex = this.referenceOrigin + ((vector.Row >> 3) * this.referenceStride) + (vector.Column >> 3);
+ TOperator.Predict(
+ this.reference,
+ this.referenceStride,
+ referenceIndex,
+ this.prediction,
+ this.blockSize.Width,
+ this.blockSize.Height,
+ vector.Column & 7,
+ vector.Row & 7,
+ taps,
+ this.bitDepth);
+
+ TOperator.GetMoments(
+ this.prediction,
+ this.blockSize.Width,
+ this.source,
+ this.sourceStride,
+ this.blockSize.Width,
+ this.blockSize.Height,
+ out int sum,
+ out long squares);
+
+ int precisionShift = this.bitDepth - 8;
+ if (precisionShift != 0)
+ {
+ // Prediction is the first operand: signed rounding is asymmetric for negative residual
+ // sums. Normalize that sum and its squares independently before subtracting the mean.
+ sum = (sum + (1 << (precisionShift - 1))) >> precisionShift;
+ int squaredShift = precisionShift * 2;
+ squares = (squares + (1L << (squaredShift - 1))) >> squaredShift;
+ }
+
+ int variance = (int)Math.Max(squares - (((long)sum * sum) / (this.blockSize.Width * this.blockSize.Height)), 0);
+ int rate = this.costs.GetCost(vector, this.referenceVector);
+ int motionCost = Av1RateDistortion.GetMotionSearchCost(this.rateMultiplier, rate, 0);
+ return new FractionalResult(vector, variance, (int)squares, motionCost);
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.Operator.cs
new file mode 100644
index 0000000000..94741cdd7f
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.Operator.cs
@@ -0,0 +1,118 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
+
+internal static partial class Av1MotionSearchBase
+{
+ ///
+ /// Measures unsigned sample planes without changing the motion controller's error domains.
+ ///
+ /// The unsigned component storage type.
+ public interface IMotionSearchOperator
+ where TSample : unmanaged
+ {
+ ///
+ /// Builds the final inter predictor and its residual for transform-based winner selection.
+ ///
+ /// The source samples at the block origin.
+ /// The source row stride.
+ /// The complete bordered reference plane.
+ /// The reference row stride.
+ /// The displaced integer reference origin.
+ /// The packed prediction destination.
+ /// The packed residual destination.
+ /// The signed intermediate convolution storage.
+ /// The prediction width.
+ /// The prediction height.
+ /// The final horizontal interpolation family.
+ /// The final vertical interpolation family.
+ /// The horizontal phase in one-sixteenth-sample units.
+ /// The vertical phase in one-sixteenth-sample units.
+ /// The coded sample precision.
+ public static abstract void PreparePrediction(
+ ReadOnlySpan source,
+ int sourceStride,
+ ReadOnlySpan reference,
+ int referenceStride,
+ int referenceOrigin,
+ Span prediction,
+ Span residual,
+ Span scratch,
+ int width,
+ int height,
+ Av1InterpolationFilter horizontalFilter,
+ Av1InterpolationFilter verticalFilter,
+ int horizontalPhase,
+ int verticalPhase,
+ int bitDepth);
+
+ ///
+ /// Produces a packed fractional prediction with each filter pass rounded to sample precision.
+ ///
+ /// The bordered reference plane.
+ /// The reference row stride.
+ /// The integer prediction origin.
+ /// The borrowed prediction and intermediate buffer.
+ /// The block width.
+ /// The block height.
+ /// The horizontal eighth-sample phase.
+ /// The vertical eighth-sample phase.
+ /// The search filter's tap count.
+ /// The coded precision.
+ static abstract void Predict(
+ ReadOnlySpan reference,
+ int referenceStride,
+ int referenceOrigin,
+ Span buffer,
+ int width,
+ int height,
+ int horizontalPhase,
+ int verticalPhase,
+ int taps,
+ int bitDepth);
+
+ ///
+ /// Measures raw absolute differences, doubling alternate-row results before precision normalization.
+ ///
+ /// The source block samples.
+ /// The source row stride.
+ /// The prediction block samples.
+ /// The prediction row stride.
+ /// The block width.
+ /// The block height.
+ /// One for all rows or two for alternate rows.
+ /// The raw absolute-difference sum.
+ static abstract int SumAbsoluteDifferences(
+ ReadOnlySpan source,
+ int sourceStride,
+ ReadOnlySpan prediction,
+ int predictionStride,
+ int width,
+ int height,
+ int rowStep);
+
+ ///
+ /// Measures raw signed and squared residual sums without materializing a residual plane.
+ ///
+ /// The source block samples.
+ /// The source row stride.
+ /// The prediction block samples.
+ /// The prediction row stride.
+ /// The block width.
+ /// The block height.
+ /// The raw signed residual sum.
+ /// The raw squared residual sum.
+ static abstract void GetMoments(
+ ReadOnlySpan source,
+ int sourceStride,
+ ReadOnlySpan prediction,
+ int predictionStride,
+ int width,
+ int height,
+ out int sum,
+ out long squares);
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.SingleReference.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.SingleReference.cs
new file mode 100644
index 0000000000..0b0fe44f47
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.SingleReference.cs
@@ -0,0 +1,651 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
+using static SixLabors.ImageSharp.Formats.Heif.Av1.Motion.Av1MotionSearchSettings;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
+
+internal static partial class Av1MotionSearchBase
+{
+ ///
+ /// Converts a full-sample motion extent to the initial number of excluded search stages.
+ ///
+ /// The frame dimension or retained spatial motion magnitude.
+ /// The initial search-step parameter.
+ public static int GetInitialStepParameter(int size)
+ {
+ size = Math.Max(size, 16);
+ int step = 0;
+ while ((size << step) < 1023)
+ {
+ step++;
+ }
+
+ return Math.Min(step, 9);
+ }
+
+ ///
+ /// Collects weighted temporal starting vectors for one prediction block.
+ ///
+ /// The rounded spatial reference displacement in full samples.
+ /// The temporal analysis vectors at the block's analysis-grid origin.
+ /// The analysis row stride in vectors.
+ /// The number of analysis columns and rows covered by the block.
+ /// Storage for the spatial start and every covered analysis block.
+ /// The represented weight, or zero when analysis is incomplete.
+ /// The number of collected starting candidates.
+ public static int CollectStartingCandidates(
+ Point spatialStart,
+ ReadOnlySpan temporalVectors,
+ int temporalStride,
+ Size analysisSize,
+ Span candidates,
+ out int totalWeight)
+ {
+ candidates[0] = new StartingCandidate(spatialStart, 0);
+ totalWeight = 0;
+ int count = 1;
+ int analysisCount = analysisSize.Width * analysisSize.Height;
+ if (analysisCount != 0)
+ {
+ // The spatial start receives one vote per analysis block before temporal votes are added.
+ // It therefore remains among the first starts even when the temporal field is fragmented.
+ candidates[0] = new StartingCandidate(spatialStart, analysisCount);
+ for (int y = 0; y < analysisSize.Height; y++)
+ {
+ for (int x = 0; x < analysisSize.Width; x++)
+ {
+ Av1MotionVector vector = temporalVectors[(y * temporalStride) + x];
+ if (vector.Row == short.MinValue && vector.Column == short.MinValue)
+ {
+ // Analysis may end partway through a block. Retain the collected prefix, but do not
+ // apply completed-field weighting or reorder it as if all temporal votes were available.
+ return count;
+ }
+
+ Point position = new(vector.Column >> 3, vector.Row >> 3);
+ int rowGroup = (position.Y + 3 + (position.Y >= 0 ? 1 : 0)) >> 3;
+ int columnGroup = (position.X + 3 + (position.X >= 0 ? 1 : 0)) >> 3;
+ int index = 0;
+ for (; index < count; index++)
+ {
+ Point existing = candidates[index].Vector;
+
+ // Temporal starts are grouped into rounded eight-sample cells after conversion to
+ // full samples. Keep the first representative position while accumulating its votes.
+ if (((existing.Y + 3 + (existing.Y >= 0 ? 1 : 0)) >> 3) == rowGroup
+ && ((existing.X + 3 + (existing.X >= 0 ? 1 : 0)) >> 3) == columnGroup)
+ {
+ candidates[index] = new StartingCandidate(existing, candidates[index].Weight + 1);
+ break;
+ }
+ }
+
+ if (index == count)
+ {
+ candidates[count++] = new StartingCandidate(position, 1);
+ }
+ }
+ }
+
+ totalWeight = 2 * analysisCount;
+ if (count > 2)
+ {
+ candidates[..count].Sort(default(StartingCandidateWeightComparer));
+ }
+ }
+
+ return count;
+ }
+
+ ///
+ /// Holds one weighted full-sample starting position from spatial or temporal analysis.
+ ///
+ public readonly struct StartingCandidate
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The starting displacement in full samples.
+ /// The number of represented analysis blocks.
+ public StartingCandidate(Point vector, int weight)
+ {
+ this.Vector = vector;
+ this.Weight = weight;
+ }
+
+ ///
+ /// Gets the full-sample displacement.
+ ///
+ public Point Vector { get; }
+
+ ///
+ /// Gets the number of represented analysis blocks.
+ ///
+ public int Weight { get; }
+ }
+
+ ///
+ /// Retains motion-search results and mode decisions for one differential-reference choice.
+ ///
+ public struct ReferenceSearchResult
+ {
+ public Av1MotionVector ReferenceVector;
+ public Av1MotionVector FullVector;
+ public Av1MotionVector Vector;
+ public int FullRate;
+ public int FullCost;
+ public int Rate;
+ public int DrlRate;
+ public bool HasFullResult;
+ public bool IsValid;
+ public bool Skip;
+ }
+
+ ///
+ /// Retains the six possible starts and three reference results across one block's new-motion modes.
+ ///
+ public struct SingleReferenceState
+ {
+ public InlineArray6 Starts;
+ public InlineArray6 StartReferenceIndices;
+ public InlineArray3 References;
+ public int StartCount;
+ }
+
+ ///
+ /// Coordinates single-reference starting candidates, full-pixel search, fractional refinement, and winner estimation.
+ ///
+ /// The unsigned sample storage type.
+ /// The sample-specific prediction and error operations.
+ public readonly ref struct SingleReferenceSearch
+ where TSample : unmanaged
+ where TOperator : struct, IMotionSearchOperator
+ {
+ private readonly ReadOnlySpan source;
+ private readonly int sourceStride;
+ private readonly ReadOnlySpan reference;
+ private readonly int referenceStride;
+ private readonly int referenceOrigin;
+ private readonly Av1BlockSize blockSize;
+ private readonly Rectangle frameBounds;
+ private readonly Av1EncoderBlockWorkspace workspace;
+ private readonly Span prediction;
+ private readonly Span residual;
+ private readonly Span convolutionScratch;
+ private readonly Span quantized;
+ private readonly Av1SymbolEncoder writer;
+ private readonly ReadOnlySpan aboveContexts;
+ private readonly ReadOnlySpan leftContexts;
+ private readonly Av1BitDepth bitDepth;
+ private readonly int qIndex;
+ private readonly int dcDeltaQ;
+ private readonly int sharpness;
+ private readonly bool lossless;
+ private readonly int rateMultiplier;
+ private readonly int transformSizeRate;
+ private readonly int noSkipRate;
+ private readonly int skipRate;
+ private readonly Av1InterpolationFilter horizontalFilter;
+ private readonly Av1InterpolationFilter verticalFilter;
+ private readonly Av1MotionVectorCosts motionCosts;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The source samples at the prediction-block origin.
+ /// The source row stride.
+ /// The complete bordered reference plane.
+ /// The reference row stride.
+ /// The reference origin corresponding to zero displacement.
+ /// The containing prediction block size.
+ /// The full-sample frame search limits before differential-vector limits.
+ /// The worker transform and search-site storage.
+ /// The worker search prediction buffer, also reused for final predictions.
+ /// The packed block residual destination.
+ /// The signed intermediate storage for final prediction.
+ /// The scratch quantized coefficients for one transform.
+ /// The current tile probability state.
+ /// The incoming top coefficient contexts.
+ /// The incoming left coefficient contexts.
+ /// The coded sample precision.
+ /// The effective segment quantizer index.
+ /// The luma DC quantizer adjustment.
+ /// The quantization sharpness setting.
+ /// Whether the segment is coded losslessly.
+ /// The block rate-distortion multiplier.
+ /// The transform partition rate used by winner estimation.
+ /// The rate of a non-skipped prediction block.
+ /// The rate of a skipped prediction block.
+ /// The final horizontal interpolation family.
+ /// The final vertical interpolation family.
+ /// The retained differential motion-rate table.
+ public SingleReferenceSearch(
+ ReadOnlySpan source,
+ int sourceStride,
+ ReadOnlySpan reference,
+ int referenceStride,
+ int referenceOrigin,
+ Av1BlockSize blockSize,
+ Rectangle frameBounds,
+ Av1EncoderBlockWorkspace workspace,
+ Span prediction,
+ Span residual,
+ Span convolutionScratch,
+ Span quantized,
+ Av1SymbolEncoder writer,
+ ReadOnlySpan aboveContexts,
+ ReadOnlySpan leftContexts,
+ Av1BitDepth bitDepth,
+ int qIndex,
+ int dcDeltaQ,
+ int sharpness,
+ bool lossless,
+ int rateMultiplier,
+ int transformSizeRate,
+ int noSkipRate,
+ int skipRate,
+ Av1InterpolationFilter horizontalFilter,
+ Av1InterpolationFilter verticalFilter,
+ Av1MotionVectorCosts motionCosts)
+ {
+ this.source = source;
+ this.sourceStride = sourceStride;
+ this.reference = reference;
+ this.referenceStride = referenceStride;
+ this.referenceOrigin = referenceOrigin;
+ this.blockSize = blockSize;
+ this.frameBounds = frameBounds;
+ this.workspace = workspace;
+ this.prediction = prediction;
+ this.residual = residual;
+ this.convolutionScratch = convolutionScratch;
+ this.quantized = quantized;
+ this.writer = writer;
+ this.aboveContexts = aboveContexts;
+ this.leftContexts = leftContexts;
+ this.bitDepth = bitDepth;
+ this.qIndex = qIndex;
+ this.dcDeltaQ = dcDeltaQ;
+ this.sharpness = sharpness;
+ this.lossless = lossless;
+ this.rateMultiplier = rateMultiplier;
+ this.transformSizeRate = transformSizeRate;
+ this.noSkipRate = noSkipRate;
+ this.skipRate = skipRate;
+ this.horizontalFilter = horizontalFilter;
+ this.verticalFilter = verticalFilter;
+ this.motionCosts = motionCosts;
+ }
+
+ ///
+ /// Searches one differential-reference choice while retaining state for subsequent choices.
+ ///
+ /// The resolved frame search policy.
+ /// The frame's initial number of excluded outer search stages.
+ /// The largest full-sample magnitude in this reference's spatial context.
+ /// Whether the current frame is presented.
+ /// The optional range reduction, or the maximum integer for no reduction.
+ /// Whether the frame prohibits fractional motion vectors.
+ /// Whether eighth-sample vectors are permitted.
+ /// Whether content classification caps the first mesh interval.
+ /// The current dynamic-reference index.
+ /// The differential coding reference in eighth-sample units.
+ /// The syntax rate selecting this differential reference.
+ /// Weighted starting positions in decreasing weight order.
+ /// The total represented weight before selecting the first two starts.
+ /// The block's retained results; initialize once before its first new-motion mode.
+ /// The selected displacement and prediction-error statistics.
+ /// Whether the search produced a valid candidate.
+ public bool Search(
+ Av1MotionSearchSettings settings,
+ int frameStepParameter,
+ int spatialMagnitude,
+ bool showFrame,
+ int searchRange,
+ bool forceInteger,
+ bool allowHighPrecision,
+ bool fineMeshInterval,
+ int referenceIndex,
+ Av1MotionVector referenceVector,
+ int drlRate,
+ ReadOnlySpan starts,
+ int totalWeight,
+ ref SingleReferenceState state,
+ out FractionalResult result)
+ {
+ ref ReferenceSearchResult current = ref state.References[referenceIndex];
+ current.ReferenceVector = referenceVector;
+ current.DrlRate = drlRate;
+ int stepParameter = frameStepParameter;
+ if (settings.AutomaticStepSizeLevel != 0 && showFrame)
+ {
+ stepParameter = (GetInitialStepParameter(spatialMagnitude) + frameStepParameter) / 2;
+ }
+
+ // The frame may supply many temporal starts, but only its first two ranked candidates enter
+ // this search. Record both before searching: the weight cutoff does not undo start history.
+ int candidateCount = Math.Min(2, starts.Length);
+ Span rejected = stackalloc bool[2];
+ rejected.Clear();
+ Point fullReference = new(
+ (referenceVector.Column + 3 + (referenceVector.Column >= 0 ? 1 : 0)) >> 3,
+ (referenceVector.Row + 3 + (referenceVector.Row >= 0 ? 1 : 0)) >> 3);
+
+ if (settings.StartCandidatePruningLevel != 0)
+ {
+ for (int candidateIndex = 0; candidateIndex < candidateCount; candidateIndex++)
+ {
+ Point start = starts[candidateIndex].Vector;
+ for (int historyIndex = 0; historyIndex < state.StartCount; historyIndex++)
+ {
+ int previousIndex = state.StartReferenceIndices[historyIndex];
+ ref ReferenceSearchResult previous = ref state.References[previousIndex];
+ if (!previous.IsValid && previousIndex != referenceIndex)
+ {
+ continue;
+ }
+
+ Point previousStart = state.Starts[historyIndex];
+ Av1MotionVector previousReference = previous.ReferenceVector;
+ int previousColumn = (previousReference.Column + 3 + (previousReference.Column >= 0 ? 1 : 0)) >> 3;
+ int previousRow = (previousReference.Row + 3 + (previousReference.Row >= 0 ? 1 : 0)) >> 3;
+ int startX = Math.Abs(start.X - previousStart.X);
+ int startY = Math.Abs(start.Y - previousStart.Y);
+ int referenceX = Math.Abs(fullReference.X - previousColumn);
+ int referenceY = Math.Abs(fullReference.Y - previousRow);
+ bool duplicates = settings.StartCandidatePruningLevel >= 2
+ ? startX <= 1 && startY <= 1 && referenceX <= 1 && referenceY <= 1
+ : startX + startY <= 1 && referenceX + referenceY <= 1;
+
+ if (duplicates)
+ {
+ rejected[candidateIndex] = true;
+ break;
+ }
+ }
+
+ if (!rejected[candidateIndex])
+ {
+ state.Starts[state.StartCount] = start;
+ state.StartReferenceIndices[state.StartCount++] = (byte)referenceIndex;
+ }
+ }
+ }
+
+ FullPixelSearchMethod method = settings.GetFullPixelMethod(this.blockSize);
+ Av1MotionSearchSites sites = this.workspace.GetMotionSearchSites(method, this.referenceStride);
+ if (searchRange < int.MaxValue)
+ {
+ if (searchRange < 1)
+ {
+ stepParameter = sites.StageCount;
+ }
+ else
+ {
+ while (sites.StageCount - stepParameter - 1 > 0
+ && sites.GetRadius(sites.StageCount - stepParameter - 1) > (searchRange << 1))
+ {
+ stepParameter++;
+ }
+ }
+ }
+
+ Size size = new(this.blockSize.GetWidth(), this.blockSize.GetHeight());
+ FullPixelSearch fullSearch = new(
+ this.source,
+ this.sourceStride,
+ this.reference,
+ this.referenceStride,
+ this.referenceOrigin,
+ size,
+ referenceVector.GetFullPixelSearchBounds(this.frameBounds),
+ referenceVector,
+ this.motionCosts,
+ this.bitDepth,
+ Av1RateDistortion.GetMotionSearchSadPerBit(this.qIndex, this.bitDepth),
+ this.rateMultiplier);
+
+ FullPixelResult best = default;
+ Point? second = null;
+ bool hasBest = false;
+ int sumWeight = 0;
+ for (int candidateIndex = 0; candidateIndex < candidateCount; candidateIndex++)
+ {
+ if (rejected[candidateIndex])
+ {
+ continue;
+ }
+
+ // Non-realtime motion policy disables neighborhood publication. Fractional pruning therefore
+ // measures its own candidates instead of fitting the optional five-cost integer surface.
+ FullPixelResult candidate = fullSearch.Search(
+ starts[candidateIndex].Vector,
+ stepParameter,
+ method,
+ sites,
+ settings,
+ keyFrame: false,
+ fineMeshInterval,
+ Span.Empty,
+ out Point? candidateSecond);
+
+ if (candidate.Cost < (hasBest ? best.Cost : int.MaxValue))
+ {
+ best = candidate;
+ second = candidateSecond;
+ hasBest = true;
+ }
+
+ sumWeight += starts[candidateIndex].Weight;
+ if (4 * sumWeight > 3 * totalWeight)
+ {
+ break;
+ }
+ }
+
+ result = default;
+ if (!hasBest)
+ {
+ return false;
+ }
+
+ Av1MotionVector integerVector = new(best.Vector.Y * 8, best.Vector.X * 8);
+ int integerRate = ((this.motionCosts.GetCost(integerVector, referenceVector) * 108) + 64) >> 7;
+ current.FullVector = integerVector;
+ current.FullCost = best.Cost;
+ current.FullRate = integerRate;
+ current.HasFullResult = true;
+ int pruningLevel = settings.ReferenceCandidatePruningLevel;
+ if (pruningLevel >= 2)
+ {
+ for (int previousIndex = 0; previousIndex < referenceIndex; previousIndex++)
+ {
+ ref ReferenceSearchResult previous = ref state.References[previousIndex];
+ if (!previous.HasFullResult)
+ {
+ continue;
+ }
+
+ if (previous.FullVector == integerVector && previous.FullRate + previous.DrlRate <= integerRate + drlRate)
+ {
+ return false;
+ }
+
+ // Level three permits a quarter more search error; level four compares the original
+ // error directly. This only prunes when the earlier reference also has cheaper selection syntax.
+ int threshold = pruningLevel == 3 ? previous.FullCost + (previous.FullCost >> 2) : previous.FullCost;
+ if (pruningLevel >= 3 && best.Cost > threshold && previous.DrlRate < drlRate)
+ {
+ return false;
+ }
+ }
+ }
+
+ result = new FractionalResult(integerVector, best.Variance, best.SquaredError, best.MotionCost);
+ if (!forceInteger && best.Cost < int.MaxValue)
+ {
+ Rectangle fractionalBounds = referenceVector.GetSubpixelSearchBounds(this.frameBounds);
+ FractionalSearch fractionalSearch = new(
+ this.source,
+ this.sourceStride,
+ this.reference,
+ this.referenceStride,
+ this.referenceOrigin,
+ this.prediction,
+ size,
+ fractionalBounds,
+ referenceVector,
+ this.motionCosts,
+ this.bitDepth,
+ this.rateMultiplier);
+
+ Span centers = stackalloc Av1MotionVector[3];
+ centers.Fill(new Av1MotionVector(short.MinValue, short.MinValue));
+ int firstCost = fractionalSearch.Search(
+ integerVector,
+ best,
+ settings.FractionalMethod,
+ SearchPrecision.EighthSample,
+ allowHighPrecision,
+ settings.FractionalIterationsPerStep,
+ settings.FractionalInterpolationTaps,
+ ReadOnlySpan.Empty,
+ centers,
+ out result);
+
+ if (second.HasValue && second.Value != best.Vector && settings.SecondCandidateSelection <= CandidateSelection.Variance)
+ {
+ Point secondPoint = second.Value;
+ Av1MotionVector secondStart = new(secondPoint.Y * 8, secondPoint.X * 8);
+ if (fractionalBounds.Contains(secondStart.Column, secondStart.Row))
+ {
+ int secondCost = fractionalSearch.Search(
+ secondStart,
+ null,
+ settings.FractionalMethod,
+ SearchPrecision.EighthSample,
+ allowHighPrecision,
+ settings.FractionalIterationsPerStep,
+ settings.FractionalInterpolationTaps,
+ ReadOnlySpan.Empty,
+ centers,
+ out FractionalResult secondResult);
+
+ if (settings.SecondCandidateSelection == CandidateSelection.RateDistortion && secondCost != int.MaxValue)
+ {
+ long firstRateDistortion = this.EstimateCandidate(result.Vector, referenceVector);
+ long secondRateDistortion = this.EstimateCandidate(secondResult.Vector, referenceVector);
+ if (secondRateDistortion < firstRateDistortion)
+ {
+ result = secondResult;
+ }
+ }
+ else if (secondCost < firstCost)
+ {
+ result = secondResult;
+ }
+ }
+ }
+
+ if (pruningLevel >= 1)
+ {
+ int fractionalRate = ((this.motionCosts.GetCost(result.Vector, referenceVector) * 108) + 64) >> 7;
+ for (int previousIndex = 0; previousIndex < referenceIndex; previousIndex++)
+ {
+ ref ReferenceSearchResult previous = ref state.References[previousIndex];
+ if (!previous.IsValid || previous.Vector != result.Vector)
+ {
+ continue;
+ }
+
+ // A previously skipped matching mode remains skipped regardless of rate. Otherwise,
+ // preserve the earlier mode whenever its motion-plus-reference syntax is no more expensive.
+ if (previous.Skip || previous.Rate + previous.DrlRate <= fractionalRate + drlRate)
+ {
+ current.Skip = true;
+ break;
+ }
+ }
+ }
+ }
+
+ // Weight only the motion-vector syntax. The transform and differential-reference rates retain
+ // their own 1/512-bit units; applying this factor to their sum would change the mode decision.
+ current.Rate = ((this.motionCosts.GetCost(result.Vector, referenceVector) * 108) + 64) >> 7;
+ current.Vector = result.Vector;
+ current.IsValid = true;
+ return true;
+ }
+
+ ///
+ /// Compares a refined vector using final prediction, transform rate, and differential motion rate.
+ ///
+ /// The refined candidate vector.
+ /// The differential coding reference.
+ /// The rate-distortion estimate excluding the block skip-header cost.
+ private long EstimateCandidate(Av1MotionVector vector, Av1MotionVector referenceVector)
+ {
+ int width = this.blockSize.GetWidth();
+ int height = this.blockSize.GetHeight();
+ int origin = this.referenceOrigin + ((vector.Row >> 3) * this.referenceStride) + (vector.Column >> 3);
+ TOperator.PreparePrediction(
+ this.source,
+ this.sourceStride,
+ this.reference,
+ this.referenceStride,
+ origin,
+ this.prediction,
+ this.residual,
+ this.convolutionScratch,
+ width,
+ height,
+ this.horizontalFilter,
+ this.verticalFilter,
+ (vector.Column & 7) << 1,
+ (vector.Row & 7) << 1,
+ this.bitDepth.GetBitCount());
+
+ Av1TransformBlockEncoder.EstimateInterTransform(
+ this.workspace,
+ this.residual,
+ width,
+ this.quantized,
+ this.writer,
+ this.aboveContexts,
+ this.leftContexts,
+ this.blockSize,
+ new Size(width, height),
+ this.blockSize.GetMaximumTransformSize(),
+ this.qIndex,
+ this.dcDeltaQ,
+ this.bitDepth,
+ this.sharpness,
+ this.lossless,
+ this.rateMultiplier,
+ this.transformSizeRate,
+ this.noSkipRate,
+ this.skipRate,
+ long.MaxValue,
+ out Av1RateDistortionStatistics statistics,
+ out _,
+ out _);
+
+ int motionRate = ((this.motionCosts.GetCost(vector, referenceVector) * 108) + 64) >> 7;
+ return Av1RateDistortion.GetCost(this.rateMultiplier, statistics.Rate + motionRate, statistics.Distortion);
+ }
+ }
+
+ ///
+ /// Orders temporal starts by descending represented analysis weight.
+ ///
+ private readonly struct StartingCandidateWeightComparer : IComparer
+ {
+ ///
+ public int Compare(StartingCandidate x, StartingCandidate y) => y.Weight.CompareTo(x.Weight);
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.UInt16Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.UInt16Operator.cs
new file mode 100644
index 0000000000..5eb0c06d8d
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.UInt16Operator.cs
@@ -0,0 +1,90 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
+
+internal static partial class Av1MotionSearchBase
+{
+ ///
+ /// Measures high-bit-depth sample errors with the shared vector-width residual traversal.
+ ///
+ public readonly struct UInt16Operator : IMotionSearchOperator
+ {
+ ///
+ public static void PreparePrediction(
+ ReadOnlySpan source,
+ int sourceStride,
+ ReadOnlySpan reference,
+ int referenceStride,
+ int referenceOrigin,
+ Span prediction,
+ Span residual,
+ Span scratch,
+ int width,
+ int height,
+ Av1InterpolationFilter horizontalFilter,
+ Av1InterpolationFilter verticalFilter,
+ int horizontalPhase,
+ int verticalPhase,
+ int bitDepth)
+ {
+ Av1TranslationalInterPredictor.Predict(
+ reference,
+ referenceStride,
+ referenceOrigin,
+ prediction,
+ width,
+ width,
+ height,
+ horizontalFilter,
+ verticalFilter,
+ horizontalPhase,
+ verticalPhase,
+ bitDepth,
+ scratch);
+
+ Av1ResidualBuilder.Subtract(source, sourceStride, prediction, width, residual, width, width, height);
+ }
+
+ ///
+ public static void Predict(
+ ReadOnlySpan reference,
+ int referenceStride,
+ int referenceOrigin,
+ Span buffer,
+ int width,
+ int height,
+ int horizontalPhase,
+ int verticalPhase,
+ int taps,
+ int bitDepth)
+ => Av1TranslationalInterPredictor.PredictForSearch(
+ reference, referenceStride, referenceOrigin, buffer, width, height, horizontalPhase, verticalPhase, taps, bitDepth);
+
+ ///
+ public static int SumAbsoluteDifferences(
+ ReadOnlySpan source,
+ int sourceStride,
+ ReadOnlySpan prediction,
+ int predictionStride,
+ int width,
+ int height,
+ int rowStep)
+ => Av1ResidualBuilder.SumAbsoluteDifferences(source, sourceStride, prediction, predictionStride, width, height, rowStep);
+
+ ///
+ public static void GetMoments(
+ ReadOnlySpan source,
+ int sourceStride,
+ ReadOnlySpan prediction,
+ int predictionStride,
+ int width,
+ int height,
+ out int sum,
+ out long squares)
+ => Av1ResidualBuilder.GetMoments(source, sourceStride, prediction, predictionStride, width, height, out sum, out squares);
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.cs
new file mode 100644
index 0000000000..f5db64cceb
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchBase.cs
@@ -0,0 +1,674 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
+using static SixLabors.ImageSharp.Formats.Heif.Av1.Motion.Av1MotionSearchSettings;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
+
+///
+/// Owns motion-search traversal while closed sample operators measure prediction errors.
+///
+internal static partial class Av1MotionSearchBase
+{
+ ///
+ /// Retains the integer winner's distortion and rate separately for fractional refinement.
+ ///
+ public readonly struct FullPixelResult
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The displacement in full samples.
+ /// The variance in the eight-bit error domain.
+ /// The squared residual sum in the eight-bit error domain.
+ /// The variance-domain motion-rate cost.
+ public FullPixelResult(Point vector, int variance, int squaredError, int motionCost)
+ {
+ this.Vector = vector;
+ this.Variance = variance;
+ this.SquaredError = squaredError;
+ this.MotionCost = motionCost;
+ }
+
+ ///
+ /// Gets the displacement in full samples.
+ ///
+ public Point Vector { get; }
+
+ ///
+ /// Gets the normalized residual variance.
+ ///
+ public int Variance { get; }
+
+ ///
+ /// Gets the normalized squared residual sum.
+ ///
+ public int SquaredError { get; }
+
+ ///
+ /// Gets the variance-domain motion-rate cost.
+ ///
+ public int MotionCost { get; }
+
+ ///
+ /// Gets the total cost used to compare completed search paths.
+ ///
+ public int Cost => this.Variance + this.MotionCost;
+ }
+
+ ///
+ /// Borrows source, reference, and rate state for all integer candidates of a prediction block.
+ ///
+ /// The unsigned component storage type.
+ /// The closed sample-error operator.
+ public readonly ref struct FullPixelSearch
+ where TSample : unmanaged
+ where TOperator : struct, IMotionSearchOperator
+ {
+ private readonly ReadOnlySpan source;
+ private readonly ReadOnlySpan reference;
+ private readonly int sourceStride;
+ private readonly int referenceStride;
+ private readonly int referenceOrigin;
+ private readonly Size blockSize;
+ private readonly Rectangle bounds;
+ private readonly Av1MotionVector referenceVector;
+ private readonly Av1MotionVector integerReferenceVector;
+ private readonly Av1MotionVectorCosts costs;
+ private readonly int precisionShift;
+ private readonly int sadPerBit;
+ private readonly int rateMultiplier;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// Source samples beginning at the block origin.
+ /// The source row stride in samples.
+ /// The complete retained reference storage including its border.
+ /// The reference row stride in samples.
+ /// The reference index corresponding to the current block origin.
+ /// The prediction dimensions.
+ /// The permitted displacement rectangle, with exclusive upper edges.
+ /// The spatial reference in eighth-sample units.
+ /// The retained motion-rate tables.
+ /// The coded component precision.
+ /// The quantizer-derived rate scale for absolute differences.
+ /// The block rate multiplier for variance costs.
+ public FullPixelSearch(
+ ReadOnlySpan source,
+ int sourceStride,
+ ReadOnlySpan reference,
+ int referenceStride,
+ int referenceOrigin,
+ Size blockSize,
+ Rectangle bounds,
+ Av1MotionVector referenceVector,
+ Av1MotionVectorCosts costs,
+ Av1BitDepth bitDepth,
+ int sadPerBit,
+ int rateMultiplier)
+ {
+ this.source = source;
+ this.sourceStride = sourceStride;
+ this.reference = reference;
+ this.referenceStride = referenceStride;
+ this.referenceOrigin = referenceOrigin;
+ this.blockSize = blockSize;
+ this.bounds = bounds;
+ this.referenceVector = referenceVector;
+ this.costs = costs;
+ this.precisionShift = bitDepth.GetBitCount() - 8;
+ this.sadPerBit = sadPerBit;
+ this.rateMultiplier = rateMultiplier;
+
+ // Nearest full-sample rounding breaks half-sample ties away from zero. SAD compares integer
+ // differences from that rounded reference; variance retains the original subpixel difference.
+ int row = (referenceVector.Row + 3 + (referenceVector.Row >= 0 ? 1 : 0)) >> 3;
+ int column = (referenceVector.Column + 3 + (referenceVector.Column >= 0 ? 1 : 0)) >> 3;
+ this.integerReferenceVector = new Av1MotionVector(row * 8, column * 8);
+ }
+
+ ///
+ /// Runs the selected full-pixel search, restarts, mesh decision, and neighboring cost publication.
+ ///
+ /// The initial displacement in full samples.
+ /// The number of outer search stages already excluded by frame and block policy.
+ /// The block-selected search method.
+ /// The retained geometry configured for this method and reference stride.
+ /// The resolved frame motion policy.
+ /// Whether key-frame policy prevents adaptive alternate-row SAD.
+ /// Whether content classification caps the initial mesh interval at four.
+ /// Five costs: center, left, down, right, and up; empty when neighborhood publication is disabled.
+ /// The preceding integer winner, when the selected traversal supplies one.
+ /// The integer winner with its retained variance, squared error, and motion cost.
+ public FullPixelResult Search(
+ Point start,
+ int stepParameter,
+ FullPixelSearchMethod method,
+ Av1MotionSearchSites sites,
+ Av1MotionSearchSettings settings,
+ bool keyFrame,
+ bool fineMeshInterval,
+ Span costList,
+ out Point? secondBest)
+ {
+ Point clampedStart = this.Clamp(start);
+ int rowStep = 1;
+ if (this.blockSize.Height >= 16)
+ {
+ if (settings.DownsampledSadLevel == 2)
+ {
+ rowStep = 2;
+ }
+ else if (settings.DownsampledSadLevel == 1 && !keyFrame)
+ {
+ int evenSad = this.GetSad(clampedStart, 2, 0);
+ int oddSad = this.GetSad(clampedStart, 2, 1);
+ if ((Math.Abs(evenSad - oddSad) * 4) < evenSad)
+ {
+ rowStep = 2;
+ }
+ }
+ }
+
+ // An alternate-row search may alias vertical texture. If its final candidate exposes that aliasing,
+ // repeat the same complete search with full SAD; candidate state and cost-list state both restart.
+ while (true)
+ {
+ secondBest = null;
+ FullPixelResult best;
+ bool centerCostOnly = false;
+ if (method <= FullPixelSearchMethod.ClampedDiamond)
+ {
+ best = this.SearchDiamond(clampedStart, stepParameter, sites, rowStep, ref secondBest);
+ }
+ else
+ {
+ best = this.SearchPattern(clampedStart, stepParameter, method, sites, rowStep, out centerCostOnly);
+ }
+
+ if (centerCostOnly && !costList.IsEmpty)
+ {
+ // An initial finest-scale winner skips the four-point refinement stage. Its neighbors
+ // have not been published, so fractional pruning must see them as unavailable.
+ costList.Fill(int.MaxValue);
+ costList[0] = this.GetSadCost(best.Vector, rowStep);
+ }
+ else if (!costList.IsEmpty)
+ {
+ this.FillCostList(best.Vector, rowStep, costList);
+ }
+
+ int areaLog2 = BitOperations.Log2((uint)(this.blockSize.Width * this.blockSize.Height));
+ bool runMesh = method is FullPixelSearchMethod.NStep or FullPixelSearchMethod.EightPointNStep
+ && best.Cost > (settings.MeshErrorThreshold >> (14 - areaLog2));
+
+ // Distance is measured from the caller's original start, before range clamping.
+ if (settings.MeshPruningLevel == 2 &&
+ Math.Max(Math.Abs(start.X - best.Vector.X), Math.Abs(start.Y - best.Vector.Y)) <= 4)
+ {
+ runMesh = false;
+ }
+
+ if (rowStep == 2)
+ {
+ int fullSad = this.GetSad(best.Vector, 1, 0);
+ int skippedSad = this.GetSad(best.Vector, 2, 0);
+ int threshold = (this.blockSize.Width * this.blockSize.Height) >> 4;
+ if (fullSad > threshold && Math.Abs(skippedSad - fullSad) * 10 >= Math.Max(fullSad, 1) * 9)
+ {
+ rowStep = 1;
+ continue;
+ }
+ }
+
+ if (runMesh)
+ {
+ FullPixelResult mesh = this.SearchMesh(
+ best.Vector, settings.GetMeshPattern(intraBlockCopy: false), fineMeshInterval, rowStep, ref secondBest);
+
+ // The mesh publishes its neighborhood and preceding winner before its final variance comparison.
+ // Keep that publication order so later fractional selection sees the same retained search state.
+ if (!costList.IsEmpty)
+ {
+ this.FillCostList(mesh.Vector, rowStep, costList);
+ }
+
+ if (mesh.Cost < best.Cost)
+ {
+ best = mesh;
+ }
+ }
+
+ return best;
+ }
+ }
+
+ ///
+ /// Runs decreasing-radius searches from the same start and compares their winners using variance.
+ ///
+ private FullPixelResult SearchDiamond(
+ Point start,
+ int stepParameter,
+ Av1MotionSearchSites sites,
+ int rowStep,
+ ref Point? secondBest)
+ {
+ int startCost = this.GetSadCost(start, rowStep);
+ Point winner = this.SearchDiamondSteps(start, startCost, stepParameter, sites, rowStep, ref secondBest, out int centeredSteps);
+ FullPixelResult best = this.GetVarianceResult(winner);
+ int furtherSteps = sites.StageCount - 1 - stepParameter;
+ while (centeredSteps < furtherSteps)
+ {
+ centeredSteps++;
+ winner = this.SearchDiamondSteps(
+ start, startCost, stepParameter + centeredSteps, sites, rowStep, ref secondBest, out int skippedSteps);
+
+ FullPixelResult candidate = this.GetVarianceResult(winner);
+ if (candidate.Cost < best.Cost)
+ {
+ best = candidate;
+ }
+
+ centeredSteps += skippedSteps;
+ }
+
+ return best;
+ }
+
+ ///
+ /// Visits ordered sites once per radius, retaining initial center stays for later restart pruning.
+ ///
+ private Point SearchDiamondSteps(
+ Point start,
+ int startCost,
+ int stepParameter,
+ Av1MotionSearchSites sites,
+ int rowStep,
+ ref Point? secondBest,
+ out int centeredSteps)
+ {
+ Point best = start;
+ int bestCost = startCost;
+ bool movedFromStart = false;
+ centeredSteps = 0;
+ for (int stage = sites.StageCount - stepParameter - 1; stage >= 0; stage--)
+ {
+ ReadOnlySpan stageSites = sites.GetSites(stage);
+ int centerIndex = this.referenceOrigin + (best.Y * this.referenceStride) + best.X;
+ int bestSite = 0;
+ for (int index = 1; index <= sites.GetCandidateCount(stage); index++)
+ {
+ Av1MotionSearchSites.Site site = stageSites[index];
+ Point candidate = new(best.X + site.Column, best.Y + site.Row);
+ if (this.bounds.Contains(candidate) && this.TryImproveSad(candidate, centerIndex + site.Offset, rowStep, ref bestCost))
+ {
+ bestSite = index;
+ }
+ }
+
+ if (bestSite != 0)
+ {
+ secondBest = best;
+ Av1MotionSearchSites.Site site = stageSites[bestSite];
+ best = new Point(best.X + site.Column, best.Y + site.Row);
+ movedFromStart = true;
+ }
+
+ if (!movedFromStart)
+ {
+ centeredSteps++;
+ }
+
+ // Repeated outer radii can be skipped after a center stay; after a move they must remain eligible.
+ if (bestSite == 0 && stage > 2)
+ {
+ while (stage > 2 && sites.GetRadius(stage - 1) == sites.GetRadius(stage))
+ {
+ centeredSteps++;
+ stage--;
+ }
+ }
+ }
+
+ return best;
+ }
+
+ ///
+ /// Selects an initial scale, then walks adjacent sites around each winning direction before reducing scale.
+ ///
+ private FullPixelResult SearchPattern(
+ Point start,
+ int stepParameter,
+ FullPixelSearchMethod method,
+ Av1MotionSearchSites sites,
+ int rowStep,
+ out bool centerCostOnly)
+ {
+ bool initialSearch = method is FullPixelSearchMethod.Hexagon or FullPixelSearchMethod.BigDiamond;
+ centerCostOnly = false;
+ int minimumStep = method switch
+ {
+ FullPixelSearchMethod.FastBigDiamond => 8,
+ FullPixelSearchMethod.FastDiamond => 9,
+ FullPixelSearchMethod.VeryFastDiamond => 10,
+ _ => 0
+ };
+
+ int initialScale = 10 - Math.Min(Math.Max(stepParameter, minimumStep), 10);
+ int bestCost = this.GetSadCost(start, rowStep);
+ Point best = start;
+ int direction = -1;
+ if (initialSearch)
+ {
+ int maximumScale = initialScale;
+ initialScale = -1;
+ for (int scale = 0; scale <= maximumScale; scale++)
+ {
+ int candidateIndex = this.FindBestSite(start, scale, sites, rowStep, ref bestCost);
+ if (candidateIndex >= 0)
+ {
+ initialScale = scale;
+ direction = candidateIndex;
+ }
+ }
+
+ if (initialScale >= 0)
+ {
+ Av1MotionSearchSites.Site site = sites.GetSites(initialScale)[direction];
+ best = new Point(start.X + site.Column, start.Y + site.Row);
+ }
+ }
+
+ if (initialScale >= 0)
+ {
+ bool fourPointFinalStage = sites.GetCandidateCount(0) == 4;
+ centerCostOnly = fourPointFinalStage && initialSearch && initialScale == 0;
+ int lastScale = fourPointFinalStage ? 1 : 0;
+ for (int scale = initialScale; scale >= lastScale; scale--)
+ {
+ ReadOnlySpan stageSites = sites.GetSites(scale);
+ if (!initialSearch || scale != initialScale)
+ {
+ int candidateIndex = this.FindBestSite(best, scale, sites, rowStep, ref bestCost);
+ if (candidateIndex < 0)
+ {
+ continue;
+ }
+
+ direction = candidateIndex;
+ Av1MotionSearchSites.Site site = stageSites[direction];
+ best = new Point(best.X + site.Column, best.Y + site.Row);
+ }
+
+ best = this.FollowPatternDirection(best, scale, direction, sites, rowStep, ref bestCost);
+ }
+
+ // Four-point patterns retain a separate final-stage entry decision. When the initial scale
+ // is already zero, its initial winner is published without another directional walk.
+ if (fourPointFinalStage && (!initialSearch || initialScale != 0))
+ {
+ int candidateIndex = this.FindBestSite(best, 0, sites, rowStep, ref bestCost);
+ if (candidateIndex >= 0)
+ {
+ Av1MotionSearchSites.Site site = sites.GetSites(0)[candidateIndex];
+ best = new Point(best.X + site.Column, best.Y + site.Row);
+ best = this.FollowPatternDirection(best, 0, candidateIndex, sites, rowStep, ref bestCost);
+ }
+ }
+ }
+
+ return this.GetVarianceResult(best);
+ }
+
+ ///
+ /// Tests the complete stage around a fixed center, keeping the first candidate on equal cost.
+ ///
+ private int FindBestSite(Point center, int stage, Av1MotionSearchSites sites, int rowStep, ref int bestCost)
+ {
+ ReadOnlySpan stageSites = sites.GetSites(stage);
+ int centerIndex = this.referenceOrigin + (center.Y * this.referenceStride) + center.X;
+ int count = sites.GetCandidateCount(stage);
+ int radius = sites.GetRadius(stage);
+ if (center.X - radius >= this.bounds.Left && center.X + radius < this.bounds.Right &&
+ center.Y - radius >= this.bounds.Top && center.Y + radius < this.bounds.Bottom)
+ {
+ // Interior pattern stages visit complete four-site groups. For a six-site hexagon the final
+ // two sites are visited only by the boundary path, so range classification affects selection.
+ count &= ~3;
+ }
+
+ int bestIndex = -1;
+ for (int index = 0; index < count; index++)
+ {
+ Av1MotionSearchSites.Site site = stageSites[index];
+ Point candidate = new(center.X + site.Column, center.Y + site.Row);
+ if (this.bounds.Contains(candidate) && this.TryImproveSad(candidate, centerIndex + site.Offset, rowStep, ref bestCost))
+ {
+ bestIndex = index;
+ }
+ }
+
+ return bestIndex;
+ }
+
+ ///
+ /// Walks the previous, same, and next directions around the ring until none improves the current center.
+ ///
+ private Point FollowPatternDirection(
+ Point center,
+ int stage,
+ int direction,
+ Av1MotionSearchSites sites,
+ int rowStep,
+ ref int bestCost)
+ {
+ int count = sites.GetCandidateCount(stage);
+ ReadOnlySpan stageSites = sites.GetSites(stage);
+ while (true)
+ {
+ int centerIndex = this.referenceOrigin + (center.Y * this.referenceStride) + center.X;
+ int bestIndex = -1;
+ for (int relative = -1; relative <= 1; relative++)
+ {
+ int index = (direction + relative + count) % count;
+ Av1MotionSearchSites.Site site = stageSites[index];
+ Point candidate = new(center.X + site.Column, center.Y + site.Row);
+ if (this.bounds.Contains(candidate) && this.TryImproveSad(candidate, centerIndex + site.Offset, rowStep, ref bestCost))
+ {
+ bestIndex = index;
+ }
+ }
+
+ if (bestIndex < 0)
+ {
+ return center;
+ }
+
+ direction = bestIndex;
+ Av1MotionSearchSites.Site winningSite = stageSites[direction];
+ center = new Point(center.X + winningSite.Column, center.Y + winningSite.Row);
+ }
+ }
+
+ ///
+ /// Runs content-selected mesh passes, adjusting the initial range to the current displacement magnitude.
+ ///
+ private FullPixelResult SearchMesh(Point start, ReadOnlySpan pattern, bool fineInterval, int rowStep, ref Point? secondBest)
+ {
+ int originalRange = pattern[0];
+ int interval = pattern[1];
+ int range = Math.Min(Math.Max(originalRange, (5 * Math.Max(Math.Abs(start.X), Math.Abs(start.Y))) / 4), 256);
+ interval = Math.Max(interval, range / (originalRange / interval));
+ if (fineInterval)
+ {
+ interval = Math.Min(interval, 4);
+ }
+
+ Point best = this.SearchMeshPass(start, range, interval, rowStep, ref secondBest);
+ if (interval > 1 && range > 7)
+ {
+ for (int pass = 1; pass < 4; pass++)
+ {
+ best = this.SearchMeshPass(best, pattern[pass * 2], pattern[(pass * 2) + 1], rowStep, ref secondBest);
+ if (pattern[(pass * 2) + 1] == 1)
+ {
+ break;
+ }
+ }
+ }
+
+ return this.GetVarianceResult(best);
+ }
+
+ ///
+ /// Scans mesh rows from a fixed center; each strict replacement retains the previous winner.
+ ///
+ private Point SearchMeshPass(Point start, int range, int interval, int rowStep, ref Point? secondBest)
+ {
+ start = this.Clamp(start);
+ Point best = start;
+ int bestCost = this.GetSadCost(start, rowStep);
+ int minimumRow = Math.Max(-range, this.bounds.Top - start.Y);
+ int maximumRow = Math.Min(range, this.bounds.Bottom - 1 - start.Y);
+ int minimumColumn = Math.Max(-range, this.bounds.Left - start.X);
+ int maximumColumn = Math.Min(range, this.bounds.Right - 1 - start.X);
+ int columnStep = interval > 1 ? interval : 4;
+ for (int row = minimumRow; row <= maximumRow; row += interval)
+ {
+ for (int column = minimumColumn; column <= maximumColumn; column += columnStep)
+ {
+ // A complete unit-step group visits four adjacent columns in order. The partial terminal
+ // group has an exclusive end; preserve that edge rule rather than widening the searched set.
+ int count = interval > 1 ? 1 : column + 3 <= maximumColumn ? 4 : maximumColumn - column;
+ for (int index = 0; index < count; index++)
+ {
+ Point candidate = new(start.X + column + index, start.Y + row);
+ if (this.TryImproveSad(candidate, rowStep, ref bestCost))
+ {
+ secondBest = best;
+ best = candidate;
+ }
+ }
+ }
+ }
+
+ return best;
+ }
+
+ ///
+ /// Publishes SAD-plus-rate values at the center and its four axial neighbors for fractional pruning.
+ ///
+ private void FillCostList(Point best, int rowStep, Span costList)
+ {
+ costList[0] = this.GetSadCost(best, rowStep);
+ ReadOnlySpan offsets = [0, -1, 1, 0, 0, 1, -1, 0];
+ for (int index = 0; index < 4; index++)
+ {
+ Point candidate = new(best.X + offsets[(index * 2) + 1], best.Y + offsets[index * 2]);
+ costList[index + 1] = this.bounds.Contains(candidate) ? this.GetSadCost(candidate, rowStep) : int.MaxValue;
+ }
+ }
+
+ ///
+ /// Clamps a starting displacement to the prediction-distinct full-pixel range.
+ ///
+ private Point Clamp(Point vector)
+ => new(Math.Clamp(vector.X, this.bounds.Left, this.bounds.Right - 1), Math.Clamp(vector.Y, this.bounds.Top, this.bounds.Bottom - 1));
+
+ ///
+ /// Rejects candidates whose prediction error alone already reaches the best combined cost.
+ ///
+ private bool TryImproveSad(Point vector, int rowStep, ref int bestCost)
+ => this.TryImproveSad(vector, this.referenceOrigin + (vector.Y * this.referenceStride) + vector.X, rowStep, ref bestCost);
+
+ ///
+ /// Measures a candidate using the retained site's offset, avoiding repeated stride multiplication.
+ ///
+ private bool TryImproveSad(Point vector, int referenceIndex, int rowStep, ref int bestCost)
+ {
+ int sad = this.GetSad(referenceIndex, rowStep, 0);
+ if (sad >= bestCost)
+ {
+ return false;
+ }
+
+ int rate = this.costs.GetCost(new Av1MotionVector(vector.Y * 8, vector.X * 8), this.integerReferenceVector);
+ int cost = Av1RateDistortion.GetMotionSearchSadCost(this.sadPerBit, rate, sad);
+ if (cost >= bestCost)
+ {
+ return false;
+ }
+
+ bestCost = cost;
+ return true;
+ }
+
+ ///
+ /// Measures the complete absolute-difference cost in the eight-bit error domain.
+ ///
+ private int GetSadCost(Point vector, int rowStep)
+ {
+ int sad = this.GetSad(vector, rowStep, 0);
+ int rate = this.costs.GetCost(new Av1MotionVector(vector.Y * 8, vector.X * 8), this.integerReferenceVector);
+ return Av1RateDistortion.GetMotionSearchSadCost(this.sadPerBit, rate, sad);
+ }
+
+ ///
+ /// Measures raw sample differences and truncates only after alternate-row scaling.
+ ///
+ private int GetSad(Point vector, int rowStep, int firstRow)
+ => this.GetSad(this.referenceOrigin + (vector.Y * this.referenceStride) + vector.X, rowStep, firstRow);
+
+ ///
+ /// Measures the requested row parity at a retained reference offset.
+ ///
+ private int GetSad(int referenceIndex, int rowStep, int firstRow)
+ {
+ referenceIndex += firstRow * this.referenceStride;
+ int sad = TOperator.SumAbsoluteDifferences(
+ this.source[(firstRow * this.sourceStride)..],
+ this.sourceStride,
+ this.reference[referenceIndex..],
+ this.referenceStride,
+ this.blockSize.Width,
+ this.blockSize.Height - firstRow,
+ rowStep);
+
+ return sad >> this.precisionShift;
+ }
+
+ ///
+ /// Retains normalized moments and subpixel-reference motion rate for a completed integer winner.
+ ///
+ private FullPixelResult GetVarianceResult(Point vector)
+ {
+ int referenceIndex = this.referenceOrigin + (vector.Y * this.referenceStride) + vector.X;
+ TOperator.GetMoments(
+ this.source,
+ this.sourceStride,
+ this.reference[referenceIndex..],
+ this.referenceStride,
+ this.blockSize.Width,
+ this.blockSize.Height,
+ out int sum,
+ out long squares);
+
+ if (this.precisionShift != 0)
+ {
+ // Signed sums and squared sums have different scales. Round each before removing the mean;
+ // cancellation may make the rounded variance negative, so clamp the final variance to zero.
+ sum = (sum + (1 << (this.precisionShift - 1))) >> this.precisionShift;
+ int squaredShift = this.precisionShift * 2;
+ squares = (squares + (1L << (squaredShift - 1))) >> squaredShift;
+ }
+
+ int variance = (int)Math.Max(squares - (((long)sum * sum) / (this.blockSize.Width * this.blockSize.Height)), 0);
+ int rate = this.costs.GetCost(new Av1MotionVector(vector.Y * 8, vector.X * 8), this.referenceVector);
+ int motionCost = Av1RateDistortion.GetMotionSearchCost(this.rateMultiplier, rate, 0);
+ return new FullPixelResult(vector, variance, (int)squares, motionCost);
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchSettings.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchSettings.cs
new file mode 100644
index 0000000000..9827bcf680
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchSettings.cs
@@ -0,0 +1,457 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
+
+///
+/// Resolves frame-dependent motion-search policies before block traversal.
+///
+internal readonly struct Av1MotionSearchSettings
+{
+ private readonly HeifEncodingSpeed speed;
+ private readonly FullPixelSearchMethod fullPixelMethod;
+ private readonly int fasterSearchMinimumDimension;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The encoding speed.
+ /// Whether every frame is coded independently.
+ /// The visible frame dimensions.
+ /// The base quantizer index.
+ /// Whether this is a key, golden, or alternate-reference frame with boosted quality.
+ /// Whether the content classification identifies graphics or screen content.
+ public Av1MotionSearchSettings(
+ HeifEncodingSpeed speed,
+ bool intraOnly,
+ Size frameSize,
+ int qIndex,
+ bool boostedFrame,
+ bool screenContent)
+ {
+ this.speed = speed;
+ this.fullPixelMethod = FullPixelSearchMethod.NStep;
+ this.FractionalMethod = FractionalSearchMethod.TwoLevelTree;
+ this.FractionalIterationsPerStep = 2;
+ this.FractionalInterpolationTaps = 8;
+ this.SimpleMotionPrecision = SearchPrecision.EighthSample;
+ this.SecondCandidateSelection = CandidateSelection.RateDistortion;
+ this.AllowIntraBlockCopy = true;
+ this.MeshErrorThreshold = 1 << (screenContent ? 20 : 25);
+
+ // Apply coding-mode choices before resolution and quantizer overrides. Reversing that order can
+ // incorrectly suppress a second motion candidate or replace a quantizer-selected search pattern.
+ if (speed >= HeifEncodingSpeed.Level1)
+ {
+ this.MeshErrorThreshold <<= 1;
+ }
+
+ if (intraOnly)
+ {
+ this.PruneIntraBlockCopyHashCandidates = speed >= HeifEncodingSpeed.Level1;
+ this.AutomaticStepSizeLevel = speed >= HeifEncodingSpeed.Level2 ? 1 : 0;
+ this.LimitFullPixelStartingCandidates = speed >= HeifEncodingSpeed.Level3;
+ if (speed >= HeifEncodingSpeed.Level3)
+ {
+ this.fullPixelMethod = FullPixelSearchMethod.Diamond;
+ }
+
+ if (speed >= HeifEncodingSpeed.Level4)
+ {
+ this.FractionalMethod = FractionalSearchMethod.MorePrunedTree;
+ this.SimpleMotionPrecision = SearchPrecision.HalfSample;
+ this.ReduceSearchRange = true;
+ this.LimitIntraBlockCopyHashBlockSize = true;
+ }
+
+ this.MeshPruningLevel = speed >= HeifEncodingSpeed.Level5 ? 2 : 0;
+ if (speed >= HeifEncodingSpeed.Level6)
+ {
+ this.fasterSearchMinimumDimension = 32;
+ this.UseFastIntraBlockCopySearch = true;
+ }
+ }
+ else
+ {
+ this.DisableExtensiveJointSearch = true;
+ if (speed >= HeifEncodingSpeed.Level1)
+ {
+ this.UseRefiningObmcSearch = true;
+ this.FractionalInterpolationTaps = 4;
+ }
+
+ if (speed >= HeifEncodingSpeed.Level2)
+ {
+ this.SimpleMotionPrecision = SearchPrecision.QuarterSample;
+ this.FractionalIterationsPerStep = 1;
+ this.ReduceSearchRange = true;
+ }
+
+ if (speed >= HeifEncodingSpeed.Level3)
+ {
+ this.FractionalMethod = FractionalSearchMethod.PrunedTree;
+ this.fullPixelMethod = FullPixelSearchMethod.Diamond;
+ this.SecondCandidateSelection = CandidateSelection.FirstOnly;
+ this.MeshPruningLevel = 1;
+ this.AllowIntraBlockCopy = false;
+ }
+
+ if (speed >= HeifEncodingSpeed.Level4)
+ {
+ this.FractionalMethod = FractionalSearchMethod.MorePrunedTree;
+ this.SimpleMotionPrecision = SearchPrecision.HalfSample;
+ this.MeshPruningLevel = 2;
+ }
+
+ this.UseDiamondWarpSearch = speed >= HeifEncodingSpeed.Level5;
+ if (speed >= HeifEncodingSpeed.Level6)
+ {
+ this.SimpleMotionPrecision = SearchPrecision.Integer;
+ }
+ }
+
+ // Resolution classes use the shorter dimension, so rotating a frame does not change its class.
+ int minimumDimension = Math.Min(frameSize.Width, frameSize.Height);
+ bool is720pOrLarger = minimumDimension >= 720;
+ this.DownsampledSadLevel = is720pOrLarger ? 2 : 0;
+ if (!intraOnly)
+ {
+ this.ReferenceCandidatePruningLevel = speed >= HeifEncodingSpeed.Level5 ? 4
+ : speed >= HeifEncodingSpeed.Level4 && minimumDimension <= 480 ? 3
+ : speed >= HeifEncodingSpeed.Level3 ? 2
+ : speed >= HeifEncodingSpeed.Level1 ? 1 : 0;
+
+ if (speed >= HeifEncodingSpeed.Level2)
+ {
+ this.AutomaticStepSizeLevel = is720pOrLarger ? 1 : 2;
+ this.SecondCandidateSelection = !is720pOrLarger
+ ? CandidateSelection.Variance
+ : boostedFrame ? CandidateSelection.RateDistortion : CandidateSelection.FirstOnly;
+ }
+
+ if (speed >= HeifEncodingSpeed.Level4 && minimumDimension < 480)
+ {
+ this.StartCandidatePruningLevel = boostedFrame ? 0 : 1;
+ }
+
+ if (speed >= HeifEncodingSpeed.Level5)
+ {
+ this.StartCandidatePruningLevel = boostedFrame ? 0 : 1;
+ if (!is720pOrLarger)
+ {
+ this.DownsampledSadLevel = 1;
+ }
+ }
+
+ if (speed >= HeifEncodingSpeed.Level6)
+ {
+ this.StartCandidatePruningLevel = boostedFrame ? 0 : 2;
+ this.fasterSearchMinimumDimension = is720pOrLarger ? 128 : 64;
+ }
+ }
+
+ // Coarse quantization selects a less expensive full-pixel pattern even at the slower speed levels.
+ // These thresholds apply to the coding pass; first-pass statistics use a separate configuration.
+ if (speed <= HeifEncodingSpeed.Level2)
+ {
+ int coarseThreshold;
+ int intermediateThreshold;
+ if (is720pOrLarger)
+ {
+ coarseThreshold = speed == HeifEncodingSpeed.Level2 ? 200 : 255;
+ intermediateThreshold = speed == HeifEncodingSpeed.Level0 ? 200 : -1;
+ }
+ else
+ {
+ coarseThreshold = speed == HeifEncodingSpeed.Level0 ? 200 : 170;
+ intermediateThreshold = speed switch
+ {
+ HeifEncodingSpeed.Level0 => 70,
+ HeifEncodingSpeed.Level1 => 50,
+ _ => 40
+ };
+ }
+
+ if (qIndex > coarseThreshold)
+ {
+ this.fullPixelMethod = is720pOrLarger
+ ? FullPixelSearchMethod.Diamond
+ : FullPixelSearchMethod.ClampedDiamond;
+ }
+ else if (qIndex > intermediateThreshold)
+ {
+ this.fullPixelMethod = FullPixelSearchMethod.EightPointNStep;
+ }
+ }
+ }
+
+ ///
+ /// The full-pixel search pattern.
+ ///
+ public enum FullPixelSearchMethod
+ {
+ ///
+ /// Repeated shrinking diamond searches.
+ ///
+ Diamond,
+
+ ///
+ /// Searches with eight or twelve sites at progressively smaller radii.
+ ///
+ NStep,
+
+ ///
+ /// Searches with eight sites at every radius.
+ ///
+ EightPointNStep,
+
+ ///
+ /// Diamond search with repeated, bounded initial radii.
+ ///
+ ClampedDiamond,
+
+ ///
+ /// Hexagonal search followed by local refinement.
+ ///
+ Hexagon,
+
+ ///
+ /// Large diamond search followed by local refinement.
+ ///
+ BigDiamond,
+
+ ///
+ /// Diamond search beginning at a reduced scale.
+ ///
+ FastDiamond,
+
+ ///
+ /// Large diamond search beginning at a reduced scale.
+ ///
+ FastBigDiamond,
+
+ ///
+ /// Diamond search with only the smallest scales.
+ ///
+ VeryFastDiamond
+ }
+
+ ///
+ /// The fractional-pixel search traversal.
+ ///
+ public enum FractionalSearchMethod
+ {
+ ///
+ /// Cardinal and selected diagonal searches with a second refinement level.
+ ///
+ TwoLevelTree,
+
+ ///
+ /// Pruned tree search using the integer cost neighborhood when available.
+ ///
+ PrunedTree,
+
+ ///
+ /// Pruned tree search with additional quadratic cost-surface prediction.
+ ///
+ MorePrunedTree
+ }
+
+ ///
+ /// The finest displacement examined by a fractional search.
+ ///
+ public enum SearchPrecision
+ {
+ ///
+ /// One eighth of a luma sample.
+ ///
+ EighthSample,
+
+ ///
+ /// One quarter of a luma sample.
+ ///
+ QuarterSample,
+
+ ///
+ /// One half of a luma sample.
+ ///
+ HalfSample,
+
+ ///
+ /// Whole luma samples.
+ ///
+ Integer
+ }
+
+ ///
+ /// The comparison used after refining a second motion candidate.
+ ///
+ public enum CandidateSelection
+ {
+ ///
+ /// Compare estimated transform rate and distortion.
+ ///
+ RateDistortion,
+
+ ///
+ /// Compare prediction variance and motion-vector rate.
+ ///
+ Variance,
+
+ ///
+ /// Refine only the first full-pixel winner.
+ ///
+ FirstOnly
+ }
+
+ ///
+ /// Gets the adaptation level for the initial full-pixel step.
+ ///
+ public int AutomaticStepSizeLevel { get; }
+
+ ///
+ /// Gets the fractional search traversal.
+ ///
+ public FractionalSearchMethod FractionalMethod { get; }
+
+ ///
+ /// Gets the refinement iterations at each fractional precision.
+ ///
+ public int FractionalIterationsPerStep { get; }
+
+ ///
+ /// Gets the interpolation tap count used during fractional search.
+ ///
+ public int FractionalInterpolationTaps { get; }
+
+ ///
+ /// Gets the finest precision used by preliminary simple-motion analysis.
+ ///
+ public SearchPrecision SimpleMotionPrecision { get; }
+
+ ///
+ /// Gets the variance threshold for following a stepped search with a mesh search.
+ ///
+ public int MeshErrorThreshold { get; }
+
+ ///
+ /// Gets a value indicating whether earlier reference-index results restrict subsequent search ranges.
+ ///
+ public bool ReduceSearchRange { get; }
+
+ ///
+ /// Gets the level used to prune mesh search based on motion displacement.
+ ///
+ public int MeshPruningLevel { get; }
+
+ ///
+ /// Gets a value indicating whether overlapped prediction uses local full-pixel refinement.
+ ///
+ public bool UseRefiningObmcSearch { get; }
+
+ ///
+ /// Gets a value indicating whether full-pixel search omits additional temporal-analysis starting candidates.
+ ///
+ public bool LimitFullPixelStartingCandidates { get; }
+
+ ///
+ /// Gets a value indicating whether intra-block-copy motion search is enabled.
+ ///
+ public bool AllowIntraBlockCopy { get; }
+
+ ///
+ /// Gets a value indicating whether block-copy hash search stops after the first 64 candidates.
+ ///
+ public bool PruneIntraBlockCopyHashCandidates { get; }
+
+ ///
+ /// Gets a value indicating whether block copy restricts geometry and uses pixel search only after hash search fails.
+ ///
+ public bool UseFastIntraBlockCopySearch { get; }
+
+ ///
+ /// Gets a value indicating whether block-copy hashing is restricted to 4x4 and 8x8 blocks.
+ ///
+ public bool LimitIntraBlockCopyHashBlockSize { get; }
+
+ ///
+ /// Gets the row-subsampling policy: zero disables it, one checks the starting SAD, and two checks only the final SAD.
+ ///
+ public int DownsampledSadLevel { get; }
+
+ ///
+ /// Gets a value indicating whether compound motion omits the extensive joint refinement search.
+ ///
+ public bool DisableExtensiveJointSearch { get; }
+
+ ///
+ /// Gets how two fractional motion candidates are compared.
+ ///
+ public CandidateSelection SecondCandidateSelection { get; }
+
+ ///
+ /// Gets a value indicating whether zero, four, or eight neighboring start/reference positions can reuse an earlier search.
+ ///
+ public int StartCandidatePruningLevel { get; }
+
+ ///
+ /// Gets the pruning level applied across dynamic reference-vector choices after motion search.
+ ///
+ public int ReferenceCandidatePruningLevel { get; }
+
+ ///
+ /// Gets a value indicating whether warped-motion refinement uses a diamond instead of a square.
+ ///
+ public bool UseDiamondWarpSearch { get; }
+
+ ///
+ /// Gets the full-pixel method for the current block geometry.
+ ///
+ /// The prediction block size.
+ /// The frame-selected method after its block-size override.
+ public FullPixelSearchMethod GetFullPixelMethod(Av1BlockSize blockSize)
+ {
+ int minimumDimension = Math.Min(blockSize.GetWidth(), blockSize.GetHeight());
+ if (this.fasterSearchMinimumDimension == 0 || minimumDimension < this.fasterSearchMinimumDimension)
+ {
+ return this.fullPixelMethod;
+ }
+
+ return this.fullPixelMethod switch
+ {
+ FullPixelSearchMethod.NStep or FullPixelSearchMethod.EightPointNStep => FullPixelSearchMethod.Diamond,
+ FullPixelSearchMethod.Diamond or FullPixelSearchMethod.ClampedDiamond => FullPixelSearchMethod.BigDiamond,
+ FullPixelSearchMethod.BigDiamond => FullPixelSearchMethod.Hexagon,
+ FullPixelSearchMethod.Hexagon => FullPixelSearchMethod.FastDiamond,
+ FullPixelSearchMethod.FastDiamond => FullPixelSearchMethod.VeryFastDiamond,
+ _ => this.fullPixelMethod
+ };
+ }
+
+ ///
+ /// Gets the successive mesh ranges and sampling intervals in full luma samples.
+ ///
+ /// Whether the search references the current reconstruction.
+ /// Four range/interval pairs. Traversal ends after the first interval of one.
+ public ReadOnlySpan GetMeshPattern(bool intraBlockCopy)
+ {
+ // The alternating range/interval layout is immutable static storage. A frame or candidate does not
+ // allocate a pattern, and an interval of one terminates refinement before unused trailing entries.
+ if (intraBlockCopy)
+ {
+ return this.speed switch
+ {
+ <= HeifEncodingSpeed.Level1 => [256, 1, 256, 1, 0, 0, 0, 0],
+ <= HeifEncodingSpeed.Level3 => [64, 1, 64, 1, 0, 0, 0, 0],
+ _ => [64, 4, 16, 1, 0, 0, 0, 0]
+ };
+ }
+
+ return this.speed switch
+ {
+ <= HeifEncodingSpeed.Level1 => [64, 8, 28, 4, 15, 1, 7, 1],
+ HeifEncodingSpeed.Level2 => [64, 8, 14, 2, 7, 1, 7, 1],
+ _ => [64, 16, 24, 8, 12, 4, 7, 1]
+ };
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchSites.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchSites.cs
new file mode 100644
index 0000000000..51111cb46a
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionSearchSites.cs
@@ -0,0 +1,189 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+using static SixLabors.ImageSharp.Formats.Heif.Av1.Motion.Av1MotionSearchSettings;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
+
+///
+/// Borrows one retained search-site configuration, including stride-relative sample offsets.
+///
+internal readonly ref struct Av1MotionSearchSites
+{
+ ///
+ /// The number of integer storage elements for sites, stage metadata, and the configured stride.
+ ///
+ public const int StorageLength = (22 * 17 * 2) + (22 * 2) + 2;
+
+ private const int StageCapacity = 22;
+ private const int SitesPerStage = 17;
+ private const int SiteStorageLength = StageCapacity * SitesPerStage * 2;
+ private const int StageCountOffset = SiteStorageLength + (StageCapacity * 2);
+ private readonly Span storage;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The retained configuration storage, whose stride slot is initialized by its owner.
+ public Av1MotionSearchSites(Span storage) => this.storage = storage;
+
+ ///
+ /// Gets the number of populated search stages.
+ ///
+ public int StageCount => this.storage[StageCountOffset];
+
+ ///
+ /// Gets the number of non-center candidates at the given stage.
+ ///
+ /// The stage, ordered from the smallest search radius.
+ /// The candidate count.
+ public int GetCandidateCount(int stage) => this.storage[SiteStorageLength + stage];
+
+ ///
+ /// Gets the radius of the given stage in full samples.
+ ///
+ /// The stage, ordered from the smallest search radius.
+ /// The search radius.
+ public int GetRadius(int stage) => this.storage[SiteStorageLength + StageCapacity + stage];
+
+ ///
+ /// Gets the ordered candidate sites for a stage.
+ ///
+ /// The stage, ordered from the smallest search radius.
+ /// The fixed stage slot; only the configured candidate entries are populated.
+ public ReadOnlySpan GetSites(int stage)
+ => MemoryMarshal.Cast(this.storage[..SiteStorageLength]).Slice(stage * SitesPerStage, SitesPerStage);
+
+ ///
+ /// Initializes or refreshes offsets when the retained reference plane's stride changes.
+ ///
+ /// The distinct search shape owned by this configuration.
+ /// The reference plane stride in samples.
+ public void Configure(FullPixelSearchMethod method, int stride)
+ {
+ if (this.storage[StageCountOffset + 1] == stride)
+ {
+ return;
+ }
+
+ Span sites = MemoryMarshal.Cast(this.storage[..SiteStorageLength]);
+ Span counts = this.storage.Slice(SiteStorageLength, StageCapacity);
+ Span radii = this.storage.Slice(SiteStorageLength + StageCapacity, StageCapacity);
+ bool nStep = method is FullPixelSearchMethod.NStep or FullPixelSearchMethod.EightPointNStep;
+ bool diamond = method is FullPixelSearchMethod.Diamond or FullPixelSearchMethod.ClampedDiamond;
+ int stageCount = nStep ? (method == FullPixelSearchMethod.NStep ? 15 : 16) : 11;
+ int radius = 1;
+ for (int stage = 0; stage < stageCount; stage++)
+ {
+ Span stageSites = sites.Slice(stage * SitesPerStage, SitesPerStage);
+ if (diamond)
+ {
+ // The clamped shape repeats its three outer stages at radius 256. Retaining those stages
+ // matters because a move at one stage permits another move at the same radius.
+ radius = 1 << Math.Min(stage, method == FullPixelSearchMethod.ClampedDiamond ? 8 : 10);
+ }
+ else if (!nStep)
+ {
+ radius = 1 << stage;
+ }
+
+ radii[stage] = radius;
+ if (nStep || diamond)
+ {
+ bool twelveSites = nStep && radius > 5 && method != FullPixelSearchMethod.EightPointNStep;
+ int tangent = twelveSites ? Math.Max((int)(0.41 * radius), 1) : radius;
+ counts[stage] = twelveSites ? 12 : 8;
+ stageSites[0] = new Site(0, 0, stride);
+ stageSites[1] = new Site(-radius, 0, stride);
+ stageSites[2] = new Site(radius, 0, stride);
+ stageSites[3] = new Site(0, -radius, stride);
+ stageSites[4] = new Site(0, radius, stride);
+ stageSites[5] = new Site(-radius, -tangent, stride);
+ stageSites[6] = new Site(radius, tangent, stride);
+ stageSites[7] = new Site(-tangent, radius, stride);
+ stageSites[8] = new Site(tangent, -radius, stride);
+ if (twelveSites)
+ {
+ stageSites[9] = new Site(-radius, tangent, stride);
+ stageSites[10] = new Site(radius, -tangent, stride);
+ stageSites[11] = new Site(tangent, radius, stride);
+ stageSites[12] = new Site(-tangent, -radius, stride);
+ }
+
+ // N-step radii grow by rounded halves through stage twelve, then retain the outer radius.
+ if (nStep && stage < 12)
+ {
+ radius = Math.Max(((3 * radius) + 1) / 2, radius + 1);
+ }
+ }
+ else
+ {
+ // Pattern sites omit the center. Pairs are row then column, in traversal order.
+ // Beyond scale zero, multiply the half-radius by these integer coordinates.
+ ReadOnlySpan coordinates;
+ int scale;
+ if (method == FullPixelSearchMethod.Hexagon)
+ {
+ coordinates = stage == 0
+ ? [-1, -1, 0, -1, 1, -1, 1, 0, 1, 1, 0, 1, -1, 1, -1, 0]
+ : [-1, -2, 1, -2, 2, 0, 1, 2, -1, 2, -2, 0];
+
+ scale = stage == 0 ? 1 : radius / 2;
+ }
+ else
+ {
+ coordinates = stage == 0
+ ? [0, -1, 1, 0, 0, 1, -1, 0]
+ : [-1, -1, 0, -2, 1, -1, 2, 0, 1, 1, 0, 2, -1, 1, -2, 0];
+
+ scale = stage == 0 ? 1 : radius / 2;
+ }
+
+ counts[stage] = coordinates.Length / 2;
+ for (int index = 0; index < counts[stage]; index++)
+ {
+ stageSites[index] = new Site(coordinates[index * 2] * scale, coordinates[(index * 2) + 1] * scale, stride);
+ }
+ }
+ }
+
+ this.storage[StageCountOffset] = stageCount;
+ this.storage[StageCountOffset + 1] = stride;
+ }
+
+ ///
+ /// Stores one full-sample displacement and its reference-plane offset in eight bytes.
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public readonly struct Site
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The vertical full-sample displacement.
+ /// The horizontal full-sample displacement.
+ /// The reference row stride in samples.
+ public Site(int row, int column, int stride)
+ {
+ this.Row = (short)row;
+ this.Column = (short)column;
+ this.Offset = (row * stride) + column;
+ }
+
+ ///
+ /// Gets the vertical full-sample displacement.
+ ///
+ public short Row { get; }
+
+ ///
+ /// Gets the horizontal full-sample displacement.
+ ///
+ public short Column { get; }
+
+ ///
+ /// Gets the signed displacement in reference-plane samples.
+ ///
+ public int Offset { get; }
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs
index 2854d1562f..59c741e7c3 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs
@@ -105,6 +105,68 @@ internal readonly struct Av1MotionVector : IEquatable
/// when either component differs; otherwise, .
public static bool operator !=(Av1MotionVector left, Av1MotionVector right) => !left.Equals(right);
+ ///
+ /// Computes the full-pixel displacement region that can produce distinct, padded block predictions.
+ ///
+ /// The block's luma rectangle in the coded frame.
+ /// The frame dimensions rounded to mode-information units.
+ /// The allocated luma border on every side.
+ /// The displacement region with exclusive right and bottom edges.
+ public static Rectangle GetFrameSearchBounds(Rectangle block, Size frameSize, int border)
+ {
+ // Reserve eight samples for interpolation support and restrict candidates beyond a replicated edge
+ // once moving farther cannot change the prediction. Bounds describe displacement from this block,
+ // so interior blocks can move across the frame rather than being restricted to the border width.
+ int minimumColumn = Math.Max(-(block.X + border - 8), -(block.Right + 8));
+ int minimumRow = Math.Max(-(block.Y + border - 8), -(block.Bottom + 8));
+ int maximumColumn = Math.Min(frameSize.Width - block.Right + border - 8, frameSize.Width - block.X + 8);
+ int maximumRow = Math.Min(frameSize.Height - block.Bottom + border - 8, frameSize.Height - block.Y + 8);
+ return Rectangle.FromLTRB(minimumColumn, minimumRow, maximumColumn + 1, maximumRow + 1);
+ }
+
+ ///
+ /// Restricts a frame displacement region to representable full-pixel candidates around this reference.
+ ///
+ /// The full-pixel region from .
+ /// The full-pixel search region with exclusive right and bottom edges.
+ public Rectangle GetFullPixelSearchBounds(Rectangle frameBounds)
+ {
+ const int MaximumDisplacement = 1023;
+
+ // Both endpoints must fit inside a 1023-pixel displacement from the fractional reference. Round the
+ // lower endpoint toward positive infinity and the upper toward negative infinity, including for
+ // negative references. Keep the reserved vector-domain endpoints out of the search as well.
+ int minimumColumn = Math.Max(frameBounds.Left, Math.Max(((this.Column + 7) >> 3) - MaximumDisplacement, (LowerBound >> 3) + 1));
+ int minimumRow = Math.Max(frameBounds.Top, Math.Max(((this.Row + 7) >> 3) - MaximumDisplacement, (LowerBound >> 3) + 1));
+ int maximumColumn = Math.Min(frameBounds.Right - 1, Math.Min((this.Column >> 3) + MaximumDisplacement, (UpperBound >> 3) - 1));
+ int maximumRow = Math.Min(frameBounds.Bottom - 1, Math.Min((this.Row >> 3) + MaximumDisplacement, (UpperBound >> 3) - 1));
+ maximumColumn = Math.Max(minimumColumn, maximumColumn);
+ maximumRow = Math.Max(minimumRow, maximumRow);
+ return Rectangle.FromLTRB(minimumColumn, minimumRow, maximumColumn + 1, maximumRow + 1);
+ }
+
+ ///
+ /// Restricts a frame displacement region to representable fractional candidates around this reference.
+ ///
+ /// The full-pixel region from .
+ /// The eighth-sample search region with exclusive right and bottom edges.
+ public Rectangle GetSubpixelSearchBounds(Rectangle frameBounds)
+ {
+ const int MaximumDisplacement = 1023 * SubpixelScale;
+
+ // Refine against the original frame region, not the rounded full-pixel intersection. Otherwise the
+ // fractional portion between an integer endpoint and the reference-centered limit would be lost.
+ int minimumColumn = Math.Max(frameBounds.Left * SubpixelScale, this.Column - MaximumDisplacement);
+ int minimumRow = Math.Max(frameBounds.Top * SubpixelScale, this.Row - MaximumDisplacement);
+ int maximumColumn = Math.Min((frameBounds.Right - 1) * SubpixelScale, this.Column + MaximumDisplacement);
+ int maximumRow = Math.Min((frameBounds.Bottom - 1) * SubpixelScale, this.Row + MaximumDisplacement);
+ maximumColumn = Math.Min(UpperBound - 1, Math.Max(minimumColumn, maximumColumn));
+ maximumRow = Math.Min(UpperBound - 1, Math.Max(minimumRow, maximumRow));
+ minimumColumn = Math.Max(LowerBound + 1, minimumColumn);
+ minimumRow = Math.Max(LowerBound + 1, minimumRow);
+ return Rectangle.FromLTRB(minimumColumn, minimumRow, maximumColumn + 1, maximumRow + 1);
+ }
+
///
/// Reduces this vector to the motion-vector precision selected by the current frame.
///
diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderBlockWorkspace.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderBlockWorkspace.cs
index 62e982c752..efd96ea31f 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderBlockWorkspace.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderBlockWorkspace.cs
@@ -3,6 +3,7 @@
using System.Buffers;
using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
@@ -26,7 +27,7 @@ internal sealed class Av1EncoderBlockWorkspace : IDisposable
public const int MaximumCoefficientCount = (Av1Constants.MaxTransformSize / 2) * (Av1Constants.MaxTransformSize / 2);
///
- /// The complete workspace length in signed-integer storage elements.
+ /// The base workspace length in signed-integer storage elements, excluding inter-motion state.
///
public const int StorageLength =
ResidualStorageLength +
@@ -37,6 +38,8 @@ internal sealed class Av1EncoderBlockWorkspace : IDisposable
PartitionContextStorageLength;
private const int ResidualStorageLength = MaximumResidualCount / 2;
+ private const int MotionSearchSiteCount = 6;
+ private const int MotionSearchSiteStorageOffset = StorageLength + Av1MotionVectorCosts.StorageLength;
private const int TransformCoefficientOffset = ResidualStorageLength;
private const int DequantizedCoefficientOffset = TransformCoefficientOffset + MaximumCoefficientCount;
private const int TransformWorkspaceOffset = DequantizedCoefficientOffset + MaximumCoefficientCount;
@@ -117,7 +120,34 @@ internal sealed class Av1EncoderBlockWorkspace : IDisposable
///
/// The configuration providing the encoder allocator.
public Av1EncoderBlockWorkspace(Configuration configuration)
- => this.owner = configuration.MemoryAllocator.Allocate(StorageLength);
+ : this(configuration, allocateInterMotionCosts: false)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class for a fixed encoding mode.
+ ///
+ /// The configuration providing the encoder allocator.
+ /// Whether the worker will encode inter frames.
+ public Av1EncoderBlockWorkspace(Configuration configuration, bool allocateInterMotionCosts)
+ {
+ // Motion rates belong to the worker, not a block candidate or frame. Keep both precision pairs after
+ // the existing scratch regions so sequence frames can change precision while retaining one owner.
+ int length = StorageLength +
+ (allocateInterMotionCosts ? Av1MotionVectorCosts.StorageLength + (MotionSearchSiteCount * Av1MotionSearchSites.StorageLength) : 0);
+
+ this.owner = configuration.MemoryAllocator.Allocate(length);
+ if (allocateInterMotionCosts)
+ {
+ // Each shape retains its offsets across frames. A zero stride marks its first use; every populated
+ // site and stage is subsequently overwritten when the reference stride changes.
+ Span storage = this.owner.Memory.Span;
+ for (int index = 0; index < MotionSearchSiteCount; index++)
+ {
+ storage[MotionSearchSiteStorageOffset + ((index + 1) * Av1MotionSearchSites.StorageLength) - 1] = 0;
+ }
+ }
+ }
///
/// Gets the maximum-size spatial residual workspace as a compact 16-bit view of the aligned owner.
@@ -148,6 +178,33 @@ internal sealed class Av1EncoderBlockWorkspace : IDisposable
///
public ref Av1ReferenceMotionVectors ReferenceMotionVectors => ref this.referenceMotionVectors;
+ ///
+ /// Borrows the inter-motion rate tables for the current frame's precision.
+ ///
+ /// The fractional precision selected by the frame.
+ /// The worker's reusable motion-rate view.
+ public Av1MotionVectorCosts GetMotionVectorCosts(Av1MotionVectorPrecision precision)
+ => new(this.owner.Memory.Span.Slice(StorageLength, Av1MotionVectorCosts.StorageLength), precision);
+
+ ///
+ /// Gets the retained full-pixel search geometry for the reference plane's current stride.
+ ///
+ /// The block-selected search method.
+ /// The reference row stride in samples.
+ /// The configured non-owning search-site view.
+ public Av1MotionSearchSites GetMotionSearchSites(Av1MotionSearchSettings.FullPixelSearchMethod method, int stride)
+ {
+ // Fast diamond variants differ in stage selection, so they share the big-diamond geometry slot.
+ Av1MotionSearchSettings.FullPixelSearchMethod shape = method > Av1MotionSearchSettings.FullPixelSearchMethod.BigDiamond
+ ? Av1MotionSearchSettings.FullPixelSearchMethod.BigDiamond
+ : method;
+
+ int offset = MotionSearchSiteStorageOffset + ((int)shape * Av1MotionSearchSites.StorageLength);
+ Av1MotionSearchSites sites = new(this.owner.Memory.Span.Slice(offset, Av1MotionSearchSites.StorageLength));
+ sites.Configure(shape, stride);
+ return sites;
+ }
+
///
/// Gets the disjoint edge snapshot used to restore one square partition-search level.
///
diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Search.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Search.cs
new file mode 100644
index 0000000000..c4edbfcc4d
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Search.cs
@@ -0,0 +1,240 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
+
+internal static partial class Av1TranslationalInterPredictor
+{
+ ///
+ /// The sample capacity for a 128-column search prediction and its eight-tap vertical support.
+ ///
+ public const int SearchPredictionBufferLength = 136 * 128;
+
+ ///
+ /// Produces a search prediction, rounding and clipping each separable pass to the component precision.
+ ///
+ /// The bordered reference plane.
+ /// The reference row stride in samples.
+ /// The integer prediction origin.
+ /// The borrowed search buffer; the packed result occupies its first width times height samples.
+ /// The prediction width.
+ /// The prediction height.
+ /// The horizontal fraction in eighth-sample units.
+ /// The vertical fraction in eighth-sample units.
+ /// The selected two-, four-, or eight-tap search filter.
+ public static void PredictForSearch(
+ ReadOnlySpan source,
+ int sourceStride,
+ int sourceOrigin,
+ Span buffer,
+ int width,
+ int height,
+ int horizontalPhase,
+ int verticalPhase,
+ int taps)
+ {
+ if ((horizontalPhase | verticalPhase) == 0)
+ {
+ Copy(source, sourceStride, sourceOrigin, buffer, width, width, height);
+ return;
+ }
+
+ ReadOnlySpan table = taps == 2 ? Bilinear : taps == 4 ? RegularFourTap : RegularEightTap;
+ ReadOnlySpan horizontal = GetPhase(table, horizontalPhase * 2);
+ ReadOnlySpan vertical = GetPhase(table, verticalPhase * 2);
+ GetEffectiveKernel(horizontal, out int horizontalFirst, out int horizontalCount);
+ GetEffectiveKernel(vertical, out int verticalFirst, out int verticalCount);
+
+ // Search interpolation applies a single Q7 rounding and clips after each pass. Keeping both
+ // passes in sample storage preserves those clipped values when they feed the vertical filter.
+ if (verticalPhase == 0)
+ {
+ FilterDirect(
+ source,
+ sourceStride,
+ sourceOrigin,
+ buffer,
+ width,
+ width,
+ height,
+ horizontal[horizontalFirst..],
+ horizontalCount,
+ horizontalFirst - 3,
+ 1,
+ FilterBits,
+ 0);
+
+ return;
+ }
+
+ if (horizontalPhase == 0)
+ {
+ FilterDirect(
+ source,
+ sourceStride,
+ sourceOrigin,
+ buffer,
+ width,
+ width,
+ height,
+ vertical[verticalFirst..],
+ verticalCount,
+ (verticalFirst - 3) * sourceStride,
+ sourceStride,
+ FilterBits,
+ 0);
+
+ return;
+ }
+
+ // The vertical support occupies seven additional rows at a fixed 128-sample stride. The final
+ // top-to-bottom pass packs its output over consumed rows of the same buffer: width never exceeds
+ // that stride, and a written column cannot affect another column's vertical convolution.
+ FilterDirect(
+ source,
+ sourceStride,
+ sourceOrigin - (3 * sourceStride),
+ buffer,
+ 128,
+ width,
+ height + 7,
+ horizontal[horizontalFirst..],
+ horizontalCount,
+ horizontalFirst - 3,
+ 1,
+ FilterBits,
+ 0);
+
+ FilterDirect(
+ buffer,
+ 128,
+ 3 * 128,
+ buffer,
+ width,
+ width,
+ height,
+ vertical[verticalFirst..],
+ verticalCount,
+ (verticalFirst - 3) * 128,
+ 128,
+ FilterBits,
+ 0);
+ }
+
+ ///
+ /// Produces a search prediction, rounding and clipping each separable pass to the component precision.
+ ///
+ /// The bordered reference plane.
+ /// The reference row stride in samples.
+ /// The integer prediction origin.
+ /// The borrowed search buffer; the packed result occupies its first width times height samples.
+ /// The prediction width.
+ /// The prediction height.
+ /// The horizontal fraction in eighth-sample units.
+ /// The vertical fraction in eighth-sample units.
+ /// The selected two-, four-, or eight-tap search filter.
+ /// The coded component precision.
+ public static void PredictForSearch(
+ ReadOnlySpan source,
+ int sourceStride,
+ int sourceOrigin,
+ Span buffer,
+ int width,
+ int height,
+ int horizontalPhase,
+ int verticalPhase,
+ int taps,
+ int bitDepth)
+ {
+ if ((horizontalPhase | verticalPhase) == 0)
+ {
+ Copy(source, sourceStride, sourceOrigin, buffer, width, width, height);
+ return;
+ }
+
+ ReadOnlySpan table = taps == 2 ? Bilinear : taps == 4 ? RegularFourTap : RegularEightTap;
+ ReadOnlySpan horizontal = GetPhase(table, horizontalPhase * 2);
+ ReadOnlySpan vertical = GetPhase(table, verticalPhase * 2);
+ GetEffectiveKernel(horizontal, out int horizontalFirst, out int horizontalCount);
+ GetEffectiveKernel(vertical, out int verticalFirst, out int verticalCount);
+
+ // Search interpolation applies a single Q7 rounding and clips after each pass. Keeping both
+ // passes in sample storage preserves those clipped values when they feed the vertical filter.
+ if (verticalPhase == 0)
+ {
+ FilterDirect(
+ source,
+ sourceStride,
+ sourceOrigin,
+ buffer,
+ width,
+ width,
+ height,
+ horizontal[horizontalFirst..],
+ horizontalCount,
+ horizontalFirst - 3,
+ 1,
+ FilterBits,
+ 0,
+ bitDepth);
+
+ return;
+ }
+
+ if (horizontalPhase == 0)
+ {
+ FilterDirect(
+ source,
+ sourceStride,
+ sourceOrigin,
+ buffer,
+ width,
+ width,
+ height,
+ vertical[verticalFirst..],
+ verticalCount,
+ (verticalFirst - 3) * sourceStride,
+ sourceStride,
+ FilterBits,
+ 0,
+ bitDepth);
+
+ return;
+ }
+
+ // The vertical support occupies seven additional rows at a fixed 128-sample stride. The final
+ // top-to-bottom pass packs its output over consumed rows of the same buffer: width never exceeds
+ // that stride, and a written column cannot affect another column's vertical convolution.
+ FilterDirect(
+ source,
+ sourceStride,
+ sourceOrigin - (3 * sourceStride),
+ buffer,
+ 128,
+ width,
+ height + 7,
+ horizontal[horizontalFirst..],
+ horizontalCount,
+ horizontalFirst - 3,
+ 1,
+ FilterBits,
+ 0,
+ bitDepth);
+
+ FilterDirect(
+ buffer,
+ 128,
+ 3 * 128,
+ buffer,
+ width,
+ width,
+ height,
+ vertical[verticalFirst..],
+ verticalCount,
+ (verticalFirst - 3) * 128,
+ 128,
+ FilterBits,
+ 0,
+ bitDepth);
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/HeifEncodingSpeed.cs b/src/ImageSharp/Formats/Heif/HeifEncodingSpeed.cs
new file mode 100644
index 0000000000..6d5ff249ad
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/HeifEncodingSpeed.cs
@@ -0,0 +1,61 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Heif;
+
+///
+/// Specifies the balance between encoding speed and compression efficiency for AV1 images.
+/// Higher levels prioritize encoding speed over compression efficiency.
+///
+public enum HeifEncodingSpeed
+{
+ ///
+ /// The slowest encoding level and the default setting.
+ ///
+ Level0 = 0,
+
+ ///
+ /// Encoding speed level 1.
+ ///
+ Level1 = 1,
+
+ ///
+ /// Encoding speed level 2.
+ ///
+ Level2 = 2,
+
+ ///
+ /// Encoding speed level 3.
+ ///
+ Level3 = 3,
+
+ ///
+ /// Encoding speed level 4.
+ ///
+ Level4 = 4,
+
+ ///
+ /// Encoding speed level 5.
+ ///
+ Level5 = 5,
+
+ ///
+ /// Encoding speed level 6.
+ ///
+ Level6 = 6,
+
+ ///
+ /// Encoding speed level 7.
+ ///
+ Level7 = 7,
+
+ ///
+ /// Encoding speed level 8.
+ ///
+ Level8 = 8,
+
+ ///
+ /// The fastest encoding level.
+ ///
+ Level9 = 9
+}
diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionSearchSettingsTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionSearchSettingsTests.cs
new file mode 100644
index 0000000000..d27379f0e8
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionSearchSettingsTests.cs
@@ -0,0 +1,117 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Heif;
+using SixLabors.ImageSharp.Formats.Heif.Av1;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
+using static SixLabors.ImageSharp.Formats.Heif.Av1.Motion.Av1MotionSearchSettings;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
+
+public class Av1MotionSearchSettingsTests
+{
+ [Theory]
+ [InlineData(HeifEncodingSpeed.Level0, false, 719, 70, Av1BlockSize.Block8x8, FullPixelSearchMethod.NStep)]
+ [InlineData(HeifEncodingSpeed.Level0, false, 719, 71, Av1BlockSize.Block8x8, FullPixelSearchMethod.EightPointNStep)]
+ [InlineData(HeifEncodingSpeed.Level0, false, 719, 200, Av1BlockSize.Block8x8, FullPixelSearchMethod.EightPointNStep)]
+ [InlineData(HeifEncodingSpeed.Level0, false, 719, 201, Av1BlockSize.Block8x8, FullPixelSearchMethod.ClampedDiamond)]
+ [InlineData(HeifEncodingSpeed.Level0, false, 720, 200, Av1BlockSize.Block8x8, FullPixelSearchMethod.NStep)]
+ [InlineData(HeifEncodingSpeed.Level0, false, 720, 201, Av1BlockSize.Block8x8, FullPixelSearchMethod.EightPointNStep)]
+ [InlineData(HeifEncodingSpeed.Level1, false, 719, 50, Av1BlockSize.Block8x8, FullPixelSearchMethod.NStep)]
+ [InlineData(HeifEncodingSpeed.Level1, false, 719, 51, Av1BlockSize.Block8x8, FullPixelSearchMethod.EightPointNStep)]
+ [InlineData(HeifEncodingSpeed.Level1, false, 720, 0, Av1BlockSize.Block8x8, FullPixelSearchMethod.EightPointNStep)]
+ [InlineData(HeifEncodingSpeed.Level2, false, 719, 40, Av1BlockSize.Block8x8, FullPixelSearchMethod.NStep)]
+ [InlineData(HeifEncodingSpeed.Level2, false, 719, 41, Av1BlockSize.Block8x8, FullPixelSearchMethod.EightPointNStep)]
+ [InlineData(HeifEncodingSpeed.Level2, false, 719, 170, Av1BlockSize.Block8x8, FullPixelSearchMethod.EightPointNStep)]
+ [InlineData(HeifEncodingSpeed.Level2, false, 719, 171, Av1BlockSize.Block8x8, FullPixelSearchMethod.ClampedDiamond)]
+ [InlineData(HeifEncodingSpeed.Level2, false, 720, 200, Av1BlockSize.Block8x8, FullPixelSearchMethod.EightPointNStep)]
+ [InlineData(HeifEncodingSpeed.Level2, false, 720, 201, Av1BlockSize.Block8x8, FullPixelSearchMethod.Diamond)]
+ [InlineData(HeifEncodingSpeed.Level6, false, 719, 120, Av1BlockSize.Block32x64, FullPixelSearchMethod.Diamond)]
+ [InlineData(HeifEncodingSpeed.Level6, false, 719, 120, Av1BlockSize.Block64x64, FullPixelSearchMethod.BigDiamond)]
+ [InlineData(HeifEncodingSpeed.Level6, false, 720, 120, Av1BlockSize.Block64x128, FullPixelSearchMethod.Diamond)]
+ [InlineData(HeifEncodingSpeed.Level6, false, 720, 120, Av1BlockSize.Block128x128, FullPixelSearchMethod.BigDiamond)]
+ [InlineData(HeifEncodingSpeed.Level2, true, 719, 171, Av1BlockSize.Block8x8, FullPixelSearchMethod.ClampedDiamond)]
+ [InlineData(HeifEncodingSpeed.Level6, true, 719, 120, Av1BlockSize.Block16x32, FullPixelSearchMethod.Diamond)]
+ [InlineData(HeifEncodingSpeed.Level6, true, 719, 120, Av1BlockSize.Block32x32, FullPixelSearchMethod.BigDiamond)]
+ public void FullPixelPatternUsesQuantizerResolutionAndBlockGeometry(
+ HeifEncodingSpeed speed,
+ bool intraOnly,
+ int minimumDimension,
+ int qIndex,
+ int blockSize,
+ int expected)
+ {
+ Av1MotionSearchSettings landscape = new(speed, intraOnly, new Size(1920, minimumDimension), qIndex, false, false);
+ Av1MotionSearchSettings portrait = new(speed, intraOnly, new Size(minimumDimension, 1920), qIndex, false, false);
+
+ Assert.Equal((FullPixelSearchMethod)expected, landscape.GetFullPixelMethod((Av1BlockSize)blockSize));
+ Assert.Equal((FullPixelSearchMethod)expected, portrait.GetFullPixelMethod((Av1BlockSize)blockSize));
+ }
+
+ [Theory]
+ [InlineData(HeifEncodingSpeed.Level1, 719, false, CandidateSelection.RateDistortion, 0)]
+ [InlineData(HeifEncodingSpeed.Level2, 719, false, CandidateSelection.Variance, 2)]
+ [InlineData(HeifEncodingSpeed.Level3, 719, false, CandidateSelection.Variance, 2)]
+ [InlineData(HeifEncodingSpeed.Level9, 719, true, CandidateSelection.Variance, 2)]
+ [InlineData(HeifEncodingSpeed.Level2, 720, false, CandidateSelection.FirstOnly, 1)]
+ [InlineData(HeifEncodingSpeed.Level3, 720, true, CandidateSelection.RateDistortion, 1)]
+ [InlineData(HeifEncodingSpeed.Level9, 720, true, CandidateSelection.RateDistortion, 1)]
+ public void ResolutionAndFrameRoleOverrideInitialSecondCandidatePolicy(
+ HeifEncodingSpeed speed,
+ int minimumDimension,
+ bool boostedFrame,
+ int expectedSelection,
+ int expectedAutomaticStepSize)
+ {
+ Av1MotionSearchSettings settings = new(speed, false, new Size(1920, minimumDimension), 120, boostedFrame, false);
+
+ Assert.Equal((CandidateSelection)expectedSelection, settings.SecondCandidateSelection);
+ Assert.Equal(expectedAutomaticStepSize, settings.AutomaticStepSizeLevel);
+ }
+
+ [Fact]
+ public void IndependentFrameAndSequenceModesRetainDifferentMotionPolicies()
+ {
+ Av1MotionSearchSettings intraOnly = new(HeifEncodingSpeed.Level9, true, new Size(256, 256), 120, true, true);
+ Av1MotionSearchSettings sequence = new(HeifEncodingSpeed.Level9, false, new Size(256, 256), 120, true, true);
+
+ Assert.True(intraOnly.AllowIntraBlockCopy);
+ Assert.True(intraOnly.PruneIntraBlockCopyHashCandidates);
+ Assert.True(intraOnly.UseFastIntraBlockCopySearch);
+ Assert.True(intraOnly.LimitIntraBlockCopyHashBlockSize);
+ Assert.True(intraOnly.LimitFullPixelStartingCandidates);
+ Assert.False(sequence.AllowIntraBlockCopy);
+ Assert.False(sequence.LimitFullPixelStartingCandidates);
+ Assert.Equal(8, intraOnly.FractionalInterpolationTaps);
+ Assert.Equal(4, sequence.FractionalInterpolationTaps);
+ Assert.Equal(2, intraOnly.FractionalIterationsPerStep);
+ Assert.Equal(1, sequence.FractionalIterationsPerStep);
+ Assert.Equal(SearchPrecision.HalfSample, intraOnly.SimpleMotionPrecision);
+ Assert.Equal(SearchPrecision.Integer, sequence.SimpleMotionPrecision);
+ Assert.Equal(2_097_152, intraOnly.MeshErrorThreshold);
+ Assert.Equal(2_097_152, sequence.MeshErrorThreshold);
+ }
+
+ [Theory]
+ [InlineData(HeifEncodingSpeed.Level0, 0, 0, 2, FractionalSearchMethod.TwoLevelTree)]
+ [InlineData(HeifEncodingSpeed.Level1, 0, 0, 2, FractionalSearchMethod.TwoLevelTree)]
+ [InlineData(HeifEncodingSpeed.Level2, 0, 0, 1, FractionalSearchMethod.TwoLevelTree)]
+ [InlineData(HeifEncodingSpeed.Level3, 1, 0, 1, FractionalSearchMethod.PrunedTree)]
+ [InlineData(HeifEncodingSpeed.Level4, 2, 1, 1, FractionalSearchMethod.MorePrunedTree)]
+ [InlineData(HeifEncodingSpeed.Level5, 2, 1, 1, FractionalSearchMethod.MorePrunedTree)]
+ [InlineData(HeifEncodingSpeed.Level6, 2, 2, 1, FractionalSearchMethod.MorePrunedTree)]
+ public void SequenceSearchStagesUseTheSelectedSpeedPolicy(
+ HeifEncodingSpeed speed,
+ int expectedMeshPruning,
+ int expectedStartPruning,
+ int expectedIterations,
+ int expectedFractionalMethod)
+ {
+ Av1MotionSearchSettings settings = new(speed, false, new Size(256, 256), 120, false, false);
+
+ Assert.Equal(expectedMeshPruning, settings.MeshPruningLevel);
+ Assert.Equal(expectedStartPruning, settings.StartCandidatePruningLevel);
+ Assert.Equal(expectedIterations, settings.FractionalIterationsPerStep);
+ Assert.Equal((FractionalSearchMethod)expectedFractionalMethod, settings.FractionalMethod);
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionSearchTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionSearchTests.cs
new file mode 100644
index 0000000000..f0cf9ffa82
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionSearchTests.cs
@@ -0,0 +1,579 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Formats.Heif;
+using SixLabors.ImageSharp.Formats.Heif.Av1;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
+using Xunit;
+using static SixLabors.ImageSharp.Formats.Heif.Av1.Motion.Av1MotionSearchSettings;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
+
+///
+/// Verifies retained search geometry and the statistics published by complete integer search paths.
+///
+public class Av1MotionSearchTests
+{
+ ///
+ /// Exercises the coordinated search across three differential references and exports its retained state.
+ ///
+ /// The coded component precision.
+ [Theory]
+ [InlineData(8)]
+ [InlineData(10)]
+ [InlineData(12)]
+ public void SingleReferenceSearchRetainsCoordinatedDecisions(int bits)
+ {
+ if (bits == 8)
+ {
+ VerifySingleReferenceSearches(Av1BitDepth.EightBit, bits);
+ }
+ else
+ {
+ VerifySingleReferenceSearches(
+ bits == 10 ? Av1BitDepth.TenBit : Av1BitDepth.TwelveBit,
+ bits);
+ }
+ }
+
+ ///
+ /// Checks exact integer matches and publishes textured, fractional, and repeated-reference decisions.
+ ///
+ private static void VerifySingleReferenceSearches(Av1BitDepth bitDepth, int bits)
+ where TSample : unmanaged, IBinaryInteger
+ where TOperator : struct, Av1MotionSearchBase.IMotionSearchOperator
+ {
+ const int QIndex = 90;
+ const int ReferenceStride = 192;
+ const int ReferenceOrigin = (64 * ReferenceStride) + 64;
+ string directory = Path.Combine(TestEnvironment.ActualOutputDirectoryFullPath, "Heif", "Av1", "SingleReferenceSearch");
+ Directory.CreateDirectory(directory);
+ using Av1EncoderBlockWorkspace workspace = new(Configuration.Default, allocateInterMotionCosts: true);
+ using Av1SymbolEncoder writer = new(Configuration.Default, 64, QIndex, updateCdf: true);
+ Av1MotionVectorCosts costs = workspace.GetMotionVectorCosts(Av1MotionVectorPrecision.EighthSample);
+ writer.FillMotionVectorCosts(costs);
+ int multiplier = Av1RateDistortion.GetKeyFrameRateMultiplier(QIndex, bitDepth);
+ TSample[] prediction = new TSample[136 * 128];
+ short[] residual = new short[128 * 128];
+ short[] scratch = new short[136 * 128];
+ int[] quantized = new int[64 * 64];
+ byte[] contexts = new byte[32];
+ int maximum = (1 << bits) - 1;
+ foreach (Av1BlockSize blockSize in new[] { Av1BlockSize.Block8x8, Av1BlockSize.Block16x16, Av1BlockSize.Block64x64 })
+ {
+ int width = blockSize.GetWidth();
+ int height = blockSize.GetHeight();
+ int sourceStride = width + 3;
+ TSample[] source = new TSample[sourceStride * height];
+ TSample[] reference = new TSample[ReferenceStride * ReferenceStride];
+ for (int pattern = 0; pattern < 7; pattern++)
+ {
+ uint random = (uint)(173 + pattern);
+ for (int index = 0; index < reference.Length; index++)
+ {
+ random = unchecked((random * 1664525) + 1013904223);
+ reference[index] = TSample.CreateChecked((random >> 16) & (uint)maximum);
+ }
+
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ int offset = ReferenceOrigin + ((y + 3) * ReferenceStride) + x - 2;
+ int sample = int.CreateChecked(reference[offset]);
+ if (pattern == 1)
+ {
+ sample = (sample + int.CreateChecked(reference[offset + 1]) + 1) >> 1;
+ }
+ else if (pattern >= 2)
+ {
+ sample = (sample + ((x * 13) ^ (y * 37)) + pattern) & maximum;
+ }
+
+ source[(y * sourceStride) + x] = TSample.CreateChecked(sample);
+ }
+ }
+
+ TSample[] originalSource = source.ToArray();
+ TSample[] originalReference = reference.ToArray();
+ foreach (int speed in new[] { 0, 1, 2, 3, 4, 5, 8 })
+ {
+ Size frameSize = pattern == 3 ? new Size(1280, 720) : new Size(640, 480);
+ Av1MotionSearchSettings settings = new((HeifEncodingSpeed)speed, false, frameSize, QIndex, false, false);
+ foreach (bool forceInteger in new[] { false, true })
+ {
+ int frameStep = 5;
+ int spatialMagnitude = pattern == 2 ? 128 : 16;
+ int searchRange = pattern == 3 ? 4 : int.MaxValue;
+ Rectangle bounds = Rectangle.FromLTRB(-24, -24, 25, 25);
+ Av1MotionSearchBase.SingleReferenceSearch search = new(
+ source,
+ sourceStride,
+ reference,
+ ReferenceStride,
+ ReferenceOrigin,
+ blockSize,
+ bounds,
+ workspace,
+ prediction,
+ residual,
+ scratch,
+ quantized,
+ writer,
+ contexts,
+ contexts,
+ bitDepth,
+ QIndex,
+ 0,
+ 0,
+ false,
+ multiplier,
+ 512,
+ 1024,
+ 256,
+ Av1InterpolationFilter.Regular,
+ Av1InterpolationFilter.Regular,
+ costs);
+
+ Av1MotionSearchBase.SingleReferenceState state = default;
+ using FileStream stream = File.Create(Path.Combine(directory, $"{bits}-{width}-{pattern}-{speed}-{forceInteger}.bin"));
+ using BinaryWriter output = new(stream);
+ int[] header =
+ [
+ 3, bits, width, height, sourceStride, ReferenceStride, ReferenceOrigin,
+ bounds.Left, bounds.Top, bounds.Right, bounds.Bottom, QIndex, speed,
+ (int)settings.GetFullPixelMethod(blockSize), frameStep, spatialMagnitude, 1, searchRange,
+ forceInteger ? 1 : 0, 1, 0, multiplier, Av1RateDistortion.GetMotionSearchSadPerBit(QIndex, bitDepth),
+ settings.AutomaticStepSizeLevel, settings.StartCandidatePruningLevel, settings.ReferenceCandidatePruningLevel,
+ (int)settings.FractionalMethod, settings.FractionalIterationsPerStep, settings.FractionalInterpolationTaps,
+ (int)settings.SecondCandidateSelection, settings.MeshErrorThreshold, settings.MeshPruningLevel,
+ settings.DownsampledSadLevel, frameSize.Width, frameSize.Height, 0, 0, 512, 1024, 256, 0, 0, 3,
+ source.Length, reference.Length, pattern
+ ];
+
+ output.Write(MemoryMarshal.AsBytes(header.AsSpan()));
+ output.Write(MemoryMarshal.AsBytes(source.AsSpan()));
+ output.Write(MemoryMarshal.AsBytes(reference.AsSpan()));
+ for (int referenceIndex = 0; referenceIndex < 3; referenceIndex++)
+ {
+ Av1MotionVector referenceVector = referenceIndex == 1
+ ? new Av1MotionVector(21, -13) : new Av1MotionVector(24, -16);
+
+ Point start = new(
+ (referenceVector.Column + 3 + (referenceVector.Column >= 0 ? 1 : 0)) >> 3,
+ (referenceVector.Row + 3 + (referenceVector.Row >= 0 ? 1 : 0)) >> 3);
+
+ int analysisWidth = pattern >= 4 ? width / 16 : 0;
+ Av1MotionVector[] temporal = new Av1MotionVector[analysisWidth * analysisWidth];
+ for (int index = 0; index < temporal.Length; index++)
+ {
+ // Repeated cells accumulate votes; the final pattern interrupts analysis after a prefix.
+ temporal[index] = pattern == 6 && index == temporal.Length / 2
+ ? new Av1MotionVector(short.MinValue, short.MinValue)
+ : new Av1MotionVector(8 * (index % 3 == 0 ? 16 : -16), 8 * (index % 2 == 0 ? 16 : -16));
+ }
+
+ Av1MotionSearchBase.StartingCandidate[] starts = new Av1MotionSearchBase.StartingCandidate[temporal.Length + 1];
+ int startCount = Av1MotionSearchBase.CollectStartingCandidates(
+ start, temporal, analysisWidth, new Size(analysisWidth, analysisWidth), starts, out int totalWeight);
+
+ int drlRate = referenceIndex * 256;
+ bool valid = search.Search(
+ settings,
+ frameStep,
+ spatialMagnitude,
+ true,
+ searchRange,
+ forceInteger,
+ true,
+ false,
+ referenceIndex,
+ referenceVector,
+ drlRate,
+ starts.AsSpan(0, startCount),
+ totalWeight,
+ ref state,
+ out Av1MotionSearchBase.FractionalResult result);
+
+ ref Av1MotionSearchBase.ReferenceSearchResult retained = ref state.References[referenceIndex];
+ Assert.Equal(valid, retained.IsValid);
+ if (referenceIndex == 0)
+ {
+ Assert.True(valid);
+ if (pattern == 0)
+ {
+ Assert.Equal(new Av1MotionVector(24, -16), result.Vector);
+ Assert.Equal(0, result.SquaredError);
+ }
+ }
+
+ if (valid && forceInteger)
+ {
+ Assert.Equal(0, result.Vector.Row & 7);
+ Assert.Equal(0, result.Vector.Column & 7);
+ }
+
+ int[] decision =
+ [
+ referenceVector.Row, referenceVector.Column, drlRate, valid ? 1 : 0,
+ valid ? result.Vector.Row : 0, valid ? result.Vector.Column : 0,
+ valid ? retained.Rate : 0, retained.Skip ? 1 : 0, state.StartCount,
+ retained.HasFullResult ? 1 : 0, retained.FullVector.Row, retained.FullVector.Column,
+ retained.FullRate, retained.FullCost
+ ];
+
+ output.Write(MemoryMarshal.AsBytes(decision.AsSpan()));
+ }
+
+ Assert.Equal(originalSource, source);
+ Assert.Equal(originalReference, reference);
+ Assert.All(contexts, value => Assert.Equal(0, value));
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Checks all search methods at each sample precision and exports their inputs for independent reference verification.
+ ///
+ /// The coded component precision.
+ [Theory]
+ [InlineData(8)]
+ [InlineData(10)]
+ [InlineData(12)]
+ public void FullPixelSearchPublishesScalarVerifiedStatistics(int bits)
+ {
+ if (bits == 8)
+ {
+ VerifySearches(Av1BitDepth.EightBit, bits);
+ }
+ else
+ {
+ VerifySearches(bits == 10 ? Av1BitDepth.TenBit : Av1BitDepth.TwelveBit, bits);
+ }
+ }
+
+ ///
+ /// Checks all retained coordinate offsets after initial configuration and a stride change.
+ ///
+ [Fact]
+ public void SearchSitesPreserveShapeAcrossStrideChanges()
+ {
+ using Av1EncoderBlockWorkspace workspace = new(Configuration.Default, allocateInterMotionCosts: true);
+ for (int methodIndex = 0; methodIndex <= (int)FullPixelSearchMethod.VeryFastDiamond; methodIndex++)
+ {
+ FullPixelSearchMethod method = (FullPixelSearchMethod)methodIndex;
+ foreach (int stride in new[] { 192, 224 })
+ {
+ Av1MotionSearchSites sites = workspace.GetMotionSearchSites(method, stride);
+ Assert.Equal(method == FullPixelSearchMethod.NStep ? 15 : method == FullPixelSearchMethod.EightPointNStep ? 16 : 11, sites.StageCount);
+ for (int stage = 0; stage < sites.StageCount; stage++)
+ {
+ ReadOnlySpan entries = sites.GetSites(stage);
+ int first = method <= FullPixelSearchMethod.ClampedDiamond ? 1 : 0;
+ for (int index = first; index < first + sites.GetCandidateCount(stage); index++)
+ {
+ Assert.Equal((entries[index].Row * stride) + entries[index].Column, entries[index].Offset);
+ }
+ }
+
+ Assert.Equal(1, sites.GetRadius(0));
+ int outerRadius = method is FullPixelSearchMethod.NStep or FullPixelSearchMethod.EightPointNStep
+ ? 210 : method == FullPixelSearchMethod.ClampedDiamond ? 256 : 1024;
+
+ Assert.Equal(outerRadius, sites.GetRadius(sites.StageCount - 1));
+ }
+ }
+
+ Assert.Equal(8, Unsafe.SizeOf());
+ Assert.Equal(794, Av1MotionSearchSites.StorageLength);
+ }
+
+ ///
+ /// Exercises exact matches, textured residuals, alternate-row policies, and fractional spatial references.
+ ///
+ private static void VerifySearches(Av1BitDepth bitDepth, int bits)
+ where TSample : unmanaged, IBinaryInteger
+ where TOperator : struct, Av1MotionSearchBase.IMotionSearchOperator
+ {
+ const int QIndex = 90;
+ const int ReferenceStride = 192;
+ const int ReferenceOrigin = (64 * ReferenceStride) + 64;
+ int maximum = (1 << bits) - 1;
+ string directory = Path.Combine(TestEnvironment.ActualOutputDirectoryFullPath, "Heif", "Av1", "FullPixelSearch");
+ Directory.CreateDirectory(directory);
+ using Av1EncoderBlockWorkspace workspace = new(Configuration.Default, allocateInterMotionCosts: true);
+ using Av1SymbolEncoder writer = new(Configuration.Default, 64, QIndex, updateCdf: true);
+ Av1MotionVectorCosts costs = workspace.GetMotionVectorCosts(Av1MotionVectorPrecision.EighthSample);
+ writer.FillMotionVectorCosts(costs);
+ int multiplier = Av1RateDistortion.GetKeyFrameRateMultiplier(QIndex, bitDepth);
+ int sadPerBit = Av1RateDistortion.GetMotionSearchSadPerBit(QIndex, bitDepth);
+ int[] costList = new int[5];
+ foreach (int width in new[] { 8, 16, 64 })
+ {
+ int height = width;
+ int sourceStride = width + 3;
+ TSample[] source = new TSample[sourceStride * height];
+ TSample[] reference = new TSample[ReferenceStride * ReferenceStride];
+ for (int pattern = 0; pattern < 6; pattern++)
+ {
+ HeifEncodingSpeed speed = pattern is 0 or 3 or 4
+ ? HeifEncodingSpeed.Level0 : pattern == 1 ? HeifEncodingSpeed.Level5 : HeifEncodingSpeed.Level8;
+
+ Size frameSize = pattern is 2 or 5 ? new Size(1280, 720) : new Size(320, 240);
+ bool screenContent = pattern == 4;
+ Av1MotionSearchSettings settings = new(speed, false, frameSize, QIndex, false, screenContent);
+ uint state = (uint)(173 + pattern);
+ for (int index = 0; index < reference.Length; index++)
+ {
+ state = unchecked((state * 1664525) + 1013904223);
+ reference[index] = TSample.CreateChecked((state >> 16) & (uint)maximum);
+ }
+
+ Point start = pattern == 4 ? new(40, -40) : new(-2, 3);
+ Point predictionOffset = pattern == 3 ? new(-1, 3) : pattern == 4 ? new(-24, 24) : start;
+ Av1MotionVector referenceVector = pattern == 0 ? new Av1MotionVector(24, -16) : new Av1MotionVector(21, -13);
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ int sample = int.CreateChecked(reference[ReferenceOrigin + ((y + predictionOffset.Y) * ReferenceStride) + x + predictionOffset.X]);
+ if (pattern is 1 or 2)
+ {
+ // Independent signed perturbations retain nonzero prediction variance and high-depth rounding residues.
+ sample = (sample + ((x * 13) ^ (y * 37)) + pattern) & maximum;
+ }
+ else if (pattern == 5 && (y & 1) != 0)
+ {
+ // Even rows match exactly while odd rows disagree, forcing the alternate-row reliability check.
+ sample = maximum - sample;
+ }
+
+ source[(y * sourceStride) + x] = TSample.CreateChecked(sample);
+ }
+ }
+
+ Rectangle frameBounds = Rectangle.FromLTRB(-24, -24, 25, 25);
+ Rectangle bounds = referenceVector.GetFullPixelSearchBounds(frameBounds);
+ for (int methodIndex = 0; methodIndex <= (int)FullPixelSearchMethod.VeryFastDiamond; methodIndex++)
+ {
+ FullPixelSearchMethod method = (FullPixelSearchMethod)methodIndex;
+ Av1MotionSearchSites sites = workspace.GetMotionSearchSites(method, ReferenceStride);
+ Av1MotionSearchBase.FullPixelSearch search = new(
+ source,
+ sourceStride,
+ reference,
+ ReferenceStride,
+ ReferenceOrigin,
+ new Size(width, height),
+ bounds,
+ referenceVector,
+ costs,
+ bitDepth,
+ sadPerBit,
+ multiplier);
+
+ Av1MotionSearchBase.FullPixelResult result = search.Search(
+ start, 5, method, sites, settings, false, false, costList, out Point? secondBest);
+
+ Assert.True(bounds.Contains(result.Vector));
+ if (secondBest.HasValue)
+ {
+ Assert.True(bounds.Contains(secondBest.Value));
+ }
+
+ long sum = 0;
+ long squares = 0;
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ int prediction = int.CreateChecked(
+ reference[ReferenceOrigin + ((y + result.Vector.Y) * ReferenceStride) + x + result.Vector.X]);
+
+ long residual = int.CreateChecked(source[(y * sourceStride) + x]) - prediction;
+ sum += residual;
+ squares += residual * residual;
+ }
+ }
+
+ if (bits != 8)
+ {
+ sum = (sum + (1L << (bits - 9))) >> (bits - 8);
+ squares = (squares + (1L << (((bits - 8) * 2) - 1))) >> ((bits - 8) * 2);
+ }
+
+ int expectedVariance = (int)Math.Max(squares - ((sum * sum) / (width * height)), 0);
+ Av1MotionVector resultVector = new(result.Vector.Y * 8, result.Vector.X * 8);
+ int syntaxRate = writer.GetMotionVectorCost(resultVector, referenceVector, Av1MotionVectorPrecision.EighthSample);
+ int expectedMotionCost = (int)((((long)syntaxRate * Math.Max(multiplier >> 6, 1)) + 8192) >> 14);
+ Assert.Equal(expectedVariance, result.Variance);
+ Assert.Equal((int)squares, result.SquaredError);
+ Assert.Equal(expectedMotionCost, result.MotionCost);
+ if (pattern == 0)
+ {
+ Assert.Equal(start, result.Vector);
+ Assert.Equal(0, result.SquaredError);
+ }
+
+ // This export records both source inputs and the published state. A native comparison can
+ // establish controller agreement without calling native code from the managed test process.
+ using BinaryWriter output = new(File.Create(Path.Combine(directory, $"{bits}-{width}-{pattern}-{methodIndex}.bin")));
+ foreach (int value in new[]
+ {
+ 1, bits, width, height, sourceStride, ReferenceStride, ReferenceOrigin,
+ frameBounds.Left, frameBounds.Top, frameBounds.Right, frameBounds.Bottom,
+ referenceVector.Row, referenceVector.Column, start.X, start.Y, 5, methodIndex, (int)speed,
+ frameSize.Width, frameSize.Height, QIndex, 0, screenContent ? 1 : 0, 0, 0, source.Length, reference.Length,
+ result.Vector.X, result.Vector.Y, result.Variance, result.SquaredError, result.MotionCost,
+ secondBest.HasValue ? 1 : 0, secondBest.GetValueOrDefault().X, secondBest.GetValueOrDefault().Y, multiplier, sadPerBit
+ })
+ {
+ output.Write(value);
+ }
+
+ foreach (int cost in costList)
+ {
+ output.Write(cost);
+ }
+
+ output.Write(MemoryMarshal.AsBytes(source.AsSpan()));
+ output.Write(MemoryMarshal.AsBytes(reference.AsSpan()));
+ if (method == FullPixelSearchMethod.NStep)
+ {
+ VerifyFractionalSearches(
+ bitDepth,
+ bits,
+ pattern,
+ width,
+ source,
+ sourceStride,
+ reference,
+ ReferenceStride,
+ ReferenceOrigin,
+ frameBounds,
+ referenceVector,
+ costs,
+ multiplier,
+ result,
+ costList,
+ writer,
+ directory,
+ methodIndex);
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Exercises fractional policy, retained statistics, precision stops, and repeated-center termination.
+ ///
+ private static void VerifyFractionalSearches(
+ Av1BitDepth bitDepth,
+ int bits,
+ int pattern,
+ int width,
+ TSample[] source,
+ int sourceStride,
+ TSample[] reference,
+ int referenceStride,
+ int referenceOrigin,
+ Rectangle frameBounds,
+ Av1MotionVector referenceVector,
+ Av1MotionVectorCosts costs,
+ int multiplier,
+ Av1MotionSearchBase.FullPixelResult integerResult,
+ int[] costList,
+ Av1SymbolEncoder writer,
+ string directory,
+ int fullPixelMethod)
+ where TSample : unmanaged
+ where TOperator : struct, Av1MotionSearchBase.IMotionSearchOperator
+ {
+ TSample[] prediction = new TSample[Av1TranslationalInterPredictor.SearchPredictionBufferLength];
+ Av1MotionVector[] centers = new Av1MotionVector[3];
+ Av1MotionVector start = new(integerResult.Vector.Y * 8, integerResult.Vector.X * 8);
+ Rectangle bounds = referenceVector.GetSubpixelSearchBounds(frameBounds);
+ Av1MotionSearchBase.FractionalSearch search = new(
+ source,
+ sourceStride,
+ reference,
+ referenceStride,
+ referenceOrigin,
+ prediction,
+ new Size(width, width),
+ bounds,
+ referenceVector,
+ costs,
+ bitDepth,
+ multiplier);
+
+ using BinaryWriter output = new(File.Create(Path.Combine(directory, $"{bits}-{width}-{pattern}-{fullPixelMethod}.fractional")));
+ foreach (FractionalSearchMethod method in Enum.GetValues())
+ {
+ foreach (int taps in new[] { 2, 4, 8 })
+ {
+ for (int variant = 0; variant < 4; variant++)
+ {
+ SearchPrecision precision = (SearchPrecision)variant;
+ bool allowHighPrecision = (pattern & 1) == 0;
+ int iterations = (pattern & 1) + 1;
+ bool retainStatistics = pattern % 3 != 0;
+ bool retainCosts = pattern % 3 != 1;
+ Array.Fill(centers, new Av1MotionVector(short.MinValue, short.MinValue));
+ int cost = search.Search(
+ start,
+ retainStatistics ? integerResult : null,
+ method,
+ precision,
+ allowHighPrecision,
+ iterations,
+ taps,
+ retainCosts ? costList : ReadOnlySpan.Empty,
+ centers,
+ out Av1MotionSearchBase.FractionalResult result);
+
+ Assert.True(bounds.Contains(result.Vector.Column, result.Vector.Row));
+ int syntaxRate = writer.GetMotionVectorCost(result.Vector, referenceVector, Av1MotionVectorPrecision.EighthSample);
+ int expectedMotionCost = (int)((((long)syntaxRate * Math.Max(multiplier >> 6, 1)) + 8192) >> 14);
+ Assert.Equal(expectedMotionCost, result.MotionCost);
+ Assert.Equal(result.Cost, cost);
+
+ // Repeating the same start must stop at the retained first precision center. Integer-only
+ // searches never visit that center and therefore still publish their ordinary total cost.
+ int repeatedCost = search.Search(
+ start,
+ retainStatistics ? integerResult : null,
+ method,
+ precision,
+ allowHighPrecision,
+ iterations,
+ taps,
+ retainCosts ? costList : ReadOnlySpan.Empty,
+ centers,
+ out Av1MotionSearchBase.FractionalResult repeated);
+
+ Assert.Equal(precision == SearchPrecision.Integer ? cost : int.MaxValue, repeatedCost);
+ foreach (int value in new[]
+ {
+ (int)method, taps, variant, allowHighPrecision ? 1 : 0, iterations, retainStatistics ? 1 : 0, retainCosts ? 1 : 0,
+ cost, result.Vector.Column, result.Vector.Row, result.Variance, result.SquaredError, result.MotionCost,
+ centers[0].Row, centers[0].Column, centers[1].Row, centers[1].Column, centers[2].Row, centers[2].Column,
+ repeatedCost, repeated.Vector.Column, repeated.Vector.Row, repeated.Variance, repeated.SquaredError, repeated.MotionCost
+ })
+ {
+ output.Write(value);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorCostsTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorCostsTests.cs
new file mode 100644
index 0000000000..1675235365
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorCostsTests.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
+
+public class Av1MotionVectorCostsTests
+{
+ ///
+ /// Checks the table recurrence against symbol-by-symbol rate accumulation over every legal signed magnitude.
+ ///
+ /// The frame motion precision.
+ /// The corresponding increment between representable components.
+ /// Whether to adapt the entropy distributions before building the table.
+ [Theory]
+ [InlineData(Av1MotionVectorPrecision.Integer, 8, false)]
+ [InlineData(Av1MotionVectorPrecision.QuarterSample, 2, false)]
+ [InlineData(Av1MotionVectorPrecision.EighthSample, 1, false)]
+ [InlineData(Av1MotionVectorPrecision.Integer, 8, true)]
+ [InlineData(Av1MotionVectorPrecision.QuarterSample, 2, true)]
+ [InlineData(Av1MotionVectorPrecision.EighthSample, 1, true)]
+ public void EveryLegalComponentMatchesIndependentSymbolTraversal(int precisionValue, int step, bool adapt)
+ {
+ Av1MotionVectorPrecision precision = (Av1MotionVectorPrecision)precisionValue;
+ using Av1SymbolEncoder writer = new(Configuration.Default, 65536, 128, updateCdf: true);
+ if (adapt)
+ {
+ // Exercise asymmetric axes and signs, class-zero offsets, fractional symbols, and larger classes.
+ // The oracle below walks the syntax bits; it does not reuse the table's magnitude recurrence.
+ for (int i = 0; i < 1000; i++)
+ {
+ int row = ((i * 37) % 1024) * step;
+ int column = -((i * 71) % 2048) * step;
+ writer.WriteMotionVector(new(row, column), default, precision);
+ }
+ }
+
+ int[] storage = new int[Av1MotionVectorCosts.StorageLength + 2];
+ storage.AsSpan().Fill(-1234567);
+ Av1MotionVectorCosts costs = new(storage.AsSpan(1, Av1MotionVectorCosts.StorageLength), precision);
+ writer.FillMotionVectorCosts(costs);
+ Av1MotionVector reference = new(16, -24);
+ int maximum = (Av1MotionVectorCosts.MaximumComponent / step) * step;
+ for (int difference = -maximum; difference <= maximum; difference += step)
+ {
+ Av1MotionVector vertical = new(reference.Row + difference, reference.Column);
+ Av1MotionVector horizontal = new(reference.Row, reference.Column + difference);
+ Av1MotionVector both = new(reference.Row + difference, reference.Column - difference);
+ Assert.Equal(writer.GetMotionVectorCost(vertical, reference, precision), costs.GetCost(vertical, reference));
+ Assert.Equal(writer.GetMotionVectorCost(horizontal, reference, precision), costs.GetCost(horizontal, reference));
+ Assert.Equal(writer.GetMotionVectorCost(both, reference, precision), costs.GetCost(both, reference));
+ }
+
+ Assert.Equal(-1234567, storage[0]);
+ Assert.Equal(-1234567, storage[^1]);
+ }
+
+ ///
+ /// Verifies that table construction preserves the other precision pair and that adaptation requires an explicit refresh.
+ ///
+ [Fact]
+ public void SnapshotRetainsRatesUntilExplicitRefreshAndPrecisionPairsDoNotOverlap()
+ {
+ using Av1SymbolEncoder writer = new(Configuration.Default, 65536, 128, updateCdf: true);
+ int[] storage = new int[Av1MotionVectorCosts.StorageLength];
+ Av1MotionVectorCosts quarter = new(storage, Av1MotionVectorPrecision.QuarterSample);
+ Av1MotionVectorCosts eighth = new(storage, Av1MotionVectorPrecision.EighthSample);
+ writer.FillMotionVectorCosts(quarter);
+ Av1MotionVector value = new(24, -48);
+ int original = quarter.GetCost(value, default);
+ writer.FillMotionVectorCosts(eighth);
+ Assert.Equal(original, quarter.GetCost(value, default));
+
+ for (int i = 0; i < 100; i++)
+ {
+ writer.WriteMotionVector(value, default, Av1MotionVectorPrecision.QuarterSample);
+ }
+
+ Assert.Equal(original, quarter.GetCost(value, default));
+ int adapted = writer.GetMotionVectorCost(value, default, Av1MotionVectorPrecision.QuarterSample);
+ Assert.NotEqual(original, adapted);
+ writer.FillMotionVectorCosts(quarter);
+ Assert.Equal(adapted, quarter.GetCost(value, default));
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorTests.cs
index 7aaaae5381..79191e4638 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorTests.cs
@@ -11,6 +11,95 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
[Trait("Format", "Avif")]
public class Av1MotionVectorTests
{
+ ///
+ /// Checks frame-relative displacement limits and their inclusive last candidate.
+ ///
+ /// The block column.
+ /// The block row.
+ /// The block width.
+ /// The block height.
+ /// The allocated luma border.
+ /// The first legal horizontal displacement.
+ /// The first legal vertical displacement.
+ /// The last legal horizontal displacement.
+ /// The last legal vertical displacement.
+ [Theory]
+ [InlineData(0, 0, 8, 8, 96, -16, -16, 264, 264)]
+ [InlineData(128, 64, 8, 8, 96, -144, -80, 136, 200)]
+ [InlineData(252, 252, 8, 8, 96, -268, -268, 12, 12)]
+ [InlineData(0, 0, 128, 128, 96, -88, -88, 216, 216)]
+ [InlineData(0, 0, 128, 128, 160, -136, -136, 264, 264)]
+ public void FrameSearchBoundsIncludePositionBlockExtentAndInterpolation(
+ int x,
+ int y,
+ int width,
+ int height,
+ int border,
+ int minimumColumn,
+ int minimumRow,
+ int maximumColumn,
+ int maximumRow)
+ {
+ Rectangle bounds = Av1MotionVector.GetFrameSearchBounds(new Rectangle(x, y, width, height), new Size(256, 256), border);
+ Assert.Equal(Rectangle.FromLTRB(minimumColumn, minimumRow, maximumColumn + 1, maximumRow + 1), bounds);
+ Assert.True(bounds.Contains(minimumColumn, minimumRow));
+ Assert.True(bounds.Contains(maximumColumn, maximumRow));
+ Assert.False(bounds.Contains(maximumColumn + 1, maximumRow));
+ Assert.False(bounds.Contains(maximumColumn, maximumRow + 1));
+ }
+
+ ///
+ /// Checks inward rounding around fractional references and exclusion of reserved vector endpoints.
+ ///
+ /// The reference row in eighth-sample units.
+ /// The reference column in eighth-sample units.
+ /// The first full-pixel column.
+ /// The first full-pixel row.
+ /// The last full-pixel column.
+ /// The last full-pixel row.
+ /// The first eighth-sample column.
+ /// The first eighth-sample row.
+ /// The last eighth-sample column.
+ /// The last eighth-sample row.
+ [Theory]
+ [InlineData(1, -1, -1023, -1022, 1022, 1023, -8185, -8183, 8183, 8185)]
+ [InlineData(-1, 1, -1022, -1023, 1023, 1022, -8183, -8185, 8185, 8183)]
+ [InlineData(16376, -16376, -2047, 1024, -1024, 2047, -16383, 8192, -8192, 16383)]
+ [InlineData(-16376, 16376, 1024, -2047, 2047, -1024, 8192, -16383, 16383, -8192)]
+ public void SearchBoundsRoundInwardAndExcludeReservedVectorEndpoints(
+ int row,
+ int column,
+ int fullMinimumColumn,
+ int fullMinimumRow,
+ int fullMaximumColumn,
+ int fullMaximumRow,
+ int fractionalMinimumColumn,
+ int fractionalMinimumRow,
+ int fractionalMaximumColumn,
+ int fractionalMaximumRow)
+ {
+ Av1MotionVector reference = new(row, column);
+ Rectangle frameBounds = Rectangle.FromLTRB(-4000, -4000, 4001, 4001);
+ Rectangle full = reference.GetFullPixelSearchBounds(frameBounds);
+ Rectangle fractional = reference.GetSubpixelSearchBounds(frameBounds);
+ Assert.Equal(Rectangle.FromLTRB(fullMinimumColumn, fullMinimumRow, fullMaximumColumn + 1, fullMaximumRow + 1), full);
+ Assert.Equal(Rectangle.FromLTRB(fractionalMinimumColumn, fractionalMinimumRow, fractionalMaximumColumn + 1, fractionalMaximumRow + 1), fractional);
+ Assert.True(fractional.Contains(full.Left * 8, full.Top * 8));
+ Assert.True(fractional.Contains((full.Right - 1) * 8, (full.Bottom - 1) * 8));
+ }
+
+ ///
+ /// Verifies that reference-centered limits cannot widen a tighter padded-frame region.
+ ///
+ [Fact]
+ public void FullAndFractionalSearchRetainTighterFrameBounds()
+ {
+ Rectangle frameBounds = Rectangle.FromLTRB(-17, -29, 32, 44);
+ Av1MotionVector reference = new(1, -1);
+ Assert.Equal(frameBounds, reference.GetFullPixelSearchBounds(frameBounds));
+ Assert.Equal(Rectangle.FromLTRB(-136, -232, 249, 345), reference.GetSubpixelSearchBounds(frameBounds));
+ }
+
///
/// Verifies that high-precision vectors retain their one-eighth-sample components unchanged.
///