Browse Source

Complete AV1 scaled-reference decoding checkpoint

pull/2633/head
James Jackson-South 3 days ago
parent
commit
658a9cd1b6
  1. 45
      HEIF_IMPLEMENTATION_PLAN.md
  2. 111
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.Operator.cs
  3. 205
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.cs
  4. 130
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs
  5. 287
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs
  6. 8
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs
  7. 218
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ScaledInterPredictorTests.cs
  8. 0
      tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeScaledReferenceMatchesCurrentLibaomReferences_Rgba32_libavif-webp-logo-scaled-reference.png
  9. 0
      tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectedProgressiveSpatialLayerMatchesCurrentLibaomReferences_Rgba32_libavif-webp-logo-scaled-reference-lsel0.png
  10. 33
      tests/Images/Input/Heif/Av1/Conformance/README.md

45
HEIF_IMPLEMENTATION_PLAN.md

@ -30,9 +30,8 @@ Reference checkout evidence on 2026-08-31:
Reconciled with the worktree on 2026-08-31. Reconciled with the worktree on 2026-08-31.
- [~] The bounded container reader, still-image path, sequence parser, AV1 decoder, color pipeline, presentation pipeline, and broad AV1 test suite exist locally. - [~] The bounded container reader, still-image path, sequence parser, AV1 decoder, color pipeline, presentation pipeline, and broad AV1 test suite exist locally.
- [~] The inter-frame decoder has verified checkpoints through OBMC. Scaled references, local warped - [~] The inter-frame decoder has verified checkpoints through scaled-reference prediction. Local warped
motion, and global motion exist locally but remain open until their ordered checkpoints below are motion and global motion exist locally but remain open until their ordered checkpoints below are completed.
completed.
- [~] Loop filtering, CDEF, super-resolution, restoration, film grain, layered presentation, alpha composition, and color conversion exist locally. Shared-source cleanup changed the current tree, so final production-path verification is open. - [~] Loop filtering, CDEF, super-resolution, restoration, film grain, layered presentation, alpha composition, and color conversion exist locally. Shared-source cleanup changed the current tree, so final production-path verification is open.
- [~] AV1 writer primitives, forward transforms, symbol encoding, and tile-writing source exist locally, but they are not connected to the public encoder. - [~] AV1 writer primitives, forward transforms, symbol encoding, and tile-writing source exist locally, but they are not connected to the public encoder.
- [ ] The public AV1 encoder is not implemented. HeifEncoderCore.Encode throws NotSupportedException when AV1 is selected. - [ ] The public AV1 encoder is not implemented. HeifEncoderCore.Encode throws NotSupportedException when AV1 is selected.
@ -184,8 +183,8 @@ The single-reference syntax, buffer, reconstruction, and ownership foundation is
- [x] Wedge compound prediction. - [x] Wedge compound prediction.
- [x] Difference-weighted compound prediction. - [x] Difference-weighted compound prediction.
- [x] OBMC. - [x] OBMC.
- [~] Scaled-reference prediction. Next item. - [x] Scaled-reference prediction.
- [~] Local warped prediction. - [~] Local warped prediction. Next item.
- [~] Non-translational global prediction. - [~] Non-translational global prediction.
- [~] Inter deblocking decisions and reference/mode deltas. - [~] Inter deblocking decisions and reference/mode deltas.
@ -394,6 +393,42 @@ Verified OBMC checkpoint evidence on 2026-08-31:
- [x] The focused Release checkpoint set passes 18/18 on net10.0 and 18/18 on net11.0, with zero - [x] The focused Release checkpoint set passes 18/18 on net10.0 and 18/18 on net11.0, with zero
failures or skips. Scoped analyzer and whitespace verification pass for every changed C# file. failures or skips. Scoped analyzer and whitespace verification pass for every changed C# file.
Roslynk reports zero compiler errors, `git diff --check` passes, and `.gitattributes` is unchanged. Roslynk reports zero compiler errors, `git diff --check` passes, and `.gitattributes` is unchanged.
- [x] The completed checkpoint was committed as `7e7e3cbe6438d63926b31d966795d2652e221939`
with author and committer `James Jackson-South <james_south@hotmail.com>`.
Verified scaled-reference checkpoint evidence on 2026-08-31:
- [x] Audited reference-size validation and variable-scale coordinates, filters, edge extension, convolution
rounding, and compound intermediates against current libaom `av1/common/scale.c`,
`av1/decoder/decodeframe.c`, and `av1/common/convolve.c`. The frame boundary accepts the same
half-to-sixteen-times dimension range and requires at least one compatible selected reference.
- [x] Corrected the production scaled-compound branch. It previously rounded each scaled reference into
native pixels before blending; current libaom retains both `CONV_BUF_TYPE` values with
`COMPOUND_ROUND1_BITS` equal to seven and performs one final rounding after the selected compound blend.
- [x] Kept native-pixel and compound output in the existing `Av1ScaledInterPredictor` traversal with
semantic `NativeOperator` and `CompoundOperator` output contracts. The closed generic traversal shares
variable-phase arithmetic across byte and ushort sources, dispatches Vector512, Vector256, Vector128,
then scalar, and adds no per-block allocation or copy.
- [x] Added independent FeatureTestRunner oracles for native and no-round compound output across 8, 10,
and 12 bits, variable phases, all interpolation families, reduced kernels, vector tails, and destination
padding. A complete `Av1BlockDecoder.DecodeBlock()` regression covers scaled compound prediction across
all, AVX-512-disabled, AVX-disabled, and scalar configurations and proves the vector differs from an
incorrectly early-rounded blend.
- [x] Decoded the 2,195-byte layered payload with refreshed current libaom `aomdec`, using one thread,
row threading disabled, all layers selected, and raw 8-bit output. The 40x40 YUV444 base and 80x80
YUV444 dependent frames total 24,000 samples with SHA-256
`DD219E41B52C6C9343A92CD0A2D451DF57B73B25F10124811675B4CB2F8D666F`; both match their retained
native references with zero differing samples.
- [x] The production tests compare both native frames exactly, compare selected-layer and final RGBA
presentation through ImageSharp's established reference-output API, and repeat both paths with a
1,024-byte constrained tracked allocator whose allocations have balanced exactly-once returns.
- [x] Renamed the two stale pinned-reference tests and their contract-derived PNGs together. Their Git blob
identifiers remain unchanged, and their SHA-256 values remain
`DC4C6DBE6BD92C5FCE1E3E23700AFA603EF04ED02EDD336213EBBA1E3BD84BA0` and
`678C5E5D4650EA6F0C590302E7DB9E3C6608851BC577453DA4A6837BDB4D3AF3`.
- [x] The focused Release checkpoint set passes 10/10 on net10.0 and 10/10 on net11.0, with zero failures
or skips. Scoped analyzer and whitespace verification pass for every changed C# file. Roslynk reports
zero compiler errors, `git diff --check` passes, and `.gitattributes` is unchanged.
For every item: For every item:

111
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.Operator.cs

@ -14,9 +14,9 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
internal static partial class Av1ScaledInterPredictor internal static partial class Av1ScaledInterPredictor
{ {
/// <summary> /// <summary>
/// Defines variable-phase scaled prediction for one native sample storage type. /// Defines source and convolution arithmetic shared by variable-phase scaled prediction.
/// </summary> /// </summary>
private interface IAv1ScaledPredictionOperator private interface IAv1ScaledArithmeticOperator
{ {
/// <summary> /// <summary>
/// Loads one native source sample as a signed accumulator value. /// Loads one native source sample as a signed accumulator value.
@ -136,9 +136,30 @@ internal static partial class Av1ScaledInterPredictor
Vector512<int> initial, Vector512<int> initial,
out Vector512<int> result0, out Vector512<int> result0,
out Vector512<int> result1); out Vector512<int> result1);
}
/// <summary>
/// Defines the output domain produced by variable-phase scaled prediction.
/// </summary>
private interface IAv1ScaledPredictionOperator
{
/// <summary> /// <summary>
/// Clips and stores one completed prediction. /// Gets the vertical convolution shift for the selected output domain.
/// </summary>
/// <param name="horizontalRound">The horizontal convolution shift.</param>
/// <returns>The vertical convolution shift.</returns>
public static abstract int GetVerticalRound(int horizontalRound);
/// <summary>
/// Gets the bias removed after vertical convolution for the selected output domain.
/// </summary>
/// <param name="offsetBits">The biased intermediate precision.</param>
/// <param name="verticalRound">The vertical convolution shift.</param>
/// <returns>The bias removed before storing the result.</returns>
public static abstract int GetRoundOffset(int offsetBits, int verticalRound);
/// <summary>
/// Stores one completed prediction in the selected output domain.
/// </summary> /// </summary>
/// <typeparam name="T">The native sample storage type.</typeparam> /// <typeparam name="T">The native sample storage type.</typeparam>
/// <param name="destination">The first destination sample.</param> /// <param name="destination">The first destination sample.</param>
@ -149,7 +170,7 @@ internal static partial class Av1ScaledInterPredictor
where T : unmanaged; where T : unmanaged;
/// <summary> /// <summary>
/// Clips and stores eight completed predictions. /// Stores eight completed predictions in the selected output domain.
/// </summary> /// </summary>
/// <typeparam name="T">The native sample storage type.</typeparam> /// <typeparam name="T">The native sample storage type.</typeparam>
/// <param name="destination">The first destination sample.</param> /// <param name="destination">The first destination sample.</param>
@ -161,7 +182,7 @@ internal static partial class Av1ScaledInterPredictor
where T : unmanaged; where T : unmanaged;
/// <summary> /// <summary>
/// Clips and stores sixteen completed predictions. /// Stores sixteen completed predictions in the selected output domain.
/// </summary> /// </summary>
/// <typeparam name="T">The native sample storage type.</typeparam> /// <typeparam name="T">The native sample storage type.</typeparam>
/// <param name="destination">The first destination sample.</param> /// <param name="destination">The first destination sample.</param>
@ -173,7 +194,7 @@ internal static partial class Av1ScaledInterPredictor
where T : unmanaged; where T : unmanaged;
/// <summary> /// <summary>
/// Clips and stores thirty-two completed predictions. /// Stores thirty-two completed predictions in the selected output domain.
/// </summary> /// </summary>
/// <typeparam name="T">The native sample storage type.</typeparam> /// <typeparam name="T">The native sample storage type.</typeparam>
/// <param name="destination">The first destination sample.</param> /// <param name="destination">The first destination sample.</param>
@ -186,10 +207,20 @@ internal static partial class Av1ScaledInterPredictor
} }
/// <summary> /// <summary>
/// Implements variable-phase scaled prediction for scalar and SIMD lane groups. /// Produces native-pixel scaled prediction for scalar and SIMD lane groups.
/// </summary> /// </summary>
private readonly struct ScaledOperator : IAv1ScaledPredictionOperator private readonly struct NativeOperator : IAv1ScaledArithmeticOperator, IAv1ScaledPredictionOperator
{ {
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetVerticalRound(int horizontalRound)
=> (2 * FilterBits) - horizontalRound;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetRoundOffset(int offsetBits, int verticalRound)
=> (1 << (offsetBits - verticalRound)) + (1 << (offsetBits - verticalRound - 1));
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Load<T>(ref T source, int index) public static int Load<T>(ref T source, int index)
@ -361,4 +392,68 @@ internal static partial class Av1ScaledInterPredictor
.StoreUnsafe(ref Unsafe.As<T, ushort>(ref destination), (nuint)index); .StoreUnsafe(ref Unsafe.As<T, ushort>(ref destination), (nuint)index);
} }
} }
/// <summary>
/// Produces no-round compound intermediates for scalar and SIMD lane groups.
/// </summary>
private readonly struct CompoundOperator : IAv1ScaledPredictionOperator
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetVerticalRound(int horizontalRound)
=> Av1CompoundInterPredictor.CompoundRound1Bits;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetRoundOffset(int offsetBits, int verticalRound) => 0;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Store<T>(ref T destination, int index, int value, int bitDepth)
where T : unmanaged
{
// Compound entry points close T as ushort. Their no-round values retain the positive convolution bias,
// so storing the normative unsigned intermediate needs neither pixel clipping nor a storage-type branch.
Unsafe.Add(ref Unsafe.As<T, ushort>(ref destination), index) = (ushort)value;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Store<T>(
ref T destination,
int index,
Vector128<int> result0,
Vector128<int> result1,
int bitDepth)
where T : unmanaged
=> Av1IntraPredictorBase.Narrow(result0, result1)
.AsUInt16()
.StoreUnsafe(ref Unsafe.As<T, ushort>(ref destination), (nuint)index);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Store<T>(
ref T destination,
int index,
Vector256<int> result0,
Vector256<int> result1,
int bitDepth)
where T : unmanaged
=> Av1IntraPredictorBase.Narrow(result0, result1)
.AsUInt16()
.StoreUnsafe(ref Unsafe.As<T, ushort>(ref destination), (nuint)index);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Store<T>(
ref T destination,
int index,
Vector512<int> result0,
Vector512<int> result1,
int bitDepth)
where T : unmanaged
=> Av1IntraPredictorBase.Narrow(result0, result1)
.AsUInt16()
.StoreUnsafe(ref Unsafe.As<T, ushort>(ref destination), (nuint)index);
}
} }

205
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.cs

@ -47,7 +47,7 @@ internal static partial class Av1ScaledInterPredictor
int verticalPhase, int verticalPhase,
int verticalStep, int verticalStep,
Span<short> scratch) Span<short> scratch)
=> DispatchScaled<byte, ScaledOperator>( => DispatchScaled<byte, byte, NativeOperator>(
source, source,
sourceStride, sourceStride,
sourceOrigin, sourceOrigin,
@ -83,7 +83,78 @@ internal static partial class Av1ScaledInterPredictor
int verticalStep, int verticalStep,
int bitDepth, int bitDepth,
Span<short> scratch) Span<short> scratch)
=> DispatchScaled<ushort, ScaledOperator>( => DispatchScaled<ushort, ushort, NativeOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalFilter,
verticalFilter,
horizontalPhase,
horizontalStep,
verticalPhase,
verticalStep,
bitDepth,
scratch);
/// <summary>
/// Reconstructs an 8-bit scaled predictor into the no-round compound intermediate domain.
/// </summary>
public static void PredictScaledCompound(
ReadOnlySpan<byte> source,
int sourceStride,
int sourceOrigin,
Span<ushort> destination,
int destinationStride,
int width,
int height,
Av1InterpolationFilter horizontalFilter,
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int horizontalStep,
int verticalPhase,
int verticalStep,
Span<short> scratch)
=> DispatchScaled<byte, ushort, CompoundOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalFilter,
verticalFilter,
horizontalPhase,
horizontalStep,
verticalPhase,
verticalStep,
8,
scratch);
/// <summary>
/// Reconstructs an 8-, 10-, or 12-bit scaled predictor into the no-round compound intermediate domain.
/// </summary>
public static void PredictScaledCompound(
ReadOnlySpan<ushort> source,
int sourceStride,
int sourceOrigin,
Span<ushort> destination,
int destinationStride,
int width,
int height,
Av1InterpolationFilter horizontalFilter,
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int horizontalStep,
int verticalPhase,
int verticalStep,
int bitDepth,
Span<short> scratch)
=> DispatchScaled<ushort, ushort, CompoundOperator>(
source, source,
sourceStride, sourceStride,
sourceOrigin, sourceOrigin,
@ -103,11 +174,11 @@ internal static partial class Av1ScaledInterPredictor
/// <summary> /// <summary>
/// Selects the horizontal filter family for scaled prediction. /// Selects the horizontal filter family for scaled prediction.
/// </summary> /// </summary>
private static void DispatchScaled<T, TSample>( private static void DispatchScaled<TSource, TDestination, TOperator>(
ReadOnlySpan<T> source, ReadOnlySpan<TSource> source,
int sourceStride, int sourceStride,
int sourceOrigin, int sourceOrigin,
Span<T> destination, Span<TDestination> destination,
int destinationStride, int destinationStride,
int width, int width,
int height, int height,
@ -119,13 +190,14 @@ internal static partial class Av1ScaledInterPredictor
int verticalStep, int verticalStep,
int bitDepth, int bitDepth,
Span<short> scratch) Span<short> scratch)
where T : unmanaged where TSource : unmanaged
where TSample : struct, IAv1ScaledPredictionOperator where TDestination : unmanaged
where TOperator : struct, IAv1ScaledPredictionOperator
{ {
switch (horizontalFilter) switch (horizontalFilter)
{ {
case Av1InterpolationFilter.Regular: case Av1InterpolationFilter.Regular:
DispatchScaledVertical<T, TSample, RegularOperator>( DispatchScaledVertical<TSource, TDestination, TOperator, RegularOperator>(
source, source,
sourceStride, sourceStride,
sourceOrigin, sourceOrigin,
@ -143,7 +215,7 @@ internal static partial class Av1ScaledInterPredictor
break; break;
case Av1InterpolationFilter.Smooth: case Av1InterpolationFilter.Smooth:
DispatchScaledVertical<T, TSample, SmoothOperator>( DispatchScaledVertical<TSource, TDestination, TOperator, SmoothOperator>(
source, source,
sourceStride, sourceStride,
sourceOrigin, sourceOrigin,
@ -161,7 +233,7 @@ internal static partial class Av1ScaledInterPredictor
break; break;
case Av1InterpolationFilter.Sharp: case Av1InterpolationFilter.Sharp:
DispatchScaledVertical<T, TSample, SharpOperator>( DispatchScaledVertical<TSource, TDestination, TOperator, SharpOperator>(
source, source,
sourceStride, sourceStride,
sourceOrigin, sourceOrigin,
@ -179,7 +251,7 @@ internal static partial class Av1ScaledInterPredictor
break; break;
default: default:
DispatchScaledVertical<T, TSample, BilinearOperator>( DispatchScaledVertical<TSource, TDestination, TOperator, BilinearOperator>(
source, source,
sourceStride, sourceStride,
sourceOrigin, sourceOrigin,
@ -202,11 +274,11 @@ internal static partial class Av1ScaledInterPredictor
/// <summary> /// <summary>
/// Selects the vertical filter family for a closed horizontal scaled-prediction operator. /// Selects the vertical filter family for a closed horizontal scaled-prediction operator.
/// </summary> /// </summary>
private static void DispatchScaledVertical<T, TSample, THorizontal>( private static void DispatchScaledVertical<TSource, TDestination, TOperator, THorizontal>(
ReadOnlySpan<T> source, ReadOnlySpan<TSource> source,
int sourceStride, int sourceStride,
int sourceOrigin, int sourceOrigin,
Span<T> destination, Span<TDestination> destination,
int destinationStride, int destinationStride,
int width, int width,
int height, int height,
@ -217,14 +289,15 @@ internal static partial class Av1ScaledInterPredictor
int verticalStep, int verticalStep,
int bitDepth, int bitDepth,
Span<short> scratch) Span<short> scratch)
where T : unmanaged where TSource : unmanaged
where TSample : struct, IAv1ScaledPredictionOperator where TDestination : unmanaged
where TOperator : struct, IAv1ScaledPredictionOperator
where THorizontal : struct, IAv1InterPredictorOperator where THorizontal : struct, IAv1InterPredictorOperator
{ {
switch (verticalFilter) switch (verticalFilter)
{ {
case Av1InterpolationFilter.Regular: case Av1InterpolationFilter.Regular:
PredictScaled<T, TSample, THorizontal, RegularOperator>( PredictScaled<TSource, TDestination, TOperator, THorizontal, RegularOperator>(
source, source,
sourceStride, sourceStride,
sourceOrigin, sourceOrigin,
@ -241,7 +314,7 @@ internal static partial class Av1ScaledInterPredictor
break; break;
case Av1InterpolationFilter.Smooth: case Av1InterpolationFilter.Smooth:
PredictScaled<T, TSample, THorizontal, SmoothOperator>( PredictScaled<TSource, TDestination, TOperator, THorizontal, SmoothOperator>(
source, source,
sourceStride, sourceStride,
sourceOrigin, sourceOrigin,
@ -258,7 +331,7 @@ internal static partial class Av1ScaledInterPredictor
break; break;
case Av1InterpolationFilter.Sharp: case Av1InterpolationFilter.Sharp:
PredictScaled<T, TSample, THorizontal, SharpOperator>( PredictScaled<TSource, TDestination, TOperator, THorizontal, SharpOperator>(
source, source,
sourceStride, sourceStride,
sourceOrigin, sourceOrigin,
@ -275,7 +348,7 @@ internal static partial class Av1ScaledInterPredictor
break; break;
default: default:
PredictScaled<T, TSample, THorizontal, BilinearOperator>( PredictScaled<TSource, TDestination, TOperator, THorizontal, BilinearOperator>(
source, source,
sourceStride, sourceStride,
sourceOrigin, sourceOrigin,
@ -297,11 +370,11 @@ internal static partial class Av1ScaledInterPredictor
/// <summary> /// <summary>
/// Applies variable-phase horizontal filtering followed by variable-phase vertical filtering. /// Applies variable-phase horizontal filtering followed by variable-phase vertical filtering.
/// </summary> /// </summary>
private static void PredictScaled<T, TSample, THorizontal, TVertical>( private static void PredictScaled<TSource, TDestination, TOperator, THorizontal, TVertical>(
ReadOnlySpan<T> source, ReadOnlySpan<TSource> source,
int sourceStride, int sourceStride,
int sourceOrigin, int sourceOrigin,
Span<T> destination, Span<TDestination> destination,
int destinationStride, int destinationStride,
int width, int width,
int height, int height,
@ -311,13 +384,14 @@ internal static partial class Av1ScaledInterPredictor
int verticalStep, int verticalStep,
int bitDepth, int bitDepth,
Span<short> scratch) Span<short> scratch)
where T : unmanaged where TSource : unmanaged
where TSample : struct, IAv1ScaledPredictionOperator where TDestination : unmanaged
where TOperator : struct, IAv1ScaledPredictionOperator
where THorizontal : struct, IAv1InterPredictorOperator where THorizontal : struct, IAv1InterPredictorOperator
where TVertical : struct, IAv1InterPredictorOperator where TVertical : struct, IAv1InterPredictorOperator
{ {
ref T sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); ref TSource sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin);
ref T destinationBase = ref MemoryMarshal.GetReference(destination); ref TDestination destinationBase = ref MemoryMarshal.GetReference(destination);
ref short scratchBase = ref MemoryMarshal.GetReference(scratch); ref short scratchBase = ref MemoryMarshal.GetReference(scratch);
int scratchStride = Math.Max(width, Vector128<short>.Count); int scratchStride = Math.Max(width, Vector128<short>.Count);
int intermediateHeight = ((((height - 1) * verticalStep) + verticalPhase) >> Av1ReferenceScale.SubpixelBits) + FilterCoefficientCount; int intermediateHeight = ((((height - 1) * verticalStep) + verticalPhase) >> Av1ReferenceScale.SubpixelBits) + FilterCoefficientCount;
@ -331,7 +405,7 @@ internal static partial class Av1ScaledInterPredictor
// vectors gather those independent positions into one multiply-accumulate chain without allocating an index map. // vectors gather those independent positions into one multiply-accumulate chain without allocating an index map.
for (int row = 0; row < intermediateHeight; row++) for (int row = 0; row < intermediateHeight; row++)
{ {
ref T sourceRow = ref Unsafe.Add(ref sourceBase, (row - 3) * sourceStride); ref TSource sourceRow = ref Unsafe.Add(ref sourceBase, (row - 3) * sourceStride);
ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride);
int column = 0; int column = 0;
@ -340,7 +414,7 @@ internal static partial class Av1ScaledInterPredictor
int oneVectorFromEnd = width - Vector512<int>.Count; int oneVectorFromEnd = width - Vector512<int>.Count;
for (; column <= oneVectorFromEnd; column += Vector512<int>.Count) for (; column <= oneVectorFromEnd; column += Vector512<int>.Count)
{ {
Vector512<int> result = FilterScaledHorizontalVector512<T, TSample, THorizontal>( Vector512<int> result = FilterScaledHorizontalVector512<TSource, THorizontal>(
ref sourceRow, ref sourceRow,
horizontalPhase, horizontalPhase,
horizontalStep, horizontalStep,
@ -360,7 +434,7 @@ internal static partial class Av1ScaledInterPredictor
int oneVectorFromEnd = width - Vector256<int>.Count; int oneVectorFromEnd = width - Vector256<int>.Count;
for (; column <= oneVectorFromEnd; column += Vector256<int>.Count) for (; column <= oneVectorFromEnd; column += Vector256<int>.Count)
{ {
Vector256<int> result = FilterScaledHorizontalVector256<T, TSample, THorizontal>( Vector256<int> result = FilterScaledHorizontalVector256<TSource, THorizontal>(
ref sourceRow, ref sourceRow,
horizontalPhase, horizontalPhase,
horizontalStep, horizontalStep,
@ -379,7 +453,7 @@ internal static partial class Av1ScaledInterPredictor
{ {
for (; column <= width - Vector128<int>.Count; column += Vector128<int>.Count) for (; column <= width - Vector128<int>.Count; column += Vector128<int>.Count)
{ {
Vector128<int> result = FilterScaledHorizontalVector128<T, TSample, THorizontal>( Vector128<int> result = FilterScaledHorizontalVector128<TSource, THorizontal>(
ref sourceRow, ref sourceRow,
horizontalPhase, horizontalPhase,
horizontalStep, horizontalStep,
@ -405,17 +479,17 @@ internal static partial class Av1ScaledInterPredictor
int sum = horizontalBias; int sum = horizontalBias;
for (int tap = 0; tap < FilterCoefficientCount; tap++) for (int tap = 0; tap < FilterCoefficientCount; tap++)
{ {
sum = TSample.MultiplyAdd(sum, TSample.Load(ref sourceRow, sourceColumn + tap), coefficients[tap]); sum = NativeOperator.MultiplyAdd(sum, NativeOperator.Load(ref sourceRow, sourceColumn + tap), coefficients[tap]);
} }
Unsafe.Add(ref scratchRow, column) = (short)RoundPowerOfTwo(sum, round0); Unsafe.Add(ref scratchRow, column) = (short)RoundPowerOfTwo(sum, round0);
} }
} }
int round1 = (2 * FilterBits) - round0; int round1 = TOperator.GetVerticalRound(round0);
int offsetBits = bitDepth + (2 * FilterBits) - round0; int offsetBits = bitDepth + (2 * FilterBits) - round0;
int verticalBias = 1 << offsetBits; int verticalBias = 1 << offsetBits;
int roundOffset = (1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1)); int roundOffset = TOperator.GetRoundOffset(offsetBits, round1);
for (int row = 0; row < height; row++) for (int row = 0; row < height; row++)
{ {
int position = verticalPhase + (row * verticalStep); int position = verticalPhase + (row * verticalStep);
@ -426,7 +500,7 @@ internal static partial class Av1ScaledInterPredictor
ref short scratchRow = ref Unsafe.Add(ref scratchBase, sourceRowIndex * scratchStride); ref short scratchRow = ref Unsafe.Add(ref scratchBase, sourceRowIndex * scratchStride);
ref short coefficientBase = ref MemoryMarshal.GetReference(coefficients); ref short coefficientBase = ref MemoryMarshal.GetReference(coefficients);
ref T destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); ref TDestination destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride);
int column = 0; int column = 0;
if (Vector512.IsHardwareAccelerated) if (Vector512.IsHardwareAccelerated)
@ -436,7 +510,7 @@ internal static partial class Av1ScaledInterPredictor
int oneVectorFromEnd = width - (Vector512<int>.Count * 2); int oneVectorFromEnd = width - (Vector512<int>.Count * 2);
for (; column <= oneVectorFromEnd; column += Vector512<int>.Count * 2) for (; column <= oneVectorFromEnd; column += Vector512<int>.Count * 2)
{ {
TSample.Convolve( NativeOperator.Convolve(
ref scratchRow, ref scratchRow,
scratchStride, scratchStride,
(nuint)column, (nuint)column,
@ -448,7 +522,7 @@ internal static partial class Av1ScaledInterPredictor
result0 = RoundPowerOfTwo(result0, round1) - offset; result0 = RoundPowerOfTwo(result0, round1) - offset;
result1 = RoundPowerOfTwo(result1, round1) - offset; result1 = RoundPowerOfTwo(result1, round1) - offset;
TSample.Store(ref destinationRow, column, result0, result1, bitDepth); TOperator.Store(ref destinationRow, column, result0, result1, bitDepth);
} }
} }
@ -459,7 +533,7 @@ internal static partial class Av1ScaledInterPredictor
int oneVectorFromEnd = width - (Vector256<int>.Count * 2); int oneVectorFromEnd = width - (Vector256<int>.Count * 2);
for (; column <= oneVectorFromEnd; column += Vector256<int>.Count * 2) for (; column <= oneVectorFromEnd; column += Vector256<int>.Count * 2)
{ {
TSample.Convolve( NativeOperator.Convolve(
ref scratchRow, ref scratchRow,
scratchStride, scratchStride,
(nuint)column, (nuint)column,
@ -471,7 +545,7 @@ internal static partial class Av1ScaledInterPredictor
result0 = RoundPowerOfTwo(result0, round1) - offset; result0 = RoundPowerOfTwo(result0, round1) - offset;
result1 = RoundPowerOfTwo(result1, round1) - offset; result1 = RoundPowerOfTwo(result1, round1) - offset;
TSample.Store(ref destinationRow, column, result0, result1, bitDepth); TOperator.Store(ref destinationRow, column, result0, result1, bitDepth);
} }
} }
@ -482,7 +556,7 @@ internal static partial class Av1ScaledInterPredictor
int oneVectorFromEnd = width - (Vector128<int>.Count * 2); int oneVectorFromEnd = width - (Vector128<int>.Count * 2);
for (; column <= oneVectorFromEnd; column += Vector128<int>.Count * 2) for (; column <= oneVectorFromEnd; column += Vector128<int>.Count * 2)
{ {
TSample.Convolve( NativeOperator.Convolve(
ref scratchRow, ref scratchRow,
scratchStride, scratchStride,
(nuint)column, (nuint)column,
@ -494,19 +568,19 @@ internal static partial class Av1ScaledInterPredictor
result0 = RoundPowerOfTwo(result0, round1) - offset; result0 = RoundPowerOfTwo(result0, round1) - offset;
result1 = RoundPowerOfTwo(result1, round1) - offset; result1 = RoundPowerOfTwo(result1, round1) - offset;
TSample.Store(ref destinationRow, column, result0, result1, bitDepth); TOperator.Store(ref destinationRow, column, result0, result1, bitDepth);
} }
} }
for (; column < width; column++) for (; column < width; column++)
{ {
int sum = verticalBias + TSample.Convolve( int sum = verticalBias + NativeOperator.Convolve(
ref Unsafe.Add(ref scratchRow, column), ref Unsafe.Add(ref scratchRow, column),
scratchStride, scratchStride,
ref coefficientBase, ref coefficientBase,
FilterCoefficientCount); FilterCoefficientCount);
TSample.Store( TOperator.Store(
ref destinationRow, ref destinationRow,
column, column,
RoundPowerOfTwo(sum, round1) - roundOffset, RoundPowerOfTwo(sum, round1) - roundOffset,
@ -518,7 +592,7 @@ internal static partial class Av1ScaledInterPredictor
/// <summary> /// <summary>
/// Filters four independently positioned horizontal samples through a closed scaled-prediction operator. /// Filters four independently positioned horizontal samples through a closed scaled-prediction operator.
/// </summary> /// </summary>
private static Vector128<int> FilterScaledHorizontalVector128<T, TOperator, TFilter>( private static Vector128<int> FilterScaledHorizontalVector128<T, TFilter>(
ref T source, ref T source,
int phase, int phase,
int step, int step,
@ -527,15 +601,14 @@ internal static partial class Av1ScaledInterPredictor
int bias, int bias,
int round) int round)
where T : unmanaged where T : unmanaged
where TOperator : struct, IAv1ScaledPredictionOperator
where TFilter : struct, IAv1InterPredictorOperator where TFilter : struct, IAv1InterPredictorOperator
{ {
Vector128<int> result = Vector128.Create(bias); Vector128<int> result = Vector128.Create(bias);
for (int tap = 0; tap < FilterCoefficientCount; tap++) for (int tap = 0; tap < FilterCoefficientCount; tap++)
{ {
result = TOperator.MultiplyAdd( result = NativeOperator.MultiplyAdd(
result, result,
LoadScaledSamplesVector128<T, TOperator>(ref source, phase, step, column, tap), LoadScaledSamplesVector128<T>(ref source, phase, step, column, tap),
LoadScaledCoefficientsVector128<TFilter>(phase, step, column, tap, useReducedFilter)); LoadScaledCoefficientsVector128<TFilter>(phase, step, column, tap, useReducedFilter));
} }
@ -545,7 +618,7 @@ internal static partial class Av1ScaledInterPredictor
/// <summary> /// <summary>
/// Filters eight independently positioned horizontal samples through a closed scaled-prediction operator. /// Filters eight independently positioned horizontal samples through a closed scaled-prediction operator.
/// </summary> /// </summary>
private static Vector256<int> FilterScaledHorizontalVector256<T, TOperator, TFilter>( private static Vector256<int> FilterScaledHorizontalVector256<T, TFilter>(
ref T source, ref T source,
int phase, int phase,
int step, int step,
@ -554,21 +627,20 @@ internal static partial class Av1ScaledInterPredictor
int bias, int bias,
int round) int round)
where T : unmanaged where T : unmanaged
where TOperator : struct, IAv1ScaledPredictionOperator
where TFilter : struct, IAv1InterPredictorOperator where TFilter : struct, IAv1InterPredictorOperator
{ {
Vector256<int> result = Vector256.Create(bias); Vector256<int> result = Vector256.Create(bias);
for (int tap = 0; tap < FilterCoefficientCount; tap++) for (int tap = 0; tap < FilterCoefficientCount; tap++)
{ {
Vector256<int> samples = Vector256.Create( Vector256<int> samples = Vector256.Create(
LoadScaledSamplesVector128<T, TOperator>(ref source, phase, step, column, tap), LoadScaledSamplesVector128<T>(ref source, phase, step, column, tap),
LoadScaledSamplesVector128<T, TOperator>(ref source, phase, step, column + Vector128<int>.Count, tap)); LoadScaledSamplesVector128<T>(ref source, phase, step, column + Vector128<int>.Count, tap));
Vector256<int> coefficients = Vector256.Create( Vector256<int> coefficients = Vector256.Create(
LoadScaledCoefficientsVector128<TFilter>(phase, step, column, tap, useReducedFilter), LoadScaledCoefficientsVector128<TFilter>(phase, step, column, tap, useReducedFilter),
LoadScaledCoefficientsVector128<TFilter>(phase, step, column + Vector128<int>.Count, tap, useReducedFilter)); LoadScaledCoefficientsVector128<TFilter>(phase, step, column + Vector128<int>.Count, tap, useReducedFilter));
result = TOperator.MultiplyAdd(result, samples, coefficients); result = NativeOperator.MultiplyAdd(result, samples, coefficients);
} }
return RoundPowerOfTwo(result, round); return RoundPowerOfTwo(result, round);
@ -577,7 +649,7 @@ internal static partial class Av1ScaledInterPredictor
/// <summary> /// <summary>
/// Filters sixteen independently positioned horizontal samples through a closed scaled-prediction operator. /// Filters sixteen independently positioned horizontal samples through a closed scaled-prediction operator.
/// </summary> /// </summary>
private static Vector512<int> FilterScaledHorizontalVector512<T, TOperator, TFilter>( private static Vector512<int> FilterScaledHorizontalVector512<T, TFilter>(
ref T source, ref T source,
int phase, int phase,
int step, int step,
@ -586,19 +658,18 @@ internal static partial class Av1ScaledInterPredictor
int bias, int bias,
int round) int round)
where T : unmanaged where T : unmanaged
where TOperator : struct, IAv1ScaledPredictionOperator
where TFilter : struct, IAv1InterPredictorOperator where TFilter : struct, IAv1InterPredictorOperator
{ {
Vector512<int> result = Vector512.Create(bias); Vector512<int> result = Vector512.Create(bias);
for (int tap = 0; tap < FilterCoefficientCount; tap++) for (int tap = 0; tap < FilterCoefficientCount; tap++)
{ {
Vector256<int> sampleLower = Vector256.Create( Vector256<int> sampleLower = Vector256.Create(
LoadScaledSamplesVector128<T, TOperator>(ref source, phase, step, column, tap), LoadScaledSamplesVector128<T>(ref source, phase, step, column, tap),
LoadScaledSamplesVector128<T, TOperator>(ref source, phase, step, column + Vector128<int>.Count, tap)); LoadScaledSamplesVector128<T>(ref source, phase, step, column + Vector128<int>.Count, tap));
Vector256<int> sampleUpper = Vector256.Create( Vector256<int> sampleUpper = Vector256.Create(
LoadScaledSamplesVector128<T, TOperator>(ref source, phase, step, column + Vector256<int>.Count, tap), LoadScaledSamplesVector128<T>(ref source, phase, step, column + Vector256<int>.Count, tap),
LoadScaledSamplesVector128<T, TOperator>(ref source, phase, step, column + Vector256<int>.Count + Vector128<int>.Count, tap)); LoadScaledSamplesVector128<T>(ref source, phase, step, column + Vector256<int>.Count + Vector128<int>.Count, tap));
Vector256<int> coefficientLower = Vector256.Create( Vector256<int> coefficientLower = Vector256.Create(
LoadScaledCoefficientsVector128<TFilter>(phase, step, column, tap, useReducedFilter), LoadScaledCoefficientsVector128<TFilter>(phase, step, column, tap, useReducedFilter),
@ -608,7 +679,7 @@ internal static partial class Av1ScaledInterPredictor
LoadScaledCoefficientsVector128<TFilter>(phase, step, column + Vector256<int>.Count, tap, useReducedFilter), LoadScaledCoefficientsVector128<TFilter>(phase, step, column + Vector256<int>.Count, tap, useReducedFilter),
LoadScaledCoefficientsVector128<TFilter>(phase, step, column + Vector256<int>.Count + Vector128<int>.Count, tap, useReducedFilter)); LoadScaledCoefficientsVector128<TFilter>(phase, step, column + Vector256<int>.Count + Vector128<int>.Count, tap, useReducedFilter));
result = TOperator.MultiplyAdd( result = NativeOperator.MultiplyAdd(
result, result,
Vector512.Create(sampleLower, sampleUpper), Vector512.Create(sampleLower, sampleUpper),
Vector512.Create(coefficientLower, coefficientUpper)); Vector512.Create(coefficientLower, coefficientUpper));
@ -620,19 +691,18 @@ internal static partial class Av1ScaledInterPredictor
/// <summary> /// <summary>
/// Gathers four variable-position source samples for one horizontal filter tap. /// Gathers four variable-position source samples for one horizontal filter tap.
/// </summary> /// </summary>
private static Vector128<int> LoadScaledSamplesVector128<T, TOperator>( private static Vector128<int> LoadScaledSamplesVector128<T>(
ref T source, ref T source,
int phase, int phase,
int step, int step,
int column, int column,
int tap) int tap)
where T : unmanaged where T : unmanaged
where TOperator : struct, IAv1ScaledPredictionOperator
=> Vector128.Create( => Vector128.Create(
LoadScaledSample<T, TOperator>(ref source, phase, step, column, tap), LoadScaledSample(ref source, phase, step, column, tap),
LoadScaledSample<T, TOperator>(ref source, phase, step, column + 1, tap), LoadScaledSample(ref source, phase, step, column + 1, tap),
LoadScaledSample<T, TOperator>(ref source, phase, step, column + 2, tap), LoadScaledSample(ref source, phase, step, column + 2, tap),
LoadScaledSample<T, TOperator>(ref source, phase, step, column + 3, tap)); LoadScaledSample(ref source, phase, step, column + 3, tap));
/// <summary> /// <summary>
/// Gathers four variable-phase coefficients for one horizontal filter tap. /// Gathers four variable-phase coefficients for one horizontal filter tap.
@ -653,13 +723,12 @@ internal static partial class Av1ScaledInterPredictor
/// <summary> /// <summary>
/// Loads one variable-position source sample for a horizontal filter tap. /// Loads one variable-position source sample for a horizontal filter tap.
/// </summary> /// </summary>
private static int LoadScaledSample<T, TOperator>(ref T source, int phase, int step, int column, int tap) private static int LoadScaledSample<T>(ref T source, int phase, int step, int column, int tap)
where T : unmanaged where T : unmanaged
where TOperator : struct, IAv1ScaledPredictionOperator
{ {
int position = phase + (column * step); int position = phase + (column * step);
int sourceColumn = (position >> Av1ReferenceScale.SubpixelBits) - 3; int sourceColumn = (position >> Av1ReferenceScale.SubpixelBits) - 3;
return TOperator.Load(ref source, sourceColumn + tap); return NativeOperator.Load(ref source, sourceColumn + tap);
} }
/// <summary> /// <summary>

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

@ -362,14 +362,9 @@ internal sealed class Av1BlockDecoder : IDisposable
predictionScratch); predictionScratch);
int referenceCount = usesSub8x8ChromaPrediction ? 0 : isCompound ? 2 : 1; int referenceCount = usesSub8x8ChromaPrediction ? 0 : isCompound ? 2 : 1;
bool hasScaledCompoundReference = isCompound &&
(referenceFrameBuffer!.Width != this.frameHeader.FrameSize.FrameWidth || // Compound convolution is combined before its final rounding step. Scaled and ordinary translational
referenceFrameBuffer.Height != this.frameHeader.FrameSize.FrameHeight || // predictors share that no-round domain; high-bit-depth warped/global models retain their own kernels.
secondaryReferenceFrameBuffer!.Width != this.frameHeader.FrameSize.FrameWidth ||
secondaryReferenceFrameBuffer.Height != this.frameHeader.FrameSize.FrameHeight);
// Compound convolution is combined before its final rounding step. Scaled references and high-bit-depth
// warped/global models have separate kernels and remain with their owning later prediction checkpoints.
bool useHighBitDepthCompoundIntermediates = bool useHighBitDepthCompoundIntermediates =
highBitDepth && highBitDepth &&
modeInfo.CompoundType is modeInfo.CompoundType is
@ -382,7 +377,6 @@ internal sealed class Av1BlockDecoder : IDisposable
bool useCompoundIntermediates = bool useCompoundIntermediates =
isCompound && isCompound &&
!hasScaledCompoundReference &&
(!highBitDepth || useHighBitDepthCompoundIntermediates); (!highBitDepth || useHighBitDepthCompoundIntermediates);
for (int referenceIndex = 0; referenceIndex < referenceCount; referenceIndex++) for (int referenceIndex = 0; referenceIndex < referenceCount; referenceIndex++)
@ -535,7 +529,14 @@ internal sealed class Av1BlockDecoder : IDisposable
{ {
Span<byte> scaledDestination = default; Span<byte> scaledDestination = default;
Span<ushort> scaledHighBitDepthDestination = default; Span<ushort> scaledHighBitDepthDestination = default;
if (highBitDepth) Span<ushort> scaledCompoundDestination = default;
if (useCompoundIntermediates)
{
scaledCompoundDestination = referenceIndex == 0
? firstCompoundPrediction
: highBitDepthSecondPrediction;
}
else if (highBitDepth)
{ {
scaledHighBitDepthDestination = referenceIndex == 0 scaledHighBitDepthDestination = referenceIndex == 0
? MemoryMarshal.Cast<short, ushort>(highBitDepthBlockReconstructionBuffer[reconstructionStride..]) ? MemoryMarshal.Cast<short, ushort>(highBitDepthBlockReconstructionBuffer[reconstructionStride..])
@ -561,6 +562,7 @@ internal sealed class Av1BlockDecoder : IDisposable
modeInfo.InterpolationFilters[0], modeInfo.InterpolationFilters[0],
scaledDestination, scaledDestination,
scaledHighBitDepthDestination, scaledHighBitDepthDestination,
scaledCompoundDestination,
destinationStride, destinationStride,
predictionScratch); predictionScratch);
@ -1493,6 +1495,7 @@ internal sealed class Av1BlockDecoder : IDisposable
candidate.InterpolationFilters[0], candidate.InterpolationFilters[0],
scaledDestination, scaledDestination,
scaledHighBitDepthDestination, scaledHighBitDepthDestination,
default,
reconstructionStride, reconstructionStride,
predictionScratch); predictionScratch);
} }
@ -1623,6 +1626,7 @@ internal sealed class Av1BlockDecoder : IDisposable
Av1InterpolationFilter verticalFilter, Av1InterpolationFilter verticalFilter,
Span<byte> destination, Span<byte> destination,
Span<ushort> highBitDepthDestination, Span<ushort> highBitDepthDestination,
Span<ushort> compoundDestination,
int destinationStride, int destinationStride,
Span<short> predictionScratch) Span<short> predictionScratch)
{ {
@ -1669,22 +1673,44 @@ internal sealed class Av1BlockDecoder : IDisposable
sourceOrigin.X + sourceOrigin.X +
(sourceColumnQ10 >> Av1ReferenceScale.SubpixelBits); (sourceColumnQ10 >> Av1ReferenceScale.SubpixelBits);
Av1ScaledInterPredictor.PredictScaled( if (compoundDestination.IsEmpty)
source, {
sourceStride, Av1ScaledInterPredictor.PredictScaled(
sourceIndex, source,
highBitDepthDestination, sourceStride,
destinationStride, sourceIndex,
predictionWidth, highBitDepthDestination,
predictionHeight, destinationStride,
horizontalFilter, predictionWidth,
verticalFilter, predictionHeight,
horizontalPhase, horizontalFilter,
scale.HorizontalStep, verticalFilter,
verticalPhase, horizontalPhase,
scale.VerticalStep, scale.HorizontalStep,
this.frameBuffer.BitDepth.GetBitCount(), verticalPhase,
predictionScratch); scale.VerticalStep,
this.frameBuffer.BitDepth.GetBitCount(),
predictionScratch);
}
else
{
Av1ScaledInterPredictor.PredictScaledCompound(
source,
sourceStride,
sourceIndex,
compoundDestination,
destinationStride,
predictionWidth,
predictionHeight,
horizontalFilter,
verticalFilter,
horizontalPhase,
scale.HorizontalStep,
verticalPhase,
scale.VerticalStep,
this.frameBuffer.BitDepth.GetBitCount(),
predictionScratch);
}
} }
else else
{ {
@ -1700,21 +1726,42 @@ internal sealed class Av1BlockDecoder : IDisposable
sourceOrigin.X + sourceOrigin.X +
(sourceColumnQ10 >> Av1ReferenceScale.SubpixelBits); (sourceColumnQ10 >> Av1ReferenceScale.SubpixelBits);
Av1ScaledInterPredictor.PredictScaled( if (compoundDestination.IsEmpty)
source, {
sourceStride, Av1ScaledInterPredictor.PredictScaled(
sourceIndex, source,
destination, sourceStride,
destinationStride, sourceIndex,
predictionWidth, destination,
predictionHeight, destinationStride,
horizontalFilter, predictionWidth,
verticalFilter, predictionHeight,
horizontalPhase, horizontalFilter,
scale.HorizontalStep, verticalFilter,
verticalPhase, horizontalPhase,
scale.VerticalStep, scale.HorizontalStep,
predictionScratch); verticalPhase,
scale.VerticalStep,
predictionScratch);
}
else
{
Av1ScaledInterPredictor.PredictScaledCompound(
source,
sourceStride,
sourceIndex,
compoundDestination,
destinationStride,
predictionWidth,
predictionHeight,
horizontalFilter,
verticalFilter,
horizontalPhase,
scale.HorizontalStep,
verticalPhase,
scale.VerticalStep,
predictionScratch);
}
} }
} }
@ -1949,6 +1996,7 @@ internal sealed class Av1BlockDecoder : IDisposable
neighbor.InterpolationFilters[0], neighbor.InterpolationFilters[0],
destination, destination,
highBitDepthDestination, highBitDepthDestination,
default,
predictionWidth, predictionWidth,
predictionScratch); predictionScratch);

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

@ -239,6 +239,15 @@ public class Av1CompoundBlockDecoderTests
} }
} }
/// <summary>
/// Verifies that scaled predictors retain their no-round precision until compound averaging.
/// </summary>
[Fact]
public void DecodeBlockReconstructsScaledCompoundPrediction()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(
ValidateScaledCompoundPrediction,
CompoundPredictionConfigurations);
/// <summary> /// <summary>
/// Verifies selectable compound reconstruction through the production block branch at every supported bit depth. /// Verifies selectable compound reconstruction through the production block branch at every supported bit depth.
/// </summary> /// </summary>
@ -658,6 +667,262 @@ public class Av1CompoundBlockDecoderTests
return modeInfo; return modeInfo;
} }
/// <summary>
/// Reconstructs the scaled compound regression at every supported source precision.
/// </summary>
private static void ValidateScaledCompoundPrediction()
{
foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.EightBit, Av1BitDepth.TenBit, Av1BitDepth.TwelveBit })
{
ValidateScaledCompoundPredictionAtBitDepth(bitDepth);
}
}
/// <summary>
/// Reconstructs one scaled compound block and compares the production branch with the no-round pipeline.
/// </summary>
/// <param name="bitDepth">The native sample depth.</param>
private static void ValidateScaledCompoundPredictionAtBitDepth(Av1BitDepth bitDepth)
{
const int currentSize = 8;
const int referenceSize = 16;
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth, referenceSize);
ObuFrameHeader frameHeader = CreateFrameHeader(currentSize);
frameHeader.GetReferenceFrameIndices()[0] = 0;
frameHeader.GetReferenceFrameIndices()[1] = 1;
using Av1ReferenceFrameStore referenceFrames = new();
Assert.True(referenceFrames.Commit(
1,
CreateScaledPatternReferenceFrame(sequenceHeader),
showFrame: false));
Assert.True(referenceFrames.Commit(
2,
CreateScaledPatternReferenceFrame(
sequenceHeader,
horizontalScale: 3,
verticalScale: 7,
nonlinearScale: 3),
showFrame: false));
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty)
{
Skip = true,
YMode = Av1PredictionMode.NearestNearestMotionVector,
CompoundIndex = true,
CompoundType = Av1CompoundType.Average,
};
modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last;
modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Last2;
modeInfo.InterpolationFilters.Fill(Av1InterpolationFilter.Bilinear);
modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1);
Av1ReferenceScale scale = new(referenceSize, referenceSize, currentSize, currentSize);
int sourceColumnQ10 = scale.ScaleHorizontal(0) + Av1ReferenceScale.ExtraOffset;
int sourceRowQ10 = scale.ScaleVertical(0) + Av1ReferenceScale.ExtraOffset;
int horizontalPhase = sourceColumnQ10 & Av1ReferenceScale.SubpixelMask;
int verticalPhase = sourceRowQ10 & Av1ReferenceScale.SubpixelMask;
ushort[] firstIntermediate = new ushort[currentSize * currentSize];
ushort[] secondIntermediate = new ushort[currentSize * currentSize];
byte[] firstRounded8 = new byte[currentSize * currentSize];
byte[] secondRounded8 = new byte[currentSize * currentSize];
ushort[] firstRoundedHigh = new ushort[currentSize * currentSize];
ushort[] secondRoundedHigh = new ushort[currentSize * currentSize];
short[] predictionScratch = new short[
Av1ScaledInterPredictor.GetScaledScratchLength(
currentSize,
currentSize,
verticalPhase,
scale.VerticalStep)];
for (int referenceIndex = 0; referenceIndex < 2; referenceIndex++)
{
Av1FrameBuffer<byte> reference = referenceFrames.Resolve(referenceIndex)!.FrameBuffer;
Span<ushort> intermediate = referenceIndex == 0 ? firstIntermediate : secondIntermediate;
int sourceIndex;
if (bitDepth == Av1BitDepth.EightBit)
{
Span<byte> source = reference.GetPaddedPlaneSpan(
Av1Plane.Y,
0,
0,
out int sourceStride,
out Point sourceOrigin);
sourceIndex =
((sourceOrigin.Y + (sourceRowQ10 >> Av1ReferenceScale.SubpixelBits)) * sourceStride) +
sourceOrigin.X +
(sourceColumnQ10 >> Av1ReferenceScale.SubpixelBits);
Av1ScaledInterPredictor.PredictScaledCompound(
source,
sourceStride,
sourceIndex,
intermediate,
currentSize,
currentSize,
currentSize,
Av1InterpolationFilter.Bilinear,
Av1InterpolationFilter.Bilinear,
horizontalPhase,
scale.HorizontalStep,
verticalPhase,
scale.VerticalStep,
predictionScratch);
Av1ScaledInterPredictor.PredictScaled(
source,
sourceStride,
sourceIndex,
referenceIndex == 0 ? firstRounded8 : secondRounded8,
currentSize,
currentSize,
currentSize,
Av1InterpolationFilter.Bilinear,
Av1InterpolationFilter.Bilinear,
horizontalPhase,
scale.HorizontalStep,
verticalPhase,
scale.VerticalStep,
predictionScratch);
}
else
{
Span<ushort> source = reference.GetPaddedPlaneSpan16(
Av1Plane.Y,
0,
0,
out int sourceStride,
out Point sourceOrigin);
sourceIndex =
((sourceOrigin.Y + (sourceRowQ10 >> Av1ReferenceScale.SubpixelBits)) * sourceStride) +
sourceOrigin.X +
(sourceColumnQ10 >> Av1ReferenceScale.SubpixelBits);
Av1ScaledInterPredictor.PredictScaledCompound(
source,
sourceStride,
sourceIndex,
intermediate,
currentSize,
currentSize,
currentSize,
Av1InterpolationFilter.Bilinear,
Av1InterpolationFilter.Bilinear,
horizontalPhase,
scale.HorizontalStep,
verticalPhase,
scale.VerticalStep,
bitDepth.GetBitCount(),
predictionScratch);
Av1ScaledInterPredictor.PredictScaled(
source,
sourceStride,
sourceIndex,
referenceIndex == 0 ? firstRoundedHigh : secondRoundedHigh,
currentSize,
currentSize,
currentSize,
Av1InterpolationFilter.Bilinear,
Av1InterpolationFilter.Bilinear,
horizontalPhase,
scale.HorizontalStep,
verticalPhase,
scale.VerticalStep,
bitDepth.GetBitCount(),
predictionScratch);
}
}
using Av1FrameBuffer<byte> frameBuffer = new(
Configuration.Default,
sequenceHeader,
Av1ColorFormat.Yuv400,
false);
frameBuffer.Width = currentSize;
frameBuffer.Height = currentSize;
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,
Point.Empty,
Av1BlockSize.Block8x8,
superblockInfo,
new Av1TileInfo(0, 0, frameHeader));
if (bitDepth == Av1BitDepth.EightBit)
{
byte[] expected = new byte[currentSize * currentSize];
Av1CompoundIntermediateAveragePredictor.AverageIntermediate(
expected,
currentSize,
firstIntermediate,
currentSize,
secondIntermediate,
currentSize,
currentSize,
currentSize,
bitDepth.GetBitCount());
byte[] prematurelyRounded = new byte[currentSize * currentSize];
for (int index = 0; index < prematurelyRounded.Length; index++)
{
prematurelyRounded[index] = (byte)((firstRounded8[index] + secondRounded8[index] + 1) >> 1);
}
Assert.False(expected.AsSpan().SequenceEqual(prematurelyRounded));
for (int row = 0; row < currentSize; row++)
{
Span<byte> actual = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row);
Assert.Equal(expected.AsSpan(row * currentSize, currentSize), actual[..currentSize]);
}
}
else
{
ushort[] expected = new ushort[currentSize * currentSize];
Av1CompoundIntermediateAveragePredictor.AverageIntermediate(
expected,
currentSize,
firstIntermediate,
currentSize,
secondIntermediate,
currentSize,
currentSize,
currentSize,
bitDepth.GetBitCount());
ushort[] prematurelyRounded = new ushort[currentSize * currentSize];
for (int index = 0; index < prematurelyRounded.Length; index++)
{
prematurelyRounded[index] = (ushort)((firstRoundedHigh[index] + secondRoundedHigh[index] + 1) >> 1);
}
Assert.False(expected.AsSpan().SequenceEqual(prematurelyRounded));
for (int row = 0; row < currentSize; row++)
{
Span<ushort> actual = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0);
Assert.Equal(expected.AsSpan(row * currentSize, currentSize), actual[..currentSize]);
}
}
}
/// <summary> /// <summary>
/// Reconstructs a compound global-warp block at every supported native sample depth. /// Reconstructs a compound global-warp block at every supported native sample depth.
/// </summary> /// </summary>
@ -1273,9 +1538,17 @@ public class Av1CompoundBlockDecoderTests
} }
/// <summary> /// <summary>
/// Creates a 16x16 retained frame whose linear pattern has an exact half-sample bilinear result. /// Creates a 16x16 retained frame whose deterministic pattern exposes scaled bilinear precision.
/// </summary> /// </summary>
private static Av1ReferenceFrame CreateScaledPatternReferenceFrame(ObuSequenceHeader sequenceHeader) /// <param name="sequenceHeader">The sequence dimensions and sample precision.</param>
/// <param name="horizontalScale">The horizontal linear contribution.</param>
/// <param name="verticalScale">The vertical linear contribution.</param>
/// <param name="nonlinearScale">The contribution that makes neighboring interpolation errors differ.</param>
private static Av1ReferenceFrame CreateScaledPatternReferenceFrame(
ObuSequenceHeader sequenceHeader,
int horizontalScale = 2,
int verticalScale = 8,
int nonlinearScale = 0)
{ {
Av1FrameBuffer<byte> frameBuffer = new( Av1FrameBuffer<byte> frameBuffer = new(
Configuration.Default, Configuration.Default,
@ -1292,7 +1565,10 @@ public class Av1CompoundBlockDecoderTests
Span<byte> samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row); Span<byte> samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row);
for (int column = 0; column < 16; column++) for (int column = 0; column < 16; column++)
{ {
samples[column] = (byte)((column * 2) + (row * 8)); samples[column] = (byte)(
(column * horizontalScale) +
(row * verticalScale) +
(((column * row) & 7) * nonlinearScale));
} }
} }
else else
@ -1300,7 +1576,10 @@ public class Av1CompoundBlockDecoderTests
Span<ushort> samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0); Span<ushort> samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0);
for (int column = 0; column < 16; column++) for (int column = 0; column < 16; column++)
{ {
samples[column] = (ushort)((column * 2) + (row * 8)); samples[column] = (ushort)(
(column * horizontalScale) +
(row * verticalScale) +
(((column * row) & 7) * nonlinearScale));
} }
} }
} }

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

@ -717,12 +717,12 @@ public class Av1ReconstructionConformanceTests
/// <summary> /// <summary>
/// Verifies that an essential lsel property returns the selected base spatial layer rather than the final /// Verifies that an essential lsel property returns the selected base spatial layer rather than the final
/// progressive layer, with exact pinned-libaom native planes and pinned-libavif presentation. /// progressive layer, with exact current-libaom native planes and the retained exact presentation reference.
/// </summary> /// </summary>
/// <param name="provider">The selected-layer AVIF input and matching reference-output naming context.</param> /// <param name="provider">The selected-layer AVIF input and matching reference-output naming context.</param>
[Theory] [Theory]
[WithFile(TestImages.Heif.Av1ScaledReferenceSelectedLayerAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.Av1ScaledReferenceSelectedLayerAvif, PixelTypes.Rgba32)]
public void DecodeSelectedProgressiveSpatialLayerMatchesPinnedReferences( public void DecodeSelectedProgressiveSpatialLayerMatchesCurrentLibaomReferences(
TestImageProvider<Rgba32> provider) TestImageProvider<Rgba32> provider)
=> FeatureTestRunner.RunWithHwIntrinsicsFeature( => FeatureTestRunner.RunWithHwIntrinsicsFeature(
ValidateSelectedProgressiveSpatialLayerWithDefaultConfiguration, ValidateSelectedProgressiveSpatialLayerWithDefaultConfiguration,
@ -753,12 +753,12 @@ public class Av1ReconstructionConformanceTests
/// <summary> /// <summary>
/// Verifies an independently encoded 40x40 retained layer scaled into an 80x80 dependent layer against exact /// Verifies an independently encoded 40x40 retained layer scaled into an 80x80 dependent layer against exact
/// pinned-libaom native planes and pinned-libavif presentation. /// current-libaom native planes and the retained exact presentation reference.
/// </summary> /// </summary>
/// <param name="provider">The AVIF input and matching reference-output naming context.</param> /// <param name="provider">The AVIF input and matching reference-output naming context.</param>
[Theory] [Theory]
[WithFile(TestImages.Heif.Av1ScaledReferenceAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.Av1ScaledReferenceAvif, PixelTypes.Rgba32)]
public void DecodeScaledReferenceMatchesPinnedReferences(TestImageProvider<Rgba32> provider) public void DecodeScaledReferenceMatchesCurrentLibaomReferences(TestImageProvider<Rgba32> provider)
=> FeatureTestRunner.RunWithHwIntrinsicsFeature( => FeatureTestRunner.RunWithHwIntrinsicsFeature(
ValidateScaledReferenceFixtureWithDefaultConfiguration, ValidateScaledReferenceFixtureWithDefaultConfiguration,
ReconstructionConfigurations, ReconstructionConfigurations,

218
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ScaledInterPredictorTests.cs

@ -7,7 +7,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
/// <summary> /// <summary>
/// Verifies AV1 reference scaling and variable-phase inter convolution against an independent libaom-shaped oracle. /// Verifies AV1 reference scaling and variable-phase inter convolution against current libaom arithmetic.
/// </summary> /// </summary>
[Trait("Format", "Avif")] [Trait("Format", "Avif")]
public class Av1ScaledInterPredictorTests public class Av1ScaledInterPredictorTests
@ -22,6 +22,11 @@ public class Av1ScaledInterPredictorTests
/// </summary> /// </summary>
private const int Round0Bits = 3; private const int Round0Bits = 3;
/// <summary>
/// The vertical shift that preserves AV1's compound intermediate precision.
/// </summary>
private const int CompoundRound1Bits = 7;
/// <summary> /// <summary>
/// The number of samples in every stored interpolation row. /// The number of samples in every stored interpolation row.
/// </summary> /// </summary>
@ -63,10 +68,10 @@ public class Av1ScaledInterPredictorTests
private const HwIntrinsics PredictorConfigurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic; private const HwIntrinsics PredictorConfigurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic;
/// <summary> /// <summary>
/// Verifies the pinned Q14 scale factors, Q10 steps, and signed coordinate rounding. /// Verifies current libaom's Q14 scale factors, Q10 steps, and signed coordinate rounding.
/// </summary> /// </summary>
[Fact] [Fact]
public void ReferenceScaleMatchesPinnedLibaomFixedPointRules() public void ReferenceScaleMatchesCurrentLibaomFixedPointRules()
{ {
Av1ReferenceScale downscaledReference = new(40, 24, 64, 48); Av1ReferenceScale downscaledReference = new(40, 24, 64, 48);
@ -96,16 +101,30 @@ public class Av1ScaledInterPredictorTests
/// Verifies exact scaled 8-bit output, variable filter phases, vector tails, and untouched destination padding. /// Verifies exact scaled 8-bit output, variable filter phases, vector tails, and untouched destination padding.
/// </summary> /// </summary>
[Fact] [Fact]
public void BytePredictionMatchesLibaomOracleAcrossIntrinsicConfigurations() public void BytePredictionMatchesCurrentLibaomOracleAcrossIntrinsicConfigurations()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateBytePredictions, PredictorConfigurations); => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateBytePredictions, PredictorConfigurations);
/// <summary> /// <summary>
/// Verifies exact scaled 8-, 10-, and 12-bit output under the native vector and scalar configurations. /// Verifies exact scaled 8-, 10-, and 12-bit output under the native vector and scalar configurations.
/// </summary> /// </summary>
[Fact] [Fact]
public void HighBitDepthPredictionMatchesLibaomOracleAcrossIntrinsicConfigurations() public void HighBitDepthPredictionMatchesCurrentLibaomOracleAcrossIntrinsicConfigurations()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthPredictions, PredictorConfigurations); => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthPredictions, PredictorConfigurations);
/// <summary>
/// Verifies exact scaled 8-bit no-round compound intermediates under native vector and scalar configurations.
/// </summary>
[Fact]
public void ByteCompoundPredictionMatchesCurrentLibaomOracleAcrossIntrinsicConfigurations()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateByteCompoundPredictions, PredictorConfigurations);
/// <summary>
/// Verifies exact scaled 8-, 10-, and 12-bit no-round compound intermediates under native vector and scalar configurations.
/// </summary>
[Fact]
public void HighBitDepthCompoundPredictionMatchesCurrentLibaomOracleAcrossIntrinsicConfigurations()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthCompoundPredictions, PredictorConfigurations);
/// <summary> /// <summary>
/// Applies each scaled-prediction scenario to byte storage. /// Applies each scaled-prediction scenario to byte storage.
/// </summary> /// </summary>
@ -191,6 +210,91 @@ public class Av1ScaledInterPredictorTests
} }
} }
/// <summary>
/// Applies each scaled compound-prediction scenario to byte source storage.
/// </summary>
private static void ValidateByteCompoundPredictions()
{
foreach (ScaledPredictionCase testCase in CreatePredictionCases())
{
byte[] source = CreateByteSource(testCase, out int sourceStride, out int sourceOrigin);
int destinationStride = testCase.Width + DestinationRowPadding;
ushort[] expected = CreateUInt16Destination(testCase, destinationStride);
ushort[] actual = (ushort[])expected.Clone();
short[] scratch = new short[
Av1ScaledInterPredictor.GetScaledScratchLength(
testCase.Width,
testCase.Height,
testCase.VerticalPhase,
testCase.VerticalStep)];
ApplyCompoundReference(source, sourceStride, sourceOrigin, expected, destinationStride, testCase, 8);
Av1ScaledInterPredictor.PredictScaledCompound(
source,
sourceStride,
sourceOrigin,
actual.AsSpan(DestinationPrefix),
destinationStride,
testCase.Width,
testCase.Height,
testCase.HorizontalFilter,
testCase.VerticalFilter,
testCase.HorizontalPhase,
testCase.HorizontalStep,
testCase.VerticalPhase,
testCase.VerticalStep,
scratch);
Assert.Equal(expected, actual);
}
}
/// <summary>
/// Applies each scaled compound-prediction scenario to every supported high-bit-depth precision.
/// </summary>
private static void ValidateHighBitDepthCompoundPredictions()
{
int[] bitDepths = [8, 10, 12];
foreach (int bitDepth in bitDepths)
{
foreach (ScaledPredictionCase testCase in CreatePredictionCases())
{
ushort[] source = CreateUInt16Source(testCase, bitDepth, out int sourceStride, out int sourceOrigin);
int destinationStride = testCase.Width + DestinationRowPadding;
ushort[] expected = CreateUInt16Destination(testCase, destinationStride);
ushort[] actual = (ushort[])expected.Clone();
short[] scratch = new short[
Av1ScaledInterPredictor.GetScaledScratchLength(
testCase.Width,
testCase.Height,
testCase.VerticalPhase,
testCase.VerticalStep)];
ApplyCompoundReference(source, sourceStride, sourceOrigin, expected, destinationStride, testCase, bitDepth);
Av1ScaledInterPredictor.PredictScaledCompound(
source,
sourceStride,
sourceOrigin,
actual.AsSpan(DestinationPrefix),
destinationStride,
testCase.Width,
testCase.Height,
testCase.HorizontalFilter,
testCase.VerticalFilter,
testCase.HorizontalPhase,
testCase.HorizontalStep,
testCase.VerticalPhase,
testCase.VerticalStep,
bitDepth,
scratch);
Assert.Equal(expected, actual);
}
}
}
/// <summary> /// <summary>
/// Creates cases covering variable phases, every filter family, reduced kernels, and vector tails. /// Creates cases covering variable phases, every filter family, reduced kernels, and vector tails.
/// </summary> /// </summary>
@ -348,6 +452,74 @@ public class Av1ScaledInterPredictorTests
WriteReference(intermediate, intermediateStride, destination, destinationStride, testCase, bitDepth); WriteReference(intermediate, intermediateStride, destination, destinationStride, testCase, bitDepth);
} }
/// <summary>
/// Applies independent scaled compound convolution to byte source storage.
/// </summary>
private static void ApplyCompoundReference(
byte[] source,
int sourceStride,
int sourceOrigin,
ushort[] destination,
int destinationStride,
ScaledPredictionCase testCase,
int bitDepth)
{
short[] intermediate = CreateIntermediate(testCase);
int intermediateStride = testCase.Width;
int round0 = GetRound0Bits(bitDepth);
int horizontalBias = 1 << (bitDepth + FilterBits - 1);
Span<short> coefficients = stackalloc short[FilterTapCount];
for (int row = 0; row < intermediate.Length / intermediateStride; row++)
{
for (int column = 0; column < testCase.Width; column++)
{
int position = testCase.HorizontalPhase + (column * testCase.HorizontalStep);
int sourceColumn = (position >> Av1ReferenceScale.SubpixelBits) - 3;
FillCoefficients(testCase.HorizontalFilter, (position & Av1ReferenceScale.SubpixelMask) >> 6, testCase.Width <= 4, coefficients);
int sourceIndex = sourceOrigin + ((row - 3) * sourceStride) + sourceColumn;
int sum = horizontalBias + Convolve(source, sourceIndex, coefficients);
intermediate[(row * intermediateStride) + column] = (short)RoundPowerOfTwo(sum, round0);
}
}
WriteCompoundReference(intermediate, intermediateStride, destination, destinationStride, testCase, bitDepth);
}
/// <summary>
/// Applies independent scaled compound convolution to ushort source storage.
/// </summary>
private static void ApplyCompoundReference(
ushort[] source,
int sourceStride,
int sourceOrigin,
ushort[] destination,
int destinationStride,
ScaledPredictionCase testCase,
int bitDepth)
{
short[] intermediate = CreateIntermediate(testCase);
int intermediateStride = testCase.Width;
int round0 = GetRound0Bits(bitDepth);
int horizontalBias = 1 << (bitDepth + FilterBits - 1);
Span<short> coefficients = stackalloc short[FilterTapCount];
for (int row = 0; row < intermediate.Length / intermediateStride; row++)
{
for (int column = 0; column < testCase.Width; column++)
{
int position = testCase.HorizontalPhase + (column * testCase.HorizontalStep);
int sourceColumn = (position >> Av1ReferenceScale.SubpixelBits) - 3;
FillCoefficients(testCase.HorizontalFilter, (position & Av1ReferenceScale.SubpixelMask) >> 6, testCase.Width <= 4, coefficients);
int sourceIndex = sourceOrigin + ((row - 3) * sourceStride) + sourceColumn;
int sum = horizontalBias + Convolve(source, sourceIndex, coefficients);
intermediate[(row * intermediateStride) + column] = (short)RoundPowerOfTwo(sum, round0);
}
}
WriteCompoundReference(intermediate, intermediateStride, destination, destinationStride, testCase, bitDepth);
}
/// <summary> /// <summary>
/// Allocates the oracle's independently shaped intermediate block. /// Allocates the oracle's independently shaped intermediate block.
/// </summary> /// </summary>
@ -409,6 +581,40 @@ public class Av1ScaledInterPredictorTests
} }
} }
/// <summary>
/// Writes the biased no-round compound output from the horizontally filtered intermediate block.
/// </summary>
private static void WriteCompoundReference(
short[] intermediate,
int intermediateStride,
ushort[] destination,
int destinationStride,
ScaledPredictionCase testCase,
int bitDepth)
{
int round0 = GetRound0Bits(bitDepth);
int offsetBits = bitDepth + (2 * FilterBits) - round0;
int verticalBias = 1 << offsetBits;
Span<short> coefficients = stackalloc short[FilterTapCount];
for (int row = 0; row < testCase.Height; row++)
{
int position = testCase.VerticalPhase + (row * testCase.VerticalStep);
int sourceRow = position >> Av1ReferenceScale.SubpixelBits;
FillCoefficients(testCase.VerticalFilter, (position & Av1ReferenceScale.SubpixelMask) >> 6, testCase.Height <= 4, coefficients);
for (int column = 0; column < testCase.Width; column++)
{
int sum = verticalBias + Convolve(
intermediate,
(sourceRow * intermediateStride) + column,
intermediateStride,
coefficients);
destination[DestinationPrefix + (row * destinationStride) + column] =
(ushort)RoundPowerOfTwo(sum, CompoundRound1Bits);
}
}
}
/// <summary> /// <summary>
/// Removes both normative convolution biases after the vertical pass. /// Removes both normative convolution biases after the vertical pass.
/// </summary> /// </summary>
@ -471,7 +677,7 @@ public class Av1ScaledInterPredictorTests
} }
/// <summary> /// <summary>
/// Selects one pinned coefficient row without reading production filter storage. /// Selects one current-libaom coefficient row without reading production filter storage.
/// </summary> /// </summary>
private static void FillCoefficients(Av1InterpolationFilter filter, int phase, bool reduced, Span<short> destination) private static void FillCoefficients(Av1InterpolationFilter filter, int phase, bool reduced, Span<short> destination)
{ {

0
tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeScaledReferenceMatchesPinnedReferences_Rgba32_libavif-webp-logo-scaled-reference.png → tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeScaledReferenceMatchesCurrentLibaomReferences_Rgba32_libavif-webp-logo-scaled-reference.png

0
tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectedProgressiveSpatialLayerMatchesPinnedReferences_Rgba32_libavif-webp-logo-scaled-reference-lsel0.png → tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectedProgressiveSpatialLayerMatchesCurrentLibaomReferences_Rgba32_libavif-webp-logo-scaled-reference-lsel0.png

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

@ -46,6 +46,7 @@ The reference builds use `AOM_TARGET_CPU=generic` and disable libyuv. Native rec
| `libavif-webp-logo-difference-weighted-compound` | Difference-weighted compound prediction with both mask orientations | | `libavif-webp-logo-difference-weighted-compound` | Difference-weighted compound prediction with both mask orientations |
| `libavif-webp-logo-inter-intra` | Smooth and wedge inter-intra prediction | | `libavif-webp-logo-inter-intra` | Smooth and wedge inter-intra prediction |
| `libavif-webp-logo-obmc` | Overlapping motion compensation through a 19-frame dependent sequence | | `libavif-webp-logo-obmc` | Overlapping motion compensation through a 19-frame dependent sequence |
| `libavif-webp-logo-scaled-reference` | A 40x40 retained reference scaled into an 80x80 dependent frame |
| `libavif-rotating-grid-local-warp` | Multi-sample local affine projection and warped prediction through a two-frame dependent sequence | | `libavif-rotating-grid-local-warp` | Multi-sample local affine projection and warped prediction through a two-frame dependent sequence |
| `libavif-rotating-grid-global-warp` | Non-translational rotation/zoom GLOBALMV prediction through a two-frame dependent sequence | | `libavif-rotating-grid-global-warp` | Non-translational rotation/zoom GLOBALMV prediction through a two-frame dependent sequence |
@ -194,6 +195,38 @@ PNG's SHA-256 is
`D2CB388C9092EF17C4F0382C0150DD30D6F9D0EE247FF45AB5D7D4D312CEB23C`; its pixels were not changed `D2CB388C9092EF17C4F0382C0150DD30D6F9D0EE247FF45AB5D7D4D312CEB23C`; its pixels were not changed
when its contract-derived filename was updated with the test name. when its contract-derived filename was updated with the test name.
## Scaled-reference fixture
The `libavif-webp-logo-scaled-reference.avif` and
`libavif-webp-logo-scaled-reference-lsel0.avif` files are retained solely as interoperability inputs.
Their shared 2,195-byte `.bit` payload contains a 701-byte independent base layer followed by the
dependent layer; no container implementation is used as an AV1 arithmetic reference.
On 2026-08-31 the payload was decoded with `aomdec` from the freshly updated clean checkout of current
official libaom `main`, observed at `441c439b9916474cac15d2822af47a9ad70674a8`:
```text
aomdec --codec=av1 --threads=1 --row-mt=0 --all-layers --rawvideo --output-bit-depth=8 -o current-libaom-scaled-reference-all.yuv libavif-webp-logo-scaled-reference.bit
```
Current libaom produced a 40x40 YUV444 base frame and an 80x80 YUV444 dependent frame. The combined
24,000 native samples have SHA-256
`DD219E41B52C6C9343A92CD0A2D451DF57B73B25F10124811675B4CB2F8D666F`. The base frame matches
`libavif-webp-logo-scaled-reference-base-libaom.yuv` exactly, and the dependent frame matches the native
planes in `libavif-webp-logo-scaled-reference-libaom.y4m` exactly, with zero differing samples.
The production tests require the retained 40x40 base and the 80x80 dependent reconstruction, compare both
native frames exactly, compare the selected base layer and final RGBA presentation through ImageSharp's
established reference-output API, and repeat both layer selections with constrained tracked allocation.
The scaled-prediction FeatureTestRunner tests cover native and no-round compound output at 8, 10, and
12 bits, and a complete `Av1BlockDecoder.DecodeBlock()` test proves that scaled compound references remain
in the no-round intermediate domain until the final blend.
The two presentation PNGs were renamed with their current-libaom test contracts without changing their
bytes. Their SHA-256 values remain
`DC4C6DBE6BD92C5FCE1E3E23700AFA603EF04ED02EDD336213EBBA1E3BD84BA0` and
`678C5E5D4650EA6F0C590302E7DB9E3C6608851BC577453DA4A6837BDB4D3AF3`.
## Local warped-motion fixture ## Local warped-motion fixture
The `libavif-rotating-grid-local-warp.avif` fixture was encoded from a deterministic two-frame 256x256 limited-range YUV444 source. The source combines checkerboard, ring, and chroma-gradient detail; its second frame rotates the first by 2.5 degrees with nearest-neighbor sampling and edge clamping. The two-frame source Y4M SHA-256 is `82C1468C95C996B05165590417184F59373D67896F8C7B0EB398582C29C9C7A7`. Pinned scalar libavif and libaom generated the fixture and references with: The `libavif-rotating-grid-local-warp.avif` fixture was encoded from a deterministic two-frame 256x256 limited-range YUV444 source. The source combines checkerboard, ring, and chroma-gradient detail; its second frame rotates the first by 2.5 degrees with nearest-neighbor sampling and edge clamping. The two-frame source Y4M SHA-256 is `82C1468C95C996B05165590417184F59373D67896F8C7B0EB398582C29C9C7A7`. Pinned scalar libavif and libaom generated the fixture and references with:

Loading…
Cancel
Save