diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 119c381f16..2b6e0d1560 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -44,6 +44,7 @@ Checkboxes may be marked complete only when the implementation and the verificat - [x] Implement sequential pooled HEVC conversion in both directions for monochrome, 4:2:0, 4:2:2, and 4:4:4; independent luma/chroma precision; 8/10/12-bit full/limited ranges; and all six progressive 4:2:0 chroma sample locations. Eight-bit paths use JPEG's optimized RGB plane contracts, while high-bit-depth paths retain 16-bit packed pixels. - [x] Implement allocation-free SIMD-first HEVC intra prediction for all 35 modes, 4/8/16/32 blocks, and 8/10/12-bit samples, including three-tap and strong-bilinear reference filtering, negative-angle reference extension, luma boundary filters, and SIMD horizontal transposition. Verify the normal and forced-scalar paths against one scalar oracle. - [x] Implement allocation-free reconstructed-plane reference collection and normative substitution for complete, empty, and partially available borders, including asymmetric 4:2:2 availability units and the HEVC mode/size/component filter-selection rules. + - [x] Implement allocation-free SIMD-first HEVC inverse quantization for flat and custom scaling lists, 4/8/16/32 transforms, transform-skip scaling rules, extended precision, signed normalization shifts, and transform-range clipping. Store compact and transform-sized scaling matrices in one contiguous parameter-set allocation so the hot path can widen consecutive weights without coordinate division. - [ ] Derive each reference-unit availability flag from coding-tree, slice, tile, constrained-intra, and picture-boundary state, then connect prediction to transform-unit traversal and the bounded item decoder lifecycle. - [x] Implement the shared SIMD-first YUV/CICP paths in both directions for AV1 and HEVC bit depths, chroma formats, ranges, matrix coefficients, transfer functions, color primaries, and chroma positions, with scalar fallback only after the available vector widths are exhausted. - [ ] Complete the independently generated AV1 and HEVC conformance matrix for every supported color-signaling combination before closing the Phase 5 color exit gate. @@ -468,7 +469,8 @@ Implement and verify in dependency order: - [ ] Build each availability flag from reconstructed-plane and coding state, and connect the predictor to transform-unit traversal. - [ ] Scaling lists, inverse quantization, transform skip, every required inverse transform, range-extension precision, and lossless reconstruction. - [x] Implement allocation-free SIMD-first inverse DCT for every 4/8/16/32 width and height combination, the four-by-four intra-luma inverse DST, normative intermediate and residual clipping, and saturated prediction addition for 8/10/12-bit samples. Verify normal and forced-scalar execution against fixed results and a dense scalar oracle. - - [ ] Implement scaling-list selection and inverse quantization for every transform size, component, prediction mode, quantization parameter, and range-extension precision path. + - [x] Implement the SIMD-first inverse-quantization primitive for flat and scaling-list paths across every transform size, component matrix, prediction mode, transform-skip scaling rule, signed normalization direction, and range-extension precision path. + - [ ] Derive effective luma and chroma quantization parameters, including bit-depth offsets and chroma-format mapping, then connect scaling-list selection and inverse quantization to transform-unit traversal. - [ ] Implement transform skip, coefficient rotation, implicit and explicit residual DPCM, transquant bypass, and lossless reconstruction. - [ ] Connect coefficient decoding, inverse quantization, transform selection, reusable scratch, and add/clip to transform-unit traversal. - [ ] Deblocking and sample-adaptive offset for every signaled luma/chroma and bit-depth path. @@ -567,6 +569,7 @@ Tasks: - [x] Add a permanent frame-wide HEVC intra-prediction benchmark. On .NET 10, SIMD planar, vertical-angular, and horizontal-angular prediction measured 155.2, 150.7, and 255.0 microseconds per padded 1920x1088 frame, compared with forced-scalar timings of 1.890, 1.081, and 0.979 milliseconds: 12.2, 7.2, and 3.8 times faster with zero managed allocations. - [x] Add a permanent frame-wide HEVC reference-preparation benchmark. On .NET 10, complete and partially substituted borders measured 465.3 and 566.7 microseconds per 2,040-block padded 1920x1088 frame, compared with forced-scalar timings of 477.5 and 608.0 microseconds, with zero managed allocations. - [x] Add a permanent frame-wide HEVC inverse-transform benchmark. On .NET 10, dense-coefficient 32x32 twelve-bit inverse DCT, transposition, and add/clip measured 3.643 milliseconds per padded 1920x1088 frame, compared with 45.23 milliseconds with hardware intrinsics disabled: 12.4 times faster with zero managed allocations. + - [x] Add a permanent frame-wide HEVC inverse-quantization benchmark. On .NET 10, dense 32x32 twelve-bit flat and scaling-list paths measured 69.98 and 238.4 microseconds per padded 1920x1088 frame, compared with forced-scalar timings of 1.469 and 1.609 milliseconds: 21.0 and 6.7 times faster with zero managed allocations. Pre-expanding the scaling matrices once reduced the SIMD scaling-list path from 4.974 milliseconds to 238.4 microseconds. - [ ] Implement vector paths only for confirmed hot loops, using existing `Vector128`, `Vector256`, and `Vector512` helper and dispatch patterns where supported. - [ ] Prioritize shared color conversion and pixel packing, chroma upsampling, inverse-transform add-and-clip, intra predictors, HEVC deblock/SAO, AV1 loop filter/CDEF/restoration, and contiguous grid copies. - [ ] Benchmark the complete decode color pipeline on representative 8/10/12-bit AVIF and HEIC images with and without embedded ICC profiles. Report absolute end-to-end timings and allocations in addition to the isolated YUV/CICP and ICC stage costs. diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcInverseQuantizer.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcInverseQuantizer.cs new file mode 100644 index 0000000000..6330160eac --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcInverseQuantizer.cs @@ -0,0 +1,401 @@ +// 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.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc; + +/// +/// Reconstructs dequantized HEVC transform coefficients. +/// +internal static class HevcInverseQuantizer +{ + /// + /// Gets the inverse quantization scale selected by the quantization-parameter remainder. + /// + private static ReadOnlySpan InverseQuantizationScales => [40, 45, 51, 57, 64, 72]; + + /// + /// Dequantizes one square transform block using the effective component quantization parameter. + /// + /// The decoded quantized coefficients in raster order. + /// The destination dequantized coefficients in raster order. + /// The base-two logarithm of the transform-block side. + /// The reconstructed component precision. + /// The transform dynamic range excluding its sign bit. + /// The effective nonnegative component quantization parameter including its bit-depth offset. + /// Whether the governing sequence enables scaling lists. + /// The effective picture scaling matrices. + /// The reconstructed color plane. + /// Whether the transform block belongs to an intra-predicted coding unit. + /// Whether the transform block bypasses the inverse transform. + /// Whether transform-skip precision is extended by the sequence. + public static void Dequantize( + ReadOnlySpan quantized, + Span destination, + int log2Size, + int bitDepth, + int maxTransformDynamicRange, + int quantizationParameter, + bool scalingListEnabled, + HevcScalingList scalingList, + HevcPlane plane, + bool isIntraPredicted, + bool transformSkip, + bool extendedPrecisionProcessingEnabled) + { + DebugGuard.MustBeBetweenOrEqualTo(log2Size, 2, 5, nameof(log2Size)); + DebugGuard.MustBeGreaterThanOrEqualTo(quantizationParameter, 0, nameof(quantizationParameter)); + int size = 1 << log2Size; + int sampleCount = size * size; + DebugGuard.IsTrue(quantized.Length >= sampleCount, "The quantized coefficient span is shorter than the transform block."); + DebugGuard.IsTrue(destination.Length >= sampleCount, "The dequantized coefficient span is shorter than the transform block."); + + int transformShift = maxTransformDynamicRange - bitDepth - log2Size; + if (transformSkip && extendedPrecisionProcessingEnabled) + { + transformShift = Math.Max(0, transformShift); + } + + int quantizationParameterPer = quantizationParameter / 6; + int quantizationParameterRemainder = quantizationParameter % 6; + int inverseQuantizationScale = InverseQuantizationScales[quantizationParameterRemainder]; + bool useScalingList = scalingListEnabled && (!transformSkip || log2Size == 2); + int rightShift = 6 - (transformShift + quantizationParameterPer) + (useScalingList ? 4 : 0); + int outputMinimum = -(1 << maxTransformDynamicRange); + int outputMaximum = (1 << maxTransformDynamicRange) - 1; + + // The input clip is part of the normative dequantization process. Its right-shift dependency ensures the + // following signed 32-bit multiplication and optional left shift cannot overflow for any valid coefficient. + int scaleBits = useScalingList ? 15 : 7; + int targetInputBitDepth = Math.Min(maxTransformDynamicRange + 1, 32 + rightShift - scaleBits); + int inputMinimum = -(1 << (targetInputBitDepth - 1)); + int inputMaximum = (1 << (targetInputBitDepth - 1)) - 1; + + if (!useScalingList) + { + DequantizeUniform( + quantized[..sampleCount], + destination[..sampleCount], + inverseQuantizationScale, + rightShift, + inputMinimum, + inputMaximum, + outputMinimum, + outputMaximum); + + return; + } + + int sizeId = log2Size - 2; + int matrixId = (isIntraPredicted ? 0 : 3) + (int)plane; + ReadOnlySpan matrix = scalingList.GetExpandedMatrix(sizeId, matrixId); + DequantizeScalingList( + quantized[..sampleCount], + destination[..sampleCount], + matrix, + inverseQuantizationScale, + rightShift, + inputMinimum, + inputMaximum, + outputMinimum, + outputMaximum); + } + + /// + /// Dequantizes coefficients using one uniform inverse-quantization scale. + /// + /// The complete quantized coefficient block. + /// The complete dequantized coefficient block. + /// The inverse-quantization scale. + /// The signed normalization shift. + /// The inclusive quantized input minimum. + /// The inclusive quantized input maximum. + /// The inclusive dequantized output minimum. + /// The inclusive dequantized output maximum. + private static void DequantizeUniform( + ReadOnlySpan source, + Span destination, + int scale, + int rightShift, + int inputMinimum, + int inputMaximum, + int outputMinimum, + int outputMaximum) + { + ref int sourceBase = ref MemoryMarshal.GetReference(source); + ref int destinationBase = ref MemoryMarshal.GetReference(destination); + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = source.Length - Vector512.Count; + Vector512 weights = Vector512.Create(scale); + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + Vector512 values = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); + Dequantize(values, weights, rightShift, inputMinimum, inputMaximum, outputMinimum, outputMaximum).StoreUnsafe(ref destinationBase, (nuint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = source.Length - Vector256.Count; + Vector256 weights = Vector256.Create(scale); + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 values = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); + Dequantize(values, weights, rightShift, inputMinimum, inputMaximum, outputMinimum, outputMaximum).StoreUnsafe(ref destinationBase, (nuint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = source.Length - Vector128.Count; + Vector128 weights = Vector128.Create(scale); + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + Vector128 values = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); + Dequantize(values, weights, rightShift, inputMinimum, inputMaximum, outputMinimum, outputMaximum).StoreUnsafe(ref destinationBase, (nuint)i); + } + } + + for (; i < source.Length; i++) + { + destination[i] = Dequantize(source[i], scale, rightShift, inputMinimum, inputMaximum, outputMinimum, outputMaximum); + } + } + + /// + /// Dequantizes coefficients using the expanded scaling matrix selected for the transform block. + /// + /// The complete quantized coefficient block. + /// The complete dequantized coefficient block. + /// The scaling matrix expanded to the transform dimensions. + /// The inverse-quantization scale. + /// The signed normalization shift. + /// The inclusive quantized input minimum. + /// The inclusive quantized input maximum. + /// The inclusive dequantized output minimum. + /// The inclusive dequantized output maximum. + private static void DequantizeScalingList( + ReadOnlySpan source, + Span destination, + ReadOnlySpan matrix, + int inverseQuantizationScale, + int rightShift, + int inputMinimum, + int inputMaximum, + int outputMinimum, + int outputMaximum) + { + ref int sourceBase = ref MemoryMarshal.GetReference(source); + ref int destinationBase = ref MemoryMarshal.GetReference(destination); + ref byte matrixBase = ref MemoryMarshal.GetReference(matrix); + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = source.Length - Vector512.Count; + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + Vector512 values = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); + Vector512 weights = LoadScalingWeightsVector512(ref Unsafe.Add(ref matrixBase, i), inverseQuantizationScale); + Dequantize(values, weights, rightShift, inputMinimum, inputMaximum, outputMinimum, outputMaximum).StoreUnsafe(ref destinationBase, (nuint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = source.Length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 values = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); + Vector256 weights = LoadScalingWeightsVector256(ref Unsafe.Add(ref matrixBase, i), inverseQuantizationScale); + Dequantize(values, weights, rightShift, inputMinimum, inputMaximum, outputMinimum, outputMaximum).StoreUnsafe(ref destinationBase, (nuint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = source.Length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + Vector128 values = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); + Vector128 weights = LoadScalingWeightsVector128(ref Unsafe.Add(ref matrixBase, i), inverseQuantizationScale); + Dequantize(values, weights, rightShift, inputMinimum, inputMaximum, outputMinimum, outputMaximum).StoreUnsafe(ref destinationBase, (nuint)i); + } + } + + for (; i < source.Length; i++) + { + int weight = Unsafe.Add(ref matrixBase, i) * inverseQuantizationScale; + destination[i] = Dequantize(source[i], weight, rightShift, inputMinimum, inputMaximum, outputMinimum, outputMaximum); + } + } + + /// + /// Loads and widens sixteen scaling coefficients for a 512-bit coefficient vector. + /// + /// The first scaling coefficient. + /// The inverse-quantization scale. + /// The sixteen ordered dequantization weights. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 LoadScalingWeightsVector512(ref byte source, int scale) + { + Vector128 packed = Vector128.LoadUnsafe(ref source); + (Vector128 lower16, Vector128 upper16) = Vector128.Widen(packed); + Vector256 lower32 = Vector256.Create(Vector128.WidenLower(lower16), Vector128.WidenUpper(lower16)); + Vector256 upper32 = Vector256.Create(Vector128.WidenLower(upper16), Vector128.WidenUpper(upper16)); + return Vector512.Create(lower32, upper32).AsInt32() * Vector512.Create(scale); + } + + /// + /// Loads and widens eight scaling coefficients for a 256-bit coefficient vector. + /// + /// The first scaling coefficient. + /// The inverse-quantization scale. + /// The eight ordered dequantization weights. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 LoadScalingWeightsVector256(ref byte source, int scale) + { + ulong packed = Unsafe.ReadUnaligned(ref source); + Vector128 values16 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()); + Vector256 values32 = Vector256.Create(Vector128.WidenLower(values16), Vector128.WidenUpper(values16)); + return values32.AsInt32() * Vector256.Create(scale); + } + + /// + /// Loads and widens four scaling coefficients for a 128-bit coefficient vector. + /// + /// The first scaling coefficient. + /// The inverse-quantization scale. + /// The four ordered dequantization weights. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 LoadScalingWeightsVector128(ref byte source, int scale) + { + uint packed = Unsafe.ReadUnaligned(ref source); + Vector128 values16 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()); + Vector128 values32 = Vector128.WidenLower(values16); + return values32.AsInt32() * Vector128.Create(scale); + } + + /// + /// Dequantizes sixteen signed coefficients with independent scaling weights. + /// + /// The quantized coefficients. + /// The dequantization weights. + /// The signed normalization shift. + /// The inclusive quantized input minimum. + /// The inclusive quantized input maximum. + /// The inclusive dequantized output minimum. + /// The inclusive dequantized output maximum. + /// The dequantized coefficients. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Dequantize( + Vector512 values, + Vector512 weights, + int rightShift, + int inputMinimum, + int inputMaximum, + int outputMinimum, + int outputMaximum) + { + values = Vector512.Clamp(values, Vector512.Create(inputMinimum), Vector512.Create(inputMaximum)); + Vector512 result = values * weights; + result = rightShift > 0 + ? (result + Vector512.Create(1 << (rightShift - 1))) >> rightShift + : result << -rightShift; + + return Vector512.Clamp(result, Vector512.Create(outputMinimum), Vector512.Create(outputMaximum)); + } + + /// + /// Dequantizes eight signed coefficients with independent scaling weights. + /// + /// The quantized coefficients. + /// The dequantization weights. + /// The signed normalization shift. + /// The inclusive quantized input minimum. + /// The inclusive quantized input maximum. + /// The inclusive dequantized output minimum. + /// The inclusive dequantized output maximum. + /// The dequantized coefficients. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Dequantize( + Vector256 values, + Vector256 weights, + int rightShift, + int inputMinimum, + int inputMaximum, + int outputMinimum, + int outputMaximum) + { + values = Vector256.Clamp(values, Vector256.Create(inputMinimum), Vector256.Create(inputMaximum)); + Vector256 result = values * weights; + result = rightShift > 0 + ? (result + Vector256.Create(1 << (rightShift - 1))) >> rightShift + : result << -rightShift; + + return Vector256.Clamp(result, Vector256.Create(outputMinimum), Vector256.Create(outputMaximum)); + } + + /// + /// Dequantizes four signed coefficients with independent scaling weights. + /// + /// The quantized coefficients. + /// The dequantization weights. + /// The signed normalization shift. + /// The inclusive quantized input minimum. + /// The inclusive quantized input maximum. + /// The inclusive dequantized output minimum. + /// The inclusive dequantized output maximum. + /// The dequantized coefficients. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Dequantize( + Vector128 values, + Vector128 weights, + int rightShift, + int inputMinimum, + int inputMaximum, + int outputMinimum, + int outputMaximum) + { + values = Vector128.Clamp(values, Vector128.Create(inputMinimum), Vector128.Create(inputMaximum)); + Vector128 result = values * weights; + result = rightShift > 0 + ? (result + Vector128.Create(1 << (rightShift - 1))) >> rightShift + : result << -rightShift; + + return Vector128.Clamp(result, Vector128.Create(outputMinimum), Vector128.Create(outputMaximum)); + } + + /// + /// Dequantizes one signed coefficient. + /// + /// The quantized coefficient. + /// The dequantization weight. + /// The signed normalization shift. + /// The inclusive quantized input minimum. + /// The inclusive quantized input maximum. + /// The inclusive dequantized output minimum. + /// The inclusive dequantized output maximum. + /// The dequantized coefficient. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Dequantize( + int value, + int weight, + int rightShift, + int inputMinimum, + int inputMaximum, + int outputMinimum, + int outputMaximum) + { + int result = Math.Clamp(value, inputMinimum, inputMaximum) * weight; + result = rightShift > 0 ? (result + (1 << (rightShift - 1))) >> rightShift : result << -rightShift; + return Math.Clamp(result, outputMinimum, outputMaximum); + } +} diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcScalingList.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcScalingList.cs index e5e8d5061a..c3971d836c 100644 --- a/src/ImageSharp/Formats/Heif/Hevc/HevcScalingList.cs +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcScalingList.cs @@ -14,9 +14,60 @@ internal sealed class HevcScalingList private const int MatrixCount = 6; /// - /// The flat default matrix used by four-by-four transforms. + /// The number of decoded scaling coefficients stored for every transform-size category. /// - private static readonly byte[] Default4x4 = + private const int CompactCoefficientCount = (16 * MatrixCount) + (64 * MatrixCount * 3); + + /// + /// The number of separately coded DC coefficients. + /// + private const int DcCoefficientCount = 4 * MatrixCount; + + /// + /// The first separately coded DC coefficient in the contiguous coefficient store. + /// + private const int DcCoefficientOffset = CompactCoefficientCount; + + /// + /// The first transform-sized matrix in the contiguous coefficient store. + /// + private const int ExpandedCoefficientOffset = DcCoefficientOffset + DcCoefficientCount; + + /// + /// The number of transform-sized coefficients stored across every size and matrix identifier. + /// + private const int ExpandedCoefficientCount = MatrixCount * ((4 * 4) + (8 * 8) + (16 * 16) + (32 * 32)); + + /// + /// The compact syntax matrices, separately coded DC values, and transform-sized matrices. + /// + private readonly byte[] coefficients = new byte[ExpandedCoefficientOffset + ExpandedCoefficientCount]; + + /// + /// Initializes a new instance of the class with the normative default matrices. + /// + public HevcScalingList() + { + for (int sizeId = 0; sizeId < 4; sizeId++) + { + for (int matrixId = 0; matrixId < MatrixCount; matrixId++) + { + ReadOnlySpan source = sizeId == 0 + ? Default4x4 + : matrixId < 3 ? DefaultIntra8x8 : DefaultInter8x8; + + source.CopyTo(this.GetWritableMatrix(sizeId, matrixId)); + this.coefficients[GetDcCoefficientOffset(sizeId, matrixId)] = 16; + } + } + + this.ExpandMatrices(); + } + + /// + /// Gets the flat default matrix used by four-by-four transforms. + /// + private static ReadOnlySpan Default4x4 => [ 16, 16, 16, 16, 16, 16, 16, 16, @@ -25,9 +76,9 @@ internal sealed class HevcScalingList ]; /// - /// The default intra-predicted matrix used by transforms of eight-by-eight and larger. + /// Gets the default intra-predicted matrix used by transforms of eight-by-eight and larger. /// - private static readonly byte[] DefaultIntra8x8 = + private static ReadOnlySpan DefaultIntra8x8 => [ 16, 16, 16, 16, 17, 18, 21, 24, 16, 16, 16, 16, 17, 19, 22, 25, @@ -40,9 +91,9 @@ internal sealed class HevcScalingList ]; /// - /// The default inter-predicted matrix used by transforms of eight-by-eight and larger. + /// Gets the default inter-predicted matrix used by transforms of eight-by-eight and larger. /// - private static readonly byte[] DefaultInter8x8 = + private static ReadOnlySpan DefaultInter8x8 => [ 16, 16, 16, 16, 17, 18, 20, 24, 16, 16, 16, 17, 18, 20, 24, 25, @@ -55,46 +106,23 @@ internal sealed class HevcScalingList ]; /// - /// The diagonal coefficient order for four-by-four matrices. - /// - private static readonly byte[] DiagonalScan4x4 = CreateDiagonalScan(4); - - /// - /// The diagonal coefficient order for matrices of eight-by-eight and larger. + /// Gets the diagonal coefficient order for four-by-four matrices. /// - private static readonly byte[] DiagonalScan8x8 = CreateDiagonalScan(8); - - /// - /// The decoded matrices, indexed by transform-size category and matrix identifier. - /// - private readonly byte[][] matrices = new byte[4 * MatrixCount][]; + private static ReadOnlySpan DiagonalScan4x4 => + [ + 0, 4, 1, 8, 5, 2, 12, 9, 6, 3, 13, 10, 7, 14, 11, 15 + ]; /// - /// The DC coefficients for sixteen-by-sixteen and thirty-two-by-thirty-two matrices. + /// Gets the diagonal coefficient order for matrices of eight-by-eight and larger. /// - private readonly byte[] dcCoefficients = new byte[4 * MatrixCount]; - - /// - /// Initializes a new instance of the class with the normative default matrices. - /// - public HevcScalingList() - { - for (int sizeId = 0; sizeId < 4; sizeId++) - { - int coefficientCount = sizeId == 0 ? 16 : 64; - for (int matrixId = 0; matrixId < MatrixCount; matrixId++) - { - byte[] matrix = new byte[coefficientCount]; - ReadOnlySpan source = sizeId == 0 - ? Default4x4 - : matrixId < 3 ? DefaultIntra8x8 : DefaultInter8x8; - - source.CopyTo(matrix); - this.matrices[GetIndex(sizeId, matrixId)] = matrix; - this.dcCoefficients[GetIndex(sizeId, matrixId)] = 16; - } - } - } + private static ReadOnlySpan DiagonalScan8x8 => + [ + 0, 8, 1, 16, 9, 2, 24, 17, 10, 3, 32, 25, 18, 11, 4, 40, + 33, 26, 19, 12, 5, 48, 41, 34, 27, 20, 13, 6, 56, 49, 42, 35, + 28, 21, 14, 7, 57, 50, 43, 36, 29, 22, 15, 58, 51, 44, 37, 30, + 23, 59, 52, 45, 38, 31, 60, 53, 46, 39, 61, 54, 47, 62, 55, 63 + ]; /// /// Reads a complete scaling-list-data structure. @@ -110,7 +138,6 @@ internal sealed class HevcScalingList int matrixStep = sizeId == 3 ? 3 : 1; for (int matrixId = 0; matrixId < MatrixCount; matrixId += matrixStep) { - int destinationIndex = GetIndex(sizeId, matrixId); bool predictionMode = reader.ReadFlag(); if (!predictionMode) { @@ -133,12 +160,12 @@ internal sealed class HevcScalingList int referenceMatrixId = matrixId - (int)matrixIdDelta; if (referenceMatrixId != matrixId) { - int referenceIndex = GetIndex(sizeId, referenceMatrixId); - // Span copying uses ImageSharp's runtime-optimized memory path and preserves one scalar // behavior model for these small, infrequently parsed coefficient tables. - scalingList.matrices[referenceIndex].CopyTo(scalingList.matrices[destinationIndex], 0); - scalingList.dcCoefficients[destinationIndex] = scalingList.dcCoefficients[referenceIndex]; + scalingList.GetMatrix(sizeId, referenceMatrixId).CopyTo(scalingList.GetWritableMatrix(sizeId, matrixId)); + byte dcCoefficient = scalingList.coefficients[GetDcCoefficientOffset(sizeId, referenceMatrixId)]; + + scalingList.coefficients[GetDcCoefficientOffset(sizeId, matrixId)] = dcCoefficient; } continue; @@ -148,11 +175,11 @@ internal sealed class HevcScalingList if (sizeId > 1) { nextCoefficient = (int)(((long)reader.ReadSignedExpGolomb() + 8) & 255); - scalingList.dcCoefficients[destinationIndex] = (byte)(nextCoefficient & 255); + scalingList.coefficients[GetDcCoefficientOffset(sizeId, matrixId)] = (byte)(nextCoefficient & 255); } - byte[] scan = sizeId == 0 ? DiagonalScan4x4 : DiagonalScan8x8; - byte[] matrix = scalingList.matrices[destinationIndex]; + ReadOnlySpan scan = sizeId == 0 ? DiagonalScan4x4 : DiagonalScan8x8; + Span matrix = scalingList.GetWritableMatrix(sizeId, matrixId); for (int coefficient = 0; coefficient < matrix.Length; coefficient++) { nextCoefficient = (int)(((long)nextCoefficient + reader.ReadSignedExpGolomb()) & 255); @@ -170,14 +197,13 @@ internal sealed class HevcScalingList continue; } - int destinationIndex = GetIndex(sizeId, matrixId); - int referenceIndex = GetIndex(sizeId - 1, matrixId); - scalingList.matrices[referenceIndex].CopyTo(scalingList.matrices[destinationIndex], 0); - scalingList.dcCoefficients[destinationIndex] = scalingList.dcCoefficients[referenceIndex]; + scalingList.GetMatrix(sizeId - 1, matrixId).CopyTo(scalingList.GetWritableMatrix(sizeId, matrixId)); + scalingList.coefficients[GetDcCoefficientOffset(sizeId, matrixId)] = scalingList.coefficients[GetDcCoefficientOffset(sizeId - 1, matrixId)]; } } } + scalingList.ExpandMatrices(); return scalingList; } @@ -191,60 +217,136 @@ internal sealed class HevcScalingList { DebugGuard.MustBeBetweenOrEqualTo(sizeId, 0, 3, nameof(sizeId)); DebugGuard.MustBeBetweenOrEqualTo(matrixId, 0, MatrixCount - 1, nameof(matrixId)); - return this.matrices[GetIndex(sizeId, matrixId)]; + int coefficientCount = GetCompactMatrixLength(sizeId); + return this.coefficients.AsSpan(GetCompactMatrixOffset(sizeId, matrixId), coefficientCount); } /// - /// Gets the DC scaling coefficient for a large transform matrix. + /// Gets a scaling matrix expanded to its transform dimensions. /// - /// The transform-size category. + /// The transform-size category from zero for 4x4 through three for 32x32. /// The prediction and color-component matrix identifier. - /// The decoded DC coefficient. - public byte GetDcCoefficient(int sizeId, int matrixId) + /// The transform-sized scaling coefficients in raster order. + public ReadOnlySpan GetExpandedMatrix(int sizeId, int matrixId) { DebugGuard.MustBeBetweenOrEqualTo(sizeId, 0, 3, nameof(sizeId)); DebugGuard.MustBeBetweenOrEqualTo(matrixId, 0, MatrixCount - 1, nameof(matrixId)); - return this.dcCoefficients[GetIndex(sizeId, matrixId)]; + int coefficientCount = GetExpandedMatrixLength(sizeId); + return this.coefficients.AsSpan(GetExpandedMatrixOffset(sizeId, matrixId), coefficientCount); } /// - /// Gets the flattened storage index for a size and matrix identifier. + /// Gets the DC scaling coefficient for a large transform matrix. /// /// The transform-size category. - /// The matrix identifier. - /// The flattened storage index. - private static int GetIndex(int sizeId, int matrixId) => (sizeId * MatrixCount) + matrixId; + /// The prediction and color-component matrix identifier. + /// The decoded DC coefficient. + public byte GetDcCoefficient(int sizeId, int matrixId) + { + DebugGuard.MustBeBetweenOrEqualTo(sizeId, 0, 3, nameof(sizeId)); + DebugGuard.MustBeBetweenOrEqualTo(matrixId, 0, MatrixCount - 1, nameof(matrixId)); + return this.coefficients[GetDcCoefficientOffset(sizeId, matrixId)]; + } /// - /// Creates the HEVC up-right diagonal scan for a square coefficient block. + /// Expands every syntax matrix once so inverse quantization can consume consecutive weights without coordinate division. /// - /// The coefficient block width and height. - /// The raster indices in coded order. - private static byte[] CreateDiagonalScan(int size) + private void ExpandMatrices() { - byte[] scan = new byte[size * size]; - int row = 0; - int column = 0; - for (int position = 0; position < scan.Length; position++) + for (int sizeId = 0; sizeId < 4; sizeId++) { - scan[position] = (byte)((row * size) + column); - if (column == size - 1 || row == 0) + int size = 1 << (sizeId + 2); + int ratio = Math.Max(1, size >> 3); + int sourceSide = Math.Min(size, 8); + for (int matrixId = 0; matrixId < MatrixCount; matrixId++) { - row += column + 1; - column = 0; - if (row >= size) + ReadOnlySpan source = this.GetMatrix(sizeId, matrixId); + Span destination = this.coefficients.AsSpan(GetExpandedMatrixOffset(sizeId, matrixId), size * size); + for (int y = 0; y < size; y++) { - column += row - (size - 1); - row = size - 1; + int sourceRowOffset = (y / ratio) * sourceSide; + int destinationRowOffset = y * size; + for (int x = 0; x < size; x++) + { + destination[destinationRowOffset + x] = source[sourceRowOffset + (x / ratio)]; + } + } + + if (sizeId > 1) + { + // Sixteen- and thirty-two-point matrices code their DC weight separately from the 8x8 body. + destination[0] = this.coefficients[GetDcCoefficientOffset(sizeId, matrixId)]; } - } - else - { - column++; - row--; } } + } + + /// + /// Gets a writable compact syntax matrix. + /// + /// The transform-size category. + /// The matrix identifier. + /// The writable compact matrix. + private Span GetWritableMatrix(int sizeId, int matrixId) + => this.coefficients.AsSpan(GetCompactMatrixOffset(sizeId, matrixId), GetCompactMatrixLength(sizeId)); + + /// + /// Gets the number of coefficients coded for one syntax matrix. + /// + /// The transform-size category. + /// The compact coefficient count. + private static int GetCompactMatrixLength(int sizeId) => sizeId == 0 ? 16 : 64; + + /// + /// Gets the number of coefficients in one transform-sized matrix. + /// + /// The transform-size category. + /// The expanded coefficient count. + private static int GetExpandedMatrixLength(int sizeId) => 1 << ((sizeId + 2) * 2); - return scan; + /// + /// Gets the compact-matrix offset for a size and matrix identifier. + /// + /// The transform-size category. + /// The matrix identifier. + /// The compact-matrix offset. + private static int GetCompactMatrixOffset(int sizeId, int matrixId) + { + int sizeOffset = sizeId switch + { + 0 => 0, + 1 => 16 * MatrixCount, + 2 => (16 * MatrixCount) + (64 * MatrixCount), + _ => (16 * MatrixCount) + (64 * MatrixCount * 2), + }; + + return sizeOffset + (matrixId * GetCompactMatrixLength(sizeId)); } + + /// + /// Gets the expanded-matrix offset for a size and matrix identifier. + /// + /// The transform-size category. + /// The matrix identifier. + /// The expanded-matrix offset. + private static int GetExpandedMatrixOffset(int sizeId, int matrixId) + { + int sizeOffset = sizeId switch + { + 0 => 0, + 1 => 16 * MatrixCount, + 2 => (16 * MatrixCount) + (64 * MatrixCount), + _ => (16 * MatrixCount) + (64 * MatrixCount) + (256 * MatrixCount), + }; + + return ExpandedCoefficientOffset + sizeOffset + (matrixId * GetExpandedMatrixLength(sizeId)); + } + + /// + /// Gets the separately coded DC-coefficient offset for a size and matrix identifier. + /// + /// The transform-size category. + /// The matrix identifier. + /// The DC-coefficient offset. + private static int GetDcCoefficientOffset(int sizeId, int matrixId) => DcCoefficientOffset + (sizeId * MatrixCount) + matrixId; } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/HevcInverseQuantizationBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/HevcInverseQuantizationBenchmarks.cs new file mode 100644 index 0000000000..aa538c016d --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Heif/HevcInverseQuantizationBenchmarks.cs @@ -0,0 +1,105 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Formats.Heif.Hevc; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; + +/// +/// Measures complete coded-frame traversal through HEVC inverse quantization. +/// +[MemoryDiagnoser(displayGenColumns: false)] +public class HevcInverseQuantizationBenchmarks +{ + /// + /// The coded frame width, which is an exact multiple of the maximum transform-block side. + /// + private const int Width = 1920; + + /// + /// The coded frame height including the final padded coding-tree row for a 1080-line presentation. + /// + private const int Height = 1088; + + /// + /// The base-two logarithm of the benchmark transform-block side. + /// + private const int BlockLog2 = 5; + + /// + /// The transform-block side in samples. + /// + private const int BlockSize = 1 << BlockLog2; + + /// + /// The deterministic quantized coefficients reused by each benchmark block. + /// + private readonly int[] quantized = new int[BlockSize * BlockSize]; + + /// + /// The reusable dequantized coefficient block. + /// + private readonly int[] destination = new int[BlockSize * BlockSize]; + + /// + /// The effective default scaling matrices. + /// + private readonly HevcScalingList scalingList = new(); + + /// + /// Populates a dense, deterministic twelve-bit transform workload outside the measured frame traversal. + /// + [GlobalSetup] + public void Setup() + { + for (int i = 0; i < this.quantized.Length; i++) + { + this.quantized[i] = (((i * 7919) + 257) & 131071) - 65536; + } + } + + /// + /// Measures frame-wide inverse quantization using one uniform scale. + /// + /// The final coefficient, keeping the block output observable. + [Benchmark(Baseline = true)] + public int DequantizeFlatFrame() => this.DequantizeFrame(false); + + /// + /// Measures frame-wide inverse quantization using an expanded thirty-two-by-thirty-two scaling matrix. + /// + /// The final coefficient, keeping the block output observable. + [Benchmark] + public int DequantizeScalingListFrame() => this.DequantizeFrame(true); + + /// + /// Dequantizes every maximum-size transform block in the coded benchmark frame. + /// + /// Whether the default intra-luma scaling matrix applies. + /// The final dequantized coefficient. + private int DequantizeFrame(bool scalingListEnabled) + { + for (int y = 0; y < Height; y += BlockSize) + { + for (int x = 0; x < Width; x += BlockSize) + { + HevcInverseQuantizer.Dequantize( + this.quantized, + this.destination, + BlockLog2, + 12, + 18, + 75, + scalingListEnabled, + this.scalingList, + HevcPlane.Y, + true, + false, + true); + } + } + + return this.destination[^1]; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcInverseQuantizerTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcInverseQuantizerTests.cs new file mode 100644 index 0000000000..9516a13233 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcInverseQuantizerTests.cs @@ -0,0 +1,187 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Hevc; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Hevc; + +/// +/// Verifies HEVC inverse quantization, scaling-list expansion, clipping, and SIMD behavior. +/// +[Trait("Format", "Heic")] +public class HevcInverseQuantizerTests +{ + /// + /// Verifies fixed flat-scale results including signed rounding and both transform-range limits. + /// + [Fact] + public void FlatScaleMatchesFixedResults() + { + int[] quantized = [1, -1, 2, -2, int.MaxValue, int.MinValue, 3, -3, 0, 0, 0, 0, 0, 0, 0, 0]; + int[] actual = new int[quantized.Length]; + HevcScalingList scalingList = new(); + + HevcInverseQuantizer.Dequantize(quantized, actual, 2, 8, 15, 0, false, scalingList, HevcPlane.Y, true, false, false); + + int[] expected = [20, -20, 40, -40, 32767, -32768, 60, -60, 0, 0, 0, 0, 0, 0, 0, 0]; + Assert.True(expected.AsSpan().SequenceEqual(actual)); + } + + /// + /// Verifies fixed nonuniform values from the default eight-by-eight intra-luma scaling matrix. + /// + [Fact] + public void DefaultEightByEightScalingMatrixMatchesFixedResults() + { + int[] quantized = new int[64]; + quantized.AsSpan().Fill(1); + int[] actual = new int[quantized.Length]; + HevcScalingList scalingList = new(); + + HevcInverseQuantizer.Dequantize(quantized, actual, 3, 8, 15, 0, true, scalingList, HevcPlane.Y, true, false, false); + + Assert.Equal(10, actual[0]); + Assert.Equal(11, actual[4]); + Assert.Equal(13, actual[6]); + Assert.Equal(72, actual[63]); + } + + /// + /// Compares SIMD inverse quantization with a scalar oracle across transform sizes, precisions, scaling modes, and shift directions. + /// + /// The base-two logarithm of the tested transform-block side. + /// The reconstructed component precision. + /// The transform dynamic range excluding its sign bit. + /// The effective component quantization parameter. + /// Whether scaling lists are enabled. + /// Whether the transform is skipped. + /// Whether transform-skip precision is extended. + [Theory] + [InlineData(2, 8, 15, 0, false, false, false)] + [InlineData(2, 8, 15, 27, true, false, false)] + [InlineData(2, 10, 15, 39, true, true, false)] + [InlineData(2, 12, 18, 51, true, true, true)] + [InlineData(3, 10, 15, 45, true, false, false)] + [InlineData(3, 10, 15, 45, true, true, false)] + [InlineData(4, 12, 18, 63, true, false, false)] + [InlineData(5, 12, 18, 75, true, false, false)] + [InlineData(5, 12, 15, 51, false, false, false)] + public void DequantizeMatchesScalarOracle( + int log2Size, + int bitDepth, + int maxTransformDynamicRange, + int quantizationParameter, + bool scalingListEnabled, + bool transformSkip, + bool extendedPrecisionProcessingEnabled) + { + int size = 1 << log2Size; + int[] quantized = new int[size * size]; + for (int i = 0; i < quantized.Length; i++) + { + quantized[i] = (((i * 7919) + (size * 257)) & 131071) - 65536; + } + + HevcScalingList scalingList = new(); + int[] expected = new int[quantized.Length]; + int[] actual = new int[quantized.Length]; + DequantizeScalar( + quantized, + expected, + log2Size, + bitDepth, + maxTransformDynamicRange, + quantizationParameter, + scalingListEnabled, + scalingList, + HevcPlane.Y, + true, + transformSkip, + extendedPrecisionProcessingEnabled); + + HevcInverseQuantizer.Dequantize( + quantized, + actual, + log2Size, + bitDepth, + maxTransformDynamicRange, + quantizationParameter, + scalingListEnabled, + scalingList, + HevcPlane.Y, + true, + transformSkip, + extendedPrecisionProcessingEnabled); + + Assert.True(expected.AsSpan().SequenceEqual(actual), $"The {size}x{size} inverse quantizer did not match the scalar HEVC oracle."); + } + + /// + /// Applies the HEVC inverse-quantization equations directly for one complete transform block. + /// + /// The quantized coefficients. + /// The dequantized coefficients. + /// The base-two logarithm of the transform-block side. + /// The reconstructed component precision. + /// The transform dynamic range excluding its sign bit. + /// The effective component quantization parameter. + /// Whether the sequence enables scaling matrices. + /// The effective picture scaling matrices. + /// The reconstructed color plane. + /// Whether the transform block belongs to an intra-predicted coding unit. + /// Whether the transform is skipped. + /// Whether transform-skip precision is extended. + private static void DequantizeScalar( + ReadOnlySpan source, + Span destination, + int log2Size, + int bitDepth, + int maxTransformDynamicRange, + int quantizationParameter, + bool scalingListEnabled, + HevcScalingList scalingList, + HevcPlane plane, + bool isIntraPredicted, + bool transformSkip, + bool extendedPrecisionProcessingEnabled) + { + ReadOnlySpan inverseScales = [40, 45, 51, 57, 64, 72]; + int size = 1 << log2Size; + int transformShift = maxTransformDynamicRange - bitDepth - log2Size; + if (transformSkip && extendedPrecisionProcessingEnabled) + { + transformShift = Math.Max(0, transformShift); + } + + int inverseScale = inverseScales[quantizationParameter % 6]; + bool useScalingList = scalingListEnabled && (!transformSkip || log2Size == 2); + int rightShift = 6 - (transformShift + (quantizationParameter / 6)) + (useScalingList ? 4 : 0); + int targetInputBitDepth = Math.Min(maxTransformDynamicRange + 1, 32 + rightShift - (useScalingList ? 15 : 7)); + int inputMinimum = -(1 << (targetInputBitDepth - 1)); + int inputMaximum = (1 << (targetInputBitDepth - 1)) - 1; + int outputMinimum = -(1 << maxTransformDynamicRange); + int outputMaximum = (1 << maxTransformDynamicRange) - 1; + int sizeId = log2Size - 2; + int matrixId = (isIntraPredicted ? 0 : 3) + (int)plane; + ReadOnlySpan matrix = scalingList.GetMatrix(sizeId, matrixId); + int ratio = Math.Max(1, size >> 3); + int matrixSide = Math.Min(size, 8); + byte dcCoefficient = scalingList.GetDcCoefficient(sizeId, matrixId); + + for (int y = 0; y < size; y++) + { + for (int x = 0; x < size; x++) + { + int coefficient = useScalingList + ? ratio > 1 && x == 0 && y == 0 + ? dcCoefficient + : matrix[((y / ratio) * matrixSide) + (x / ratio)] + : 1; + + int value = Math.Clamp(source[(y * size) + x], inputMinimum, inputMaximum) * inverseScale * coefficient; + value = rightShift > 0 ? (value + (1 << (rightShift - 1))) >> rightShift : value << -rightShift; + destination[(y * size) + x] = Math.Clamp(value, outputMinimum, outputMaximum); + } + } + } +}