Browse Source

Complete AV1 distance-weighted compound decoding checkpoint

pull/2633/head
James Jackson-South 4 days ago
parent
commit
7e2de7a2c2
  1. 39
      HEIF_IMPLEMENTATION_PLAN.md
  2. 186
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.Operator.cs
  3. 127
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.cs
  4. 40
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs
  5. 88
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs
  6. 98
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundInterPredictorTests.cs
  7. 16
      tests/Images/Input/Heif/Av1/Conformance/README.md

39
HEIF_IMPLEMENTATION_PLAN.md

@ -178,8 +178,8 @@ The single-reference syntax, buffer, reconstruction, and ownership foundation is
- [x] Compound reference selection, paired reference-MV derivation, and equal averaging. - [x] Compound reference selection, paired reference-MV derivation, and equal averaging.
- [x] Inter-intra prediction. - [x] Inter-intra prediction.
- [~] Distance-weighted compound prediction. Current item. - [x] Distance-weighted compound prediction.
- [~] Wedge compound prediction. - [~] Wedge compound prediction. Current item.
- [~] Difference-weighted compound prediction. - [~] Difference-weighted compound prediction.
- [~] OBMC. - [~] OBMC.
- [~] Scaled-reference prediction. - [~] Scaled-reference prediction.
@ -251,6 +251,41 @@ Verified inter-intra checkpoint evidence on 2026-08-31:
failures or skips. Scoped analyzer and whitespace verification pass for both changed C# files. failures or skips. Scoped analyzer and whitespace verification pass for both changed C# files.
Roslynk reports zero compiler errors and no diagnostics in the changed files, `git diff --check` Roslynk reports zero compiler errors and no diagnostics in the changed files, `git diff --check`
passes, and `.gitattributes` is unchanged. passes, and `.gitattributes` is unchanged.
- [x] The completed checkpoint was committed as `18b1c881271a3494489ca6f410ab140544902e2d`
with author and committer `James Jackson-South <james_south@hotmail.com>`.
Verified distance-weighted compound checkpoint evidence on 2026-08-31:
- [x] Audited reference-distance quantization against `quant_dist_weight` and
`quant_dist_lookup_table` in current libaom `av1/common/common_data.h`, and audited order-hint
distance selection and forward/backward reference assignment against
`av1_dist_wtd_comp_weight_assign` in `av1/common/reconinter.c`.
- [x] Audited reconstruction against current libaom `av1/common/convolve.c`. Corrected the production
10/12-bit subpixel path, which incorrectly finalized its two no-round compound intermediates with an
equal average instead of the signaled distance weights. The fixed path applies libaom's 4-bit weighted
shift before bias removal, final rounding, and clipping.
- [x] Added descending Vector512, Vector256, Vector128, and scalar traversal to the existing semantic
distance-weighted intermediate predictor family. Unsigned widening preserves the biased 12-bit
intermediate range. No per-block, per-row, or per-scanline allocation or copy was added.
- [x] Added FeatureTestRunner coverage for every current-libaom distance-weight class in both reference
orders, and for 10/12-bit copy, horizontal, vertical, and separable subpixel prediction at widths 9,
17, 33, and 65, with an independent no-round oracle and row-padding sentinels.
- [x] Added a complete `Av1BlockDecoder.DecodeBlock` 10/12-bit half-sample regression that selects the
13:3 distance weights through real order hints. Its first reconstructed sample differs from the old
equal-average result, so the test proves the corrected production branch is executed.
- [x] Extracted the fixture's 5,372-byte AV1 `mdat` payload and decoded it with the refreshed current
libaom `aomdec`, using one thread with row threading disabled. All 19 frames decoded. The final 19,200
YUV444 samples have SHA-256
`E8CAA650F1571C5B9CACAF8C06E1DDF5F5D2ED35F65F1C34377076C573425899` and match the retained native
reference with zero differing samples.
- [x] The real 19-frame production sequence requires decoded distance-weighted compound blocks, compares
the final native Y, Cb, and Cr 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 44/44 on net10.0 and 44/44 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, `git diff --check` passes, and
`.gitattributes` is unchanged.
For every item: For every item:

186
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.Operator.cs

@ -102,6 +102,86 @@ internal static partial class Av1CompoundIntermediateDistanceWeightedPredictor
int secondWeight, int secondWeight,
int roundBits, int roundBits,
int roundOffset); int roundOffset);
/// <summary>
/// Distance-weights 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="firstWeight">The first predictor weight.</param>
/// <param name="secondWeight">The second predictor weight.</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 DistanceWeightedHighBitDepth(
ushort first,
ushort second,
int firstWeight,
int secondWeight,
int roundBits,
int roundOffset,
int maximum);
/// <summary>
/// Distance-weights 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="firstWeight">The first predictor weight.</param>
/// <param name="secondWeight">The second predictor weight.</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> DistanceWeightedHighBitDepth(
Vector128<ushort> first,
Vector128<ushort> second,
int firstWeight,
int secondWeight,
int roundBits,
int roundOffset,
int maximum);
/// <summary>
/// Distance-weights 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="firstWeight">The first predictor weight.</param>
/// <param name="secondWeight">The second predictor weight.</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> DistanceWeightedHighBitDepth(
Vector256<ushort> first,
Vector256<ushort> second,
int firstWeight,
int secondWeight,
int roundBits,
int roundOffset,
int maximum);
/// <summary>
/// Distance-weights 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="firstWeight">The first predictor weight.</param>
/// <param name="secondWeight">The second predictor weight.</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> DistanceWeightedHighBitDepth(
Vector512<ushort> first,
Vector512<ushort> second,
int firstWeight,
int secondWeight,
int roundBits,
int roundOffset,
int maximum);
} }
/// <summary> /// <summary>
@ -124,6 +204,22 @@ internal static partial class Av1CompoundIntermediateDistanceWeightedPredictor
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 DistanceWeightedHighBitDepth(
ushort first,
ushort second,
int firstWeight,
int secondWeight,
int roundBits,
int roundOffset,
int maximum)
{
int result = ((first * firstWeight) + (second * secondWeight)) >> DistanceWeightBits;
result -= roundOffset;
return (ushort)Math.Clamp(RoundPowerOfTwo(result, roundBits), 0, maximum);
}
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> DistanceWeighted( public static Vector128<byte> DistanceWeighted(
@ -169,6 +265,96 @@ internal static partial class Av1CompoundIntermediateDistanceWeightedPredictor
DistanceWeighted(first0, second0, firstWeight, secondWeight, roundBits, roundOffset), DistanceWeighted(first0, second0, firstWeight, secondWeight, roundBits, roundOffset),
DistanceWeighted(first1, second1, firstWeight, secondWeight, roundBits, roundOffset)); DistanceWeighted(first1, second1, firstWeight, secondWeight, roundBits, roundOffset));
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<ushort> DistanceWeightedHighBitDepth(
Vector128<ushort> first,
Vector128<ushort> second,
int firstWeight,
int secondWeight,
int roundBits,
int roundOffset,
int maximum)
{
Vector128<uint> firstLower = Vector128.WidenLower(first);
Vector128<uint> firstUpper = Vector128.WidenUpper(first);
Vector128<uint> secondLower = Vector128.WidenLower(second);
Vector128<uint> secondUpper = Vector128.WidenUpper(second);
Vector128<uint> lower =
((firstLower * Vector128.Create((uint)firstWeight)) +
(secondLower * Vector128.Create((uint)secondWeight))) >> DistanceWeightBits;
Vector128<uint> upper =
((firstUpper * Vector128.Create((uint)firstWeight)) +
(secondUpper * Vector128.Create((uint)secondWeight))) >> DistanceWeightBits;
return FinalizeHighBitDepthIntermediate(
Vector128.Narrow(lower, upper),
roundBits,
roundOffset,
maximum);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<ushort> DistanceWeightedHighBitDepth(
Vector256<ushort> first,
Vector256<ushort> second,
int firstWeight,
int secondWeight,
int roundBits,
int roundOffset,
int maximum)
{
Vector256<uint> firstLower = Vector256.WidenLower(first);
Vector256<uint> firstUpper = Vector256.WidenUpper(first);
Vector256<uint> secondLower = Vector256.WidenLower(second);
Vector256<uint> secondUpper = Vector256.WidenUpper(second);
Vector256<uint> lower =
((firstLower * Vector256.Create((uint)firstWeight)) +
(secondLower * Vector256.Create((uint)secondWeight))) >> DistanceWeightBits;
Vector256<uint> upper =
((firstUpper * Vector256.Create((uint)firstWeight)) +
(secondUpper * Vector256.Create((uint)secondWeight))) >> DistanceWeightBits;
return FinalizeHighBitDepthIntermediate(
Vector256.Narrow(lower, upper),
roundBits,
roundOffset,
maximum);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<ushort> DistanceWeightedHighBitDepth(
Vector512<ushort> first,
Vector512<ushort> second,
int firstWeight,
int secondWeight,
int roundBits,
int roundOffset,
int maximum)
{
Vector512<uint> firstLower = Vector512.WidenLower(first);
Vector512<uint> firstUpper = Vector512.WidenUpper(first);
Vector512<uint> secondLower = Vector512.WidenLower(second);
Vector512<uint> secondUpper = Vector512.WidenUpper(second);
Vector512<uint> lower =
((firstLower * Vector512.Create((uint)firstWeight)) +
(secondLower * Vector512.Create((uint)secondWeight))) >> DistanceWeightBits;
Vector512<uint> upper =
((firstUpper * Vector512.Create((uint)firstWeight)) +
(secondUpper * Vector512.Create((uint)secondWeight))) >> DistanceWeightBits;
return FinalizeHighBitDepthIntermediate(
Vector512.Narrow(lower, upper),
roundBits,
roundOffset,
maximum);
}
/// <summary> /// <summary>
/// Distance-weights 128-bit lanes without overflowing the unsigned intermediate range. /// Distance-weights 128-bit lanes without overflowing the unsigned intermediate range.
/// </summary> /// </summary>

127
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.cs

@ -153,4 +153,131 @@ internal static partial class Av1CompoundIntermediateDistanceWeightedPredictor
} }
} }
} }
/// <summary>
/// Combines two high-bit-depth compound intermediates using the decoded display-distance weights.
/// </summary>
public static void DistanceWeightedIntermediate(
Span<ushort> destination,
int destinationStride,
ReadOnlySpan<ushort> first,
int firstStride,
ReadOnlySpan<ushort> second,
int secondStride,
int width,
int height,
int firstWeight,
int secondWeight,
int bitDepth)
=> DistanceWeightedIntermediate<CompoundIntermediateDistanceWeightedOperator>(
destination,
destinationStride,
first,
firstStride,
second,
secondStride,
width,
height,
firstWeight,
secondWeight,
bitDepth);
/// <summary>
/// Executes one closed high-bit-depth distance-weighted compound-intermediate operator.
/// </summary>
/// <typeparam name="TOperator">The compound-intermediate operator.</typeparam>
private static void DistanceWeightedIntermediate<TOperator>(
Span<ushort> destination,
int destinationStride,
ReadOnlySpan<ushort> first,
int firstStride,
ReadOnlySpan<ushort> second,
int secondStride,
int width,
int height,
int firstWeight,
int secondWeight,
int bitDepth)
where TOperator : struct, IAv1CompoundIntermediateDistanceWeightedOperator
{
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.DistanceWeightedHighBitDepth(
firstVector,
secondVector,
firstWeight,
secondWeight,
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.DistanceWeightedHighBitDepth(
firstVector,
secondVector,
firstWeight,
secondWeight,
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.DistanceWeightedHighBitDepth(
firstVector,
secondVector,
firstWeight,
secondWeight,
roundBits,
roundOffset,
maximum).StoreUnsafe(ref destinationReference, (nuint)column);
}
}
for (; column < width; column++)
{
destinationRow[column] = TOperator.DistanceWeightedHighBitDepth(
firstRow[column],
secondRow[column],
firstWeight,
secondWeight,
roundBits,
roundOffset,
maximum);
}
}
}
} }

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

@ -714,16 +714,36 @@ internal sealed class Av1BlockDecoder : IDisposable
Span<ushort> highBitDepthDestination = MemoryMarshal.Cast<short, ushort>( Span<ushort> highBitDepthDestination = MemoryMarshal.Cast<short, ushort>(
highBitDepthBlockReconstructionBuffer[reconstructionStride..]); highBitDepthBlockReconstructionBuffer[reconstructionStride..]);
Av1CompoundIntermediateAveragePredictor.AverageIntermediate( if (modeInfo.CompoundType == Av1CompoundType.DistanceWeighted)
highBitDepthDestination, {
reconstructionStride, // Distance weighting must consume the no-round intermediates. Equal-averaging the
first, // already filtered references loses the decoded display-distance contribution.
predictionWidth, Av1CompoundIntermediateDistanceWeightedPredictor.DistanceWeightedIntermediate(
highBitDepthSecondPrediction, highBitDepthDestination,
predictionWidth, reconstructionStride,
predictionWidth, first,
predictionHeight, predictionWidth,
this.frameBuffer.BitDepth.GetBitCount()); highBitDepthSecondPrediction,
predictionWidth,
predictionWidth,
predictionHeight,
firstCompoundWeight,
secondCompoundWeight,
this.frameBuffer.BitDepth.GetBitCount());
}
else
{
Av1CompoundIntermediateAveragePredictor.AverageIntermediate(
highBitDepthDestination,
reconstructionStride,
first,
predictionWidth,
highBitDepthSecondPrediction,
predictionWidth,
predictionWidth,
predictionHeight,
this.frameBuffer.BitDepth.GetBitCount());
}
} }
else else
{ {

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

@ -132,6 +132,15 @@ public class Av1CompoundBlockDecoderTests
ValidateSubpixelHighBitDepthEqualAverageCompoundPrediction, ValidateSubpixelHighBitDepthEqualAverageCompoundPrediction,
CompoundPredictionConfigurations); CompoundPredictionConfigurations);
/// <summary>
/// Verifies that high-bit-depth subpixel predictors retain no-round precision until distance weighting.
/// </summary>
[Fact]
public void DecodeBlockReconstructsSubpixelHighBitDepthDistanceWeightedCompoundPrediction()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(
ValidateSubpixelHighBitDepthDistanceWeightedCompoundPrediction,
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>
@ -649,7 +658,18 @@ public class Av1CompoundBlockDecoderTests
{ {
foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.TenBit, Av1BitDepth.TwelveBit }) foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.TenBit, Av1BitDepth.TwelveBit })
{ {
ValidateSubpixelHighBitDepthEqualAverageCompoundPredictionAtBitDepth(bitDepth); ValidateSubpixelHighBitDepthCompoundPredictionAtBitDepth(bitDepth, Av1CompoundType.Average);
}
}
/// <summary>
/// Reconstructs the high-bit-depth subpixel distance-weighted regression at every supported source precision.
/// </summary>
private static void ValidateSubpixelHighBitDepthDistanceWeightedCompoundPrediction()
{
foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.TenBit, Av1BitDepth.TwelveBit })
{
ValidateSubpixelHighBitDepthCompoundPredictionAtBitDepth(bitDepth, Av1CompoundType.DistanceWeighted);
} }
} }
@ -657,15 +677,23 @@ public class Av1CompoundBlockDecoderTests
/// Reconstructs one high-bit-depth half-sample compound block and compares it with the scalar no-round pipeline. /// Reconstructs one high-bit-depth half-sample compound block and compares it with the scalar no-round pipeline.
/// </summary> /// </summary>
/// <param name="bitDepth">The native sample depth.</param> /// <param name="bitDepth">The native sample depth.</param>
private static void ValidateSubpixelHighBitDepthEqualAverageCompoundPredictionAtBitDepth(Av1BitDepth bitDepth) /// <param name="compoundType">The final compound operation.</param>
private static void ValidateSubpixelHighBitDepthCompoundPredictionAtBitDepth(
Av1BitDepth bitDepth,
Av1CompoundType compoundType)
{ {
const int frameSize = 32; const int frameSize = 32;
const int blockOrigin = 8; const int blockOrigin = 8;
const int blockSize = 8; const int blockSize = 8;
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth, frameSize); ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth, frameSize);
sequenceHeader.OrderHintInfo.EnableOrderHint = true;
sequenceHeader.OrderHintInfo.OrderHintBits = 5;
ObuFrameHeader frameHeader = CreateFrameHeader(frameSize); ObuFrameHeader frameHeader = CreateFrameHeader(frameSize);
frameHeader.OrderHint = 10;
frameHeader.GetReferenceFrameIndices()[0] = 0; frameHeader.GetReferenceFrameIndices()[0] = 0;
frameHeader.GetReferenceFrameIndices()[1] = 1; frameHeader.GetReferenceFrameIndices()[1] = 1;
frameHeader.GetReferenceOrderHints()[0] = 9;
frameHeader.GetReferenceOrderHints()[1] = 5;
using Av1ReferenceFrameStore referenceFrames = new(); using Av1ReferenceFrameStore referenceFrames = new();
Assert.True(referenceFrames.Commit( Assert.True(referenceFrames.Commit(
@ -682,8 +710,8 @@ public class Av1CompoundBlockDecoderTests
{ {
Skip = true, Skip = true,
YMode = Av1PredictionMode.NearestNearestMotionVector, YMode = Av1PredictionMode.NearestNearestMotionVector,
CompoundIndex = true, CompoundIndex = compoundType != Av1CompoundType.DistanceWeighted,
CompoundType = Av1CompoundType.Average, CompoundType = compoundType,
}; };
modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last;
@ -735,18 +763,46 @@ public class Av1CompoundBlockDecoderTests
} }
ushort[] expected = new ushort[blockSize * blockSize]; ushort[] expected = new ushort[blockSize * blockSize];
Av1CompoundIntermediateAveragePredictor.AverageIntermediate( if (compoundType == Av1CompoundType.DistanceWeighted)
expected, {
blockSize, Av1CompoundDistanceWeights.Derive(
expectedFirst, sequenceHeader.OrderHintInfo,
blockSize, frameHeader,
expectedSecond, modeInfo.ReferenceFrames[0],
blockSize, modeInfo.ReferenceFrames[1],
blockSize, out int firstWeight,
blockSize, out int secondWeight);
bitDepth.GetBitCount());
Av1CompoundIntermediateDistanceWeightedPredictor.DistanceWeightedIntermediate(
Assert.Equal((ushort)60, expected[0]); expected,
blockSize,
expectedFirst,
blockSize,
expectedSecond,
blockSize,
blockSize,
blockSize,
firstWeight,
secondWeight,
bitDepth.GetBitCount());
Assert.NotEqual((ushort)60, expected[0]);
}
else
{
Av1CompoundIntermediateAveragePredictor.AverageIntermediate(
expected,
blockSize,
expectedFirst,
blockSize,
expectedSecond,
blockSize,
blockSize,
blockSize,
bitDepth.GetBitCount());
Assert.Equal((ushort)60, expected[0]);
}
using Av1FrameBuffer<byte> frameBuffer = new( using Av1FrameBuffer<byte> frameBuffer = new(
Configuration.Default, Configuration.Default,

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities;
@ -34,14 +35,62 @@ public class Av1CompoundInterPredictorTests
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthAverage, PredictorConfigurations); => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthAverage, PredictorConfigurations);
/// <summary> /// <summary>
/// Verifies 10/12-bit no-round prediction and final equal averaging across every intrinsic width. /// Verifies 10/12-bit no-round prediction and compound finalization across every intrinsic width.
/// </summary> /// </summary>
[Fact] [Fact]
public void HighBitDepthIntermediateAverageMatchesIndependentOracleAcrossIntrinsicWidths() public void HighBitDepthCompoundIntermediatesMatchIndependentOracleAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature( => FeatureTestRunner.RunWithHwIntrinsicsFeature(
ValidateHighBitDepthIntermediateAverage, ValidateHighBitDepthCompoundIntermediates,
PredictorConfigurations); PredictorConfigurations);
/// <summary>
/// Verifies every current libaom display-distance quantization class in both temporal directions.
/// </summary>
/// <param name="firstOrderHint">The first reference order hint.</param>
/// <param name="secondOrderHint">The second reference order hint.</param>
/// <param name="expectedFirstWeight">The expected first predictor weight.</param>
/// <param name="expectedSecondWeight">The expected second predictor weight.</param>
[Theory]
[InlineData(13, 20, 9, 7)]
[InlineData(15, 18, 11, 5)]
[InlineData(15, 19, 12, 4)]
[InlineData(15, 20, 13, 3)]
[InlineData(12, 19, 7, 9)]
[InlineData(14, 17, 5, 11)]
[InlineData(13, 17, 4, 12)]
[InlineData(12, 17, 3, 13)]
[InlineData(12, 16, 3, 13)]
[InlineData(16, 20, 13, 3)]
public void DistanceWeightsMatchCurrentLibaomQuantization(
int firstOrderHint,
int secondOrderHint,
int expectedFirstWeight,
int expectedSecondWeight)
{
ObuOrderHintInfo orderHintInfo = new()
{
EnableOrderHint = true,
OrderHintBits = 5,
};
ObuFrameHeader frameHeader = new() { OrderHint = 16 };
frameHeader.GetReferenceFrameIndices()[0] = 0;
frameHeader.GetReferenceFrameIndices()[1] = 1;
frameHeader.GetReferenceOrderHints()[0] = (uint)firstOrderHint;
frameHeader.GetReferenceOrderHints()[1] = (uint)secondOrderHint;
Av1CompoundDistanceWeights.Derive(
orderHintInfo,
frameHeader,
Av1ReferenceFrameType.Last,
Av1ReferenceFrameType.Last2,
out int firstWeight,
out int secondWeight);
Assert.Equal(expectedFirstWeight, firstWeight);
Assert.Equal(expectedSecondWeight, secondWeight);
}
/// <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>
@ -294,7 +343,7 @@ public class Av1CompoundInterPredictorTests
/// <summary> /// <summary>
/// Applies the high-bit-depth no-round convolution equations independently of the production operators. /// Applies the high-bit-depth no-round convolution equations independently of the production operators.
/// </summary> /// </summary>
private static void ValidateHighBitDepthIntermediateAverage() private static void ValidateHighBitDepthCompoundIntermediates()
{ {
ReadOnlySpan<int> widths = [9, 17, 33, 65]; ReadOnlySpan<int> widths = [9, 17, 33, 65];
ReadOnlySpan<(int Horizontal, int Vertical)> phases = ReadOnlySpan<(int Horizontal, int Vertical)> phases =
@ -503,6 +552,47 @@ public class Av1CompoundInterPredictorTests
bitDepth); bitDepth);
Assert.Equal(expectedDestination, actualDestination); Assert.Equal(expectedDestination, actualDestination);
ReadOnlySpan<int> distanceWeights = [9, 7, 11, 5, 12, 4, 13, 3];
for (int weightIndex = 0; weightIndex < distanceWeights.Length; weightIndex += 2)
{
ushort[] expectedWeighted = new ushort[destinationStride * height];
ushort[] actualWeighted = new ushort[destinationStride * height];
expectedWeighted.AsSpan().Fill(0xA5A5);
actualWeighted.AsSpan().Fill(0xA5A5);
int firstWeight = distanceWeights[weightIndex];
int secondWeight = distanceWeights[weightIndex + 1];
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
int intermediateIndex = (row * intermediateStride) + column;
int result = ((expectedFirst[intermediateIndex] * firstWeight) +
(expectedSecond[intermediateIndex] * secondWeight)) >> 4;
result -= roundOffset;
result = (result + (1 << (roundBits - 1))) >> roundBits;
expectedWeighted[(row * destinationStride) + column] =
(ushort)Math.Clamp(result, 0, maximum);
}
}
Av1CompoundIntermediateDistanceWeightedPredictor.DistanceWeightedIntermediate(
actualWeighted,
destinationStride,
actualFirst,
intermediateStride,
actualSecond,
intermediateStride,
width,
height,
firstWeight,
secondWeight,
bitDepth);
Assert.Equal(expectedWeighted, actualWeighted);
}
} }
} }
} }

16
tests/Images/Input/Heif/Av1/Conformance/README.md

@ -144,11 +144,17 @@ with row threading disabled. Current `aomdec` produced all 19 YUV444 frames. The
native samples have SHA-256 `E8B776C2751DC30CA838931A4B74535FC6E681179568A1278747A38CFF2E5BFA` native samples have SHA-256 `E8B776C2751DC30CA838931A4B74535FC6E681179568A1278747A38CFF2E5BFA`
and match the retained Y4M with zero differing samples. and match the retained Y4M with zero differing samples.
The production tests independently require their decoded mode states. The inter-intra input must exercise The distance-weighted fixture's 5,372-byte AV1 `mdat` payload was decoded under the same current-libaom
both smooth and wedge inter-intra prediction, decode all preceding samples, compare the final native Y, conditions. Current `aomdec` produced all 19 YUV444 frames. The final frame's 19,200 native samples have
Cb, and Cr planes exactly, compare final RGBA presentation through ImageSharp's established SHA-256 `E8CAA650F1571C5B9CACAF8C06E1DDF5F5D2ED35F65F1C34377076C573425899` and match the retained
reference-output API, and repeat reconstruction with constrained tracked allocation. The retained PNG is Y4M with zero differing samples.
presentation evidence only and is not an AV1 reconstruction reference.
The production tests independently require their decoded mode states. The distance-weighted input must
exercise distance-weighted compound prediction and the inter-intra input must exercise both smooth and
wedge inter-intra prediction. The tests decode all preceding samples, compare final native Y, Cb, and Cr
planes exactly, compare final RGBA presentation through ImageSharp's established reference-output API,
and repeat reconstruction with constrained tracked allocation. The retained PNG files are presentation
evidence only and are not AV1 reconstruction references.
## Overlapping motion-compensation fixture ## Overlapping motion-compensation fixture

Loading…
Cancel
Save