mirror of https://github.com/SixLabors/ImageSharp
18 changed files with 1846 additions and 57 deletions
@ -0,0 +1,449 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using System.Runtime.Intrinsics; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; |
|||
|
|||
/// <content>
|
|||
/// Reconstructs reference-scaled inter prediction through variable-phase separable convolution.
|
|||
/// </content>
|
|||
internal static partial class Av1InterPredictor |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the scratch capacity required by one scaled prediction block.
|
|||
/// </summary>
|
|||
public static int GetScaledScratchLength(int width, int height, int verticalPhase, int verticalStep) |
|||
{ |
|||
int intermediateHeight = ((((height - 1) * verticalStep) + verticalPhase) >> Av1ReferenceScale.SubpixelBits) + FilterCoefficientCount; |
|||
return Math.Max(width, Vector128<short>.Count) * intermediateHeight; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a dimension-only upper bound for one scaled prediction block's scratch capacity.
|
|||
/// </summary>
|
|||
public static int GetMaximumScaledScratchLength(int width, int height) |
|||
=> Math.Max(width, Vector128<short>.Count) * ((height * 2) + FilterCoefficientCount); |
|||
|
|||
/// <summary>
|
|||
/// Reconstructs an 8-bit scaled prediction using variable source positions and phases.
|
|||
/// </summary>
|
|||
public static void PredictScaled( |
|||
ReadOnlySpan<byte> source, |
|||
int sourceStride, |
|||
int sourceOrigin, |
|||
Span<byte> destination, |
|||
int destinationStride, |
|||
int width, |
|||
int height, |
|||
Av1InterpolationFilter horizontalFilter, |
|||
Av1InterpolationFilter verticalFilter, |
|||
int horizontalPhase, |
|||
int horizontalStep, |
|||
int verticalPhase, |
|||
int verticalStep, |
|||
Span<short> scratch) |
|||
=> DispatchScaled<byte, ScaledByteOperator>( |
|||
source, |
|||
sourceStride, |
|||
sourceOrigin, |
|||
destination, |
|||
destinationStride, |
|||
width, |
|||
height, |
|||
horizontalFilter, |
|||
verticalFilter, |
|||
horizontalPhase, |
|||
horizontalStep, |
|||
verticalPhase, |
|||
verticalStep, |
|||
8, |
|||
scratch); |
|||
|
|||
/// <summary>
|
|||
/// Reconstructs an 8-, 10-, or 12-bit scaled prediction using variable source positions and phases.
|
|||
/// </summary>
|
|||
public static void PredictScaled( |
|||
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, ScaledUInt16Operator>( |
|||
source, |
|||
sourceStride, |
|||
sourceOrigin, |
|||
destination, |
|||
destinationStride, |
|||
width, |
|||
height, |
|||
horizontalFilter, |
|||
verticalFilter, |
|||
horizontalPhase, |
|||
horizontalStep, |
|||
verticalPhase, |
|||
verticalStep, |
|||
bitDepth, |
|||
scratch); |
|||
|
|||
/// <summary>
|
|||
/// Selects the horizontal filter family for scaled prediction.
|
|||
/// </summary>
|
|||
private static void DispatchScaled<T, TSample>( |
|||
ReadOnlySpan<T> source, |
|||
int sourceStride, |
|||
int sourceOrigin, |
|||
Span<T> destination, |
|||
int destinationStride, |
|||
int width, |
|||
int height, |
|||
Av1InterpolationFilter horizontalFilter, |
|||
Av1InterpolationFilter verticalFilter, |
|||
int horizontalPhase, |
|||
int horizontalStep, |
|||
int verticalPhase, |
|||
int verticalStep, |
|||
int bitDepth, |
|||
Span<short> scratch) |
|||
where T : unmanaged |
|||
where TSample : struct, IScaledSampleOperator<T> |
|||
{ |
|||
switch (horizontalFilter) |
|||
{ |
|||
case Av1InterpolationFilter.Regular: |
|||
DispatchScaledVertical<T, TSample, RegularOperator>( |
|||
source, |
|||
sourceStride, |
|||
sourceOrigin, |
|||
destination, |
|||
destinationStride, |
|||
width, |
|||
height, |
|||
verticalFilter, |
|||
horizontalPhase, |
|||
horizontalStep, |
|||
verticalPhase, |
|||
verticalStep, |
|||
bitDepth, |
|||
scratch); |
|||
|
|||
break; |
|||
case Av1InterpolationFilter.Smooth: |
|||
DispatchScaledVertical<T, TSample, SmoothOperator>( |
|||
source, |
|||
sourceStride, |
|||
sourceOrigin, |
|||
destination, |
|||
destinationStride, |
|||
width, |
|||
height, |
|||
verticalFilter, |
|||
horizontalPhase, |
|||
horizontalStep, |
|||
verticalPhase, |
|||
verticalStep, |
|||
bitDepth, |
|||
scratch); |
|||
|
|||
break; |
|||
case Av1InterpolationFilter.Sharp: |
|||
DispatchScaledVertical<T, TSample, SharpOperator>( |
|||
source, |
|||
sourceStride, |
|||
sourceOrigin, |
|||
destination, |
|||
destinationStride, |
|||
width, |
|||
height, |
|||
verticalFilter, |
|||
horizontalPhase, |
|||
horizontalStep, |
|||
verticalPhase, |
|||
verticalStep, |
|||
bitDepth, |
|||
scratch); |
|||
|
|||
break; |
|||
default: |
|||
DispatchScaledVertical<T, TSample, BilinearOperator>( |
|||
source, |
|||
sourceStride, |
|||
sourceOrigin, |
|||
destination, |
|||
destinationStride, |
|||
width, |
|||
height, |
|||
verticalFilter, |
|||
horizontalPhase, |
|||
horizontalStep, |
|||
verticalPhase, |
|||
verticalStep, |
|||
bitDepth, |
|||
scratch); |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Selects the vertical filter family for a closed horizontal scaled-prediction operator.
|
|||
/// </summary>
|
|||
private static void DispatchScaledVertical<T, TSample, THorizontal>( |
|||
ReadOnlySpan<T> source, |
|||
int sourceStride, |
|||
int sourceOrigin, |
|||
Span<T> destination, |
|||
int destinationStride, |
|||
int width, |
|||
int height, |
|||
Av1InterpolationFilter verticalFilter, |
|||
int horizontalPhase, |
|||
int horizontalStep, |
|||
int verticalPhase, |
|||
int verticalStep, |
|||
int bitDepth, |
|||
Span<short> scratch) |
|||
where T : unmanaged |
|||
where TSample : struct, IScaledSampleOperator<T> |
|||
where THorizontal : struct, IAv1InterPredictorOperator |
|||
{ |
|||
switch (verticalFilter) |
|||
{ |
|||
case Av1InterpolationFilter.Regular: |
|||
PredictScaled<T, TSample, THorizontal, RegularOperator>( |
|||
source, |
|||
sourceStride, |
|||
sourceOrigin, |
|||
destination, |
|||
destinationStride, |
|||
width, |
|||
height, |
|||
horizontalPhase, |
|||
horizontalStep, |
|||
verticalPhase, |
|||
verticalStep, |
|||
bitDepth, |
|||
scratch); |
|||
|
|||
break; |
|||
case Av1InterpolationFilter.Smooth: |
|||
PredictScaled<T, TSample, THorizontal, SmoothOperator>( |
|||
source, |
|||
sourceStride, |
|||
sourceOrigin, |
|||
destination, |
|||
destinationStride, |
|||
width, |
|||
height, |
|||
horizontalPhase, |
|||
horizontalStep, |
|||
verticalPhase, |
|||
verticalStep, |
|||
bitDepth, |
|||
scratch); |
|||
|
|||
break; |
|||
case Av1InterpolationFilter.Sharp: |
|||
PredictScaled<T, TSample, THorizontal, SharpOperator>( |
|||
source, |
|||
sourceStride, |
|||
sourceOrigin, |
|||
destination, |
|||
destinationStride, |
|||
width, |
|||
height, |
|||
horizontalPhase, |
|||
horizontalStep, |
|||
verticalPhase, |
|||
verticalStep, |
|||
bitDepth, |
|||
scratch); |
|||
|
|||
break; |
|||
default: |
|||
PredictScaled<T, TSample, THorizontal, BilinearOperator>( |
|||
source, |
|||
sourceStride, |
|||
sourceOrigin, |
|||
destination, |
|||
destinationStride, |
|||
width, |
|||
height, |
|||
horizontalPhase, |
|||
horizontalStep, |
|||
verticalPhase, |
|||
verticalStep, |
|||
bitDepth, |
|||
scratch); |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Applies variable-phase horizontal filtering followed by variable-phase vertical filtering.
|
|||
/// </summary>
|
|||
private static void PredictScaled<T, TSample, THorizontal, TVertical>( |
|||
ReadOnlySpan<T> source, |
|||
int sourceStride, |
|||
int sourceOrigin, |
|||
Span<T> destination, |
|||
int destinationStride, |
|||
int width, |
|||
int height, |
|||
int horizontalPhase, |
|||
int horizontalStep, |
|||
int verticalPhase, |
|||
int verticalStep, |
|||
int bitDepth, |
|||
Span<short> scratch) |
|||
where T : unmanaged |
|||
where TSample : struct, IScaledSampleOperator<T> |
|||
where THorizontal : struct, IAv1InterPredictorOperator |
|||
where TVertical : struct, IAv1InterPredictorOperator |
|||
{ |
|||
ref T sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); |
|||
ref T destinationBase = ref MemoryMarshal.GetReference(destination); |
|||
ref short scratchBase = ref MemoryMarshal.GetReference(scratch); |
|||
int scratchStride = Math.Max(width, Vector128<short>.Count); |
|||
int intermediateHeight = ((((height - 1) * verticalStep) + verticalPhase) >> Av1ReferenceScale.SubpixelBits) + FilterCoefficientCount; |
|||
int horizontalBias = 1 << (bitDepth + FilterBits - 1); |
|||
int intermediateRange = bitDepth + FilterBits - Round0Bits + 2; |
|||
int round0 = Round0Bits + Math.Max(intermediateRange - 16, 0); |
|||
bool useReducedHorizontalFilter = width <= 4; |
|||
bool useReducedVerticalFilter = height <= 4; |
|||
|
|||
// Scaled positions change both the integer source sample and filter phase at each output column. Four-lane
|
|||
// vectors gather those independent positions into one multiply-accumulate chain without allocating an index map.
|
|||
for (int row = 0; row < intermediateHeight; row++) |
|||
{ |
|||
ref T sourceRow = ref Unsafe.Add(ref sourceBase, (row - 3) * sourceStride); |
|||
ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); |
|||
int column = 0; |
|||
if (Vector128.IsHardwareAccelerated) |
|||
{ |
|||
for (; column <= width - Vector128<int>.Count; column += Vector128<int>.Count) |
|||
{ |
|||
int position0 = horizontalPhase + (column * horizontalStep); |
|||
int position1 = position0 + horizontalStep; |
|||
int position2 = position1 + horizontalStep; |
|||
int position3 = position2 + horizontalStep; |
|||
int source0 = (position0 >> Av1ReferenceScale.SubpixelBits) - 3; |
|||
int source1 = (position1 >> Av1ReferenceScale.SubpixelBits) - 3; |
|||
int source2 = (position2 >> Av1ReferenceScale.SubpixelBits) - 3; |
|||
int source3 = (position3 >> Av1ReferenceScale.SubpixelBits) - 3; |
|||
ReadOnlySpan<short> coefficients0 = THorizontal.GetCoefficients((position0 & Av1ReferenceScale.SubpixelMask) >> 6, useReducedHorizontalFilter); |
|||
ReadOnlySpan<short> coefficients1 = THorizontal.GetCoefficients((position1 & Av1ReferenceScale.SubpixelMask) >> 6, useReducedHorizontalFilter); |
|||
ReadOnlySpan<short> coefficients2 = THorizontal.GetCoefficients((position2 & Av1ReferenceScale.SubpixelMask) >> 6, useReducedHorizontalFilter); |
|||
ReadOnlySpan<short> coefficients3 = THorizontal.GetCoefficients((position3 & Av1ReferenceScale.SubpixelMask) >> 6, useReducedHorizontalFilter); |
|||
Vector128<int> result = Vector128.Create(horizontalBias); |
|||
for (int tap = 0; tap < FilterCoefficientCount; tap++) |
|||
{ |
|||
Vector128<int> samples = Vector128.Create( |
|||
TSample.Load(ref sourceRow, source0 + tap), |
|||
TSample.Load(ref sourceRow, source1 + tap), |
|||
TSample.Load(ref sourceRow, source2 + tap), |
|||
TSample.Load(ref sourceRow, source3 + tap)); |
|||
|
|||
Vector128<int> coefficients = Vector128.Create( |
|||
(int)coefficients0[tap], |
|||
coefficients1[tap], |
|||
coefficients2[tap], |
|||
coefficients3[tap]); |
|||
|
|||
result += samples * coefficients; |
|||
} |
|||
|
|||
Vector64<short> intermediate = Av1IntraPredictorBase.Narrow( |
|||
RoundPowerOfTwo(result, round0), |
|||
Vector128<int>.Zero).GetLower(); |
|||
|
|||
intermediate.StoreUnsafe(ref scratchRow, (nuint)column); |
|||
} |
|||
} |
|||
|
|||
for (; column < width; column++) |
|||
{ |
|||
int position = horizontalPhase + (column * horizontalStep); |
|||
int sourceColumn = (position >> Av1ReferenceScale.SubpixelBits) - 3; |
|||
ReadOnlySpan<short> coefficients = THorizontal.GetCoefficients( |
|||
(position & Av1ReferenceScale.SubpixelMask) >> 6, |
|||
useReducedHorizontalFilter); |
|||
|
|||
int sum = horizontalBias; |
|||
for (int tap = 0; tap < FilterCoefficientCount; tap++) |
|||
{ |
|||
sum += coefficients[tap] * TSample.Load(ref sourceRow, sourceColumn + tap); |
|||
} |
|||
|
|||
Unsafe.Add(ref scratchRow, column) = (short)RoundPowerOfTwo(sum, round0); |
|||
} |
|||
} |
|||
|
|||
int round1 = (2 * FilterBits) - round0; |
|||
int offsetBits = bitDepth + (2 * FilterBits) - round0; |
|||
int verticalBias = 1 << offsetBits; |
|||
int roundOffset = (1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1)); |
|||
for (int row = 0; row < height; row++) |
|||
{ |
|||
int position = verticalPhase + (row * verticalStep); |
|||
int sourceRowIndex = position >> Av1ReferenceScale.SubpixelBits; |
|||
ReadOnlySpan<short> coefficients = TVertical.GetCoefficients( |
|||
(position & Av1ReferenceScale.SubpixelMask) >> 6, |
|||
useReducedVerticalFilter); |
|||
|
|||
ref short scratchRow = ref Unsafe.Add(ref scratchBase, sourceRowIndex * scratchStride); |
|||
ref short coefficientBase = ref MemoryMarshal.GetReference(coefficients); |
|||
ref T destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); |
|||
int column = 0; |
|||
if (Vector128.IsHardwareAccelerated) |
|||
{ |
|||
Vector128<int> initial = Vector128.Create(verticalBias); |
|||
Vector128<int> offset = Vector128.Create(roundOffset); |
|||
for (; column <= width - Vector128<short>.Count; column += Vector128<short>.Count) |
|||
{ |
|||
Convolve( |
|||
ref scratchRow, |
|||
scratchStride, |
|||
(nuint)column, |
|||
ref coefficientBase, |
|||
FilterCoefficientCount, |
|||
initial, |
|||
out Vector128<int> result0, |
|||
out Vector128<int> result1); |
|||
|
|||
result0 = RoundPowerOfTwo(result0, round1) - offset; |
|||
result1 = RoundPowerOfTwo(result1, round1) - offset; |
|||
TSample.StoreVector(ref destinationRow, column, result0, result1, bitDepth); |
|||
} |
|||
} |
|||
|
|||
for (; column < width; column++) |
|||
{ |
|||
int sum = verticalBias + ConvolveScalar( |
|||
ref Unsafe.Add(ref scratchRow, column), |
|||
scratchStride, |
|||
ref coefficientBase, |
|||
FilterCoefficientCount); |
|||
|
|||
TSample.StoreScalar( |
|||
ref destinationRow, |
|||
column, |
|||
RoundPowerOfTwo(sum, round1) - roundOffset, |
|||
bitDepth); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,105 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.Intrinsics; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; |
|||
|
|||
/// <content>
|
|||
/// Defines sample-storage operators for reference-scaled prediction.
|
|||
/// </content>
|
|||
internal static partial class Av1InterPredictor |
|||
{ |
|||
/// <summary>
|
|||
/// Supplies sample loading, clipping, and storage for a scaled predictor pipeline.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The native sample storage type.</typeparam>
|
|||
private interface IScaledSampleOperator<T> |
|||
where T : unmanaged |
|||
{ |
|||
/// <summary>
|
|||
/// Loads one source sample as a signed accumulator value.
|
|||
/// </summary>
|
|||
/// <param name="source">The first source sample.</param>
|
|||
/// <param name="index">The sample offset.</param>
|
|||
/// <returns>The widened sample value.</returns>
|
|||
public static abstract int Load(ref T source, int index); |
|||
|
|||
/// <summary>
|
|||
/// Clips and stores eight completed vector lanes.
|
|||
/// </summary>
|
|||
/// <param name="destination">The first destination sample.</param>
|
|||
/// <param name="index">The output offset.</param>
|
|||
/// <param name="result0">The first four completed lanes.</param>
|
|||
/// <param name="result1">The second four completed lanes.</param>
|
|||
/// <param name="bitDepth">The decoded sample precision.</param>
|
|||
public static abstract void StoreVector( |
|||
ref T destination, |
|||
int index, |
|||
Vector128<int> result0, |
|||
Vector128<int> result1, |
|||
int bitDepth); |
|||
|
|||
/// <summary>
|
|||
/// Clips and stores one completed scalar value.
|
|||
/// </summary>
|
|||
/// <param name="destination">The first destination sample.</param>
|
|||
/// <param name="index">The output offset.</param>
|
|||
/// <param name="value">The completed sample value.</param>
|
|||
/// <param name="bitDepth">The decoded sample precision.</param>
|
|||
public static abstract void StoreScalar(ref T destination, int index, int value, int bitDepth); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Implements scaled prediction storage for 8-bit samples.
|
|||
/// </summary>
|
|||
private readonly struct ScaledByteOperator : IScaledSampleOperator<byte> |
|||
{ |
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static int Load(ref byte source, int index) => Unsafe.Add(ref source, index); |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static void StoreVector( |
|||
ref byte destination, |
|||
int index, |
|||
Vector128<int> result0, |
|||
Vector128<int> result1, |
|||
int bitDepth) |
|||
=> PackBytes(result0, result1, Vector128<int>.Zero, Vector128<int>.Zero) |
|||
.GetLower() |
|||
.StoreUnsafe(ref destination, (nuint)index); |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static void StoreScalar(ref byte destination, int index, int value, int bitDepth) |
|||
=> Unsafe.Add(ref destination, index) = (byte)Math.Clamp(value, byte.MinValue, byte.MaxValue); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Implements scaled prediction storage for 8-, 10-, and 12-bit samples.
|
|||
/// </summary>
|
|||
private readonly struct ScaledUInt16Operator : IScaledSampleOperator<ushort> |
|||
{ |
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static int Load(ref ushort source, int index) => Unsafe.Add(ref source, index); |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static void StoreVector( |
|||
ref ushort destination, |
|||
int index, |
|||
Vector128<int> result0, |
|||
Vector128<int> result1, |
|||
int bitDepth) |
|||
=> PackHighBitDepth(result0, result1, (1 << bitDepth) - 1).StoreUnsafe(ref destination, (nuint)index); |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static void StoreScalar(ref ushort destination, int index, int value, int bitDepth) |
|||
=> Unsafe.Add(ref destination, index) = (ushort)Math.Clamp(value, 0, (1 << bitDepth) - 1); |
|||
} |
|||
} |
|||
@ -0,0 +1,94 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; |
|||
|
|||
/// <summary>
|
|||
/// Converts current-frame prediction coordinates into a retained reference frame's sample grid.
|
|||
/// </summary>
|
|||
internal readonly struct Av1ReferenceScale |
|||
{ |
|||
/// <summary>
|
|||
/// The identity scale in the normative Q14 representation.
|
|||
/// </summary>
|
|||
private const int IdentityScale = 1 << 14; |
|||
|
|||
/// <summary>
|
|||
/// The number of fractional bits carried by scaled prediction positions and steps.
|
|||
/// </summary>
|
|||
public const int SubpixelBits = 10; |
|||
|
|||
/// <summary>
|
|||
/// The mask selecting one scaled sample's fractional position.
|
|||
/// </summary>
|
|||
public const int SubpixelMask = (1 << SubpixelBits) - 1; |
|||
|
|||
/// <summary>
|
|||
/// The half-unit offset that centers Q4 input coordinates on the Q10 reference grid.
|
|||
/// </summary>
|
|||
public const int ExtraOffset = 1 << (SubpixelBits - 4 - 1); |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Av1ReferenceScale"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="referenceWidth">The retained reference width.</param>
|
|||
/// <param name="referenceHeight">The retained reference height.</param>
|
|||
/// <param name="currentWidth">The current coded-frame width.</param>
|
|||
/// <param name="currentHeight">The current coded-frame height.</param>
|
|||
public Av1ReferenceScale(int referenceWidth, int referenceHeight, int currentWidth, int currentHeight) |
|||
{ |
|||
this.HorizontalScale = ((referenceWidth << 14) + (currentWidth >> 1)) / currentWidth; |
|||
this.VerticalScale = ((referenceHeight << 14) + (currentHeight >> 1)) / currentHeight; |
|||
this.HorizontalStep = (this.HorizontalScale + 8) >> 4; |
|||
this.VerticalStep = (this.VerticalScale + 8) >> 4; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the horizontal Q14 scale factor.
|
|||
/// </summary>
|
|||
public int HorizontalScale { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the vertical Q14 scale factor.
|
|||
/// </summary>
|
|||
public int VerticalScale { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the horizontal per-output-sample step in Q10 reference samples.
|
|||
/// </summary>
|
|||
public int HorizontalStep { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the vertical per-output-sample step in Q10 reference samples.
|
|||
/// </summary>
|
|||
public int VerticalStep { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether either reference dimension differs from the current frame.
|
|||
/// </summary>
|
|||
public bool IsScaled => this.HorizontalScale != IdentityScale || this.VerticalScale != IdentityScale; |
|||
|
|||
/// <summary>
|
|||
/// Scales one horizontal Q4 current-frame coordinate into the Q10 reference grid.
|
|||
/// </summary>
|
|||
public int ScaleHorizontal(int value) => Scale(value, this.HorizontalScale); |
|||
|
|||
/// <summary>
|
|||
/// Scales one vertical Q4 current-frame coordinate into the Q10 reference grid.
|
|||
/// </summary>
|
|||
public int ScaleVertical(int value) => Scale(value, this.VerticalScale); |
|||
|
|||
/// <summary>
|
|||
/// Applies libaom's signed fixed-point rounding without relying on implementation-defined negative shifts.
|
|||
/// </summary>
|
|||
private static int Scale(int value, int scale) |
|||
{ |
|||
long offset = (scale - IdentityScale) * 8L; |
|||
long scaled = ((long)value * scale) + offset; |
|||
const int shift = 8; |
|||
const long rounding = 1L << (shift - 1); |
|||
return scaled < 0 |
|||
? (int)-((-scaled + rounding) >> shift) |
|||
: (int)((scaled + rounding) >> shift); |
|||
} |
|||
} |
|||
@ -0,0 +1,612 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; |
|||
using SixLabors.ImageSharp.Tests.TestUtilities; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
|||
|
|||
/// <summary>
|
|||
/// Verifies AV1 reference scaling and variable-phase inter convolution against an independent libaom-shaped oracle.
|
|||
/// </summary>
|
|||
[Trait("Format", "Avif")] |
|||
public class Av1ScaledInterPredictorTests |
|||
{ |
|||
/// <summary>
|
|||
/// The number of fractional bits in each interpolation coefficient.
|
|||
/// </summary>
|
|||
private const int FilterBits = 7; |
|||
|
|||
/// <summary>
|
|||
/// The ordinary first-pass rounding distance.
|
|||
/// </summary>
|
|||
private const int Round0Bits = 3; |
|||
|
|||
/// <summary>
|
|||
/// The number of samples in every stored interpolation row.
|
|||
/// </summary>
|
|||
private const int FilterTapCount = 8; |
|||
|
|||
/// <summary>
|
|||
/// The source border retained around the independently generated active coordinates.
|
|||
/// </summary>
|
|||
private const int SourcePadding = 16; |
|||
|
|||
/// <summary>
|
|||
/// The guarded destination elements before the active block.
|
|||
/// </summary>
|
|||
private const int DestinationPrefix = 11; |
|||
|
|||
/// <summary>
|
|||
/// The guarded destination elements after each active row.
|
|||
/// </summary>
|
|||
private const int DestinationRowPadding = 9; |
|||
|
|||
/// <summary>
|
|||
/// The guarded destination elements after the final row.
|
|||
/// </summary>
|
|||
private const int DestinationSuffix = 17; |
|||
|
|||
/// <summary>
|
|||
/// The byte value used to detect writes outside the active destination block.
|
|||
/// </summary>
|
|||
private const byte ByteSentinel = 0xD3; |
|||
|
|||
/// <summary>
|
|||
/// The ushort value used to detect writes outside the active destination block.
|
|||
/// </summary>
|
|||
private const ushort UInt16Sentinel = 0xDEAD; |
|||
|
|||
/// <summary>
|
|||
/// Exercises the native vector path and the complete scalar fallback in separate processes.
|
|||
/// </summary>
|
|||
private const HwIntrinsics PredictorConfigurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic; |
|||
|
|||
/// <summary>
|
|||
/// Verifies the pinned Q14 scale factors, Q10 steps, and signed coordinate rounding.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void ReferenceScaleMatchesPinnedLibaomFixedPointRules() |
|||
{ |
|||
Av1ReferenceScale downscaledReference = new(40, 24, 64, 48); |
|||
|
|||
Assert.True(downscaledReference.IsScaled); |
|||
Assert.Equal(10240, downscaledReference.HorizontalScale); |
|||
Assert.Equal(8192, downscaledReference.VerticalScale); |
|||
Assert.Equal(640, downscaledReference.HorizontalStep); |
|||
Assert.Equal(512, downscaledReference.VerticalStep); |
|||
Assert.Equal(ScaleCoordinate(37, 10240), downscaledReference.ScaleHorizontal(37)); |
|||
Assert.Equal(ScaleCoordinate(-37, 10240), downscaledReference.ScaleHorizontal(-37)); |
|||
|
|||
Av1ReferenceScale enlargedReference = new(96, 72, 64, 48); |
|||
|
|||
Assert.Equal(24576, enlargedReference.HorizontalScale); |
|||
Assert.Equal(24576, enlargedReference.VerticalScale); |
|||
Assert.Equal(1536, enlargedReference.HorizontalStep); |
|||
Assert.Equal(1536, enlargedReference.VerticalStep); |
|||
|
|||
Av1ReferenceScale identity = new(64, 48, 64, 48); |
|||
|
|||
Assert.False(identity.IsScaled); |
|||
Assert.Equal(1024, identity.HorizontalStep); |
|||
Assert.Equal(1024, identity.VerticalStep); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies exact scaled 8-bit output, variable filter phases, vector tails, and untouched destination padding.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void BytePredictionMatchesLibaomOracleAcrossIntrinsicConfigurations() |
|||
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateBytePredictions, PredictorConfigurations); |
|||
|
|||
/// <summary>
|
|||
/// Verifies exact scaled 8-, 10-, and 12-bit output under the native vector and scalar configurations.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void HighBitDepthPredictionMatchesLibaomOracleAcrossIntrinsicConfigurations() |
|||
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthPredictions, PredictorConfigurations); |
|||
|
|||
/// <summary>
|
|||
/// Applies each scaled-prediction scenario to byte storage.
|
|||
/// </summary>
|
|||
private static void ValidateBytePredictions() |
|||
{ |
|||
foreach (ScaledPredictionCase testCase in CreatePredictionCases()) |
|||
{ |
|||
byte[] source = CreateByteSource(testCase, out int sourceStride, out int sourceOrigin); |
|||
int destinationStride = testCase.Width + DestinationRowPadding; |
|||
byte[] expected = CreateByteDestination(testCase, destinationStride); |
|||
byte[] actual = (byte[])expected.Clone(); |
|||
short[] scratch = new short[ |
|||
Av1InterPredictor.GetScaledScratchLength( |
|||
testCase.Width, |
|||
testCase.Height, |
|||
testCase.VerticalPhase, |
|||
testCase.VerticalStep)]; |
|||
|
|||
ApplyReference(source, sourceStride, sourceOrigin, expected, destinationStride, testCase, 8); |
|||
|
|||
Av1InterPredictor.PredictScaled( |
|||
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-prediction scenario to every supported high-bit-depth precision.
|
|||
/// </summary>
|
|||
private static void ValidateHighBitDepthPredictions() |
|||
{ |
|||
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[ |
|||
Av1InterPredictor.GetScaledScratchLength( |
|||
testCase.Width, |
|||
testCase.Height, |
|||
testCase.VerticalPhase, |
|||
testCase.VerticalStep)]; |
|||
|
|||
ApplyReference(source, sourceStride, sourceOrigin, expected, destinationStride, testCase, bitDepth); |
|||
|
|||
Av1InterPredictor.PredictScaled( |
|||
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>
|
|||
/// Creates cases covering variable phases, every filter family, reduced kernels, and vector tails.
|
|||
/// </summary>
|
|||
private static ScaledPredictionCase[] CreatePredictionCases() => |
|||
[ |
|||
new("fixture-regular-8x8", 8, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 800, 512, 800, 512), |
|||
new("fixture-regular-4x8", 4, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 800, 512, 800, 512), |
|||
new("fixture-regular-8x4", 8, 4, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 800, 512, 800, 512), |
|||
new("bilinear-variable-phase", 13, 9, Av1InterpolationFilter.Bilinear, Av1InterpolationFilter.Bilinear, 192, 1536, 512, 640), |
|||
new("regular-smooth-wide", 20, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Smooth, 64, 2048, 448, 2048), |
|||
new("sharp-bilinear-tail", 12, 5, Av1InterpolationFilter.Sharp, Av1InterpolationFilter.Bilinear, 512, 2048, 192, 2048), |
|||
new("reduced-regular", 4, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Smooth, 192, 2048, 448, 2048), |
|||
new("reduced-sharp-maps-to-regular", 4, 8, Av1InterpolationFilter.Sharp, Av1InterpolationFilter.Smooth, 192, 2048, 448, 2048), |
|||
new("reduced-smooth", 8, 4, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Smooth, 64, 2048, 832, 2048) |
|||
]; |
|||
|
|||
/// <summary>
|
|||
/// Creates deterministic padded byte source storage for one prediction case.
|
|||
/// </summary>
|
|||
private static byte[] CreateByteSource(ScaledPredictionCase testCase, out int stride, out int origin) |
|||
{ |
|||
GetSourceGeometry(testCase, out int width, out int height); |
|||
stride = width; |
|||
origin = (SourcePadding * stride) + SourcePadding; |
|||
byte[] source = new byte[width * height]; |
|||
for (int row = 0; row < height; row++) |
|||
{ |
|||
for (int column = 0; column < width; column++) |
|||
{ |
|||
source[(row * stride) + column] = (byte)(((row * 29) + (column * 47) + (row * column * 3)) & byte.MaxValue); |
|||
} |
|||
} |
|||
|
|||
return source; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates deterministic padded ushort source storage for one prediction case.
|
|||
/// </summary>
|
|||
private static ushort[] CreateUInt16Source(ScaledPredictionCase testCase, int bitDepth, out int stride, out int origin) |
|||
{ |
|||
GetSourceGeometry(testCase, out int width, out int height); |
|||
stride = width; |
|||
origin = (SourcePadding * stride) + SourcePadding; |
|||
int maximum = (1 << bitDepth) - 1; |
|||
ushort[] source = new ushort[width * height]; |
|||
for (int row = 0; row < height; row++) |
|||
{ |
|||
for (int column = 0; column < width; column++) |
|||
{ |
|||
source[(row * stride) + column] = (ushort)(((row * 269) + (column * 443) + (row * column * 31)) & maximum); |
|||
} |
|||
} |
|||
|
|||
return source; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Computes storage dimensions that keep every requested eight-tap read inside the test source.
|
|||
/// </summary>
|
|||
private static void GetSourceGeometry(ScaledPredictionCase testCase, out int width, out int height) |
|||
{ |
|||
int maximumHorizontalPosition = testCase.HorizontalPhase + ((testCase.Width - 1) * testCase.HorizontalStep); |
|||
int maximumVerticalPosition = testCase.VerticalPhase + ((testCase.Height - 1) * testCase.VerticalStep); |
|||
width = (2 * SourcePadding) + (maximumHorizontalPosition >> Av1ReferenceScale.SubpixelBits) + FilterTapCount; |
|||
height = (2 * SourcePadding) + (maximumVerticalPosition >> Av1ReferenceScale.SubpixelBits) + FilterTapCount; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a guarded byte destination initialized to its sentinel.
|
|||
/// </summary>
|
|||
private static byte[] CreateByteDestination(ScaledPredictionCase testCase, int stride) |
|||
{ |
|||
byte[] destination = new byte[DestinationPrefix + (stride * testCase.Height) + DestinationSuffix]; |
|||
Array.Fill(destination, ByteSentinel); |
|||
return destination; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a guarded ushort destination initialized to its sentinel.
|
|||
/// </summary>
|
|||
private static ushort[] CreateUInt16Destination(ScaledPredictionCase testCase, int stride) |
|||
{ |
|||
ushort[] destination = new ushort[DestinationPrefix + (stride * testCase.Height) + DestinationSuffix]; |
|||
Array.Fill(destination, UInt16Sentinel); |
|||
return destination; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Applies the independent variable-phase two-pass reference convolution to byte storage.
|
|||
/// </summary>
|
|||
private static void ApplyReference( |
|||
byte[] source, |
|||
int sourceStride, |
|||
int sourceOrigin, |
|||
byte[] 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); |
|||
} |
|||
} |
|||
|
|||
WriteReference(intermediate, intermediateStride, destination, destinationStride, testCase, bitDepth); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Applies the independent variable-phase two-pass reference convolution to ushort storage.
|
|||
/// </summary>
|
|||
private static void ApplyReference( |
|||
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); |
|||
} |
|||
} |
|||
|
|||
WriteReference(intermediate, intermediateStride, destination, destinationStride, testCase, bitDepth); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Allocates the oracle's independently shaped intermediate block.
|
|||
/// </summary>
|
|||
private static short[] CreateIntermediate(ScaledPredictionCase testCase) |
|||
{ |
|||
int height = ((((testCase.Height - 1) * testCase.VerticalStep) + testCase.VerticalPhase) >> Av1ReferenceScale.SubpixelBits) + FilterTapCount; |
|||
return new short[testCase.Width * height]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Completes byte output from the horizontally filtered intermediate block.
|
|||
/// </summary>
|
|||
private static void WriteReference( |
|||
short[] intermediate, |
|||
int intermediateStride, |
|||
byte[] destination, |
|||
int destinationStride, |
|||
ScaledPredictionCase testCase, |
|||
int bitDepth) |
|||
{ |
|||
int maximum = byte.MaxValue; |
|||
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 value = FinishConvolution(intermediate, (sourceRow * intermediateStride) + column, intermediateStride, coefficients, bitDepth); |
|||
destination[DestinationPrefix + (row * destinationStride) + column] = (byte)Math.Clamp(value, 0, maximum); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Completes high-bit-depth output from the horizontally filtered intermediate block.
|
|||
/// </summary>
|
|||
private static void WriteReference( |
|||
short[] intermediate, |
|||
int intermediateStride, |
|||
ushort[] destination, |
|||
int destinationStride, |
|||
ScaledPredictionCase testCase, |
|||
int bitDepth) |
|||
{ |
|||
int maximum = (1 << bitDepth) - 1; |
|||
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 value = FinishConvolution(intermediate, (sourceRow * intermediateStride) + column, intermediateStride, coefficients, bitDepth); |
|||
destination[DestinationPrefix + (row * destinationStride) + column] = (ushort)Math.Clamp(value, 0, maximum); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes both normative convolution biases after the vertical pass.
|
|||
/// </summary>
|
|||
private static int FinishConvolution( |
|||
short[] intermediate, |
|||
int sourceIndex, |
|||
int sourceStride, |
|||
ReadOnlySpan<short> coefficients, |
|||
int bitDepth) |
|||
{ |
|||
int round0 = GetRound0Bits(bitDepth); |
|||
int round1 = (2 * FilterBits) - round0; |
|||
int offsetBits = bitDepth + (2 * FilterBits) - round0; |
|||
int verticalBias = 1 << offsetBits; |
|||
int roundOffset = (1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1)); |
|||
int sum = verticalBias + Convolve(intermediate, sourceIndex, sourceStride, coefficients); |
|||
return RoundPowerOfTwo(sum, round1) - roundOffset; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Computes one byte convolution sum.
|
|||
/// </summary>
|
|||
private static int Convolve(byte[] source, int sourceIndex, ReadOnlySpan<short> coefficients) |
|||
{ |
|||
int sum = 0; |
|||
for (int tap = 0; tap < FilterTapCount; tap++) |
|||
{ |
|||
sum += source[sourceIndex + tap] * coefficients[tap]; |
|||
} |
|||
|
|||
return sum; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Computes one ushort convolution sum.
|
|||
/// </summary>
|
|||
private static int Convolve(ushort[] source, int sourceIndex, ReadOnlySpan<short> coefficients) |
|||
{ |
|||
int sum = 0; |
|||
for (int tap = 0; tap < FilterTapCount; tap++) |
|||
{ |
|||
sum += source[sourceIndex + tap] * coefficients[tap]; |
|||
} |
|||
|
|||
return sum; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Computes one vertical convolution sum from the biased intermediate block.
|
|||
/// </summary>
|
|||
private static int Convolve(short[] source, int sourceIndex, int sourceStride, ReadOnlySpan<short> coefficients) |
|||
{ |
|||
int sum = 0; |
|||
for (int tap = 0; tap < FilterTapCount; tap++) |
|||
{ |
|||
sum += source[sourceIndex + (tap * sourceStride)] * coefficients[tap]; |
|||
} |
|||
|
|||
return sum; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Selects one pinned coefficient row without reading production filter storage.
|
|||
/// </summary>
|
|||
private static void FillCoefficients(Av1InterpolationFilter filter, int phase, bool reduced, Span<short> destination) |
|||
{ |
|||
destination.Clear(); |
|||
if (filter == Av1InterpolationFilter.Bilinear) |
|||
{ |
|||
destination[3] = (short)(128 - (phase * 8)); |
|||
destination[4] = (short)(phase * 8); |
|||
return; |
|||
} |
|||
|
|||
if (reduced && filter == Av1InterpolationFilter.Sharp) |
|||
{ |
|||
filter = Av1InterpolationFilter.Regular; |
|||
} |
|||
|
|||
ReadOnlySpan<short> source = (filter, reduced, phase) switch |
|||
{ |
|||
(Av1InterpolationFilter.Regular, false, 1) => [0, 2, -6, 126, 8, -2, 0, 0], |
|||
(Av1InterpolationFilter.Regular, false, 4) => [0, 2, -14, 110, 38, -10, 2, 0], |
|||
(Av1InterpolationFilter.Regular, false, 12) => [0, 2, -10, 38, 110, -14, 2, 0], |
|||
(Av1InterpolationFilter.Smooth, false, 7) => [0, -2, 16, 54, 48, 12, 0, 0], |
|||
(Av1InterpolationFilter.Sharp, false, 8) => [-4, 12, -24, 80, 80, -24, 12, -4], |
|||
(Av1InterpolationFilter.Regular, true, 3) => [0, 0, -10, 116, 28, -6, 0, 0], |
|||
(Av1InterpolationFilter.Regular, true, 4) => [0, 0, -12, 110, 38, -8, 0, 0], |
|||
(Av1InterpolationFilter.Regular, true, 12) => [0, 0, -8, 38, 110, -12, 0, 0], |
|||
(Av1InterpolationFilter.Smooth, true, 13) => [0, 0, 4, 40, 62, 22, 0, 0], |
|||
_ => throw new InvalidOperationException($"The scaled oracle has no row for {filter}, phase {phase}, reduced {reduced}.") |
|||
}; |
|||
|
|||
source.CopyTo(destination); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Computes libaom's bit-depth-dependent first-pass shift.
|
|||
/// </summary>
|
|||
private static int GetRound0Bits(int bitDepth) |
|||
{ |
|||
int intermediateRange = bitDepth + FilterBits - Round0Bits + 2; |
|||
return Round0Bits + Math.Max(intermediateRange - 16, 0); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Applies integer power-of-two rounding.
|
|||
/// </summary>
|
|||
private static int RoundPowerOfTwo(int value, int bits) => (value + (1 << (bits - 1))) >> bits; |
|||
|
|||
/// <summary>
|
|||
/// Independently applies libaom's signed Q14-to-Q10 scale conversion.
|
|||
/// </summary>
|
|||
private static int ScaleCoordinate(int value, int scale) |
|||
{ |
|||
long scaled = ((long)value * scale) + ((scale - (1 << 14)) * 8L); |
|||
const int shift = 8; |
|||
const long rounding = 1L << (shift - 1); |
|||
return scaled < 0 |
|||
? (int)-((-scaled + rounding) >> shift) |
|||
: (int)((scaled + rounding) >> shift); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Describes one scaled prediction case.
|
|||
/// </summary>
|
|||
private readonly struct ScaledPredictionCase |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ScaledPredictionCase"/> struct.
|
|||
/// </summary>
|
|||
public ScaledPredictionCase( |
|||
string name, |
|||
int width, |
|||
int height, |
|||
Av1InterpolationFilter horizontalFilter, |
|||
Av1InterpolationFilter verticalFilter, |
|||
int horizontalPhase, |
|||
int horizontalStep, |
|||
int verticalPhase, |
|||
int verticalStep) |
|||
{ |
|||
this.Name = name; |
|||
this.Width = width; |
|||
this.Height = height; |
|||
this.HorizontalFilter = horizontalFilter; |
|||
this.VerticalFilter = verticalFilter; |
|||
this.HorizontalPhase = horizontalPhase; |
|||
this.HorizontalStep = horizontalStep; |
|||
this.VerticalPhase = verticalPhase; |
|||
this.VerticalStep = verticalStep; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the diagnostic case name.
|
|||
/// </summary>
|
|||
public string Name { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the output width.
|
|||
/// </summary>
|
|||
public int Width { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the output height.
|
|||
/// </summary>
|
|||
public int Height { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the horizontal interpolation filter.
|
|||
/// </summary>
|
|||
public Av1InterpolationFilter HorizontalFilter { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the vertical interpolation filter.
|
|||
/// </summary>
|
|||
public Av1InterpolationFilter VerticalFilter { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the initial horizontal Q10 position.
|
|||
/// </summary>
|
|||
public int HorizontalPhase { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the horizontal Q10 source step.
|
|||
/// </summary>
|
|||
public int HorizontalStep { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the initial vertical Q10 position.
|
|||
/// </summary>
|
|||
public int VerticalPhase { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the vertical Q10 source step.
|
|||
/// </summary>
|
|||
public int VerticalStep { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public override string ToString() => this.Name; |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:873dc1ab5623910fbf8053cbf684399f4f3bddba4110492e3e11de86e01fec1b |
|||
size 4800 |
|||
File diff suppressed because one or more lines are too long
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:eb239f31ec8dbf5e97ad6f52670fca6497ae2a933822cfe724c75f66aaa2520b |
|||
size 2494 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:b7e30e04a935414a517baa2df06ab756da18ba7c291220d9d7c063297761ae82 |
|||
size 2195 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:dc4c6dbe6bd92c5fce1e3e23700afa603ef04ed02edd336213ebba1e3bd84ba0 |
|||
size 5759 |
|||
Loading…
Reference in new issue