Browse Source

Complete AV1 equal-average compound decoding checkpoint

pull/2633/head
James Jackson-South 3 days ago
parent
commit
4075a08448
  1. 42
      HEIF_IMPLEMENTATION_PLAN.md
  2. 2
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs
  3. 57
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Arithmetic.cs
  4. 242
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Operator.cs
  5. 558
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.cs
  6. 119
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.Operator.cs
  7. 101
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.cs
  8. 249
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs
  9. 159
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs
  10. 226
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundInterPredictorTests.cs
  11. 2
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundReferenceEntropyTests.cs
  12. 8
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs
  13. 0
      tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeRealLibavifSequenceWithEqualAverageCompoundMatchesVerifiedReferences_Rgba32_libavif-webp-logo-average-compound.png
  14. 21
      tests/Images/Input/Heif/Av1/Conformance/README.md

42
HEIF_IMPLEMENTATION_PLAN.md

@ -120,7 +120,7 @@ Checkpoint gate:
- [x] Release builds for net10.0 and net11.0 pass with zero errors. - [x] Release builds for net10.0 and net11.0 pass with zero errors.
- [x] Focused Release tests pass with zero failures or skips. - [x] Focused Release tests pass with zero failures or skips.
- [x] Scoped semantic, StyleCop, whitespace, and git diff checks pass. - [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 <james_south@hotmail.com>`.
Verified single-reference checkpoint evidence on 2026-08-31: 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 ### 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. - [x] Compound reference selection, paired reference-MV derivation, and equal averaging.
- [~] Inter-intra prediction. - [~] Inter-intra prediction. Current item.
- [~] Distance-weighted compound prediction. - [~] Distance-weighted compound prediction.
- [~] Wedge compound prediction. - [~] Wedge compound prediction.
- [~] Difference-weighted 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. - [~] Non-translational global prediction.
- [~] Inter deblocking decisions and reference/mode deltas. - [~] 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: For every item:
- [ ] Trace syntax and arithmetic to the current libaom `main` tree. - [ ] Trace syntax and arithmetic to the current libaom `main` tree.

2
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 // 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++) for (int index = 0; index < 2; index++)
{ {
this.candidates[index] = primaryList[index]; this.candidates[index] = primaryList[index];

57
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Arithmetic.cs

@ -68,6 +68,63 @@ internal static partial class Av1CompoundInterPredictor
return result.AsUInt16(); return result.AsUInt16();
} }
/// <summary>
/// Removes compound bias and fractional precision from 128-bit high-bit-depth lanes.
/// </summary>
public static Vector128<ushort> FinalizeHighBitDepthIntermediate(
Vector128<ushort> value,
int roundBits,
int roundOffset,
int maximum)
{
Vector128<short> result = (value - Vector128.Create((ushort)roundOffset)).AsInt16();
if (roundBits != 0)
{
result = (result + Vector128.Create((short)(1 << (roundBits - 1)))) >> roundBits;
}
result = Vector128.Max(Vector128<short>.Zero, Vector128.Min(Vector128.Create((short)maximum), result));
return result.AsUInt16();
}
/// <summary>
/// Removes compound bias and fractional precision from 256-bit high-bit-depth lanes.
/// </summary>
public static Vector256<ushort> FinalizeHighBitDepthIntermediate(
Vector256<ushort> value,
int roundBits,
int roundOffset,
int maximum)
{
Vector256<short> result = (value - Vector256.Create((ushort)roundOffset)).AsInt16();
if (roundBits != 0)
{
result = (result + Vector256.Create((short)(1 << (roundBits - 1)))) >> roundBits;
}
result = Vector256.Max(Vector256<short>.Zero, Vector256.Min(Vector256.Create((short)maximum), result));
return result.AsUInt16();
}
/// <summary>
/// Removes compound bias and fractional precision from 512-bit high-bit-depth lanes.
/// </summary>
public static Vector512<ushort> FinalizeHighBitDepthIntermediate(
Vector512<ushort> value,
int roundBits,
int roundOffset,
int maximum)
{
Vector512<short> result = (value - Vector512.Create((ushort)roundOffset)).AsInt16();
if (roundBits != 0)
{
result = (result + Vector512.Create((short)(1 << (roundBits - 1)))) >> roundBits;
}
result = Vector512.Max(Vector512<short>.Zero, Vector512.Min(Vector512.Create((short)maximum), result));
return result.AsUInt16();
}
/// <summary> /// <summary>
/// Removes the compound bias and final fractional precision from 128-bit widened lanes. /// Removes the compound bias and final fractional precision from 128-bit widened lanes.
/// </summary> /// </summary>

242
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Operator.cs

@ -27,6 +27,51 @@ internal static partial class Av1CompoundInterPredictor
/// <returns>The biased compound intermediate.</returns> /// <returns>The biased compound intermediate.</returns>
public static abstract ushort Copy(byte sample, int roundBits, int roundOffset); public static abstract ushort Copy(byte sample, int roundBits, int roundOffset);
/// <summary>
/// Converts one high-bit-depth integer-position sample to the compound intermediate representation.
/// </summary>
/// <param name="sample">The source sample.</param>
/// <param name="roundBits">The final reconstruction shift.</param>
/// <param name="roundOffset">The compound intermediate bias.</param>
/// <returns>The biased compound intermediate.</returns>
public static abstract ushort CopyHighBitDepth(ushort sample, int roundBits, int roundOffset);
/// <summary>
/// Converts 128 bits of high-bit-depth integer-position samples to compound intermediates.
/// </summary>
/// <param name="samples">The source samples.</param>
/// <param name="roundBits">The final reconstruction shift.</param>
/// <param name="roundOffset">The compound intermediate bias.</param>
/// <returns>The biased compound intermediates.</returns>
public static abstract Vector128<ushort> CopyHighBitDepth(
Vector128<ushort> samples,
int roundBits,
int roundOffset);
/// <summary>
/// Converts 256 bits of high-bit-depth integer-position samples to compound intermediates.
/// </summary>
/// <param name="samples">The source samples.</param>
/// <param name="roundBits">The final reconstruction shift.</param>
/// <param name="roundOffset">The compound intermediate bias.</param>
/// <returns>The biased compound intermediates.</returns>
public static abstract Vector256<ushort> CopyHighBitDepth(
Vector256<ushort> samples,
int roundBits,
int roundOffset);
/// <summary>
/// Converts 512 bits of high-bit-depth integer-position samples to compound intermediates.
/// </summary>
/// <param name="samples">The source samples.</param>
/// <param name="roundBits">The final reconstruction shift.</param>
/// <param name="roundOffset">The compound intermediate bias.</param>
/// <returns>The biased compound intermediates.</returns>
public static abstract Vector512<ushort> CopyHighBitDepth(
Vector512<ushort> samples,
int roundBits,
int roundOffset);
/// <summary> /// <summary>
/// Converts 128 bits of integer-position samples to compound intermediates. /// Converts 128 bits of integer-position samples to compound intermediates.
/// </summary> /// </summary>
@ -161,6 +206,57 @@ internal static partial class Av1CompoundInterPredictor
/// <returns>The rounded intermediates.</returns> /// <returns>The rounded intermediates.</returns>
public static abstract Vector512<short> PrepareHorizontal(Vector512<int> lower, Vector512<int> upper); public static abstract Vector512<short> PrepareHorizontal(Vector512<int> lower, Vector512<int> upper);
/// <summary>
/// Applies first-pass compound rounding to one biased high-bit-depth horizontal convolution result.
/// </summary>
/// <param name="result">The horizontal convolution result.</param>
/// <param name="bias">The bit-depth-dependent horizontal bias.</param>
/// <param name="round">The bit-depth-dependent first-pass shift.</param>
/// <returns>The rounded intermediate.</returns>
public static abstract short PrepareHighBitDepthHorizontal(int result, int bias, int round);
/// <summary>
/// Applies first-pass compound rounding to 128-bit widened high-bit-depth horizontal results.
/// </summary>
/// <param name="lower">The lower convolution results.</param>
/// <param name="upper">The upper convolution results.</param>
/// <param name="bias">The bit-depth-dependent horizontal bias.</param>
/// <param name="round">The bit-depth-dependent first-pass shift.</param>
/// <returns>The rounded intermediates.</returns>
public static abstract Vector128<short> PrepareHighBitDepthHorizontal(
Vector128<int> lower,
Vector128<int> upper,
int bias,
int round);
/// <summary>
/// Applies first-pass compound rounding to 256-bit widened high-bit-depth horizontal results.
/// </summary>
/// <param name="lower">The lower convolution results.</param>
/// <param name="upper">The upper convolution results.</param>
/// <param name="bias">The bit-depth-dependent horizontal bias.</param>
/// <param name="round">The bit-depth-dependent first-pass shift.</param>
/// <returns>The rounded intermediates.</returns>
public static abstract Vector256<short> PrepareHighBitDepthHorizontal(
Vector256<int> lower,
Vector256<int> upper,
int bias,
int round);
/// <summary>
/// Applies first-pass compound rounding to 512-bit widened high-bit-depth horizontal results.
/// </summary>
/// <param name="lower">The lower convolution results.</param>
/// <param name="upper">The upper convolution results.</param>
/// <param name="bias">The bit-depth-dependent horizontal bias.</param>
/// <param name="round">The bit-depth-dependent first-pass shift.</param>
/// <returns>The rounded intermediates.</returns>
public static abstract Vector512<short> PrepareHighBitDepthHorizontal(
Vector512<int> lower,
Vector512<int> upper,
int bias,
int round);
/// <summary> /// <summary>
/// Applies second-pass compound rounding to one biased vertical convolution result. /// Applies second-pass compound rounding to one biased vertical convolution result.
/// </summary> /// </summary>
@ -191,6 +287,50 @@ internal static partial class Av1CompoundInterPredictor
/// <param name="upper">The upper convolution results.</param> /// <param name="upper">The upper convolution results.</param>
/// <returns>The compound intermediates.</returns> /// <returns>The compound intermediates.</returns>
public static abstract Vector512<ushort> PrepareVertical(Vector512<int> lower, Vector512<int> upper); public static abstract Vector512<ushort> PrepareVertical(Vector512<int> lower, Vector512<int> upper);
/// <summary>
/// Applies second-pass compound rounding to one biased high-bit-depth vertical convolution result.
/// </summary>
/// <param name="result">The vertical convolution result.</param>
/// <param name="bias">The bit-depth-dependent vertical bias.</param>
/// <returns>The compound intermediate.</returns>
public static abstract ushort PrepareHighBitDepthVertical(int result, int bias);
/// <summary>
/// Applies second-pass compound rounding to 128-bit widened high-bit-depth vertical results.
/// </summary>
/// <param name="lower">The lower convolution results.</param>
/// <param name="upper">The upper convolution results.</param>
/// <param name="bias">The bit-depth-dependent vertical bias.</param>
/// <returns>The compound intermediates.</returns>
public static abstract Vector128<ushort> PrepareHighBitDepthVertical(
Vector128<int> lower,
Vector128<int> upper,
int bias);
/// <summary>
/// Applies second-pass compound rounding to 256-bit widened high-bit-depth vertical results.
/// </summary>
/// <param name="lower">The lower convolution results.</param>
/// <param name="upper">The upper convolution results.</param>
/// <param name="bias">The bit-depth-dependent vertical bias.</param>
/// <returns>The compound intermediates.</returns>
public static abstract Vector256<ushort> PrepareHighBitDepthVertical(
Vector256<int> lower,
Vector256<int> upper,
int bias);
/// <summary>
/// Applies second-pass compound rounding to 512-bit widened high-bit-depth vertical results.
/// </summary>
/// <param name="lower">The lower convolution results.</param>
/// <param name="upper">The upper convolution results.</param>
/// <param name="bias">The bit-depth-dependent vertical bias.</param>
/// <returns>The compound intermediates.</returns>
public static abstract Vector512<ushort> PrepareHighBitDepthVertical(
Vector512<int> lower,
Vector512<int> upper,
int bias);
} }
/// <summary> /// <summary>
@ -206,6 +346,35 @@ internal static partial class Av1CompoundInterPredictor
public static ushort Copy(byte sample, int roundBits, int roundOffset) public static ushort Copy(byte sample, int roundBits, int roundOffset)
=> (ushort)((sample << roundBits) + roundOffset); => (ushort)((sample << roundBits) + roundOffset);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort CopyHighBitDepth(ushort sample, int roundBits, int roundOffset)
=> (ushort)((sample << roundBits) + roundOffset);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<ushort> CopyHighBitDepth(
Vector128<ushort> samples,
int roundBits,
int roundOffset)
=> (samples << roundBits) + Vector128.Create((ushort)roundOffset);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<ushort> CopyHighBitDepth(
Vector256<ushort> samples,
int roundBits,
int roundOffset)
=> (samples << roundBits) + Vector256.Create((ushort)roundOffset);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<ushort> CopyHighBitDepth(
Vector512<ushort> samples,
int roundBits,
int roundOffset)
=> (samples << roundBits) + Vector512.Create((ushort)roundOffset);
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Copy( public static void Copy(
@ -315,6 +484,44 @@ internal static partial class Av1CompoundInterPredictor
RoundPowerOfTwo(lower + Vector512.Create(HorizontalBias), Round0Bits), RoundPowerOfTwo(lower + Vector512.Create(HorizontalBias), Round0Bits),
RoundPowerOfTwo(upper + Vector512.Create(HorizontalBias), Round0Bits)); RoundPowerOfTwo(upper + Vector512.Create(HorizontalBias), Round0Bits));
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static short PrepareHighBitDepthHorizontal(int result, int bias, int round)
=> (short)RoundPowerOfTwo(bias + result, round);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<short> PrepareHighBitDepthHorizontal(
Vector128<int> lower,
Vector128<int> upper,
int bias,
int round)
=> Av1IntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector128.Create(bias), round),
RoundPowerOfTwo(upper + Vector128.Create(bias), round));
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<short> PrepareHighBitDepthHorizontal(
Vector256<int> lower,
Vector256<int> upper,
int bias,
int round)
=> Av1IntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector256.Create(bias), round),
RoundPowerOfTwo(upper + Vector256.Create(bias), round));
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<short> PrepareHighBitDepthHorizontal(
Vector512<int> lower,
Vector512<int> upper,
int bias,
int round)
=> Av1IntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector512.Create(bias), round),
RoundPowerOfTwo(upper + Vector512.Create(bias), round));
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort PrepareVertical(int result) public static ushort PrepareVertical(int result)
@ -340,5 +547,40 @@ internal static partial class Av1CompoundInterPredictor
=> Av1IntraPredictorBase.Narrow( => Av1IntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector512.Create(VerticalBias), CompoundRound1Bits), RoundPowerOfTwo(lower + Vector512.Create(VerticalBias), CompoundRound1Bits),
RoundPowerOfTwo(upper + Vector512.Create(VerticalBias), CompoundRound1Bits)).AsUInt16(); RoundPowerOfTwo(upper + Vector512.Create(VerticalBias), CompoundRound1Bits)).AsUInt16();
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort PrepareHighBitDepthVertical(int result, int bias)
=> (ushort)RoundPowerOfTwo(bias + result, CompoundRound1Bits);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<ushort> PrepareHighBitDepthVertical(
Vector128<int> lower,
Vector128<int> upper,
int bias)
=> Av1IntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector128.Create(bias), CompoundRound1Bits),
RoundPowerOfTwo(upper + Vector128.Create(bias), CompoundRound1Bits)).AsUInt16();
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<ushort> PrepareHighBitDepthVertical(
Vector256<int> lower,
Vector256<int> upper,
int bias)
=> Av1IntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector256.Create(bias), CompoundRound1Bits),
RoundPowerOfTwo(upper + Vector256.Create(bias), CompoundRound1Bits)).AsUInt16();
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<ushort> PrepareHighBitDepthVertical(
Vector512<int> lower,
Vector512<int> upper,
int bias)
=> Av1IntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector512.Create(bias), CompoundRound1Bits),
RoundPowerOfTwo(upper + Vector512.Create(bias), CompoundRound1Bits)).AsUInt16();
} }
} }

558
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.cs

@ -65,6 +65,39 @@ internal static partial class Av1CompoundInterPredictor
scratch, scratch,
useSimd: true); useSimd: true);
/// <summary>
/// Reconstructs one high-bit-depth translational reference into AV1's unsigned compound intermediate format.
/// </summary>
public static void PredictCompound(
ReadOnlySpan<ushort> source,
int sourceStride,
int sourceOrigin,
Span<ushort> destination,
int destinationStride,
int width,
int height,
Av1InterpolationFilter horizontalFilter,
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int verticalPhase,
int bitDepth,
Span<short> scratch)
=> PredictCompound<CompoundPredictionOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalFilter,
verticalFilter,
horizontalPhase,
verticalPhase,
bitDepth,
scratch,
useSimd: true);
/// <summary> /// <summary>
/// Executes one closed compound-prediction conversion operator. /// Executes one closed compound-prediction conversion operator.
/// </summary> /// </summary>
@ -175,6 +208,124 @@ internal static partial class Av1CompoundInterPredictor
useSimd); useSimd);
} }
/// <summary>
/// Executes one closed high-bit-depth compound-prediction conversion operator.
/// </summary>
/// <typeparam name="TOperator">The compound-prediction conversion operator.</typeparam>
private static void PredictCompound<TOperator>(
ReadOnlySpan<ushort> source,
int sourceStride,
int sourceOrigin,
Span<ushort> destination,
int destinationStride,
int width,
int height,
Av1InterpolationFilter horizontalFilter,
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int verticalPhase,
int bitDepth,
Span<short> scratch,
bool useSimd)
where TOperator : struct, IAv1CompoundPredictionOperator
{
ReadOnlySpan<short> horizontalCoefficients = GetCompoundCoefficients(horizontalFilter, horizontalPhase, width <= 4);
ReadOnlySpan<short> 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<TOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
roundBits,
roundOffset,
useSimd);
return;
}
if (verticalPhase == 0)
{
GetEffectiveKernel(horizontalCoefficients, out int firstCoefficient, out int tapCount);
FilterCompoundDirect<TOperator>(
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<TOperator>(
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<TOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients[firstHorizontalCoefficient..],
horizontalTapCount,
firstHorizontalCoefficient - 3,
verticalCoefficients[firstVerticalCoefficient..],
verticalTapCount,
firstVerticalCoefficient - 3,
bitDepth,
round0,
scratch,
useSimd);
}
/// <summary> /// <summary>
/// Reconstructs one compound intermediate without explicit hardware intrinsics. /// Reconstructs one compound intermediate without explicit hardware intrinsics.
/// </summary> /// </summary>
@ -206,6 +357,39 @@ internal static partial class Av1CompoundInterPredictor
scratch, scratch,
useSimd: false); useSimd: false);
/// <summary>
/// Reconstructs one high-bit-depth compound intermediate without explicit hardware intrinsics.
/// </summary>
public static void PredictCompoundScalar(
ReadOnlySpan<ushort> source,
int sourceStride,
int sourceOrigin,
Span<ushort> destination,
int destinationStride,
int width,
int height,
Av1InterpolationFilter horizontalFilter,
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int verticalPhase,
int bitDepth,
Span<short> scratch)
=> PredictCompound<CompoundPredictionOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalFilter,
verticalFilter,
horizontalPhase,
verticalPhase,
bitDepth,
scratch,
useSimd: false);
/// <summary> /// <summary>
/// Copies integer-position samples through one closed compound-prediction operator. /// Copies integer-position samples through one closed compound-prediction operator.
/// </summary> /// </summary>
@ -275,6 +459,75 @@ internal static partial class Av1CompoundInterPredictor
} }
} }
/// <summary>
/// Copies high-bit-depth integer-position samples through one closed compound-prediction operator.
/// </summary>
/// <typeparam name="TOperator">The compound-prediction conversion operator.</typeparam>
private static void CopyCompound<TOperator>(
ReadOnlySpan<ushort> source,
int sourceStride,
int sourceOrigin,
Span<ushort> 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<ushort>.Count;
for (; column <= vectorEnd; column += Vector512<ushort>.Count)
{
Vector512<ushort> 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<ushort>.Count;
for (; column <= vectorEnd; column += Vector256<ushort>.Count)
{
Vector256<ushort> 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<ushort>.Count;
for (; column <= vectorEnd; column += Vector128<ushort>.Count)
{
Vector128<ushort> 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);
}
}
}
/// <summary> /// <summary>
/// Applies one compound convolution direction through one closed conversion operator. /// Applies one compound convolution direction through one closed conversion operator.
/// </summary> /// </summary>
@ -395,6 +648,112 @@ internal static partial class Av1CompoundInterPredictor
} }
} }
/// <summary>
/// Applies one high-bit-depth compound convolution direction through one closed conversion operator.
/// </summary>
/// <typeparam name="TOperator">The compound-prediction conversion operator.</typeparam>
private static void FilterCompoundDirect<TOperator>(
ReadOnlySpan<ushort> source,
int sourceStride,
int sourceOrigin,
Span<ushort> destination,
int destinationStride,
int width,
int height,
ReadOnlySpan<short> 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<ushort, short>(ref sourceRowUnsigned);
ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride);
int column = 0;
if (useSimd && Vector512.IsHardwareAccelerated)
{
int vectorEnd = width - Vector512<ushort>.Count;
for (; column <= vectorEnd; column += Vector512<ushort>.Count)
{
Convolve(
ref sourceRow,
tapStride,
(nuint)column,
ref coefficientBase,
tapCount,
Vector512<int>.Zero,
out Vector512<int> lower,
out Vector512<int> upper);
TOperator.PrepareDirect(lower, upper, preShift, round, roundOffset)
.StoreUnsafe(ref destinationRow, (nuint)column);
}
}
if (useSimd && Vector256.IsHardwareAccelerated)
{
int vectorEnd = width - Vector256<ushort>.Count;
for (; column <= vectorEnd; column += Vector256<ushort>.Count)
{
Convolve(
ref sourceRow,
tapStride,
(nuint)column,
ref coefficientBase,
tapCount,
Vector256<int>.Zero,
out Vector256<int> lower,
out Vector256<int> upper);
TOperator.PrepareDirect(lower, upper, preShift, round, roundOffset)
.StoreUnsafe(ref destinationRow, (nuint)column);
}
}
if (useSimd && Vector128.IsHardwareAccelerated)
{
int vectorEnd = width - Vector128<ushort>.Count;
for (; column <= vectorEnd; column += Vector128<ushort>.Count)
{
Convolve(
ref sourceRow,
tapStride,
(nuint)column,
ref coefficientBase,
tapCount,
Vector128<int>.Zero,
out Vector128<int> lower,
out Vector128<int> 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);
}
}
}
/// <summary> /// <summary>
/// Applies separable compound convolution through caller-owned signed scratch. /// Applies separable compound convolution through caller-owned signed scratch.
/// </summary> /// </summary>
@ -593,6 +952,205 @@ internal static partial class Av1CompoundInterPredictor
} }
} }
/// <summary>
/// Applies separable high-bit-depth compound convolution through caller-owned signed scratch.
/// </summary>
/// <typeparam name="TOperator">The compound-prediction conversion operator.</typeparam>
private static void FilterCompound2D<TOperator>(
ReadOnlySpan<ushort> source,
int sourceStride,
int sourceOrigin,
Span<ushort> destination,
int destinationStride,
int width,
int height,
ReadOnlySpan<short> horizontalCoefficients,
int horizontalTapCount,
int horizontalSourceOffset,
ReadOnlySpan<short> verticalCoefficients,
int verticalTapCount,
int verticalSourceOffset,
int bitDepth,
int round0,
Span<short> 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<ushort, short>(ref sourceRowUnsigned);
ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride);
int column = 0;
if (useSimd && Vector512.IsHardwareAccelerated)
{
int vectorEnd = width - Vector512<ushort>.Count;
for (; column <= vectorEnd; column += Vector512<ushort>.Count)
{
Convolve(
ref sourceRow,
1,
(nuint)column,
ref horizontalCoefficientBase,
horizontalTapCount,
Vector512<int>.Zero,
out Vector512<int> lower,
out Vector512<int> upper);
TOperator.PrepareHighBitDepthHorizontal(lower, upper, horizontalBias, round0)
.StoreUnsafe(ref scratchRow, (nuint)column);
}
}
if (useSimd && Vector256.IsHardwareAccelerated)
{
int vectorEnd = width - Vector256<ushort>.Count;
for (; column <= vectorEnd; column += Vector256<ushort>.Count)
{
Convolve(
ref sourceRow,
1,
(nuint)column,
ref horizontalCoefficientBase,
horizontalTapCount,
Vector256<int>.Zero,
out Vector256<int> lower,
out Vector256<int> upper);
TOperator.PrepareHighBitDepthHorizontal(lower, upper, horizontalBias, round0)
.StoreUnsafe(ref scratchRow, (nuint)column);
}
}
if (useSimd && Vector128.IsHardwareAccelerated)
{
int vectorEnd = width - Vector128<ushort>.Count;
for (; column <= vectorEnd; column += Vector128<ushort>.Count)
{
Convolve(
ref sourceRow,
1,
(nuint)column,
ref horizontalCoefficientBase,
horizontalTapCount,
Vector128<int>.Zero,
out Vector128<int> lower,
out Vector128<int> 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<short>.Count;
for (; column <= vectorEnd; column += Vector512<short>.Count)
{
Convolve(
ref scratchRow,
scratchStride,
(nuint)column,
ref verticalCoefficientBase,
verticalTapCount,
Vector512<int>.Zero,
out Vector512<int> lower,
out Vector512<int> upper);
TOperator.PrepareHighBitDepthVertical(lower, upper, verticalBias)
.StoreUnsafe(ref destinationRow, (nuint)column);
}
}
if (useSimd && Vector256.IsHardwareAccelerated)
{
int vectorEnd = width - Vector256<short>.Count;
for (; column <= vectorEnd; column += Vector256<short>.Count)
{
Convolve(
ref scratchRow,
scratchStride,
(nuint)column,
ref verticalCoefficientBase,
verticalTapCount,
Vector256<int>.Zero,
out Vector256<int> lower,
out Vector256<int> upper);
TOperator.PrepareHighBitDepthVertical(lower, upper, verticalBias)
.StoreUnsafe(ref destinationRow, (nuint)column);
}
}
if (useSimd && Vector128.IsHardwareAccelerated)
{
int vectorEnd = width - Vector128<short>.Count;
for (; column <= vectorEnd; column += Vector128<short>.Count)
{
Convolve(
ref scratchRow,
scratchStride,
(nuint)column,
ref verticalCoefficientBase,
verticalTapCount,
Vector128<int>.Zero,
out Vector128<int> lower,
out Vector128<int> 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);
}
}
}
/// <summary> /// <summary>
/// Gets the selected interpolation kernel for compound traversal. /// Gets the selected interpolation kernel for compound traversal.
/// </summary> /// </summary>

119
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.Operator.cs

@ -29,6 +29,22 @@ internal static partial class Av1CompoundIntermediateAveragePredictor
/// <returns>The reconstructed sample.</returns> /// <returns>The reconstructed sample.</returns>
public static abstract byte Average(ushort first, ushort second, int roundBits, int roundOffset); public static abstract byte Average(ushort first, ushort second, int roundBits, int roundOffset);
/// <summary>
/// Equal-averages and finalizes one pair of high-bit-depth compound intermediate samples.
/// </summary>
/// <param name="first">The first compound intermediate.</param>
/// <param name="second">The second compound intermediate.</param>
/// <param name="roundBits">The final reconstruction shift.</param>
/// <param name="roundOffset">The compound intermediate bias.</param>
/// <param name="maximum">The maximum reconstructed sample value.</param>
/// <returns>The reconstructed sample.</returns>
public static abstract ushort AverageHighBitDepth(
ushort first,
ushort second,
int roundBits,
int roundOffset,
int maximum);
/// <summary> /// <summary>
/// Equal-averages and finalizes 128 bits of compound intermediate samples. /// Equal-averages and finalizes 128 bits of compound intermediate samples.
/// </summary> /// </summary>
@ -82,6 +98,54 @@ internal static partial class Av1CompoundIntermediateAveragePredictor
Vector512<ushort> second1, Vector512<ushort> second1,
int roundBits, int roundBits,
int roundOffset); int roundOffset);
/// <summary>
/// Equal-averages and finalizes 128 bits of high-bit-depth compound intermediate samples.
/// </summary>
/// <param name="first">The first compound intermediates.</param>
/// <param name="second">The second compound intermediates.</param>
/// <param name="roundBits">The final reconstruction shift.</param>
/// <param name="roundOffset">The compound intermediate bias.</param>
/// <param name="maximum">The maximum reconstructed sample value.</param>
/// <returns>The reconstructed samples.</returns>
public static abstract Vector128<ushort> AverageHighBitDepth(
Vector128<ushort> first,
Vector128<ushort> second,
int roundBits,
int roundOffset,
int maximum);
/// <summary>
/// Equal-averages and finalizes 256 bits of high-bit-depth compound intermediate samples.
/// </summary>
/// <param name="first">The first compound intermediates.</param>
/// <param name="second">The second compound intermediates.</param>
/// <param name="roundBits">The final reconstruction shift.</param>
/// <param name="roundOffset">The compound intermediate bias.</param>
/// <param name="maximum">The maximum reconstructed sample value.</param>
/// <returns>The reconstructed samples.</returns>
public static abstract Vector256<ushort> AverageHighBitDepth(
Vector256<ushort> first,
Vector256<ushort> second,
int roundBits,
int roundOffset,
int maximum);
/// <summary>
/// Equal-averages and finalizes 512 bits of high-bit-depth compound intermediate samples.
/// </summary>
/// <param name="first">The first compound intermediates.</param>
/// <param name="second">The second compound intermediates.</param>
/// <param name="roundBits">The final reconstruction shift.</param>
/// <param name="roundOffset">The compound intermediate bias.</param>
/// <param name="maximum">The maximum reconstructed sample value.</param>
/// <returns>The reconstructed samples.</returns>
public static abstract Vector512<ushort> AverageHighBitDepth(
Vector512<ushort> first,
Vector512<ushort> second,
int roundBits,
int roundOffset,
int maximum);
} }
/// <summary> /// <summary>
@ -98,6 +162,19 @@ internal static partial class Av1CompoundIntermediateAveragePredictor
return (byte)Math.Clamp(RoundPowerOfTwo(result, roundBits), 0, byte.MaxValue); return (byte)Math.Clamp(RoundPowerOfTwo(result, roundBits), 0, byte.MaxValue);
} }
/// <inheritdoc/>
[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);
}
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Average( public static Vector128<byte> Average(
@ -136,5 +213,47 @@ internal static partial class Av1CompoundIntermediateAveragePredictor
=> Vector512.Narrow( => Vector512.Narrow(
FinalizeIntermediate((first0 & second0) + ((first0 ^ second0) >> 1), roundBits, roundOffset), FinalizeIntermediate((first0 & second0) + ((first0 ^ second0) >> 1), roundBits, roundOffset),
FinalizeIntermediate((first1 & second1) + ((first1 ^ second1) >> 1), roundBits, roundOffset)); FinalizeIntermediate((first1 & second1) + ((first1 ^ second1) >> 1), roundBits, roundOffset));
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<ushort> AverageHighBitDepth(
Vector128<ushort> first,
Vector128<ushort> second,
int roundBits,
int roundOffset,
int maximum)
=> FinalizeHighBitDepthIntermediate(
(first & second) + ((first ^ second) >> 1),
roundBits,
roundOffset,
maximum);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<ushort> AverageHighBitDepth(
Vector256<ushort> first,
Vector256<ushort> second,
int roundBits,
int roundOffset,
int maximum)
=> FinalizeHighBitDepthIntermediate(
(first & second) + ((first ^ second) >> 1),
roundBits,
roundOffset,
maximum);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<ushort> AverageHighBitDepth(
Vector512<ushort> first,
Vector512<ushort> second,
int roundBits,
int roundOffset,
int maximum)
=> FinalizeHighBitDepthIntermediate(
(first & second) + ((first ^ second) >> 1),
roundBits,
roundOffset,
maximum);
} }
} }

101
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.cs

@ -121,4 +121,105 @@ internal static partial class Av1CompoundIntermediateAveragePredictor
} }
} }
} }
/// <summary>
/// Combines two high-bit-depth compound intermediates by equal averaging.
/// </summary>
public static void AverageIntermediate(
Span<ushort> destination,
int destinationStride,
ReadOnlySpan<ushort> first,
int firstStride,
ReadOnlySpan<ushort> second,
int secondStride,
int width,
int height,
int bitDepth)
=> AverageIntermediate<CompoundIntermediateAverageOperator>(
destination,
destinationStride,
first,
firstStride,
second,
secondStride,
width,
height,
bitDepth);
/// <summary>
/// Executes one closed high-bit-depth equal-average compound-intermediate operator.
/// </summary>
/// <typeparam name="TOperator">The compound-intermediate operator.</typeparam>
private static void AverageIntermediate<TOperator>(
Span<ushort> destination,
int destinationStride,
ReadOnlySpan<ushort> first,
int firstStride,
ReadOnlySpan<ushort> 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<ushort> destinationRow = destination.Slice(row * destinationStride, width);
ReadOnlySpan<ushort> firstRow = first.Slice(row * firstStride, width);
ReadOnlySpan<ushort> 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<ushort>.Count;
for (; column <= vectorEnd; column += Vector512<ushort>.Count)
{
Vector512<ushort> firstVector = Vector512.LoadUnsafe(ref firstReference, (nuint)column);
Vector512<ushort> 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<ushort>.Count;
for (; column <= vectorEnd; column += Vector256<ushort>.Count)
{
Vector256<ushort> firstVector = Vector256.LoadUnsafe(ref firstReference, (nuint)column);
Vector256<ushort> 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<ushort>.Count;
for (; column <= vectorEnd; column += Vector128<ushort>.Count)
{
Vector128<ushort> firstVector = Vector128.LoadUnsafe(ref firstReference, (nuint)column);
Vector128<ushort> 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);
}
}
}
} }

249
src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs

@ -368,10 +368,18 @@ internal sealed class Av1BlockDecoder : IDisposable
secondaryReferenceFrameBuffer!.Width != this.frameHeader.FrameSize.FrameWidth || secondaryReferenceFrameBuffer!.Width != this.frameHeader.FrameSize.FrameWidth ||
secondaryReferenceFrameBuffer.Height != this.frameHeader.FrameSize.FrameHeight); secondaryReferenceFrameBuffer.Height != this.frameHeader.FrameSize.FrameHeight);
// Eight-bit compound prediction uses the normative no-round intermediate path below. Scaled and // Compound convolution is combined before its final rounding step. Scaled references and high-bit-depth
// high-bit-depth variants remain on their existing paths until their matching kernels are selected. // 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 = bool useCompoundIntermediates =
isCompound && !highBitDepth && !hasScaledCompoundReference; isCompound &&
!hasScaledCompoundReference &&
(!highBitDepth || useHighBitDepthCompoundIntermediates);
for (int referenceIndex = 0; referenceIndex < referenceCount; referenceIndex++) for (int referenceIndex = 0; referenceIndex < referenceCount; referenceIndex++)
{ {
@ -598,24 +606,48 @@ internal sealed class Av1BlockDecoder : IDisposable
int sourceIndex = int sourceIndex =
((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + sourceOrigin.X + (sourceColumnQ4 >> 4); ((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + sourceOrigin.X + (sourceColumnQ4 >> 4);
Span<ushort> destination = referenceIndex == 0 if (useCompoundIntermediates)
? MemoryMarshal.Cast<short, ushort>(highBitDepthBlockReconstructionBuffer[reconstructionStride..]) {
: highBitDepthSecondPrediction; Span<ushort> destination = referenceIndex == 0
? firstCompoundPrediction
: highBitDepthSecondPrediction;
Av1InterPredictor.Predict( Av1CompoundInterPredictor.PredictCompound(
source, source,
sourceStride, sourceStride,
sourceIndex, sourceIndex,
destination, destination,
destinationStride, predictionWidth,
predictionWidth, predictionWidth,
predictionHeight, predictionHeight,
modeInfo.InterpolationFilters[1], modeInfo.InterpolationFilters[1],
modeInfo.InterpolationFilters[0], modeInfo.InterpolationFilters[0],
horizontalPhase, horizontalPhase,
verticalPhase, verticalPhase,
this.frameBuffer.BitDepth.GetBitCount(), this.frameBuffer.BitDepth.GetBitCount(),
predictionScratch); predictionScratch);
}
else
{
Span<ushort> destination = referenceIndex == 0
? MemoryMarshal.Cast<short, ushort>(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 else
{ {
@ -676,98 +708,117 @@ internal sealed class Av1BlockDecoder : IDisposable
{ {
if (useCompoundIntermediates) if (useCompoundIntermediates)
{ {
Span<byte> destination = blockReconstructionBuffer[reconstructionStride..];
ReadOnlySpan<ushort> first = firstCompoundPrediction[..(predictionWidth * predictionHeight)]; ReadOnlySpan<ushort> first = firstCompoundPrediction[..(predictionWidth * predictionHeight)];
switch (modeInfo.CompoundType) if (highBitDepth)
{ {
case Av1CompoundType.Average: Span<ushort> highBitDepthDestination = MemoryMarshal.Cast<short, ushort>(
Av1CompoundIntermediateAveragePredictor.AverageIntermediate( highBitDepthBlockReconstructionBuffer[reconstructionStride..]);
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);
break; Av1CompoundIntermediateAveragePredictor.AverageIntermediate(
case Av1CompoundType.Wedge: highBitDepthDestination,
Av1WedgeMask.Fill( reconstructionStride,
compoundMask, first,
predictionWidth, predictionWidth,
blockSize, highBitDepthSecondPrediction,
modeInfo.CompoundWedgeIndex, predictionWidth,
modeInfo.CompoundWedgeSign, predictionWidth,
subX, predictionHeight,
subY, this.frameBuffer.BitDepth.GetBitCount());
invert: false); }
else
{
Span<byte> destination = blockReconstructionBuffer[reconstructionStride..];
switch (modeInfo.CompoundType)
{
case Av1CompoundType.Average:
Av1CompoundIntermediateAveragePredictor.AverageIntermediate(
destination,
reconstructionStride,
first,
predictionWidth,
highBitDepthSecondPrediction,
predictionWidth,
predictionWidth,
predictionHeight,
bitDepth: 8);
Av1CompoundIntermediateMaskBlendPredictor.BlendIntermediate( break;
destination, case Av1CompoundType.DistanceWeighted:
reconstructionStride, Av1CompoundIntermediateDistanceWeightedPredictor.DistanceWeightedIntermediate(
first, destination,
predictionWidth, reconstructionStride,
highBitDepthSecondPrediction, first,
predictionWidth, predictionWidth,
compoundMask, highBitDepthSecondPrediction,
predictionWidth, predictionWidth,
predictionWidth, predictionWidth,
predictionHeight, predictionHeight,
subX: 0, firstCompoundWeight,
subY: 0, secondCompoundWeight,
bitDepth: 8); bitDepth: 8);
break; break;
default: case Av1CompoundType.Wedge:
int lumaWidth = blockSize.GetWidth(); Av1WedgeMask.Fill(
if (plane == 0)
{
Av1CompoundIntermediateDifferenceWeightedMaskBuilder.FillDifferenceWeightedIntermediateMask(
compoundMask, compoundMask,
lumaWidth, predictionWidth,
blockSize,
modeInfo.CompoundWedgeIndex,
modeInfo.CompoundWedgeSign,
subX,
subY,
invert: false);
Av1CompoundIntermediateMaskBlendPredictor.BlendIntermediate(
destination,
reconstructionStride,
first, first,
predictionWidth, predictionWidth,
highBitDepthSecondPrediction, highBitDepthSecondPrediction,
predictionWidth, predictionWidth,
compoundMask,
predictionWidth,
predictionWidth, predictionWidth,
predictionHeight, predictionHeight,
bitDepth: 8, subX: 0,
modeInfo.DifferenceWeightedMaskType); subY: 0,
} bitDepth: 8);
Av1CompoundIntermediateMaskBlendPredictor.BlendIntermediate( break;
destination, default:
reconstructionStride, int lumaWidth = blockSize.GetWidth();
first, if (plane == 0)
predictionWidth, {
highBitDepthSecondPrediction, Av1CompoundIntermediateDifferenceWeightedMaskBuilder.FillDifferenceWeightedIntermediateMask(
predictionWidth, compoundMask,
compoundMask, lumaWidth,
lumaWidth, first,
predictionWidth, predictionWidth,
predictionHeight, highBitDepthSecondPrediction,
subX, predictionWidth,
subY, predictionWidth,
bitDepth: 8); 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) else if (highBitDepth)

159
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs

@ -26,6 +26,12 @@ public class Av1CompoundBlockDecoderTests
/// </summary> /// </summary>
private const HwIntrinsics GlobalWarpConfigurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic; private const HwIntrinsics GlobalWarpConfigurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic;
/// <summary>
/// The hardware configurations covering every compound-prediction vector width and the scalar fallback.
/// </summary>
private const HwIntrinsics CompoundPredictionConfigurations =
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic;
/// <summary> /// <summary>
/// Verifies that two retained reference planes are predicted and averaged before residual reconstruction. /// Verifies that two retained reference planes are predicted and averaged before residual reconstruction.
/// </summary> /// </summary>
@ -75,7 +81,7 @@ public class Av1CompoundBlockDecoderTests
modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last;
modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Last2; modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Last2;
modeInfo.InterpolationFilters.Fill(Av1InterpolationFilter.Regular); modeInfo.InterpolationFilters.Clear();
modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1);
Av1LoopFilterContext loopFilterContext = new(sequenceHeader); Av1LoopFilterContext loopFilterContext = new(sequenceHeader);
@ -117,6 +123,15 @@ public class Av1CompoundBlockDecoderTests
} }
} }
/// <summary>
/// Verifies that high-bit-depth subpixel predictors retain their no-round precision until the compound average.
/// </summary>
[Fact]
public void DecodeBlockReconstructsSubpixelHighBitDepthEqualAverageCompoundPrediction()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(
ValidateSubpixelHighBitDepthEqualAverageCompoundPrediction,
CompoundPredictionConfigurations);
/// <summary> /// <summary>
/// Verifies that both references of a GLOBAL_GLOBALMV block use their complete matrix before compound averaging. /// Verifies that both references of a GLOBAL_GLOBALMV block use their complete matrix before compound averaging.
/// </summary> /// </summary>
@ -627,6 +642,148 @@ public class Av1CompoundBlockDecoderTests
} }
} }
/// <summary>
/// Reconstructs the high-bit-depth subpixel compound regression at every supported source precision.
/// </summary>
private static void ValidateSubpixelHighBitDepthEqualAverageCompoundPrediction()
{
foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.TenBit, Av1BitDepth.TwelveBit })
{
ValidateSubpixelHighBitDepthEqualAverageCompoundPredictionAtBitDepth(bitDepth);
}
}
/// <summary>
/// Reconstructs one high-bit-depth half-sample compound block and compares it with the scalar no-round pipeline.
/// </summary>
/// <param name="bitDepth">The native sample depth.</param>
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<ushort> expectedPredictions = expectedFirst;
short[] predictionScratch = new short[128 * (blockSize + 8)];
for (int referenceIndex = 0; referenceIndex < 2; referenceIndex++)
{
Av1FrameBuffer<byte> reference = referenceFrames.Resolve(referenceIndex)!.FrameBuffer;
Span<ushort> 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<byte> 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<ushort> actual = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, blockOrigin + row, 0, 0);
Assert.Equal(
expected.AsSpan(row * blockSize, blockSize),
actual.Slice(blockOrigin, blockSize));
}
}
/// <summary> /// <summary>
/// Reconstructs one compound global-warp block and compares it with independently invoked scalar predictors. /// Reconstructs one compound global-warp block and compares it with independently invoked scalar predictors.
/// </summary> /// </summary>

226
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundInterPredictorTests.cs

@ -33,6 +33,15 @@ public class Av1CompoundInterPredictorTests
public void HighBitDepthAverageMatchesIndependentOracleAcrossIntrinsicWidths() public void HighBitDepthAverageMatchesIndependentOracleAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthAverage, PredictorConfigurations); => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthAverage, PredictorConfigurations);
/// <summary>
/// Verifies 10/12-bit no-round prediction and final equal averaging across every intrinsic width.
/// </summary>
[Fact]
public void HighBitDepthIntermediateAverageMatchesIndependentOracleAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(
ValidateHighBitDepthIntermediateAverage,
PredictorConfigurations);
/// <summary> /// <summary>
/// Verifies 8-bit distance and per-sample mask blending across every intrinsic width and scalar tail. /// Verifies 8-bit distance and per-sample mask blending across every intrinsic width and scalar tail.
/// </summary> /// </summary>
@ -282,6 +291,223 @@ public class Av1CompoundInterPredictorTests
} }
} }
/// <summary>
/// Applies the high-bit-depth no-round convolution equations independently of the production operators.
/// </summary>
private static void ValidateHighBitDepthIntermediateAverage()
{
ReadOnlySpan<int> 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<ushort> source = predictorIndex == 0 ? firstSource : secondSource;
Span<ushort> 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);
}
}
}
}
/// <summary> /// <summary>
/// Applies independent byte arithmetic to every selectable compound blend. /// Applies independent byte arithmetic to every selectable compound blend.
/// </summary> /// </summary>

2
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; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
[Trait("Format", "Avif")] [Trait("Format", "Avif")]
public class Av1CompoundReferenceEntropyTests public class Av1CompoundReferenceEntropyTests

8
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs

@ -847,12 +847,12 @@ public class Av1ReconstructionConformanceTests
} }
/// <summary> /// <summary>
/// Verifies exact native reconstruction and presentation for a genuine pinned-libavif image sequence that uses /// Verifies exact native reconstruction and presentation for an image sequence that exercises equal-weight
/// equal-weight compound prediction. /// compound prediction. The native reference has been reverified against current official libaom main.
/// </summary> /// </summary>
[Theory] [Theory]
[WithFile(TestImages.Heif.Av1AverageCompoundSequenceAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.Av1AverageCompoundSequenceAvif, PixelTypes.Rgba32)]
public void DecodeRealLibavifSequenceWithEqualAverageCompoundMatchesPinnedReferences( public void DecodeRealLibavifSequenceWithEqualAverageCompoundMatchesVerifiedReferences(
TestImageProvider<Rgba32> provider) TestImageProvider<Rgba32> provider)
=> FeatureTestRunner.RunWithHwIntrinsicsFeature( => FeatureTestRunner.RunWithHwIntrinsicsFeature(
@ -945,7 +945,7 @@ public class Av1ReconstructionConformanceTests
} }
catch (InvalidImageContentException exception) 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<Rgba32> frame = decodedFrame; using ImageFrame<Rgba32> frame = decodedFrame;

0
tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeRealLibavifSequenceWithEqualAverageCompoundMatchesPinnedReferences_Rgba32_libavif-webp-logo-average-compound.png → tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeRealLibavifSequenceWithEqualAverageCompoundMatchesVerifiedReferences_Rgba32_libavif-webp-logo-average-compound.png

21
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 ## 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 ```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 ./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 ```text
./avifdec -j 1 -c aom --index 18 libavif-webp-logo-average-compound.avif libavif-webp-logo-average-compound-libaom.y4m aomdec --codec=av1 --threads=1 --row-mt=0 --output-bit-depth=8 -o compound-current-main.y4m compound-current-main.obu
./avifdec -j 1 -c aom --index 18 libavif-webp-logo-average-compound.avif libavif-webp-logo-average-compound-libavif.png
``` ```
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 ## Selectable compound and inter-intra fixtures

Loading…
Cancel
Save