diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1ForwardQuantizer.HighBitDepthRegularQuantizationOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1ForwardQuantizer.HighBitDepthRegularQuantizationOperator.cs new file mode 100644 index 0000000000..7aa7dfcf7a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1ForwardQuantizer.HighBitDepthRegularQuantizationOperator.cs @@ -0,0 +1,131 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; + +internal static partial class Av1ForwardQuantizer +{ + /// + /// Quantizes high-bit-depth magnitudes without the sixteen-bit saturation used for byte samples. + /// + public readonly struct HighBitDepthRegularQuantizationOperator : IRegularQuantizationOperator + { + /// + public static Vector128 Quantize( + Vector128 coefficients, + Vector128 zeroBin, + Vector128 rounding, + Vector128 quantizer, + Vector128 shift, + Vector128 dequantizer, + int logScale, + out Vector128 dequantizedCoefficients) + { + Vector128 sign = coefficients >> 31; + Vector128 magnitude = Vector128.Abs(coefficients); + Vector128 mask = ~Vector128.GreaterThan(zeroBin, magnitude); + Vector128 rounded = magnitude + rounding; + + // Preserve signed products in two widened halves. The reciprocal correction can be negative; + // an arithmetic Q16 shift restores its implicit leading bit before the second multiplication. + Vector128 lower = Vector128.WidenLower(rounded); + Vector128 upper = Vector128.WidenUpper(rounded); + lower += (lower * Vector128.WidenLower(quantizer)) >> 16; + upper += (upper * Vector128.WidenUpper(quantizer)) >> 16; + lower = (lower * Vector128.WidenLower(shift)) >> (16 - logScale); + upper = (upper * Vector128.WidenUpper(shift)) >> (16 - logScale); + Vector128 quantizedMagnitude = Vector128.Narrow(lower, upper) & mask; + Vector128 dequantizedMagnitude = (quantizedMagnitude * dequantizer) >> logScale; + dequantizedCoefficients = (dequantizedMagnitude ^ sign) - sign; + return (quantizedMagnitude ^ sign) - sign; + } + + /// + public static Vector256 Quantize( + Vector256 coefficients, + Vector256 zeroBin, + Vector256 rounding, + Vector256 quantizer, + Vector256 shift, + Vector256 dequantizer, + int logScale, + out Vector256 dequantizedCoefficients) + { + Vector256 sign = coefficients >> 31; + Vector256 magnitude = Vector256.Abs(coefficients); + Vector256 mask = ~Vector256.GreaterThan(zeroBin, magnitude); + Vector256 rounded = magnitude + rounding; + + // Preserve signed products in two widened halves. The reciprocal correction can be negative; + // an arithmetic Q16 shift restores its implicit leading bit before the second multiplication. + Vector256 lower = Vector256.WidenLower(rounded); + Vector256 upper = Vector256.WidenUpper(rounded); + lower += (lower * Vector256.WidenLower(quantizer)) >> 16; + upper += (upper * Vector256.WidenUpper(quantizer)) >> 16; + lower = (lower * Vector256.WidenLower(shift)) >> (16 - logScale); + upper = (upper * Vector256.WidenUpper(shift)) >> (16 - logScale); + Vector256 quantizedMagnitude = Vector256.Narrow(lower, upper) & mask; + Vector256 dequantizedMagnitude = (quantizedMagnitude * dequantizer) >> logScale; + dequantizedCoefficients = (dequantizedMagnitude ^ sign) - sign; + return (quantizedMagnitude ^ sign) - sign; + } + + /// + public static Vector512 Quantize( + Vector512 coefficients, + Vector512 zeroBin, + Vector512 rounding, + Vector512 quantizer, + Vector512 shift, + Vector512 dequantizer, + int logScale, + out Vector512 dequantizedCoefficients) + { + Vector512 sign = coefficients >> 31; + Vector512 magnitude = Vector512.Abs(coefficients); + Vector512 mask = ~Vector512.GreaterThan(zeroBin, magnitude); + Vector512 rounded = magnitude + rounding; + + // Preserve signed products in two widened halves. The reciprocal correction can be negative; + // an arithmetic Q16 shift restores its implicit leading bit before the second multiplication. + Vector512 lower = Vector512.WidenLower(rounded); + Vector512 upper = Vector512.WidenUpper(rounded); + lower += (lower * Vector512.WidenLower(quantizer)) >> 16; + upper += (upper * Vector512.WidenUpper(quantizer)) >> 16; + lower = (lower * Vector512.WidenLower(shift)) >> (16 - logScale); + upper = (upper * Vector512.WidenUpper(shift)) >> (16 - logScale); + Vector512 quantizedMagnitude = Vector512.Narrow(lower, upper) & mask; + Vector512 dequantizedMagnitude = (quantizedMagnitude * dequantizer) >> logScale; + dequantizedCoefficients = (dequantizedMagnitude ^ sign) - sign; + return (quantizedMagnitude ^ sign) - sign; + } + + /// + public static int Quantize( + int coefficients, + int zeroBin, + int rounding, + int quantizer, + int shift, + int dequantizer, + int logScale, + out int dequantizedCoefficients) + { + int sign = coefficients >> 31; + int magnitude = (coefficients ^ sign) - sign; + int quantizedMagnitude = 0; + if (magnitude >= zeroBin) + { + long rounded = (long)magnitude + rounding; + long corrected = rounded + ((rounded * quantizer) >> 16); + quantizedMagnitude = (int)((corrected * shift) >> (16 - logScale)); + } + + int dequantizedMagnitude = (quantizedMagnitude * dequantizer) >> logScale; + dequantizedCoefficients = (dequantizedMagnitude ^ sign) - sign; + return (quantizedMagnitude ^ sign) - sign; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1ForwardQuantizer.Regular.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1ForwardQuantizer.Regular.cs new file mode 100644 index 0000000000..69dcc5f0d4 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1ForwardQuantizer.Regular.cs @@ -0,0 +1,175 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; + +internal static partial class Av1ForwardQuantizer +{ + /// + /// Quantizes a transform with the regular zero-bin and reciprocal-correction arithmetic. + /// + /// The raster-order transformed coefficients. + /// The raster-order coding coefficients. + /// The raster-order reconstruction coefficients. + /// The transform dimensions. + /// The transform type selecting coefficient scan order. + /// The base quantizer index. + /// The DC index adjustment. + /// The AC index adjustment. + /// The coded sample precision. + /// The encoder sharpness setting from zero through seven. + /// The one-based final nonzero scan position. + public static ushort QuantizeRegular( + ReadOnlySpan coefficients, + Span quantizedCoefficients, + Span dequantizedCoefficients, + Av1TransformSize transformSize, + Av1TransformType transformType, + int qIndex, + int dcDeltaQ, + int acDeltaQ, + Av1BitDepth bitDepth, + int sharpness) + => bitDepth == Av1BitDepth.EightBit + ? QuantizeRegular( + coefficients, quantizedCoefficients, dequantizedCoefficients, transformSize, transformType, qIndex, dcDeltaQ, acDeltaQ, bitDepth, sharpness) + : QuantizeRegular( + coefficients, quantizedCoefficients, dequantizedCoefficients, transformSize, transformType, qIndex, dcDeltaQ, acDeltaQ, bitDepth, sharpness); + + /// + /// Traverses regular quantization with one DC coefficient followed by contiguous AC vectors and a scalar tail. + /// + private static ushort QuantizeRegular( + ReadOnlySpan coefficients, + Span quantizedCoefficients, + Span dequantizedCoefficients, + Av1TransformSize transformSize, + Av1TransformType transformType, + int qIndex, + int dcDeltaQ, + int acDeltaQ, + Av1BitDepth bitDepth, + int sharpness) + where TOperator : struct, IRegularQuantizationOperator + { + int count = transformSize.GetAdjusted().GetSize2d(); + int logScale = transformSize.GetScale(); + int zeroBinFactor = Av1InverseTransformMath.GetQzbinFactor(qIndex, bitDepth); + int roundingFactor = qIndex == 0 ? 64 : sharpness == 0 ? 48 : 64 - (16 * (7 - sharpness) / 7); + int dcDequantizer = Av1QuantizationLookup.GetDcQuant(qIndex, dcDeltaQ, bitDepth); + int acDequantizer = Av1QuantizationLookup.GetAcQuant(qIndex, acDeltaQ, bitDepth); + Av1InverseTransformMath.InvertQuantization(out int dcQuantizer, out int dcShift, dcDequantizer); + Av1InverseTransformMath.InvertQuantization(out int acQuantizer, out int acShift, acDequantizer); + + // Zero-bin constants round twice: once from Q7 and once for the transform scale. Rounding + // constants first truncate from Q7, then round for that same scale. Combining either pair of + // shifts would change coefficients at the quantization boundary. + int dcZeroBin = RoundPowerOfTwo(RoundPowerOfTwo(zeroBinFactor * dcDequantizer, 7), logScale); + int acZeroBin = RoundPowerOfTwo(RoundPowerOfTwo(zeroBinFactor * acDequantizer, 7), logScale); + int dcRounding = RoundPowerOfTwo((roundingFactor * dcDequantizer) >> 7, logScale); + int acRounding = RoundPowerOfTwo((roundingFactor * acDequantizer) >> 7, logScale); + ref int sourceBase = ref MemoryMarshal.GetReference(coefficients); + ref int quantizedBase = ref MemoryMarshal.GetReference(quantizedCoefficients); + ref int dequantizedBase = ref MemoryMarshal.GetReference(dequantizedCoefficients); + + // DC has different constants. Processing it once leaves a uniform AC traversal with no lane masks + // for DC and no scratch coefficient copy; every output position is overwritten on each candidate. + quantizedBase = TOperator.Quantize( + sourceBase, dcZeroBin, dcRounding, dcQuantizer, dcShift, dcDequantizer, logScale, out dequantizedBase); + + int index = 1; + + if (Vector512.IsHardwareAccelerated) + { + Vector512 zeroBin = Vector512.Create(acZeroBin); + Vector512 rounding = Vector512.Create(acRounding); + Vector512 quantizer = Vector512.Create(acQuantizer); + Vector512 shift = Vector512.Create(acShift); + Vector512 dequantizer = Vector512.Create(acDequantizer); + + // Each lane owns one contiguous coefficient. Narrower tiers resume at the first unread + // coefficient so unaligned starts and vector tails need neither padding nor overlapping stores. + for (; index <= count - Vector512.Count; index += Vector512.Count) + { + Vector512 values = Vector512.LoadUnsafe(ref sourceBase, (nuint)index); + Vector512 quantized = TOperator.Quantize( + values, zeroBin, rounding, quantizer, shift, dequantizer, logScale, out Vector512 dequantized); + + quantized.StoreUnsafe(ref quantizedBase, (nuint)index); + dequantized.StoreUnsafe(ref dequantizedBase, (nuint)index); + } + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 zeroBin = Vector256.Create(acZeroBin); + Vector256 rounding = Vector256.Create(acRounding); + Vector256 quantizer = Vector256.Create(acQuantizer); + Vector256 shift = Vector256.Create(acShift); + Vector256 dequantizer = Vector256.Create(acDequantizer); + + // Each lane owns one contiguous coefficient. Narrower tiers resume at the first unread + // coefficient so unaligned starts and vector tails need neither padding nor overlapping stores. + for (; index <= count - Vector256.Count; index += Vector256.Count) + { + Vector256 values = Vector256.LoadUnsafe(ref sourceBase, (nuint)index); + Vector256 quantized = TOperator.Quantize( + values, zeroBin, rounding, quantizer, shift, dequantizer, logScale, out Vector256 dequantized); + + quantized.StoreUnsafe(ref quantizedBase, (nuint)index); + dequantized.StoreUnsafe(ref dequantizedBase, (nuint)index); + } + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 zeroBin = Vector128.Create(acZeroBin); + Vector128 rounding = Vector128.Create(acRounding); + Vector128 quantizer = Vector128.Create(acQuantizer); + Vector128 shift = Vector128.Create(acShift); + Vector128 dequantizer = Vector128.Create(acDequantizer); + + // Each lane owns one contiguous coefficient. Narrower tiers resume at the first unread + // coefficient so unaligned starts and vector tails need neither padding nor overlapping stores. + for (; index <= count - Vector128.Count; index += Vector128.Count) + { + Vector128 values = Vector128.LoadUnsafe(ref sourceBase, (nuint)index); + Vector128 quantized = TOperator.Quantize( + values, zeroBin, rounding, quantizer, shift, dequantizer, logScale, out Vector128 dequantized); + + quantized.StoreUnsafe(ref quantizedBase, (nuint)index); + dequantized.StoreUnsafe(ref dequantizedBase, (nuint)index); + } + } + + for (; index < count; index++) + { + Unsafe.Add(ref quantizedBase, index) = TOperator.Quantize( + Unsafe.Add(ref sourceBase, index), + acZeroBin, + acRounding, + acQuantizer, + acShift, + acDequantizer, + logScale, + out Unsafe.Add(ref dequantizedBase, index)); + } + + // Coding and reconstruction retain raster order. Only the end position is reduced in scan order. + ReadOnlySpan scan = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType).Scan; + for (int scanIndex = count - 1; scanIndex >= 0; scanIndex--) + { + if (Unsafe.Add(ref quantizedBase, scan[scanIndex]) != 0) + { + return (ushort)(scanIndex + 1); + } + } + + return 0; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1ForwardQuantizer.RegularOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1ForwardQuantizer.RegularOperator.cs new file mode 100644 index 0000000000..d400ab6b90 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1ForwardQuantizer.RegularOperator.cs @@ -0,0 +1,103 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; + +internal static partial class Av1ForwardQuantizer +{ + /// + /// Applies regular quantization while preserving the precision-specific rounded-magnitude contract. + /// + public interface IRegularQuantizationOperator + { + /// + /// Applies the zero bin, corrected reciprocal, dequantization, and coefficient sign. + /// + /// The signed transform coefficients. + /// The inclusive magnitude threshold after transform scaling. + /// The rounded magnitude adjustment. + /// The signed reciprocal correction below its implicit leading bit. + /// The power-of-two reciprocal scale. + /// The reconstruction quantizer. + /// The transform coefficient scale. + /// The signed reconstruction coefficients. + /// The signed coding coefficients. + static abstract Vector128 Quantize( + Vector128 coefficients, + Vector128 zeroBin, + Vector128 rounding, + Vector128 quantizer, + Vector128 shift, + Vector128 dequantizer, + int logScale, + out Vector128 dequantizedCoefficients); + + /// + /// Applies the zero bin, corrected reciprocal, dequantization, and coefficient sign. + /// + /// The signed transform coefficients. + /// The inclusive magnitude threshold after transform scaling. + /// The rounded magnitude adjustment. + /// The signed reciprocal correction below its implicit leading bit. + /// The power-of-two reciprocal scale. + /// The reconstruction quantizer. + /// The transform coefficient scale. + /// The signed reconstruction coefficients. + /// The signed coding coefficients. + static abstract Vector256 Quantize( + Vector256 coefficients, + Vector256 zeroBin, + Vector256 rounding, + Vector256 quantizer, + Vector256 shift, + Vector256 dequantizer, + int logScale, + out Vector256 dequantizedCoefficients); + + /// + /// Applies the zero bin, corrected reciprocal, dequantization, and coefficient sign. + /// + /// The signed transform coefficients. + /// The inclusive magnitude threshold after transform scaling. + /// The rounded magnitude adjustment. + /// The signed reciprocal correction below its implicit leading bit. + /// The power-of-two reciprocal scale. + /// The reconstruction quantizer. + /// The transform coefficient scale. + /// The signed reconstruction coefficients. + /// The signed coding coefficients. + static abstract Vector512 Quantize( + Vector512 coefficients, + Vector512 zeroBin, + Vector512 rounding, + Vector512 quantizer, + Vector512 shift, + Vector512 dequantizer, + int logScale, + out Vector512 dequantizedCoefficients); + + /// + /// Applies the zero bin, corrected reciprocal, dequantization, and coefficient sign. + /// + /// The signed transform coefficients. + /// The inclusive magnitude threshold after transform scaling. + /// The rounded magnitude adjustment. + /// The signed reciprocal correction below its implicit leading bit. + /// The power-of-two reciprocal scale. + /// The reconstruction quantizer. + /// The transform coefficient scale. + /// The signed reconstruction coefficients. + /// The signed coding coefficients. + static abstract int Quantize( + int coefficients, + int zeroBin, + int rounding, + int quantizer, + int shift, + int dequantizer, + int logScale, + out int dequantizedCoefficients); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1ForwardQuantizer.RegularQuantizationOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1ForwardQuantizer.RegularQuantizationOperator.cs new file mode 100644 index 0000000000..ec86601788 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1ForwardQuantizer.RegularQuantizationOperator.cs @@ -0,0 +1,116 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; + +internal static partial class Av1ForwardQuantizer +{ + /// + /// Quantizes byte-source coefficients with saturated sixteen-bit rounded magnitudes. + /// + public readonly struct RegularQuantizationOperator : IRegularQuantizationOperator + { + /// + public static Vector128 Quantize( + Vector128 coefficients, + Vector128 zeroBin, + Vector128 rounding, + Vector128 quantizer, + Vector128 shift, + Vector128 dequantizer, + int logScale, + out Vector128 dequantizedCoefficients) + { + Vector128 sign = coefficients >> 31; + Vector128 magnitude = Vector128.Abs(coefficients); + Vector128 mask = ~Vector128.GreaterThan(zeroBin, magnitude); + Vector128 rounded = Vector128.Min(magnitude + rounding, Vector128.Create((int)short.MaxValue)); + + // Saturation bounds both signed products to 32-bit lanes. The first Q16 multiplication + // restores the reciprocal's implicit leading bit; the second removes its power-of-two scale. + Vector128 corrected = rounded + ((rounded * quantizer) >> 16); + Vector128 quantizedMagnitude = ((corrected * shift) >> (16 - logScale)) & mask; + Vector128 dequantizedMagnitude = (quantizedMagnitude * dequantizer) >> logScale; + dequantizedCoefficients = (dequantizedMagnitude ^ sign) - sign; + return (quantizedMagnitude ^ sign) - sign; + } + + /// + public static Vector256 Quantize( + Vector256 coefficients, + Vector256 zeroBin, + Vector256 rounding, + Vector256 quantizer, + Vector256 shift, + Vector256 dequantizer, + int logScale, + out Vector256 dequantizedCoefficients) + { + Vector256 sign = coefficients >> 31; + Vector256 magnitude = Vector256.Abs(coefficients); + Vector256 mask = ~Vector256.GreaterThan(zeroBin, magnitude); + Vector256 rounded = Vector256.Min(magnitude + rounding, Vector256.Create((int)short.MaxValue)); + + // Saturation bounds both signed products to 32-bit lanes. The first Q16 multiplication + // restores the reciprocal's implicit leading bit; the second removes its power-of-two scale. + Vector256 corrected = rounded + ((rounded * quantizer) >> 16); + Vector256 quantizedMagnitude = ((corrected * shift) >> (16 - logScale)) & mask; + Vector256 dequantizedMagnitude = (quantizedMagnitude * dequantizer) >> logScale; + dequantizedCoefficients = (dequantizedMagnitude ^ sign) - sign; + return (quantizedMagnitude ^ sign) - sign; + } + + /// + public static Vector512 Quantize( + Vector512 coefficients, + Vector512 zeroBin, + Vector512 rounding, + Vector512 quantizer, + Vector512 shift, + Vector512 dequantizer, + int logScale, + out Vector512 dequantizedCoefficients) + { + Vector512 sign = coefficients >> 31; + Vector512 magnitude = Vector512.Abs(coefficients); + Vector512 mask = ~Vector512.GreaterThan(zeroBin, magnitude); + Vector512 rounded = Vector512.Min(magnitude + rounding, Vector512.Create((int)short.MaxValue)); + + // Saturation bounds both signed products to 32-bit lanes. The first Q16 multiplication + // restores the reciprocal's implicit leading bit; the second removes its power-of-two scale. + Vector512 corrected = rounded + ((rounded * quantizer) >> 16); + Vector512 quantizedMagnitude = ((corrected * shift) >> (16 - logScale)) & mask; + Vector512 dequantizedMagnitude = (quantizedMagnitude * dequantizer) >> logScale; + dequantizedCoefficients = (dequantizedMagnitude ^ sign) - sign; + return (quantizedMagnitude ^ sign) - sign; + } + + /// + public static int Quantize( + int coefficients, + int zeroBin, + int rounding, + int quantizer, + int shift, + int dequantizer, + int logScale, + out int dequantizedCoefficients) + { + int sign = coefficients >> 31; + int magnitude = (coefficients ^ sign) - sign; + int quantizedMagnitude = 0; + if (magnitude >= zeroBin) + { + int rounded = Math.Min(magnitude + rounding, short.MaxValue); + int corrected = rounded + ((rounded * quantizer) >> 16); + quantizedMagnitude = (corrected * shift) >> (16 - logScale); + } + + int dequantizedMagnitude = (quantizedMagnitude * dequantizer) >> logScale; + dequantizedCoefficients = (dequantizedMagnitude ^ sign) - sign; + return (quantizedMagnitude ^ sign) - sign; + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1RegularQuantizerTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1RegularQuantizerTests.cs new file mode 100644 index 0000000000..22ffb8ce99 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1RegularQuantizerTests.cs @@ -0,0 +1,148 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +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 regular quantization's precision, scan position, and buffer boundaries across hardware paths. +/// +[Trait("Format", "Avif")] +public class Av1RegularQuantizerTests +{ + /// + /// Exercises regular quantization under each available vector width and scalar fallback. + /// + [Fact] + public void RegularQuantizationPreservesCoefficientContracts() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateQuantization, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Exports inputs and results for independent native comparison while checking observable storage contracts. + /// + private static void ValidateQuantization() + { + string vectorWidth = Vector512.IsHardwareAccelerated ? "512" + : Vector256.IsHardwareAccelerated ? "256" + : Vector128.IsHardwareAccelerated ? "128" : "0"; + + string directory = Path.Combine(TestEnvironment.ActualOutputDirectoryFullPath, "Heif", "Av1", "RegularQuantization", vectorWidth); + + Directory.CreateDirectory(directory); + foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.EightBit, Av1BitDepth.TenBit, Av1BitDepth.TwelveBit }) + { + foreach (Av1TransformSize size in new[] + { + Av1TransformSize.Size4x4, Av1TransformSize.Size8x8, Av1TransformSize.Size16x16, + Av1TransformSize.Size32x32, Av1TransformSize.Size64x64, Av1TransformSize.Size8x16 + }) + { + int count = size.GetAdjusted().GetSize2d(); + int[] input = new int[count + 2]; + int[] quantized = new int[count + 2]; + int[] dequantized = new int[count + 2]; + ReadOnlySpan scan = Av1ScanOrderConstants.GetScanOrder(size, Av1TransformType.DctDct).Scan; + foreach (int qIndex in new[] { 0, 1, 10, 90, 200, 255 }) + { + foreach (int sharpness in new[] { 0, 3, 7 }) + { + for (int pattern = 0; pattern < 4; pattern++) + { + int dcDelta = pattern == 1 ? -20 : pattern == 2 ? 17 : 0; + int acDelta = pattern == 1 ? 13 : pattern == 2 ? -11 : 0; + int bits = bitDepth.GetBitCount(); + int maximum = (1 << (bits + 7)) - 1; + uint state = (uint)(qIndex + 123); + Array.Fill(input, int.MinValue); + Array.Fill(quantized, int.MinValue); + Array.Fill(dequantized, int.MinValue); + for (int index = 0; index < count; index++) + { + state = unchecked((state * 1664525) + 1013904223); + int magnitude = (int)(state % (uint)maximum); + if (pattern == 1) + { + int divisor = index == 0 + ? Av1QuantizationLookup.GetDcQuant(qIndex, dcDelta, bitDepth) + : Av1QuantizationLookup.GetAcQuant(qIndex, acDelta, bitDepth); + + int threshold = ((Av1InverseTransformMath.GetQzbinFactor(qIndex, bitDepth) * divisor) + 64) >> 7; + int scale = size.GetScale(); + threshold = scale == 0 ? threshold : (threshold + (1 << (scale - 1))) >> scale; + magnitude = threshold + (index % 3) - 1; + } + else if (pattern == 2) + { + magnitude = maximum; + } + else if (pattern == 3) + { + // A sparse final scan position exposes stale output coefficients across candidate reuse. + magnitude = index == scan[count / 3] ? 97 : 0; + } + + input[index + 1] = (index & 1) == 0 ? magnitude : -magnitude; + } + + int[] original = (int[])input.Clone(); + ushort end = Av1ForwardQuantizer.QuantizeRegular( + input.AsSpan(1, count), + quantized.AsSpan(1, count), + dequantized.AsSpan(1, count), + size, + Av1TransformType.DctDct, + qIndex, + dcDelta, + acDelta, + bitDepth, + sharpness); + + Assert.Equal(original, input); + Assert.Equal(int.MinValue, quantized[0]); + Assert.Equal(int.MinValue, quantized[^1]); + Assert.Equal(int.MinValue, dequantized[0]); + Assert.Equal(int.MinValue, dequantized[^1]); + int expectedEnd = 0; + for (int index = 0; index < count; index++) + { + int level = quantized[index + 1]; + int divisor = index == 0 + ? Av1QuantizationLookup.GetDcQuant(qIndex, dcDelta, bitDepth) + : Av1QuantizationLookup.GetAcQuant(qIndex, acDelta, bitDepth); + + // Signed dequantization truncates the magnitude before restoring sign. + int restored = (Math.Abs(level) * divisor) >> size.GetScale(); + Assert.Equal(level < 0 ? -restored : restored, dequantized[index + 1]); + if (quantized[scan[index] + 1] != 0) + { + expectedEnd = index + 1; + } + } + + Assert.Equal(expectedEnd, end); + using BinaryWriter output = new(File.Create(Path.Combine( + directory, $"{bits}-{(int)size}-{qIndex}-{sharpness}-{pattern}.bin"))); + + foreach (int value in new[] { bits, (int)size, qIndex, dcDelta, acDelta, sharpness, count, end }) + { + output.Write(value); + } + + output.Write(MemoryMarshal.AsBytes(input.AsSpan(1, count))); + output.Write(MemoryMarshal.AsBytes(quantized.AsSpan(1, count))); + output.Write(MemoryMarshal.AsBytes(dequantized.AsSpan(1, count))); + } + } + } + } + } + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs b/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs index d0b77882d2..c1ff4e007b 100644 --- a/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs +++ b/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs @@ -415,6 +415,12 @@ public static class FeatureTestRunner features.Add(key, nameof(HwIntrinsics.AllowAll)); break; #if NET11_0_OR_GREATER + case nameof(HwIntrinsics.DisableAVX512F): + + // AVX-512's baseline features share one switch in .NET 11. The old F-only switch is + // ignored, which would run 512-bit code again instead of exercising narrower vectors. + features.Add(key, "EnableAVX512"); + break; case nameof(HwIntrinsics.DisableSSE42): // SSE3 through SSE4.2 and POPCNT are x86-64-v2 baseline in .NET 11+ and the