// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
///
/// Verifies AV1 forward quantization against current libaom's fast no-matrix arithmetic.
///
[Trait("Format", "Avif")]
public class Av1ForwardQuantizerTests
{
///
/// The hardware configurations covering every quantizer vector tier and the scalar fallback.
///
private const HwIntrinsics QuantizerConfigurations =
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic;
///
/// Verifies raster quantization and scan-order EOB selection at every SIMD tier.
///
[Fact]
public void FastQuantizerMatchesLibaomReferenceAcrossHardwareWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateQuantizer, QuantizerConfigurations);
///
/// Verifies that repeated transform quantization uses only caller-owned buffers.
///
[Fact]
public void QuantizerDoesNotAllocatePerTransform()
{
const int coefficientCount = 64;
int[] coefficients = new int[coefficientCount];
int[] quantized = new int[coefficientCount];
int[] dequantized = new int[coefficientCount];
FillCoefficients(coefficients, 73);
Av1ForwardQuantizer.QuantizeLossy(
coefficients,
quantized,
dequantized,
Av1TransformSize.Size8x8,
Av1TransformType.DctDct,
73,
-1,
3,
Av1BitDepth.TenBit);
long before = GC.GetAllocatedBytesForCurrentThread();
for (int iteration = 0; iteration < 32; iteration++)
{
Av1ForwardQuantizer.QuantizeLossy(
coefficients,
quantized,
dequantized,
Av1TransformSize.Size8x8,
Av1TransformType.DctDct,
73,
-1,
3,
Av1BitDepth.TenBit);
}
Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before);
}
///
/// Exercises each transform-scale category, coded 64-point layout, quantizer range, and sample precision.
///
private static void ValidateQuantizer()
{
ReadOnlySpan transformSizes =
[
Av1TransformSize.Size4x4,
Av1TransformSize.Size8x8,
Av1TransformSize.Size16x16,
Av1TransformSize.Size32x32,
Av1TransformSize.Size64x16,
Av1TransformSize.Size64x64,
];
ReadOnlySpan quantizerIndices = [1, 73, 173, 255];
ReadOnlySpan bitDepths = [Av1BitDepth.EightBit, Av1BitDepth.TenBit, Av1BitDepth.TwelveBit];
foreach (Av1TransformSize transformSize in transformSizes)
{
int coefficientCount = transformSize.GetAdjusted().GetSize2d();
int[] coefficients = new int[coefficientCount];
int[] expectedQuantized = new int[coefficientCount];
int[] expectedDequantized = new int[coefficientCount];
int[] actualQuantized = new int[coefficientCount];
int[] actualDequantized = new int[coefficientCount];
foreach (int qIndex in quantizerIndices)
{
FillCoefficients(coefficients, qIndex);
foreach (Av1BitDepth bitDepth in bitDepths)
{
ushort expectedEndOfBlock = QuantizeReference(
coefficients,
expectedQuantized,
expectedDequantized,
transformSize,
Av1TransformType.DctDct,
qIndex,
-1,
3,
bitDepth);
ushort actualEndOfBlock = Av1ForwardQuantizer.QuantizeLossy(
coefficients,
actualQuantized,
actualDequantized,
transformSize,
Av1TransformType.DctDct,
qIndex,
-1,
3,
bitDepth);
Assert.Equal(expectedEndOfBlock, actualEndOfBlock);
Assert.Equal(expectedQuantized, actualQuantized);
Assert.Equal(expectedDequantized, actualDequantized);
}
}
}
}
///
/// Fills one transform with deterministic signed values spanning threshold, rounding, and clamp behavior.
///
private static void FillCoefficients(Span coefficients, int seed)
{
for (int i = 0; i < coefficients.Length; i++)
{
coefficients[i] = (((i * 7919) + (seed * 313)) % 90001) - 45000;
}
coefficients[0] = 0;
coefficients[1] = 1;
coefficients[2] = -1;
coefficients[3] = short.MaxValue;
coefficients[4] = -short.MaxValue;
}
///
/// Mirrors av1_quantize_fp_no_qmatrix from current libaom without sharing the production traversal.
///
private static ushort QuantizeReference(
ReadOnlySpan coefficients,
Span quantizedCoefficients,
Span dequantizedCoefficients,
Av1TransformSize transformSize,
Av1TransformType transformType,
int qIndex,
int dcDeltaQ,
int acDeltaQ,
Av1BitDepth bitDepth)
{
quantizedCoefficients.Clear();
dequantizedCoefficients.Clear();
int logScale = transformSize.GetScale();
int dcDequantizer = Av1QuantizationLookup.GetDcQuant(qIndex, dcDeltaQ, bitDepth);
int acDequantizer = Av1QuantizationLookup.GetAcQuant(qIndex, acDeltaQ, bitDepth);
int dcQuantizer = (1 << 16) / dcDequantizer;
int acQuantizer = (1 << 16) / acDequantizer;
int dcRounding = RoundPowerOfTwo((64 * dcDequantizer) >> 7, logScale);
int acRounding = RoundPowerOfTwo((64 * acDequantizer) >> 7, logScale);
ReadOnlySpan scan = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType).Scan;
ushort endOfBlock = 0;
for (int scanIndex = 0; scanIndex < scan.Length; scanIndex++)
{
int coefficientIndex = scan[scanIndex];
int coefficient = coefficients[coefficientIndex];
int coefficientSign = coefficient >> 31;
long magnitude = ((long)coefficient ^ coefficientSign) - coefficientSign;
int dequantizer = coefficientIndex == 0 ? dcDequantizer : acDequantizer;
int quantizer = coefficientIndex == 0 ? dcQuantizer : acQuantizer;
int rounding = coefficientIndex == 0 ? dcRounding : acRounding;
int quantizedMagnitude = 0;
if ((magnitude << (1 + logScale)) >= dequantizer)
{
magnitude = Math.Clamp(magnitude + rounding, short.MinValue, short.MaxValue);
quantizedMagnitude = (int)((magnitude * quantizer) >> (16 - logScale));
}
if (quantizedMagnitude != 0)
{
quantizedCoefficients[coefficientIndex] = (quantizedMagnitude ^ coefficientSign) - coefficientSign;
int dequantizedMagnitude = (quantizedMagnitude * dequantizer) >> logScale;
dequantizedCoefficients[coefficientIndex] = (dequantizedMagnitude ^ coefficientSign) - coefficientSign;
endOfBlock = (ushort)(scanIndex + 1);
}
}
return endOfBlock;
}
///
/// Applies libaom's positive round-power-of-two operation.
///
private static int RoundPowerOfTwo(int value, int shift)
=> shift == 0 ? value : (value + (1 << (shift - 1))) >> shift;
}