diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 592abcc2a..14a4dd183 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -357,7 +357,7 @@ This assessment was reconciled with the source tree on 2026-08-26. Unless a resu - Loop-restoration unit parsing records tile-local switchable/Wiener/self-guided filter selections and coefficients in frame-owned plane grids, including super-resolution-adjusted unit corners and the corrected conditional 64x64-superblock unit-size bit. The active restoration stage implements the normative unit geometry, striped deblocked boundaries, Wiener filtering, self-guided projection, and 8/10/12-bit clipping, while reusing the existing SIMD-backed super-resolution and adjacent multiply/add primitives. Independently encoded fixtures covering every parameter set, plane layout, bit depth, and frame-edge geometry are still required. Other normative independently decodable still-image syntax paths still contain `NotImplementedException` or equivalent unsupported branches. Tile-local palette CDF adaptation is present; the remaining still-image frame-context behavior requires a separate source audit without introducing sequence playback state. - The frame buffer now establishes two-byte native sample storage, logical plane rows, and sample-unit block strides for 10/12-bit frames. The active intra-prediction, inverse-transform, and block-reconstruction path selects native 16-bit samples for 10/12-bit frames and has focused pipeline wiring coverage. Chroma-from-luma storage, 4:4:4/4:2:2/4:2:0 subsampling, rounded mean subtraction, U/V sharing, and 8/10/12-bit prediction now traverse AVX2 and cross-platform `Vector128` paths before an exact scalar fallback. `FeatureTestRunner` verifies every tier against independent fixed-point definitions across every supported block width; independently encoded high-bit-depth and chroma-from-luma AVIF conformance files are still required. - `Av1YuvConverter` now consumes the signaled full or limited range, every non-reserved AV1 H.273 matrix coefficient, transfer characteristics where the matrix definition requires them, subsampling, and chroma sample position for 8, 10, and 12-bit output. Its high-bit-depth decode and encode paths use allocator-backed `Rgb48` rows and the existing `PixelOperations` conversions, avoiding the former eight-bit intermediate. Encoder conversion covers monochrome, YUV 4:2:0, 4:2:2, and 4:4:4 with libavif-compatible box averaging. Identity, full/limited-range YCgCo, the fixed non-constant-luminance matrices, both fixed and chromaticity-derived constant/non-constant-luminance systems, SMPTE ST 2085, and PQ/HLG ICtCp are active in both directions. Independent vectors for every matrix, transfer, range, bit depth, sampling layout, and chroma position remain required before the complete color matrix is externally verified. -- Forward and inverse transforms use operation-owned workspace, explicit sequential fixed storage, and stateless static-generic operator structs for every valid DCT, ADST, and identity size. Named configuration factories keep the encoder's three shifts and variable cosine precision separate from the decoder's two shifts, fixed 12-bit cosine precision, and 8/10/12-bit clamp ranges. Forward traversal shares one libaom-shaped stage network across scalar, `Vector128`, `Vector256`, and `Vector512`; inverse production traversal uses the verified scalar, `Vector128`, and `Vector256` tiers. The focused 511-case Release matrix passes across the `FeatureTestRunner` hardware configurations, and the production 32x32 benchmark reports zero allocation with the paired stage implementation. +- Forward and inverse transforms use operation-owned workspace, explicit sequential fixed storage, and stateless static-generic operator structs for every valid DCT, ADST, and identity size. Named configuration factories keep the encoder's three shifts and variable cosine precision separate from the decoder's two shifts, fixed 12-bit cosine precision, and 8/10/12-bit clamp ranges. Forward traversal shares one libaom-shaped stage network across scalar, `Vector128`, `Vector256`, and `Vector512`; inverse production traversal uses the verified scalar, `Vector128`, and `Vector256` tiers. Lossless segments now bypass the DCT pipeline and apply the reversible four-by-four inverse Walsh-Hadamard transform through the same byte/high-bit-depth output operators, with a `Vector128` production path and caller-workspace scalar fallback. The focused Release matrix passes across the `FeatureTestRunner` hardware configurations, and the production transform benchmarks report zero allocation. - Core intra prediction, chroma-from-luma, and palette reconstruction now use SIMD-first operator or packed-kernel architectures with exact scalar fallbacks. Nonlinear self-guided restoration remains scalar and therefore does not meet the completion architecture. It must be redesigned around vector-friendly storage, shared operator contracts, and SIMD-first traversal with benchmark-gated lane selection, with its current results retained only as fallback/parity behavior. Transform traversal is already SIMD-first, while normative super-resolution and Wiener horizontal products reuse ImageSharp's cross-platform adjacent multiply/add SIMD helper with exact scalar fallbacks. ### AV1 encoder @@ -499,6 +499,8 @@ Implement and verify in dependency order: - [x] Implement allocation-free SIMD-first palette reconstruction for palette sizes 2-8, transform widths 4-64, and 8/10/12-bit samples with exact scalar fallback and `FeatureTestRunner` parity. - [ ] Verify palette mode syntax and presented reconstruction with an independently encoded palette AVIF fixture. - [ ] Lossless and high-bit-depth reconstruction with correct clipping and intermediate precision. + - [x] Route lossless 4x4 blocks through allocation-free reversible inverse Walsh-Hadamard reconstruction for 8/10/12-bit samples, including the DC-only specialization, `Vector128` production traversal, scalar fallback, exact clipping, and `FeatureTestRunner` parity. + - [ ] Verify lossless syntax, inverse quantization, prediction, and presented reconstruction with independently encoded 8/10/12-bit AVIF fixtures. - [ ] Deblocking loop filter. - [ ] CDEF. - [ ] Super-resolution scaling. @@ -639,6 +641,7 @@ Tasks: - [x] Add a permanent frame-wide HEVC grouped coefficient-scan benchmark. On .NET 10, generating the complete diagonal 32x32 scan for all 2,040 transform blocks in a padded 1920x1088 frame measured 1.432 milliseconds with zero managed allocations. CABAC decisions remain serial and require an independently encoded complete-slice workload after transform-unit traversal is connected. - [x] Add a permanent frame-wide AV1 chroma-from-luma prediction benchmark. The lane-selection run rejected the explicit `Vector512` experiment because AVX2 was 2.6% faster at 8-bit and 4.8% faster at 12-bit on the test system. With that path removed, the selected hardware and forced-AVX2 paths are equivalent; the final .NET 10 run measured 88.76 and 74.89 microseconds per padded 1920x1088 frame at 8 and 12 bits, compared with forced-scalar timings of 1.594 and 1.604 milliseconds: 18.0 and 21.4 times faster with zero managed allocations. - [x] Add a permanent frame-wide AV1 palette-reconstruction benchmark. On .NET 10, runtime-selected hardware measured 20.40 and 62.99 microseconds per padded 1920x1088 frame at 8 and 12 bits, compared with forced-scalar timings of 488.96 and 509.58 microseconds: 24.0 and 8.1 times faster with zero managed allocations. Forced `Vector512` measured 20.81 and 62.70 microseconds, so production continues to honor the runtime's preferred vector width instead of overriding this machine's `Vector256` selection. + - [x] Add a permanent frame-wide AV1 lossless inverse-transform benchmark. Dense four-by-four reconstruction across a padded 1920x1088 frame measured 595.6 microseconds at 8 bits and 512.4 microseconds at 12 bits, compared with forced-scalar timings of 2.074 and 2.270 milliseconds: 3.5 and 4.4 times faster with zero managed allocations. - [ ] 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/Av1/Transform/Av1InverseTransformerFactory.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformerFactory.cs index 366e69d46..744f43929 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformerFactory.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformerFactory.cs @@ -29,6 +29,21 @@ internal static class Av1InverseTransformerFactory { Guard.MustBeLessThanOrEqualTo(transformFunctionParameters.BitDepth, 8, nameof(transformFunctionParameters)); Guard.IsFalse(transformFunctionParameters.Is16BitPipeline, nameof(transformFunctionParameters), "Calling 8-bit pipeline while 16-bit is requested."); + + if (transformFunctionParameters.IsLossless) + { + Av1InverseWalshHadamardTransformer.TransformAdd( + coefficients, + readBuffer, + readStride, + writeBuffer, + writeStride, + transformFunctionParameters.EndOfBuffer, + workspace); + + return; + } + Av1Transform2dFlipConfiguration config = Av1Transform2dFlipConfiguration.CreateInverse( transformFunctionParameters.TransformType, transformFunctionParameters.TransformSize, @@ -57,6 +72,22 @@ internal static class Av1InverseTransformerFactory Span workspace) { Guard.IsTrue(transformFunctionParameters.Is16BitPipeline, nameof(transformFunctionParameters), "Calling 16-bit pipeline while 8-bit is requested."); + + if (transformFunctionParameters.IsLossless) + { + Av1InverseWalshHadamardTransformer.TransformAdd( + coefficients, + readBuffer, + readStride, + writeBuffer, + writeStride, + transformFunctionParameters.EndOfBuffer, + workspace, + transformFunctionParameters.BitDepth); + + return; + } + Av1Transform2dFlipConfiguration config = Av1Transform2dFlipConfiguration.CreateInverse( transformFunctionParameters.TransformType, transformFunctionParameters.TransformSize, diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseWalshHadamardTransformer.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseWalshHadamardTransformer.cs new file mode 100644 index 000000000..05385ef85 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseWalshHadamardTransformer.cs @@ -0,0 +1,301 @@ +// 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.Transform; + +/// +/// Applies the reversible four-by-four inverse Walsh-Hadamard transform used by lossless AV1 segments. +/// +internal static class Av1InverseWalshHadamardTransformer +{ + /// + /// The coefficient normalization shift applied before the first transform dimension. + /// + private const int UnitQuantizationShift = 2; + + /// + /// Reconstructs a lossless transform block into eight-bit sample storage. + /// + /// The sixteen dequantized coefficients in raster order. + /// The predicted samples read by reconstruction. + /// The number of read samples between rows. + /// The destination reconstructed samples. + /// The number of destination samples between rows. + /// The decoded coefficient end position. + /// The reusable transform workspace. + public static void TransformAdd( + ReadOnlySpan coefficients, + Span readBuffer, + int readStride, + Span writeBuffer, + int writeStride, + int coefficientCount, + Span workspace) + => TransformAdd( + coefficients, + readBuffer, + readStride, + writeBuffer, + writeStride, + coefficientCount, + workspace, + 8); + + /// + /// Reconstructs a lossless transform block into high-bit-depth sample storage. + /// + /// The sixteen dequantized coefficients in raster order. + /// The predicted samples read by reconstruction. + /// The number of read samples between rows. + /// The destination reconstructed samples. + /// The number of destination samples between rows. + /// The decoded coefficient end position. + /// The reusable transform workspace. + /// The coded sample bit depth. + public static void TransformAdd( + ReadOnlySpan coefficients, + Span readBuffer, + int readStride, + Span writeBuffer, + int writeStride, + int coefficientCount, + Span workspace, + int bitDepth) + => TransformAdd( + coefficients, + readBuffer, + readStride, + writeBuffer, + writeStride, + coefficientCount, + workspace, + bitDepth); + + /// + /// Selects the packed or scalar four-by-four reconstruction path. + /// + private static void TransformAdd( + ReadOnlySpan coefficients, + Span readBuffer, + int readStride, + Span writeBuffer, + int writeStride, + int coefficientCount, + Span workspace, + int bitDepth) + where TSample : unmanaged + where TOutputOperator : struct, IAv1InverseTransformOutputOperator + { + if (Vector128.IsHardwareAccelerated) + { + TransformVector(coefficients, readBuffer, readStride, writeBuffer, writeStride, coefficientCount, bitDepth); + return; + } + + TransformScalar(coefficients, readBuffer, readStride, writeBuffer, writeStride, coefficientCount, workspace, bitDepth); + } + + /// + /// Applies both reversible transform dimensions to four packed coefficient rows. + /// + private static void TransformVector( + ReadOnlySpan coefficients, + Span readBuffer, + int readStride, + Span writeBuffer, + int writeStride, + int coefficientCount, + int bitDepth) + where TSample : unmanaged + where TOutputOperator : struct, IAv1InverseTransformOutputOperator + { + ref int coefficientBase = ref MemoryMarshal.GetReference(coefficients); + Vector128 row0; + Vector128 row1; + Vector128 row2; + Vector128 row3; + + if (coefficientCount == 1) + { + // The DC-only form bypasses fifteen known-zero coefficients and both full butterflies. These divisions + // deliberately use arithmetic shifts because negative coefficients must round toward negative infinity. + int first = Unsafe.Add(ref coefficientBase, 0) >> UnitQuantizationShift; + int half = first >> 1; + Vector128 intermediate = Vector128.Create(first - half, half, half, half); + + row1 = intermediate >> 1; + row0 = intermediate - row1; + row2 = row1; + row3 = row1; + } + else + { + row0 = Vector128.LoadUnsafe(ref coefficientBase) >> UnitQuantizationShift; + row1 = Vector128.LoadUnsafe(ref coefficientBase, 4) >> UnitQuantizationShift; + row2 = Vector128.LoadUnsafe(ref coefficientBase, 8) >> UnitQuantizationShift; + row3 = Vector128.LoadUnsafe(ref coefficientBase, 12) >> UnitQuantizationShift; + + Transform(ref row0, ref row1, ref row2, ref row3); + + // The first pass operates down four columns in parallel. Transposition turns those intermediate columns + // into packed rows so the same reversible butterfly implements the second dimension without scratch. + Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3); + Transform(ref row0, ref row1, ref row2, ref row3); + } + + AddRows(readBuffer, readStride, writeBuffer, writeStride, row0, row1, row2, row3, bitDepth); + } + + /// + /// Applies both reversible transform dimensions without hardware intrinsics. + /// + private static void TransformScalar( + ReadOnlySpan coefficients, + Span readBuffer, + int readStride, + Span writeBuffer, + int writeStride, + int coefficientCount, + Span workspace, + int bitDepth) + where TSample : unmanaged + where TOutputOperator : struct, IAv1InverseTransformOutputOperator + { + ref TSample readBase = ref MemoryMarshal.GetReference(readBuffer); + ref TSample writeBase = ref MemoryMarshal.GetReference(writeBuffer); + + if (coefficientCount == 1) + { + int first = coefficients[0] >> UnitQuantizationShift; + int half = first >> 1; + int firstResidual = first - half; + + for (int column = 0; column < 4; column++) + { + int intermediate = column == 0 ? firstResidual : half; + int repeatedResidual = intermediate >> 1; + int topResidual = intermediate - repeatedResidual; + + Unsafe.Add(ref writeBase, column) = TOutputOperator.Add(Unsafe.Add(ref readBase, column), topResidual, bitDepth); + + for (int row = 1; row < 4; row++) + { + int readOffset = (row * readStride) + column; + int writeOffset = (row * writeStride) + column; + Unsafe.Add(ref writeBase, writeOffset) = TOutputOperator.Add(Unsafe.Add(ref readBase, readOffset), repeatedResidual, bitDepth); + } + } + + return; + } + + ref int coefficientBase = ref MemoryMarshal.GetReference(coefficients); + ref int intermediateBase = ref MemoryMarshal.GetReference(workspace); + + // The scalar fallback retains the column-first traversal. The caller-owned transform workspace keeps the + // complete first dimension without introducing per-block stack or managed allocations. + for (int column = 0; column < 4; column++) + { + int a = Unsafe.Add(ref coefficientBase, column) >> UnitQuantizationShift; + int c = Unsafe.Add(ref coefficientBase, 4 + column) >> UnitQuantizationShift; + int d = Unsafe.Add(ref coefficientBase, 8 + column) >> UnitQuantizationShift; + int b = Unsafe.Add(ref coefficientBase, 12 + column) >> UnitQuantizationShift; + + Transform(ref a, ref b, ref c, ref d); + Unsafe.Add(ref intermediateBase, column) = a; + Unsafe.Add(ref intermediateBase, 4 + column) = b; + Unsafe.Add(ref intermediateBase, 8 + column) = c; + Unsafe.Add(ref intermediateBase, 12 + column) = d; + } + + for (int column = 0; column < 4; column++) + { + int intermediateOffset = column * 4; + int a = Unsafe.Add(ref intermediateBase, intermediateOffset); + int c = Unsafe.Add(ref intermediateBase, intermediateOffset + 1); + int d = Unsafe.Add(ref intermediateBase, intermediateOffset + 2); + int b = Unsafe.Add(ref intermediateBase, intermediateOffset + 3); + + Transform(ref a, ref b, ref c, ref d); + + Unsafe.Add(ref writeBase, column) = TOutputOperator.Add(Unsafe.Add(ref readBase, column), a, bitDepth); + Unsafe.Add(ref writeBase, writeStride + column) = TOutputOperator.Add(Unsafe.Add(ref readBase, readStride + column), b, bitDepth); + Unsafe.Add(ref writeBase, (2 * writeStride) + column) = TOutputOperator.Add(Unsafe.Add(ref readBase, (2 * readStride) + column), c, bitDepth); + Unsafe.Add(ref writeBase, (3 * writeStride) + column) = TOutputOperator.Add(Unsafe.Add(ref readBase, (3 * readStride) + column), d, bitDepth); + } + } + + /// + /// Applies one packed four-point reversible Walsh-Hadamard dimension. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Transform( + ref Vector128 row0, + ref Vector128 row1, + ref Vector128 row2, + ref Vector128 row3) + { + Vector128 a = row0; + Vector128 c = row1; + Vector128 d = row2; + Vector128 b = row3; + + a += c; + d -= b; + Vector128 middle = (a - d) >> 1; + b = middle - b; + c = middle - c; + a -= b; + d += c; + + // The transform's arithmetic names the fourth input b and the second input c. Restore raster row order + // explicitly so the transpose and packed output stages see a, b, c, d exactly as the reference does. + row0 = a; + row1 = b; + row2 = c; + row3 = d; + } + + /// + /// Applies one scalar four-point reversible Walsh-Hadamard dimension. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Transform(ref int a, ref int b, ref int c, ref int d) + { + a += c; + d -= b; + int middle = (a - d) >> 1; + b = middle - b; + c = middle - c; + a -= b; + d += c; + } + + /// + /// Adds four packed residual rows to their prediction rows through the active sample operator. + /// + private static void AddRows( + Span readBuffer, + int readStride, + Span writeBuffer, + int writeStride, + Vector128 row0, + Vector128 row1, + Vector128 row2, + Vector128 row3, + int bitDepth) + where TSample : unmanaged + where TOutputOperator : struct, IAv1InverseTransformOutputOperator + { + ref TSample readBase = ref MemoryMarshal.GetReference(readBuffer); + ref TSample writeBase = ref MemoryMarshal.GetReference(writeBuffer); + TOutputOperator.Add(ref readBase, ref writeBase, row0, bitDepth); + TOutputOperator.Add(ref Unsafe.Add(ref readBase, readStride), ref Unsafe.Add(ref writeBase, writeStride), row1, bitDepth); + TOutputOperator.Add(ref Unsafe.Add(ref readBase, 2 * readStride), ref Unsafe.Add(ref writeBase, 2 * writeStride), row2, bitDepth); + TOutputOperator.Add(ref Unsafe.Add(ref readBase, 3 * readStride), ref Unsafe.Add(ref writeBase, 3 * writeStride), row3, bitDepth); + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1LosslessTransformBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1LosslessTransformBenchmarks.cs new file mode 100644 index 000000000..c2103a301 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1LosslessTransformBenchmarks.cs @@ -0,0 +1,138 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; + +/// +/// Measures frame-wide AV1 lossless inverse-transform reconstruction. +/// +[Config(typeof(Configuration))] +[MemoryDiagnoser(displayGenColumns: false)] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class Av1LosslessTransformBenchmarks +{ + /// + /// The coded frame width in samples. + /// + 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 lossless AV1 transform side in samples. + /// + private const int TransformSize = 4; + + /// + /// The dense dequantized coefficient block used by every transform. + /// + private readonly int[] coefficients = + [ + 320, -192, 64, -448, + 128, 256, -320, 96, + -224, 160, 384, -128, + 448, -64, -256, 192 + ]; + + /// + /// The caller-owned transform workspace reused across the frame. + /// + private readonly int[] workspace = new int[Av1TransformWorkspace.MaximumLength]; + + /// + /// The frame-wide eight-bit reconstruction surface. + /// + private readonly byte[] destination8 = new byte[Width * Height]; + + /// + /// The frame-wide twelve-bit reconstruction surface. + /// + private readonly short[] destination12 = new short[Width * Height]; + + /// + /// Measures dense eight-bit lossless reconstruction across a padded 1920-by-1088 frame. + /// + /// The final reconstructed sample, keeping the frame output observable. + [Benchmark] + [BenchmarkCategory("8Bit")] + public byte Reconstruct8BitFrame() + { + for (int row = 0; row < Height; row += TransformSize) + { + for (int column = 0; column < Width; column += TransformSize) + { + Av1InverseTransformer.Reconstruct8Bit( + this.coefficients, + this.destination8.AsSpan((row * Width) + column), + Width, + Av1TransformSize.Size4x4, + Av1TransformType.DctDct, + 0, + this.coefficients.Length, + true, + this.workspace); + } + } + + return this.destination8[^1]; + } + + /// + /// Measures dense twelve-bit lossless reconstruction across a padded 1920-by-1088 frame. + /// + /// The final reconstructed sample, keeping the frame output observable. + [Benchmark] + [BenchmarkCategory("12Bit")] + public short Reconstruct12BitFrame() + { + for (int row = 0; row < Height; row += TransformSize) + { + for (int column = 0; column < Width; column += TransformSize) + { + Av1InverseTransformer.ReconstructHighBitDepth( + this.coefficients, + this.destination12.AsSpan((row * Width) + column), + Width, + Av1TransformSize.Size4x4, + Av1TransformType.DctDct, + 0, + this.coefficients.Length, + true, + Av1BitDepth.TwelveBit, + this.workspace); + } + } + + return this.destination12[^1]; + } + + /// + /// Configures production-process measurements for hardware and scalar reconstruction. + /// + public sealed class Configuration : ManualConfig + { + /// + /// Initializes a new instance of the class. + /// + public Configuration() + { + this.AddJob(Job.ShortRun.WithId("Hardware").AsBaseline()); + + this.AddJob( + Job.ShortRun + .WithId("Scalar") + .WithEnvironmentVariable("DOTNET_EnableHWIntrinsic", "0")); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseTransformTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseTransformTests.cs index dc5883e2e..6f2fe1865 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseTransformTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseTransformTests.cs @@ -10,6 +10,9 @@ using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; +/// +/// Verifies AV1 forward and inverse transform reconstruction across supported sizes, precisions, and intrinsic tiers. +/// [Trait("Format", "Avif")] public class Av1InverseTransformTests { @@ -166,6 +169,80 @@ public class Av1InverseTransformTests DispatchColumn(transformType, transformSize, bitDepth, ref config); } + /// + /// Verifies lossless inverse Walsh-Hadamard reconstruction against an independent definition. + /// + [Fact] + public void LosslessWalshHadamardMatchesReferenceAcrossIntrinsicTiers() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertLosslessWalshHadamardParity, TransformConfigurations); + + /// + /// Exercises DC-only and complete lossless blocks at every supported sample precision. + /// + private static void AssertLosslessWalshHadamardParity() + { + const int stride = 7; + int[] workspace = new int[Av1TransformWorkspace.MaximumLength]; + int[][] coefficientCases = + [ + [512, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [-516, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [320, -192, 64, -448, 128, 256, -320, 96, -224, 160, 384, -128, 448, -64, -256, 192] + ]; + + for (int coefficientCase = 0; coefficientCase < coefficientCases.Length; coefficientCase++) + { + int[] coefficients = coefficientCases[coefficientCase]; + int coefficientCount = coefficientCase < 2 ? 1 : coefficients.Length; + byte[] expectedBytes = new byte[stride * 4]; + + Array.Fill(expectedBytes, (byte)233); + + PopulatePrediction(expectedBytes, stride, byte.MaxValue); + byte[] actualBytes = (byte[])expectedBytes.Clone(); + + ApplyWalshHadamardReference(coefficients, expectedBytes, stride, coefficientCount, 8); + Av1InverseTransformer.Reconstruct8Bit( + coefficients, + actualBytes, + stride, + Av1TransformSize.Size4x4, + Av1TransformType.DctDct, + 0, + coefficientCount, + true, + workspace); + + Assert.Equal(expectedBytes, actualBytes); + + foreach (int bitDepth in new[] { 10, 12 }) + { + int maximum = (1 << bitDepth) - 1; + short[] expected = new short[stride * 4]; + + Array.Fill(expected, (short)-1); + + PopulatePrediction(expected, stride, maximum); + short[] actual = (short[])expected.Clone(); + + ApplyWalshHadamardReference(coefficients, expected, stride, coefficientCount, bitDepth); + Av1InverseTransformer.ReconstructHighBitDepth( + coefficients, + actual, + stride, + Av1TransformSize.Size4x4, + Av1TransformType.DctDct, + 0, + coefficientCount, + true, + bitDepth == 10 ? Av1BitDepth.TenBit : Av1BitDepth.TwelveBit, + workspace); + + Assert.Equal(expected, actual); + } + } + } + [Fact] public void ReconstructionDispatchDoesNotAllocatePerBlock() { @@ -243,6 +320,143 @@ public class Av1InverseTransformTests Assert.All(reconstruction, value => Assert.Equal((short)0, value)); } + /// + /// Populates active eight-bit prediction samples while preserving row-padding sentinels. + /// + private static void PopulatePrediction(Span prediction, int stride, int maximum) + { + for (int row = 0; row < 4; row++) + { + for (int column = 0; column < 4; column++) + { + prediction[(row * stride) + column] = (byte)(((row * 101) + (column * 67) + 19) & maximum); + } + } + } + + /// + /// Populates active high-bit-depth prediction samples while preserving row-padding sentinels. + /// + private static void PopulatePrediction(Span prediction, int stride, int maximum) + { + for (int row = 0; row < 4; row++) + { + for (int column = 0; column < 4; column++) + { + prediction[(row * stride) + column] = (short)(((row * 911) + (column * 593) + 37) & maximum); + } + } + } + + /// + /// Applies the normative inverse Walsh-Hadamard definition to an eight-bit prediction block. + /// + private static void ApplyWalshHadamardReference(ReadOnlySpan coefficients, Span destination, int stride, int coefficientCount, int bitDepth) + { + int[] residuals = CalculateWalshHadamardReference(coefficients, coefficientCount); + int maximum = (1 << bitDepth) - 1; + + for (int row = 0; row < 4; row++) + { + for (int column = 0; column < 4; column++) + { + int offset = (row * stride) + column; + destination[offset] = (byte)Math.Clamp(destination[offset] + residuals[(row * 4) + column], 0, maximum); + } + } + } + + /// + /// Applies the normative inverse Walsh-Hadamard definition to a high-bit-depth prediction block. + /// + private static void ApplyWalshHadamardReference(ReadOnlySpan coefficients, Span destination, int stride, int coefficientCount, int bitDepth) + { + int[] residuals = CalculateWalshHadamardReference(coefficients, coefficientCount); + int maximum = (1 << bitDepth) - 1; + + for (int row = 0; row < 4; row++) + { + for (int column = 0; column < 4; column++) + { + int offset = (row * stride) + column; + destination[offset] = (short)Math.Clamp(destination[offset] + residuals[(row * 4) + column], 0, maximum); + } + } + } + + /// + /// Calculates the exact four-by-four residual matrix defined by AV1's reversible transform. + /// + private static int[] CalculateWalshHadamardReference(ReadOnlySpan coefficients, int coefficientCount) + { + int[] residuals = new int[16]; + + if (coefficientCount == 1) + { + int first = coefficients[0] >> 2; + int half = first >> 1; + int firstIntermediate = first - half; + + for (int column = 0; column < 4; column++) + { + int intermediate = column == 0 ? firstIntermediate : half; + int repeatedResidual = intermediate >> 1; + residuals[column] = intermediate - repeatedResidual; + residuals[4 + column] = repeatedResidual; + residuals[8 + column] = repeatedResidual; + residuals[12 + column] = repeatedResidual; + } + + return residuals; + } + + int[] intermediateValues = new int[16]; + for (int column = 0; column < 4; column++) + { + int a = coefficients[column] >> 2; + int c = coefficients[4 + column] >> 2; + int d = coefficients[8 + column] >> 2; + int b = coefficients[12 + column] >> 2; + + ApplyWalshHadamardReference(ref a, ref b, ref c, ref d); + intermediateValues[column] = a; + intermediateValues[4 + column] = b; + intermediateValues[8 + column] = c; + intermediateValues[12 + column] = d; + } + + for (int column = 0; column < 4; column++) + { + int offset = column * 4; + int a = intermediateValues[offset]; + int c = intermediateValues[offset + 1]; + int d = intermediateValues[offset + 2]; + int b = intermediateValues[offset + 3]; + + ApplyWalshHadamardReference(ref a, ref b, ref c, ref d); + residuals[column] = a; + residuals[4 + column] = b; + residuals[8 + column] = c; + residuals[12 + column] = d; + } + + return residuals; + } + + /// + /// Applies one scalar four-point reversible Walsh-Hadamard dimension for the independent test definition. + /// + private static void ApplyWalshHadamardReference(ref int a, ref int b, ref int c, ref int d) + { + a += c; + d -= b; + int middle = (a - d) >> 1; + b = middle - b; + c = middle - c; + a -= b; + d += c; + } + /// /// Compares one inverse transform operator across scalar and the supported SIMD lane widths. ///