diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 6806599fd..ce3e5da4e 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -120,7 +120,7 @@ Checkpoint gate: - [x] Release builds for net10.0 and net11.0 pass with zero errors. - [x] Focused Release tests pass with zero failures or skips. - [x] Scoped semantic, StyleCop, whitespace, and git diff checks pass. -- [ ] Only after all evidence is recorded may this checkpoint be committed. +- [x] The completed checkpoint was committed as `54bb6cbe59bd113058854a3ee31448cf61f462ca` with author and committer `James Jackson-South `. Verified single-reference checkpoint evidence on 2026-08-31: @@ -174,10 +174,10 @@ foreach ($aomEntry in $aomEnvironment) ### 3. Reverify downstream inter prediction in recorded order -These implementations exist locally but inherit the open single-reference syntax, buffer, and ownership foundation. +The single-reference syntax, buffer, reconstruction, and ownership foundation is verified by `54bb6cbe59bd113058854a3ee31448cf61f462ca`. Reverify the existing downstream implementations in this exact order, treating each as locally implemented but unverified until its current-main evidence is recorded. -- [~] Compound reference selection, paired reference-MV derivation, and equal averaging. -- [~] Inter-intra prediction. +- [x] Compound reference selection, paired reference-MV derivation, and equal averaging. +- [~] Inter-intra prediction. Current item. - [~] Distance-weighted compound prediction. - [~] Wedge compound prediction. - [~] Difference-weighted compound prediction. @@ -187,6 +187,40 @@ These implementations exist locally but inherit the open single-reference syntax - [~] Non-translational global prediction. - [~] Inter deblocking decisions and reference/mode deltas. +Verified equal-average compound checkpoint evidence on 2026-08-31: + +- [x] Refreshed the clean official libaom `main` checkout and audited the observed revision + `441c439b9916474cac15d2822af47a9ad70674a8`. Reference selection and compound mode syntax match + `read_comp_reference_type` and `read_ref_frames` in `av1/decoder/decodemv.c`; contexts match + `av1/common/pred_common.c`; paired reference-MV construction and eight-entry extension match + `process_compound_ref_mv_candidate` and `setup_ref_mv_list` in `av1/common/mvref_common.c`. +- [x] Audited equal-average reconstruction against `av1/common/convolve.c` and + `av1/common/convolve.h`. Corrected the unscaled 10/12-bit translational path so both references + retain libaom's no-round compound intermediates until the sole final average and clipping step, + including the larger first-round shift required for 12-bit horizontal intermediates. +- [x] Added descending Vector512, Vector256, Vector128, and scalar high-bit-depth traversal to the + existing semantic compound-prediction operator families. No per-block, per-row, or per-scanline + allocation or copy was added. +- [x] Added FeatureTestRunner coverage for 10/12-bit copy, horizontal, vertical, and separable + subpixel prediction at widths 9, 17, 33, and 65, with an independent no-round bilinear oracle, + row-padding sentinels, and explicit scalar comparison. +- [x] Added a complete `Av1BlockDecoder.DecodeBlock` 10/12-bit half-sample regression whose expected + result comes from the scalar no-round pipeline. The selected vector differs by one sample from the + obsolete round-each-reference behavior, so the test proves the production branch selection. +- [x] Refreshed the official libaom `main` remote immediately before verification and decoded the + fixture's 5,465-byte AV1 `mdat` payload with current `aomdec`, one thread and row threading + disabled. All 19 frames decoded; the final 19,200 YUV444 samples have SHA-256 + `E79D2F49C260B1AC9B1B9BBBB2D611126AFD3B241DA389EB9E7BD4EA0ED42080` and match the retained native + reference with zero differing samples. +- [x] The real 19-frame production sequence requires decoded equal-average compound blocks, compares + the final native Y, U, and V planes exactly, compares final RGBA presentation through ImageSharp's + established reference-output API, and repeats the complete decode with a 1,024-byte constrained + tracked allocator and exactly-once return checks. +- [x] The focused Release checkpoint set passes 31/31 on net10.0 and 31/31 on net11.0, with zero + failures or skips. Scoped analyzer and whitespace verification pass for every changed C# file, + Roslynk reports zero compiler errors and no diagnostics in the changed files, and `git diff --check` + passes. `.gitattributes` is unchanged. + For every item: - [ ] Trace syntax and arithmetic to the current libaom `main` tree. diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs index 315e5cff3..5a26a75d7 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs @@ -1154,7 +1154,7 @@ internal sealed class Av1ReferenceMotionVectors } // The fallback list is positional rather than a weighted candidate scan. Preserve both entries even when - // they are equal so DRL indices have the same meaning as the pinned libaom implementation. + // they are equal so DRL indices have the same meaning as the current libaom main implementation. for (int index = 0; index < 2; index++) { this.candidates[index] = primaryList[index]; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Arithmetic.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Arithmetic.cs index 59b6e9ef2..bfc20d013 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Arithmetic.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Arithmetic.cs @@ -68,6 +68,63 @@ internal static partial class Av1CompoundInterPredictor return result.AsUInt16(); } + /// + /// Removes compound bias and fractional precision from 128-bit high-bit-depth lanes. + /// + public static Vector128 FinalizeHighBitDepthIntermediate( + Vector128 value, + int roundBits, + int roundOffset, + int maximum) + { + Vector128 result = (value - Vector128.Create((ushort)roundOffset)).AsInt16(); + if (roundBits != 0) + { + result = (result + Vector128.Create((short)(1 << (roundBits - 1)))) >> roundBits; + } + + result = Vector128.Max(Vector128.Zero, Vector128.Min(Vector128.Create((short)maximum), result)); + return result.AsUInt16(); + } + + /// + /// Removes compound bias and fractional precision from 256-bit high-bit-depth lanes. + /// + public static Vector256 FinalizeHighBitDepthIntermediate( + Vector256 value, + int roundBits, + int roundOffset, + int maximum) + { + Vector256 result = (value - Vector256.Create((ushort)roundOffset)).AsInt16(); + if (roundBits != 0) + { + result = (result + Vector256.Create((short)(1 << (roundBits - 1)))) >> roundBits; + } + + result = Vector256.Max(Vector256.Zero, Vector256.Min(Vector256.Create((short)maximum), result)); + return result.AsUInt16(); + } + + /// + /// Removes compound bias and fractional precision from 512-bit high-bit-depth lanes. + /// + public static Vector512 FinalizeHighBitDepthIntermediate( + Vector512 value, + int roundBits, + int roundOffset, + int maximum) + { + Vector512 result = (value - Vector512.Create((ushort)roundOffset)).AsInt16(); + if (roundBits != 0) + { + result = (result + Vector512.Create((short)(1 << (roundBits - 1)))) >> roundBits; + } + + result = Vector512.Max(Vector512.Zero, Vector512.Min(Vector512.Create((short)maximum), result)); + return result.AsUInt16(); + } + /// /// Removes the compound bias and final fractional precision from 128-bit widened lanes. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Operator.cs index 72704cc80..13bd264c8 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Operator.cs @@ -27,6 +27,51 @@ internal static partial class Av1CompoundInterPredictor /// The biased compound intermediate. public static abstract ushort Copy(byte sample, int roundBits, int roundOffset); + /// + /// Converts one high-bit-depth integer-position sample to the compound intermediate representation. + /// + /// The source sample. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The biased compound intermediate. + public static abstract ushort CopyHighBitDepth(ushort sample, int roundBits, int roundOffset); + + /// + /// Converts 128 bits of high-bit-depth integer-position samples to compound intermediates. + /// + /// The source samples. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The biased compound intermediates. + public static abstract Vector128 CopyHighBitDepth( + Vector128 samples, + int roundBits, + int roundOffset); + + /// + /// Converts 256 bits of high-bit-depth integer-position samples to compound intermediates. + /// + /// The source samples. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The biased compound intermediates. + public static abstract Vector256 CopyHighBitDepth( + Vector256 samples, + int roundBits, + int roundOffset); + + /// + /// Converts 512 bits of high-bit-depth integer-position samples to compound intermediates. + /// + /// The source samples. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The biased compound intermediates. + public static abstract Vector512 CopyHighBitDepth( + Vector512 samples, + int roundBits, + int roundOffset); + /// /// Converts 128 bits of integer-position samples to compound intermediates. /// @@ -161,6 +206,57 @@ internal static partial class Av1CompoundInterPredictor /// The rounded intermediates. public static abstract Vector512 PrepareHorizontal(Vector512 lower, Vector512 upper); + /// + /// Applies first-pass compound rounding to one biased high-bit-depth horizontal convolution result. + /// + /// The horizontal convolution result. + /// The bit-depth-dependent horizontal bias. + /// The bit-depth-dependent first-pass shift. + /// The rounded intermediate. + public static abstract short PrepareHighBitDepthHorizontal(int result, int bias, int round); + + /// + /// Applies first-pass compound rounding to 128-bit widened high-bit-depth horizontal results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The bit-depth-dependent horizontal bias. + /// The bit-depth-dependent first-pass shift. + /// The rounded intermediates. + public static abstract Vector128 PrepareHighBitDepthHorizontal( + Vector128 lower, + Vector128 upper, + int bias, + int round); + + /// + /// Applies first-pass compound rounding to 256-bit widened high-bit-depth horizontal results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The bit-depth-dependent horizontal bias. + /// The bit-depth-dependent first-pass shift. + /// The rounded intermediates. + public static abstract Vector256 PrepareHighBitDepthHorizontal( + Vector256 lower, + Vector256 upper, + int bias, + int round); + + /// + /// Applies first-pass compound rounding to 512-bit widened high-bit-depth horizontal results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The bit-depth-dependent horizontal bias. + /// The bit-depth-dependent first-pass shift. + /// The rounded intermediates. + public static abstract Vector512 PrepareHighBitDepthHorizontal( + Vector512 lower, + Vector512 upper, + int bias, + int round); + /// /// Applies second-pass compound rounding to one biased vertical convolution result. /// @@ -191,6 +287,50 @@ internal static partial class Av1CompoundInterPredictor /// The upper convolution results. /// The compound intermediates. public static abstract Vector512 PrepareVertical(Vector512 lower, Vector512 upper); + + /// + /// Applies second-pass compound rounding to one biased high-bit-depth vertical convolution result. + /// + /// The vertical convolution result. + /// The bit-depth-dependent vertical bias. + /// The compound intermediate. + public static abstract ushort PrepareHighBitDepthVertical(int result, int bias); + + /// + /// Applies second-pass compound rounding to 128-bit widened high-bit-depth vertical results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The bit-depth-dependent vertical bias. + /// The compound intermediates. + public static abstract Vector128 PrepareHighBitDepthVertical( + Vector128 lower, + Vector128 upper, + int bias); + + /// + /// Applies second-pass compound rounding to 256-bit widened high-bit-depth vertical results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The bit-depth-dependent vertical bias. + /// The compound intermediates. + public static abstract Vector256 PrepareHighBitDepthVertical( + Vector256 lower, + Vector256 upper, + int bias); + + /// + /// Applies second-pass compound rounding to 512-bit widened high-bit-depth vertical results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The bit-depth-dependent vertical bias. + /// The compound intermediates. + public static abstract Vector512 PrepareHighBitDepthVertical( + Vector512 lower, + Vector512 upper, + int bias); } /// @@ -206,6 +346,35 @@ internal static partial class Av1CompoundInterPredictor public static ushort Copy(byte sample, int roundBits, int roundOffset) => (ushort)((sample << roundBits) + roundOffset); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort CopyHighBitDepth(ushort sample, int roundBits, int roundOffset) + => (ushort)((sample << roundBits) + roundOffset); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 CopyHighBitDepth( + Vector128 samples, + int roundBits, + int roundOffset) + => (samples << roundBits) + Vector128.Create((ushort)roundOffset); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 CopyHighBitDepth( + Vector256 samples, + int roundBits, + int roundOffset) + => (samples << roundBits) + Vector256.Create((ushort)roundOffset); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 CopyHighBitDepth( + Vector512 samples, + int roundBits, + int roundOffset) + => (samples << roundBits) + Vector512.Create((ushort)roundOffset); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Copy( @@ -315,6 +484,44 @@ internal static partial class Av1CompoundInterPredictor RoundPowerOfTwo(lower + Vector512.Create(HorizontalBias), Round0Bits), RoundPowerOfTwo(upper + Vector512.Create(HorizontalBias), Round0Bits)); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short PrepareHighBitDepthHorizontal(int result, int bias, int round) + => (short)RoundPowerOfTwo(bias + result, round); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 PrepareHighBitDepthHorizontal( + Vector128 lower, + Vector128 upper, + int bias, + int round) + => Av1IntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector128.Create(bias), round), + RoundPowerOfTwo(upper + Vector128.Create(bias), round)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 PrepareHighBitDepthHorizontal( + Vector256 lower, + Vector256 upper, + int bias, + int round) + => Av1IntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector256.Create(bias), round), + RoundPowerOfTwo(upper + Vector256.Create(bias), round)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 PrepareHighBitDepthHorizontal( + Vector512 lower, + Vector512 upper, + int bias, + int round) + => Av1IntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector512.Create(bias), round), + RoundPowerOfTwo(upper + Vector512.Create(bias), round)); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ushort PrepareVertical(int result) @@ -340,5 +547,40 @@ internal static partial class Av1CompoundInterPredictor => Av1IntraPredictorBase.Narrow( RoundPowerOfTwo(lower + Vector512.Create(VerticalBias), CompoundRound1Bits), RoundPowerOfTwo(upper + Vector512.Create(VerticalBias), CompoundRound1Bits)).AsUInt16(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort PrepareHighBitDepthVertical(int result, int bias) + => (ushort)RoundPowerOfTwo(bias + result, CompoundRound1Bits); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 PrepareHighBitDepthVertical( + Vector128 lower, + Vector128 upper, + int bias) + => Av1IntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector128.Create(bias), CompoundRound1Bits), + RoundPowerOfTwo(upper + Vector128.Create(bias), CompoundRound1Bits)).AsUInt16(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 PrepareHighBitDepthVertical( + Vector256 lower, + Vector256 upper, + int bias) + => Av1IntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector256.Create(bias), CompoundRound1Bits), + RoundPowerOfTwo(upper + Vector256.Create(bias), CompoundRound1Bits)).AsUInt16(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 PrepareHighBitDepthVertical( + Vector512 lower, + Vector512 upper, + int bias) + => Av1IntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector512.Create(bias), CompoundRound1Bits), + RoundPowerOfTwo(upper + Vector512.Create(bias), CompoundRound1Bits)).AsUInt16(); } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.cs index ede6a8eed..2d07455ef 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.cs @@ -65,6 +65,39 @@ internal static partial class Av1CompoundInterPredictor scratch, useSimd: true); + /// + /// Reconstructs one high-bit-depth translational reference into AV1's unsigned compound intermediate format. + /// + public static void PredictCompound( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + int bitDepth, + Span scratch) + => PredictCompound( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalFilter, + verticalFilter, + horizontalPhase, + verticalPhase, + bitDepth, + scratch, + useSimd: true); + /// /// Executes one closed compound-prediction conversion operator. /// @@ -175,6 +208,124 @@ internal static partial class Av1CompoundInterPredictor useSimd); } + /// + /// Executes one closed high-bit-depth compound-prediction conversion operator. + /// + /// The compound-prediction conversion operator. + private static void PredictCompound( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + int bitDepth, + Span scratch, + bool useSimd) + where TOperator : struct, IAv1CompoundPredictionOperator + { + ReadOnlySpan horizontalCoefficients = GetCompoundCoefficients(horizontalFilter, horizontalPhase, width <= 4); + ReadOnlySpan verticalCoefficients = GetCompoundCoefficients(verticalFilter, verticalPhase, height <= 4); + + // Libaom increases the first-round shift only for 12-bit input. This keeps the signed horizontal + // intermediate within sixteen bits while preserving the same total Q14 convolution precision. + int intermediateRange = bitDepth + FilterBits - Round0Bits + 2; + int round0 = Round0Bits + Math.Max(intermediateRange - 16, 0); + int roundBits = (2 * FilterBits) - round0 - CompoundRound1Bits; + int offsetBits = bitDepth + (2 * FilterBits) - round0; + int roundOffset = (1 << (offsetBits - CompoundRound1Bits)) + + (1 << (offsetBits - CompoundRound1Bits - 1)); + + if (horizontalPhase == 0 && verticalPhase == 0) + { + CopyCompound( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + roundBits, + roundOffset, + useSimd); + + return; + } + + if (verticalPhase == 0) + { + GetEffectiveKernel(horizontalCoefficients, out int firstCoefficient, out int tapCount); + FilterCompoundDirect( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients[firstCoefficient..], + tapCount, + firstCoefficient - 3, + tapStride: 1, + preShift: 0, + round: round0, + roundOffset, + useSimd); + + return; + } + + if (horizontalPhase == 0) + { + GetEffectiveKernel(verticalCoefficients, out int firstCoefficient, out int tapCount); + FilterCompoundDirect( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalCoefficients[firstCoefficient..], + tapCount, + (firstCoefficient - 3) * sourceStride, + sourceStride, + FilterBits - round0, + CompoundRound1Bits, + roundOffset, + useSimd); + + return; + } + + GetEffectiveKernel(horizontalCoefficients, out int firstHorizontalCoefficient, out int horizontalTapCount); + GetEffectiveKernel(verticalCoefficients, out int firstVerticalCoefficient, out int verticalTapCount); + FilterCompound2D( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients[firstHorizontalCoefficient..], + horizontalTapCount, + firstHorizontalCoefficient - 3, + verticalCoefficients[firstVerticalCoefficient..], + verticalTapCount, + firstVerticalCoefficient - 3, + bitDepth, + round0, + scratch, + useSimd); + } + /// /// Reconstructs one compound intermediate without explicit hardware intrinsics. /// @@ -206,6 +357,39 @@ internal static partial class Av1CompoundInterPredictor scratch, useSimd: false); + /// + /// Reconstructs one high-bit-depth compound intermediate without explicit hardware intrinsics. + /// + public static void PredictCompoundScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + int bitDepth, + Span scratch) + => PredictCompound( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalFilter, + verticalFilter, + horizontalPhase, + verticalPhase, + bitDepth, + scratch, + useSimd: false); + /// /// Copies integer-position samples through one closed compound-prediction operator. /// @@ -275,6 +459,75 @@ internal static partial class Av1CompoundInterPredictor } } + /// + /// Copies high-bit-depth integer-position samples through one closed compound-prediction operator. + /// + /// The compound-prediction conversion operator. + private static void CopyCompound( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + int roundBits, + int roundOffset, + bool useSimd) + where TOperator : struct, IAv1CompoundPredictionOperator + { + ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + + for (int row = 0; row < height; row++) + { + ref ushort sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int column = 0; + + if (useSimd && Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 samples = Vector512.LoadUnsafe(ref sourceRow, (nuint)column); + TOperator.CopyHighBitDepth(samples, roundBits, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (useSimd && Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 samples = Vector256.LoadUnsafe(ref sourceRow, (nuint)column); + TOperator.CopyHighBitDepth(samples, roundBits, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (useSimd && Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 samples = Vector128.LoadUnsafe(ref sourceRow, (nuint)column); + TOperator.CopyHighBitDepth(samples, roundBits, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + for (; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = TOperator.CopyHighBitDepth( + Unsafe.Add(ref sourceRow, column), + roundBits, + roundOffset); + } + } + } + /// /// Applies one compound convolution direction through one closed conversion operator. /// @@ -395,6 +648,112 @@ internal static partial class Av1CompoundInterPredictor } } + /// + /// Applies one high-bit-depth compound convolution direction through one closed conversion operator. + /// + /// The compound-prediction conversion operator. + private static void FilterCompoundDirect( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan coefficients, + int tapCount, + int sourceOffset, + int tapStride, + int preShift, + int round, + int roundOffset, + bool useSimd) + where TOperator : struct, IAv1CompoundPredictionOperator + { + ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ref short coefficientBase = ref MemoryMarshal.GetReference(coefficients); + + for (int row = 0; row < height; row++) + { + ref ushort sourceRowUnsigned = ref Unsafe.Add(ref sourceBase, (row * sourceStride) + sourceOffset); + ref short sourceRow = ref Unsafe.As(ref sourceRowUnsigned); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int column = 0; + + if (useSimd && Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Convolve( + ref sourceRow, + tapStride, + (nuint)column, + ref coefficientBase, + tapCount, + Vector512.Zero, + out Vector512 lower, + out Vector512 upper); + + TOperator.PrepareDirect(lower, upper, preShift, round, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (useSimd && Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Convolve( + ref sourceRow, + tapStride, + (nuint)column, + ref coefficientBase, + tapCount, + Vector256.Zero, + out Vector256 lower, + out Vector256 upper); + + TOperator.PrepareDirect(lower, upper, preShift, round, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (useSimd && Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Convolve( + ref sourceRow, + tapStride, + (nuint)column, + ref coefficientBase, + tapCount, + Vector128.Zero, + out Vector128 lower, + out Vector128 upper); + + TOperator.PrepareDirect(lower, upper, preShift, round, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + for (; column < width; column++) + { + int result = ConvolveScalar( + ref Unsafe.Add(ref sourceRowUnsigned, column), + tapStride, + ref coefficientBase, + tapCount); + + Unsafe.Add(ref destinationRow, column) = TOperator.PrepareDirect(result, preShift, round, roundOffset); + } + } + } + /// /// Applies separable compound convolution through caller-owned signed scratch. /// @@ -593,6 +952,205 @@ internal static partial class Av1CompoundInterPredictor } } + /// + /// Applies separable high-bit-depth compound convolution through caller-owned signed scratch. + /// + /// The compound-prediction conversion operator. + private static void FilterCompound2D( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan horizontalCoefficients, + int horizontalTapCount, + int horizontalSourceOffset, + ReadOnlySpan verticalCoefficients, + int verticalTapCount, + int verticalSourceOffset, + int bitDepth, + int round0, + Span scratch, + bool useSimd) + where TOperator : struct, IAv1CompoundPredictionOperator + { + ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ref short scratchBase = ref MemoryMarshal.GetReference(scratch); + ref short horizontalCoefficientBase = ref MemoryMarshal.GetReference(horizontalCoefficients); + ref short verticalCoefficientBase = ref MemoryMarshal.GetReference(verticalCoefficients); + int scratchStride = Math.Max(width, MinimumScratchStride); + int intermediateHeight = height + verticalTapCount - 1; + int horizontalBias = 1 << (bitDepth + FilterBits - 1); + int verticalBias = 1 << (bitDepth + (2 * FilterBits) - round0); + + // High-bit-depth input is still below short.MaxValue. Reinterpreting the source lets the shared signed + // widening kernels apply negative filter coefficients without copying or allocating a conversion buffer. + for (int row = 0; row < intermediateHeight; row++) + { + ref ushort sourceRowUnsigned = ref Unsafe.Add( + ref sourceBase, + ((row + verticalSourceOffset) * sourceStride) + horizontalSourceOffset); + + ref short sourceRow = ref Unsafe.As(ref sourceRowUnsigned); + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + int column = 0; + + if (useSimd && Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Convolve( + ref sourceRow, + 1, + (nuint)column, + ref horizontalCoefficientBase, + horizontalTapCount, + Vector512.Zero, + out Vector512 lower, + out Vector512 upper); + + TOperator.PrepareHighBitDepthHorizontal(lower, upper, horizontalBias, round0) + .StoreUnsafe(ref scratchRow, (nuint)column); + } + } + + if (useSimd && Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Convolve( + ref sourceRow, + 1, + (nuint)column, + ref horizontalCoefficientBase, + horizontalTapCount, + Vector256.Zero, + out Vector256 lower, + out Vector256 upper); + + TOperator.PrepareHighBitDepthHorizontal(lower, upper, horizontalBias, round0) + .StoreUnsafe(ref scratchRow, (nuint)column); + } + } + + if (useSimd && Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Convolve( + ref sourceRow, + 1, + (nuint)column, + ref horizontalCoefficientBase, + horizontalTapCount, + Vector128.Zero, + out Vector128 lower, + out Vector128 upper); + + TOperator.PrepareHighBitDepthHorizontal(lower, upper, horizontalBias, round0) + .StoreUnsafe(ref scratchRow, (nuint)column); + } + } + + for (; column < width; column++) + { + int result = ConvolveScalar( + ref Unsafe.Add(ref sourceRowUnsigned, column), + 1, + ref horizontalCoefficientBase, + horizontalTapCount); + + Unsafe.Add(ref scratchRow, column) = + TOperator.PrepareHighBitDepthHorizontal(result, horizontalBias, round0); + } + } + + for (int row = 0; row < height; row++) + { + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int column = 0; + + if (useSimd && Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Convolve( + ref scratchRow, + scratchStride, + (nuint)column, + ref verticalCoefficientBase, + verticalTapCount, + Vector512.Zero, + out Vector512 lower, + out Vector512 upper); + + TOperator.PrepareHighBitDepthVertical(lower, upper, verticalBias) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (useSimd && Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Convolve( + ref scratchRow, + scratchStride, + (nuint)column, + ref verticalCoefficientBase, + verticalTapCount, + Vector256.Zero, + out Vector256 lower, + out Vector256 upper); + + TOperator.PrepareHighBitDepthVertical(lower, upper, verticalBias) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (useSimd && Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Convolve( + ref scratchRow, + scratchStride, + (nuint)column, + ref verticalCoefficientBase, + verticalTapCount, + Vector128.Zero, + out Vector128 lower, + out Vector128 upper); + + TOperator.PrepareHighBitDepthVertical(lower, upper, verticalBias) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + for (; column < width; column++) + { + int result = ConvolveScalar( + ref Unsafe.Add(ref scratchRow, column), + scratchStride, + ref verticalCoefficientBase, + verticalTapCount); + + Unsafe.Add(ref destinationRow, column) = + TOperator.PrepareHighBitDepthVertical(result, verticalBias); + } + } + } + /// /// Gets the selected interpolation kernel for compound traversal. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.Operator.cs index 7696e76fa..310e389fc 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.Operator.cs @@ -29,6 +29,22 @@ internal static partial class Av1CompoundIntermediateAveragePredictor /// The reconstructed sample. public static abstract byte Average(ushort first, ushort second, int roundBits, int roundOffset); + /// + /// Equal-averages and finalizes one pair of high-bit-depth compound intermediate samples. + /// + /// The first compound intermediate. + /// The second compound intermediate. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The maximum reconstructed sample value. + /// The reconstructed sample. + public static abstract ushort AverageHighBitDepth( + ushort first, + ushort second, + int roundBits, + int roundOffset, + int maximum); + /// /// Equal-averages and finalizes 128 bits of compound intermediate samples. /// @@ -82,6 +98,54 @@ internal static partial class Av1CompoundIntermediateAveragePredictor Vector512 second1, int roundBits, int roundOffset); + + /// + /// Equal-averages and finalizes 128 bits of high-bit-depth compound intermediate samples. + /// + /// The first compound intermediates. + /// The second compound intermediates. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The maximum reconstructed sample value. + /// The reconstructed samples. + public static abstract Vector128 AverageHighBitDepth( + Vector128 first, + Vector128 second, + int roundBits, + int roundOffset, + int maximum); + + /// + /// Equal-averages and finalizes 256 bits of high-bit-depth compound intermediate samples. + /// + /// The first compound intermediates. + /// The second compound intermediates. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The maximum reconstructed sample value. + /// The reconstructed samples. + public static abstract Vector256 AverageHighBitDepth( + Vector256 first, + Vector256 second, + int roundBits, + int roundOffset, + int maximum); + + /// + /// Equal-averages and finalizes 512 bits of high-bit-depth compound intermediate samples. + /// + /// The first compound intermediates. + /// The second compound intermediates. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The maximum reconstructed sample value. + /// The reconstructed samples. + public static abstract Vector512 AverageHighBitDepth( + Vector512 first, + Vector512 second, + int roundBits, + int roundOffset, + int maximum); } /// @@ -98,6 +162,19 @@ internal static partial class Av1CompoundIntermediateAveragePredictor return (byte)Math.Clamp(RoundPowerOfTwo(result, roundBits), 0, byte.MaxValue); } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort AverageHighBitDepth( + ushort first, + ushort second, + int roundBits, + int roundOffset, + int maximum) + { + int result = ((first + second) >> 1) - roundOffset; + return (ushort)Math.Clamp(RoundPowerOfTwo(result, roundBits), 0, maximum); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector128 Average( @@ -136,5 +213,47 @@ internal static partial class Av1CompoundIntermediateAveragePredictor => Vector512.Narrow( FinalizeIntermediate((first0 & second0) + ((first0 ^ second0) >> 1), roundBits, roundOffset), FinalizeIntermediate((first1 & second1) + ((first1 ^ second1) >> 1), roundBits, roundOffset)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 AverageHighBitDepth( + Vector128 first, + Vector128 second, + int roundBits, + int roundOffset, + int maximum) + => FinalizeHighBitDepthIntermediate( + (first & second) + ((first ^ second) >> 1), + roundBits, + roundOffset, + maximum); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AverageHighBitDepth( + Vector256 first, + Vector256 second, + int roundBits, + int roundOffset, + int maximum) + => FinalizeHighBitDepthIntermediate( + (first & second) + ((first ^ second) >> 1), + roundBits, + roundOffset, + maximum); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AverageHighBitDepth( + Vector512 first, + Vector512 second, + int roundBits, + int roundOffset, + int maximum) + => FinalizeHighBitDepthIntermediate( + (first & second) + ((first ^ second) >> 1), + roundBits, + roundOffset, + maximum); } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.cs index bab8c7aa8..8a099ec1b 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.cs @@ -121,4 +121,105 @@ internal static partial class Av1CompoundIntermediateAveragePredictor } } } + + /// + /// Combines two high-bit-depth compound intermediates by equal averaging. + /// + public static void AverageIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int bitDepth) + => AverageIntermediate( + destination, + destinationStride, + first, + firstStride, + second, + secondStride, + width, + height, + bitDepth); + + /// + /// Executes one closed high-bit-depth equal-average compound-intermediate operator. + /// + /// The compound-intermediate operator. + private static void AverageIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int bitDepth) + where TOperator : struct, IAv1CompoundIntermediateAverageOperator + { + GetIntermediateRounding(bitDepth, out int roundBits, out int roundOffset); + int maximum = (1 << bitDepth) - 1; + + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan firstRow = first.Slice(row * firstStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ref ushort destinationReference = ref MemoryMarshal.GetReference(destinationRow); + ref ushort firstReference = ref MemoryMarshal.GetReference(firstRow); + ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 firstVector = Vector512.LoadUnsafe(ref firstReference, (nuint)column); + Vector512 secondVector = Vector512.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.AverageHighBitDepth(firstVector, secondVector, roundBits, roundOffset, maximum) + .StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 firstVector = Vector256.LoadUnsafe(ref firstReference, (nuint)column); + Vector256 secondVector = Vector256.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.AverageHighBitDepth(firstVector, secondVector, roundBits, roundOffset, maximum) + .StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 firstVector = Vector128.LoadUnsafe(ref firstReference, (nuint)column); + Vector128 secondVector = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.AverageHighBitDepth(firstVector, secondVector, roundBits, roundOffset, maximum) + .StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + for (; column < width; column++) + { + destinationRow[column] = TOperator.AverageHighBitDepth( + firstRow[column], + secondRow[column], + roundBits, + roundOffset, + maximum); + } + } + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs index 1366a8b8e..cc2ab9e13 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs @@ -368,10 +368,18 @@ internal sealed class Av1BlockDecoder : IDisposable secondaryReferenceFrameBuffer!.Width != this.frameHeader.FrameSize.FrameWidth || secondaryReferenceFrameBuffer.Height != this.frameHeader.FrameSize.FrameHeight); - // Eight-bit compound prediction uses the normative no-round intermediate path below. Scaled and - // high-bit-depth variants remain on their existing paths until their matching kernels are selected. + // Compound convolution is combined before its final rounding step. Scaled references and high-bit-depth + // warped/global models have separate kernels and remain with their owning later prediction checkpoints. + bool useHighBitDepthCompoundIntermediates = + highBitDepth && + modeInfo.CompoundType == Av1CompoundType.Average && + modeInfo.MotionMode != Av1MotionMode.Warped && + modeInfo.YMode != Av1PredictionMode.GlobalGlobalMotionVector; + bool useCompoundIntermediates = - isCompound && !highBitDepth && !hasScaledCompoundReference; + isCompound && + !hasScaledCompoundReference && + (!highBitDepth || useHighBitDepthCompoundIntermediates); for (int referenceIndex = 0; referenceIndex < referenceCount; referenceIndex++) { @@ -598,24 +606,48 @@ internal sealed class Av1BlockDecoder : IDisposable int sourceIndex = ((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + sourceOrigin.X + (sourceColumnQ4 >> 4); - Span destination = referenceIndex == 0 - ? MemoryMarshal.Cast(highBitDepthBlockReconstructionBuffer[reconstructionStride..]) - : highBitDepthSecondPrediction; + if (useCompoundIntermediates) + { + Span destination = referenceIndex == 0 + ? firstCompoundPrediction + : highBitDepthSecondPrediction; - Av1InterPredictor.Predict( - source, - sourceStride, - sourceIndex, - destination, - destinationStride, - predictionWidth, - predictionHeight, - modeInfo.InterpolationFilters[1], - modeInfo.InterpolationFilters[0], - horizontalPhase, - verticalPhase, - this.frameBuffer.BitDepth.GetBitCount(), - predictionScratch); + Av1CompoundInterPredictor.PredictCompound( + source, + sourceStride, + sourceIndex, + destination, + predictionWidth, + predictionWidth, + predictionHeight, + modeInfo.InterpolationFilters[1], + modeInfo.InterpolationFilters[0], + horizontalPhase, + verticalPhase, + this.frameBuffer.BitDepth.GetBitCount(), + predictionScratch); + } + else + { + Span destination = referenceIndex == 0 + ? MemoryMarshal.Cast(highBitDepthBlockReconstructionBuffer[reconstructionStride..]) + : highBitDepthSecondPrediction; + + Av1InterPredictor.Predict( + source, + sourceStride, + sourceIndex, + destination, + destinationStride, + predictionWidth, + predictionHeight, + modeInfo.InterpolationFilters[1], + modeInfo.InterpolationFilters[0], + horizontalPhase, + verticalPhase, + this.frameBuffer.BitDepth.GetBitCount(), + predictionScratch); + } } else { @@ -676,98 +708,117 @@ internal sealed class Av1BlockDecoder : IDisposable { if (useCompoundIntermediates) { - Span destination = blockReconstructionBuffer[reconstructionStride..]; ReadOnlySpan first = firstCompoundPrediction[..(predictionWidth * predictionHeight)]; - switch (modeInfo.CompoundType) + if (highBitDepth) { - case Av1CompoundType.Average: - Av1CompoundIntermediateAveragePredictor.AverageIntermediate( - destination, - reconstructionStride, - first, - predictionWidth, - highBitDepthSecondPrediction, - predictionWidth, - predictionWidth, - predictionHeight, - bitDepth: 8); - - break; - case Av1CompoundType.DistanceWeighted: - Av1CompoundIntermediateDistanceWeightedPredictor.DistanceWeightedIntermediate( - destination, - reconstructionStride, - first, - predictionWidth, - highBitDepthSecondPrediction, - predictionWidth, - predictionWidth, - predictionHeight, - firstCompoundWeight, - secondCompoundWeight, - bitDepth: 8); + Span highBitDepthDestination = MemoryMarshal.Cast( + highBitDepthBlockReconstructionBuffer[reconstructionStride..]); - break; - case Av1CompoundType.Wedge: - Av1WedgeMask.Fill( - compoundMask, - predictionWidth, - blockSize, - modeInfo.CompoundWedgeIndex, - modeInfo.CompoundWedgeSign, - subX, - subY, - invert: false); + Av1CompoundIntermediateAveragePredictor.AverageIntermediate( + highBitDepthDestination, + reconstructionStride, + first, + predictionWidth, + highBitDepthSecondPrediction, + predictionWidth, + predictionWidth, + predictionHeight, + this.frameBuffer.BitDepth.GetBitCount()); + } + else + { + Span destination = blockReconstructionBuffer[reconstructionStride..]; + switch (modeInfo.CompoundType) + { + case Av1CompoundType.Average: + Av1CompoundIntermediateAveragePredictor.AverageIntermediate( + destination, + reconstructionStride, + first, + predictionWidth, + highBitDepthSecondPrediction, + predictionWidth, + predictionWidth, + predictionHeight, + bitDepth: 8); - Av1CompoundIntermediateMaskBlendPredictor.BlendIntermediate( - destination, - reconstructionStride, - first, - predictionWidth, - highBitDepthSecondPrediction, - predictionWidth, - compoundMask, - predictionWidth, - predictionWidth, - predictionHeight, - subX: 0, - subY: 0, - bitDepth: 8); + break; + case Av1CompoundType.DistanceWeighted: + Av1CompoundIntermediateDistanceWeightedPredictor.DistanceWeightedIntermediate( + destination, + reconstructionStride, + first, + predictionWidth, + highBitDepthSecondPrediction, + predictionWidth, + predictionWidth, + predictionHeight, + firstCompoundWeight, + secondCompoundWeight, + bitDepth: 8); - break; - default: - int lumaWidth = blockSize.GetWidth(); - if (plane == 0) - { - Av1CompoundIntermediateDifferenceWeightedMaskBuilder.FillDifferenceWeightedIntermediateMask( + break; + case Av1CompoundType.Wedge: + Av1WedgeMask.Fill( compoundMask, - lumaWidth, + predictionWidth, + blockSize, + modeInfo.CompoundWedgeIndex, + modeInfo.CompoundWedgeSign, + subX, + subY, + invert: false); + + Av1CompoundIntermediateMaskBlendPredictor.BlendIntermediate( + destination, + reconstructionStride, first, predictionWidth, highBitDepthSecondPrediction, predictionWidth, + compoundMask, + predictionWidth, predictionWidth, predictionHeight, - bitDepth: 8, - modeInfo.DifferenceWeightedMaskType); - } - - Av1CompoundIntermediateMaskBlendPredictor.BlendIntermediate( - destination, - reconstructionStride, - first, - predictionWidth, - highBitDepthSecondPrediction, - predictionWidth, - compoundMask, - lumaWidth, - predictionWidth, - predictionHeight, - subX, - subY, - bitDepth: 8); + subX: 0, + subY: 0, + bitDepth: 8); + + break; + default: + int lumaWidth = blockSize.GetWidth(); + if (plane == 0) + { + Av1CompoundIntermediateDifferenceWeightedMaskBuilder.FillDifferenceWeightedIntermediateMask( + compoundMask, + lumaWidth, + first, + predictionWidth, + highBitDepthSecondPrediction, + predictionWidth, + predictionWidth, + predictionHeight, + bitDepth: 8, + modeInfo.DifferenceWeightedMaskType); + } + + Av1CompoundIntermediateMaskBlendPredictor.BlendIntermediate( + destination, + reconstructionStride, + first, + predictionWidth, + highBitDepthSecondPrediction, + predictionWidth, + compoundMask, + lumaWidth, + predictionWidth, + predictionHeight, + subX, + subY, + bitDepth: 8); - break; + break; + } } } else if (highBitDepth) diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs index e2880ca3f..443256abe 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs @@ -26,6 +26,12 @@ public class Av1CompoundBlockDecoderTests /// private const HwIntrinsics GlobalWarpConfigurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic; + /// + /// The hardware configurations covering every compound-prediction vector width and the scalar fallback. + /// + private const HwIntrinsics CompoundPredictionConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + /// /// Verifies that two retained reference planes are predicted and averaged before residual reconstruction. /// @@ -75,7 +81,7 @@ public class Av1CompoundBlockDecoderTests modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Last2; - modeInfo.InterpolationFilters.Fill(Av1InterpolationFilter.Regular); + modeInfo.InterpolationFilters.Clear(); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); Av1LoopFilterContext loopFilterContext = new(sequenceHeader); @@ -117,6 +123,15 @@ public class Av1CompoundBlockDecoderTests } } + /// + /// Verifies that high-bit-depth subpixel predictors retain their no-round precision until the compound average. + /// + [Fact] + public void DecodeBlockReconstructsSubpixelHighBitDepthEqualAverageCompoundPrediction() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateSubpixelHighBitDepthEqualAverageCompoundPrediction, + CompoundPredictionConfigurations); + /// /// Verifies that both references of a GLOBAL_GLOBALMV block use their complete matrix before compound averaging. /// @@ -627,6 +642,148 @@ public class Av1CompoundBlockDecoderTests } } + /// + /// Reconstructs the high-bit-depth subpixel compound regression at every supported source precision. + /// + private static void ValidateSubpixelHighBitDepthEqualAverageCompoundPrediction() + { + foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.TenBit, Av1BitDepth.TwelveBit }) + { + ValidateSubpixelHighBitDepthEqualAverageCompoundPredictionAtBitDepth(bitDepth); + } + } + + /// + /// Reconstructs one high-bit-depth half-sample compound block and compares it with the scalar no-round pipeline. + /// + /// The native sample depth. + private static void ValidateSubpixelHighBitDepthEqualAverageCompoundPredictionAtBitDepth(Av1BitDepth bitDepth) + { + const int frameSize = 32; + const int blockOrigin = 8; + const int blockSize = 8; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth, frameSize); + ObuFrameHeader frameHeader = CreateFrameHeader(frameSize); + frameHeader.GetReferenceFrameIndices()[0] = 0; + frameHeader.GetReferenceFrameIndices()[1] = 1; + + using Av1ReferenceFrameStore referenceFrames = new(); + Assert.True(referenceFrames.Commit( + 1, + CreatePatternReferenceFrame(sequenceHeader, CreateFrameHeader(frameSize)), + showFrame: false)); + + Assert.True(referenceFrames.Commit( + 2, + CreatePatternReferenceFrame(sequenceHeader, CreateFrameHeader(frameSize), sampleOffset: 40), + showFrame: false)); + + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, new Point(2, 2)) + { + Skip = true, + YMode = Av1PredictionMode.NearestNearestMotionVector, + CompoundIndex = true, + CompoundType = Av1CompoundType.Average, + }; + + modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; + modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Last2; + modeInfo.MotionVectors[0] = new Av1MotionVector(0, 0); + + // The second predictor lands exactly halfway between horizontal samples. Rounding it before combining the + // references changes every result by one, so this vector distinguishes the required no-round production path. + modeInfo.MotionVectors[1] = new Av1MotionVector(0, 4); + modeInfo.InterpolationFilters.Fill(Av1InterpolationFilter.Bilinear); + modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); + + ushort[] expectedFirst = new ushort[blockSize * blockSize]; + ushort[] expectedSecond = new ushort[blockSize * blockSize]; + Span expectedPredictions = expectedFirst; + short[] predictionScratch = new short[128 * (blockSize + 8)]; + for (int referenceIndex = 0; referenceIndex < 2; referenceIndex++) + { + Av1FrameBuffer reference = referenceFrames.Resolve(referenceIndex)!.FrameBuffer; + Span source = reference.GetPaddedPlaneSpan16( + Av1Plane.Y, + 0, + 0, + out int sourceStride, + out Point sourceOrigin); + + Av1MotionVector motionVector = modeInfo.MotionVectors[referenceIndex]; + int sourceColumnQ4 = (blockOrigin << 4) + (motionVector.Column << 1); + int sourceRowQ4 = (blockOrigin << 4) + (motionVector.Row << 1); + int sourceIndex = + ((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + sourceOrigin.X + (sourceColumnQ4 >> 4); + + Av1CompoundInterPredictor.PredictCompoundScalar( + source, + sourceStride, + sourceIndex, + expectedPredictions, + blockSize, + blockSize, + blockSize, + Av1InterpolationFilter.Bilinear, + Av1InterpolationFilter.Bilinear, + sourceColumnQ4 & 15, + sourceRowQ4 & 15, + bitDepth.GetBitCount(), + predictionScratch); + + expectedPredictions = expectedSecond; + } + + ushort[] expected = new ushort[blockSize * blockSize]; + Av1CompoundIntermediateAveragePredictor.AverageIntermediate( + expected, + blockSize, + expectedFirst, + blockSize, + expectedSecond, + blockSize, + blockSize, + blockSize, + bitDepth.GetBitCount()); + + Assert.Equal((ushort)60, expected[0]); + + using Av1FrameBuffer frameBuffer = new( + Configuration.Default, + sequenceHeader, + Av1ColorFormat.Yuv400, + false); + + using Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); + Av1LoopFilterContext loopFilterContext = new(sequenceHeader); + Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); + using Av1BlockDecoder decoder = new( + sequenceHeader, + frameHeader, + frameBuffer, + loopFilterContext, + inverseQuantizer, + referenceFrames); + + decoder.UpdateSuperblock(superblockInfo); + decoder.DecodeBlock( + modeInfo, + new Point(2, 2), + Av1BlockSize.Block8x8, + superblockInfo, + new Av1TileInfo(0, 0, frameHeader)); + + for (int row = 0; row < blockSize; row++) + { + Span actual = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, blockOrigin + row, 0, 0); + Assert.Equal( + expected.AsSpan(row * blockSize, blockSize), + actual.Slice(blockOrigin, blockSize)); + } + } + /// /// Reconstructs one compound global-warp block and compares it with independently invoked scalar predictors. /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundInterPredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundInterPredictorTests.cs index b0109eec4..6124b5ddc 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundInterPredictorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundInterPredictorTests.cs @@ -33,6 +33,15 @@ public class Av1CompoundInterPredictorTests public void HighBitDepthAverageMatchesIndependentOracleAcrossIntrinsicWidths() => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthAverage, PredictorConfigurations); + /// + /// Verifies 10/12-bit no-round prediction and final equal averaging across every intrinsic width. + /// + [Fact] + public void HighBitDepthIntermediateAverageMatchesIndependentOracleAcrossIntrinsicWidths() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateHighBitDepthIntermediateAverage, + PredictorConfigurations); + /// /// Verifies 8-bit distance and per-sample mask blending across every intrinsic width and scalar tail. /// @@ -282,6 +291,223 @@ public class Av1CompoundInterPredictorTests } } + /// + /// Applies the high-bit-depth no-round convolution equations independently of the production operators. + /// + private static void ValidateHighBitDepthIntermediateAverage() + { + ReadOnlySpan widths = [9, 17, 33, 65]; + ReadOnlySpan<(int Horizontal, int Vertical)> phases = + [(0, 0), (5, 0), (0, 9), (5, 9)]; + + foreach (int bitDepth in new[] { 10, 12 }) + { + int maximum = (1 << bitDepth) - 1; + int intermediateRange = bitDepth + 7 - 3 + 2; + int round0 = 3 + Math.Max(intermediateRange - 16, 0); + int roundBits = 14 - round0 - 7; + int offsetBits = bitDepth + 14 - round0; + int roundOffset = (1 << (offsetBits - 7)) + (1 << (offsetBits - 8)); + + foreach (int width in widths) + { + const int height = 3; + int sourceStride = width + 5; + int intermediateStride = width + 3; + int destinationStride = width + 7; + ushort[] firstSource = new ushort[sourceStride * (height + 1)]; + ushort[] secondSource = new ushort[sourceStride * (height + 1)]; + + for (int row = 0; row <= height; row++) + { + for (int column = 0; column < sourceStride; column++) + { + firstSource[(row * sourceStride) + column] = + (ushort)(((row * 613) + (column * 349) + 17) & maximum); + + secondSource[(row * sourceStride) + column] = + (ushort)(((row * 947) + (column * 181) + 71) & maximum); + } + } + + foreach ((int horizontalPhase, int verticalPhase) in phases) + { + int horizontal0 = 128 - (horizontalPhase * 8); + int horizontal1 = horizontalPhase * 8; + int vertical0 = 128 - (verticalPhase * 8); + int vertical1 = verticalPhase * 8; + ushort[] expectedFirst = new ushort[intermediateStride * height]; + ushort[] expectedSecond = new ushort[intermediateStride * height]; + ushort[] actualFirst = new ushort[intermediateStride * height]; + ushort[] actualSecond = new ushort[intermediateStride * height]; + ushort[] scalarFirst = new ushort[intermediateStride * height]; + ushort[] scalarSecond = new ushort[intermediateStride * height]; + expectedFirst.AsSpan().Fill(0xA5A5); + expectedSecond.AsSpan().Fill(0xA5A5); + actualFirst.AsSpan().Fill(0xA5A5); + actualSecond.AsSpan().Fill(0xA5A5); + scalarFirst.AsSpan().Fill(0xA5A5); + scalarSecond.AsSpan().Fill(0xA5A5); + + for (int predictorIndex = 0; predictorIndex < 2; predictorIndex++) + { + ReadOnlySpan source = predictorIndex == 0 ? firstSource : secondSource; + Span expected = predictorIndex == 0 ? expectedFirst : expectedSecond; + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int sourceIndex = (row * sourceStride) + column; + int result; + if (horizontalPhase == 0 && verticalPhase == 0) + { + result = (source[sourceIndex] << roundBits) + roundOffset; + } + else if (verticalPhase == 0) + { + int sum = (horizontal0 * source[sourceIndex]) + + (horizontal1 * source[sourceIndex + 1]); + + result = ((sum + (1 << (round0 - 1))) >> round0) + roundOffset; + } + else if (horizontalPhase == 0) + { + int sum = (vertical0 * source[sourceIndex]) + + (vertical1 * source[sourceIndex + sourceStride]); + + int shifted = sum << (7 - round0); + result = ((shifted + 64) >> 7) + roundOffset; + } + else + { + int horizontalBias = 1 << (bitDepth + 6); + int firstHorizontal = horizontalBias + + (horizontal0 * source[sourceIndex]) + + (horizontal1 * source[sourceIndex + 1]); + + int secondHorizontal = horizontalBias + + (horizontal0 * source[sourceIndex + sourceStride]) + + (horizontal1 * source[sourceIndex + sourceStride + 1]); + + firstHorizontal = (firstHorizontal + (1 << (round0 - 1))) >> round0; + secondHorizontal = (secondHorizontal + (1 << (round0 - 1))) >> round0; + int verticalBias = 1 << (bitDepth + 14 - round0); + int vertical = verticalBias + + (vertical0 * firstHorizontal) + + (vertical1 * secondHorizontal); + + result = (vertical + 64) >> 7; + } + + expected[(row * intermediateStride) + column] = (ushort)result; + } + } + } + + int scratchStride = Math.Max(width, 128); + short[] scratch = new short[scratchStride * (height + 8)]; + Av1CompoundInterPredictor.PredictCompound( + firstSource, + sourceStride, + sourceOrigin: 0, + actualFirst, + intermediateStride, + width, + height, + Av1InterpolationFilter.Bilinear, + Av1InterpolationFilter.Bilinear, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + Av1CompoundInterPredictor.PredictCompound( + secondSource, + sourceStride, + sourceOrigin: 0, + actualSecond, + intermediateStride, + width, + height, + Av1InterpolationFilter.Bilinear, + Av1InterpolationFilter.Bilinear, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + Av1CompoundInterPredictor.PredictCompoundScalar( + firstSource, + sourceStride, + sourceOrigin: 0, + scalarFirst, + intermediateStride, + width, + height, + Av1InterpolationFilter.Bilinear, + Av1InterpolationFilter.Bilinear, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + Av1CompoundInterPredictor.PredictCompoundScalar( + secondSource, + sourceStride, + sourceOrigin: 0, + scalarSecond, + intermediateStride, + width, + height, + Av1InterpolationFilter.Bilinear, + Av1InterpolationFilter.Bilinear, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + Assert.Equal(expectedFirst, actualFirst); + Assert.Equal(expectedSecond, actualSecond); + Assert.Equal(expectedFirst, scalarFirst); + Assert.Equal(expectedSecond, scalarSecond); + + ushort[] expectedDestination = new ushort[destinationStride * height]; + ushort[] actualDestination = new ushort[destinationStride * height]; + expectedDestination.AsSpan().Fill(0xA5A5); + actualDestination.AsSpan().Fill(0xA5A5); + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int intermediateIndex = (row * intermediateStride) + column; + int result = ((expectedFirst[intermediateIndex] + expectedSecond[intermediateIndex]) >> 1) - + roundOffset; + + result = (result + (1 << (roundBits - 1))) >> roundBits; + expectedDestination[(row * destinationStride) + column] = + (ushort)Math.Clamp(result, 0, maximum); + } + } + + Av1CompoundIntermediateAveragePredictor.AverageIntermediate( + actualDestination, + destinationStride, + actualFirst, + intermediateStride, + actualSecond, + intermediateStride, + width, + height, + bitDepth); + + Assert.Equal(expectedDestination, actualDestination); + } + } + } + } + /// /// Applies independent byte arithmetic to every selectable compound blend. /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundReferenceEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundReferenceEntropyTests.cs index fc889aeea..0d10804f2 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundReferenceEntropyTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundReferenceEntropyTests.cs @@ -10,7 +10,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; /// -/// Verifies AV1 compound-reference selection and compound inter-mode entropy against pinned libaom. +/// Verifies AV1 compound-reference selection and compound inter-mode entropy against current official libaom main. /// [Trait("Format", "Avif")] public class Av1CompoundReferenceEntropyTests diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs index 5e8a5c67f..7adfde8ca 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs @@ -847,12 +847,12 @@ public class Av1ReconstructionConformanceTests } /// - /// Verifies exact native reconstruction and presentation for a genuine pinned-libavif image sequence that uses - /// equal-weight compound prediction. + /// Verifies exact native reconstruction and presentation for an image sequence that exercises equal-weight + /// compound prediction. The native reference has been reverified against current official libaom main. /// [Theory] [WithFile(TestImages.Heif.Av1AverageCompoundSequenceAvif, PixelTypes.Rgba32)] - public void DecodeRealLibavifSequenceWithEqualAverageCompoundMatchesPinnedReferences( + public void DecodeRealLibavifSequenceWithEqualAverageCompoundMatchesVerifiedReferences( TestImageProvider provider) => FeatureTestRunner.RunWithHwIntrinsicsFeature( @@ -945,7 +945,7 @@ public class Av1ReconstructionConformanceTests } catch (InvalidImageContentException exception) { - throw new InvalidImageContentException($"The pinned compound fixture failed at sample {sampleIndex}.", exception); + throw new InvalidImageContentException($"The verified compound fixture failed at sample {sampleIndex}.", exception); } using ImageFrame frame = decodedFrame; diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeRealLibavifSequenceWithEqualAverageCompoundMatchesPinnedReferences_Rgba32_libavif-webp-logo-average-compound.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeRealLibavifSequenceWithEqualAverageCompoundMatchesVerifiedReferences_Rgba32_libavif-webp-logo-average-compound.png similarity index 100% rename from tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeRealLibavifSequenceWithEqualAverageCompoundMatchesPinnedReferences_Rgba32_libavif-webp-logo-average-compound.png rename to tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeRealLibavifSequenceWithEqualAverageCompoundMatchesVerifiedReferences_Rgba32_libavif-webp-logo-average-compound.png diff --git a/tests/Images/Input/Heif/Av1/Conformance/README.md b/tests/Images/Input/Heif/Av1/Conformance/README.md index 82a6d487b..605bc5f6c 100644 --- a/tests/Images/Input/Heif/Av1/Conformance/README.md +++ b/tests/Images/Input/Heif/Av1/Conformance/README.md @@ -90,20 +90,31 @@ Exact pinned libaom decodes the corrected logical payload into two 33x11 YUV444 ## Equal-average compound fixture -The `libavif-webp-logo-average-compound.avif` fixture was encoded from the pinned libavif tree's `tests/data/webp_logo_animated.y4m` source. The source SHA-256 is `0872208D9C19B68B10A1647FA6849CFC4E2B21A19561ACD672E0629C70EFACA2`. It was generated with: +The `libavif-webp-logo-average-compound.avif` file is retained solely as interoperability input. It was +created from `tests/data/webp_logo_animated.y4m` with the following command; libavif is not used as an +AV1 implementation or reconstruction reference: ```text ./avifenc -j 1 -c aom -s 4 -q 80 -a enable-dist-wtd-comp=0 -a enable-masked-comp=0 -a enable-interintra-comp=0 -a enable-obmc=0 -a enable-warped-motion=0 -a enable-global-motion=0 tests/data/webp_logo_animated.y4m libavif-webp-logo-average-compound.avif ``` -Pinned scalar libavif generated the retained references with: +On 2026-08-31 the clean official libaom `main` checkout was refreshed from its upstream remote. At the +observed revision `441c439b9916474cac15d2822af47a9ad70674a8`, current `aomdec` decoded the 5,465-byte +`mdat` payload at file offset 1,065 as 19 shown 80x80 YUV444 frames: ```text -./avifdec -j 1 -c aom --index 18 libavif-webp-logo-average-compound.avif libavif-webp-logo-average-compound-libaom.y4m -./avifdec -j 1 -c aom --index 18 libavif-webp-logo-average-compound.avif libavif-webp-logo-average-compound-libavif.png +aomdec --codec=av1 --threads=1 --row-mt=0 --output-bit-depth=8 -o compound-current-main.y4m compound-current-main.obu ``` -The AVIF SHA-256 is `7919049D367EEDB7C965E170309D6759660DDBFD4BB1AEF9496F9D66E314846A`. The retained frame-18 Y4M SHA-256 is `41FF2408DEB473D5483F3398882DF7F7AB6C7D376561C19798881595EB0C5C0C`, and the frame-18 PNG SHA-256 is `BCFABC1E1C7E17D8ECB40569849A04FFAC6CA1FCDF613F217B33816CA47337AC`. The test decodes every preceding hidden and shown sample to establish the same retained-reference state before comparing all native Y, U, and V samples and the final RGBA presentation. +All 19 frames decoded successfully. The final frame's 19,200 native samples have SHA-256 +`E79D2F49C260B1AC9B1B9BBBB2D611126AFD3B241DA389EB9E7BD4EA0ED42080` and match the stored Y4M's +Y, U, and V samples exactly with zero differences. The observed revision records the source used for +this verification; it does not pin the libaom checkout. + +The production test decodes every preceding sample to establish the retained-reference state, requires +actual equal-average compound blocks, and compares the final native planes exactly. It then compares the +final RGBA output through ImageSharp's established reference-output API. The PNG is presentation evidence +only and is not used to establish AV1 reconstruction arithmetic. ## Selectable compound and inter-intra fixtures